Skip to content

Commit 64fae0f

Browse files
authored
Merge pull request #11 from psiinon/master
Updated to supply apikey on all reqs and for 2.6.0
2 parents ebc1e7a + 786993f commit 64fae0f

File tree

4 files changed

+105
-46
lines changed

4 files changed

+105
-46
lines changed

setup.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,25 @@
1212
print "You must have setuptools installed to use setup.py. Exiting..."
1313
raise SystemExit(1)
1414

15-
test_requirements = [
15+
16+
install_dependencies = (
17+
'requests'
18+
)
19+
test_requirements = (
1620
'mock',
1721
'pylama',
18-
'pytest'
19-
]
22+
'pytest',
23+
'requests_mock'
24+
)
2025
setup(
2126
name="python-owasp-zap-v2.4",
22-
version="0.0.9.dev1",
23-
description="OWASP ZAP 2.5 API client",
24-
long_description="OWASP Zed Attack Proxy 2.5 API python client (the 2.4 package name has been kept to make it easier to upgrade)",
27+
version="0.0.9",
28+
description="OWASP ZAP 2.6 API client",
29+
long_description="OWASP Zed Attack Proxy 2.6 API python client (the 2.4 package name has been kept to make it easier to upgrade)",
2530
author="ZAP development team",
2631
author_email='',
2732
url="https://www.owasp.org/index.php/OWASP_Zed_Attack_Proxy_Project",
28-
download_url="https://github.com/zaproxy/zap-api-python/releases/tag/0.0.8",
33+
download_url="https://github.com/zaproxy/zap-api-python/releases/tag/0.0.9",
2934
platforms=['any'],
3035

3136
license="ASL2.0",
@@ -43,7 +48,7 @@
4348
'Intended Audience :: Developers',
4449
'Intended Audience :: Information Technology',
4550
'Programming Language :: Python'],
46-
51+
install_requires=install_dependencies,
4752
tests_require=test_requirements,
4853
extras_require={'tests': test_requirements}
4954
)

src/zapv2/__init__.py

Lines changed: 50 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,9 @@
2121

2222
__docformat__ = 'restructuredtext'
2323

24-
import json
25-
import urllib
24+
import requests
25+
from requests.packages.urllib3.exceptions import InsecureRequestWarning
26+
2627
from acsrf import acsrf
2728
from ascan import ascan
2829
from ajaxSpider import ajaxSpider
@@ -58,8 +59,7 @@ class ZAPv2(object):
5859
# base OTHER api url
5960
base_other = 'http://zap/OTHER/'
6061

61-
def __init__(self, proxies={'http': 'http://127.0.0.1:8080',
62-
'https': 'http://127.0.0.1:8080'}):
62+
def __init__(self, proxies=None, apikey=None):
6363
"""
6464
Creates an instance of the ZAP api client.
6565
@@ -69,7 +69,11 @@ def __init__(self, proxies={'http': 'http://127.0.0.1:8080',
6969
Note that all of the other classes in this directory are generated
7070
new ones will need to be manually added to this file
7171
"""
72-
self.__proxies = proxies
72+
self.__proxies = proxies or {
73+
'http': 'http://127.0.0.1:8080',
74+
'https': 'http://127.0.0.1:8080'
75+
}
76+
self.__apikey = apikey
7377

