Skip to content

Commit 60a1906

Browse files
authored
Merge pull request #393 from Hritik14/collect_mozilla
Collect Mozilla
2 parents 4ebaa48 + e6d652c commit 60a1906

8 files changed

Lines changed: 318 additions & 0 deletions

File tree

SOURCES.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,5 @@
4747
+----------------+------------------------------------------------------------------------------------------------------+----------------------------------------------------+
4848
|suse_scores | https://ftp.suse.com/pub/projects/security/yaml/suse-cvss-scores.yaml |vulnerability severity scores by SUSE |
4949
+----------------+------------------------------------------------------------------------------------------------------+----------------------------------------------------+
50+
|mozilla | https://github.com/mozilla/foundation-security-advisories |mozilla |
51+
+----------------+------------------------------------------------------------------------------------------------------+----------------------------------------------------+

pytest.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,6 @@ addopts =
3030
--ignore=vulnerabilities/importers/suse_backports.py
3131
--ignore=vulnerabilities/importers/suse_scores.py
3232
--ignore=vulnerabilities/importers/ubuntu_usn.py
33+
--ignore=vulnerabilities/importers/mozilla.py
3334
--ignore=vulnerabilities/management/commands/create_cpe_to_purl_map.py
3435
--ignore=vulnerabilities/lib_oval.py

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,5 @@ lxml>=4.6.4
1717
gunicorn>=20.1.0
1818
django-environ==0.4.5
1919
defusedxml==0.7.1
20+
21+
Markdown==3.3.4

vulnerabilities/helpers.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,41 @@ def fetch_yaml(url):
7272
create_etag = MagicMock()
7373

7474

