Skip to content

Commit a04f9fd

Browse files
committed
Add tests
Signed-off-by: Tushar Goel <tushar.goel.dav@gmail.com>
1 parent def1c95 commit a04f9fd

File tree

4 files changed

+268
-24
lines changed

4 files changed

+268
-24
lines changed

vulnerabilities/importers/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
from vulnerabilities.pipelines.v2_importers import github_osv_importer as github_osv_importer_v2
5353
from vulnerabilities.pipelines.v2_importers import gitlab_importer as gitlab_importer_v2
5454
from vulnerabilities.pipelines.v2_importers import istio_importer as istio_importer_v2
55+
from vulnerabilities.pipelines.v2_importers import mattermost_importer as mattermost_importer_v2
5556
from vulnerabilities.pipelines.v2_importers import mozilla_importer as mozilla_importer_v2
5657
from vulnerabilities.pipelines.v2_importers import npm_importer as npm_importer_v2
5758
from vulnerabilities.pipelines.v2_importers import nvd_importer as nvd_importer_v2
@@ -87,6 +88,7 @@
8788
aosp_importer_v2.AospImporterPipeline,
8889
ruby_importer_v2.RubyImporterPipeline,
8990
epss_importer_v2.EPSSImporterPipeline,
91+
mattermost_importer_v2.MattermostImporterPipeline,
9092
nvd_importer.NVDImporterPipeline,
9193
github_importer.GitHubAPIImporterPipeline,
9294
gitlab_importer.GitLabImporterPipeline,

vulnerabilities/pipelines/v2_importers/mattermost_importer.py

Lines changed: 40 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from vulnerabilities import severity_systems
1616
from vulnerabilities.importer import AdvisoryData
1717
from vulnerabilities.importer import AffectedPackageV2
18+
from vulnerabilities.importer import ReferenceV2
1819
from vulnerabilities.importer import VulnerabilitySeverity
1920
from vulnerabilities.pipelines import VulnerableCodeBaseImporterPipelineV2
2021
from vulnerabilities.utils import fetch_response
@@ -23,6 +24,8 @@
2324
"Mattermost Mobile Apps": "mattermost-mobile",
2425
"Mattermost Server": "mattermost-server",
2526
"Mattermost Desktop App": "desktop",
27+
"Mattermost Boards": "mattermost-plugin-boards",
28+
"Mattermost Plugins": "mattermost-plugin-github",
2629
}
2730

2831

@@ -57,6 +60,9 @@ def collect_advisories(self) -> Iterable[AdvisoryData]:
5760

5861
for advisory in data:
5962
vuln_id = advisory.get("issue_id")
63+
if not vuln_id or not vuln_id.startswith("MMSA-"):
64+
self.log(f"Skipping advisory with missing issue_id. {vuln_id}")
65+
continue
6066
cve_id = advisory.get("cve_id")
6167
details = advisory.get("details")
6268

@@ -66,42 +72,53 @@ def collect_advisories(self) -> Iterable[AdvisoryData]:
6672

6773
package_name = MM_REPO.get(platform)
6874

69-
if not package_name:
70-
self.log(f"Unknown platform '{platform}' in advisory '{vuln_id}'. Skipping.")
71-
continue
72-
73-
package = PackageURL(
74-
type="github",
75-
namespace="mattermost",
76-
name=MM_REPO.get(platform),
77-
)
78-
7975
affected_packages = []
80-
8176
severity = advisory.get("severity")
77+
if not package_name:
78+
self.log(f"Unknown platform '{platform}' in advisory '{vuln_id}'.")
8279

