forked from coala/coala-bears
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
160 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
from collections import namedtuple | ||
import pkg_resources | ||
import re | ||
|
||
from safety import safety | ||
|
||
from coalib.bears.LocalBear import LocalBear | ||
from coalib.bears.requirements.PipRequirement import PipRequirement | ||
from coalib.results.Result import Result | ||
from coalib.results.SourceRange import SourceRange | ||
from coalib.settings.Setting import typed_list | ||
|
||
|
||
# the safety module expects an object that looks like this | ||
# (not importing it from there because it's in a private-ish location) | ||
Package = namedtuple('Package', ('key', 'version')) | ||
|
||
|
||
class PySafetyBear(LocalBear): | ||
""" | ||
Checks if any of your Python dependencies have known security issues. | ||
Data is taken from pyup.io's vulnerability database hosted at | ||
https://github.com/pyupio/safety. | ||
""" | ||
|
||
LANGUAGES = { | ||
'Python Requirements', | ||
'Python 2 Requirements', | ||
'Python 3 Requirements', | ||
} | ||
AUTHORS = {'Bence Nagy'} | ||
REQUIREMENTS = {PipRequirement('safety', '0.3.*')} | ||
AUTHORS_EMAILS = {'[email protected]'} | ||
LICENSE = 'AGPL' | ||
CAN_DETECT = {'Security'} | ||
|
||
def run(self, filename, file): | ||
""" | ||
Checks for vulnerable package versions in requirements files. | ||
""" | ||
packages = list( | ||
Package(key=req.key, version=req.specs[0][1]) | ||
for req in self.try_parse_requirements(file) | ||
if len(req.specs) == 1 and req.specs[0][0] == '==' | ||
) | ||
|
||
if not packages: | ||
return | ||
|
||
for vulnerability in safety.check(packages=packages): | ||
if vulnerability.is_cve: | ||
message_template = ( | ||
'{vuln.name}{vuln.spec} is vulnerable to {vuln.cve_id} ' | ||
'and your project is using {vuln.version}.' | ||
) | ||
else: | ||
message_template = ( | ||
'{vuln.name}{vuln.spec} is vulnerable and your project is ' | ||
'using {vuln.version}.' | ||
) | ||
|
||
# StopIteration should not ever happen so skipping its branch | ||
line_number, line = next( # pragma: no branch | ||
(index, line) for index, line in enumerate(file, start=1) | ||
if vulnerability.name in line | ||
) | ||
version_spec_match = re.search(r'[=<>]+(\S+?)(?:$|\s|#)', line) | ||
source_range = SourceRange.from_values( | ||
filename, | ||
line_number, | ||
version_spec_match.start(1) + 1, | ||
line_number, | ||
version_spec_match.end(1) + 1, | ||
) | ||
|
||
yield Result( | ||
self, | ||
message_template.format(vuln=vulnerability), | ||
additional_info=vulnerability.description, | ||
affected_code=(source_range, ), | ||
) | ||
|
||
@staticmethod | ||
def try_parse_requirements(lines: typed_list(str)): | ||
""" | ||
Yields all package requirements parseable from the given lines. | ||
:param lines: An iterable of lines from a requirements file. | ||
""" | ||
for line in lines: | ||
try: | ||
yield from pkg_resources.parse_requirements(line) | ||
except pkg_resources.RequirementParseError: | ||
# unsupported requirement specification | ||
pass |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
import pytest | ||
from queue import Queue | ||
from unittest import mock | ||
|
||
from safety.safety import Vulnerability | ||
|
||
from bears.python.requirements.PySafetyBear import PySafetyBear, Package | ||
from coalib.settings.Section import Section | ||
from tests.LocalBearTestHelper import LocalBearTestHelper | ||
|
||
|
||
class PySafetyBearTest(LocalBearTestHelper): | ||
|
||
def setUp(self): | ||
self.uut = PySafetyBear(Section('name'), Queue()) | ||
|
||
def test_without_vulnerability(self): | ||
with mock.patch( | ||
'bears.python.requirements.PySafetyBear.safety.check', | ||
return_value=[], | ||
) as check: | ||
self.check_validity(self.uut, ['# whee', 'foo==1.0', '# whee']) | ||
check.assert_called_once_with(packages=[Package('foo', '1.0')]) | ||
|
||
def test_with_vulnerability(self): | ||
vuln_data = { | ||
'description': 'foo', | ||
'changelog': 'bar', | ||
} | ||
with mock.patch( | ||
'bears.python.requirements.PySafetyBear.safety.check', | ||
return_value=[Vulnerability('bar', '<0.2', '0.1', vuln_data)], | ||
) as check: | ||
self.check_validity(self.uut, ['foo<2', 'bar==0.1'], valid=False) | ||
check.assert_called_once_with(packages=[Package('bar', '0.1')]) | ||
|
||
def test_with_cve_vulnerability(self): | ||
vuln_data = { | ||
'description': 'foo', | ||
'cve': 'CVE-2016-9999', | ||
} | ||
with mock.patch( | ||
'bears.python.requirements.PySafetyBear.safety.check', | ||
return_value=[Vulnerability('baz', '<2.0', '1.10', vuln_data)], | ||
) as check: | ||
self.check_validity(self.uut, ['baz==1.10', '-e .'], valid=False) | ||
check.assert_called_once_with(packages=[Package('baz', '1.10')]) | ||
|
||
def test_with_no_requirements(self): | ||
with mock.patch( | ||
'bears.python.requirements.PySafetyBear.safety.check', | ||
return_value=[], | ||
) as check: | ||
self.check_validity(self.uut, []) | ||
assert not check.called | ||
|
||
def test_with_no_pinned_requirements(self): | ||
with mock.patch( | ||
'bears.python.requirements.PySafetyBear.safety.check', | ||
return_value=[], | ||
) as check: | ||
self.check_validity(self.uut, ['foo', 'bar>2']) | ||
assert not check.called |
Empty file.