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
100 changes: 100 additions & 0 deletions deps/wazuh_testing/wazuh_testing/tools/scans/dependencies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# 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 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 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 to generate a report.

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

Returns:
str: information about packages and vulnerabilities.
"""
prepare_input(False, requirements_file)
return run_report()
6 changes: 3 additions & 3 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ In this repository you will find the tests used in the CI environment to test Wa

- **[deps](deps/)**: contains a Python's framework used to automatize tasks and interact with Wazuh.
- **[tests](tests/)**: directory containing the test suite. These are tests developed using Pytest.
- **[integration](tests/integration/)**: integration tests of the different daemons/components.
- **[system](tests/system)**: system tests of Wazuh.
- **[scans](tests/scans)**: tests used to scan and verify Wazuh Python code and dependencies.
- **[integration](tests/integration/)**: integration tests for the different daemons/components.
- **[system](tests/system)**: system tests.
- **[scans](tests/scans)**: tests to validate the output of running static code and dependencies scanners looking for flaws and vulnerabilities
- **[docs](link/to/docs)**: contains the technical documentation about the code and documentation about the tests.

## Builds docs locally
Expand Down
6 changes: 2 additions & 4 deletions docs/tests/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,5 @@ Contains the test suite classified in:

- **[integration](integration#integration-tests)**: The purpose of this level of testing is to expose faults in the
interaction between integrated units.

- **[system](system)**: Testing level that evaluates the behavior of a fully integrated software system based on predetermined specifications and requirements.

- **[scans](scans)**: Testing level that checks possible vulnerabilities in the Wazuh Python code and dependencies.
- **[system](tests/system)**: system tests.
- **[scans](tests/scans)**: tests to validate the output of running static code and dependencies scanners looking for flaws and vulnerabilities
2 changes: 2 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,8 @@ nav:
- Test update password: tests/system/test_jwt_invalidation/test_update_password.md
- Scans:
- tests/scans/index.md
- Dependencies:
- Test Python dependencies: tests/scans/dependencies/test_dependencies.md
- Code Analysis:
- Test Python flaws: tests/scans/code_analysis/test_python_flaws.md
- Legacy:
Expand Down
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,6 @@ 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
bandit==1.7.0
git-repo==1.10.3
git-repo==1.10.3
2 changes: 1 addition & 1 deletion tests/scans/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ def pytest_addoption(parser):
parser.addoption("--branch", action="store", default=DEFAULT_BRANCH,
help=f"Set the repository used. Default: {DEFAULT_REPOSITORY}")
parser.addoption("--repo", action="store", default=DEFAULT_REPOSITORY,
help=f"Set the repository branch. Default: {DEFAULT_BRANCH}")
help=f"Set the repository branch. Default: {DEFAULT_BRANCH}")
95 changes: 95 additions & 0 deletions tests/scans/dependencies/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Dependencies Scanner

## Description
It's a tool used to scan for vulnerabilities in a requirements.txt file.\
It can generate reports via console output or json file. Can be run with `pytest` and manage to handle remote files under github repositories. Requirements file can be specified with `repo`, `branch`, `requirements-path` parameters giving flexibility on file location.
Output file in which the report will be generated can be specified with `report-path` parameter.

## How to use - 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: 'dependencies/report_file.json'.
```
### Scanning wazuh-qa requirements file:
```
↪ ~/git/wazuh-qa/tests/scans ⊶ feature/1612-package-vuln-scanner ⨘ python3 -m pytest -vv -x --disable-warnings dependencies/ --repo wazuh-qa --branch master --requirements-path requirements.txt
==================================================================================== test session starts =====================================================================================
platform linux -- Python 3.9.5, pytest-6.2.3, py-1.10.0, pluggy-0.13.1 -- /home/kondent/pythonEnv/qa-env/bin/python3
cachedir: .pytest_cache
metadata: {'Python': '3.9.5', 'Platform': 'Linux-5.11.0-34-generic-x86_64-with-glibc2.31', 'Packages': {'pytest': '6.2.3', 'py': '1.10.0', 'pluggy': '0.13.1'}, 'Plugins': {'html': '3.1.1', 'metadata': '1.11.0', 'testinfra': '5.0.0'}}
rootdir: /home/kondent/git/wazuh-qa/tests/scans
plugins: html-3.1.1, metadata-1.11.0, testinfra-5.0.0
collected 1 item

dependencies/test_dependencies.py::test_python_dependencies_vuln_scan FAILED [100%]

========================================================================================== FAILURES ==========================================================================================
_______________________________________________________________________________ test_python_dependencies_vuln_scan _______________________________________________________________________________

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

def test_python_dependencies_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/git/wazuh-qa/tests/scans/dependencies/report_file.json
E assert 28 == 0
E +28
E -0

dependencies/test_dependencies.py:23: AssertionError
================================================================================== short test summary info ===================================================================================
FAILED dependencies/test_dependencies.py::test_python_dependencies_vuln_scan - AssertionError: Vulnerables packages were found, full report at: /home/kondent/git/wazuh-qa/tests/scans/dependen...
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
===================================================================================== 1 failed in 1.87s ======================================================================================
↪ ~/git/wazuh-qa/tests/scans ⊶ feature/1612-package-vuln-scanner ⨘ cat dependencies/report_file.json
{
"report_date": "2021-09-10T09:49:43.471148",
"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"
},
...
...
...
]
}
```

### Scanning wazuh requirements file with a specific output path:
```
↪ ~/git/wazuh-qa/tests/scans ⊶ feature/1612-package-vuln-scanner ⨘ python3 -m pytest -vv -x --disable-warnings dependencies/ --repo wazuh --branch master --requirements-path framework/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 -- /home/kondent/pythonEnv/qa-env/bin/python3
cachedir: .pytest_cache
metadata: {'Python': '3.9.5', 'Platform': 'Linux-5.11.0-34-generic-x86_64-with-glibc2.31', 'Packages': {'pytest': '6.2.3', 'py': '1.10.0', 'pluggy': '0.13.1'}, 'Plugins': {'html': '3.1.1', 'metadata': '1.11.0', 'testinfra': '5.0.0'}}
rootdir: /home/kondent/git/wazuh-qa/tests/scans
plugins: html-3.1.1, metadata-1.11.0, testinfra-5.0.0
collected 1 item

dependencies/test_dependencies.py::test_python_dependencies_vuln_scan PASSED [100%]

===================================================================================== 1 passed in 0.68s ======================================================================================
↪ ~/git/wazuh-qa/tests/scans ⊶ feature/1612-package-vuln-scanner ⨘ cat ~/Desktop/report_file.json
{
"report_date": "2021-09-10T09:53:39.284082",
"vulnerabilities_found": 0,
"packages": []
}
```
9 changes: 9 additions & 0 deletions tests/scans/dependencies/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)
3 changes: 3 additions & 0 deletions tests/scans/dependencies/test_dependencies.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## Code documentation

::: tests.scans.dependencies.test_dependencies
29 changes: 29 additions & 0 deletions tests/scans/dependencies/test_dependencies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# 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 wazuh_testing.tools.scans.dependencies import export_report, report_for_pytest

REQUIREMENTS_TEMP_FILE = tempfile.NamedTemporaryFile()


def test_python_dependencies_vuln_scan(pytestconfig):
Kondent marked this conversation as resolved.
Show resolved Hide resolved
"""Check that the specified dependencies do not have any known vulnerabilities.

Args:
pytestconfig (fixture): Fixture that returns the :class:`_pytest.config.Config` object.
"""
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}"