Skip to content

Commit 94e76a2

Browse files
committed
Migrate Elixir Security Importer live Pipeline
Update the Elixir Security Importer so we can have a separate function for parsing yaml file Signed-off-by: ziad hany <ziadhany2016@gmail.com>
1 parent 662c1ee commit 94e76a2

5 files changed

Lines changed: 56 additions & 84 deletions

File tree

vulnerabilities/importers/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@
5252
from vulnerabilities.pipelines.v2_importers import (
5353
elixir_security_importer as elixir_security_importer_v2,
5454
)
55+
from vulnerabilities.pipelines.v2_importers import (
56+
elixir_security_live_importer as elixir_security_live_importer_v2,
57+
)
5558
from vulnerabilities.pipelines.v2_importers import epss_importer_v2
5659
from vulnerabilities.pipelines.v2_importers import fireeye_importer_v2
5760
from vulnerabilities.pipelines.v2_importers import gentoo_importer as gentoo_importer_v2
@@ -82,9 +85,6 @@
8285
from vulnerabilities.pipelines.v2_importers import vulnrichment_importer as vulnrichment_importer_v2
8386
from vulnerabilities.pipelines.v2_importers import xen_importer as xen_importer_v2
8487
from vulnerabilities.utils import create_registry
85-
from vulnerabilities.pipelines.v2_importers import (
86-
elixir_security_live_importer as elixir_security_live_importer_v2,
87-
)
8888

8989
IMPORTERS_REGISTRY = create_registry(
9090
[

vulnerabilities/pipelines/v2_importers/elixir_security_importer.py

Lines changed: 17 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -58,32 +58,26 @@ def advisories_count(self) -> int:
5858
return count
5959

6060
def collect_advisories(self) -> Iterable[AdvisoryDataV2]:
61-
try:
62-
base_path = Path(self.vcs_response.dest_dir)
63-
vuln = base_path / "packages"
64-
for file in vuln.glob("**/*.yml"):
65-
yield from self.process_file(file, base_path)
66-
finally:
67-
if self.vcs_response:
68-
self.vcs_response.delete()
61+
base_path = Path(self.vcs_response.dest_dir)
62+
vuln = base_path / "packages"
63+
for file in vuln.glob("**/*.yml"):
64+
relative_path = str(file.relative_to(base_path)).strip("/")
65+
path_segments = str(file).split("/")
66+
# use the last two segments as the advisory ID
67+
advisory_id = "/".join(path_segments[-2:]).replace(".yml", "")
68+
advisory_url = f"https://github.com/dependabot/elixir-security-advisories/blob/master/{relative_path}"
69+
70+
yaml_file = load_yaml(str(file))
71+
yield from self.build_advisory_from_text(
72+
advisory_id=advisory_id, advisory_url=advisory_url, yaml_file=yaml_file
73+
)
6974

7075
def on_failure(self):
7176
self.clean_downloads()
7277

73-
def process_file(self, file, base_path) -> Iterable[AdvisoryDataV2]:
74-
relative_path = str(file.relative_to(base_path)).strip("/")
75-
path_segments = str(file).split("/")
76-
# use the last two segments as the advisory ID
77-
advisory_id = "/".join(path_segments[-2:]).replace(".yml", "")
78-
advisory_url = (
79-
f"https://github.com/dependabot/elixir-security-advisories/blob/master/{relative_path}"
80-
)
81-
advisory_text = None
82-
with open(str(file)) as f:
83-
advisory_text = f.read()
84-
85-
yaml_file = load_yaml(str(file))
86-
78+
def build_advisory_from_text(
79+
self, advisory_id, advisory_url, yaml_file
80+
) -> Iterable[AdvisoryDataV2]:
8781
summary = yaml_file.get("description") or ""
8882
pkg_name = yaml_file.get("package") or ""
8983

@@ -138,5 +132,5 @@ def process_file(self, file, base_path) -> Iterable[AdvisoryDataV2]:
138132
affected_packages=affected_packages,
139133
url=advisory_url,
140134
date_published=date_published,
141-
original_advisory_text=advisory_text or str(yaml_file),
135+
original_advisory_text=str(yaml_file),
142136
)

vulnerabilities/pipelines/v2_importers/elixir_security_live_importer.py

Lines changed: 29 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@
1010
from typing import Iterable
1111

1212
import requests
13-
import yaml
1413
from packageurl import PackageURL
1514
from univers.versions import SemverVersion
1615

17-
from vulnerabilities.importer import AdvisoryData
16+
from vulnerabilities.importer import AdvisoryDataV2
1817
from vulnerabilities.pipelines.v2_importers.elixir_security_importer import (
1918
ElixirSecurityImporterPipeline,
2019
)
20+
from vulnerabilities.utils import fetch_yaml
2121

2222

2323
class ElixirSecurityLiveImporterPipeline(ElixirSecurityImporterPipeline):
@@ -53,34 +53,13 @@ def get_purl_inputs(self):
5353
f"PURL: {purl!s} is not among the supported package types {self.supported_types!r}"
5454
)
5555

56-
if not purl.version:
57-
raise ValueError(f"PURL: {purl!s} is expected to have a version")
58-
5956
self.purl = purl
6057

6158
def advisories_count(self) -> int:
62-
if self.purl.type != "hex":
63-
return 0
64-
65-
try:
66-
directory_url = f"https://api.github.com/repos/dependabot/elixir-security-advisories/contents/packages/{self.purl.name}"
67-
response = requests.get(directory_url)
68-
69-
if response.status_code != 200:
70-
return 0
71-
72-
yaml_files = [file for file in response.json() if file["name"].endswith(".yml")]
73-
return len(yaml_files)
74-
except Exception:
75-
return 0
76-
77-
def collect_advisories(self) -> Iterable[AdvisoryData]:
78-
if self.purl.type != "hex":
79-
self.log(f"PURL type {self.purl.type} is not supported by Elixir Security importer")
80-
return []
59+
return 0
8160

