regscale-cli 6.26.0.0__py3-none-any.whl → 6.27.0.1__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 (96) hide show
  1. regscale/_version.py +1 -1
  2. regscale/core/app/application.py +1 -1
  3. regscale/core/app/internal/evidence.py +419 -2
  4. regscale/dev/code_gen.py +24 -20
  5. regscale/integrations/commercial/__init__.py +0 -1
  6. regscale/integrations/commercial/jira.py +367 -126
  7. regscale/integrations/commercial/qualys/__init__.py +7 -8
  8. regscale/integrations/commercial/qualys/scanner.py +8 -3
  9. regscale/integrations/commercial/synqly/assets.py +17 -0
  10. regscale/integrations/commercial/synqly/vulnerabilities.py +45 -28
  11. regscale/integrations/commercial/tenablev2/cis_parsers.py +453 -0
  12. regscale/integrations/commercial/tenablev2/cis_scanner.py +447 -0
  13. regscale/integrations/commercial/tenablev2/commands.py +142 -1
  14. regscale/integrations/commercial/tenablev2/scanner.py +0 -1
  15. regscale/integrations/commercial/tenablev2/stig_parsers.py +113 -57
  16. regscale/integrations/commercial/wizv2/WizDataMixin.py +1 -1
  17. regscale/integrations/commercial/wizv2/click.py +44 -59
  18. regscale/integrations/commercial/wizv2/compliance/__init__.py +15 -0
  19. regscale/integrations/commercial/wizv2/{policy_compliance_helpers.py → compliance/helpers.py} +78 -60
  20. regscale/integrations/commercial/wizv2/compliance_report.py +10 -9
  21. regscale/integrations/commercial/wizv2/core/__init__.py +133 -0
  22. regscale/integrations/commercial/wizv2/{async_client.py → core/client.py} +3 -3
  23. regscale/integrations/commercial/wizv2/{constants.py → core/constants.py} +1 -17
  24. regscale/integrations/commercial/wizv2/core/file_operations.py +237 -0
  25. regscale/integrations/commercial/wizv2/fetchers/__init__.py +11 -0
  26. regscale/integrations/commercial/wizv2/{data_fetcher.py → fetchers/policy_assessment.py} +5 -9
  27. regscale/integrations/commercial/wizv2/issue.py +1 -1
  28. regscale/integrations/commercial/wizv2/models/__init__.py +0 -0
  29. regscale/integrations/commercial/wizv2/parsers/__init__.py +34 -0
  30. regscale/integrations/commercial/wizv2/{parsers.py → parsers/main.py} +1 -1
  31. regscale/integrations/commercial/wizv2/processors/__init__.py +11 -0
  32. regscale/integrations/commercial/wizv2/{finding_processor.py → processors/finding.py} +1 -1
  33. regscale/integrations/commercial/wizv2/reports.py +1 -1
  34. regscale/integrations/commercial/wizv2/sbom.py +1 -1
  35. regscale/integrations/commercial/wizv2/scanner.py +40 -100
  36. regscale/integrations/commercial/wizv2/utils/__init__.py +48 -0
  37. regscale/integrations/commercial/wizv2/{utils.py → utils/main.py} +116 -61
  38. regscale/integrations/commercial/wizv2/variables.py +89 -3
  39. regscale/integrations/compliance_integration.py +0 -46
  40. regscale/integrations/control_matcher.py +22 -3
  41. regscale/integrations/due_date_handler.py +14 -8
  42. regscale/integrations/public/fedramp/docx_parser.py +10 -1
  43. regscale/integrations/public/fedramp/fedramp_cis_crm.py +393 -340
  44. regscale/integrations/public/fedramp/fedramp_five.py +1 -1
  45. regscale/integrations/scanner_integration.py +127 -57
  46. regscale/models/integration_models/cisa_kev_data.json +132 -9
  47. regscale/models/integration_models/qualys.py +3 -4
  48. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  49. regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +24 -7
  50. regscale/models/integration_models/synqly_models/synqly_model.py +8 -1
  51. regscale/models/regscale_models/control_implementation.py +1 -1
  52. regscale/models/regscale_models/issue.py +0 -1
  53. {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.1.dist-info}/METADATA +1 -17
  54. {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.1.dist-info}/RECORD +94 -61
  55. tests/regscale/integrations/commercial/test_jira.py +481 -91
  56. tests/regscale/integrations/commercial/test_wiz.py +96 -200
  57. tests/regscale/integrations/commercial/wizv2/__init__.py +1 -1
  58. tests/regscale/integrations/commercial/wizv2/compliance/__init__.py +1 -0
  59. tests/regscale/integrations/commercial/wizv2/compliance/test_helpers.py +903 -0
  60. tests/regscale/integrations/commercial/wizv2/core/__init__.py +1 -0
  61. tests/regscale/integrations/commercial/wizv2/core/test_auth.py +701 -0
  62. tests/regscale/integrations/commercial/wizv2/core/test_client.py +1037 -0
  63. tests/regscale/integrations/commercial/wizv2/core/test_file_operations.py +989 -0
  64. tests/regscale/integrations/commercial/wizv2/fetchers/__init__.py +1 -0
  65. tests/regscale/integrations/commercial/wizv2/fetchers/test_policy_assessment.py +805 -0
  66. tests/regscale/integrations/commercial/wizv2/parsers/__init__.py +1 -0
  67. tests/regscale/integrations/commercial/wizv2/parsers/test_main.py +1153 -0
  68. tests/regscale/integrations/commercial/wizv2/processors/__init__.py +1 -0
  69. tests/regscale/integrations/commercial/wizv2/processors/test_finding.py +671 -0
  70. tests/regscale/integrations/commercial/wizv2/test_WizDataMixin.py +537 -0
  71. tests/regscale/integrations/commercial/wizv2/test_click_comprehensive.py +851 -0
  72. tests/regscale/integrations/commercial/wizv2/test_compliance_report_comprehensive.py +910 -0
  73. tests/regscale/integrations/commercial/wizv2/test_file_cleanup.py +283 -0
  74. tests/regscale/integrations/commercial/wizv2/test_file_operations.py +260 -0
  75. tests/regscale/integrations/commercial/wizv2/test_issue.py +1 -1
  76. tests/regscale/integrations/commercial/wizv2/test_issue_comprehensive.py +1203 -0
  77. tests/regscale/integrations/commercial/wizv2/test_reports.py +497 -0
  78. tests/regscale/integrations/commercial/wizv2/test_sbom.py +643 -0
  79. tests/regscale/integrations/commercial/wizv2/test_scanner_comprehensive.py +805 -0
  80. tests/regscale/integrations/commercial/wizv2/test_wiz_click_client_id.py +1 -1
  81. tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_report.py +72 -29
  82. tests/regscale/integrations/commercial/wizv2/test_wiz_findings_comprehensive.py +364 -0
  83. tests/regscale/integrations/commercial/wizv2/test_wiz_inventory_comprehensive.py +644 -0
  84. tests/regscale/integrations/commercial/wizv2/test_wizv2.py +946 -78
  85. tests/regscale/integrations/commercial/wizv2/test_wizv2_utils.py +97 -202
  86. tests/regscale/integrations/commercial/wizv2/utils/__init__.py +1 -0
  87. tests/regscale/integrations/commercial/wizv2/utils/test_main.py +1523 -0
  88. tests/regscale/integrations/public/test_fedramp.py +301 -0
  89. tests/regscale/integrations/test_control_matcher.py +83 -0
  90. regscale/integrations/commercial/wizv2/policy_compliance.py +0 -3543
  91. tests/regscale/integrations/commercial/wizv2/test_wiz_policy_compliance.py +0 -750
  92. /regscale/integrations/commercial/wizv2/{wiz_auth.py → core/auth.py} +0 -0
  93. {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.1.dist-info}/LICENSE +0 -0
  94. {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.1.dist-info}/WHEEL +0 -0
  95. {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.1.dist-info}/entry_points.txt +0 -0
  96. {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.1.dist-info}/top_level.txt +0 -0
