Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add test to scan all python packages #1652

Merged
merged 16 commits into from
Oct 1, 2021
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,4 @@ testinfra==5.0.0
jq==1.1.2; platform_system == "Linux" or platform_system == "MacOS"
cryptography==3.3.2; platform_system == "Linux" or platform_system == "MacOS" or platform_system=='Windows'
urllib3>=1.26.5
safety==1.10.3
7 changes: 7 additions & 0 deletions tests/security/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
DEFAULT_BRANCH = 'master'
DEFAULT_REPOSITORY = 'wazuh'


def pytest_addoption(parser):
parser.addoption("--branch", action="store", default=DEFAULT_BRANCH)
parser.addoption("--repo", action="store", default=DEFAULT_REPOSITORY)
115 changes: 115 additions & 0 deletions tests/security/test_python_packages_vuln_scan/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# Package Vulnerability Scanner

## Description
PVS is a tool used to scan for vulnerabilities in a requirements.txt file.\
It can generate reports via console output or json file and can be run with `pytest` or as a python script.\
If you use it as a script, the requirements file must be specified locally. Another way to scan for vulnerabilities is checking on `pip freeze` to get packages currently installed on the system (more information below, under `how to use` section).\
Using along with pytest, it manage to handle remote files under github repositories. Requirements file can be specified with `repo`, `branch` and `path` parameters giving flexibility on file location.

