Skip to content

Commit d796ff5

Browse files
Add curated CWE fallback mappings and coverage for issue #472 (#823)
* Add curated CWE fallback mappings * Cover CWE fallback and inheritance behavior with tests * Add local CWE refresh tooling * Add local helper scripts for issue #472 * Integrate OpenCRE map analysis support from issue #469 * Implement fallback for gap analysis in database with error handling * Update scripts/show-db-stats.sh Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Signed-off-by: Bornunique911 <69379200+Bornunique911@users.noreply.github.com> * fix: remove leading space in 'xss' keyword for CWE mapping * fix: update condition for related CWE entries to check for 'ChildOf' nature * fix: correct syntax for accessing related CWE entry attributes * fix: enhance gap analysis error handling for Heroku and fallback scenarios --------- Signed-off-by: Bornunique911 <69379200+Bornunique911@users.noreply.github.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
1 parent b637225 commit d796ff5

8 files changed

Lines changed: 637 additions & 39 deletions

File tree

application/tests/cwe_parser_test.py

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,157 @@ def iter_content(self, chunk_size=None):
116116
self.assertCountEqual(nodes[0].todict(), expected[0].todict())
117117
self.assertCountEqual(nodes[1].todict(), expected[1].todict())
118118

119+
@patch.object(requests, "get")
120+
def test_register_CWE_inherits_mappings_transitively(self, mock_requests) -> None:
121+
tmpdir = mkdtemp()
122+
tmpFile = os.path.join(tmpdir, "cwe.xml")
123+
tmpzip = os.path.join(tmpdir, "cwe.zip")
124+
with open(tmpFile, "w") as cx:
125+
cx.write(self.CWE_transitive_xml)
126+
with zipfile.ZipFile(tmpzip, "w", zipfile.ZIP_DEFLATED) as zipf:
127+
zipf.write(tmpFile, arcname="cwe.xml")
128+
129+
class fakeRequest:
130+
def iter_content(self, chunk_size=None):
131+
with open(tmpzip, "rb") as zipf:
132+
return [zipf.read()]
133+
134+
mock_requests.return_value = fakeRequest()
135+
136+
cre = defs.CRE(id="089-089", name="CRE-Injection")
137+
dbcre = self.collection.add_cre(cre=cre)
138+
dbcwe = self.collection.add_node(defs.Standard(name="CWE", sectionID="89"))
139+
self.collection.add_link(dbcre, dbcwe, defs.LinkTypes.LinkedTo)
140+
141+
entries = cwe.CWE().parse(
142+
cache=self.collection,
143+
ph=prompt_client.PromptHandler(database=self.collection),
144+
)
145+
imported_cwes = {node.sectionID: node for node in entries.results["CWE"]}
146+
147+
self.assertEqual(imported_cwes["2001"].links[0].document.todict(), cre.todict())
148+
self.assertEqual(imported_cwes["2002"].links[0].document.todict(), cre.todict())
149+
150+
@patch.object(requests, "get")
151+
def test_register_CWE_applies_fallback_family_mappings(self, mock_requests) -> None:
152+
tmpdir = mkdtemp()
153+
tmpFile = os.path.join(tmpdir, "cwe.xml")
154+
tmpzip = os.path.join(tmpdir, "cwe.zip")
155+
with open(tmpFile, "w") as cx:
156+
cx.write(self.CWE_fallback_xml)
157+
with zipfile.ZipFile(tmpzip, "w", zipfile.ZIP_DEFLATED) as zipf:
158+
zipf.write(tmpFile, arcname="cwe.xml")
159+
160+
class fakeRequest:
161+
def iter_content(self, chunk_size=None):
162+
with open(tmpzip, "rb") as zipf:
163+
return [zipf.read()]
164+
165+
mock_requests.return_value = fakeRequest()
166+
167+
injection_cre = defs.CRE(id="760-764", name="Injection protection")
168+
xss_cre = defs.CRE(id="760-765", name="XSS protection")
169+
xxe_cre = defs.CRE(id="764-507", name="Restrict XML parsing (against XXE)")
170+
auth_cre = defs.CRE(
171+
id="117-371", name="Use a centralized access control mechanism"
172+
)
173+
authn_cre = defs.CRE(
174+
id="113-133", name="Use centralized authentication mechanism"
175+
)
176+
csrf_cre = defs.CRE(id="028-727", name="CSRF protection")
177+
ssrf_cre = defs.CRE(id="028-728", name="SSRF protection")
178+
hardcoded_secret_cre = defs.CRE(
179+
id="774-888", name="Do not store secrets in the code"
180+
)
181+
password_storage_cre = defs.CRE(
182+
id="622-203", name="Store passwords salted and hashed"
183+
)
184+
credential_storage_cre = defs.CRE(
185+
id="881-321", name="Store credentials securely"
186+
)
187+
session_management_cre = defs.CRE(id="177-260", name="Session management")
188+
secure_cookie_cre = defs.CRE(
189+
id="688-081", name='Set "secure" attribute for cookie-based session tokens'
190+
)
191+
deserialization_cre = defs.CRE(id="836-068", name="Deserialization Prevention")
192+
self.collection.add_cre(cre=injection_cre)
193+
self.collection.add_cre(cre=xss_cre)
194+
self.collection.add_cre(cre=xxe_cre)
195+
self.collection.add_cre(cre=auth_cre)
196+
self.collection.add_cre(cre=authn_cre)
197+
self.collection.add_cre(cre=csrf_cre)
198+
self.collection.add_cre(cre=ssrf_cre)
199+
self.collection.add_cre(cre=hardcoded_secret_cre)
200+
self.collection.add_cre(cre=password_storage_cre)
201+
self.collection.add_cre(cre=credential_storage_cre)
202+
self.collection.add_cre(cre=session_management_cre)
203+
self.collection.add_cre(cre=secure_cookie_cre)
204+
self.collection.add_cre(cre=deserialization_cre)
205+
206+
entries = cwe.CWE().parse(
207+
cache=self.collection,
208+
ph=prompt_client.PromptHandler(database=self.collection),
209+
)
210+
imported_cwes = {node.sectionID: node for node in entries.results["CWE"]}
211+
212+
self.assertEqual(
213+
imported_cwes["89"].links[0].document.todict(), injection_cre.todict()
214+
)
215+
self.assertEqual(
216+
imported_cwes["79"].links[0].document.todict(), xss_cre.todict()
217+
)
218+
self.assertEqual(
219+
imported_cwes["611"].links[0].document.todict(), xxe_cre.todict()
220+
)
221+
self.assertEqual(
222+
imported_cwes["612"].links[0].document.todict(), auth_cre.todict()
223+
)
224+
self.assertEqual(
225+
imported_cwes["287"].links[0].document.todict(), authn_cre.todict()
226+
)
227+
self.assertEqual(
228+
imported_cwes["352"].links[0].document.todict(), csrf_cre.todict()
229+
)
230+
self.assertEqual(
231+
imported_cwes["918"].links[0].document.todict(), ssrf_cre.todict()
232+
)
233+
self.assertEqual(
234+
imported_cwes["798"].links[0].document.todict(),
235+
hardcoded_secret_cre.todict(),
236+
)
237+
self.assertEqual(
238+
imported_cwes["321"].links[0].document.todict(),
239+
hardcoded_secret_cre.todict(),
240+
)
241+
self.assertEqual(
242+
imported_cwes["256"].links[0].document.todict(),
243+
password_storage_cre.todict(),
244+
)
245+
self.assertEqual(
246+
imported_cwes["257"].links[0].document.todict(),
247+
password_storage_cre.todict(),
248+
)
249+
self.assertEqual(
250+
imported_cwes["258"].links[0].document.todict(),
251+
credential_storage_cre.todict(),
252+
)
253+
self.assertEqual(
254+
imported_cwes["260"].links[0].document.todict(),
255+
credential_storage_cre.todict(),
256+
)
257+
self.assertEqual(
258+
imported_cwes["384"].links[0].document.todict(),
259+
session_management_cre.todict(),
260+
)
261+
self.assertEqual(
262+
imported_cwes["614"].links[0].document.todict(),
263+
secure_cookie_cre.todict(),
264+
)
265+
self.assertEqual(
266+
imported_cwes["502"].links[0].document.todict(),
267+
deserialization_cre.todict(),
268+
)
269+
119270
CWE_xml = """<?xml version="1.0" encoding="UTF-8"?>
120271
<Weakness_Catalog Name="CWE" Version="4.10" Date="2023-01-31" xmlns="http://cwe.mitre.org/cwe-6"
121272
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
@@ -514,3 +665,78 @@ def iter_content(self, chunk_size=None):
514665
</Weakness>
515666
</Weaknesses>
516667
</Weakness_Catalog>"""
668+
669+
CWE_transitive_xml = """<?xml version="1.0" encoding="UTF-8"?>
670+
<Weakness_Catalog Name="CWE" Version="4.10" Date="2023-01-31" xmlns="http://cwe.mitre.org/cwe-6">
671+
<Weaknesses>
672+
<Weakness ID="2001" Name="Injection Child" Abstraction="Variant" Structure="Simple" Status="Draft">
673+
<Related_Weaknesses>
674+
<Related_Weakness Nature="ChildOf" CWE_ID="89" View_ID="1000" Ordinal="Primary" />
675+
</Related_Weaknesses>
676+
</Weakness>
677+
<Weakness ID="2002" Name="Injection Grandchild" Abstraction="Variant" Structure="Simple" Status="Draft">
678+
<Related_Weaknesses>
679+
<Related_Weakness Nature="ChildOf" CWE_ID="2001" View_ID="1000" Ordinal="Primary" />
680+
</Related_Weaknesses>
681+
</Weakness>
682+
<Weakness ID="2003" Name="Standalone" Abstraction="Variant" Structure="Simple" Status="Draft">
683+
<Description>Padding entry so xmltodict returns a list of Weakness elements.</Description>
684+
</Weakness>
685+
</Weaknesses>
686+
</Weakness_Catalog>
687+
"""
688+
689+
CWE_fallback_xml = """<?xml version="1.0" encoding="UTF-8"?>
690+
<Weakness_Catalog Name="CWE" Version="4.10" Date="2023-01-31" xmlns="http://cwe.mitre.org/cwe-6">
691+
<Weaknesses>
692+
<Weakness ID="79" Name="Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')" Abstraction="Base" Structure="Simple" Status="Draft">
693+
<Description>XSS entry.</Description>
694+
</Weakness>
695+
<Weakness ID="89" Name="Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')" Abstraction="Base" Structure="Simple" Status="Draft">
696+
<Description>SQL injection entry.</Description>
697+
</Weakness>
698+
<Weakness ID="611" Name="Improper Restriction of XML External Entity Reference" Abstraction="Base" Structure="Simple" Status="Draft">
699+
<Description>XXE entry.</Description>
700+
</Weakness>
701+
<Weakness ID="612" Name="Improper Authorization of Index Containing Sensitive Information" Abstraction="Base" Structure="Simple" Status="Draft">
702+
<Description>Authorization entry.</Description>
703+
</Weakness>
704+
<Weakness ID="287" Name="Improper Authentication" Abstraction="Base" Structure="Simple" Status="Draft">
705+
<Description>Authentication entry.</Description>
706+
</Weakness>
707+
<Weakness ID="352" Name="Cross-Site Request Forgery (CSRF)" Abstraction="Base" Structure="Simple" Status="Draft">
708+
<Description>CSRF entry.</Description>
709+
</Weakness>
710+
<Weakness ID="798" Name="Use of Hard-coded Credentials" Abstraction="Base" Structure="Simple" Status="Draft">
711+
<Description>Hard-coded credentials entry.</Description>
712+
</Weakness>
713+
<Weakness ID="321" Name="Use of Hard-coded Cryptographic Key" Abstraction="Base" Structure="Simple" Status="Draft">
714+
<Description>Hard-coded key entry.</Description>
715+
</Weakness>
716+
<Weakness ID="256" Name="Plaintext Storage of a Password" Abstraction="Base" Structure="Simple" Status="Draft">
717+
<Description>Password storage entry.</Description>
718+
</Weakness>
719+
<Weakness ID="257" Name="Storing Passwords in a Recoverable Format" Abstraction="Base" Structure="Simple" Status="Draft">
720+
<Description>Recoverable password entry.</Description>
721+
</Weakness>
722+
<Weakness ID="258" Name="Empty Password in Configuration File" Abstraction="Base" Structure="Simple" Status="Draft">
723+
<Description>Password in config entry.</Description>
724+
</Weakness>
725+
<Weakness ID="260" Name="Password in Configuration File" Abstraction="Base" Structure="Simple" Status="Draft">
726+
<Description>Password in config entry.</Description>
727+
</Weakness>
728+
<Weakness ID="384" Name="Session Fixation" Abstraction="Base" Structure="Simple" Status="Draft">
729+
<Description>Session fixation entry.</Description>
730+
</Weakness>
731+
<Weakness ID="614" Name="Sensitive Cookie in HTTPS Session Without 'Secure' Attribute" Abstraction="Base" Structure="Simple" Status="Draft">
732+
<Description>Cookie secure attribute entry.</Description>
733+
</Weakness>
734+
<Weakness ID="502" Name="Deserialization of Untrusted Data" Abstraction="Base" Structure="Simple" Status="Draft">
735+
<Description>Deserialization entry.</Description>
736+
</Weakness>
737+
<Weakness ID="918" Name="Server-Side Request Forgery (SSRF)" Abstraction="Base" Structure="Simple" Status="Draft">
738+
<Description>SSRF entry.</Description>
739+
</Weakness>
740+
</Weaknesses>
741+
</Weakness_Catalog>
742+
"""

