Skip to content

Collect Mozilla #393

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Feb 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions SOURCES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,5 @@
+----------------+------------------------------------------------------------------------------------------------------+----------------------------------------------------+
|suse_scores | https://ftp.suse.com/pub/projects/security/yaml/suse-cvss-scores.yaml |vulnerability severity scores by SUSE |
+----------------+------------------------------------------------------------------------------------------------------+----------------------------------------------------+
|mozilla | https://github.com/mozilla/foundation-security-advisories |mozilla |
+----------------+------------------------------------------------------------------------------------------------------+----------------------------------------------------+
1 change: 1 addition & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,6 @@ addopts =
--ignore=vulnerabilities/importers/suse_backports.py
--ignore=vulnerabilities/importers/suse_scores.py
--ignore=vulnerabilities/importers/ubuntu_usn.py
--ignore=vulnerabilities/importers/mozilla.py
--ignore=vulnerabilities/management/commands/create_cpe_to_purl_map.py
--ignore=vulnerabilities/lib_oval.py
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,5 @@ lxml>=4.6.4
gunicorn>=20.1.0
django-environ==0.4.5
defusedxml==0.7.1

Markdown==3.3.4
35 changes: 35 additions & 0 deletions vulnerabilities/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,41 @@ def fetch_yaml(url):
create_etag = MagicMock()


def split_markdown_front_matter(lines: str) -> Tuple[str, str]:
"""
This function splits lines into markdown front matter and the markdown body
and returns list of lines for both

for example :
lines =
---
title: ISTIO-SECURITY-2019-001
description: Incorrect access control.
cves: [CVE-2019-12243]
---
# Markdown starts here

split_markdown_front_matter(lines) would return
['title: ISTIO-SECURITY-2019-001','description: Incorrect access control.'
,'cves: [CVE-2019-12243]'],
["# Markdown starts here"]
"""

fmlines = []
mdlines = []
splitter = mdlines

for index, line in enumerate(lines.split("\n")):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about this instead?

import saneyaml

# normalize line endings just in case:
text = text.replace("\r\n", "\n")
front_matter, _, body = text.rpartition("\n---\n")
front_matter = saneyaml.load(front_matter)

For instance:

>>> import saneyaml
>>> text = """---
... title: ISTIO-SECURITY-2019-001
... subtitle: 安全公告
... description: 错误的权限控制。
... cve: [CVE-2019-12243]
... publishdate: 2019-05-28
... keywords: [CVE]
... skip_seealso: true
... aliases:
...     - /zh/blog/2019/cve-2019-12243
...     - /zh/news/2019/cve-2019-12243
... ---
... 
... {{< security_bulletin
...         cves="CVE-2019-12243"
...         cvss="8.9"
...         vector="CVSS:3.0/AV:A/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:N/E:H/RL:O/RC:C"
...         releases="1.1 to 1.1.6" >}}
... 
... ## 内容{#context}
... """
>>> text = text.replace("\r\n", "\n")
>>> front_matter, _, body = text.rpartition("\n---\n")
>>> front_matter = saneyaml.load(front_matter)
>>> print(front_matter)
{'title': 'ISTIO-SECURITY-2019-001', 'subtitle': '安全公告', 'description': '错误的权限控制。', 'cve': ['CVE-2019-12243'], 'publishda
te': '2019-05-28', 'keywords': ['CVE'], 'skip_seealso': True, 'aliases': ['/zh/blog/2019/cve-2019-12243', '/zh/news/2019/cve-2019-122
43']}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have not been using saneyaml anywhere in the project. If that is preferred, I can add it to requirements and we can start using that but I don't see any specific reason to do so here.
Regarding rpartition. We cannot have that either as a markdown is allowed to have a "\n---"\n.
Consider this

>>> text=""""---
... announced: February 4, 2014
... fixed_in:
... - Firefox 27
... - Firefox ESR 24.3
... - Thunderbird 24.3
... - Seamonkey 2.24
... impact: High
... reporter: Cody Crews
... title: Clone protected content with XBL scopes
... ---
... 
... <h3>Description</h3>
... 
... Text
... ---
... other text
... """
>>> text = text.replace("\r\n", "\n")
>>> front_matter, _, body = text.rpartition("\n---\n")
>>> 
>>> front_matter
'"---\nannounced: February 4, 2014\nfixed_in:\n- Firefox 27\n- Firefox ESR 24.3\n- Thunderbird 24.3\n- Seamonkey 2.24\nimpact: High\nreporter: Cody Crews\ntitle: Clone protected content with XBL scopes\n---\n\n<h3>Description</h3>\n\nText'
>>> 
>>> body
'other text\n'
>>> 

The official validator by mozilla parses it something like this
https://github.com/mozilla/foundation-security-advisories/blob/d43d09d204ab5da014e83b7d1743df289cefee92/check_advisories.py#L183-L208

Further, as it is a helper, we cannot have it mozilla specific. All it should do is to split the front matter and markdown.
I could have something like this

import yaml

text="""---
announced: February 4, 2014
fixed_in:
- Firefox 27
- Firefox ESR 24.3
- Thunderbird 24.3
- Seamonkey 2.24
impact: High
reporter: Cody Crews
title: Clone protected content with XBL scopes
---

<h3>Description</h3>
---
other text
"""
# normalize line endings just in case:
text = text.replace("\r\n", "\n")
linezero,_, text = text.partition("---\n")
if not linezero: # nothing before first ---
    front_matter,_, body = text.partition("---")
    front_matter = yaml.safe_load(front_matter)
else:
    front_matter = ""
    body = linezero + "---\n" + text
print(front_matter)
print(body)

which prints

{'announced': 'February 4, 2014', 'fixed_in': ['Firefox 27', 'Firefox ESR 24.3', 'Thunderbird 24.3', 'Seamonkey 2.24'], 'impact': 'High', 'reporter': 'Cody Crews', 'title': 'Clone protected content with XBL scopes'}


<h3>Description</h3>
---
other text

but doesn't look any better to me.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but doesn't look any better to me.

It feels a bit more readable may be?

The official validator by mozilla parses it something like this
https://github.com/mozilla/foundation-security-advisories/blob/d43d09d204ab5da014e83b7d1743df289cefee92/check_advisories.py#L183-L208

We could very much reuse it as-is as well. This is under an MPL license so we would have to do it in an orderly fashion with proper license tracking and code separation though.
If we do not copy it, we cannot reuse code from it though.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As it is required by multiple importers, let's move this to it's own PR. Opened #443

if index == 0 and line.strip().startswith("---"):
splitter = fmlines
elif line.strip().startswith("---"):
splitter = mdlines
else:
splitter.append(line)

return "\n".join(fmlines), "\n".join(mdlines)


def contains_alpha(string):
"""
Return True if the input 'string' contains any alphabet
Expand Down
180 changes: 180 additions & 0 deletions vulnerabilities/importers/mozilla.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import re
from typing import List
from typing import Set

import yaml
from bs4 import BeautifulSoup
from markdown import markdown
from packageurl import PackageURL

from vulnerabilities.importer import Advisory
from vulnerabilities.importer import GitImporter
from vulnerabilities.importer import Reference
from vulnerabilities.importer import VulnerabilitySeverity
from vulnerabilities.helpers import is_cve
from vulnerabilities.helpers import split_markdown_front_matter
from vulnerabilities.severity_systems import SCORING_SYSTEMS

REPOSITORY = "mozilla/foundation-security-advisories"
MFSA_FILENAME_RE = re.compile(r"mfsa(\d{4}-\d{2,3})\.(md|yml)$")


class MozillaImporter(GitImporter):
def __enter__(self):
super(MozillaImporter, self).__enter__()

if not getattr(self, "_added_files", None):
self._added_files, self._updated_files = self.file_changes(
recursive=True, subdir="announce"
)

def updated_advisories(self) -> Set[Advisory]:
files = self._updated_files.union(self._added_files)
files = [
f for f in files if f.endswith(".md") or f.endswith(".yml")
] # skip irrelevant files

advisories = []
for path in files:
advisories.extend(to_advisories(path))

return self.batch_advisories(advisories)


def to_advisories(path: str) -> List[Advisory]:
"""
Convert a file to corresponding advisories.
This calls proper method to handle yml/md files.
"""
mfsa_id = mfsa_id_from_filename(path)
if not mfsa_id:
return []

with open(path) as lines:
if path.endswith(".md"):
return get_advisories_from_md(mfsa_id, lines)
if path.endswith(".yml"):
return get_advisories_from_yml(mfsa_id, lines)

return []


def get_advisories_from_yml(mfsa_id, lines) -> List[Advisory]:
advisories = []
data = yaml.safe_load(lines)
data["mfsa_id"] = mfsa_id

fixed_package_urls = get_package_urls(data.get("fixed_in"))
references = get_yml_references(data)

if not data.get("advisories"):
return []

for cve, advisory in data["advisories"].items():
# These may contain HTML tags
summary = BeautifulSoup(advisory.get("description", ""), features="lxml").get_text()

advisories.append(
Advisory(
summary=summary,
vulnerability_id=cve if is_cve(cve) else "",
impacted_package_urls=[],
resolved_package_urls=fixed_package_urls,
references=references,
)
)

return advisories


def get_advisories_from_md(mfsa_id, lines) -> List[Advisory]:
yamltext, mdtext = split_markdown_front_matter(lines.read())
data = yaml.safe_load(yamltext)
data["mfsa_id"] = mfsa_id

fixed_package_urls = get_package_urls(data.get("fixed_in"))
references = get_yml_references(data)
cves = re.findall(r"CVE-\d+-\d+", yamltext + mdtext, re.IGNORECASE)
for cve in cves:
references.append(
Reference(
reference_id=cve,
url=f"https://cve.mitre.org/cgi-bin/cvename.cgi?name={cve}",
)
)

description = html_get_p_under_h3(markdown(mdtext), "description")

return [
Advisory(
summary=description,
vulnerability_id="",
impacted_package_urls=[],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How can you get no impacted packages and fixed packages?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Upstream doesn't provide with a list of impacted package. We might consider all packages until this package as impacted but I'm not sure about this. There are many other importers that do this too. Eg: https://github.com/nexB/vulnerablecode/blob/f254b0d4ac54b70c648055a7e8eda16c05dce0f9/vulnerabilities/importers/alpine_linux.py#L194

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point. We need a ticket though, as this may need to be interpreted as "all versions before this version are vulnerable" and it warrant some research and discussions.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ouch, is this going to be redundant now, just like the suse_backport importer. We now have a improved notion of fixed/resolved/patched package now. Due to #436

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sbs2001 That is scary. I've opened #449 to discuss this further.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like to merge this sooner than later. ... See also #449 (comment)

resolved_package_urls=fixed_package_urls,
references=references,
)
]


def html_get_p_under_h3(html, h3: str):
soup = BeautifulSoup(html, features="lxml")
h3tag = soup.find("h3", text=lambda txt: txt.lower() == h3)
p = ""
if h3tag:
for tag in h3tag.next_siblings:
if tag.name:
if tag.name != "p":
break
p += tag.get_text()
return p


def mfsa_id_from_filename(filename):
match = MFSA_FILENAME_RE.search(filename)
if match:
return "mfsa" + match.group(1)

return None


def get_package_urls(pkgs: List[str]) -> List[PackageURL]:
package_urls = [
PackageURL(
type="mozilla",
# pkg is of the form "Firefox ESR 1.21" or "Thunderbird 2.21"
name=pkg.rsplit(None, 1)[0],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this split and the one below are rather cryptic, magic and likely error prone, please use a plain loop rather than a list comprehension and try to find a more readable and explicit way to do things. partition and rpartition are usually more robust and easier to grok.

Also avoid doing the multiple time the same op (here for the name and version)

Copy link
Collaborator Author

@Hritik14 Hritik14 Apr 5, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated it to use a for loop. I've used rsplit to be consistent with the upstream.
Here's what the upstream uses: https://github.com/mozilla/bedrock/blob/bd0f50a1c0a7455115cc2bf9eb3ae9027586a024/bedrock/security/models.py#L28-L30
It is mentioned in the commit message here 05d93b2

This is how it looks now:

def get_package_urls(pkgs: List[str]) -> List[PackageURL]:
    for pkg in pkgs:
        if pkg:
            # pkg is of the form "Firefox ESR 1.21" or "Thunderbird 2.21"
            name, version = pkg.rsplit(None, 1)
            package_urls = [
                PackageURL(
                    type="mozilla",
                    name=name,
                    version=version,
                    )
                ]
    return package_urls

Copy link
Member

@pombredanne pombredanne Apr 8, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rpartition is a more robust splitter than rsplit. Your rsplit will fail when there are no spaces

>>> a, b = 'foo'.rsplit(None, 1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: not enough values to unpack (expected 2, got 1)
>>> a, _, b = 'foo'.rpartition(' ')

Copy link
Collaborator Author

@Hritik14 Hritik14 Apr 15, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there's no space, rpartition puts the entire string on b which would be version here. imo, if pkg="Mozilla" it refers to the name and not the version.
To account for that, I came up with this. Not sure if it looks hacky.

name, _, version = pkg.rpartition(' ')
if not name:
    name,version = version,name

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this works fine. Or you could do this slightly more explicit:

>>> def name_ver(s):
...     if ' ' in s:
...         name, _, ver = s.rpartition(' ')
...     else:
...         name = s
...         ver = None
...     return name, ver
... 
>>> name_ver('foo')
('foo', None)
>>> name_ver('foo 23.34')
('foo', '23.34')

version=pkg.rsplit(None, 1)[1],
)
for pkg in pkgs
if pkg
]
return package_urls


def get_yml_references(data: any) -> List[Reference]:
"""
Returns a list of references
Currently only considers the given mfsa as a reference
"""
# FIXME: Needs improvement
# Should we add 'bugs' section in references too?
# Should we add 'impact'/severity of CVE in references too?
# If yes, then fix alpine_linux importer as well
# Otherwise, do we need severity field for adversary as well?

severities = ["critical", "high", "medium", "low", "none"]
severity = "none"
if data.get("impact"):
impact = data.get("impact").lower()
for s in severities:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure I get why you do things this way. What would be data examples?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are all unique impacts

mfsa2020-25:  high
mfsa2013-50:  Critical
mfsa2009-19:  High
mfsa2017-17:  critical
mfsa2009-15:  Low
mfsa2015-75:  Moderate
mfsa2020-07:  moderate
mfsa2007-06:  Critical (Firefox 2.0 not affected in default configuration)
mfsa2015-21:  Medium
mfsa2005-02:  Moderate (on a multiuser computer)
mfsa2005-28:  Critical (local)
mfsa2005-48:  Low (High for Mozilla Suite)
mfsa2005-59:  Severe
mfsa2010-55:  Low (Critical in Gecko 1.9.1 and earlier)
mfsa2005-10:  Moderate to Critical

I did that to make look like cvssv3.1_qr but now as we have generic_textual (#415) as well, perhaps I can just put the entire impact line over there.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just noticed that entries like Critical (Firefox 2.0 not affected in default configuration) exceed maximum value length.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is at best an artistic scale.... I would like to see what it looks like for recent values, say no less than 5 years old as this will weed out old things like FF 2.0
For the funky values I would rather have an explicit hardcoded mapping....and since they document it, we should IMHO create a Mozilla scoring system
@sbs2001 what's your take?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's create a scoring system for mozilla using these values:

critical
severe
high
medium
moderate
low

in https://github.com/nexB/vulnerablecode/blob/main/vulnerabilities/severity_systems.py

if s in impact:
severity = s
break

return [
Reference(
reference_id=data["mfsa_id"],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Create a variable for data["mfsa_id"] and reuse it. Do not get multiple times.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added the mfsa_id in data to make sure everything we need about the advisory is at one place, inside data.
https://github.com/nexB/vulnerablecode/blob/5997b26ba35d3cff83c07ee6f6fd2c81b4f526ae/vulnerabilities/importers/mozilla.py#L65

Is there something wrong with this approach?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why pass down a mapping when you can pass named arguments?
It is much better to give names.
Something such as:

def get_reference(mfsa_id, impact):
    """
    Return a reference for tan mfsa and is impact value.
    """
    known_impacts = set(["critical", "high", "medium", "low", "none"])

    impact = impact or ''
    impact = impact.lower()
    severity = known_impacts.get(impact)

    severities=[]
    if severity:
        severities = [VulnerabilitySeverity(scoring_systems["generic_textual"], severity)]

    return Reference(
        reference_id=mfsa_id,
        url=f"https://www.mozilla.org/en-US/security/advisories/{mfsa_id}",
        severities=severities,
    )

Also do not return a list when you return a single value. I appreciate planning for the future, but that's usually not needed. We can always refactor later if needed.

And add other impacts that may be funky to known_impacts that you can turn in a mapping.
I would also raise an exception or some warning if there is an impact that is not known and is not in the mapping.

Copy link
Collaborator Author

@Hritik14 Hritik14 Apr 15, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks.
I've modified it to look like this

def get_yml_references(mfsa_id, impact, advisories) -> List[Reference]:
    """
    Return a list of references
    Considers the following as references:
        - mfsa
        - cve
        - bugzilla or direct reference
    """

    known_impacts = { 
            "severe": "critical",
            "critical": "critical",
            "critical (local)": "critical",
            "high": "high",
            "moderate": "moderate",
            "medium": "moderate",
            "moderate (on a multiuser computer)": "moderate",
            "low": "low",
            "critical (firefox 2.0 not affected in default configuration)": "critical",
            "low (critical in gecko 1.9.1 and earlier)": "low",
            "moderate to critical": "moderate",
            }


    impact = impact or ''
    impact = impact.lower()
    severity = known_impacts.get(impact)
    severities = []
    if severity:
        severities=[VulnerabilitySeverity(scoring_systems["generic_textual"], severity)]
    else:
        warnings.warn(f'{mfsa_id}: unknown severity "{severity}"')

    # Add mfsa as reference
    references = [
            Reference(
                reference_id=mfsa_id,
                url=f"https://www.mozilla.org/en-US/security/advisories/{mfsa_id}",
                severities=severities,
                )
            ]

    advisories = advisories or {}
    for advisory in advisories:
        # Add advisory cve as reference
        if is_cve(advisory):
            references.append(
                    Reference(
                        reference_id=advisory,
                        url=f"https://cve.mitre.org/cgi-bin/cvename.cgi?name={advisory}",
                        )
                    )

        advisory_data = advisories[advisory]
        # Some description contain cve reference too, eg mfsa2017-12, mfsa2019-01. Add them
        # FIXME: Replace after https://github.com/nexB/vulnerablecode/pull/439 is merged
        cves = set(re.findall(r"CVE-\d+-\d+", advisory_data["description"]))
        for cve in cves:
            references.append(
                    Reference(
                        reference_id=cve,
                        url=f"https://cve.mitre.org/cgi-bin/cvename.cgi?name={cve}",
                        )
                    )


        # Add "bugs" as reference
        bugs = advisory_data.get("bugs", {})
        for bug in bugs:
            # bug_ids could be a single int like 1584216 or a str like 1584216,1584218
            urls = str(bug.get("url", "")).split(",")
            for url in urls:
                url = url.strip()
                if url[:4].lower() == "http":
                    references.append(Reference(url=url))
                else:
                    references.append(
                        Reference(
                            reference_id=url,
                            url=f"https://bugzilla.mozilla.org/show_bug.cgi?id={url}",
                        )
                    )
    return references

I would be needing the references list as now I've also added the bugs and cve itself as a reference.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you pushed these updates?

url="https://www.mozilla.org/en-US/security/advisories/{}".format(data["mfsa_id"]),
severities=[VulnerabilitySeverity(scoring_systems["generic_textual"], severity)],
)
]
1 change: 1 addition & 0 deletions vulnerabilities/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,5 @@ def no_rmtree(monkeypatch):
"test_importer_yielder.py",
"test_upstream.py",
"test_istio.py",
"test_mozilla.py",
]
Binary file added vulnerabilities/tests/test_data/mozilla.zip
Binary file not shown.
97 changes: 97 additions & 0 deletions vulnerabilities/tests/test_mozilla.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import os
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add add some true unit tests too for each of your functions?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK. Replacing these with true unit tests

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant add unit tests, not replace these entirely. Both are useful

import shutil
import tempfile
import zipfile
from unittest.mock import patch

from django.test import TestCase

from vulnerabilities import models
from vulnerabilities.import_runner import ImportRunner
from vulnerabilities.importers.npm import categorize_versions

BASE_DIR = os.path.dirname(os.path.abspath(__file__))
TEST_DATA = os.path.join(BASE_DIR, "test_data/")


@patch("vulnerabilities.importers.MozillaImporter._update_from_remote")
class MozillaImportTest(TestCase):

tempdir = None

@classmethod
def setUpClass(cls) -> None:
cls.tempdir = tempfile.mkdtemp()
zip_path = os.path.join(TEST_DATA, "mozilla.zip")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please avoid zip fixtures unless really needed, e.g. extremely rarely if ever

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was inspired by npm and rust test cases. Removing it now.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we have zip in npm and rust we need to replace them too then... Do you mind to create a ticket for this>

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Created #442

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Hritik14 npm and rust are for git ... yours are just plain text, so please do not zip here.


with zipfile.ZipFile(zip_path, "r") as zip_ref:
zip_ref.extractall(cls.tempdir)

cls.importer = models.Importer.objects.create(
name="mozilla_unittests",
license="",
last_run=None,
data_source="MozillaImporter",
data_source_cfg={
"repository_url": "https://example.git",
"working_directory": os.path.join(cls.tempdir, "mozilla_test"),
"create_working_directory": False,
"remove_working_directory": False,
},
)

@classmethod
def tearDownClass(cls) -> None:
# Make sure no requests for unexpected package names have been made during the tests.
shutil.rmtree(cls.tempdir)

def test_import(self, _):
runner = ImportRunner(self.importer, 100)

# Remove if we don't need set_api in MozillaImporter
# with patch("vulnerabilities.importers.MozillaImporter.versions", new=MOCK_VERSION_API):
# with patch("vulnerabilities.importers.MozillaImporter.set_api"):
# runner.run()
runner.run()

assert models.Vulnerability.objects.count() == 9
assert models.VulnerabilityReference.objects.count() == 10
assert models.VulnerabilitySeverity.objects.count() == 9
assert models.PackageRelatedVulnerability.objects.filter(is_vulnerable=False).count() == 16

assert models.Package.objects.count() == 12

self.assert_for_package("Firefox ESR", "mfsa2021-06", "78.7.1")
self.assert_for_package("Firefox ESR", "mfsa2021-04", "78.7", "CVE-2021-23953")
self.assert_for_package("Firefox for Android", "mfsa2021-01", "84.1.3", "CVE-2020-16044")
self.assert_for_package("Thunderbird", "mfsa2014-30", "24.4")
self.assert_for_package("Thunderbird", "mfsa2014-30", "24.4")
self.assert_for_package("Mozilla Suite", "mfsa2005-29", "1.7.6")

def assert_for_package(
self,
package_name,
mfsa_id,
resolved_version,
vulnerability_id=None,
impacted_version=None,
):
vuln = None

pkg = models.Package.objects.get(name=package_name, version=resolved_version)
vuln = pkg.vulnerabilities.first()

if vulnerability_id:
assert vuln.vulnerability_id == vulnerability_id

ref_url = f"https://www.mozilla.org/en-US/security/advisories/{mfsa_id}"
assert models.VulnerabilityReference.objects.get(url=ref_url, vulnerability=vuln)

assert models.PackageRelatedVulnerability.objects.filter(
package=pkg, vulnerability=vuln, is_vulnerable=False
)


def test_categorize_versions_ranges():
# Populate if impacted version is filled
pass