From aac9e3b3a99c186b2097624ae1f840ae65ebe396 Mon Sep 17 00:00:00 2001 From: bornunique911 Date: Wed, 1 Jul 2026 18:33:23 +0530 Subject: [PATCH] feat: normalize OWASP cheat sheet references --- application/tests/cheatsheets_parser_test.py | 51 +++++++++-- .../data/owasp_cheatsheets_supplement.json | 47 ++++++++++ .../parsers/cheatsheets_parser.py | 89 +++++++++++++++++-- 3 files changed, 176 insertions(+), 11 deletions(-) create mode 100644 application/utils/external_project_parsers/data/owasp_cheatsheets_supplement.json diff --git a/application/tests/cheatsheets_parser_test.py b/application/tests/cheatsheets_parser_test.py index 1a3ba4bf0..8afa57045 100644 --- a/application/tests/cheatsheets_parser_test.py +++ b/application/tests/cheatsheets_parser_test.py @@ -34,7 +34,13 @@ class Repo: repo.working_dir = loc cre = defs.CRE(name="blah", id="223-780") self.collection.add_cre(cre) - with open(os.path.join(os.path.join(loc, "cheatsheets"), "cs.md"), "w") as mdf: + with open( + os.path.join( + os.path.join(loc, "cheatsheets"), + "Secrets_Management_Cheat_Sheet.md", + ), + "w", + ) as mdf: mdf.write(cs) mock_clone.return_value = repo entries = cheatsheets_parser.Cheatsheets().parse( @@ -45,22 +51,55 @@ class Repo: # verify the external tagging convention, not just enum wiring. expected = defs.Standard( name="OWASP Cheat Sheets", - hyperlink="https://github.com/foo/bar/tree/master/cs.md", + hyperlink="https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html", section="Secrets Management Cheat Sheet", - links=[defs.Link(document=cre, ltype=defs.LinkTypes.LinkedTo)], + links=[defs.Link(document=cre, ltype=defs.LinkTypes.AutomaticallyLinkedTo)], tags=[ "family:guidance", "subtype:cheatsheet", - "source:owasp_cheatsheets", "audience:developer", "maturity:stable", + "source:owasp_cheatsheets", ], ) self.maxDiff = None for name, nodes in entries.results.items(): self.assertEqual(name, parser.name) - self.assertEqual(len(nodes), 1) - self.assertCountEqual(expected.todict(), nodes[0].todict()) + sections = {node.section for node in nodes} + self.assertIn("Secrets Management Cheat Sheet", sections) + secret_entry = next( + ( + node + for node in nodes + if node.section == "Secrets Management Cheat Sheet" + ), + None, + ) + self.assertIsNotNone(secret_entry) + self.assertEqual(expected.todict(), secret_entry.todict()) + + def test_register_supplemental_cheatsheets(self) -> None: + for cre_id, name in [ + ("118-110", "API/web services"), + ("724-770", "Technical application access control"), + ("623-550", "Denial Of Service protection"), + ]: + self.collection.add_cre(defs.CRE(name=name, id=cre_id)) + + entries = cheatsheets_parser.Cheatsheets().register_supplemental_cheatsheets( + cache=self.collection + ) + rest = [ + entry for entry in entries if entry.section == "REST Security Cheat Sheet" + ][0] + self.assertEqual( + "https://cheatsheetseries.owasp.org/cheatsheets/REST_Security_Cheat_Sheet.html", + rest.hyperlink, + ) + self.assertEqual( + ["118-110", "724-770", "623-550"], + [link.document.id for link in rest.links], + ) cheatsheets_md = """ # Secrets Management Cheat Sheet diff --git a/application/utils/external_project_parsers/data/owasp_cheatsheets_supplement.json b/application/utils/external_project_parsers/data/owasp_cheatsheets_supplement.json new file mode 100644 index 000000000..4e06bee8c --- /dev/null +++ b/application/utils/external_project_parsers/data/owasp_cheatsheets_supplement.json @@ -0,0 +1,47 @@ +[ + { + "section": "Authorization Cheat Sheet", + "hyperlink": "https://cheatsheetseries.owasp.org/cheatsheets/Authorization_Cheat_Sheet.html", + "cre_ids": ["128-128", "117-371"] + }, + { + "section": "REST Security Cheat Sheet", + "hyperlink": "https://cheatsheetseries.owasp.org/cheatsheets/REST_Security_Cheat_Sheet.html", + "cre_ids": ["118-110", "724-770", "623-550"] + }, + { + "section": "Server Side Request Forgery Prevention Cheat Sheet", + "hyperlink": "https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html", + "cre_ids": ["028-728", "657-084"] + }, + { + "section": "Docker Security Cheat Sheet", + "hyperlink": "https://cheatsheetseries.owasp.org/cheatsheets/Docker_Security_Cheat_Sheet.html", + "cre_ids": ["233-748", "486-813"] + }, + { + "section": "Kubernetes Security Cheat Sheet", + "hyperlink": "https://cheatsheetseries.owasp.org/cheatsheets/Kubernetes_Security_Cheat_Sheet.html", + "cre_ids": ["467-784", "233-748", "486-813"] + }, + { + "section": "Secure Cloud Architecture Cheat Sheet", + "hyperlink": "https://cheatsheetseries.owasp.org/cheatsheets/Secure_Cloud_Architecture_Cheat_Sheet.html", + "cre_ids": ["155-155", "467-784"] + }, + { + "section": "LLM Prompt Injection Prevention Cheat Sheet", + "hyperlink": "https://cheatsheetseries.owasp.org/cheatsheets/LLM_Prompt_Injection_Prevention_Cheat_Sheet.html", + "cre_ids": ["161-451", "760-764"] + }, + { + "section": "AI Agent Security Cheat Sheet", + "hyperlink": "https://cheatsheetseries.owasp.org/cheatsheets/AI_Agent_Security_Cheat_Sheet.html", + "cre_ids": ["117-371", "650-560", "126-668"] + }, + { + "section": "Secure AI Model Ops Cheat Sheet", + "hyperlink": "https://cheatsheetseries.owasp.org/cheatsheets/Secure_AI_Model_Ops_Cheat_Sheet.html", + "cre_ids": ["148-853", "613-285", "613-287"] + } +] diff --git a/application/utils/external_project_parsers/parsers/cheatsheets_parser.py b/application/utils/external_project_parsers/parsers/cheatsheets_parser.py index e695414d8..16c925d3d 100644 --- a/application/utils/external_project_parsers/parsers/cheatsheets_parser.py +++ b/application/utils/external_project_parsers/parsers/cheatsheets_parser.py @@ -6,6 +6,9 @@ import os import re from application.utils.external_project_parsers import base_parser_defs +import json +from pathlib import Path +import logging from application.utils.external_project_parsers.base_parser_defs import ( ParserInterface, ParseResult, @@ -15,6 +18,13 @@ class Cheatsheets(ParserInterface): name = "OWASP Cheat Sheets" + cheatsheetseries_base_url = "https://cheatsheetseries.owasp.org/cheatsheets" + supplement_data_file = ( + Path(__file__).resolve().parent.parent + / "data" + / "owasp_cheatsheets_supplement.json" + ) + logger = logging.getLogger(__name__) def cheatsheet( self, section: str, hyperlink: str, tags: List[str] @@ -33,13 +43,31 @@ def cheatsheet( hyperlink=hyperlink, ) + def official_cheatsheet_url(self, markdown_filename: str) -> str: + html_name = os.path.splitext(markdown_filename)[0] + ".html" + return f"{self.cheatsheetseries_base_url}/{html_name}" + def parse(self, cache: db.Node_collection, ph: prompt_client.PromptHandler): c_repo = "https://github.com/OWASP/CheatSheetSeries.git" cheatsheets_path = "cheatsheets/" - repo = git.clone(c_repo, sparse_paths=["cheatsheets"], sparse_cone=True) - cheatsheets = self.register_cheatsheets( - repo=repo, cache=cache, cheatsheets_path=cheatsheets_path, repo_path=c_repo - ) + cheatsheets = [] + repo = None + try: + repo = git.clone(c_repo, sparse_paths=["cheatsheets"], sparse_cone=True) + except Exception as exc: + self.logger.warning( + "Unable to clone OWASP CheatSheetSeries, continuing with supplemental cheat sheets only: %s", + exc, + ) + if repo: + cheatsheets = self.register_cheatsheets( + repo=repo, + cache=cache, + cheatsheets_path=cheatsheets_path, + repo_path=c_repo, + ) + cheatsheets.extend(self.register_supplemental_cheatsheets(cache=cache)) + cheatsheets = self.deduplicate_entries(cheatsheets) results = {self.name: cheatsheets} base_parser_defs.validate_classification_tags(results) return ParseResult(results=results) @@ -65,7 +93,7 @@ def register_cheatsheets( name = title.group("title") cre_id = cre.group("cre") cres = cache.get_CREs(external_id=cre_id) - hyperlink = f"{repo_path.replace('.git','')}/tree/master/{cheatsheets_path}{mdfile}" + hyperlink = self.official_cheatsheet_url(mdfile) cs = self.cheatsheet(section=name, hyperlink=hyperlink, tags=[]) for cre in cres: cs.add_link( @@ -75,3 +103,54 @@ def register_cheatsheets( ) standard_entries.append(cs) return standard_entries + + def register_supplemental_cheatsheets(self, cache: db.Node_collection): + with self.supplement_data_file.open("r", encoding="utf-8") as handle: + supplement_entries = json.load(handle) + + standard_entries = [] + for entry in supplement_entries: + cs = self.cheatsheet( + section=entry["section"], + hyperlink=entry["hyperlink"], + tags=[], + ) + add_link_failures = False + for cre_id in entry.get("cre_ids", []): + cres = cache.get_CREs(external_id=cre_id) + for cre in cres: + try: + cs.add_link( + defs.Link( + document=cre.shallow_copy(), + ltype=defs.LinkTypes.AutomaticallyLinkedTo, + ) + ) + except Exception as exc: + self.logger.warning( + "Failed to add link for cre_id %s to cheatsheet %s: %s", + cre_id, + entry.get("section", ""), + exc, + ) + add_link_failures = True + if cs.links and not add_link_failures: + standard_entries.append(cs) + return standard_entries + + def deduplicate_entries(self, entries: List[defs.Standard]) -> List[defs.Standard]: + deduped = {} + for entry in entries: + key = (entry.section, entry.hyperlink) + if key in deduped: + # Merge duplicates: union links into existing entry + existing_entry = deduped[key] + existing_link_ids = {link.document.id for link in existing_entry.links} + for link in entry.links: + if link.document.id not in existing_link_ids: + existing_entry.add_link(link) + existing_link_ids.add(link.document.id) + else: + # First occurrence: store the entry + deduped[key] = entry + return list(deduped.values())