@@ -849,3 +849,304 @@ class TestFedRamp(CLITestFixture):
849
849
 
850
850
  assert asset.get("ipAddress") is None
851
851
  assert asset.get("iPv6Address") is None
852
+
853
+ @staticmethod
854
+ def test_ssp_parser_vertical_table_single_header():
855
+ """Test vertical table detection with single header (original behavior)."""
856
+ from regscale.integrations.public.fedramp.docx_parser import SSPDocParser
857
+ from unittest.mock import MagicMock
858
+
859
+ xml_content = b"""
860
+ <w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
861
+ <w:tbl>
862
+ <w:tr>
863
+ <w:tc><w:p><w:r><w:t>System Owner Information</w:t></w:r></w:p></w:tc>
864
+ </w:tr>
865
+ <w:tr>
866
+ <w:tc><w:p><w:r><w:t>Name</w:t></w:r></w:p></w:tc>
867
+ <w:tc><w:p><w:r><w:t>John Doe</w:t></w:r></w:p></w:tc>
868
+ </w:tr>
869
+ <w:tr>
870
+ <w:tc><w:p><w:r><w:t>Title</w:t></w:r></w:p></w:tc>
871
+ <w:tc><w:p><w:r><w:t>System Owner</w:t></w:r></w:p></w:tc>
872
+ </w:tr>
873
+ </w:tbl>
874
+ </w:document>
875
+ """
876
+
877
+ parser = SSPDocParser.__new__(SSPDocParser)
878
+ parser.doc = MagicMock()
879
+ parser.doc.element.body = [] # No preceding text needed for this test
880
+
881
+ tables = parser.parse_xml_for_tables(xml_content)
882
+
883
+ assert len(tables) == 1
884
+ table_data = tables[0]["table_data"]
885
+ assert len(table_data) == 2
886
+ assert table_data[0] == {"Name": "John Doe"}
887
+ assert table_data[1] == {"Title": "System Owner"}
888
+
889
+ @staticmethod
890
+ def test_ssp_parser_vertical_table_two_headers_empty_second():
891
+ """Test vertical table detection with two headers where second is empty (new fix)."""
892
+ from regscale.integrations.public.fedramp.docx_parser import SSPDocParser
893
+ from unittest.mock import MagicMock
894
+
895
+ xml_content = b"""
896
+ <w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
897
+ <w:tbl>
898
+ <w:tr>
899
+ <w:tc><w:p><w:r><w:t>System Information</w:t></w:r></w:p></w:tc>
900
+ <w:tc><w:p><w:r><w:t></w:t></w:r></w:p></w:tc>
901
+ </w:tr>
902
+ <w:tr>
903
+ <w:tc><w:p><w:r><w:t>CSP Name:</w:t></w:r></w:p></w:tc>
904
+ <w:tc><w:p><w:r><w:t>Public Safety Platform (PSP)</w:t></w:r></w:p></w:tc>
905
+ </w:tr>
906
+ <w:tr>
907
+ <w:tc><w:p><w:r><w:t>CSO Name:</w:t></w:r></w:p></w:tc>
908
+ <w:tc><w:p><w:r><w:t>Mark43, Inc.</w:t></w:r></w:p></w:tc>
909
+ </w:tr>
910
+ <w:tr>
911
+ <w:tc><w:p><w:r><w:t>Service Model:</w:t></w:r></w:p></w:tc>
912
+ <w:tc><w:p><w:r><w:t>Software-as-a-Service (SaaS)</w:t></w:r></w:p></w:tc>
913
+ </w:tr>
914
+ </w:tbl>
915
+ </w:document>
916
+ """
917
+
918
+ parser = SSPDocParser.__new__(SSPDocParser)
919
+ parser.doc = MagicMock()
920
+ parser.doc.element.body = [] # No preceding text needed for this test
921
+
922
+ tables = parser.parse_xml_for_tables(xml_content)
923
+
924
+ assert len(tables) == 1
925
+ table_data = tables[0]["table_data"]
926
+ assert len(table_data) == 3
927
+ assert table_data[0] == {"CSP Name:": "Public Safety Platform (PSP)"}
928
+ assert table_data[1] == {"CSO Name:": "Mark43, Inc."}
929
+ assert table_data[2] == {"Service Model:": "Software-as-a-Service (SaaS)"}
930
+
931
+ @staticmethod
932
+ def test_ssp_parser_vertical_table_two_headers_whitespace_second():
933
+ """Test vertical table detection with two headers where second is only whitespace."""
934
+ from regscale.integrations.public.fedramp.docx_parser import SSPDocParser
935
+ from unittest.mock import MagicMock
936
+
937
+ xml_content = b"""
938
+ <w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
939
+ <w:tbl>
940
+ <w:tr>
941
+ <w:tc><w:p><w:r><w:t>System Information</w:t></w:r></w:p></w:tc>
942
+ <w:tc><w:p><w:r><w:t> </w:t></w:r></w:p></w:tc>
943
+ </w:tr>
944
+ <w:tr>
945
+ <w:tc><w:p><w:r><w:t>FedRAMP Package ID:</w:t></w:r></w:p></w:tc>
946
+ <w:tc><w:p><w:r><w:t>FR2235965777</w:t></w:r></w:p></w:tc>
947
+ </w:tr>
948
+ <w:tr>
949
+ <w:tc><w:p><w:r><w:t>FIPS PUB 199 Level:</w:t></w:r></w:p></w:tc>
950
+ <w:tc><w:p><w:r><w:t>High</w:t></w:r></w:p></w:tc>
951
+ </w:tr>
952
+ </w:tbl>
953
+ </w:document>
954
+ """
955
+
956
+ parser = SSPDocParser.__new__(SSPDocParser)
957
+ parser.doc = MagicMock()
958
+ parser.doc.element.body = [] # No preceding text needed for this test
959
+
960
+ tables = parser.parse_xml_for_tables(xml_content)
961
+
962
+ assert len(tables) == 1
963
+ table_data = tables[0]["table_data"]
964
+ assert len(table_data) == 2
965
+ assert table_data[0] == {"FedRAMP Package ID:": "FR2235965777"}
966
+ assert table_data[1] == {"FIPS PUB 199 Level:": "High"}
967
+
968
+ @staticmethod
969
+ def test_ssp_parser_horizontal_table_not_detected_as_vertical():
970
+ """Test that horizontal tables with meaningful headers are not detected as vertical."""
971
+ from regscale.integrations.public.fedramp.docx_parser import SSPDocParser
972
+ from unittest.mock import MagicMock
973
+
974
+ xml_content = b"""
975
+ <w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
976
+ <w:tbl>
977
+ <w:tr>
978
+ <w:tc><w:p><w:r><w:t>Date</w:t></w:r></w:p></w:tc>
979
+ <w:tc><w:p><w:r><w:t>Description</w:t></w:r></w:p></w:tc>
980
+ <w:tc><w:p><w:r><w:t>Version</w:t></w:r></w:p></w:tc>
981
+ </w:tr>
982
+ <w:tr>
983
+ <w:tc><w:p><w:r><w:t>05/14/2024</w:t></w:r></w:p></w:tc>
984
+ <w:tc><w:p><w:r><w:t>Initial version</w:t></w:r></w:p></w:tc>
985
+ <w:tc><w:p><w:r><w:t>1.0</w:t></w:r></w:p></w:tc>
986
+ </w:tr>
987
+ <w:tr>
988
+ <w:tc><w:p><w:r><w:t>11/01/2024</w:t></w:r></w:p></w:tc>
989
+ <w:tc><w:p><w:r><w:t>Updated version</w:t></w:r></w:p></w:tc>
990
+ <w:tc><w:p><w:r><w:t>2.0</w:t></w:r></w:p></w:tc>
991
+ </w:tr>
992
+ </w:tbl>
993
+ </w:document>
994
+ """
995
+
996
+ parser = SSPDocParser.__new__(SSPDocParser)
997
+ parser.doc = MagicMock()
998
+ parser.doc.element.body = [] # No preceding text needed for this test
999
+
1000
+ tables = parser.parse_xml_for_tables(xml_content)
1001
+
1002
+ assert len(tables) == 1
1003
+ table_data = tables[0]["table_data"]
1004
+ assert len(table_data) == 2
1005
+ # Should be horizontal format with named columns
1006
+ assert table_data[0] == {"Date": "05/14/2024", "Description": "Initial version", "Version": "1.0"}
1007
+ assert table_data[1] == {"Date": "11/01/2024", "Description": "Updated version", "Version": "2.0"}
1008
+
1009
+ @staticmethod
1010
+ def test_ssp_parser_two_column_table_non_vertical():
1011
+ """Test that 2-column tables with meaningful second header are not detected as vertical."""
1012
+ from regscale.integrations.public.fedramp.docx_parser import SSPDocParser
1013
+ from unittest.mock import MagicMock
1014
+
1015
+ xml_content = b"""
1016
+ <w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
1017
+ <w:tbl>
1018
+ <w:tr>
1019
+ <w:tc><w:p><w:r><w:t>Security Objective</w:t></w:r></w:p></w:tc>
1020
+ <w:tc><w:p><w:r><w:t>Level</w:t></w:r></w:p></w:tc>
1021
+ </w:tr>
1022
+ <w:tr>
1023
+ <w:tc><w:p><w:r><w:t>Confidentiality</w:t></w:r></w:p></w:tc>
1024
+ <w:tc><w:p><w:r><w:t>High</w:t></w:r></w:p></w:tc>
1025
+ </w:tr>
1026
+ <w:tr>
1027
+ <w:tc><w:p><w:r><w:t>Integrity</w:t></w:r></w:p></w:tc>
1028
+ <w:tc><w:p><w:r><w:t>High</w:t></w:r></w:p></w:tc>
1029
+ </w:tr>
1030
+ </w:tbl>
1031
+ </w:document>
1032
+ """
1033
+
1034
+ parser = SSPDocParser.__new__(SSPDocParser)
1035
+ parser.doc = MagicMock()
1036
+ parser.doc.element.body = [] # No preceding text needed for this test
1037
+
1038
+ tables = parser.parse_xml_for_tables(xml_content)
1039
+
1040
+ assert len(tables) == 1
1041
+ table_data = tables[0]["table_data"]
1042
+ assert len(table_data) == 2
1043
+ # Should be horizontal format since second header is not empty
1044
+ assert table_data[0] == {"Security Objective": "Confidentiality", "Level": "High"}
1045
+ assert table_data[1] == {"Security Objective": "Integrity", "Level": "High"}
1046
+
1047
+ @staticmethod
1048
+ def test_ssp_parser_vertical_table_first_header_not_in_list():
1049
+ """Test that tables with first header not in vertical_tables list are not detected as vertical."""
1050
+ from regscale.integrations.public.fedramp.docx_parser import SSPDocParser
1051
+ from unittest.mock import MagicMock
1052
+
1053
+ xml_content = b"""
1054
+ <w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
1055
+ <w:tbl>
1056
+ <w:tr>
1057
+ <w:tc><w:p><w:r><w:t>Random Table Header</w:t></w:r></w:p></w:tc>
1058
+ <w:tc><w:p><w:r><w:t></w:t></w:r></w:p></w:tc>
1059
+ </w:tr>
1060
+ <w:tr>
1061
+ <w:tc><w:p><w:r><w:t>Field One:</w:t></w:r></w:p></w:tc>
1062
+ <w:tc><w:p><w:r><w:t>Value One</w:t></w:r></w:p></w:tc>
1063
+ </w:tr>
1064
+ <w:tr>
1065
+ <w:tc><w:p><w:r><w:t>Field Two:</w:t></w:r></w:p></w:tc>
1066
+ <w:tc><w:p><w:r><w:t>Value Two</w:t></w:r></w:p></w:tc>
1067
+ </w:tr>
1068
+ </w:tbl>
1069
+ </w:document>
1070
+ """
1071
+
1072
+ parser = SSPDocParser.__new__(SSPDocParser)
1073
+ parser.doc = MagicMock()
1074
+ parser.doc.element.body = [] # No preceding text needed for this test
1075
+
1076
+ tables = parser.parse_xml_for_tables(xml_content)
1077
+
1078
+ assert len(tables) == 1
1079
+ table_data = tables[0]["table_data"]
1080
+ assert len(table_data) == 2
1081
+ # Should be horizontal format since "Random Table Header" is not in vertical_tables list
1082
+ assert table_data[0] == {"Random Table Header": "Field One:", "": "Value One"}
1083
+ assert table_data[1] == {"Random Table Header": "Field Two:", "": "Value Two"}
1084
+
1085
+ @staticmethod
1086
+ def test_ssp_parser_all_vertical_table_types():
1087
+ """Test all table types that should be detected as vertical."""
1088
+ from regscale.integrations.public.fedramp.docx_parser import SSPDocParser
1089
+ from unittest.mock import MagicMock
1090
+
1091
+ vertical_table_headers = [
1092
+ "Identification of Organization that Prepared this Document",
1093
+ "Identification of Cloud Service Provider",
1094
+ "System Owner Information",
1095
+ "System Information",
1096
+ "System Component Information",
1097
+ "ISSO (or Equivalent) Point of Contact",
1098
+ ]
1099
+
1100
+ for header in vertical_table_headers:
1101
+ # Test with single header
1102
+ xml_content = f"""
1103
+ <w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
1104
+ <w:tbl>
1105
+ <w:tr>
1106
+ <w:tc><w:p><w:r><w:t>{header}</w:t></w:r></w:p></w:tc>
1107
+ </w:tr>
1108
+ <w:tr>
1109
+ <w:tc><w:p><w:r><w:t>Field:</w:t></w:r></w:p></w:tc>
1110
+ <w:tc><w:p><w:r><w:t>Value</w:t></w:r></w:p></w:tc>
1111
+ </w:tr>
1112
+ </w:tbl>
1113
+ </w:document>
1114
+ """.encode()
1115
+
1116
+ parser = SSPDocParser.__new__(SSPDocParser)
1117
+ parser.doc = MagicMock()
1118
+ parser.doc.element.body = [] # No preceding text needed for this test
1119
+
1120
+ tables = parser.parse_xml_for_tables(xml_content)
1121
+
1122
+ assert len(tables) == 1
1123
+ table_data = tables[0]["table_data"]
1124
+ assert len(table_data) == 1
1125
+ assert table_data[0] == {"Field:": "Value"}
1126
+
1127
+ # Test with two headers (empty second)
1128
+ xml_content_two_headers = f"""
1129
+ <w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
1130
+ <w:tbl>
1131
+ <w:tr>
1132
+ <w:tc><w:p><w:r><w:t>{header}</w:t></w:r></w:p></w:tc>
1133
+ <w:tc><w:p><w:r><w:t></w:t></w:r></w:p></w:tc>
1134
+ </w:tr>
1135
+ <w:tr>
1136
+ <w:tc><w:p><w:r><w:t>Field:</w:t></w:r></w:p></w:tc>
1137
+ <w:tc><w:p><w:r><w:t>Value</w:t></w:r></w:p></w:tc>
1138
+ </w:tr>
1139
+ </w:tbl>
1140
+ </w:document>
1141
+ """.encode()
1142
+
1143
+ parser2 = SSPDocParser.__new__(SSPDocParser)
1144
+ parser2.doc = MagicMock()
1145
+ parser2.doc.element.body = [] # No preceding text needed for this test
1146
+
1147
+ tables2 = parser2.parse_xml_for_tables(xml_content_two_headers)
1148
+
1149
+ assert len(tables2) == 1
1150
+ table_data2 = tables2[0]["table_data"]
1151
+ assert len(table_data2) == 1
1152
+ assert table_data2[0] == {"Field:": "Value"}
@@ -201,6 +201,89 @@ class TestControlMatcherParseControlId:
201
201
  assert result is None, f"Expected None for input {input_id}, got {result}"