## How to use
### A - Script
```
↪ ~/git/wazuh-qa/tests/security/test_python_packages_vuln_scan ⊶ feature/1612-package-vuln-scanner ⨘ python3 python_packages_vuln_scan.py -h
usage: python_packages_vuln_scan.py [-h] (-r INPUT | -p) [-o OUTPUT]

optional arguments:
-h, --help show this help message and exit
-r INPUT specify requirements file path.
-p enable pip scan mode.
-o OUTPUT specify output file.
```
#### pip scan mode with console output:
```
↪ ~/git/wazuh-qa/tests/security/test_python_packages_vuln_scan ⊶ feature/1612-package-vuln-scanner ⨘ python3 python_packages_vuln_scan.py -p
{
"report_date": "2021-09-02T09:22:40.224599",
"vulnerabilities_found": 0,
"packages": []
}
```
#### requirements file with json output file:
```
↪ ~/git/wazuh-qa/tests/security/test_python_packages_vuln_scan ⊶ feature/1612-package-vuln-scanner ⨘ python3 python_packages_vuln_scan.py -r ~/git/wazuh/framework/requirements.txt -o json_output.json
↪ ~/git/wazuh-qa/tests/security/test_python_packages_vuln_scan ⊶ feature/1612-package-vuln-scanner ⨘ cat json_output.json
{
"report_date": "2021-09-02T09:23:09.390008",
"vulnerabilities_found": 0,
"packages": []
}
```
---
### B - Pytest
```
Parameters:
--repo: repository name. Default: 'wazuh'.
--branch: branch name of specified repository. Default: 'master'.
--requirements-path: requirements file path. Default: 'framework/requirements.txt'.
--report-path: output file path. Default: 'test_python_packages_vuln_scan/report_file.json'.
```
#### scanning wazuh-qa requirements file:
```
↪ ~/git/wazuh-qa/tests/security ⊶ feature/1612-package-vuln-scanner ⨘ python3 -m pytest test_python_packages_vuln_scan/ --repo wazuh-qa --branch master --requirements-path requirements.txt --report-path ~/Desktop/report_file.json
==================================================================================== test session starts =====================================================================================
platform linux -- Python 3.9.5, pytest-6.2.3, py-1.10.0, pluggy-0.13.1
rootdir: /home/kondent/git/wazuh-qa/tests/security
plugins: html-3.1.1, metadata-1.11.0, testinfra-5.0.0
collected 1 item

test_python_packages_vuln_scan/test_python_packages_vuln_scan.py F [100%]

========================================================================================== FAILURES ==========================================================================================
_______________________________________________________________________________ test_python_packages_vuln_scan _______________________________________________________________________________

pytestconfig = <_pytest.config.Config object at 0x7f11d3b87e20>

def test_python_packages_vuln_scan(pytestconfig):
branch = pytestconfig.getoption('--branch')
repo = pytestconfig.getoption('--repo')
requirements_path = pytestconfig.getoption('--requirements-path')
report_path = pytestconfig.getoption('--report-path')
requirements_url = f'https://raw.githubusercontent.com/wazuh/{repo}/{branch}/{requirements_path}'
urlretrieve(requirements_url, REQUIREMENTS_TEMP_FILE.name)
result = report_for_pytest(REQUIREMENTS_TEMP_FILE.name)
REQUIREMENTS_TEMP_FILE.close()
export_report(result, report_path)
> assert loads(result)['vulnerabilities_found'] == 0, f'Vulnerables packages were found, full report at: ' \
f'{report_path}'
E AssertionError: Vulnerables packages were found, full report at: /home/kondent/Desktop/report_file.json
E assert 28 == 0

test_python_packages_vuln_scan/test_python_packages_vuln_scan.py:23: AssertionError
====================================================================================== warnings summary ======================================================================================
test_python_packages_vuln_scan/python_packages_vuln_scan.py:82
/home/kondent/git/wazuh-qa/tests/security/test_python_packages_vuln_scan/python_packages_vuln_scan.py:82: DeprecationWarning: invalid escape sequence \d
package_version = max(re.findall('\d+\.+\d*\.*\d', line))

-- Docs: https://docs.pytest.org/en/stable/warnings.html
================================================================================== short test summary info ===================================================================================
FAILED test_python_packages_vuln_scan/test_python_packages_vuln_scan.py::test_python_packages_vuln_scan - AssertionError: Vulnerables packages were found, full report at: /home/kondent/De...
================================================================================ 1 failed, 1 warning in 1.44s ================================================================================

↪ ~/git/wazuh-qa/tests/security ⊶ feature/1612-package-vuln-scanner ⨘ cat ~/Desktop/report_file.json
{
"report_date": "2021-09-02T09:24:54.168627",
"vulnerabilities_found": 28,
"packages": [
{
"package_name": "pillow",
"package_version": "6.2.0",
"package_affected_version": "<6.2.2",
"vuln_description": "libImaging/TiffDecode.c in Pillow before 6.2.2 has a TIFF decoding integer overflow, related to realloc. See: CVE-2020-5310.",
"safety_id": "37779"
},
...
...
...
{
"package_name": "pillow",
"package_version": "6.2.0",
"package_affected_version": ">6.0,<6.2.2",
"vuln_description": "There is a DoS vulnerability in Pillow before 6.2.2 caused by FpxImagePlugin.py calling the range function on an unvalidated 32-bit integer if the number of bands is large. On Windows running 32-bit Python, this results in an OverflowError or MemoryError due to the 2 GB limit. However, on Linux running 64-bit Python this results in the process being terminated by the OOM killer. See: CVE-2019-19911.",
"safety_id": "37772"
}
]
}
```
9 changes: 9 additions & 0 deletions tests/security/test_python_packages_vuln_scan/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import os

DEFAULT_REQUIREMENTS_PATH = 'framework/requirements.txt'
DEFAULT_REPORT_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'report_file.json')


def pytest_addoption(parser):
parser.addoption("--requirements-path", action="store", default=DEFAULT_REQUIREMENTS_PATH)
parser.addoption("--report-path", action="store", default=DEFAULT_REPORT_PATH)
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# Copyright (C) 2015-2021, Wazuh Inc.
# Created by Wazuh, Inc. <[email protected]>.
# This program is free software; you can redistribute it and/or modify it under the terms of GPLv2
import re
import subprocess
from argparse import ArgumentParser
from collections import namedtuple
from datetime import datetime
from json import dumps, loads
from os import environ

from safety.formatter import report
from safety.safety import check

python_bin = environ['_']
package_list = []
package_tuple = namedtuple('Package', ['key', 'version'])


def get_args():
"""Command line argument parsing method

Returns:
Namespace(args*): Optional and Positional Parsing
"""
parser = ArgumentParser()
input_group = parser.add_mutually_exclusive_group(required=True)
input_group.add_argument('-r', dest='input', type=str, help='specify requirements file path.')
input_group.add_argument('-p', dest='pip_mode', action='store_true', help='enable pip scan mode.')
parser.add_argument('-o', dest='output', type=str, help='specify output file.')
return parser.parse_args()