61+
def collect_advisories(self) -> Iterable[AdvisoryDataV2]:
8262
package_name = self.purl.name
83-
8463
try:
8564
directory_url = f"https://api.github.com/repos/dependabot/elixir-security-advisories/contents/packages/{package_name}"
8665
response = requests.get(directory_url)
@@ -94,47 +73,46 @@ def collect_advisories(self) -> Iterable[AdvisoryData]:
9473
for entry in yaml_entries:
9574
# entry["path"] looks like: packages/<pkg>/<file>.yml
9675
file_path = entry["path"]
97-
content_url = f"https://api.github.com/repos/dependabot/elixir-security-advisories/contents/{file_path}"
98-
content_response = requests.get(
99-
content_url, headers={"Accept": "application/vnd.github.v3.raw"}
76+
advisory_url = f"https://api.github.com/repos/dependabot/elixir-security-advisories/contents/{file_path}"
77+
advisory_text = fetch_yaml(
78+
advisory_url, headers={"Accept": "application/vnd.github.v3.raw"}
10079
)
10180

102-
if content_response.status_code != 200:
103-
self.log(f"Failed to fetch file content for {file_path}")
104-
continue
81+
path_segments = str(file_path).split("/")
82+
# use the last two segments as the advisory ID
83+
advisory_id = "/".join(path_segments[-2:]).replace(".yml", "")
10584

106-
advisory_text = content_response.text
107-
108-
try:
109-
yaml_file = yaml.safe_load(advisory_text) or {}
110-
except Exception as e:
111-
self.log(f"Failed to parse YAML for {file_path}: {e}")
112-
continue
113-
114-
for advisory in self.build_advisory_from_yaml(
115-
yaml_file=yaml_file, advisory_text=advisory_text, relative_path=file_path
85+
for advisory in self.build_advisory_from_text(
86+
advisory_id=advisory_id,
87+
yaml_file=advisory_text,
88+
advisory_url=advisory_url,
11689
):
117-
if self.purl.version and not self._advisory_affects_version(advisory):
90+
if self.purl.version and not self.validate_advisory(advisory):
11891
continue
11992
yield advisory
12093

12194
except Exception as e:
12295
self.log(f"Error fetching advisories for {self.purl}: {str(e)}")
12396
return []
12497

125-
def _advisory_affects_version(self, advisory: AdvisoryData) -> bool:
98+
def validate_advisory(self, advisory: AdvisoryDataV2) -> bool:
12699
if not self.purl.version:
127100
return True
128101

129102
for affected_package in advisory.affected_packages:
130-
if affected_package.affected_version_range:
131-
try:
132-
purl_version = SemverVersion(self.purl.version)
133-
134-
if purl_version in affected_package.affected_version_range:
135-
return True
136-
except Exception as e:
137-
self.log(f"Failed to parse version {self.purl.version}: {str(e)}")
103+
try:
104+
purl_version = SemverVersion(self.purl.version)
105+
if (
106+
affected_package.affected_version_range
107+
and purl_version in affected_package.affected_version_range
108+
) or (
109+
affected_package.fixed_version_range
110+
and purl_version in affected_package.fixed_version_range
111+
):
138112
return True
139113

114+
except Exception as e:
115+
self.log(f"Failed to parse version {self.purl.version}: {str(e)}")
116+
# Since we have a small package file, if we fail to parse the versions, we can just return all of them
117+
return True
140118
return False

vulnerabilities/tests/pipelines/v2_importers/test_elixir_security_live_importer_v2.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,13 @@
77
# See https://aboutcode.org for more information about nexB OSS projects.
88
#
99

10-
import shutil
1110
from pathlib import Path
1211
from unittest.mock import MagicMock
1312
from unittest.mock import patch
1413

1514
import pytest
1615
from packageurl import PackageURL
1716

18-
from vulnerabilities.importer import AdvisoryData
1917
from vulnerabilities.pipelines.v2_importers.elixir_security_live_importer import (
2018
ElixirSecurityLiveImporterPipeline,
2119
)
@@ -39,7 +37,7 @@ def test_package_first_mode_with_version_filter(mock_get, test_data_dir):
3937

4038
content_response = MagicMock()
4139
content_response.status_code = 200
42-
content_response.text = advisory_content
40+
content_response.content = advisory_content
4341

4442
mock_get.side_effect = [directory_response, content_response]
4543

@@ -67,8 +65,9 @@ def test_package_first_mode_no_advisories(mock_get):
6765

6866
purl = PackageURL(type="hex", name="nonexistent-package")
6967
importer = ElixirSecurityLiveImporterPipeline(purl=purl)
70-
with pytest.raises(ValueError):
71-
importer.get_purl_inputs()
68+
importer.get_purl_inputs()
69+
advisories = list(importer.collect_advisories())
70+
assert len(advisories) == 0
7271

7372

7473
@patch("requests.get")
@@ -81,6 +80,7 @@ def test_package_first_mode_api_error(mock_get):
8180

8281
content_response = MagicMock()
8382
content_response.status_code = 500
83+
content_response.content = b""
8484

8585
mock_get.side_effect = [directory_response, content_response]
8686

vulnerabilities/utils.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,8 @@ def load_toml(path):
7777
return toml.load(f)
7878

7979

80-
def fetch_yaml(url):
81-
response = requests.get(url)
80+
def fetch_yaml(url, headers=None):
81+
response = requests.get(url, headers=headers)
8282
return saneyaml.load(response.content)
8383

8484

0 commit comments

Comments
 (0)