202
202
 
203
203
 
204
+ class TestControlMatcherParseControlIdWithSpaces:
205
+ """Test cases for parse_control_id method with spaces in control IDs"""
206
+
207
+ @patch("regscale.integrations.control_matcher.Api")
208
+ @patch("regscale.integrations.control_matcher.Application")
209
+ def test_parse_control_id_with_space_before_parenthesis(self, mock_app_class, mock_api_class):
210
+ """Test parsing control ID with space before parenthesis"""
211
+ matcher = ControlMatcher()
212
+ test_cases = [
213
+ ("AC-1 (1)", "AC-1.1"),
214
+ ("AC-2 (3)", "AC-2.3"),
215
+ ("SI-4 (10)", "SI-4.10"),
216
+ ("CM-6 (1)", "CM-6.1"),
217
+ ]
218
+
219
+ for input_id, expected in test_cases:
220
+ result = matcher.parse_control_id(input_id)
221
+ assert result == expected, f"Failed for input {input_id}"
222
+
223
+ @patch("regscale.integrations.control_matcher.Api")
224
+ @patch("regscale.integrations.control_matcher.Application")
225
+ def test_parse_control_id_with_spaces_inside_parentheses(self, mock_app_class, mock_api_class):
226
+ """Test parsing control ID with spaces inside parentheses"""
227
+ matcher = ControlMatcher()
228
+ test_cases = [
229
+ ("AC-1( 1 )", "AC-1.1"),
230
+ ("AC-2( 3 )", "AC-2.3"),
231
+ ("SI-4( 10)", "SI-4.10"),
232
+ ("CM-6(1 )", "CM-6.1"),
233
+ ]
234
+
235
+ for input_id, expected in test_cases:
236
+ result = matcher.parse_control_id(input_id)
237
+ assert result == expected, f"Failed for input {input_id}"
238
+
239
+ @patch("regscale.integrations.control_matcher.Api")
240
+ @patch("regscale.integrations.control_matcher.Application")
241
+ def test_parse_control_id_with_leading_zeros_and_spaces(self, mock_app_class, mock_api_class):
242
+ """Test parsing control ID with both leading zeros and spaces"""
243
+ matcher = ControlMatcher()
244
+ test_cases = [
245
+ ("AC-01 (01)", "AC-1.1"),
246
+ ("AC-02 (04)", "AC-2.4"),
247
+ ("AC-17 (02)", "AC-17.2"),
248
+ ("SI-04 (05)", "SI-4.5"),
249
+ ]
250
+
251
+ for input_id, expected in test_cases:
252
+ result = matcher.parse_control_id(input_id)
253
+ assert result == expected, f"Failed for input {input_id}"
254
+
255
+ @patch("regscale.integrations.control_matcher.Api")
256
+ @patch("regscale.integrations.control_matcher.Application")
257
+ def test_parse_control_id_with_various_space_combinations(self, mock_app_class, mock_api_class):
258
+ """Test parsing control ID with various space combinations"""
259
+ matcher = ControlMatcher()
260
+ test_cases = [
261
+ ("AC-1 (1)", "AC-1.1"), # Multiple spaces before
262
+ ("AC-1 ( 1 )", "AC-1.1"), # Spaces everywhere
263
+ ("AC-1 ( 1 )", "AC-1.1"), # Multiple spaces everywhere
264
+ ("AC-01 ( 04 )", "AC-1.4"), # Leading zeros and multiple spaces
265
+ ]
266
+
267
+ for input_id, expected in test_cases:
268
+ result = matcher.parse_control_id(input_id)
269
+ assert result == expected, f"Failed for input {input_id}"
270
+
271
+ @patch("regscale.integrations.control_matcher.Api")
272
+ @patch("regscale.integrations.control_matcher.Application")
273
+ def test_parse_control_id_with_spaces_in_text(self, mock_app_class, mock_api_class):
274
+ """Test parsing control ID with spaces in descriptive text"""
275
+ matcher = ControlMatcher()
276
+ test_cases = [
277
+ ("Access Control AC-1 (1)", "AC-1.1"),
278
+ ("AC-2 (3) Account Management", "AC-2.3"),
279
+ ("NIST Control AC-17 (02)", "AC-17.2"),
280
+ ]
281
+
282
+ for input_id, expected in test_cases:
283
+ result = matcher.parse_control_id(input_id)
284
+ assert result == expected, f"Failed for input {input_id}"
285
+
286
+
204
287
  class TestControlMatcherFindControlInCatalog:
205
288
  """Test cases for find_control_in_catalog method"""
206
289