diff --git a/.gitignore b/.gitignore index 79d2d87e..433e02bb 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ coverage.xml dist htmlcov venv +*.sw[op] diff --git a/.travis.yml b/.travis.yml index 4dc412bf..69ab3c78 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,19 +1,22 @@ + language: python +cache: pip notifications: email: false +dist: xenial sudo: false python: - 2.7 - pypy - 3.4 - 3.5 - - 3.6 + - 3.6 + - 3.7 - pypy3 matrix: include: - python: 2.7 dist: trusty - sudo: required virtualenv: system_site_packages: true addons: @@ -22,11 +25,13 @@ matrix: - python-requests - python-coverage - python-mock + - python: 3.7 + dist: xenial install: - pip install -r tests/requirements.txt - python setup.py install script: - - py.test tests/test.py --cov=codecov + - pytest tests/test.py --cov=codecov after_success: - codecov diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f869d2f..a7c25289 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +### `2.0.16` +- fixed reported command injection vulnerability. + ### `2.0.15` - add `-X s3` to disable direct to S3 uploading @@ -127,7 +130,7 @@ ### `1.1.5` - search for all `lcov|gcov` files -- depreciate `--min-coverage`, use Github Status Update feature +- depreciate `--min-coverage`, use GitHub Status Update feature - pre-process xml => json ### `1.1.4` diff --git a/README.md b/README.md index 59e03699..0959d646 100644 --- a/README.md +++ b/README.md @@ -14,11 +14,11 @@ Find coverage reports for all the [languages below](#languages), gather them and ## Usage ```sh -pip install --user codecov && codecov -t the-repository-upload-token +pip install --user codecov && codecov -t ``` or ```sh -conda install -c conda-forge codecov && codecov -t the-repository-upload-token +conda install -c conda-forge codecov && codecov -t ``` > `--user` argument not needed for Python projects. [See example here](https://github.com/codecov/example-python). @@ -35,7 +35,7 @@ Just please make sure to pass all the necessary environment variables through: ``` [testenv] -passenv = TOXENV CI TRAVIS TRAVIS_* +passenv = TOXENV CI TRAVIS TRAVIS_* CODECOV_* deps = codecov>=1.4.0 commands = codecov -e TOXENV ``` @@ -76,23 +76,22 @@ after_success: ## CI Providers | Company | Supported | Token Required | | --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- | -| [Travis CI](https://travis-ci.org/) | Yes [![Build Status](https://secure.travis-ci.org/codecov/codecov-python.svg?branch=master)](http://travis-ci.org/codecov/codecov-python) | Private only | +| [Travis CI](https://travis-ci.org/) | Yes [![Build Status](https://secure.travis-ci.org/codecov/codecov-python.svg?branch=master)](https://travis-ci.org/codecov/codecov-python) | Private only | | [CircleCI](https://circleci.com/) | Yes | Private only | | [Codeship](https://codeship.com/) | Yes | Public & Private | | [Jenkins](https://jenkins-ci.org/) | Yes | Public & Private | | [Semaphore](https://semaphoreci.com/) | Yes | Public & Private | | [Drone.io](https://drone.io/) | Yes | Public & Private | -| [AppVeyor](http://www.appveyor.com/) | Yes [![Build status](https://ci.appveyor.com/api/projects/status/sw18lsj7786bw806/branch/master?svg=true)](https://ci.appveyor.com/project/stevepeak/codecov-python/branch/master) | Private only | +| [AppVeyor](https://www.appveyor.com/) | Yes [![Build status](https://ci.appveyor.com/api/projects/status/sw18lsj7786bw806/branch/master?svg=true)](https://ci.appveyor.com/project/stevepeak/codecov-python/branch/master) | Private only | | [Wercker](http://wercker.com/) | Yes | Public & Private | | [Magnum CI](https://magnum-ci.com/) | Yes | Public & Private | -| [Shippable](http://www.shippable.com/) | Yes | Public & Private | +| [Shippable](https://www.shippable.com/) | Yes | Public & Private | | [Gitlab CI](https://about.gitlab.com/gitlab-ci/) | Yes | Public & Private | -| git / mercurial | Yes (as a fallback) | Public & Private | -| [Buildbot](http://buildbot.net/) | `coming soon` [buildbot/buildbot#1671](https://github.com/buildbot/buildbot/pull/1671) | | +| Git / Mercurial | Yes (as a fallback) | Public & Private | +| [Buildbot](https://buildbot.net/) | `coming soon` [buildbot/buildbot#1671](https://github.com/buildbot/buildbot/pull/1671) | | | [Bamboo](https://www.atlassian.com/software/bamboo) | `coming soon` | | | [Solano Labs](https://www.solanolabs.com/) | `coming soon` | | -> Using **Travis CI**? Uploader is compatible with `sudo: false` which can speed up your builds. :+1: @@ -102,4 +101,4 @@ after_success: ## Copyright -> Copyright 2014-2017 codecov +> Copyright 2014-2019 codecov diff --git a/appveyor.yml b/appveyor.yml index a4c6b26e..88cac480 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -13,35 +13,37 @@ environment: # a later point release. - PYTHON: "C:\\Python27" - PYTHON_VERSION: "2.7.x" # currently 2.7.9 + PYTHON_VERSION: "2.7.x" # currently 2.7.15 PYTHON_ARCH: "32" - PYTHON: "C:\\Python27-x64" - PYTHON_VERSION: "2.7.x" # currently 2.7.9 + PYTHON_VERSION: "2.7.x" # currently 2.7.15 PYTHON_ARCH: "64" - - PYTHON: "C:\\Python33" - PYTHON_VERSION: "3.3.x" # currently 3.3.5 + - PYTHON: "C:\\Python34" + PYTHON_VERSION: "3.4.x" # currently 3.4.4 PYTHON_ARCH: "32" - - PYTHON: "C:\\Python33-x64" - PYTHON_VERSION: "3.3.x" # currently 3.3.5 + - PYTHON: "C:\\Python34-x64" + PYTHON_VERSION: "3.4.x" # currently 3.4.4 PYTHON_ARCH: "64" - - PYTHON: "C:\\Python34" - PYTHON_VERSION: "3.4.x" # currently 3.4.3 + - PYTHON: "C:\\Python36" + PYTHON_VERSION: "3.6.x" # currently 3.6.6 PYTHON_ARCH: "32" - - PYTHON: "C:\\Python34-x64" - PYTHON_VERSION: "3.4.x" # currently 3.4.3 + - PYTHON: "C:\\Python36-x64" + PYTHON_VERSION: "3.6.x" # currently 3.6.6 PYTHON_ARCH: "64" - # Also test Python 2.6.6 not pre-installed - - - PYTHON: "C:\\Python266" - PYTHON_VERSION: "2.6.6" + - PYTHON: "C:\\Python37" + PYTHON_VERSION: "3.7.x" # currently 3.7.1 PYTHON_ARCH: "32" + - PYTHON: "C:\\Python37-x64" + PYTHON_VERSION: "3.7.x" # currently 3.7.1 + PYTHON_ARCH: "64" + install: # Download the Appveyor Python build accessories into subdirectory .\appveyor - mkdir appveyor diff --git a/codecov/__init__.py b/codecov/__init__.py index 82e12136..40ba7a73 100644 --- a/codecov/__init__.py +++ b/codecov/__init__.py @@ -22,12 +22,8 @@ import subprocess # https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning -try: - import logging - logging.captureWarnings(True) -except: - # not py2.6 compatible - pass +import logging +logging.captureWarnings(True) version = VERSION = __version__ = '2.0.15' @@ -38,6 +34,10 @@ remove_token = re.compile(r'token=[^\&]+').sub +def sanitize_arg(replacement, arg): + return re.sub(r'[\&]+', replacement, arg, 0, re.MULTILINE) + + ignored_path = re.compile(r'(/vendor)|' r'(/js/generated/coverage)|' r'(/__pycache__)|' @@ -177,12 +177,26 @@ def check_output(cmd, **popen_args): return output.decode('utf-8') -def try_to_run(cmd): +def try_to_run(cmd, shell=True): try: - return check_output(cmd, shell=True) + return check_output(cmd, shell=shell) except subprocess.CalledProcessError as e: - write(' Error running `%s`: %s' % (cmd, str(getattr(e, 'output', str(e))))) - + write(' Error running `%s`: %s' % (cmd, e.output or str(e))) + +def run_python_coverage(args): + """Run the Python coverage tool + + If it's importable in this Python, launch it using 'python -m'. + Otherwise, look it up on PATH like any other command. + """ + try: + import coverage + except ImportError: + # Coverage is not installed on this Python. Hope it's on PATH. + try_to_run(['coverage'] + args, shell=False) + else: + # Coverage is installed on this Python. Run it as a module. + try_to_run([sys.executable, '-m', 'coverage'] + args, shell=False) def remove_non_ascii(data): try: @@ -220,7 +234,7 @@ def main(*argv, **kwargs): gcov.add_argument('--gcov-args', default='', help="extra arguments to pass to gcov") advanced = parser.add_argument_group('======================== Advanced ========================') - advanced.add_argument('-X', '--disable', nargs="*", default=[], help="Disable features. Accepting **search** to disable crawling through directories, **detect** to disable detecting CI provider, **gcov** disable gcov commands, `pycov` disables running python `coverage xml`, **fix** to disable report adjustments http://bit.ly/1O4eBpt") + advanced.add_argument('-X', '--disable', nargs="*", default=[], help="Disable features. Accepting **search** to disable crawling through directories, **detect** to disable detecting CI provider, **gcov** disable gcov commands, `pycov` disables running python `coverage xml`, **fix** to disable report adjustments https://docs.codecov.io/docs/fixing-reports") advanced.add_argument('--root', default=None, help="Project directory. Default: current direcory or provided in CI environment variables") advanced.add_argument('--commit', '-c', default=None, help="Commit SHA, set automatically") advanced.add_argument('--prefix', '-P', default=None, help="Prefix network paths to help resolve paths: https://github.com/codecov/support/issues/472") @@ -236,7 +250,7 @@ def main(*argv, **kwargs): debugging = parser.add_argument_group('======================== Debugging ========================') debugging.add_argument('--dump', action="store_true", help="Dump collected data and do not send to Codecov") - debugging.add_argument('-v', '--verbose', action="store_true", help="Not configured yet") + debugging.add_argument('-v', '--verbose', action="store_true", help="Be verbose, e.g. dump the collected data") debugging.add_argument('--no-color', action="store_true", help="Do not output with color") # Parse Arguments @@ -405,7 +419,7 @@ def main(*argv, **kwargs): # -------- # AppVeyor # -------- - elif os.getenv('CI') == "True" and os.getenv('APPVEYOR') == 'True': + elif os.getenv('CI', 'false').lower() == 'true' and os.getenv('APPVEYOR', 'false').lower() == 'true': # http://www.appveyor.com/docs/environment-variables query.update(dict(branch=os.getenv('APPVEYOR_REPO_BRANCH'), service="appveyor", @@ -614,11 +628,11 @@ def main(*argv, **kwargs): ) write('==> Processing gcov (disable by -X gcov)') cmd = "find %s %s -type f -name '*.gcno' %s -exec %s -pb %s {} +" % ( - (codecov.gcov_root or root), + (sanitize_arg('', codecov.gcov_root or root)), dont_search_here, " ".join(map(lambda a: "-not -path '%s'" % a, codecov.gcov_glob)), - (codecov.gcov_exec or ''), - (codecov.gcov_args or '')) + (sanitize_arg('', codecov.gcov_exec or '')), + (sanitize_arg('', codecov.gcov_args or ''))) write(' Executing gcov (%s)' % cmd) try_to_run(cmd) @@ -666,16 +680,16 @@ def main(*argv, **kwargs): # ----------------------------------------- # Ran from current directory if glob.glob(opj(os.getcwd(), '.coverage.*')): - write(' Mergeing coverage reports') + write(' Merging coverage reports') # The `-a` option is mandatory here. If we # have a `.coverage` in the current directory, calling # without the option would delete the previous data - try_to_run('coverage combine -a') + run_python_coverage(['combine', '-a']) if os.path.exists(opj(os.getcwd(), '.coverage')) and not os.path.exists(opj(os.getcwd(), 'coverage.xml')): write(' Generating coverage xml reports for Python') # using `-i` to ignore "No source for code" error - try_to_run('coverage xml -i') + run_python_coverage(['xml', '-i']) reports.append(read(opj(os.getcwd(), 'coverage.xml'))) reports = list(filter(bool, reports)) @@ -711,6 +725,10 @@ def main(*argv, **kwargs): write('==> Uploading') write(' .url ' + codecov.url) write(' .query ' + remove_token('token=', urlargs)) + if codecov.verbose: + write('-------------------- Reports --------------------') + write(reports) + write('-------------------------------------------------') s3 = None trys = 0 diff --git a/setup.py b/setup.py index 5e961aeb..2800c7b1 100644 --- a/setup.py +++ b/setup.py @@ -1,37 +1,36 @@ #!/usr/bin/env python from setuptools import setup -import sys -version = '2.0.15' +version = '2.0.16' classifiers = ["Development Status :: 5 - Production/Stable", "Environment :: Plugins", "Intended Audience :: Developers", "Programming Language :: Python", + "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", "Programming Language :: Python :: Implementation :: PyPy", "License :: OSI Approved :: Apache Software License", "Topic :: Software Development :: Testing"] -if sys.version_info >= (2, 7): - install_requires = ["requests>=2.7.9", "coverage"] -else: - install_requires = ["requests>=2.7.9", "coverage", "argparse"] - setup(name='codecov', version=version, - description="Hosted coverage reports for Github, Bitbucket and Gitlab", + description="Hosted coverage reports for GitHub, Bitbucket and Gitlab", long_description=None, classifiers=classifiers, keywords='coverage codecov code python java scala php', author='@codecov', author_email='hello@codecov.io', - url='http://github.com/codecov/codecov-python', + url='https://github.com/codecov/codecov-python', license='http://www.apache.org/licenses/LICENSE-2.0', packages=['codecov'], include_package_data=True, zip_safe=True, - install_requires=install_requires, - tests_require=["unittest2"], - entry_points={'console_scripts': ['codecov=codecov:main']}) + install_requires=["requests>=2.7.9", "coverage"], + entry_points={'console_scripts': ['codecov=codecov:main']}, + python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*', + ) diff --git a/tests/requirements.txt b/tests/requirements.txt index 7a8e6842..62de37c5 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,8 +1,7 @@ -coverage +coverage>=4.4.0 ddt mock -pytest +pytest>=3.6.0 pytest-cov funcsigs requests -unittest2 diff --git a/tests/test.py b/tests/test.py index 5d3f23e2..cf001d61 100644 --- a/tests/test.py +++ b/tests/test.py @@ -4,7 +4,7 @@ import itertools from ddt import ddt, data from mock import patch, Mock -import unittest2 as unittest +import unittest import subprocess @@ -204,6 +204,7 @@ def test_disable_search(self): else: raise Exception("Did not raise AssertionError") + @unittest.skipIf(os.getenv('CI') == "True" and os.getenv('APPVEYOR') == 'True', 'Skip AppVeyor CI test') def test_prefix(self): self.fake_report() res = self.run_cli(prefix='/foo/bar/', dump=True, token='a', branch='b', commit='c') @@ -314,6 +315,9 @@ def test_none_found(self): else: raise Exception("Did not raise AssertionError") + def test_sanitize_arg(self): + self.assertEqual(codecov.sanitize_arg('', '& echo test > vuln1.txt'), ' echo test > vuln1.txt') + @unittest.skipUnless(os.getenv('JENKINS_URL'), 'Skip Jenkins CI test') def test_ci_jenkins(self): self.set_env(BUILD_URL='https://....', diff --git a/tox.ini b/tox.ini index 45fed510..84880fa6 100644 --- a/tox.ini +++ b/tox.ini @@ -1,8 +1,8 @@ [tox] -envlist = py26, py27, py34 +envlist = py27, py34, py35, py36, py37 [testenv] deps = -r{toxinidir}/tests/requirements.txt commands = - py.test tests/test.py + pytest tests/test.py