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