@@ -34,6 +34,32 @@ def get_status(self):
3434 return rq .job .JobStatus .STARTED
3535
3636
37+ class MockFinishedJobResult :
38+ class Type :
39+ SUCCESSFUL = "successful"
40+ FAILED = "failed"
41+
42+ type = Type .SUCCESSFUL
43+
44+ @property
45+ def return_value (self ):
46+ return [
47+ [
48+ "OWASP Top 10 for LLM and Gen AI Apps 2025" ,
49+ "OWASP Cheat Sheets" ,
50+ ],
51+ {"status" : "done" },
52+ ]
53+
54+
55+ class MockFinishedJob :
56+ def get_status (self ):
57+ return rq .job .JobStatus .FINISHED
58+
59+ def latest_result (self ):
60+ return MockFinishedJobResult ()
61+
62+
3763class TestMain (unittest .TestCase ):
3864 def tearDown (self ) -> None :
3965 sqla .session .remove ()
@@ -878,6 +904,180 @@ def test_gap_analysis_returns_direct_cre_overlap_when_backends_fail(
878904 self .assertIn (compare .id , payload ["result" ][base .id ]["paths" ])
879905 db_mock .return_value .add_gap_analysis_result .assert_called_once ()
880906
907+ @patch .object (web_main .cre_main , "fetch_upstream_json" )
908+ @patch .object (web_main .gap_analysis , "schedule" )
909+ @patch .object (db , "Node_collection" )
910+ def test_gap_analysis_filters_generic_cheatsheets_for_llm_top10 (
911+ self , db_mock , schedule_mock , upstream_fetch_mock
912+ ) -> None :
913+ broad_cre = defs .CRE (id = "064-808" , name = "Output encoding" , description = "" )
914+ llm_specific_cre = defs .CRE (
915+ id = "161-451" , name = "Prompt boundary protection" , description = ""
916+ )
917+
918+ llm = defs .Standard (
919+ name = "OWASP Top 10 for LLM and Gen AI Apps 2025" ,
920+ sectionID = "LLM05" ,
921+ section = "Improper Output Handling" ,
922+ )
923+ llm .add_link (
924+ defs .Link (ltype = defs .LinkTypes .LinkedTo , document = broad_cre .shallow_copy ())
925+ )
926+ llm .add_link (
927+ defs .Link (
928+ ltype = defs .LinkTypes .LinkedTo , document = llm_specific_cre .shallow_copy ()
929+ )
930+ )
931+
932+ generic_cheatsheet = defs .Standard (
933+ name = "OWASP Cheat Sheets" ,
934+ section = "Cross Site Scripting Prevention Cheat Sheet" ,
935+ )
936+ generic_cheatsheet .add_link (
937+ defs .Link (ltype = defs .LinkTypes .LinkedTo , document = broad_cre .shallow_copy ())
938+ )
939+
940+ llm_cheatsheet = defs .Standard (
941+ name = "OWASP Cheat Sheets" ,
942+ section = "LLM Prompt Injection Prevention Cheat Sheet" ,
943+ )
944+ llm_cheatsheet .add_link (
945+ defs .Link (
946+ ltype = defs .LinkTypes .LinkedTo , document = llm_specific_cre .shallow_copy ()
947+ )
948+ )
949+
950+ db_mock .return_value .get_gap_analysis_result .return_value = None
951+ db_mock .return_value .gap_analysis_exists .return_value = False
952+ db_mock .return_value .get_nodes .side_effect = lambda name = None , ** kwargs : (
953+ [llm ]
954+ if name == "OWASP Top 10 for LLM and Gen AI Apps 2025"
955+ else (
956+ [generic_cheatsheet , llm_cheatsheet ]
957+ if name == "OWASP Cheat Sheets"
958+ else []
959+ )
960+ )
961+ schedule_mock .side_effect = RuntimeError ("redis unavailable" )
962+ upstream_fetch_mock .side_effect = RuntimeError ("upstream unavailable" )
963+
964+ with self .app .test_client () as client :
965+ response = client .get (
966+ "/rest/v1/map_analysis?standard=OWASP%20Top%2010%20for%20LLM%20and%20Gen%20AI%20Apps%202025&standard=OWASP%20Cheat%20Sheets" ,
967+ headers = {"Content-Type" : "application/json" },
968+ )
969+
970+ payload = json .loads (response .data )
971+ self .assertEqual (200 , response .status_code )
972+ self .assertIn ("result" , payload )
973+ self .assertEqual (
974+ "AI / LLM Cheat Sheets" ,
975+ payload ["specialized_cheatsheet_section" ]["category" ],
976+ )
977+ self .assertIn (llm .id , payload ["result" ])
978+ self .assertNotIn (llm_cheatsheet .id , payload ["result" ][llm .id ]["paths" ])
979+ self .assertNotIn (generic_cheatsheet .id , payload ["result" ][llm .id ]["paths" ])
980+
981+ @patch .object (web_main .cre_main , "fetch_upstream_json" )
982+ @patch .object (web_main .gap_analysis , "schedule" )
983+ @patch .object (db , "Node_collection" )
984+ def test_gap_analysis_keeps_prompt_injection_cheatsheet_for_llm01 (
985+ self , db_mock , schedule_mock , upstream_fetch_mock
986+ ) -> None :
987+ prompt_cre = defs .CRE (
988+ id = "161-451" , name = "Prompt boundary protection" , description = ""
989+ )
990+
991+ llm = defs .Standard (
992+ name = "OWASP Top 10 for LLM and Gen AI Apps 2025" ,
993+ sectionID = "LLM01" ,
994+ section = "Prompt Injection" ,
995+ )
996+ llm .add_link (
997+ defs .Link (ltype = defs .LinkTypes .LinkedTo , document = prompt_cre .shallow_copy ())
998+ )
999+
1000+ llm_cheatsheet = defs .Standard (
1001+ name = "OWASP Cheat Sheets" ,
1002+ section = "LLM Prompt Injection Prevention Cheat Sheet" ,
1003+ )
1004+ llm_cheatsheet .add_link (
1005+ defs .Link (ltype = defs .LinkTypes .LinkedTo , document = prompt_cre .shallow_copy ())
1006+ )
1007+
1008+ db_mock .return_value .get_gap_analysis_result .return_value = None
1009+ db_mock .return_value .gap_analysis_exists .return_value = False
1010+ db_mock .return_value .get_nodes .side_effect = lambda name = None , ** kwargs : (
1011+ [llm ]
1012+ if name == "OWASP Top 10 for LLM and Gen AI Apps 2025"
1013+ else [llm_cheatsheet ]
1014+ if name == "OWASP Cheat Sheets"
1015+ else []
1016+ )
1017+ schedule_mock .side_effect = RuntimeError ("redis unavailable" )
1018+ upstream_fetch_mock .side_effect = RuntimeError ("upstream unavailable" )
1019+
1020+ with self .app .test_client () as client :
1021+ response = client .get (
1022+ "/rest/v1/map_analysis?standard=OWASP%20Top%2010%20for%20LLM%20and%20Gen%20AI%20Apps%202025&standard=OWASP%20Cheat%20Sheets" ,
1023+ headers = {"Content-Type" : "application/json" },
1024+ )
1025+
1026+ payload = json .loads (response .data )
1027+ self .assertEqual (200 , response .status_code )
1028+ self .assertIn (llm .id , payload ["result" ])
1029+ self .assertIn (llm_cheatsheet .id , payload ["result" ][llm .id ]["paths" ])
1030+
1031+ @patch .object (web_main .cre_main , "fetch_upstream_json" )
1032+ @patch .object (web_main .gap_analysis , "schedule" )
1033+ @patch .object (db , "Node_collection" )
1034+ def test_gap_analysis_adds_specialized_section_for_api_cheatsheets (
1035+ self , db_mock , schedule_mock , upstream_fetch_mock
1036+ ) -> None :
1037+ authz_cre = defs .CRE (
1038+ id = "117-371" , name = "Centralized access control" , description = ""
1039+ )
1040+ base = defs .Standard (
1041+ name = "OWASP API Security Top 10 2023" ,
1042+ sectionID = "API1" ,
1043+ section = "Broken Object Level Authorization" ,
1044+ )
1045+ base .add_link (
1046+ defs .Link (ltype = defs .LinkTypes .LinkedTo , document = authz_cre .shallow_copy ())
1047+ )
1048+
1049+ compare = defs .Standard (
1050+ name = "OWASP Cheat Sheets" ,
1051+ section = "Authorization Cheat Sheet" ,
1052+ )
1053+ compare .add_link (
1054+ defs .Link (ltype = defs .LinkTypes .LinkedTo , document = authz_cre .shallow_copy ())
1055+ )
1056+
1057+ db_mock .return_value .get_gap_analysis_result .return_value = None
1058+ db_mock .return_value .gap_analysis_exists .return_value = False
1059+ db_mock .return_value .get_nodes .side_effect = lambda name = None , ** kwargs : (
1060+ [base ]
1061+ if name == "OWASP API Security Top 10 2023"
1062+ else [compare ] if name == "OWASP Cheat Sheets" else []
1063+ )
1064+ schedule_mock .side_effect = RuntimeError ("redis unavailable" )
1065+ upstream_fetch_mock .side_effect = RuntimeError ("upstream unavailable" )
1066+
1067+ with self .app .test_client () as client :
1068+ response = client .get (
1069+ "/rest/v1/map_analysis?standard=OWASP%20API%20Security%20Top%2010%202023&standard=OWASP%20Cheat%20Sheets" ,
1070+ headers = {"Content-Type" : "application/json" },
1071+ )
1072+
1073+ payload = json .loads (response .data )
1074+ self .assertEqual (200 , response .status_code )
1075+ self .assertEqual (
1076+ "API Cheat Sheets" ,
1077+ payload ["specialized_cheatsheet_section" ]["category" ],
1078+ )
1079+ self .assertIn (base .id , payload ["specialized_cheatsheet_section" ]["result" ])
1080+
8811081 @patch .object (web_main .gap_analysis , "schedule" )
8821082 @patch .object (db , "Node_collection" )
8831083 def test_gap_analysis_supports_opencre_as_standard (
@@ -1222,6 +1422,62 @@ def test_gap_analysis_heroku_cache_miss_returns_404(
12221422 self .assertEqual (404 , response .status_code )
12231423 redis_conn_mock .assert_not_called ()
12241424
1425+ @patch .object (web_main .redis , "connect" )
1426+ @patch .object (rq .job .Job , "fetch" )
1427+ @patch .object (db , "Node_collection" )
1428+ def test_ma_job_results_adds_specialized_cheatsheet_section (
1429+ self , db_mock , fetch_mock , redis_connect_mock
1430+ ) -> None :
1431+ shared_cre = defs .CRE (id = "161-451" , name = "Prompt boundary protection" , description = "" )
1432+ llm = defs .Standard (
1433+ name = "OWASP Top 10 for LLM and Gen AI Apps 2025" ,
1434+ sectionID = "LLM01" ,
1435+ section = "Prompt Injection" ,
1436+ )
1437+ llm .add_link (
1438+ defs .Link (ltype = defs .LinkTypes .LinkedTo , document = shared_cre .shallow_copy ())
1439+ )
1440+ cheatsheet = defs .Standard (
1441+ name = "OWASP Cheat Sheets" ,
1442+ section = "LLM Prompt Injection Prevention Cheat Sheet" ,
1443+ )
1444+ cheatsheet .add_link (
1445+ defs .Link (ltype = defs .LinkTypes .LinkedTo , document = shared_cre .shallow_copy ())
1446+ )
1447+
1448+ cached_gap = {
1449+ "result" : {
1450+ llm .id : {
1451+ "start" : llm .shallow_copy ().todict (),
1452+ "paths" : {
1453+ cheatsheet .id : {
1454+ "end" : cheatsheet .shallow_copy ().todict (),
1455+ "path" : [],
1456+ "score" : 0 ,
1457+ }
1458+ },
1459+ "extra" : 0 ,
1460+ }
1461+ }
1462+ }
1463+
1464+ fetch_mock .return_value = MockFinishedJob ()
1465+ redis_connect_mock .return_value .exists .return_value = True
1466+ db_mock .return_value .get_gap_analysis_result .return_value = json .dumps (cached_gap )
1467+
1468+ with self .app .test_client () as client :
1469+ response = client .get (
1470+ "/rest/v1/ma_job_results?id=ABC" ,
1471+ headers = {"Content-Type" : "application/json" },
1472+ )
1473+
1474+ payload = json .loads (response .data )
1475+ self .assertEqual (200 , response .status_code )
1476+ self .assertEqual (
1477+ "AI / LLM Cheat Sheets" ,
1478+ payload ["specialized_cheatsheet_section" ]["category" ],
1479+ )
1480+
12251481 @patch .object (redis , "from_url" )
12261482 @patch .object (db , "Node_collection" )
12271483 def test_standards_from_db (self , node_mock , redis_conn_mock ) -> None :
0 commit comments