Skip to content

Commit 03f453c

Browse files
committed
Add basic implementation for time travel in imports
Signed-off-by: Shivam Sandbhor <shivam.sandbhor@gmail.com>
1 parent 24e3396 commit 03f453c

6 files changed

Lines changed: 85 additions & 36 deletions

File tree

vulnerabilities/importers/debian_oval.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,8 @@
2323

2424
import asyncio
2525
import dataclasses
26-
from typing import Iterable
27-
from typing import List
28-
from typing import Mapping
29-
from typing import Set
3026
import xml.etree.ElementTree as ET
3127

32-
from aiohttp import ClientSession
33-
from aiohttp.client_exceptions import ClientResponseError, ServerDisconnectedError
3428
import requests
3529

3630
from vulnerabilities.data_source import OvalDataSource, DataSourceConfiguration

vulnerabilities/importers/github.py

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
import os
2525
import dataclasses
2626
import json
27+
from xml.etree.ElementTree import parse
28+
from dateutil import parser
2729
from typing import Set
2830
from typing import Tuple
2931
from typing import List
@@ -34,6 +36,7 @@
3436
from packageurl import PackageURL
3537
from univers.version_specifier import VersionSpecifier
3638
from univers.versions import version_class_by_package_type
39+
from univers.versions import InvalidVersion
3740