def run_report():
"""Perform vulnerability scan using Safety to check all packages listed.

Returns:
str: information about packages and vulnerabilities.
"""
json_report = []
vulns = check(packages=package_list, key='', db_mirror='', cached=False, ignore_ids=(), proxy={})
output_report = report(vulns=vulns, full=True, json_report=True, bare_report=False,
checked_packages=len(package_list), db='', key='')
for package_information in loads(output_report):
json_report.append({
'package_name': package_information[0],
'package_version': package_information[2],
'package_affected_version': package_information[1],
'vuln_description': package_information[3],
'safety_id': package_information[4]
})
json_data = {
'report_date': datetime.now().isoformat(),
'vulnerabilities_found': len(json_report),
'packages': json_report
}
return dumps(json_data, indent=4)


def prepare_input(pip_mode, input_file_path):
"""Create temp input file with all packages listed and prepared to be scanned later on.

Args:
pip_mode (bool): enable/disable pip freeze to retrieve package information.
input_file_path (str): path to the input file (used if pip_mode is disabled).
"""
python_process = subprocess.run([python_bin, '--version'], stdout=subprocess.PIPE, universal_newlines=True)
pkg = python_process.stdout.strip().split()
package_list.append(package_tuple(pkg[0], pkg[1]))
if pip_mode:
pip_mode_process = subprocess.run([python_bin, '-m', 'pip', 'freeze'], stdout=subprocess.PIPE,
universal_newlines=True)
for package_line in pip_mode_process.stdout.strip().split('\n'):
pkg = package_line.strip().split('==')
package_list.append(package_tuple(pkg[0], pkg[1]))
else:
with open(input_file_path, mode='r') as input_file:
lines = input_file.readlines()
for line in lines:
line = re.sub('[<>~]', '=', line)
if ',' in line:
package_version = max(re.findall('\d+\.+\d*\.*\d', line))
package_name = re.findall('([a-z]+)', line)[0]
line = f'{package_name}=={package_version}\n'
if ';' in line:
line = line.split(';')[0] + '\n'
pkg = line.strip().split('==')
package_list.append(package_tuple(pkg[0], pkg[1]))


def export_report(output, output_file_path):
"""Export report to a file or console as a message.

Args:
output (str): information about packages and vulnerabilities.
output_file_path (str): path to file.
"""
if output_file_path:
with open(output_file_path, mode='w') as output_file:
output_file.write(output)
else:
print(output)


def report_for_pytest(requirements_file):
"""Method used by pytest since it does not use this as a script.

Args:
requirements_file (str): path to the input file.

Returns:
str: information about packages and vulnerabilities.
"""
prepare_input(False, requirements_file)
return run_report()


if __name__ == '__main__':
options = get_args()
opt_pip_mode = options.pip_mode
opt_output_file_path = options.output
opt_input_file_path = options.input

prepare_input(opt_pip_mode, opt_input_file_path)
output_data = run_report()
export_report(output_data, opt_output_file_path)
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Copyright (C) 2015-2021, Wazuh Inc.
# Created by Wazuh, Inc. <[email protected]>.
# This program is free software; you can redistribute it and/or modify it under the terms of GPLv2
import tempfile
from json import loads
from urllib.request import urlretrieve

from python_packages_vuln_scan import export_report, report_for_pytest

REQUIREMENTS_TEMP_FILE = tempfile.NamedTemporaryFile()


def test_python_packages_vuln_scan(pytestconfig):
branch = pytestconfig.getoption('--branch')
repo = pytestconfig.getoption('--repo')
requirements_path = pytestconfig.getoption('--requirements-path')
report_path = pytestconfig.getoption('--report-path')
requirements_url = f'https://raw.githubusercontent.com/wazuh/{repo}/{branch}/{requirements_path}'
urlretrieve(requirements_url, REQUIREMENTS_TEMP_FILE.name)
result = report_for_pytest(REQUIREMENTS_TEMP_FILE.name)
REQUIREMENTS_TEMP_FILE.close()
export_report(result, report_path)
assert loads(result)['vulnerabilities_found'] == 0, f'Vulnerables packages were found, full report at: ' \
f'{report_path}'