77# See https://aboutcode.org for more information about nexB OSS projects.
88#
99
10+ import logging
1011import re
12+ from pathlib import Path
13+ from typing import Iterable
1114from typing import List
12- from typing import Set
1315
1416import yaml
1517from bs4 import BeautifulSoup
1618from markdown import markdown
1719from packageurl import PackageURL
20+ from univers .versions import SemverVersion
1821
1922from vulnerabilities import severity_systems
2023from vulnerabilities .importer import AdvisoryData
21- from vulnerabilities .importer import GitImporter
24+ from vulnerabilities .importer import AffectedPackage
25+ from vulnerabilities .importer import Importer
2226from vulnerabilities .importer import Reference
2327from vulnerabilities .importer import VulnerabilitySeverity
2428from vulnerabilities .utils import is_cve
2529from vulnerabilities .utils import split_markdown_front_matter
2630
27- REPOSITORY = "mozilla/foundation-security-advisories"
2831MFSA_FILENAME_RE = re .compile (r"mfsa(\d{4}-\d{2,3})\.(md|yml)$" )
32+ logger = logging .getLogger (__name__ )
2933
3034
31- class MozillaImporter (GitImporter ):
32- def __enter__ (self ):
33- super (MozillaImporter , self ).__enter__ ()
35+ class MozillaImporter (Importer ):
36+ spdx_license_expression = "MPL-2.0"
37+ license_url = "https://github.com/mozilla/foundation-security-advisories/blob/master/LICENSE"
38+ repo_url = "git+https://github.com/mozilla/foundation-security-advisories/"
3439
35- if not getattr (self , "_added_files" , None ):
36- self ._added_files , self ._updated_files = self .file_changes (
37- recursive = True , subdir = "announce"
38- )
39-
40- def updated_advisories (self ) -> Set [AdvisoryData ]:
41- files = self ._updated_files .union (self ._added_files )
42- files = [
43- f for f in files if f .endswith (".md" ) or f .endswith (".yml" )
44- ] # skip irrelevant files
40+ def advisory_data (self ) -> Iterable [AdvisoryData ]:
41+ try :
42+ self .clone (self .repo_url )
43+ path = Path (self .vcs_response .dest_dir )
4544
46- advisories = []
47- for path in files :
48- advisories .extend (to_advisories (path ))
45+ vuln = path / "announce"
46+ paths = list (vuln .glob ("**/*.yml" )) + list (vuln .glob ("**/*.md" ))
47+ for file_path in paths :
48+ yield from to_advisories (file_path )
49+ finally :
50+ if self .vcs_response :
51+ self .vcs_response .delete ()
4952
50- return self .batch_advisories (advisories )
5153
52-
53- def to_advisories (path : str ) -> List [AdvisoryData ]:
54+ def to_advisories (path : Path ) -> List [AdvisoryData ]:
5455 """
5556 Convert a file to corresponding advisories.
5657 This calls proper method to handle yml/md files.
5758 """
59+ path = str (path )
5860 mfsa_id = mfsa_id_from_filename (path )
5961 if not mfsa_id :
6062 return []
6163
6264 with open (path ) as lines :
6365 if path .endswith (".md" ):
64- return get_advisories_from_md (mfsa_id , lines )
66+ yield from get_advisories_from_md (mfsa_id , lines )
6567 if path .endswith (".yml" ):
66- return get_advisories_from_yml (mfsa_id , lines )
68+ yield from get_advisories_from_yml (mfsa_id , lines )
6769
6870 return []
6971
7072
7173def get_advisories_from_yml (mfsa_id , lines ) -> List [AdvisoryData ]:
72- advisories = []
7374 data = yaml .safe_load (lines )
7475 data ["mfsa_id" ] = mfsa_id
7576
76- fixed_package_urls = get_package_urls (data .get ("fixed_in" ))
77+ affected_packages = get_affected_packages (data .get ("fixed_in" ) or [] )
7778 references = get_yml_references (data )
7879
7980 if not data .get ("advisories" ):
@@ -82,47 +83,35 @@ def get_advisories_from_yml(mfsa_id, lines) -> List[AdvisoryData]:
8283 for cve , advisory in data ["advisories" ].items ():
8384 # These may contain HTML tags
8485 summary = BeautifulSoup (advisory .get ("description" , "" ), features = "lxml" ).get_text ()
85-
86- advisories .append (
87- AdvisoryData (
86+ if is_cve (cve ):
87+ yield AdvisoryData (
8888 summary = summary ,
89- vulnerability_id = cve if is_cve (cve ) else "" ,
90- impacted_package_urls = [],
91- resolved_package_urls = fixed_package_urls ,
89+ aliases = [cve ],
9290 references = references ,
91+ affected_packages = list (affected_packages ),
9392 )
94- )
95-
96- return advisories
9793
9894
9995def get_advisories_from_md (mfsa_id , lines ) -> List [AdvisoryData ]:
10096 yamltext , mdtext = split_markdown_front_matter (lines .read ())
10197 data = yaml .safe_load (yamltext )
10298 data ["mfsa_id" ] = mfsa_id
10399
104- fixed_package_urls = get_package_urls (data .get ("fixed_in" ))
100+ affected_packages = get_affected_packages (data .get ("fixed_in" ) or [] )
105101 references = get_yml_references (data )
106102 cves = re .findall (r"CVE-\d+-\d+" , yamltext + mdtext , re .IGNORECASE )
103+ description = html_get_p_under_h3 (markdown (mdtext ), "description" )
107104 for cve in cves :
108- references .append (
109- Reference (
110- reference_id = cve ,
111- url = f"https://cve.mitre.org/cgi-bin/cvename.cgi?name={ cve } " ,
112- )
105+ cve_ref = Reference (
106+ reference_id = cve ,
107+ url = f"https://cve.mitre.org/cgi-bin/cvename.cgi?name={ cve } " ,
113108 )
114-
115- description = html_get_p_under_h3 (markdown (mdtext ), "description" )
116-
117- return [
118- AdvisoryData (
109+ yield AdvisoryData (
119110 summary = description ,
120- vulnerability_id = "" ,
121- impacted_package_urls = [],
122- resolved_package_urls = fixed_package_urls ,
123- references = references ,
111+ aliases = [cve ],
112+ affected_packages = list (affected_packages ),
113+ references = references + [cve_ref ],
124114 )
125- ]
126115
127116
128117def html_get_p_under_h3 (html , h3 : str ):
@@ -146,18 +135,28 @@ def mfsa_id_from_filename(filename):
146135 return None
147136
148137
149- def get_package_urls (pkgs : List [str ]) -> List [PackageURL ]:
150- package_urls = [
151- PackageURL (
152- type = "mozilla" ,
138+ def get_affected_packages (pkgs : List [str ]) -> List [PackageURL ]:
139+ for pkg in pkgs :
140+ if not pkg :
141+ continue
153142 # pkg is of the form "Firefox ESR 1.21" or "Thunderbird 2.21"
154- name = pkg .rsplit (None , 1 )[0 ],
155- version = pkg .rsplit (None , 1 )[1 ],
156- )
157- for pkg in pkgs
158- if pkg
159- ]
160- return package_urls
143+ name , _ , version = pkg .rpartition (" " )
144+ if version and name :
145+ try :
146+ # count no of "." in version
147+ # if 3, then it is not a valid semver version
148+ if version .count ("." ) == 3 :
149+ continue
150+ fixed_version = SemverVersion (version )
151+ yield AffectedPackage (
152+ package = PackageURL (
153+ type = "mozilla" ,
154+ name = name ,
155+ ),
156+ fixed_version = fixed_version ,
157+ )
158+ except Exception :
159+ logger .exception (f"Error parsing version { version } for { name } " )
161160
162161
163162def get_yml_references (data : any ) -> List [Reference ]:
0 commit comments