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 re
11+ import logging
12+ import requests
13+ from bs4 import BeautifulSoup
14+ from packageurl import PackageURL
15+ from univers .version_range import GenericVersionRange
16+ from univers .versions import GenericVersion
17+ from vulnerabilities .importer import AdvisoryData , AffectedPackage , Importer
18+
19+ logging .basicConfig (level = logging .INFO )
20+ logger = logging .getLogger (__name__ )
21+
22+ class HuaweiImporter (Importer ):
23+ root_url = "https://consumer.huawei.com/en/support/bulletin/"
24+ spdx_license_expression = "NOASSERTION"
25+ importer_name = "Huawei Security Bulletin Importer"
26+
27+ def advisory_data (self ):
28+ years_months = [
29+ ('2024' , range (7 , 13 )), # July 2024 to December 2024
30+ ('2025' , range (1 , 2 )) # January 2025
31+ ]
32+ for year , months in years_months :
33+ for month in months :
34+ url = f"{ self .root_url } { year } /{ month } /"
35+ try :
36+ response = requests .get (url )
37+ response .raise_for_status ()
38+ yield from self .to_advisories (response .content , url )
39+ except requests .RequestException as e :
40+ logger .error (f"Failed to fetch URL { url } : { e } " )
41+ continue
42+
43+ def parse_version (self , version_str ):
44+ """Parse version string and separate OS type and version number."""
45+ version_str = version_str .strip ()
46+
47+ harmony_match = re .match (r"HarmonyOS\s*(\d+\.\d+\.\d+)" , version_str )
48+ if harmony_match :
49+ return "harmony" , harmony_match .group (1 )
50+
51+ emui_match = re .match (r"EMUI\s*(\d+\.\d+\.\d+)" , version_str )
52+ if emui_match :
53+ return "emui" , emui_match .group (1 )
54+
55+ return None , None
56+
57+ def group_versions_by_os (self , versions ):
58+ """Group versions by OS type."""
59+ grouped = {
60+ "harmony" : [],
61+ "emui" : []
62+ }
63+
64+ for version in versions :
65+ os_type , version_num = self .parse_version (version )
66+ if os_type and version_num :
67+ grouped [os_type ].append (version_num )
68+ else :
69+ logger .warning (f"Skipping unparseable version: { version } " )
70+
71+ return grouped
72+
73+ def create_affected_packages (self , os_type , versions , fixed = False ):
74+ """Create AffectedPackage objects for a given OS type and versions."""
75+ if not versions :
76+ return []
77+
78+ package = PackageURL (
79+ name = os_type ,
80+ type = "generic" ,
81+ )
82+
83+ if fixed :
84+ return [
85+ AffectedPackage (
86+ package = package ,
87+ fixed_version = GenericVersion (version )
88+ )
89+ for version in versions
90+ ]
91+ else :
92+ return [
93+ AffectedPackage (
94+ package = package ,
95+ affected_version_range = GenericVersionRange .from_versions (versions )
96+ )
97+ ]
98+
99+ def to_advisories (self , content , url ):
100+ soup = BeautifulSoup (content , features = "lxml" )
101+ tables = soup .find_all ('table' )
102+ if len (tables ) < 2 :
103+ logger .warning (f"Expected at least 2 tables, found { len (tables )} at { url } " )
104+ return
105+
106+ affected_table = tables [0 ]
107+ fixed_table = tables [1 ]
108+ cve_data = {}
109+
110+ for row in affected_table .find_all ('tr' ):
111+ cols = row .find_all ('td' )
112+ if len (cols ) >= 5 :
113+ cve_id = cols [0 ].text .strip ()
114+ versions = [v .strip () for v in cols [4 ].text .strip ().split (',' ) if v .strip ()]
115+ grouped_versions = self .group_versions_by_os (versions )
116+
117+ if cve_id not in cve_data :
118+ cve_data [cve_id ] = {
119+ 'affected_versions' : grouped_versions ,
120+ 'fixed_versions' : {'harmony' : [], 'emui' : []}
121+ }
122+ else :
123+ for os_type in grouped_versions :
124+ cve_data [cve_id ]['affected_versions' ][os_type ].extend (grouped_versions [os_type ])
125+
126+ for row in fixed_table .find_all ('tr' ):
127+ cols = row .find_all ('td' )
128+ if len (cols ) >= 3 :
129+ cve_id = cols [0 ].text .strip ()
130+ versions = [v .strip () for v in cols [2 ].text .strip ().split (',' ) if v .strip ()]
131+ grouped_versions = self .group_versions_by_os (versions )
132+
133+ if cve_id not in cve_data :
134+ cve_data [cve_id ] = {
135+ 'affected_versions' : {'harmony' : [], 'emui' : []},
136+ 'fixed_versions' : grouped_versions
137+ }
138+ else :
139+ for os_type in grouped_versions :
140+ cve_data [cve_id ]['fixed_versions' ][os_type ].extend (grouped_versions [os_type ])
141+
142+ for cve_id , data in cve_data .items ():
143+ affected_packages = []
144+
145+ affected_packages .extend (
146+ self .create_affected_packages ('harmony' , data ['affected_versions' ]['harmony' ])
147+ )
148+ affected_packages .extend (
149+ self .create_affected_packages ('harmony' , data ['fixed_versions' ]['harmony' ], fixed = True )
150+ )
151+
152+ affected_packages .extend (
153+ self .create_affected_packages ('emui' , data ['affected_versions' ]['emui' ])
154+ )
155+ affected_packages .extend (
156+ self .create_affected_packages ('emui' , data ['fixed_versions' ]['emui' ], fixed = True )
157+ )
158+
159+ if affected_packages :
160+ yield AdvisoryData (
161+ aliases = [cve_id ],
162+ summary = "" ,
163+ references = [],
164+ affected_packages = affected_packages ,
165+ url = url
166+ )
0 commit comments