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

First push of new independent Python API for ZAP #1

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ __pycache__/
*.py[cod]
*$py.class

.idea/
# C extensions
*.so

Expand Down
5 changes: 3 additions & 2 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
Expand Down Expand Up @@ -178,15 +179,15 @@
APPENDIX: How to apply the Apache License to your work.

To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.

Copyright {yyyy} {name of copyright owner}
Copyright [yyyy] [name of copyright owner]

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down
15 changes: 15 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
include LICENSE requirements.txt

recursive-exclude * __pycache__
recursive-exclude * *.pyc
recursive-exclude * *.pyo
recursive-exclude * *.orig
recursive-exclude * .DS_Store
global-exclude __pycache__/*
global-exclude .deps/*
global-exclude *.so
global-exclude *.pyd
global-exclude *.pyc
global-exclude .git*
global-exclude .DS_Store
global-exclude .mailmap
58 changes: 58 additions & 0 deletions examples/usage_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# -*- coding: utf-8 -*-
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As an aside, how much do people look at / use the examples? I've got a more complete example (more configurable, option to use ajax spider) that we could include later..

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally, I love the examples. Although the documentation were really good, look a real example of program it's useful. So, I put in an independent directory.

Unlike Java, with Python, these examples are a complete programs. Without more dependencies, compilers, or so on. So there're os useful for programmers.

If you have more examples it could be great.


from __future__ import print_function

import time

from pprint import pprint
from zapv2 import ZAPv2

target = 'http://127.0.0.1'

# By default ZAP API client will connect to port 8080
zap = ZAPv2()

# Or, you can configure your own IP/Port
# zap_9090 = ZAPv2(proxies={'http': '127.0.0.1:9090', 'https': '127.0.0.1:9090'})

# Use the line below if ZAP is not listening on port 8080, for example, if listening on port 8090
# zap = ZAPv2(proxies={'http': 'http://127.0.0.1:8090', 'https': 'http://127.0.0.1:8090'})

# do stuff
print('Accessing target %s' % target)

# try have a unique enough session...
zap.urlopen(target)

# Give the sites tree a chance to get updated
time.sleep(2)

print('Spidering target %s' % target)
scanid = zap.spider.scan(target)

# Give the Spider a chance to start
time.sleep(2)

while int(zap.spider.status(scanid)) < 100:
print('Spider progress %: ' + zap.spider.status(scanid))
time.sleep(2)

print('Spider completed')

# Give the passive scanner a chance to finish
time.sleep(5)

print('Scanning target %s' % target)

scanid = zap.ascan.scan(target)
while int(zap.ascan.status(scanid)) < 100:
print('Scan progress %: ' + zap.ascan.status(scanid))
time.sleep(5)

print('Scan completed')

# Report the results

print('Hosts: ' + ', '.join(zap.core.hosts))
print('Alerts: ')
pprint((zap.core.alerts()))
4 changes: 4 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
six
ujson
requests
requests-cache
52 changes: 52 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#!/usr/bin/env python

"""
Standard build script.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whats the process for building and releasing this library now? Be good to document it, esp for python noobs like me ;) Could be documented in this repo or on https://github.com/zaproxy/zaproxy/wiki/GeneratingTheFullRelease#generate-the-release-add-ons

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hahaha oks, no problem :)

As a said above, to release a new version to Pypi, we'll need to do:

Register the new package
If we want to unify the python packages versions (look the image I put above) maybe we'll need to delete the rest of version from Pypi. You'll must login in at:

https://pypi.python.org/pypi?%3Aaction=login_form
And remove the old versions.

Then It'll necessary to register the new version, running (in python 2 o 3):
python setup.py sdist register upload

Uploads
In next releases, we only need to do 2 things:

  • Change the version param in setup(..). If there's no change of this value, Pypi will reject the update, because this version is already available in Pypi.
  • Run then command: python setup.py sdist upload

"""

from __future__ import print_function

from os.path import dirname, join

try:
from setuptools import setup, find_packages
except ImportError:
print("You must have setuptools installed to use setup.py. Exiting...")
raise SystemExit(1)

__docformat__ = 'restructuredtext'

# Import requirements
with open(join(dirname(__file__), 'requirements.txt')) as f:
required = f.read().splitlines()

setup(
name="python-owasp-zap-v2.4",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hum, should this be renamed to python-owasp-zap-v2.5 ? As 2.5.0 is coming out soon?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really, IMHO, the better choice si to use a standard name and unify the version. So there's a "version" param in setup(..). Currently in Pypi are available 3 different libraries:
shell

This is not a the correct way to send an update to Pypi. I'll change the name to:
setup(name="python-owasp-zap" ...
And only update the "version" param:
setup(... version=1.0.0 ...

For Python developers is more intuitive and easy if only one library is available, with different versions release.

When we update for a new version, not change the name name of library, only de version param, doing:
python setup.py sdist upload

Of, for the first release (if we change the name):
python setup.py sdist register upload

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you agree, I'll send a PR with the change

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Python library is using those names to prevent "incompatibility issues" between releases of ZAP. For example, newer versions include functionalities that will not work with older versions of ZAP, so the v2.4 (or v2.5) indicates the version of ZAP that the client implementation is targeting.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To do that, I think a better approach with having only one library, but with a different API. This is:

For version 2.4 of ZAP
from zap import ZAP_24
c = ZAP_24()

For version 2.5 of ZAP
from zap import ZAP_25
c = ZAP_25()

And so on. This way, all ZAP versions are unified in only one library.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The API is continually evolving, and will carry on doing so for the foreseeable future.
And we know users arent able to update to new versions of ZAP immediately, especially if any refactoring is involved.
I think we do need a plan for this, and I also like the idea of having just one library.
So a proposal would be great :)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea could be:

New organization of code, refactoring the Python entry point classes (ZAP_23, ZAP_25..) but, the entry points generated automatically with Java. Doing this, maybe we can unify the two approaches.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Works for me :)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oka, I'll send a PR proposal

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just sent the proposal :)

version="0.0.8",
description="OWASP ZAP 2.4 API client",
install_requires=required,
long_description="OWASP Zed Attack Proxy 2.4 API python client",
author="ZAP development team",
author_email='',
url="https://www.owasp.org/index.php/OWASP_Zed_Attack_Proxy_Project",
download_url="https://github.com/zaproxy/zaproxy/releases/tag/2.4.3",
platforms=['any'],

license="ASL2.0",

package_dir={
'': 'src',
},
packages=find_packages('src'),

classifiers=[
'License :: OSI Approved :: Apache Software License',
'Development Status :: 4 - Beta',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do we need to do to get to Release status? Hopefully its had a fair amount of use now..

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really. Most of these kinds of things I was kept it for not change the original library :)

'Topic :: Security',
'Topic :: Software Development :: Libraries :: Python Modules',
'Intended Audience :: Developers',
'Intended Audience :: Information Technology',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 3',
'Programming Language :: Python'],
)
194 changes: 194 additions & 0 deletions zapv2/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
# Zed Attack Proxy (ZAP) and its related class files.
#
# ZAP is an HTTP/HTTPS proxy for assessing web application security.
#
# Copyright 2012 ZAP development team
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2016?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oka, Same response as above :)

#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
Client implementation for using the ZAP pentesting proxy remotely.
"""

try:
# High performance json library
import ujson as json
except ImportError:
import json

import os
import six
import requests
import requests_cache
requests_cache.install_cache('zap_cache', backend="memory")

# Improving Python 2 & 3 compatibility
if six.PY2:
from urllib import urlencode, urlopen
from urlparse import urlparse, urljoin
else:
from urllib.parse import urlparse, urlencode, urljoin
from urllib.request import urlopen

from .acsrf import acsrf
from .ascan import ascan
from .ajaxSpider import ajaxSpider
from .authentication import authentication
from .autoupdate import autoupdate
from .brk import brk
from .context import context
from .core import core
from .forcedUser import forcedUser
from .httpSessions import httpSessions
from .importLogFiles import importLogFiles
from .params import params
from .pnh import pnh
from .pscan import pscan
from .reveal import reveal
from .script import script
from .search import search
from .selenium import selenium
from .sessionManagement import sessionManagement
from .spider import spider
from .users import users

__docformat__ = 'restructuredtext'


class ZapError(Exception):
"""
Base ZAP exception.
"""
pass


class ZAPv2(object):
"""
Client API implementation for integrating with ZAP v2.
"""

# base JSON api url
base = 'http://zap/JSON/'
# base OTHER api url
base_other = 'http://zap/OTHER/'

def __init__(self, proxies=None):
"""
Creates an instance of the ZAP api client.

Example:
>>> z=ZAPv2()

Example with custom proxies
>>> my_proxies = {'http': 'http://10.0.1.1:9090', 'https': 'http://10.0.1.1:9090'}
>>> z=ZAPv2(proxies=my_proxies)

:param proxies: Dict with the scheme as key and PROXY url as value
:type proxies: dict(str:str)

"""
if proxies is None:
# Set default
proxies = {'http': 'http://127.0.0.1:8080',
'https': 'http://127.0.0.1:8080'}
self.__proxies = proxies

self.acsrf = acsrf(self)
self.ajaxSpider = ajaxSpider(self)
self.ascan = ascan(self)
self.authentication = authentication(self)
self.autoupdate = autoupdate(self)
self.brk = brk(self)
self.context = context(self)
self.core = core(self)
self.forcedUser = forcedUser(self)
self.httpsessions = httpSessions(self)
self.importLogFiles = importLogFiles(self)
self.params = params(self)
self.pnh = pnh(self)
self.pscan = pscan(self)
self.reveal = reveal(self)
self.script = script(self)
self.search = search(self)
self.selenium = selenium(self)
self.sessionManagement = sessionManagement(self)
self.spider = spider(self)
self.users = users(self)

def _expect_ok(self, json_data):
"""
Checks that we have an OK response, else raises an exception.

:param json_data: the json data to look at.
:type json_data: json
"""
if isinstance(json_data, list) and json_data[0] == u'OK':
return json_data

raise ZapError(*json_data.values())

def urlopen(self, *args, **kwargs):
"""
Opens a url forcing the proxies to be used.

:param args: all non-keyword arguments.
:type args: list()

:param kwarg: all non-keyword arguments.
:type kwarg: dict()
"""
# return urlopen(*args, **kwargs).read()
return requests.get(*args, proxies=self.__proxies).text

def status_code(self, *args, **kwargs):
"""
Open a url forcing the proxies to be used.

:param args: all non-keyword arguments.
:type args: list()

:param kwarg: all non-keyword arguments.
:type kwarg: dict()
"""
# return urlopen(*args, **kwargs).getcode()
return requests.get(*args, proxies=self.__proxies).status_code

def _request(self, url, get=None):
"""
Shortcut for a GET request.

:param url: the url to GET at.
:type url: str

:param get: the dictionary to turn into GET variables.
:type get: dict(str:str)
"""
if get is None:
get = {}

return json.loads(self.urlopen("%s?%s" % (url, urlencode(get))))

def _request_other(self, url, get=None):
"""
Shortcut for an API OTHER GET request.

:param url: the url to GET at.
:type url: str

:param get: the dictionary to turn into GET variables.
:type get: dict(str:str)
"""
if get is None:
get = {}

return self.urlopen("%s?%s" % (url, urlencode(get)))