7478
self.acsrf = acsrf(self)
7579
self.ajaxSpider = ajaxSpider(self)
@@ -95,6 +99,15 @@ def __init__(self, proxies={'http': 'http://127.0.0.1:8080',
9599
self.stats = stats(self)
96100
self.users = users(self)
97101

102+
# not very nice, but prevents warnings when accessing the ZAP API via https
103+
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
104+
105+
# Currently create a new session for each request to prevent request failing
106+
# e.g. when polling the spider status
107+
#self.session = requests.Session()
108+
#if apikey is not None:
109+
# self.session.headers['X-ZAP-API-Key'] = apikey
110+
98111
def urlopen(self, *args, **kwargs):
99112
"""
100113
Opens a url forcing the proxies to be used.
@@ -103,25 +116,50 @@ def urlopen(self, *args, **kwargs):
103116
- `args`: all non-keyword arguments.
104117
- `kwargs`: all other keyword arguments.
105118
"""
106-
kwargs['proxies'] = self.__proxies
107-
return urllib.urlopen(*args, **kwargs).read()
119+
# Must never leak the API key via proxied requests
120+
return requests.get(*args, proxies=self.__proxies, verify=False, **kwargs).text
121+
122+
def _request_api(self, url, query=None):
123+
"""
124+
Shortcut for an API request. Will always add the apikey (if defined)
125+
126+
:Parameters:
127+
- `url`: the url to GET at.
128+
"""
129+
if not url.startswith('http://zap/'):
130+
# Only allow requests to the API so that we never leak the apikey
131+
raise ValueError('A non ZAP API url was specified ' + url)
132+
return;
133+
134+
# In theory we should be able to reuse the session,
135+
# but there have been problems with that
136+
self.session = requests.Session()
137+
if self.__apikey is not None:
138+
self.session.headers['X-ZAP-API-Key'] = self.__apikey
139+
140+
query = query or {}
141+
if self.__apikey is not None:
142+
# Add the apikey to get params for backwards compatibility
143+
if not query.get('apikey'):
144+
query['apikey'] = self.__apikey
145+
return self.session.get(url, params=query, proxies=self.__proxies, verify=False)
108146

109147
def _request(self, url, get=None):
110148
"""
111149
Shortcut for a GET request.
112150
113151
:Parameters:
114152
- `url`: the url to GET at.
115-
- `get`: the disctionary to turn into GET variables.
153+
- `get`: the dictionary to turn into GET variables.
116154
"""
117-
return json.loads(self.urlopen(url + '?' + urllib.urlencode(get or {})))
155+
return self._request_api(url, get).json()
118156

119-
def _request_other(self, url, get={}):
157+
def _request_other(self, url, get=None):
120158
"""
121159
Shortcut for an API OTHER GET request.
122160
123161
:Parameters:
124162
- `url`: the url to GET at.
125-
- `get`: the disctionary to turn into GET variables.
163+
- `get`: the dictionary to turn into GET variables.
126164
"""
127-
return self.urlopen(url + '?' + urllib.urlencode(get or {}))
165+
return self._request_api(url, get).text

tests/unit/conftest.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
1-
from mock import patch
21
import pytest
32

3+
import requests_mock
4+
45
from zapv2 import ZAPv2
56

7+
68
@pytest.yield_fixture
79
def zap():
810
"""
911
All tests will be able to share the instance of client with the same settings."""
10-
yield ZAPv2()
12+
yield ZAPv2(apikey='testapikey')
1113

1214

13-
@pytest.yield_fixture
14-
def urllib_mock():
15+
@pytest.yield_fixture(autouse=True)
16+
def client_mock():
1517
"""Fixture create a mock for urllib library."""
16-
with patch('zapv2.urllib.urlopen') as urllib_mock:
17-
yield urllib_mock
18+
with requests_mock.mock() as mock:
19+
yield mock

tests/unit/test_client.py

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,53 @@
11
"""
22
Tests related to the main Zap Client class
33
"""
4-
from mock import call
54

65
TEST_PROXIES = {
76
'http': 'http://127.0.0.1:8080',
87
'https': 'http://127.0.0.1:8080',
98
}
109

1110

12-
def test_urlopen_proxies(zap, urllib_mock):
13-
"""Check if Zap client passes proxy to urllib call."""
14-
urllib_mock.return_value.read.return_value = 'contents'
11+
def assert_api_key(response, apikey='testapikey'):
12+
"""Some requests should contain valid ZAP api key."""
13+
assert response._request.headers['X-ZAP-API-Key'] == apikey
14+
assert 'apikey=%s' % apikey in response.query
1515

16-
assert zap.urlopen() == 'contents'
17-
assert urllib_mock.mock_calls[0][2]['proxies'] == TEST_PROXIES
16+
17+
def test_urlopen(zap, client_mock):
18+
"""Request method should return a python object from parsed output"""
19+
api_response ='{"testkey": "testvalue"}'
20+
client_mock.get('http://localhost:8080', text=api_response)
21+
22+
assert zap.urlopen('http://localhost:8080', {'querykey': 'queryvalue'}) == api_response
23+
24+
response = client_mock.request_history[0]
25+
26+
assert 'X-ZAP-API-Key' not in response._request.headers
27+
assert 'testapikey' not in response.query
28+
assert response.proxies == TEST_PROXIES
1829

1930

20-
def test_request_response(zap, urllib_mock):
31+
def test_request_response(zap, client_mock):
2132
"""Request method should return a python object from parsed output"""
22-
urllib_mock.return_value.read.return_value = '{"testkey": "testvalue"}'
33+
client_mock.get('http://zap/test', text='{"testkey": "testvalue"}')
2334

24-
assert zap._request('http://allizom.org', {'querykey': 'queryvalue'}) == {'testkey': 'testvalue'}
25-
assert urllib_mock.mock_calls == [
26-
call('http://allizom.org?querykey=queryvalue', proxies=TEST_PROXIES),
27-
call().read()
28-
]
35+
assert zap._request('http://zap/test', {'querykey': 'queryvalue'}) == {'testkey': 'testvalue'}
2936

37+
response = client_mock.request_history[0]
3038

31-
def test_request_other(zap, urllib_mock):
39+
assert_api_key(response)
40+
assert response.proxies == TEST_PROXIES
41+
42+
43+
def test_request_other(zap, client_mock):
3244
"""_request_other should simply return a retrieved content."""
33-
urllib_mock.return_value.read.return_value = '{"testkey": "testvalue"}'
45+
api_response = '{"testkey": "testvalue"}'
46+
client_mock.get('http://zap/test', text=api_response)
47+
48+
assert zap._request_other('http://zap/test', {'querykey': 'queryvalue'}) == api_response
49+
50+
response = client_mock.request_history[0]
3451

35-
assert zap._request('http://allizom.org', {'querykey': 'queryvalue'}) == {'testkey': 'testvalue'}
36-
assert urllib_mock.mock_calls == [
37-
call('http://allizom.org?querykey=queryvalue', proxies=TEST_PROXIES),
38-
call().read()
39-
]
52+
assert_api_key(response)
53+
assert response.proxies == TEST_PROXIES

0 commit comments

Comments
 (0)