mirror of https://github.com/yandex/gixy
Initial commit
commit
2ff6ac1f8b
|
@ -0,0 +1,62 @@
|
|||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
env/
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*,cover
|
||||
cover
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
venv/
|
||||
venv3/
|
||||
.idea/
|
|
@ -0,0 +1,4 @@
|
|||
The following authors have created the source code of "Gixy"
|
||||
published and distributed by YANDEX LLC as the owner:
|
||||
|
||||
Andrew Krasichkov <buglloc@yandex-team.ru>
|
|
@ -0,0 +1,8 @@
|
|||
Contributions to Gixy are always welcome! You can help us in different ways:
|
||||
* Open an issue with suggestions for improvements and errors you're facing;
|
||||
* Fork this repository and submit a pull request;
|
||||
* Improve the documentation.
|
||||
|
||||
# Code guidelines:
|
||||
* Python code style should follow [PEP8 standards][pep8] standards whenever possible;
|
||||
* Pull requests with new plugins must contain unit tests for it.
|
|
@ -0,0 +1,355 @@
|
|||
(C) YANDEX LLC, 2017
|
||||
|
||||
Mozilla Public License Version 2.0
|
||||
==================================
|
||||
|
||||
1. Definitions
|
||||
--------------
|
||||
|
||||
1.1. "Contributor"
|
||||
means each individual or legal entity that creates, contributes to
|
||||
the creation of, or owns Covered Software.
|
||||
|
||||
1.2. "Contributor Version"
|
||||
means the combination of the Contributions of others (if any) used
|
||||
by a Contributor and that particular Contributor's Contribution.
|
||||
|
||||
1.3. "Contribution"
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
1.4. "Covered Software"
|
||||
means Source Code Form to which the initial Contributor has attached
|
||||
the notice in Exhibit A, the Executable Form of such Source Code
|
||||
Form, and Modifications of such Source Code Form, in each case
|
||||
including portions thereof.
|
||||
|
||||
1.5. "Incompatible With Secondary Licenses"
|
||||
means
|
||||
|
||||
(a) that the initial Contributor has attached the notice described
|
||||
in Exhibit B to the Covered Software; or
|
||||
|
||||
(b) that the Covered Software was made available under the terms of
|
||||
version 1.1 or earlier of the License, but not also under the
|
||||
terms of a Secondary License.
|
||||
|
||||
1.6. "Executable Form"
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
1.7. "Larger Work"
|
||||
means a work that combines Covered Software with other material, in
|
||||
a separate file or files, that is not Covered Software.
|
||||
|
||||
1.8. "License"
|
||||
means this document.
|
||||
|
||||
1.9. "Licensable"
|
||||
means having the right to grant, to the maximum extent possible,
|
||||
whether at the time of the initial grant or subsequently, any and
|
||||
all of the rights conveyed by this License.
|
||||
|
||||
1.10. "Modifications"
|
||||
means any of the following:
|
||||
|
||||
(a) any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered
|
||||
Software; or
|
||||
|
||||
(b) any new file in Source Code Form that contains any Covered
|
||||
Software.
|
||||
|
||||
1.11. "Patent Claims" of a Contributor
|
||||
means any patent claim(s), including without limitation, method,
|
||||
process, and apparatus claims, in any patent Licensable by such
|
||||
Contributor that would be infringed, but for the grant of the
|
||||
License, by the making, using, selling, offering for sale, having
|
||||
made, import, or transfer of either its Contributions or its
|
||||
Contributor Version.
|
||||
|
||||
1.12. "Secondary License"
|
||||
means either the GNU General Public License, Version 2.0, the GNU
|
||||
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||
Public License, Version 3.0, or any later versions of those
|
||||
licenses.
|
||||
|
||||
1.13. "Source Code Form"
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
1.14. "You" (or "Your")
|
||||
means an individual or a legal entity exercising rights under this
|
||||
License. For legal entities, "You" includes any entity that
|
||||
controls, is controlled by, or is under common control with You. For
|
||||
purposes of this definition, "control" means (a) the power, direct
|
||||
or indirect, to cause the direction or management of such entity,
|
||||
whether by contract or otherwise, or (b) ownership of more than
|
||||
fifty percent (50%) of the outstanding shares or beneficial
|
||||
ownership of such entity.
|
||||
|
||||
2. License Grants and Conditions
|
||||
--------------------------------
|
||||
|
||||
2.1. Grants
|
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
(a) under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or
|
||||
as part of a Larger Work; and
|
||||
|
||||
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||
for sale, have made, import, and otherwise transfer either its
|
||||
Contributions or its Contributor Version.
|
||||
|
||||
2.2. Effective Date
|
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution
|
||||
become effective for each Contribution on the date the Contributor first
|
||||
distributes such Contribution.
|
||||
|
||||
2.3. Limitations on Grant Scope
|
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under
|
||||
this License. No additional rights or licenses will be implied from the
|
||||
distribution or licensing of Covered Software under this License.
|
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||
Contributor:
|
||||
|
||||
(a) for any code that a Contributor has removed from Covered Software;
|
||||
or
|
||||
|
||||
(b) for infringements caused by: (i) Your and any other third party's
|
||||
modifications of Covered Software, or (ii) the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
|
||||
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||
its Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks,
|
||||
or logos of any Contributor (except as may be necessary to comply with
|
||||
the notice requirements in Section 3.4).
|
||||
|
||||
2.4. Subsequent Licenses
|
||||
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this
|
||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||
permitted under the terms of Section 3.3).
|
||||
|
||||
2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its
|
||||
Contributions are its original creation(s) or it has sufficient rights
|
||||
to grant the rights to its Contributions conveyed by this License.
|
||||
|
||||
2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under
|
||||
applicable copyright doctrines of fair use, fair dealing, or other
|
||||
equivalents.
|
||||
|
||||
2.7. Conditions
|
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||
in Section 2.1.
|
||||
|
||||
3. Responsibilities
|
||||
-------------------
|
||||
|
||||
3.1. Distribution of Source Form
|
||||
|
||||
All distribution of Covered Software in Source Code Form, including any
|
||||
Modifications that You create or to which You contribute, must be under
|
||||
the terms of this License. You must inform recipients that the Source
|
||||
Code Form of the Covered Software is governed by the terms of this
|
||||
License, and how they can obtain a copy of this License. You may not
|
||||
attempt to alter or restrict the recipients' rights in the Source Code
|
||||
Form.
|
||||
|
||||
3.2. Distribution of Executable Form
|
||||
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
(a) such Covered Software must also be made available in Source Code
|
||||
Form, as described in Section 3.1, and You must inform recipients of
|
||||
the Executable Form how they can obtain a copy of such Source Code
|
||||
Form by reasonable means in a timely manner, at a charge no more
|
||||
than the cost of distribution to the recipient; and
|
||||
|
||||
(b) You may distribute such Executable Form under the terms of this
|
||||
License, or sublicense it under different terms, provided that the
|
||||
license for the Executable Form does not attempt to limit or alter
|
||||
the recipients' rights in the Source Code Form under this License.
|
||||
|
||||
3.3. Distribution of a Larger Work
|
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for
|
||||
the Covered Software. If the Larger Work is a combination of Covered
|
||||
Software with a work governed by one or more Secondary Licenses, and the
|
||||
Covered Software is not Incompatible With Secondary Licenses, this
|
||||
License permits You to additionally distribute such Covered Software
|
||||
under the terms of such Secondary License(s), so that the recipient of
|
||||
the Larger Work may, at their option, further distribute the Covered
|
||||
Software under the terms of either this License or such Secondary
|
||||
License(s).
|
||||
|
||||
3.4. Notices
|
||||
|
||||
You may not remove or alter the substance of any license notices
|
||||
(including copyright notices, patent notices, disclaimers of warranty,
|
||||
or limitations of liability) contained within the Source Code Form of
|
||||
the Covered Software, except that You may alter any license notices to
|
||||
the extent required to remedy known factual inaccuracies.
|
||||
|
||||
3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of Covered
|
||||
Software. However, You may do so only on Your own behalf, and not on
|
||||
behalf of any Contributor. You must make it absolutely clear that any
|
||||
such warranty, support, indemnity, or liability obligation is offered by
|
||||
You alone, and You hereby agree to indemnify every Contributor for any
|
||||
liability incurred by such Contributor as a result of warranty, support,
|
||||
indemnity or liability terms You offer. You may include additional
|
||||
disclaimers of warranty and limitations of liability specific to any
|
||||
jurisdiction.
|
||||
|
||||
4. Inability to Comply Due to Statute or Regulation
|
||||
---------------------------------------------------
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this
|
||||
License with respect to some or all of the Covered Software due to
|
||||
statute, judicial order, or regulation then You must: (a) comply with
|
||||
the terms of this License to the maximum extent possible; and (b)
|
||||
describe the limitations and the code they affect. Such description must
|
||||
be placed in a text file included with all distributions of the Covered
|
||||
Software under this License. Except to the extent prohibited by statute
|
||||
or regulation, such description must be sufficiently detailed for a
|
||||
recipient of ordinary skill to be able to understand it.
|
||||
|
||||
5. Termination
|
||||
--------------
|
||||
|
||||
5.1. The rights granted under this License will terminate automatically
|
||||
if You fail to comply with any of its terms. However, if You become
|
||||
compliant, then the rights granted under this License from a particular
|
||||
Contributor are reinstated (a) provisionally, unless and until such
|
||||
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||
ongoing basis, if such Contributor fails to notify You of the
|
||||
non-compliance by some reasonable means prior to 60 days after You have
|
||||
come back into compliance. Moreover, Your grants from a particular
|
||||
Contributor are reinstated on an ongoing basis if such Contributor
|
||||
notifies You of the non-compliance by some reasonable means, this is the
|
||||
first time You have received notice of non-compliance with this License
|
||||
from such Contributor, and You become compliant prior to 30 days after
|
||||
Your receipt of the notice.
|
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions,
|
||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||
directly or indirectly infringes any patent, then the rights granted to
|
||||
You by any and all Contributors for the Covered Software under Section
|
||||
2.1 of this License shall terminate.
|
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||
end user license agreements (excluding distributors and resellers) which
|
||||
have been validly granted by You or Your distributors under this License
|
||||
prior to termination shall survive termination.
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 6. Disclaimer of Warranty *
|
||||
* ------------------------- *
|
||||
* *
|
||||
* Covered Software is provided under this License on an "as is" *
|
||||
* basis, without warranty of any kind, either expressed, implied, or *
|
||||
* statutory, including, without limitation, warranties that the *
|
||||
* Covered Software is free of defects, merchantable, fit for a *
|
||||
* particular purpose or non-infringing. The entire risk as to the *
|
||||
* quality and performance of the Covered Software is with You. *
|
||||
* Should any Covered Software prove defective in any respect, You *
|
||||
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||
* essential part of this License. No use of any Covered Software is *
|
||||
* authorized under this License except under this disclaimer. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 7. Limitation of Liability *
|
||||
* -------------------------- *
|
||||
* *
|
||||
* Under no circumstances and under no legal theory, whether tort *
|
||||
* (including negligence), contract, or otherwise, shall any *
|
||||
* Contributor, or anyone who distributes Covered Software as *
|
||||
* permitted above, be liable to You for any direct, indirect, *
|
||||
* special, incidental, or consequential damages of any character *
|
||||
* including, without limitation, damages for lost profits, loss of *
|
||||
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||
* and all other commercial damages or losses, even if such party *
|
||||
* shall have been informed of the possibility of such damages. This *
|
||||
* limitation of liability shall not apply to liability for death or *
|
||||
* personal injury resulting from such party's negligence to the *
|
||||
* extent applicable law prohibits such limitation. Some *
|
||||
* jurisdictions do not allow the exclusion or limitation of *
|
||||
* incidental or consequential damages, so this exclusion and *
|
||||
* limitation may not apply to You. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
8. Litigation
|
||||
-------------
|
||||
|
||||
Any litigation relating to this License may be brought only in the
|
||||
courts of a jurisdiction where the defendant maintains its principal
|
||||
place of business and such litigation shall be governed by laws of that
|
||||
jurisdiction, without reference to its conflict-of-law provisions.
|
||||
Nothing in this Section shall prevent a party's ability to bring
|
||||
cross-claims or counter-claims.
|
||||
|
||||
9. Miscellaneous
|
||||
----------------
|
||||
|
||||
This License represents the complete agreement concerning the subject
|
||||
matter hereof. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent
|
||||
necessary to make it enforceable. Any law or regulation which provides
|
||||
that the language of a contract shall be construed against the drafter
|
||||
shall not be used to construe this License against a Contributor.
|
||||
|
||||
10. Versions of the License
|
||||
---------------------------
|
||||
|
||||
10.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section
|
||||
10.3, no one other than the license steward has the right to modify or
|
||||
publish new versions of this License. Each version will be given a
|
||||
distinguishing version number.
|
||||
|
||||
10.2. Effect of New Versions
|
||||
|
||||
You may distribute the Covered Software under the terms of the version
|
||||
of the License under which You originally received the Covered Software,
|
||||
or under the terms of any subsequent version published by the license
|
||||
steward.
|
||||
|
||||
10.3. Modified Versions
|
||||
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a
|
||||
modified version of this License if you rename the license and remove
|
||||
any references to the name of the license steward (except to note that
|
||||
such modified license differs from this License).
|
||||
|
||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||
Licenses
|
||||
|
||||
If You choose to distribute Source Code Form that is Incompatible With
|
||||
Secondary Licenses under the terms of this version of the License, the
|
||||
notice described in Exhibit B of this License must be attached.
|
|
@ -0,0 +1 @@
|
|||
include gixy/formatters/templates/*
|
|
@ -0,0 +1,78 @@
|
|||
GIXY
|
||||
====
|
||||
|
||||
# Overview
|
||||
Gixy is a tool for Nginx configuration analyzing. The main goal of Gixy is to prevent misconfiguration and automate flaw detection.
|
||||
Currently supported Python versions is 2.7 and 3.4+.
|
||||
Disclaimer: Gixy is well tested only on GNU/Linux, in other OS may have some issues.
|
||||
|
||||
# Installation
|
||||
Gixy is distributed on PyPI. The best way to install it is with pip:
|
||||
```bash
|
||||
pip install bandit
|
||||
```
|
||||
|
||||
Run Gixy and check results:
|
||||
```bash
|
||||
gixy
|
||||
```
|
||||
|
||||
# Usage
|
||||
By default Gixy will try to analyze Nginx configuration placed in `/etc/nginx/nginx.conf`. But you can always specify needed path:
|
||||
```bash
|
||||
$ gixy /etc/nginx/nginx.conf
|
||||
|
||||
==================== Results ===================
|
||||
|
||||
Problem: [http_splitting] Possible HTTP-Splitting vulnerability.
|
||||
Description: Using variables that can contain "\n" may lead to http injection.
|
||||
Additional info: https://github.com/yandex/gixy/wiki/ru/httpsplitting
|
||||
Reason: At least variable "$action" can contain "\n"
|
||||
Pseudo config:
|
||||
include /etc/nginx/sites/default.conf;
|
||||
|
||||
server {
|
||||
|
||||
location ~ /v1/((?<action>[^.]*)\.json)?$ {
|
||||
add_header X-Action $action;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
==================== Summary ===================
|
||||
Total issues:
|
||||
Unspecified: 0
|
||||
Low: 0
|
||||
Medium: 0
|
||||
High: 1
|
||||
```
|
||||
|
||||
Or skip some tests:
|
||||
```
|
||||
$ ./gixy-cli.py --skips http_splitting /etc/nginx/nginx.conf
|
||||
|
||||
==================== Results ===================
|
||||
No issues found.
|
||||
|
||||
==================== Summary ===================
|
||||
Total issues:
|
||||
Unspecified: 0
|
||||
Low: 0
|
||||
Medium: 0
|
||||
High: 0
|
||||
```
|
||||
|
||||
You can achieve all other `gixy` arguments with the help command: `gixy --help`
|
||||
|
||||
# Documentation
|
||||
Full documentation and recommendations can be found [here](https://github.com/yandex/gixy/wiki/ru/) (sorry, but Russian language only so far)
|
||||
|
||||
# Contributing
|
||||
Contributions to Gixy are always welcome! You can help us in different ways:
|
||||
* Open an issue with suggestions for improvements and errors you're facing;
|
||||
* Fork this repository and submit a pull request;
|
||||
* Improve the documentation.
|
||||
|
||||
Code guidelines:
|
||||
* Python code style should follow [PEP8 standards][pep8] standards whenever possible;
|
||||
* Pull requests with new plugins must contain unit tests for it.
|
|
@ -0,0 +1,5 @@
|
|||
# flake8: noqa
|
||||
|
||||
from gixy.core import severity
|
||||
|
||||
version = '0.0.14'
|
|
@ -0,0 +1,160 @@
|
|||
# flake8: noqa
|
||||
|
||||
from configargparse import *
|
||||
from six.moves import StringIO
|
||||
|
||||
from gixy.core.plugins_manager import PluginsManager
|
||||
|
||||
|
||||
# used while parsing args to keep track of where they came from
|
||||
_COMMAND_LINE_SOURCE_KEY = 'command_line'
|
||||
_ENV_VAR_SOURCE_KEY = 'environment_variables'
|
||||
_CONFIG_FILE_SOURCE_KEY = 'config_file'
|
||||
_DEFAULTS_SOURCE_KEY = 'defaults'
|
||||
|
||||
|
||||
class GixyConfigParser(DefaultConfigFileParser):
|
||||
def get_syntax_description(self):
|
||||
return ''
|
||||
|
||||
def parse(self, stream):
|
||||
"""Parses the keys + values from a config file."""
|
||||
|
||||
items = OrderedDict()
|
||||
prefix = ''
|
||||
for i, line in enumerate(stream):
|
||||
line = line.strip()
|
||||
if not line or line[0] in ['#', ';'] or line.startswith('---'):
|
||||
continue
|
||||
if line[0] == '[':
|
||||
prefix = '%s-' % line[1:-1].replace('_', '-')
|
||||
continue
|
||||
|
||||
white_space = '\\s*'
|
||||
key = '(?P<key>[^:=;#\s]+?)'
|
||||
value = white_space+'[:=\s]'+white_space+'(?P<value>.+?)'
|
||||
comment = white_space+'(?P<comment>\\s[;#].*)?'
|
||||
|
||||
key_only_match = re.match('^' + key + comment + '$', line)
|
||||
if key_only_match:
|
||||
key = key_only_match.group('key')
|
||||
items[key] = 'true'
|
||||
continue
|
||||
|
||||
key_value_match = re.match('^'+key+value+comment+'$', line)
|
||||
if key_value_match:
|
||||
key = key_value_match.group('key')
|
||||
value = key_value_match.group('value')
|
||||
|
||||
if value.startswith('[') and value.endswith(']'):
|
||||
# handle special case of lists
|
||||
value = [elem.strip() for elem in value[1:-1].split(',')]
|
||||
|
||||
items[prefix + key] = value
|
||||
continue
|
||||
|
||||
raise ConfigFileParserException('Unexpected line %s in %s: %s' % (i,
|
||||
getattr(stream, 'name', 'stream'), line))
|
||||
return items
|
||||
|
||||
def serialize(self, items):
|
||||
"""Does the inverse of config parsing by taking parsed values and
|
||||
converting them back to a string representing config file contents.
|
||||
"""
|
||||
r = StringIO()
|
||||
for key, value in items.items():
|
||||
if type(value) == OrderedDict:
|
||||
r.write('\n[%s]\n' % key)
|
||||
r.write(self.serialize(value))
|
||||
else:
|
||||
value, help = value
|
||||
if help:
|
||||
r.write('; %s\n' % help)
|
||||
r.write('%s = %s\n' % (key, value))
|
||||
return r.getvalue()
|
||||
|
||||
|
||||
class GixyHelpFormatter(HelpFormatter):
|
||||
def format_help(self):
|
||||
manager = PluginsManager()
|
||||
help_message = super(GixyHelpFormatter, self).format_help()
|
||||
if 'plugins options:' in help_message:
|
||||
# Print available blugins _only_ if we prints options for it
|
||||
plugins = '\n'.join('\t' + plugin.__name__ for plugin in manager.plugins_classes)
|
||||
help_message = '{orig}\n\navailable plugins:\n{plugins}\n'.format(orig=help_message, plugins=plugins)
|
||||
return help_message
|
||||
|
||||
|
||||
class ArgsParser(ArgumentParser):
|
||||
def get_possible_config_keys(self, action):
|
||||
"""This method decides which actions can be set in a config file and
|
||||
what their keys will be. It returns a list of 0 or more config keys that
|
||||
can be used to set the given action's value in a config file.
|
||||
"""
|
||||
keys = []
|
||||
for arg in action.option_strings:
|
||||
if arg in {'--config', '--write-config', '--version'}:
|
||||
continue
|
||||
if any([arg.startswith(2*c) for c in self.prefix_chars]):
|
||||
keys += [arg[2:], arg] # eg. for '--bla' return ['bla', '--bla']
|
||||
|
||||
return keys
|
||||
|
||||
def get_items_for_config_file_output(self, source_to_settings,
|
||||
parsed_namespace):
|
||||
"""Converts the given settings back to a dictionary that can be passed
|
||||
to ConfigFormatParser.serialize(..).
|
||||
|
||||
Args:
|
||||
source_to_settings: the dictionary described in parse_known_args()
|
||||
parsed_namespace: namespace object created within parse_known_args()
|
||||
Returns:
|
||||
an OrderedDict where keys are strings and values are either strings
|
||||
or lists
|
||||
"""
|
||||
config_file_items = OrderedDict()
|
||||
for source, settings in source_to_settings.items():
|
||||
if source == _COMMAND_LINE_SOURCE_KEY:
|
||||
_, existing_command_line_args = settings['']
|
||||
for action in self._actions:
|
||||
config_file_keys = self.get_possible_config_keys(action)
|
||||
if config_file_keys and not action.is_positional_arg and \
|
||||
already_on_command_line(existing_command_line_args,
|
||||
action.option_strings):
|
||||
value = getattr(parsed_namespace, action.dest, None)
|
||||
if value is not None:
|
||||
if type(value) is bool:
|
||||
value = str(value).lower()
|
||||
if ':' in action.dest:
|
||||
section, key = action.dest.split(':', 2)
|
||||
key = key.replace('_', '-')
|
||||
if section not in config_file_items:
|
||||
config_file_items[section] = OrderedDict()
|
||||
config_file_items[section][key] = (value, action.help)
|
||||
else:
|
||||
config_file_items[config_file_keys[0]] = (value, action.help)
|
||||
elif source.startswith(_CONFIG_FILE_SOURCE_KEY):
|
||||
for key, (action, value) in settings.items():
|
||||
if ':' in action.dest:
|
||||
section, key = action.dest.split(':', 2)
|
||||
key = key.replace('_', '-')
|
||||
if section not in config_file_items:
|
||||
config_file_items[section] = OrderedDict()
|
||||
config_file_items[section][key] = (value, action.help)
|
||||
else:
|
||||
config_file_items[key] = (value, action.help)
|
||||
return config_file_items
|
||||
|
||||
|
||||
def create_parser():
|
||||
return ArgsParser(
|
||||
description='Gixy - a Nginx configuration [sec]analyzer\n\n',
|
||||
formatter_class=GixyHelpFormatter,
|
||||
config_file_parser_class=GixyConfigParser,
|
||||
auto_env_var_prefix='GIXY_',
|
||||
add_env_var_help=False,
|
||||
default_config_files=['/etc/gixy/gixy.cfg', '~/.config/gixy/gixy.conf'],
|
||||
args_for_setting_config_path=['-c', '--config'],
|
||||
args_for_writing_out_config_file=['--write-config'],
|
||||
add_config_file_help=False
|
||||
)
|
|
@ -0,0 +1,173 @@
|
|||
import os
|
||||
import sys
|
||||
import logging
|
||||
import copy
|
||||
|
||||
import gixy
|
||||
from gixy.core.manager import Manager as Gixy
|
||||
from gixy.formatters import get_all as formatters
|
||||
from gixy.core.plugins_manager import PluginsManager
|
||||
from gixy.core.config import Config
|
||||
from gixy.cli.argparser import create_parser
|
||||
|
||||
LOG = logging.getLogger()
|
||||
|
||||
|
||||
def _init_logger(debug=False):
|
||||
LOG.handlers = []
|
||||
log_level = logging.DEBUG if debug else logging.INFO
|
||||
logging.captureWarnings(True)
|
||||
|
||||
LOG.setLevel(log_level)
|
||||
handler = logging.StreamHandler(sys.stderr)
|
||||
handler.setFormatter(logging.Formatter('[%(module)s]\t%(levelname)s\t%(message)s'))
|
||||
LOG.addHandler(handler)
|
||||
LOG.debug("logging initialized")
|
||||
|
||||
|
||||
def _create_plugin_help(option):
|
||||
if isinstance(option, (tuple, list, set)):
|
||||
default = ','.join(list(option))
|
||||
else:
|
||||
default = str(option)
|
||||
|
||||
return 'Default: {}'.format(default)
|
||||
|
||||
|
||||
def _get_cli_parser():
|
||||
parser = create_parser()
|
||||
parser.add_argument('nginx_file', nargs='?', type=str, default='/etc/nginx/nginx.conf', metavar='nginx.conf',
|
||||
help='Path to nginx.conf, e.g. /etc/nginx/nginx.conf')
|
||||
|
||||
parser.add_argument(
|
||||
'-v', '--version', action='version',
|
||||
version='Gixy v{}'.format(gixy.version))
|
||||
|
||||
parser.add_argument(
|
||||
'-l', '--level', dest='level', action='count', default=0,
|
||||
help='Report issues of a given severity level or higher (-l for LOW, -ll for MEDIUM, -lll for HIGH)')
|
||||
|
||||
default_formatter = 'console' if sys.stdout.isatty() else 'text'
|
||||
available_formatters = formatters().keys()
|
||||
parser.add_argument(
|
||||
'-f', '--format', dest='output_format', choices=available_formatters, default=default_formatter,
|
||||
type=str, help='Specify output format')
|
||||
|
||||
parser.add_argument(
|
||||
'-o', '--output', dest='output_file', type=str,
|
||||
help='Write report to file')
|
||||
|
||||
parser.add_argument(
|
||||
'-d', '--debug', dest='debug', action='store_true', default=False,
|
||||
help='Turn on debug mode')
|
||||
|
||||
parser.add_argument(
|
||||
'--tests', dest='tests', type=str,
|
||||
help='Comma-separated list of tests to run')
|
||||
|
||||
parser.add_argument(
|
||||
'--skips', dest='skips', type=str,
|
||||
help='Comma-separated list of tests to skip')
|
||||
|
||||
parser.add_argument(
|
||||
'--disable-includes', dest='disable_includes', action='store_true', default=False,
|
||||
help='Disable "include" directive processing')
|
||||
|
||||
group = parser.add_argument_group('plugins options')
|
||||
for plugin_cls in PluginsManager().plugins_classes:
|
||||
name = plugin_cls.__name__
|
||||
if not plugin_cls.options:
|
||||
continue
|
||||
|
||||
options = copy.deepcopy(plugin_cls.options)
|
||||
for opt_key, opt_val in options.items():
|
||||
option_name = '--{plugin}-{key}'.format(plugin=name, key=opt_key).replace('_', '-')
|
||||
dst_name = '{plugin}:{key}'.format(plugin=name, key=opt_key)
|
||||
opt_type = str if isinstance(opt_val, (tuple, list, set)) else type(opt_val)
|
||||
group.add_argument(
|
||||
option_name, metavar=opt_key, dest=dst_name, type=opt_type,
|
||||
help=_create_plugin_help(opt_val)
|
||||
)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def _is_nginx_file(file_path):
|
||||
s = open(file_path).read()
|
||||
return 'server {' in s or 'http {' in s
|
||||
|
||||
|
||||
def main():
|
||||
parser = _get_cli_parser()
|
||||
args = parser.parse_args()
|
||||
_init_logger(args.debug)
|
||||
|
||||
path = os.path.expanduser(args.nginx_file)
|
||||
if not os.path.isfile(path):
|
||||
sys.stderr.write('Please specify path to Nginx configuration.\n\n')
|
||||
parser.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
if not _is_nginx_file(path):
|
||||
sys.stderr.write('This is nginx config? Rly?\n')
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
severity = gixy.severity.ALL[args.level]
|
||||
except IndexError:
|
||||
sys.stderr.write('Too high level filtering. Maximum level: -{}\n'.format('l' * (len(gixy.severity.ALL) - 1)))
|
||||
sys.exit(1)
|
||||
|
||||
if args.tests:
|
||||
tests = [x.strip() for x in args.tests.split(',')]
|
||||
else:
|
||||
tests = None
|
||||
|
||||
if args.skips:
|
||||
skips = [x.strip() for x in args.skips.split(',')]
|
||||
else:
|
||||
skips = None
|
||||
|
||||
config = Config(
|
||||
severity=severity,
|
||||
output_format=args.output_format,
|
||||
output_file=args.output_file,
|
||||
plugins=tests,
|
||||
skips=skips,
|
||||
allow_includes=not args.disable_includes
|
||||
)
|
||||
|
||||
for plugin_cls in PluginsManager().plugins_classes:
|
||||
name = plugin_cls.__name__
|
||||
options = copy.deepcopy(plugin_cls.options)
|
||||
for opt_key, opt_val in options.items():
|
||||
option_name = '{}:{}'.format(name, opt_key)
|
||||
if option_name not in args:
|
||||
continue
|
||||
|
||||
val = getattr(args, option_name)
|
||||
if val is None:
|
||||
continue
|
||||
|
||||
if isinstance(opt_val, tuple):
|
||||
val = tuple([x.strip() for x in val.split(',')])
|
||||
elif isinstance(opt_val, set):
|
||||
val = set([x.strip() for x in val.split(',')])
|
||||
elif isinstance(opt_val, list):
|
||||
val = [x.strip() for x in val.split(',')]
|
||||
options[opt_key] = val
|
||||
config.set_for(name, options)
|
||||
|
||||
with Gixy(config=config) as yoda:
|
||||
yoda.audit(path)
|
||||
formatted = formatters()[config.output_format]().format(yoda)
|
||||
if args.output_file:
|
||||
with open(config.output_file, 'w') as f:
|
||||
f.write(formatted)
|
||||
else:
|
||||
print(formatted)
|
||||
|
||||
if sum(yoda.stats.values()) > 0:
|
||||
# If something found - exit code must be 1, otherwise 0
|
||||
sys.exit(1)
|
||||
sys.exit(0)
|
|
@ -0,0 +1,266 @@
|
|||
from gixy.core.regexp import Regexp
|
||||
from gixy.core.variable import Variable
|
||||
|
||||
|
||||
BUILTIN_VARIABLES = {
|
||||
# http://nginx.org/en/docs/http/ngx_http_core_module.html#var_uri
|
||||
'uri': '/[^\x20\t]*',
|
||||
# http://nginx.org/en/docs/http/ngx_http_core_module.html#var_document_uri
|
||||
'document_uri': '/[^\x20\t]*',
|
||||
# http://nginx.org/en/docs/http/ngx_http_core_module.html#var_arg_
|
||||
'arg_': '[^\s&]+',
|
||||
# http://nginx.org/en/docs/http/ngx_http_core_module.html#var_args
|
||||
'args': '[^\s]+',
|
||||
# http://nginx.org/en/docs/http/ngx_http_core_module.html#var_query_string
|
||||
'query_string': '[^\s]+',
|
||||
# http://nginx.org/en/docs/http/ngx_http_core_module.html#var_request_uri
|
||||
'request_uri': '/[^\s]*',
|
||||
# http://nginx.org/en/docs/http/ngx_http_core_module.html#var_http_
|
||||
'http_': '[\x21-\x7e]',
|
||||
|
||||
# http://nginx.org/en/docs/http/ngx_http_upstream_module.html#var_upstream_http_
|
||||
'upstream_http_': '',
|
||||
# http://nginx.org/en/docs/http/ngx_http_upstream_module.html#var_upstream_cookie_
|
||||
'upstream_cookie_': '',
|
||||
# http://nginx.org/en/docs/http/ngx_http_proxy_module.html#var_proxy_add_x_forwarded_for
|
||||
'proxy_add_x_forwarded_for': '',
|
||||
# http://nginx.org/en/docs/http/ngx_http_proxy_module.html#var_proxy_host
|
||||
'proxy_host': '',
|
||||
# http://nginx.org/en/docs/http/ngx_http_proxy_module.html#var_proxy_port
|
||||
'proxy_port': '',
|
||||
# http://nginx.org/en/docs/http/ngx_http_core_module.html#var_proxy_protocol_addr
|
||||
# http://nginx.org/en/docs/stream/ngx_stream_core_module.html#var_proxy_protocol_addr
|
||||
# http://nginx.org/en/docs/http/ngx_http_core_module.html#var_proxy_protocol_port
|
||||
# http://nginx.org/en/docs/stream/ngx_stream_core_module.html#var_proxy_protocol_port
|
||||
'proxy_protocol_port': '',
|
||||
# http://nginx.org/en/docs/http/ngx_http_fastcgi_module.html#var_fastcgi_path_info
|
||||
'fastcgi_path_info': '',
|
||||
# http://nginx.org/en/docs/http/ngx_http_fastcgi_module.html#var_fastcgi_script_name
|
||||
'fastcgi_script_name': '',
|
||||
# http://nginx.org/en/docs/http/ngx_http_core_module.html#var_content_type
|
||||
'content_type': '',
|
||||
# http://nginx.org/en/docs/http/ngx_http_core_module.html#var_cookie_
|
||||
'cookie_': '',
|
||||
# http://nginx.org/en/docs/http/ngx_http_core_module.html#var_host
|
||||
'host': '',
|
||||
# http://nginx.org/en/docs/http/ngx_http_core_module.html#var_hostname
|
||||
# http://nginx.org/en/docs/stream/ngx_stream_core_module.html#var_hostname
|
||||
'hostname': '',
|
||||
# http://nginx.org/en/docs/http/ngx_http_core_module.html#var_limit_rate
|
||||
'limit_rate': '',
|
||||
# http://nginx.org/en/docs/http/ngx_http_memcached_module.html#var_memcached_key
|
||||
'memcached_key': '',
|
||||
# http://nginx.org/en/docs/http/ngx_http_core_module.html#var_realpath_root
|
||||
'realpath_root': '',
|
||||
# http://nginx.org/en/docs/http/ngx_http_core_module.html#var_remote_user
|
||||
'remote_user': '',
|
||||
# http://nginx.org/en/docs/http/ngx_http_core_module.html#var_request
|
||||
'request': '',
|
||||
# http://nginx.org/en/docs/http/ngx_http_core_module.html#var_request_body
|
||||
'request_body': '',
|
||||
# http://nginx.org/en/docs/http/ngx_http_core_module.html#var_request_completion
|
||||
'request_completion': '',
|
||||
# http://nginx.org/en/docs/http/ngx_http_core_module.html#var_request_filename
|
||||
'request_filename': '',
|
||||
# http://nginx.org/en/docs/http/ngx_http_core_module.html#var_request_id
|
||||
'request_id': '',
|
||||
# http://nginx.org/en/docs/http/ngx_http_slice_module.html#var_slice_range
|
||||
'slice_range': '',
|
||||
# http://nginx.org/en/docs/http/ngx_http_secure_link_module.html#var_secure_link
|
||||
'secure_link': '',
|
||||
# http://nginx.org/en/docs/http/ngx_http_secure_link_module.html#var_secure_link_expires
|
||||
'secure_link_expires': '',
|
||||
# http://nginx.org/en/docs/http/ngx_http_core_module.html#var_sent_http_
|
||||
'sent_http_': '',
|
||||
# http://nginx.org/en/docs/http/ngx_http_core_module.html#var_server_name
|
||||
'server_name': '',
|
||||
|
||||
# "Secure" variables that can't content or strictly limited user input
|
||||
|
||||
# http://nginx.org/en/docs/http/ngx_http_browser_module.html#var_ancient_browser
|
||||
'ancient_browser': None,
|
||||
# http://nginx.org/en/docs/http/ngx_http_core_module.html#var_binary_remote_addr
|
||||
# http://nginx.org/en/docs/stream/ngx_stream_core_module.html#var_binary_remote_addr
|
||||
'binary_remote_addr': None,
|
||||
# http://nginx.org/en/docs/http/ngx_http_core_module.html#var_body_bytes_sent
|
||||
'body_bytes_sent': None,
|
||||
# http://nginx.org/en/docs/stream/ngx_stream_core_module.html#var_bytes_received
|
||||
'bytes_received': None,
|
||||
# http://nginx.org/en/docs/http/ngx_http_core_module.html#var_bytes_sent
|
||||
# http://nginx.org/en/docs/http/ngx_http_log_module.html#var_bytes_sent
|
||||
# http://nginx.org/en/docs/stream/ngx_stream_core_module.html#var_bytes_sent
|
||||
'bytes_sent': None,
|
||||
# http://nginx.org/en/docs/http/ngx_http_core_module.html#var_connection
|
||||
# http://nginx.org/en/docs/http/ngx_http_log_module.html#var_connection
|
||||
# http://nginx.org/en/docs/stream/ngx_stream_core_module.html#var_connection
|
||||
'connection': None,
|
||||
# http://nginx.org/en/docs/http/ngx_http_core_module.html#var_connection_requests
|
||||
# http://nginx.org/en/docs/http/ngx_http_log_module.html#var_connection_requests
|
||||
'connection_requests': None,
|
||||
# http://nginx.org/en/docs/http/ngx_http_stub_status_module.html#var_connections_active
|
||||
'connections_active': None,
|
||||
# http://nginx.org/en/docs/http/ngx_http_stub_status_module.html#var_connections_reading
|
||||
'connections_reading': None,
|
||||
# http://nginx.org/en/docs/http/ngx_http_stub_status_module.html#var_connections_waiting
|
||||
'connections_waiting': None,
|
||||
# http://nginx.org/en/docs/http/ngx_http_stub_status_module.html#var_connections_writing
|
||||
'connections_writing': None,
|
||||
# http://nginx.org/en/docs/http/ngx_http_core_module.html#var_content_length
|
||||
'content_length': None,
|
||||
# http://nginx.org/en/docs/http/ngx_http_ssi_module.html#var_date_gmt
|
||||
'date_gmt': None,
|
||||
# http://nginx.org/en/docs/http/ngx_http_ssi_module.html#var_date_local
|
||||
'date_local': None,
|
||||
# http://nginx.org/en/docs/http/ngx_http_core_module.html#var_document_root
|
||||
'document_root': '/etc/nginx',
|
||||
# http://nginx.org/en/docs/http/ngx_http_geoip_module.html
|
||||
# http://nginx.org/en/docs/stream/ngx_stream_geoip_module.html
|
||||
'geoip_': None,
|
||||
# http://nginx.org/en/docs/http/ngx_http_gzip_module.html#var_gzip_ratio
|
||||
'gzip_ratio': None,
|
||||
# http://nginx.org/en/docs/http/ngx_http_v2_module.html#var_http2
|
||||
'http2': None,
|
||||
# http://nginx.org/en/docs/http/ngx_http_core_module.html#var_https
|
||||
'https': None,
|
||||
# http://nginx.org/en/docs/http/ngx_http_referer_module.html#var_invalid_referer
|
||||
'invalid_referer': None,
|
||||
# http://nginx.org/en/docs/http/ngx_http_core_module.html#var_is_args
|
||||
'is_args': None,
|
||||
# http://nginx.org/en/docs/http/ngx_http_auth_jwt_module.html
|
||||
'jwt_': None,
|
||||
# http://nginx.org/en/docs/http/ngx_http_browser_module.html#var_modern_browser
|
||||
'modern_browser': None,
|
||||
# http://nginx.org/en/docs/http/ngx_http_core_module.html#var_msec
|
||||
# http://nginx.org/en/docs/http/ngx_http_log_module.html#var_msec
|
||||
# http://nginx.org/en/docs/stream/ngx_stream_core_module.html#var_msec
|
||||
'msec': None,
|
||||
# http://nginx.org/en/docs/http/ngx_http_browser_module.html#var_msie
|
||||
'msie': None,
|
||||
# http://nginx.org/en/docs/http/ngx_http_core_module.html#var_nginx_version
|
||||
# http://nginx.org/en/docs/stream/ngx_stream_core_module.html#var_nginx_version
|
||||
'nginx_version': None,
|
||||
# http://nginx.org/en/docs/http/ngx_http_core_module.html#var_pid
|
||||
# http://nginx.org/en/docs/stream/ngx_stream_core_module.html#var_pid
|
||||
'pid': None,
|
||||
# http://nginx.org/en/docs/http/ngx_http_core_module.html#var_pipe
|
||||
# http://nginx.org/en/docs/http/ngx_http_log_module.html#var_pipe
|
||||
'pipe': None,
|
||||
# http://nginx.org/en/docs/stream/ngx_stream_core_module.html#var_protocol
|
||||
'protocol': None,
|
||||
# http://nginx.org/en/docs/http/ngx_http_realip_module.html#var_realip_remote_addr
|
||||
# http://nginx.org/en/docs/stream/ngx_stream_realip_module.html#var_realip_remote_addr
|
||||
# http://nginx.org/en/docs/http/ngx_http_realip_module.html#var_realip_remote_port
|
||||
# http://nginx.org/en/docs/stream/ngx_stream_realip_module.html#var_realip_remote_port
|
||||
'realip_remote_port': None,
|
||||
# http://nginx.org/en/docs/http/ngx_http_core_module.html#var_remote_addr
|
||||
# http://nginx.org/en/docs/stream/ngx_stream_core_module.html#var_remote_addr
|
||||
'remote_addr': None,
|
||||
# http://nginx.org/en/docs/http/ngx_http_core_module.html#var_remote_port
|
||||
# http://nginx.org/en/docs/stream/ngx_stream_core_module.html#var_remote_port
|
||||
'remote_port': None,
|
||||
# http://nginx.org/en/docs/http/ngx_http_core_module.html#var_request_body_file
|
||||
'request_body_file': None,
|
||||
# http://nginx.org/en/docs/http/ngx_http_core_module.html#var_request_length
|
||||
# http://nginx.org/en/docs/http/ngx_http_log_module.html#var_request_length
|
||||
'request_length': None,
|
||||
# http://nginx.org/en/docs/http/ngx_http_core_module.html#var_request_method
|
||||
'request_method': None,
|
||||
# http://nginx.org/en/docs/http/ngx_http_core_module.html#var_request_time
|
||||
# http://nginx.org/en/docs/http/ngx_http_log_module.html#var_request_time
|
||||
'request_time': None,
|
||||
# http://nginx.org/en/docs/http/ngx_http_core_module.html#var_scheme
|
||||
'scheme': None,
|
||||
# http://nginx.org/en/docs/http/ngx_http_core_module.html#var_server_addr
|
||||
# http://nginx.org/en/docs/stream/ngx_stream_core_module.html#var_server_addr
|
||||
'server_addr': None,
|
||||
# http://nginx.org/en/docs/http/ngx_http_core_module.html#var_server_port
|
||||
# http://nginx.org/en/docs/stream/ngx_stream_core_module.html#var_server_port
|
||||
'server_port': None,
|
||||
# http://nginx.org/en/docs/http/ngx_http_core_module.html#var_server_protocol
|
||||
'server_protocol': None,
|
||||
# http://nginx.org/en/docs/http/ngx_http_session_log_module.html#var_session_log_binary_id
|
||||
'session_log_binary_id': None,
|
||||
# http://nginx.org/en/docs/http/ngx_http_session_log_module.html#var_session_log_id
|
||||
'session_log_id': None,
|
||||
# http://nginx.org/en/docs/stream/ngx_stream_core_module.html#var_session_time
|
||||
'session_time': None,
|
||||
# http://nginx.org/en/docs/http/ngx_http_spdy_module.html#var_spdy
|
||||
'spdy': None,
|
||||
# http://nginx.org/en/docs/http/ngx_http_spdy_module.html#var_spdy_request_priority
|
||||
'spdy_request_priority': None,
|
||||
# http://nginx.org/en/docs/http/ngx_http_ssl_module.html
|
||||
# http://nginx.org/en/docs/stream/ngx_stream_ssl_module.html
|
||||
'ssl_': None,
|
||||
# http://nginx.org/en/docs/http/ngx_http_core_module.html#var_status
|
||||
# http://nginx.org/en/docs/http/ngx_http_log_module.html#var_status
|
||||
# http://nginx.org/en/docs/stream/ngx_stream_core_module.html#var_status
|
||||
'status': None,
|
||||
# http://nginx.org/en/docs/http/ngx_http_core_module.html
|
||||
'tcpinfo_': None,
|
||||
# http://nginx.org/en/docs/http/ngx_http_core_module.html
|
||||
# http://nginx.org/en/docs/http/ngx_http_log_module.html
|
||||
# http://nginx.org/en/docs/stream/ngx_stream_core_module.html
|
||||
'time_iso8601': None,
|
||||
# http://nginx.org/en/docs/http/ngx_http_core_module.html
|
||||
# http://nginx.org/en/docs/http/ngx_http_log_module.html
|
||||
# http://nginx.org/en/docs/stream/ngx_stream_core_module.html
|
||||
'time_local': None,
|
||||
# http://nginx.org/en/docs/http/ngx_http_userid_module.html#var_uid_got
|
||||
'uid_got': None,
|
||||
# http://nginx.org/en/docs/http/ngx_http_userid_module.html#var_uid_reset
|
||||
'uid_reset': None,
|
||||
# http://nginx.org/en/docs/http/ngx_http_userid_module.html#var_uid_set
|
||||
'uid_set': None,
|
||||
# http://nginx.org/en/docs/http/ngx_http_upstream_module.html#var_upstream_addr
|
||||
# http://nginx.org/en/docs/stream/ngx_stream_upstream_module.html#var_upstream_addr
|
||||
'upstream_addr': None,
|
||||
# http://nginx.org/en/docs/http/ngx_http_upstream_module.html#var_upstream_bytes_received
|
||||
# http://nginx.org/en/docs/stream/ngx_stream_upstream_module.html#var_upstream_bytes_received
|
||||
'upstream_bytes_received': None,
|
||||
# http://nginx.org/en/docs/stream/ngx_stream_upstream_module.html#var_upstream_bytes_sent
|
||||
'upstream_bytes_sent': None,
|
||||
# http://nginx.org/en/docs/http/ngx_http_upstream_module.html#var_upstream_cache_status
|
||||
'upstream_cache_status': None,
|
||||
# http://nginx.org/en/docs/http/ngx_http_upstream_module.html#var_upstream_connect_time
|
||||
# http://nginx.org/en/docs/stream/ngx_stream_upstream_module.html#var_upstream_connect_time
|
||||
'upstream_connect_time': None,
|
||||
# http://nginx.org/en/docs/stream/ngx_stream_upstream_module.html#var_upstream_first_byte_time
|
||||
'upstream_first_byte_time': None,
|
||||
# http://nginx.org/en/docs/http/ngx_http_upstream_module.html#var_upstream_header_time
|
||||
'upstream_header_time': None,
|
||||
# http://nginx.org/en/docs/http/ngx_http_upstream_module.html#var_upstream_response_length
|
||||
'upstream_response_length': None,
|
||||
# http://nginx.org/en/docs/http/ngx_http_upstream_module.html#var_upstream_response_time
|
||||
'upstream_response_time': None,
|
||||
# http://nginx.org/en/docs/stream/ngx_stream_upstream_module.html#var_upstream_session_time
|
||||
'upstream_session_time': None,
|
||||
# http://nginx.org/en/docs/http/ngx_http_upstream_module.html#var_upstream_status
|
||||
'upstream_status': None
|
||||
}
|
||||
|
||||
|
||||
def is_builtin(name):
|
||||
if isinstance(name, int):
|
||||
# Indexed variables can't be builtin
|
||||
return False
|
||||
for builtin in BUILTIN_VARIABLES:
|
||||
if builtin.endswith('_'):
|
||||
if name.startswith(builtin):
|
||||
return True
|
||||
elif name == builtin:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def builtin_var(name):
|
||||
for builtin, regexp in BUILTIN_VARIABLES.items():
|
||||
if builtin.endswith('_'):
|
||||
if not name.startswith(builtin):
|
||||
continue
|
||||
elif name != builtin:
|
||||
continue
|
||||
|
||||
if regexp:
|
||||
return Variable(name=name, value=Regexp(regexp, strict=True, case_sensitive=False))
|
||||
return Variable(name=name, value='builtin', have_script=False)
|
||||
return None
|
|
@ -0,0 +1,30 @@
|
|||
import gixy
|
||||
|
||||
|
||||
class Config(object):
|
||||
def __init__(self,
|
||||
plugins=None,
|
||||
skips=None,
|
||||
severity=gixy.severity.UNSPECIFIED,
|
||||
output_format=None,
|
||||
output_file=None,
|
||||
allow_includes=True):
|
||||
|
||||
self.severity = severity
|
||||
self.output_format = output_format
|
||||
self.output_file = output_file
|
||||
self.plugins = plugins
|
||||
self.skips = skips
|
||||
self.allow_includes = allow_includes
|
||||
self.plugins_options = {}
|
||||
|
||||
def set_for(self, name, options):
|
||||
self.plugins_options[name] = options
|
||||
|
||||
def get_for(self, name):
|
||||
if self.has_for(name):
|
||||
return self.plugins_options[name]
|
||||
return {}
|
||||
|
||||
def has_for(self, name):
|
||||
return name in self.plugins_options
|
|
@ -0,0 +1,91 @@
|
|||
import logging
|
||||
import copy
|
||||
|
||||
from gixy.core.utils import is_indexed_name
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
CONTEXTS = []
|
||||
|
||||
|
||||
def get_context():
|
||||
return CONTEXTS[-1]
|
||||
|
||||
|
||||
def purge_context():
|
||||
del CONTEXTS[:]
|
||||
|
||||
|
||||
def push_context(block):
|
||||
if len(CONTEXTS):
|
||||
context = copy.deepcopy(get_context())
|
||||
else:
|
||||
context = Context()
|
||||
context.set_block(block)
|
||||
CONTEXTS.append(context)
|
||||
return context
|
||||
|
||||
|
||||
def pop_context():
|
||||
return CONTEXTS.pop()
|
||||
|
||||
|
||||
class Context(object):
|
||||
def __init__(self):
|
||||
self.block = None
|
||||
self.variables = {
|
||||
'index': {},
|
||||
'name': {}
|
||||
}
|
||||
|
||||
def set_block(self, directive):
|
||||
self.block = directive
|
||||
return self
|
||||
|
||||
def clear_index_vars(self):
|
||||
self.variables['index'] = {}
|
||||
return self
|
||||
|
||||
def add_var(self, name, var):
|
||||
if is_indexed_name(name):
|
||||
var_type = 'index'
|
||||
name = int(name)
|
||||
else:
|
||||
var_type = 'name'
|
||||
|
||||
self.variables[var_type][name] = var
|
||||
return self
|
||||
|
||||
def get_var(self, name):
|
||||
if is_indexed_name(name):
|
||||
var_type = 'index'
|
||||
name = int(name)
|
||||
else:
|
||||
var_type = 'name'
|
||||
|
||||
result = None
|
||||
try:
|
||||
result = self.variables[var_type][name]
|
||||
except KeyError:
|
||||
if var_type == 'name':
|
||||
# Only named variables can be builtins
|
||||
import gixy.core.builtin_variables as builtins
|
||||
|
||||
if builtins.is_builtin(name):
|
||||
result = builtins.builtin_var(name)
|
||||
|
||||
if not result:
|
||||
LOG.info("Can't find variable '{}'".format(name))
|
||||
return result
|
||||
|
||||
def __deepcopy__(self, memo):
|
||||
cls = self.__class__
|
||||
result = cls.__new__(cls)
|
||||
memo[id(self)] = result
|
||||
result.block = copy.copy(self.block)
|
||||
result.variables = {
|
||||
'index': copy.copy(self.variables['index']),
|
||||
'name': copy.copy(self.variables['name'])
|
||||
}
|
||||
return result
|
|
@ -0,0 +1,16 @@
|
|||
class Issue(object):
|
||||
|
||||
def __init__(self, plugin, summary=None, description=None,
|
||||
severity=None, reason=None, help_url=None, directives=None):
|
||||
self.plugin = plugin
|
||||
self.summary = summary
|
||||
self.description = description
|
||||
self.severity = severity
|
||||
self.reason = reason
|
||||
self.help_url = help_url
|
||||
if not directives:
|
||||
self.directives = []
|
||||
elif not hasattr(directives, '__iter__'):
|
||||
self.directives = [directives]
|
||||
else:
|
||||
self.directives = directives
|
|
@ -0,0 +1,59 @@
|
|||
import gixy
|
||||
from gixy.core.plugins_manager import PluginsManager
|
||||
from gixy.core.context import get_context, pop_context, push_context, purge_context
|
||||
from gixy.parser.nginx_parser import NginxParser
|
||||
from gixy.core.config import Config
|
||||
|
||||
|
||||
class Manager(object):
|
||||
def __init__(self, config=None):
|
||||
self.root = None
|
||||
self.parser = None
|
||||
self.auditor = None
|
||||
self.config = config or Config()
|
||||
self.stats = {gixy.severity.UNSPECIFIED: 0,
|
||||
gixy.severity.LOW: 0,
|
||||
gixy.severity.MEDIUM: 0,
|
||||
gixy.severity.HIGH: 0}
|
||||
|
||||
def audit(self, file_path):
|
||||
self.auditor = PluginsManager(config=self.config)
|
||||
self.parser = NginxParser(file_path, allow_includes=self.config.allow_includes)
|
||||
self.root = self.parser.parse(file_path)
|
||||
push_context(self.root)
|
||||
self._audit_recursive(self.root.children)
|
||||
|
||||
def get_results(self):
|
||||
for plugin in self.auditor.plugins:
|
||||
if plugin.issues:
|
||||
self.stats[plugin.severity] += len(plugin.issues)
|
||||
yield plugin
|
||||
|
||||
def _audit_recursive(self, tree):
|
||||
for directive in tree:
|
||||
self._update_variables(directive)
|
||||
self.auditor.audit(directive)
|
||||
if directive.is_block:
|
||||
if directive.self_context:
|
||||
push_context(directive)
|
||||
self._audit_recursive(directive.children)
|
||||
if directive.self_context:
|
||||
pop_context()
|
||||
|
||||
def _update_variables(self, directive):
|
||||
# TODO(buglloc): finish him!
|
||||
if not directive.provide_variables:
|
||||
return
|
||||
|
||||
context = get_context()
|
||||
for var in directive.variables:
|
||||
if var.name == 0:
|
||||
# All regexps must clean indexed variables
|
||||
context.clear_index_vars()
|
||||
context.add_var(var.name, var)
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
purge_context()
|
|
@ -0,0 +1,75 @@
|
|||
import os
|
||||
|
||||
import gixy
|
||||
from gixy.plugins.plugin import Plugin
|
||||
|
||||
|
||||
class PluginsManager(object):
|
||||
|
||||
def __init__(self, config=None):
|
||||
self.imported = False
|
||||
self.config = config
|
||||
self._plugins = []
|
||||
|
||||
def import_plugins(self):
|
||||
if self.imported:
|
||||
return
|
||||
|
||||
files_list = os.listdir(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'plugins'))
|
||||
for plugin_file in files_list:
|
||||
if not plugin_file.endswith('.py') or plugin_file.startswith('_'):
|
||||
continue
|
||||
__import__('gixy.plugins.'+os.path.splitext(plugin_file)[0], None, None, [''])
|
||||
|
||||
self.imported = True
|
||||
|
||||
def init_plugins(self):
|
||||
self.import_plugins()
|
||||
|
||||
exclude = self.config.skips if self.config else None
|
||||
include = self.config.plugins if self.config else None
|
||||
severity = self.config.severity if self.config else None
|
||||
for plugin_cls in Plugin.__subclasses__():
|
||||
name = plugin_cls.__name__
|
||||
if include and name not in include:
|
||||
# Skip not needed plugins
|
||||
continue
|
||||
if exclude and name in exclude:
|
||||
# Skipped plugins
|
||||
continue
|
||||
if severity and not gixy.severity.is_acceptable(plugin_cls.severity, severity):
|
||||
# Skip plugin by severity level
|
||||
continue
|
||||
if self.config and self.config.has_for(name):
|
||||
options = self.config.get_for(name)
|
||||
else:
|
||||
options = plugin_cls.options
|
||||
self._plugins.append(plugin_cls(options))
|
||||
|
||||
@property
|
||||
def plugins(self):
|
||||
if not self._plugins:
|
||||
self.init_plugins()
|
||||
return self._plugins
|
||||
|
||||
@property
|
||||
def plugins_classes(self):
|
||||
self.import_plugins()
|
||||
return Plugin.__subclasses__()
|
||||
|
||||
def get_plugins_descriptions(self):
|
||||
return map(lambda a: a.name, self.plugins)
|
||||
|
||||
def audit(self, directive):
|
||||
for plugin in self.plugins:
|
||||
if plugin.directives and directive.name not in plugin.directives:
|
||||
continue
|
||||
plugin.audit(directive)
|
||||
|
||||
def issues(self):
|
||||
result = []
|
||||
for plugin in self.plugins:
|
||||
if not plugin.issues:
|
||||
continue
|
||||
result.extend(plugin.issues)
|
||||
return result
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,9 @@
|
|||
UNSPECIFIED = 'UNSPECIFIED'
|
||||
LOW = 'LOW'
|
||||
MEDIUM = 'MEDIUM'
|
||||
HIGH = 'HIGH'
|
||||
ALL = [UNSPECIFIED, LOW, MEDIUM, HIGH]
|
||||
|
||||
|
||||
def is_acceptable(current_severity, min_severity):
|
||||
return ALL.index(current_severity) >= ALL.index(min_severity)
|
|
@ -0,0 +1,222 @@
|
|||
# flake8: noqa
|
||||
|
||||
#
|
||||
# Secret Labs' Regular Expression Engine
|
||||
#
|
||||
# various symbols used by the regular expression engine.
|
||||
# run this script to update the _sre include files!
|
||||
#
|
||||
# Copyright (c) 1998-2001 by Secret Labs AB. All rights reserved.
|
||||
#
|
||||
# See the sre.py file for information on usage and redistribution.
|
||||
#
|
||||
|
||||
"""Internal support module for sre"""
|
||||
|
||||
# update when constants are added or removed
|
||||
|
||||
MAGIC = 20031017
|
||||
|
||||
try:
|
||||
from _sre import MAXREPEAT
|
||||
except ImportError:
|
||||
import _sre
|
||||
MAXREPEAT = _sre.MAXREPEAT = 65535
|
||||
|
||||
# SRE standard exception (access as sre.error)
|
||||
# should this really be here?
|
||||
|
||||
class error(Exception):
|
||||
pass
|
||||
|
||||
# operators
|
||||
|
||||
FAILURE = "failure"
|
||||
SUCCESS = "success"
|
||||
|
||||
ANY = "any"
|
||||
ANY_ALL = "any_all"
|
||||
ASSERT = "assert"
|
||||
ASSERT_NOT = "assert_not"
|
||||
AT = "at"
|
||||
BIGCHARSET = "bigcharset"
|
||||
BRANCH = "branch"
|
||||
CALL = "call"
|
||||
CATEGORY = "category"
|
||||
CHARSET = "charset"
|
||||
GROUPREF = "groupref"
|
||||
GROUPREF_IGNORE = "groupref_ignore"
|
||||
GROUPREF_EXISTS = "groupref_exists"
|
||||
IN = "in"
|
||||
IN_IGNORE = "in_ignore"
|
||||
INFO = "info"
|
||||
JUMP = "jump"
|
||||
LITERAL = "literal"
|
||||
LITERAL_IGNORE = "literal_ignore"
|
||||
MARK = "mark"
|
||||
MAX_REPEAT = "max_repeat"
|
||||
MAX_UNTIL = "max_until"
|
||||
MIN_REPEAT = "min_repeat"
|
||||
MIN_UNTIL = "min_until"
|
||||
NEGATE = "negate"
|
||||
NOT_LITERAL = "not_literal"
|
||||
NOT_LITERAL_IGNORE = "not_literal_ignore"
|
||||
RANGE = "range"
|
||||
REPEAT = "repeat"
|
||||
REPEAT_ONE = "repeat_one"
|
||||
SUBPATTERN = "subpattern"
|
||||
MIN_REPEAT_ONE = "min_repeat_one"
|
||||
|
||||
# positions
|
||||
AT_BEGINNING = "at_beginning"
|
||||
AT_BEGINNING_LINE = "at_beginning_line"
|
||||
AT_BEGINNING_STRING = "at_beginning_string"
|
||||
AT_BOUNDARY = "at_boundary"
|
||||
AT_NON_BOUNDARY = "at_non_boundary"
|
||||
AT_END = "at_end"
|
||||
AT_END_LINE = "at_end_line"
|
||||
AT_END_STRING = "at_end_string"
|
||||
AT_LOC_BOUNDARY = "at_loc_boundary"
|
||||
AT_LOC_NON_BOUNDARY = "at_loc_non_boundary"
|
||||
AT_UNI_BOUNDARY = "at_uni_boundary"
|
||||
AT_UNI_NON_BOUNDARY = "at_uni_non_boundary"
|
||||
|
||||
# categories
|
||||
CATEGORY_DIGIT = "category_digit"
|
||||
CATEGORY_NOT_DIGIT = "category_not_digit"
|
||||
CATEGORY_SPACE = "category_space"
|
||||
CATEGORY_NOT_SPACE = "category_not_space"
|
||||
CATEGORY_WORD = "category_word"
|
||||
CATEGORY_NOT_WORD = "category_not_word"
|
||||
CATEGORY_LINEBREAK = "category_linebreak"
|
||||
CATEGORY_NOT_LINEBREAK = "category_not_linebreak"
|
||||
CATEGORY_LOC_WORD = "category_loc_word"
|
||||
CATEGORY_LOC_NOT_WORD = "category_loc_not_word"
|
||||
CATEGORY_UNI_DIGIT = "category_uni_digit"
|
||||
CATEGORY_UNI_NOT_DIGIT = "category_uni_not_digit"
|
||||
CATEGORY_UNI_SPACE = "category_uni_space"
|
||||
CATEGORY_UNI_NOT_SPACE = "category_uni_not_space"
|
||||
CATEGORY_UNI_WORD = "category_uni_word"
|
||||
CATEGORY_UNI_NOT_WORD = "category_uni_not_word"
|
||||
CATEGORY_UNI_LINEBREAK = "category_uni_linebreak"
|
||||
CATEGORY_UNI_NOT_LINEBREAK = "category_uni_not_linebreak"
|
||||
|
||||
OPCODES = [
|
||||
|
||||
# failure=0 success=1 (just because it looks better that way :-)
|
||||
FAILURE, SUCCESS,
|
||||
|
||||
ANY, ANY_ALL,
|
||||
ASSERT, ASSERT_NOT,
|
||||
AT,
|
||||
BRANCH,
|
||||
CALL,
|
||||
CATEGORY,
|
||||
CHARSET, BIGCHARSET,
|
||||
GROUPREF, GROUPREF_EXISTS, GROUPREF_IGNORE,
|
||||
IN, IN_IGNORE,
|
||||
INFO,
|
||||
JUMP,
|
||||
LITERAL, LITERAL_IGNORE,
|
||||
MARK,
|
||||
MAX_UNTIL,
|
||||
MIN_UNTIL,
|
||||
NOT_LITERAL, NOT_LITERAL_IGNORE,
|
||||
NEGATE,
|
||||
RANGE,
|
||||
REPEAT,
|
||||
REPEAT_ONE,
|
||||
SUBPATTERN,
|
||||
MIN_REPEAT_ONE
|
||||
|
||||
]
|
||||
|
||||
ATCODES = [
|
||||
AT_BEGINNING, AT_BEGINNING_LINE, AT_BEGINNING_STRING, AT_BOUNDARY,
|
||||
AT_NON_BOUNDARY, AT_END, AT_END_LINE, AT_END_STRING,
|
||||
AT_LOC_BOUNDARY, AT_LOC_NON_BOUNDARY, AT_UNI_BOUNDARY,
|
||||
AT_UNI_NON_BOUNDARY
|
||||
]
|
||||
|
||||
CHCODES = [
|
||||
CATEGORY_DIGIT, CATEGORY_NOT_DIGIT, CATEGORY_SPACE,
|
||||
CATEGORY_NOT_SPACE, CATEGORY_WORD, CATEGORY_NOT_WORD,
|
||||
CATEGORY_LINEBREAK, CATEGORY_NOT_LINEBREAK, CATEGORY_LOC_WORD,
|
||||
CATEGORY_LOC_NOT_WORD, CATEGORY_UNI_DIGIT, CATEGORY_UNI_NOT_DIGIT,
|
||||
CATEGORY_UNI_SPACE, CATEGORY_UNI_NOT_SPACE, CATEGORY_UNI_WORD,
|
||||
CATEGORY_UNI_NOT_WORD, CATEGORY_UNI_LINEBREAK,
|
||||
CATEGORY_UNI_NOT_LINEBREAK
|
||||
]
|
||||
|
||||
def makedict(list):
|
||||
d = {}
|
||||
i = 0
|
||||
for item in list:
|
||||
d[item] = i
|
||||
i = i + 1
|
||||
return d
|
||||
|
||||
OPCODES = makedict(OPCODES)
|
||||
ATCODES = makedict(ATCODES)
|
||||
CHCODES = makedict(CHCODES)
|
||||
|
||||
# replacement operations for "ignore case" mode
|
||||
OP_IGNORE = {
|
||||
GROUPREF: GROUPREF_IGNORE,
|
||||
IN: IN_IGNORE,
|
||||
LITERAL: LITERAL_IGNORE,
|
||||
NOT_LITERAL: NOT_LITERAL_IGNORE
|
||||
}
|
||||
|
||||
AT_MULTILINE = {
|
||||
AT_BEGINNING: AT_BEGINNING_LINE,
|
||||
AT_END: AT_END_LINE
|
||||
}
|
||||
|
||||
AT_LOCALE = {
|
||||
AT_BOUNDARY: AT_LOC_BOUNDARY,
|
||||
AT_NON_BOUNDARY: AT_LOC_NON_BOUNDARY
|
||||
}
|
||||
|
||||
AT_UNICODE = {
|
||||
AT_BOUNDARY: AT_UNI_BOUNDARY,
|
||||
AT_NON_BOUNDARY: AT_UNI_NON_BOUNDARY
|
||||
}
|
||||
|
||||
CH_LOCALE = {
|
||||
CATEGORY_DIGIT: CATEGORY_DIGIT,
|
||||
CATEGORY_NOT_DIGIT: CATEGORY_NOT_DIGIT,
|
||||
CATEGORY_SPACE: CATEGORY_SPACE,
|
||||
CATEGORY_NOT_SPACE: CATEGORY_NOT_SPACE,
|
||||
CATEGORY_WORD: CATEGORY_LOC_WORD,
|
||||
CATEGORY_NOT_WORD: CATEGORY_LOC_NOT_WORD,
|
||||
CATEGORY_LINEBREAK: CATEGORY_LINEBREAK,
|
||||
CATEGORY_NOT_LINEBREAK: CATEGORY_NOT_LINEBREAK
|
||||
}
|
||||
|
||||
CH_UNICODE = {
|
||||
CATEGORY_DIGIT: CATEGORY_UNI_DIGIT,
|
||||
CATEGORY_NOT_DIGIT: CATEGORY_UNI_NOT_DIGIT,
|
||||
CATEGORY_SPACE: CATEGORY_UNI_SPACE,
|
||||
CATEGORY_NOT_SPACE: CATEGORY_UNI_NOT_SPACE,
|
||||
CATEGORY_WORD: CATEGORY_UNI_WORD,
|
||||
CATEGORY_NOT_WORD: CATEGORY_UNI_NOT_WORD,
|
||||
CATEGORY_LINEBREAK: CATEGORY_UNI_LINEBREAK,
|
||||
CATEGORY_NOT_LINEBREAK: CATEGORY_UNI_NOT_LINEBREAK
|
||||
}
|
||||
|
||||
# flags
|
||||
SRE_FLAG_TEMPLATE = 1 # template mode (disable backtracking)
|
||||
SRE_FLAG_IGNORECASE = 2 # case insensitive
|
||||
SRE_FLAG_LOCALE = 4 # honour system locale
|
||||
SRE_FLAG_MULTILINE = 8 # treat target as multiline string
|
||||
SRE_FLAG_DOTALL = 16 # treat target as a single string
|
||||
SRE_FLAG_UNICODE = 32 # use unicode locale
|
||||
SRE_FLAG_VERBOSE = 64 # ignore whitespace and comments
|
||||
SRE_FLAG_DEBUG = 128 # debugging
|
||||
|
||||
# flags for INFO primitive
|
||||
SRE_INFO_PREFIX = 1 # has prefix
|
||||
SRE_INFO_LITERAL = 2 # entire pattern is literal (given by prefix)
|
||||
SRE_INFO_CHARSET = 4 # pattern starts with character from given set
|
||||
|
|
@ -0,0 +1,829 @@
|
|||
# flake8: noqa
|
||||
|
||||
#
|
||||
# Secret Labs' Regular Expression Engine
|
||||
#
|
||||
# convert re-style regular expression to sre pattern
|
||||
#
|
||||
# Copyright (c) 1998-2001 by Secret Labs AB. All rights reserved.
|
||||
#
|
||||
# See the sre.py file for information on usage and redistribution.
|
||||
#
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
"""Internal support module for sre"""
|
||||
|
||||
from sre_constants import *
|
||||
|
||||
SPECIAL_CHARS = ".\\[{()*+?^$|"
|
||||
REPEAT_CHARS = "*+?{"
|
||||
|
||||
DIGITS = set("0123456789")
|
||||
|
||||
OCTDIGITS = set("01234567")
|
||||
HEXDIGITS = set("0123456789abcdefABCDEF")
|
||||
|
||||
WHITESPACE = set(" \t\n\r\v\f")
|
||||
|
||||
ESCAPES = {
|
||||
r"\a": (LITERAL, ord("\a")),
|
||||
r"\b": (LITERAL, ord("\b")),
|
||||
r"\f": (LITERAL, ord("\f")),
|
||||
r"\n": (LITERAL, ord("\n")),
|
||||
r"\r": (LITERAL, ord("\r")),
|
||||
r"\t": (LITERAL, ord("\t")),
|
||||
r"\v": (LITERAL, ord("\v")),
|
||||
r"\\": (LITERAL, ord("\\"))
|
||||
}
|
||||
|
||||
CATEGORIES = {
|
||||
r"\A": (AT, AT_BEGINNING_STRING), # start of string
|
||||
r"\b": (AT, AT_BOUNDARY),
|
||||
r"\B": (AT, AT_NON_BOUNDARY),
|
||||
r"\d": (IN, [(CATEGORY, CATEGORY_DIGIT)]),
|
||||
r"\D": (IN, [(CATEGORY, CATEGORY_NOT_DIGIT)]),
|
||||
r"\s": (IN, [(CATEGORY, CATEGORY_SPACE)]),
|
||||
r"\S": (IN, [(CATEGORY, CATEGORY_NOT_SPACE)]),
|
||||
r"\w": (IN, [(CATEGORY, CATEGORY_WORD)]),
|
||||
r"\W": (IN, [(CATEGORY, CATEGORY_NOT_WORD)]),
|
||||
r"\Z": (AT, AT_END_STRING), # end of string
|
||||
}
|
||||
|
||||
FLAGS = {
|
||||
# standard flags
|
||||
"i": SRE_FLAG_IGNORECASE,
|
||||
"L": SRE_FLAG_LOCALE,
|
||||
"m": SRE_FLAG_MULTILINE,
|
||||
"s": SRE_FLAG_DOTALL,
|
||||
"x": SRE_FLAG_VERBOSE,
|
||||
# extensions
|
||||
"t": SRE_FLAG_TEMPLATE,
|
||||
"u": SRE_FLAG_UNICODE,
|
||||
}
|
||||
|
||||
|
||||
class Pattern:
|
||||
# master pattern object. keeps track of global attributes
|
||||
def __init__(self):
|
||||
self.flags = 0
|
||||
self.open = []
|
||||
self.groups = 1
|
||||
self.groupdict = {}
|
||||
self.lookbehind = 0
|
||||
|
||||
def opengroup(self, name=None):
|
||||
gid = self.groups
|
||||
self.groups = gid + 1
|
||||
if name is not None:
|
||||
ogid = self.groupdict.get(name, None)
|
||||
if ogid is not None:
|
||||
raise error(("redefinition of group name %s as group %d; "
|
||||
"was group %d" % (repr(name), gid, ogid)))
|
||||
self.groupdict[name] = gid
|
||||
self.open.append(gid)
|
||||
return gid
|
||||
|
||||
def closegroup(self, gid):
|
||||
self.open.remove(gid)
|
||||
|
||||
def checkgroup(self, gid):
|
||||
return gid < self.groups and gid not in self.open
|
||||
|
||||
|
||||
class SubPattern:
|
||||
# a subpattern, in intermediate form
|
||||
def __init__(self, pattern, data=None):
|
||||
self.pattern = pattern
|
||||
if data is None:
|
||||
data = []
|
||||
self.data = data
|
||||
self.width = None
|
||||
|
||||
def __repr__(self):
|
||||
return repr(self.data)
|
||||
|
||||
def __len__(self):
|
||||
return len(self.data)
|
||||
|
||||
def __delitem__(self, index):
|
||||
del self.data[index]
|
||||
|
||||
def __getitem__(self, index):
|
||||
if isinstance(index, slice):
|
||||
return SubPattern(self.pattern, self.data[index])
|
||||
return self.data[index]
|
||||
|
||||
def __setitem__(self, index, code):
|
||||
self.data[index] = code
|
||||
|
||||
def insert(self, index, code):
|
||||
self.data.insert(index, code)
|
||||
|
||||
def append(self, code):
|
||||
self.data.append(code)
|
||||
|
||||
def getwidth(self):
|
||||
# determine the width (min, max) for this subpattern
|
||||
if self.width:
|
||||
return self.width
|
||||
lo = hi = 0
|
||||
UNITCODES = (ANY, RANGE, IN, LITERAL, NOT_LITERAL, CATEGORY)
|
||||
REPEATCODES = (MIN_REPEAT, MAX_REPEAT)
|
||||
for op, av in self.data:
|
||||
if op is BRANCH:
|
||||
i = MAXREPEAT - 1
|
||||
j = 0
|
||||
for av in av[1]:
|
||||
l, h = av.getwidth()
|
||||
i = min(i, l)
|
||||
j = max(j, h)
|
||||
lo = lo + i
|
||||
hi = hi + j
|
||||
elif op is CALL:
|
||||
i, j = av.getwidth()
|
||||
lo = lo + i
|
||||
hi = hi + j
|
||||
elif op is SUBPATTERN:
|
||||
i, j = av[1].getwidth()
|
||||
lo = lo + i
|
||||
hi = hi + j
|
||||
elif op in REPEATCODES:
|
||||
i, j = av[2].getwidth()
|
||||
lo = lo + i * av[0]
|
||||
hi = hi + j * av[1]
|
||||
elif op in UNITCODES:
|
||||
lo = lo + 1
|
||||
hi = hi + 1
|
||||
elif op == SUCCESS:
|
||||
break
|
||||
self.width = min(lo, MAXREPEAT - 1), min(hi, MAXREPEAT)
|
||||
return self.width
|
||||
|
||||
|
||||
class Tokenizer:
|
||||
def __init__(self, string):
|
||||
self.string = string
|
||||
self.index = 0
|
||||
self.__next()
|
||||
|
||||
def __next(self):
|
||||
if self.index >= len(self.string):
|
||||
self.next = None
|
||||
return
|
||||
char = self.string[self.index]
|
||||
if char[0] == "\\":
|
||||
try:
|
||||
c = self.string[self.index + 1]
|
||||
except IndexError:
|
||||
raise error("bogus escape (end of line)")
|
||||
char = char + c
|
||||
self.index = self.index + len(char)
|
||||
self.next = char
|
||||
|
||||
def match(self, char, skip=1):
|
||||
if char == self.next:
|
||||
if skip:
|
||||
self.__next()
|
||||
return 1
|
||||
return 0
|
||||
|
||||
def get(self):
|
||||
this = self.next
|
||||
self.__next()
|
||||
return this
|
||||
|
||||
def tell(self):
|
||||
return self.index, self.next
|
||||
|
||||
def seek(self, index):
|
||||
self.index, self.next = index
|
||||
|
||||
|
||||
def isident(char):
|
||||
return "a" <= char <= "z" or "A" <= char <= "Z" or char == "_"
|
||||
|
||||
|
||||
def isdigit(char):
|
||||
return "0" <= char <= "9"
|
||||
|
||||
|
||||
def isname(name):
|
||||
# check that group name is a valid string
|
||||
if not isident(name[0]):
|
||||
return False
|
||||
for char in name[1:]:
|
||||
if not isident(char) and not isdigit(char):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _class_escape(source, escape):
|
||||
# handle escape code inside character class
|
||||
code = ESCAPES.get(escape)
|
||||
if code:
|
||||
return code
|
||||
code = CATEGORIES.get(escape)
|
||||
if code and code[0] == IN:
|
||||
return code
|
||||
try:
|
||||
c = escape[1:2]
|
||||
if c == "x":
|
||||
# hexadecimal escape (exactly two digits)
|
||||
while source.next in HEXDIGITS and len(escape) < 4:
|
||||
escape = escape + source.get()
|
||||
escape = escape[2:]
|
||||
if len(escape) != 2:
|
||||
raise error("bogus escape: %s" % repr("\\" + escape))
|
||||
return LITERAL, int(escape, 16) & 0xff
|
||||
elif c in OCTDIGITS:
|
||||
# octal escape (up to three digits)
|
||||
while source.next in OCTDIGITS and len(escape) < 4:
|
||||
escape = escape + source.get()
|
||||
escape = escape[1:]
|
||||
return LITERAL, int(escape, 8) & 0xff
|
||||
elif c in DIGITS:
|
||||
raise error("bogus escape: %s" % repr(escape))
|
||||
if len(escape) == 2:
|
||||
return LITERAL, ord(escape[1])
|
||||
except ValueError:
|
||||
pass
|
||||
raise error("bogus escape: %s" % repr(escape))
|
||||
|
||||
|
||||
def _escape(source, escape, state):
|
||||
# handle escape code in expression
|
||||
code = CATEGORIES.get(escape)
|
||||
if code:
|
||||
return code
|
||||
code = ESCAPES.get(escape)
|
||||
if code:
|
||||
return code
|
||||
try:
|
||||
c = escape[1:2]
|
||||
if c == "x":
|
||||
# hexadecimal escape
|
||||
while source.next in HEXDIGITS and len(escape) < 4:
|
||||
escape = escape + source.get()
|
||||
if len(escape) != 4:
|
||||
raise ValueError
|
||||
return LITERAL, int(escape[2:], 16) & 0xff
|
||||
elif c == "0":
|
||||
# octal escape
|
||||
while source.next in OCTDIGITS and len(escape) < 4:
|
||||
escape = escape + source.get()
|
||||
return LITERAL, int(escape[1:], 8) & 0xff
|
||||
elif c in DIGITS:
|
||||
# octal escape *or* decimal group reference (sigh)
|
||||
if source.next in DIGITS:
|
||||
escape = escape + source.get()
|
||||
if (escape[1] in OCTDIGITS and escape[2] in OCTDIGITS and
|
||||
source.next in OCTDIGITS):
|
||||
# got three octal digits; this is an octal escape
|
||||
escape = escape + source.get()
|
||||
return LITERAL, int(escape[1:], 8) & 0xff
|
||||
# not an octal escape, so this is a group reference
|
||||
group = int(escape[1:])
|
||||
if group < state.groups:
|
||||
if not state.checkgroup(group):
|
||||
raise error("cannot refer to open group")
|
||||
if state.lookbehind:
|
||||
import warnings
|
||||
warnings.warn('group references in lookbehind '
|
||||
'assertions are not supported',
|
||||
RuntimeWarning)
|
||||
return GROUPREF, group
|
||||
raise ValueError
|
||||
if len(escape) == 2:
|
||||
return LITERAL, ord(escape[1])
|
||||
except ValueError:
|
||||
pass
|
||||
raise error("bogus escape: %s" % repr(escape))
|
||||
|
||||
|
||||
def _parse_sub(source, state, nested=1):
|
||||
# parse an alternation: a|b|c
|
||||
|
||||
items = []
|
||||
itemsappend = items.append
|
||||
sourcematch = source.match
|
||||
while 1:
|
||||
itemsappend(_parse(source, state))
|
||||
if sourcematch("|"):
|
||||
continue
|
||||
if not nested:
|
||||
break
|
||||
if not source.next or sourcematch(")", 0):
|
||||
break
|
||||
else:
|
||||
raise error("pattern not properly closed")
|
||||
|
||||
if len(items) == 1:
|
||||
return items[0]
|
||||
|
||||
subpattern = SubPattern(state)
|
||||
subpatternappend = subpattern.append
|
||||
|
||||
# check if all items share a common prefix
|
||||
while 1:
|
||||
prefix = None
|
||||
for item in items:
|
||||
if not item:
|
||||
break
|
||||
if prefix is None:
|
||||
prefix = item[0]
|
||||
elif item[0] != prefix:
|
||||
break
|
||||
else:
|
||||
# all subitems start with a common "prefix".
|
||||
# move it out of the branch
|
||||
for item in items:
|
||||
del item[0]
|
||||
subpatternappend(prefix)
|
||||
continue # check next one
|
||||
break
|
||||
|
||||
# check if the branch can be replaced by a character set
|
||||
for item in items:
|
||||
if len(item) != 1 or item[0][0] != LITERAL:
|
||||
break
|
||||
else:
|
||||
# we can store this as a character set instead of a
|
||||
# branch (the compiler may optimize this even more)
|
||||
set = []
|
||||
setappend = set.append
|
||||
for item in items:
|
||||
setappend(item[0])
|
||||
subpatternappend((IN, set))
|
||||
return subpattern
|
||||
|
||||
subpattern.append((BRANCH, (None, items)))
|
||||
return subpattern
|
||||
|
||||
|
||||
def _parse_sub_cond(source, state, condgroup):
|
||||
item_yes = _parse(source, state)
|
||||
if source.match("|"):
|
||||
item_no = _parse(source, state)
|
||||
if source.match("|"):
|
||||
raise error("conditional backref with more than two branches")
|
||||
else:
|
||||
item_no = None
|
||||
if source.next and not source.match(")", 0):
|
||||
raise error("pattern not properly closed")
|
||||
subpattern = SubPattern(state)
|
||||
subpattern.append((GROUPREF_EXISTS, (condgroup, item_yes, item_no)))
|
||||
return subpattern
|
||||
|
||||
|
||||
_PATTERNENDERS = set("|)")
|
||||
_ASSERTCHARS = set("=!<")
|
||||
_LOOKBEHINDASSERTCHARS = set("=!")
|
||||
_REPEATCODES = set([MIN_REPEAT, MAX_REPEAT])
|
||||
|
||||
|
||||
def _parse(source, state):
|
||||
# parse a simple pattern
|
||||
subpattern = SubPattern(state)
|
||||
|
||||
# precompute constants into local variables
|
||||
subpatternappend = subpattern.append
|
||||
sourceget = source.get
|
||||
sourcematch = source.match
|
||||
_len = len
|
||||
PATTERNENDERS = _PATTERNENDERS
|
||||
ASSERTCHARS = _ASSERTCHARS
|
||||
LOOKBEHINDASSERTCHARS = _LOOKBEHINDASSERTCHARS
|
||||
REPEATCODES = _REPEATCODES
|
||||
|
||||
while 1:
|
||||
|
||||
if source.next in PATTERNENDERS:
|
||||
break # end of subpattern
|
||||
this = sourceget()
|
||||
if this is None:
|
||||
break # end of pattern
|
||||
|
||||
if state.flags & SRE_FLAG_VERBOSE:
|
||||
# skip whitespace and comments
|
||||
if this in WHITESPACE:
|
||||
continue
|
||||
if this == "#":
|
||||
while 1:
|
||||
this = sourceget()
|
||||
if this in (None, "\n"):
|
||||
break
|
||||
continue
|
||||
|
||||
if this and this[0] not in SPECIAL_CHARS:
|
||||
subpatternappend((LITERAL, ord(this)))
|
||||
|
||||
elif this == "[":
|
||||
# character set
|
||||
set = []
|
||||
setappend = set.append
|
||||
## if sourcematch(":"):
|
||||
## pass # handle character classes
|
||||
if sourcematch("^"):
|
||||
setappend((NEGATE, None))
|
||||
# check remaining characters
|
||||
start = set[:]
|
||||
while 1:
|
||||
this = sourceget()
|
||||
if this == "]" and set != start:
|
||||
break
|
||||
elif this and this[0] == "\\":
|
||||
code1 = _class_escape(source, this)
|
||||
elif this:
|
||||
code1 = LITERAL, ord(this)
|
||||
else:
|
||||
raise error("unexpected end of regular expression")
|
||||
if sourcematch("-"):
|
||||
# potential range
|
||||
this = sourceget()
|
||||
if this == "]":
|
||||
if code1[0] is IN:
|
||||
code1 = code1[1][0]
|
||||
setappend(code1)
|
||||
setappend((LITERAL, ord("-")))
|
||||
break
|
||||
elif this:
|
||||
if this[0] == "\\":
|
||||
code2 = _class_escape(source, this)
|
||||
else:
|
||||
code2 = LITERAL, ord(this)
|
||||
if code1[0] != LITERAL or code2[0] != LITERAL:
|
||||
raise error("bad character range")
|
||||
lo = code1[1]
|
||||
hi = code2[1]
|
||||
if hi < lo:
|
||||
raise error("bad character range")
|
||||
setappend((RANGE, (lo, hi)))
|
||||
else:
|
||||
raise error("unexpected end of regular expression")
|
||||
else:
|
||||
if code1[0] is IN:
|
||||
code1 = code1[1][0]
|
||||
setappend(code1)
|
||||
|
||||
# XXX: <fl> should move set optimization to compiler!
|
||||
if _len(set) == 1 and set[0][0] is LITERAL:
|
||||
subpatternappend(set[0]) # optimization
|
||||
elif _len(set) == 2 and set[0][0] is NEGATE and set[1][0] is LITERAL:
|
||||
subpatternappend((NOT_LITERAL, set[1][1])) # optimization
|
||||
else:
|
||||
# XXX: <fl> should add charmap optimization here
|
||||
subpatternappend((IN, set))
|
||||
|
||||
elif this and this[0] in REPEAT_CHARS:
|
||||
# repeat previous item
|
||||
if this == "?":
|
||||
min, max = 0, 1
|
||||
elif this == "*":
|
||||
min, max = 0, MAXREPEAT
|
||||
|
||||
elif this == "+":
|
||||
min, max = 1, MAXREPEAT
|
||||
elif this == "{":
|
||||
if source.next == "}":
|
||||
subpatternappend((LITERAL, ord(this)))
|
||||
continue
|
||||
here = source.tell()
|
||||
min, max = 0, MAXREPEAT
|
||||
lo = hi = ""
|
||||
while source.next in DIGITS:
|
||||
lo = lo + source.get()
|
||||
if sourcematch(","):
|
||||
while source.next in DIGITS:
|
||||
hi = hi + sourceget()
|
||||
else:
|
||||
hi = lo
|
||||
if not sourcematch("}"):
|
||||
subpatternappend((LITERAL, ord(this)))
|
||||
source.seek(here)
|
||||
continue
|
||||
if lo:
|
||||
min = int(lo)
|
||||
if min >= MAXREPEAT:
|
||||
raise OverflowError("the repetition number is too large")
|
||||
if hi:
|
||||
max = int(hi)
|
||||
if max >= MAXREPEAT:
|
||||
raise OverflowError("the repetition number is too large")
|
||||
if max < min:
|
||||
raise error("bad repeat interval")
|
||||
else:
|
||||
raise error("not supported")
|
||||
# figure out which item to repeat
|
||||
if subpattern:
|
||||
item = subpattern[-1:]
|
||||
else:
|
||||
item = None
|
||||
if not item or (_len(item) == 1 and item[0][0] == AT):
|
||||
raise error("nothing to repeat")
|
||||
if item[0][0] in REPEATCODES:
|
||||
raise error("multiple repeat")
|
||||
if sourcematch("?"):
|
||||
subpattern[-1] = (MIN_REPEAT, (min, max, item))
|
||||
else:
|
||||
subpattern[-1] = (MAX_REPEAT, (min, max, item))
|
||||
|
||||
elif this == ".":
|
||||
subpatternappend((ANY, None))
|
||||
|
||||
elif this == "(":
|
||||
group = 1
|
||||
name = None
|
||||
condgroup = None
|
||||
if sourcematch("?"):
|
||||
group = 0
|
||||
# options
|
||||
if sourcematch("P"):
|
||||
# python extensions
|
||||
if sourcematch("<"):
|
||||
# named group: skip forward to end of name
|
||||
name = ""
|
||||
while 1:
|
||||
char = sourceget()
|
||||
if char is None:
|
||||
raise error("unterminated name")
|
||||
if char == ">":
|
||||
break
|
||||
name = name + char
|
||||
group = 1
|
||||
if not name:
|
||||
raise error("missing group name")
|
||||
if not isname(name):
|
||||
raise error("bad character in group name %r" %
|
||||
name)
|
||||
elif sourcematch("="):
|
||||
# named backreference
|
||||
name = ""
|
||||
while 1:
|
||||
char = sourceget()
|
||||
if char is None:
|
||||
raise error("unterminated name")
|
||||
if char == ")":
|
||||
break
|
||||
name = name + char
|
||||
if not name:
|
||||
raise error("missing group name")
|
||||
if not isname(name):
|
||||
raise error("bad character in backref group name "
|
||||
"%r" % name)
|
||||
gid = state.groupdict.get(name)
|
||||
if gid is None:
|
||||
msg = "unknown group name: {0!r}".format(name)
|
||||
raise error(msg)
|
||||
if state.lookbehind:
|
||||
import warnings
|
||||
warnings.warn('group references in lookbehind '
|
||||
'assertions are not supported',
|
||||
RuntimeWarning)
|
||||
subpatternappend((GROUPREF, gid))
|
||||
continue
|
||||
else:
|
||||
char = sourceget()
|
||||
if char is None:
|
||||
raise error("unexpected end of pattern")
|
||||
raise error("unknown specifier: ?P%s" % char)
|
||||
elif sourcematch(":"):
|
||||
# non-capturing group
|
||||
group = 2
|
||||
elif sourcematch("#"):
|
||||
# comment
|
||||
while 1:
|
||||
if source.next is None or source.next == ")":
|
||||
break
|
||||
sourceget()
|
||||
if not sourcematch(")"):
|
||||
raise error("unbalanced parenthesis")
|
||||
continue
|
||||
elif source.next in ASSERTCHARS:
|
||||
# lookahead assertions
|
||||
char = sourceget()
|
||||
dir = 1
|
||||
if char == "<":
|
||||
if source.next not in LOOKBEHINDASSERTCHARS:
|
||||
raise error("syntax error")
|
||||
dir = -1 # lookbehind
|
||||
char = sourceget()
|
||||
state.lookbehind += 1
|
||||
p = _parse_sub(source, state)
|
||||
if dir < 0:
|
||||
state.lookbehind -= 1
|
||||
if not sourcematch(")"):
|
||||
raise error("unbalanced parenthesis")
|
||||
if char == "=":
|
||||
subpatternappend((ASSERT, (dir, p)))
|
||||
else:
|
||||
subpatternappend((ASSERT_NOT, (dir, p)))
|
||||
continue
|
||||
elif sourcematch("("):
|
||||
# conditional backreference group
|
||||
condname = ""
|
||||
while 1:
|
||||
char = sourceget()
|
||||
if char is None:
|
||||
raise error("unterminated name")
|
||||
if char == ")":
|
||||
break
|
||||
condname = condname + char
|
||||
group = 2
|
||||
if not condname:
|
||||
raise error("missing group name")
|
||||
if isname(condname):
|
||||
condgroup = state.groupdict.get(condname)
|
||||
if condgroup is None:
|
||||
msg = "unknown group name: {0!r}".format(condname)
|
||||
raise error(msg)
|
||||
else:
|
||||
try:
|
||||
condgroup = int(condname)
|
||||
except ValueError:
|
||||
raise error("bad character in group name")
|
||||
if state.lookbehind:
|
||||
import warnings
|
||||
warnings.warn('group references in lookbehind '
|
||||
'assertions are not supported',
|
||||
RuntimeWarning)
|
||||
else:
|
||||
# flags
|
||||
if not source.next in FLAGS:
|
||||
raise error("unexpected end of pattern")
|
||||
while source.next in FLAGS:
|
||||
state.flags = state.flags | FLAGS[sourceget()]
|
||||
if group:
|
||||
# parse group contents
|
||||
if group == 2:
|
||||
# anonymous group
|
||||
group = None
|
||||
else:
|
||||
group = state.opengroup(name)
|
||||
if condgroup:
|
||||
p = _parse_sub_cond(source, state, condgroup)
|
||||
else:
|
||||
p = _parse_sub(source, state)
|
||||
if not sourcematch(")"):
|
||||
raise error("unbalanced parenthesis")
|
||||
if group is not None:
|
||||
state.closegroup(group)
|
||||
subpatternappend((SUBPATTERN, (group, p)))
|
||||
else:
|
||||
while 1:
|
||||
char = sourceget()
|
||||
if char is None:
|
||||
raise error("unexpected end of pattern")
|
||||
if char == ")":
|
||||
break
|
||||
raise error("unknown extension")
|
||||
|
||||
elif this == "^":
|
||||
subpatternappend((AT, AT_BEGINNING))
|
||||
|
||||
elif this == "$":
|
||||
subpattern.append((AT, AT_END))
|
||||
|
||||
elif this and this[0] == "\\":
|
||||
code = _escape(source, this, state)
|
||||
subpatternappend(code)
|
||||
|
||||
else:
|
||||
raise error("parser error")
|
||||
|
||||
return subpattern
|
||||
|
||||
|
||||
def parse(str, flags=0, pattern=None):
|
||||
# parse 're' pattern into list of (opcode, argument) tuples
|
||||
|
||||
source = Tokenizer(str)
|
||||
|
||||
if pattern is None:
|
||||
pattern = Pattern()
|
||||
pattern.flags = flags
|
||||
pattern.str = str
|
||||
|
||||
p = _parse_sub(source, pattern, 0)
|
||||
|
||||
tail = source.get()
|
||||
if tail == ")":
|
||||
raise error("unbalanced parenthesis")
|
||||
elif tail:
|
||||
raise error("bogus characters at end of regular expression")
|
||||
|
||||
if not (flags & SRE_FLAG_VERBOSE) and p.pattern.flags & SRE_FLAG_VERBOSE:
|
||||
# the VERBOSE flag was switched on inside the pattern. to be
|
||||
# on the safe side, we'll parse the whole thing again...
|
||||
return parse(str, p.pattern.flags)
|
||||
|
||||
if flags & SRE_FLAG_DEBUG:
|
||||
p.dump()
|
||||
|
||||
return p
|
||||
|
||||
|
||||
def parse_template(source, pattern):
|
||||
# parse 're' replacement string into list of literals and
|
||||
# group references
|
||||
s = Tokenizer(source)
|
||||
sget = s.get
|
||||
p = []
|
||||
a = p.append
|
||||
|
||||
def literal(literal, p=p, pappend=a):
|
||||
if p and p[-1][0] is LITERAL:
|
||||
p[-1] = LITERAL, p[-1][1] + literal
|
||||
else:
|
||||
pappend((LITERAL, literal))
|
||||
|
||||
sep = source[:0]
|
||||
if type(sep) is type(""):
|
||||
makechar = chr
|
||||
else:
|
||||
makechar = unichr
|
||||
while 1:
|
||||
this = sget()
|
||||
if this is None:
|
||||
break # end of replacement string
|
||||
if this and this[0] == "\\":
|
||||
# group
|
||||
c = this[1:2]
|
||||
if c == "g":
|
||||
name = ""
|
||||
if s.match("<"):
|
||||
while 1:
|
||||
char = sget()
|
||||
if char is None:
|
||||
raise error("unterminated group name")
|
||||
if char == ">":
|
||||
break
|
||||
name = name + char
|
||||
if not name:
|
||||
raise error("missing group name")
|
||||
try:
|
||||
index = int(name)
|
||||
if index < 0:
|
||||
raise error("negative group number")
|
||||
except ValueError:
|
||||
if not isname(name):
|
||||
raise error("bad character in group name")
|
||||
try:
|
||||
index = pattern.groupindex[name]
|
||||
except KeyError:
|
||||
msg = "unknown group name: {0!r}".format(name)
|
||||
raise IndexError(msg)
|
||||
a((MARK, index))
|
||||
elif c == "0":
|
||||
if s.next in OCTDIGITS:
|
||||
this = this + sget()
|
||||
if s.next in OCTDIGITS:
|
||||
this = this + sget()
|
||||
literal(makechar(int(this[1:], 8) & 0xff))
|
||||
elif c in DIGITS:
|
||||
isoctal = False
|
||||
if s.next in DIGITS:
|
||||
this = this + sget()
|
||||
if (c in OCTDIGITS and this[2] in OCTDIGITS and
|
||||
s.next in OCTDIGITS):
|
||||
this = this + sget()
|
||||
isoctal = True
|
||||
literal(makechar(int(this[1:], 8) & 0xff))
|
||||
if not isoctal:
|
||||
a((MARK, int(this[1:])))
|
||||
else:
|
||||
try:
|
||||
this = makechar(ESCAPES[this][1])
|
||||
except KeyError:
|
||||
pass
|
||||
literal(this)
|
||||
else:
|
||||
literal(this)
|
||||
# convert template to groups and literals lists
|
||||
i = 0
|
||||
groups = []
|
||||
groupsappend = groups.append
|
||||
literals = [None] * len(p)
|
||||
for c, s in p:
|
||||
if c is MARK:
|
||||
groupsappend((i, s))
|
||||
# literal[i] is already None
|
||||
else:
|
||||
literals[i] = s
|
||||
i = i + 1
|
||||
return groups, literals
|
||||
|
||||
|
||||
def expand_template(template, match):
|
||||
g = match.group
|
||||
sep = match.string[:0]
|
||||
groups, literals = template
|
||||
literals = literals[:]
|
||||
try:
|
||||
for index, group in groups:
|
||||
literals[index] = s = g(group)
|
||||
if s is None:
|
||||
raise error("unmatched group")
|
||||
except IndexError:
|
||||
raise error("invalid group reference")
|
||||
return sep.join(literals)
|
|
@ -0,0 +1,2 @@
|
|||
def is_indexed_name(name):
|
||||
return isinstance(name, int) or (len(name) == 1 and '1' <= name <= '9')
|
|
@ -0,0 +1,114 @@
|
|||
import re
|
||||
import logging
|
||||
|
||||
from gixy.core.regexp import Regexp
|
||||
from gixy.core.context import get_context
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
# See ngx_http_script_compile in http/ngx_http_script.c
|
||||
EXTRACT_RE = re.compile(r'\$([1-9]|[a-z_][a-z0-9_]*|\{[a-z0-9_]+\})', re.IGNORECASE)
|
||||
|
||||
|
||||
def compile_script(script):
|
||||
depends = []
|
||||
context = get_context()
|
||||
for i, var in enumerate(EXTRACT_RE.split(str(script))):
|
||||
if i % 2:
|
||||
# Variable
|
||||
var = var.strip('{}\x20')
|
||||
var = context.get_var(var)
|
||||
if var:
|
||||
depends.append(var)
|
||||
elif var:
|
||||
# Literal
|
||||
depends.append(Variable(name=None, value=var, have_script=False))
|
||||
return depends
|
||||
|
||||
|
||||
class Variable(object):
|
||||
def __init__(self, name, value=None, boundary=None, provider=None, have_script=True):
|
||||
self.name = name
|
||||
self.value = value
|
||||
self.regexp = None
|
||||
self.depends = None
|
||||
self.boundary = boundary
|
||||
self.provider = provider
|
||||
if isinstance(value, Regexp):
|
||||
self.regexp = value
|
||||
elif have_script:
|
||||
self.depends = compile_script(value)
|
||||
|
||||
def can_contain(self, char):
|
||||
# First of all check boundary set
|
||||
if self.boundary and not self.boundary.can_contain(char):
|
||||
return False
|
||||
|
||||
# Then regexp
|
||||
if self.regexp:
|
||||
return self.regexp.can_contain(char, skip_literal=True)
|
||||
|
||||
# Then dependencies
|
||||
if self.depends:
|
||||
return any(dep.can_contain(char) for dep in self.depends)
|
||||
|
||||
# Otherwise user can't control value of this variable
|
||||
return False
|
||||
|
||||
def can_startswith(self, char):
|
||||
# First of all check boundary set
|
||||
if self.boundary and not self.boundary.can_startswith(char):
|
||||
return False
|
||||
|
||||
# Then regexp
|
||||
if self.regexp:
|
||||
return self.regexp.can_startswith(char)
|
||||
|
||||
# Then dependencies
|
||||
if self.depends:
|
||||
return self.depends[0].can_startswith(char)
|
||||
|
||||
# Otherwise user can't control value of this variable
|
||||
return False
|
||||
|
||||
def must_contain(self, char):
|
||||
# First of all check boundary set
|
||||
if self.boundary and self.boundary.must_contain(char):
|
||||
return True
|
||||
|
||||
# Then regexp
|
||||
if self.regexp:
|
||||
return self.regexp.must_contain(char)
|
||||
|
||||
# Then dependencies
|
||||
if self.depends:
|
||||
return any(dep.must_contain(char) for dep in self.depends)
|
||||
|
||||
# Otherwise checks literal
|
||||
return self.value and char in self.value
|
||||
|
||||
def must_startswith(self, char):
|
||||
# First of all check boundary set
|
||||
if self.boundary and self.boundary.must_startswith(char):
|
||||
return True
|
||||
|
||||
# Then regexp
|
||||
if self.regexp:
|
||||
return self.regexp.must_startswith(char)
|
||||
|
||||
# Then dependencies
|
||||
if self.depends:
|
||||
return self.depends[0].must_startswith(char)
|
||||
|
||||
# Otherwise checks literal
|
||||
return self.value and self.value[0] == char
|
||||
|
||||
@property
|
||||
def providers(self):
|
||||
result = []
|
||||
if self.provider:
|
||||
result.append(self.provider)
|
||||
if self.depends:
|
||||
for dep in self.depends:
|
||||
result += dep.providers
|
||||
return result
|
|
@ -0,0 +1,26 @@
|
|||
import os
|
||||
from gixy.directives.directive import Directive
|
||||
|
||||
|
||||
DIRECTIVES = {}
|
||||
|
||||
|
||||
def import_directives():
|
||||
files_list = os.listdir(os.path.dirname(__file__))
|
||||
for directive_file in files_list:
|
||||
if not directive_file.endswith(".py") or directive_file.startswith('_'):
|
||||
continue
|
||||
__import__('gixy.directives.' + os.path.splitext(directive_file)[0], None, None, [''])
|
||||
|
||||
|
||||
def get_all():
|
||||
if len(DIRECTIVES):
|
||||
return DIRECTIVES
|
||||
|
||||
import_directives()
|
||||
for klass in Directive.__subclasses__():
|
||||
if not klass.nginx_name:
|
||||
continue
|
||||
DIRECTIVES[klass.nginx_name] = klass
|
||||
|
||||
return DIRECTIVES
|
|
@ -0,0 +1,175 @@
|
|||
from cached_property import cached_property
|
||||
|
||||
from gixy.directives.directive import Directive
|
||||
from gixy.core.variable import Variable
|
||||
from gixy.core.regexp import Regexp
|
||||
|
||||
|
||||
def get_overrides():
|
||||
result = {}
|
||||
for klass in Block.__subclasses__():
|
||||
if not klass.nginx_name:
|
||||
continue
|
||||
|
||||
if not klass.__name__.endswith('Block'):
|
||||
continue
|
||||
|
||||
result[klass.nginx_name] = klass
|
||||
return result
|
||||
|
||||
|
||||
class Block(Directive):
|
||||
nginx_name = None
|
||||
is_block = True
|
||||
self_context = True
|
||||
|
||||
def __init__(self, name, args):
|
||||
super(Block, self).__init__(name, args)
|
||||
self.children = []
|
||||
|
||||
def some(self, name, flat=True):
|
||||
for child in self.children:
|
||||
if child.name == name:
|
||||
return child
|
||||
if flat and child.is_block and not child.self_context:
|
||||
result = child.some(name, flat=flat)
|
||||
if result:
|
||||
return result
|
||||
return None
|
||||
|
||||
def find(self, name, flat=False):
|
||||
result = []
|
||||
for child in self.children:
|
||||
if child.name == name:
|
||||
result.append(child)
|
||||
if flat and child.is_block and not child.self_context:
|
||||
result += child.find(name)
|
||||
return result
|
||||
|
||||
def find_recursive(self, name):
|
||||
result = []
|
||||
for child in self.children:
|
||||
if child.name == name:
|
||||
result.append(child)
|
||||
if child.is_block:
|
||||
result += child.find_recursive(name)
|
||||
return result
|
||||
|
||||
def append(self, directive):
|
||||
directive.set_parent(self)
|
||||
self.children.append(directive)
|
||||
|
||||
def __str__(self):
|
||||
return '{} {} {}'.format(self.name, ' '.join(self.args), '{')
|
||||
|
||||
|
||||
class Root(Block):
|
||||
nginx_name = None
|
||||
|
||||
def __init__(self):
|
||||
super(Root, self).__init__(None, [])
|
||||
|
||||
|
||||
class HttpBlock(Block):
|
||||
nginx_name = 'http'
|
||||
|
||||
def __init__(self, name, args):
|
||||
super(HttpBlock, self).__init__(name, args)
|
||||
|
||||
|
||||
class ServerBlock(Block):
|
||||
nginx_name = 'server'
|
||||
|
||||
def __init__(self, name, args):
|
||||
super(ServerBlock, self).__init__(name, args)
|
||||
|
||||
def get_names(self):
|
||||
return self.find('server_name')
|
||||
|
||||
def __str__(self):
|
||||
server_names = [str(sn) for sn in self.find('server_name')]
|
||||
if server_names:
|
||||
return 'server {{\n{}'.format('\n'.join(server_names[:2]))
|
||||
return 'server {'
|
||||
|
||||
|
||||
class LocationBlock(Block):
|
||||
nginx_name = 'location'
|
||||
provide_variables = True
|
||||
|
||||
def __init__(self, name, args):
|
||||
super(LocationBlock, self).__init__(name, args)
|
||||
if len(args) == 2:
|
||||
self.modifier, self.path = args
|
||||
else:
|
||||
self.modifier = None
|
||||
self.path = args[0]
|
||||
|
||||
@property
|
||||
def is_internal(self):
|
||||
return self.some('internal') is not None
|
||||
|
||||
@cached_property
|
||||
def variables(self):
|
||||
if not self.modifier or self.modifier not in ('~', '~*'):
|
||||
return []
|
||||
|
||||
regexp = Regexp(self.path, case_sensitive=self.modifier == '~')
|
||||
result = []
|
||||
for name, group in regexp.groups.items():
|
||||
result.append(Variable(name=name, value=group, boundary=None, provider=self))
|
||||
return result
|
||||
|
||||
|
||||
class IfBlock(Block):
|
||||
nginx_name = 'if'
|
||||
self_context = False
|
||||
|
||||
def __init__(self, name, args):
|
||||
super(IfBlock, self).__init__(name, args)
|
||||
self.operand = None
|
||||
self.value = None
|
||||
self.variable = None
|
||||
|
||||
if len(args) == 1:
|
||||
# if ($slow)
|
||||
self.variable = args[0]
|
||||
elif len(args) == 2:
|
||||
# if (!-e $foo)
|
||||
self.operand, self.value = args
|
||||
elif len(args) == 3:
|
||||
# if ($request_method = POST)
|
||||
self.variable, self.operand, self.value = args
|
||||
else:
|
||||
raise Exception('Unknown "if" definition')
|
||||
|
||||
def __str__(self):
|
||||
return '{} ({}) {{'.format(self.name, ' '.join(self.args))
|
||||
|
||||
|
||||
class IncludeBlock(Block):
|
||||
nginx_name = 'include'
|
||||
self_context = False
|
||||
|
||||
def __init__(self, name, args):
|
||||
super(IncludeBlock, self).__init__(name, args)
|
||||
self.file_path = args[0]
|
||||
|
||||
def __str__(self):
|
||||
return 'include {};'.format(self.file_path)
|
||||
|
||||
|
||||
class MapBlock(Block):
|
||||
nginx_name = 'map'
|
||||
self_context = False
|
||||
provide_variables = True
|
||||
|
||||
def __init__(self, name, args):
|
||||
super(MapBlock, self).__init__(name, args)
|
||||
self.source = args[0]
|
||||
self.variable = args[1].strip('$')
|
||||
|
||||
@cached_property
|
||||
def variables(self):
|
||||
# TODO(buglloc): Finish him!
|
||||
return [Variable(name=self.variable, value='', boundary=None, provider=self, have_script=False)]
|
|
@ -0,0 +1,119 @@
|
|||
from gixy.core.variable import Variable
|
||||
from gixy.core.regexp import Regexp
|
||||
|
||||
|
||||
def get_overrides():
|
||||
result = {}
|
||||
for klass in Directive.__subclasses__():
|
||||
if not klass.nginx_name:
|
||||
continue
|
||||
|
||||
if not klass.__name__.endswith('Directive'):
|
||||
continue
|
||||
|
||||
result[klass.nginx_name] = klass
|
||||
return result
|
||||
|
||||
|
||||
class Directive(object):
|
||||
nginx_name = None
|
||||
is_block = False
|
||||
provide_variables = False
|
||||
|
||||
def __init__(self, name, args, raw=None):
|
||||
self.name = name
|
||||
self.parent = None
|
||||
self.args = args
|
||||
self._raw = raw
|
||||
|
||||
def set_parent(self, parent):
|
||||
self.parent = parent
|
||||
|
||||
@property
|
||||
def parents(self):
|
||||
parent = self.parent
|
||||
while parent:
|
||||
yield parent
|
||||
parent = parent.parent
|
||||
|
||||
@property
|
||||
def variables(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def __str__(self):
|
||||
return '{} {};'.format(self.name, ' '.join(self.args))
|
||||
|
||||
|
||||
class AddHeaderDirective(Directive):
|
||||
nginx_name = 'add_header'
|
||||
|
||||
def __init__(self, name, args):
|
||||
super(AddHeaderDirective, self).__init__(name, args)
|
||||
self.header = args[0].lower()
|
||||
self.value = args[1]
|
||||
self.always = False
|
||||
if len(args) > 2 and args[2] == 'always':
|
||||
self.always = True
|
||||
|
||||
|
||||
class SetDirective(Directive):
|
||||
nginx_name = 'set'
|
||||
provide_variables = True
|
||||
|
||||
def __init__(self, name, args):
|
||||
super(SetDirective, self).__init__(name, args)
|
||||
self.variable = args[0].strip('$')
|
||||
self.value = args[1]
|
||||
|
||||
@property
|
||||
def variables(self):
|
||||
return [Variable(name=self.variable, value=self.value, provider=self)]
|
||||
|
||||
|
||||
class SetByLuaDirective(Directive):
|
||||
nginx_name = 'set_by_lua'
|
||||
provide_variables = True
|
||||
|
||||
def __init__(self, name, args):
|
||||
super(SetByLuaDirective, self).__init__(name, args)
|
||||
self.variable = args[0].strip('$')
|
||||
self.value = args[1]
|
||||
|
||||
@property
|
||||
def variables(self):
|
||||
return [Variable(name=self.variable, provider=self, have_script=False)]
|
||||
|
||||
|
||||
class RewriteDirective(Directive):
|
||||
nginx_name = 'rewrite'
|
||||
provide_variables = True
|
||||
boundary = Regexp('[^\s\r\n]')
|
||||
|
||||
def __init__(self, name, args):
|
||||
super(RewriteDirective, self).__init__(name, args)
|
||||
self.pattern = args[0]
|
||||
self.replace = args[1]
|
||||
self.flag = None
|
||||
if len(args) > 2:
|
||||
self.flag = args[2]
|
||||
|
||||
@property
|
||||
def variables(self):
|
||||
regexp = Regexp(self.pattern, case_sensitive=True)
|
||||
result = []
|
||||
for name, group in regexp.groups.items():
|
||||
result.append(Variable(name=name, value=group, boundary=self.boundary, provider=self))
|
||||
return result
|
||||
|
||||
|
||||
class RootDirective(Directive):
|
||||
nginx_name = 'root'
|
||||
provide_variables = True
|
||||
|
||||
def __init__(self, name, args):
|
||||
super(RootDirective, self).__init__(name, args)
|
||||
self.path = args[0]
|
||||
|
||||
@property
|
||||
def variables(self):
|
||||
return [Variable(name='document_root', value=self.path, provider=self)]
|
|
@ -0,0 +1,23 @@
|
|||
import os
|
||||
from gixy.formatters.base import BaseFormatter
|
||||
|
||||
FORMATTERS = {}
|
||||
|
||||
|
||||
def import_formatters():
|
||||
files_list = os.listdir(os.path.dirname(__file__))
|
||||
for formatter_file in files_list:
|
||||
if not formatter_file.endswith(".py") or formatter_file.startswith('_'):
|
||||
continue
|
||||
__import__('gixy.formatters.' + os.path.splitext(formatter_file)[0], None, None, [''])
|
||||
|
||||
|
||||
def get_all():
|
||||
if len(FORMATTERS):
|
||||
return FORMATTERS
|
||||
|
||||
import_formatters()
|
||||
for klass in BaseFormatter.__subclasses__():
|
||||
FORMATTERS[klass.__name__.replace('Formatter', '').lower()] = klass
|
||||
|
||||
return FORMATTERS
|
|
@ -0,0 +1,84 @@
|
|||
from __future__ import absolute_import
|
||||
|
||||
from gixy.directives import block
|
||||
|
||||
|
||||
class BaseFormatter(object):
|
||||
skip_parents = {block.Root, block.HttpBlock}
|
||||
|
||||
def format_reports(self, reports, stats):
|
||||
raise NotImplementedError("Formatter must override format_reports function")
|
||||
|
||||
def format(self, manager):
|
||||
reports = []
|
||||
for result in manager.get_results():
|
||||
report = self._prepare_result(manager.root,
|
||||
summary=result.summary,
|
||||
severity=result.severity,
|
||||
description=result.description,
|
||||
issues=result.issues,
|
||||
plugin=result.name,
|
||||
help_url=result.help_url)
|
||||
reports.extend(report)
|
||||
|
||||
return self.format_reports(reports, manager.stats)
|
||||
|
||||
def _prepare_result(self, root, issues, severity, summary, description, plugin, help_url):
|
||||
result = {}
|
||||
for issue in issues:
|
||||
report = dict(
|
||||
plugin=plugin,
|
||||
summary=issue.summary or summary,
|
||||
severity=issue.severity or severity,
|
||||
description=issue.description or description,
|
||||
help_url=issue.help_url or help_url,
|
||||
reason=issue.reason or '',
|
||||
)
|
||||
key = ''.join(report.values())
|
||||
report['directives'] = issue.directives
|
||||
if key in result:
|
||||
result[key]['directives'].extend(report['directives'])
|
||||
else:
|
||||
result[key] = report
|
||||
|
||||
for report in result.values():
|
||||
if report['directives']:
|
||||
config = self._resolve_config(root, report['directives'])
|
||||
else:
|
||||
config = ''
|
||||
|
||||
del report['directives']
|
||||
report['config'] = config
|
||||
yield report
|
||||
|
||||
def _resolve_config(self, root, directives):
|
||||
points = set()
|
||||
for directive in directives:
|
||||
points.add(directive)
|
||||
points.update(p for p in directive.parents)
|
||||
|
||||
result = self._traverse_tree(root, points, 0)
|
||||
return '\n'.join(result)
|
||||
|
||||
def _traverse_tree(self, tree, points, level):
|
||||
result = []
|
||||
for leap in tree.children:
|
||||
if leap not in points:
|
||||
continue
|
||||
printable = type(leap) not in self.skip_parents
|
||||
# Special hack for includes
|
||||
# TODO(buglloc): fix me
|
||||
have_parentheses = type(leap) != block.IncludeBlock
|
||||
|
||||
if printable:
|
||||
if leap.is_block:
|
||||
result.append('')
|
||||
directive = str(leap).replace('\n', '\n' + '\t' * (level + 1))
|
||||
result.append('{:s}{:s}'.format('\t' * level, directive))
|
||||
|
||||
if leap.is_block:
|
||||
result.extend(self._traverse_tree(leap, points, level + 1 if printable else level))
|
||||
if printable and have_parentheses:
|
||||
result.append('{:s}}}'.format('\t' * level))
|
||||
|
||||
return result
|
|
@ -0,0 +1,13 @@
|
|||
from __future__ import absolute_import
|
||||
from jinja2 import Environment, PackageLoader
|
||||
|
||||
from gixy.formatters.base import BaseFormatter
|
||||
|
||||
|
||||
class ConsoleFormatter(BaseFormatter):
|
||||
def __init__(self):
|
||||
env = Environment(loader=PackageLoader('gixy', 'formatters/templates'), trim_blocks=True, lstrip_blocks=True)
|
||||
self.template = env.get_template('console.j2')
|
||||
|
||||
def format_reports(self, reports, stats):
|
||||
return self.template.render(issues=reports, stats=stats)
|
|
@ -0,0 +1,10 @@
|
|||
from __future__ import absolute_import
|
||||
|
||||
import json
|
||||
|
||||
from gixy.formatters.base import BaseFormatter
|
||||
|
||||
|
||||
class JsonFormatter(BaseFormatter):
|
||||
def format_reports(self, reports, stats):
|
||||
return json.dumps(reports, sort_keys=True, indent=2, separators=(',', ': '))
|
|
@ -0,0 +1,36 @@
|
|||
{% set colors = {'DEF': '\033[0m', 'TITLE': '\033[95m', 'UNSPECIFIED': '\033[0m', 'LOW': '\033[94m', 'MEDIUM': '\033[93m', 'HIGH': '\033[91m'} %}
|
||||
|
||||
|
||||
{{ colors['TITLE'] }}==================== Results ==================={{ colors['DEF'] }}
|
||||
{% if not issues %}
|
||||
No issues found.
|
||||
{% else %}
|
||||
|
||||
{% for issue in issues|sort(attribute='severity') %}
|
||||
{{ colors[issue.severity] }}Problem: [{{ issue.plugin }}] {{ issue.summary }}
|
||||
{% if issue.description %}
|
||||
Description: {{ issue.description }}
|
||||
{% endif %}
|
||||
{% if issue.help_url %}
|
||||
Additional info: {{ issue.help_url }}
|
||||
{% endif %}
|
||||
{% if issue.reason %}
|
||||
Reason: {{ issue.reason }}
|
||||
{% endif %}
|
||||
{{ colors['DEF'] }}Pseudo config:{{ issue.config }}
|
||||
|
||||
{% if not loop.last %}
|
||||
--------8<--------8<--------8<--------8<--------
|
||||
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% if stats %}
|
||||
{{ colors['TITLE'] }}==================== Summary ==================={{ colors['DEF'] }}
|
||||
Total issues:
|
||||
Unspecified: {{ stats['UNSPECIFIED'] }}
|
||||
Low: {{ stats['LOW'] }}
|
||||
Medium: {{ stats['MEDIUM'] }}
|
||||
High: {{ stats['HIGH'] }}
|
||||
{% endif %}
|
|
@ -0,0 +1,35 @@
|
|||
|
||||
==================== Results ===================
|
||||
{% if not issues %}
|
||||
No issues found.
|
||||
{% else %}
|
||||
|
||||
{% for issue in issues|sort(attribute='severity') %}
|
||||
Problem: [{{ issue.plugin }}] {{ issue.summary }}
|
||||
Severity: {{ issue.severity }}
|
||||
{% if issue.description %}
|
||||
Description: {{ issue.description }}
|
||||
{% endif %}
|
||||
{% if issue.help_url %}
|
||||
Additional info: {{ issue.help_url }}
|
||||
{% endif %}
|
||||
{% if issue.reason %}
|
||||
Reason: {{ issue.reason }}
|
||||
{% endif %}
|
||||
Pseudo config: {{ issue.config }}
|
||||
|
||||
{% if not loop.last %}
|
||||
--------8<--------8<--------8<--------8<--------
|
||||
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% if stats %}
|
||||
==================== Summary ===================
|
||||
Total issues:
|
||||
Unspecified: {{ stats['UNSPECIFIED'] }}
|
||||
Low: {{ stats['LOW'] }}
|
||||
Medium: {{ stats['MEDIUM'] }}
|
||||
High: {{ stats['HIGH'] }}
|
||||
{% endif %}
|
|
@ -0,0 +1,13 @@
|
|||
from __future__ import absolute_import
|
||||
from jinja2 import Environment, PackageLoader
|
||||
|
||||
from gixy.formatters.base import BaseFormatter
|
||||
|
||||
|
||||
class TextFormatter(BaseFormatter):
|
||||
def __init__(self):
|
||||
env = Environment(loader=PackageLoader('gixy', 'formatters/templates'), trim_blocks=True, lstrip_blocks=True)
|
||||
self.template = env.get_template('text.j2')
|
||||
|
||||
def format_reports(self, reports, stats):
|
||||
return self.template.render(issues=reports, stats=stats)
|
|
@ -0,0 +1,142 @@
|
|||
"""NginxParser is a member object of the NginxConfigurator class."""
|
||||
import os
|
||||
import glob
|
||||
import logging
|
||||
import fnmatch
|
||||
|
||||
from pyparsing import ParseException
|
||||
|
||||
from gixy.parser import raw_parser
|
||||
from gixy.directives import block, directive
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NginxParser(object):
|
||||
|
||||
def __init__(self, file_path, allow_includes=True):
|
||||
self.base_file_path = file_path
|
||||
self.cwd = os.path.dirname(file_path)
|
||||
self.configs = {}
|
||||
self.is_dump = False
|
||||
self.allow_includes = allow_includes
|
||||
self.directives = {}
|
||||
self._init_directives()
|
||||
|
||||
def parse(self, file_path, root=None):
|
||||
LOG.debug("Parse file: {}".format(file_path))
|
||||
|
||||
if not root:
|
||||
root = block.Root()
|
||||
parser = raw_parser.RawParser()
|
||||
parsed = parser.parse(file_path)
|
||||
if len(parsed) and parsed[0].getName() == 'file_delimiter':
|
||||
# Were parse nginx dump
|
||||
LOG.info('Switched to parse nginx configuration dump.')
|
||||
root_filename = self._prepare_dump(parsed)
|
||||
self.is_dump = True
|
||||
self.cwd = os.path.dirname(root_filename)
|
||||
parsed = self.configs[root_filename]
|
||||
|
||||
self.parse_block(parsed, root)
|
||||
return root
|
||||
|
||||
def parse_block(self, parsed_block, parent):
|
||||
for parsed in parsed_block:
|
||||
parsed_type = parsed.getName()
|
||||
parsed_name = parsed[0]
|
||||
parsed_args = parsed[1:]
|
||||
if parsed_type == 'include':
|
||||
# TODO: WTF?!
|
||||
self._resolve_include(parsed_args, parent)
|
||||
else:
|
||||
directive_inst = self.directive_factory(parsed_type, parsed_name, parsed_args)
|
||||
if directive_inst:
|
||||
parent.append(directive_inst)
|
||||
|
||||
def directive_factory(self, parsed_type, parsed_name, parsed_args):
|
||||
klass = self._get_directive_class(parsed_type, parsed_name)
|
||||
if not klass:
|
||||
return None
|
||||
|
||||
if klass.is_block:
|
||||
args = [str(v).strip() for v in parsed_args[0]]
|
||||
children = parsed_args[1]
|
||||
|
||||
inst = klass(parsed_name, args)
|
||||
self.parse_block(children, inst)
|
||||
return inst
|
||||
else:
|
||||
args = [str(v).strip() for v in parsed_args]
|
||||
return klass(parsed_name, args)
|
||||
|
||||
def _get_directive_class(self, parsed_type, parsed_name):
|
||||
if parsed_type in self.directives and parsed_name in self.directives[parsed_type]:
|
||||
return self.directives[parsed_type][parsed_name]
|
||||
elif parsed_type == 'block':
|
||||
return block.Block
|
||||
elif parsed_type == 'directive':
|
||||
return directive.Directive
|
||||
elif parsed_type == 'unparsed_block':
|
||||
LOG.warning('Skip unparseable block: "%s"', parsed_name)
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
|
||||
def _init_directives(self):
|
||||
self.directives['block'] = block.get_overrides()
|
||||
self.directives['directive'] = directive.get_overrides()
|
||||
|
||||
def _resolve_include(self, args, parent):
|
||||
pattern = args[0]
|
||||
# TODO(buglloc): maybe file providers?
|
||||
if self.is_dump:
|
||||
return self._resolve_dump_include(pattern=pattern, parent=parent)
|
||||
if not self.allow_includes:
|
||||
LOG.debug('Includes are disallowed, skip: {}'.format(pattern))
|
||||
return
|
||||
|
||||
return self._resolve_file_include(pattern=pattern, parent=parent)
|
||||
|
||||
def _resolve_file_include(self, pattern, parent):
|
||||
path = os.path.join(self.cwd, pattern)
|
||||
file_path = None
|
||||
for file_path in glob.iglob(path):
|
||||
include = block.IncludeBlock('include', [file_path])
|
||||
parent.append(include)
|
||||
try:
|
||||
self.parse(file_path, include)
|
||||
except ParseException as e:
|
||||
LOG.error('Failed to parse include "{file}": {error}'.format(file=file_path, error=str(e)))
|
||||
|
||||
if not file_path:
|
||||
LOG.warning("File not found: {}".format(path))
|
||||
|
||||
def _resolve_dump_include(self, pattern, parent):
|
||||
path = os.path.join(self.cwd, pattern)
|
||||
founded = False
|
||||
for file_path, parsed in self.configs.items():
|
||||
if fnmatch.fnmatch(file_path, path):
|
||||
founded = True
|
||||
include = block.IncludeBlock('include', [file_path])
|
||||
parent.append(include)
|
||||
try:
|
||||
self.parse_block(parsed, include)
|
||||
except ParseException as e:
|
||||
LOG.error('Failed to parse include "{file}": {error}'.format(file=file_path, error=str(e)))
|
||||
|
||||
if not founded:
|
||||
LOG.warning("File not found: {}".format(path))
|
||||
|
||||
def _prepare_dump(self, parsed_block):
|
||||
filename = ''
|
||||
root_filename = ''
|
||||
for parsed in parsed_block:
|
||||
if parsed.getName() == 'file_delimiter':
|
||||
if not filename:
|
||||
root_filename = parsed[0]
|
||||
filename = parsed[0]
|
||||
self.configs[filename] = []
|
||||
continue
|
||||
self.configs[filename].append(parsed)
|
||||
return root_filename
|
|
@ -0,0 +1,164 @@
|
|||
"""Very low-level nginx config parser based on pyparsing."""
|
||||
import re
|
||||
import logging
|
||||
from cached_property import cached_property
|
||||
|
||||
from pyparsing import (
|
||||
Literal, Suppress, White, Word, alphanums, Forward, Group, Optional, Combine,
|
||||
Keyword, OneOrMore, ZeroOrMore, Regex, QuotedString, nestedExpr)
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NginxQuotedString(QuotedString):
|
||||
def __init__(self, quoteChar):
|
||||
super(NginxQuotedString, self).__init__(quoteChar, escChar='\\', multiline=True)
|
||||
# Nginx parse quoted values in special manner:
|
||||
# '^https?:\/\/yandex\.ru\/\00\'\"' -> ^https?:\/\/yandex\.ru\/\00'"
|
||||
# TODO(buglloc): research and find another special characters!
|
||||
|
||||
self.escCharReplacePattern = '\\\\(\'|")'
|
||||
|
||||
|
||||
class RawParser(object):
|
||||
"""
|
||||
A class that parses nginx configuration with pyparsing
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._script = None
|
||||
|
||||
def parse(self, file_path):
|
||||
"""
|
||||
Returns the parsed tree.
|
||||
"""
|
||||
# Temporary, dirty hack :(
|
||||
content = open(file_path).read()
|
||||
content = re.sub(r'(if\s.+)\)\)(\s*\{)?$', '\\1) )\\2', content, flags=re.MULTILINE)
|
||||
return self.script.parseString(content, parseAll=True)
|
||||
# return self.script.parseFile(file_path, parseAll=True)
|
||||
|
||||
@cached_property
|
||||
def script(self):
|
||||
# constants
|
||||
left_bracket = Suppress("{")
|
||||
right_bracket = Suppress("}")
|
||||
semicolon = Suppress(";")
|
||||
space = White().suppress()
|
||||
keyword = Word(alphanums + ".+-_/")
|
||||
path = Word(alphanums + ".-_/")
|
||||
variable = Word("$_-" + alphanums)
|
||||
value_wq = Regex(r'(?:\([^\s;]*\)|\$\{\w+\}|[^\s;(){}])+')
|
||||
value_sq = NginxQuotedString(quoteChar="'")
|
||||
value_dq = NginxQuotedString(quoteChar='"')
|
||||
value = (value_dq | value_sq | value_wq)
|
||||
# modifier for location uri [ = | ~ | ~* | ^~ ]
|
||||
location_modifier = (
|
||||
Keyword("=") |
|
||||
Keyword("~*") | Keyword("~") |
|
||||
Keyword("^~"))
|
||||
# modifier for if statement
|
||||
if_modifier = Combine(Optional("!") + (
|
||||
Keyword("=") |
|
||||
Keyword("~*") | Keyword("~") |
|
||||
(Literal("-") + (Literal("f") | Literal("d") | Literal("e") | Literal("x")))))
|
||||
condition = (
|
||||
(if_modifier + Optional(space) + value) |
|
||||
(variable + Optional(space + if_modifier + Optional(space) + value))
|
||||
)
|
||||
|
||||
# rules
|
||||
include = (
|
||||
Keyword("include") +
|
||||
space +
|
||||
value +
|
||||
semicolon
|
||||
)("include")
|
||||
|
||||
directive = (
|
||||
keyword +
|
||||
ZeroOrMore(space + value) +
|
||||
semicolon
|
||||
)("directive")
|
||||
|
||||
file_delimiter = (
|
||||
Suppress("# configuration file ") +
|
||||
path +
|
||||
Suppress(":")
|
||||
)("file_delimiter")
|
||||
|
||||
comment = (
|
||||
Suppress('#') +
|
||||
Regex(r".*")
|
||||
)("comment")
|
||||
|
||||
hash_value = Group(
|
||||
value +
|
||||
ZeroOrMore(space + value) +
|
||||
semicolon
|
||||
)("hash_value")
|
||||
|
||||
generic_block = Forward()
|
||||
if_block = Forward()
|
||||
location_block = Forward()
|
||||
hash_block = Forward()
|
||||
unparsed_block = Forward()
|
||||
|
||||
sub_block = OneOrMore(Group(if_block |
|
||||
location_block |
|
||||
hash_block |
|
||||
generic_block |
|
||||
include |
|
||||
directive |
|
||||
file_delimiter |
|
||||
comment |
|
||||
unparsed_block))
|
||||
|
||||
if_block << (
|
||||
Keyword("if") +
|
||||
Suppress("(") +
|
||||
Group(condition) +
|
||||
Suppress(")") +
|
||||
Group(
|
||||
left_bracket +
|
||||
Optional(sub_block) +
|
||||
right_bracket)
|
||||
)("block")
|
||||
|
||||
location_block << (
|
||||
Keyword("location") +
|
||||
Group(
|
||||
Optional(space + location_modifier) +
|
||||
Optional(space) + value) +
|
||||
Group(
|
||||
left_bracket +
|
||||
Optional(sub_block) +
|
||||
right_bracket)
|
||||
)("block")
|
||||
|
||||
hash_block << (
|
||||
keyword +
|
||||
Group(OneOrMore(space + variable)) +
|
||||
Group(
|
||||
left_bracket +
|
||||
Optional(OneOrMore(hash_value)) +
|
||||
right_bracket)
|
||||
)("block")
|
||||
|
||||
generic_block << (
|
||||
keyword +
|
||||
Group(ZeroOrMore(space + variable)) +
|
||||
Group(
|
||||
left_bracket +
|
||||
Optional(sub_block) +
|
||||
right_bracket)
|
||||
)("block")
|
||||
|
||||
unparsed_block << (
|
||||
keyword +
|
||||
Group(ZeroOrMore(space + variable)) +
|
||||
nestedExpr(opener="{", closer="}")
|
||||
)("unparsed_block")
|
||||
|
||||
return sub_block
|
|
@ -0,0 +1,87 @@
|
|||
import re
|
||||
import logging
|
||||
import gixy
|
||||
from gixy.plugins.plugin import Plugin
|
||||
from gixy.core.regexp import Regexp
|
||||
from gixy.core.variable import EXTRACT_RE
|
||||
from gixy.core.utils import is_indexed_name
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
# TODO(buglloc): Complete it!
|
||||
|
||||
|
||||
class internal_rewrite(Plugin):
|
||||
"""
|
||||
Insecure example:
|
||||
location ~* ^/internal-proxy/(https?)/(.*?)/(.*) {
|
||||
internal;
|
||||
proxy_pass $1://$2/$3;
|
||||
}
|
||||
|
||||
rewrite "^/([^?.]+[^/?.])(?:\?(.*))?$" "/$1.xml" last;
|
||||
"""
|
||||
|
||||
summary = 'Some internal rewrite'
|
||||
severity = gixy.severity.HIGH
|
||||
description = 'Some descr'
|
||||
help_url = 'https://github.com/yandex/gixy/wiki/ru/internalrewrite'
|
||||
directives = ['location']
|
||||
|
||||
def audit(self, directive):
|
||||
if not directive.is_internal:
|
||||
# Not internal location
|
||||
return
|
||||
|
||||
values = _gen_location_values(directive)
|
||||
# print([x for x in values])
|
||||
for rewrite in directive.parent.find('rewrite', flat=True):
|
||||
if rewrite.flag not in {None, 'last', 'break'}:
|
||||
# Not internal rewrite
|
||||
continue
|
||||
rewrite_regex = _construct_rewrite_regex(rewrite)
|
||||
if not rewrite_regex:
|
||||
# We can't build results regexp :(
|
||||
continue
|
||||
|
||||
for value in values:
|
||||
if re.match(rewrite_regex, value):
|
||||
# YAY!
|
||||
self.add_issue([directive, rewrite])
|
||||
|
||||
|
||||
def _gen_location_values(location):
|
||||
if location.modifier not in ('~', '~*'):
|
||||
# Prefixed location
|
||||
return [location.path]
|
||||
|
||||
regex = Regexp(location.path, case_sensitive=location.modifier == '~*', strict=True)
|
||||
return regex.generate(char='a', anchored=False)
|
||||
|
||||
|
||||
def _construct_rewrite_regex(rewrite):
|
||||
regex = Regexp(rewrite.pattern, case_sensitive=True)
|
||||
parts = {}
|
||||
for name, group in regex.groups.items():
|
||||
parts[name] = group
|
||||
|
||||
return _compile_script(rewrite.replace, parts)
|
||||
|
||||
|
||||
def _compile_script(script, parts):
|
||||
result = []
|
||||
for i, var in enumerate(EXTRACT_RE.split(str(script))):
|
||||
if i % 2:
|
||||
# Variable
|
||||
var = var.strip('{}\x20')
|
||||
if is_indexed_name(var):
|
||||
var = int(var)
|
||||
if var not in parts:
|
||||
LOG.warn('Can\'t find variable "{}"'.format(var))
|
||||
return
|
||||
result.append(str(parts[var]))
|
||||
elif var:
|
||||
# Literal
|
||||
result.append(var)
|
||||
return ''.join(result)
|
|
@ -0,0 +1,46 @@
|
|||
import gixy
|
||||
from gixy.plugins.plugin import Plugin
|
||||
|
||||
|
||||
class add_header_multiline(Plugin):
|
||||
"""
|
||||
Insecure example:
|
||||
add_header Content-Security-Policy "
|
||||
default-src: 'none';
|
||||
img-src data: https://mc.yandex.ru https://yastatic.net *.yandex.net https://mc.yandex.${tld} https://mc.yandex.ru;
|
||||
font-src data: https://yastatic.net;";
|
||||
"""
|
||||
summary = 'Found a multi-line header.'
|
||||
severity = gixy.severity.LOW
|
||||
description = ('Multi-line headers are deprecated (see RFC 7230). '
|
||||
'Some clients never supports them (e.g. IE/Edge).')
|
||||
help_url = 'https://github.com/yandex/gixy/wiki/ru/addheadermultiline'
|
||||
directives = ['add_header', 'more_set_headers']
|
||||
|
||||
def audit(self, directive):
|
||||
header_values = get_header_values(directive)
|
||||
for value in header_values:
|
||||
if '\n\x20' in value or '\n\t' in value:
|
||||
self.add_issue(directive=directive)
|
||||
break
|
||||
|
||||
|
||||
def get_header_values(directive):
|
||||
if directive.name == 'add_header':
|
||||
return [directive.args[1]]
|
||||
|
||||
# See headers more documentation: https://github.com/openresty/headers-more-nginx-module#description
|
||||
result = []
|
||||
skip_next = False
|
||||
for arg in directive.args:
|
||||
if arg in {'-s', '-t'}:
|
||||
# Skip next value, because it's not a header
|
||||
skip_next = True
|
||||
elif arg.startswith('-'):
|
||||
# Skip any options
|
||||
pass
|
||||
elif skip_next:
|
||||
skip_next = False
|
||||
elif not skip_next:
|
||||
result.append(arg)
|
||||
return result
|
|
@ -0,0 +1,69 @@
|
|||
import gixy
|
||||
from gixy.plugins.plugin import Plugin
|
||||
|
||||
|
||||
class add_header_redefinition(Plugin):
|
||||
"""
|
||||
Insecure example:
|
||||
server {
|
||||
add_header X-Content-Type-Options nosniff;
|
||||
location / {
|
||||
add_header X-Frame-Options DENY;
|
||||
}
|
||||
}
|
||||
"""
|
||||
summary = 'Nested "add_header" drops parent headers.'
|
||||
severity = gixy.severity.MEDIUM
|
||||
description = ('"add_header" replaces ALL parent headers. '
|
||||
'See documentation: http://nginx.org/en/docs/http/ngx_http_headers_module.html#add_header')
|
||||
help_url = 'https://github.com/yandex/gixy/wiki/ru/addheaderredefinition'
|
||||
directives = ['server', 'location', 'if']
|
||||
options = {'headers': {'x-frame-options',
|
||||
'x-content-type-options',
|
||||
'x-xss-protection',
|
||||
'content-security-policy',
|
||||
'strict-transport-security',
|
||||
'cache-control'}
|
||||
}
|
||||
|
||||
def __init__(self, config):
|
||||
super(add_header_redefinition, self).__init__(config)
|
||||
self.interesting_headers = self.config.get('headers')
|
||||
|
||||
def audit(self, directive):
|
||||
if not directive.is_block:
|
||||
# Skip all not block directives
|
||||
return
|
||||
|
||||
actual_headers = get_headers(directive)
|
||||
if not actual_headers:
|
||||
return
|
||||
|
||||
for parent in directive.parents:
|
||||
parent_headers = get_headers(parent)
|
||||
if not parent_headers:
|
||||
continue
|
||||
|
||||
diff = (parent_headers - actual_headers) & self.interesting_headers
|
||||
|
||||
if len(diff):
|
||||
self._report_issue(directive, parent, diff)
|
||||
|
||||
break
|
||||
|
||||
def _report_issue(self, current, parent, diff):
|
||||
directives = []
|
||||
# Add headers from parent level
|
||||
directives.extend(parent.find('add_header'))
|
||||
# Add headers from current level
|
||||
directives.extend(current.find('add_header'))
|
||||
reason = 'Parent headers "{headers}" was dropped in current level'.format(headers='", "'.join(diff))
|
||||
self.add_issue(directive=directives, reason=reason)
|
||||
|
||||
|
||||
def get_headers(directive):
|
||||
headers = directive.find('add_header')
|
||||
if not headers:
|
||||
return set()
|
||||
|
||||
return set(map(lambda d: d.header, headers))
|
|
@ -0,0 +1,20 @@
|
|||
import gixy
|
||||
from gixy.plugins.plugin import Plugin
|
||||
|
||||
|
||||
class force_https(Plugin):
|
||||
"""
|
||||
Insecure example:
|
||||
rewrite ^.*/(foo)(/|/index.xml)?$ http://test.com/foo?;
|
||||
"""
|
||||
summary = 'Found redirection to HTTP URL.'
|
||||
severity = gixy.severity.LOW
|
||||
description = 'Should be https://... URL while redirection.'
|
||||
help_url = 'https://github.com/yandex/gixy/wiki/ru/forcehttps'
|
||||
directives = ['rewrite', 'return']
|
||||
|
||||
def audit(self, directive):
|
||||
for a in directive.args:
|
||||
if a.startswith('http://'):
|
||||
self.add_issue(directive=directive)
|
||||
break
|
|
@ -0,0 +1,23 @@
|
|||
import gixy
|
||||
from gixy.plugins.plugin import Plugin
|
||||
|
||||
|
||||
class host_spoofing(Plugin):
|
||||
"""
|
||||
Insecure example:
|
||||
proxy_set_header Host $http_host
|
||||
"""
|
||||
summary = 'The proxied Host header may be spoofed.'
|
||||
severity = gixy.severity.MEDIUM
|
||||
description = 'In most cases "$host" variable are more appropriate, just use it.'
|
||||
help_url = 'https://github.com/yandex/gixy/wiki/ru/hostspoofing'
|
||||
directives = ['proxy_set_header']
|
||||
|
||||
def audit(self, directive):
|
||||
name, value = directive.args
|
||||
if name.lower() != 'host':
|
||||
# Not a "Host" header
|
||||
return
|
||||
|
||||
if value == '$http_host' or value.startswith('$arg_'):
|
||||
self.add_issue(directive=directive)
|
|
@ -0,0 +1,43 @@
|
|||
import gixy
|
||||
from gixy.plugins.plugin import Plugin
|
||||
from gixy.core.variable import compile_script
|
||||
|
||||
|
||||
class http_splitting(Plugin):
|
||||
"""
|
||||
Insecure examples:
|
||||
rewrite ^ http://$host$uri;
|
||||
return 301 http://$host$uri;
|
||||
proxy_set_header "X-Original-Uri" $uri;
|
||||
proxy_pass http://upstream$document_uri;
|
||||
|
||||
location ~ /proxy/(a|b)/(\W*)$ {
|
||||
set $path $2;
|
||||
proxy_pass http://storage/$path;
|
||||
}
|
||||
"""
|
||||
|
||||
summary = 'Possible HTTP-Splitting vulnerability.'
|
||||
severity = gixy.severity.HIGH
|
||||
description = 'Using variables that can contain "\\n" may lead to http injection.'
|
||||
help_url = 'https://github.com/yandex/gixy/wiki/ru/httpsplitting'
|
||||
directives = ['rewrite', 'return', 'add_header', 'proxy_set_header', 'proxy_pass']
|
||||
|
||||
def audit(self, directive):
|
||||
value = _get_value(directive)
|
||||
if not value:
|
||||
return
|
||||
|
||||
for var in compile_script(value):
|
||||
if not var.can_contain('\n'):
|
||||
continue
|
||||
reason = 'At least variable "${var}" can contain "\\n"'.format(var=var.name)
|
||||
self.add_issue(directive=[directive] + var.providers, reason=reason)
|
||||
|
||||
|
||||
def _get_value(directive):
|
||||
if directive.name == 'proxy_pass' and len(directive.args) >= 1:
|
||||
return directive.args[0]
|
||||
elif len(directive.args) >= 2:
|
||||
return directive.args[1]
|
||||
return None
|
|
@ -0,0 +1,71 @@
|
|||
import re
|
||||
import logging
|
||||
import gixy
|
||||
from gixy.plugins.plugin import Plugin
|
||||
from gixy.core.regexp import Regexp
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class origins(Plugin):
|
||||
"""
|
||||
Insecure example:
|
||||
if ($http_referer !~ "^https?://([^/]+metrika.*yandex\.ru/"){
|
||||
add_header X-Frame-Options SAMEORIGIN;
|
||||
}
|
||||
"""
|
||||
summary = 'Validation regex for "origin" or "referrer" matches untrusted domain.'
|
||||
severity = gixy.severity.MEDIUM
|
||||
description = 'Improve the regular expression to match only trusted referrers.'
|
||||
help_url = 'https://github.com/yandex/gixy/wiki/ru/origins'
|
||||
directives = ['if']
|
||||
options = {
|
||||
'domains': ['*'],
|
||||
'https_only': False
|
||||
}
|
||||
|
||||
def __init__(self, config):
|
||||
super(origins, self).__init__(config)
|
||||
if self.config.get('domains') and self.config.get('domains')[0] and self.config.get('domains')[0] != '*':
|
||||
domains = '|'.join(re.escape(d) for d in self.config.get('domains'))
|
||||
else:
|
||||
domains = '[^/.]*\.[^/]{2,7}'
|
||||
|
||||
scheme = 'https{http}'.format(http=('?' if not self.config.get('https_only') else ''))
|
||||
regex = r'^{scheme}://(?:[^/.]*\.){{0,10}}(?:{domains})(?::\d*)?(?:/|\?|$)'.format(
|
||||
scheme=scheme,
|
||||
domains=domains
|
||||
)
|
||||
self.valid_re = re.compile(regex)
|
||||
|
||||
def audit(self, directive):
|
||||
if directive.operand not in {'~', '~*', '!~', '!~*'}:
|
||||
# Not regexp
|
||||
return
|
||||
|
||||
if directive.variable not in {'$http_referer', '$http_origin'}:
|
||||
# Not interesting
|
||||
return
|
||||
|
||||
invalid_referers = set()
|
||||
regexp = Regexp(directive.value, case_sensitive=(directive.operand in {'~', '!~'}))
|
||||
for value in regexp.generate('/', anchored=True):
|
||||
if value.startswith('^'):
|
||||
value = value[1:]
|
||||
else:
|
||||
value = 'http://evil.com/' + value
|
||||
|
||||
if value.endswith('$'):
|
||||
value = value[:-1]
|
||||
elif not value.endswith('/'):
|
||||
value += '.evil.com'
|
||||
|
||||
if not self.valid_re.match(value):
|
||||
invalid_referers.add(value)
|
||||
|
||||
if invalid_referers:
|
||||
invalid_referers = '", "'.join(invalid_referers)
|
||||
name = 'origin' if directive.variable == '$http_origin' else 'referrer'
|
||||
severity = gixy.severity.HIGH if directive.variable == '$http_origin' else gixy.severity.MEDIUM
|
||||
reason = 'Regex matches "{value}" as a valid {name}.'.format(value=invalid_referers, name=name)
|
||||
self.add_issue(directive=directive, reason=reason, severity=severity)
|
|
@ -0,0 +1,30 @@
|
|||
import gixy
|
||||
from gixy.core.issue import Issue
|
||||
|
||||
|
||||
class Plugin(object):
|
||||
summary = ''
|
||||
description = ''
|
||||
help_url = ''
|
||||
severity = gixy.severity.UNSPECIFIED
|
||||
directives = []
|
||||
options = {}
|
||||
|
||||
def __init__(self, config):
|
||||
self._issues = []
|
||||
self.config = config
|
||||
|
||||
def add_issue(self, directive, summary=None, severity=None, description=None, reason=None, help_url=None):
|
||||
self._issues.append(Issue(self, directives=directive, summary=summary, severity=severity,
|
||||
description=description, reason=reason, help_url=help_url))
|
||||
|
||||
def audit(self, directive):
|
||||
pass
|
||||
|
||||
@property
|
||||
def issues(self):
|
||||
return self._issues
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.__class__.__name__
|
|
@ -0,0 +1,62 @@
|
|||
import re
|
||||
|
||||
import gixy
|
||||
from gixy.plugins.plugin import Plugin
|
||||
from gixy.core.context import get_context
|
||||
from gixy.core.variable import compile_script
|
||||
|
||||
|
||||
class ssrf(Plugin):
|
||||
"""
|
||||
Insecure examples:
|
||||
location ~ /proxy/(.*)/(.*)/(.*)$ {
|
||||
set $scheme $1;
|
||||
set $host $2;
|
||||
set $path $3;
|
||||
proxy_pass $scheme://$host/$path;
|
||||
}
|
||||
|
||||
location /proxy/ {
|
||||
proxy_pass $arg_some;
|
||||
}
|
||||
"""
|
||||
|
||||
summary = 'Possible SSRF (Server Side Request Forgery) vulnerability.'
|
||||
severity = gixy.severity.HIGH
|
||||
description = 'The configuration may allow attacker to create a arbitrary requests from the vulnerable server.'
|
||||
help_url = 'https://github.com/yandex/gixy/wiki/ru/ssrf'
|
||||
directives = ['proxy_pass']
|
||||
|
||||
def __init__(self, config):
|
||||
super(ssrf, self).__init__(config)
|
||||
self.parse_uri_re = re.compile(r'(?P<scheme>[^?#/)]+://)?(?P<host>[^?#/)]+)')
|
||||
|
||||
def audit(self, directive):
|
||||
value = directive.args[0]
|
||||
if not value:
|
||||
return
|
||||
|
||||
context = get_context()
|
||||
if context.block.name == 'location' and context.block.is_internal:
|
||||
# Exclude internal locations
|
||||
return
|
||||
|
||||
parsed = self.parse_uri_re.match(value)
|
||||
if not parsed:
|
||||
return
|
||||
|
||||
res = self._check_script(parsed.group('scheme'), directive)
|
||||
if not res:
|
||||
self._check_script(parsed.group('host'), directive)
|
||||
|
||||
def _check_script(self, script, directive):
|
||||
for var in compile_script(script):
|
||||
if var.must_contain('/'):
|
||||
# Skip variable checks
|
||||
return False
|
||||
if var.can_contain('.'):
|
||||
# Yay! Our variable can contain any symbols!
|
||||
reason = 'At least variable "${var}" can contain untrusted user input'.format(var=var.name)
|
||||
self.add_issue(directive=[directive] + var.providers, reason=reason)
|
||||
return True
|
||||
return False
|
|
@ -0,0 +1,18 @@
|
|||
import gixy
|
||||
from gixy.plugins.plugin import Plugin
|
||||
|
||||
|
||||
class valid_referers(Plugin):
|
||||
"""
|
||||
Insecure example:
|
||||
valid_referers none server_names *.webvisor.com;
|
||||
"""
|
||||
summary = 'Used "none" as valid referer.'
|
||||
severity = gixy.severity.HIGH
|
||||
description = 'Never trust undefined referer.'
|
||||
help_url = 'https://github.com/yandex/gixy/wiki/ru/validreferers'
|
||||
directives = ['valid_referers']
|
||||
|
||||
def audit(self, directive):
|
||||
if 'none' in directive.args:
|
||||
self.add_issue(directive=directive)
|
|
@ -0,0 +1,4 @@
|
|||
nose>=1.3.7
|
||||
mock>=2.0.0
|
||||
coverage>=4.3
|
||||
flake8>=3.2
|
|
@ -0,0 +1,6 @@
|
|||
pyparsing>=1.5.5
|
||||
cached-property>=1.2.0
|
||||
argparse>=1.4.0
|
||||
six>=1.1.0
|
||||
Jinja2>=2.8
|
||||
ConfigArgParse>=0.11.0
|
|
@ -0,0 +1,40 @@
|
|||
import re
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
with open('gixy/__init__.py', 'r') as fd:
|
||||
version = re.search(r'^version\s*=\s*[\'"]([^\'"]*)[\'"]',
|
||||
fd.read(), re.MULTILINE).group(1)
|
||||
|
||||
if not version:
|
||||
raise RuntimeError('Cannot find version information')
|
||||
|
||||
setup(
|
||||
name='gixy',
|
||||
version=version,
|
||||
description='Nginx configuration [sec]analyzer',
|
||||
keywords='nginx security lint static-analysis',
|
||||
author='Yandex IS Team',
|
||||
url='https://github.com/yandex/gixy',
|
||||
install_requires=[
|
||||
'pyparsing>=1.5.5',
|
||||
'cached-property>=1.2.0',
|
||||
'argparse>=1.4.0',
|
||||
'six>=1.1.0',
|
||||
'Jinja2>=2.8',
|
||||
'ConfigArgParse>=0.11.0'
|
||||
],
|
||||
entry_points={
|
||||
'console_scripts': ['gixy=gixy.cli.main:main'],
|
||||
},
|
||||
packages=find_packages(exclude=['tests', 'tests.*']),
|
||||
classifiers=[
|
||||
'Development Status :: 3 - Alpha',
|
||||
'Environment :: Console',
|
||||
'Intended Audience :: System Administrators',
|
||||
'Intended Audience :: Developers',
|
||||
'Topic :: Security',
|
||||
'Topic :: Software Development :: Quality Assurance',
|
||||
'Topic :: Software Development :: Testing'
|
||||
],
|
||||
include_package_data=True,
|
||||
)
|
|
@ -0,0 +1,137 @@
|
|||
from nose.tools import with_setup, assert_equals, assert_not_equals, assert_true
|
||||
from gixy.core.context import get_context, pop_context, push_context, purge_context, CONTEXTS, Context
|
||||
from gixy.directives.block import Root
|
||||
from gixy.core.variable import Variable
|
||||
from gixy.core.regexp import Regexp
|
||||
|
||||
|
||||
def setup():
|
||||
assert_equals(len(CONTEXTS), 0)
|
||||
|
||||
|
||||
def tear_down():
|
||||
purge_context()
|
||||
|
||||
|
||||
@with_setup(setup, tear_down)
|
||||
def test_push_pop_context():
|
||||
root_a = Root()
|
||||
push_context(root_a)
|
||||
assert_equals(len(CONTEXTS), 1)
|
||||
root_b = Root()
|
||||
push_context(root_b)
|
||||
assert_equals(len(CONTEXTS), 2)
|
||||
|
||||
poped = pop_context()
|
||||
assert_equals(len(CONTEXTS), 1)
|
||||
assert_equals(poped.block, root_b)
|
||||
poped = pop_context()
|
||||
assert_equals(len(CONTEXTS), 0)
|
||||
assert_equals(poped.block, root_a)
|
||||
|
||||
|
||||
@with_setup(setup, tear_down)
|
||||
def test_push_get_purge_context():
|
||||
root = Root()
|
||||
push_context(root)
|
||||
assert_equals(len(CONTEXTS), 1)
|
||||
assert_equals(get_context().block, root)
|
||||
root = Root()
|
||||
push_context(root)
|
||||
assert_equals(len(CONTEXTS), 2)
|
||||
assert_equals(get_context().block, root)
|
||||
|
||||
purge_context()
|
||||
assert_equals(len(CONTEXTS), 0)
|
||||
|
||||
|
||||
@with_setup(setup, tear_down)
|
||||
def test_add_variables():
|
||||
context = push_context(Root())
|
||||
assert_equals(len(context.variables['index']), 0)
|
||||
assert_equals(len(context.variables['name']), 0)
|
||||
|
||||
one_str_var = Variable('1')
|
||||
context.add_var('1', one_str_var)
|
||||
one_int_var = Variable(1)
|
||||
context.add_var(1, one_int_var)
|
||||
some_var = Variable('some')
|
||||
context.add_var('some', some_var)
|
||||
|
||||
assert_equals(len(context.variables['index']), 1)
|
||||
assert_equals(context.variables['index'][1], one_int_var)
|
||||
assert_equals(len(context.variables['name']), 1)
|
||||
assert_equals(context.variables['name']['some'], some_var)
|
||||
context.clear_index_vars()
|
||||
assert_equals(len(context.variables['index']), 0)
|
||||
assert_equals(len(context.variables['name']), 1)
|
||||
assert_equals(context.variables['name']['some'], some_var)
|
||||
|
||||
|
||||
@with_setup(setup, tear_down)
|
||||
def test_get_variables():
|
||||
context = push_context(Root())
|
||||
assert_equals(len(context.variables['index']), 0)
|
||||
assert_equals(len(context.variables['name']), 0)
|
||||
|
||||
one_var = Variable(1)
|
||||
context.add_var(1, one_var)
|
||||
some_var = Variable('some')
|
||||
context.add_var('some', some_var)
|
||||
|
||||
assert_equals(context.get_var(1), one_var)
|
||||
assert_equals(context.get_var('some'), some_var)
|
||||
# Checks not existed variables, for now context may return None
|
||||
assert_equals(context.get_var(0), None)
|
||||
assert_equals(context.get_var('not_existed'), None)
|
||||
# Checks builtins variables
|
||||
assert_true(context.get_var('uri'))
|
||||
assert_true(context.get_var('document_uri'))
|
||||
assert_true(context.get_var('arg_asdsadasd'))
|
||||
assert_true(context.get_var('args'))
|
||||
|
||||
|
||||
@with_setup(setup, tear_down)
|
||||
def test_context_depend_variables():
|
||||
push_context(Root())
|
||||
assert_equals(len(get_context().variables['index']), 0)
|
||||
assert_equals(len(get_context().variables['name']), 0)
|
||||
|
||||
get_context().add_var(1, Variable(1, value='one'))
|
||||
get_context().add_var('some', Variable('some', value='some'))
|
||||
|
||||
assert_equals(get_context().get_var(1).value, 'one')
|
||||
assert_equals(get_context().get_var('some').value, 'some')
|
||||
|
||||
# Checks top context variables are still exists
|
||||
push_context(Root())
|
||||
assert_equals(get_context().get_var(1).value, 'one')
|
||||
assert_equals(get_context().get_var('some').value, 'some')
|
||||
|
||||
# Checks variable overriding
|
||||
get_context().add_var('some', Variable('some', value='some_new'))
|
||||
get_context().add_var('foo', Variable('foo', value='foo'))
|
||||
assert_not_equals(get_context().get_var('some').value, 'some')
|
||||
assert_equals(get_context().get_var('some').value, 'some_new')
|
||||
assert_equals(get_context().get_var('foo').value, 'foo')
|
||||
assert_equals(get_context().get_var(1).value, 'one')
|
||||
|
||||
# Checks variables after restore previous context
|
||||
pop_context()
|
||||
assert_not_equals(get_context().get_var('some').value, 'some_new')
|
||||
assert_equals(get_context().get_var('some').value, 'some')
|
||||
assert_equals(get_context().get_var('foo'), None)
|
||||
assert_equals(get_context().get_var(1).value, 'one')
|
||||
|
||||
|
||||
@with_setup(setup, tear_down)
|
||||
def test_push_failed_with_regexp_py35_gixy_10():
|
||||
push_context(Root())
|
||||
assert_equals(len(get_context().variables['index']), 0)
|
||||
assert_equals(len(get_context().variables['name']), 0)
|
||||
|
||||
regexp = Regexp('^/some/(.*?)')
|
||||
for name, group in regexp.groups.items():
|
||||
get_context().add_var(name, Variable(name=name, value=group))
|
||||
|
||||
push_context(Root())
|
|
@ -0,0 +1,401 @@
|
|||
from nose.tools import assert_true, assert_false, assert_equals
|
||||
from gixy.core.regexp import Regexp
|
||||
|
||||
'''
|
||||
CATEGORIES:
|
||||
sre_parse.CATEGORY_SPACE
|
||||
sre_parse.CATEGORY_NOT_SPACE
|
||||
sre_parse.CATEGORY_DIGIT
|
||||
sre_parse.CATEGORY_NOT_DIGIT
|
||||
sre_parse.CATEGORY_WORD
|
||||
sre_parse.CATEGORY_NOT_WORD
|
||||
ANY
|
||||
'''
|
||||
|
||||
|
||||
def test_positive_contains():
|
||||
cases = (
|
||||
(r'[a-z]', 'a'),
|
||||
(r'[a-z]*', 'a'),
|
||||
(r'[a-z]*?', 'a'),
|
||||
(r'[a-z]+?', 'a'),
|
||||
(r'[a-z]', 'z'),
|
||||
(r'(?:a|b)', 'b'),
|
||||
(r'(/|:|[a-z])', 'g'),
|
||||
(r'[^a-z]', '/'),
|
||||
(r'[^a-z]', '\n'),
|
||||
(r'[^0]', '9'),
|
||||
(r'[^0-2]', '3'),
|
||||
(r'[^0123a-z]', '9'),
|
||||
(r'\s', '\x20'),
|
||||
(r'[^\s]', 'a'),
|
||||
(r'\d', '1'),
|
||||
(r'[^\d]', 'b'),
|
||||
(r'\w', '_'),
|
||||
(r'[^\w]', '\n'),
|
||||
(r'\W', '\n'),
|
||||
(r'[^\W]', 'a'),
|
||||
(r'.', 'a')
|
||||
)
|
||||
for case in cases:
|
||||
regexp, char = case
|
||||
yield check_positive_contain, regexp, char
|
||||
|
||||
|
||||
def test_negative_contains():
|
||||
cases = (
|
||||
('[a-z]', '1'),
|
||||
('[a-z]*', '2'),
|
||||
('[a-z]*?', '3'),
|
||||
('[a-z]+?', '4'),
|
||||
('[a-z]', '\n'),
|
||||
('(?:a|b)', 'c'),
|
||||
('(/|:|[a-z])', '\n'),
|
||||
('[^a-z]', 'a'),
|
||||
('[^0]', '0'),
|
||||
('[^0-2]', '0'),
|
||||
('[^0123a-z]', 'z'),
|
||||
(r'\s', 'a'),
|
||||
(r'[^\s]', '\n'),
|
||||
(r'\d', 'f'),
|
||||
(r'[^\d]', '2'),
|
||||
(r'\w', '\n'),
|
||||
(r'[^\w]', '_'),
|
||||
(r'\W', 'a'),
|
||||
(r'[^\W]', '\n'),
|
||||
(r'.', '\n')
|
||||
)
|
||||
for case in cases:
|
||||
regexp, char = case
|
||||
yield check_negative_contain, regexp, char
|
||||
|
||||
|
||||
def test_groups_names():
|
||||
cases = (
|
||||
('foo', [0]),
|
||||
('(1)(2)(?:3)', [0, 1, 2]),
|
||||
('(1)((2)|(?:3))', [0, 1, 2, 3]),
|
||||
("(?'pcre_7'1as)(?P<outer>(?<inner>2)|(?:3))", [0, 1, 2, 3, 'pcre_7', 'outer', 'inner']),
|
||||
('/proxy/(?<proxy>.*)$', [0, 1, 'proxy'])
|
||||
)
|
||||
for case in cases:
|
||||
regexp, groups = case
|
||||
yield check_groups_names, regexp, groups
|
||||
|
||||
|
||||
def test_to_string():
|
||||
cases = (
|
||||
(r'foo', 'foo'),
|
||||
(r'(1)(2)(?:3)', '(1)(2)(?:3)'),
|
||||
(r'(1)((2)|(?:3))', '(1)((?:(2)|(?:3)))'),
|
||||
(r'\w|1|3-5|[a-z]', '(?:[\w]|1|3\\-5|[a-z])'),
|
||||
(r'(1|(?:3)|([4-6]))', '((?:1|(?:3)|([4-6])))'),
|
||||
(r'(1|(?:3)|(?P<aaa>[4-6]))', '((?:1|(?:3)|([4-6])))'),
|
||||
(r'^sss', '^sss'),
|
||||
(r'(^bb|11)$', '((?:^bb|11))$'),
|
||||
(r'(http|https)', '(http(?:|s))'),
|
||||
(r'1*', '1*'),
|
||||
(r'1*?', '1*?'),
|
||||
(r'1+', '1+'),
|
||||
)
|
||||
for case in cases:
|
||||
regexp, string = case
|
||||
yield check_to_string, regexp, string
|
||||
|
||||
|
||||
def test_positive_startswith():
|
||||
cases = (
|
||||
(r'foo', 'q', False),
|
||||
(r'foo', 'f', True),
|
||||
(r'^foo', 'f', False),
|
||||
(r'(^foo)', 'f', False),
|
||||
(r'(^foo)', 'f', True),
|
||||
(r'(^foo|g)', 'f', True),
|
||||
(r'(^foo|g)', 'g', True),
|
||||
(r'(^foo|g)', 'q', False),
|
||||
(r'^[^/]+', '\n', True),
|
||||
(r'/[^/]+', '/', True),
|
||||
(r'((a))', 'a', False),
|
||||
(r'((a))', 'b', False),
|
||||
(r'^[a-z]{0}0', '0', False),
|
||||
(r'^[a-z]{1}0', 'a', False),
|
||||
)
|
||||
for case in cases:
|
||||
regexp, check, strict = case
|
||||
yield check_positive_startswith, regexp, check, strict
|
||||
|
||||
|
||||
def test_negative_startswith():
|
||||
cases = (
|
||||
(r'foo', '\n', False),
|
||||
(r'foo', 'o', True),
|
||||
(r'^foo', 'o', False),
|
||||
(r'(^foo)', 'q', False),
|
||||
(r'(^foo)', 'q', True),
|
||||
(r'(^foo|g)', 'q', True),
|
||||
(r'(^foo|g)', 'o', True),
|
||||
(r'(^foo|g)', '\n', False),
|
||||
(r'^[^/]+', '/', True),
|
||||
(r'/[^/]+', 'a', True),
|
||||
(r'((abc)|(ss))', 'b', True),
|
||||
(r'^[a-z]{0}0', 'a', False),
|
||||
(r'^[a-z]{0}0', 'g', False),
|
||||
)
|
||||
for case in cases:
|
||||
regexp, check, strict = case
|
||||
yield check_negative_startswith, regexp, check, strict
|
||||
|
||||
|
||||
def test_positive_must_contain():
|
||||
cases = (
|
||||
(r'abc', 'a'),
|
||||
(r'abc', 'b'),
|
||||
(r'abc', 'c'),
|
||||
(r'3+', '3'),
|
||||
(r'[0]', '0'),
|
||||
(r'([0])', '0'),
|
||||
(r'(?:[0])', '0'),
|
||||
(r'(?:[0])|0|((((0))))', '0'),
|
||||
)
|
||||
for case in cases:
|
||||
regexp, char = case
|
||||
yield check_positive_must_contain, regexp, char
|
||||
|
||||
|
||||
def test_negative_must_contain():
|
||||
cases = (
|
||||
(r'[a-z]', '1'),
|
||||
(r'2{0}1', '2'),
|
||||
(r'3?', '3'),
|
||||
(r'3*', '3'),
|
||||
(r'3*?', '3'),
|
||||
(r'3+a', 'b'),
|
||||
(r'[a-z]', 'a'),
|
||||
(r'(?:a|b)', 'a'),
|
||||
(r'(?:a|b)', 'b'),
|
||||
(r'(/|:|[a-z])', '/'),
|
||||
(r'(/|:|[a-z])', 'z'),
|
||||
(r'[^a-z]', '\n'),
|
||||
(r'[^0]', '0'),
|
||||
(r'[^0-2]', '0'),
|
||||
(r'[^0123a-z]', 'z'),
|
||||
(r'\s', '\x20'),
|
||||
(r'[^\s]', '\n'),
|
||||
(r'\d', '3'),
|
||||
(r'[^\d]', 'a'),
|
||||
(r'\w', 'a'),
|
||||
(r'[^\w]', '\n'),
|
||||
(r'\W', '\n'),
|
||||
(r'[^\W]', 'a'),
|
||||
(r'.', '\n')
|
||||
)
|
||||
for case in cases:
|
||||
regexp, char = case
|
||||
yield check_negative_must_contain, regexp, char
|
||||
|
||||
|
||||
def test_positive_must_startswith():
|
||||
cases = (
|
||||
(r'foo', 'f', True),
|
||||
(r'^foo', 'f', False),
|
||||
(r'(^foo)', 'f', True),
|
||||
(r'^((a))', 'a', False),
|
||||
(r'((a))', 'a', True),
|
||||
(r'^[a-z]{0}0', '0', False),
|
||||
(r'^a{1}0', 'a', False),
|
||||
)
|
||||
for case in cases:
|
||||
regexp, check, strict = case
|
||||
yield check_positive_must_startswith, regexp, check, strict
|
||||
|
||||
|
||||
def test_negative_must_startswith():
|
||||
cases = (
|
||||
(r'foo', 'o', False),
|
||||
(r'^foo', 'o', False),
|
||||
(r'(^foo)', 'o', False),
|
||||
(r'[a-z]', '1', True),
|
||||
(r'[a-z]', 'a', True),
|
||||
(r'/[^/]+', 'a', True),
|
||||
(r'3?', '3', True),
|
||||
(r'3*', '3', True),
|
||||
(r'3*?', '3', True),
|
||||
(r'3+a', 'b', True),
|
||||
(r'^((a))', 'b', False),
|
||||
(r'((a))', 'a', False),
|
||||
(r'^a{0}0', 'a', False),
|
||||
)
|
||||
for case in cases:
|
||||
regexp, check, strict = case
|
||||
yield check_negative_must_startswith, regexp, check, strict
|
||||
|
||||
|
||||
def test_generate():
|
||||
cases = (
|
||||
(r'foo', {'foo'}),
|
||||
(r'^sss', {'^sss'}),
|
||||
(r'(1)(2)(3)', {'123'}),
|
||||
(r'(1)((2)|(?:3))', {'12', '13'}),
|
||||
(r'(^1?2?|aa/)', {'^', '^1', '^2', '^12', 'aa/'}),
|
||||
(r'^https?://yandex.ru', {'^http://yandex|ru', '^https://yandex|ru'}),
|
||||
(r'(^bb|11)$', {'^bb$', '11$'}),
|
||||
(r'(http|https)', {'http', 'https'}),
|
||||
(r'1*', {'', '11111'}),
|
||||
(r'1*?', {'', '11111'}),
|
||||
(r'1{0}?2', {'2'}),
|
||||
(r'1{0}2', {'2'}),
|
||||
(r'1+', {'11111'}),
|
||||
(r'[^/]?', {'', '|'}),
|
||||
(r'^http://(foo|bar)|baz', {'^http://foo', '^http://bar', 'baz'}),
|
||||
(r'[^\x00-\x7b|\x7e-\xff]', {'\x7d'}),
|
||||
(r'(a|b|c)', {'a', 'b', 'c'}),
|
||||
(r'[xyz]', {'x', 'y', 'z'})
|
||||
)
|
||||
for case in cases:
|
||||
regexp, values = case
|
||||
yield check_generate, regexp, values
|
||||
|
||||
|
||||
def test_strict_generate():
|
||||
reg = Regexp('^foo|bar', strict=True)
|
||||
assert_equals(sorted(reg.generate('|', anchored=True)), sorted({'^foo', '^bar'}))
|
||||
|
||||
|
||||
def test_gen_anchor():
|
||||
|
||||
reg = Regexp('^some$')
|
||||
val = next(reg.generate('', anchored=False))
|
||||
assert_equals(val, 'some')
|
||||
|
||||
reg = Regexp('^some$')
|
||||
val = next(reg.generate('', anchored=True))
|
||||
assert_equals(val, '^some$')
|
||||
|
||||
reg = Regexp('^some$', strict=True)
|
||||
val = next(reg.generate('', anchored=False))
|
||||
assert_equals(val, 'some')
|
||||
|
||||
reg = Regexp('^some$', strict=True)
|
||||
val = next(reg.generate('', anchored=True))
|
||||
assert_equals(val, '^some$')
|
||||
|
||||
|
||||
def test_group_can_contains():
|
||||
source = '/some/(?P<action>[^/:.]+)/'
|
||||
reg = Regexp(source)
|
||||
assert_true(reg.can_contain('\n'),
|
||||
'Whole regex "{}" can contains "{}"'.format(source, '\\n'))
|
||||
|
||||
assert_true(reg.group(0).can_contain('\n'),
|
||||
'Group 0 from regex "{}" can contains "{}"'.format(source, '\\n'))
|
||||
|
||||
assert_true(reg.group('action').can_contain('\n'),
|
||||
'Group "action" from regex "{}" can contains "{}"'.format(source, '\\n'))
|
||||
|
||||
assert_true(reg.group(1).can_contain('\n'),
|
||||
'Group 1 from regex "{}" can contains "{}"'.format(source, '\\n'))
|
||||
|
||||
assert_false(reg.group('action').can_contain('/'),
|
||||
'Group "action" from regex "{}" CAN\'T (!) contain "{}"'.format(source, '/'))
|
||||
|
||||
|
||||
def check_positive_contain(regexp, char):
|
||||
reg = Regexp(regexp, case_sensitive=True)
|
||||
assert_true(reg.can_contain(char),
|
||||
'"{}" should contain "{}"'.format(regexp, char))
|
||||
|
||||
reg = Regexp(regexp, case_sensitive=False)
|
||||
char = char.upper()
|
||||
assert_true(reg.can_contain(char),
|
||||
'"{}" (case insensitive) should contain "{}"'.format(regexp, char))
|
||||
|
||||
|
||||
def check_negative_contain(regexp, char):
|
||||
reg = Regexp(regexp, case_sensitive=True)
|
||||
assert_false(reg.can_contain(char),
|
||||
'"{}" should not contain "{}"'.format(regexp, char))
|
||||
|
||||
reg = Regexp(regexp, case_sensitive=False)
|
||||
char = char.upper()
|
||||
assert_false(reg.can_contain(char),
|
||||
'"{}" (case insensitive) should not contain "{}"'.format(regexp, char))
|
||||
|
||||
|
||||
def check_positive_startswith(regexp, char, strict):
|
||||
reg = Regexp(regexp, case_sensitive=True, strict=strict)
|
||||
assert_true(reg.can_startswith(char),
|
||||
'"{}" can start\'s with "{}"'.format(regexp, char))
|
||||
|
||||
reg = Regexp(regexp, case_sensitive=False, strict=strict)
|
||||
char = char.upper()
|
||||
assert_true(reg.can_startswith(char),
|
||||
'"{}" (case insensitive) can start\'s with "{}"'.format(regexp, char))
|
||||
|
||||
|
||||
def check_negative_startswith(regexp, char, strict):
|
||||
reg = Regexp(regexp, case_sensitive=True, strict=strict)
|
||||
assert_false(reg.can_startswith(char),
|
||||
'"{}" can\'t start\'s with "{}"'.format(regexp, char))
|
||||
|
||||
reg = Regexp(regexp, case_sensitive=False, strict=strict)
|
||||
char = char.upper()
|
||||
assert_false(reg.can_startswith(char),
|
||||
'"{}" (case insensitive) can\'t start\'s with "{}"'.format(regexp, char))
|
||||
|
||||
|
||||
def check_groups_names(regexp, groups):
|
||||
reg = Regexp(regexp)
|
||||
assert_equals(set(reg.groups.keys()), set(groups))
|
||||
|
||||
|
||||
def check_to_string(regexp, string):
|
||||
reg = Regexp(regexp)
|
||||
assert_equals(str(reg), string)
|
||||
|
||||
|
||||
def check_positive_must_contain(regexp, char):
|
||||
reg = Regexp(regexp, case_sensitive=True)
|
||||
assert_true(reg.must_contain(char),
|
||||
'"{}" must contain with "{}"'.format(regexp, char))
|
||||
|
||||
reg = Regexp(regexp, case_sensitive=False)
|
||||
char = char.upper()
|
||||
assert_true(reg.must_contain(char),
|
||||
'"{}" (case insensitive) must contain with "{}"'.format(regexp, char))
|
||||
|
||||
|
||||
def check_negative_must_contain(regexp, char):
|
||||
reg = Regexp(regexp, case_sensitive=True)
|
||||
assert_false(reg.must_contain(char),
|
||||
'"{}" must NOT contain with "{}"'.format(regexp, char))
|
||||
|
||||
reg = Regexp(regexp, case_sensitive=False)
|
||||
char = char.upper()
|
||||
assert_false(reg.must_contain(char),
|
||||
'"{}" (case insensitive) must NOT contain with "{}"'.format(regexp, char))
|
||||
|
||||
|
||||
def check_positive_must_startswith(regexp, char, strict):
|
||||
reg = Regexp(regexp, case_sensitive=True, strict=strict)
|
||||
assert_true(reg.must_startswith(char),
|
||||
'"{}" MUST start\'s with "{}"'.format(regexp, char))
|
||||
|
||||
reg = Regexp(regexp, case_sensitive=False, strict=strict)
|
||||
char = char.upper()
|
||||
assert_true(reg.must_startswith(char),
|
||||
'"{}" (case insensitive) MUST start\'s with "{}"'.format(regexp, char))
|
||||
|
||||
|
||||
def check_negative_must_startswith(regexp, char, strict):
|
||||
reg = Regexp(regexp, case_sensitive=True, strict=strict)
|
||||
assert_false(reg.must_startswith(char),
|
||||
'"{}" MUST NOT start\'s with "{}"'.format(regexp, char))
|
||||
|
||||
reg = Regexp(regexp, case_sensitive=False, strict=strict)
|
||||
char = char.upper()
|
||||
assert_false(reg.must_startswith(char),
|
||||
'"{}" (case insensitive) MUST NOT start\'s with "{}"'.format(regexp, char))
|
||||
|
||||
def check_generate(regexp, values):
|
||||
reg = Regexp(regexp)
|
||||
assert_equals(sorted(reg.generate('|', anchored=True)), sorted(values))
|
|
@ -0,0 +1,99 @@
|
|||
from nose.tools import assert_true, assert_false, assert_equals, with_setup
|
||||
from gixy.core.context import get_context, push_context, purge_context
|
||||
from gixy.directives.block import Root
|
||||
from gixy.core.regexp import Regexp
|
||||
from gixy.core.variable import Variable
|
||||
|
||||
def setup():
|
||||
push_context(Root())
|
||||
|
||||
|
||||
def tear_down():
|
||||
purge_context()
|
||||
|
||||
|
||||
@with_setup(setup, tear_down)
|
||||
def test_literal():
|
||||
var = Variable(name='simple', value='$uri', have_script=False)
|
||||
assert_false(var.depends)
|
||||
assert_false(var.regexp)
|
||||
assert_equals(var.value, '$uri')
|
||||
|
||||
assert_false(var.can_startswith('$'))
|
||||
assert_false(var.can_contain('i'))
|
||||
assert_true(var.must_contain('$'))
|
||||
assert_true(var.must_contain('u'))
|
||||
assert_false(var.must_contain('a'))
|
||||
assert_true(var.must_startswith('$'))
|
||||
assert_false(var.must_startswith('u'))
|
||||
|
||||
|
||||
@with_setup(setup, tear_down)
|
||||
def test_regexp():
|
||||
var = Variable(name='simple', value=Regexp('^/.*'))
|
||||
assert_false(var.depends)
|
||||
assert_true(var.regexp)
|
||||
|
||||
assert_true(var.can_startswith('/'))
|
||||
assert_false(var.can_startswith('a'))
|
||||
assert_true(var.can_contain('a'))
|
||||
assert_false(var.can_contain('\n'))
|
||||
assert_true(var.must_contain('/'))
|
||||
assert_false(var.must_contain('a'))
|
||||
assert_true(var.must_startswith('/'))
|
||||
assert_false(var.must_startswith('a'))
|
||||
|
||||
|
||||
@with_setup(setup, tear_down)
|
||||
def test_script():
|
||||
get_context().add_var('foo', Variable(name='foo', value=Regexp('.*')))
|
||||
var = Variable(name='simple', value='/$foo')
|
||||
assert_true(var.depends)
|
||||
assert_false(var.regexp)
|
||||
|
||||
assert_false(var.can_startswith('/'))
|
||||
assert_false(var.can_startswith('a'))
|
||||
assert_true(var.can_contain('/'))
|
||||
assert_true(var.can_contain('a'))
|
||||
assert_false(var.can_contain('\n'))
|
||||
assert_true(var.must_contain('/'))
|
||||
assert_false(var.must_contain('a'))
|
||||
assert_true(var.must_startswith('/'))
|
||||
assert_false(var.must_startswith('a'))
|
||||
|
||||
|
||||
@with_setup(setup, tear_down)
|
||||
def test_regexp_boundary():
|
||||
var = Variable(name='simple', value=Regexp('.*'), boundary=Regexp('/[a-z]', strict=True))
|
||||
assert_false(var.depends)
|
||||
assert_true(var.regexp)
|
||||
|
||||
assert_true(var.can_startswith('/'))
|
||||
assert_false(var.can_startswith('a'))
|
||||
assert_false(var.can_contain('/'))
|
||||
assert_true(var.can_contain('a'))
|
||||
assert_false(var.can_contain('0'))
|
||||
assert_false(var.can_contain('\n'))
|
||||
assert_true(var.must_contain('/'))
|
||||
assert_false(var.must_contain('a'))
|
||||
assert_true(var.must_startswith('/'))
|
||||
assert_false(var.must_startswith('a'))
|
||||
|
||||
|
||||
@with_setup(setup, tear_down)
|
||||
def test_script_boundary():
|
||||
get_context().add_var('foo', Variable(name='foo', value=Regexp('.*'), boundary=Regexp('[a-z]', strict=True)))
|
||||
var = Variable(name='simple', value='/$foo', boundary=Regexp('[/a-z0-9]', strict=True))
|
||||
assert_true(var.depends)
|
||||
assert_false(var.regexp)
|
||||
|
||||
assert_false(var.can_startswith('/'))
|
||||
assert_false(var.can_startswith('a'))
|
||||
assert_false(var.can_contain('/'))
|
||||
assert_true(var.can_contain('a'))
|
||||
assert_false(var.can_contain('\n'))
|
||||
assert_false(var.can_contain('0'))
|
||||
assert_true(var.must_contain('/'))
|
||||
assert_false(var.must_contain('a'))
|
||||
assert_true(var.must_startswith('/'))
|
||||
assert_false(var.must_startswith('a'))
|
|
@ -0,0 +1,208 @@
|
|||
from nose.tools import assert_equals, assert_is_instance, assert_is_not_none, assert_is_none, assert_true, assert_false
|
||||
import mock
|
||||
from six import StringIO
|
||||
from six.moves import builtins
|
||||
from gixy.parser.nginx_parser import NginxParser
|
||||
from gixy.directives.block import *
|
||||
|
||||
# TODO(buglloc): what about include block?
|
||||
|
||||
def _get_parsed(config):
|
||||
with mock.patch('%s.open' % builtins.__name__) as mock_open:
|
||||
mock_open.return_value = StringIO(config)
|
||||
root = NginxParser('/foo/bar', allow_includes=False).parse('/foo/bar')
|
||||
return root.children[0]
|
||||
|
||||
|
||||
def test_block():
|
||||
config = 'some {some;}'
|
||||
|
||||
directive = _get_parsed(config)
|
||||
assert_is_instance(directive, Block)
|
||||
assert_true(directive.is_block)
|
||||
assert_true(directive.self_context)
|
||||
assert_false(directive.provide_variables)
|
||||
|
||||
|
||||
def test_http():
|
||||
config = '''
|
||||
http {
|
||||
default_type application/octet-stream;
|
||||
sendfile on;
|
||||
keepalive_timeout 65;
|
||||
}
|
||||
'''
|
||||
|
||||
directive = _get_parsed(config)
|
||||
assert_is_instance(directive, HttpBlock)
|
||||
assert_true(directive.is_block)
|
||||
assert_true(directive.self_context)
|
||||
assert_false(directive.provide_variables)
|
||||
|
||||
|
||||
def test_server():
|
||||
config = '''
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
server_name cool.io;
|
||||
}
|
||||
|
||||
'''
|
||||
|
||||
directive = _get_parsed(config)
|
||||
assert_is_instance(directive, ServerBlock)
|
||||
assert_true(directive.is_block)
|
||||
assert_true(directive.self_context)
|
||||
assert_equals([d.args[0] for d in directive.get_names()], ['_', 'cool.io'])
|
||||
assert_false(directive.provide_variables)
|
||||
|
||||
|
||||
def test_location():
|
||||
config = '''
|
||||
location / {
|
||||
}
|
||||
'''
|
||||
|
||||
directive = _get_parsed(config)
|
||||
assert_is_instance(directive, LocationBlock)
|
||||
assert_true(directive.is_block)
|
||||
assert_true(directive.self_context)
|
||||
assert_true(directive.provide_variables)
|
||||
assert_is_none(directive.modifier)
|
||||
assert_equals(directive.path, '/')
|
||||
assert_false(directive.is_internal)
|
||||
|
||||
|
||||
def test_location_internal():
|
||||
config = '''
|
||||
location / {
|
||||
internal;
|
||||
}
|
||||
'''
|
||||
|
||||
directive = _get_parsed(config)
|
||||
assert_is_instance(directive, LocationBlock)
|
||||
assert_true(directive.is_internal)
|
||||
|
||||
|
||||
def test_location_modifier():
|
||||
config = '''
|
||||
location = / {
|
||||
}
|
||||
'''
|
||||
|
||||
directive = _get_parsed(config)
|
||||
assert_is_instance(directive, LocationBlock)
|
||||
assert_equals(directive.modifier, '=')
|
||||
assert_equals(directive.path, '/')
|
||||
|
||||
|
||||
def test_if():
|
||||
config = '''
|
||||
if ($some) {
|
||||
}
|
||||
'''
|
||||
|
||||
directive = _get_parsed(config)
|
||||
assert_is_instance(directive, IfBlock)
|
||||
assert_true(directive.is_block)
|
||||
assert_false(directive.self_context)
|
||||
assert_false(directive.provide_variables)
|
||||
assert_equals(directive.variable, '$some')
|
||||
assert_is_none(directive.operand)
|
||||
assert_is_none(directive.value)
|
||||
|
||||
|
||||
def test_if_modifier():
|
||||
config = '''
|
||||
if (-f /some) {
|
||||
}
|
||||
'''
|
||||
|
||||
directive = _get_parsed(config)
|
||||
assert_is_instance(directive, IfBlock)
|
||||
assert_equals(directive.operand, '-f')
|
||||
assert_equals(directive.value, '/some')
|
||||
assert_is_none(directive.variable)
|
||||
|
||||
|
||||
def test_if_variable():
|
||||
config = '''
|
||||
if ($http_some = '/some') {
|
||||
}
|
||||
'''
|
||||
|
||||
directive = _get_parsed(config)
|
||||
assert_is_instance(directive, IfBlock)
|
||||
assert_equals(directive.variable, '$http_some')
|
||||
assert_equals(directive.operand, '=')
|
||||
assert_equals(directive.value, '/some')
|
||||
|
||||
|
||||
def test_block_some_flat():
|
||||
config = '''
|
||||
some {
|
||||
default_type application/octet-stream;
|
||||
sendfile on;
|
||||
if (-f /some/) {
|
||||
keepalive_timeout 65;
|
||||
}
|
||||
}
|
||||
'''
|
||||
|
||||
directive = _get_parsed(config)
|
||||
for d in {'default_type', 'sendfile', 'keepalive_timeout'}:
|
||||
c = directive.some(d, flat=True)
|
||||
assert_is_not_none(c)
|
||||
assert_equals(c.name, d)
|
||||
|
||||
|
||||
def test_block_some_not_flat():
|
||||
config = '''
|
||||
some {
|
||||
default_type application/octet-stream;
|
||||
sendfile on;
|
||||
if (-f /some/) {
|
||||
keepalive_timeout 65;
|
||||
}
|
||||
}
|
||||
'''
|
||||
|
||||
directive = _get_parsed(config)
|
||||
c = directive.some('keepalive_timeout', flat=False)
|
||||
assert_is_none(c)
|
||||
|
||||
|
||||
def test_block_find_flat():
|
||||
config = '''
|
||||
some {
|
||||
directive 1;
|
||||
if (-f /some/) {
|
||||
directive 2;
|
||||
}
|
||||
}
|
||||
'''
|
||||
|
||||
directive = _get_parsed(config)
|
||||
finds = directive.find('directive', flat=True)
|
||||
assert_equals(len(finds), 2)
|
||||
assert_equals([x.name for x in finds], ['directive', 'directive'])
|
||||
assert_equals([x.args[0] for x in finds], ['1', '2'])
|
||||
|
||||
|
||||
def test_block_find_not_flat():
|
||||
config = '''
|
||||
some {
|
||||
directive 1;
|
||||
if (-f /some/) {
|
||||
directive 2;
|
||||
}
|
||||
}
|
||||
'''
|
||||
|
||||
directive = _get_parsed(config)
|
||||
finds = directive.find('directive', flat=False)
|
||||
assert_equals(len(finds), 1)
|
||||
assert_equals([x.name for x in finds], ['directive'])
|
||||
assert_equals([x.args[0] for x in finds], ['1'])
|
|
@ -0,0 +1,104 @@
|
|||
from nose.tools import assert_equals, assert_is_instance, assert_false, assert_true
|
||||
import mock
|
||||
from six import StringIO
|
||||
from six.moves import builtins
|
||||
from gixy.parser.nginx_parser import NginxParser
|
||||
from gixy.directives.directive import *
|
||||
|
||||
|
||||
def _get_parsed(config):
|
||||
with mock.patch('%s.open' % builtins.__name__) as mock_open:
|
||||
mock_open.return_value = StringIO(config)
|
||||
return NginxParser('/foo/bar', allow_includes=False).parse('/foo/bar').children[0]
|
||||
|
||||
|
||||
def test_directive():
|
||||
config = 'some "foo" "bar";'
|
||||
|
||||
directive = _get_parsed(config)
|
||||
assert_is_instance(directive, Directive)
|
||||
assert_equals(directive.name, 'some')
|
||||
assert_equals(directive.args, ['foo', 'bar'])
|
||||
assert_equals(str(directive), 'some foo bar;')
|
||||
|
||||
|
||||
def test_add_header():
|
||||
config = 'add_header "X-Foo" "bar";'
|
||||
|
||||
directive = _get_parsed(config)
|
||||
assert_is_instance(directive, AddHeaderDirective)
|
||||
assert_equals(directive.name, 'add_header')
|
||||
assert_equals(directive.args, ['X-Foo', 'bar'])
|
||||
assert_equals(directive.header, 'x-foo')
|
||||
assert_equals(directive.value, 'bar')
|
||||
assert_false(directive.always)
|
||||
assert_equals(str(directive), 'add_header X-Foo bar;')
|
||||
|
||||
|
||||
def test_add_header_always():
|
||||
config = 'add_header "X-Foo" "bar" always;'
|
||||
|
||||
directive = _get_parsed(config)
|
||||
assert_is_instance(directive, AddHeaderDirective)
|
||||
assert_equals(directive.name, 'add_header')
|
||||
assert_equals(directive.args, ['X-Foo', 'bar', 'always'])
|
||||
assert_equals(directive.header, 'x-foo')
|
||||
assert_equals(directive.value, 'bar')
|
||||
assert_true(directive.always)
|
||||
assert_equals(str(directive), 'add_header X-Foo bar always;')
|
||||
|
||||
|
||||
def test_set():
|
||||
config = 'set $foo bar;'
|
||||
|
||||
directive = _get_parsed(config)
|
||||
assert_is_instance(directive, SetDirective)
|
||||
assert_equals(directive.name, 'set')
|
||||
assert_equals(directive.args, ['$foo', 'bar'])
|
||||
assert_equals(directive.variable, 'foo')
|
||||
assert_equals(directive.value, 'bar')
|
||||
assert_equals(str(directive), 'set $foo bar;')
|
||||
assert_true(directive.provide_variables)
|
||||
|
||||
|
||||
def test_rewrite():
|
||||
config = 'rewrite ^ http://some;'
|
||||
|
||||
directive = _get_parsed(config)
|
||||
assert_is_instance(directive, RewriteDirective)
|
||||
assert_equals(directive.name, 'rewrite')
|
||||
assert_equals(directive.args, ['^', 'http://some'])
|
||||
assert_equals(str(directive), 'rewrite ^ http://some;')
|
||||
assert_true(directive.provide_variables)
|
||||
|
||||
assert_equals(directive.pattern, '^')
|
||||
assert_equals(directive.replace, 'http://some')
|
||||
assert_equals(directive.flag, None)
|
||||
|
||||
|
||||
def test_rewrite_flags():
|
||||
config = 'rewrite ^/(.*)$ http://some/$1 redirect;'
|
||||
|
||||
directive = _get_parsed(config)
|
||||
assert_is_instance(directive, RewriteDirective)
|
||||
assert_equals(directive.name, 'rewrite')
|
||||
assert_equals(directive.args, ['^/(.*)$', 'http://some/$1', 'redirect'])
|
||||
assert_equals(str(directive), 'rewrite ^/(.*)$ http://some/$1 redirect;')
|
||||
assert_true(directive.provide_variables)
|
||||
|
||||
assert_equals(directive.pattern, '^/(.*)$')
|
||||
assert_equals(directive.replace, 'http://some/$1')
|
||||
assert_equals(directive.flag, 'redirect')
|
||||
|
||||
|
||||
def test_root():
|
||||
config = 'root /var/www/html;'
|
||||
|
||||
directive = _get_parsed(config)
|
||||
assert_is_instance(directive, RootDirective)
|
||||
assert_equals(directive.name, 'root')
|
||||
assert_equals(directive.args, ['/var/www/html'])
|
||||
assert_equals(str(directive), 'root /var/www/html;')
|
||||
assert_true(directive.provide_variables)
|
||||
|
||||
assert_equals(directive.path, '/var/www/html')
|
|
@ -0,0 +1,114 @@
|
|||
from nose.tools import assert_is_instance, assert_equal
|
||||
import mock
|
||||
from six import StringIO
|
||||
from six.moves import builtins
|
||||
from gixy.parser.nginx_parser import NginxParser
|
||||
from gixy.directives.directive import *
|
||||
from gixy.directives.block import *
|
||||
|
||||
|
||||
def _parse(config):
|
||||
with mock.patch('%s.open' % builtins.__name__) as mock_open:
|
||||
mock_open.return_value = StringIO(config)
|
||||
return NginxParser('/foo/bar', allow_includes=False).parse('/foo/bar')
|
||||
|
||||
|
||||
def test_directive():
|
||||
configs = [
|
||||
'access_log syslog:server=127.0.0.1,tag=nginx_sentry toolsformat;',
|
||||
'user http;',
|
||||
'internal;',
|
||||
'set $foo "bar";',
|
||||
"set $foo 'bar';",
|
||||
'proxy_pass http://unix:/run/sock.socket;',
|
||||
'rewrite ^/([a-zA-Z0-9]+)$ /$1/${arg_v}.pb break;'
|
||||
]
|
||||
|
||||
expected = [
|
||||
[Directive],
|
||||
[Directive],
|
||||
[Directive],
|
||||
[Directive, SetDirective],
|
||||
[Directive],
|
||||
[Directive, RewriteDirective]
|
||||
]
|
||||
|
||||
for i, config in enumerate(configs):
|
||||
return assert_config, config, expected[i]
|
||||
|
||||
|
||||
def test_blocks():
|
||||
configs = [
|
||||
'if (-f /some) {}',
|
||||
'location / {}'
|
||||
]
|
||||
|
||||
expected = [
|
||||
[Directive, Block, IfBlock],
|
||||
[Directive, Block, LocationBlock],
|
||||
]
|
||||
|
||||
for i, config in enumerate(configs):
|
||||
yield assert_config, config, expected[i]
|
||||
|
||||
|
||||
def test_dump_simple():
|
||||
config = '''
|
||||
# configuration file /etc/nginx/nginx.conf:
|
||||
http {
|
||||
include sites/*.conf;
|
||||
}
|
||||
|
||||
# configuration file /etc/nginx/conf.d/listen:
|
||||
listen 80;
|
||||
|
||||
# configuration file /etc/nginx/sites/default.conf:
|
||||
server {
|
||||
include conf.d/listen;
|
||||
}
|
||||
'''
|
||||
|
||||
tree = _parse(config)
|
||||
assert_is_instance(tree, Directive)
|
||||
assert_is_instance(tree, Block)
|
||||
assert_is_instance(tree, Root)
|
||||
|
||||
assert_equal(len(tree.children), 1)
|
||||
http = tree.children[0]
|
||||
assert_is_instance(http, Directive)
|
||||
assert_is_instance(http, Block)
|
||||
assert_is_instance(http, HttpBlock)
|
||||
|
||||
assert_equal(len(http.children), 1)
|
||||
include_server = http.children[0]
|
||||
assert_is_instance(include_server, Directive)
|
||||
assert_is_instance(include_server, IncludeBlock)
|
||||
assert_equal(include_server.file_path, '/etc/nginx/sites/default.conf')
|
||||
|
||||
assert_equal(len(include_server.children), 1)
|
||||
server = include_server.children[0]
|
||||
assert_is_instance(server, Directive)
|
||||
assert_is_instance(server, Block)
|
||||
assert_is_instance(server, ServerBlock)
|
||||
|
||||
assert_equal(len(server.children), 1)
|
||||
include_listen = server.children[0]
|
||||
assert_is_instance(include_listen, Directive)
|
||||
assert_is_instance(include_listen, IncludeBlock)
|
||||
assert_equal(include_listen.file_path, '/etc/nginx/conf.d/listen')
|
||||
|
||||
assert_equal(len(include_listen.children), 1)
|
||||
listen = include_listen.children[0]
|
||||
assert_is_instance(listen, Directive)
|
||||
assert_equal(listen.args, ['80'])
|
||||
|
||||
|
||||
def assert_config(config, expected):
|
||||
tree = _parse(config)
|
||||
assert_is_instance(tree, Directive)
|
||||
assert_is_instance(tree, Block)
|
||||
assert_is_instance(tree, Root)
|
||||
|
||||
child = tree.children[0]
|
||||
for ex in expected:
|
||||
assert_is_instance(child, ex)
|
|
@ -0,0 +1,470 @@
|
|||
from nose.tools import assert_equals
|
||||
import mock
|
||||
from six import StringIO
|
||||
from six.moves import builtins
|
||||
from gixy.parser.raw_parser import *
|
||||
|
||||
|
||||
def test_directive():
|
||||
config = '''
|
||||
access_log syslog:server=127.0.0.1,tag=nginx_sentry toolsformat;
|
||||
user http;
|
||||
internal;
|
||||
set $foo "bar";
|
||||
set $foo 'bar';
|
||||
proxy_pass http://unix:/run/sock.socket;
|
||||
rewrite ^/([a-zA-Z0-9]+)$ /$1/${arg_v}.pb break;
|
||||
server_name some.tld ~^(www\.)?podberi.(?:ru|com|ua)$
|
||||
~^(www\.)?guru.yandex.ru$;
|
||||
'''
|
||||
|
||||
expected = [
|
||||
['access_log', 'syslog:server=127.0.0.1,tag=nginx_sentry', 'toolsformat'],
|
||||
['user', 'http'],
|
||||
['internal'],
|
||||
['set', '$foo', 'bar'],
|
||||
['set', '$foo', 'bar'],
|
||||
['proxy_pass', 'http://unix:/run/sock.socket'],
|
||||
['rewrite', '^/([a-zA-Z0-9]+)$', '/$1/${arg_v}.pb', 'break'],
|
||||
['server_name', 'some.tld', '~^(www\.)?podberi.(?:ru|com|ua)$', '~^(www\.)?guru.yandex.ru$']
|
||||
]
|
||||
|
||||
assert_config(config, expected)
|
||||
|
||||
|
||||
def test_block():
|
||||
config = '''
|
||||
http {
|
||||
}
|
||||
'''
|
||||
|
||||
expected = [['http', [], []]]
|
||||
assert_config(config, expected)
|
||||
|
||||
|
||||
def test_block_with_child():
|
||||
config = '''
|
||||
http {
|
||||
gzip on;
|
||||
}
|
||||
'''
|
||||
|
||||
expected = [['http', [], [['gzip', 'on']]]]
|
||||
assert_config(config, expected)
|
||||
|
||||
|
||||
def test_location_simple():
|
||||
config = '''
|
||||
location / {
|
||||
}
|
||||
location = /foo {
|
||||
}
|
||||
location ~ ^/bar {
|
||||
}
|
||||
location ~* ^/baz$ {
|
||||
}
|
||||
location ^~ ^/bazz {
|
||||
}
|
||||
# Whitespace may be omitted:((
|
||||
location ~\.(js|css)$ {
|
||||
}
|
||||
'''
|
||||
|
||||
expected = [['location', ['/'], []],
|
||||
['location', ['=', '/foo'], []],
|
||||
['location', ['~', '^/bar'], []],
|
||||
['location', ['~*', '^/baz$'], []],
|
||||
['location', ['^~', '^/bazz'], []],
|
||||
['Whitespace may be omitted:(('],
|
||||
['location', ['~', '\.(js|css)$'], []]]
|
||||
|
||||
assert_config(config, expected)
|
||||
|
||||
|
||||
def test_quoted_strings():
|
||||
config = '''
|
||||
some_sq '\\'la\\.\\/\\"';
|
||||
some_dq '\\'la\\.\\/\\"';
|
||||
'''
|
||||
|
||||
expected = [['some_sq', '\'la\\.\\/\"'],
|
||||
['some_dq', '\'la\\.\\/\"']]
|
||||
|
||||
assert_config(config, expected)
|
||||
|
||||
|
||||
def test_location_child():
|
||||
config = '''
|
||||
location = /foo {
|
||||
proxy_pass http://unix:/run/sock.socket;
|
||||
}
|
||||
'''
|
||||
|
||||
expected = [['location', ['=', '/foo'], [
|
||||
['proxy_pass', 'http://unix:/run/sock.socket']
|
||||
]]]
|
||||
assert_config(config, expected)
|
||||
|
||||
|
||||
def test_nested_location():
|
||||
config = '''
|
||||
location ~* ^/foo {
|
||||
location = /foo/bar {
|
||||
internal;
|
||||
proxy_pass http://any.yandex.ru;
|
||||
}
|
||||
|
||||
location = /foo/baz {
|
||||
proxy_pass upstream;
|
||||
}
|
||||
}
|
||||
'''
|
||||
|
||||
expected = [['location', ['~*', '^/foo'], [
|
||||
['location', ['=', '/foo/bar'], [
|
||||
['internal'],
|
||||
['proxy_pass', 'http://any.yandex.ru']
|
||||
]],
|
||||
['location', ['=', '/foo/baz'], [
|
||||
['proxy_pass', 'upstream']
|
||||
]],
|
||||
]]]
|
||||
|
||||
assert_config(config, expected)
|
||||
|
||||
|
||||
def test_hash_block():
|
||||
config = '''
|
||||
geo $geo {
|
||||
default 0;
|
||||
|
||||
127.0.0.1 2;
|
||||
192.168.1.0/24 1;
|
||||
10.1.0.0/16 1;
|
||||
|
||||
::1 2;
|
||||
2001:0db8::/32 1;
|
||||
}
|
||||
'''
|
||||
|
||||
expected = [['geo', ['$geo'], [
|
||||
['default', '0'],
|
||||
['127.0.0.1', '2'],
|
||||
['192.168.1.0/24', '1'],
|
||||
['10.1.0.0/16', '1'],
|
||||
['::1', '2'],
|
||||
['2001:0db8::/32', '1']
|
||||
]]]
|
||||
|
||||
assert_config(config, expected)
|
||||
|
||||
|
||||
def test_hash_block_in_location():
|
||||
config = '''
|
||||
location /iphone/ {
|
||||
types {
|
||||
text/html html htm shtml;
|
||||
application/json json;
|
||||
application/rss+xml rss;
|
||||
text/vnd.sun.j2me.app-descriptor jad;
|
||||
}
|
||||
}
|
||||
'''
|
||||
|
||||
expected = [['location', ['/iphone/'], [
|
||||
['types', [], [
|
||||
['text/html', 'html', 'htm', 'shtml'],
|
||||
['application/json', 'json'],
|
||||
['application/rss+xml', 'rss'],
|
||||
['text/vnd.sun.j2me.app-descriptor', 'jad']
|
||||
]],
|
||||
]]]
|
||||
|
||||
assert_config(config, expected)
|
||||
|
||||
|
||||
def test_named_location():
|
||||
config = '''
|
||||
location @foo {
|
||||
proxy_pass http://any.yandex.ru;
|
||||
}
|
||||
'''
|
||||
|
||||
expected = [['location', ['@foo'], [
|
||||
['proxy_pass', 'http://any.yandex.ru']
|
||||
]]]
|
||||
|
||||
assert_config(config, expected)
|
||||
|
||||
|
||||
def test_if():
|
||||
config = '''
|
||||
# http://nginx.org/ru/docs/http/ngx_http_rewrite_module.html#if
|
||||
|
||||
if ($http_user_agent ~ MSIE) {
|
||||
rewrite ^(.*)$ /msie/$1 break;
|
||||
}
|
||||
|
||||
if ($http_cookie ~* "id=([^;]+)(?:;|$)") {
|
||||
set $id $1;
|
||||
}
|
||||
|
||||
if ($request_method = POST) {
|
||||
return 405;
|
||||
}
|
||||
|
||||
if ($slow) {
|
||||
limit_rate 10k;
|
||||
}
|
||||
|
||||
if ($invalid_referer) {
|
||||
return 403;
|
||||
}
|
||||
|
||||
if (!-e "/var/data/$dataset") {
|
||||
return 503;
|
||||
}
|
||||
|
||||
if ($https_or_slb = (by_slb|https)) {
|
||||
}
|
||||
|
||||
if ($host ~* (lori|rage2)\.yandex\.(ru|ua|com|com\.tr)) {
|
||||
set $x_frame_options ALLOW;
|
||||
}
|
||||
'''
|
||||
|
||||
expected = [
|
||||
['http://nginx.org/ru/docs/http/ngx_http_rewrite_module.html#if'],
|
||||
['if', ['$http_user_agent', '~', 'MSIE'], [
|
||||
['rewrite', '^(.*)$', '/msie/$1', 'break']
|
||||
]],
|
||||
['if', ['$http_cookie', '~*', 'id=([^;]+)(?:;|$)'], [
|
||||
['set', '$id', '$1']
|
||||
]],
|
||||
['if', ['$request_method', '=', 'POST'], [
|
||||
['return', '405']
|
||||
]],
|
||||
['if', ['$slow'], [
|
||||
['limit_rate', '10k']
|
||||
]],
|
||||
['if', ['$invalid_referer'], [
|
||||
['return', '403']
|
||||
]],
|
||||
['if', ['!-e', '/var/data/$dataset'], [
|
||||
['return', '503']
|
||||
]],
|
||||
['if', ['$https_or_slb', '=', '(by_slb|https)'], [
|
||||
]],
|
||||
['if', ['$host', '~*', '(lori|rage2)\.yandex\.(ru|ua|com|com\.tr)'], [
|
||||
['set', '$x_frame_options', 'ALLOW']
|
||||
]],
|
||||
]
|
||||
|
||||
assert_config(config, expected)
|
||||
|
||||
|
||||
def test_hash_block_map():
|
||||
config = '''
|
||||
# http://nginx.org/ru/docs/http/ngx_http_map_module.html
|
||||
|
||||
map $http_host $name {
|
||||
hostnames;
|
||||
|
||||
default 0;
|
||||
|
||||
example.com 1;
|
||||
*.example.com 1;
|
||||
example.org 2;
|
||||
*.example.org 2;
|
||||
.example.net 3;
|
||||
wap.* 4;
|
||||
}
|
||||
|
||||
map $http_user_agent $mobile {
|
||||
default 0;
|
||||
"~Opera Mini" 1;
|
||||
}
|
||||
'''
|
||||
|
||||
expected = [
|
||||
['http://nginx.org/ru/docs/http/ngx_http_map_module.html'],
|
||||
['map', ['$http_host', '$name'], [
|
||||
['hostnames'],
|
||||
['default', '0'],
|
||||
['example.com', '1'],
|
||||
['*.example.com', '1'],
|
||||
['example.org', '2'],
|
||||
['*.example.org', '2'],
|
||||
['.example.net', '3'],
|
||||
['wap.*', '4'],
|
||||
]],
|
||||
['map', ['$http_user_agent', '$mobile'], [
|
||||
['default', '0'],
|
||||
['~Opera Mini', '1'],
|
||||
]]
|
||||
]
|
||||
|
||||
assert_config(config, expected)
|
||||
|
||||
|
||||
def test_upstream():
|
||||
config = '''
|
||||
# http://nginx.org/ru/docs/http/ngx_http_upstream_module.html
|
||||
|
||||
upstream backend {
|
||||
server backend1.example.com weight=5;
|
||||
server backend2.example.com:8080;
|
||||
server unix:/tmp/backend3;
|
||||
|
||||
server backup1.example.com:8080 backup;
|
||||
server backup2.example.com:8080 backup;
|
||||
}
|
||||
|
||||
server {
|
||||
location / {
|
||||
proxy_pass http://backend;
|
||||
}
|
||||
}
|
||||
'''
|
||||
|
||||
expected = [
|
||||
['http://nginx.org/ru/docs/http/ngx_http_upstream_module.html'],
|
||||
['upstream', ['backend'], [
|
||||
['server', 'backend1.example.com', 'weight=5'],
|
||||
['server', 'backend2.example.com:8080'],
|
||||
['server', 'unix:/tmp/backend3'],
|
||||
['server', 'backup1.example.com:8080', 'backup'],
|
||||
['server', 'backup2.example.com:8080', 'backup'],
|
||||
]],
|
||||
['server', [], [
|
||||
['location', ['/'], [
|
||||
['proxy_pass', 'http://backend']
|
||||
]]
|
||||
]]]
|
||||
|
||||
assert_config(config, expected)
|
||||
|
||||
|
||||
def test_issue_8():
|
||||
config = '''
|
||||
# http://nginx.org/ru/docs/http/ngx_http_upstream_module.html
|
||||
if ($http_referer ~* (\.(ru|ua|by|kz)/(pages/music|partners/|page-no-rights\.xml)) ) {
|
||||
set $temp A;
|
||||
}
|
||||
'''
|
||||
|
||||
expected = [
|
||||
['http://nginx.org/ru/docs/http/ngx_http_upstream_module.html'],
|
||||
['if', ['$http_referer', '~*', '(\.(ru|ua|by|kz)/(pages/music|partners/|page-no-rights\.xml))'], [
|
||||
['set', '$temp', 'A']
|
||||
]]
|
||||
]
|
||||
|
||||
assert_config(config, expected)
|
||||
|
||||
|
||||
def test_issue_11():
|
||||
config = '''
|
||||
init_by_lua_block {
|
||||
tvm = require "nginx.tvm"
|
||||
}
|
||||
'''
|
||||
|
||||
expected = [
|
||||
['init_by_lua_block', [], ['tvm', '=', 'require', '"nginx.tvm"']]
|
||||
]
|
||||
|
||||
assert_config(config, expected)
|
||||
|
||||
|
||||
def test_lua_block():
|
||||
config = '''
|
||||
# https://github.com/openresty/lua-nginx-module#typical-uses
|
||||
location = /lua {
|
||||
# MIME type determined by default_type:
|
||||
default_type 'text/plain';
|
||||
|
||||
content_by_lua_block {
|
||||
local res = ngx.location.capture("/some_other_location")
|
||||
if res then
|
||||
ngx.say("status: ", res.status)
|
||||
ngx.say("body:")
|
||||
ngx.print(res.body)
|
||||
end
|
||||
}
|
||||
}
|
||||
'''
|
||||
|
||||
expected = [
|
||||
['https://github.com/openresty/lua-nginx-module#typical-uses'],
|
||||
['location', ['=', '/lua'], [
|
||||
['MIME type determined by default_type:'],
|
||||
['default_type', 'text/plain'],
|
||||
['content_by_lua_block', [], [
|
||||
'local', 'res', '=', 'ngx.location.capture(', '"/some_other_location"', ')',
|
||||
'if', 'res', 'then',
|
||||
'ngx.say(', '"status: "', ',', 'res.status)',
|
||||
'ngx.say(', '"body:"', ')',
|
||||
'ngx.print(res.body)',
|
||||
'end']]
|
||||
]]
|
||||
]
|
||||
|
||||
assert_config(config, expected)
|
||||
|
||||
|
||||
def test_lua_block_brackets():
|
||||
config = '''
|
||||
location = /foo {
|
||||
rewrite_by_lua_block {
|
||||
res = ngx.location.capture("/memc",
|
||||
{ args = { cmd = "incr", key = ngx.var.uri } }
|
||||
)
|
||||
}
|
||||
|
||||
proxy_pass http://blah.blah.com;
|
||||
}
|
||||
'''
|
||||
|
||||
expected = [
|
||||
['location', ['=', '/foo'], [
|
||||
['rewrite_by_lua_block', [], [
|
||||
'res', '=', 'ngx.location.capture(', '"/memc"', ',',
|
||||
['args', '=', ['cmd', '=', '"incr"', ',', 'key', '=', 'ngx.var.uri']],
|
||||
')']],
|
||||
['proxy_pass', 'http://blah.blah.com']
|
||||
]]
|
||||
]
|
||||
|
||||
assert_config(config, expected)
|
||||
|
||||
|
||||
def test_file_delims():
|
||||
config = '''
|
||||
# configuration file /etc/nginx/nginx.conf:
|
||||
http {
|
||||
include sites/*.conf;
|
||||
}
|
||||
|
||||
# configuration file /etc/nginx/sites/default.conf:
|
||||
server {
|
||||
|
||||
}
|
||||
'''
|
||||
|
||||
expected = [
|
||||
['/etc/nginx/nginx.conf'],
|
||||
['http', [], [
|
||||
['include', 'sites/*.conf']
|
||||
]],
|
||||
['/etc/nginx/sites/default.conf'],
|
||||
['server', [], []]
|
||||
]
|
||||
|
||||
assert_config(config, expected)
|
||||
|
||||
def assert_config(config, expected):
|
||||
with mock.patch('%s.open' % builtins.__name__) as mock_open:
|
||||
mock_open.return_value = StringIO(config)
|
||||
actual = RawParser().parse('/foo/bar')
|
||||
assert_equals(actual.asList(), expected)
|
|
@ -0,0 +1,3 @@
|
|||
add_header Content-Security-Policy "
|
||||
default-src: 'none';
|
||||
font-src data: https://yastatic.net;";
|
|
@ -0,0 +1 @@
|
|||
add_header X-Foo foo;
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"severity": "LOW"
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
more_set_headers -t 'text/html text/plain'
|
||||
'X-Foo: Bar
|
||||
multiline';
|
|
@ -0,0 +1,2 @@
|
|||
more_set_headers -t 'text/html text/plain'
|
||||
'X-Foo: Bar multiline';
|
|
@ -0,0 +1,7 @@
|
|||
more_set_headers -t 'text/html text/plain'
|
||||
'X-Foo: some
|
||||
multiline'
|
||||
'X-Bar: some
|
||||
multiline'
|
||||
'X-Baz: some
|
||||
multiline';
|
|
@ -0,0 +1,2 @@
|
|||
more_set_headers -r 'Foo:
|
||||
multiline';
|
|
@ -0,0 +1 @@
|
|||
more_set_headers -r 'Foo: multiline';
|
|
@ -0,0 +1 @@
|
|||
more_set_headers -s 404 -s '500 503' 'Foo: bar';
|
|
@ -0,0 +1,2 @@
|
|||
more_set_headers -t 'text/html
|
||||
text/plain' 'X-Foo: some';
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"severity": "MEDIUM"
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
http {
|
||||
add_header X-Frame-Options "DENY" always;
|
||||
server {
|
||||
location /new-headers {
|
||||
add_header X-Frame-Options "DENY" always;
|
||||
add_header X-Foo foo;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
add_header X-Frame-Options "DENY" always;
|
||||
|
||||
if (1) {
|
||||
add_header X-Foo foo;
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
add_header X-Frame-Options "DENY" always;
|
||||
|
||||
location /new-headers {
|
||||
add_header X-Foo foo;
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
add_header X-Frame-Options "DENY" always;
|
||||
server "some";
|
||||
add_header X-Foo foo;
|
|
@ -0,0 +1,5 @@
|
|||
add_header X-Bar bar;
|
||||
|
||||
location /new-headers {
|
||||
add_header X-Foo foo;
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
add_header X-Bar bar;
|
||||
|
||||
location /new-headers {
|
||||
add_header X-Frame-Options "DENY" always;
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
http {
|
||||
add_header X-Frame-Options "DENY" always;
|
||||
server {
|
||||
location /new-headers {
|
||||
add_header X-Foo foo;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"severity": "LOW"
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
return 301 http://some.yandex.ru/;
|
|
@ -0,0 +1 @@
|
|||
rewrite ^ http://some.yandex.ru/ permanent;
|
|
@ -0,0 +1,2 @@
|
|||
rewrite ^ https://some.yandex.ru/ permanent;
|
||||
return 301 https://some.yandex.ru/;
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"severity": "MEDIUM"
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
proxy_set_header Host $host;
|
|
@ -0,0 +1 @@
|
|||
proxy_set_header Host $http_host;
|
|
@ -0,0 +1 @@
|
|||
proxy_set_header HoSt $http_host;
|
|
@ -0,0 +1 @@
|
|||
proxy_set_header host $arg_host;
|
|
@ -0,0 +1 @@
|
|||
add_header X-Uri $uri;
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"severity": "HIGH"
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
location ~ /proxy/(a|b)/(\W*)$ {
|
||||
proxy_pass http://storage/$some;
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
location ~ /proxy/(a|b)/(\W*)$ {
|
||||
proxy_pass http://storage/$2;
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
location ~ /proxy/(a|b)/(\W*)$ {
|
||||
set $p $2;
|
||||
proxy_pass http://storage/$p;
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
location ~ /proxy/(a|b)/(\W*)$ {
|
||||
set $p $1;
|
||||
proxy_pass http://storage/$p;
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
location ~ /proxy/(a|b)/(?<p>\W*)$ {
|
||||
set $upstream "http://$1/$p?";
|
||||
proxy_pass $upstream;
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
proxy_pass http://upstream$document_uri;
|
|
@ -0,0 +1 @@
|
|||
proxy_set_header "X-Original-Uri" $document_uri;
|
|
@ -0,0 +1 @@
|
|||
return 403;
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue