Skip to content

Commit b32789d

Browse files
committed
Add charts for Package, Severity and Importer Panel
Signed-off-by: Sampurna Pyne <sampurnapyne1710@gmail.com>
1 parent 1aa7f71 commit b32789d

7 files changed

Lines changed: 678 additions & 2 deletions

File tree

‎insights/charts/__init__.py‎

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
from functools import partial
2+
3+
from insights.models import ChartDefinition
4+
5+
from .importer_panel import _get_snapshot_data
6+
from .importer_panel import build_importer_exploit_columns
7+
from .importer_panel import build_importer_package_columns
8+
from .importer_panel import collect_importers
9+
from .package_panel import collect_cwes
10+
from .package_panel import collect_ecosystem_distribution
11+
from .package_panel import collect_packages
12+
from .package_panel import format_ecosystem_distribution
13+
from .package_panel import format_top_cwes
14+
from .package_panel import format_top_packages
15+
from .severity_panel import collect_severities
16+
from .severity_panel import get_severity_snapshot_data
17+
18+
CHARTS = [
19+
ChartDefinition(
20+
id="pkg-dist-donut",
21+
title="Ecosystem Distribution",
22+
panel="package_panel",
23+
chart_type="donut",
24+
formatter_fn=format_ecosystem_distribution,
25+
collect_fn=collect_ecosystem_distribution,
26+
),
27+
ChartDefinition(
28+
id="pkg-name-donut",
29+
title="Top 10 Packages",
30+
panel="package_panel",
31+
chart_type="donut",
32+
formatter_fn=format_top_packages,
33+
collect_fn=collect_packages,
34+
is_per_package=True,
35+
),
36+
ChartDefinition(
37+
id="pkg-cwe-bar",
38+
title="Top 10 CWE Distribution",
39+
panel="package_panel",
40+
chart_type="colored_bar",
41+
formatter_fn=format_top_cwes,
42+
collect_fn=collect_cwes,
43+
is_per_package=False,
44+
has_search=True,
45+
),
46+
ChartDefinition(
47+
id="severity-scatter-plot",
48+
title="Severity Distribution across Packages",
49+
panel="severity_panel",
50+
chart_type="scatter",
51+
formatter_fn=get_severity_snapshot_data,
52+
collect_fn=collect_severities,
53+
has_search=True,
54+
),
55+
ChartDefinition(
56+
id="importer-empty-pkg-bar",
57+
title="Affected Package Coverage across Importers",
58+
panel="importer_panel",
59+
chart_type="importer_bar",
60+
formatter_fn=partial(_get_snapshot_data, build_columns_fn=build_importer_package_columns),
61+
collect_fn=collect_importers,
62+
),
63+
ChartDefinition(
64+
id="importer-exploit-bar",
65+
title="Exploit Coverage across Importers",
66+
panel="importer_panel",
67+
chart_type="importer_bar",
68+
formatter_fn=partial(_get_snapshot_data, build_columns_fn=build_importer_exploit_columns),
69+
collect_fn=collect_importers,
70+
),
71+
]
72+
73+
PANELS = [
74+
"overview_panel",
75+
"package_panel",
76+
"severity_panel",
77+
"importer_panel",
78+
"data_quality_panel",
79+
]
80+
PANEL_LABELS = {
81+
"overview_panel": "Overview",
82+
"package_panel": "Package Analytics",
83+
"severity_panel": "Severity Analytics",
84+
"importer_panel": "Importer Analytics",
85+
"data_quality_panel": "Data Quality Analytics",
86+
}
87+
PANEL_LAYOUTS = {
88+
"package_panel": "split_top",
89+
}