application/tests/web_main_test.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -734,13 +734,32 @@ def test_gap_analysis_fallback_without_redis(
734734
self.assertEqual({"result": {"k": {}}}, json.loads(response.data))
735735
db_gap_analysis_mock.assert_called_once()
736736

737-
@patch.dict(os.environ, {"HEROKU": "True"}, clear=False)
738737
@patch.object(db, "Node_collection")
738+
@patch.object(db, "gap_analysis")
739739
@patch.object(redis, "from_url")
740-
def test_gap_analysis_heroku_cache_miss_returns_404(
740+
def test_gap_analysis_fallback_backend_failure_returns_503(
741+
self, redis_conn_mock, db_gap_analysis_mock, db_mock
742+
) -> None:
743+
redis_conn_mock.side_effect = RuntimeError("redis down")
744+
db_gap_analysis_mock.side_effect = RuntimeError("neo unavailable")
745+
db_mock.return_value.get_gap_analysis_result.return_value = None
746+
with self.app.test_client() as client:
747+
response = client.get(
748+
"/rest/v1/map_analysis?standard=aaa&standard=bbb",
749+
headers={"Content-Type": "application/json"},
750+
)
751+
self.assertEqual(503, response.status_code)
752+
db_gap_analysis_mock.assert_called_once()
753+
754+
@patch.dict(os.environ, {"DYNO": "web.1"}, clear=False)
755+
@patch.object(db, "Node_collection")
756+
@patch.object(redis, "from_url")
757+
def test_gap_analysis_dyno_missing_standard_returns_404(
741758
self, redis_conn_mock, db_mock
742759
) -> None:
743760
db_mock.return_value.get_gap_analysis_result.return_value = None
761+
db_mock.return_value.gap_analysis_exists.return_value = False
762+
db_mock.return_value.standards.return_value = ["aaa"]
744763
with self.app.test_client() as client:
745764
response = client.get(
746765
"/rest/v1/map_analysis?standard=aaa&standard=bbb",
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
[
2+
{
3+
"keywords": [
4+
"xml external entity",
5+
"xxe"
6+
],
7+
"cre_id": "764-507"
8+
},
9+
{
10+
"keywords": [
11+
"cross-site scripting",
12+
"xss",
13+
"(xss)"
14+
],
15+
"cre_id": "760-765"
16+
},
17+
{
18+
"keywords": [
19+
"authorization",
20+
"access control"
21+
],
22+
"cre_id": "117-371"
23+
},
24+
{
25+
"keywords": [
26+
"improper authentication",
27+
"missing authentication",
28+
"authentication bypass"
29+
],
30+
"cre_id": "113-133"
31+
},
32+
{
33+
"keywords": [
34+
"cross-site request forgery",
35+
"(csrf)",
36+
"csrf"
37+
],
38+
"cre_id": "028-727"
39+
},
40+
{
41+
"keywords": [
42+
"server-side request forgery",
43+
"(ssrf)",
44+
"ssrf"
45+
],
46+
"cre_id": "028-728"
47+
},
48+
{
49+
"keywords": [
50+
"plaintext storage of a password",
51+
"storing passwords in a recoverable format"
52+
],
53+
"cre_id": "622-203"
54+
},
55+
{
56+
"keywords": [
57+
"empty password in configuration file",
58+
"password in configuration file"
59+
],
60+
"cre_id": "881-321"
61+
},
62+
{
63+
"keywords": [
64+
"hard-coded password",
65+
"hardcoded password",
66+
"hard-coded credentials",
67+
"hardcoded credentials",
68+
"hard-coded credential",
69+
"hardcoded credential",
70+
"hard-coded cryptographic key",
71+
"hardcoded cryptographic key",
72+
"hard-coded key",
73+
"hardcoded key"
74+
],
75+
"cre_id": "774-888"
76+
},
77+
{
78+
"keywords": [
79+
"session fixation"
80+
],
81+
"cre_id": "177-260"
82+
},
83+
{
84+
"keywords": [
85+
"sensitive cookie in https session without 'secure' attribute"
86+
],
87+
"cre_id": "688-081"
88+
},
89+
{
90+
"keywords": [
91+
"deserialization of untrusted data"
92+
],
93+
"cre_id": "836-068"
94+
},
95+
{
96+
"keywords": [
97+
"injection",
98+
"query logic"
99+
],
100+
"cre_id": "760-764"
101+
}
102+
]

0 commit comments

Comments
 (0)