regscale-cli 6.27.0.1__py3-none-any.whl → 6.27.2.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.
- regscale/_version.py +1 -1
- regscale/core/app/utils/app_utils.py +41 -7
- regscale/integrations/commercial/aws/scanner.py +3 -2
- regscale/integrations/commercial/microsoft_defender/defender_api.py +1 -1
- regscale/integrations/commercial/sicura/api.py +65 -29
- regscale/integrations/commercial/sicura/scanner.py +36 -7
- regscale/integrations/commercial/tenablev2/commands.py +4 -4
- regscale/integrations/commercial/tenablev2/scanner.py +1 -2
- regscale/integrations/commercial/wizv2/scanner.py +40 -16
- regscale/integrations/public/cci_importer.py +400 -9
- regscale/models/integration_models/aqua.py +2 -2
- regscale/models/integration_models/cisa_kev_data.json +164 -3
- regscale/models/integration_models/flat_file_importer/__init__.py +4 -6
- regscale/models/integration_models/synqly_models/capabilities.json +1 -1
- regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +11 -10
- regscale/models/integration_models/synqly_models/ocsf_mapper.py +48 -8
- regscale/models/integration_models/synqly_models/synqly_model.py +34 -12
- {regscale_cli-6.27.0.1.dist-info → regscale_cli-6.27.2.0.dist-info}/METADATA +1 -1
- {regscale_cli-6.27.0.1.dist-info → regscale_cli-6.27.2.0.dist-info}/RECORD +26 -26
- tests/regscale/integrations/commercial/test_sicura.py +0 -1
- tests/regscale/integrations/commercial/wizv2/test_wizv2.py +86 -0
- tests/regscale/integrations/public/test_cci.py +596 -1
- {regscale_cli-6.27.0.1.dist-info → regscale_cli-6.27.2.0.dist-info}/LICENSE +0 -0
- {regscale_cli-6.27.0.1.dist-info → regscale_cli-6.27.2.0.dist-info}/WHEEL +0 -0
- {regscale_cli-6.27.0.1.dist-info → regscale_cli-6.27.2.0.dist-info}/entry_points.txt +0 -0
- {regscale_cli-6.27.0.1.dist-info → regscale_cli-6.27.2.0.dist-info}/top_level.txt +0 -0
|
@@ -59,6 +59,7 @@ class TestCCIImporter(CLITestFixture):
|
|
|
59
59
|
assert importer.reference_version == "4"
|
|
60
60
|
assert importer.verbose is True
|
|
61
61
|
assert importer.normalized_cci == {}
|
|
62
|
+
assert importer.cci_grouped_by_index == {}
|
|
62
63
|
assert importer._user_context is None
|
|
63
64
|
|
|
64
65
|
def test_parse_control_id(self, cci_importer_instance):
|
|
@@ -309,6 +310,563 @@ class TestCCIImporter(CLITestFixture):
|
|
|
309
310
|
result = cci_importer_instance.get_normalized_cci()
|
|
310
311
|
assert result == test_data
|
|
311
312
|
|
|
313
|
+
def test_format_index(self, cci_importer_instance):
|
|
314
|
+
"""Test formatting CCI index strings."""
|
|
315
|
+
# Basic formatting
|
|
316
|
+
assert cci_importer_instance.format_index("AC-1 a 1") == "AC-1(a)(1)"
|
|
317
|
+
assert cci_importer_instance.format_index("AC-2 a") == "AC-2(a)"
|
|
318
|
+
|
|
319
|
+
# Already formatted with parentheses
|
|
320
|
+
assert cci_importer_instance.format_index("IA-13 (03) (a)") == "IA-13(03)(a)"
|
|
321
|
+
|
|
322
|
+
# Mixed format
|
|
323
|
+
assert cci_importer_instance.format_index("AC-1 a 1 (a)") == "AC-1(a)(1)(a)"
|
|
324
|
+
|
|
325
|
+
# Single component (no parts)
|
|
326
|
+
assert cci_importer_instance.format_index("SC-7") == "SC-7"
|
|
327
|
+
|
|
328
|
+
# Empty or whitespace
|
|
329
|
+
assert cci_importer_instance.format_index("") == ""
|
|
330
|
+
assert cci_importer_instance.format_index(" ") == ""
|
|
331
|
+
|
|
332
|
+
def test_parse_objective_id(self, cci_importer_instance):
|
|
333
|
+
"""Test parsing objective otherId strings."""
|
|
334
|
+
# Basic control with part
|
|
335
|
+
control_base, part = cci_importer_instance.parse_objective_id("ac-1_smt.a")
|
|
336
|
+
assert control_base == "AC-1"
|
|
337
|
+
assert part == "a"
|
|
338
|
+
|
|
339
|
+
# Enhancement with part
|
|
340
|
+
control_base, part = cci_importer_instance.parse_objective_id("ac-2.3_smt.a")
|
|
341
|
+
assert control_base == "AC-2(3)"
|
|
342
|
+
assert part == "a"
|
|
343
|
+
|
|
344
|
+
# Enhancement with part (different enhancement number)
|
|
345
|
+
control_base, part = cci_importer_instance.parse_objective_id("au-10.1_smt.b")
|
|
346
|
+
assert control_base == "AU-10(1)"
|
|
347
|
+
assert part == "b"
|
|
348
|
+
|
|
349
|
+
# Enhancement without part
|
|
350
|
+
control_base, part = cci_importer_instance.parse_objective_id("ac-2.4_smt")
|
|
351
|
+
assert control_base == "AC-2(4)"
|
|
352
|
+
assert part is None
|
|
353
|
+
|
|
354
|
+
# Invalid format
|
|
355
|
+
control_base, part = cci_importer_instance.parse_objective_id("invalid")
|
|
356
|
+
assert control_base is None
|
|
357
|
+
assert part is None
|
|
358
|
+
|
|
359
|
+
# Invalid format (no _smt)
|
|
360
|
+
control_base, part = cci_importer_instance.parse_objective_id("ac-1.a")
|
|
361
|
+
assert control_base is None
|
|
362
|
+
assert part is None
|
|
363
|
+
|
|
364
|
+
def test_parse_objective_id_revision_4(self, cci_importer_instance):
|
|
365
|
+
"""Test parsing objective otherId strings in NIST 800-53 Revision 4 format."""
|
|
366
|
+
# Rev 4 format: control_smt.part.subpart
|
|
367
|
+
control_base, part = cci_importer_instance.parse_objective_id("ac-1_smt.a.1")
|
|
368
|
+
assert control_base == "AC-1"
|
|
369
|
+
assert part == "a"
|
|
370
|
+
|
|
371
|
+
control_base, part = cci_importer_instance.parse_objective_id("ac-1_smt.b.2")
|
|
372
|
+
assert control_base == "AC-1"
|
|
373
|
+
assert part == "b"
|
|
374
|
+
|
|
375
|
+
# Rev 4 with enhancement
|
|
376
|
+
control_base, part = cci_importer_instance.parse_objective_id("ac-2.3_smt.d.1")
|
|
377
|
+
assert control_base == "AC-2(3)"
|
|
378
|
+
assert part == "d"
|
|
379
|
+
|
|
380
|
+
# Rev 4 with multiple digit subpart
|
|
381
|
+
control_base, part = cci_importer_instance.parse_objective_id("au-10.1_smt.c.15")
|
|
382
|
+
assert control_base == "AU-10(1)"
|
|
383
|
+
assert part == "c"
|
|
384
|
+
|
|
385
|
+
def test_find_matching_ccis(self, cci_importer_instance):
|
|
386
|
+
"""Test finding matching CCIs by control base and part."""
|
|
387
|
+
cci_map = {
|
|
388
|
+
"AC-1(a)": "CCI-000001",
|
|
389
|
+
"AC-1(a)(1)": "CCI-000002",
|
|
390
|
+
"AC-1(a)(2)": "CCI-000003",
|
|
391
|
+
"AC-1(b)": "CCI-000004",
|
|
392
|
+
"AC-2(3)": "CCI-000005",
|
|
393
|
+
"AC-2(3)(a)": "CCI-000006",
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
# Match with part 'a'
|
|
397
|
+
matches = cci_importer_instance.find_matching_ccis("AC-1", "a", cci_map)
|
|
398
|
+
assert len(matches) == 3
|
|
399
|
+
assert "CCI-000001" in matches
|
|
400
|
+
assert "CCI-000002" in matches
|
|
401
|
+
assert "CCI-000003" in matches
|
|
402
|
+
|
|
403
|
+
# Match with part 'b'
|
|
404
|
+
matches = cci_importer_instance.find_matching_ccis("AC-1", "b", cci_map)
|
|
405
|
+
assert len(matches) == 1
|
|
406
|
+
assert "CCI-000004" in matches
|
|
407
|
+
|
|
408
|
+
# Enhancement without part (exact match only)
|
|
409
|
+
matches = cci_importer_instance.find_matching_ccis("AC-2(3)", None, cci_map)
|
|
410
|
+
assert len(matches) == 1
|
|
411
|
+
assert "CCI-000005" in matches
|
|
412
|
+
|
|
413
|
+
# No matches
|
|
414
|
+
matches = cci_importer_instance.find_matching_ccis("AC-99", "a", cci_map)
|
|
415
|
+
assert len(matches) == 0
|
|
416
|
+
|
|
417
|
+
def test_find_matching_ccis_by_name(self, cci_importer_instance):
|
|
418
|
+
"""Test finding CCIs by name fallback method."""
|
|
419
|
+
cci_map = {
|
|
420
|
+
"AC-1(a)": "CCI-000001",
|
|
421
|
+
"AC-1(a)(1)": "CCI-000002",
|
|
422
|
+
"AC-2(4)": "CCI-000003",
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
# Exact match by name
|
|
426
|
+
matches = cci_importer_instance.find_matching_ccis_by_name("AC-2(4)", "AC-2(4)", cci_map)
|
|
427
|
+
assert len(matches) == 1
|
|
428
|
+
assert "CCI-000003" in matches
|
|
429
|
+
|
|
430
|
+
# Single letter match
|
|
431
|
+
matches = cci_importer_instance.find_matching_ccis_by_name("AC-1", "a.", cci_map)
|
|
432
|
+
assert len(matches) == 2
|
|
433
|
+
assert "CCI-000001" in matches
|
|
434
|
+
assert "CCI-000002" in matches
|
|
435
|
+
|
|
436
|
+
# Single letter without period
|
|
437
|
+
matches = cci_importer_instance.find_matching_ccis_by_name("AC-1", "a", cci_map)
|
|
438
|
+
assert len(matches) == 2
|
|
439
|
+
|
|
440
|
+
# No match
|
|
441
|
+
matches = cci_importer_instance.find_matching_ccis_by_name("AC-1", "xyz", cci_map)
|
|
442
|
+
assert len(matches) == 0
|
|
443
|
+
|
|
444
|
+
def test_find_matching_ccis_by_name_revision_4(self, cci_importer_instance):
|
|
445
|
+
"""Test finding CCIs by name using NIST 800-53 Revision 4 label formats."""
|
|
446
|
+
cci_map = {
|
|
447
|
+
"AC-1(a)": "CCI-000001",
|
|
448
|
+
"AC-1(a)(1)": "CCI-000002",
|
|
449
|
+
"AC-1(b)": "CCI-000003",
|
|
450
|
+
"AC-1(b)(2)": "CCI-000004",
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
# Rev 4 label format: "a.1."
|
|
454
|
+
matches = cci_importer_instance.find_matching_ccis_by_name("AC-1", "a.1.", cci_map)
|
|
455
|
+
assert len(matches) == 2
|
|
456
|
+
assert "CCI-000001" in matches
|
|
457
|
+
assert "CCI-000002" in matches
|
|
458
|
+
|
|
459
|
+
# Rev 4 label format without trailing period: "a.1"
|
|
460
|
+
matches = cci_importer_instance.find_matching_ccis_by_name("AC-1", "a.1", cci_map)
|
|
461
|
+
assert len(matches) == 2
|
|
462
|
+
|
|
463
|
+
# Rev 4 label format: "b.2."
|
|
464
|
+
matches = cci_importer_instance.find_matching_ccis_by_name("AC-1", "b.2.", cci_map)
|
|
465
|
+
assert len(matches) == 2
|
|
466
|
+
assert "CCI-000003" in matches
|
|
467
|
+
assert "CCI-000004" in matches
|
|
468
|
+
|
|
469
|
+
# Invalid rev 4 format (no digit)
|
|
470
|
+
matches = cci_importer_instance.find_matching_ccis_by_name("AC-1", "a.", cci_map)
|
|
471
|
+
assert len(matches) == 2 # Should still match as single letter
|
|
472
|
+
|
|
473
|
+
def test_ccis_already_present(self, cci_importer_instance):
|
|
474
|
+
"""Test checking for duplicate CCI IDs."""
|
|
475
|
+
# CCIs already present
|
|
476
|
+
current = "ac-1_smt.a, CCI-000001, CCI-000002"
|
|
477
|
+
new = "CCI-000002, CCI-000003"
|
|
478
|
+
assert cci_importer_instance.ccis_already_present(current, new) is True
|
|
479
|
+
|
|
480
|
+
# No overlap
|
|
481
|
+
current = "ac-1_smt.a, CCI-000001, CCI-000002"
|
|
482
|
+
new = "CCI-000003, CCI-000004"
|
|
483
|
+
assert cci_importer_instance.ccis_already_present(current, new) is False
|
|
484
|
+
|
|
485
|
+
# Empty current
|
|
486
|
+
current = ""
|
|
487
|
+
new = "CCI-000001"
|
|
488
|
+
assert cci_importer_instance.ccis_already_present(current, new) is False
|
|
489
|
+
|
|
490
|
+
# No CCI IDs in current
|
|
491
|
+
current = "ac-1_smt.a"
|
|
492
|
+
new = "CCI-000001"
|
|
493
|
+
assert cci_importer_instance.ccis_already_present(current, new) is False
|
|
494
|
+
|
|
495
|
+
def test_parse_cci_creates_grouped_index(self, cci_importer_instance):
|
|
496
|
+
"""Test that parse_cci creates both normalized_cci and cci_grouped_by_index."""
|
|
497
|
+
cci_importer_instance.parse_cci()
|
|
498
|
+
|
|
499
|
+
# Check normalized_cci was created
|
|
500
|
+
assert len(cci_importer_instance.normalized_cci) > 0
|
|
501
|
+
|
|
502
|
+
# Check cci_grouped_by_index was created
|
|
503
|
+
assert len(cci_importer_instance.cci_grouped_by_index) > 0
|
|
504
|
+
|
|
505
|
+
# Verify formatted indices
|
|
506
|
+
# The sample data has "AC-1 a" and "AC-1 b" and "AC-2 a 1"
|
|
507
|
+
assert "AC-1(a)" in cci_importer_instance.cci_grouped_by_index
|
|
508
|
+
assert "AC-1(b)" in cci_importer_instance.cci_grouped_by_index
|
|
509
|
+
assert "AC-2(a)(1)" in cci_importer_instance.cci_grouped_by_index
|
|
510
|
+
|
|
511
|
+
@patch("regscale.integrations.public.cci_importer.ControlObjective")
|
|
512
|
+
def test_map_to_control_objectives(self, mock_objective_class, cci_importer_instance):
|
|
513
|
+
"""Test mapping CCIs to control objectives."""
|
|
514
|
+
# Setup sample grouped index data
|
|
515
|
+
cci_importer_instance.cci_grouped_by_index = {
|
|
516
|
+
"AC-1(a)": "CCI-000001, CCI-000002",
|
|
517
|
+
"AC-1(b)": "CCI-000003",
|
|
518
|
+
"AC-2(3)": "CCI-000004",
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
# Create mock objectives
|
|
522
|
+
mock_obj1 = MagicMock()
|
|
523
|
+
mock_obj1.otherId = "ac-1_smt.a"
|
|
524
|
+
mock_obj1.name = "a."
|
|
525
|
+
|
|
526
|
+
mock_obj2 = MagicMock()
|
|
527
|
+
mock_obj2.otherId = "ac-1_smt.b"
|
|
528
|
+
mock_obj2.name = "b."
|
|
529
|
+
|
|
530
|
+
mock_obj3 = MagicMock()
|
|
531
|
+
mock_obj3.otherId = "ac-2.3_smt"
|
|
532
|
+
mock_obj3.name = "AC-2(3)"
|
|
533
|
+
|
|
534
|
+
mock_obj4 = MagicMock()
|
|
535
|
+
mock_obj4.otherId = "invalid"
|
|
536
|
+
mock_obj4.name = "invalid"
|
|
537
|
+
|
|
538
|
+
mock_objective_class.get_by_catalog.return_value = [mock_obj1, mock_obj2, mock_obj3, mock_obj4]
|
|
539
|
+
|
|
540
|
+
# Run the mapping
|
|
541
|
+
result = cci_importer_instance.map_to_control_objectives(catalog_id=1)
|
|
542
|
+
|
|
543
|
+
# Verify results
|
|
544
|
+
assert result["updated"] == 3
|
|
545
|
+
assert result["not_found"] == 1
|
|
546
|
+
assert result["skipped"] == 0
|
|
547
|
+
assert result["total_processed"] == 4
|
|
548
|
+
|
|
549
|
+
# Verify CCIs were added to otherId
|
|
550
|
+
assert "CCI-000001, CCI-000002" in mock_obj1.otherId
|
|
551
|
+
assert "CCI-000003" in mock_obj2.otherId
|
|
552
|
+
assert "CCI-000004" in mock_obj3.otherId
|
|
553
|
+
|
|
554
|
+
# Verify save was called
|
|
555
|
+
assert mock_obj1.save.call_count == 1
|
|
556
|
+
assert mock_obj2.save.call_count == 1
|
|
557
|
+
assert mock_obj3.save.call_count == 1
|
|
558
|
+
|
|
559
|
+
@patch("regscale.integrations.public.cci_importer.ControlObjective")
|
|
560
|
+
def test_map_to_control_objectives_with_duplicates(self, mock_objective_class, cci_importer_instance):
|
|
561
|
+
"""Test that duplicate CCIs are not added again."""
|
|
562
|
+
cci_importer_instance.cci_grouped_by_index = {"AC-1(a)": "CCI-000001, CCI-000002"}
|
|
563
|
+
|
|
564
|
+
# Create mock objective with existing CCIs
|
|
565
|
+
mock_obj = MagicMock()
|
|
566
|
+
mock_obj.otherId = "ac-1_smt.a, CCI-000001"
|
|
567
|
+
mock_obj.name = "a."
|
|
568
|
+
|
|
569
|
+
mock_objective_class.get_by_catalog.return_value = [mock_obj]
|
|
570
|
+
|
|
571
|
+
# Run the mapping
|
|
572
|
+
result = cci_importer_instance.map_to_control_objectives(catalog_id=1)
|
|
573
|
+
|
|
574
|
+
# Should be skipped due to duplicate
|
|
575
|
+
assert result["skipped"] == 1
|
|
576
|
+
assert result["updated"] == 0
|
|
577
|
+
|
|
578
|
+
# Verify save was NOT called
|
|
579
|
+
mock_obj.save.assert_not_called()
|
|
580
|
+
|
|
581
|
+
@patch("regscale.integrations.public.cci_importer.ControlObjective")
|
|
582
|
+
def test_map_to_control_objectives_fallback_by_name(self, mock_objective_class, cci_importer_instance):
|
|
583
|
+
"""Test fallback matching by name when otherId doesn't match."""
|
|
584
|
+
cci_importer_instance.cci_grouped_by_index = {"AC-1(a)": "CCI-000001"}
|
|
585
|
+
|
|
586
|
+
# Create mock objective with non-standard otherId but valid name
|
|
587
|
+
mock_obj = MagicMock()
|
|
588
|
+
mock_obj.otherId = "custom_id"
|
|
589
|
+
mock_obj.name = "a."
|
|
590
|
+
|
|
591
|
+
# This would normally fail otherId parsing, but should work with name fallback
|
|
592
|
+
# However, the parse_objective_id will return None, None for "custom_id"
|
|
593
|
+
# So it won't match. Let me adjust the test.
|
|
594
|
+
|
|
595
|
+
# Actually, looking at the code, if parse_objective_id returns None, it skips
|
|
596
|
+
# So the fallback only works if control_base is valid
|
|
597
|
+
# Let me create a better test
|
|
598
|
+
|
|
599
|
+
mock_obj.otherId = "ac-1_smt" # Valid but no part
|
|
600
|
+
mock_obj.name = "a" # Single letter should match as part
|
|
601
|
+
|
|
602
|
+
mock_objective_class.get_by_catalog.return_value = [mock_obj]
|
|
603
|
+
|
|
604
|
+
# This should use fallback matching
|
|
605
|
+
result = cci_importer_instance.map_to_control_objectives(catalog_id=1)
|
|
606
|
+
|
|
607
|
+
# With the current logic, ac-1_smt will parse to ("AC-1", None)
|
|
608
|
+
# Then it tries to match with find_matching_ccis("AC-1", None, cci_map)
|
|
609
|
+
# which will only match exact "AC-1" with no remainder
|
|
610
|
+
# So it won't find "AC-1(a)"
|
|
611
|
+
# Then it falls back to find_matching_ccis_by_name("AC-1", "a", cci_map)
|
|
612
|
+
# which should find "AC-1(a)"
|
|
613
|
+
|
|
614
|
+
assert result["updated"] == 1
|
|
615
|
+
|
|
616
|
+
@patch("regscale.integrations.public.cci_importer.ControlObjective")
|
|
617
|
+
def test_map_to_control_objectives_revision_4_format(self, mock_objective_class):
|
|
618
|
+
"""Test mapping CCIs to control objectives using NIST 800-53 Revision 4 formats."""
|
|
619
|
+
# Create XML with rev 4 references
|
|
620
|
+
xml_content = """<?xml version="1.0" encoding="utf-8"?>
|
|
621
|
+
<cci_list xmlns="http://iase.disa.mil/cci">
|
|
622
|
+
<cci_item id="CCI-000001">
|
|
623
|
+
<definition>Rev 4 definition for AC-1 a 1</definition>
|
|
624
|
+
<references>
|
|
625
|
+
<reference creator="NIST" title="NIST SP 800-53 Revision 4" version="4"
|
|
626
|
+
location="AC-1" index="AC-1 a 1" />
|
|
627
|
+
</references>
|
|
628
|
+
</cci_item>
|
|
629
|
+
<cci_item id="CCI-000002">
|
|
630
|
+
<definition>Rev 4 definition for AC-1 a 2</definition>
|
|
631
|
+
<references>
|
|
632
|
+
<reference creator="NIST" title="NIST SP 800-53 Revision 4" version="4"
|
|
633
|
+
location="AC-1" index="AC-1 a 2" />
|
|
634
|
+
</references>
|
|
635
|
+
</cci_item>
|
|
636
|
+
<cci_item id="CCI-000003">
|
|
637
|
+
<definition>Rev 4 definition for AC-1 b 1</definition>
|
|
638
|
+
<references>
|
|
639
|
+
<reference creator="NIST" title="NIST SP 800-53 Revision 4" version="4"
|
|
640
|
+
location="AC-1" index="AC-1 b 1" />
|
|
641
|
+
</references>
|
|
642
|
+
</cci_item>
|
|
643
|
+
</cci_list>"""
|
|
644
|
+
|
|
645
|
+
root = ET.fromstring(xml_content)
|
|
646
|
+
importer = CCIImporter(root, version="4", verbose=False)
|
|
647
|
+
importer.parse_cci()
|
|
648
|
+
|
|
649
|
+
# Create mock objectives with rev 4 format (otherId with subparts)
|
|
650
|
+
mock_obj1 = MagicMock()
|
|
651
|
+
mock_obj1.otherId = "ac-1_smt.a.1" # Rev 4 format: part.subpart
|
|
652
|
+
mock_obj1.name = "a.1."
|
|
653
|
+
|
|
654
|
+
mock_obj2 = MagicMock()
|
|
655
|
+
mock_obj2.otherId = "ac-1_smt.a.2"
|
|
656
|
+
mock_obj2.name = "a.2."
|
|
657
|
+
|
|
658
|
+
mock_obj3 = MagicMock()
|
|
659
|
+
mock_obj3.otherId = "ac-1_smt.b.1"
|
|
660
|
+
mock_obj3.name = "b.1."
|
|
661
|
+
|
|
662
|
+
mock_objective_class.get_by_catalog.return_value = [mock_obj1, mock_obj2, mock_obj3]
|
|
663
|
+
|
|
664
|
+
# Run the mapping
|
|
665
|
+
result = importer.map_to_control_objectives(catalog_id=1)
|
|
666
|
+
|
|
667
|
+
# Verify all rev 4 objectives were successfully mapped
|
|
668
|
+
assert result["updated"] == 3
|
|
669
|
+
assert result["not_found"] == 0
|
|
670
|
+
assert result["skipped"] == 0
|
|
671
|
+
assert result["total_processed"] == 3
|
|
672
|
+
|
|
673
|
+
# Verify CCIs were added to otherId
|
|
674
|
+
assert "CCI-000001" in mock_obj1.otherId
|
|
675
|
+
assert "CCI-000002" in mock_obj2.otherId
|
|
676
|
+
assert "CCI-000003" in mock_obj3.otherId
|
|
677
|
+
|
|
678
|
+
# Verify save was called for all
|
|
679
|
+
assert mock_obj1.save.call_count == 1
|
|
680
|
+
assert mock_obj2.save.call_count == 1
|
|
681
|
+
assert mock_obj3.save.call_count == 1
|
|
682
|
+
|
|
683
|
+
@patch("regscale.integrations.public.cci_importer.ControlObjective")
|
|
684
|
+
def test_map_to_control_objectives_revision_5_format(self, mock_objective_class):
|
|
685
|
+
"""Test mapping CCIs to control objectives using NIST 800-53 Revision 5 formats."""
|
|
686
|
+
# Create XML with rev 5 references
|
|
687
|
+
xml_content = """<?xml version="1.0" encoding="utf-8"?>
|
|
688
|
+
<cci_list xmlns="http://iase.disa.mil/cci">
|
|
689
|
+
<cci_item id="CCI-000001">
|
|
690
|
+
<definition>Rev 5 definition for AC-1 a 1 (a)</definition>
|
|
691
|
+
<references>
|
|
692
|
+
<reference creator="NIST" title="NIST SP 800-53 Revision 5" version="5"
|
|
693
|
+
location="AC-1" index="AC-1 a 1 (a)" />
|
|
694
|
+
</references>
|
|
695
|
+
</cci_item>
|
|
696
|
+
<cci_item id="CCI-000002">
|
|
697
|
+
<definition>Rev 5 definition for AC-1 a 2</definition>
|
|
698
|
+
<references>
|
|
699
|
+
<reference creator="NIST" title="NIST SP 800-53 Revision 5" version="5"
|
|
700
|
+
location="AC-1" index="AC-1 a 2" />
|
|
701
|
+
</references>
|
|
702
|
+
</cci_item>
|
|
703
|
+
<cci_item id="CCI-000003">
|
|
704
|
+
<definition>Rev 5 definition for AC-1 c 1</definition>
|
|
705
|
+
<references>
|
|
706
|
+
<reference creator="NIST" title="NIST SP 800-53 Revision 5" version="5"
|
|
707
|
+
location="AC-1" index="AC-1 c 1" />
|
|
708
|
+
</references>
|
|
709
|
+
</cci_item>
|
|
710
|
+
</cci_list>"""
|
|
711
|
+
|
|
712
|
+
root = ET.fromstring(xml_content)
|
|
713
|
+
importer = CCIImporter(root, version="5", verbose=False)
|
|
714
|
+
importer.parse_cci()
|
|
715
|
+
|
|
716
|
+
# Create mock objectives with rev 5 format (no subparts)
|
|
717
|
+
mock_obj1 = MagicMock()
|
|
718
|
+
mock_obj1.otherId = "ac-1_smt.a" # Rev 5 format: just part letter
|
|
719
|
+
mock_obj1.name = "a"
|
|
720
|
+
|
|
721
|
+
mock_obj2 = MagicMock()
|
|
722
|
+
mock_obj2.otherId = "ac-1_smt.c"
|
|
723
|
+
mock_obj2.name = "c"
|
|
724
|
+
|
|
725
|
+
mock_objective_class.get_by_catalog.return_value = [mock_obj1, mock_obj2]
|
|
726
|
+
|
|
727
|
+
# Run the mapping
|
|
728
|
+
result = importer.map_to_control_objectives(catalog_id=1)
|
|
729
|
+
|
|
730
|
+
# Verify rev 5 objectives were successfully mapped
|
|
731
|
+
# obj1 should get both CCI-000001 (AC-1(a)(1)(a)) and CCI-000002 (AC-1(a)(2))
|
|
732
|
+
# obj2 should get CCI-000003 (AC-1(c)(1))
|
|
733
|
+
assert result["updated"] == 2
|
|
734
|
+
assert result["not_found"] == 0
|
|
735
|
+
assert result["skipped"] == 0
|
|
736
|
+
assert result["total_processed"] == 2
|
|
737
|
+
|
|
738
|
+
# Verify CCIs were added to otherId
|
|
739
|
+
assert "CCI-000001" in mock_obj1.otherId or "CCI-000002" in mock_obj1.otherId
|
|
740
|
+
assert "CCI-000003" in mock_obj2.otherId
|
|
741
|
+
|
|
742
|
+
# Verify save was called for both
|
|
743
|
+
assert mock_obj1.save.call_count == 1
|
|
744
|
+
assert mock_obj2.save.call_count == 1
|
|
745
|
+
|
|
746
|
+
@patch("regscale.integrations.public.cci_importer.ControlObjective")
|
|
747
|
+
def test_map_to_control_objectives_mixed_revisions(self, mock_objective_class):
|
|
748
|
+
"""Test mapping CCIs with mixed revision 4 and 5 control objectives."""
|
|
749
|
+
# Create XML with both rev 4 and rev 5 references
|
|
750
|
+
xml_content = """<?xml version="1.0" encoding="utf-8"?>
|
|
751
|
+
<cci_list xmlns="http://iase.disa.mil/cci">
|
|
752
|
+
<cci_item id="CCI-000001">
|
|
753
|
+
<definition>AC-1 part a definition</definition>
|
|
754
|
+
<references>
|
|
755
|
+
<reference creator="NIST" title="NIST SP 800-53 Revision 4" version="4"
|
|
756
|
+
location="AC-1" index="AC-1 a 1" />
|
|
757
|
+
<reference creator="NIST" title="NIST SP 800-53 Revision 5" version="5"
|
|
758
|
+
location="AC-1" index="AC-1 a 1 (a)" />
|
|
759
|
+
</references>
|
|
760
|
+
</cci_item>
|
|
761
|
+
</cci_list>"""
|
|
762
|
+
|
|
763
|
+
root = ET.fromstring(xml_content)
|
|
764
|
+
|
|
765
|
+
# Test with rev 4 importer
|
|
766
|
+
importer_v4 = CCIImporter(root, version="4", verbose=False)
|
|
767
|
+
importer_v4.parse_cci()
|
|
768
|
+
|
|
769
|
+
mock_obj_v4 = MagicMock()
|
|
770
|
+
mock_obj_v4.otherId = "ac-1_smt.a.1"
|
|
771
|
+
mock_obj_v4.name = "a.1."
|
|
772
|
+
|
|
773
|
+
mock_objective_class.get_by_catalog.return_value = [mock_obj_v4]
|
|
774
|
+
|
|
775
|
+
result_v4 = importer_v4.map_to_control_objectives(catalog_id=1)
|
|
776
|
+
assert result_v4["updated"] == 1
|
|
777
|
+
assert "CCI-000001" in mock_obj_v4.otherId
|
|
778
|
+
|
|
779
|
+
# Test with rev 5 importer
|
|
780
|
+
importer_v5 = CCIImporter(root, version="5", verbose=False)
|
|
781
|
+
importer_v5.parse_cci()
|
|
782
|
+
|
|
783
|
+
mock_obj_v5 = MagicMock()
|
|
784
|
+
mock_obj_v5.otherId = "ac-1_smt.a"
|
|
785
|
+
mock_obj_v5.name = "a"
|
|
786
|
+
|
|
787
|
+
mock_objective_class.get_by_catalog.return_value = [mock_obj_v5]
|
|
788
|
+
|
|
789
|
+
result_v5 = importer_v5.map_to_control_objectives(catalog_id=1)
|
|
790
|
+
assert result_v5["updated"] == 1
|
|
791
|
+
assert "CCI-000001" in mock_obj_v5.otherId
|
|
792
|
+
|
|
793
|
+
@patch("regscale.integrations.public.cci_importer.ControlObjective")
|
|
794
|
+
def test_map_to_control_objectives_revision_4_with_enhancements(self, mock_objective_class):
|
|
795
|
+
"""Test mapping CCIs with revision 4 control enhancements."""
|
|
796
|
+
xml_content = """<?xml version="1.0" encoding="utf-8"?>
|
|
797
|
+
<cci_list xmlns="http://iase.disa.mil/cci">
|
|
798
|
+
<cci_item id="CCI-000001">
|
|
799
|
+
<definition>AC-2(3) enhancement definition</definition>
|
|
800
|
+
<references>
|
|
801
|
+
<reference creator="NIST" title="NIST SP 800-53 Revision 4" version="4"
|
|
802
|
+
location="AC-2(3)" index="AC-2 (3)" />
|
|
803
|
+
</references>
|
|
804
|
+
</cci_item>
|
|
805
|
+
<cci_item id="CCI-000002">
|
|
806
|
+
<definition>AC-2(3) part d definition</definition>
|
|
807
|
+
<references>
|
|
808
|
+
<reference creator="NIST" title="NIST SP 800-53 Revision 4" version="4"
|
|
809
|
+
location="AC-2(3)" index="AC-2 (3) d" />
|
|
810
|
+
</references>
|
|
811
|
+
</cci_item>
|
|
812
|
+
</cci_list>"""
|
|
813
|
+
|
|
814
|
+
root = ET.fromstring(xml_content)
|
|
815
|
+
importer = CCIImporter(root, version="4", verbose=False)
|
|
816
|
+
importer.parse_cci()
|
|
817
|
+
|
|
818
|
+
# Create mock objectives with rev 4 enhancement format
|
|
819
|
+
mock_obj1 = MagicMock()
|
|
820
|
+
mock_obj1.otherId = "ac-2.3_smt" # Enhancement without part
|
|
821
|
+
mock_obj1.name = "AC-2(3)"
|
|
822
|
+
|
|
823
|
+
mock_obj2 = MagicMock()
|
|
824
|
+
mock_obj2.otherId = "ac-2.3_smt.d.1" # Enhancement with part and subpart
|
|
825
|
+
mock_obj2.name = "d.1."
|
|
826
|
+
|
|
827
|
+
mock_objective_class.get_by_catalog.return_value = [mock_obj1, mock_obj2]
|
|
828
|
+
|
|
829
|
+
result = importer.map_to_control_objectives(catalog_id=1)
|
|
830
|
+
|
|
831
|
+
assert result["updated"] == 2
|
|
832
|
+
assert result["not_found"] == 0
|
|
833
|
+
|
|
834
|
+
# Verify the enhancement without part got the base CCI
|
|
835
|
+
assert "CCI-000001" in mock_obj1.otherId
|
|
836
|
+
# Verify the enhancement with part got the part-specific CCI
|
|
837
|
+
assert "CCI-000002" in mock_obj2.otherId
|
|
838
|
+
|
|
839
|
+
@patch("regscale.integrations.public.cci_importer.ControlObjective")
|
|
840
|
+
def test_map_to_control_objectives_revision_4_label_fallback(self, mock_objective_class):
|
|
841
|
+
"""Test that revision 4 label format fallback matching works correctly."""
|
|
842
|
+
xml_content = """<?xml version="1.0" encoding="utf-8"?>
|
|
843
|
+
<cci_list xmlns="http://iase.disa.mil/cci">
|
|
844
|
+
<cci_item id="CCI-000001">
|
|
845
|
+
<definition>AC-1 part a definition</definition>
|
|
846
|
+
<references>
|
|
847
|
+
<reference creator="NIST" title="NIST SP 800-53 Revision 4" version="4"
|
|
848
|
+
location="AC-1" index="AC-1 a" />
|
|
849
|
+
</references>
|
|
850
|
+
</cci_item>
|
|
851
|
+
</cci_list>"""
|
|
852
|
+
|
|
853
|
+
root = ET.fromstring(xml_content)
|
|
854
|
+
importer = CCIImporter(root, version="4", verbose=False)
|
|
855
|
+
importer.parse_cci()
|
|
856
|
+
|
|
857
|
+
# Create objective with rev 4 format where otherId parsing should work
|
|
858
|
+
mock_obj = MagicMock()
|
|
859
|
+
mock_obj.otherId = "ac-1_smt" # No part specified
|
|
860
|
+
mock_obj.name = "a.1." # Rev 4 label format should trigger fallback
|
|
861
|
+
|
|
862
|
+
mock_objective_class.get_by_catalog.return_value = [mock_obj]
|
|
863
|
+
|
|
864
|
+
result = importer.map_to_control_objectives(catalog_id=1)
|
|
865
|
+
|
|
866
|
+
# Should successfully map using fallback name matching
|
|
867
|
+
assert result["updated"] == 1
|
|
868
|
+
assert "CCI-000001" in mock_obj.otherId
|
|
869
|
+
|
|
312
870
|
|
|
313
871
|
class TestCCIImporterCLI:
|
|
314
872
|
"""Test cases for the CCI importer CLI command."""
|
|
@@ -342,7 +900,7 @@ class TestCCIImporterCLI:
|
|
|
342
900
|
@patch("regscale.integrations.public.cci_importer._load_xml_file")
|
|
343
901
|
@patch("regscale.integrations.public.cci_importer.CCIImporter")
|
|
344
902
|
def test_cci_importer_command_with_database(self, mock_importer_class, mock_load_xml):
|
|
345
|
-
"""Test CLI command with database operations."""
|
|
903
|
+
"""Test CLI command with database operations (default: maps to objectives)."""
|
|
346
904
|
runner = CliRunner()
|
|
347
905
|
|
|
348
906
|
# Setup mocks
|
|
@@ -357,6 +915,12 @@ class TestCCIImporterCLI:
|
|
|
357
915
|
"skipped": 1,
|
|
358
916
|
"total_processed": 2,
|
|
359
917
|
}
|
|
918
|
+
mock_importer.map_to_control_objectives.return_value = {
|
|
919
|
+
"updated": 10,
|
|
920
|
+
"skipped": 2,
|
|
921
|
+
"not_found": 1,
|
|
922
|
+
"total_processed": 13,
|
|
923
|
+
}
|
|
360
924
|
mock_importer_class.return_value = mock_importer
|
|
361
925
|
|
|
362
926
|
result = runner.invoke(cci_importer, ["-n", "4", "-c", "2"])
|
|
@@ -368,6 +932,37 @@ class TestCCIImporterCLI:
|
|
|
368
932
|
assert result.exit_code == 0, f"Expected exit code 0, got {result.exit_code}. Output: {result.output}"
|
|
369
933
|
mock_importer_class.assert_called_once_with(mock_root, version="4", verbose=False)
|
|
370
934
|
mock_importer.map_to_security_controls.assert_called_with(2)
|
|
935
|
+
# By default, should also map to objectives
|
|
936
|
+
mock_importer.map_to_control_objectives.assert_called_with(2)
|
|
937
|
+
|
|
938
|
+
@patch("regscale.integrations.public.cci_importer._load_xml_file")
|
|
939
|
+
@patch("regscale.integrations.public.cci_importer.CCIImporter")
|
|
940
|
+
def test_cci_importer_command_with_disable_objectives(self, mock_importer_class, mock_load_xml):
|
|
941
|
+
"""Test CLI command with --disable-objectives flag."""
|
|
942
|
+
runner = CliRunner()
|
|
943
|
+
|
|
944
|
+
# Setup mocks
|
|
945
|
+
mock_root = MagicMock()
|
|
946
|
+
mock_load_xml.return_value = mock_root
|
|
947
|
+
|
|
948
|
+
mock_importer = MagicMock()
|
|
949
|
+
mock_importer.get_normalized_cci.return_value = {"AC-1": []}
|
|
950
|
+
mock_importer.map_to_security_controls.return_value = {
|
|
951
|
+
"created": 5,
|
|
952
|
+
"updated": 3,
|
|
953
|
+
"skipped": 1,
|
|
954
|
+
"total_processed": 2,
|
|
955
|
+
}
|
|
956
|
+
mock_importer_class.return_value = mock_importer
|
|
957
|
+
|
|
958
|
+
result = runner.invoke(cci_importer, ["-n", "4", "-c", "2", "--disable-objectives"])
|
|
959
|
+
|
|
960
|
+
# Should succeed
|
|
961
|
+
assert result.exit_code == 0
|
|
962
|
+
mock_importer_class.assert_called_once_with(mock_root, version="4", verbose=False)
|
|
963
|
+
mock_importer.map_to_security_controls.assert_called_with(2)
|
|
964
|
+
# Should NOT map to objectives when flag is set
|
|
965
|
+
mock_importer.map_to_control_objectives.assert_not_called()
|
|
371
966
|
|
|
372
967
|
@patch("regscale.integrations.public.cci_importer._load_xml_file")
|
|
373
968
|
def test_cci_importer_command_xml_error(self, mock_load_xml):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|