88#
99
1010import re
11+ import logging
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
34+ class MozillaImporter (Importer ):
35+ spdx_license_expression = "MPL-2.0"
36+ license_url = "https://github.com/mozilla/foundation-security-advisories/blob/master/LICENSE"
37+ repo_url = "git+https://github.com/mozilla/foundation-security-advisories/"
3038
31- class MozillaImporter (GitImporter ):
32- def __enter__ (self ):
33- super (MozillaImporter , self ).__enter__ ()
39+ def advisory_data (self ) -> Iterable [AdvisoryData ]:
40+ try :
41+ self .clone (self .repo_url )
42+ path = Path (self .vcs_response .dest_dir )
3443
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
45-
46- advisories = []
47- for path in files :
48- advisories .extend (to_advisories (path ))
49-
50- return self .batch_advisories (advisories )
44+ vuln = path / "announce"
45+ paths = list (vuln .glob ("**/*.yml" )) + list (vuln .glob ("**/*.md" ))
46+ for file_path in paths :
47+ yield from to_advisories (file_path )
48+ finally :
49+ if self .vcs_response :
50+ self .vcs_response .delete ()
5151
5252
5353def to_advisories (path : str ) -> List [AdvisoryData ]:
5454 """
5555 Convert a file to corresponding advisories.
5656 This calls proper method to handle yml/md files.
5757 """
58+ path = str (path )
5859 mfsa_id = mfsa_id_from_filename (path )
5960 if not mfsa_id :
6061 return []
6162
6263 with open (path ) as lines :
6364 if path .endswith (".md" ):
64- return get_advisories_from_md (mfsa_id , lines )
65+ yield from get_advisories_from_md (mfsa_id , lines )
6566 if path .endswith (".yml" ):
66- return get_advisories_from_yml (mfsa_id , lines )
67+ yield from get_advisories_from_yml (mfsa_id , lines )
6768
6869 return []
6970
7071
7172def get_advisories_from_yml (mfsa_id , lines ) -> List [AdvisoryData ]:
72- advisories = []
7373 data = yaml .safe_load (lines )
7474 data ["mfsa_id" ] = mfsa_id
7575
76- fixed_package_urls = get_package_urls (data .get ("fixed_in" ))
76+ affected_packages = get_affected_packages (data .get ("fixed_in" ) or [] )
7777 references = get_yml_references (data )
7878
7979 if not data .get ("advisories" ):
@@ -82,47 +82,36 @@ def get_advisories_from_yml(mfsa_id, lines) -> List[AdvisoryData]:
8282 for cve , advisory in data ["advisories" ].items ():
8383 # These may contain HTML tags
8484 summary = BeautifulSoup (advisory .get ("description" , "" ), features = "lxml" ).get_text ()
85-
86- advisories . append (
87- AdvisoryData (
85+ alias = is_cve ( cve )
86+ if alias :
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,25 @@ 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+ version = pkg .rsplit (None , 1 )[1 ]
144+ name = pkg .rsplit (None , 1 )[0 ]
145+ if version and name :
146+ try :
147+ fixed_version = SemverVersion (version )
148+ yield AffectedPackage (
149+ package = PackageURL (
150+ type = "mozilla" ,
151+ name = name ,
152+ ),
153+ fixed_version = fixed_version ,
154+ )
155+ except Exception :
156+ logger .exception (f"Error parsing version { version } for { name } " )
161157
162158
163159def get_yml_references (data : any ) -> List [Reference ]:
0 commit comments