Skip to content

Commit 7f640f4

Browse files
committed
feat: normalize OWASP cheat sheet references
1 parent 4485936 commit 7f640f4

3 files changed

Lines changed: 176 additions & 11 deletions

File tree

application/tests/cheatsheets_parser_test.py

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,13 @@ class Repo:
3434
repo.working_dir = loc
3535
cre = defs.CRE(name="blah", id="223-780")
3636
self.collection.add_cre(cre)
37-
with open(os.path.join(os.path.join(loc, "cheatsheets"), "cs.md"), "w") as mdf:
37+
with open(
38+
os.path.join(
39+
os.path.join(loc, "cheatsheets"),
40+
"Secrets_Management_Cheat_Sheet.md",
41+
),
42+
"w",
43+
) as mdf:
3844
mdf.write(cs)
3945
mock_clone.return_value = repo
4046
entries = cheatsheets_parser.Cheatsheets().parse(
@@ -45,22 +51,55 @@ class Repo:
4551
# verify the external tagging convention, not just enum wiring.
4652
expected = defs.Standard(
4753
name="OWASP Cheat Sheets",
48-
hyperlink="https://github.com/foo/bar/tree/master/cs.md",
54+
hyperlink="https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html",
4955
section="Secrets Management Cheat Sheet",
50-
links=[defs.Link(document=cre, ltype=defs.LinkTypes.LinkedTo)],
56+
links=[defs.Link(document=cre, ltype=defs.LinkTypes.AutomaticallyLinkedTo)],
5157
tags=[
5258
"family:guidance",
5359
"subtype:cheatsheet",
54-
"source:owasp_cheatsheets",
5560
"audience:developer",
5661
"maturity:stable",
62+
"source:owasp_cheatsheets",
5763
],
5864
)
5965
self.maxDiff = None
6066
for name, nodes in entries.results.items():
6167
self.assertEqual(name, parser.name)
62-
self.assertEqual(len(nodes), 1)
63-
self.assertCountEqual(expected.todict(), nodes[0].todict())
68+
sections = {node.section for node in nodes}
69+
self.assertIn("Secrets Management Cheat Sheet", sections)
70+
secret_entry = next(
71+
(
72+
node
73+
for node in nodes
74+
if node.section == "Secrets Management Cheat Sheet"
75+
),
76+
None,
77+
)
78+
self.assertIsNotNone(secret_entry)
79+
self.assertEqual(expected.todict(), secret_entry.todict())
80+
81+
def test_register_supplemental_cheatsheets(self) -> None:
82+
for cre_id, name in [
83+
("118-110", "API/web services"),
84+
("724-770", "Technical application access control"),
85+
("623-550", "Denial Of Service protection"),
86+
]:
87+
self.collection.add_cre(defs.CRE(name=name, id=cre_id))
88+
89+
entries = cheatsheets_parser.Cheatsheets().register_supplemental_cheatsheets(
90+
cache=self.collection
91+
)
92+
rest = [
93+
entry for entry in entries if entry.section == "REST Security Cheat Sheet"
94+
][0]
95+
self.assertEqual(
96+
"https://cheatsheetseries.owasp.org/cheatsheets/REST_Security_Cheat_Sheet.html",
97+
rest.hyperlink,
98+
)
99+
self.assertEqual(
100+
["118-110", "724-770", "623-550"],
101+
[link.document.id for link in rest.links],
102+
)
64103

65104
cheatsheets_md = """ # Secrets Management Cheat Sheet
66105
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
[
2+
{
3+
"section": "Authorization Cheat Sheet",
4+
"hyperlink": "https://cheatsheetseries.owasp.org/cheatsheets/Authorization_Cheat_Sheet.html",
5+
"cre_ids": ["128-128", "117-371"]
6+
},
7+
{
8+
"section": "REST Security Cheat Sheet",
9+
"hyperlink": "https://cheatsheetseries.owasp.org/cheatsheets/REST_Security_Cheat_Sheet.html",
10+
"cre_ids": ["118-110", "724-770", "623-550"]
11+
},
12+
{
13+
"section": "Server Side Request Forgery Prevention Cheat Sheet",
14+
"hyperlink": "https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html",
15+
"cre_ids": ["028-728", "657-084"]
16+
},
17+
{
18+
"section": "Docker Security Cheat Sheet",
19+
"hyperlink": "https://cheatsheetseries.owasp.org/cheatsheets/Docker_Security_Cheat_Sheet.html",
20+
"cre_ids": ["233-748", "486-813"]
21+
},
22+
{
23+
"section": "Kubernetes Security Cheat Sheet",
24+
"hyperlink": "https://cheatsheetseries.owasp.org/cheatsheets/Kubernetes_Security_Cheat_Sheet.html",
25+
"cre_ids": ["467-784", "233-748", "486-813"]
26+
},
27+
{
28+
"section": "Secure Cloud Architecture Cheat Sheet",
29+
"hyperlink": "https://cheatsheetseries.owasp.org/cheatsheets/Secure_Cloud_Architecture_Cheat_Sheet.html",
30+
"cre_ids": ["155-155", "467-784"]
31+
},
32+
{
33+
"section": "LLM Prompt Injection Prevention Cheat Sheet",
34+
"hyperlink": "https://cheatsheetseries.owasp.org/cheatsheets/LLM_Prompt_Injection_Prevention_Cheat_Sheet.html",
35+
"cre_ids": ["161-451", "760-764"]
36+
},
37+
{
38+
"section": "AI Agent Security Cheat Sheet",
39+
"hyperlink": "https://cheatsheetseries.owasp.org/cheatsheets/AI_Agent_Security_Cheat_Sheet.html",
40+
"cre_ids": ["117-371", "650-560", "126-668"]
41+
},
42+
{
43+
"section": "Secure AI Model Ops Cheat Sheet",
44+
"hyperlink": "https://cheatsheetseries.owasp.org/cheatsheets/Secure_AI_Model_Ops_Cheat_Sheet.html",
45+
"cre_ids": ["148-853", "613-285", "613-287"]
46+
}
47+
]

application/utils/external_project_parsers/parsers/cheatsheets_parser.py

Lines changed: 84 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
import os
77
import re
88
from application.utils.external_project_parsers import base_parser_defs
9+
import json
10+
from pathlib import Path
11+
import logging
912
from application.utils.external_project_parsers.base_parser_defs import (
1013
ParserInterface,
1114
ParseResult,
@@ -15,6 +18,13 @@
1518

1619
class Cheatsheets(ParserInterface):
1720
name = "OWASP Cheat Sheets"
21+
cheatsheetseries_base_url = "https://cheatsheetseries.owasp.org/cheatsheets"
22+
supplement_data_file = (
23+
Path(__file__).resolve().parent.parent
24+
/ "data"
25+
/ "owasp_cheatsheets_supplement.json"
26+
)
27+
logger = logging.getLogger(__name__)
1828

1929
def cheatsheet(
2030
self, section: str, hyperlink: str, tags: List[str]
@@ -33,13 +43,31 @@ def cheatsheet(
3343
hyperlink=hyperlink,
3444
)
3545

46+
def official_cheatsheet_url(self, markdown_filename: str) -> str:
47+
html_name = os.path.splitext(markdown_filename)[0] + ".html"
48+
return f"{self.cheatsheetseries_base_url}/{html_name}"
49+
3650
def parse(self, cache: db.Node_collection, ph: prompt_client.PromptHandler):
3751
c_repo = "https://github.com/OWASP/CheatSheetSeries.git"
3852
cheatsheets_path = "cheatsheets/"
39-
repo = git.clone(c_repo, sparse_paths=["cheatsheets"], sparse_cone=True)
40-
cheatsheets = self.register_cheatsheets(
41-
repo=repo, cache=cache, cheatsheets_path=cheatsheets_path, repo_path=c_repo
42-
)
53+
cheatsheets = []
54+
repo = None
55+
try:
56+
repo = git.clone(c_repo, sparse_paths=["cheatsheets"], sparse_cone=True)
57+
except Exception as exc:
58+
self.logger.warning(
59+
"Unable to clone OWASP CheatSheetSeries, continuing with supplemental cheat sheets only: %s",
60+
exc,
61+
)
62+
if repo:
63+
cheatsheets = self.register_cheatsheets(
64+
repo=repo,
65+
cache=cache,
66+
cheatsheets_path=cheatsheets_path,
67+
repo_path=c_repo,
68+
)
69+
cheatsheets.extend(self.register_supplemental_cheatsheets(cache=cache))
70+
cheatsheets = self.deduplicate_entries(cheatsheets)
4371
results = {self.name: cheatsheets}
4472
base_parser_defs.validate_classification_tags(results)
4573
return ParseResult(results=results)
@@ -65,7 +93,7 @@ def register_cheatsheets(
6593
name = title.group("title")
6694
cre_id = cre.group("cre")
6795
cres = cache.get_CREs(external_id=cre_id)
68-
hyperlink = f"{repo_path.replace('.git','')}/tree/master/{cheatsheets_path}{mdfile}"
96+
hyperlink = self.official_cheatsheet_url(mdfile)
6997
cs = self.cheatsheet(section=name, hyperlink=hyperlink, tags=[])
7098
for cre in cres:
7199
cs.add_link(
@@ -75,3 +103,54 @@ def register_cheatsheets(
75103
)
76104
standard_entries.append(cs)
77105
return standard_entries
106+
107+
def register_supplemental_cheatsheets(self, cache: db.Node_collection):
108+
with self.supplement_data_file.open("r", encoding="utf-8") as handle:
109+
supplement_entries = json.load(handle)
110+
111+
standard_entries = []
112+
for entry in supplement_entries:
113+
cs = self.cheatsheet(
114+
section=entry["section"],
115+
hyperlink=entry["hyperlink"],
116+
tags=[],
117+
)
118+
add_link_failures = False
119+
for cre_id in entry.get("cre_ids", []):
120+
cres = cache.get_CREs(external_id=cre_id)
121+
for cre in cres:
122+
try:
123+
cs.add_link(
124+
defs.Link(
125+
document=cre.shallow_copy(),
126+
ltype=defs.LinkTypes.AutomaticallyLinkedTo,
127+
)
128+
)
129+
except Exception as exc:
130+
self.logger.warning(
131+
"Failed to add link for cre_id %s to cheatsheet %s: %s",
132+
cre_id,
133+
entry.get("section", "<unknown>"),
134+
exc,
135+
)
136+
add_link_failures = True
137+
if cs.links and not add_link_failures:
138+
standard_entries.append(cs)
139+
return standard_entries
140+
141+
def deduplicate_entries(self, entries: List[defs.Standard]) -> List[defs.Standard]:
142+
deduped = {}
143+
for entry in entries:
144+
key = (entry.section, entry.hyperlink)
145+
if key in deduped:
146+
# Merge duplicates: union links into existing entry
147+
existing_entry = deduped[key]
148+
existing_link_ids = {link.document.id for link in existing_entry.links}
149+
for link in entry.links:
150+
if link.document.id not in existing_link_ids:
151+
existing_entry.add_link(link)
152+
existing_link_ids.add(link.document.id)
153+
else:
154+
# First occurrence: store the entry
155+
deduped[key] = entry
156+
return list(deduped.values())

0 commit comments

Comments
 (0)