Skip to content

Commit 34d5be7

Browse files
committed
Add golang importer pipeline
Signed-off-by: Tushar Goel <tushar.goel.dav@gmail.com>
1 parent 8af2bd3 commit 34d5be7

9 files changed

Lines changed: 1144 additions & 14 deletions

File tree

‎vulnerabilities/importers/__init__.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@
2626
from vulnerabilities.pipelines.v2_importers import github_osv_importer as github_osv_importer_v2
2727
from vulnerabilities.pipelines.v2_importers import gitlab_importer as gitlab_importer_v2
2828
from vulnerabilities.pipelines.v2_importers import glibc_importer as glibc_importer_v2
29+
from vulnerabilities.pipelines.v2_importers import (
30+
golang_vulndb_importer as golang_vulndb_importer_v2,
31+
)
2932
from vulnerabilities.pipelines.v2_importers import istio_importer as istio_importer_v2
3033
from vulnerabilities.pipelines.v2_importers import linux_kernel_importer as linux_kernel_importer_v2
3134
from vulnerabilities.pipelines.v2_importers import mattermost_importer as mattermost_importer_v2
@@ -64,6 +67,7 @@
6467
apache_httpd_v2.ApacheHTTPDImporterPipeline,
6568
pypa_importer_v2.PyPaImporterPipeline,
6669
gitlab_importer_v2.GitLabImporterPipeline,
70+
golang_vulndb_importer_v2.GolangVulndbImporterPipeline,
6771
pysec_importer_v2.PyPIImporterPipeline,
6872
xen_importer_v2.XenImporterPipeline,
6973
curl_importer_v2.CurlImporterPipeline,
Lines changed: 362 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,362 @@
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+
from pathlib import Path
12+
from typing import Iterable
13+
from typing import List
14+
from typing import Optional
15+
from typing import Tuple
16+
17+
import dateparser
18+
import saneyaml
19+
from fetchcode.vcs import fetch_via_vcs
20+
from packageurl import PackageURL
21+
from univers.version_constraint import VersionConstraint
22+
from univers.version_range import GolangVersionRange
23+
from univers.versions import InvalidVersion
24+
25+
from vulnerabilities.importer import AdvisoryDataV2
26+
from vulnerabilities.importer import AffectedPackageV2
27+
from vulnerabilities.importer import ReferenceV2
28+
from vulnerabilities.pipelines import VulnerableCodeBaseImporterPipelineV2
29+
from vulnerabilities.pipes.osv_v2 import parse_advisory_data_v3
30+
from vulnerabilities.utils import build_description
31+
from vulnerabilities.utils import get_advisory_url
32+
from vulnerabilities.utils import load_json
33+
34+
CWE_ID_RE = re.compile(r"CWE-(\d+)", re.IGNORECASE)
35+
36+
37+
class GolangVulndbImporterPipeline(VulnerableCodeBaseImporterPipelineV2):
38+
"""
39+
Go Vulnerability Database importer pipeline.
40+
41+
Clone https://github.com/golang/vulndb and import:
42+
- OSV JSON from data/osv
43+
- YAML reports from data/reports
44+
- CVE 5.0 records from data/cve/v5
45+
"""
46+
47+
pipeline_id = "golang_vulndb_importer_v2"
48+
datasource_id = "golang_vulndb"
49+
spdx_license_expression = "CC-BY-4.0"
50+
license_url = "https://github.com/golang/vulndb/blob/master/LICENSE"
51+
repo_url = "git+https://github.com/golang/vulndb"
52+
github_blob_base_url = "https://github.com/golang/vulndb/blob/master/"
53+
54+
precedence = 500
55+
56+
@classmethod
57+
def steps(cls):
58+
return (
59+
cls.clone,
60+
cls.collect_and_store_advisories,
61+
cls.clean_downloads,
62+
)
63+
64+
def clone(self):
65+
self.log(f"Cloning `{self.repo_url}`")
66+
self.vcs_response = fetch_via_vcs(self.repo_url)
67+
68+
def _repo_root(self) -> Path:
69+
return Path(self.vcs_response.dest_dir)
70+
71+
def _osv_dir(self) -> Path:
72+
return self._repo_root() / "data" / "osv"
73+
74+
def _reports_dir(self) -> Path:
75+
return self._repo_root() / "data" / "reports"
76+
77+
def _cve_v5_dir(self) -> Path:
78+
return self._repo_root() / "data" / "cve" / "v5"
79+
80+
def advisories_count(self):
81+
return (
82+
sum(1 for _ in self._osv_dir().glob("*.json"))
83+
+ sum(1 for _ in self._reports_dir().glob("*.yaml"))
84+
+ sum(1 for _ in self._cve_v5_dir().glob("*.json"))
85+
)
86+
87+
def collect_advisories(self) -> Iterable[AdvisoryDataV2]:
88+
for advisory in self._osv_dir().glob("*.json"):
89+
yield self.to_advisory_data_from_osv(advisory)
90+
91+
for advisory in self._reports_dir().glob("*.yaml"):
92+
yield self.to_advisory_data_from_report(advisory)
93+
94+
for advisory in self._cve_v5_dir().glob("*.json"):
95+
yield self.to_advisory_data_from_cve5(advisory)
96+
97+
def to_advisory_data(self, file: Path) -> Optional[AdvisoryDataV2]:
98+
return self.to_advisory_data_from_osv(file)
99+
100+
def _advisory_url(self, file: Path, data: Optional[dict] = None) -> str:
101+
advisory_url = (data or {}).get("database_specific", {}).get("url") if data else None
102+
if advisory_url:
103+
return advisory_url
104+
return get_advisory_url(
105+
file=file,
106+
base_path=self._repo_root(),
107+
url=self.github_blob_base_url,
108+
)
109+
110+
def to_advisory_data_from_osv(self, file: Path) -> Optional[AdvisoryDataV2]:
111+
data = load_json(file)
112+
advisory_id = data.get("id")
113+
if not advisory_id:
114+
self.log(f"Advisory ID not found in {file}")
115+
return
116+
117+
advisory = parse_advisory_data_v3(
118+
raw_data=data,
119+
supported_ecosystems=["golang"],
120+
advisory_url=self._advisory_url(file, data),
121+
advisory_text=file.read_text(),
122+
advisory_id=advisory_id,
123+
)
124+
if not advisory:
125+
return
126+
127+
# Generated OSV files sometimes use the zero date as a placeholder.
128+
if advisory.date_published and advisory.date_published.year < 1970:
129+
advisory.date_published = None
130+
return advisory
131+
132+
def to_advisory_data_from_report(self, file: Path) -> Optional[AdvisoryDataV2]:
133+
advisory_text = file.read_text()
134+
data = saneyaml.load(advisory_text) or {}
135+
advisory_id = data.get("id")
136+
if not advisory_id:
137+
self.log(f"Advisory ID not found in {file}")
138+
return
139+
140+
aliases = []
141+
aliases.extend(data.get("cves") or [])
142+
aliases.extend(data.get("ghsas") or [])
143+
cve_metadata = data.get("cve_metadata") or {}
144+
if cve_id := cve_metadata.get("id"):
145+
aliases.append(cve_id)
146+
aliases = [alias for alias in aliases if alias and alias != advisory_id]
147+
148+
references = []
149+
for ref in data.get("references") or []:
150+
if isinstance(ref, dict):
151+
for url in ref.values():
152+
if url:
153+
references.append(ReferenceV2(url=url))
154+
elif isinstance(ref, str) and ref:
155+
references.append(ReferenceV2(url=ref))
156+
157+
pkg_go_dev_url = f"https://pkg.go.dev/vuln/{advisory_id}"
158+
if pkg_go_dev_url not in [ref.url for ref in references]:
159+
references.append(ReferenceV2(reference_id=advisory_id, url=pkg_go_dev_url))
160+
161+
weaknesses = extract_cwe_ids(cve_metadata.get("cwe") or "")
162+
date_published = parse_go_date(data.get("published"))
163+
164+
return AdvisoryDataV2(
165+
advisory_id=advisory_id,
166+
aliases=aliases,
167+
summary=build_description(
168+
summary=data.get("summary") or "",
169+
description=data.get("description") or "",
170+
),
171+
affected_packages=affected_packages_from_report(data.get("modules") or []),
172+
references=references,
173+
date_published=date_published,
174+
weaknesses=weaknesses,
175+
url=self._advisory_url(file),
176+
original_advisory_text=advisory_text,
177+
)
178+
179+
def to_advisory_data_from_cve5(self, file: Path) -> Optional[AdvisoryDataV2]:
180+
data = load_json(file)
181+
cve_metadata = data.get("cveMetadata") or {}
182+
cve_id = cve_metadata.get("cveId")
183+
go_id = file.stem
184+
advisory_id = cve_id or go_id
185+
if not advisory_id:
186+
self.log(f"Advisory ID not found in {file}")
187+
return
188+
189+
cna = (data.get("containers") or {}).get("cna") or {}
190+
summary = ""
191+
for description in cna.get("descriptions") or []:
192+
if description.get("lang") in ("en", "en-US"):
193+
summary = description.get("value") or ""
194+
break
195+
if not summary:
196+
summary = cna.get("title") or ""
197+
198+
aliases = []
199+
if go_id and go_id != advisory_id:
200+
aliases.append(go_id)
201+
202+
references = []
203+
for ref in cna.get("references") or []:
204+
url = ref.get("url") if isinstance(ref, dict) else ref
205+
if url:
206+
references.append(ReferenceV2(url=url))
207+
208+
weaknesses = []
209+
for problem_type in cna.get("problemTypes") or []:
210+
for description in problem_type.get("descriptions") or []:
211+
weaknesses.extend(extract_cwe_ids(description.get("cweId") or ""))
212+
weaknesses.extend(extract_cwe_ids(description.get("description") or ""))
213+
weaknesses = sorted(set(weaknesses))
214+
215+
date_published = parse_go_date(cve_metadata.get("datePublished"))
216+
217+
return AdvisoryDataV2(
218+
advisory_id=advisory_id,
219+
aliases=aliases,
220+
summary=summary,
221+
affected_packages=affected_packages_from_cve5(cna.get("affected") or []),
222+
references=references,
223+
date_published=date_published,
224+
weaknesses=weaknesses,
225+
url=self._advisory_url(file),
226+
original_advisory_text=file.read_text(),
227+
)
228+
229+
def clean_downloads(self):
230+
if self.vcs_response:
231+
self.log(f"Removing cloned repository")
232+
self.vcs_response.delete()
233+
234+
def on_failure(self):
235+
self.clean_downloads()
236+
237+
238+
def parse_go_date(value):
239+
if not value or not isinstance(value, str):
240+
return None
241+
parsed = dateparser.parse(value)
242+
if parsed and parsed.year < 1970:
243+
return None
244+
return parsed
245+
246+
247+
def extract_cwe_ids(value: str) -> List[int]:
248+
if not value:
249+
return []
250+
return [int(match) for match in CWE_ID_RE.findall(value)]
251+
252+
253+
def golang_module_to_purl(module_path: str) -> PackageURL:
254+
if module_path == "std":
255+
module_path = "stdlib"
256+
purl = PackageURL(type="golang", name=module_path)
257+
return PackageURL.from_string(str(purl))
258+
259+
260+
def golang_ranges_from_constraints(
261+
affected_constraints: List[VersionConstraint],
262+
fixed_constraints: List[VersionConstraint],
263+
) -> Tuple[Optional[GolangVersionRange], Optional[GolangVersionRange]]:
264+
affected_range = None
265+
fixed_range = None
266+
if affected_constraints:
267+
affected_range = GolangVersionRange(
268+
constraints=VersionConstraint.simplify(affected_constraints)
269+
)
270+
if fixed_constraints:
271+
fixed_range = GolangVersionRange(constraints=VersionConstraint.simplify(fixed_constraints))
272+
return affected_range, fixed_range
273+
274+
275+
def semver_constraint(comparator: str, version: str) -> Optional[VersionConstraint]:
276+
if not version or version in ("0", "*"):
277+
return None
278+
try:
279+
return VersionConstraint(
280+
comparator=comparator, version=GolangVersionRange.version_class(version)
281+
)
282+
except (InvalidVersion, ValueError, TypeError):
283+
return None
284+
285+
286+
def affected_packages_from_report(modules: List[dict]) -> List[AffectedPackageV2]:
287+
affected_packages = []
288+
for module in modules:
289+
module_path = module.get("module")
290+
if not module_path:
291+
continue
292+
293+
affected_constraints = []
294+
fixed_constraints = []
295+
for version in module.get("versions") or []:
296+
introduced = semver_constraint(">=", version.get("introduced"))
297+
if introduced:
298+
affected_constraints.append(introduced)
299+
fixed = version.get("fixed")
300+
if fixed_lt := semver_constraint("<", fixed):
301+
affected_constraints.append(fixed_lt)
302+
if fixed_eq := semver_constraint("=", fixed):
303+
fixed_constraints.append(fixed_eq)
304+
305+
affected_range, fixed_range = golang_ranges_from_constraints(
306+
affected_constraints, fixed_constraints
307+
)
308+
if not affected_range and not fixed_range:
309+
continue
310+
311+
try:
312+
affected_packages.append(
313+
AffectedPackageV2(
314+
package=golang_module_to_purl(module_path),
315+
affected_version_range=affected_range,
316+
fixed_version_range=fixed_range,
317+
)
318+
)
319+
except Exception:
320+
continue
321+
return affected_packages
322+
323+
324+
def affected_packages_from_cve5(affected: List[dict]) -> List[AffectedPackageV2]:
325+
affected_packages = []
326+
for item in affected:
327+
module_path = item.get("packageName") or item.get("product")
328+
if not module_path:
329+
continue
330+
331+
affected_constraints = []
332+
fixed_constraints = []
333+
for version in item.get("versions") or []:
334+
if version.get("status") not in (None, "affected"):
335+
continue
336+
introduced = semver_constraint(">=", version.get("version"))
337+
if introduced:
338+
affected_constraints.append(introduced)
339+
if less_than := semver_constraint("<", version.get("lessThan")):
340+
affected_constraints.append(less_than)
341+
if fixed_eq := semver_constraint("=", version.get("lessThan")):
342+
fixed_constraints.append(fixed_eq)
343+
if less_equal := semver_constraint("<=", version.get("lessThanOrEqual")):
344+
affected_constraints.append(less_equal)
345+
346+
affected_range, fixed_range = golang_ranges_from_constraints(
347+
affected_constraints, fixed_constraints
348+
)
349+
if not affected_range and not fixed_range:
350+
continue
351+
352+
try:
353+
affected_packages.append(
354+
AffectedPackageV2(
355+
package=golang_module_to_purl(module_path),
356+
affected_version_range=affected_range,
357+
fixed_version_range=fixed_range,
358+
)
359+
)
360+
except Exception:
361+
continue
362+
return affected_packages

0 commit comments

Comments
 (0)