diff --git a/application/cmd/cre_main.py b/application/cmd/cre_main.py index e9872e752..d5e92ab48 100644 --- a/application/cmd/cre_main.py +++ b/application/cmd/cre_main.py @@ -975,6 +975,36 @@ def run(args: argparse.Namespace) -> None: # pragma: no cover BaseParser().register_resource( secure_headers.SecureHeaders, db_connection_str=args.cache_file ) + if args.owasp_top10_2025_in: + from application.utils.external_project_parsers.parsers import owasp_top10_2025 + + BaseParser().register_resource( + owasp_top10_2025.OwaspTop10_2025, db_connection_str=args.cache_file + ) + if args.owasp_api_top10_2023_in: + from application.utils.external_project_parsers.parsers import ( + owasp_api_top10_2023, + ) + + BaseParser().register_resource( + owasp_api_top10_2023.OwaspApiTop10_2023, + db_connection_str=args.cache_file, + ) + if args.owasp_llm_top10_2025_in: + from application.utils.external_project_parsers.parsers import ( + owasp_llm_top10_2025, + ) + + BaseParser().register_resource( + owasp_llm_top10_2025.OwaspLlmTop10_2025, + db_connection_str=args.cache_file, + ) + if args.owasp_aisvs_in: + from application.utils.external_project_parsers.parsers import owasp_aisvs + + BaseParser().register_resource( + owasp_aisvs.OwaspAisvs, db_connection_str=args.cache_file + ) if args.pci_dss_4_in: from application.utils.external_project_parsers.parsers import pci_dss @@ -1158,7 +1188,7 @@ def run_librarian( # resolver may auto-link to (W2 seeded this from the golden set; here it is # the real DB-backed registry). cre_embeddings = database.get_embeddings_by_doc_type(defs.Credoctypes.CRE.value) - known_ids = set(cre_embeddings.keys()) + known_ids = {cre.external_id for cre in database.get_CREs()} # in_memory loads the hub matrix; pgvector ranks in the DB over the # embedding_vec column (no in-RAM pool). Both honor the same retrieve(). pool = ( @@ -1204,6 +1234,23 @@ def run_librarian( explicit += 1 logger.info("[explicit] %s -> %s", section.chunk_id, resolution.cre_ids[0]) continue + if resolution.outcome == ResolutionOutcome.no_reference: + # Continue to semantic retrieval below + pass + elif resolution.outcome in ( + ResolutionOutcome.unknown_reference, + ResolutionOutcome.conflicting_references, + ): + logger.info("[review] %s -> %s", section.chunk_id, resolution.outcome) + continue + else: + rejected += 1 + logger.warning( + "[review] %s skipped: unexpected resolution outcome %s", + section.chunk_id, + resolution.outcome, + ) + continue try: audit = retriever.retrieve(section.text) diff --git a/application/tests/chat_completion_test.py b/application/tests/chat_completion_test.py index 4eb6e6725..f601d7df5 100644 --- a/application/tests/chat_completion_test.py +++ b/application/tests/chat_completion_test.py @@ -50,7 +50,7 @@ def test_completion_returns_503_json_on_gemini_429(self) -> None: self.assertIn("error", data) self.assertIn("rate-limited", data["error"]) - def test_completion_returns_500_on_non_429_genai_error(self) -> None: + def test_completion_returns_provider_status_on_non_429_genai_error(self) -> None: os.environ["NO_LOGIN"] = "1" err = genai_errors.ClientError( 400, @@ -71,7 +71,7 @@ def test_completion_returns_500_on_non_429_genai_error(self) -> None: json={"prompt": "test"}, content_type="application/json", ) - self.assertEqual(500, response.status_code) + self.assertEqual(400, response.status_code) data = json.loads(response.data) self.assertIn("error", data) self.assertIn("AI Service Error", data["error"]) diff --git a/application/tests/fixtures/owasp_mappings/owasp_aisvs_1_0.json b/application/tests/fixtures/owasp_mappings/owasp_aisvs_1_0.json index c4880546f..c06c4d5ad 100644 --- a/application/tests/fixtures/owasp_mappings/owasp_aisvs_1_0.json +++ b/application/tests/fixtures/owasp_mappings/owasp_aisvs_1_0.json @@ -1,86 +1,74 @@ [ { "section_id": "AISVS1", - "section": "Training Data Governance & Bias Management", - "hyperlink": "https://github.com/OWASP/AISVS/tree/main/1.0/en/0x10-C01-Training-Data-Governance.md", + "section": "Training Data Integrity & Traceability", + "hyperlink": "https://github.com/OWASP/AISVS/blob/main/1.0/en/0x10-C01-Training-Data-Integrity-and-Traceability.md", "cre_ids": ["227-045", "307-507"] }, { "section_id": "AISVS2", - "section": "User Input Validation", - "hyperlink": "https://github.com/OWASP/AISVS/tree/main/1.0/en/0x10-C02-User-Input-Validation.md", + "section": "Input Validation", + "hyperlink": "https://github.com/OWASP/AISVS/blob/main/1.0/en/0x10-C02-Input-Validation.md", "cre_ids": ["031-447", "760-764"] }, { "section_id": "AISVS3", "section": "Model Lifecycle Management & Change Control", - "hyperlink": "https://github.com/OWASP/AISVS/tree/main/1.0/en/0x10-C03-Model-Lifecycle-Management.md", + "hyperlink": "https://github.com/OWASP/AISVS/blob/main/1.0/en/0x10-C03-Model-Lifecycle-Management.md", "cre_ids": ["148-853", "613-285"] }, { "section_id": "AISVS4", "section": "Infrastructure, Configuration & Deployment Security", - "hyperlink": "https://github.com/OWASP/AISVS/tree/main/1.0/en/0x10-C04-Infrastructure.md", + "hyperlink": "https://github.com/OWASP/AISVS/blob/main/1.0/en/0x10-C04-Infrastructure.md", "cre_ids": ["233-748", "486-813"] }, { "section_id": "AISVS5", "section": "Access Control & Identity for AI Components & Users", - "hyperlink": "https://github.com/OWASP/AISVS/tree/main/1.0/en/0x10-C05-Access-Control-and-Identity.md", + "hyperlink": "https://github.com/OWASP/AISVS/blob/main/1.0/en/0x10-C05-Access-Control-and-Identity.md", "cre_ids": ["633-428", "724-770"] }, { "section_id": "AISVS6", "section": "Supply Chain Security for Models, Frameworks & Data", - "hyperlink": "https://github.com/OWASP/AISVS/tree/main/1.0/en/0x10-C06-Supply-Chain.md", + "hyperlink": "https://github.com/OWASP/AISVS/blob/main/1.0/en/0x10-C06-Supply-Chain.md", "cre_ids": ["613-285", "613-287", "863-521"] }, { "section_id": "AISVS7", "section": "Model Behavior, Output Control & Safety Assurance", - "hyperlink": "https://github.com/OWASP/AISVS/tree/main/1.0/en/0x10-C07-Model-Behavior.md", + "hyperlink": "https://github.com/OWASP/AISVS/blob/main/1.0/en/0x10-C07-Model-Behavior.md", "cre_ids": ["064-808", "141-555"] }, { "section_id": "AISVS8", "section": "Memory, Embeddings & Vector Database Security", - "hyperlink": "https://github.com/OWASP/AISVS/tree/main/1.0/en/0x10-C08-Memory-Embeddings-and-Vector-Database.md", + "hyperlink": "https://github.com/OWASP/AISVS/blob/main/1.0/en/0x10-C08-Memory-Embeddings-and-Vector-Database.md", "cre_ids": ["126-668", "538-770"] }, { "section_id": "AISVS9", "section": "Autonomous Orchestration & Agentic Action Security", - "hyperlink": "https://github.com/OWASP/AISVS/tree/main/1.0/en/0x10-C09-Orchestration-and-Agentic-Action.md", + "hyperlink": "https://github.com/OWASP/AISVS/blob/main/1.0/en/0x10-C09-Orchestration-and-Agentic-Action.md", "cre_ids": ["117-371", "650-560"] }, { "section_id": "AISVS10", "section": "Model Context Protocol (MCP) Security", - "hyperlink": "https://github.com/OWASP/AISVS/tree/main/1.0/en/0x10-C10-MCP-Security.md", + "hyperlink": "https://github.com/OWASP/AISVS/blob/main/1.0/en/0x10-C10-MCP-Security.md", "cre_ids": ["307-507", "715-223"] }, { "section_id": "AISVS11", "section": "Adversarial Robustness & Privacy Defense", - "hyperlink": "https://github.com/OWASP/AISVS/tree/main/1.0/en/0x10-C11-Adversarial-Robustness.md", + "hyperlink": "https://github.com/OWASP/AISVS/blob/main/1.0/en/0x10-C11-Adversarial-Robustness.md", "cre_ids": ["141-555", "623-550"] }, { "section_id": "AISVS12", - "section": "Privacy Protection & Personal Data Management", - "hyperlink": "https://github.com/OWASP/AISVS/tree/main/1.0/en/0x10-C12-Privacy.md", - "cre_ids": ["126-668", "227-045", "482-866"] - }, - { - "section_id": "AISVS13", "section": "Monitoring, Logging & Anomaly Detection", - "hyperlink": "https://github.com/OWASP/AISVS/tree/main/1.0/en/0x10-C13-Monitoring-and-Logging.md", + "hyperlink": "https://github.com/OWASP/AISVS/tree/main/1.0/en/0x10-C12-Monitoring-and-Logging.md", "cre_ids": ["058-083", "148-420", "402-706", "843-841"] - }, - { - "section_id": "AISVS14", - "section": "Human Oversight, Accountability & Governance", - "hyperlink": "https://github.com/OWASP/AISVS/tree/main/1.0/en/0x10-C14-Human-Oversight.md", - "cre_ids": ["162-655", "766-162"] } ] diff --git a/application/tests/owasp_aisvs_parser_test.py b/application/tests/owasp_aisvs_parser_test.py new file mode 100644 index 000000000..3a83299af --- /dev/null +++ b/application/tests/owasp_aisvs_parser_test.py @@ -0,0 +1,62 @@ +import unittest + +from application import create_app, sqla # type: ignore +from application.database import db +from application.defs import cre_defs as defs +from application.prompt_client import prompt_client +from application.utils.external_project_parsers.parsers import owasp_aisvs + + +class TestOwaspAisvsParser(unittest.TestCase): + def tearDown(self) -> None: + sqla.session.remove() + sqla.drop_all() + self.app_context.pop() + + def setUp(self) -> None: + self.app = create_app(mode="test") + self.app_context = self.app.app_context() + self.app_context.push() + sqla.create_all() + self.collection = db.Node_collection() + + def test_parse(self) -> None: + for cre_id, name in [ + ("227-045", "Identify sensitive data and subject it to a policy"), + ( + "307-507", + "Allow only trusted sources both build time and runtime; therefore perform integrity checks on all resources and code", + ), + ( + "058-083", + "Generate and retain audit logs for security events", + ), + ]: + self.collection.add_cre(defs.CRE(id=cre_id, name=name, description="")) + + result = owasp_aisvs.OwaspAisvs().parse( + self.collection, prompt_client.PromptHandler(database=self.collection) + ) + + entries = result.results["OWASP AI Security Verification Standard (AISVS)"] + self.assertEqual(12, len(entries)) + self.assertEqual("AISVS1", entries[0].sectionID) + self.assertEqual("Training Data Integrity & Traceability", entries[0].section) + self.assertEqual( + "https://github.com/OWASP/AISVS/blob/main/1.0/en/0x10-C01-Training-Data-Integrity-and-Traceability.md", + entries[0].hyperlink, + ) + self.assertEqual( + ["227-045", "307-507"], + [link.document.id for link in entries[0].links], + ) + self.assertEqual("AISVS12", entries[-1].sectionID) + self.assertEqual("Monitoring, Logging & Anomaly Detection", entries[-1].section) + self.assertEqual( + "https://github.com/OWASP/AISVS/tree/main/1.0/en/0x10-C12-Monitoring-and-Logging.md", + entries[-1].hyperlink, + ) + self.assertEqual( + ["058-083"], + [link.document.id for link in entries[-1].links], + ) diff --git a/application/tests/owasp_api_top10_2023_parser_test.py b/application/tests/owasp_api_top10_2023_parser_test.py new file mode 100644 index 000000000..4f7d8e1b9 --- /dev/null +++ b/application/tests/owasp_api_top10_2023_parser_test.py @@ -0,0 +1,47 @@ +import unittest + +from application import create_app, sqla # type: ignore +from application.database import db +from application.defs import cre_defs as defs +from application.prompt_client import prompt_client +from application.utils.external_project_parsers.parsers import owasp_api_top10_2023 + + +class TestOwaspApiTop10_2023Parser(unittest.TestCase): + def tearDown(self) -> None: + sqla.session.remove() + sqla.drop_all() + self.app_context.pop() + + def setUp(self) -> None: + self.app = create_app(mode="test") + self.app_context = self.app.app_context() + self.app_context.push() + sqla.create_all() + self.collection = db.Node_collection() + + def test_parse(self) -> None: + for cre_id, name in [ + ("304-667", "Protect API against unauthorized access/modification (IDOR)"), + ("724-770", "Technical application access control"), + ("715-223", "Ensure trusted origin of third party resources"), + ]: + self.collection.add_cre(defs.CRE(id=cre_id, name=name, description="")) + + result = owasp_api_top10_2023.OwaspApiTop10_2023().parse( + self.collection, prompt_client.PromptHandler(database=self.collection) + ) + + entries = result.results["OWASP API Security Top 10 2023"] + self.assertEqual(10, len(entries)) + self.assertEqual("API1", entries[0].sectionID) + self.assertEqual("Broken Object Level Authorization", entries[0].section) + self.assertEqual( + ["304-667", "724-770"], + [link.document.id for link in entries[0].links], + ) + self.assertEqual("API10", entries[-1].sectionID) + self.assertEqual( + ["715-223"], + [link.document.id for link in entries[-1].links], + ) diff --git a/application/tests/owasp_llm_top10_2025_parser_test.py b/application/tests/owasp_llm_top10_2025_parser_test.py new file mode 100644 index 000000000..f31e4316e --- /dev/null +++ b/application/tests/owasp_llm_top10_2025_parser_test.py @@ -0,0 +1,45 @@ +import unittest + +from application import create_app, sqla # type: ignore +from application.database import db +from application.defs import cre_defs as defs +from application.prompt_client import prompt_client +from application.utils.external_project_parsers.parsers import owasp_llm_top10_2025 + + +class TestOwaspLlmTop10_2025Parser(unittest.TestCase): + def tearDown(self) -> None: + sqla.session.remove() + sqla.drop_all() + self.app_context.pop() + + def setUp(self) -> None: + self.app = create_app(mode="test") + self.app_context = self.app.app_context() + self.app_context.push() + sqla.create_all() + self.collection = db.Node_collection() + + def test_parse(self) -> None: + for cre_id, name in [ + ("161-451", "Output encoding and injection prevention"), + ("064-808", "Encode output context-specifically"), + ("760-764", "Injection protection"), + ("623-550", "Denial Of Service protection"), + ]: + self.collection.add_cre(defs.CRE(id=cre_id, name=name, description="")) + + result = owasp_llm_top10_2025.OwaspLlmTop10_2025().parse( + self.collection, prompt_client.PromptHandler(database=self.collection) + ) + + entries = result.results["OWASP Top 10 for LLM and Gen AI Apps 2025"] + self.assertEqual(10, len(entries)) + self.assertEqual("LLM01", entries[0].sectionID) + self.assertEqual("Prompt Injection", entries[0].section) + self.assertEqual( + ["161-451", "760-764"], [link.document.id for link in entries[0].links] + ) + self.assertEqual(["064-808"], [link.document.id for link in entries[4].links]) + self.assertEqual("LLM10", entries[-1].sectionID) + self.assertEqual(["623-550"], [link.document.id for link in entries[-1].links]) diff --git a/application/tests/owasp_top10_2025_parser_test.py b/application/tests/owasp_top10_2025_parser_test.py new file mode 100644 index 000000000..de4f86a9f --- /dev/null +++ b/application/tests/owasp_top10_2025_parser_test.py @@ -0,0 +1,80 @@ +import unittest + +from application import create_app, sqla # type: ignore +from application.database import db +from application.defs import cre_defs as defs +from application.prompt_client import prompt_client +from application.utils.external_project_parsers.parsers import owasp_top10_2025 + + +class TestOwaspTop10_2025Parser(unittest.TestCase): + def tearDown(self) -> None: + sqla.session.remove() + sqla.drop_all() + self.app_context.pop() + + def setUp(self) -> None: + self.app = create_app(mode="test") + self.app_context = self.app.app_context() + self.app_context.push() + sqla.create_all() + self.collection = db.Node_collection() + + def test_parse(self) -> None: + self.collection.add_cre( + defs.CRE(id="177-260", name="Session management", description="") + ) + self.collection.add_cre( + defs.CRE( + id="117-371", + name="Use a centralized access control mechanism", + description="", + ) + ) + self.collection.add_cre( + defs.CRE( + id="724-770", + name="Technical application access control", + description="", + ) + ) + self.collection.add_cre( + defs.CRE( + id="031-447", name="Whitelist all external (HTTP) input", description="" + ) + ) + self.collection.add_cre( + defs.CRE( + id="064-808", name="Encode output context-specifically", description="" + ) + ) + self.collection.add_cre( + defs.CRE(id="760-764", name="Injection protection", description="") + ) + self.collection.add_cre( + defs.CRE(id="513-183", name="Error handling", description="") + ) + + result = owasp_top10_2025.OwaspTop10_2025().parse( + self.collection, + prompt_client.PromptHandler(database=self.collection), + ) + + entries = result.results["OWASP Top 10 2025"] + self.assertEqual(10, len(entries)) + self.assertEqual("A01", entries[0].sectionID) + self.assertEqual("Broken Access Control", entries[0].section) + self.assertEqual( + "https://owasp.org/Top10/2025/A01_2025-Broken_Access_Control/", + entries[0].hyperlink, + ) + self.assertEqual( + ["117-371", "177-260", "724-770"], + [link.document.id for link in entries[0].links], + ) + self.assertEqual( + ["031-447", "064-808", "760-764"], + [link.document.id for link in entries[4].links], + ) + self.assertEqual("A10", entries[-1].sectionID) + self.assertEqual(["513-183"], [link.document.id for link in entries[-1].links]) diff --git a/application/tests/web_main_test.py b/application/tests/web_main_test.py index 1bff0f94e..092e727b0 100644 --- a/application/tests/web_main_test.py +++ b/application/tests/web_main_test.py @@ -1561,7 +1561,6 @@ def test_import_from_cre_csv(self) -> None: buffered=True, content_type="multipart/form-data", ) - print(f"\nSTATUS CODE: {response.status_code}, DATA: {response.data}") self.assertEqual(200, response.status_code) data = json.loads(response.data) self.assertEqual("success", data.get("status")) diff --git a/application/utils/external_project_parsers/parsers/owasp_aisvs.py b/application/utils/external_project_parsers/parsers/owasp_aisvs.py new file mode 100644 index 000000000..d32595666 --- /dev/null +++ b/application/utils/external_project_parsers/parsers/owasp_aisvs.py @@ -0,0 +1,51 @@ +import json +from pathlib import Path + +from application.database import db +from application.defs import cre_defs as defs +from application.prompt_client import prompt_client +from application.utils.external_project_parsers.base_parser_defs import ( + ParseResult, + ParserInterface, +) + + +class OwaspAisvs(ParserInterface): + name = "OWASP AI Security Verification Standard (AISVS)" + data_file = ( + Path(__file__).resolve().parents[3] + / "tests" + / "fixtures" + / "owasp_mappings" + / "owasp_aisvs_1_0.json" + ) + + def parse(self, cache: db.Node_collection, ph: prompt_client.PromptHandler): + with self.data_file.open("r", encoding="utf-8") as handle: + raw_entries = json.load(handle) + + entries = [] + for entry in raw_entries: + standard = defs.Standard( + name=self.name, + sectionID=entry["section_id"], + section=entry["section"], + hyperlink=entry["hyperlink"], + ) + for cre_id in entry.get("cre_ids", []): + cres = cache.get_CREs(external_id=cre_id) + if not cres: + continue + standard.add_link( + defs.Link( + ltype=defs.LinkTypes.LinkedTo, + document=cres[0].shallow_copy(), + ) + ) + entries.append(standard) + + return ParseResult( + results={self.name: entries}, + calculate_gap_analysis=False, + calculate_embeddings=False, + ) diff --git a/application/utils/external_project_parsers/parsers/owasp_api_top10_2023.py b/application/utils/external_project_parsers/parsers/owasp_api_top10_2023.py new file mode 100644 index 000000000..18c8310b2 --- /dev/null +++ b/application/utils/external_project_parsers/parsers/owasp_api_top10_2023.py @@ -0,0 +1,51 @@ +import json +from pathlib import Path + +from application.database import db +from application.defs import cre_defs as defs +from application.prompt_client import prompt_client +from application.utils.external_project_parsers.base_parser_defs import ( + ParseResult, + ParserInterface, +) + + +class OwaspApiTop10_2023(ParserInterface): + name = "OWASP API Security Top 10 2023" + data_file = ( + Path(__file__).resolve().parents[3] + / "tests" + / "fixtures" + / "owasp_mappings" + / "owasp_api_top10_2023.json" + ) + + def parse(self, cache: db.Node_collection, ph: prompt_client.PromptHandler): + with self.data_file.open("r", encoding="utf-8") as handle: + raw_entries = json.load(handle) + + entries = [] + for entry in raw_entries: + standard = defs.Standard( + name=self.name, + sectionID=entry["section_id"], + section=entry["section"], + hyperlink=entry["hyperlink"], + ) + for cre_id in entry.get("cre_ids", []): + cres = cache.get_CREs(external_id=cre_id) + if not cres: + continue + standard.add_link( + defs.Link( + ltype=defs.LinkTypes.LinkedTo, + document=cres[0].shallow_copy(), + ) + ) + entries.append(standard) + + return ParseResult( + results={self.name: entries}, + calculate_gap_analysis=False, + calculate_embeddings=False, + ) diff --git a/application/utils/external_project_parsers/parsers/owasp_llm_top10_2025.py b/application/utils/external_project_parsers/parsers/owasp_llm_top10_2025.py new file mode 100644 index 000000000..233596cd6 --- /dev/null +++ b/application/utils/external_project_parsers/parsers/owasp_llm_top10_2025.py @@ -0,0 +1,51 @@ +import json +from pathlib import Path + +from application.database import db +from application.defs import cre_defs as defs +from application.prompt_client import prompt_client +from application.utils.external_project_parsers.base_parser_defs import ( + ParseResult, + ParserInterface, +) + + +class OwaspLlmTop10_2025(ParserInterface): + name = "OWASP Top 10 for LLM and Gen AI Apps 2025" + data_file = ( + Path(__file__).resolve().parents[3] + / "tests" + / "fixtures" + / "owasp_mappings" + / "owasp_llm_top10_2025.json" + ) + + def parse(self, cache: db.Node_collection, ph: prompt_client.PromptHandler): + with self.data_file.open("r", encoding="utf-8") as handle: + raw_entries = json.load(handle) + + entries = [] + for entry in raw_entries: + standard = defs.Standard( + name=self.name, + sectionID=entry["section_id"], + section=entry["section"], + hyperlink=entry["hyperlink"], + ) + for cre_id in entry.get("cre_ids", []): + cres = cache.get_CREs(external_id=cre_id) + if not cres: + continue + standard.add_link( + defs.Link( + ltype=defs.LinkTypes.LinkedTo, + document=cres[0].shallow_copy(), + ) + ) + entries.append(standard) + + return ParseResult( + results={self.name: entries}, + calculate_gap_analysis=False, + calculate_embeddings=False, + ) diff --git a/application/utils/external_project_parsers/parsers/owasp_top10_2025.py b/application/utils/external_project_parsers/parsers/owasp_top10_2025.py new file mode 100644 index 000000000..51b59262f --- /dev/null +++ b/application/utils/external_project_parsers/parsers/owasp_top10_2025.py @@ -0,0 +1,51 @@ +import json +from pathlib import Path + +from application.database import db +from application.defs import cre_defs as defs +from application.prompt_client import prompt_client +from application.utils.external_project_parsers.base_parser_defs import ( + ParseResult, + ParserInterface, +) + + +class OwaspTop10_2025(ParserInterface): + name = "OWASP Top 10 2025" + data_file = ( + Path(__file__).resolve().parents[3] + / "tests" + / "fixtures" + / "owasp_mappings" + / "owasp_top10_2025.json" + ) + + def parse(self, cache: db.Node_collection, ph: prompt_client.PromptHandler): + with self.data_file.open("r", encoding="utf-8") as handle: + raw_entries = json.load(handle) + + entries = [] + for entry in raw_entries: + standard = defs.Standard( + name=self.name, + sectionID=entry["section_id"], + section=entry["section"], + hyperlink=entry["hyperlink"], + ) + for cre_id in entry.get("cre_ids", []): + cres = cache.get_CREs(external_id=cre_id) + if not cres: + continue + standard.add_link( + defs.Link( + ltype=defs.LinkTypes.LinkedTo, + document=cres[0].shallow_copy(), + ) + ) + entries.append(standard) + + return ParseResult( + results={self.name: entries}, + calculate_gap_analysis=False, + calculate_embeddings=False, + ) diff --git a/application/web/web_main.py b/application/web/web_main.py index d338ed23f..ec6924de5 100644 --- a/application/web/web_main.py +++ b/application/web/web_main.py @@ -53,6 +53,30 @@ MAX_ITEMS_PER_PAGE = 100 OPENCRE_STANDARD_NAME = gap_analysis.OPENCRE_STANDARD_NAME + +def _llm_error_status_code(err: BaseException) -> int | None: + """Best-effort extraction of an HTTP-like status code from provider errors.""" + for attr in ("status", "status_code", "http_status", "code"): + value = getattr(err, attr, None) + if isinstance(value, int) and 400 <= value <= 599: + return value + + if isinstance(getattr(err, "args", None), tuple) and err.args: + nested = err.args[0] + if isinstance(nested, dict): + for key in ("code", "status_code"): + value = nested.get(key) + if isinstance(value, int) and 400 <= value <= 599: + return value + nested_error = nested.get("error") + if isinstance(nested_error, dict): + for key in ("code", "status_code"): + value = nested_error.get(key) + if isinstance(value, int) and 400 <= value <= 599: + return value + return None + + app = Blueprint( "web", __name__, @@ -1196,9 +1220,10 @@ def chat_cre() -> Any: ), 503, ) + status_code = _llm_error_status_code(e) or 500 return ( jsonify({"error": f"AI Service Error: {str(e)}"}), - 500, + status_code, ) return jsonify(response) diff --git a/cre.py b/cre.py index 5cbe86ae5..2b7e36ebd 100644 --- a/cre.py +++ b/cre.py @@ -168,6 +168,26 @@ def main() -> None: action="store_true", help="import owasp secure headers", ) + parser.add_argument( + "--owasp_top10_2025_in", + action="store_true", + help="import OWASP Top 10 2025", + ) + parser.add_argument( + "--owasp_api_top10_2023_in", + action="store_true", + help="import OWASP API Security Top 10 2023", + ) + parser.add_argument( + "--owasp_llm_top10_2025_in", + action="store_true", + help="import OWASP Top 10 for LLM and Gen AI Apps 2025", + ) + parser.add_argument( + "--owasp_aisvs_in", + action="store_true", + help="import OWASP AI Security Verification Standard (AISVS)", + ) parser.add_argument( "--pci_dss_3_2_in", action="store_true",