pax_global_header00006660000000000000000000000064143042223620014510gustar00rootroot0000000000000052 comment=926ba5f9a4ab0230612b9f4e969d2481dcb52caf python-libnmap-0.7.3/000077500000000000000000000000001430422236200144605ustar00rootroot00000000000000python-libnmap-0.7.3/.coveragerc000066400000000000000000000000751430422236200166030ustar00rootroot00000000000000[report] omit = */python?.?/* */site-packages/nose/* python-libnmap-0.7.3/.github/000077500000000000000000000000001430422236200160205ustar00rootroot00000000000000python-libnmap-0.7.3/.github/workflows/000077500000000000000000000000001430422236200200555ustar00rootroot00000000000000python-libnmap-0.7.3/.github/workflows/preflight_check.yaml000066400000000000000000000041641430422236200240670ustar00rootroot00000000000000name: Preflight Check on: push: branches: - '**' pull_request: branches: - '**' jobs: lint: runs-on: ubuntu-latest strategy: matrix: python-version: [3.6, 3.7, 3.8] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install black isort flake8 - name: Format checker with psf/black uses: psf/black@stable with: options: "--check -l 79 --exclude docs/" version: "22.8.0" - name: Format checker with isort run: isort --check-only -m 3 -l 79 --profile=black . - name: Lint with flake8 run: flake8 --exclude test,docs,examples . test: runs-on: ubuntu-latest strategy: matrix: python-version: [2.7, 3.5, 3.6, 3.7, 3.8] steps: - uses: actions/checkout@v2 - name: Setup Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Setup Environment run: | python -m pip install --upgrade pip pip install pytest pytest-cov defusedxml pip install coveralls sudo apt-get install -y nmap if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - name: Test with pytest run: | pytest --cov=libnmap/ --ignore=libnmap/test/test_backend_plugin_factory.py - name: Upload Coverage if: matrix.python-version != '2.7' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} COVERALLS_FLAG_NAME: ${{ matrix.python-version }} COVERALLS_PARALLEL: true run: | coveralls --service=github coveralls: name: Finish Coveralls needs: test runs-on: ubuntu-latest container: python:3-slim steps: - name: Finished run: | pip3 install --upgrade coveralls coveralls --finish --service=github env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} python-libnmap-0.7.3/.github/workflows/pypi_publish.yaml000066400000000000000000000011411430422236200234450ustar00rootroot00000000000000name: Upload Python Package on: release: types: [published] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v2 with: python-version: '3.x' - name: Install dependencies run: | python -m pip install --upgrade pip pip install setuptools wheel twine - name: Build and publish env: TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} run: | python setup.py sdist twine upload dist/* python-libnmap-0.7.3/.gitignore000066400000000000000000000006201430422236200164460ustar00rootroot00000000000000*.py[cod] *.swp .pylintrc *~ *.lock *.DS_Store *.swp *.out *.sqlite3 *~ # Packages *.egg *.egg-info dist build eggs parts bin var sdist develop-eggs .installed.cfg lib lib64 # Installer logs pip-log.txt # Unit test / coverage reports .coverage .tox nosetests.xml # Translations *.mo # Mr Developer .mr.developer.cfg .project .pydevproject .swp __pycache__ .vscode/settings.json .noseids _build python-libnmap-0.7.3/.pre-commit-config.yaml000066400000000000000000000011041430422236200207350ustar00rootroot00000000000000exclude: ^(test/|.tox/|docs) repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v2.3.0 hooks: - id: check-yaml - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/psf/black rev: 22.8.0 hooks: - id: black args: [--line-length=79] files: ^libnmap - repo: https://github.com/pre-commit/mirrors-isort rev: v5.6.4 hooks: - id: isort args: [--multi-line=3, --line-length=79, --profile=black] - repo: https://gitlab.com/pycqa/flake8 rev: 3.8.4 hooks: - id: flake8 python-libnmap-0.7.3/CHANGELOG.md000066400000000000000000000207661430422236200163040ustar00rootroot00000000000000# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). (or tries to...) ## [v0.7.3] 2022-09-01 ### Fixed - Linting and coveralls issues ### Security - Fix for security issue on arguments injections - [CVE-2022-30284](https://nvd.nist.gov/vuln/detail/CVE-2022-30284) ## [v0.7.2] 2020-12-16 ### Added - Added pre-commit hook support to enforce code style (black, isort) - Added unittest for defusedxml to fix billionlaugh and external entities security issues - Added extra_requires for plugins deps and defusedxml - Added banner_dict support + unittest (Merge edited PR from @cfoulds) - Added black, isort in tox environment - Added more unit tests in several modules to improve code collaboration and automated tested - Added GitHub action pipeline to run pytests, black and isort checks - Added GitHub action pipeline to publish pypi package ### Changed - Code linted and styled with black and isort - Changed Licence from CC-BY to Apache 2.0, considering that CC is [not appropriate for code licensing](https://creativecommons.org/faq/#can-i-apply-a-creative-commons-license-to-software) - Changelog now using [Keep-a-changelog](https://keepachangelog.com/en/1.0.0/) specs ### Removed - Removed travis build in favor of GitHub Actions pipelines ### Fixed - Fix empty nmap outputs due to subprocess race condition (Merge PR#79 from @Shouren) - Add extra_requires for plugins deps and defusedxml - Removed code duplication in sudo_run and sudo_run_background from process.py ### Security - Fix for security issue on XXE (XML External Entities) - CVE-2019-1010017 ## [v0.7.0] - 28/02/2016 ### Fixed - Fix of endless loop in Nmap.Process. Fix provided by @rcarrillo, many thanks! ## [v0.6.3] - 18/08/2015 ### Added - Merged pull requests for automatic pypi upload, thanks @bmx0r ## [v0.6.2] - 03/01/2015 ### Added - Added cpe_list method - Added elasticsearch example code ### Fixed - Fixed issues: 37, 41, 42, 43, 44, 46 ## [v0.6.1] - 29/06/2014 ### Added - Added full support for python 3.X: python now supports python 2.6, 2.7, 3.3, 3.4 ## [v0.5.1] - 26/05/2014 ### Added - Basic API for class CPE interface similar to python-cpe for more advanced usage of CPE, I strongly recommend you to use python-cpe. ## [v0.5.0] - 17/05/2014 ### Added - Added NmapTask class - Added NmapProcess.current_task - Added NmapProcess.tasks list - Use of semaphore to not consume CPU while looping ### Fixed - Removed Threads to read stdout/stderr - Fixed bug in NmapProcess.state ## [v0.4.9] - 14/05/2014 ### Added - Added [code samples](examples/) ### Fixed - Fix for issue 28 ## [v0.4.8] - 05/05/2014 ### Changed - Changes in OS fingerprint data API - NmapHost now holds its OS fingerprint data in NmapHost.os (NmapOSFingerpring object) - fingerprint is now a property which means you have to call it without (); e.g.: NmapHost.os.fingerprint - NmapHost.os.fingerprints return an array of os fingerprints (strings) - NmapHost.os.fingerprint return a concatenated string of all fingerprints - Fingerprints data are now accessible via NmapHost.os.osmatches which returns a list of NmapOSMatch objects - NmapOSMatch objects might contain a list of NmapOSClass objects matching with it - NmapOSClass objects might contain a list of CPE object related to the os class (CPE class will be improved and API enriched) ## [v0.4.7] - 03/05/2014 ### Added - added support for if present in : accessible via NmapService.owner ### Fixed - Minor fix for issue25 - Fixed exception when optional service tag is not present in tag ## [v0.4.6] - 06/04/2014 ### Added - Added support to run scan in background with sudo support - Added NmapProcess.sudo_run_background() ### Fixed - Corrected missing incomplete parameter on parse_fromfile and parse_fromstring - Fixed issue with run() blocking when an error triggered during the scan ## [v0.4.5] - 06/04/2014 ### Added - Added "incomplete" argument in NmapReport.parse() in order to enable parsing of incomplete or interrupted nmap scans. Could be useful to use with a background scan to parse incomplete data blocks from callback function (thanks @Sibwara for the idea). - Added NmapReport.endtimestr - Added and tested cElementTree support (performance) ### Fixed - Fixed bug when NmapReport.summary is empty ## [v0.4.4] - 04/04/2014 ### Added - Added support for tunnel attribute from tag - Added support for servicefp (service fingerprint) in attributes from tag - Added support for reasons attributes from tag - Added support for extraports/extrareasons tags ### Fixed - corrected bug in serialization: missing extra data (pull request from @DougRoyal) ## [v0.4.3] - 14/03/2014 ### Changed - API change for NmapService.scripts_results: - NmapHost.address property returns the IPv4 or IPv6 or MAC in that preference order. Use specific calls for determinists results - NmapService.scripts_results is now a property - NmapService.scripts_results return an array of scripts results ### Added - Added new properties in hosts object API: - NmapHost.ipv4 - NmapHost.ipv6 - NmapHost.mac ### Fixed - Fix issue#14: better scripts parsing - Fix issue#9 address field not correcly parsed: MAC Address would erase an ipv4 address type. - Fix API issue#10: os_ports_used ## [v0.4.2] - 26/12/2013 ### Fixed - Fixed #issue8: There is no guarantee that "finished" or "runstats" will be received by event parser of process.py. - Summary functions are now flagged as deprecated. To use data from scan summary of numbers of hosts up, the user of the lib will have to use NmapParser.parse() and the appropriate accessors. ## [v0.4.1] - 26/12/2013 ### Fixed - Fixed issue#6: Infinite loop while launching several nmap scans in background ## [v0.4.0] - 28/10/2013 ### Added - Added stop() to terminate nmap scan running in background ### Fixed - Bug corrected in missing data from nmap scan output ## [v0.3.1] - 17/06/2013 ### Changed - Refactory of objects to isolate each nmap object in a separate file ## [v0.3.0] - 17/06/2013 ### Added - Added fingerprint class - Added NmapOSFingerprint class to provide better API to fingerprint data - Added unit tests for basic NmapHost API check - Added unit test for basic NmapOSFingerprint class ## [v0.2.9] - 17/06/2013 ### Added - Add S3 plugin, allow to store nmapreport object to aws S3 compatible object storage backend (via boto) ## [v0.2.8] - 11/06/2013 ### Added - Prepare packaging for pypi ## [v0.2.1] - 17/05/2013 ### Added - Code Docstring and added support for additional data - Added support for scripts in NmapService - Added support for hosts extra data in NmapHost (uptime, distance,...) - Added support for OS fingerprint data in NmapHost - Added python docstrings for all modules - Added sphinx documentation ### Fixed - Reviewed API for libnmap objects - Fixed errors with hash() in diff - Fixed errors/exceptions in NmapParser ## [v0.2.0] - 18/04/2013 ### Added - Added Serialization and Plugin support - Added serialization encoders and decoders for NmapReport - Added basic plugin capability to NmapReport - Added basic mongodb plugin to validate plugin setup ## [v0.1.5] - 08/04/2013 ### Changed Refactory of NmapDiff system - Rework of NmapHost and NmapService API - Added __hash__, id and get_dict() for common Nmap Objects - Added NmapDiff class - Full rework of unittests - NmapParser now supports parsing from file - NmapParser is able to handle nmap XML portions - Added import in reports ## [v0.1.4] - 05/04/2013 -- Bug Fixes and improvements ### Added - Added unittest for diff on NmapHost - Added unittest for diff on NmapService ### Fixed - Fixed: __eq__ in NmapService: protocol not honoured - Fixed: sudo_run hardened and added exception handling ## [v0.1.3] - 04/04/2013 ### Added - Full refactory of NmapParser with static method - Added support for diffing NmapHost and NmapService - Added NmapParserException class - Added NmapReport class - Added unittest for report api - Added unittest for parser ### Fixed - Corrected en hardened code for NmapParser ## [v0.1.2] - 13/03/2013 ### Added - Added scaninfo parsing ### Fixed - Corrected unused variables and wrong unittests - Parse() method reviewed to call "independent" XML bloc parsers ## [v0.1.1] - 12/03/2013 ### Added - Complete refactory of code to isolate NMAP objects. ## [v0.1.0] - 11/03/2013 ### Added - First developement release packaged for Project Ninaval python-libnmap-0.7.3/CODEOWNERS000066400000000000000000000000161430422236200160500ustar00rootroot00000000000000* @savon-noir python-libnmap-0.7.3/LICENCE000066400000000000000000000011061430422236200154430ustar00rootroot00000000000000 Copyright 2020 Ronald Bister 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. python-libnmap-0.7.3/MANIFEST000066400000000000000000000013601430422236200156110ustar00rootroot00000000000000# file GENERATED by distutils, do NOT edit README.rst TODO requirements-dev.txt setup.py docs/diff.rst docs/index.rst docs/objects.rst docs/parser.rst docs/plugins_s3.rst docs/process.rst docs/objects/cpe.rst docs/objects/nmaphost.rst docs/objects/nmapreport.rst docs/objects/nmapservice.rst docs/objects/os.rst libnmap/__init__.py libnmap/diff.py libnmap/parser.py libnmap/process.py libnmap/reportjson.py libnmap/objects/__init__.py libnmap/objects/cpe.py libnmap/objects/host.py libnmap/objects/os.py libnmap/objects/report.py libnmap/objects/service.py libnmap/plugins/__init__.py libnmap/plugins/backendplugin.py libnmap/plugins/backendpluginFactory.py libnmap/plugins/es.py libnmap/plugins/mongodb.py libnmap/plugins/s3.py libnmap/plugins/sql.py python-libnmap-0.7.3/MANIFEST.in000066400000000000000000000000761430422236200162210ustar00rootroot00000000000000include TODO include *.rst *.txt recursive-include docs *.rst python-libnmap-0.7.3/NOTICE000066400000000000000000000001471430422236200153660ustar00rootroot00000000000000python-libnmap Copyright 2020 Ronald Bister This product includes software developed by Ronald Bister python-libnmap-0.7.3/README.rst000066400000000000000000000112561430422236200161540ustar00rootroot00000000000000python-libnmap ============== Code status ----------- |preflight-check| |Coverage Status| |License| Use cases --------- libnmap is a python library enabling python developers to manipulate nmap process and data. libnmap is what you were looking for if you need to implement the following: - automate or schedule nmap scans on a regular basis - manipulate nmap scans results to do reporting - compare and diff nmap scans to generate graphs - batch process scan reports - … The above uses cases will be easy to implement with the help of the libnmap modules. libnmap modules --------------- The lib currently offers the following modules: - **process**: enables you to launch nmap scans - **parse**: enables you to parse nmap reports or scan results (only XML so far) from a file, a string,… - **report**: enables you to manipulate a parsed scan result and de/serialize scan results in a json format - **diff**: enables you to see what changed between two scans - **common**: contains basic nmap objects like NmapHost and NmapService. It is to note that each object can be "diff()ed" with another similar object. - **plugins**: enables you to support datastores for your scan results directly in the "NmapReport" object. from report module: - mongodb: insert/get/getAll/delete - sqlalchemy: insert/get/getAll/delete - aws s3: insert/get/getAll/delete (not supported for python3 since boto is not supporting py3) - csv: todo (easy to implement) - elastic search: todo Documentation ------------- All the documentation is available on `read the docs `__. This documentation contains small code samples that you directly reuse. Dependencies ------------ libnmap has by default no dependencies, except defusedxml if you need to import untrusted XML scans data. The only additional python modules you’ll have to install depends if you wish to use libnmap to store reports on an exotic data store via libnmap’s independents plugins. Below the list of optional dependencies: - `sqlalchemy `__ (+the driver ie:MySQL-python) - `pymongo `__ - `boto `__ Security -------- If you are importing/parsing untrusted XML scan outputs with python-libnmap, install defusedxml library: .. code:: bash ronald@brouette:~/dev$ pip install defusedxml This will prevent you from being vulnerable to `XML External Entities attacks `__. For more information, read the `official libnmap documentation `__ This note relates to a cascaded CVE vulnerability from the python core library XML ElementTree. Nevertheless, python-libnmap has been assigned an `official CVE `__ to track this issue. This CVE is addressed from v0.7.2. Python Support -------------- The libnmap code is tested against the following python interpreters: - Python 2.7 - Python 3.6 - Python 3.7 - Python 3.8 Install ------- You can install libnmap via pip: .. code:: bash ronald@brouette:~$ pip install python-libnmap or via git and dist utils (à l’ancienne): .. code:: bash ronald@brouette:~$ git clone https://github.com/savon-noir/python-libnmap.git ronald@brouette:~$ cd python-libnmap ronald@brouette:~$ python setup.py install or via git and pip: .. code:: bash ronald@brouette:~$ git clone https://github.com/savon-noir/python-libnmap.git ronald@brouette:~$ cd python-libnmap ronald@brouette:~$ pip install . Examples -------- Some codes samples are available in the examples directory or in the `documentation `__. Among other example, you notice an sample code pushing nmap scan reports in an ElasticSearch instance and allowing you to create fancy dashboards in Kibana like the screenshot below: .. figure:: https://github.com/savon-noir/python-libnmap/blob/es/examples/kibanalibnmap.png :alt: Kibanane Contributors ------------ Mike @bmx0r Boutillier for S3 and SQL-Alechemy plugins and for the constructive critics. Thanks! .. |preflight-check| image:: https://github.com/savon-noir/python-libnmap/workflows/Preflight%20Check/badge.svg .. |Coverage Status| image:: https://coveralls.io/repos/github/savon-noir/python-libnmap/badge.svg?branch=master :target: https://coveralls.io/github/savon-noir/python-libnmap?branch=master .. |License| image:: https://img.shields.io/badge/License-Apache%202.0-blue.svg :target: https://opensource.org/licenses/Apache-2.0 python-libnmap-0.7.3/TODO000066400000000000000000000025501430422236200151520ustar00rootroot000000000000000.7.2: clean-up blacked code and pylint it 0.7.2: add unittest for defusedxml to fix billionlaugh and external entities security issues 0.7.2: Change License from CC-BY to Apache 2.0 0.7.2: Enabled defusedxml support as preferred option for parsing () 0.7.2: add extra_requires for plugins deps and defusedxml 0.7.2: Remove code duplication in sudo_run and sudo_run_background from process.py 0.7.2: Fix empty nmap outputs due to subprocess race condition (Merge PR79 from @Shouren) 0.7.2: Added banner_dict support + unittest (Merge edited PR from @cfoulds) release: - changelog date not respecting KACL specs - check https://github.com/anton-yurchenko/git-release - https://github.com/sean0x42/markdown-extract Contribution file: - specify where version needs to be set before adding tag to commit - libnmap/__init__.py - docs/conf.py - setup.py - CHANGELOG.md (set correct date) 0.7.3: add CSV backend support 0.7.3: improve API for NSE scripts 0.7.3: add support for post,pre and host scripts 0.7.3: add a Contribution guideline page 0.7.3: add development environment config and setup 0.7.3: add pre-commit hooks to enforce black and isort 0.7.3: automate in github actions the git workflow + doc update + pypi update 0.7.4: Add support and tests for traceroute in nmap 0.7.5: create complete python testing environment based on docker-compose and some examples python-libnmap-0.7.3/config/000077500000000000000000000000001430422236200157255ustar00rootroot00000000000000python-libnmap-0.7.3/config/database.yml000066400000000000000000000002231430422236200202110ustar00rootroot00000000000000sqlite: adapter: sqlite3 database: "/tmp/reportdb.sql" timeout: 500 mysql: adapter: mysql2 database: poulet username: encoding: utf8 python-libnmap-0.7.3/docs/000077500000000000000000000000001430422236200154105ustar00rootroot00000000000000python-libnmap-0.7.3/docs/Makefile000066400000000000000000000151561430422236200170600ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/libnmap.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/libnmap.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/libnmap" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/libnmap" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." python-libnmap-0.7.3/docs/conf.py000066400000000000000000000177521430422236200167230ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # libnmap documentation build configuration file, created by # sphinx-quickstart on Thu May 16 15:19:55 2013. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import os import sys # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath("..")) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ["sphinx.ext.autodoc", "sphinx.ext.todo", "sphinx.ext.viewcode"] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # The suffix of source filenames. source_suffix = ".rst" # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = "index" # General information about the project. project = u"libnmap" copyright = u"Apache 2.0 2020, Ronald Bister" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = "0.7" # The full version, including alpha/beta/rc tags. release = "0.7.2" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ["_build"] # The reST default role (used for this markup: `text`) to use for all documents. # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. # keep_warnings = False # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = "default" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". # html_title = None # A shorter title for the navigation bar. Default is the same as html_title. # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. # html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. # html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # html_additional_pages = {} # If false, no module index is generated. # html_domain_indices = True # If false, no index is generated. # html_use_index = True # If true, the index is split into individual pages for each letter. # html_split_index = False # If true, links to the reST sources are added to the pages. # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = "libnmapdoc" # -- Options for LaTeX output -------------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ( "index", "libnmap.tex", u"libnmap Documentation", u"Ronald Bister", "manual", ) ] # The name of an image file (relative to this directory) to place at the top of # the title page. # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # latex_use_parts = False # If true, show page references after internal links. # latex_show_pagerefs = False # If true, show URL addresses after external links. # latex_show_urls = False # Documents to append as an appendix to all manuals. # latex_appendices = [] # If false, no module index is generated. # latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ("index", "libnmap", u"libnmap Documentation", [u"Ronald Bister"], 1) ] # If true, show URL addresses after external links. # man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ( "index", "libnmap", u"libnmap Documentation", u"Ronald Bister", "libnmap", "One line description of project.", "Miscellaneous", ) ] # Documents to append as an appendix to all manuals. # texinfo_appendices = [] # If false, no module index is generated. # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. # texinfo_no_detailmenu = False python-libnmap-0.7.3/docs/diff.rst000066400000000000000000000070011430422236200170500ustar00rootroot00000000000000libnmap.diff ============== Using libnmap.diff module ------------------------- This modules enables the user to diff two NmapObjects: NmapService, NmapHost, NmapReport. The constructor returns a NmapDiff object which he can then use to call its inherited methods: - added() - removed() - changed() - unchanged() Those methods return a python set() of keys which have been changed/added/removed/unchanged from one object to another. The keys of each objects could be found in the implementation of the get_dict() methods of the compared objects. The example below is a heavy version of going through all nested objects to see what has changed after a diff:: #!/usr/bin/env python from libnmap.parser import NmapParser rep1 = NmapParser.parse_fromfile('libnmap/test/files/1_hosts.xml') rep2 = NmapParser.parse_fromfile('libnmap/test/files/1_hosts_diff.xml') rep1_items_changed = rep1.diff(rep2).changed() changed_host_id = rep1_items_changed.pop().split('::')[1] changed_host1 = rep1.get_host_byid(changed_host_id) changed_host2 = rep2.get_host_byid(changed_host_id) host1_items_changed = changed_host1.diff(changed_host2).changed() changed_service_id = host1_items_changed.pop().split('::')[1] changed_service1 = changed_host1.get_service_byid(changed_service_id) changed_service2 = changed_host2.get_service_byid(changed_service_id) service1_items_changed = changed_service1.diff(changed_service2).changed() for diff_attr in service1_items_changed: print "diff({0}, {1}) [{2}:{3}] [{4}:{5}]".format(changed_service1.id, changed_service2.id, diff_attr, getattr(changed_service1, diff_attr), diff_attr, getattr(changed_service2, diff_attr)) This outputs the following line:: (pydev)$ python /tmp/z.py diff(tcp.3306, tcp.3306) [state:open] [state:filtered] (pydev)$ Of course, the above code is quite ugly and heavy but the idea behind diff was to be as generic as possible in order to let the user of the lib defines its own algorithms to extract the data. A less manual and more clever approach would be to recursively retrieve the changed attributes and values of nested objects. Below, you will find a small code example doing it .. literalinclude:: ../examples/diff_sample2.py This code will output the following:: ~ NmapReport: started at 1361737906 hosts up 2/2 hosts_total: 1 => 2 ~ NmapReport: started at 1361737906 hosts up 2/2 commandline: nmap -sT -vv -oX 1_hosts.xml localhost => nmap -sS -vv -oX 2_hosts.xml localhost scanme.nmap.org ~ NmapReport: started at 1361737906 hosts up 2/2 hosts_up: 1 => 2 ~ NmapService: [closed 25/tcp smtp ()] state: open => closed + NmapService: [open 23/tcp telnet ()] - NmapService: [open 111/tcp rpcbind ()] ~ NmapReport: started at 1361737906 hosts up 2/2 scan_type: connect => syn ~ NmapReport: started at 1361737906 hosts up 2/2 elapsed: 0.14 => 134.36 + NmapHost: [74.207.244.221 (scanme.nmap.org scanme.nmap.org) - up] Note that, in the above example, lines prefixed with: 1. '~' means values changed 2. '+ means values were added 3. '-' means values were removed NmapDiff methods ---------------- .. automodule:: libnmap.diff .. autoclass:: NmapDiff :members: python-libnmap-0.7.3/docs/index.rst000066400000000000000000000033571430422236200172610ustar00rootroot00000000000000Welcome to libnmap's documentation! =================================== About libnmap ------------- libnmap is a python toolkit for manipulating nmap. It currently offers the following modules: - process: enables you to launch nmap scans - parse: enables you to parse nmap reports or scan results (only XML so far) from a file, a string,... - report: enables you to manipulate a parsed scan result and de/serialize scan results in a json format - diff: enables you to see what changed between two scans - objects: contains basic nmap objects like NmapHost and NmapService. It is to note that each object can be "diff()ed" with another similar object. - report: contains NmapReport class definition - host: contains NmapHost class definition - service: contains NmapService class definition - os: contains NmapOSFingerprint class definition and some other classes like NmapOSMatch, NmapOSClass,... - cpe: contains CPE class defdinition - plugins: enables you to support datastores for your scan results directly in the "NmapReport" object from report module - mongodb: only plugin implemented so far, ultra basic, for POC purpose only - sqlalchemy: Allow to store/retrieve NmapReport to sqlite/mysql/... all engine supported by sqlalchemy - rabbitMQ : todo - couchdb: todo - elastic search: todo - csv: todo libnmap's modules ----------------- The full `source code `_ is available on GitHub. Please, do not hesitate to fork it and issue pull requests. The different modules are documented below: .. toctree:: :maxdepth: 2 :glob: process parser objects objects/* diff plugins_s3 Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` python-libnmap-0.7.3/docs/make.bat000066400000000000000000000150571430422236200170250ustar00rootroot00000000000000@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . set I18NSPHINXOPTS=%SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over all changed/added/deprecated items echo. xml to make Docutils-native XML files echo. pseudoxml to make pseudoxml-XML files for display purposes echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) %SPHINXBUILD% 2> nul if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\libnmap.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\libnmap.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdf" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf cd %BUILDDIR%/.. echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdfja" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf-ja cd %BUILDDIR%/.. echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) if "%1" == "xml" ( %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml if errorlevel 1 exit /b 1 echo. echo.Build finished. The XML files are in %BUILDDIR%/xml. goto end ) if "%1" == "pseudoxml" ( %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml if errorlevel 1 exit /b 1 echo. echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. goto end ) :end python-libnmap-0.7.3/docs/objects.rst000066400000000000000000000031231430422236200175720ustar00rootroot00000000000000libnmap.objects =============== Using libnmap.objects module ---------------------------- This module contains the definition and API of all "NmapObjects" which enables user to manipulate nmap data: 1. NmapReport 2. NmapHost 3. NmapService The three objects above are the most common one that one would manipulate. For more advanced usage, the following objects might be useful 1. NmapOSFingerprint (contains: NmapOSMatch, NmapOSClass, OSFPPortUsed) 2. CPE (Common platform enumeration contained in NmapService or NmapOSClass) The following structure applies by default: NmapReport contains: - Scan "header" data (start time, nmap command, nmap version, ...) - List of NmapHosts (0 to X scanned hosts could be nested in a nmap report) - Scan "footer" data (end time, summary, ...) NmapHost contains: - Host "header" data (state, hostnames, ip, ...) - List of NmapService (0 to X scanned services could be nested in a scanned host) - Host "footer" data (os version, fingerprint, uptime, ...) NmapService contains: - scan results for this service: - service state, service name - optional: service banner - optional: NSE scripts data Each of the above-mentioned objects have a diff() method which enables the user of the lib the compare two different objects of the same type. If you read the code you'll see the dirty trick with id() which ensures that proper objects are being compared. The logic of diff will certainly change overtime but the API (i/o) will be kept as is. For more info on diff, please check the module's `documentation _`. python-libnmap-0.7.3/docs/objects/000077500000000000000000000000001430422236200170415ustar00rootroot00000000000000python-libnmap-0.7.3/docs/objects/cpe.rst000066400000000000000000000003201430422236200203350ustar00rootroot00000000000000libnmap.objects.cpe =================== Using libnmap.objects.cpe module -------------------------------- TODO CPE methods ----------- .. automodule:: libnmap.objects.cpe .. autoclass:: CPE :members: python-libnmap-0.7.3/docs/objects/nmaphost.rst000066400000000000000000000003371430422236200214270ustar00rootroot00000000000000libnmap.objects.host ==================== Using libnmap.objects.host module --------------------------------- TODO NmapHost methods ---------------- .. automodule:: libnmap.objects .. autoclass:: NmapHost :members: python-libnmap-0.7.3/docs/objects/nmapreport.rst000066400000000000000000000003551430422236200217650ustar00rootroot00000000000000libnmap.objects.report ====================== Using libnmap.objects.report module ----------------------------------- TODO NmapReport methods ------------------ .. automodule:: libnmap.objects .. autoclass:: NmapReport :members: python-libnmap-0.7.3/docs/objects/nmapservice.rst000066400000000000000000000003641430422236200221120ustar00rootroot00000000000000libnmap.objects.service ======================= Using libnmap.objects.service module ------------------------------------ TODO NmapService methods ------------------- .. automodule:: libnmap.objects .. autoclass:: NmapService :members: python-libnmap-0.7.3/docs/objects/os.rst000066400000000000000000000007611430422236200202200ustar00rootroot00000000000000libnmap.objects.os ================== Using libnmap.objects.os module ------------------------------- TODO NmapOSFingerprint methods ------------------------- .. automodule:: libnmap.objects.os .. autoclass:: NmapOSFingerprint :members: NmapOSMatch methods ------------------- .. autoclass:: NmapOSMatch :members: NmapOSClass methods ------------------- .. autoclass:: NmapOSClass :members: OSFPPortUsed methods -------------------- .. autoclass:: OSFPPortUsed :members: python-libnmap-0.7.3/docs/parser.rst000066400000000000000000000100541430422236200174360ustar00rootroot00000000000000libnmap.parser ============== Security note for libnmap.parser -------------------------------- **TLDR:** if you are importing/parsing untrusted XML scan outputs with python-libnmap, install defusedxml library: .. code-block:: bash ronald@brouette:~/dev$ pip install defusedxml By default, python-libnmap's parser module does not enforces an extra XML parser module than the one provided in the python core distribution. In versions previous to 0.7.2, by default, the `ElementTree XML API was used `_. This XML library is vulnerable to several `XML External Entities attacks `_ which may lead to: - Denial of Service attacks - Remote and local files inclusions - Remote code execution This implies, de facto, that parsing any untrusted XML file could result in any of the above. Fortunately, one of the python core developer is maintaining an alternative Python XML parsing library: `defusedxml `_ which addresses all the above vulnerabilities. Since the above vulnerabilities will only affect you if you are parsing untrusted XML scan outputs, by default, the defusedxml library is not enforced. But if the defusedxml library is installed, it will be the preferred XML parser picked by python-libnmap. Consider the following lines from libnmap.parser module: .. literalinclude:: ../libnmap/parser.py :linenos: :lines: 3-10 - Line 4 first tries to import defusedxml - if it fails, it then tries to load cElementTree (known to be more performant) - if it fails, it then defaults to XML ElementTree. Purpose of libnmap.parser ------------------------- This modules enables you to parse nmap scans' output. For now on, only XML parsing is supported. NmapParser is a factory which will return a NmapReport, NmapHost or NmapService object. All these objects' API are documented. The module is capable of parsing: - a complete nmap XML scan report - an incomplete/interrupted nmap XML scan report - partial nmap xml tags: , and Input the above capabilities could be either a string or a file path. Based on the provided data, NmapParse.parse() could return the following: - NmapReport object: in case a full nmap xml/dict report was prodivded - NmapHost object: in case a nmap xml section was provided - NmapService object: in case a nmap xml section was provided - Python dict with following keys: ports and extraports; python lists. Using libnmap.parser module --------------------------- NmapParser parse the whole data and returns nmap objects usable via their documented API. The NmapParser should never be instantiated and only the following methods should be called: - NmapParser.parse(string) - NmapParser.parse_fromfile(file_path) - NmapParser.parse_fromstring(string) All of the above methods can receive as input: - a full XML nmap scan result and returns a NmapReport object - a scanned host in XML (... tag) and will return a NmapHost object - a list of scanned services in XML (... tag) and will return a python array of NmapService objects - a scanned service in XML (... tag) and will return a NmapService object Small example: .. code-block:: python from libnmap.parser import NmapParser nmap_report = NmapParser.parse_fromfile('libnmap/test/files/1_os_banner_scripts.xml') print "Nmap scan summary: {0}".format(nmap_report.summary) Basic usage from a processed scan: .. code-block:: python from libnmap.process import NmapProcess from libnmap.parser import NmapParser nm = NmapProcess("127.0.0.1, scanme.nmap.org") nm.run() nmap_report = NmapParser.parse(nm.stdout) for scanned_hosts in nmap_report.hosts: print scanned_hosts For more details on using the results from NmapParser, refer to the API of class: NmapReport, NmapHost, NmapService. NmapParser methods ------------------ .. automodule:: libnmap.parser .. autoclass:: NmapParser :members: python-libnmap-0.7.3/docs/plugins_s3.rst000066400000000000000000000005111430422236200202250ustar00rootroot00000000000000libnmap.plugins.s3.NmapS3Plugin =============================== Using libnmap.plugins.s3 ------------------------ This modules enables the user to directly use S3 buckets to store and retrieve NmapReports. NmapS3Plugin methods -------------------- .. automodule:: libnmap.plugins.s3 .. autoclass:: NmapS3Plugin :members: python-libnmap-0.7.3/docs/process.rst000066400000000000000000000126611430422236200176260ustar00rootroot00000000000000libnmap.process =============== Purpose of libnmap.process -------------------------- The purpose of this module is to enable the lib users to launch and control nmap scans. This module will consequently fire the nmap command following the specified parameters provided in the constructor. It is to note that this module will not perform a full inline parsing of the data. Only specific events are parsed and exploitable via either a callback function defined by the user and provided in the constructor or by running the process in the background and accessing the NmapProcess attributes while the scan is running. To run an nmap scan, you need to: - instantiate NmapProcess - call the run*() methods Raw results of the scans will be available in the following properties: - NmapProcess.stdout: string, XML output - NmapProcess.stderr: string, text error message from nmap process To instantiate an NmapProcess instance, call the constructor with the appropriate parameters Processing of events -------------------- While Nmap is running, some events are processed and parsed. This would enable you to: - evaluate estimated time to completion and progress in percentage - find out which task is running and how many nmap tasks have been executed - know the start time and nmap version As you may know, depending on the nmap options you specified, nmap will execute several tasks like "DNS Resolve", "Ping Scan", "Connect Scan", "NSE scripts",... This is of course independent from libnmap but the lib is able to parse these tasks and will instantiate a NmapTask object for any task executed. The list of executed task is available via the following properties: - NmapProcess.tasks: list of NmapTask object (executed nmap tasks) - NmapProcess.current_task: returns the currently running NmapTask You will find below the list of attributes you can use when dealing with NmapTask: - name: task name (check nmap documentation for the complete list) - etc: unix timestamp of estimated time to completion - progress: estimated percentage of task completion - percent: estimated percentage of task completion (same as progress) - remaining: estimated number of seconds to completion - status: status of the task ('started' or 'ended') - starttime: unix timestamp of when the task started - endtime: unix timestamp of when the task ended, 0 if not completed yet - extrainfo: extra information stored for specific tasks - updated: unix timestamp of last data update for this task Using libnmap.process --------------------- This modules enables you to launch nmap scans with simples python commands:: from libnmap.process import NmapProcess nm = NmapProcess("scanme.nmap.org", options="-sV") rc = nm.run() if nm.rc == 0: print nm.stdout else: print nm.stderr This module is also able to trigger a callback function provided by the user. This callback will be triggered each time nmap returns data to the lib. It is to note that the lib forces nmap to return its status (progress and etc) every two seconds. The event callback could then play around with those values while running. To go a bit further, you can always use the threading capabilities of the NmapProcess class and run the class in the background .. literalinclude:: ../examples/proc_async.py The above code will print out the following on standard output:: (pydev)[dev@bouteille python-nmap-lib]$ python examples/proc_async.py Nmap Scan running: ETC: 0 DONE: 0% Nmap Scan running: ETC: 1369433951 DONE: 2.45% Nmap Scan running: ETC: 1369433932 DONE: 13.55% Nmap Scan running: ETC: 1369433930 DONE: 25.35% Nmap Scan running: ETC: 1369433931 DONE: 33.40% Nmap Scan running: ETC: 1369433932 DONE: 41.50% Nmap Scan running: ETC: 1369433931 DONE: 52.90% Nmap Scan running: ETC: 1369433931 DONE: 62.55% Nmap Scan running: ETC: 1369433930 DONE: 75.55% Nmap Scan running: ETC: 1369433931 DONE: 81.35% Nmap Scan running: ETC: 1369433931 DONE: 99.99% rc: 0 output: Nmap done at Sat May 25 00:18:51 2013; 1 IP address (1 host up) scanned in 22.02 seconds (pydev)[dev@bouteille python-nmap-lib]$ Another and last example of a simple use of the NmapProcess class. The code below prints out the scan results a la nmap .. literalinclude:: ../examples/proc_nmap_like.py The above code will print out the following on standard output:: (pydev)[dev@bouteille python-nmap-lib]$ python examples/proc_nmap_like.py Starting Nmap 5.51 ( http://nmap.org ) at Sat May 25 00:14:54 2013 Nmap scan report for localhost (127.0.0.1) Host is up. PORT STATE SERVICE 22/tcp open ssh (product: OpenSSH extrainfo: protocol 2.0 version: 5.3) 25/tcp open smtp (product: Postfix smtpd hostname: bouteille.localdomain) 80/tcp open http (product: nginx version: 1.0.15) 111/tcp open rpcbind (version: 2-4 extrainfo: rpc #100000) 631/tcp open ipp (product: CUPS version: 1.4) Nmap done at Sat May 25 00:15:00 2013; 1 IP address (1 host up) scanned in 6.25 seconds (pydev)[dev@bouteille python-nmap-lib]$ The full `source code `_ is available on GitHub. Please, do not hesitate to fork it and issue pull requests. NmapProcess methods ------------------- .. automodule:: libnmap.process .. autoclass:: NmapProcess :members: .. automethod:: __init__ NmapTask methods ------------------- .. autoclass:: NmapTask :members: python-libnmap-0.7.3/examples/000077500000000000000000000000001430422236200162765ustar00rootroot00000000000000python-libnmap-0.7.3/examples/check_cpe.py000066400000000000000000000024321430422236200205550ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- from libnmap.parser import NmapParser rep = NmapParser.parse_fromfile("libnmap/test/files/full_sudo6.xml") print( "Nmap scan discovered {0}/{1} hosts up".format( rep.hosts_up, rep.hosts_total ) ) for _host in rep.hosts: if _host.is_up(): print( "+ Host: {0} {1}".format(_host.address, " ".join(_host.hostnames)) ) # get CPE from service if available for s in _host.services: print( " Service: {0}/{1} ({2})".format( s.port, s.protocol, s.state ) ) # NmapService.cpelist returns an array of CPE objects for _serv_cpe in s.cpelist: print(" CPE: {0}".format(_serv_cpe.cpestring)) if _host.os_fingerprinted: print(" OS Fingerprints") for osm in _host.os.osmatches: print( " Found Match:{0} ({1}%)".format(osm.name, osm.accuracy) ) # NmapOSMatch.get_cpe() method return an array of string # unlike NmapOSClass.cpelist which returns an array of CPE obj for cpe in osm.get_cpe(): print("\t CPE: {0}".format(cpe)) python-libnmap-0.7.3/examples/diff_sample1.py000066400000000000000000000021321430422236200212000ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- from libnmap.parser import NmapParser rep1 = NmapParser.parse_fromfile("libnmap/test/files/1_hosts.xml") rep2 = NmapParser.parse_fromfile("libnmap/test/files/1_hosts_diff.xml") rep1_items_changed = rep1.diff(rep2).changed() changed_host_id = rep1_items_changed.pop().split("::")[1] changed_host1 = rep1.get_host_byid(changed_host_id) changed_host2 = rep2.get_host_byid(changed_host_id) host1_items_changed = changed_host1.diff(changed_host2).changed() changed_service_id = host1_items_changed.pop().split("::")[1] changed_service1 = changed_host1.get_service_byid(changed_service_id) changed_service2 = changed_host2.get_service_byid(changed_service_id) service1_items_changed = changed_service1.diff(changed_service2).changed() for diff_attr in service1_items_changed: print( "diff({0}, {1}) [{2}:{3}] [{4}:{5}]".format( changed_service1.id, changed_service2.id, diff_attr, getattr(changed_service1, diff_attr), diff_attr, getattr(changed_service2, diff_attr), ) ) python-libnmap-0.7.3/examples/diff_sample2.py000066400000000000000000000044041430422236200212050ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- from libnmap.parser import NmapParser def nested_obj(objname): rval = None splitted = objname.split("::") if len(splitted) == 2: rval = splitted return rval def print_diff_added(obj1, obj2, added): for akey in added: nested = nested_obj(akey) if nested is not None: if nested[0] == "NmapHost": subobj1 = obj1.get_host_byid(nested[1]) elif nested[0] == "NmapService": subobj1 = obj1.get_service_byid(nested[1]) print("+ {0}".format(subobj1)) else: print("+ {0} {1}: {2}".format(obj1, akey, getattr(obj1, akey))) def print_diff_removed(obj1, obj2, removed): for rkey in removed: nested = nested_obj(rkey) if nested is not None: if nested[0] == "NmapHost": subobj2 = obj2.get_host_byid(nested[1]) elif nested[0] == "NmapService": subobj2 = obj2.get_service_byid(nested[1]) print("- {0}".format(subobj2)) else: print("- {0} {1}: {2}".format(obj2, rkey, getattr(obj2, rkey))) def print_diff_changed(obj1, obj2, changes): for mkey in changes: nested = nested_obj(mkey) if nested is not None: if nested[0] == "NmapHost": subobj1 = obj1.get_host_byid(nested[1]) subobj2 = obj2.get_host_byid(nested[1]) elif nested[0] == "NmapService": subobj1 = obj1.get_service_byid(nested[1]) subobj2 = obj2.get_service_byid(nested[1]) print_diff(subobj1, subobj2) else: print( "~ {0} {1}: {2} => {3}".format( obj1, mkey, getattr(obj2, mkey), getattr(obj1, mkey) ) ) def print_diff(obj1, obj2): ndiff = obj1.diff(obj2) print_diff_changed(obj1, obj2, ndiff.changed()) print_diff_added(obj1, obj2, ndiff.added()) print_diff_removed(obj1, obj2, ndiff.removed()) def main(): newrep = NmapParser.parse_fromfile( "libnmap/test/files/2_hosts_achange.xml" ) oldrep = NmapParser.parse_fromfile("libnmap/test/files/1_hosts.xml") print_diff(newrep, oldrep) if __name__ == "__main__": main() python-libnmap-0.7.3/examples/elastikibana.py000066400000000000000000000061161430422236200213030ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- from datetime import datetime import pygeoip from elasticsearch import Elasticsearch from libnmap.parser import NmapParser def store_report(nmap_report, database, index): rval = True for nmap_host in nmap_report.hosts: rv = store_reportitem(nmap_host, database, index) if rv is False: print( "Failed to store host {0} in " "elasticsearch".format(nmap_host.address) ) rval = False return rval def get_os(nmap_host): rval = {"vendor": "unknown", "product": "unknown"} if nmap_host.is_up() and nmap_host.os_fingerprinted: cpelist = nmap_host.os.os_cpelist() if len(cpelist): mcpe = cpelist.pop() rval.update( {"vendor": mcpe.get_vendor(), "product": mcpe.get_product()} ) return rval def get_geoip_code(address): gi = pygeoip.GeoIP("/usr/share/GeoIP/GeoIP.dat") return gi.country_code_by_addr(address) def store_reportitem(nmap_host, database, index): host_keys = [ "starttime", "endtime", "address", "hostnames", "ipv4", "ipv6", "mac", "status", ] jhost = {} for hkey in host_keys: if hkey == "starttime" or hkey == "endtime": val = getattr(nmap_host, hkey) jhost[hkey] = datetime.fromtimestamp(int(val) if len(val) else 0) else: jhost[hkey] = getattr(nmap_host, hkey) jhost.update({"country": get_geoip_code(nmap_host.address)}) jhost.update(get_os(nmap_host)) for nmap_service in nmap_host.services: reportitems = get_item(nmap_service) for ritem in reportitems: ritem.update(jhost) database.index(index=index, doc_type="NmapItem", body=ritem) return jhost def get_item(nmap_service): service_keys = ["port", "protocol", "state"] ritems = [] # create report item for basic port scan jservice = {} for skey in service_keys: jservice[skey] = getattr(nmap_service, skey) jservice["type"] = "port-scan" jservice["service"] = nmap_service.service jservice["service-data"] = nmap_service.banner ritems.append(jservice) # create report items from nse script output for nse_item in nmap_service.scripts_results: jnse = {} for skey in service_keys: jnse[skey] = getattr(nmap_service, skey) jnse["type"] = "nse-script" jnse["service"] = nse_item["id"] jnse["service-data"] = nse_item["output"] ritems.append(jnse) return ritems xmlscans = [ "../libnmap/test/files/1_hosts.xml", "../libnmap/test/files/full_sudo6.xml", "/vagrant/nmap_switches.xml", "/vagrant/nmap-5hosts.xml", ] for xmlscan in xmlscans: nmap_report = NmapParser.parse_fromfile(xmlscan) if nmap_report: rep_date = datetime.fromtimestamp(int(nmap_report.started)) index = "nmap-{0}".format(rep_date.strftime("%Y-%m-%d")) db = Elasticsearch() j = store_report(nmap_report, db, index) python-libnmap-0.7.3/examples/es_plugin.py000066400000000000000000000010511430422236200206320ustar00rootroot00000000000000#!/usr/bin/env python import json from datetime import datetime from libnmap.parser import NmapParser from libnmap.plugins.es import NmapElasticsearchPlugin from libnmap.reportjson import ReportDecoder nmap_report = NmapParser.parse_fromfile("libnmap/test/files/1_hosts.xml") mindex = datetime.fromtimestamp(nmap_report.started).strftime("%Y-%m-%d") db = NmapElasticsearchPlugin(index=mindex) dbid = db.insert(nmap_report) nmap_json = db.get(dbid) nmap_obj = json.loads(json.dumps(nmap_json), cls=ReportDecoder) print(nmap_obj) # print(db.getall()) python-libnmap-0.7.3/examples/json_serialize.py000066400000000000000000000010061430422236200216650ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- import json from libnmap.parser import NmapParser from libnmap.reportjson import ReportDecoder, ReportEncoder nmap_report_obj = NmapParser.parse_fromfile("libnmap/test/files/1_hosts.xml") # create a json object from an NmapReport instance nmap_report_json = json.dumps(nmap_report_obj, cls=ReportEncoder) print(nmap_report_json) # create a NmapReport instance from a json object nmap_report_obj = json.loads(nmap_report_json, cls=ReportDecoder) print(nmap_report_obj) python-libnmap-0.7.3/examples/kibanalibnmap.png000066400000000000000000011645261430422236200216130ustar00rootroot00000000000000PNG  IHDR IDATxxT0Yki{2! ;EbA@J&HGwiD@z("MPDTt0$${2u)}ZwporQv#ۑ(((qB0|>isl3{َ4 EQEQEBLc9ܛ\nv$~i(((_0 g&tsG#)5MEQEQEQa|>49xg&e;I) ,߯i((('0 9;s7(;ܑHJa~M@QEQEQʈ""v?Ba3MswporQv#ۑoE+D D/@!"Q@{D ((ׁBDd" B D"bH$bu]'"Da|>49xg&e;I) ,߯i  V̒@Dx c,!i5Ҋpݦ1$)-+& qM'a,6uD"~1k5 @䚮1F"qP K 2R㺦1$DVL\$@m6,+fYL׹Y~Am6!(((zDDŤʕ+)))--o"ber/_t]GD"BD1 j֬٣Gy<]^9pڢo9fjuk"M7QB$$3>9?͝Զw~:dа]%t#ݞ꾴g{sZ230\zg|ȌBM;ejtmbҗ/9sFx+덝V Dt2b)氃(('@-Z4mn#'VZu)IDQrrrڴi^o߾R|"BDHӴh4ZRYfխ[ xf, !"DBH)@uDbRJ~!"0 g&tsG#)5MB *brs9QP1zCZ`4sD43#Dߕ+*ۑ];N9 7.>*vͮ[!#snH4&%;Kwr&53S' s0l]ԿzNY:_'}wF jw%1 O;iոٝ{ȖLW/U4͌!?H3ΠTԤ}MOZfoP Mi J 5[xOv(((VDDԭ[#G^xO> 6:tCz]׉X رСCڵk;?vYYY-R¿DDh4 kժխ[ ٳgٲe_u]J4M96ޙýElGR k@[k^Q o]Z:ˆ]릔-޺{6\xznډ<Ń-nSpco,jl }8kw.Ju)VfuKkѺ#wvfex1TK]lj;$B6OV>XR/=.vm%?,? t^/k{*mF-6,ܶisYZX^p7x49xg&e;I) ,߯i)=>x\6S塖ڶmxkܹ˥^ஞ[TZku2{;;8} u xLϕ;bմ2eG?ivB%GYs{?wWk9`7OWn7b.ۑʩ5N-cw΋j>rЎW2 0Y2v~흨q٣l:gaAZ${ViT>Ҝsu]0a~=̓ߜ-s&EQEQ?+D4Msݻw真?W^=ڵtf #HժU5k֩S'4Ǐ 'MT\-[[ɓdYVrf̘qܹ+V4mڴcǎK.z왘8|+WpAP(T~I&/_^JY\\~z0 g&tsG#)5M_ juڭޞY9ri:^p>}ࢷR=^Comξ]opOm/.?bH=1{auo=:*hs ^ڒ \o_h5Q;}vVGz<>ݟnj9`AƄjgfLw\ČnoΨf ә3?ekwS7*`OeUk7ipn)5.~~^pqj'&6=*90-\۫>Sc]^\l((ʟZ4뮻VX1c.\x7Ϛ5I&/NOO'"D4M+((h׮ݔ)S^o0O>gΜeZ/EѤď>e˖fӧϮ]4h[PP`A`0Xv%K$%%} 6 ~m60 g&tsG#)5MBH"BЦ_s.|CzQ%7e_z˽յ+GdJxOe5d;ؘ y}LkqHL -Ya.o>wSkO8>zH;Լz%oȔ^Vpk_7F`7Wn ",/s?Ԡt^푭Y놶N_N[4 oMy#ǀ@QEQE""F۷3fO8t:5jtAk"&M <ѣ 4p:۶m-[r%"BD())y{UvC͟?qqq@D"[ouժU})((ʪ[nϞ=?c#0 9;s7(;ܑHJa~MDD4^wԬVRܙ'%hPP;]6O?;Sr[8*=_PHa'5GĢDpX| !d)s%ٕӹQ[|b ]+%:?eky O'-rnI$19D̲$ H).eI _ JO|]r.|=Z4R+BE,-/'TƁ1DH.[d((ʟb7o޼N:{ݲeK^^R"";Dh !222ʔ)K/իW1&HNN={vz7xι"UTIIIIeʔ9q1#0 9;s7(;ܑHJa~MW#)¡tI\v2+m:Zшv %Q;m0 G0D@I`w8t E,ǀqID#q$ Gf#+p9#t@BQMchD"c(! DlY’MB1ӡsH$L;t$ ψH9: EQEQO"\u)e,k&DDcѨ뚦!eYhf!b8NKKԩӲev;!"aDFn@,,p!"0 g&tsG#)5M""n@d@$ D"5D"!) gD !!"DDPEQEQoR0 HJ 1D0"BD!D$s"BD_ADwa|>49M.Nv$~i(((!D7 B0|>isl&gO KEa~M@QEQEQ8!a>4M9^SG{ˎh)^MH(((qB0|>isf'q/{Вk(( ! |ir1jsG4$~M@QEQEQ8!a>4M9F2Smn;;,~_4PEQEQ/Na3MsT[d?N0K4 EQEQEBLc$3{<̒5MEQEQEQa|>49LE^4$~M@QEQEQ8!a>4M9F2Smn;;,~_4"dt _u@"(((B0|>isd"cwQwY~iDd,gHy$%H5@gnk$!"((( B0|>isd"cwQwY~iC@'$G͢P"@D !@ $ݺyB{Њ +bcLsh3)%"¿DDu!cH7 "%2Mc$D"I 1ƀ@QEQ/Ba3MsT[d?N0K4 H @"I D1VZz닆\3Æ#@DV I D k$IJD5"@ AtHDC@ p;^:a#$" "BIKZ40dshR"ψ1$$D$"dL !8cDqD )$@$"D8ѨE6M'?1R!"1ΐʐY$d\X1bȑ1dB!21HBH"B@8J!1(%1IY`4 (( 0 9Hf-2zuy%k tI`@bt; u !D d,TRrߦ=6n0I_!*˒BYpmdHR8@@Jn@":k: Ae ^sP|e3EC9a TwԆ{~Gayn _"FB[UxͮECg o]JpI!#h2 zX^i &Pr.`i% 1A*7s~:"DW(Vٜ.I!""!"DkD@H( g|!5V8agnjYS9a4dM)o/ Gh$qnE, -A5Ӱ !Y0c C #.wΑE$Xa Gd KکK~D((rB0|>isd"cwQwY~iwRP5AthU:kt nY@E$vN1B́((nCƀIJ NR""\Mmj3pK_kx `w!B#fQ ŧ_HKpw،Ymv4 Ehw"YQ##khuH ys0\Q8?V%LGBTiKߒWÅ@@J&UЩ3%^gPzxecD lN8/wἓM@"A<>Yϗou!`$R;ciϚ6e:Iu߯`YyM*wp 7D#1L;:R8Yi`"nshLi}:nx "+#vYX$$؝NQ,jIᰡ$)"Ez}9=}R8yk^tV3wc[6C2ET^|EDV}yS Wwi<[ sOye f{(!-[|ڗ?9)h ;z\شVUOwWegIz4JG_9 v((jB0|>isd"cwQwY~iwDx5mlFȉdTq[K>'[=9"b)&t"qC2L(rMMW^>NcN7HX4do|>ݡx' <szwʮ]b'FQ$5Rwb\ 5*9ꍏf Cx^fn#b U^K.",F":,I%WESȜ'(*ǐZw :v]Yo=~oJ|4d1H@Ғ8/~hi 9C" /OԿc*~'ΘѱMf~a5ٳ|㘘s'/~#;7R&}|Z߱ur#>qǹFx$ ; IDAT)' ht1RWKNt s~ ].^&)ΑecfaCNťkYͱj/cbOsg6`ӎed @EQEQ?1!a>4M9F2Smn;;,~_4!Pj!Uk=|êwv !%_60"EY.)/ rLʼ=sŁi>|VѻCN. ~!.ֹoB.K?xFرO$RNq'9^ZU-a˻ͷB0RŷU+l]68TSu9=?|n~B>M@6;*{^|c{jUufqfqfCfjݡ++ewIjf !PIƣUweE2nޛqީ+S#hcMoVYF^vU.>~ av0~^?=bGCya |}lGa]gcym(o(Uhnc~&ʇG-K,,rN~͛ϫ0u~fDBJY].B Un9|r#v~m%|qN8}Ӏ*`OlK_QZ& _ow,ބҡZ{:ˣGI`]'WuX5=^]!ڋZtTU;.ٔJd1AEԅwnu :|{ې]+; {{wqvL֬WU;k%KKS,=Jnj-z֡"+^(( ! |ir1jsGifI1)tp}9ӓp[‰je[/|Ձ 7vYݽ4/-Un0s8deϘ7\6p-zbeh'9NKcUsތ| 2T;tezq.IM,A3mw7Tf!"@DRðm]e#"!b(~e锥[ h>m뽹f'zkڸsSVp g7?mމ-9فj>yA󏤮߽(K]xsڔ?)sP]|G>NhӞؔ%omܩUr^0m醼j]2<{aMW=#+VN]1SYA:|q+6yuӦVٗjio޼GGelagMxz)gg?7&~⫻<#)fnYEVoXc$((ʟ0 g&#6ԝaD߯i͕)A- (xdk?;ua9i>|VѻCN. ~AV [Bn MR{^ܾ,g @@8{%*fWO./JAJ{^p/b=v\SJ}>sGޖ_VٙdY!Em[OOG>:ooXw|3"pպCWWױ?C!3r#UX[D2nR".Н:\ %h,Нhܻ7׍횵ᰡnꢱimP^9tn1>=u? ן#o*h6 ~ ?[]3zO{|BLfѿKy*Yf'_S{wwק?Zg/u-|Wjoa ^(uvm1c\5m֛R?7#( m=rIsP%g"7bRgwN2~^5s۽7Wv_(\q|`7/ ((fB0|>isd"cwQwY~iR95f $-8IQi\sbUKRG[>vD`&|_;BlτО}̓0̴mLr̞Vq9$c,luO>jPK_5R to]4wguyUO\@LȀ5j[\@0kT/8Me^ZEӁ霸H)OneqmpqږnYxC}N8 "U]^a\.KR4KBX]DVD㈀$,錳,:͎oU+|"a-Op$:{n܄¬™3Cs',ؓJrdx~Xc?f"26:'j5zw(%Q,ʸ;z"!dI=u"P$x揠(/EPaj$l;>pwȚj]J&ֻGvfwm(z}v.h40+djІi kim^b?f-0n[?z#3R˩}ܐ-Lc!3\Еu첻$5Ah(V]5h0Fͩ]>Woœwc2֬.9~=uUj`OqQ)e5zVkևzY8Xѱgqܲ.MHD0iӄVƄ)<6UYyozvKΤcZ8{;5nf{0y/>-;dRL]a [6 j}k^QL9j2xIjaO[g鞼 ͉\aTQYozoꚱ k[+&Zf/à~Ly>uCu7ٛ7Zdz=q|W]v%94M9F2Smn;;,~_4oRp=q{Jo%%vo~s-Exb};_ǝ A&åuu;d,lW7|V1{BN. X4^^f D;d!wY2b;5784,f8#n ڍSX_otv8EDgN{[']yd6 cmҺCoQ.Oԡk1[q6m!זz9̦p:CVWB=% %~AD0RtOʽOߖT?+gM p. & h;{ |Iu^׾ ~ǻ*n nǛK+Wһc _t$7ؿw_6_n|Gw/ڹڷxOh/\oo+䕚?h͚5V͞iӕXaĒU7_J,e[V?3ϴV>Y=wk.U步Q Q,{y%^0+v^UMt~v~O._O4c *zdyo\qUڽ| wqg~غo~H"`G[[l;/AVj}u<@F><Ѳ_}Q]<zm֠:g<ڢE#Y!((ʟ0 g&#6ԝaD߯iwB2P8; l2%G92^N;3#D "d$ crn3#v\$ "` (TJ0p6!:u*V/Y8;\@DV1bDH Hqd".EFdH- D2`N'@sNBbvfQU .AY\JrE xEd%BEh!P WoiG?qgqGc [F+!٭<^,]R""L%q_f<ӭM"ZĄҋ1[PĊݱlow8)&1—ż(XRW7 ռ°agi DLt9)MڸM\^߼e=ǾqRy(ʓEWQ6&N-{"\GHŁ0'z]6.-Ip]_ {gmj_q4M9F2Smn;;,~_4="B %!G I\$$r@b$guQ8 ~q@"k$@G5) uc@Q@I d )@@ȁ3 ap ( . Wֈԩ}\R. " KXQAD1gRJD5xۯ;q Cc ,K01v&%"2MHƋmٷqG_!"d\gnI B)aפif"tΤE4LJAH`ł6σZ%pG'cҲ$4n@2 "D98+fI"5$iYf#,ѫ&/'sx0>ٿR DD DfcRZB rqeIIi ˒\DRC$0r&dR-0bGDEQEQ?7!a>4M9fb^/RLHD@@DRH`!A@R @ 5D@D  \Cp  Iv]>s_Wݕݯ3%XɃlj01;-Lll` cpD <p F` ֮D ȐR pHDBD"t(D<$" Q!R YDSDEs4C(" QE= 3*" ;kDD!DϚ "|o-H"{$I۝Zk݁GON( ";AD "7'pH>k)y""GGBSDOs5ʯƍlȌEQy$v\k'h#mEQy$v\kkyB??Ǣ(^It| yç6:  "DEQ) "{$I۝ZkAl.<|j( "gDdf9vX]Z"GQqZ)%"kkkkkkkkkkk'Iv{' x,&QJuOOi<꯾xLD=؏. "0x$v\k=Omt@<EAD,"cǎooֳg~~<@$"kkkkkkkkkkk'Iv{' x,&Dq?|;|ի^u_u~wMMՊ ""E^QD 0("B "Dx!@DDx!@D @D"/("D! "Eƒ",υ>In;ϵx߃ύ1pD!0"B "DD" " "OD!0"<D" "FD!x"!"@D">In;ϵx߃3GQtԩK.Gd2afDP1sYAD̙3y#"ZEʕ+D|j IDATΥK6kjz*ADDkeR kqEa00s.]""s*|Ic {$ɓ',3@جիBk a֞:u\BD0-ܲW׵R ovggg\VUEDɓ'fQx }$nw>kyB??Ǣ(nr׿ϟ__~zyk}82EQ |̙,!Tɓ'1ADwvv& "Bج^ODfADDk}<ϵ6kmժ*"X)smmmq\1̍FyND0Rjooa'In<7@جᰪb0Xk>3f3"9+WVJAVk2c `4M>AsvleiOs5 6O>X b]כ~_җ>eً_׽uǏЇ>O8Rp9~eA0y8Ëb8;NDaD !l^'"ٌ "N425Zj `"N4s9qY37^eAsJ=c {$Iaê`<͈F鴮k{j&1fMt\9nfY8z$I|>Z}b<~~pSEQ<(s7nh4u]78_̈OE0sE~?2"`0p8,Bqw:<ω "8 1"Bج^ODfADDkieZk6MjUUADDkiJ)snkk+,1fn4^/2"9綷R{{{>Iv1fUU- 5Zy697i]J)~ssjM&c Zr'"sn7Ͳ,1pIt| yç6:  "DD){0Z)YD3GQ,#"3â(!Tq "`<#"ZDd6@DifYYk4]VUULDiy9綶8.a`F,#"sn{{[)g0x$iyc lpXUbZó  M)GZy697i]J)~ssjM&c Zr'"sn7Ͳ,1pIt| yç6:  "3GQ,#"3â(!Tq "`<#"ZDd6@DifYYk4]VUULDiy9綶8.a`F,#"sn{{[)g0x$iyc lpXUbZóڱcD1MRZ 5kmglFD0h4Nu]+ `V5L10kmr ιvl6˲4'Iv{' x,ӈ3!DTJ^Dֈ("G(YaQ̌*fyNDA`0fz"2͈ "Z4M,ZCجiV&"Z4MgEQϲ <`fDP1sǝN's"0"x6kmlFDZiefMtZUUED0ZiR ڊ,Kc F˲ `ιmޞ1OnynYkaUUBk =g-7ڨ?M/VQDYk>3f3"9Ft:Z)onnZdbYk4].DapεfY1>In;ϵx߃~˗;}3<\.Rpn}jn?{\PD9:3WUED0}W^Z)oz'zڵkZkFc:Z&"ZӧOc9wĉ8/_l00svy9tRa;vncc Z{̙nܸC`koF6`q'\ڏ$GZ""sn8^rk{j0@O^.O?4As[[[fxD9}$nw>k~D@Γ8vBcQD7!b;~{{~~mo{ۛ??xӛ5_5Ok("x }$nw>kŷLLXC D(YYkn?7M{x[k'$82EQϲ <`fDP1sǝN's"0"x6kmlFDZiefMtZUUED0ZiR ڊ,Kc F˲ `ιmޞ1OnynYkaUUBk =gjm^&Q}ffDs΍FtZ׵R lZ֦i\.kf,Kc =}$nw>k엾;i4"(&QJVcǎ>yUU5y~oo(1sE~?2"`0p8,Bqw:<ω "8 1"Bج^ODfADDkieZk6MjUUADDkiJ)x!?DYs[[[qei00szY93@I<ύ16kp8jXh"ڟwzZ:XAsvleiOs5!f "xQJ9nܸADιcǎc˥9ID#Q,ˈ( fFD3qt<'" `0Ljaz=fDuYi!l4MWUUUDuy+yg'Z+EJDnrmmmq\1̍FeYFD0Rjooa'In<7@جᰪbC`kwuk6`qL/VQDYk>3f3"9Ft:Z)onnZdbYk4].DapεfY1>In;ϵָ 9"(RED)"bfAD8JEQϲ <`fDP1sǝN's"0"x6kmlFDZiefMtZUUED0ZiR >Dq#Dq9o(fS)uppCDh6("G(YaQ̌*fyNDA`0fz"2͈ "Z4M,ZCجiV&"Z4MIv1fUU- 5B[s[o\Qdz$GZy697i]J)~ssjM&c Zr'"sn7Ͳ,1pIt|]8Q@QD""'>u_zc__//ollfgYFD fEQ03"B9N9ADxɋ1#,psnkk+,1fn4^/2"9綷R{{{>Iv1fUU- 5B[s[o\Qdz$GZy697i]J)~ssjM&c Zr'"sn7Ͳ,1pIt|]8Q@QDR?S?5 Zַ|_yȏ+^񊷼-u]|AD1sE~?2"`0p8,Bqw:<ω "8 1"Bج^ODfADDkieZk6MjUUADDkiJ)x>;eI}@>y":fnrmmmq\1̍FeYFD0Rjooa'In<7@جᰪbC`k_t׭k6`qL/VQDYk>3f3"9Ft:Z)onnZdbYk4].DapεfY1>In;ϵָ 9"(&!WկʯW]uoo{_m>c3f 3GQtԩK.Gd2afDP1sYAD̙3y#"ZEʕ+DΥK6kjz*ADDkeR Ջ\]胕 DnjMιvqQ37~%"9ݮR'4@I10kr ι'O6͢(1!R }$nw>kqww1sEPu;;Їz衟E_e_eY{ッ}k[4nGD"t:/_ZC0错Bfĉ/_&" b';yTUEDZw˗/+ lιn{pp̆uy IDAT3h!`"z/_VJAXFWإS܁%WOj5+W(:y˗0\VJ=SDa`fy'| lιSN=3J)-[gVupg.Бt:\UAOzR Odcccoo `ιn[k״ǣ(r gMDDώ>In;ϵָ 9"(Y^=łn??;w~//:u{|AD瀙(YaQ̌*fyNDA`0fz"2͈ "Z4M,ZCجiV&"Z4MgEQϲ <`fDP1sǝN's"0"x6kmlFDZiefMtZUUED0ZiR λoozqIٕy":fnrmmmq\1̍FeYFD0Rjooa'In<7@جᰪbC`kwuk6`qL/VQDYk>3f3"9Ft:Z)onnZdbYk4].DapεfY1>In;ϵָ 9"(׮]KlsDZ/V"pd9~eA0y8Ëb8;NDaD !l^'"ٌ ">}teD7"B`iV&"Z4Mkqww1sEPg""ދZi"p9~eA0y8Ëb8;NDaD !l^'"ٌ "٥ P@RQXaMtZUUED0ZiR λo^R/4)[}-cFX&VeYc h4z^eDsmoo+1}$v;sc Z;Z,Zk89{]޸Vkb5I3l6#"sn4MӺR0f՚L&6MrOD\n6eYcy$v\kp((> "yQ,ˈ( fFD3qt<'" `0Ljaz=fDzgh'x_~zzXk4]VUULDiy{_ԋMcG9qY37^eAsJ=c {$Iaê!{q׭7ڨC?2XEMf<͈F鴮k{j&1fMt\9nfY8z$I|>Z.b(( "ND3GQ,#"3â(!Tq "`<#"ZDd6@D҃љ[E/ӗ}>^OSD 4MWUUUDuy+%@>y":fnrmmmq\1̍FeYFD0Rjooa'In<7@جᰪbC`kwuk6`qL/VQDYk>3f3"9Ft:Z)onnZdbYk4].DapεfY1>In;ϵָ 9"(YDs{^kMDF"pd9~eA0y8Ëb8;NDaD !l^'"ٌ ">s˙pu4陽O_FpXk4]VUULDiy{_ԋMcG9qY37's,Wv[k}kwulK:-ٌAa+# NqČn`[@pR+Dn\׍l8241wGji9޽[uRJMj׹Z_l}DYkr4iι(8NTk ~3t:(fR t/>Jrrx jh2 "xZvqYRJsnuuummm8jcƘ$I6"qW<ϵְsQ5t^Qe"̬f'OG:thkk=wZ~s.K'" j}DoQɲ+" ðVifBlll !l6y2 "xTcѳRWqg4}"@1?1I,(b_G?#//82,03| (VADN'2"B( Z)"Y1 7cLddfVR{}]K׋| 0$IX,@D3+$ITJ 9:'5FzY֮a@DJl}DYkr4iι(8NTk ~3t:(fR t/ӥr9[^xmYUdfiZD4L-Pxާ嵻ًrybq]5xRIi*YMo.vKjϿǃJ01<`8 ,˂ ?QRi67oZǬz]Jy֭ q -n[."!`oxϙUn廕P3~[.&d1c̱cqYRJsnuuummm0A[.GEQhƘCU,˂ WpEQh4өRJz=CDA@e0[׼r|[kk7=NZkx\EZkZZR ADIF#"B( Gy$sD?0[[[BСC\"YIJZo/+jvg' km,rggG)cfT\J GN|ۆ[.77Gbbx9VV1"R:t(sxZDZΝ;~ 0 <7k#Gvvv77]\޷7~K;  Z[Ո( D9${nYRJs[n!"xZl6Ž{R+xo$J[??Ko BG pkkKk ~ `0@D𘵶VI)[YYY__Zk1fccc{{J)#ҽ8u|TZ.g?bPEfoƘfIDwAD𘵶n߾}{\J)cιeYc92_|ED?Xk׫֖j,`bB8(j4T)%z!  2D;s//o?}sO<[ۯʯq? HEDJjݼyDv! !WDR>"Yn4Bߌ1Vo߾fFC'Ʊ B{?E 0=ztXܽ{čI})%< ׽bD ~?qaeZkUfyMDYk֭[Zksq}5vUJ)`Yo}Wvrw+U$f9|0ݾ}ccǎb!9 5xs|^"VYi&`f!^ \EFc:*D׃=DdYڕ__RJ}x׻ggЇ._}8paQV#"x:NeD$_QZ-MSD?0bccc0!oƘf̬̓jkŏ.v;saIdXE1fVJ%Irtg}-JSol>+kzykDTTfGD𘵶^K)G(4M7cL)b6)``JwOTZ.g+7̰ߌ1V& "Ǭnw<e)9VWWֆáooo#"ZqZ\k 9EQјNJ)`AY!"n=].uwF.]/ 3$b(c̬J$MS)%< ' V40 FD6js.F1NR" ,CDx fVJe9ϫVVVݻJ,WWW,}CDAZ~ "t:Y|EDaj4MB` c&3O&D0}>?z]*]^;(f1&IbQ"ǘY)$IRJxѩOyrT(M^|h&ay?QRi6~cz.FZks.84Zߌ1N(l=B) O>uRi-\6,*2~3ƴZ-"L&vx\<[]]][[Zk1&I|kmj5s5?\EFc:*D׃=DdY?cf)9G){R9fB~" ZVGDu:,ˈH"0 kZ~`f!`0Bߌ1f' "YI>n=].uwF.]/ 3$b(c̬J$MS)%< ' V40 FD6js.F1NR" ,CD1B)%`f3K)R@{`QV#"x:NeD$_QZ-MSD?0bccc0!oƘf̬̓jkŏ.v;saIdXE1fVJ%Irtg}-JSol>+kzykDTTfGD𘵶^K)G(4M7cL)b6)``JwOTZ.g+7̰ߌ1V& "Ǭnw<e)9VWWֆáooo#"ZqZ\k 9EQјNJ)`AY!"3 !s9TEZbq}"VQ# ZVGDu:,ˈH"0 kZ~`f!`0Bߌ1f' "YI>n=].uwF.]/ 3$b(c̬J$MS)%< ' V40 FD6js.F1NR" ,CDx"It:#__|O~BODA Ne !WDaVK, B1l2d2AD3+sgݥhܥEbc$Y,EQ "xRIi*zęg/gKҔgfbxZaZkU*f .||=3 /|>s;kR ^5# ZVGDu:,ˈH"0 kZ~`f!`0Bߌ1f' "YI>nrftwF.]/ 3$b(c̬J$MS)%< ',ҔgfbxZaZkU*f/~ɓ/_d7}{{gy~t:ZWB{kCDAZ~ "t:Y|EDaj4MB` c&3O&D0s9'ϧeoԿV;(f1&IbQ"ǘY)$IRJx7_SZ.g_> V40 FD6j^9DW9EQјNJ)`AY!"d2y?_w]z'omm}ӛt…]5s.zB_3#b^\) $IF !WDTV%Yk:EQ "x7֗s53ӏ}yzam4rggG)cf)e\J ;{ķm(ssmU?8*۷@DAj<ϕR1kmR;w "0O8p+=! ZVGDu:,ˈH"0 kZ~`f!`0Bߌ1f' "YI>?toO /޺_"@1?1I,(+kzykDTTfGD𘵶^K)G(4M7cL)b6)``JԻ5U?oAacZM&DYkx<.RJ sέ C5x$|>FD6j^9)%ι(t:UJ^{(Ȳ %1Fmo{[^{4_׽uoWVV>OR(VADN'2"B( Z)"Y1 7cLddfVRGvK]ŝK׋| 0$IX,@D3+$ITJ 9:3>^Ζ )76ςv}}= <ϵ"T*f#"xZ[ץHk ~pEQqZk1E1͔RG0;ɧ߿W*-啋ƛEPEfoƘVEDcn;˲RǜskkkPk 3$I2Ϸ`Zy看hLSz ,`f!vgg{ jGyDB7DA Ne !WDaVK, B1l2d2AD3+sgݥhܥEbc$Y,EQ "xRIi*zęg/gKҔgfbxZaZkU*f"7dYFDBaX4ED3 !666fi6wz`Ƙ$IEQcfT$iJ)a G>qR4zY֮a@DJl}DYkr4iι(8NTk ~3t:(fR t/>Jrrx jh2 "xZvqYRJsnuuummm8jcƘ$I6"qW<ϵְsQ5t^Qe"W#" j}DoQɲ+" ðVifBlll !l6y2 "xTcѳRWqg4}"@1?1I,(n=].uwF.]/ 3$b(c̬J$MS)%< ' V40 FD6js.F1NR" ,CDx )l6;}{'|77?zK.}ӟ}oo8peYafQV#"x:NeD$_QZ-MSD?0bccc0!oƘf̬̓jk_N93;saIdXE1fVJ%IrtO|kBiJw`E31<`]__0s5*Ju)h4ZsQqft:EQf3L_8 ͧKrrx jh2 "xZvqYRJsnuuummm8jcƘ$I6"qW<ϵְsQ5t^Qe"K!y;ww~w~S>?]͛??}w|%D䜃 Ap7o""xp8$"!0~,hi !oƘV̷oFD3+:;ݧj=y|>-Pxާ嵻ً(f1ѣ( D1Rѣ~_J 97~k%j\_A֮dY?QRiZ7oDD𘵶hH)oݺ?8(:tPZߌ1nݻL){S7oxlg*w?7ݠ̰ߌ1&۷o#"xZ{رhT<[]]=x`0Znj1G6"qae^95ι(t:UJ^{(Ȳ %YJ\.>~K_z{~[[~G~֭[??3ϼNZkx" 0czJ)%I2H"j9"Yq᭭-!Z{!f.,^׿z{7|^ ÐkZF1R """!0x8""IdY&Yk:3߹s̬J׭椼og~vFJKfË_TJǘY)up({CoztyHt_0u"RpYkkۈ~ Gy$2DYk=/޿_J {3WXLJ\bfm4DtD9666&IYRJsnuO>|x>(ιGyZ޺u b]BW9EQјNJ)`AY!"b:~>gy>?~~YJ _ 3; ]DTTZ͛7ADnw8|ED+++Z#"v;MS!jۈ`f>?ti[>?E 0=ztXܽ{čI})%< {۩nZ-/_V80̲Lk ~ jl6o޼1kmѐR޺uKk ~p8p ~cnQJ)#,K~7z@0Jac>LDoFDرcxXH)cι=@k 3=zt>EkCjeZk !YW9EQјNJ)`AY!"rgg糟y~W~/؏82,Aj>"7dYFDBaX4ED3 !666fi6cF4RJz+++5MfFQQQ bs@#%owCnX:.[Ymy@ " hڴRNY4͖-[֦R)d1)rgϞ4 aDQ4MkZ5MOJp8|>_$cqq1d(t]/cBDp)t]oڴiEER+RfUVV ! ;"6o޼ !QPP@DuuuBDoqE]W2MkVXq4P:'aFaaa2 BbD9/,,d}k:.Ks{i (,,L$Biz^Z]]i\|RJ"!C):~!2y}}ɓ{/˲eڴi3~x a4Mkڴiee%h+Yּ*9d"B͛WUU!"d74}>s! g-깪sQ2jhVx.5F4f͚%F9d1"VUU!"'Ji`T/Z R?Rls ! ;sbRʼ}ZYC)ժUJ"BRJvW^^.@Dزe3g "d70.2"B@ "W]qymoouh<As"a4o)zvٳg5M젔Z]vӧL,((`;wN4RJәW^^id70ZjU__F9琁FJ9%Wł)T4C?^ " h֬RVY4֭[Od1YLJvsss+**4M,fFx B@v0M3//j={V4@( !t8>/pα2RB/B?~ƌ;w6mڹs&MԷoGQM(vUJ8v= "d "r\X jFQDlt:(d=n'd2rYs!gsm*f {(2J!"d7Dt8H?ZZ7d sc\0"bb1d"H$ňj"b<gAv "!jD"1nJ)˕L&MDD@%_{ fZ"CeS\ \lJ)NDd!RJD,FDFcŔRCJJY,y,cRJ"?GJp8|>_$cqq1d(t]/_92eСC^xo;vas+D$lJ)D)8eszDCRr0 kJ4"c4R cH)93mIM (1لRszJ)`A6!"'R2;#iiMc+J)`A֓R2e0 )0R cp!("?GJp8|>_$cqq1d(t]/_TM6+W\~7||w7nt:D )K.h!"%PKAA\rɿsBJp8|>_$cqq1d(t]/_FDP(4tБ#GZ#GpX !"\r%\r%\r%I)D"s,.. wc,X,s0ƈK.K.K.RJ"!C):~!#D$R*t]7M.K.K. t8>/pα2RB?01RJ"rL$"@DιsNDRJD "B "H)RHD1ιiDcsJ))%!J)Dι@D "!ADs(HD(R*%D$@DR*QD$$"@DιQ!TJ!"d@JBD"#"3@)%DD9"iD9WD$ ")%"Bqc TJ!"18i"¿QicsJ))%"0Mcs4QD$$"9c 2J)D81R*s`&!"pΥDDs"R""d1RJcsJ))%"¿QD$$"RRJD$"a&"18i"!HJ ,2J)D qcR""4M"BDK)9D$DD "9c J)2T8icsD$"bRJ "DBH)RDsD419"R?81R*QD$$"@DιsNDRJD "B "H)R9GD0M.!2L$"4MD$"a&"18i"!HJID199D$@DR*QD$$"@DιQ!TJ!"d@JBD!dID"bqD9gR)D9)%"q4MD "bq@)%3gRJ"ED("RcsJ))%"0Mcs4QD$DD9LJBDpD sH)RHDsR""MDD "RRJ9c ~f&!"KH TJ!HJ Ds)%!"!BJBD4MBϔRRJD GD("RcsJ))%"D㜛IDDB ")%qcR""s8icsDd&"0MQ!TJ!""rL$"4MDp|H$9bPJ~_ !"H8{<]׉1LZ,KD 21 nەRHDBp8DG4)%}z˖-Dd"/hlldgΜ/m6RsXRRr-[Ը\p8ܺuo>bseD9D"W_}?/讻zMӢhϞ=SNY,DFwucl`׿u~ ؽ{d2p8駟VVVZ,@D9B={ԩScc֭[~n/--˿m۶ !RbٳÇ8cLJ߿7|sIͦbB~p 555~(r8P]v۷ߵka1eD9_wߝNN8۷o~͛7GQDL&}*++u]Gx<޻wX,vaͦB466vرgϞx|Ν?CNNN,+))9p5M Gi1E[nb_~yiiG9| W\qů벲2]cx[neee`P*cԨQ͚5;y;c7pCqqR4̓>}b-D$7|]wwy)!۴iSYY!3sDLR9zfSJqoB۶mlٲwacÇ1.16jԨnǎTuC =zrvڵjjVW^ٱc?0H04M]>_Vcǎ]wR*J8pNDp!H ;ut]w%?B 0m۶UUU~*""0{ĉMӈ(NfSJq΃`׮]wwq̙;nsO:ua@DDFn۶s6o =ȑ#srr?ٳpD".]ݻ3RTNNN޽>\]]:!b</--ꪫΜ9e˖xf"/ "y0ۻuo~]ׇںuq˖-`cO>BcRsOyyW_}eٔR`0XRRҹs熆[>pٳs Jnݺb˖-HiӦ#Fl}t:poᆲ2!"&Iww۷^40Msȑ͛7?uԎ;e]6dȐܣGٳt*~q΃`޽;vnZ]]mۇ^XXx;v!ɤjٳCcLJ9߿';ͦb{7MMMl6СC~͛730."RQF9?{5MO>Db׮]N`sVfCp8|7jj׮]K$>oVO?ݻwo^~_R))e"8ru~eH)GѼyӧOoݺ5- IDAT7o޼TӴC9rrB֭[_{{u1Ś7o~mٳ' !@J F?oٲk;SRĉ6FDD"qejgvXYYD">[o-++c!b*ݻϝ;:@2,--+O>m۶h4z嗗߿v+.p|H$9bPJ~_ c,teٲe|IVƍGlٲeҥ۷۷Ax)/Z/(( P(ԫWŋ۷뮫9sf$B z~' daaڵkt:ݦM7klɒ%wСCEEţ>ڴiӒɓ'ϝ;w͍W]uզMÇ7i҄W^zSNi֬Yz̙3D<Æ +++x}k=ëV`0XZZO޽SN~9sɤj}W۷oߵkX,i2X<kV\lѣGxsٵk׭zsjժgϞ&L"b:tXbűcVԩSkjjh̙o{uq˥ ̙ӷo;+W\n;>#'N޽ѣhC)eGyv… tr޽{ז-[V\rΜ9cx/n+oaEoW_=ve]fFݯ_ɓ'ڵ;(++{۶mۧO#F >/t8XK.7n\bŜ9s (w^:tЕW^'N8piӦ9r:th͚5En>^۶mo]WWF۶m;eʔ=z<գFza K/ԣG.][,/W/JJJ}Ç[0ZD|Wݮ FoVXqqr޽{O6mΝ=z8ps=צM^z#GĉopQ"7o^II{jժ 6kǏ{ϟ{H)0p~,X:TWWO2;8ЫW7xc7tӀw޻wﺺ:MbСC_yӧ]6??#f͚r-'NxfΜٷoߝ;w{۶m[p "@RJMVXwԩnݺ͞=|7\=i$4)S|GgO$n{ڵ^{,0 ӹrJXUUUΝz衢QF;w.//SNC=rVJ/K$sݻܳgϷzk͚5ׯ7Mܹs7x3?w}̘1a 4t*L|gKKKuV^^nۉ(J-Zsݻ%KN:O666Q>}Νꫯ噦 4M˵j*8w\NMH$V^}С-[)S;sʔ)W~DAAozm1J|5k466O0cǎ-Zh۶m~[ADp!cPhĈ?xYY7|ə3g>쳿o>Ӟ={.Yd˖-;w4hЍ7XRR8Dbر-=zۛ4iBDpx衇vս{^ge^zر㡇UJ "4_x?%%%K.ݳgƍ5\3y~G'N<}1c4iN9˗/[tب:e,_+/KJJfϞ]^^jժj^~ӧO?}b!"p8/pα2RBE9?%Kn۶mcǎŋYfݺuƍ{ᇇ rOFAAСCϜ9999onw^z饂4SڵkG}nX~9s6op8DbժUm۶Of̘D"O>u]7bݾiӦ?'ի+/^G*BpV^Mֵk׷z駟8qs=}3g6l̘1%%%c i^~ׯk֬7MS)~ ,;VX~dzg3f̙3g&O cӦMѣGiin߷o߆ :utرg}w˖-=ztvFy5׌?O?ZJ 6\~{}'Rhw;vlaaM6lذdɒaÆ=ss].R .ܹW^YjSO=5xƍ6lW^ǟ{zs~ѣNgcc;o޼G}ɓ#JjjڵHTUU !.rmݺuΜ9[lYlM7ݴuA <8Jmܸ[zSO=թSC=C@`رoϞ=;mڴjŒJ:tlٲ &=ztѢEڵ{.\lٲgVWWqMӈ29?y~_w}]vYYY{k׮˗2dÇҥː!CF?snXz&Ml߾}>OJ_{X,6ykfӦMS=ԩS'Nxĉ_|ꫯyfΜ?t=77w.K)s~Kl &\wu۷o_pȑ#W\aÆx`ڴiC 8p`߾} RYY 7ߴZoe LL֭;yYn馵k>SӦM{뭷>w^D tzΝk֬yW}q-_|ĉF|u]q-1fB^z=suuuO/'|rM7m^X,D(LmvŊcǎ=~MOn׮ȑ#}>͛ׯ_W_=7pի-ZԤI@ SO 0ɓ&MR"b<{N:tП~7`7.??_kڵ_w:J) "D|K/nݺgyW=za׮] ^xyuĉcǎu:`pРAs̩yGT*uWYf'瞾}ZX,|d29k,!DF]t?#wU^^Nsrr&LpUW?|]]݌3:t /' .Y[n_}ɓ- bq 4hx|Æ ~nrʵk׾;NS)DO744{_}ٳ˗?SN9sÇϟߥKѣGO6{RRD"Ѯ]Tjɒ%۶mkҤa{7_y啁.^xС555Xl̙7|>FcAD)yf͚^{G}ԯ_;s7Ν;cǎ>}mhh8qC=TYY9eʔ뺞L&;vnر_~/XXXwީSu]n{?͛^cE";vܹs&L={ŋL2~o_.**6mڬYtR__?dy$[.]y晃i.YdÆ ǏϟK/3رceeeǎ7oRR:D8X\\ J)] !!"M7TUUu{l̘1O>3~ߞ={yoƌ3N:-_|„ rժUD"LΟ?I&dRӴ5klܸqΝNsʕ7oo6lذiӦ.]̙3/ "]imڴ۷wޯ?>bĈ~{n{ժU7o޸qW\aÆ+WJ'xoϋ|AәJEv9s8+++;|p֭m۶yf%McDwtdƢEŋO>}`WՒ%K&LPQQqw.X`Ռ7|p8qb'L\u-YO?Ynݒ%KRҥKynmϟ'"u I)۷o:4rȅ >#&MZdѣG=Ϻu/_{6mڬ[n3M/**xεaT,`5=1DM,ذQT"( *ĂcIb%Fc5F`]zfd&g1{]nv]UVUUUZJ?zhÆ vnݺQR!"!D 1j?.ݻ>|XUUէOٳg+ɓ'><88㸄={qaÆoO>ŗ/_V(vGyyy ]tIOO߼y Ο?jM&STTԛ7o!P(_HԣGׯ_;wn޼yqqqIIIqqqnj֬YEEEffw_^|Y5k(͖f(ׯ?tЎ;Z5kN81q6mTWW8pA!@ށ,ˍ7?|ZgԪU8F3w܎;FDDf͚͜9SP|%%%|v}Jn2dHLLԩSc~aDDĞ={O:XɄ@)yIjjjΞ=4jwwgvu ,8$IWj/^e9%%f_~yĉm۶iիW>}zر7n!@ Bݻٳg_zUQQwޠ0`LbXƍg]]]o~ǎtvۻl޼y?nٲի=sZ-rttݻw !RB /ЩW^>ӧM?gϞnzڵ7nׯߊ+f͚uY7nh2DQ|٢E4ibAشiӖ-[<׮]|駔RѤI'NAcG?bΝ;7dȐ3g*cN0a=x'BZly~gϞV:sJEEE3f̨-**:zhddÇ]\\ CttgO "@>}_~͢,]t޼yf2;v\dɬY>~Ϟ=3gj$p̘1fR*'N5jԴicٝ;w?~^E1<@1w~1Ys) 22RRM6mȐ!!!!<'%%uuʔ)ǵo~ݺu{ׯŋwBa{ꕝlX ?~n'9CD )}>|zj??ׇ:oō?h4*ʯ~kٲѣGm֨Q#޴i 6ݸq}ӦM-:qD>}֯_}9Np8(AߑeI&;vgΜy-[T*վ}&NB)mժUYYYfffZZׯϟi""y,kڦML& 81A <}y_RبQϞ=ƍ7n3gγgϚ5k_^re%%%3g͛7a„ବ'O.YV:|JZz3g tƍ}]688㸌 Jp"R G^dիKKKu=zTє۶mݾ}{YSV IDATY٦M>ŋϝ;o߾}=ztLL'| //K.T$R) ***8|r77;w[h4}zllf.,,ҥǏviӦݻwT+VFaϞ=VݻwTTT||Vp–-[cRnc ;w^bn0aBTTT-4͌3uh 5jҪUu֝l6Ϟ={ҤIcɒ%J2;;{ӦM˗/駟-[ٳ={FFF>{iӦǎٿ֭[kkk7omZ#GjD$R=k֬۷o_>""ŋk׮MLL? tҰWΟ?رXlY=zԣGcǎ7n;ɓ?B8sLee%Ȼ1 ڵ+,,lݺQ 2bĈ3f(#FL:u鈸`>h*jժU555+WܲeKRRҽ{ A^zյkװ0кu뢢'N̟?;wrgΜi2YV#" D ww)Sl%KL>m۶˖-|ј1c222&N޽{\,--]bGzꕝ}ENv_5,,rժU˗/oӦMfff\\c{Q((v_~/^\b/_hv{VV뫫unݺUV #G7<|qもjjj\]]_~RBBBz5cƌN:m۶-55j:))ɓ'_V( "`0xxx9rرiiiSNEsyyyaaao޼:ujTTԸqjkk󽽽cccׯ_};wXǽ{NHHu떋ƍ,YrђOOϨ(<`3gJt @ c>Xre}}}PPP|||F5MDDܹsz%KA1c:?XbҤInnnjzxxBcǎ'Or7lذضm~'7n~ZFDo C֭F6m4>Øӧ[9s7nر%Kpe˖_~eҥzٳg={xEӦMKKK7n8o޼?ydܸqeee>5kjݻwcj5"w ˲Vmڴd8cLBӧOy'5 b&M;vs̙Ν;'%%?~wKJJBBB^~]YYYUUT*5Mv^zs 6x{{[ݻC},Ν[x!CgΜ!W1B3y似J77͛7ݻwӦMm۶ݺuҥK[__߭[^믿^vQ.\дif͚ݸq_~/^6lXMMMJJV]|JtO>ym\\\Ν7n͛'OfѲeŋ7k,**ʕ+}]tiddduuѣ&L#G<_YYj߿ߺukXLLLK3g^zڵk_߾}yoo#G^zذa,߹s_8Y5Mrr~߷lr˖-eee~mΝ7nܘrԩ.]TTTdee:u_~/_hҤIvvW?xՂ&M.]HT[[ä>}h4/^ORJލn۷oQQ޾};wܡC:mk4>`ѣF*//sf~zVVր &߹s'00pK,1'OtK.}7|駟L/޼yST""ꚕիW'Nnذ!//ɓ{^zuDDĵkzv9sܾ}{۶m:tv횧NKJJeSNϟ?:u#GJKK=<<6o|С?~n2$55522rРA ɓ'Ofy7cС .}Ze v{ǎM&STTǏǍ5}oFFF=zܸqYfM֭[,GEE)ʝ;w>yСC}1ˣGA@DGN,YRWWɓI&͞={鵵QQQ#F?~|xݷl͛ox"!!GZf&$$\pa֭رcm6[~~~~ fX:) ndYjM65LA@@qb @y)俆RZ[[RWW޽7߼|r̙WرI àA&Mt֭[zA={QWW'˲dZraÆ>jԨ:NwaJŅ0L#G\fͼy.^آEWwĈ&MJNN?~|uuuve˖}W۷wuuEĐ6mDGG?~n744.]tgϞ nnn{YvheYP4jԈR@GҺ~IP(={o߾ .,XRl1c,\>:]|yA円۷5*"""<<|ƌ$)޽{%%%9sT*^' CHHH^^̙3ܹ~-Z3&..nڴi=ҥW_}tرm6j"##u:]ll˗/EQ4L ,ZjUQQxƍ{e˖֭۶mdb ШQ# 8p파f͚yYf|MrrS?k׮BZ׹sQF5o޼iӦ&$$,]o51Nw~)--oZ?c^^^``(w.---**RՈj5c ȟ(uuus yEƍo.ԩS/^?͛7۸qcXXy5o޼$aѢEAAA#Fׯߒ%K ߿x-[Rtqqnc*ѣ?cqqq˖-vXCC$INھ}{IIݻ7o޼?}֭֭SՈU*c ߡ͟??&&&$$nnnUUU[l :400׽{޴iSxxK|||\\\8l555(644O>}={\bEtttAA#G.\XVV=vطoRtqqaΝ b޼yzjʲu9rEGGO2""͛G/.\xV$]\\xGD Rj06nغupAc{-,,ܷo۷o#""188x| cE*-==+W41Ͷo>R9q?|РA:uZ~}dd 5jd6F#h4V@,zȑ#\|Ǐwf͚/^h֬Yɒ$T*teȿ$)88xԩ/_t<ϗܼysʕ'NPJ===֮]믿 >>>''Ç("VWWoڴUV1ެYVZgIVkFF͛7U*:ɲ 8^zeee Rh>\VV_tWRRT*)<(++h4(Ƒ#G&$$T*B(M:U۷o~zѢEXD8R,#"%IR- J%cLܾ};--m̙AAA$UUU0GDDwzzBp8fC)))-z^'X!C$&&BY,ΉR ;p8NJJQ*j׮];v())8n۶m6lPTJr…gϞݾ}ZEd2}G72qΜ9cƌeeɒ%?.++STeYFD A5L:h4&&&v%%%Eׯ_ÇO8qfY$֨QEٳl֬"Z:溸@AAC }=ZM8dYDQ-((eRh.]駟2Ξ=( ,8n…FEԫWs禥YVJEcƌby&::533 A`cz~ѢE͛7lZիW ,O$IzyBBBCCf8qdYEj6o}}Ah׮ݻwQDl6#"1F)j ^^^7ny^PBNZ3*y!v;!SNw޵lӍ7AP(bHq"Bj$IΝ;v]V#$IVURJ8fU*,˄ebԼzťARVWW˲R?3̈́J)c8Fc49UUU)J!v]$VK'Y-Z81F*߭[$IRTUUU8D@1Ͳ,sFcەJ͛7eYV*Qm6Vq2 B!DQt8ϟ?FǧT*J$Gģ:j۶m"QJ577}]vݳgoa b۶m{Ѩ "y\_y^III~Bлᄏ}v۶U"f2d2ٱcݻw_^k  >M6EQAD(QJeْc={ヲH$8NUUU"ؼyaЊD3L;tJicqgG}yX,̈&"l6 c͈Nw~Gd25ky˲xDr\vر~z0Ld2X죏>RJAQk >í[F"ڏ?X)NDq׮]۷oܸmffDbrxQGm۶MkRSSsG[o8p 33"BkZ?ڵkw1xm׮]hGDR}---ׯOӆanݺ@SSiЊDD)fKKK;Gydn_B3#" "3g2r!6m2 #ݻf׮]oiDR F7olud۷ E%"JD"ѯ_?y={B!q:u>4kYրB|m6fٳg6}D᫦m;H8d2 lYR)0ࠑdƌse}]v3g/\[[K.9?30|ɋ-ڶma_1cgСC3g^ڲ,(6i׮]޽g͚5gΜ+Wϟ?o߾{)//0aEZ"f2^[n=×,Y_r޼yrʎ;ԩS}4p8x.]d\.w777w}ĉ;Yꫯ䭷Z___WWw>/|ڴi_~}7s+Wfqf6lȑ#kkkǎO5jҤI#FHRg޾}e*++Z":~ժUsΝ:ua,X0wܡCoׯ_ח13U:K~]tEl_jՓO>`c=矿83CkZ<3˖-[tٳ-\pҥ{ƍ?+7xceevM2N۵k矿s|+޽in-[D"(6?qs4hЙgs뮻K.y7ǍD2 5꣏>z衇RO>yݺu#F, Zh9'xb…<{|nN۳g~xŕZk(mۗ_~AD{nѣG]wu=ЛoEZyD_:u /v .SOM$O> zꩲ2fADqnDה IDAT"1rȣ>ԩS}ߟ9sfϞ=e˖t:=z1c\tE>>Gq衇qÆ 6lFEs\Ϟ=Nz'\K/---}gz%K 8sΙ1c@Qrnݺ=SǏ_z?yވ#"ҥK___?wD">߿?я.ŠG}~ws}p (?7oȑ#7oc[.N}'|1?WKKKjZk۶8J)L&P̖e@*2 kΜ9\rɾ}z`1cd2W]uՐ!CvmD}ds]vm"x-Z4tC9w9Ӗ,Y#D"(*Ap8\[[{w|O?ٳ=ܭ[VWWYo}0s84hoۏ?;۷I1>SNYpo~H$E%"\nԩW]uU$ꪫjkk'MԱcɓ'bZkh{p±cܹ#Xhѵ^;gΜ?|'pٳ?ٶPT"bF(4hаaZ|9H,{{9˲Dx9}O=TCCòe:u4x:kӟԱc &B!fFD(r#F?j(0 vg5r ᰈ@+}.$ .\m۶L&ӧOӧ]623aYI'4mڴaÆ555-]cǎڵ{VXa"Bk}cǎ'pC=>y%K0sn݆ޭ[)Slܸ13=/X"/]7/٭[EKwqGEE3Ck\.7}x<>uH$2}t۶׮];{^xO>{6mZ:6 SN-//1bD~.]m۶ruo 6PTuڵO>i\r͚5&M??eUUUCH>?]yZZZnΝ;ϙ3'J]}ՕZkh-"Fy '?5 cȑw?hѢ?:u W\qŮ],imN$(0LB3[T0 8 ={F8Nmmm6qM6s97p\k.41s<͛~X,vW wiӦ~-]F"EED͵>M73L-2eΝ;.]_bڵZC"d2PhҤIǏ:toG1nܸn瞳m[Dɓɓ#ѣ{=vP(hѢѣGkx׭[[ne{ر㭷zm͚5{GΙ3gb #"Xo߾QF?~DbŊ &y晣GHbs1SWW7~CUW]USSy"BH>ۻw7DP(4mڴO?2eJvЊ#N:qY5pO?Zpƍ+++?-[ᰈWMkmv"pG)d ٲ,HRa~7tӸqvy'^{Ç߹sYg5{ .>3 Z "_RR|Ekwu~SN9eر[lꪫ:p@"b#l6ۥK_W7xO?`:\ve|~֬Yrȥ^ZQQZ|С v:qĵk4(>ZKvs---ef(fSNرW^Df̘QVV6a„yb1cƔiU b>޽|~oذaL[o6lXii)3CQ:KF=pP(X/--}wXLD<ܹmVQQ1~w}n8q?ޱcnݺ-^D""E""\n᧞zرcEdر'pMӜK/x?߫Wh4:k֬_=O.7o8s΍ .ܱcǬY,YJMVYY""\/>SƎKDǏݻwss]꧞z駟DjZk۶8J)L&P̖e@*2 RDV\bŊ˗{РA\sΙ={y睷k.48=޽{/K/t ,8qI?ϛ7N;3-˂b#L&sa=Cs}G&NxWqw~W^{3gVUUi'"DN-[VRR2f̘l6#O>pw}g1M.*--ef(DlnnT*f͚|޼y ,m{ԨQZkh-"BD/s=w5,^Nxdž:|x 1Rk FD?5j777_x\|a6l`۶@"\.7af&^zу гg9ska@Qe~=#G|WV^O4)N6O~DDZ0s,{Xk׮V8pE]o߾{l?ڷo""q:nᢋ.ڸq%Ktr7ߔ mO>k6k֬xs:k׮]W~ǎSN}KJJDJD\]hQYYو#{x`޼y]w%\}"r]7H<>y8 00>g͚e0xh߾}'N3f)?ϟ?D{ݶm۔)SڵkVflٲ/W_}7֭[Fƍ~˗WTT03|մֶm' qRL&-T*e4Q.͛7^}իW}?30| ,HRK.}G~|I.\_:"r.];ux⪪L&>S˲DZֺ4;vo~ӦM7xt{vM6k8b}?r-ݻwO---'NliiZٶ=a„RfV!"d 2u-[vaӧO.]ZVVgϞ:̘1_D""ņt/1bĐ!Cp۷oܹo"BQ!<#l?L&8e]hȐ!r3#"39sر#?~ΝN<Æ "+͎3fĉ[lܹW\k׮{4t:]^^>eʔuE"BĖSO=u̙FڲeK~/^o߾ʷ~k}ۻw+Vl޼_|ŧzjٲe\ZZZݻw E\K.K,<_Ѷ˗KK,)++hEG[o[n-#G4hдi6oܱc{oƌLF)&"tMeeeǏ,kر'Nw>'x"COӒ{^y啻w,KD૦m;H8d2 lYR)0 "kjjvO?4麮 ""ADJJJN:M6}e1s7|?, "ey'0/Zk0DU)"iZkquݞ={&5kִ)"#"("?癦)"iyHD1WWWs1֭kll BZ^zUTTl0 AD8*p]}ߏb't֭[7n QDGDTA$1 Ck---DkGDGݱc^{Ų,1 \ED(|>ߩS>}]O? BܷoX,zju Db"2Mu]Z[uI'ٳgݺui " Qm#"3kc=3RJDGDQk:8[oAD`&3ЊDE۶믻kF$9Rƍ @DAD(*4'"۶D!h|߯())iiiQJ3&u6JkmvR Oq*USah|߯afQDRa466* Ц_]]D$"D mUUUP(m "QWTTi0s<ojjr] hֶm' qRxƪFU^y>ܾh?J6f}ED)eFccR mJ|>OD"BDP Ц_UU rض "@~EEEiii:VJ3&u6Jkmv"pG)gjTU뜗;Sah|߯afQDRa466* Ц_]]D$"D mUUUP(m "QWTTi0s<ojjr] hֶm' qRxƪFU^y>ܾh?J6f}ED)eFccR mJ|>OD"BDP Ц_UU rض "@~EEEiii:VJ3&u6Jkmv"pG)gjTU뜗;Sah|߯afQDRa466* Ц_]]D$"D mUUUP(m "QWTTi0s<ojjr] hֶm' qRxƪFU^y>ܾh?J6f}ED)eFccR mJ|>OD"BDP Ц_UU rض "@~EEEiii:VJ3&u6Jkmv"pG)gjTU뜗;Sah-"f}ED)eFccR EZJ|>OD"BDP *D DZUUUP(m "|D U_QQQZZNRxu]"@k&"X Bkmv"pG)gjTU뜗;SaDH,HJ!k"DAfD;D(`f$B/DkFR\UD$"DDdf/ID QD@D$Dt],h-0("J)0R"""2 R. IDAT$"("P "_y ~uuR*Q(jhh "hsDEY -""epX ~WUUB\.GD "m744"%"HJ!5)B(aEI)B"D;@DZQBI?"")A"$fDoD$"ADafAD ""—!"Dv]7 "||߯(--MJ)`x<.%H@5"JD0 ,H!?0kD"+LXkAD|HD"""oAD"H> "_?mۉDqQWs^ORa|%q( ^ej2B Kȸ %6ۜ#!YRs !aKZ#"|uD 2L>GDba03"#"HD躮aX%","_F&ҥ |'hQD@DH '0("J)0Rжd HDbi"_!yQ(fD/#v1~ǑH߅""BDR?}Z)HD( 544E3 "mۖe|"BD=#"˲Qk-"_ b.k߾}ee|-"BDRD$BP.#"۶D_"Jizxuyƍi "|I"BDRĎ?}4N+xSSDBDy}, `(+NWÿPܼ/OhĢav[y "薖'JkBa9dbEn6ApY,K75%1E;!"40s8FRň0" AD ѣ| "a"—$"D ẌmۉDqQWs^ORaO@k=cl_۶Oҫo^_w8}O<4mPSM̼_')s@L Y1Y-%_3"WADQDGdjÆ o޽{MD/FD|6ݻgC$ BD_ֺ[n/+bJ)D\׵,<0~MM 3"2 Q)m߭[}viƍׯߵki"ňH>f=z86l /X R"衇z:t4i҆ RkY3yJ)DJ|>OD"BDPmDv /ܺu3< Ba"7o^>}7xC)_ya"OD("UUUP(m "_nw?vqA̮^s?b~U߳%DEzV]Yt6{[OWN0`s9ҭ[7n;~RUnܹl}7nz!i@Dܽ{wXn̙3^"U^t`0x<*Uj9#R !8犢deeq>BD1ӥKDño߾.]xӰ+Vx^9 ! Կ^}T9{AD1[Zk׮)x*W\XX8{QQQs#"Ƙ1"BD /;O?ԩ&M:p`rBjժnݺ@`̘1'NXre*U/_t:9P>xz=|pټwqƕ(DPDsիWrE#WU2f "D! l?0mdW@|M c^m=^#h9gQMfKQ_f*}&\8b3U z%3ڋn[ۑ՞|kLh͜$S-;L .l=jW?!J߱ xɳykamϫ׫ }bǂ{n-Λϝ1sN^G^o&MTUݸqcVbbbym۶ 7n37h̙?5knܸnwv&Nrĉdá*݆PDsݱ%%%n[&,**t{vf\.9H)VkNNg ?i|a]aW}uOSʑHc7}zsM rE$Q~k ٚy&cV.}Ue*5k_ܯ5LڽvD|G8|t}{ =v4_ޏԫS8<|>_jզOnݺu>K,Yl(#ZjΝ/_d&NXj۷?^UU"?ED+++k֬ccc/_~E uhY]juHxcoQ{뽹JO?ZQ7Cs^{6([-*ٙWn*![n*yYUdGG|5Va}GEQh^{-[|ɏ<Ç vuEQ~fۺukLLLYY`|:o>;;[UU(!"J)#""e(PnXVV6hР>`۷0a¡CRRR6"&&FJ)@D"+9o۶YfΝ;?裡C>gΜIHHTA4͛իWZZj4N::ub܍i@D&MTRRRҵkcRJEQcqqq޴i̙3[lvd!"QQQs#"Ƙ1߲ebŊ_eO^r%!!ɓ:fddj֭[&I*TУG}l6)%܍i ZFjڴibb 8s:۷3fݻSRRԩ3m45|p;!UUz1 "p8W|^DA;!WOT[uarcۖm8ܸNM: +^\ݥ?ry hW3/@2_bu7(ӞފM~Ͱ'v=+;鋪m 5nզMzCDə_7zEфW^[zYN>4yi,+klåR\9ԩ+'UU@͚5-ZTFӧOϙ3cǎ/RIIIRRݻz= q Ν۫W|@D^7""bĈ+W B ")yotm}"J)9僈niӦ3f8}ѣm6[zzzŊBn\.V5''3|eK\1w,W=^.4xB_Pa2έk O<6G?ּiq"fHFXug:kpx^.mxYм*Z>F4;uZtq--17yɠ/>uZOyEߞ[@!Dhh… 5kvMin:EQN5jԸq "EEE͞=;%%%44TmСCRRҴiӾ "RrUReܹ%%%III.+55I&NpMfsXXc]Y<88csg!44( |n=a*oʦJ&N:Jb3Un7xȴ9:տzlQs;niY?9/ػSNy^ ;_zHä˾~c Rg@I B>o޼-Z:ujƍ*Uz饗BBB8-횦""PСCsܹ}=cM4ϦM?FD1[ͻ~ҥKQQQC]hŋFnceeerrr>f͚5h}---111RJ!"\Q,9/H48k֬vڝ?~AAA:tt-JOOlRJ)bׯ_BBիWwUn'xb)))W^e#"ƘTWZuԩy+2f̘5k;vd2I)1t: 駟֮]?w\^rssE;!8>1FD1UUc #RJD2eJ/_n:Dرcll^_~رcf3H)FcnFcǎ*U<3Ǐ4icp7@ 888))iǎ_~ӛ7o_XX8y>쫯2RJDa9rرş~iJyBpUU^/c l6;"BD_7flصiη9M_6}.wlP#cqNߣ -pj ]7lԵ>j}{FLSUM^[%)z|0I2Tq*5a3V ߷MPyK7dܜ>1=zQBҔ7C^Ith:mقO'wVw6[F*ټGf zW8?KLL:th^^ަMnݺ/׬Y`0ݻwРAx )ҶmT)姟~jZ_|E1iҤCqΉi:nĈ.\Xjȑ#{5hРǏ[l1LRJDAD٥K z<;v(ҡC7xСCz 6rq@JiZsrr~?c ++.NͮX6y̔ 4Ks#>cW{,qSo4zK /8? 2^^cճ:fϾ?Gv.o6;/Վvߚ4|ĎLV4q)Ó:,MSvZ4oXʮl4D,++zyf+q\>/y(w|ɃFF=hh|GׯX[eT"BBIJ[NU81dɒ5kO>d!!!Rю;bbbEӧ[lfr={)WTi5kUPa„ /c ?zYUU᷄111RJ!"\Q,9G|>_tt-[*Tp߼ysM6ÇwfI)O!4h rzmܸ1$$gϞ.]tDŽ6m…=ؙ3g@ttNs8B:u 8&IJ 4LFzWEAϧ9rDUU"BD-!DTT1ƈ1`}DaZ?輼aÆ9sf޼y>i۷p7[nZu֊Ι3J*7n\۷ѣGUU?i^;wn6mΞ=vfsVVU2dÇ-ʁEIHHի^G+WL6D[BpUU^/c l6;"BD$Ar=[F.?2*o`guo?)n^qW-Zɥk7MW|z>)cSBeN̋NѩUTԩ\N۽VOY=\A;w,[SoqaߟZMw ?wa^kç-T lQCmx2<6Azk}G$<_9'|xC^:$߰aCFJKKǏgM:5..\6mڸ\.9)EQ+\jURRRPPPFFF&Mc#Gܾ}d""c~?555...330<}!ϟ/t3f,ZbH) :thRRRm+V ͽuVLLLxxiӖ,Y$!"DBcƌ %"˵xUV"4M3aaaNsmwe〈ΝHDvv( DYon IDATT i`ۯOg`&͞dv/-֦gd?z}w^a\eEU22蹺,Է̼ųM>Rh5|mj ÑEc>(H"իWoڵ:b,YdիVUe۶m 횦 TX1!!am۶ܹs#F(,,@D6o޼ ٳ'4n;Zn}aÆz"!"XaaaΝΝL&ӢELbۥwBH)HDsEQ8p|ћ6mlfy3g\tizoٳjRŸBD={-[FDD?kѢիFիW( 1!j5kVVt:6lh4?iZ֭^p%@Dp7D+**jݺ%Kd2_~ԨQo;!8>1FD1UUc #BŲe˖(ѸgϞѣGϞ=|.\xWEaO2m۶sIII]uBCC_tY^LJ(ԩS;ut7o֫W/$$ѣnUV%%% 'O4LDwCDƍXd2;v 2D ~"<<\UU"2~!Ia_\ţNY1^ڇ& <eئ/=QJH%7UЕ5{jMM Zf+s޼to &fe'%?S8FE;?ߧv~ç95,0 |#J_f1V-b ]35  Efϒ;׻̵}zaa#;w\Q]QFzٳ>|x=\.WIIIvN'֭[ {裏 b322ԩSXX8iҤ} "? pĉ˗/׬Y366ɓ-[TU5))iVUJ wCDszX؉'r vf\.9H)VkNNg? a|n͖<JT+9/g 뇜jټ2@:=57 t٢+=g@\ k]pݫљhj熻wQ_[:g°߃HcŖ7)93ZA=ME;]0Z.?YݮTkDZMlKk&Ā;0Ɗ.\ؾ}{ٳgv풒N'cǎ/^TU缰sIIIGٳg Xt>[\\nذh4D,++۷رc/_|ԩU>#gϞjܸqttɓ׮]k68h4.[iӦB˗/i[氰09ǶxpxqT Ch$h";;[Q",yx,qG<`yD^BB~&'k^V9yǧk8Fѷ3f--HhSvf]oՙW 5϶mYғ"[[-9d\ff͝;UV~ ^yXZZ+T RZ֤>}\xk׮Wޮ]&O( 1"b踸˗߼ysm2wu֓&M 1Z`0A#B "qEwp̜9>KJJѣdzΝ$RƁ:p|wժUkҤ7|3a„K.qco˖-Ν[`Aʕc޺u?h6wYfiiiUV iڒ%K.\9GD"**scDSUp0>"D &;77w޼yk֬8p`m6ۊ+RRRf3H)u:]^~킂o622?}q~EQn`0`޽{<8cƌ'x".. 11qϞ=GZRJDAD[NS֩SGADf͚Yf"᪪z^lv8D "&_aN%ǿ?uwV tݗ#jV[kߙl/=}y-<=6}" z7_m :8USZ{N\1uИ۝o8|}zG_[ob_{{i^o݂2_Ue^avw%--MJ'L&&FaZn^y+W c@saaaSNEıc޼y4MtRJDr`+W i^}ӧOիswBH)HDsEQ8pADOϟ?ƍ~O>9sL1`\9 I)EYzu^oPPx>~6MJ QJ 1ٜشiӤ .0NR""缸xĉÆ rĉ7n}Z᪪z^lv8D(b^v5L7oȨh1WGt_#C>|FNddd{EQq΋{YRRbZ8<<|ܸq~nB ")"Rru:]>}v:v1zc{={v~~~JJJPPĉc{p!nl.sRJ՚c/:qW }ƨzՂ~8yޏ4*)HL7mHuY%O5 9_]NmPEƽ{txgυaki{lQ|MZi =Qfwֈ7NU7 >wIGS^Z\R׬]4rfniDN;tF$xwifX,Xмycn߾=<<|ԩ͚51bg}nHQÇ=(((1VTTdɒ'!F^JLLLOOߵk~EQUV7nx<鹹ӦM 5jԚ5kTU;hf6œN'S;3 lEQWHBlA֩͊K8ban;*uٻ#U+*Ͻe;TᡉS=fgo]t,Þyp_r"bҠzxh[ZzK3.+&HD@Dս{\PPp…'z(7ϛ1cƬXb߾}v}۶mDP(`ul2n8)(D5mڴW^K,9vXtttrr~eDDp!DLLRD9W%++sD,++k׮ݐ!CBCCV[]6yLUUnHѢE0uO>dÆ UTٴio?EDHDB1csw=rHۭ }>_Z~$''OWVU~"**scDSUp0eeeO>Qm6[QQNxwoIZYQG_үC7_nh[o ]') qV,q=R/tC괏?_Yce/[WrIQr^hř,J$ UzY\ݵhŇ[v=2p߶hɪmCfOkܶ[*QA&e]v]5:"BDzk׮=a„ʕ+̙3/4P>D$}]6s±c6h࣏>***"2JKK_|AeffL&)% !Dpppbbӧ׬Y98p`Ŋ̙9p!nl.sRJ՚cODZ@h>Ӥ~D J=6U$!cҹZ5XóMCW.^Ɖ{7~JU׫ClQܧsf6omR#%yݰp樞FuzB 䦠{~lAjɭ#[اqxOZj˗oذ`0@m*TܹSSS\vݻw_vMQ"BDƘz̙SbÇEJID# .k…>{M61cFQQN;hf6œN'S;3 lEQDRs9K<~iT:KK}m zKJB6fżsK F[>{D凪] 6**p%bR4( ZF`@ ¿ !덎nذappŋO:%Py< >G=x𠪪F"44t֬YtMEQ"~k0TUBnιd""D;!bbbBD$"ι(YYYscDDD4j(,,ҥK'Ox<:ժU?={8& ^hfX###~UUU!z Mn7X,"BD"**scDSUp0BD1k5jTRk׮8qt*DD(7!VZ۶mvIDf|:nҤI 6LJJ:}`R""!vz(,++#"ł{᪪z^lv8D+D p4Tpdq PՈR:Gj gѩ WQ$!ct Fj򻝥eI@p" \fVu.)qITtC 1Y j4x("ڹӹ[p1PZ\1dP8Kހ! WY'ƪ^𣮂Y/ ?1|>lnԨQժUoܸqĉ DD(7MӜNgDDDvn'|z- cʁQ1bĈ_~y„ _}hR""!in[Q'XVD#6rq@JiZsrr~?c ~ o茕jSTU\ 3Yz<7X&_I') ng+t YǣY>*Pf\%%n/ A)Uc˗n9+!HDx!EiР?\\\|ĉUU܈fFq֭VsND@D1۳gA͝;wӦMFQJPnDRJۍf^X,s"BD-MfsXXc]Y<88csg!44(  2p) GMIĘ[@Q N! ) i\3u$2(&40J@hD^D9Ga0F#"J)ʍmrFBDRJD>/""rssUU%"D{DD6y"rΉH4D#R !8犢deeqCD@ vL&DR""ۼ^4 VDp/~n7Lh4J)m9'"M~"**scDSUp0CD1Mnw &1&DD(7"bRNRJ"BD@b+WF)%"=""D1Bzc@Dfp"o"474Mp7L1IIDqRh2pE&& W8M##MhqB>7q4\iBq0B#di^_#4U$DDAD1)|:d2qΥFD914ʍ\xh4J)!"眈CDMӈBn\.V5''3׈Pq7L1zI3_#"DIBp~&QQ8"iB\1 M\AQg04 8W_#R2>? Lh"cDx^(FQ "BDιiRJͦ(iDPnDU^=33S"=""D8爨i!"A4t:9~e^^k144-;;[Q7D3R!"1$")%" "cHDRJD3CDy!";"bqΥ!";"~!"%111RJ!"\Q,9ܧ16""\Ji"½#"ƘBJi !DTT1ƈ1`} cHDRJ""bqΉHp1i`0hBzc@Dfp" "d!I) g!"B"?0D~FDRJ""BDEQH4"BDGDF*Bn\.V5''3D!"B"$Ißr "DdsB"BDADc4*氰09_[9=c>j";;[Qx/ADqB IDAT1R""U111RJ!"\Q,9< "H)*B(ιccLUUDI)*BpUU^/c l6;"BDxcDvf\.9H)VkNNg#"DdI)/il s:s~b'+HRvv(}M#B "qE5!DTT1ƈ1`5!Dxx^1Dd6!"<}Jam6j~<}J4t:9Hu`a16IJE??"oI"@dDDE@mD!¿m!bbbBD$"ι(YYYs "_ H "oI ݆D!" 6 $"??mQQQs#"Ƙ1?"@DB?`[DBD@؃uQ߷֞ٓL&&DtTD{"M"=4"H Hd3r={}{bIW"C "D_/!D@D "@D-z /?!Dx "&&FU`0"Z{J !NsRv׫ic {JaZ].㜣IQXp(bŹ0w7$*O""dIׂ!]"  3 1&DC!ab2Li$@ "B&UU!4MGM`()a*=w"R !8犢p?!BӅDD EUU - /0 `RuQ1a*B!]7٢*`@7$ S,!IEadH``@2U/lH@@AB!@@n&pݜP(#"Ƙ1A hZLL!I !!n SP4ܬZ8J #$ b2LP@77YT3" MӅqjQ0 )b2 -/#&٬@ "ɬ*!U kW8CH@LjH1M "@?2!DLL`1DdZ=!"B8N9 ^W42 j\.9bf$J:odBbjj(>1Eݑ&ALk۝(UHBD"BdZ^L…#JFzZz\\&5snH<tɄTEQoRP`LZ" RngoqkRA~nmÜ5bXθ s2 t޺?|DpKѱU%tzk٬*]'3JsqDŽ^(QI{/7?1iQ3r"R !8犢p d"ɋC-㸹\N{S#UamMV8Τ?#ee3vjabH&pݜP(#"Ƙ1ez#~ʸε˪& eܸ`78sZ(̴9nV(LMAK8M xӁ-z-`Bp0k=X"qn2[`e*ڼ/2<{qzmס{'֨H{ެ]ܡVc}Y|!D\\RD9W%%%sOz"ݣZx3CZ" 4fQsjF2#WeMrj{U˺tl⅝vn^`zfXu]ZE;n}jZRi i`Oo6Vvq˩Ytg6ɹ[VY~SR9WӹW{fiTf7&|֒㛃{|1v|ێ+]DZA !n7< 1ƈ1a3w-O!4_̑BǙ3}~2PQ0+퍈>zgE.ZMWN_bQ?v *|#8-2 .hv=9u^U\Ip7#QKK˳Fڄ sSjkQ7.B74rDL8}.-:Ȅ111AjxCfx8*-6 j_zo*['s{]1|Ԍ0mG挘ٙ<Կ&8e݊^ e٣`! -VO:zr&ћv_qZ'T4ߜׯт/3~/W.Zqj{5,ժIM1;BI)HDsEQRRR8h%3zűM.;TR:7>nﭥg/Բm}tRJ.>+#L^pi+]*^i@G&$$^0 ;ΙܿzLJ8|T%&yfN;G7t݁o=7-a5™6{ߌ7%i߼]\ڡ= YRmRܝZܓ~-:x剋`1'&pݜP(#"Ƙ1OBpkĂ_6+8ry_eU}_=xBbZ6CpsK7{x∱+Wi0oW 4[M\Zաk?mϾFSӶzxULUɵc&{\qm5r#yw\1Vj{юs7X'-xӸ&}JԳ3${'W=fe&4Ӗ蜼lXssG<.WoFj쨼 ޾ ؕ}7%uњwwl*|=}jž3#0|is3?6!DLL`1DdZ=!"B8N9 ^W42 j\.9bf$J:odBbjj($#cHܪj('OoڽxS?e5ۮXZ؝={hѻOt/C}e٭|U]◙M^ut\vёa ,Z5qn Yddgi5=X2՚[ 3cWm- 7@v\3p\9Ѳx~]~ѝKeޱ鶔H6se?lbc34盳lpw;M9/Ky7ݻieMVC !bbbTU 1 "x!_t:9)nz1 _aVr|>96#pTy[l$SSSE_< t7ڽUx2?in^m0jua/oe[x{霁=e! ݑ?fڲV jСOY@ XP])TU;9y&]b53'L)[Z5=zwF.v6 'PZ/Fgгh0 ^Fv,YT*8U8='dvsCccz<GSeG_,^ߎ]WhVľXS_|*숮=Hr{'7^rȻ[&/vnjzFv,LK`ONG! Ȅ111AjxCfx8*-6 t ~xhY/>o5{:9F,聳Ƀ:ar6_5x5f_6sEצTwgrf.e=-~k/KӇs-?To6fƕ}AE%BI)HDsEQRRR8 _mDے?:Ubߑ NܵzX?[vގ/6| *%=dż3F_Y8Сfls~/ +kEXd:?ңao%_Pճtjo|l@qܹc<ݪ~b_ӻ  }$yN▴*]ظ/f~z٤vsCccz<<5Ί]37X0+r(Ѡ:VJܷ:la; b\9q+ }zv,+u*[E cYW|W:nxe~3z㆙}wot0כg6\lDݎG]ђ^г3ͯY{Nb:f\i1!D'B "qEIIIHI{Բ#_+v`b1EUoL\rxt1dȕ|iذqu:m]Nꕓ/bޯw6?TV[8h䑇aSb.EF_pmhU(SeXV܄1}w}"Nk(q+o& ݽ|f0;dK^W}v 9|`vsCccz<r懈2(tTJtB14SЈlP&}VMխ,uhXתP/^AluZCͷZH?~ꂎ^^ѥ{TKVO}DB -ϖu={.#ԮSܖ1QŞ Se[ftLž37kvvmv?;܎Y_tehcz%Z3UG~b}T򓫻;)EvLiPk?=HZ" ܻ(P3G&QU5 2V!"D'"J)5MCD] L&ȗBt:n{^Mc/#"DCH_ !"yv (Qnf8Ŕq3OK{> m&ggە8%ag7s~B8)s(JJJ @fX]-ZcI?"8@ lq2yJ;gsݝNh;|F/*` eT[(Ot_X IDATI3͋Vr*LةLLWwd9˷akv؂0y~Jr-T/Z(nyݾ=SL;O]1k9!'?Y6kϋ--w<9~3" !n7< 1ƈ1aoi!lٗg^Bv{8 W߲Uц ?)jV>yK丫myXڦۦ{comY֑~Hn yuǔVSg>~ܰ]"7{7ygԆ% !,Ψ̼0v1M3/<X4P$5x\3lx_;or-{&aPd]+UN{w[.GF8_?IK[Z=v/{{DӖ'Mv)m$4111Ajx96#pTy[l$SSSE@Djtҥ_ӏo٬@0bLJ!t10i (&32 aHbIAFB TJWy3"3܅͜t=$pJPx1~z5Aߏ"..NJ)@D"+9 B(!40$ S! ] *Uv $"C S!<"P]⢲^8s3!cB UPHoO] aqz (Ysyt2nC&.Rϕ)]~KoYĄns cDSU0?!F0 +V zjV@R)€L ]#xMfBH8ZѪ+>t|JjSa! $<b+V-ms}m cRz(*W@ɳlFC DE11.d2"C ipsBW8G$CCI"xYQ111Ajx念o0h+Cb2aKu+<{=tyf%\32ǟ鳕-~5iRvO<nA !Ql43fy 0F?z)k2$4+7`جy.sм Gf&|-YD&\1zB%%Vedؙu#/w(7a2ǼW-4᷈1C Y T:]<˜?ֹw^F 2+6i NC-z͖pD˜J^rQϙoL]0hvۏ&vкClwXâuo ڂ!!0 'լ!2 Ϙz4Э`pZ(7#-G2n9 DYrsr&ubt-z%fWtI"|j:evVf> tΔa7DF3::j60V|s -vmFᨤH&$*AAG~WD"D@"BDxDh!ݐ\1M /@f9I #R !8犢p!G? ahZHbVM?""ZH̪ޓP*]me,}nc_n8G~kblykmho̐UoWܸ%a7#"I AVd)% "*-Z 9?ݩt0Rޟcm*͘}tog(v .ؼsw?Tڌ1.8;,?5ha[Fn LefNd)*C@'T;k \?b޽1Y?_ʘIDzU4oşq{o6iݢϛ 9Gp{z/ M9'ߏ1k!舵X;~`݅;p''[&ԵCa&~`Z."|! @dQXzᙻEAϢgѶ[@S_ մ%7~.׻6q?=;\>yZK qI&rhNf1ę]7(^+~Ő6k򣰛MaXV8Z6یQImLHLMMU"BH$BDAWDDx~w%R !8犢p#!GoCDR!"[3R2;~ahcL泶%uy}u \\7Ui֨՝SקӾS: q y7'tYܭWוDG y]Pn ]浛,b:t} W$BNlJجN΀2") d./EY{&Mo޹v"-UT)+DXhN?ܴeoECܿuw:m^U,[?Vn3um?[o߫ f}/EZ͒7yB/YJX4 Ƙ$0#~ڲ х4zd+ ZV]o8w ϚEËJU̷6_Z=bf7J5 h ;:_L MfNշÆ/n#vH7an;,:i Zaa\3$g$v3z7xZ \na?ZכNEyj76C7R FnM>AddbEqIW^Zkds5gHĮ?:I2 j\.9bf$J:odBbjj(&R !8犢p!SMv9P1FD1UU=c =Մ111Ajx_~ 6~Imk>{Ǵ3Z6˷{5VZY9~} ]xQK'q6{tްK:yynhGs2:*0<2&uv ic@$ rr'TM\v;[7=wyB+o;1gQ{DŽG;2 ;G[Ol]r*tє nϙ}tN7;:k7KQp) 3vSi8G1if{DуgW/jlpcչ?Mo:|V{UB=YU?<7ׄ=kw`դy~kyy{'[s +F_KՓ[ҧU/ NˆI !=`tz:qCJN{}@Ng_^ǹ']sƝumVij9TS;g^=Qn;ʮ 1]g/h9jmV,0ۈ20GTv6.ЯE'|g,9S>{Xz+jmm8~[Ϧ6S878Ӳ7nUe$@dy]&Q@`{w\/ؚa2]× =s~ٸ'yKvWM/-߰x{2zm_3mr>myӛki Z5e[{ʼP{׮ znJG ʆ%} "B A{=y~&V>ߣVyo#L|sH^?t3װm6733׭y s멩w=FVjftugz,:i ðZ.q1mb#( { !⤔BD$"ι()))sTBny(bcLUU|O5!DLL`1DdZ=!"t]_t)]v%JذaÅ j֬bŊe˖7cǎ]t#b 4 !_E6 "p:s@Ji۽^i1cZ>_y !Uz\I{ku'є5^=Fn}[K):q܃;&uz#`ʐ5{*xju{?xˑk4uşм2W=:Y}/3멯91ZspkG; IDiljlH%IƘR#;t8rajMJftdu[ln0JkFY ׏jFɚͿ=co4ya:ث~Xy;?QG 6~^ yuw=C'Qp) @ka1Wg/+&r9!Cf23oͽ;,Qoazݚ 7yho|U3eߢv+ݮ jҦw#9Qp@ysň˿E-~7Kh=zj7;fo6j #'!0V6fji2V^qYjOpXJ7;g}_3`!GzJIN96#pTy[l$SSSE|O5!D\\RD9W%%%sjB9B1"bxc&QU5 2V!"D'B*UZٳ fuܹk׮]t9uTڵ.]GFF1"BDȗaDO !NsRv׫ic eҐvjE+@"S#d1d^HLNަ'Z9Y&w9lM.>$Ш>~խer P^vVOڲ>z元IS^-6k|p[;5f Q*Dkff #CX#{^? ɛzU~8wǟnL]g~}^n w/zЧEӷK6޴.N!؃8'q{+J:Mh:A@Q7D7QFqEQPq_QpQTĥAqPQpF$Kҝʭ{̼?o&跏OfG!p#IXwkC,8>YhW䧉i:넯/Nqǜss;)+9=4ӷ^~kasܪ@0n黼]=~[ OMx3Be&?aBp|ݣm#Z[$>qGb"N6|!O9{'}ȭb+?5=&_w=}փWpZWg7>; |i= #O]7ˏ&i|߷,+eY!?vnTҘNPRԔRdRkBDfBH)SJR*H!\%"f&"4m&"()jJx5@ABBs]DAe?}Rewb3XD ֞GD5"0 .8NQ7@   `#ES& })XgH`Z]An{cμ=4ÄuLAD̀̀)@dYY4+FdH,x^ $!uvCqOcBAo# U,A纞OHD^{̱MoL`@}˲bX6B;>cx`aM%)tZJ %EM)L&J)Ddf!2J !)u]"bf"2MӶm"i:CD̖eٶ̈?̸3"jq fFD?R*F"\.'u8d2. f܂@;]/ ,D{K[,7a`f nEbnk[o Y3wsO]Qz`@Vgr65$33f 3 &̌H ̈̀̚ @!\CW.kj""؂Y&A^{uo7@@300 "00"3"23"23 3 "oDi=tGoH H\pBuk_kG$D|߷,+eY!:!TҘNPRԔRdRkBDfBH)SJR*H!\%"f&"4m&"()jJxif@"fF`}_;[?,k}_HC {@בn'}4|͈Y+J[D`fDԪf6ä0k.!%!kf"M֌HDfFo&R֬VZaDr ]P.?X,hh PLmu[J;f@` f@MXk@"x<i@f$B،̈DȾr܂4$BkDNuƮ5I`](dn`f`ܰӟ)2¯0BYPDDP/{ y\KiWA+PVnnsD [,(7ФQBF\Թo8mz{=6?.+/s ܔ(/ MdrױOsOn~_7:_q#Uvʥ#V~vUPUF>0v}R z?[MH""ly*4M dXP(m{J(sϏȾc_["#X^nNSCdy(B|sSc]={?t_=kMҚr!Pg>t/%7)-gĺ^ Kp[Ų٬G`%/IΩUNPRԔRdRkBDfBH)SJR*H!\%"f&"4m&"()jJxsO=A̳/07x7uq]u/:_g 4/0x\/O/#SFѤ_2h:R^YYW6po+4wZpk~E/: gM9?i_u ku/^ GwmVv~D5mm~˥9G`%8|}|#7}k1Oh_#|߷,+eY!J^\S1NK))ɤZ),RR)!5T"BKDLDiڶMDPRԔRx4Mqٲ,۶H)h$rBZL&yZKײQ 4k'=tfI_v. WLWbCw:yoZܴ=^X;>5\zwf?IkhOq]/A۳?}v9Ǿ4q%=:&~o׽smNN?u΀!"BkcPh$؄5˯^E4-a'<~)o m^l-IB|UbchX5{biucws՚'=Qg^V>/>뻵ߧCܓ^7?_3<^gmg1w8&vg zO\:t˱/z݆#5* d֬˩ݬM~ %M?ܔפ3BMz#BSZ|ǮY)7jiSٹ>ZԨȱgj3}3k>^{vބSϽ3+o.^T g祋{_2~*t&|v~]W ;'0w/qv{wboYV,fB9&Y;:Tc:RBIQSJ%IR Y!LRB()jJD"!p]LӴm)i0eYm33"BIIRJEH$ÙL<"mS`n!hNɨahf@1eYjS0:NLk?,rk7jyر%ܕ[+'sOǟ3_nO7;{wv{V^2],s VӿǞvcgL|҈t[Ϝ8A/׊^}z×_ķUUϾ|⧎<^@JFF D`fԠtg _k׽pԠ[xa~wu?_ǴC^uO2$rq3W{mwsF|lh.oL9qԩ\pcN쾯62lq:{}ӑoolLpMN،O]ۧkgy.N T ?ebl6+1XɋksCU>i)%5T2Z+RT*%J$Bu4M۶JR*83[e63#")T4D"\NZp8d<#"6+xlC "03hKJT[w:>w۫_{e?+"􏫌!y7E3vuhޫ?>zXSX៰f?G(P kA˻VN1"|#ϴꡳFLy |y}5h~eeMt>3oyɓows{|nh5dw6G }Acr52V.|־H(D~c񠦃{/=uՆWاGStU š|o٭M_}K|򨫧6ͺhȘ˟?׭Ϛv؟='}oa}&;O?/$i-/|_S>O~\efόn7a<|Ss2~UNgŌ~-Qn;+-wmIBy򐻞^^.\sMkq_vyjUn V2)#_sĿ#W>^\֫/M<ು]q0ߝ.kˏ-ѽ{x W;˓ [;1uڄw.b많ѷ2kkz4Om~sƭ)h[Ų٬G`%/IΩUNPRԔRdRkBDfBH)SJR*H!\%"f&"4m&"()jJxB+R^Q/_0i>ux韮*3g.wMꃏkݒ>_Vr{xdžW,y~X{=ՠ ~ˣ fbD IDAT۰}C$HֻJ}{xh >I|ٓ|ۆ]<𘎱מ熻C{sU|m<~;r_̪y u~i@up_1>Yvy`'ֹrE}i/<4kG?{_ZdUA33F@wxh4rgŜ;+L2ص/Xҽq{wЎ-9sv֠;(;O>u-J/^]V]oGַߝ>7>MuhÆߚϮtwV^k{|¢z0֖~hz3G|8V}2'|g7>yᵏe0-ˊblV#gc$kT}LRJ()jJd2VJ!"3 !TJ%EM)H$3im5T<7Mq"f,˶mfFD())RJh4Drp8xGD͘] V IѾ$ &ÊHɣ=ZVyQ\B}D+Zh 78ݯSO7(8m{?{8Sz/Tkݜ˖G*~uC's~ZR_hf5f&$O{ T S V29,*6)-"|.X6ryR27AAPGF$bܘ`¬khHY),u BVP:沊BsGfe"߰US1LUUB._v 7=vs9/}}߲X,f8r6+yqMvNu t:-J&Zk"2BJJPRԔRDB.13i6AIQSJq4!"`f˲lffD߅?R*F"\.'u8d2 @ Bd@،iI5}Y$+I&A3 B$N>ysΚPPk׌HB_(pɟ;}Ù+ $3 3HBkMZ)_JA)-d_̆W@50־$$!035w{_xʂČ@s;ԣCy,UV ̈D 78Hy/[׈j |(oQ}_ )X) _ TkFBЬ1+$)HkM~A!$j4Xk D B f@"hz{,tA6B87[kD?ebl6+1XɋksCU>i)%5T2Z+RT*%J$Bu4M۶JR*83[e63#"^ "BIɟL)F#H.B:g2~afFD؂WafFD;@،}l bfD`*0D&cD؂Wa +iRhD`fDy!2" 3 ߱*xd 7 3 `fD- F c)fր-ˊblV#gc$kT}LRJ()jJd2VJ!"3 !TJ%EM)H$3im5T<7Mq"f,˶mfFD$#(AP|D?R*F"\.'u8d2cf7AD f̀5"af܂@DfMa+7M;1o fFDsoYV,fB9&Y;:Tc:RBIQSJ%IR Y!LRB()jJD"!p]LӴm)i0eYm33"03"HDZkfFB7uاm3hnpHlki)%5T2Z+RT*%J$Bu4M۶JR*83[e63#"3fnnnVJ"VTT@.s]7 B!f|Ca#!/$Ѿ. >7ICBIɟI)F#H.B:g2{ "k͈03"o@Df̀53"6@``@03̸fFDoeYX, !pl VdPtZJ %EM)L&J)Ddf!2J !)u]"bf"2MӶm"i:CD̖eٶ̈[ѹsh4 ,Xcm~WB$7{nEsBl`Ȝ?c7sjPRgRJEH$ÙL<"߂q3`"ɛiD?3 (!f<_K_af$76cfD)%5"¶afB (gFD*fFWh"l^aPk@ >!X3h03ľ ̌$1`ÞJIk@^[Ų٬G`%/IΩUNPRԔRdRkBDfBH)SJR*H!\%"f&"4m&"()jJxi)%5T2Z+RT*%J$Bu4M۶JR*83[e63#"L>?<1c(t"1cƤIQF 0` *wwm ImM+_~3AI[ "Rh4r9!hp&2 bP(h1lfr9c RJkե°1&*0dfaf<3cVd|߯j @D "BDxz~Gl?;?K/=?{_|?ԓvRUiƆ')=_eE6SOYkNSTRQJp%bE3cÈ8v}L@D揦S.ͯo7j7yüˮ͆MM7N>u~u葿[|yCgμS%ʆ޿rϵ_|+~p!ˏ8f ۈsvY]_b A"g<ԣ0=e~zqrDmg|+~m({O?dG=VFfnEw/"D7utt,]tڵr;g}={lԩ]wݩ:y;;vRUiƆ'\|e:VJaذ #"D$"_}̌wdIөTR(8dXa/qBLD3?}E_ahz|?ƙ{7rv%~'Whޏˮ3Ľ_{۞vUeg׌V{W\ۿhN< g_{dwL_|㟿삻~zDx+٦ ;%&D9=U^Q]rW%qߝyWg~ 7}꒟}aϸi541N]'Ī;׏n?{G鳧|71ʼnGҋ+9v֕_>zOͿū?}m̑^OЧ8wy{NtүL90csP ˲=Ȅ{˜C#[K/5Uv~um;)|G⪿[~z'= 7xh_3c>xO˿Dpgګ H Ry̿eK!bP(h1lfr9c RJkե°1&*0dfaf<3cVd|߯j @D "BDxsZ.];?;sϽ..lʔ)眸MvgLJ365)zJ+ Oikk#"ct*T*J)ιd2Y,(bflﴸ z+{( w=eճs-g[KϿ6;OlOw_֍8̹?ɯyַ|Q'|{+}a̤ ɸ[g\~c~E]u鯟u# |ǟO9mGt¨>~OHlj6S;D@DXSpɝ]z_p7?si']?ܿ_).[drmvlK(/gNhyo* 1@x8?D[cL&}V13 ϋ "Rwu/T*e|s9c<5kִ\tE{osKse]iϘZ)Uflgzۂ}Yia)!"#cL:NRJE)9L&bĚ )M8bݣzj+~+ׅK?zG֟'[[ۄ>[w_WOC}Χevh 7|מNW>G?8rͷ>z]xfM71/|(g"3766ѾZg+b φu~y_~kL;k?Y3'ߝ2fO~rn7] uL5 O>ie#ụ'LO;~0N]kfW{s8~姞}y9xS'}>*Z]QG|? }׌qR"._S`-q,y;QJ#~/^@nϣqL/ &߇]7Κ?諟9u_139؎CP Wܽ$M_wx8Np_y H RveOX9<BAk-@ 'ΊsN8+xVL 2"BDؒcrsCD"Zwuu)%f"bh`3ucs9g_Z+"DDJ1ޠ""Zi "' lь1lV)!33[4 "Y'NCd x6d 8bX33D`Kgd2j5f "Ay!"]Vknnc=:;; yz=M0駟ZP+>cjTUauFyfw*0lP2ƤT*UTRsdX,FQ@#[wIGǽ*f w&Hnx\xMEw3uoo BJ?rY]Pj>wS_?V<QGO/i ׭l;7}m3qmܭ߾7θz?u~«>ڃϹK'dvo!g:eﮬǮWotw,_Cϛuwkt/AsuϹs֬?} tT IDATx,~{wKN<%}Uǿ۲r's˘mFD=0S;7_<~>sёƵyO_%b o'DuwJ嬣&⨆ BG_>wܫ3f~bʟvT8|)M5/|^r_ҙz:ryE3?|nӚ3> _}?ލL}}oi+lAeutA֊ 5XD58뜕XK4Ƙ b " C" A7t*Q'juVĚYR̚!2r91DD)RJaD1Qz9'k]jhiknjI⍍HxA֚Jo(^魔V/0Ji==@De2dYT,"~>gflD$"q:kRO l\z Nh}ۮaH:uSDU"Z X3+fESXƘL&~Vcf"A>"c5(dy@DrmqgLJ36.ϝ5oN JƘt:J*R s.L(1'O'^^"">>:eܶ/>uϛ߳ǙN{|O>`;f?Գ>رW%znӾw3@j~GoWVVy?xc/_>K82e_|;~~{~ǯڿ Ck=[wj5yθںhSrWUy @LGqs%9TjW'bKM?=?+<@j:>va}O=os :#LWwPnD#?0)PRsm3'O:|gTҮ/4 HG} ݥs8/i!@nc;{M0?bNq̹?1}şeǧ0~iTO]2kuլ}/g6ï+u>۶_^rvտmpEA^.RсAιX,P(huΊ16=)51 Z[F?'}/&_ !T{C[JX o]R_W]]Y=Ś"–s"ֺK)͞^'k1Fn1 AC[1L5ښRT!4D "Acy1@Ta@oO9 uk֯_ۻgեꗟU+<"&D-1&*0dfaf<3cK#"g3΋l252ِ͉Ds"ޠT><5Vҽc&aeW{jōu$k="Z%2=Z_Էo}o]T!&1d2k3 NDh-HD0M1R*0κx?wּE;V6l(ct*T*J)ιd2Y,(bfl0!"c"V=}+kQ" "ׅFu!@}}u;sicWUCSzk9Sacƌ\#A*{9R:{g߂3e]={˷?ѹ/Xӟ{8#~θF4A 3{tCS!V+)\"Ĩ֦GX5ٶRn*}fWyA1n#텫w;'W>šX2jJTVvзnv?-Um d0VvdĄe+b* ^C"B0UQ^]qV]|L.ť/Q RA«WB$'pU~t7<6f1fEPrS?_d)9Ԉa e \.+sX @PZcGL'NXkͥFЖ١1)/y3N8'op։uD`x+z"V z^zh*֮\S*u8+ĚI`gr9c RJkեf9S7935$'m;qm툑# q/zXkŊuu")f ""4+GZ-W}Ҫu.{e_Z+㈠=A2dYT,"~>gflkYGD-q1ͣ~b=_fW +L-ܞ<}:oqvQMJMDĊIzhjDS[tmKkʡ8qNY@3d2k3 =b&N7}J4c8YTZaذdIөTR(8dX񮑈.bL ADX{,."ᬅR0񸂀XĬ&S7Nf0t<@,ZH '7/)DFUOrqGQ]<4֐8 VX31 /"Bgu"8!zh!EuK:qÚb㠘ZG"`k#"S[o}kM9M=4q$RX'(%"7Q-txLfraDBXc0+&&ۇcpTw< k9"jYt/Qi[&kmrY)E䜋b !4aS~k4n3edf6)5kO'oED "x+ i+`gnmk {WWj֭k8fs"ֺK)͏3s=WkoJ5g>cljgzyb:c8 "a-Do!" įS2uEە+V+O=b_?c~LDs"BDc٬R* Cff}?336[fFLh'D0bn\JkҞ""g9'N S]:uYkţw1'׷VIÛDLĊq6εEݥBOիFǔq!cL&}V13 ϋ#jomqgLJ36.ϝ5oN JƘt:J*R s.L(^# "Bd$ DD`%ĉ062J3 ^#"&8'H^"D8'D$"DL^G$FD@:3 G" "1d@da `@Dq׉9T R:::09 56C1mlkh6ewn;eTf @`MD "Du] V^Tվ|-6CƘ\.3(]]]J)lfDP di=6^msr֊@Ds""M&&YŠyY0o寬hL N1dYT,"~>gfln ۰ljݦi̮F7x:D7DTԎ ^hm`ǂO'7vT "xb"L] W\gUE)%4" c2ZH|^D1U{k>cjTUauFyfw*0lP2ƤT*UTRsdX,FQ"Xāb "l\B@ 8K9X cIDlBbfED)$k ˉCD Dae \.+sX @PZcc" [G}zdD--CLD"=csꥥsݝ%R=͏1&91D$"J)uWWR (Ȥ&NSviƠVLND 0Do!NVFu ,x9x͏1&*0dfaf<3c㬫LKi=Z3;68@x@H\b'pJǟjD7vTaÈJVU/V---_jgkl~1LZD$|>/"DwM1R*0κx?wּE;V6l(ct*T*J)ιd2Y,(bf  :Xz.dSM6nDed 5"xod-3>T{Cu&d"SuY1 wZA{{{\VJQGG9bBfଈuF4b;iN5U0aM@q2b ODkWnC/u+&El&1\9g!"QJiRDd =w:舏nv#2m"UsDDщ@D qu,?tg04aS3dYT,"~>gflr@+9Mm7zb{zt+׌4DD@AgwJ;[|SRKoun ^'qBD^\RT}ѥ>\kd|߯j @D "BDx[8mS+Ҍ 㬋7sg[4SiaÆ1&NRJK&b1"fưw@@D ȰfֺD%|cAM agfljD$"x}}Ǝ-kZ"†s)S{ ;7}L*κ\cN5wdSeri) Y8Md|߯j @D "BDx[8mS+Ҍ 㬋7sg[4SiaÆ1&NRJK&b1"fư"+*ƤH8ă6O'?;z(4< OCA@ AbniRiԉD@ĊXq8c8T{=++_Y556Q͘*fYZA{{{\VJQGG9bB&""4)bAl.#w|ܤ rƉ6c"RĊŹ+_KkawZ5xda1r91DD)RJa"bjДmɻObbk@D،af1G}[И9!"l"Ƙl6 ÐE}̌MDDhSVQvb~nYV'@D'Yѽ#kͱY<%ޅݷ:DpRDL̴/+UOqBDD1LZD$|>/"DwM1R*0κx?wּE;V6l(ct*T*J)ιd2Y,(bf {auB,T,5fXC&NL$B1+9 p E9ɥ@H  DHqv{iiͫ*}J׬Y@0u R:::09 56bBe.a̔QLhA@ "^\;^]b:w{&&l:Ƙ\.3(]]]J)l"Ddv{.wqk!aa !"$"X̋{W=J+D1&*0dfaf<3cS!?:qڸkU4z[i*V;ջVR"@D/Z8s3E&b!T1LZD$|>/"DwM1R*0κx?wּE;V6l(ct*T*J)ιd2Y,(bf $"!7fx:ǃQ /P8+ bHgwd@bNiril ;5RW=BWuwJ^\`kmrY)E䜋b A&O޹TO;͕r313p9qG{}_^o"a#2dYT,"~>gfl|15$LmG% "qŠ/!bXmSxueV'<)z5vTahq j>_Yb0@dd2j5f "Ay!"{TM6nRUiƆq-ݩ°aCNSTRQJp%bE3=HĉI,McRcDkorFlA@D\[q N@w&b144@u%")SՕ/{ŠV@< D@kmrY)E䜋b 6y ͣ8g1a ffZ<ů_.֩@D1&91D$"J)uWWR Р(tL9mO(klՕRDXR:Ks?'F1@6cL6UJa"̾yf""4ɄM!; -+"iEX=9ܬ{4vTa(9HTem‡_ZWў "Dd|߯j @D "BDx[8mS+Ҍ 㬋7sg[4SiaÆ1&NRJK&b1"f{1g(ubS2'889=srN@w&b144@uMD @i|m}w/_5SAeutt`s.( Zkl"BDm]hgbVGYҧɚza0r91DD)RJafZ-7aa_YkV ̌y f/-z_=$yYGD(1lV)!33kAsO u17Yu\&ݿr#cFz7vTa@Ĭc|ū$N1&_՘AES6qڸ3VJUYoΚhv Æ %cL:NRJE)9L&bĚ [s:vANMq' "c"8?5yTq ;S\ :/%\b˥uOZA{{{\VJQGG9bBAp9'OOnlICCD[)@ WWk' 5CqcrsCD"Zwuu)QksIhQ lDWGm1g1&*0dfaf<3ccqΙЎ{}rR6ȈD9Ҷ~B|a޷j#cΌfݷ9Dpz"@:hcjTUauFyfw*0lP2ƤT*UTRsdX,FQx kCuSy\gf )lz ovyҌ!$!؂ݷjGƜh<}SMBDpEK.<}עU}^\C "Ddd2j5f "Ay!"{TM6nRUiƆq-ݩ°aCNSTRQJp%bE3=@Yk ^˸`d=@x8,=bWk @,&ȶ" =qNX^˞.xaҊ ZA{{{\VJQGG9bBƐb'n۶m#dwaV 19fbϯz JSD$"D!cr9c RJkեfv΅a4nϝvԤ'ku+{sx_+o])sD!cfJ0 YD|>2"BDi&?xlǨ mn +Ɛq GȘ3ѱݷ9Dpظĉ^\yW/<׭"E℈0d1LZD$|>/"DwM1R*0κx?wּE;V6l(ct*T*J)ιd2Y,(bfl<:&kwn;@OqSuWy@bNiril ""Ί)\-_j^,u:D> R:::09 5sS {c;L58!" cL.scHDRZ뮮.0uD}?Q'>"4HDD9Xr{GhO[kCfRa20|1^ٝGɨZ'"BDB" nt'Zzboݷ9DpdTZw\(O9Cd|߯j @D "BDx[8mS+Ҍ 㬋7sg[4SiaÆ1&NRJK&b1"fE "bkՃxzTۄ| ğ-m7C Dԅwb΁ @bNiri؃p9nyY]&  X#HczhuZOn[{iOkUֺmA++9 pCH ɚYkyy{/VwVrDx|∘o{wGc9SCDxr !$IR.ݮP^mM]`(}R8MSf`fI4 3#",1ӵVoڼc,O۶l߹u8/JbU- v;2f1̈ U էtәs> ayjA(O> a@H3>lkjc#3 a",xξݝtkLp !$IR.ݮP^1vϮ{/}Z`fq7 fƑbAUo~Jg2&"&<1l褙^|;?ߕnwU*`srcMo4sW*84eff$I03"13"GD"BDfB03Ok7޴y}X mپs.q{_*bZ(věc1! 'W j^Ui~2ÞOCRYFÊex$`Su59 A%` d`?ݺfˢ%"BIn+"T1OU(js8r0/_<̐`QOD^kn&N`j5Usl3#"?+ sޝ"ȣ2B21~{/϶99j*"͌8n4̌#GF}y3F3s'ٷO8|5ɾU$(3S\>7w4W*84eff$I03"ffDdfynrBAHVoڼc,O۶l߹u8/JbU- v;2f1laV9!g"tWO@8j*<6}]馧T&<κΚ RcPS4Iy N"3BIn+"T1OU(js8lfFDe ܕg?cad s_&űVLDsfSDp̌|'Ko? 3#"<3#"{w{|v{8RU"aWU lfq`f63#"S#k_f4O !xE辎nY̊K{ULK[4;w]it35"aW*84eff$I03"03f!r}SSS_~ի/UVz뭟ԧ<Ź3_pڦ{8hDm[wMRT,{PBn,cfHMCN:itCx}?ؙHf&^[ɳ*3Bx$`Su59 A%י?;a!E!$Ir똧QhZ93#&JLO{Y ,QˍvxXԈ{_T{ODf&"ιf)"8v;.H\ӠD཯V"2 ̘9F8|f,곞ҧa 1 gX_fv7[PH~UeXbl˻=/dH40x+Ji233Khg0s۴i͛.]v]ve]tѷ\r;wIxީ6u |~p?r##dŊD/JbU- v;2f1̈يe<lC@a0V5qn\LE"EUB`/d0YYDX4(1HZWo촧 0p!$IR.ݮP^~W_}g}%|Ckr֜ʦ{8˜= cdlٲ%2fƣޗJbD ve3af>d\}g}'?߿kr֜ʦ{8˜GtƬ;cd\yᐙRX,z=BngY8Fi7=(g&!@~'0%jJDXU|I7fL ::k6r b C Ǝaa !$IR.ݮP^k^~o}[. 睺i^/0ɱ r8!YlxKRXz"@U Bβ@|sk+{jC11s6H.' gfLR_A ::k6rJL.'س.rBH\.w]zyEV1AX~䦋'+ƃ #JNדHLxZ{"23q5M Ҭz '2d"bfD`fD"233o'y JDX<}Z`fq7 f!0^w[=?$9X:T!w>ݾ~ݒ$A4@PUfY۷wygjDW*84eff$I03"B7u/~g?ً/nz?Y90vM:}qѠq;nb8!QU3#"<*}T*^ODjPhY13~yR}͊@#GZhE?쮺)r*(0R ?,`f#!p0ThVGA <919C@X2M{XP@Luul056G-w۾ӚvKV!IrvE:jEZs9Y;q9EWU lfq`f,`a=+0;x߯n>?n/$Aq,PUfg$bx+Ji233Kh᱘ͳy4?ݰzN_ca4h~"޶eέ FF&}T*^ODjPhY131 ^bVb&BY@8<c?v1`f]9U|0@Luul0Mf udNOD0,A!$Ir똧QhZ9,0=y8̌LrwoƜ{_T{ODf&"ιf)"X03o ѤQ.u"fFDX0}Z`fq7 fƂ1 S95OӠ0,\WnWٮɽ[T& cw߹GaJ%4M%Ih4̌xԟNnXi^/04?o۲}]##GT*^'"TP(,˘K:xn( vО]rI@D8*ȁNd>1^_{ = 3 a"4xkrFDXJBIn+"T1OU(jsX sN13Ucfe]}ύ{ ,VLDsfSD0f&"9/x{s!fQD>7D{_VEd001sǍFp?<d^}`b,3s|n۟[/,ݢ2IP+ jeM߹#Ay+Ji233Kha?ݰzN_ca4h~"޶eέ FF&}T*^ODjPhY13+6+  Rʵ>>.D@D8l %oُ<Ȉ`/V= Dy`Su59 AcМ ?]x%&$I\v"BzT5"V90Asy=%?4#GZw{~z}a Ὧj'"3\,k4?ݼ0"dfjGў}y;"DWU lfq`f,ӆO?L ",af&UwۃR/QIaC?f%Ǧ Bx+Ji233Kha?ݰzN_ca4h~"޶eέ FF&}T*^ODjPhY13q/UEj0"20䔦= "a1:{k1J}{t?,`f#!p3T}e?'?B$ruS(Z-`Dyg쐘`qa|wnrfF ,VLDsfSDco}{ӳ,LDy\'caajUD33qh4 6>w>'%eig(S2ܷEe8V 3OxԗX`{_T8NӔY$F̈GLt՛6u A-wn%N02r4yKRXz"@U Bβ4Q_ӗ.|02P䁇)` ۷ʙN FJ\LA κΚ “U'io/\ܹ/˙a!$IR.ݮP^3&]AAT& cW f2qW*84eff$I03"S:]a{8hDm[wMRT,{PBn,cf,fO_ŏBGP1Xe„ f"a03wV+0̔8;A,`f#!|ǣN{ۗn=8rK@!IrvE:jEZs ĵǿDF <2S#VLDsfSDX,pV_|<"̈(0}>}rQ ཯V"2 ̘9Fx,6GmbKecJD8F.;x7mfwqIb02'۷"x+Ji233Kha?ݰzN_ca4h~"޶eέ FF&}T*^ODjPhY138fFDC|_~HGd0bs }xLaPSG}zSo$&2㱩Bw:B/ ::k6rO"fܞ/ÖOB$ruS(Z-w=gճO @Dy޵K]̌ZMUDdf"k6"Gef<ۛ?p0DPӜna~DjUD33qh4Xx3)` T3ܱqSM SHڷ٫W&y+Ji233Kha?ݰzN_ca4h~"޶eέ FF&}T*^ODjPhY13 c@+{81 4## ș;h CCe`wr\JF c2!WҜ1 κΚ “s0cS#"~ Z`0`f3c83㱄ɲ+ 33fFD $whc]n3 D8j.r߾t[s^9cW*84eff$I03"S:]a{8hDm[wCaG/JbU- v;2f` wWd dD , 3\-5᱐٬3^i`?,`f#!ddrGnF3#"7NLB ::k6rOR6G-Jr7玫ff"OB$ruS(Z-aN?nOaK j|qP<ON'c1'nejLCDXzTE23WC(ZMUDdf"k6"GFLu'K+Jᐙ@afd],rc4d,X`^}hGUEd0|?wvL {_VEd001sǍFh,xo蹕WdCc0HMa'씉d!֌=#+Sߟz~l1 UeaS_^{ܘQx+Ji233Kha?ݰzN_ca4h~"v 7oż|802ȼRX,z=BngYx 4dcfț\wrIx\DCb0\gΪ l03`s;t߷N8S*'C>9u߃tv0L  D̈ }x,f#j5Usl~o{Ձt.̌`l ,cee+'jr鞪x?w{gϞ?SWZrWV,8rofi߇aXRbf4d2uUg(VSxժ f63fh03!S/g!D̘f3^sqVMt|hϾξƝFsيʪSO~.vyW/zrTƋW"P""Ah,_kMh\kk8HU1vc jn/\JT67Z՞Ɗ?~XU T7 &"fV}'~ıcwϧ{X.$0_yb+sB$ 3*3GQT,[DMQ;`̄\#q̌DUȰ$ee¸1:Vj4 U"BLL3'q{=zggLpU"P""ƚJ_/\ʚ0bks|>ZT5bD} SN=S510%-uSw/Bvqa%QÄв.w%k0 9l&)"N;;;:3c;R/#["aPI0>^_1@xvQ׈}gBD;TnѣW1)Jc~Ğ7@ HWGADqrRd6l$"abkI}ԱGu@ 0d*7|Z=fߣ;v} ,}((*6R(FVKsӦM;#=xg+VTCn5K+FM9*"9"RUc!TWW-cC$a;g.y|}T* D7*h4jI-%zشxA;xKϮ U0ds~Ĩ׷ܕδW"lsP(c$afUe("3c\97"L,*}cG?|>wy'(Ea @\IR٦t ls.GQT՘q\,US6a鳧{2FZ.^4oΣFLV5LQ -r3]KaseL&S.1D$Nwvvuf P]$R'?&f/=s,oE:~?gj 1>/iӡ+ 1Jc~Ğ7@To^|/FM#}ǹ\T*c H::: 12cT0U @zN#Q嶛ovtt̎lnnVUQU 13]3OI3?toV4B7 " %l_|Go{VD [kmm1ZnV1|#1Cp=^~v~9;2*6bJҳ0pʉ|O3⳽ APU"ojnZW_|}b--q cL$̬EQXdfln :};}\#q̌A"PQ<_rM/@DTU՞u=#F ?OϘ9<ۥk *ah/ASb7<^뫓%aKs|>ZT5bD} SN=S510%-uyK `qa%QÄ.7sNww,a9f3L\6t:YיۋB GLS/&DTB V FQ$^ [P@oY6za&@A;H%`Z'v~qX^R3g4 @Dذz+>2jU{q.+JjkkF"!k-}&sxdELTE\+:NGw'7ʟgFR)1Z[TJ{8S+w>ڃR""T+'w^zO[kmm1ZnV6"}-߈!fSU"֨2r=F~?|8nnnvyocֻw=8'93]KWU0*ыκ55G"\P0$I̪Q IDATEf)T p8۬"D!@UJ }4?k]̢TJ9[lu v]O9{_61V@-r(j3P8Ţ:b&L;}rOXTKŋ-)䂹ƅD @TC˻9ݽ>v:\6d2r@Dtggg^gfl 0kƞM≉@x;(6P4XFk4J "Ed|GEIGRZwJ1#!=㧱oנv;ky8rRCmmmHD0aŖ=>iz1 6Uek}?[kX6""*o_νw}~6Y a)TE>t/3ιVqclz>aOsuGLTD|Ҩ>W.EYce#"FJ{7v=co}v"CƘ@UM[~ %f–8 1&IfVUfX,23DҐťLW){>1 /|MDU%"klFa1߾l}Rw<~ )”ODl UQ:\?? &ls.GQT՘q\,US6a鳧{2FZ.^4oI!̝4.$j0Zf%9l&)"N;;;:3c{~_lSm` },[MF"SanL:U% V:_)@z3~ὄMGo^R(ۅ>\.W*1ֆD$ CZl_:t a*'.IGLS{s.knjx[r9?:11w>&"`SUBW>lCD\kk8HU1vc DU_ }{a*yn?OxK.^jUKK UۊTt:}.gnZjUU"`SUcLg{煟71[+ Ƙ$IYU9bx3Iqg7}FTN>| 6rW.}-@7""M@V `z]W4͢BDl*BVuكX-q(j3P8Ţ:b&L;}rOXTKŋ-)䂹ƅD @TC˻9ݽ>v:\6d2r@Dtggg^gfl56/>]vCLD@ RHl" %"lIӌ쓓5D P={OcID$l Ѣ}5jTA{q.+JjkkF"!k-L!^2;W0֨*aP)aW4CϾ]>Q#G1Uef\o_Egp+B @TLj7߼͜s"#"U5Xkۍ1߸a &z_d2Z!"l"ҳ3Ʒ.^Jb*0D^9W(1I02sEb& UѦtBDljܹx?jJEa$"Dm@UikVԩ/eyҦYTJ7 "{׷ZjXls.GQT՘q\,U%  "ʆ} SN=S510%-uyK `qa%QÄ.7sNww,a9f3L\6t:Yי۞BE%k.dz&6P"կ" ]kw)v鄅  {&ƾ" 1Wzkw}X۞>\.W*1ֆD$ CZlKc<}T1a*'.ޔ=堳www)WU"6cE^pOkІBD<"Lxϼ 2Ms"#"U5Xkۍ1x"+Ǟ2>"l;Cv^K/twVU!"l3JD8߽I9*5;R!9W(1I02sEbfz1]9uaf 6/O=sC]ѣF3ͨ*6X߻7V\46 aP {^y/03oQj5fqEU%"lNDazsa:a$ YkA SN=S510%-uyK `qa%QÄ.7sNww,a9f3L\6t:Yיۘ2qaXm¶ xi}+a (/zuw'"lH*}|A * _(@z3~NPؔyex%/Dm{q.+JjkkF"!k-ތPoǾW{#xE߽W2fFBUӹ_>]!D U†凗{¦a)ś8Z[[E9GDjֶc9U֬_O|8_# 0x Lpo7/fPދasEi3N+.Hl"FsO=8,"D9 1&IfVUfX,23 >yIU4i U%^̑g}K_DmOU0\j?|߹ኟ<~mwߚ(D0xT ^w=Ć68|EZjbQUpΥK.dĉFk8~#F袋^}温cϞRjܺxѼ%\0wVvҸaj`hy9[fL\.cH:̌mI Hmrzv: "l{jP:Ɏ4J%" B:ν԰JJD"QaVYw=y/ _(@z3~N _ʨ9DئqrRd6l$"abs)~.xbQU".zO=sw}o\z񮻶6NUۅKu2MYQOD<5h%)" \kk8HU1vc {U_ާmrL\M*m_wƌWU"vDAx} A QK~D7q cL$̬EQXdflNRUYmL\71xTղ)w~:;a; }ɧ{Rh cP*3%UwDžcbs.GQT՘q\,UUMA4s)S̜9SNꪫVXqe]|Ƙ38#j_2a껦ϞRjܺxѼ%\0wVvҸaj`hy9[fL\.cH:̌mJ!HE|~w}!Z~dPHD4=T aT8swy$ԝ1gbi ;eV{Bl lK8s\T2P[[60 tttXk7dÏuP&qBD$d랬r9pDNU֯h?ZT5bD7xbqiӾ^qg}}7u믿~֬Y>,{Cw>{Jj,c`KyK `~j  ]n^X;ls.f2rl "t^33!عjfܱG?xDǚ*+?M]&= F SVG]΂VHC=W1gbi ;n jS,۔>\.W*1ֆD$ CZlr}1{*3c0aSާvJa57ѧ ŋ'" Q!>r/=4ιVqcl֗}34<@ " 28i$;g30HT@JVH `_v/2#6+ Ƙ$IYU9bj/ulČP&&Grϯ!]+|7a~f \P \>V13UX,*aDٹϝ;㎻;Ν{W^xᅏ>ɓo]tE _ ngO)Te w)zO=u+c|ԟej`h٪iY;3+ιl6d1zؖ7y;ke=k]}H--:JDf C5(b##+aj8(a@ q{ӽ^&P'|{=1gbi ;_jT ¶㽏8˕J%c a# @GGRuգ>=՘0TwɈ9SN8u a1fuGqu?]i*A74SN9=kkB!fʸ#>3#FxDU'5c,H:~|l9W(1I02sEb7I}ұ{rU6LD$;9gwuQya*ՒNp_>wuKS6%aӍ¦w^z_SKJD)\>V13UX,* ϒ%Kرc=?.G}k9qm)垪Rxrdlc74`@*C*2'Bk_ZZ5reL&S.1D$Nwvvufƶ qh^YՓV}lek:mBPM>6F@D(qX [ 彫KOP={Ocށ Hمw<Ъۈ>\.W*1ֆD$ CZlND-џ;W^ia Ue2jߌ|ϽMͪJDjI#?niJ a0x/QsP|f}?G؜sUDsDkm{{1Ǚ/Yد_1`PV=}>!UeJr׾qٟxS&U%" IK_qU+0*6+ Ƙ$IYU9b؜Cv׍a j:uG{Ic0NU_> >\3ﲖ0(TԄ~Ukk_yT`SιB`IU(*̌M)LtYǍruL$;kgjԨN:sΙ:u~3f~sɎ̖O:v)垪/葟,Z4oI!̝|TI0aD50lUc^XHDpeL&S.1D$Nwvvuf6^ga`jг+ˇt'Â, %d,Vo(Č(+'ڷ|_1ꕛOPUA  @=L?}DxRU(lh^zh7>jE x8rRCmmmHD0aŦjwP036|cag-:;\h4D!@U XjoGMaJ_r[dmPl9*"9"RUcMj}8-0 pMc6cvj4D!@U Xյo|/|L "Lq7oxdt&Ql9W(1I02sEb9W|/,^$iԎyÕ#=CGn4D!@UW{ιg}u Vb&;C<_zPl9(j @U8.JD؄x?M2_?O??}硇zl`+սc)垪/h[Rsge' +& ws{}` ;uιl6d1zFġ%| ăʪIzWݻ{S-U%bcaQ BD( xZ?R%*`kL  @=L?}DxZ_t,ۂ>\.W*1ֆD$ CZlڸ) mPwG6;9GDbDdX:}]7L 53(6kmm1Zn&׿^xyc 0ԋ675>23UꗿNAb0xiJ?#3p cL$̬EQXdflШ?~qa0gO'>rrk-F~O|s,S- Ÿ?ԝ/5OWl9(j @U8.JDx(jZt:IjEQVBL:v)垪/h[Rsge' +& ws{}` ;uιl6d1zx۩-q9p @ &`boZٻ{'{[dX To׼ _P۵MqcCX_s0  @=L?}DxSUM[ґx!"ݼqrRd6l$"abĨ&>y_f-j7N>̗YvviFAD2T5իWqGOLC0IK6-|Wґ*6kmm1Zn&W/⼞55DNN['䒏~#G:CAr뮻q\ IDAT5l۝:F7OQ 6+ Ƙ$IYU9b\#q;Z6 5*Ll}0jRU"avvvιtGfPf`'Mã7><ZT5bD} SN=S510%-uyK `qa%QÄ.7sNww,a9f3L\6t:Yיo; ӊ#/@zg10,~],U# z "MQʂQzS a 7d:B"J:(=.5llaDk]ʣljrom5U _'0[L B DĆ뵵zAn{q.+JjkkF"!k-JERqxkgFձalo0}OLturɆ aH6|O^E?۝6eV|U*l l_"jCp@78|EZjbQUo1Uj>{Jj,c`K%ZpErYIJ  ]n^X;ls.f2rl "t^33v.ϣ^l@X4ժXᮤuJDx(THM8 1!UwLdƋK\.W*1ֆD$ CZl5|zd1?hĘwLؾ`p{Qr~J5a*4z}4%6/w{Z)1`ιVqcl5ܰl\ϻs)ʔ _z7T$IvU]p l"b]/_ڊTSxsP(c$afUe("3c*ʖ]߻o1a{SY߿vԳSsfU%" II8~0 .d{'e] ~̳;/)h4쏆YTabkYVTZj)^Q咢y r]vgvgg~u,Bz7&,ʨbGϓY]XMMMMAT* ðP(arwea&.uQv fO_`|Λ61;rV~`Oҕv”.v}8km6d2RIk t{{{J)l]"q70dOJcs@" J[LO%mGُQ)RJk"@;Yk>uG8xť.~o~&"?VՊ).&aq΅aŢ֚ZZZ3M6r ;_ydI0 Tq9?dD"zzzg߇bI℗jKDV5=hVW{}XkZKD"6ƴjч! 1923:Wh8`Oϯ *\.3O]ѽjND˧.^^2ldZ(R" P((Ї(OxőaYgPkhp8䢣#̌{3O|~&@P["BDμmN+/,ZARQJ0 6)*wWF6nRgYalj` f,i#hE4-]i'Lr!l݇fL&S*9NqecV[HD *xЪuZ%JW<@2!f Sir;D cj9iHyg<q].L "-JǟxcOqXzg !6"@zm{fA"9a.+Zkjii>6c 6!5X71a&,D"R=acF '| {6??䒿6/"UqMϣ?UFD$"ZkcLkk}تmp,3jj\fǝ~oF EUZmhhxGⰴd{ E6L&|^kERJDRA `&0|:vLD-UA>EY?W__/"Y)׿>iFmUc;9m R`#kmSSSJE)@D0, "BD|]1vظIKem'.8M̎FXӴt0yvZf3LTZ`t:DZR [D"Q -&b]-+;O^QJ1B fG9櫗_jcx{FOY @"iD$DH"f6YU19a.+Zkjii>6c 6!T+6;ī$|fQJu^Yuy,羰t"¨-fVZ+YݫJ7ZZ"1Uk>quPS{w~+0R FQa~/ښH$Ds.JwVֲ^R -f&nzwa"ZQ)DD)APPJg] ΞzRJDEBqrƹ瞻hѢ0 E3 ߕ4 㙷)<‚MMMAT* ðP(arwea&.uQv fO_`|Λ61;rV~`Oҕv”.v}8km6d2RIk t{{{J)l҈y|Lc="l jfz{?1E^3, `r\zMw}HY*Cϻar˴smg0 8DD1\*@*)ZHYsAgђFXkJDs_}'^K&"-kg)XkZKD"6ƴjGTwܹ?G6@mfq/_ع< `lz}$@P["d fRW 6yuEJ)QJAP(R) $jR4rQ-O{9D"!"D:;9f_x{C*P["ŽS'n}Y'‚MMMAT* ðP(arwea&.uQv fO_`|Λ61;rV~`Oҕv”.v}8km6d2RIk t{{{J)l 7`p^V%UEV.?uy"'LGbQD60XC?^54zBZőe8q~SN]5Lx@6 "lU"BDJ)V7&9n̷y᭙ ?H0a!Pd+MCj ~4$2g?ݧ^rv H` /- "̜ x~)Q" 0E5`f}@[[1rH)%"J  R } JPcDuO6u?s(Jc`D+']uEpUNJ>q닯?N*ldmjj R(HBAD+#7itBD]0{3s޴ّH"xttt9Ykl&)JZk̜N8VJa T:G~4Rw@@d<{_D!>h鞟04JL|$?]{?6cD5DD /W/OnR`(rO9?VWEҾvIZ>%k1v~[/7^@ "|$l|sV80rbQkM---؀}f&\q}J1RZ5U[K&*o_wPzCͱ ':Ml%"ZcZ[[#w7UʑR 5GRxОՓ(.tŗ%AYk5|Y6k(RJR*BB8HgOOD" BG.x"V^tޗ qPsrؘxxd}BX)J "a !"l>RT;lܤѥβ6 Îu Xy&fG{#ъ,iZN~_: 2R{ᜉc__VǚVD5$"j]v[n?tu ?@.ΦOrG;z\N5#v?e :KGq.]^Kq#{~5d=E%sG~}3aZkcĴ~|'<0׋Z:AU s~ïaѲOi^s6hm𗗾pʁ_WtiUk{:}OG%"8N,z{@rι0 s\XZSKK 6`fc ZC5~G "Bm)R]kpZzuyS5`Vنjz{o`R 9 zIJgVDP„eŊ~߻)csM:ckUW\5I?NL l.xʁ,j8 ^ ^R IDAT1ZGQTBA)>uA?s87"BD-=Ûq9͛N3?\ן>Y"-!jdg6+/,ZARQJ0 6)*wWF6nRgYalj` f,i#hE4-]i'Lr!l݇fL&S*9NqV@{0$9 PD%a]vx~ҲUD!|Cӧ0Kg<:.]@D{Sn޸J'Y9?\%k։7z݇gQ{GRucu󒎷/>*Y%ǦZr7sarbl̾hkk3`\c>=1t "D"RQ]=zO>}'" Try{7 R"fWhOkmss33[kHDƘV5vPSuwLnlldf"Bm `mup:7qO9}ѢETjfg̘ѭ; k&jKDT]˧TBD6k(RJR*BB_;*S#R-U|C9ɧff2@3bYg5=}s}.H0]K5gw[F]ûw{yo[19h~avaD-,|ٿD- 0E5`f}@[[1l6Tg(Vv2;SYtI"@ED===߃q;׵G͉)Y[~GW_kmss33[kHDƘV5U5^w䆆!"ԚXB?}>{ҩ/r: Tq2}5FԜ@P O}wRaRD6k(RJR*BB¢o#B(b+KWd5Ɖ!").{Ћ_ d}&À$"D.v )/k A@±lvŘ!NHjL"}9^7_K&SzzJ>?DHB16kUiƏ/՞Fffh1ZkQ쐆koAsJ)Ԝ:gէ"D$㝛w~^i؂@ 3+R+o~`#km>ZGQTBA)>DGsОGb#GP[@Loe13*l}XS}蟿^۳FƄŕ洿Ozlbmjj R(HBAD+#7itBD]0{3s޴ّH"xttt9Ykl&)JZk̜N8VJa˹AضPUD|ũ{BD'!lO57zvׯ3$ J=+.o\v׳?^kп(H  EJDXX)-"BD2 O:hs. \.W,҂ }mmml"pSฯcRZEfmOs< T"R|#No?qPst|^\*ЇD$"ZkcLkk}XSKg8Ҩ9D=?{OkMD*įLK\K 5ߺɷ(`#km>ZGQTBA)>Y):j,)B)ҝ՟;܎+ץ  0QOoϹqo_r37P 9q}vYדw]=E ldmjj R(HBAD+#7itBD]0{3s޴ّH"xttt9Ykl&)JZk̜N8VJa @&`pHaDAlVXxf;k !Bι0 s\XZSKK 6`fcЇ8;n)Q{~\r쑟1mFDsC_s0غ ѷ^"}XkZKD"6ƴjчK?wY'z֨94;իWcPJzBNKs֥^c=q9VF|>H)%"J  R dvÎR)ZiԞzˎ\PD0R[~'^em -Y_v%Ї)J "a !"|(ZyXR;lܤѥβ6 Îu Xy&fG{#ъ,iZN[>.W:;:=öA_SAzzJ\p j{%SIf!&|>H)%"J  R Tv+H&wͅc.k76DF)U,O?konVۂ.IԻOr&ڦ *R aX(DЇ8wqr; 4hʕV<$ʈ4YFq.=}9o~o$ZE[p΅aŢ֚ZZZ3>HQoWec8ˇҴjND.>vU!=;sDCD<[|̙34^x) ԞD/d}BXЇD$"ZkcLkk}(EJswnzڢ6Psqִ{uQ\ZKD0DB?>r-y'"κ̠]?{km>ZGQTBA)qNqQOL6@U3CwaGRI!" "Koz75'9I~0'ZARQJ0 VͷrK*1cƷSN9euuu'O~3 }6nRgYalj` f,i#hE4-]i'Lr!l݇fL&S*9Nq:<) d"0(6ڋz>t1Eԟtnf9"ǐ(~Q'x3?M ¿!"l1f{H‚- 0E5`f}@[[1}Q'6/ E ۂ6HNy/if "Z뮮Ɲp-S{sN)E  Yf6ʲ }XkZKD"6ƴj)t7tX"a[`fRt^9ʇ~$8RX,zws=o{6d=/y/fd1,ZQ)DD)APPJ/lIW B_I)ԜQosѓogL}9GDw=w$f{[,ZARQJ0 6(N>qp7|^}Տ?~뮻;쳫T>jqF:(;Bc7mbvp7"+)]3pl6dJ38RD(wAV |,0{y|tdD(?yK^O488a3a#!"/X"Ac 7{I5$ s. \.W,҂ }mmmE;dX؋G 2)6!(4-N3uif&" *?/<̤amTת;tv@CЗD$"ZkcLkk}Eh=r\Z¶ "ju|糟L'J=OjD$Z_Y)aA|>H)%"J  R K'xԎ{ S-uvϡiU>QٝldVF0W|olZg۔xױ./V;5O5w]4}XkZKD"6ƴjAոι˿?qvXa`f9|sw1xjJDئDDk}¸sߛe6!`a3+W鵷A2aZ(R" P((З@ v'>1nJ)Zaaf_=q͵a8&" Jeιge` ,ʨwYZ)Ї)J "a !"l$"J)֭;䓧N6mɓ_|C=?5\3=eܤѥβ6 ÎƣKޙ2g򾻠C+B?Qh_B-,[Gl6dJ38Rbzj앜 [!$1VwW9mcoU1#( >xߟ_TPc>pPZ=uZJ۹N"E_^[hM=*OҊ;L@90rbQkM---؀}f DXZ6ۂ@4u;Y\j1j\W~zsTƉ#g]>%O6>ZZ"1Uk>BBNucDqeݰ\fmDۚs( Rۂs.K-Zƍ_წ|^kERJDRA >3#Ȟuea)G=vS;3zua;a"-{1z7peKDĉI5:>ZARQJ0 bxW\z饿o>xڴi_~/x!~W\qż4{{eܤѥβ6 $pdvNVr~cj k{Ϳv9|yɬ|΁IDՊx)?t[Lc1~{LBXQ90rbQkM---؀}f:{=1)Zۄky]5dRj]׺k\~mO @&d= ^z96&dmnnffk-ڪi:V=+`N*DΩSՕT%D!; F KD4F͌(7(F%TD6VB:s=wg=Tjda<((q|+_|yt;Ua* zʩ_ta<ڟ߲ߚ1sm9DZ[(9""RU" X,<6/?jO?Y@ $ @hË?G3+:NEE@'JDCǿ턯]|wn;:0.t6W;$UZ h6DaXTUDl~X|駟~wc8ugx≧rp}Uw={ɊE8::\r_>{zͼ&h =5zoyaLbl6j5fIӕJ%c""ġ =rvT0jɩ>/њ IDAT!u(v86x+O?~?̄w0^64n{}#"AD[TkVs77?z/_hrHlyU>+ְ/g]85yO c 0UUfn%"@\6_@h5Zs:C6'DAAкVgbħ;nKR9D=NUZ:|n" !ZwP(kmWWXkQUS*#޼h2H:uEATU?a a"<<Z3_oB &aX,U8vΉ b"@(T @D@hVy,Y`CqHWoܰvS!Z]0ׯGʄQlKٱcyaҤ]fL&S՘D$NW*8ƪ%Ϝ&EQa0"( " yS&3,4;1A'^'A|uFAT@zd/ȂDw=\ZQUD7|P /HU[QI?F~CW/[⡭{ssN8W͏^/qj}֋a+\\Z23vww(}1 !ЂWss HKG|tt$ U=8r-w5~&H*(D$n~|Ht\ڮ."21T*13< -)'V"q qza凷w*h9D=D+..;}GDo]s羝H*ljbQUῨ*<Sc9o%+l#N`n*U˳ H mv9;v : Lkl6dj3J1 @0q3}J_ŸCUV#5^f#7Ly"gT-9Md:Ĩcg BTPk~d2Q(>_Nֳ$:p/ȉyܷ,;g"Ozd7?zm5 ^}Ǿ7>Z4RPQd6oƝ}A *s. \.WVarϡ.uG7UiFѻУ?~3AQ^}հot}?NID񦪈(N~{ɽOoZ%"ZDTUf6ƔJ%f@f('?{wn ! 8^R̪Z_\m}?O&:@UƕpggbC$Dx.kmWWXkQUS*W|9O޶,"¸RUD(jraO/GJbG s/)Uefl+_?.PUƕa_̈a- E*AP,Cl/Gu"QD@fWy^}G>:RlZRU4N%S_g/ǺРA#*JG!/C3||>fIDaEUED}Hj[ec <!# |\lƛ"6F# g_旙t&LZ-UED*"z7<<\}ܹ懞ޘMU?{n`a]]]"bEDUefcLTbf؅?v[qLH0T ]2nƗ<]Tj*"_MUF144%oWφM#""7U<osDZ[(9""RU" X,BzzɇDq>g%U˿jT%bxgcz9|鶇x>D&NL`>{wj5-1 &aX,Ua!ac9o%+l#N`n*U˳ H m#6<M;0h GUv~e3{Z' ?S U@=EU_]/xPSfj%1-1AIx8& G7~ug}3*쥈hgYZs~g 9I=?{5B0&ι0 s\ZefQ">ec mL>*33!L jԢp{}8*Õ b"DTU zO|{`FWԢ:q o-6`k-"*3cJ3.8K>k%" TbCk_ۯ߽f3 C@DPUl6EdҏS|UDADZ'uKoWe- E*AP,vAEE.V "L @-{\٘qϯ7>00NU@Uaw @Q}ҏxjǦL,"ĠجEkq玧 Z h6DaXTUD݇sXTk!h8I7nXV-.#eBhz[ev?_CQq^G׳ä6f2Z "tRqLD0V?>sZs" Ϣ #P @W|`N;5aG^ {_|P4`؃ DKc?} 80rjFP.1 :Bt;^W63L٦8=tÎ-խ|mnCF۲1TTTG(DkmNybKI=ŇaN"„!Vy`LjvZ%"ZDTUf6ƔJ%f]PUQ9C ՘& UE$j^6uNesϪUnVEDc@UaBD@D@Dpq3{ݗv fIDaEUED}Hj[sknklVDLL̆=sZF'ҙ,,LIgΓcS[x^+.'PT&kP®XkDZƘR̰ 8\pV "L ""wfuo{nf&&&60Xk(NfΜ S817kF_k=OZk=CDUEDTI"Ye "fIDaEUED}Hj[\ jt:=utg*Lwt$/q!ew>=On0FVPE@Hd|o巗#Va]]]"bEDUefcLTbf5g]*U6l`"QPUuu)/ef:՝ÿ۸7ڨP٩Sө0!3#?lQYQ:!fBE@HsqӅCcPkmP`(HU(bHDk:w}P& E@+.]݇{xsz{oyphkõd2f;ӝT: Sxakz[RĎ궧wl[=CFAa"'^ &1 삵6Al6T5 b Cy,Y`CqHWoܰvS!Z]0ׯGʄQl.(m+jA{T=n=&լl6j5fIӕJ%c"1iNaTG]abS o߶?|jMvʏ%̰WkZSf.G;tP OUp_ l9a.V̌0JD|rl]Q@f5z;Yx+ZQ0Q!`lSA}_”}$I T:3SN txI׷?3X޲YI IDAT+pLBAa&0믺_=֑IS@kmWWXkQUS*vAU:X=-&1!"L`Vu-F33Sed&8jՇ};wHwM6eڔ0A Q1w6= {0tW^y?:}aB(""U% " @뭮Cg &P@B0vQ# Sޑ˶iC}}aGrl*OT-7& L <~dgB"®Xk|f@U0,1ԜxΒj} 6'T~ k7rުszLmUq6삁KO[FoyGyasxa^Zf3LVcft:]T8&"|~:S"g|;+?'WSUu|R;~ LxJL}_].nXb1q΅a*3cww7\.cH?3GO-tbKD0Q*!pp܊;L*љ :~2d4jFdьCZ<d "D%"^TV#G]]]"bEDUefcLTbfxAIGg_/A[n03LT#ZVqxl|?=Zی"l܊bGqKӰu(D%"w?[Yk 3GQDDJDAE"]SPPcO?û՘ DDH ڈ델ᅝ)I#$/8gQ3j5qՇ1 {*03V%C|>fIDaEUED}Hj[ Oz:UD LUЉubEe""!"!23UEDT@ "욵KDl)J lxzG?0)( UU?#/DH#&0~_vvD] fT (D/@C!o( "L`FY2BEUF* !2 *0G~m4QUD] &aX,Ua!ac9o%+l#N`n*U˳ H mv=󗞶pYc37^l.'"W3l}f7.hqoϲiSehͅz`Lsarj D}(xA:_|cǔ "O0BU%;^KDl)J /HU }əO+nL @DjDB(""U% " RU`CoQ9Y["""9T\]ֈ^6Al6T5 b Cy,Y`CqHWoܰvS!Z]0ׯGʄQl.(m+jA{T='^H *B[NCV&M`l6dj3J1X ٳGaFϘ˿OƌYsWfIDaEUED}Hj[}LՌ1{gNMOT+Ck.\[c 0UUfn%"@\6Qgݔ>{LTaبFן:3‹vuuU1RŨ82#s%gd:39"I{S^AB(""U% "QUDPշ8rּba#ঋ,?`A4M"U ðX,*"CPs9KV,5G$R7Tyg2!AT=- J{/=mʳ=y;~y@.%7Z„U=>z,&0km6d2Z@DtR㘈`Lj##gLR{Ɨ?wEjܻ[~{ֱ5"ʊ LxJL5-b1q΅a*3cww7\.c ,8#shag)XHw͛cD6XkDZƘRUmŭ~õ1&qL뮼'̌kmP`(HU(bHDr[1=YL'uWnD"@Dxq|>A$"P0 Ţ"">$l 5-dŢZ_ A{I"_qMjyv\)BDcܲ.`gf_oB0isv~̐ ‹ &aX,Ua!ac9o%+l#N`n*U˳ H mv=󗞶pYU@L#D_⦫Τsm fT (DmBY/qܧDBU BG!]W?M/N|>A$"P0 Ţ"">$l 5-dŢZ_ A{I"_qMjyv\)BDcܲ.`g<4G%l^fFgcV_O0 ڴ3>gMP" `ι\.^Ռ1D$qWEEIijpcNk֓M<6҂W.fxkۿf<2v󯟾ꍢ "4M0jC]]]!"֢iJDI>5gqqLaRކMxXU%"49W*D9GDjV*c lx:8%aXJӴ5sU?? Z|%"49W,1Q12sr4Ue;n>r2[@ hT%:-{ %8-vIdߣZh]͟U8ھ{5qCnGP+ A4 faeU%"7j!4MUD9Ue= w/_]7I%[q[yo3H  ;}s-z{zavK6]33\1ï'bU m(Äq0\.kooj"f{{{8ff.=jSw&DŽJJa[_YhG TPw/eWy68H*A?G7̖}i|Zc #D}===Z U@xO:bROiw m[_eWsRIDsDkmR1`o*d?eI3 "TUDZZ2Onz/b%l sX,c(bfUe 23co*EԳqaj[_S;/h+n߽3;?-9o1~w=}/]z~hZ;y/ &2sP(Ah4aX.U{TyFc„ ̜@E---DB^8{H*`nR{+/͝EjQ ml훳hɼe _yY~=Ä&gh5C&;9r\{{{V3l613c`grl|C8g{ʕ{]]5+P />#杫:'DS I-ڍ0&M0 |Z5PWWFzzz{iOm O,dȱa{^WdcsRIDsDkmR1`%+qR:$"c ƍ4yqgxm~*sX,c(bfUe 23c/L~тܴ4bɼX'.id{WOju?q5cltc7~'(/+1DCi {9W( h4 @U0,˪JDCDcǎSO=裏[sYg5sGyOzf3ίhi ֭޸-żܙP MUn־9[vY=QI0~%׮1Lhz6Y?a¸s.˵j5c fq33v'_u)4eqvM>!c="+s5lT'߼ UI% ~4 0WUc uuuaZד'pmzuc͑Tпڇ7żſxz Ä&e־c pic<4G%mV\ԭ7 /s^éaBT2ؕwD ƽDDUs.˵j5c fq33 /:I>8)fU'ݻON3D+QES Snt=ZApbˏC}i|Zc #D}===Z#uQz⻏9\h':z5\b7ιR$"9"RUcRcOP޳Йq#_(AKp-?}=JDKιbhU (̌}C9o9?.;h j؞5o Ә򽉋O<8i}?TTn{G<_lE[!^. O~ו0(@[ιBA`fa\VU"IP( 'wyM>kYlٵ^|K/=s*=qԂûίhN -w=~!S_\:qv`Ca4!-o{6z=l?]xيg9:m]~^ >WZFTijh_WoSk^Ds.˵j5c fq33 TNɶ.?|J֚Tp@RU #"V:Cs}yx%R(^Sjwkƶ O7}v-Uajj.XkTT-#'v$rLaqP0/~oFĤD+J"#"U5Xk+1{OUIE^rNn 0nTD?uym`xFE{9W,1Q12sr s;)D73?{0=c'}5>rClg6rd[wm_4lذqi5VKObkOWذsP(Ah4aX.U1<̿˿o>3;/| 'yZ;% IDATӟ?úίhOĶS{&4AT Zسӂ߭{Z4G%%ДC(2LhҞ(sųEBL/$ 3OrrZf "l7cfƾS=Sfd2 CB0DG/$. uGs_'O("xQ4ƷC\lf`* Jm#žI4 |>_V1Յ">k-F!ui{?mr)1aܨ;}CsT*sTcT*.v&ԩS(bf55냟O=5PosbE3*3AP. "^`9DqI+UўٲU$M<1T*elE'=y-F9W( h4 @U0,˪JD=y^^`wsY|eO߃䩇u/_]7USnӾ[PN2jH፛^ؓ6>zifc$ }f$87QM` Ueh?& 'j "{*8s.˵j5c fq33 H/1ePaP2JDvh<1G&qNٓ޳>7 4uDWUAR_M=xABD偾Ͼ|,Uajj.Xk gO[|7?yIG фTķi{;z9 |\wg1SZFTijsCg|810yd"9k1d8T#GL}@*/0%@DUC41 #P'LBK`Dwwg?f'iJDx`={w?U_<@4zlpcbMajj.Xk1JL=SJ?$DF`^R2~7 T+J"#"U5Xk+1꼅',&1ny-럿~:17AD1:ιbhU (̌QPU"J"7Ȏ7k2a&)FUD")ဤ#d|sp˓-F9W( h4 @U0,˪JDC{iZ{WV*˗XbpphKkЋ ^:n,9J_C?:ӮH1  QOlq̹6v`CsTRڶ嶛wǛ&5k85LhZE<_wʄq/4MU'9r\{{{V3l613c(`})tN|ޥ~/!U1:D"dYyӦ񤉔IT9y]u~CS$+zsε7ʇG7~ &i뤖?~?xP4M0jC]]]!"bԈ)g? *ca\S FDi*w_a˺r*QsΕJ%qcJQcЂO9JD㚠D$"gVEEF9W,1Q12sr1ziKN@ '6xP*asB!FT5 rD?dٽ{{1| _xϟO~rҤI===_~ ڲm>mFuc͑T2m7ܾV^;0 ھ-^1P7gђy.9*f/d53:U:z" MPUЦ9k3@7s\VcH6㘙 K>1m 0Ƙ(P&XQڰmwLD QZߓ5?2aOU~/+_[pPPUIuBpwx˓[T*M0 |Z5PWWFzzz5U%h0>-G\p &"DĖr|:hU0jιR$"9"RUcRc0jʆ_(RQafkhmkkBDqEcLE̬A\ffy =D 3c\th?MmQr 3P0 岪2`$&L`zn6m֭[=d2 mT w/_]7I%[q[yo3H  ;}s-z{zavK6]33\1ï'bU meZ {D7Ɯs\VcH6㘙ImZ0*al(~2`I0-ZݔV_U4 0WUc uuuaZAUHDN8cvۏ @̄qMI_̸S--9XP#U 1 imW7&7i홖Tݯ? }m[j~zxj|>K4 |>_V1Յ">k-UT>w:T$!"~CksT*sTcT* T9751Om-pUaqEcLE̬A\ffWIu  a;1BT01Q@ Q!"D=y׶{V?) sP(Ah4aX.U?QU#hS}OcKv׍e4GRɴVopb[yAnL(RÄ&ghx@ekߜEK-Ǟ樤^]k Wuw&4AU=CvƋo:ӡpHg_<&4GD3?ٟ |fKι\.^Ռ1D$q DYn )3  HR"DF#ЩSF<h,)\ $ظ㚇_m"BD8hb{ǡ7>-3AU@8XH*~;u.NA Uajj.Xk*8z6;uUq anl!"UaqΕJ%qcJDUH.q<4G%l^fFgcV_O0 ڴ3^|SSt^4cG<&4GT3?w@g¸rZ@DlooǒQ `(&]X "Q,Q`W@sM͙4UMI;/Dw`n-g^oZιCK|?u !"DĆ7kfTi|Zc #D}===ZCӦ/8g.1.ef)"3i [y}(ƂsT*sTcT*_  V̇-{.I~0ث|mima,8Ţ1&"fVUf\.337U%"uM4>ts'wXC;5嘻IvWyg[Ϛo;k}0Dǿy a 8 BFjrYU{{N^:n,9J-Xz۷ rsgC&4AT=Cw+*[,Z2oe><4G%l^fFgcV_O0 ڴ3^|Sӥ΋uDŽj⧟c3wB1rZ@DlooǒщE浵\>}eRQbBs @ ()Ut"鴼JDTACU87c~좏OZL vu׿a&*DSH~yY/c4M0jC]]]!"bzrSO=51 ß=&e;;V1+J"#"U5Xk+164gS?~o 50ϞpWuְEU16sbE3*3AP.c%M|y]]@ "$4~~̷e6n\} gtFҸwXuTʽw=IDm`ZGk[&"as 3P0 岪14f/޽t~mwXFs$L[n o)潕"5LhzpW TYd޲}yhJ/ټv͌pUwǬaBT3igmCɧKM;  O?{O$P{ ¸?9k1d8z&Q%bI 0CA"P(HMag5ӂ!HDD "wCfg؏n3w DdEO|/3"*֚=o߿ 1i|Zc #D}===ZbjT9S_Y9 Q x]ꍽ3@E1fsRIDsDkmR1`0꜓gŇzHԈ0̨*dZ2˯k #"\X4DQ̪Aef!(dלuQŠcВS/"^x_.~nz3?{f՛6̿Y6;w<ֻ ח!0w t{v#C U%"\Ph03U ð\.*aS}OcKv׍e4GRɴVopb[yAnL(RÄ&ghx@ekߜEK-Ǟ樤^]k Wuw&4AU=CvƋo6|yѴQ0_sw T}fN BS @>8\.kooj"f{{{8ff|ƴZ*bER?AOv٤4rY߂A"H @1Gχ's :37lؖͦiJD8c^xጿ<]mSaЦP\oOxP0iaժ10BD|cQBpI>vCR(3CŸ"dx!/c1+J"#"U5Xk+1cCD z^84{NqfVU"Ÿ"P5x7_o{l " \X4DQ̪AefƘ"Ij}sg>RSuNaUa( iζB={ֻ:Z&%i]1+ܬ)_f&&s϶߻-a^`{7sC/Ua8 BFjrYU{{N^:n,9J-Xz۷ rsgC&4AT=Cw+*[,Z2oe><4G%l^fFgcV_O0 ڴ3^|Sӥ΋uDŽjx2x%85A3T s`qs.˵j5c fq33FG'Ή<$_KSÌ?LdIi N^;9&a U"T %N;?yg}.dDpSUf:㯹7 @8`+[π0Jiaժ10BD|cXRUbJx7NS 4Cpn1qι\.^Ռ1D$qF [{T)%D#HՀIJS]6dc|O XR$)#rQe;w]384Hzthګ+HAGT2F-M0 |Z5PWWFzzzcJL*&R:p{MpqltPbU~y| 1(a9J8HU1JbSUf9g_C7baf"+]Lg w~?[YBDcιbhU (V IDAŤ1D i$t_Ij*4 c#vѩz_~=4lQG۔S8퉾GftCW?\z}/,\"[,l`}ӨF}dob*JDcιBA`fa\VU"#ӻίhi ֭޸-żܙP MUn־9[vY=QI0~%׮1Lhz6Կm(ti;b19a;^ӖxAA3T ᆏ@crZ@DlooǒQKD:<fRwI--JbdtrP$E*J VkЖ[՛w$i<\;;;UgUlB lT۲'ߓ4M0jC]]]!"b*1AD=c;h 1(a9 BFjrYU{{N^:n,9J-Xz۷ rsgC&4AT=Cw+*[,Z2oe><4G%l^fFgcV_O0 ڴ3^|Sӥ΋uDŽj{N[qP3\>I@qs.˵j5c fq33FG*OQpFLb!)D*%K8#_[?~aVdZ4%"0TZ;8X+~5w~ =Q!"T2ꍏ߹ 4M0jC]]]!"⥡aX 9c'$f+–m`wWWV0/ \T1Z[T1xI*ıo~C냍4M1x%Q "ٖzvw_4PU"K9W,1Q12srR"hIyuᄷ.ΧI*NNUSMER&ST0@*aN-a A?\u/>~ԉ U+ A4 f؃89>}wvhwF Xe/jVlI&M}N$ CcNNr:qslls12UFIhfjgvgw;'iGV d$FJwwpyHXnw?Jx-ŔN02trpo}o⫯ٺ̔!Cä[wxr綱cQoN :3mofbƧ3`0FTo:0|Ƹjj [: *|{!U/{_.KRq0b899e<_?<-t(D `"b>\5{j6GGG"d^y+>;w?Ha"Bp \IT*v휓 ,3(Z p*Yg_Û^zqS/"D9HqO||jB<Tu3ދI\f9SaxyoWۮzGFF^*"D9HFQϳ/}O}O< ^tZKTUIjǍFCU{>NW^~dpA/"$A /=w$N" jqSU$$i4$EOT ܰ;G.P r+ݽU;/o-t*~s}S_}vg4 1Z&]ݲwǓ;m}s*ɞl}37o\7>P1zӁ0U;Pa$TSsQzar\*:s,T?0z$0)R|G 8&.ޜ/|/v}1=VܡCo='n8 (&,́~Ź 'BIT*v휓 ,3(Z p*O[޶W9 4B "8 !*;gsߙQшS{_{/"$sA4MN4fMoK^L4e*$,O='>z}-T`,t`(q.Au U\C^\.JN`fbqrr22Uʼn ?2//,δ}" u-,~rO\wn:ޱ_x{>ckתHy~컮}{kDyrTt:9fV,''',SUË~eWUiK4p?/3ox_J۽;CG-\D 3Ngsj:_wS_* zɣ>SZd N7|O=(#N<ϓ$T*v9'XffQhZAT! r|T6l=we }+IgC~;Щ 8Uz̼"B9At!)q{RWߧtsqzI'SU3IÇ]vw}`n0bl::604a4Bo? y$Jn;dbb,"V+$e B5tޥ笻ja!%F,Hb ,qfZͯOt$ "{_{/"$sA4MV"f9)fK\~[ C7"I$EDUE%Wß[L)&.pfFRDxks.MSU%q7 UŊ!"$n?7w9^Ss% HbEGKdw?3ߞjINiJ཯Vqz=U@2IFARDpD;߻ ns`,HݻZ%MbJ:98>7ulqkwfJaRu-{w޻q c\K-@AB>=rT*u:3+Y*Nmoq,tUCf^{e]@IN_y?&sw~gرBTsH&"b~y޺og0h@@wy^dgQ@ s8l'NIT*v휓 ,3(Z b|$,%lxٹsKai4$!*T͹}5{}h1]ȢBHB+^^DH: h69$E9eW ZW_WU]f&KTN(*ι HK{Գӏ]gf>/)D4V }VsΥi$U5FX1Hn?]q<9sR[{Y.Th,ICKN]ynsx;7Zߘ>6u HB+ZqT$II|+7lΑ  #Cw~'k[6E) `d~\sW_uǭݙ) C IqmymcǢnߜ @2tg6~׍Og>T`,t`(q.Au U\GT@qL 8yrTt:9fV,''',SU(AP1zӁ0U;Pa$TS-d+-kI" }n _4g4}\.JN9̊de*vkŭ>U+ HN~Mn}G8gD3(ʖJ5ȟ~v3SSS*qPHDBDziKT*gW^qT遻秾".82/D VHD$DR(*f<NE'TIT*v휓 ,3(Z p )Typahu$)Gq0ZF+IE$gDc~/^,{~Suž\:* p HNznf{! l:p )""difYFFJIGCÅ0 kc6Ԫ R*3[ ÇS GZnͤ4enoq;.¸A@&Z9T8tA@ Q!a|?ϳ7+}:`! pwU [PxTD]c4TZ3<|pX0DD4z kanp&  F 椉Fx_i{D}VsΥi$U5F8,(h4o4>(NUETԩPqo@\ h9*KTqW8{ $I )"xay }Ї>O={b0׻ns`,HݻZ%MbJ:98>7ulqkwfJaRu-{w޻q c\K-@AB>O{>;ow4" }:$,w=穈b$}_ I眈x2I/˥R8YX̲LUqPx؛7 @"H/~v>_ = 'f178𮵟Z(ii}uB@ G t* /ytnh}BE }]y$Jn;dbb,"V+1!o#"[%"%g }^73htAl6s8"A "G [KdCg}VsΥi$U5F8 s%$A- #"X&X""X"xqW8{ $I )"x"tl|w;ٟ'>!^7]qp\`,H|ugU|bJ:98co]ݙ) C I+| 4y,ͩ`$C'ߞ_ś{jcәU0# }zsQ_9 [: *| ]msH>qӟ:$)TD1Nn:KՉHITD0ÅBHfY|?r\*:s,T'@pk?{iׯ,"TQ s H,}mu֌çT'Ar=T@w >_:젟A+Og&!.$I*JvYEZVXuFu3ދI\f9Ug4}VsΥi$U5FXuFW8{ $I )"xnn}{yWUwqG{4Ë޸7\6;y4V(_}O{v_^{x!S x}~kۋShb0<}߸VG핋*bf @2t'j.d{Ig~͆{S|\> 0=?19H $/gJTE1# {>W\y."~HAo|q{"BRD0&)"rT*u:3+Y*N#ol='dF+ٚ{.$B'雮s׵c;{Üt~mU)"XH0}_ "-$I*JvYEZVXuFu3ދI\f9Ug4}VsΥi$U5FXuFW8{ $I )"x:??e}رcǣ>:11qw}GF|n^yBuzl>Gz`dH("A}~n:rvxzg( ݦr8"!>8ua=2 DBzQ:,A@RfzcaG 3CQw9 @@ fA@̬\. xt:yzȑ}s{Yܻwo窊{_.KRq0b899eD#sKGo~TC ?faup$Dp=w];X8H,1`(7?˴<*yNN933{gɐy$Jn;dbb,"V+:yyEs.fê3V94UUqPU:yjǽ^OULhYXX袋~>OOoón:2T-yBjn<cQ<U8..0̍]O#  ELVP Dħ$ҧﳏD 3pD$p1YXXȲ #9<<1I9rH'}\.JN9̊dexN//]"S̡\C B'雮s׵c;{UA5=/ Ga9 @p;_ C[&"XjޗRts̬X,NNNfY8,T^Y$M]>o9*H``P"@@ucb0$E牐9 A c0 /b'Sɔy$Jn;dbb,"V+:yyEs.fê3V94UUqPU:yjǽ^OULh|?$eIZuޗRts̬X,NNNfY8iH8E'W[#rsxp >E|uq" bU# }{vlg 10=@AO1@E9v4Õz ^,fnaxdr>dy'IRTsN&&&̢(j 3^^DH: h69:yks.MSU%q7 UŪ3ZqT$IIUg(}\.JN9̊deD8e-}@DpHhtߟV7^.o\X:}c! U 雮s׵c;{UDPiTҨw8ә/By|O91#ӱaN>. ?ODq`Ty'IRTsN&&&̢(j 3^^DH: h69:yks.MSU%q7 UŪ3ZqT$IIUg(}\.JN9̊de$ waϛ⤣QbG>Λ >͈%#_y9tH) @HtcB W;Zїep#ml;3UG@\AD= 7$=1B)hSUprq1,|{3w>{S|y'IRTsN&&&̢(j 3^^DH: h69:yks.MSU%q7 UŪ3ZqT$IIJBR\""Q|?$D "8IHKD "D@N4? h4/˥R8YX̲LUqPA'^0"4hǮxߡE,'"jlҰy=Z8a(#yx&N雮s׵c;{Uډҥ?eZ?Z{\6&ݙ7qW߰ܳ3;Z@`c QBo!jԘ'0Z(z%LPޱVq]igfOyG =.UGDዩXv\8?GT/~zsSQ"s. rl6, "<4kmkkXkHU1絷c0h@ֶc8ffUe J3cЀf-ADQT5 RDCUGX+DعaU%"q:1g5WSɉ*8'J*=JlT*":;;$af*VqMێ$)[:GRV>}fZ!D*5Ɣ9cևg̈өgV5圂 >lTs/9ÊՄGN4irb!8T~%^n@!kq0D;5yy47N3*P/'y)(@oqgR/3D<\rCmmmX@D|y hVc%HzR;/zJDBDI|tꜙPr*BD}sc49"PtH@5g|D!d9y\+"s Zw5z*JDXYk B>T*":;;$af|Wb+ mԛتaSI]3rvH$~X8mҜ[HS}vnEr`;b83ĉxs>'ke80lnn.jkk">0h@ֶTy^{{14kmKK1&cfVUfT*13 hbAE @U0,JJDX8`/iѰ̺K6 WѮ=SjRY?.;jWUAP@DPU@@UT]k }5#W:O,a{l{y]}}wl3WekN=k;sg}j "Z[(|R1\.ٙ$ 3;C˱bٛZ%[ fhҴ,mHD ͊ԧ2ߒC_{i~Uɡsړs*|uYÊՄGN4irb!AW*x߸Hm_{jfM5dWׯ|vvrb*3jܡ ]ƃ*RQ=x;L=(3ι0 1ڰ< Ь"b%"U5xnZb㘙U (J̌AX,AE3P0 KMܐ%Fq! cʌGz3vM|8kn;] $q!TD 8cTʞΪIznixGn]|}኉\jMg=s[nmѳv?{+_Ň9M8ql 8A*Esb?ŪJDR t&ME5(L*0d(Q=rN)H*2k}ι whi.-RR;ۗ~?y[\'̺o^{A~_sSn}v{o뛇"g(@=x{L*{^D* |D};x#1_bزIyUrw!3oKagƈ=O7]ғ?{i{mFeΌpYݎ\hFv}lTa7NKf?&!:Gp%i_n3\rCmmmX@D|y hVc{rۜ[ /^<;|tY{y_}?NUaRF0h@ֶTy^{{14kmKK1&cfVUfT*13 hbAE @U0,JJDX86-[A:EIYO}D8Sejw*CwY!8sf.37}7g.Y7MXgʋ̾Wj>um ڳYw&ȃ-_m{hw53'.ބںzhHh(QR5d @Uh.C{1)lTpUm푫̜L3vWk]~/7cy{گ}7e-6ܲ{vN\/5LOV>ݶ?̞n׹enԟ?ඳW|GƇvytg[*VAq#On$OXn,0)h(f,˄&9Ne>;qs$uxO9hfv?ן^ޘ)Na4אaQ,⬵B!W*c rI03C0rdf RU ߱Ds-@'NPP!"|Da(@5S'kiX( Bk Tk $PJ{w9Aړ9|kȾ!)&ч/W/N!\rCmmmX@D|y hVc[ޘN&ꚻi,ílJ pZfH+M|I~]}2oƒ{tOnog n}%ZyH)1%_0,?BK5Fv^?]c}581m#Gqʸo{԰Qoo~.֖hklJTh |>_T1D$uvv&InHo<`J1} .GƯmݴII E :i\H&ŷ7 |Oz.]>yjAo@? Lxn3S*JDn90lnn.jkk">0h@ֶTy^{{14kmKK1&cfVUfT*13 hbAE @U0,JJDX8κl>~&mIFo Nx޷ݔK9Q8_]?v\oU 0 v;eֻl6U隻Wr\vv:r>^'Eu߳la~yםp%w\wۮ=_ 57^sۻ3w>#7v_O?a>)\ɴZWBaH//zoyCNaiE\+/W9be¿xd&/?j_lSN؋~;v[ތTSſ]qMϸGFږ,}_\k[k\ܖyx]roץhMI#8p?j fbkN}>^ԐM*mB+1r$IM-6%e_rY^RQY#¢D :i\H&"EՁ20k&)3$Nl'?zHU9\rCmmmX@D|y hVc0h@ֶTy^{{14kmKK1&cfVUfT*13 hbAE @U0,JJDXh*NWj1LzWx'?b $ݸaGȃ+ -}oNxFowܸM[ӕs~J6nz)t~WҜ^N_4mɇoΙgֲՑc7ksI/Z='Uz+G/L>+uE V-r 5>^wCU̖Mz/+㣈C38;g ?el%>zG7hgϜ| n!@u~Ώq% WO˯tלx]g|q'wz`xN;auBG KV\TXW̙뗟7k׺g/ˏuǝÚӟ? ^4enxI<޲+l{Wl|Q[3^9.0CxFiMq9?yso~|f8uz^8)k:큽[ǎ?"Z[(|R1\.ٙ$ 3GMFd/rhƃ%b",8w=J+x4)S"RU'zܱ\ш!~ IC6:hwJ;5(yD(%帰iϑ<{׃&jKeq;O>stWjuuZFB^WP<{M$Uߒ-U7{2ulZT)*3U\yO $ftaNux)>=vXyMvXm+T+GeөS8f^{m%V d b\U G__qA1=VٯFUc:WW79ZH՜uB+1r$IUxr)5P0ϓy7.)*aQՄGN4irb b?qJaLDA#","\:u8!"|Osa677ec a}@GGy4Yk[[[EZKDj<A13*3AP*4kmX "faJ%U%"|]bcGU1P:{Upq\K3':QKb#B'KmޣꮿRC%6`k`0aZuNϼ` |U0ÉV#utP*]9~ڹd~Uo..t/q]^s#S [d S&D*3>YDRˮ;~;W>5/aq *shoԸoƯy˅53 >3TmTZ R"7y6K^yӻ0j T2{d)R @R17;5Ŀ2q։Ğg]k7nyg*Zȳ |>_T1D$uvv&I*b'nP8pr수_Il ~Ub7(> "S§_T9uҸɑL "E?U o2rCFAD* 7Ҟ?ι0 1ڰ< Ь"b%"U5xnZb㘙U (J̌AX,AE3P0 K "P,$U%"f~DTbg0"JD_ N{R)9Q"— T! U*1*/9b8q)U~QUAUPrs )*V)91HU!D@R쥍:+ RU"g*cJbP"P$!mD |>_T1D$uvv&Ib]9QC "|1U6~\y"\MO |c "|038Ub;qiLTe!Rȩ5M4`R|5J R#$jɫ#2"! "TA'=ڻ0BDRky;`Ϩ/\rCmmmX@D|y hVcDB+1r$I7 Z9YgLo"L "|Pue_aK)3aEϤ[<_ Kd͆փp3Wˇ-^p{[;bŗSj#N49ҀIe /iԍzPّ{ֺ_<2rGܗ*o߰o~GN5ߘ*HڻZq Mu=t3bQ ð\.c :::<àZ*"Z"RUcy Ь---Ƙ8YU9R4YkbQ13U ðT**aѠD~ PU"B?U%",TODU%"|*OU*kmPJ@Dr\ggg$̌E*s|]-Mώkua; ]5"—Qj#N49ҀIT8ER IDAT&^lTD^.}_:0nAW:d֓ CDTAGA^nW@%k @*>|_T`)a ð\.c :::<PU"bC"Nc℈v։*c":g(*c8kEUDDۍ1 1A!Nc*/8]JysN`3YDHUɤ<g(aпֶc8ffUe J3K†APQ,bT<g( uJxLPq kmX "faJ%U%",4U5LޚԥXDQS*! aU˨*1X&ٸZú0ĞD]8͇FB!GU Q,ZF*{zz3uoDp UI(]Z&ga%6NA/ ITgPD~*bJjr_6=U"¿KUMWVȯL "FxV_L6^)R$ ",B!W*c rI03jzkP͆,Wgx~eBR>'[77CK iHypfC󍾷DF߫ *dGT9uҸɑL/ ?w0}Nb?QKm&/D93w/Dh?4۳ 8!f'J{ĵ/ժT(a ð\.c :::<×*tUe&?Q"·OUxK޺|}ొtkJ7I=Q" /bmmmk-1vc !$q&uEPaJDGİjS&_*S5w.rOQRUl>mĞTs> @Ϭ---Ƙ8YU9RbDzkD6=Xd*aR7+gVPy=՘ #6?/q7cEGXkbQ13U ðT**a0iy Cd(18eW_eWĞG:%6LTD 8ĬI>`kyU_yɗ{i~Dưo>[~T L:YO, zM퍟.)DX@U&"hw{sN3LB1LPsDVj͵'nų)CgDW RiӲhCõ<ަo@DJ̆O}T+䣟̋Ӿ(8%GsN@l D"OV+/kvmo}mgUCY-@XXk B>T*":;;$af,J0J"-9-n j2Fz_w/Wb"@"b~%$e:Lhvynb7/֯X)W_䐋ߟYcRj#N49ҀI٥L5aDbs+/S~82u:=u@0|3T2ds+tIۣ^br_*}B׾dckR)ι0 1ڰ<|!e7Q\)]̫~_ R SU|T?UU%"|Ҟ9^y6x;mϡ.\7^lh(Jp]]+;ޏriŠ/dmmmk-1vc .2fh61S "|Mb-9:zɗ)D fPU1 "UkR%nC}^*Fk]Lr)W]vx+a?ֶc8ffUe J3s)jknZP+dq۳{#@ |UQX@ELT "&RTedG~U2;[/s3FxݷYCB X,AE3P0 K9m¶C`@,o](sI.qohыI.SZ%:QMe2>3Wgq7\i[qӨ]9p;r%_gu۞vX.7\˼w8fVJ{A%Շ5UCHDuDTXlR.T{ˎo<. DQWzzbJe[N(´jMݣ7e{c7˽^*8iV?/ёw43NA' ֪:qN.Ql<{M{RiU!bE}U+ |T/֞g2L2 " 4X#UlJ"ETA ("`Ê]A($$3If^krTùS@JYxeYw~zt}M- %1JC%˓.e+JK eC^q𴹩f,ۚi YX'Wd{s@DC gD 3{fL#qTkNBHSvfg81Id0/&[o{ta! >wX>)x/-0D8h`4RJG%cm -A]qZDyI˶A8fL58ڭR 2 "֦ZmM%:t]MӰYc:]hY틮زk,7y5Qo7ayqQJcRDRJ˲ 8 >IN,dx ol Nj[;6qȨTʓ>hlGz ¶a&" H5o.-~alO}z ܺ湟AsVYZW5cPwU# =Tnn2J !Y8N,B S/VZZR%7|{'ـ¬bamx}frGذ|$]Q|>idܔ',K0[_uuNxwqG>x'$׵\gȬ-3]mxOV5֚_A)8N2B``0ŘpxխQƮr-q-&~VO<.wf:yӒ / />םeU0;X'fWkM/l:Ly'/fEO_U;X!?Իž(7phӚg=4٣ ¯Kk?vƼ w|;&u雌v|q/v_wu| ˃d{J\)e*B0qX,&hOWuy2^J3KK1}Et wJN;qァzbG jUgg_<0iηc?>*v mz;$=$T8z/qR9\1}7z ׽PZ!TNN8dR`,cf"I)s| iB0k6)LQSh\d{F.qʒMu[zr(vwʟ6Ώkߡ߇Λ:o#{L[>¬cz~8+gW't䗞iXvBkk0wB )5Mu(ݒK5MYvK7 xOSn=H{=rڄgޚ]Gyܢ;^ɪmԠKڮ#+*ڎ{|en? OطW&}~fo@O7h0ad[,#^񎪗\4mUxΓ}ɇ>S^ʊWOhri%c}-Nwo8?Nؐ>G~FL~ d~gn\4xcN6$#9c2+\vn_*JJ3yv)=7ozN"3-[;>3[ >k:z[?|Sϝzl>'6١e=ڼ\eФ~7x /]0%ܫ{oشۻ0e;7뛖jˎ#۷5xOnAnիA)8N2B``0Řp(Lèπ>]%vV4^*3t`֫L}{;cHy_&\vMOzϹq5'޻ϷjlI e+_QickZYwCϷ?oyG|ȵ,/xhUkc""¯Duj1K@H"mca 4ejxqc^ڳuf˚d ;\i B>ӭ3vj^_U,^=u;NYrY7L,n7E˞9/ y4r{f1Ww.'ղJ3~$r|J VDxg7/ +{[.h_8 Guo5k>ǯN=mg,S[mrV}Ңm]~|ڵ3 `O3m(Uڰ#>tg>wZ7=b7Gc^n՛t\4Aޥ.ᮊ-Lm^olCA[08JH$1& +APƛOx+3 !  @`fDA򌨽afIv130 13"0@`"16ۈco'H "bf"‘Jk h<RR~~>*clPXXhYBd NRJa?tZϿ{'cr7Sn?xzjmgQyj,𤦗1=;sc43 7޹sjWk8{&/)xӟ2kzuX9D8搔Ryyy1Ҳ)%B*J9~Z^RQ%Z`f` XX8\<Щenw/̔5kwSҳVv~ۧU+m[Nl#?|S?_;Z\ڢ)g6ɩsj>cϯ1K7)h`R*77WJJ,p' !pT^lڹQ'I*Jopז-qi _~v3/NoyG4=iͮ~fC@G(qj6=^o~Cfaoޝ13)/SԪMƝ6Zp4sG'<ѳ^ѭDAq'L !0s0bLD8 $e-ɲS!8Rmf6=7䪾P;w[7qvu^hs64|VwG_8K=?eڝ7}mO;2|̎uX397ЭsUƑ 4Hs8zU3F&凥2,+YVZ3l]|˫}e-vxKqotu6|5y{rJ_f:IC8{fo8i/*{mx> n k֪IˮE+^Ogf |rkݙ0 |<뗈lU-re#nfo7W>3ϵs.Zb_;^^Nlœ]-N?v֮XuUG_qeǭ.Y(7M=f'{/OѰs(VW|v^}Oer3TM_bkfrw4jM9;1iOo=پ۷qڂqQJE"p8H$1Pu]!d 1uu^wBaJ )q4a<#jo3{eA31lG&G5oy7E30~KMjȂ ;3}Kn^bי\՗p2rj}1!~(McM)gQJ3K)-*((R DH{ .{^0𭞲ǵ1ìWc IDAT[D&O7n3jBmv\MS9/P+~~s1";k羚y]3g68MWgo1z㤅ņw\ZnXͮ3oسun/f߷OU۳B68הRRT*%`f!8XLn&5/Ox?'ٯֽC φ[fx`ѠOɎ2gS}; :yg /<\2{JsUXs}{޴pZw{t|&/pxT2_[{[[ߖ>c ;yw;^}aɈ)YAf(rrrI&B c3f :7gaDr4xxm_zCn=|MW-S6nh'k.l{;u# |gRdn~9>o:!bhḟFzcVɏ}6o;^p͇yM$[_wϖ/#G)Dp"R0ƄB"u81DnY@(x " ψf^dG(aHOo}Qȼ⟶P0: Fx<.|T2ض в,&#r@a/1ǵ5~UxB ^kԋ{~<铗J7o]-ki}gL>v]}Zd`(ٓ:݈eSpom|ӏxZƟ:tC"{숭sOi3vf6?3(eH)ql4ΕDTR ;`mYY$%q!d{nѳ˸nikMW%.;|mu#ᗷg-8]aKjGzwƐg^2ދ9N9_UXJ Fl}-R*77WJJ,p' !p(ߺ5TS)e o֌Ғ8q:u-i-#Q\cf{4}(չ}YםF만}g!נ[U^늅r/6{w|h^]ۡ}Z:=/>ֿ΃%ۿ+A/q'L !0s0bLD8 `I]բ^Pa]Ya"K>lWudayޛO|9n6}`Z_}mQ);>Xk?myk5^rqusvɨQ w1Q #_Ȥb+Yjψ`0YVVonreSun.~@hF˱7_xgZdbk֧Vk.ۋO$㻲O]:SڭsM}u鍗=9^w]QGV5WS+>}p=5i.ֵ=T㣺p{ -SOHKK>cp{f-lum%CS;be{ .~K&֕{RO3UWsyU,yRH$' )%cL(***r]W#3 *+K?.Q{k4mK@BD81@7왽2Ɏ @iIHI/[6qhh4K))??1m(,,, c<դcPvs+o*JS'؀#N8 t9LmӦR];{ a3e᪵U2ӕ2D"ǽtWO~Qxr;=;V.3+j'GKU 6 13(eH)qОW5!;_]>Lڒ ~*W5_>l|du<ҽ+Lrm=OkeW,CD8׈# 3V~{%kM~S>ˋ=dm/-mp̯(rssTJBqba02iY;5Z3[XvѺE-0 ҫ48&}{=c5e伲]=Ǎ'.mS~f˒DBNh|˨iwվUMt#qP((3fmi "]YF'~YR+e9]ToDܗv7X`|T>s2?h͎}23]='9Ü+m>s^j2Z3 "m̚\RH[Qb',`0@F+͖eF)-%Dri׾?d/,K*4\x9%|>DiZ6jv{~ՎSSoYw|S,nz\) ]ZTzqӕ=6*M/j"w~·?Es}i;r2epQJE"p8H$1Pu]!"BT Oy 3JTR|`~D8B1@7왽2Ɏ ƑD`Ҝ槊$_ZeR’Č: Fx<.|T2ض в,1ԭ߽y,-c(IԮcV֫VMpUo_l.g6D0Cx҄CRŮU^od_8#DUyi\aa"1D)gQJ3K)-*((Rie,[ԉg(^iiI0@83H-O%drrNkm n7H  W/K |TnSOo}iU,f"J2t` p(rssTJBqbh#zBV8'=bO{ܤ| Apw*Q3ݻλ~Ⴑ6b pGZVX[{Ye;ߴ])J+P_ #IKBGj/Hڧ[Mŋ)a GDJqɤ3X,DbW3$[dR[g13cf" *1Hx .o6ߓYȺSM3\6.3A>"/D#?T|R}MƱ)̀`S2rS0bԔYaVƶ] 6P.X$B1@-*,3~NR$l@(z8H߱+,)!t"=ký6<0d`#UZ]@w}V @8("H8N$RJƘP(TTT亮Gf1FyuZ__CyniD8r1@7왽2Ɏ AVE}*iqZh4ǥJ۶Z?D*6'-[`f"¡13 A˪޻ ^YRh 03 iIJf!,vS ,f513V T^^1F)ED,,@J?DK) ˶030ņ|D^2Y>H`ӯC>iaf%XkmHH)dl' bf,FkDc͕RR)!3 !ljbBf&"m&"f&!,""*+@ MY欌 Khٲ8 X) FkLD8)!"cfOcRyyy1Ҳ)%\𧘙x?"BsXyp1aLD8(rssTJBqb@ 33 !` 3kO i1Al i03M)8N2B``0Řp`fheK FyI"2 "1qi|ned D8Ji`4RJG%cm -¿3p3p̿R*//""fRZUPP Ŀp̿R*77WJJ,p' ! " "oq'L !0s0bLD8昿(T$ ÉDBJ  \BGMDv%-H~BDc<#jo3{eA#3` HOMjY`pZh4ǥJ۶ZcҔRyyy1Ҳ)%KSJJ)Sb1!KSJ8L&9 b1f&"#L L @8l D~a?fO0@ :+AJLc3~H 1LD83DD3 ~ĕa&"fJ| " ~@؏D)Dp"R0ƄB"u81C0eHSNs[0q]T""1@7왽2Ɏ 3cR^)ݼ]g `vZ`0F񸔒Qc6B˲p_R*//""fRZUPP 1iJ\)e*B0qX,&1iJqɤ3X,D̂aG$`[L #r`\fl[bnEU$ZJ鳤`DC`f"bfc013O0Ƥ\WlK f&"S03)E>۶,y)O9 <-,%0x1>PIf[DnXJ3~L)A?L^d "祒Lm[LDFkS>ۖ "SdKU\Ay"g 6LD84T$ ÉDBJ  \B̴^"nO;޵7El,Ԟ c   ψf^dG?a KI> "i`/ӣՏfĶ쪞^ysH DC1˳m;ܹS)%df"2ƤG KD0e:N(nE2zm`=eRHԪVPzF 5K' QJ3@²Tܓf#{JÒ`婌\G%*FBo+KOr]qŚ*clPXXhYKSJcRDRJ˲ 8/M)+LRBfB8ńSB>6sLDRZ80̆!`a&".f&")% HJi6R*''qd2)`013֒2bI)v_}Ih]ggb8iU#aMۛ**ۗJ)v+9Ɓ%%2WY>'33dwB2loI/3ݷUs돞3OOvʊw9mzM69U>q^8ilMndC38KN>),f0[7,H./)֖? T$JRZs*e_ٜLi#T)Wi-Hҽqm`#!Q egTqq deQQZRaZ/JtwS:Lny qmwl-[zC_ݰߍ7IFP 1/nժJk֬YlRJd*:|_~S-`2j6]1뤲 ۞Z֡=ih`"^R/[~S{|'cev]e)mٶ%?Nأo3eK{a᫒3x3/n屌8~zϺ{PVR"XL=>σ-eiY+ydZW<ХsߚjA D"p8HH)cBPQQB0DlvSvj_g;xNDn gD 3{fL#ogACDX}qMso'}$1D+Zh4ǥJ۶ZcҔRyyy1Ҳ)%KSJJ)Sb1!rQUKM(MD8f`JKJdd8Ɨ؃uaGȯ IDAT3ٙL2zʠ ]Tt"]"Mi"U# M(RBG:!L={_Ճ޿|+x\@HDpD"TvV+sH;!b!BP(eY1 "0L$"D?Hj<]wy\̅HvҿA~޿u+wkں} y3Ojyz %5_v_ F+qlw4XoO]ӳ5,XM{%Ǯ]C'5Dv73=e-\XkCz9аU >NmxSfnrC[DH.T3\6u.?ሹ1DZZ2\|Ə\J R i~wW}¢]^wHRɾw칬R?涏箉owO't|V ;l8$>_zk4W\rR\Դfr҃ǎ-nq1(gXыy J FDRիiF)%DDz}իsؓxͳ2-plm_xT倘5K% 7Ǽ6o{b,۷g_P&rEWlQ~_=sj^v;X辗_IXK?K=jeйljW:7}^2=EksZfN͹Y?hk!3%"9\~ _ 7BF9|m3VHD"'Xx+^~J²/_ t+6s:tO n8ᖜ r_D,)a`0p1 J) ?iwKB-ZT)%@D"kvY9҄ b11t]7M17!P7x4OL8C"@ ƜHB萷1e:bwp4f$X#.㒶HmB"SʉYVSBqp;4.W) c﹚IBB!]-ba&!"9>: R\Cϼ溒1ҒGُz|rҠuw/-:_)wm[t޹w/};T;J>x3/7{|rΦuƷ8M?d:INXYkϒ}j)泍Dc֠[w%nWksptą_xaʲ_tJݻvc.?PJ5oO..c<'/o&(P(W,J6O7u7#sy`[]\t4-G6;|Z\s]ih6w;uhq~ayb*޴uH>.j/T}j;,Ypݻe {ԤM3x47!APJi־}J*b19(cyyyFb ޜ3i5L?دKk>~mj7Y~{}YsƽGK4jQ򫋷7o?%[^ >hkV^ߺڴ٫y7>{ 4]}f5%j-.^w=ؿ!0k:t}?g@-_-mM ȱEU[7e^gy]@;@GQ9(|>_zzmی1U"r.,K9vb⩕zyJ )" CD"t+6s:taDKCi5;rw\7z\(ڤaH$9p8 7(\."OA-MQhQy"cr={sq4!DBcm3ƈ1i1}$mOc($qlKYDp3Rda~rG5Jߴ+R: yxR  =9YB^O_`>w|⩉iR]}* I !D(uݲ,ai"Ÿy*)>hI!u@đg:Y# YA)sުԨQU^Wi익|LW'_o3YFZ+["_ܲ .5rrgo9uN&Uc'j۩ێ4x^]2s_9y칝xt[OT jfmXRknhi^X >7`N3 8yV/Mpז]3eI)^ "@D"D4iӧ]+ϊ=7nArKV=5ǎ=VeT}'jՋ{s #Lqoݍ+)g~Q9wUn]|E_?2wTkfeܷfҼI-.*Yd+px++[܀2s5,oӻumW7BF9|m3F!,Rʝ)_wI"/3D)b%fuN]gΐ?H0ÃY꛴9GM;DHJ"ڤaH$9p8 7H)u]WJi҄ŋ!"1v9҄Eq\eqΉ1i1 Cȱwz3<,(8gd,27_ ~ ~8;?^՜k.|;-\L"K8Qԗ;wyvU d5d^wԲBtk?+6¡©ʜ?ؕǂ~]*_A&s#m֩@ѯ8}~qBB!]-ba&!"9 (b>_BdD 8KόD*hU?۽c+L9oꊆ^ػ]JpA/ ch6;J_HJ]K]*'Eu1U;p(tb?UޔMN~sS"}%gփ4h=_5g{C:V86d_9Hci٦ש,-ݾvHJO"܍'csr痵53o7ʰwn3p]&۵zW|Rwo2R ޮ'ӈT?]2/?yw|yXGrƉkEJΦvktNLO=u%lbQ(F^F |ݧۭmoӕ~C)ruVxq)%c)Q233ƭ崏8G͞or3Y;ܻ{|r LrՌ[M,Q)#\yq`_l7u]<\>sv&v+?2i׀gm,\D du1;e)\C_ZO]>㞎:}B{Yrvϔo; ,uG[+?)~p}!D hsJ)ϗn6c nyD@BRұ*t{KgHf /#t+6s:t"  ˱'i;sN;72C@ @I) HspnPJix8wKR~˲1ifKh6=vΎx}ym2˳vlӟz~Fݎ^qt>:Lօ$oD+Wwz^p&w I !D(uݲ,ai"Ÿ@@@/My֐Vg|G;_c>=[ٳCLof[vVO jtң O^)gà/xd1elhڇOb;{|א+_su!s;Ϙ5inhzAm3j2S:XzZz޼zWz7{6~nRꩊ%DD2N HYJl}g?qt{+yuGu.vN>jU~fwodu5U]gGwyd5_=zesC _"gPS-lr4it-`r-ФׄmU&zI+keEH0"5"R>M4D"1R&&&ݻwܹJ)d˳M[QFΙyobzڷYfU|v7ݹ}qI;2EKe޷yߤpVnkj T%ֽ}&&3`bE?3}C5Ov {w/il]繩)DE'^iLzn>bً/*瑷|zvش~n/=x#aJ) !FsPJ|t۶cpBT$+Qf|/P+c1@]b%fuN]gΐF?C^‘-[3D4$ې0`0D8"BDۀRADHDr8wPJ9CDHD1]MdM4ɡm6Z1lGOfz}~V)UzhNtyv:Z+ڽu§39_DJWE0jOsGO_@;M5kCE7ڴ}F<Jօ$ "B%@vrwן BB t],1DdiDIZk6DsnΥe9*!#)5֯SŠa:OQmKzڽcمct|ގrfU /l{^ozv:9ZWqk7:o<:󗪰K$hs\?7_{EpS^[P?9fJF֑/ )}U'}ƳF)(݉7)6;=GVŲcʣ{=mJռG'gS#D0i%-Ik}^\3[υ}ݥF<|wn۟n*mR=}צ-_E*?{вO~g|{=9"RZ2C^z/m?ڲ746+?RJy/==ݶm~CƤmۑ,Or DrwP\@DJRAD@XɴYSY3$0Ɛ!0Bں?o~9q悈8*nCRJ0`$c8_QJAm1Du4M܄!Xp5}BKC!3agf,ڔ/K7\KTGX eݴӪ1#wكtG?UL'/]w/ZX~V(SlLTzߍk;,uǂ~T7qbVr&v}oOOrKE !D(uݲ,ai"ŸC@(''WU-#I=]`P#$ @A{k[j׭>v`C)wO6 4(ɝq4_猄}e]5[6[wDxإ+9+eFl#Tk'LdeJPW΋.zǁֽsíD$Vxw۟ i=c@R Bw%LX P]vܕ4ˎp\TGkrçŐsΎX.ß&y9yyQT _u53r/ιZFi[VA/%ۺ %`֕›/%Qv%!-9_ dFXWgx,ҴŀSQwrJJR"]N?'Ub־)]_#/ !<ϝwY|y]O8q MϗɌ Wz2JoZ^r^}豇(nGg]Bo-dEfLJIqidg\BWKl)˓jٵ'n` OMhn̓HT̓"ײc՞nR=koR3KyOθf :'U߼~ʑ⌿ ͅN*" 7BF9|m3E!$/H VQGKcK&r@(V2mVu 4" "P׹4tq8:,sշG~wε(;Win9H n[RJ0`$c81t]7M17!8풛?g#H;sQ!AE=֬[Z۾z;q'_k# VN1{yЗs y-}e1XiKdoJ󷨐>w> ol:rP@&v>] E BP(eY1 "0L$"D? @! Ɛ @J Du#"sur#pE\.nbR"n7#vs5NL4v̑D8RLcHD\s1A5Vۯijѥl7g(+ 붙F:cJM"@@ "m9c\8#)sk\91G*@4@:#8 t mbn mKp(GBT 󲜒;e^]f;dpף&R=;!R(;"9+ұ%KwIQ u&#qT3mAknK9-$0(@riDś߫.Z9>k^cxu 4Δ%l[J\s34Ȏي2d11+Tk搞v1@k ~ohRݿZr!!D hsJ)ϗn6c nsDPR0h`FBIԠ۟G,eۤ ^3Cb%fuN]gΐp =:2 ZD^ɒO:2%GAcHD@pR #0ƈ1i1R ='ը)/g&})<.TpUFel:trEKst(bB-.no~J;>`OŇ2ټT>C^|֝9[4/N݇zBhfjW\L Ǜ|wN^dh+BB!]-ba&!"_E  >@wrVt֎u@6i˖"WOdp5D7#"D_" ͈n ψn ΝhVe%Y IDATdΉ2Z:tR"k:@EDȑrrbHѕ%x$"BΝhVt9HEwb ~K@GQ9(|>_zzmی1=J 䔤b%-+~Hq-! %IJ%@;Dob%fuN]o!h5aVs8y9|8qNX]̭cqGJiF0D"s w#"Ƙi2)"GPZ(_V pqTp3`Nnf%ʳ4FPH Ka¡șv.OHJLЅg#/==ݶmd IJi2f)CxtQ75 G)@ ""(pL{k<0!0 8ڂ.]g/;2yQ^sWKrJB݅!fRJ0`$c81t]7M1 lr$i=.!4FRcP)Dΐ=:d$T  l+< RȐl[=:CBHEbJ!WPHu˲c@DaID "b"BD"BdJ)D&"b"BD dJ)DEu))!"1D1PJ!"1!D hsJ)ϗn6c !CGٶtl/uֹ3ۓ0 N(PȝWBB@@I Dܭ',,5OyR7Z挲se-~<'$?]ӝ˙*fSnr-9#DpPw"PD@q)a`0p1C\\mc?F@D@""D"Do " "n@DW !Be1 0MnQB@ (R>/==ݶmS!"REJI @3DD@D"@)@R TpC` \!OR #0ƈ1i1 !Be1 0Mo "߇:"D!C$@GQ9(|>_zzmی1!uDHDkDp"!"" 0`0D8!..6FD1]MdA-M t],1DdiD#""¯"p!2D@D xHIG˥TLlGtwkBF9|m3 .%4 # F"9acLu4cwKBB!]-ba&!"3ƘD"@$B|e]9yw9utH0#J" HIHJIq $B)\GD@D ) I)dSR #R""H|U ؟74X9vZw8zrWN" !FsPJ|t۶cwR #0ƈ1i1 !Be1 0M4!999_|s"R^t)0qyߥF3W}ŋVyY&ki [2V˿R^bRs"9y /_ -˖$lˎ:ZbˉHNpaffKfj(L!aܾ$'sw'kʼx> byO\]?amkMGߙ߭I nDkBF9|m3 .%4 # F"9acLu4c?#D@" DKBB!]-ba&!"9t/P Rܹs9991mメmوNn=kyc'ͨ6sOS vж+?XbY9ʔ(hb'}v* fSZ턬sG.~Pn>{F={/ kVw3VNyLaqm^8znӲWO^-m\ܣQh%}ʔν,}vG6^aq9gc)vi=KJNcʼ0yȺ+%o6fy{t#WsfWOCcˡ?Sd8\.MOZ,1E朏lD~m/"gu׹hsO,=ddtqZ/\se=~{S(# !FsPJJ)D[q΅yyy1 w#"Ƙi2&Dy~Ê=ZO1 ojG#1=$)QKe,y^uyŃ?}&+=ECǾ&] &e]ڳˈ?\JwgEz:d;݇~x`GH_aLfgvJOUyښSd]H_!Be1 0M"BĂ |>)%"q333/\%b;mcmmZ+R{O^^&&-a:amMvc5{jd粎_H_)(|ִ%6{gIrж Zd6G~>]=^+;h[hУ|ۑsF7YґC \Am]98aPs{qZ&[ȷ[O)_|t 6}ݛF~TqeB@ (n "D!"`81t]7M17cLD ?=.\ ƞo_Г/@`;v>W$U;S{iȳN[Ga?}gq9Í5y?7V{+Ssvͫi>a`.$ J+/õT޳}qL]:w I !D(uݲ,ai"Ÿx< u]J󜜜;?;уulZKPM*]L5?8<;ca95^ow_'z촏f;{;f)3Up)IBYY+z)V-:xS·=[W5wig[=p'Nyk_7~?c.is~ީoo 2y["GYz/uhc.5vSF}>Hٲwm /M~4C\ 0ƈ1i1 rqo^"cHDJOJy&^"%mp[siF+4̻xI/TГ0 F}]jPB/$鈪Jjڼ۾Yި{~qF= o0pƏSزJFR!uS mt]̷wvY틷85 @2!D hsqq+ w#"Ƙi2&ȹqrvV7tlOJj;1 Ho))Xj,:5{tل-/|̕:LyB뻽޿bc垝ʰ{̬ϺC͗?q~3>=Q(PoAˆvxd]H "T {nڎ^3A[H_!Be1 0M4"BD"r@J 3t"= nNJH XNnr&MHt1%b<\ض\L: KD@C,/'/T"'f#wȵ%R$f8.]cFnG-7A"]HH$X O^ k1bA(59Of7" !Fs]a81t]7M17CJZoL<83%2.7ֻ)){mW<ԓ&h]L+|]M[ۣҞ &7,UdJ썗G Shaon='^t]S6};޹t;[ ,ݱhW&YXِKz7ysщ]HV^v=c>2 $" nYc 0 4?@@}D"@@7o"MHn/HZ n B@ (nW?`UU0rSNHBƎQD MJ"H^H4Q""**E,(59HOBN뗼7_獾2 @ӴJca37ѿu[UQv_|vd8Ą~E)~mZK?|_պ>gEڽk~jE=!Q Z}ޯ?Yڢ'Muj٬AW{ۓzڟα’.?2"5%7F;/9~ r]7--0P("2M3??*" !""C څuݤR9hZeYYYiZ%FD10c7!cpI19'~dSNYH "4KnyIyČ+wxl|uJc%`$6WR`Ll\"DB*\r.}p]7--0P("2M3??4"nRRRBBBii)4¬,4#"Ƙa1;\WI1"w" )@dJE %!c !"d  H0WJ RR)BD-D8gJIA릥 c@Di"]\MJJJHH(--圃UVUbD3 #??1 ^D!" B@ C@u B1 "4A.R&%%%$$rA*+M*1"bnZZaP1Ddf~~>!"hEuݤR9hZeYYYiZ%FD10c]\MKK3 # 1L'"DMHPZZ9M0++ 4MĈ1fF~~>c iiiaB!iDi)uJKK9ifeei1 gvQs]7--0P("2M3??4"nRRR|||YY4¬,4#"Ƙa1.j릥 c@D&"DMHBUVUbD3 #??1Euݴ40Bc G""BDдRJ) 4¬,4#"Ƙa1.j릥 c@DhŎRDifeei1 gvQs]7--0P( DVi "hZ%YYYiZ%FD10c]\MKKzp1iZ%YYYiZ%FD109n*U|>_0䜃iV`VVhV~)%h;"x<`02@4Md0++ 4M܈+V PDM4,4#"* DM4R¬,4M4M4,T4M4M4MX`n@P.)shiivQ ")R VZ'Ɩ1@4M4MӴ RrKKK9x,6>ŦĀr|!hiivR!޿D$.Tn/-k۶4M4M4MI)MLMM---}V)/T%mBiii8)ie99f x 8w1חҵm[ii]औiZ8m*܉\_j:H׶m!hiivRiY8slҊs':s} ]۶iiiNJieYpα٦H+ΝltmBiii8)ie99f x 8w1חҵm[ii]औiZ8m*܉\_j:H׶m!hiivRiY8slҊs':s} ]۶+ߠ/<@ @!""E1<!"6"E   DM4M4"T[ĥ IDAT_\B!"RiY8slҊs':s} ]۶T4JBD"@/+P CR`U=1>$E (*JdH8qz#FWy}>2.)"z HUDB0%n1)FBa{=)qyL 1_# A4M4McL) R` 10DBD}Dt!c "BD " J)D"4MӲ,q8lSNt6RAm !b*US=2*:'㏡pyT5.4~͸H־<5FpU2Bg~:vlyӫU 'ujFѹ#Iljx 3J4?Egm;#] @,* $@Op/.VSg,5#@R""r:~<)0]G<{.5RiiEczի_uUO޿3g@D!1115|7o.--xFvS"|mGǺ{muҼ/?kDZoZ\iiڅR~ԩ-[_~ӧ^h42z1cƄBz~ƍ;|_RiY8slҊs':s} ]۶W.+G}́<:3bx{rjNm {}<}ڡ'y1]/Ӿ|׆u~Շ5o3_ݪ.w]qް6T^盶]WӦE]"N>dW~ԠI&}gna065~ݖFK{[9gȠH*U.NrL{h\sWW7n˂^zKѳVl '%0@4M4M(6mL>= mݺu~W^iާOR!#X^^ްaÙ3g}嗑H[o-**=zRBmz<9stiҤI^f͠A7lKD"5n7 +WsgӅę/~UڶQ: ѳ*ʹӶDz?|wL~ر7?؝nΜD;sc޻˗.$FQ0"@M4M4B+..={v6ms=bŊC7@V<CDqϞ=۱caÆ%&&~W=X$3gΝwY\\k۷o?f۶w]Vu~wG-zg׭[KD;c[n}|RJXrevvvYYV_RiY8slҊs':s} ]۶W.gW5϶bn4g],6r_[tڸp4RvS^N2m]v]W{Gv҆mFTAVkw:?6rșu{O^>uiﺬg ֺ,qo5hkk F4Zp3%O||ounՒlc}[ N?}o'R9&Neu_Z-ne1,o&Rii]cK,{RmwڵuC-))|Z À̙3=z=z4|˖-;vTJnݚ16mڴ IDw 8Ç߿AAmUT9~xaaa5222O>k֬x#D9BKr-hѣݻw/((R""4M˲cMm6lذo}Ĉ)))޽{QQ?| 67n\LL̪UB?9sڵk0"GDAǙ?~ 6nܸu 4̙c!"J)y~ur]wÆ 'N,++ïH)MӴ,q96TiŹtmB+D*-~k=߱KҪ5̃ۗ}p\fP;ܣk6W6(rޠ^9N;5xˤ$Y=NȨwSV{on~zݨϳgL.a/.5剪 K#vEUO7_r_v<3ŧI/V/z5ޞs؍Wnu]v~WƦ7|갾3Diiu]˲.]Zv'|rʕUV9s}7bĈ\!#D$8sLG!xd8^. yָ˓OGw1֌ iivD"III5_ӧm۶gV"BD)e({G~9s|>"BD wu…c!">D!u]"b !H)MӴ,q96TiŹtmB[D@D@Ș FapTDPPD*5|):c$Hy>2D"@D "@D"DFJ)"DDH$ǣA4M4M(BP8|> a8JU@D݈H)9[>S * "4M˲cGx 0wRɦ1U@J۶2H"BD@DC$"ψR1$""iiv1""DdH)ED_BD9)%!"W~")ie99F@bwV&J׶m!hii?s.$"D )ie99ǡ׊}o<9.9m[ii]औiZ8i[&ZlBiii8)ie99Dz^، mBiii8)ie99pv/rv3жm!hiivRiY8s ggxrZ^ g?3 mBiii8)ie99pv/rv3жm!hiivRiY8s ggxrZ^ g?3 mB?P* "hii4M˲c8;9;Uh۶~ au(0Ƽ^/"*4M4M4M?CJieYp1iyD*m["Bx NҰJLWnl퉏"b<D!)V믮oG!nUp'}>Kirˢ'n| PD@HE"q2qyd8Q1<\*B@F€\ND !F8(C"D$7%d4*rFDd4q'^/G"BD4뺙u91" 4MӲ,q8崼@Nf"sڶ-_✟9sfpp8 sΝ?7jMY:5|!RMxFU3SZ=qWC&̝2ޣu0{Yt3uymK~-72'g@3+_T,S6cOImw<.>GFocAJ!5_I=Ga%D]H+ 3'ظCDι'{eӒV]۾z Faᒺ/2GLۯ]cmw( #(iֆ}u)DJR+TQQ) G0H4\F%E)*) F$EiZ8^LdB۶KB'O?O>Ν߾}aΞ=;a„xUV'Ozivc YdhRdV+GmfŲSz"P^aԢQzi;:ɾ_|(rIh Gn#ҫ񻻿;S=-p6~fxo'O_lG:]R(oj0u^M&E.^롑϶ڔg[/~2kՇ^Ӵ{6Lܼq͖?D=ro {u_]7}CC5#op7_BF}c٫o޴nfboykWy;<9x,Xrl>D~aY'%"_nXjwmv jه -[%^/M8Թs礔J);iҤ;r͛7k,0h\RJ4-rs _N g&2WmB%Xiii&MfΜ}={BYf5o\~aÆE"9h]1)Lj/̺vo╘[Bi4ek(Kzpk%~ӥ+V4OG/,ue1W;Uvn:3e%l1XlfuwWyy'F4%Oy}m@eɗYv}^u]z!So3Ͱ'd/ Vjv0=e>c4nV?[uաeON &t#sxeϧVHyW;w=f,Y{޻i3vN״ۇcf:n cΝXkqK>})aב#;ݶuzc鉭K6/&.w'➣I˧xus5 IDAT4Fwѣs:efO{O}iwZ}>  #w~5mN}.w|f! [mx@Ӵ?uݸ+\׍έxA'RiY8s ggxrZ^ g?3 mBoFÆ ׯߞ={ ҵkvI)  g0@4@]lR9g{tY|9ُyG |))kpSϖ;{Gͺr ɻb#dL^;iUy&t֢lLNxJǪ[?>t}+:`gzlJ4š?zZukȍgĕt(c^c"Ǔ6j7T~{sNsisu#wD=mRZE'B>hkW5ĥuN5ol3`ʟޗo^r= ^g6 >%ɻ}g|pc_J8-"Ҍ=VR܉Ftlwrzڪ_}Dɫ^3O~hݤ36'&Νuon1|Ʉ+xӪ}1}xmpcͯ NA%ti'F c7wX4sI |mG6# @Ӵ?" ըQcǎ7 R^szD?4M˲c8;9;Uh۶~QJ Cm׮eYs߿;vDi2*/vxq|ì†k^_v}3e2#bk'.xewɽ=V7B/6#goCbSȼvuONܶhdO\v/KLUx*ڰO1rWm4<;{ܮ"1(9+ۘyO54x>A񷵜9e^SS;Y՚vZtNmvOhG?|t)R*66^zD-Z4sL9htRJ4-rs _N g&2WmB"BDn_aǏ߲em۱J)DMӴJ==^8}˽v{U~z?mTz ,|/?0~jmwmI:RqUvR\3zܧZ.ըSNW;{wYcK^]V4;G﹬ZBYh6p9v& eOo=wǯh|W1\3}[36l΍ӷlZ,5tsnկI 0C'l=qrC͇ h}qJW,|oo/z*xGZc+ǴOߓo$ծQNkv?17G*A'r_{Jozo&GTLCRU-'~Κ03.I)=_aNθ;LHh-k_,zy>{v,dž݆iۦ3k $'ߌ5Zt}tcΚϫ>w<֮ç?%xh_'\v_hѢiӦy<ƘR ARiY8s ggxrZ^ g?3 mBirN`\IχD@ G񺑈R|>T4J>sÒ8gccDPB*>WIȀHD  |>g@ x=`T"201"BDEn$$Qx#zE7" 2"dHB$@$?C@ B Ą0<$C!P0@ /P0">8ݩnݞ+3b>QEyYѹ~sz435S>}טܯK)#0N81Czwt$֪~ó>J4uݚ5k^q6mιR AWRiY8s egxrz^ g?3 mBi2DH~DHyD@D *`"@D"E HyD<:g@t"" " "@oCD "D D$"D$"@" " !!"BDr#T% " 8ι5[ |^&{W󻳮-;|vN&4Moq]WJz)ie99 _N 寚\m !@4MO"Dn$Jz"BD"KKcq.eƙ1 "DM;+hjRJ4-rst_N 3.3 mBi_EDPJ"o "cHJJ?D8g))%"iDJieYpq*KoV!J\2B۶iiiNJieYpq%·oT=a Ѷm!hiivRiY8s}j7S$ڶ-M4M4M.pRJ4-rsCנ/vQt D۶iiiNJieYpq%7ܜOh۶4M4M4MI)MӴ,q9]DF)@{XE. }ofNJrHB-4AEQ"(R&M *(JH'tH2?d p8 !t]apqt'R΢ @@Qp8p88!~0 9]DY:ݗ$0(p8'u9ǡKНH92VREQp8p\~088t )gQFt_ @ ( 8p8kBuo.Aw",[KEp8qB %NEy})@(p8p8qB]~as݉(#ou/H` P~-"B"H)2A # qRH!%"FDȸ™BJ  -$" "d! !c ED1KDՈ [HD?"B"H!FD8猤B""8ED1DB"_rH I4Dqm D !rB!"\FDC)%!"8PB]~as݉(#ou/H` P~"@F( eLܺ [PI>%DDq D0@DfЈj^]A QKwqD"@$%"µ"@!"DF3Mz8""BddŢ .ïBD "@ P\D!c". EIHI@D"ƀ DȤ7RD$%"oI D2fGB% + #_g1<2pZ_yژVM&oޣl&$L+Vy<`ĄKy> ,3@s1D"I @<8s C$3 E,)pǽ1ҳ?E,.Ln|${tk;wLvƤ5 |WHdj ř6PQ7E$p3jӹdID"ITQ  Ҵ5_v(Tn]F .KAAL|pЈZM![*ƌF_fr[3*%)޶LKb孃ۘR"1 )J" 2@4F d+v, YݜB01ǟlӭ:Ji[,9#R"yV]y8EÀ X86 .anb" @}.KBa` @p%p"h5Sm`lǧn5 sy3$" jC6` " 0Mt,8u$DF"ELU*o-ּDD@"_Z6G'ӓ(hXȸ]*DȘ4ZFv !TT Dc2R*ՕE*)@d(I"Pݺ⡠a "@GU `x|^_hntd1Ӗ\>.P$" qݫ4/"h.j?Xg%*c*d\חD4m[=J"(*$ q-B %NEy})@(KYF=C-]G O>+o|[F$gj1E싙Geٌ⭃s!rtܜyjZdeʹs{W\LX邩≡v/ODץI I/jn|0id /W9i ?,e{`3f:bd8^Tps/>qש ?\!,TiV|S;0IjU(XjdV}"綋/{hdGĉ̣õIJVywvIXɑi/T 6sC׎1ݢ'8>}:3V~pHP-qyvczAfS_1^5wTe?6VeRswx Z?in{mI |nOb$/|W'\]T^or~]@oGtI$nwJ"Ο\ [J[zHk]񜱫+ {nH$o#508y1/oh7wSi7R=I1s'u@& %y/=PQA5z }<#S]m:vp״8Me_L᯴L@s7GFQ,[)Q ՗3[ƤPJ-}~vpD IDATxn-5S";~=ٙ]xuY3l=ukBJdܶHyG>U7Kݻeӆ\1Q?Um0~W矏I@DY1<J;#E0HD'u9ǡKНH92VREQ!pJVku$X\H$%SGǒ-srV~/kb,YRBW.Ϝ@$wѹcŸi!S*NO+d܅J퟾w,yޣ?+UD̤JgFz@" 8$l^j#:KDn/i£썹J]>wȮrO R\%9%=N~_|Ju9\.C6(޻hbQ^Bwg^)䲩/`q:>~{#iG}۽?Zۇ "=8xcPREk4w]ʧPIq 2uk%zRFvlBRRFڛ?pwhrVڬy9V=2tT Yvyd DB<7i!\"ca5ɤww:5{_[u 7K:5gQNvf-F70ʈM:=ƛ3NHx1uf9gfm ojٻ}_]aݾ{5+':t2n k'<`Q3 ϿwAd @Za\Ѽ\Vǭ=?2-k 7M:|舖P"'~jw},Xx]3O+6I+W߻WOb%Fn}ܿÇj|E ,`պj޵/Uկ<6fbS:$3tOK^>G|STŶloҼ -IڱPٕy)'+F;}N:>Ḻ !t]apqt'R΢ @@Qe(̈Vew?>qiOm>pPu?8bLNX^EnY>su>5ڹݢo^adrogVԷmh^޺ojzbU?7[wB:?rtW>ת5E犧=idn/W"Bw ;ܰ U U6վݹCҪS9GAlG_j;?|0%/Z-漥>wggjۯwlڷ|ڏ.k˜Ur>ߛu|"ޗs+*  RF,2f 6K*0dZyp[TT`[:cw擜*{ӣNw+/|5K{l؟ju:;Zj`ٝ݋6:3oM{>6{!1O msKz1R!be3۳ɘ9"%dFxcX|WK߸U2tNkhx&x`[̆ggߵl[ 6I"ۏ%4I#6j u=>y7|xg~hÚ'ߩϸx;&9X%?|G{+.)3޻4_tN/ 5qC聯̋ ]e=Ϳ]W93#ӭ]>{sKJ"!Ueipd ߍ@=!alf3㒤;+󿹧mîEl:%vԳ/wꊣ&s5z߯5>:bUoFvs[]w[zwp{jlsv[cn+sf]N]( kJH҄/̚vO'0@ET0d.OxE&D4TX]3Ω> wӭi] nܞeN}w@ٽ{Ţewq9;-aS]{ӣM3p }.$7@RcGyo).-3޻ʹ3z[;y,\z/'4xS tXrC{t_|;/-a-[ìukДxGʑ{{H +ҥ?lz׽ͭ wk+ 8ɺNׇ-hY&wtnqs^:5jQ{=jT6ZiJѤ>&f ]C/y[mɑEm̼]%o<'Nb8Ḋ !t]apqt'R΢ @@Q%D=ǶIS*glv٠]R~]zUUso.M+|Y㛏Ơ[+wG,⥶n3g洚4 .8dWW}/oŒjM_{1B5yqg#KFQI_5wFF9G@pz$'=<ݼ}́ _掅[5mr#zfuyldcI{i<\&gPO{o|Qx_g=muo^<ɪXt|S\vvmٴdzZ V;g?Ӵ`}kE珕gqcdFՆ7{U.,wSНVi ?|S,6owWv~ؼ~P북O;ֹS_d o1lٛw%`7Jm]I۰j9ϢhKS^Wo"@j.IɃӟAfeҮ|Y[Nk_8`F2׽ަפ~c>`߻^a>Hڻ6K{t?j'yĹ߿{_v ɶ5{<5O4{;]WV#[>w\rb J)3[woh8\Lx|UhMif̼c7 znjE]X 3̸S7R\ڼjYZ'f(u{}Τl ( Ze&}ݲ2QIY1fӄSHXœCr%:CWIά_3#TThyPMZ{v]\rAoa\9 f$)d zy0rb}Az:nsT^ǞRuֵmsp̨\Z\O, ڪ)J%o*DC"@)l=mYafD8lh j ˷4~=>Y+^|C]k1usy/}qS )Uj&0tgu;Lз<7u7 3Ce3VW4QkFT5˲o~tx5hZ wb܆ۋXz=w}f=\?G^T#_67oRoivwٍl^=Vv~^dRfU$%x-?_V-Npꛟ-|wWEウk]_z{*t‘Wjz&壓_|l˲{:yd`qD|W;9\=Dp\ń~088t )gQFt_ @ ( 24*5Xebk;=xyąoۯoB(g5u<|cY>~Tc=2o:ytJO+=列KwFzNywm0ekkCx{=ې۪ceNɏgZw983ΕHM2X4ni/#߽W"Ȍ0`F/b?oł+7ܝS[Tg/2䡆Ţo7U'nSɸLבW{&"ݚ',дo٤G:[xYooS\ơU^wGќ;y=Jp#J{='--T˲ CߤHxw r o)do}ų`(9 ﯉KO֞lRƉ^w֤g7~&=+;xorP7zc/7˝#-l/[ڤw*~XpLt_Lj޵sӿ;}{ƶo5LR2x: gO>q@wCh͊?9d\euylu['۹ɓU\O|7ƷY+4kʼn7-X|2]ޫV_1i:P^T{W aCmt~ƛqSȔrs>6-y[wDT?pJ7t]PdK:ؤ˓]cŊc;Y'YVֺGzQCuL7u`;].Xgxӣ$3Y\R3XxѽUhT@sOp\ń~088t )gQFt_ @ ( 7PAsw]Xs_9oٔAFeזmE3}Ԇw;N-vtSWI/TEvа\*75 /9rķ?|nCC;aYόRAg~a"%ʢ  BC=HB4`GLbRe7X>{@9U!-N.YO>xFQ0ԲىюԺçNjߤ[3?>BׇܼEmX=XWo&%eqUH pm"acB QJ3´ѝ_,ĹųVR/(<{l [& #+u3Sܜm[{{EAg^|Kmz6I=!sSb )*!a!@w@@R`B ! B,s/.cߚ9}R)[ti󦵪]vӼ!ʴ|G$7ݖ5l^Bg;7rq;:0l耂8]QՀ~D8QH(f ("3-q\^z|42aL.}7rؠ!ϞrxHuu`刁gtyAѨUH !ddBa@!5AӦ۰8rG'HC>{u{GRφ6NA'>X&;6b^ru&sִ\Ytݼi#|ssj O?B"ReyD@GGDD "c 5} 7I/ŻKf0Mko?~Bv*13dY'A3=nMY?^^pX0nk/$I"E 2D"`($3f3-Z w>ډK`3fgcBi gbrq fҌ9*{W-~ߔƼhJɂq/2I@cfeUH,lYҮi#:~K,{DɅOˮM1c- ߞ1fR$6KN)ųLsbIḺ !t]apO;vZ4_$@@Q%DD 3*Wˬںy3HV\* *%؞#MRHʑ-g$$!G `I*ܶLB*\ؖ$ٶe6.Sؿ9@DHF%D0FdX u|rqe+ԩmј(Lv:\-{\. $VXbj Ooٙk}Lר&vl a&eVZ.-m~B2$$)@p#a#ㄈD *UVlɶ8"II l3>z}s.Z*'i AhfuephФ:gYDH~G$ldCje5xtl-JJ MhBDgDWDHH¶l5 (e#wi*"Di,@fLj_ipMS e5ME +!bKU?L4Mc iY2 "D_H´,B \ŠY+.U!" "@4qUUao IDATmZB4΀ mB?!~0 9v S7xNs{ I"DˈKeDH!"8~kDQ)D"RT C)%"! K@#",DψKeCDI"B$D؄Oץ2NR""C*P)D $"DDZ@DB$E$$D $Da) HD DLj!DH!"8/D!IIFDp%"@DBDX 2 D@ CD"@D D $D$J!"a) CB cHwId!(9c@$iKd!Hp8~_B]~aslҤ1 ! ("" Dpe@!"8;B cvv6Rj@@Qp8p88!~0 9fggC))iEGDp8p8bO7 sPJJiEQ Dp8p8bDp!~0 9fggC))iE+H)UUMJJR""8p81ƢhII c u9l(%4 (p)iIIIRJDp8q "X4-..f~08瘝 @ P 4-))IJp8p8DFŌ1BuoR4%%%I)p8Uch1WB cvv6Rj@@QRӴ$)%"p8jc,3 B]~asΆRRJM (WRj$DDp8p8W "bEb\A7 sPJJiEQ RJMӒH   B)"@!D"@7"@_FpM!Dᯈ W!"@R "@ $D(E*A""\Fp p8RDFŌ1BuoR4%%%I)H1ΤMae B T\GB )rD$"ж,[jI6)-* MS "qE IӲ$2$%. mْ1F$WI"!%S$bmK` IJP]*"f 9 ŽZ6cHpͥe)-* C@¶-#"`\S,3䚪0DW5B^F Ce 8'!qM´, GaK cDDR )dLQ@e P؂k.N´cH0US.KR1UH@RH)$TT!"\3!$Bdp "B)R!"\ˈmʉ!%D/RDRJBD߁HXr$"ж,[imۂ#Ir@pU ۲I ۲%j6-h@DH cI"@pcpI)2D(E$C$,HJIp8DFŌ1BuoR4%%% )9cf$PKWC3v |gm,?ɯ?f%YҨC:T?EqfI37zҀOtY4Jiv~<,Zq_DyiTłvb=KwqBux|cmpy+gMLo숑t0}2kG_Аa.҅a{ST%$4ڸ; A\w иkJU~f}w5son]7vXÞ $,4`rHqqv|U ~ !#mTsi[9O^yS4 "k.'Cƌ6sl psuN(` K $ /U~ku}ewyJz~.x/LBO eJ9qTw=!#UW&r^fƬ_STvt$:mNE"Jw6ޓ%shzZe{i1m3n]Y T5Z #Vg2h"տ en@&k=nyυ$MF1)s~` KD RRxv;9Wb8ZBel$7,fH7M%jp2vj;5xٍf$j`a_s5 5&=e*'l^ZYoS%DGr6gyWo=d==x*(uҽLΤ jt`ᤛG=<8f\|:C{WS: ͧGVX۽Pd1,ؑzIOnd"[ Aɤh0EJ=(RUk˛7~P  (E)?,1TN})˓7\a`m|]ؚ;CǑCcjWOH2%@@R!"vJ)|s(bq81bZ+!,`$Io!dY6͈Ƚnm 9|]7j0n[j6إcG[ۚwڐtjUjU˗5]75]/ 83Ñ}VܥF Rsna]~QB&ؠy;St'vFij>36q$y5-7J5>oVӍ*R .Xjjq#Ojem]:-{*r$^{&%:k[bԐa7_joWEN\d bRKTlQ%h@+M;oG:wE߈/]A:I ^)+RN9@"K{ʘSZMIt"Ac삛ESܻYH?xYWWN^ .X7hw0rfH-j\+<̛k}‚;ᴱA8n)wh׮)eaD/ێ]1Vƛg_d%nH,aOh (Zgo3] |/R)h +/> ݧ):_ Shۺw[NO?x)m?5MUb^<|=O;/yhG^+CsB*/)nSJsEQ,` _ !dY&I|C!˲lB"jCCL/ˌn?RdT}xKծG5%:fiOҵȻj [Ί9—ً(SMJZg*P̡ݏ~QN}۞5zF[֪ρ?|5rj; 44K8k^Ա*SGb_6yib~퇾}+]L7?Ԯӂ[f_ WV[u {c-,L=jy7}ڑCeLd&i.mȞ٘si:܋ $}BBi㛤bFo:ssXs=s˲qw?9t=#/{pEؿ|κrMGԥ×)>1TNMBBom~Өx_?79qvߌ]:x,- ?J*hb "(^!ss;VӸq~q 5hKFԜk@B(i P/jl2LijEf22V۔5|ߞ#ҍ/=6ݟ2i@B/Vw䋉%ۗe#ǜpYrVĭ=/ї^c9]b_{b2p!?Nɵ#zdVH%u:ietz@k؟b"]y I~ǐ9vǩy+*n|dit87/؝ -\bd0+\ () vljѕ,z͞lxb4L٢Ns7aɉ,u,1}g !όj]>jԶ_{eN꿹bπamu=I札zN"q簾y+7V ) ~vdJErzJ%Y=JS —0zZM7ƛhiS2e@@?"RWi?ѰCO sEM|h8v]S ޔ G,iZ6{f/'&%~:FDDyB&@:}cc7.N@ui~mJAO2ss[ga&Y Dkɽ_ɠw3ފ Zm߉#u? ('3vo/]5ʁϘfnnYֲҲ{V 2'\kquU;5qgs}+Kz4l6t6m!Tݴþ֏ii15}μ5ﭔpež oVL[Qyꨲ'ʦٮr}j|h KJ[|#|'FD5 Y>zƈ8ioq G]8.,%{N\wSLZ_wn [L[`Cѣ&Ɵ xjQ.,oڰ3uʼnv;LK?}fR{ _0JlyoxtJgY{ im5;w.LuZgvZGϚ[Ӻy̕G]&S~kzz{ 3 ]ƯŚw;OzuA٫gYٱ/r.8e"5t8gƅǾ}fdcfj/<jSt4z@$𧅈pMݺ˱f\G-cˆIiOBvx}v[/. n4|v=./q;޴ӊ'T۸5$%:>/*bU{{tlq&ElV57]]qQ>#>Y:I-WwE\ 4Oo]PAY.[Dۿq'´#T:Dz<N)opEX,1FV+|%el6$I !,fYBhArU2 ȸ;D*;v,l\ٻy7A/fWX>qȁd.[1$D( Am߳Jvƒ'Bֱ\<УW/a~grui{$wj]+]Hsڡ˗*DOyh*W+CiŲeeOv'{"'O^+VcFդsKpx>~Y,I~qo֣yB9>;d}JK IDAT Z]ړso& IFU?>wdǾIA+Y:4ԠC%VGa^TF-SR:}zxuV.f\|VZpqf/PX>|v-Z׿2,Rr'hQ{n۷eGO|dlܺ[G5ƼFϵ/?y)(8?*fUWV]yu*ntB%\zZ ͧzjp"Rtlj ɀ V)ѹOEJlإQV㻮w8: E<)T&ٍ_嵖Ρ{K._pe-{=6oU%*poݤJ \:{==FF)t_he:V/OW=mX2eg;tZ*/)nSJsEQ,` _ !dY&I|C!˲lSJ4@D$LdCO`=tOd("!~OF( "!Hj & c:oô44w{A1AIZ%@НHs4`> (q9/Q6Y9G fu,(HiFHwd}o|NTL-S+ZLz_Z:54Ks@ӛ=%~!}ng:WLF"h4xPvs."ZSpeO#bk3-h ^"N:p%QoRG(饲9Đt$ȤH |t&Eb.n 2j\B1%@(R>IR>`*+u`\a!"q_V\.dԥ&3[{lbٳ4H _"J`K@}GR"=́Q t!$" !=@t;{}(TJ|Ljb103=O(>-ͥ3i@ TDe"]5ЮyOjPBϑ1LK%J PTuH)x:cI`9qFQD$TdO%H RFDsdҀpnGA AzYL.G#1tۓ=~"iBB+)駌IpatcOsrzShNBO%8G P3՞!Lc kӞqIk/26B<4!8Ȩ@|HI}iQ(8Sҽ&ifߛf{B@ HHILgkN;|e)Ĥ'N0f`BD"i4+5"Dhkw&@ TD >3NC:y))@H$V K(PJ'-' #4) DBG i(錊NA(|^gz VcD K(\Z NdJR8犢X,#VBȲ 6M$JhX4KPŋ'%\V^^;z &Cx"Urj0JxR?7SS ҅1tE2<GH%MSgLnRrL잫/WoT]pI+nlF > J* ѓ]F[H.tĮ_cFbY_9g 7P<sX0fXdJ2ػ{'zPgD8 쵫w-#ITfEs\;}ieSZ\!'^aZ kOMԠZ שV.K0+}^ZRr['vKj)?aOHp9\!D8sլT˗>,"(ED!! a e̒')ًn`eP$_\B!ߓBErS_ݓo~B 2p 5͵SS c*fK>svGX/yɝwP4M*յϮy5^/dȜ71ɳ̘@.v.K r!cȅ@B !1( RU^'zx!MP(A9_!r!2F(J)!\$JR8犢X,#VBȲ 6M$(MS7xUK=^2ìC6s`>kPa $4l' 2$􍋻ڬȬPP7`.^yZjS^sX'OV ,pd^WRk-2*NΠcڽ}-b W/i"SWuN.QK+g.]L'{vI沾]Ka'y!67k53y#\p"Dw,=Xr$xkj2gK:ȭ=l{|%#|A:H(DHej6lٳVH]h;d:/^,Z7IJrl9cN-_R|רc ;wU\ܹ!Κi\,&?ҭVj2Z+nزSBI[QuQ4TvV u;b`GtRYoozS3(zY1!۴:gCIV/Ak!^wVO}q{۱/u|-o\=fEf5%ӟ{ٳMV5mV|螥ϯC ُwo?H~ ̄vd哯E<7xl\Y@5&ܝC@ɨPB+t@h姎|X2S?L(!׆P8]n$ &=Nd4)p:B<._DWZ  !RT\Qp8cjWBYfI^'f*|マ"lqH;uݗjUK'VɟW`qo1N(\}Iګ%}/ Tkǵ|^˖H-{9aHœa@]_JGf5qOYgSȽI &kvp-=b-£bz۽} ~Slcמ]4aKc3rzkٍ+>4IgrJ8O48\ծC홛[s#=Ѩc2w5&ׁ7'ru<䇷 1;r|:kYz>"ha蹟Y+4vQl-"_|yş_x oxgҙ1%6:!?j7pPA "b7V_2RNo[ṓ|:NÆU[z f~I_aҒ~:تGO>VBE.VPx厽\~04(~ شl9_Q &܅J2c0KYpc&=?*0KU:d6r3k /zASk"}/Q1 b&sSM=5#?9&߀*Q.j5omZi5'>]'Z[KԬ[04p mRK{ƢPLɰ0Agy:f|]f u &u&p}1wZHVٽە#o{`4h (cmXfj\ٽV>vZn%zu,زDQ1w^;Gf=zp稾v\ezV8rΟbeu˂>N=jݦ[ț_Yۃ{]~f4jrGcn~IQsֵfE_8ҲHqg5tk|+6D}˚v:L6iYc+{~\9=6R:y'Ȓ֞}WX]'L@2)BBsM~;Q9tح9ݢf[t/Zv %mߑДqkxpL&tˆk|6":~ŠVmHÔ1G[CRQ%L51zF,M} -EDB$O\>kVgVΤKsܦ .U?vǂRͻ~1~Mlk[ETȒ6\3Vg첕%';Dm6J.pq~YŒ}f\bm9>eq^V]ѣh({ެ~J}|&@) Ml]Kȳ/\IEQMٴB}e#f[F#f>r{ag c"~Zt1q4j-_cd|Br~ ͒̈ϓ^#j?~$-FȮ瓭{^Y{Q>gǜN3r[Y )67YC Fg(bp|KV6vn:uj5_lu&Gn^4M/nffnsٌ4wOWjۺr͙pᆵ]D7zї1uZGF?M>ɫ"J>B"5p^sRG2lVszkQ\ϜjMLi'3ԋ1J}!S ,whFLKtA_dʆ3>2&nٱzW=ɍ\Dr.޽$벒a>*V;]}fmާԲnO<&]W"ulu?(fۦ&|S IDATF GWx!c#[--#?ԪӠRŒ W³ߺ[ԯW{$2p;/P999iM&E^??a17lRj6cn3!ȋ-c:b$F7O+AϢIs68Ihtapѕ+8v]8l@Qnc%?hZWNT\xɟVټ{ﺝ1ȹ< m5oD[~J\ ˒|yk^1c̋r ۲Z\!ęt/^wG-Mv޻7^~|~vMޤnkNirc3>ѐ_q˕h0؋F-"X-ΒnV0Czwm?[l?5j˖y&;:N\U3l]g\]ݢvB%闪LiIAy5-g9x/w"w8_Nso_ԕXV۵St~zrm77~Kş-1mz;yˤ}ۏ9c9^:0k~K57|!L9JRSsEQ,` _ !dY&I"C{}eĔ!d[/M/po)W,59mjo~=^~]ê79ahj悤oK%[&ԫ^iӼӆhS+_66pv@D`dAYm;s"~#"nl^閟XO$騄1CZMdH͇{Ǐum76ENN[u N_0Qw~˭ ] m{fnF[SqNGtw;f.l|ݼ5ߌSycüG뺢!%<%=ӥ~Cc{?TVe~X3JZ{]4vks{gO']P8!bp#] zX1`섽\Q3)9WJ=j`u[5qB1ew>u!=|j\]$8Q"zL_!ٍefzjo{/tƑ~퓿Z#W>~tIaKquK..6~ytcD)֫˖ w|SYqE$ ً uZqӛc3 \~asz&ŋm"DMm?J`ZNJtxi? YGCn ӿKQNg0, h4 \$0L(¿a+VoVڪyޠFUzIآ!ã>.YӆҸq8Hr l 8o}ߊ"!+,oФ Xŋg*]ڥ!jO'W mJ%=W): 2Jިt^a\LSJq9-x`Čt=~*njv"3H/+-ݽc,nu-oV+#'-1rI`[̲RՕ',uҪc7Mis9.v<ЫG Ͷ'֋twGN{cYEþo6/ݿ^i/%\'6xq8mCy|m[jMѝ Sǹ:0+;{=pδE_{@?9 GϼxZnа_Um)g)n.()uoMl˵0u1Ъx>^s2zxxp 1![9sۜ>?fA\}W9jLCvj7" e٪7 dbyUW{;9"$*c[MVvymkYcxi/*YWY,n/ -TT" ?5 uUSmoxcByf/[]Ct)=oj[A0o \vlv[hJpOeT0cT s^sө8RH_ &t#5b޽'_?1?6)ozbf/r^UqjqL#fF=l}OISE "F(9$ L&(DĹfCIR!( D*[5N{AxG$Ib!ǭV iD^RV^9yGPiYڭZJ._ep6 ^zfXl)4|jl$?{-Dr 6%I@6J-(dJd\V Lbv~}ZpU ]nѰ#'nU R *h+TkҰ_~}J@!j4+ '&1@"pE;$K =|j^_(6V 9'd jCATDI%Wv{-cڮ~/kV\\@rՂl˨VK A$Ibl;H"WvIjC$DAҢr4~G/7Xa]܄k):@DPD %U ޥ)ui}Ο>R/hۓ_$iFDjVZ$iknYε_Z`Z키Q1@DPdAjZZۧm[㍋?77;1Q% 8HQ<|Gr^vKBŁbZ~Vs=}T76wӅ6Z"^b [5uy~mEd +y/D"[]j{vEg7ִi+}c񒠘toUp旫 PT.; $"ӿKQNg0, h4 \$0L(¿"6@Q DADx16"j4* @Dͦ $ "jSVPrnsZ2a"AD"!F*I- 90Fls8D  {v,j" " {@D"QQ1 D$"`"CfuS%f'IFD@D@D@D  1n%F "q>zOQ !Ɋ fq)3 WV<`Ew!pjWT*ID@@z"dnJD ? "Ɋ( m*c"$5j5Y;jp"D$8lvԒ! " 8!0DTVDN>{3KE"BI`D@;"DQNg0, h4 \$0L(߉19'Da @!焈7D sBD"Bd@ĉDe _2 D 8"gD"'NDXD"WCEb;0F"Ÿ!"CD!rn-%ȀsBD ADO*!"D"w]< %?#D@X "GPDw 8C "np‚"BFĉ9"IQNg0, h4 \$0L( #( 0 ?/@ Q@"@?"@A;288NNNN( bF#K&IEprr/#"|=D?7"2DO!qDp@D  t:`XA@e8$dEEtbFp%I$"kDNNNNNNNNNNN1DMQNg0, h4 \$0L(#FDPE "F(9$ L&(ιJsD''''''''''1ƬVkaa!c >(N3 E4Ps.IL&QsIz=AD1j6cEQt:`X, h2sId2K9NNNNNNNNNNN "bYV( bF#K&IE\$^9GDprrrrrrrrrrhcjf|@QNg0, h4 \$0L(8$z9"GcVl63t:`XA@e8$dE>9$Is@2)"2@D@NN+"bYV( bF#K&IE\$^9GD@=@?FD@D"D"DWop?C!D"@!"DCOFP#  ‡CDH {DNN  Lj!;" nLT" oA$Eq8 H B"BDEe!D(C"  1ƬVlfEtbFp%I$"|s.I^""E20A%2DI% (9D!"1á!! e  BD A""@)*#&sQ+LDD L+ jM W(H9D 'PI*"sdDA` EVd NLD= Q`P8e*Aq880((,;U*EA\PDH !)A@Pv ( b+J.sJŐDD 2"D29'BD##"d L!cq|q RdBL`HpRD` eC!EC@@rQv3A✃"Ӑ"DJ$"QRdT*D"Ή鿌11{9{PsN "B"'29Gd1DnwJsN |! P.T8 焈@d١ d *eYTbwih4޻tYH WdNL, \QdN$1{I۸IzuB%(eLR9"2Ơ 92D@D+"N"N{9{9'\Q("A%p9* # 1es@r]f́ CD(Cĉ!"8}1fZf3c >(N3 E4Ps.IL&QsIz¹ijau?ޘXQ0eQ~c?3a>hTBո=I]UȬ>ƮjЖuib+%clZ$)/\]QM1a6yw9GdvˎFuJL:}Zf!#@ԅ{54d\wJHMJM⒢-6W˒㷟Ms̝/Z#" 7*Q'Zs_TceW"G"/EVc|7)`}d]q9aڌox`y? 5GԊ*I-jWȲIJ"*@`T[Ҩ>XqYJ%62Q2fWs]:)O+ Lڤ<)qI'[ \>M5jʷsT٭q%ǧn& " qtS4DVWQ!}iwujT~}梻DYv V"2"B&"9d.5:+Y&QS`iQP*%vYQdEUȁ>D(۬v+]!\tjVZjSEѸSiQ́L\]DAQҒ"Rq!&m𶂏;b-)E+Uqm_ҳ)F׳\:j;;GQi$pBD@[Jl2DW75"͡0T^lspEe\B941Mm/;,[F21(0Q@pXcjf|@QNg0, h4 \$0L(8$zNd[҄u_N{bgnNZ{Fvk^GvlH{`Xp҃פ"Y7k]v;1r<5"4}3nϽb[ {y+;{[Ec366';Sߊ ?WϬq |03&⪤HumbT+o1zYp= ϼj=t Z:.o\WmLkzSAKV)p&Y5xگ6P.?umM~<+զG jE!9dG1]Zd0>xbS+wMl~ Wtl܊Znv)?=#ims'$5]qJ#o3S&6nk.^.|~6/%a7*75^+'rIj>:Oy#έ_jsD?3D[qOO,10vBE}Ui%j]2lw<\JMZ6~tYP*G~=9b7{F- R7.{汭كBY[^-jGkͧ~PzD֩Ѥ7wwDG%i IDATf|Q׋eE&Y_faw8e֍IIZU.jvB=1L{wsNUܽ~NKׯk/vbťjjZU z7xuO}j7oXSJ~a^QO[*Qj~u o\{ܽ[Zo^VVr4-uo)M'm~ >:O9[7i@Q '-Uۋ\9ܦרEr7n]ZA%FԼ~Di0t:[˛{۴h"Y r ^(("8}1fZf3c >(N3 E4Ps.IL&QsIz=)RMf]W:S^m^q߀4U}#cޒ42>sZ+s\E*,,YUFl>iakӿJ9խ +ﹸ~d:J[Z?w骃5h#t'}9ə}=?]=ֆiK/xw Y>I~<ÇW~ymyusiy=νخm[կ`Ġ"wP+ GMMQ2> ݺهS&q[u>Cw +x?U+tSj5i'-v]ecju z#}r_nH{yV ?i4b9fxX~n>Lp6VRH X|Ml3gC?ίE϶>YP" >N*.]'92mOQsi |_t@b@@C"o1*uFZ-1<3gMzsz#rup [b ŸڭōNY1kVJɃ?[?,2aVTfS&nWi_TvCwl;!i[?8Pw~tf7zy~=)hzSHyfSS59tn/[ oԴRQ Sc ]zV}h+WlzV\Vkk#e\;4a8XmfTօ,Z+usݴpksB+x+\r"^>߲ O^ݪ%1S]tݿu^=∁ry,VSG{'_͟\PQe1.Ƿt^ݯ>vuAӴn}NVQ͇<+[AX>0;+9]_v+\o7MEKi_u*}i8_70)w+J|=i:s-6BЌU5,/i6؈m^ɣ\oH'6~sry®^:D5;'w+|L|K\Q%G}:, j9d/LF3wxPc'y;y榛b╯7^o5smf]8UǧXdy>2*p{ڄI>^3{.g;7< 7$:s(u Y!x2&x5†M)aOGkַ0bROF$իr6ㆢC}IĈ'M]=#3/w pNI6婽{`ʖIiۮ[׵ *t&U{m%|oIqF.R˂3fю?oݣhDĴz |dU[aZ¢?ИY=̿4}aSO:1o%q?"`͹LVँK~nxzʷWxsr_D7>i _w V^MSq]Ov2H1,#ޜ~>l֡rA DzIG~!+ =C7vibVK[rxGw֞(N3 E4Ps.IL&QsIzp& p9obD.7/J0fN^#CR?}qh]}(3#z8d`K[,^~6qr+k&~;~X@}0=9nguH \3;m&ϩ+;cWm|Rbֳw-ٲ{(!үq%lujٛ)S{w],% ʵKʎm}?syKWY&yeN?o:7:u·'2o 42}ITwjEJN446Bc>ӢGi(yx^_{|~ԎyG6ywIPl߿/SְEծ%LXxDqQK*ỵkoL_A54RϾYՕK['=>s&nX}=s'ǬT޲M- ~9I1mVmi Z͟qicZh.U`꥛W) <5jHBPዂV7} 0+죕 U^Ȏ(gEn(&j~A/g (:xe}, O:ifː.R8?X㪑+:ZojqG)s |NXzf}U/= [Zos|oyt{y8n.(yWA ۾gzS'vfN f9|Ye)3WFDaw%f/.Quqqmwf$fnD3tpY.&q*5*[ãEߧk׶WZnׅ#&TlqE拆O<Ÿ[+dz\jO\qU/aZyܭc#VZ=vlW?SP蠥Gu)># dV+tfфy[o6Mv /Wp0}Zڮ[|BiGM& 9j˞?,2޿х 5ǀMZ%lT؝R-L^`fЮ+o\#y!W+^ip=׹.~Q5kGmzU7M[^"i; TQ藹 2/:hnNсX7jsx$+"^)z52Gɞa)*7zoڄ~y&q?(d ~=<ѯ驑\-)}hXzᱡ]%|w__^i+Εt9"[6n5x*oy6.8*90m:Ur/R^ZTž>_Xxhb×;v-\H|M&]eyX1*\KiWƤkZ'.^qFޅ6f 'kT} ȬU[$O֞]vշ_]4f_6jZ{+_5[%WƦi=9uLW {?ϡ\bLåxt Gvx UK kb?ų= >TiE{Z;*~ՅvBkm~/Q{izWrFQ>g弳93V3|Wo7cE_uIv;jԱ㲩9$Il-q`Ѿ-7WNMS+61yu[6?i[F.=scb!7rd)dSXG#~𖾜+*laL|﹎_, <:wC%WlUˬ =U<7?0,ۧM5xWۑɫ^z.^qlo›9Gֆq.?VZ2}gOns/79͚8)n` 5JMڇO_yhu]3/~^(7dPf6uͼM=sCP⥙:DF(G>aU7~epf_ܕm蠺hU[^8ԬExryzz})ז-4"sÚu&bl}'`Z? ;bstNO]+fg?W{몕.B:Hׂ'>>%ǰ+|j!~?~פ }G1zJҫR7IP ^SסI+d%*?|Vv {A\6lë&NpJDfhu3ZY9WV+2v}$@pZOMĬ@q|GX8yEC҈ Lq SWl_Z4vv|r>-uE5t@3aD[\u i;ojp@Plbe}Wud%O*Nnl^Fq8 ^먞>_L lxiu`ѷ}ՊB@} mxߏaW'':26Q[FS}g j.yaC w漨zh2vow2Ai~{3mZnv;2bVqR׮ےg+^Q؅7 "7ǿɾ= J-wLm67`Ώ;7;{gG}7ɣ/]рIOJ=qKŻDݧ۲ȭC;y%M=-m|tsnzb㑋yU׆ eK#'4 k[ʸ^Uw,qbmsxjDʞ/ۏ\2"vX=פU+=$n_[:_Xk|okAR $4* yV[(_ܗ7+-`>YQZ;7Ƶ_#˕*5+/\~^̉ӖbSi߾HZ̾yr՝uO8½vi韈cVl63t:`XA@e8$dE>9$IsΉsέi/TBL1?yv^m3;oyVd4Ϝ.H*@&(ߢu=8 Ϊ?-$ CB ;,"etCv8택ڇ>tδ: D H! HbI9>>Tt^n  o'ҹvlN@/6v_U߽>ϹPɱ/hE=(]huMgN}+aŶ6* Z?9]6ќKfmhEY0sdJI햪{eϼiMipLUS_0OYܚ+J-g,ȝLνvh[S߰|B4:o}u[Ij6_\*X6c|ϠȘsx!M*o^hn>S/l^^<{ӧ*{yS)~Q_q;Ɣ IcG *0u'-J+M75IKl򘟨|fYOgɪD1ES4t ) `sǽ%;P1?}H.Q8@6yw>9;9"o۽vg1-\͑!ǔ#VkIIDAT␧s&ݻ.9 ӷش2vó93sn}]ś 2@89ۖ[]x/+b%S\n>eE!K)J/v+\cKpwGٺ75!wQ$,)_oHLDe6;_gیdŮGN^:?TG{$ {s, y.|~i'߯;Ng>Z`Ⅶ#=xE׬(͞;\w2P$r+xBf) x~z%s,unvv_Q6{t;Xz>6Sӌ*!YvF T34!:n0eb5RMO6Kw릩0Dzb$,;" -t\& ]f#oc~&PJ !yyye /q>NʥIENDB`python-libnmap-0.7.3/examples/nmap_task.py000066400000000000000000000010001430422236200206140ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- from libnmap.process import NmapProcess def mycallback(nmaptask): nmaptask = nmap_proc.current_task if nmaptask: print( "Task {0} ({1}): ETC: {2} DONE: {3}%".format( nmaptask.name, nmaptask.status, nmaptask.etc, nmaptask.progress ) ) nmap_proc = NmapProcess( targets="scanme.nmap.org", options="-sV", event_callback=mycallback ) nmap_proc.run() print(nmap_proc.stdout) print(nmap_proc.stderr) python-libnmap-0.7.3/examples/nmap_task_bg.py000066400000000000000000000010601430422236200212720ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- from libnmap.process import NmapProcess nmap_proc = NmapProcess(targets="scanme.nmap.org", options="-sV") nmap_proc.run_background() while nmap_proc.is_running(): nmaptask = nmap_proc.current_task if nmaptask: print( "Task {0} ({1}): ETC: {2} DONE: {3}%".format( nmaptask.name, nmaptask.status, nmaptask.etc, nmaptask.progress ) ) print("rc: {0} output: {1}".format(nmap_proc.rc, nmap_proc.summary)) print(nmap_proc.stdout) print(nmap_proc.stderr) python-libnmap-0.7.3/examples/os_fingerprint.py000066400000000000000000000015021430422236200216760ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- from libnmap.parser import NmapParser rep = NmapParser.parse_fromfile("libnmap/test/files/os_scan6.xml") print("{0}/{1} hosts up".format(rep.hosts_up, rep.hosts_total)) for _host in rep.hosts: if _host.is_up(): print("{0} {1}".format(_host.address, " ".join(_host.hostnames))) if _host.os_fingerprinted: print("OS Fingerprint:") msg = "" for osm in _host.os.osmatches: print("Found Match:{0} ({1}%)".format(osm.name, osm.accuracy)) for osc in osm.osclasses: print("\tOS Class: {0}".format(osc.description)) for cpe in osc.cpelist: print("\tCPE: {0}".format(cpe.cpestring)) else: print("No fingerprint available") python-libnmap-0.7.3/examples/proc_async.py000066400000000000000000000007001430422236200210050ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- from time import sleep from libnmap.process import NmapProcess nmap_proc = NmapProcess(targets="scanme.nmap.org", options="-sT") nmap_proc.run_background() while nmap_proc.is_running(): print( "Nmap Scan running: ETC: {0} DONE: {1}%".format( nmap_proc.etc, nmap_proc.progress ) ) sleep(2) print("rc: {0} output: {1}".format(nmap_proc.rc, nmap_proc.summary)) python-libnmap-0.7.3/examples/proc_nmap_like.py000066400000000000000000000031351430422236200216340ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- from libnmap.parser import NmapParser, NmapParserException from libnmap.process import NmapProcess # start a new nmap scan on localhost with some specific options def do_scan(targets, options): parsed = None nmproc = NmapProcess(targets, options) rc = nmproc.run() if rc != 0: print("nmap scan failed: {0}".format(nmproc.stderr)) try: parsed = NmapParser.parse(nmproc.stdout) except NmapParserException as e: print("Exception raised while parsing scan: {0}".format(e.msg)) return parsed # print scan results from a nmap report def print_scan(nmap_report): print( "Starting Nmap {0} ( http://nmap.org ) at {1}".format( nmap_report.version, nmap_report.started ) ) for host in nmap_report.hosts: if len(host.hostnames): tmp_host = host.hostnames.pop() else: tmp_host = host.address print("Nmap scan report for {0} ({1})".format(tmp_host, host.address)) print("Host is {0}.".format(host.status)) print(" PORT STATE SERVICE") for serv in host.services: pserv = "{0:>5s}/{1:3s} {2:12s} {3}".format( str(serv.port), serv.protocol, serv.state, serv.service ) if len(serv.banner): pserv += " ({0})".format(serv.banner) print(pserv) print(nmap_report.summary) if __name__ == "__main__": report = do_scan("127.0.0.1", "-sV") if report: print_scan(report) else: print("No results returned") python-libnmap-0.7.3/libnmap/000077500000000000000000000000001430422236200161025ustar00rootroot00000000000000python-libnmap-0.7.3/libnmap/__init__.py000066400000000000000000000003571430422236200202200ustar00rootroot00000000000000# -*- coding: utf-8 -*- __author__ = "Ronald Bister, Mike Boutillier" __credits__ = ["Ronald Bister", "Mike Boutillier"] __maintainer__ = "Ronald Bister" __email__ = "mini.pelle@gmail.com" __license__ = "Apache 2.0" __version__ = "0.7.2" python-libnmap-0.7.3/libnmap/diff.py000066400000000000000000000052741430422236200173740ustar00rootroot00000000000000# -*- coding: utf-8 -*- class DictDiffer(object): """ Calculate the difference between two dictionaries as: (1) items added (2) items removed (3) keys same in both but changed values (4) keys same in both and unchanged values """ def __init__(self, current_dict, past_dict): self.current_dict = current_dict self.past_dict = past_dict self.set_current = set(current_dict.keys()) self.set_past = set(past_dict.keys()) self.intersect = self.set_current.intersection(self.set_past) def added(self): return self.set_current - self.intersect def removed(self): return self.set_past - self.intersect def changed(self): return set( o for o in self.intersect if self.past_dict[o] != self.current_dict[o] ) def unchanged(self): return set( o for o in self.intersect if self.past_dict[o] == self.current_dict[o] ) class NmapDiff(DictDiffer): """ NmapDiff compares two objects of same type to enable the user to check: - what has changed - what has been added - what has been removed - what was kept unchanged NmapDiff inherit from DictDiffer which makes the actual comparison. The different methods from DictDiffer used by NmapDiff are the following: - NmapDiff.changed() - NmapDiff.added() - NmapDiff.removed() - NmapDiff.unchanged() Each of the returns a python set() of key which have changed in the compared objects. To check the different keys that could be returned, refer to the get_dict() method of the objects you which to compare (i.e: libnmap.objects.NmapHost, NmapService,...). """ def __init__(self, nmap_obj1, nmap_obj2): """ Constructor of NmapDiff: - Checks if the two objects are of the same class - Checks if the objects are "comparable" via a call to id() (dirty) - Inherits from DictDiffer and """ if ( nmap_obj1.__class__ != nmap_obj2.__class__ or nmap_obj1.id != nmap_obj2.id ): raise NmapDiffException("Comparing objects with non-matching id") self.object1 = nmap_obj1.get_dict() self.object2 = nmap_obj2.get_dict() DictDiffer.__init__(self, self.object1, self.object2) def __repr__(self): return ( "added: [{0}] -- changed: [{1}] -- " "unchanged: [{2}] -- removed [{3}]".format( self.added(), self.changed(), self.unchanged(), self.removed() ) ) class NmapDiffException(Exception): def __init__(self, msg): self.msg = msg python-libnmap-0.7.3/libnmap/objects/000077500000000000000000000000001430422236200175335ustar00rootroot00000000000000python-libnmap-0.7.3/libnmap/objects/__init__.py000066400000000000000000000003261430422236200216450ustar00rootroot00000000000000# -*- coding: utf-8 -*- from libnmap.objects.host import NmapHost from libnmap.objects.report import NmapReport from libnmap.objects.service import NmapService __all__ = ["NmapReport", "NmapHost", "NmapService"] python-libnmap-0.7.3/libnmap/objects/cpe.py000066400000000000000000000044101430422236200206530ustar00rootroot00000000000000# -*- coding: utf-8 -*- class CPE(object): """ CPE class offers an API for basic CPE objects. These objects could be found in NmapService or in tag within NmapHost. :todo: interpret CPE string and provide appropriate API """ def __init__(self, cpestring): self._cpestring = cpestring zk = [ "cpe", "part", "vendor", "product", "version", "update", "edition", "language", ] self._cpedict = dict((k, "") for k in zk) splitup = cpestring.split(":") self._cpedict.update(dict(zip(zk, splitup))) @property def cpestring(self): """ Accessor for the full CPE string. """ return self._cpestring @property def cpedict(self): """ Accessor for _cpedict """ return self._cpedict def __repr__(self): return self._cpestring def get_part(self): """ Returns the cpe part (/o, /h, /a) """ return self._cpedict["part"] def get_vendor(self): """ Returns the vendor name """ return self._cpedict["vendor"] def get_product(self): """ Returns the product name """ return self._cpedict["product"] def get_version(self): """ Returns the version of the cpe """ return self._cpedict["version"] def get_update(self): """ Returns the update version """ return self._cpedict["update"] def get_edition(self): """ Returns the cpe edition """ return self._cpedict["edition"] def get_language(self): """ Returns the cpe language """ return self._cpedict["language"] def is_application(self): """ Returns True if cpe describes an application """ return self.get_part() == "/a" def is_hardware(self): """ Returns True if cpe describes a hardware """ return self.get_part() == "/h" def is_operating_system(self): """ Returns True if cpe describes an operating system """ return self.get_part() == "/o" python-libnmap-0.7.3/libnmap/objects/host.py000066400000000000000000000332071430422236200210670ustar00rootroot00000000000000# -*- coding: utf-8 -*- from libnmap.diff import NmapDiff from libnmap.objects.os import NmapOSFingerprint class NmapHost(object): """ NmapHost is a class representing a host object of NmapReport """ def __init__( self, starttime="", endtime="", address=None, status=None, hostnames=None, services=None, extras=None, ): """ NmapHost constructor :param starttime: unix timestamp of when the scan against that host started :type starttime: string :param endtime: unix timestamp of when the scan against that host ended :type endtime: string :param address: dict ie :{'addr': '127.0.0.1', 'addrtype': 'ipv4'} :param status: dict ie:{'reason': 'localhost-response', 'state': 'up'} :return: NmapHost: """ self._starttime = starttime self._endtime = endtime self._hostnames = hostnames if hostnames is not None else [] self._status = status if status is not None else {} self._services = services if services is not None else [] self._extras = extras if extras is not None else {} self._osfingerprinted = False self.os = None if "os" in self._extras: self.os = NmapOSFingerprint(self._extras["os"]) self._osfingerprinted = True else: self.os = NmapOSFingerprint({}) self._ipv4_addr = None self._ipv6_addr = None self._mac_addr = None self._vendor = None for addr in address: if addr["addrtype"] == "ipv4": self._ipv4_addr = addr["addr"] elif addr["addrtype"] == "ipv6": self._ipv6_addr = addr["addr"] elif addr["addrtype"] == "mac": self._mac_addr = addr["addr"] if "vendor" in addr: self._vendor = addr["vendor"] self._main_address = self._ipv4_addr or self._ipv6_addr or "" self._address = address def __eq__(self, other): """ Compare eq NmapHost based on : - hostnames - address - if an associated services has changed :return: boolean """ rval = False if self.__class__ == other.__class__ and self.id == other.id: rval = self.changed(other) == 0 return rval def __ne__(self, other): """ Compare ne NmapHost based on: - hostnames - address - if an associated services has changed :return: boolean """ rval = True if self.__class__ == other.__class__ and self.id == other.id: rval = self.changed(other) > 0 return rval def __repr__(self): """ String representing the object :return: string """ return "{0}: [{1} ({2}) - {3}]".format( self.__class__.__name__, self.address, " ".join(self._hostnames), self.status, ) def __hash__(self): """ Hash is needed to be able to use our object in sets :return: hash """ return ( hash(self.status) ^ hash(self.address) ^ hash(self._mac_addr) ^ hash(frozenset(self._services)) ^ hash(frozenset(" ".join(self._hostnames))) ) def changed(self, other): """ return the number of attribute who have changed :param other: NmapHost object to compare :return int """ return len(self.diff(other).changed()) @property def starttime(self): """ Accessor for the unix timestamp of when the scan was started :return: string """ return self._starttime @property def endtime(self): """ Accessor for the unix timestamp of when the scan ended :return: string """ return self._endtime @property def address(self): """ Accessor for the IP address of the scanned host :return: IP address as a string """ return self._main_address @address.setter def address(self, addrdict): """ Setter for the address dictionnary. :param addrdict: valid dict is {'addr': '1.1.1.1', 'addrtype': 'ipv4'} """ if addrdict["addrtype"] == "ipv4": self._ipv4_addr = addrdict["addr"] elif addrdict["addrtype"] == "ipv6": self._ipv6_addr = addrdict["addr"] elif addrdict["addrtype"] == "mac": self._mac_addr = addrdict["addr"] if "vendor" in addrdict: self._vendor = addrdict["vendor"] self._main_address = self._ipv4_addr or self._ipv6_addr or "" self._address = addrdict @property def ipv4(self): """ Accessor for the IPv4 address of the scanned host :return: IPv4 address as a string """ return self._ipv4_addr or "" @property def mac(self): """ Accessor for the MAC address of the scanned host :return: MAC address as a string """ return self._mac_addr or "" @property def vendor(self): """ Accessor for the vendor attribute of the scanned host :return: string (vendor) of empty string if no vendor defined """ return self._vendor or "" @property def ipv6(self): """ Accessor for the IPv6 address of the scanned host :return: IPv6 address as a string """ return self._ipv6_addr or "" @property def status(self): """ Accessor for the host's status (up, down, unknown...) :return: string """ return self._status["state"] @status.setter def status(self, statusdict): """ Setter for the status dictionnary. :param statusdict: valid dict is {"state": "open", "reason": "syn-ack", "reason_ttl": "0"} 'state' is the only mandatory key. """ self._status = statusdict def is_up(self): """ method to determine if host is up or not :return: bool """ rval = False if self.status == "up": rval = True return rval @property def hostnames(self): """ Accessor returning the list of hostnames (array of strings). :return: array of string """ return self._hostnames @property def services(self): """ Accessor for the array of scanned services for that host. An array of NmapService objects is returned. :return: array of NmapService """ return self._services def get_ports(self): """ Retrieve a list of the port used by each service of the NmapHost :return: list: of tuples (port,'proto') ie:[(22,'tcp'),(25, 'tcp')] """ return [(p.port, p.protocol) for p in self._services] def get_open_ports(self): """ Same as get_ports() but only for open ports :return: list: of tuples (port,'proto') ie:[(22,'tcp'),(25, 'tcp')] """ return [ (p.port, p.protocol) for p in self._services if p.state == "open" ] def get_service(self, portno, protocol="tcp"): """ :param portno: int the portnumber :param protocol='tcp': string ('tcp','udp') :return: NmapService or None """ plist = [ p for p in self._services if (p.port == portno and p.protocol == protocol) ] if len(plist) > 1: raise Exception("Duplicate services found in NmapHost object") return plist.pop() if len(plist) else None def get_service_byid(self, service_id): """ Returns a NmapService by providing its id. The id of a nmap service is a python tupl made of (protocol, port) """ rval = None for _tmpservice in self._services: if _tmpservice.id == service_id: rval = _tmpservice return rval def os_class_probabilities(self): """ Returns an array of possible OS class detected during the OS fingerprinting. :return: Array of NmapOSClass objects """ rval = [] if self.os is not None: rval = self.os.osclasses return rval def os_match_probabilities(self): """ Returns an array of possible OS match detected during the OS fingerprinting :return: array of NmapOSMatches objects """ rval = [] if self.os is not None: rval = self.os.osmatches return rval @property def os_fingerprinted(self): """ Specify if the host has OS fingerprint data available :return: Boolean """ return self._osfingerprinted @property def os_fingerprint(self): """ Returns the fingerprint of the scanned system. :return: string """ rval = "" if self.os is not None: rval = "\n".join(self.os.fingerprints) return rval def os_ports_used(self): """ Returns an array of the ports used for OS fingerprinting :return: array of ports used: [{'portid': '22', 'proto': 'tcp', 'state': 'open'},] """ rval = [] try: rval = self._extras["os"]["ports_used"] except (KeyError, TypeError): pass return rval @property def tcpsequence(self): """ Returns the difficulty to determine remotely predict the tcp sequencing. return: string """ rval = "" try: rval = self._extras["tcpsequence"]["difficulty"] except (KeyError, TypeError): pass return rval @property def ipsequence(self): """ Return the class of ip sequence of the remote hosts. :return: string """ rval = "" try: rval = self._extras["ipidsequence"]["class"] except (KeyError, TypeError): pass return rval @property def uptime(self): """ uptime of the remote host (if nmap was able to determine it) :return: string (in seconds) """ rval = 0 try: rval = int(self._extras["uptime"]["seconds"]) except (KeyError, TypeError): pass return rval @property def lastboot(self): """ Since when the host was booted. :return: string """ rval = "" try: rval = self._extras["uptime"]["lastboot"] except (KeyError, TypeError): pass return rval @property def distance(self): """ Number of hops to host :return: int """ rval = 0 try: rval = int(self._extras["distance"]["value"]) except (KeyError, TypeError): pass return rval @property def scripts_results(self): """ Scripts results specific to the scanned host :return: array of cpe:/a:mysql:mysql:5.7.16 cpe:/a:memcached:memcached:1.4.25 python-libnmap-0.7.3/libnmap/test/files/full_sudo5.xml000066400000000000000000001445521430422236200230010ustar00rootroot00000000000000
cpe:/o:linux:linux_kernel:2.6.13
cpe:/o:microsoft:windows cpe:/o:microsoft:windows cpe:/o:microsoft:windows_server_2008::beta3 cpe:/o:microsoft:windows_7::-:professional cpe:/o:microsoft:windows cpe:/o:microsoft:windows_vista::-cpe:/o:microsoft:windows_vista::sp1 cpe:/o:microsoft:windows_server_2008::sp1 cpe:/o:microsoft:windows_7 cpe:/o:microsoft:windows_vista::sp2 cpe:/o:microsoft:windows_7::sp1 cpe:/o:microsoft:windows_server_2008
cpe:/a:heimdal:kerberos cpe:/o:microsoft:windows cpe:/o:apple:mac_os_x
cpe:/a:openbsd:openssh:5.9p1cpe:/o:linux:linux_kernel cpe:/a:apache:http_server:2.2.22 python-libnmap-0.7.3/libnmap/test/files/os_scan5.xml000066400000000000000000001126671430422236200224340ustar00rootroot00000000000000
python-libnmap-0.7.3/libnmap/test/files/os_scan6.xml000066400000000000000000001273261430422236200224330ustar00rootroot00000000000000
cpe:/o:linux:linux_kernel:2.6.13
cpe:/o:microsoft:windows_server_2008::beta3 cpe:/o:microsoft:windows_7::-:professional cpe:/o:microsoft:windows cpe:/o:microsoft:windows_vista::-cpe:/o:microsoft:windows_vista::sp1 cpe:/o:microsoft:windows_server_2008::sp1 cpe:/o:microsoft:windows_7 cpe:/o:microsoft:windows_vista::sp2 cpe:/o:microsoft:windows_7::sp1 cpe:/o:microsoft:windows_server_2008
cpe:/o:apple:mac_os_x:10.8 cpe:/o:apple:mac_os_x:10.8 cpe:/o:apple:iphone_os:5 cpe:/o:apple:iphone_os:5 python-libnmap-0.7.3/libnmap/test/files/test_osclass.xml000066400000000000000000000171651430422236200234250ustar00rootroot00000000000000
cpe:/o:linux:linux_kernel:3 python-libnmap-0.7.3/libnmap/test/process-stressbox/000077500000000000000000000000001430422236200225715ustar00rootroot00000000000000python-libnmap-0.7.3/libnmap/test/process-stressbox/check_fqp_nmap.py000066400000000000000000000035501430422236200261040ustar00rootroot00000000000000#!/usr/bin/env python from libnmap.parser import NmapParser, NmapParserException from libnmap.process import NmapProcess # start a new nmap scan on localhost with some specific options def do_scan(targets, options, fqp=None): parsed = None nm = NmapProcess(targets, options, fqp=fqp) rc = nm.run() if rc != 0: print("nmap scan failed: {0}".format(nm.stderr)) try: parsed = NmapParser.parse(nm.stdout) except NmapParserException as e: print("Exception raised while parsing scan: {0}".format(e.msg)) return parsed # print scan results from a nmap report def print_scan(nmap_report): print( "Starting Nmap {0} ( http://nmap.org ) at {1}".format( nmap_report.version, nmap_report.started ) ) for host in nmap_report.hosts: if len(host.hostnames): tmp_host = host.hostnames.pop() else: tmp_host = host.address print("Nmap scan report for {0} ({1})".format(tmp_host, host.address)) print("Host is {0}.".format(host.status)) print(" PORT STATE SERVICE") for serv in host.services: pserv = "{0:>5s}/{1:3s} {2:12s} {3}".format( str(serv.port), serv.protocol, serv.state, serv.service ) if len(serv.banner): pserv += " ({0})".format(serv.banner) print(pserv) print(nmap_report.summary) if __name__ == "__main__": report = do_scan("127.0.0.1", "-sT") print_scan(report) # test with full path to bin # /usr/bin/nmap report = do_scan("127.0.0.1", "-sT", fqp="/usr/bin/nmap") print_scan(report) # /usr/bin/lol --> will throw exception try: report = do_scan("127.0.0.1", "-sV", fqp="/usr/bin/lol") print("lolbin") print_scan(report) except Exception as exc: print(exc) python-libnmap-0.7.3/libnmap/test/process-stressbox/multi_nmap_process.py000066400000000000000000000011341430422236200270450ustar00rootroot00000000000000#!/usr/bin/env python from libnmap.process import NmapProcess def make_nmproc_obj(targets, options): return NmapProcess(targets=targets, options=options) def start_all(nmprocs): for nmp in nmprocs: print("Starting scan for host {0}".format(nmp.targets)) nmp.run() def summarize(nmprocs): for nmp in nmprocs: print("rc: {0} output: {1}".format(nmp.rc, len(nmp.stdout))) nm_targets = [] for h in range(20): nm_targets.append("localhost") nm_opts = "-sT" nm_procs = [make_nmproc_obj(t, nm_opts) for t in nm_targets] start_all(nm_procs) summarize(nm_procs) python-libnmap-0.7.3/libnmap/test/process-stressbox/multi_nmap_process_background.py000066400000000000000000000014731430422236200312520ustar00rootroot00000000000000#!/usr/bin/env python from time import sleep from libnmap.process import NmapProcess def make_nmproc_obj(targets, options): return NmapProcess(targets=targets, options=options) def start_all_bg(nmprocs): for nmp in nmprocs: nmp.run_background() def any_running(nmprocs): return any([nmp.is_running() for nmp in nmprocs]) def summarize(nmprocs): for nmp in nmprocs: print( "rc: {0} output: {1} stdout len: {2}".format( nmp.rc, nmp.summary, len(nmp.stdout) ) ) nm_targets = [] for h in range(10): nm_targets.append("scanme.nmap.org") nm_opts = "-sT" nm_procs = [make_nmproc_obj(t, nm_opts) for t in nm_targets] start_all_bg(nm_procs) while any_running(nm_procs): print("Nmap Scan running...") sleep(2) summarize(nm_procs) python-libnmap-0.7.3/libnmap/test/process-stressbox/proc_async.py000066400000000000000000000010501430422236200252770ustar00rootroot00000000000000#!/usr/bin/env python from time import sleep from libnmap.process import NmapProcess nmap_proc = NmapProcess(targets="scanme.nmap.org", options="-sT") nmap_proc.run_background() while nmap_proc.is_running(): nmaptask = nmap_proc.current_task if nmaptask: print( "Task {0} ({1}): ETC: {2} DONE: {3}%".format( nmaptask.name, nmaptask.status, nmaptask.etc, nmaptask.progress ) ) sleep(0.5) print("rc: {0} output: {1}".format(nmap_proc.rc, nmap_proc.summary)) print(nmap_proc.stdout) python-libnmap-0.7.3/libnmap/test/process-stressbox/proc_nmap_like.py000066400000000000000000000027711430422236200261340ustar00rootroot00000000000000#!/usr/bin/env python from libnmap.parser import NmapParser, NmapParserException from libnmap.process import NmapProcess # start a new nmap scan on localhost with some specific options def do_scan(targets, options): nm = NmapProcess(targets, options) rc = nm.run() if rc != 0: print("nmap scan failed: {0}".format(nm.stderr)) try: parsed = NmapParser.parse(nm.stdout) except NmapParserException as e: print("Exception raised while parsing scan: {0}".format(e.msg)) return parsed # print scan results from a nmap report def print_scan(nmap_report): print( "Starting Nmap {0} ( http://nmap.org ) at {1}".format( nmap_report._nmaprun["version"], nmap_report._nmaprun["startstr"] ) ) for host in nmap_report.hosts: if len(host.hostnames): tmp_host = host.hostnames.pop() else: tmp_host = host.address print("Nmap scan report for {0} ({1})".format(tmp_host, host.address)) print("Host is {0}.".format(host.status)) print(" PORT STATE SERVICE") for serv in host.services: pserv = "{0:>5s}/{1:3s} {2:12s} {3}".format( str(serv.port), serv.protocol, serv.state, serv.service ) if len(serv.banner): pserv += " ({0})".format(serv.banner) print(pserv) print(nmap_report.summary) if __name__ == "__main__": report = do_scan("127.0.0.1", "-sV") print_scan(report) python-libnmap-0.7.3/libnmap/test/process-stressbox/stop_scan.py000066400000000000000000000011231430422236200251310ustar00rootroot00000000000000#!/usr/bin/env python from time import sleep from libnmap.process import NmapProcess nmap_proc = NmapProcess(targets="scanme.nmap.org", options="-sV") nmap_proc.run_background() while nmap_proc.is_running(): nmaptask = nmap_proc.current_task if nmaptask: print( "Task {0} ({1}): ETC: {2} DONE: {3}%".format( nmaptask.name, nmaptask.status, nmaptask.etc, nmaptask.progress ) ) sleep(3) nmap_proc.stop() print("rc: {0} output: {1}".format(nmap_proc.rc, nmap_proc.summary)) print(nmap_proc.stdout) print(nmap_proc.stderr) python-libnmap-0.7.3/libnmap/test/process-stressbox/stressback.py000066400000000000000000000013551430422236200253130ustar00rootroot00000000000000#!/usr/bin/env python from time import sleep from libnmap.process import NmapProcess def make_nmproc_obj(targets, options): return NmapProcess(targets=targets, options=options) def start_all_bg(nmprocs): for nmp in nmprocs: nmp.run_background() def any_running(nmprocs): return any([nmp.is_running() for nmp in nmprocs]) def summarize(nmprocs): for nmp in nmprocs: print("rc: {0} output: {1}".format(nmp.rc, len(nmp.stdout))) print(nmp.stdout) nb_targets = 10 nm_target = "localhost" nm_opts = "-sP" nm_targets = [nm_target for i in range(nb_targets)] nm_procs = [make_nmproc_obj(t, nm_opts) for t in nm_targets] start_all_bg(nm_procs) while any_running(nm_procs): sleep(5) summarize(nm_procs) python-libnmap-0.7.3/libnmap/test/process-stressbox/stresstest.py000066400000000000000000000006311430422236200253660ustar00rootroot00000000000000#!/usr/bin/env python from libnmap.parser import NmapParser, NmapParserException from libnmap.process import NmapProcess nm = NmapProcess("127.0.0.1", "-sP") rc = nm.run() if rc != 0: print("nmap scan failed: {0}".format(nm.stderr)) try: report = NmapParser.parse(nm.stdout) except NmapParserException as e: print("Exception raised while parsing scan: {0}".format(e.msg)) print(len(nm.stdout)) python-libnmap-0.7.3/libnmap/test/test_backend_plugin_factory.py000066400000000000000000000124141430422236200251700ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- import os import unittest from libnmap.parser import NmapParser from libnmap.plugins.backendplugin import NmapBackendPlugin from libnmap.plugins.backendpluginFactory import BackendPluginFactory class TestNmapBackendPlugin(unittest.TestCase): """ This testing class will tests each plugins The following test need to be done : - test the factory - test all the method of the class NmapBackendPlugin: - Verify implmented/notImplemented - Verify the behaviour (ie insert must insert) To support a new plugin or a new way to instanciate a plugin, add a dict with the necessary parameter in the urls table define in setUp All testcase must loop thru theses urls to validate a plugins """ def setUp(self): fdir = os.path.dirname(os.path.realpath(__file__)) self.flist_full = [ {"file": "{0}/{1}".format(fdir, "files/2_hosts.xml"), "hosts": 2}, {"file": "{0}/{1}".format(fdir, "files/1_hosts.xml"), "hosts": 1}, { "file": "{0}/{1}".format( fdir, "files/1_hosts_banner_ports_notsyn.xml" ), "hosts": 1, }, { "file": "{0}/{1}".format( fdir, "files/1_hosts_banner_ports.xml" ), "hosts": 1, }, { "file": "{0}/{1}".format(fdir, "files/1_hosts_banner.xml"), "hosts": 1, }, { "file": "{0}/{1}".format(fdir, "files/2_hosts_version.xml"), "hosts": 2, }, { "file": "{0}/{1}".format(fdir, "files/2_tcp_hosts.xml"), "hosts": 2, }, { "file": "{0}/{1}".format(fdir, "files/1_hosts_nohostname.xml"), "hosts": 1, }, ] self.flist = self.flist_full # build a list of NmapReport self.reportList = [] for testfile in self.flist: fd = open(testfile["file"], "r") s = fd.read() fd.close() nrp = NmapParser.parse(s) self.reportList.append(nrp) self.urls = [ {"plugin_name": "mongodb"}, { "plugin_name": "sql", "url": "sqlite:////tmp/reportdb.sql", "echo": False, }, { "plugin_name": "sql", "url": "mysql+pymysql://root@localhost/poulet", "echo": False, }, ] def test_backend_factory(self): """test_factory BackendPluginFactory.create(**url) Invoke factory and test that the object is of the right classes """ for url in self.urls: backend = BackendPluginFactory.create(**url) self.assertEqual(isinstance(backend, NmapBackendPlugin), True) className = "Nmap%sPlugin" % url["plugin_name"].title() self.assertEqual(backend.__class__.__name__, className, True) def test_backend_insert(self): """test_insert best way to insert is to call save() of nmapreport :P """ for nrp in self.reportList: for url in self.urls: # create the backend factory object backend = BackendPluginFactory.create(**url) # save the report returncode = nrp.save(backend) # test return code self.assertNotEqual(returncode, None) def test_backend_get(self): """test_backend_get inset all report and save the returned id in a list then get each id and create a new list of report compare each report (assume eq) """ id_list = [] result_list = [] for url in self.urls: backend = BackendPluginFactory.create(**url) for nrp in self.reportList: id_list.append(nrp.save(backend)) for rep_id in id_list: result_list.append(backend.get(rep_id)) self.assertEqual(len(result_list), len(self.reportList)) self.assertEqual((result_list), (self.reportList)) id_list = [] result_list = [] def test_backend_getall(self): pass def test_backend_delete(self): """test_backend_delete inset all report and save the returned id in a list for each id remove the item and test if not present """ id_list = [] result_list = [] for url in self.urls: backend = BackendPluginFactory.create(**url) for nrp in self.reportList: id_list.append(nrp.save(backend)) for rep_id in id_list: result_list.append(backend.delete(rep_id)) self.assertEqual(backend.get(rep_id), None) id_list = [] result_list = [] if __name__ == "__main__": test_suite = [ "test_backend_factory", "test_backend_insert", "test_backend_get", "test_backend_getall", "test_backend_delete", ] suite = unittest.TestSuite(map(TestNmapBackendPlugin, test_suite)) test_result = unittest.TextTestRunner(verbosity=5).run(suite) python-libnmap-0.7.3/libnmap/test/test_cpe.py000066400000000000000000000056371430422236200212540ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- import unittest from libnmap.objects.os import CPE class TestNmapFP(unittest.TestCase): def setUp(self): self.cpelist = [ "cpe:/a:apache:http_server:2.2.22", "cpe:/a:heimdal:kerberos", "cpe:/a:openbsd:openssh:5.9p1", "cpe:/o:apple:iphone_os:5", "cpe:/o:apple:mac_os_x:10.8", "cpe:/o:apple:mac_os_x", "cpe:/o:linux:linux_kernel:2.6.13", "cpe:/o:linux:linux_kernel", "cpe:/o:microsoft:windows_7", "cpe:/o:microsoft:windows_7::-:professional", "cpe:/o:microsoft:windows_7::sp1", "cpe:/o:microsoft:windows", "cpe:/o:microsoft:windows_server_2008::beta3", "cpe:/o:microsoft:windows_server_2008", "cpe:/o:microsoft:windows_server_2008::sp1", "cpe:/o:microsoft:windows_vista::-", "cpe:/o:microsoft:windows_vista::sp1", "cpe:/o:microsoft:windows_vista::sp2", ] def test_cpe(self): apa = CPE(self.cpelist[0]) self.assertTrue(apa.is_application()) self.assertFalse(apa.is_hardware()) self.assertFalse(apa.is_operating_system()) self.assertEqual( apa.cpedict, { "cpe": "cpe", "edition": "", "language": "", "part": "/a", "product": "http_server", "update": "", "vendor": "apache", "version": "2.2.22", }, ) win = CPE(self.cpelist[12]) self.assertEqual(win.get_vendor(), "microsoft") self.assertEqual(win.get_product(), "windows_server_2008") self.assertEqual(win.get_version(), "") self.assertEqual(win.get_update(), "beta3") self.assertEqual(win.get_edition(), "") self.assertEqual(win.get_language(), "") def test_full_cpe(self): cpestr = "cpe:/a:mozilla:firefox:2.0::osx:es-es" resdict = { "part": "/a", "vendor": "mozilla", "product": "firefox", "version": "2.0", "update": "", "edition": "osx", "language": "es-es", } ocpe = CPE(cpestr) objdict = { "part": ocpe.get_part(), "vendor": ocpe.get_vendor(), "product": ocpe.get_product(), "version": ocpe.get_version(), "update": ocpe.get_update(), "language": ocpe.get_language(), "edition": ocpe.get_edition(), } self.assertEqual(objdict, resdict) # self.assertEqual(ocpe.cpedict, resdict) self.assertEqual(str(ocpe), cpestr) if __name__ == "__main__": test_suite = ["test_cpe", "test_full_cpe"] suite = unittest.TestSuite(map(TestNmapFP, test_suite)) test_result = unittest.TextTestRunner(verbosity=2).run(suite) python-libnmap-0.7.3/libnmap/test/test_defusedxml.py000066400000000000000000000043061430422236200226350ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- import os import sys import unittest from libnmap.parser import NmapParser, NmapParserException class TestDefusedXML(unittest.TestCase): def setUp(self): if int(sys.version[0]) == 3: self._assertRaisesRegex = self.assertRaisesRegex else: self._assertRaisesRegex = self.assertRaisesRegexp self.billionlaugh = """ ]> &lol9; """ self.fdir = os.path.dirname(os.path.realpath(__file__)) self.billionlaugh_file = "{0}/files/{1}".format( self.fdir, "billion_laugh.xml" ) self.external_entities_file = "{0}/files/{1}".format( self.fdir, "defused_et_local_includer.xml" ) def test_billion_laugh(self): self._assertRaisesRegex( NmapParserException, ".*EntitiesForbidden", NmapParser.parse_fromstring, self.billionlaugh, ) def test_external_entities(self): self._assertRaisesRegex( NmapParserException, ".*EntitiesForbidden", NmapParser.parse_fromfile, self.external_entities_file, ) if __name__ == "__main__": # test_suite = ["test_external_entities"] test_suite = ["test_billion_laugh", "test_external_entities"] suite = unittest.TestSuite(map(TestDefusedXML, test_suite)) test_result = unittest.TextTestRunner(verbosity=2).run(suite) python-libnmap-0.7.3/libnmap/test/test_fp.py000066400000000000000000000315231430422236200211030ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- import os import unittest from libnmap.parser import NmapParser class TestNmapFP(unittest.TestCase): def setUp(self): fdir = os.path.dirname(os.path.realpath(__file__)) self.flist_full = [ { "file": "%s/%s" % (fdir, "files/1_os_banner_scripts.xml"), "os": 1, }, {"file": "%s/%s" % (fdir, "files/2_hosts_version.xml"), "os": 1}, { "file": "%s/%s" % (fdir, "files/1_hosts_banner_ports_notsyn.xml"), "os": 0, }, {"file": "%s/%s" % (fdir, "files/1_hosts_banner.xml"), "os": 0}, {"file": "%s/%s" % (fdir, "files/1_hosts_down.xml"), "os": 0}, ] self.flist = self.flist_full self.flist_os = { "nv6": {"file": "%s/%s" % (fdir, "files/full_sudo6.xml"), "os": 0}, "fullscan": { "file": "%s/%s" % (fdir, "files/fullscan.xml"), "os": 0, }, "nv5": {"file": "%s/%s" % (fdir, "files/os_scan5.xml"), "os": 0}, } self.fos_class_probabilities = "{0}/{1}".format( fdir, "files/test_osclass.xml" ) def test_fp(self): for file_e in self.flist_full: rep = NmapParser.parse_fromfile(file_e["file"]) for _host in rep.hosts: if file_e["os"] != 0: self.assertTrue(_host.os_fingerprinted) elif file_e["os"] == 0: self.assertFalse(_host.os_fingerprinted) else: raise Exception def test_osclasses_new(self): oclines = [ [ [ { "type": "general purpose", "accuracy": 100, "vendor": "Apple", "osfamily": "Mac OS X", "osgen": "10.8.X", }, { "type": "phone", "accuracy": 100, "vendor": "Apple", "osfamily": "iOS", "osgen": "5.X", }, { "type": "media device", "accuracy": 100, "vendor": "Apple", "osfamily": "iOS", "osgen": "5.X", }, ] ], [ [ { "type": "general purpose", "accuracy": 100, "vendor": "Microsoft", "osfamily": "Windows", "osgen": "2008", } ], [ { "type": "general purpose", "accuracy": 100, "vendor": "Microsoft", "osfamily": "Windows", "osgen": "7", } ], [ { "type": "phone", "accuracy": 100, "vendor": "Microsoft", "osfamily": "Windows", "osgen": "Phone", } ], [ { "type": "general purpose", "accuracy": 100, "vendor": "Microsoft", "osfamily": "Windows", "osgen": "Vista", }, { "type": "general purpose", "accuracy": 100, "vendor": "Microsoft", "osfamily": "Windows", "osgen": "2008", }, { "type": "general purpose", "accuracy": 100, "vendor": "Microsoft", "osfamily": "Windows", "osgen": "7", }, ], [ { "type": "general purpose", "accuracy": 100, "vendor": "Microsoft", "osfamily": "Windows", "osgen": "Vista", }, { "type": "general purpose", "accuracy": 100, "vendor": "Microsoft", "osfamily": "Windows", "osgen": "7", }, { "type": "general purpose", "accuracy": 100, "vendor": "Microsoft", "osfamily": "Windows", "osgen": "2008", }, ], ], ] rep = NmapParser.parse_fromfile(self.flist_os["nv6"]["file"]) hlist = [] hlist.append(rep.hosts.pop()) hlist.append(rep.hosts.pop()) i = 0 j = 0 k = 0 for h in hlist: for om in h.os.osmatches: for oc in om.osclasses: tdict = { "type": oc.type, "accuracy": oc.accuracy, "vendor": oc.vendor, "osfamily": oc.osfamily, "osgen": oc.osgen, } self.assertEqual(oclines[i][j][k], tdict) k += 1 j += 1 k = 0 j = 0 i += 1 def test_osmatches_new(self): rep = NmapParser.parse_fromfile(self.flist_os["nv6"]["file"]) hlist = [] hlist.append(rep.hosts.pop()) hlist.append(rep.hosts.pop()) baseline = [ [ { "line": 6014, "accuracy": 100, "name": "Apple Mac OS X 10.8 - 10.8.1 (Mountain Lion) (Darwin 12.0.0 - 12.1.0) or iOS 5.0.1", } ], [ { "line": 52037, "accuracy": 100, "name": "Microsoft Windows Server 2008 Beta 3", }, { "line": 52938, "accuracy": 100, "name": "Microsoft Windows 7 Professional", }, { "line": 54362, "accuracy": 100, "name": "Microsoft Windows Phone 7.5", }, { "line": 54897, "accuracy": 100, "name": "Microsoft Windows Vista SP0 or SP1, Windows Server 2008 SP1, or Windows 7", }, { "line": 55210, "accuracy": 100, "name": "Microsoft Windows Vista SP2, Windows 7 SP1, or Windows Server 2008", }, ], ] i = 0 j = 0 for h in hlist: for om in h.os.osmatches: tdict = { "line": om.line, "accuracy": om.accuracy, "name": om.name, } self.assertEqual(baseline[i][j], tdict) j += 1 j = 0 i += 1 def test_osmatches_old(self): rep = NmapParser.parse_fromfile(self.flist_os["nv5"]["file"]) h1 = rep.hosts[4] h1osmatches = [ { "line": -1, "accuracy": 95, "name": "general purpose:Linux:Linux", }, {"line": -1, "accuracy": 90, "name": "WAP:Gemtek:embedded"}, { "line": -1, "accuracy": 89, "name": "general purpose:Nokia:Linux", }, {"line": -1, "accuracy": 88, "name": "webcam:AXIS:Linux"}, ] j = 0 for om in h1.os.osmatches: tdict = {"line": om.line, "accuracy": om.accuracy, "name": om.name} self.assertEqual(h1osmatches[j], tdict) j += 1 def test_fpv6(self): fpval = "OS:SCAN(V=6.40-2%E=4%D=5/9%OT=88%CT=%CU=%PV=Y%DS=0%DC=L%G=N%TM=536BFF2F%P=x\nOS:86_64-apple-darwin10.8.0)SEQ(SP=F9%GCD=1%ISR=103%TI=RD%TS=A)OPS(O1=M3FD8\nOS:NW4NNT11SLL%O2=M3FD8NW4NNT11SLL%O3=M3FD8NW4NNT11%O4=M3FD8NW4NNT11SLL%O5=\nOS:M3FD8NW4NNT11SLL%O6=M3FD8NNT11SLL)WIN(W1=FFFF%W2=FFFF%W3=FFFF%W4=FFFF%W5\nOS:=FFFF%W6=FFFF)ECN(R=Y%DF=Y%TG=40%W=FFFF%O=M3FD8NW4SLL%CC=N%Q=)T1(R=Y%DF=\nOS:Y%TG=40%S=O%A=S+%F=AS%RD=0%Q=)T2(R=N)T3(R=N)T4(R=Y%DF=Y%TG=40%W=0%S=A%A=\nOS:Z%F=R%O=%RD=0%Q=)U1(R=N)IE(R=N)\n" fparray = [ "OS:SCAN(V=6.40-2%E=4%D=5/9%OT=88%CT=%CU=%PV=Y%DS=0%DC=L%G=N%TM=536BFF2F%P=x\nOS:86_64-apple-darwin10.8.0)SEQ(SP=F9%GCD=1%ISR=103%TI=RD%TS=A)OPS(O1=M3FD8\nOS:NW4NNT11SLL%O2=M3FD8NW4NNT11SLL%O3=M3FD8NW4NNT11%O4=M3FD8NW4NNT11SLL%O5=\nOS:M3FD8NW4NNT11SLL%O6=M3FD8NNT11SLL)WIN(W1=FFFF%W2=FFFF%W3=FFFF%W4=FFFF%W5\nOS:=FFFF%W6=FFFF)ECN(R=Y%DF=Y%TG=40%W=FFFF%O=M3FD8NW4SLL%CC=N%Q=)T1(R=Y%DF=\nOS:Y%TG=40%S=O%A=S+%F=AS%RD=0%Q=)T2(R=N)T3(R=N)T4(R=Y%DF=Y%TG=40%W=0%S=A%A=\nOS:Z%F=R%O=%RD=0%Q=)U1(R=N)IE(R=N)\n" ] rep = NmapParser.parse_fromfile(self.flist_os["nv6"]["file"]) h1 = rep.hosts.pop() self.assertEqual(h1.os.fingerprint, fpval) self.assertEqual(h1.os.fingerprints, fparray) def test_fpv5(self): fpval = "OS:SCAN(V=5.21%D=5/8%OT=22%CT=1%CU=37884%PV=Y%DS=0%DC=L%G=Y%TM=536BFE32%P=x\nOS:86_64-unknown-linux-gnu)SEQ(SP=100%GCD=1%ISR=106%TI=Z%CI=Z%II=I%TS=8)SEQ\nOS:(SP=101%GCD=1%ISR=107%TI=Z%CI=Z%II=I%TS=8)OPS(O1=M400CST11NW3%O2=M400CST\nOS:11NW3%O3=M400CNNT11NW3%O4=M400CST11NW3%O5=M400CST11NW3%O6=M400CST11)WIN(\nOS:W1=8000%W2=8000%W3=8000%W4=8000%W5=8000%W6=8000)ECN(R=Y%DF=Y%T=40%W=8018\nOS:%O=M400CNNSNW3%CC=Y%Q=)T1(R=Y%DF=Y%T=40%S=O%A=S+%F=AS%RD=0%Q=)T2(R=N)T3(\nOS:R=Y%DF=Y%T=40%W=8000%S=O%A=S+%F=AS%O=M400CST11NW3%RD=0%Q=)T4(R=Y%DF=Y%T=\nOS:40%W=0%S=A%A=Z%F=R%O=%RD=0%Q=)T5(R=Y%DF=Y%T=40%W=0%S=Z%A=S+%F=AR%O=%RD=0\nOS:%Q=)T6(R=Y%DF=Y%T=40%W=0%S=A%A=Z%F=R%O=%RD=0%Q=)T7(R=Y%DF=Y%T=40%W=0%S=Z\nOS:%A=S+%F=AR%O=%RD=0%Q=)U1(R=Y%DF=N%T=40%IPL=164%UN=0%RIPL=G%RID=G%RIPCK=G\nOS:%RUCK=G%RUD=G)IE(R=Y%DFI=N%T=40%CD=S)\n" fparray = [ "OS:SCAN(V=5.21%D=5/8%OT=22%CT=1%CU=37884%PV=Y%DS=0%DC=L%G=Y%TM=536BFE32%P=x\nOS:86_64-unknown-linux-gnu)SEQ(SP=100%GCD=1%ISR=106%TI=Z%CI=Z%II=I%TS=8)SEQ\nOS:(SP=101%GCD=1%ISR=107%TI=Z%CI=Z%II=I%TS=8)OPS(O1=M400CST11NW3%O2=M400CST\nOS:11NW3%O3=M400CNNT11NW3%O4=M400CST11NW3%O5=M400CST11NW3%O6=M400CST11)WIN(\nOS:W1=8000%W2=8000%W3=8000%W4=8000%W5=8000%W6=8000)ECN(R=Y%DF=Y%T=40%W=8018\nOS:%O=M400CNNSNW3%CC=Y%Q=)T1(R=Y%DF=Y%T=40%S=O%A=S+%F=AS%RD=0%Q=)T2(R=N)T3(\nOS:R=Y%DF=Y%T=40%W=8000%S=O%A=S+%F=AS%O=M400CST11NW3%RD=0%Q=)T4(R=Y%DF=Y%T=\nOS:40%W=0%S=A%A=Z%F=R%O=%RD=0%Q=)T5(R=Y%DF=Y%T=40%W=0%S=Z%A=S+%F=AR%O=%RD=0\nOS:%Q=)T6(R=Y%DF=Y%T=40%W=0%S=A%A=Z%F=R%O=%RD=0%Q=)T7(R=Y%DF=Y%T=40%W=0%S=Z\nOS:%A=S+%F=AR%O=%RD=0%Q=)U1(R=Y%DF=N%T=40%IPL=164%UN=0%RIPL=G%RID=G%RIPCK=G\nOS:%RUCK=G%RUD=G)IE(R=Y%DFI=N%T=40%CD=S)\n" ] rep = NmapParser.parse_fromfile(self.flist_os["nv5"]["file"]) h1 = rep.hosts[4] self.assertEqual(h1.os.fingerprint, fpval) self.assertEqual(h1.os.fingerprints, fparray) def test_cpeservice(self): cpelist = ["cpe:/a:openbsd:openssh:5.9p1", "cpe:/o:linux:linux_kernel"] rep = NmapParser.parse_fromfile(self.flist_os["fullscan"]["file"]) h1 = rep.hosts.pop() s = h1.services[0] self.assertEqual(s.cpelist[0].cpestring, cpelist[0]) self.assertEqual(s.cpelist[1].cpestring, cpelist[1]) def test_os_class_probabilities(self): p = NmapParser.parse_fromfile(self.fos_class_probabilities) h = p.hosts.pop() osc = h.os_class_probabilities().pop() self.assertEqual(osc.type, "general purpose") self.assertEqual(osc.vendor, "Linux") self.assertEqual(osc.osfamily, "Linux") self.assertEqual(osc.osgen, "3.X") self.assertEqual(osc.accuracy, 100) # cpe:/o:linux:linux_kernel:3 if __name__ == "__main__": test_suite = [ "test_fp", "test_fpv6", "test_osmatches_new", "test_osclasses_new", "test_fpv5", "test_osmatches_old", "test_cpeservice", "test_os_class_probabilities", ] suite = unittest.TestSuite(map(TestNmapFP, test_suite)) test_result = unittest.TextTestRunner(verbosity=2).run(suite) python-libnmap-0.7.3/libnmap/test/test_host.py000066400000000000000000000210351430422236200214500ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- import os import unittest from libnmap.parser import NmapParser host1 = """
""" host2 = """
""" host3 = """
""" host4 = """
""" class TestNmapHost(unittest.TestCase): def setUp(self): self.fdir = os.path.dirname(os.path.realpath(__file__)) self.dionaea_path = "{0}/files/dionaea_scan.xml".format(self.fdir) def test_eq_host(self): h1 = NmapParser.parse(host1) h2 = NmapParser.parse(host2) h3 = NmapParser.parse(host3) h4 = NmapParser.parse(host4) self.assertNotEqual(h1, h2) self.assertEqual(h1, h1) self.assertNotEqual(h1, h3) self.assertEqual(h1, h4) self.assertNotEqual(h2, h3) def test_host_api(self): h = NmapParser.parse(host2) self.assertEqual(h.starttime, "1361738318") self.assertEqual(h.endtime, "13617386177") self.assertEqual(h.address, "127.0.0.1") self.assertEqual(h.status, "up") self.assertEqual(h.hostnames, ["localhost", "localhost", "localhost2"]) h2 = NmapParser.parse(host3) self.assertEqual(len(h2.services), 5) self.assertEqual(len(h2.get_ports()), 5) self.assertEqual(len(h2.get_open_ports()), 3) self.assertEqual(h2.get_service(22, "tcp").state, "open") def test_extra_ports(self): h1 = NmapParser.parse(host1) h2 = NmapParser.parse(host2) self.assertEqual( h1.extraports_state["state"], {"count": "995", "state": "WILLY_WONCKA"}, ) self.assertEqual( h1.extraports_reasons, [{"reason": "conn-refused", "count": "995"}] ) self.assertEqual( h2.extraports_state["state"], {"count": "995", "state": "closed"} ) self.assertEqual( h2.extraports_reasons, [{"reason": "conn-refused", "count": "995"}] ) def test_diff_host(self): h1 = NmapParser.parse(host1) h2 = NmapParser.parse(host2) h3 = NmapParser.parse(host3) c1 = h1.diff(h2) c2 = h1.diff(h3) c3 = h2.diff(h3) self.assertEqual(c1.changed(), set(["hostnames"])) self.assertEqual(c1.added(), set([])) self.assertEqual(c1.removed(), set([])) self.assertEqual( c1.unchanged(), set( [ "status", "NmapService::tcp.22", "NmapService::tcp.111", "NmapService::tcp.631", "NmapService::tcp.3306", "address", "NmapService::tcp.25", "mac_addr", ] ), ) self.assertEqual( c2.changed(), set(["status", "NmapService::tcp.3306"]) ) self.assertEqual(c2.added(), set(["NmapService::tcp.25"])) self.assertEqual(c2.removed(), set(["NmapService::tcp.3307"])) self.assertEqual( c2.unchanged(), set( [ "NmapService::tcp.631", "hostnames", "NmapService::tcp.22", "NmapService::tcp.111", "address", "mac_addr", ] ), ) self.assertEqual( c3.changed(), set(["status", "hostnames", "NmapService::tcp.3306"]) ) self.assertEqual(c3.added(), set(["NmapService::tcp.25"])) self.assertEqual(c3.removed(), set(["NmapService::tcp.3307"])) self.assertEqual( c3.unchanged(), set( [ "NmapService::tcp.631", "NmapService::tcp.22", "NmapService::tcp.111", "address", "mac_addr", ] ), ) if __name__ == "__main__": test_suite = ["test_eq_host", "test_host_api", "test_diff_host"] suite = unittest.TestSuite(map(TestNmapHost, test_suite)) test_result = unittest.TextTestRunner(verbosity=2).run(suite) python-libnmap-0.7.3/libnmap/test/test_new_parser.py000066400000000000000000000015771430422236200226510ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- import unittest from libnmap.parser import NmapParser, NmapParserException baddatalist = [ "aaa", None, "", 123, "ports/>>>", "", "", "", ] class TestNmapParser(unittest.TestCase): def test_parse(self): for baddata in baddatalist: self.assertRaises( NmapParserException, NmapParser.parse, baddata, "zz" ) self.assertRaises( NmapParserException, NmapParser.parse, baddata, "XML" ) self.assertRaises( NmapParserException, NmapParser.parse, baddata, "YAML" ) if __name__ == "__main__": test_suite = ["test_parse"] suite = unittest.TestSuite(map(TestNmapParser, test_suite)) test_result = unittest.TextTestRunner(verbosity=2).run(suite) python-libnmap-0.7.3/libnmap/test/test_parser.py000066400000000000000000000157731430422236200220030ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- import os import unittest from libnmap.parser import NmapParser, NmapParserException class TestNmapParser(unittest.TestCase): def setUp(self): fdir = os.path.dirname(os.path.realpath(__file__)) self.flist_full = [ {"file": "%s/%s" % (fdir, "files/2_hosts.xml"), "hosts": 2}, {"file": "%s/%s" % (fdir, "files/1_hosts.xml"), "hosts": 1}, { "file": "%s/%s" % (fdir, "files/1_hosts_banner_ports_notsyn.xml"), "hosts": 1, }, # {'file': "%s/%s" % (fdir, # 'files/1_hosts_banner_ports_xmas.xml'), # 'hosts': 1}, { "file": "%s/%s" % (fdir, "files/1_hosts_banner_ports.xml"), "hosts": 1, }, {"file": "%s/%s" % (fdir, "files/1_hosts_banner.xml"), "hosts": 1}, { "file": "%s/%s" % (fdir, "files/2_hosts_version.xml"), "hosts": 2, }, # {'file': "%s/%s" % (fdir, # 'files/2_null_hosts.xml'), 'hosts': 2}, {"file": "%s/%s" % (fdir, "files/2_tcp_hosts.xml"), "hosts": 2}, { "file": "%s/%s" % (fdir, "files/1_hosts_nohostname.xml"), "hosts": 1, }, ] self.flist = self.flist_full self.ports_string = """ """ self.ports_string2 = """ i """ self.port_string = """ """ self.port_string2 = """ """ self.port_string3 = "" self.port_string4 = "" self.port_string5 = "GINGERBREADMAN" self.port_string6 = """ """ self.port_string7 = """ """ self.port_string8 = """ """ self.port_string9 = """ """ def test_class_parser(self): for testfile in self.flist: fd = open(testfile["file"], "r") s = fd.read() fd.close() NmapParser.parse(s) def test_class_ports_parser(self): pdict = NmapParser.parse(self.ports_string) plist = pdict["ports"] self.assertEqual(len(plist), 4) self.assertEqual( sorted([p.port for p in plist]), sorted([22, 25, 9929, 80]) ) self.assertRaises(ValueError, NmapParser.parse, self.ports_string2) def test_class_port_parser(self): p = NmapParser.parse(self.port_string) self.assertEqual(p.port, 25) self.assertNotEqual(p.state, "open") self.assertEqual(p.state, "filtered") self.assertEqual(p.service, "smtp") self.assertEqual(p.reason, "admin-prohibited") self.assertEqual(p.reason_ttl, "253") self.assertEqual(p.reason_ip, "109.133.192.1") def test_port_except(self): self.assertRaises(ValueError, NmapParser.parse, self.port_string2) self.assertRaises( NmapParserException, NmapParser.parse, self.port_string3 ) self.assertRaises( NmapParserException, NmapParser.parse, self.port_string4 ) self.assertRaises( NmapParserException, NmapParser.parse, self.port_string5 ) self.assertRaises(ValueError, NmapParser.parse, self.port_string6) self.assertRaises( NmapParserException, NmapParser.parse, self.port_string7 ) self.assertRaises( NmapParserException, NmapParser.parse, self.port_string8 ) serv = NmapParser.parse(self.port_string9) self.assertEqual(serv.state, None) def test_parser_generic(self): plist = NmapParser.parse_fromstring(self.ports_string) for p in plist: print(p) if __name__ == "__main__": test_suite = [ "test_class_parser", "test_class_ports_parser", "test_class_port_parser", "test_port_except", "test_parser_generic", ] suite = unittest.TestSuite(map(TestNmapParser, test_suite)) test_result = unittest.TextTestRunner(verbosity=2).run(suite) python-libnmap-0.7.3/libnmap/test/test_process.py000066400000000000000000000124231430422236200221520ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- import os import sys import unittest from time import sleep from libnmap.objects.report import NmapReport from libnmap.parser import NmapParser from libnmap.process import NmapProcess class TestNmapProcess(unittest.TestCase): def setUp(self): if int(sys.version[0]) == 3: self._assertRaisesRegex = self.assertRaisesRegex else: self._assertRaisesRegex = self.assertRaisesRegexp self.fdir = os.path.dirname(os.path.realpath(__file__)) def test_check_valid_targets(self): valid_target_tests = [ {"value": "127.0.0.1, 1.1.1.1, 2.20.202", "size": 3}, {"value": ["127.0.0.1", "1.1.1.1", "2.20.202.2"], "size": 3}, {"value": [" 127.0.0.1", " 1.1.1.1"], "size": 2}, {"value": " 127.0.0.1, 1.1.1.1 , a", "size": 3}, {"value": ["192.168.10.0/24", "192.168.0-255.1-254"], "size": 2}, {"value": ["fe80::a8bb:ccff:fedd:eeff%eth0"], "size": 1}, {"value": ["my-domain.com", "my-num3r1c-domain.com"], "size": 2}, ] for vtarget in valid_target_tests: nmapobj = NmapProcess(targets=vtarget["value"], options="-sP") self.assertEqual(vtarget["size"], len(nmapobj.targets)) def test_check_invalid_targets(self): invalid_target_type_tests = [{"a": "bba"}, 5] invalid_target_character_tests = ["1.1.1.1$", "invalid_domain.com"] invalid_target_dash_tests = ["-invalid-target", "--option"] for vtarget in invalid_target_type_tests: self._assertRaisesRegex( Exception, "Supplied target list should be either a string or a list", NmapProcess, targets=vtarget, options="-sP", ) for vtarget in invalid_target_character_tests: self._assertRaisesRegex( Exception, "contains invalid characters", NmapProcess, targets=vtarget, options="-sP", ) for vtarget in invalid_target_dash_tests: self._assertRaisesRegex( Exception, "cannot begin or end with a dash", NmapProcess, targets=vtarget, options="-sP", ) def test_nmap_options(self): invalid_options = ["--iflist"] for invalid_option in invalid_options: self._assertRaisesRegex( Exception, "unsafe options activated while safe_mode is set True", NmapProcess, targets="127.0.0.1", options=invalid_option, ) def test_missing_binary(self): _path = os.environ["PATH"] os.environ["PATH"] = "/does_not_exists" self._assertRaisesRegex( EnvironmentError, "nmap is not installed or could not be found in system path", NmapProcess, targets="127.0.0.1", options="-sP", ) os.environ["PATH"] = _path def test_exec_env(self): self.assertRaises( EnvironmentError, NmapProcess, targets="127.0.0.1", options="-sV", fqp="/usr/bin/does-not-exists", ) def test_exec(self): nmapobj = NmapProcess(targets="127.0.0.1", options="-sP") rc = nmapobj.run() parsed = NmapParser.parse(nmapobj.stdout) self.assertEqual(rc, 0) self.assertGreater(len(nmapobj.stdout), 0) self.assertIsInstance(parsed, NmapReport) def test_sudo_exec(self): nmapobj = NmapProcess(targets="127.0.0.1", options="-sP") self._assertRaisesRegex( EnvironmentError, "Username.*does not exists", nmapobj.sudo_run, run_as="non-existing-user", ) self._assertRaisesRegex( EnvironmentError, "Username.*does not exists", nmapobj.sudo_run_background, run_as="non-existing-user", ) def test_exec_reportsize(self): def make_nmproc_obj(targets, options): return NmapProcess(targets=targets, options=options) def start_all(nmprocs): for nmp in nmprocs: nmp.run() nb_targets = 20 nm_target = "localhost" nm_opts = "-sT" nm_targets = [nm_target for i in range(nb_targets)] nm_procs = [make_nmproc_obj(t, nm_opts) for t in nm_targets] start_all(nm_procs) nm_procs = [make_nmproc_obj(t, nm_opts) for t in nm_targets] start_all(nm_procs) self.assertEqual(len(nm_procs), nb_targets) total_size = 0 for i in range(len(nm_procs)): total_size += len(nm_procs[i].stdout) average_size = int(total_size / len(nm_procs)) for nm in nm_procs: self.assertAlmostEqual( average_size, int(len(nm.stdout)), delta=200 ) if __name__ == "__main__": test_suite = [ "test_exec_env", "test_check_targets", "test_exec", "test_exec_reportsize", ] suite = unittest.TestSuite(map(TestNmapProcess, test_suite)) test_result = unittest.TextTestRunner(verbosity=2).run(suite) python-libnmap-0.7.3/libnmap/test/test_report.py000066400000000000000000000240021430422236200220030ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- import os import unittest from libnmap.diff import NmapDiffException # sys.path.append("".join([os.path.dirname(__file__), "/../"])) from libnmap.parser import NmapParser class TestNmapParser(unittest.TestCase): def setUp(self): fdir = os.path.dirname(os.path.realpath(__file__)) self.flist_full = [ {"file": "%s/%s" % (fdir, "files/2_hosts.xml"), "hosts": 2}, {"file": "%s/%s" % (fdir, "files/1_hosts.xml"), "hosts": 1}, { "file": "%s/%s" % (fdir, "files/1_hosts_banner_ports_notsyn.xml"), "hosts": 1, }, { "file": "%s/%s" % (fdir, "files/1_hosts_banner_ports.xml"), "hosts": 1, }, {"file": "%s/%s" % (fdir, "files/1_hosts_banner.xml"), "hosts": 1}, { "file": "%s/%s" % (fdir, "files/2_hosts_version.xml"), "hosts": 2, }, {"file": "%s/%s" % (fdir, "files/2_tcp_hosts.xml"), "hosts": 2}, { "file": "%s/%s" % (fdir, "files/1_hosts_nohostname.xml"), "hosts": 1, }, ] self.flist_one = [ { "file": "%s/%s" % (fdir, "files/1_hosts_nohostname.xml"), "hosts": 1, } ] self.flist_two = [ { "file": "%s/%s" % (fdir, "files/2_hosts.xml"), "hosts": 2, "elapsed": "134.36", "endtime": "1361738040", "summary": ( "Nmap done at Sun Feb 24 21:34:00 2013;" " 2 IP addresses (2 hosts up) scanned" " in 134.36 seconds" ), } ] self.hlist = [ {"hostname": "localhost", "ports": 5, "open": 5}, {"hostname": "localhost2", "ports": 4, "open": 2}, {"hostname": "scanme.nmap.org", "ports": 4, "open": 3}, {"hostname": "1.1.1.1", "ports": 2, "open": 0}, ] self.flist_banner = [ { "file": "%s/%s" % (fdir, "files/1_hosts_banner.xml"), "banner": { "631": "product: CUPS version: 1.4", "3306": "product: MySQL version: 5.1.61", "22": ( "product: OpenSSH version: 5.3" " extrainfo: protocol 2.0" ), "25": ( "product: Postfix smtpd" " hostname: jambon.localdomain" ), "111": "", }, } ] self.flist = self.flist_full def test_report_constructor(self): for testfile in self.flist: fd = open(testfile["file"], "r") s = fd.read() fd.close() nr = NmapParser.parse(s) nr2 = NmapParser.parse(s) self.assertEqual(len(nr.hosts), testfile["hosts"]) self.assertEqual(len(nr2.hosts), testfile["hosts"]) self.assertEqual( sorted(nr2.get_raw_data()), sorted(nr.get_raw_data()) ) def test_get_ports(self): for testfile in self.flist: fd = open(testfile["file"], "r") s = fd.read() fd.close() nr = NmapParser.parse(s) for h in nr.hosts: for th in self.hlist: continue # TODO FIX THIS TEST # if th['hostname'] == h.hostnames[0]: # self.assertEqual(th['ports'], len(h.get_ports())) # self.assertEqual(th['open'], len(h.get_open_ports())) for np in h.get_open_ports(): sport = h.get_service(np[0], np[1]) self.assertEqual((sport.port, sport.protocol), np) def test_runstats(self): for testfile in self.flist_two: fd = open(testfile["file"], "r") s = fd.read() fd.close() nr = NmapParser.parse(s) self.assertEqual(getattr(nr, "endtime"), int(testfile["endtime"])) self.assertEqual(getattr(nr, "summary"), testfile["summary"]) self.assertEqual( getattr(nr, "elapsed"), float(testfile["elapsed"]) ) def test_banner(self): for testfile in self.flist_banner: fd = open(testfile["file"], "r") nr = NmapParser.parse(fd.read()) fd.close() for h in nr.hosts: for service in h.services: b = service.banner self.assertEqual(b, testfile["banner"][str(service.port)]) def test_service_equal(self): for testfile in self.flist: fd = open(testfile["file"], "r") np1 = NmapParser.parse(fd.read()) fd.close() fd = open(testfile["file"], "r") np2 = NmapParser.parse(fd.read()) fd.close() host1 = np1.hosts.pop() host2 = np2.hosts.pop() """All the service of the host must be compared and the hash should be also the same""" for i in range(len(host1.services)): self.assertEqual( hash(host1.services[i]), hash(host2.services[i]) ) self.assertEqual(host1.services[i], host2.services[i]) # print host1.serviceChanged(host2) def test_service_not_equal(self): for testfile in self.flist: fd = open(testfile["file"], "r") np1 = NmapParser.parse(fd.read()) fd.close() fd = open(testfile["file"], "r") np2 = NmapParser.parse(fd.read()) fd.close() host1 = np1.hosts.pop() host2 = np2.hosts.pop() for i in range(len(host1.services)): host1.services[i]._state["state"] = "changed" self.assertNotEqual(host1.services[i], host2.services[i]) # print "-----------" # print host1.serviceChanged(host2) # print "-----------" def test_host_not_equal(self): for testfile in self.flist: fd = open(testfile["file"], "r") np1 = NmapParser.parse(fd.read()) fd.close() fd = open(testfile["file"], "r") np2 = NmapParser.parse(fd.read()) fd.close() host1 = np1.hosts.pop() host2 = np2.hosts.pop() host1.address = {"addr": "1.3.3.7", "addrtype": "ipv4"} self.assertNotEqual(host1, host2) def test_host_equal(self): for testfile in self.flist: fd = open(testfile["file"], "r") np1 = NmapParser.parse(fd.read()) fd.close() fd = open(testfile["file"], "r") np2 = NmapParser.parse(fd.read()) fd.close() host1 = np1.hosts.pop() host2 = np2.hosts.pop() host1.services[0]._portid = "23" self.assertEqual(host1, host2) def test_host_address_changed(self): fdir = os.path.dirname(os.path.realpath(__file__)) fd1 = open("%s/%s" % (fdir, "files/1_hosts_down.xml"), "r") fd2 = open("%s/%s" % (fdir, "files/1_hosts.xml"), "r") nr1 = NmapParser.parse(fd1.read()) nr2 = NmapParser.parse(fd2.read()) h1 = nr1.hosts[0] h2 = nr2.hosts[0] self.assertRaises(NmapDiffException, h1.diff, h2) def test_host_address_unchanged(self): fdir = os.path.dirname(os.path.realpath(__file__)) fd1 = open("%s/%s" % (fdir, "files/1_hosts_down.xml"), "r") fd2 = open("%s/%s" % (fdir, "files/1_hosts.xml"), "r") fd3 = open("%s/%s" % (fdir, "files/1_hosts.xml"), "r") nr1 = NmapParser.parse(fd1.read()) nr2 = NmapParser.parse(fd2.read()) nr3 = NmapParser.parse(fd3.read()) h1 = nr1.hosts.pop() h2 = nr2.hosts.pop() h3 = nr3.hosts.pop() self.assertRaises(NmapDiffException, h1.diff, h2) self.assertEqual(h2.diff(h3).changed(), set([])) self.assertEqual(h2.diff(h3).added(), set([])) self.assertEqual(h2.diff(h3).removed(), set([])) self.assertEqual( h2.diff(h3).unchanged(), set( [ "status", "NmapService::tcp.22", "NmapService::tcp.111", "NmapService::tcp.631", "hostnames", "NmapService::tcp.3306", "address", "NmapService::tcp.25", "mac_addr", ] ), ) def test_diff_mac(self): fdir = os.path.dirname(os.path.realpath(__file__)) host_ping = "{0}/files/1_host_ping.xml".format(fdir) host_ping_mac_changed = ( "{0}/files/diff_1_host_ping_mac_changed.xml".format(fdir) ) report_mac_original = NmapParser.parse_fromfile(host_ping) report_mac_changed = NmapParser.parse_fromfile(host_ping_mac_changed) report_diff = report_mac_original.diff(report_mac_changed) self.assertEqual(report_diff.changed(), set(["NmapHost::172.28.1.3"])) host_original = report_mac_original.hosts[0] host_mac_changed = report_mac_changed.hosts[0] host_diff = host_original.diff(host_mac_changed) self.assertEqual(host_diff.changed(), set(["mac_addr"])) if __name__ == "__main__": test_suite = [ "test_report_constructor", "test_get_ports", "test_runstats", "test_banner", "test_service_equal", "test_service_not_equal", "test_host_not_equal", "test_host_equal", "test_host_address_changed", "test_host_address_unchanged", ] suite = unittest.TestSuite(map(TestNmapParser, test_suite)) test_result = unittest.TextTestRunner(verbosity=2).run(suite) python-libnmap-0.7.3/libnmap/test/test_report_diff.py000066400000000000000000000053621430422236200230030ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- import os import unittest from libnmap.parser import NmapParser class TestNmapReportDiff(unittest.TestCase): def setUp(self): fdir = os.path.dirname(os.path.realpath(__file__)) self.flist_full = [ {"file": "%s/%s" % (fdir, "files/2_hosts.xml"), "hosts": 2}, {"file": "%s/%s" % (fdir, "files/1_hosts.xml"), "hosts": 1}, ] self.flist = self.flist_full def test_diff_host_list(self): fdir = os.path.dirname(os.path.realpath(__file__)) r1 = NmapParser.parse_fromfile("%s/%s" % (fdir, "files/1_hosts.xml")) r2 = NmapParser.parse_fromfile("%s/%s" % (fdir, "files/2_hosts.xml")) r3 = NmapParser.parse_fromfile("%s/%s" % (fdir, "files/1_hosts.xml")) r4 = NmapParser.parse_fromfile( "%s/%s" % (fdir, "files/2_hosts_achange.xml") ) d1 = r1.diff(r2) self.assertEqual( d1.changed(), set( [ "hosts_total", "commandline", "hosts_up", "scan_type", "elapsed", ] ), ) self.assertEqual( d1.unchanged(), set(["hosts_down", "version", "NmapHost::127.0.0.1"]), ) self.assertEqual(d1.removed(), set(["NmapHost::74.207.244.221"])) d2 = r1.diff(r3) self.assertEqual(d2.changed(), set([])) self.assertEqual( d2.unchanged(), set( [ "hosts_total", "commandline", "hosts_up", "NmapHost::127.0.0.1", "elapsed", "version", "scan_type", "hosts_down", ] ), ) self.assertEqual(d2.added(), set([])) self.assertEqual(d2.removed(), set([])) d3 = r2.diff(r4) self.assertEqual(d3.changed(), set(["NmapHost::127.0.0.1"])) self.assertEqual( d3.unchanged(), set( [ "hosts_total", "commandline", "hosts_up", "NmapHost::74.207.244.221", "version", "elapsed", "scan_type", "hosts_down", ] ), ) self.assertEqual(d3.added(), set([])) self.assertEqual(d3.removed(), set([])) if __name__ == "__main__": test_suite = ["test_diff_host_list"] suite = unittest.TestSuite(map(TestNmapReportDiff, test_suite)) test_result = unittest.TextTestRunner(verbosity=2).run(suite) python-libnmap-0.7.3/libnmap/test/test_reportjson.py000066400000000000000000000026561430422236200227100ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- import json import os import unittest from libnmap.parser import NmapParser from libnmap.reportjson import ReportDecoder, ReportEncoder class TestReportJson(unittest.TestCase): def setUp(self): self.fdir = os.path.dirname(os.path.realpath(__file__)) self.xml_ref_file = "{0}/files/2_hosts.xml".format(self.fdir) self.json_ref_file = "{0}/files/2_hosts.json".format(self.fdir) def test_reportencode(self): nmap_report_obj = NmapParser.parse_fromfile(self.xml_ref_file) nmap_report_json = json.loads( json.dumps(nmap_report_obj, cls=ReportEncoder) ) with open(self.json_ref_file, "r") as fd: nmap_report_json_ref = json.load(fd) self.assertEqual(nmap_report_json_ref, nmap_report_json) def test_reportdecode(self): nmap_report_obj_ref = NmapParser.parse_fromfile(self.xml_ref_file) with open(self.json_ref_file, "r") as fd: nmap_report_json_ref = json.dumps(json.load(fd)) nmap_report_obj = json.loads( nmap_report_json_ref, cls=ReportDecoder ) self.assertEqual(nmap_report_obj_ref, nmap_report_obj) if __name__ == "__main__": test_suite = ["test_reportencode", "test_reportdecode"] suite = unittest.TestSuite(map(TestReportJson, test_suite)) test_result = unittest.TextTestRunner(verbosity=2).run(suite) python-libnmap-0.7.3/libnmap/test/test_service.py000066400000000000000000000225601430422236200221370ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- import os import unittest from libnmap.diff import NmapDiffException from libnmap.parser import NmapParser service1 = """ """ service2 = """ """ service3 = """ """ service4 = """ """ service5 = """ """ service6 = """ """ service7 = """ """ port_string = """ """ port_string_other2 = """ """ port_string_other3 = """ """ port_string_other4 = """ """ port_string_other5 = """ """ port_string_other6 = """ """ port_string_other7 = """ """ port_string_other8 = """ """ port_string_other9 = """ """ port_string_other10 = """ """ port_string_other11 = """ """ port_string_other12 = """ """ port_string_other13 = """ """ port_noservice = """ """ port_owner = """ """ port_tunnel = """ """ class TestNmapService(unittest.TestCase): def setUp(self): self.fdir = os.path.dirname(os.path.realpath(__file__)) self.s1 = NmapParser.parse(service1) self.s2 = NmapParser.parse(service2) self.s3 = NmapParser.parse(service3) self.s4 = NmapParser.parse(service4) self.s5 = NmapParser.parse(service5) self.s6 = NmapParser.parse(service6) self.s7 = NmapParser.parse(service7) def test_port_state_changed(self): nservice1 = NmapParser.parse(port_string) nservice2 = NmapParser.parse(port_string_other2) nservice3 = NmapParser.parse(port_string_other3) nservice4 = NmapParser.parse(port_string_other4) self.assertEqual(nservice1.diff(nservice2).changed(), set(["state"])) self.assertRaises(NmapDiffException, nservice1.diff, nservice3) self.assertRaises(NmapDiffException, nservice1.diff, nservice4) # self.assertRaises(NmapDiffException, nservice2.diff, nservice3) self.assertEqual( nservice3.diff(nservice4).changed(), set(["state", "service"]) ) def test_port_state_unchanged(self): nservice1 = NmapParser.parse(port_string) nservice2 = NmapParser.parse(port_string_other2) # nservice3 = NmapParser.parse(port_string_other3) # nservice4 = NmapParser.parse(port_string_other4) self.assertEqual( nservice1.diff(nservice2).unchanged(), set(["banner", "protocol", "port", "service", "id", "reason"]), ) def test_port_service_changed(self): nservice1 = NmapParser.parse(port_string) nservice2 = NmapParser.parse(port_string_other2) nservice4 = NmapParser.parse(port_string_other4) nservice5 = NmapParser.parse(port_string_other5) nservice8 = NmapParser.parse(port_string_other8) nservice9 = NmapParser.parse(port_string_other9) self.assertEqual(nservice1.diff(nservice2).changed(), set(["state"])) self.assertEqual(nservice5.diff(nservice4).changed(), set(["service"])) # banner changed self.assertEqual(nservice8.diff(nservice9).changed(), set(["banner"])) def test_eq_service(self): self.assertNotEqual(NmapDiffException, self.s1, self.s2) self.assertNotEqual(self.s1, self.s3) self.assertNotEqual(self.s1, self.s4) self.assertNotEqual(self.s5, self.s6) self.assertEqual(self.s6, self.s7) def test_diff_service(self): self.assertRaises(NmapDiffException, self.s1.diff, self.s2) self.assertRaises(NmapDiffException, self.s1.diff, self.s3) self.assertEqual(self.s1.diff(self.s4).changed(), set(["state"])) self.assertEqual( self.s1.diff(self.s4).unchanged(), set(["banner", "protocol", "port", "service", "id", "reason"]), ) self.assertEqual(self.s5.diff(self.s6).changed(), set(["banner"])) self.assertEqual(self.s6.diff(self.s6).changed(), set([])) def test_diff_reason(self): nservice12 = NmapParser.parse(port_string_other12) nservice13 = NmapParser.parse(port_string_other13) ddict = nservice12.diff(nservice13) self.assertEqual(ddict.changed(), set(["reason"])) def test_noservice(self): noservice = NmapParser.parse(port_noservice) self.assertEqual(noservice.service, "") def test_owner(self): serviceowner = NmapParser.parse(port_owner) self.assertEqual(serviceowner.owner, "edwige") def test_tunnel(self): servicetunnel = NmapParser.parse(port_tunnel) self.assertEqual(servicetunnel.tunnel, "ssl") def test_bannerdict(self): nmapreport = NmapParser.parse_fromfile( "{0}/files/dionaea_scan.xml".format(self.fdir) ) dhttp = nmapreport.hosts[0].get_service(80) dftp = nmapreport.hosts[0].get_service(21) self.assertEqual(dhttp.banner_dict, {"product": "nginx"}) self.assertEqual( dftp.banner_dict, { "product": "Synology DiskStation NAS ftpd", "devicetype": "storage-misc", }, ) if __name__ == "__main__": test_suite = [ "test_port_state_changed", "test_port_state_unchanged", "test_port_service_changed", "test_eq_service", "test_diff_service", ] suite = unittest.TestSuite(map(TestNmapService, test_suite)) test_result = unittest.TextTestRunner(verbosity=2).run(suite) python-libnmap-0.7.3/requirements-dev.txt000066400000000000000000000001211430422236200205120ustar00rootroot00000000000000black==20.8b1 defusedxml==0.6.0 isort==5.6.4 pre-commit pytest pytest-cov flake8 python-libnmap-0.7.3/setup.py000066400000000000000000000024561430422236200162010ustar00rootroot00000000000000# -*- coding: utf-8 -*- from distutils.core import setup with open("README.rst", encoding="utf-8") as rfile: long_description = rfile.read() setup( name="python-libnmap", version="0.7.3", author="Ronald Bister", author_email="mini.pelle@gmail.com", packages=["libnmap", "libnmap.plugins", "libnmap.objects"], url="http://pypi.python.org/pypi/python-libnmap/", extras_require={ "defusedxml": ["defusedxml>=0.6.0"], }, license="Apache 2.0", description=( "Python NMAP library enabling you to start async nmap tasks, " "parse and compare/diff scan results" ), long_description=long_description, classifiers=[ "License :: OSI Approved :: Apache Software License", "Development Status :: 5 - Production/Stable", "Environment :: Console", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Topic :: System :: Networking", ], ) python-libnmap-0.7.3/tox.ini000066400000000000000000000016101430422236200157710ustar00rootroot00000000000000[tox] envlist = py27, py32, py38, flake8, pycodestyle, formatting, defusedxml, coveralls [testenv] deps=pytest pytest-cov commands=pytest --cov --cov-report term-missing --ignore=libnmap/test/test_backend_plugin_factory.py --ignore=libnmap/test/test_defusedxml.py [testenv:defusedxml] deps=pytest defusedxml commands=pytest --ignore=libnmap/test/test_backend_plugin_factory.py [testenv:dbbackend] deps=pytest pymongo sqlalchemy pymysql commands=pytest --ignore=libnmap/test/test_defusedxml.py [testenv:flake8] deps = flake8 commands = flake8 --exclude test,docs,examples,.tox . [testenv:pycodestyle] deps = pycodestyle commands = pycodestyle --exclude test,docs,examples,.tox . [testenv:formatting] deps = #black==20.8b1 black isort commands = black --check -l 79 --exclude="venv|.tox" . isort --check-only -m 3 -l 79 --profile=black .