‎insights/charts/importer_panel.py‎

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
#
2+
# Copyright (c) nexB Inc. and others. All rights reserved.
3+
# VulnerableCode is a trademark of nexB Inc.
4+
# SPDX-License-Identifier: Apache-2.0
5+
# See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
6+
# See https://github.com/aboutcode-org/vulnerablecode for support or download.
7+
# See https://aboutcode.org for more information about nexB OSS projects.
8+
#
9+
from typing import Any
10+
from typing import Dict
11+
12+
from django.db.models import Count
13+
from django.db.models import Exists
14+
from django.db.models import OuterRef
15+
from django.db.models import Q
16+
17+
from insights.models import ImporterInsight
18+
from insights.utils import format_importer_name
19+
from vulnerabilities.models import AdvisoryExploit
20+
from vulnerabilities.models import AdvisoryV2
21+
22+
# Ignore Phantom Importers that don't collect Affected Packages
23+
IGNORED_IMPORTERS = {
24+
"epss_importer_v2",
25+
"epss",
26+
"vulnrichment_importer_v2",
27+
"suse_importer_v2",
28+
"suse_score",
29+
}
30+
31+
32+
def packages_queryset():
33+
"""Return a query set of package statistics by importer."""
34+
return (
35+
AdvisoryV2.objects.filter(is_latest=True)
36+
.exclude(datasource_id__in=IGNORED_IMPORTERS)
37+
.values("datasource_id")
38+
.annotate(
39+
total_advisories=Count("avid", distinct=True),
40+
advisories_with_packages=Count(
41+
"avid", filter=Q(impacted_packages__affecting_packages__isnull=False), distinct=True
42+
),
43+
advisories_with_ghost_packages=Count(
44+
"avid",
45+
filter=Q(impacted_packages__affecting_packages__is_ghost=True),
46+
distinct=True,
47+
),
48+
)
49+
.order_by("-total_advisories")
50+
.iterator()
51+
)
52+
53+
54+
def exploits_queryset():
55+
"""Return a query set of exploit statistics by importer."""
56+
kev_exploits = AdvisoryExploit.objects.filter(advisory=OuterRef("pk"), data_source="KEV")
57+
metasploit_exploits = AdvisoryExploit.objects.filter(
58+
advisory=OuterRef("pk"), data_source="Metasploit"
59+
)
60+
exploitdb_exploits = AdvisoryExploit.objects.filter(
61+
advisory=OuterRef("pk"), data_source="Exploit-DB"
62+
)
63+
64+
return (
65+
AdvisoryV2.objects.filter(is_latest=True)
66+
.exclude(datasource_id__in=IGNORED_IMPORTERS)
67+
.annotate(
68+
has_kev=Exists(kev_exploits),
69+
has_metasploit=Exists(metasploit_exploits),
70+
has_exploitdb=Exists(exploitdb_exploits),
71+
)
72+
.values("datasource_id")
73+
.annotate(
74+
advisories_with_kev=Count("avid", filter=Q(has_kev=True), distinct=True),
75+
advisories_with_metasploit=Count(
76+
"avid", filter=Q(has_kev=False, has_metasploit=True), distinct=True
77+
),
78+
advisories_with_exploitdb=Count(
79+
"avid",
80+
filter=Q(has_kev=False, has_metasploit=False, has_exploitdb=True),
81+
distinct=True,
82+
),
83+
)
84+
.iterator()
85+
)
86+
87+
88+
def iter_importer_insights():
89+
"""Yield ImporterInsight objects by merging package and exploit statistics."""
90+
exploit_stats_by_importer = {}
91+
for record in exploits_queryset():
92+
exploit_stats_by_importer[record["datasource_id"]] = record
93+
94+
for pkg_record in packages_queryset():
95+
datasource_id = pkg_record["datasource_id"]
96+
exploit_record = exploit_stats_by_importer.get(datasource_id, {})
97+
98+
yield ImporterInsight(
99+
importer=datasource_id,
100+
total_advisories=pkg_record["total_advisories"],
101+
advisories_with_packages=pkg_record["advisories_with_packages"],
102+
advisories_with_ghost_packages=pkg_record["advisories_with_ghost_packages"],
103+
advisories_with_kev=exploit_record.get("advisories_with_kev", 0),
104+
advisories_with_metasploit=exploit_record.get("advisories_with_metasploit", 0),
105+
advisories_with_exploitdb=exploit_record.get("advisories_with_exploitdb", 0),
106+
)
107+
108+
109+
def collect_importers(pipeline):
110+
"""Calculates and stores importer insights."""
111+
112+
# Two charts use this function, so we avoid rerunning the query twice
113+
if pipeline.importer_insights:
114+
return
115+
116+
for insight in iter_importer_insights():
117+
pipeline.importer_insights.append(insight)
118+
119+
120+
def _get_snapshot_data(snapshot: Any, build_columns_fn) -> Dict[str, Any]:
121+
"""Helper to format importer statistics using a provided column builder."""
122+
all_importers = list(snapshot.importer_insights.order_by("-total_advisories"))
123+
data = {}
124+
125+
if all_importers:
126+
# Top 5 by default for global view
127+
data["global"] = build_columns_fn(all_importers[:5])
128+
129+
# Individual data for each importer
130+
for importer_insight in all_importers:
131+
formatted_name = format_importer_name(importer_insight.importer)
132+
data[formatted_name] = build_columns_fn([importer_insight])
133+
134+
return data
135+
136+
137+
def build_importer_package_columns(importers) -> Dict[str, Any]:
138+
"""Helper to build mappings for Package Coverage chart as expected by Billboard.JS"""
139+
importer_names = []
140+
total_advisories = []
141+
advisories_with_packages = []
142+
advisories_without_packages = []
143+
advisories_with_ghost_packages = []
144+
advisories_without_ghost_packages = []
145+
146+
for importer_insight in importers:
147+
importer_names.append(format_importer_name(importer_insight.importer))
148+
total_advisories.append(importer_insight.total_advisories)
149+
advisories_with_packages.append(importer_insight.advisories_with_packages)
150+
advisories_without_packages.append(
151+
importer_insight.total_advisories - importer_insight.advisories_with_packages
152+
)
153+
advisories_with_ghost_packages.append(importer_insight.advisories_with_ghost_packages)
154+
advisories_without_ghost_packages.append(
155+
importer_insight.advisories_with_packages
156+
- importer_insight.advisories_with_ghost_packages
157+
)
158+
159+
return {
160+
"x_categories": importer_names,
161+
"columns": [
162+
["total_advisories"] + total_advisories,
163+
["advisories_without_packages"] + advisories_without_packages,
164+
["advisories_with_packages"] + advisories_with_packages,
165+
["advisories_without_ghost_packages"] + advisories_without_ghost_packages,
166+
["advisories_with_ghost_packages"] + advisories_with_ghost_packages,
167+
],
168+
"groups": [
169+
["advisories_without_packages", "advisories_with_packages"],
170+
["advisories_without_ghost_packages", "advisories_with_ghost_packages"],
171+
],
172+
}
173+
174+
175+
def build_importer_exploit_columns(importers) -> Dict[str, Any]:
176+
"""Helper to build mappings for Exploit Coverage chart as expected by Billboard.JS"""
177+
importer_names = []
178+
total_advisories = []
179+
advisories_with_kev = []
180+
advisories_with_metasploit = []
181+
advisories_with_exploitdb = []
182+
advisories_without_exploits = []
183+
184+
for importer_insight in importers:
185+
importer_names.append(format_importer_name(importer_insight.importer))
186+
total_advisories.append(importer_insight.total_advisories)
187+
advisories_with_kev.append(importer_insight.advisories_with_kev)
188+
advisories_with_metasploit.append(importer_insight.advisories_with_metasploit)
189+
advisories_with_exploitdb.append(importer_insight.advisories_with_exploitdb)
190+
191+
total_with_exploits = (
192+
importer_insight.advisories_with_kev
193+
+ importer_insight.advisories_with_metasploit
194+
+ importer_insight.advisories_with_exploitdb
195+
)
196+
advisories_without_exploits.append(importer_insight.total_advisories - total_with_exploits)
197+
198+
return {
199+
"x_categories": importer_names,
200+
"columns": [
201+
["total_advisories"] + total_advisories,
202+
["advisories_without_exploits"] + advisories_without_exploits,
203+
["advisories_with_exploitdb"] + advisories_with_exploitdb,
204+
["advisories_with_metasploit"] + advisories_with_metasploit,
205+
["advisories_with_kev"] + advisories_with_kev,
206+
],
207+
"groups": [
208+
["advisories_with_exploitdb", "advisories_with_metasploit", "advisories_with_kev"]
209+
],
210+
}

0 commit comments

Comments
 (0)