3841
from vulnerabilities.data_source import Advisory
3942
from vulnerabilities.data_source import DataSource
@@ -67,6 +70,7 @@
6770
url
6871
}
6972
severity
73+
publishedAt
7074
}
7175
package {
7276
name
@@ -122,7 +126,6 @@ def fetch(self) -> Mapping[str, List[Mapping]]:
122126
end_cursor_exp = ""
123127

124128
while True:
125-
126129
query_json = {"query": query % (ecosystem, end_cursor_exp)}
127130
resp = requests.post(self.config.endpoint, headers=headers, json=query_json).json()
128131
if resp.get("message") == "Bad credentials":
@@ -197,12 +200,16 @@ def process_response(self) -> List[Advisory]:
197200
for resp_page in self.advisories[ecosystem]:
198201
for adv in resp_page["data"]["securityVulnerabilities"]["edges"]:
199202
name = adv["node"]["package"]["name"]
200-
203+
cutoff_time = parser.parse(adv["node"]["advisory"]["publishedAt"])
204+
affected_purls = []
205+
unaffected_purls = []
201206
if self.process_name(ecosystem, name):
202207
ns, pkg_name = self.process_name(ecosystem, name)
203208
aff_range = adv["node"]["vulnerableVersionRange"]
204209
aff_vers, unaff_vers = self.categorize_versions(
205-
self.version_api.package_type, aff_range, self.version_api.get(name)
210+
self.version_api.package_type,
211+
aff_range,
212+
self.version_api.get(name, until=cutoff_time)["valid"],
206213
)
207214
affected_purls = [
208215
PackageURL(name=pkg_name, namespace=ns, version=version, type=pkg_type)
@@ -213,10 +220,6 @@ def process_response(self) -> List[Advisory]:
213220
PackageURL(name=pkg_name, namespace=ns, version=version, type=pkg_type)
214221
for version in unaff_vers
215222
]
216-
else:
217-
affected_purls = []
218-
unaffected_purls = []
219-
220223
cve_ids = set()
221224
references = self.extract_references(adv["node"]["advisory"]["references"])
222225
vuln_desc = adv["node"]["advisory"]["summary"]
@@ -265,9 +268,12 @@ def categorize_versions(
265268
affected_versions = []
266269
unaffected_versions = []
267270
for version in all_versions:
268-
if version_class(version) in version_range:
269-
affected_versions.append(version)
270-
else:
271-
unaffected_versions.append(version)
271+
try:
272+
if version_class(version) in version_range:
273+
affected_versions.append(version)
274+
else:
275+
unaffected_versions.append(version)
276+
except InvalidVersion:
277+
continue
272278

273279
return (affected_versions, unaffected_versions)

vulnerabilities/importers/istio.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
# Visit https://github.com/nexB/vulnerablecode/ for support and download.
2222
import asyncio
2323
import re
24-
from typing import List
2524
from typing import Set
2625

2726
import saneyaml

vulnerabilities/importers/npm.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,11 @@
2222
# Visit https://github.com/nexB/vulnerablecode/ for support and download.
2323

2424
import asyncio
25-
from typing import Any
25+
import pytz
2626
from typing import List
27-
from typing import Mapping
2827
from typing import Set
2928
from typing import Tuple
30-
from urllib.error import HTTPError
3129
from urllib.parse import quote
32-
from urllib.request import urlopen
3330

3431
from dateutil.parser import parse
3532
from univers.version_specifier import VersionSpecifier
@@ -87,7 +84,11 @@ def process_file(self, file) -> List[Advisory]:
8784
record = load_json(file)
8885
advisories = []
8986
package_name = record["module_name"].strip()
90-
all_versions = self.versions.get(package_name)
87+
88+
publish_date = parse(record["updated_at"])
89+
publish_date.replace(tzinfo=pytz.UTC)
90+
91+
all_versions = self.versions.get(package_name, until=publish_date)
9192
aff_range = record.get("vulnerable_versions")
9293
if not aff_range:
9394
aff_range = ""

vulnerabilities/importers/rust.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,10 @@
2626
from typing import List
2727
from typing import Set
2828
from typing import Tuple
29+
from dateutil.parser import parse
2930

3031
import toml
32+
import pytz
3133
from univers.version_specifier import VersionSpecifier
3234
from univers.versions import SemverVersion
3335
from packageurl import PackageURL
@@ -98,7 +100,8 @@ def _load_advisory(self, path: str) -> Optional[Advisory]:
98100
if advisory.get("url"):
99101
references.append(Reference(url=advisory["url"]))
100102

101-
all_versions = self.crates_api.get(crate_name)
103+
publish_date = parse(advisory["date"]).replace(tzinfo=pytz.UTC)
104+
all_versions = self.crates_api.get(crate_name, publish_date)["valid"]
102105

103106
# FIXME: Avoid wildcard version ranges for now.
104107
# See https://github.com/RustSec/advisory-db/discussions/831

vulnerabilities/package_managers.py

Lines changed: 58 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
# Visit https://github.com/nexB/vulnerablecode/ for support and download.
2222

2323
import asyncio
24+
from collections import namedtuple
25+
from dateutil import parser
2426
from json import JSONDecodeError
2527
from typing import Mapping
2628
from typing import Set
@@ -32,12 +34,22 @@
3234
from aiohttp.client_exceptions import ServerDisconnectedError
3335

3436

37+
Version = namedtuple("Version", field_names=["value", "release_date"])
38+
39+
3540
class VersionAPI:
3641
def __init__(self, cache: Mapping[str, Set[str]] = None):
3742
self.cache = cache or {}
3843

39-
def get(self, package_name: str) -> Set[str]:
40-
return self.cache.get(package_name, set())
44+
def get(self, package_name, until=None) -> Set[str]:
45+
versions = {"new": set(), "valid": set()}
46+
for version in self.cache.get(package_name, set()):
47+
if until and version.release_date and version.release_date > until:
48+
versions["new"].add(version.value)
49+
continue
50+
versions["valid"].add(version.value)
51+
52+
return versions
4153

4254

4355
def client_session():
@@ -97,7 +109,16 @@ async def fetch(self, pkg, session):
97109
try:
98110
response = await session.request(method="GET", url=url)
99111
response = await response.json()
100-
versions = set(response["releases"])
112+
for version in response["releases"]:
113+
if response["releases"][version]:
114+
versions.add(
115+
Version(
116+
value=version,
117+
release_date=parser.parse(
118+
response["releases"][version][-1]["upload_time_iso_8601"]
119+
),
120+
)
121+
)
101122
except ClientResponseError:
102123
# PYPI removed this package.
103124
# https://www.zdnet.com/article/twelve-malicious-python-libraries-found-and-removed-from-pypi/ # nopep8
@@ -121,7 +142,11 @@ async def fetch(self, pkg, session):
121142
response = await response.json()
122143
versions = set()
123144
for version_info in response["versions"]:
124-
versions.add(version_info["num"])
145+
versions.add(
146+
Version(
147+
value=version_info["num"], release_date=parser.parse(version_info["updated_at"])
148+
)
149+
)
125150

126151
self.cache[pkg] = versions
127152

@@ -143,7 +168,11 @@ async def fetch(self, pkg, session):
143168
response = await session.request(method="GET", url=url)
144169
response = await response.json()
145170
for release in response:
146-
versions.add(release["number"])
171+
versions.add(
172+
Version(
173+
value=release["number"], release_date=parser.parse(release["created_at"])
174+
)
175+
)
147176
except (ClientResponseError, JSONDecodeError):
148177
pass
149178

@@ -166,7 +195,13 @@ async def fetch(self, pkg, session):
166195
try:
167196
response = await session.request(method="GET", url=url)
168197
response = await response.json()
169-
versions = {v for v in response.get("versions", [])}
198+
for version in response.get("versions", []):
199+
release_date = response.get("time", {}).get(version)
200+
if release_date:
201+
release_date = parser.parse(release_date)
202+
versions.add(Version(value=version, release_date=release_date))
203+
else:
204+
versions.add(Version(value=version, release_date=None))
170205

171206
except ClientResponseError:
172207
pass
@@ -294,7 +329,12 @@ def extract_versions(resp: dict) -> Set[str]:
294329
try:
295330
for entry_group in resp["items"]:
296331
for entry in entry_group["items"]:
297-
all_versions.add(entry["catalogEntry"]["version"])
332+
all_versions.add(
333+
Version(
334+
value=entry["catalogEntry"]["version"],
335+
release_date=parser.parse(entry["catalogEntry"]["published"]),
336+
)
337+
)
298338
# FIXME: json response for YamlDotNet.Signed triggers this exception.
299339
# Some packages with many versions give a response of a list of endpoints.
300340
# In such cases rather, we should collect data from those endpoints.
@@ -327,15 +367,21 @@ def composer_url(pkg_name: str) -> str:
327367
vendor, name = pkg_name.split("/")
328368
except ValueError:
329369
# TODO Log this
330-
return None
370+
return
331371
return f"https://repo.packagist.org/p/{vendor}/{name}.json"
332372

333373
@staticmethod
334374
def extract_versions(resp: dict, pkg_name: str) -> Set[str]:
335-
all_versions = resp["packages"][pkg_name].keys()
336-
all_versions = {
337-
version.replace("v", "") for version in all_versions if "dev" not in version
338-
}
375+
all_versions = set()
376+
for version in resp["packages"][pkg_name]:
377+
if "dev" in version:
378+
continue
379+
all_versions.add(
380+
Version(
381+
value=version.replace("v", ""),
382+
release_date=parser.parse(resp["packages"][pkg_name][version]["time"]),
383+
)
384+
)
339385
# This if statement ensures, that all_versions contains only released versions
340386
# See https://github.com/composer/composer/blob/44a4429978d1b3c6223277b875762b2930e83e8c/doc/articles/versions.md#tags # nopep8
341387
# for explanation of removing 'v'

0 commit comments

Comments
 (0)