diff --git a/application/frontend/src/pages/GapAnalysis/GapAnalysis.tsx b/application/frontend/src/pages/GapAnalysis/GapAnalysis.tsx
index 1bc72a4e4..e9480a09c 100644
--- a/application/frontend/src/pages/GapAnalysis/GapAnalysis.tsx
+++ b/application/frontend/src/pages/GapAnalysis/GapAnalysis.tsx
@@ -481,8 +481,8 @@ export const GapAnalysis = () => {
Rank
- OWASP Top 10 2021
- OWASP Top 10 2025
+ {owaspComparison.standards[0]}
+ {owaspComparison.standards[1]}
Changed
@@ -493,21 +493,21 @@ export const GapAnalysis = () => {
{item.rank}
- {item.top10_2021?.hyperlink ? (
-
- {item.top10_2021.section}
+ {item.left?.hyperlink ? (
+
+ {item.left.section}
) : (
- item.top10_2021?.section ?? Not mapped
+ item.left?.section ?? Not mapped
)}
- {item.top10_2025?.hyperlink ? (
-
- {item.top10_2025.section}
+ {item.right?.hyperlink ? (
+
+ {item.right.section}
) : (
- item.top10_2025?.section ?? Not mapped
+ item.right?.section ?? Not mapped
)}
{item.changed ? 'Yes' : 'No'}
diff --git a/application/frontend/src/types.ts b/application/frontend/src/types.ts
index ec7ece9f7..a509d95a8 100644
--- a/application/frontend/src/types.ts
+++ b/application/frontend/src/types.ts
@@ -48,8 +48,8 @@ export interface OwaspTop10ComparisonItemEntry {
export interface OwaspTop10ComparisonItem {
rank: string;
changed: boolean;
- top10_2021?: OwaspTop10ComparisonItemEntry;
- top10_2025?: OwaspTop10ComparisonItemEntry;
+ left?: OwaspTop10ComparisonItemEntry;
+ right?: OwaspTop10ComparisonItemEntry;
}
export interface OwaspTop10Comparison {
diff --git a/application/tests/owasp_kubernetes_top10_2025_parser_test.py b/application/tests/owasp_kubernetes_top10_2025_parser_test.py
index 6f444c9a9..b453975a9 100644
--- a/application/tests/owasp_kubernetes_top10_2025_parser_test.py
+++ b/application/tests/owasp_kubernetes_top10_2025_parser_test.py
@@ -42,10 +42,18 @@ def test_parse(self) -> None:
self.assertEqual(10, len(entries))
self.assertEqual("K01", entries[0].sectionID)
self.assertEqual("Insecure Workload Configurations", entries[0].section)
+ self.assertEqual(
+ "https://owasp.org/www-project-kubernetes-top-ten/2025/en/src/K01-Insecure-Workload-Configurations.html",
+ entries[0].hyperlink,
+ )
self.assertEqual(
["233-748", "486-813"], [l.document.id for l in entries[0].links]
)
self.assertEqual("K10", entries[-1].sectionID)
+ self.assertEqual(
+ "https://owasp.org/www-project-kubernetes-top-ten/2025/en/src/K10-Inadequate-Logging-And-Monitoring.html",
+ entries[-1].hyperlink,
+ )
self.assertEqual(
["148-420", "402-706", "843-841"],
[l.document.id for l in entries[-1].links],
diff --git a/application/tests/web_main_test.py b/application/tests/web_main_test.py
index 763e32ae9..00ff3c747 100644
--- a/application/tests/web_main_test.py
+++ b/application/tests/web_main_test.py
@@ -772,11 +772,11 @@ def get_nodes_side_effect(name=None, **kwargs):
)
self.assertEqual(
"Broken Access Controls",
- payload["owasp_top10_comparison"]["items"][0]["top10_2021"]["section"],
+ payload["owasp_top10_comparison"]["items"][0]["left"]["section"],
)
self.assertEqual(
"Broken Access Control",
- payload["owasp_top10_comparison"]["items"][0]["top10_2025"]["section"],
+ payload["owasp_top10_comparison"]["items"][0]["right"]["section"],
)
@patch.object(web_main.gap_analysis, "schedule")
@@ -824,7 +824,7 @@ def get_nodes_side_effect(name=None, **kwargs):
self.assertIn("owasp_top10_comparison", payload)
self.assertEqual(
"Broken Access Controls",
- payload["owasp_top10_comparison"]["items"][0]["top10_2021"]["section"],
+ payload["owasp_top10_comparison"]["items"][0]["left"]["section"],
)
@patch.object(web_main.cre_main, "fetch_upstream_json")
@@ -1078,6 +1078,157 @@ def test_gap_analysis_adds_specialized_section_for_api_cheatsheets(
)
self.assertIn(base.id, payload["specialized_cheatsheet_section"]["result"])
+ @patch.object(web_main.cre_main, "fetch_upstream_json")
+ @patch.object(web_main.gap_analysis, "schedule")
+ @patch.object(db, "Node_collection")
+ def test_gap_analysis_adds_specialized_section_for_kubernetes_2022_cheatsheets(
+ self, db_mock, schedule_mock, upstream_fetch_mock
+ ) -> None:
+ network_cre = defs.CRE(
+ id="132-146", name="Network segmentation", description=""
+ )
+ base = defs.Standard(
+ name="OWASP Kubernetes Top Ten 2022",
+ sectionID="K07",
+ section="Missing Network Segmentation Controls",
+ )
+ base.add_link(
+ defs.Link(ltype=defs.LinkTypes.LinkedTo, document=network_cre.shallow_copy())
+ )
+
+ compare = defs.Standard(
+ name="OWASP Cheat Sheets",
+ section="Kubernetes Security Cheat Sheet",
+ )
+ compare.add_link(
+ defs.Link(ltype=defs.LinkTypes.LinkedTo, document=network_cre.shallow_copy())
+ )
+
+ db_mock.return_value.get_gap_analysis_result.return_value = None
+ db_mock.return_value.gap_analysis_exists.return_value = False
+ db_mock.return_value.get_nodes.side_effect = lambda name=None, **kwargs: (
+ [base]
+ if name == "OWASP Kubernetes Top Ten 2022"
+ else [compare] if name == "OWASP Cheat Sheets" else []
+ )
+ schedule_mock.side_effect = RuntimeError("redis unavailable")
+ upstream_fetch_mock.side_effect = RuntimeError("upstream unavailable")
+
+ with self.app.test_client() as client:
+ response = client.get(
+ "/rest/v1/map_analysis?standard=OWASP%20Kubernetes%20Top%20Ten%202022&standard=OWASP%20Cheat%20Sheets",
+ headers={"Content-Type": "application/json"},
+ )
+
+ payload = json.loads(response.data)
+ self.assertEqual(200, response.status_code)
+ self.assertEqual(
+ "Cloud Cheat Sheets",
+ payload["specialized_cheatsheet_section"]["category"],
+ )
+ self.assertIn(base.id, payload["specialized_cheatsheet_section"]["result"])
+ self.assertIn(compare.id, payload["result"][base.id]["paths"])
+
+ @patch.object(web_main.cre_main, "fetch_upstream_json")
+ @patch.object(web_main.gap_analysis, "schedule")
+ @patch.object(db, "Node_collection")
+ def test_gap_analysis_adds_specialized_section_for_kubernetes_2025_cheatsheets(
+ self, db_mock, schedule_mock, upstream_fetch_mock
+ ) -> None:
+ network_cre = defs.CRE(
+ id="132-146", name="Network segmentation", description=""
+ )
+ base = defs.Standard(
+ name="OWASP Kubernetes Top Ten 2025 (Draft)",
+ sectionID="K05",
+ section="Missing Network Segmentation Controls",
+ )
+ base.add_link(
+ defs.Link(ltype=defs.LinkTypes.LinkedTo, document=network_cre.shallow_copy())
+ )
+
+ compare = defs.Standard(
+ name="OWASP Cheat Sheets",
+ section="Kubernetes Security Cheat Sheet",
+ )
+ compare.add_link(
+ defs.Link(ltype=defs.LinkTypes.LinkedTo, document=network_cre.shallow_copy())
+ )
+
+ db_mock.return_value.get_gap_analysis_result.return_value = None
+ db_mock.return_value.gap_analysis_exists.return_value = False
+ db_mock.return_value.get_nodes.side_effect = lambda name=None, **kwargs: (
+ [base]
+ if name == "OWASP Kubernetes Top Ten 2025 (Draft)"
+ else [compare] if name == "OWASP Cheat Sheets" else []
+ )
+ schedule_mock.side_effect = RuntimeError("redis unavailable")
+ upstream_fetch_mock.side_effect = RuntimeError("upstream unavailable")
+
+ with self.app.test_client() as client:
+ response = client.get(
+ "/rest/v1/map_analysis?standard=OWASP%20Kubernetes%20Top%20Ten%202025%20%28Draft%29&standard=OWASP%20Cheat%20Sheets",
+ headers={"Content-Type": "application/json"},
+ )
+
+ payload = json.loads(response.data)
+ self.assertEqual(200, response.status_code)
+ self.assertEqual(
+ "Cloud Cheat Sheets",
+ payload["specialized_cheatsheet_section"]["category"],
+ )
+ self.assertIn(base.id, payload["specialized_cheatsheet_section"]["result"])
+ self.assertIn(compare.id, payload["result"][base.id]["paths"])
+
+ @patch.object(web_main.cre_main, "fetch_upstream_json")
+ @patch.object(web_main.gap_analysis, "schedule")
+ @patch.object(db, "Node_collection")
+ def test_gap_analysis_returns_direct_overlap_for_kubernetes_and_ccm(
+ self, db_mock, schedule_mock, upstream_fetch_mock
+ ) -> None:
+ shared_cre = defs.CRE(
+ id="117-371", name="Centralized policy enforcement", description=""
+ )
+ base = defs.Standard(
+ name="OWASP Kubernetes Top Ten 2022",
+ sectionID="K04",
+ section="Lack of Centralized Policy Enforcement",
+ )
+ base.add_link(
+ defs.Link(ltype=defs.LinkTypes.LinkedTo, document=shared_cre.shallow_copy())
+ )
+
+ compare = defs.Standard(
+ name="Cloud Controls Matrix",
+ sectionID="AIS-01",
+ section="Application and Interface Security",
+ )
+ compare.add_link(
+ defs.Link(ltype=defs.LinkTypes.LinkedTo, document=shared_cre.shallow_copy())
+ )
+
+ db_mock.return_value.get_gap_analysis_result.return_value = None
+ db_mock.return_value.gap_analysis_exists.return_value = False
+ db_mock.return_value.get_nodes.side_effect = lambda name=None, **kwargs: (
+ [base]
+ if name == "OWASP Kubernetes Top Ten 2022"
+ else [compare] if name == "Cloud Controls Matrix" else []
+ )
+ schedule_mock.side_effect = RuntimeError("redis unavailable")
+ upstream_fetch_mock.side_effect = RuntimeError("upstream unavailable")
+
+ with self.app.test_client() as client:
+ response = client.get(
+ "/rest/v1/map_analysis?standard=OWASP%20Kubernetes%20Top%20Ten%202022&standard=Cloud%20Controls%20Matrix",
+ headers={"Content-Type": "application/json"},
+ )
+
+ payload = json.loads(response.data)
+ self.assertEqual(200, response.status_code)
+ self.assertIn("result", payload)
+ self.assertIn(base.id, payload["result"])
+ self.assertIn(compare.id, payload["result"][base.id]["paths"])
+
@patch.object(web_main.gap_analysis, "schedule")
@patch.object(db, "Node_collection")
def test_gap_analysis_supports_opencre_as_standard(
@@ -1312,12 +1463,90 @@ def get_nodes_side_effect(name=None, **kwargs):
self.assertIn("owasp_top10_comparison", payload)
self.assertEqual(
"Broken Access Control",
- payload["owasp_top10_comparison"]["items"][0]["top10_2025"]["section"],
+ payload["owasp_top10_comparison"]["items"][0]["right"]["section"],
)
schedule_mock.assert_called_once_with(
["OWASP Top 10 2021", "OWASP Top 10 2025"], db_mock.return_value
)
+ @patch.object(web_main.gap_analysis, "schedule")
+ @patch.object(db, "Node_collection")
+ def test_gap_analysis_adds_kubernetes_comparison_section(
+ self, db_mock, schedule_mock
+ ) -> None:
+ kubernetes_2022 = [
+ defs.Standard(
+ name="OWASP Kubernetes Top Ten 2022",
+ sectionID="K01",
+ section="Insecure Workload Configurations",
+ hyperlink="https://owasp.org/www-project-kubernetes-top-ten/2022/en/src/K01-insecure-workload-configurations",
+ ),
+ defs.Standard(
+ name="OWASP Kubernetes Top Ten 2022",
+ sectionID="K02",
+ section="Supply Chain Vulnerabilities",
+ hyperlink="https://owasp.org/www-project-kubernetes-top-ten/2022/en/src/K02-supply-chain-vulnerabilities",
+ ),
+ ]
+ kubernetes_2025 = [
+ defs.Standard(
+ name="OWASP Kubernetes Top Ten 2025 (Draft)",
+ sectionID="K01",
+ section="Insecure Workload Configurations",
+ hyperlink="https://owasp.org/www-project-kubernetes-top-ten/",
+ ),
+ defs.Standard(
+ name="OWASP Kubernetes Top Ten 2025 (Draft)",
+ sectionID="K02",
+ section="Overly Permissive Authorization Configurations",
+ hyperlink="https://owasp.org/www-project-kubernetes-top-ten/",
+ ),
+ ]
+ db_mock.return_value.get_gap_analysis_result.return_value = None
+ db_mock.return_value.gap_analysis_exists.return_value = False
+
+ def get_nodes_side_effect(name=None, **kwargs):
+ if name == "OWASP Kubernetes Top Ten 2022":
+ return kubernetes_2022
+ if name == "OWASP Kubernetes Top Ten 2025 (Draft)":
+ return kubernetes_2025
+ return []
+
+ db_mock.return_value.get_nodes.side_effect = get_nodes_side_effect
+ schedule_mock.side_effect = RuntimeError("redis unavailable")
+
+ with self.app.test_client() as client:
+ response = client.get(
+ "/rest/v1/map_analysis?standard=OWASP%20Kubernetes%20Top%20Ten%202022&standard=OWASP%20Kubernetes%20Top%20Ten%202025%20%28Draft%29",
+ headers={"Content-Type": "application/json"},
+ )
+
+ payload = json.loads(response.data)
+ self.assertEqual(200, response.status_code)
+ self.assertIn("owasp_top10_comparison", payload)
+ self.assertEqual(
+ [
+ "OWASP Kubernetes Top Ten 2022",
+ "OWASP Kubernetes Top Ten 2025 (Draft)",
+ ],
+ payload["owasp_top10_comparison"]["standards"],
+ )
+ self.assertEqual(
+ "Supply Chain Vulnerabilities",
+ payload["owasp_top10_comparison"]["items"][1]["left"]["section"],
+ )
+ self.assertEqual(
+ "Overly Permissive Authorization Configurations",
+ payload["owasp_top10_comparison"]["items"][1]["right"]["section"],
+ )
+ schedule_mock.assert_called_once_with(
+ [
+ "OWASP Kubernetes Top Ten 2022",
+ "OWASP Kubernetes Top Ten 2025 (Draft)",
+ ],
+ db_mock.return_value,
+ )
+
@patch.object(db, "Node_collection")
@patch.object(rq.job.Job, "fetch")
@patch.object(rq.Queue, "enqueue_call")
diff --git a/application/utils/external_project_parsers/data/owasp_kubernetes_top10_2025.json b/application/utils/external_project_parsers/data/owasp_kubernetes_top10_2025.json
index c55afb059..cf72881d0 100644
--- a/application/utils/external_project_parsers/data/owasp_kubernetes_top10_2025.json
+++ b/application/utils/external_project_parsers/data/owasp_kubernetes_top10_2025.json
@@ -2,70 +2,70 @@
{
"section_id": "K01",
"section": "Insecure Workload Configurations",
- "hyperlink": "https://owasp.org/www-project-kubernetes-top-ten/",
+ "hyperlink": "https://owasp.org/www-project-kubernetes-top-ten/2025/en/src/K01-Insecure-Workload-Configurations.html",
"cre_ids": ["233-748", "486-813"],
"fallback_section_ids": ["K01"]
},
{
"section_id": "K02",
"section": "Overly Permissive Authorization Configurations",
- "hyperlink": "https://owasp.org/www-project-kubernetes-top-ten/",
+ "hyperlink": "https://owasp.org/www-project-kubernetes-top-ten/2025/en/src/K02-Overly-Permissive-Authorization-Configurations.html",
"cre_ids": ["128-128", "724-770"],
"fallback_section_ids": ["K03"]
},
{
"section_id": "K03",
"section": "Secrets Management Failures",
- "hyperlink": "https://owasp.org/www-project-kubernetes-top-ten/",
+ "hyperlink": "https://owasp.org/www-project-kubernetes-top-ten/2025/en/src/K03-Secrets-Management-Failures.html",
"cre_ids": ["340-375", "774-888", "813-610"],
"fallback_section_ids": ["K08"]
},
{
"section_id": "K04",
"section": "Lack Of Cluster Level Policy Enforcement",
- "hyperlink": "https://owasp.org/www-project-kubernetes-top-ten/",
+ "hyperlink": "https://owasp.org/www-project-kubernetes-top-ten/2025/en/src/K04-Lack-Of-Cluster-Level-Policy-Enforcement.html",
"cre_ids": ["117-371"],
"fallback_section_ids": ["K04"]
},
{
"section_id": "K05",
"section": "Missing Network Segmentation Controls",
- "hyperlink": "https://owasp.org/www-project-kubernetes-top-ten/",
+ "hyperlink": "https://owasp.org/www-project-kubernetes-top-ten/2025/en/src/K05-Missing-Network-Segmentation-Controls.html",
"cre_ids": ["132-146", "467-784", "515-021"],
"fallback_section_ids": ["K07"]
},
{
"section_id": "K06",
"section": "Overly Exposed Kubernetes Components",
- "hyperlink": "https://owasp.org/www-project-kubernetes-top-ten/",
+ "hyperlink": "https://owasp.org/www-project-kubernetes-top-ten/2025/en/src/K06-Overly-Exposed-Kubernetes-Components.html",
"cre_ids": ["152-725", "640-364"],
"fallback_section_ids": ["K09"]
},
{
"section_id": "K07",
"section": "Misconfigured And Vulnerable Cluster Components",
- "hyperlink": "https://owasp.org/www-project-kubernetes-top-ten/",
+ "hyperlink": "https://owasp.org/www-project-kubernetes-top-ten/2025/en/src/K07-Misconfigured-And-Vulnerable-Cluster-Components.html",
"cre_ids": ["053-751", "233-748", "486-813", "715-334"],
"fallback_section_ids": ["K09", "K10"]
},
{
"section_id": "K08",
"section": "Cluster To Cloud Lateral Movement",
- "hyperlink": "https://owasp.org/www-project-kubernetes-top-ten/",
+ "hyperlink": "https://owasp.org/www-project-kubernetes-top-ten/2025/en/src/K08-Cluster-To-Cloud-Lateral-Movement.html",
"cre_ids": ["132-146", "640-364", "724-770"],
"fallback_section_ids": ["K03", "K07"]
},
{
"section_id": "K09",
"section": "Broken Authentication Mechanisms",
- "hyperlink": "https://owasp.org/www-project-kubernetes-top-ten/",
+ "hyperlink": "https://owasp.org/www-project-kubernetes-top-ten/2025/en/src/K09-Broken-Authentication-Mechanisms.html",
"cre_ids": ["177-260", "586-842", "633-428"],
"fallback_section_ids": ["K06"]
},
{
"section_id": "K10",
"section": "Inadequate Logging And Monitoring",
- "hyperlink": "https://owasp.org/www-project-kubernetes-top-ten/",
+ "hyperlink": "https://owasp.org/www-project-kubernetes-top-ten/2025/en/src/K10-Inadequate-Logging-And-Monitoring.html",
"cre_ids": ["058-083", "148-420", "402-706", "843-841"],
"fallback_section_ids": ["K05"]
}
diff --git a/application/web/web_main.py b/application/web/web_main.py
index b610d8b28..cc5b043a0 100644
--- a/application/web/web_main.py
+++ b/application/web/web_main.py
@@ -54,6 +54,20 @@
/ "data"
/ "owasp_top10_2025.json"
)
+OWASP_KUBERNETES_TOP10_2022_DATA_FILE = (
+ pathlib.Path(__file__).resolve().parent.parent
+ / "utils"
+ / "external_project_parsers"
+ / "data"
+ / "owasp_kubernetes_top10_2022.json"
+)
+OWASP_KUBERNETES_TOP10_2025_DATA_FILE = (
+ pathlib.Path(__file__).resolve().parent.parent
+ / "utils"
+ / "external_project_parsers"
+ / "data"
+ / "owasp_kubernetes_top10_2025.json"
+)
app = Blueprint(
"web",
@@ -185,8 +199,10 @@ def _normalize_source_name(source: Any) -> str | None:
return normalized_source[:64]
-def _load_owasp_top10_2025_entries() -> list[dict[str, str]]:
- with OWASP_TOP10_2025_DATA_FILE.open("r", encoding="utf-8") as handle:
+def _load_ranked_standard_entries(
+ data_file: pathlib.Path,
+) -> list[dict[str, str]]:
+ with data_file.open("r", encoding="utf-8") as handle:
return json.load(handle)
@@ -195,65 +211,95 @@ def _normalize_standard_name(standard: str) -> str:
return STANDARD_NAME_ALIASES.get(normalized_standard.lower(), normalized_standard)
-def _build_owasp_top10_comparison(
+def _standard_nodes_to_ranked_entries(
+ nodes: list[defs.Standard],
+) -> list[dict[str, str]]:
+ return [
+ {
+ "section_id": node.sectionID or "",
+ "section": node.section or "",
+ "hyperlink": node.hyperlink or "",
+ }
+ for node in nodes
+ ]
+
+
+def _build_ranked_standard_comparison(
standards: list[str], collection: db.Node_collection
) -> dict[str, Any] | None:
requested = {str(standard).strip().lower() for standard in standards}
- if requested != {"owasp top 10 2021", "owasp top 10 2025"}:
- return None
+ if requested == {"owasp top 10 2021", "owasp top 10 2025"}:
+ left_standard = "OWASP Top 10 2021"
+ right_standard = "OWASP Top 10 2025"
+ left_nodes = sorted(
+ collection.get_nodes(name=left_standard),
+ key=lambda node: node.sectionID or "",
+ )
+ if not left_nodes:
+ return None
+ left_entries = _standard_nodes_to_ranked_entries(left_nodes)
- top10_2021 = sorted(
- collection.get_nodes(name="OWASP Top 10 2021"),
- key=lambda node: node.sectionID or "",
- )
- if not top10_2021:
+ right_nodes = sorted(
+ collection.get_nodes(name=right_standard),
+ key=lambda node: node.sectionID or "",
+ )
+ if right_nodes:
+ right_entries = _standard_nodes_to_ranked_entries(right_nodes)
+ else:
+ right_entries = _load_ranked_standard_entries(OWASP_TOP10_2025_DATA_FILE)
+ elif requested == {
+ KUBERNETES_TOP10_2022_STANDARD_NAME.lower(),
+ KUBERNETES_TOP10_2025_STANDARD_NAME.lower(),
+ }:
+ left_standard = KUBERNETES_TOP10_2022_STANDARD_NAME
+ right_standard = KUBERNETES_TOP10_2025_STANDARD_NAME
+ left_nodes = sorted(
+ collection.get_nodes(name=left_standard),
+ key=lambda node: node.sectionID or "",
+ )
+ right_nodes = sorted(
+ collection.get_nodes(name=right_standard),
+ key=lambda node: node.sectionID or "",
+ )
+ left_entries = (
+ _standard_nodes_to_ranked_entries(left_nodes)
+ if left_nodes
+ else _load_ranked_standard_entries(OWASP_KUBERNETES_TOP10_2022_DATA_FILE)
+ )
+ right_entries = (
+ _standard_nodes_to_ranked_entries(right_nodes)
+ if right_nodes
+ else _load_ranked_standard_entries(OWASP_KUBERNETES_TOP10_2025_DATA_FILE)
+ )
+ else:
return None
- top10_2025_nodes = sorted(
- collection.get_nodes(name="OWASP Top 10 2025"),
- key=lambda node: node.sectionID or "",
- )
- if top10_2025_nodes:
- top10_2025 = [
- {
- "section_id": node.sectionID or "",
- "section": node.section or "",
- "hyperlink": node.hyperlink or "",
- }
- for node in top10_2025_nodes
- ]
- else:
- top10_2025 = _load_owasp_top10_2025_entries()
+ if not left_entries:
+ return None
- top10_2021_by_rank = {
- node.sectionID
- or "": {
- "section_id": node.sectionID or "",
- "section": node.section or "",
- "hyperlink": node.hyperlink or "",
- }
- for node in top10_2021
+ left_by_rank = {
+ entry["section_id"]: entry for entry in left_entries if entry.get("section_id")
}
- top10_2025_by_rank = {
- entry["section_id"]: entry for entry in top10_2025 if entry.get("section_id")
+ right_by_rank = {
+ entry["section_id"]: entry for entry in right_entries if entry.get("section_id")
}
- ranks = sorted(set(top10_2021_by_rank.keys()) | set(top10_2025_by_rank.keys()))
+ ranks = sorted(set(left_by_rank.keys()) | set(right_by_rank.keys()))
comparison = []
for rank in ranks:
- item_2021 = top10_2021_by_rank.get(rank)
- item_2025 = top10_2025_by_rank.get(rank)
+ left_entry = left_by_rank.get(rank)
+ right_entry = right_by_rank.get(rank)
comparison.append(
{
"rank": rank,
- "top10_2021": item_2021,
- "top10_2025": item_2025,
- "changed": (item_2021 or {}).get("section")
- != (item_2025 or {}).get("section"),
+ "left": left_entry,
+ "right": right_entry,
+ "changed": (left_entry or {}).get("section")
+ != (right_entry or {}).get("section"),
}
)
return {
- "standards": ["OWASP Top 10 2021", "OWASP Top 10 2025"],
+ "standards": [left_standard, right_standard],
"items": comparison,
}
@@ -796,7 +842,7 @@ def map_analysis() -> Any:
if direct_gap_analysis:
return jsonify(direct_gap_analysis)
abort(404, "No direct overlap found for requested standards")
- owasp_top10_comparison = _build_owasp_top10_comparison(standards, database)
+ owasp_top10_comparison = _build_ranked_standard_comparison(standards, database)
# First, check if we have cached results in the database
if database.gap_analysis_exists(standards_hash):