diff --git a/lib/manager/pip_requirements/__fixtures__/requirements7.txt b/lib/manager/pip_requirements/__fixtures__/requirements7.txt new file mode 100644 index 00000000000000..acffa2aee43e7d --- /dev/null +++ b/lib/manager/pip_requirements/__fixtures__/requirements7.txt @@ -0,0 +1,12 @@ +# Repositories +--extra-index-url http://$PIP_TEST_TOKEN:example.com/private-pypi/ +--extra-index-url http://${PIP_TEST_TOKEN}:example.com/private-pypi/ +--extra-index-url "http://$PIP_TEST_TOKEN:example.com/private-pypi/" +--extra-index-url "http://${PIP_TEST_TOKEN}:example.com/private-pypi/" +# Packages +Django[argon2]==2.0.12 +celery [redis]==4.1.1 +foo [bar] == 3.2.1 # handles extra white space +some-package==0.3.1 +some-other-package==1.0.0 +not_semver==1.9 diff --git a/lib/manager/pip_requirements/extract.spec.ts b/lib/manager/pip_requirements/extract.spec.ts index a961a81298fbe2..1619e3b98c3349 100644 --- a/lib/manager/pip_requirements/extract.spec.ts +++ b/lib/manager/pip_requirements/extract.spec.ts @@ -29,7 +29,20 @@ const requirements6 = readFileSync( 'utf8' ); +const requirements7 = readFileSync( + 'lib/manager/pip_requirements/__fixtures__/requirements7.txt', + 'utf8' +); + describe('lib/manager/pip_requirements/extract', () => { + beforeEach(() => { + delete process.env.PIP_TEST_TOKEN; + global.trustLevel = 'low'; + }); + afterEach(() => { + delete process.env.PIP_TEST_TOKEN; + global.trustLevel = 'low'; + }); describe('extractPackageFile()', () => { let config; beforeEach(() => { @@ -93,5 +106,30 @@ describe('lib/manager/pip_requirements/extract', () => { ]); expect(res.deps).toHaveLength(6); }); + it('should not replace env vars in low trust mode', () => { + process.env.PIP_TEST_TOKEN = 'its-a-secret'; + const res = extractPackageFile(requirements7, 'unused_file_name', {}); + expect(res.registryUrls).toEqual([ + 'https://pypi.org/pypi/', + 'http://$PIP_TEST_TOKEN:example.com/private-pypi/', + // eslint-disable-next-line no-template-curly-in-string + 'http://${PIP_TEST_TOKEN}:example.com/private-pypi/', + 'http://$PIP_TEST_TOKEN:example.com/private-pypi/', + // eslint-disable-next-line no-template-curly-in-string + 'http://${PIP_TEST_TOKEN}:example.com/private-pypi/', + ]); + }); + it('should replace env vars in high trust mode', () => { + process.env.PIP_TEST_TOKEN = 'its-a-secret'; + global.trustLevel = 'high'; + const res = extractPackageFile(requirements7, 'unused_file_name', {}); + expect(res.registryUrls).toEqual([ + 'https://pypi.org/pypi/', + 'http://its-a-secret:example.com/private-pypi/', + 'http://its-a-secret:example.com/private-pypi/', + 'http://its-a-secret:example.com/private-pypi/', + 'http://its-a-secret:example.com/private-pypi/', + ]); + }); }); }); diff --git a/lib/manager/pip_requirements/extract.ts b/lib/manager/pip_requirements/extract.ts index acde62f0b3486b..a0aa8c8330c923 100644 --- a/lib/manager/pip_requirements/extract.ts +++ b/lib/manager/pip_requirements/extract.ts @@ -79,7 +79,22 @@ export function extractPackageFile( } const res: PackageFile = { deps }; if (registryUrls.length > 0) { - res.registryUrls = registryUrls; + res.registryUrls = registryUrls.map((url) => { + // handle the optional quotes in eg. `--extra-index-url "https://foo.bar"` + const cleaned = url.replace(/^"/, '').replace(/"$/, ''); + if (global.trustLevel !== 'high') { + return cleaned; + } + // interpolate any environment variables + return cleaned.replace( + /(\$[A-Za-z\d_]+)|(\${[A-Za-z\d_]+})/g, + (match) => { + const envvar = match.substring(1).replace(/^{/, '').replace(/}$/, ''); + const sub = process.env[envvar]; + return sub || match; + } + ); + }); } return res; }