Skip to content

Commit 820ff5c

Browse files
committed
Add direct-only OpenCRE map analysis
1 parent 2939941 commit 820ff5c

2 files changed

Lines changed: 161 additions & 100 deletions

File tree

application/tests/web_main_test.py

Lines changed: 108 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -736,28 +736,53 @@ def test_gap_analysis_supports_opencre_as_standard(
736736

737737
@patch.object(web_main.gap_analysis, "schedule")
738738
@patch.object(db, "Node_collection")
739-
def test_gap_analysis_preserves_multiple_opencre_overlaps(
739+
def test_gap_analysis_returns_only_direct_opencre_mappings(
740740
self, db_mock, schedule_mock
741741
) -> None:
742742
compare = defs.Standard(
743743
name="CWE",
744744
sectionID="1004",
745745
section="Sensitive Cookie Without 'HttpOnly' Flag",
746746
)
747-
opencre_documents = []
748-
internal_ids = []
749-
750-
for i in range(8):
747+
direct_cre = defs.CRE(
748+
id="804-220",
749+
name="Set httponly attribute for cookie-based session tokens",
750+
description="",
751+
)
752+
direct_cre.add_link(
753+
defs.Link(ltype=defs.LinkTypes.LinkedTo, document=compare.shallow_copy())
754+
)
755+
auto_linked_cres = []
756+
for i, cre_id in enumerate(
757+
[
758+
"117-371",
759+
"166-151",
760+
"284-521",
761+
"368-633",
762+
"612-252",
763+
"664-080",
764+
"801-310",
765+
],
766+
start=1,
767+
):
751768
cre = defs.CRE(
752-
id=f"170-77{i}",
753-
name=f"Cryptography {i}",
769+
id=cre_id,
770+
name=f"Automatically mapped CRE {i}",
754771
description="",
755772
)
756-
compare.add_link(
757-
defs.Link(ltype=defs.LinkTypes.LinkedTo, document=cre.shallow_copy())
773+
cre.add_link(
774+
defs.Link(
775+
ltype=defs.LinkTypes.AutomaticallyLinkedTo,
776+
document=compare.shallow_copy(),
777+
)
758778
)
759-
opencre_documents.append(cre)
760-
internal_ids.append(SimpleNamespace(id=f"cre-internal-{i}"))
779+
auto_linked_cres.append(cre)
780+
781+
opencre_documents = [direct_cre] + auto_linked_cres
782+
internal_ids = [
783+
SimpleNamespace(id=f"cre-internal-{i}")
784+
for i in range(len(opencre_documents))
785+
]
761786

762787
db_mock.return_value.get_gap_analysis_result.return_value = None
763788
db_mock.return_value.gap_analysis_exists.return_value = False
@@ -782,17 +807,79 @@ def test_gap_analysis_preserves_multiple_opencre_overlaps(
782807
payload = json.loads(response.data)
783808
self.assertEqual(200, response.status_code)
784809
self.assertIn("result", payload)
785-
self.assertIn(compare.id, payload["result"])
786-
self.assertEqual(8, len(payload["result"][compare.id]["paths"]))
787-
self.assertEqual(
788-
8,
789-
len(
790-
{
791-
path["end"]["id"]
792-
for path in payload["result"][compare.id]["paths"].values()
793-
}
794-
),
810+
self.assertEqual([compare.id], list(payload["result"].keys()))
811+
self.assertEqual(1, len(payload["result"][compare.id]["paths"]))
812+
path = next(iter(payload["result"][compare.id]["paths"].values()))
813+
self.assertEqual(direct_cre.id, path["end"]["id"])
814+
self.assertEqual(direct_cre.name, path["end"]["name"])
815+
self.assertEqual(compare.id, path["path"][0]["start"]["id"])
816+
self.assertEqual(direct_cre.id, path["path"][0]["end"]["id"])
817+
schedule_mock.assert_not_called()
818+
819+
@patch.object(web_main.gap_analysis, "schedule")
820+
@patch.object(db, "Node_collection")
821+
def test_gap_analysis_returns_only_direct_opencre_mappings_when_opencre_is_left(
822+
self, db_mock, schedule_mock
823+
) -> None:
824+
compare = defs.Standard(
825+
name="CWE",
826+
sectionID="1004",
827+
section="Sensitive Cookie Without 'HttpOnly' Flag",
828+
)
829+
direct_cre = defs.CRE(
830+
id="804-220",
831+
name="Set httponly attribute for cookie-based session tokens",
832+
description="",
833+
)
834+
direct_cre.add_link(
835+
defs.Link(ltype=defs.LinkTypes.LinkedTo, document=compare.shallow_copy())
836+
)
837+
indirect_cre = defs.CRE(
838+
id="117-371",
839+
name="Use a centralized access control mechanism",
840+
description="",
841+
)
842+
indirect_cre.add_link(
843+
defs.Link(
844+
ltype=defs.LinkTypes.AutomaticallyLinkedTo,
845+
document=compare.shallow_copy(),
846+
)
795847
)
848+
849+
opencre_documents = [direct_cre, indirect_cre]
850+
internal_ids = [
851+
SimpleNamespace(id=f"cre-internal-{i}")
852+
for i in range(len(opencre_documents))
853+
]
854+
855+
db_mock.return_value.get_gap_analysis_result.return_value = None
856+
db_mock.return_value.gap_analysis_exists.return_value = False
857+
db_mock.return_value.get_nodes.side_effect = lambda name=None, **kwargs: (
858+
[compare] if name == "CWE" else []
859+
)
860+
db_mock.return_value.session.query.return_value.all.return_value = internal_ids
861+
db_mock.return_value.get_CREs.side_effect = lambda internal_id=None, **kwargs: [
862+
next(
863+
cre
864+
for index, cre in enumerate(opencre_documents)
865+
if internal_id == f"cre-internal-{index}"
866+
)
867+
]
868+
869+
with self.app.test_client() as client:
870+
response = client.get(
871+
"/rest/v1/map_analysis?standard=OpenCRE&standard=CWE",
872+
headers={"Content-Type": "application/json"},
873+
)
874+
875+
payload = json.loads(response.data)
876+
self.assertEqual(200, response.status_code)
877+
self.assertEqual([direct_cre.id], list(payload["result"].keys()))
878+
self.assertEqual(1, len(payload["result"][direct_cre.id]["paths"]))
879+
path = next(iter(payload["result"][direct_cre.id]["paths"].values()))
880+
self.assertEqual(compare.id, path["end"]["id"])
881+
self.assertEqual(direct_cre.id, path["path"][0]["start"]["id"])
882+
self.assertEqual(compare.id, path["path"][0]["end"]["id"])
796883
schedule_mock.assert_not_called()
797884

798885
def test_gap_analysis_weak_links_no_cache(self) -> None:

application/web/web_main.py

Lines changed: 53 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -310,71 +310,44 @@ def _get_map_analysis_documents(
310310
return collection.get_nodes(name=standard)
311311

312312

313-
def _get_document_cre_ids(document: defs.Document) -> list[str]:
314-
if document.doctype == defs.Credoctypes.CRE:
315-
return [document.id]
316-
return [
317-
link.document.id
318-
for link in document.links
319-
if link.document.doctype == defs.Credoctypes.CRE
320-
]
321-
322-
323-
def _build_direct_overlap_path(
324-
base_document: defs.Document, cre_id: str, compare_document: defs.Document
325-
) -> dict[str, Any] | None:
326-
if base_document.doctype == defs.Credoctypes.CRE:
327-
if compare_document.doctype == defs.Credoctypes.CRE:
328-
return None
329-
return {
330-
"end": compare_document.shallow_copy(),
331-
"path": [
332-
{
333-
"start": base_document.shallow_copy(),
334-
"end": compare_document.shallow_copy(),
335-
"relationship": "LINKED_TO",
336-
"score": 0,
337-
}
338-
],
339-
"score": 0,
340-
}
341-
342-
if compare_document.doctype == defs.Credoctypes.CRE:
343-
return {
344-
"end": compare_document.shallow_copy(),
345-
"path": [
346-
{
347-
"start": base_document.shallow_copy(),
348-
"end": compare_document.shallow_copy(),
349-
"relationship": "LINKED_TO",
350-
"score": 0,
351-
}
352-
],
353-
"score": 0,
354-
}
355-
313+
def _build_direct_link_path(
314+
start_document: defs.Document, end_document: defs.Document
315+
) -> dict[str, Any]:
356316
return {
357-
"end": compare_document.shallow_copy(),
317+
"end": end_document.shallow_copy(),
358318
"path": [
359319
{
360-
"start": base_document.shallow_copy(),
361-
"end": defs.CRE(id=cre_id).shallow_copy(),
320+
"start": start_document.shallow_copy(),
321+
"end": end_document.shallow_copy(),
362322
"relationship": "LINKED_TO",
363323
"score": 0,
364-
},
365-
{
366-
"start": defs.CRE(id=cre_id).shallow_copy(),
367-
"end": compare_document.shallow_copy(),
368-
"relationship": "LINKED_TO",
369-
"score": 0,
370-
},
324+
}
371325
],
372326
"score": 0,
373327
}
374328

375329

376-
def _make_direct_overlap_path_key(compare_document: defs.Document, cre_id: str) -> str:
377-
return f"{compare_document.id}::{cre_id}"
330+
def _make_direct_link_path_key(end_document: defs.Document) -> str:
331+
return end_document.id
332+
333+
334+
def _add_direct_link_result(
335+
grouped_paths: dict[str, dict[str, Any]],
336+
start_document: defs.Document,
337+
end_document: defs.Document,
338+
) -> None:
339+
shared_paths = grouped_paths.setdefault(
340+
start_document.id,
341+
{
342+
"start": start_document.shallow_copy(),
343+
"paths": {},
344+
"extra": 0,
345+
},
346+
)["paths"]
347+
shared_paths.setdefault(
348+
_make_direct_link_path_key(end_document),
349+
_build_direct_link_path(start_document, end_document),
350+
)
378351

379352

380353
def _build_direct_cre_overlap_map_analysis(
@@ -385,34 +358,36 @@ def _build_direct_cre_overlap_map_analysis(
385358
if len(standards) < 2:
386359
return None
387360

388-
base_nodes = _get_map_analysis_documents(standards[0], collection)
389-
compare_nodes = _get_map_analysis_documents(standards[1], collection)
361+
base_standard = standards[0]
362+
compare_standard = standards[1]
363+
base_nodes = _get_map_analysis_documents(base_standard, collection)
364+
compare_nodes = _get_map_analysis_documents(compare_standard, collection)
390365
if not base_nodes or not compare_nodes:
391366
return None
392367

393-
compare_nodes_by_cre: dict[str, list[defs.Document]] = {}
394-
for compare_node in compare_nodes:
395-
for cre_id in _get_document_cre_ids(compare_node):
396-
compare_nodes_by_cre.setdefault(cre_id, []).append(compare_node)
368+
base_is_opencre = base_standard == OPENCRE_STANDARD_NAME
369+
opencre_nodes = base_nodes if base_is_opencre else compare_nodes
370+
standard_nodes = compare_nodes if base_is_opencre else base_nodes
371+
372+
standard_nodes_by_id = {
373+
standard_node.id: standard_node for standard_node in standard_nodes
374+
}
375+
direct_pairs: list[tuple[defs.CRE, defs.Document]] = []
376+
for opencre_node in opencre_nodes:
377+
for link in opencre_node.links:
378+
if link.ltype != defs.LinkTypes.LinkedTo:
379+
continue
380+
standard_node = standard_nodes_by_id.get(link.document.id)
381+
if not standard_node:
382+
continue
383+
direct_pairs.append((opencre_node, standard_node))
397384

398385
grouped_paths: dict[str, dict[str, Any]] = {}
399-
for base_node in base_nodes:
400-
shared_paths: dict[str, Any] = {}
401-
for cre_id in _get_document_cre_ids(base_node):
402-
for compare_node in compare_nodes_by_cre.get(cre_id, []):
403-
path = _build_direct_overlap_path(base_node, cre_id, compare_node)
404-
if not path:
405-
continue
406-
shared_paths.setdefault(
407-
_make_direct_overlap_path_key(compare_node, cre_id), path
408-
)
409-
410-
if shared_paths:
411-
grouped_paths[base_node.id] = {
412-
"start": base_node.shallow_copy(),
413-
"paths": shared_paths,
414-
"extra": 0,
415-
}
386+
for opencre_node, standard_node in direct_pairs:
387+
if base_is_opencre:
388+
_add_direct_link_result(grouped_paths, opencre_node, standard_node)
389+
else:
390+
_add_direct_link_result(grouped_paths, standard_node, opencre_node)
416391

417392
if not grouped_paths:
418393
return None
@@ -431,7 +406,6 @@ def map_analysis() -> Any:
431406
posthog.capture(f"map_analysis", f"standards:{standards}")
432407

433408
database = db.Node_collection()
434-
standards = request.args.getlist("standard")
435409
standards_hash = gap_analysis.make_resources_key(standards)
436410

437411
if OPENCRE_STANDARD_NAME in standards:

0 commit comments

Comments
 (0)