83-
if isinstance(fixed_versions, list):
84-
fixed_versions = [v for v in fixed_versions if v and v.strip()]
85-
fixed_versions = [v.lstrip("v") for v in fixed_versions]
86-
if isinstance(fixed_versions, str):
87-
fixed_versions = [fixed_versions.lstrip("v")]
88-
89-
affected_packages.append(
90-
AffectedPackageV2(
91-
package=package,
92-
fixed_version_range=GitHubVersionRange.from_versions(fixed_versions),
80+
else:
81+
package = PackageURL(
82+
type="github",
83+
namespace="mattermost",
84+
name=MM_REPO.get(platform),
9385
)
94-
)
86+
87+
if isinstance(fixed_versions, list):
88+
fixed_versions = [v for v in fixed_versions if v and v.strip()]
89+
fixed_versions = [v.lstrip("v") for v in fixed_versions]
90+
if isinstance(fixed_versions, str):
91+
fixed_versions = [fixed_versions.lstrip("v")]
92+
93+
fixed_versions = [v.replace("and ", "") for v in fixed_versions]
94+
fixed_versions = [v.strip() for v in fixed_versions]
95+
96+
try:
97+
affected_packages.append(
98+
AffectedPackageV2(
99+
package=package,
100+
fixed_version_range=GitHubVersionRange.from_versions(fixed_versions),
101+
)
102+
)
103+
except Exception as e:
104+
self.log(
105+
f"Error processing fixed versions '{fixed_versions}' for advisory '{vuln_id}': {e}"
106+
)
95107

96108
severities = []
97109
severities.append(
98110
VulnerabilitySeverity(system=severity_systems.CVSS31_QUALITY, value=severity)
99111
)
100112

113+
reference = ReferenceV2(
114+
url="https://mattermost.com/security-updates/",
115+
)
116+
101117
yield AdvisoryData(
102118
advisory_id=vuln_id,
103119
aliases=[cve_id],
104120
summary=details,
121+
references_v2=[reference],
105122
affected_packages=affected_packages,
106-
url="https://mattermost.com/security-updates/",
123+
url=self.url,
107124
)
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
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+
10+
import pytest
11+
from packageurl import PackageURL
12+
from univers.version_range import GitHubVersionRange
13+
14+
from vulnerabilities.importer import AdvisoryData
15+
from vulnerabilities.pipelines.v2_importers.mattermost_importer import MattermostImporterPipeline
16+
17+
18+
@pytest.fixture
19+
def sample_mattermost_data():
20+
return [
21+
{
22+
"issue_id": "MMSA-2024-001",
23+
"cve_id": "CVE-2024-1234",
24+
"details": "Test vulnerability in Mattermost Server",
25+
"platform": "Mattermost Server",
26+
"severity": "HIGH",
27+
"fix_versions": ["v9.0.1", "v8.1.5"],
28+
}
29+
]
30+
31+
32+
@pytest.fixture
33+
def importer(monkeypatch, sample_mattermost_data):
34+
"""
35+
Create an importer with fetch_response mocked.
36+
"""
37+
38+
def mock_fetch_response(url):
39+
class MockResponse:
40+
def json(self_inner):
41+
return sample_mattermost_data
42+
43+
return MockResponse()
44+
45+
monkeypatch.setattr(
46+
"vulnerabilities.pipelines.v2_importers.mattermost_importer.fetch_response",
47+
mock_fetch_response,
48+
)
49+
50+
return MattermostImporterPipeline()
51+
52+
53+
def test_advisories_count(importer):
54+
assert importer.advisories_count() == 1
55+
56+
57+
def test_collect_advisories_happy_path(importer):
58+
advisories = list(importer.collect_advisories())
59+
60+
assert len(advisories) == 1
61+
advisory = advisories[0]
62+
63+
assert isinstance(advisory, AdvisoryData)
64+
assert advisory.advisory_id == "MMSA-2024-001"
65+
assert advisory.aliases == ["CVE-2024-1234"]
66+
assert "Test vulnerability" in advisory.summary
67+
68+
assert advisory.affected_packages
69+
affected = advisory.affected_packages[0]
70+
71+
assert affected.package == PackageURL(
72+
type="github",
73+
namespace="mattermost",
74+
name="mattermost-server",
75+
)
76+
77+
assert isinstance(affected.fixed_version_range, GitHubVersionRange)
78+
assert str(affected.fixed_version_range) == "vers:github/8.1.5|9.0.1"
79+
80+
81+
def test_skip_invalid_issue_id(monkeypatch):
82+
data = [
83+
{
84+
"issue_id": "INVALID-001",
85+
"platform": "Mattermost Server",
86+
}
87+
]
88+
89+
def mock_fetch_response(url):
90+
class MockResponse:
91+
def json(self):
92+
return data
93+
94+
return MockResponse()
95+
96+
monkeypatch.setattr(
97+
"vulnerabilities.pipelines.v2_importers.mattermost_importer.fetch_response",
98+
mock_fetch_response,
99+
)
100+
101+
importer = MattermostImporterPipeline()
102+
advisories = list(importer.collect_advisories())
103+
104+
assert advisories == []
105+
106+
107+
def test_unknown_platform(monkeypatch):
108+
data = [
109+
{
110+
"issue_id": "MMSA-2024-002",
111+
"platform": "Unknown Product",
112+
"fix_versions": ["1.0.0"],
113+
}
114+
]
115+
116+
def mock_fetch_response(url):
117+
class MockResponse:
118+
def json(self):
119+
return data
120+
121+
return MockResponse()
122+
123+
monkeypatch.setattr(
124+
"vulnerabilities.pipelines.v2_importers.mattermost_importer.fetch_response",
125+
mock_fetch_response,
126+
)
127+
128+
importer = MattermostImporterPipeline()
129+
advisories = list(importer.collect_advisories())
130+
131+
assert len(advisories) == 1
132+
assert advisories[0].affected_packages == []
133+
134+
135+
def test_fixed_version_string_normalization(monkeypatch):
136+
data = [
137+
{
138+
"issue_id": "MMSA-2024-003",
139+
"platform": "Mattermost Desktop App",
140+
"fix_versions": "v2.0.0",
141+
}
142+
]
143+
144+
def mock_fetch_response(url):
145+
class MockResponse:
146+
def json(self):
147+
return data
148+
149+
return MockResponse()
150+
151+
monkeypatch.setattr(
152+
"vulnerabilities.pipelines.v2_importers.mattermost_importer.fetch_response",
153+
mock_fetch_response,
154+
)
155+
156+
importer = MattermostImporterPipeline()
157+
advisories = list(importer.collect_advisories())
158+
159+
affected = advisories[0].affected_packages[0]
160+
assert "2.0.0" in str(affected.fixed_version_range)
161+
162+
163+
def test_bad_version_does_not_crash(monkeypatch):
164+
data = [
165+
{
166+
"issue_id": "MMSA-2024-004",
167+
"platform": "Mattermost Server",
168+
"fix_versions": ["not-a-version"],
169+
}
170+
]
171+
172+
def mock_fetch_response(url):
173+
class MockResponse:
174+
def json(self):
175+
return data
176+
177+
return MockResponse()
178+
179+
monkeypatch.setattr(
180+
"vulnerabilities.pipelines.v2_importers.mattermost_importer.fetch_response",
181+
mock_fetch_response,
182+
)
183+
184+
importer = MattermostImporterPipeline()
185+
advisories = list(importer.collect_advisories())
186+
187+
# Advisory should still be yielded, but without affected packages
188+
assert len(advisories) == 1
189+
assert advisories[0].affected_packages == []
190+
191+
192+
def test_fetch_is_cached(monkeypatch):
193+
call_count = {"count": 0}
194+
195+
def mock_fetch_response(url):
196+
call_count["count"] += 1
197+
198+
class MockResponse:
199+
def json(self):
200+
return []
201+
202+
return MockResponse()
203+
204+
monkeypatch.setattr(
205+
"vulnerabilities.pipelines.v2_importers.mattermost_importer.fetch_response",
206+
mock_fetch_response,
207+
)
208+
209+
importer = MattermostImporterPipeline()
210+
importer.advisories_count()
211+
importer.collect_advisories()
212+
213+
assert call_count["count"] == 1

vulnerabilities/views.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from django.contrib.auth.views import LoginView
1616
from django.core.exceptions import ValidationError
1717
from django.core.mail import send_mail
18+
from django.db.models import F
1819
from django.db.models import Prefetch
1920
from django.http.response import Http404
2021
from django.shortcuts import get_object_or_404
@@ -691,7 +692,18 @@ class AdvisoryPackagesDetails(DetailView):
691692
model = models.AdvisoryV2
692693
template_name = "advisory_package_details.html"
693694
slug_url_kwarg = "avid"
694-
slug_field = "avid"
695+
696+
def get_object(self, queryset=None):
697+
avid = self.kwargs.get(self.slug_url_kwarg)
698+
if not avid:
699+
raise Http404("Missing advisory identifier")
700+
701+
advisory = models.AdvisoryV2.objects.latest_for_avid(avid)
702+
703+
if not advisory:
704+
raise Http404(f"No advisory found for avid: {avid}")
705+
706+
return advisory
695707

696708
def get_queryset(self):
697709
"""

0 commit comments

Comments
 (0)