75+
def split_markdown_front_matter(lines: str) -> Tuple[str, str]:
76+
"""
77+
This function splits lines into markdown front matter and the markdown body
78+
and returns list of lines for both
79+
80+
for example :
81+
lines =
82+
---
83+
title: ISTIO-SECURITY-2019-001
84+
description: Incorrect access control.
85+
cves: [CVE-2019-12243]
86+
---
87+
# Markdown starts here
88+
89+
split_markdown_front_matter(lines) would return
90+
['title: ISTIO-SECURITY-2019-001','description: Incorrect access control.'
91+
,'cves: [CVE-2019-12243]'],
92+
["# Markdown starts here"]
93+
"""
94+
95+
fmlines = []
96+
mdlines = []
97+
splitter = mdlines
98+
99+
for index, line in enumerate(lines.split("\n")):
100+
if index == 0 and line.strip().startswith("---"):
101+
splitter = fmlines
102+
elif line.strip().startswith("---"):
103+
splitter = mdlines
104+
else:
105+
splitter.append(line)
106+
107+
return "\n".join(fmlines), "\n".join(mdlines)
108+
109+
75110
def contains_alpha(string):
76111
"""
77112
Return True if the input 'string' contains any alphabet
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import re
2+
from typing import List
3+
from typing import Set
4+
5+
import yaml
6+
from bs4 import BeautifulSoup
7+
from markdown import markdown
8+
from packageurl import PackageURL
9+
10+
from vulnerabilities.importer import Advisory
11+
from vulnerabilities.importer import GitImporter
12+
from vulnerabilities.importer import Reference
13+
from vulnerabilities.importer import VulnerabilitySeverity
14+
from vulnerabilities.helpers import is_cve
15+
from vulnerabilities.helpers import split_markdown_front_matter
16+
from vulnerabilities.severity_systems import SCORING_SYSTEMS
17+
18+
REPOSITORY = "mozilla/foundation-security-advisories"
19+
MFSA_FILENAME_RE = re.compile(r"mfsa(\d{4}-\d{2,3})\.(md|yml)$")
20+
21+
22+
class MozillaImporter(GitImporter):
23+
def __enter__(self):
24+
super(MozillaImporter, self).__enter__()
25+
26+
if not getattr(self, "_added_files", None):
27+
self._added_files, self._updated_files = self.file_changes(
28+
recursive=True, subdir="announce"
29+
)
30+
31+
def updated_advisories(self) -> Set[Advisory]:
32+
files = self._updated_files.union(self._added_files)
33+
files = [
34+
f for f in files if f.endswith(".md") or f.endswith(".yml")
35+
] # skip irrelevant files
36+
37+
advisories = []
38+
for path in files:
39+
advisories.extend(to_advisories(path))
40+
41+
return self.batch_advisories(advisories)
42+
43+
44+
def to_advisories(path: str) -> List[Advisory]:
45+
"""
46+
Convert a file to corresponding advisories.
47+
This calls proper method to handle yml/md files.
48+
"""
49+
mfsa_id = mfsa_id_from_filename(path)
50+
if not mfsa_id:
51+
return []
52+
53+
with open(path) as lines:
54+
if path.endswith(".md"):
55+
return get_advisories_from_md(mfsa_id, lines)
56+
if path.endswith(".yml"):
57+
return get_advisories_from_yml(mfsa_id, lines)
58+
59+
return []
60+
61+
62+
def get_advisories_from_yml(mfsa_id, lines) -> List[Advisory]:
63+
advisories = []
64+
data = yaml.safe_load(lines)
65+
data["mfsa_id"] = mfsa_id
66+
67+
fixed_package_urls = get_package_urls(data.get("fixed_in"))
68+
references = get_yml_references(data)
69+
70+
if not data.get("advisories"):
71+
return []
72+
73+
for cve, advisory in data["advisories"].items():
74+
# These may contain HTML tags
75+
summary = BeautifulSoup(advisory.get("description", ""), features="lxml").get_text()
76+
77+
advisories.append(
78+
Advisory(
79+
summary=summary,
80+
vulnerability_id=cve if is_cve(cve) else "",
81+
impacted_package_urls=[],
82+
resolved_package_urls=fixed_package_urls,
83+
references=references,
84+
)
85+
)
86+
87+
return advisories
88+
89+
90+
def get_advisories_from_md(mfsa_id, lines) -> List[Advisory]:
91+
yamltext, mdtext = split_markdown_front_matter(lines.read())
92+
data = yaml.safe_load(yamltext)
93+
data["mfsa_id"] = mfsa_id
94+
95+
fixed_package_urls = get_package_urls(data.get("fixed_in"))
96+
references = get_yml_references(data)
97+
cves = re.findall(r"CVE-\d+-\d+", yamltext + mdtext, re.IGNORECASE)
98+
for cve in cves:
99+
references.append(
100+
Reference(
101+
reference_id=cve,
102+
url=f"https://cve.mitre.org/cgi-bin/cvename.cgi?name={cve}",
103+
)
104+
)
105+
106+
description = html_get_p_under_h3(markdown(mdtext), "description")
107+
108+
return [
109+
Advisory(
110+
summary=description,
111+
vulnerability_id="",
112+
impacted_package_urls=[],
113+
resolved_package_urls=fixed_package_urls,
114+
references=references,
115+
)
116+
]
117+
118+
119+
def html_get_p_under_h3(html, h3: str):
120+
soup = BeautifulSoup(html, features="lxml")
121+
h3tag = soup.find("h3", text=lambda txt: txt.lower() == h3)
122+
p = ""
123+
if h3tag:
124+
for tag in h3tag.next_siblings:
125+
if tag.name:
126+
if tag.name != "p":
127+
break
128+
p += tag.get_text()
129+
return p
130+
131+
132+
def mfsa_id_from_filename(filename):
133+
match = MFSA_FILENAME_RE.search(filename)
134+
if match:
135+
return "mfsa" + match.group(1)
136+
137+
return None
138+
139+
140+
def get_package_urls(pkgs: List[str]) -> List[PackageURL]:
141+
package_urls = [
142+
PackageURL(
143+
type="mozilla",
144+
# pkg is of the form "Firefox ESR 1.21" or "Thunderbird 2.21"
145+
name=pkg.rsplit(None, 1)[0],
146+
version=pkg.rsplit(None, 1)[1],
147+
)
148+
for pkg in pkgs
149+
if pkg
150+
]
151+
return package_urls
152+
153+
154+
def get_yml_references(data: any) -> List[Reference]:
155+
"""
156+
Returns a list of references
157+
Currently only considers the given mfsa as a reference
158+
"""
159+
# FIXME: Needs improvement
160+
# Should we add 'bugs' section in references too?
161+
# Should we add 'impact'/severity of CVE in references too?
162+
# If yes, then fix alpine_linux importer as well
163+
# Otherwise, do we need severity field for adversary as well?
164+
165+
severities = ["critical", "high", "medium", "low", "none"]
166+
severity = "none"
167+
if data.get("impact"):
168+
impact = data.get("impact").lower()
169+
for s in severities:
170+
if s in impact:
171+
severity = s
172+
break
173+
174+
return [
175+
Reference(
176+
reference_id=data["mfsa_id"],
177+
url="https://www.mozilla.org/en-US/security/advisories/{}".format(data["mfsa_id"]),
178+
severities=[VulnerabilitySeverity(scoring_systems["generic_textual"], severity)],
179+
)
180+
]

vulnerabilities/tests/conftest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,4 +74,5 @@ def no_rmtree(monkeypatch):
7474
"test_importer_yielder.py",
7575
"test_upstream.py",
7676
"test_istio.py",
77+
"test_mozilla.py",
7778
]
35.2 KB
Binary file not shown.
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import os
2+
import shutil
3+
import tempfile
4+
import zipfile
5+
from unittest.mock import patch
6+
7+
from django.test import TestCase
8+
9+
from vulnerabilities import models
10+
from vulnerabilities.import_runner import ImportRunner
11+
from vulnerabilities.importers.npm import categorize_versions
12+
13+
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
14+
TEST_DATA = os.path.join(BASE_DIR, "test_data/")
15+
16+
17+
@patch("vulnerabilities.importers.MozillaImporter._update_from_remote")
18+
class MozillaImportTest(TestCase):
19+
20+
tempdir = None
21+
22+
@classmethod
23+
def setUpClass(cls) -> None:
24+
cls.tempdir = tempfile.mkdtemp()
25+
zip_path = os.path.join(TEST_DATA, "mozilla.zip")
26+
27+
with zipfile.ZipFile(zip_path, "r") as zip_ref:
28+
zip_ref.extractall(cls.tempdir)
29+
30+
cls.importer = models.Importer.objects.create(
31+
name="mozilla_unittests",
32+
license="",
33+
last_run=None,
34+
data_source="MozillaImporter",
35+
data_source_cfg={
36+
"repository_url": "https://example.git",
37+
"working_directory": os.path.join(cls.tempdir, "mozilla_test"),
38+
"create_working_directory": False,
39+
"remove_working_directory": False,
40+
},
41+
)
42+
43+
@classmethod
44+
def tearDownClass(cls) -> None:
45+
# Make sure no requests for unexpected package names have been made during the tests.
46+
shutil.rmtree(cls.tempdir)
47+
48+
def test_import(self, _):
49+
runner = ImportRunner(self.importer, 100)
50+
51+
# Remove if we don't need set_api in MozillaImporter
52+
# with patch("vulnerabilities.importers.MozillaImporter.versions", new=MOCK_VERSION_API):
53+
# with patch("vulnerabilities.importers.MozillaImporter.set_api"):
54+
# runner.run()
55+
runner.run()
56+
57+
assert models.Vulnerability.objects.count() == 9
58+
assert models.VulnerabilityReference.objects.count() == 10
59+
assert models.VulnerabilitySeverity.objects.count() == 9
60+
assert models.PackageRelatedVulnerability.objects.filter(is_vulnerable=False).count() == 16
61+
62+
assert models.Package.objects.count() == 12
63+
64+
self.assert_for_package("Firefox ESR", "mfsa2021-06", "78.7.1")
65+
self.assert_for_package("Firefox ESR", "mfsa2021-04", "78.7", "CVE-2021-23953")
66+
self.assert_for_package("Firefox for Android", "mfsa2021-01", "84.1.3", "CVE-2020-16044")
67+
self.assert_for_package("Thunderbird", "mfsa2014-30", "24.4")
68+
self.assert_for_package("Thunderbird", "mfsa2014-30", "24.4")
69+
self.assert_for_package("Mozilla Suite", "mfsa2005-29", "1.7.6")
70+
71+
def assert_for_package(
72+
self,
73+
package_name,
74+
mfsa_id,
75+
resolved_version,
76+
vulnerability_id=None,
77+
impacted_version=None,
78+
):
79+
vuln = None
80+
81+
pkg = models.Package.objects.get(name=package_name, version=resolved_version)
82+
vuln = pkg.vulnerabilities.first()
83+
84+
if vulnerability_id:
85+
assert vuln.vulnerability_id == vulnerability_id
86+
87+
ref_url = f"https://www.mozilla.org/en-US/security/advisories/{mfsa_id}"
88+
assert models.VulnerabilityReference.objects.get(url=ref_url, vulnerability=vuln)
89+
90+
assert models.PackageRelatedVulnerability.objects.filter(
91+
package=pkg, vulnerability=vuln, is_vulnerable=False
92+
)
93+
94+
95+
def test_categorize_versions_ranges():
96+
# Populate if impacted version is filled
97+
pass

0 commit comments

Comments
 (0)