Adding integration test system
This commit is contained in:
parent
2484cde921
commit
16f6b9f3ed
|
@ -64,3 +64,19 @@ jobs:
|
|||
uses: codecov/codecov-action@v1
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
integrate:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Run Docker Compose
|
||||
run: bash ${GITHUB_WORKSPACE}/tests/integration/run.sh
|
||||
|
||||
- name: Archive production artifact
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: diff-reports-file
|
||||
path: tests/integration/.dump_diff_file.txt
|
||||
|
|
|
@ -43,3 +43,7 @@ pip-delete-this-directory.txt
|
|||
|
||||
### Pycharm ###
|
||||
.idea/
|
||||
|
||||
### integration test system ###
|
||||
tests/integrations/.dump_diff_file.txt
|
||||
tests/integrations/.test/
|
||||
|
|
|
@ -0,0 +1,287 @@
|
|||
# Integration test documentation
|
||||
----
|
||||
Table of content
|
||||
- [Working principle](#working-principle)
|
||||
- [How to use it](#how-to-use-it)
|
||||
- [Basic usage](#basic-usage)
|
||||
- [Integration test creation guide](#integration-test-creation-guide)
|
||||
- [Creating and understanding filters](#creating-and-understanding-filters)
|
||||
- [Miscellaneous and notes](#miscellaneous-and-notes)
|
||||
----
|
||||
|
||||
## Working principle
|
||||
|
||||
This integration test system relies on scenarios. A scenario is basically made up of :
|
||||
- Wapiti attacking one or more target with some options (one or more modules, a custom endpoint etc.).
|
||||
- Wapiti generating one report per target, agnostic of any non-constant data (date, IDs, etc.) and filtered of another redundant data.
|
||||
- Reports are then compared to reference reports (also called assertions), if they look exactly the same, then the test passes, otherwise, a diff is dumped in the logs and in a text file that can be retrieved as a GitHub CI artifact.
|
||||
|
||||
A target is a website Wapiti can scan or crawl with some vulnerabilities or not.
|
||||
All the scenarios are executed on Docker with one or more container per target, one for Wapiti and another one for the endpoint.
|
||||
|
||||
During an usual test run, the system will acknowledge all the scenarios you want to test (they can be set through the ``TESTS`` environment variable), and will then try to attack targets. Scenarios and targets order are not fixed.
|
||||
|
||||
As some targets are mutualized for some scenario, all the containers (even those who will not be used) will be built and executed during a run.
|
||||
|
||||
## How to use it
|
||||
### Basic usage
|
||||
The whole system is located at tests/integration.
|
||||
|
||||
```Bash
|
||||
Entrypoint to run integration tests
|
||||
Usage: ./run.sh [options]
|
||||
Options:
|
||||
--help Display this message and exit
|
||||
--docker-clean Kill containers, remove and prune all docker images, volumes, and system, be carefull when using this option
|
||||
--verbose-build Print the build messages before running the tests
|
||||
```
|
||||
|
||||
After the run, a file named .dump_diff_file.txt containing a concatenation of all the reports differences of all the targets of all the scenarios will be created at tests/integration.
|
||||
|
||||
In case you want to disable a scenario, you can open the ``run.sh`` file and remove scenarios in the variable ``TESTS``. You can also export this variable in you shell but make sure to comment the one in ``run.sh`` to avoid overwriting. The formats of scenario in this variable should be single-space-separated.
|
||||
|
||||
```Bash
|
||||
TESTS="test_mod_wp_enum test_mod_http_headers test_mod_csp"
|
||||
```
|
||||
|
||||
There is no checking regarding what you put in this variable, use it carefully.
|
||||
|
||||
### Integration test creation guide
|
||||
|
||||
To create your own scenario:
|
||||
|
||||
1. Start by create a directory in tests/integration. By convention, follow the other directories when it comes to naming. Name start by `test_` followed by what you test, underscore-separated. Keep in mind that this will be your scenario name.
|
||||
<br/>
|
||||
|
||||
2. Populate your directory just like this:
|
||||
```
|
||||
- test_dummy_name
|
||||
- assertions
|
||||
- check.sh
|
||||
```
|
||||
It is mandatory to have a `check.sh` inside an `assertions` directory so the system can check your assertions
|
||||
<br/>
|
||||
|
||||
__check.sh__
|
||||
The system either let you the choice to set a symbolic link the default check.sh located in ``tests\integration`` to the ``assertions`` directory.
|
||||
```Bash
|
||||
# Admitting you are at the root of the git project :
|
||||
ln -s ../../check.sh test/integrations/test_dummy_name/assertions/check.sh
|
||||
```
|
||||
Or write your own if you need a specific way to check the reports. The only constraints are: be named ``check.sh`` and be a bash script
|
||||
<br/>
|
||||
|
||||
3. Populate your directory with files required by your targets (aside the assertions directory) such as php files, Dockerfiles, executables etc.
|
||||
<br/>
|
||||
|
||||
4. Modify the `docker-compose.setup.yml` to add your targets as containers .This file already contains severals shortcuts to help you setup a PHP server as well as some hashes of images. It is mandatory to:
|
||||
- Use existing or setup your own healthchecks.
|
||||
- Use images by their hashes, either the one provided or by adding them to the `.env` file
|
||||
|
||||
For shortcuts:
|
||||
|
||||
- `default_php_setup` setup a PHP web server and connect it to the test-network
|
||||
|
||||
- `default_mysql_setup` setup a Mysql database and connect it to the test-network
|
||||
|
||||
- `healthcheck_mysql` setup a healthcheck for a Mysql database
|
||||
|
||||
- `healthcheck_web` setup a healthcheck for a server hosting a website
|
||||
|
||||
|
||||
Here are 2 targets examples:
|
||||
```yml
|
||||
dummy_target:
|
||||
<<: [ *default_php_setup, *healthcheck_web ]
|
||||
depends_on:
|
||||
endpoint:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- ./test_dummy_name/target_1/php/src/:/var/www/html/
|
||||
|
||||
built_dummy_target:
|
||||
build:
|
||||
context: ./test_dummy_name/target_2/
|
||||
dockerfile: "./test_dummy_name/target_2/Dockerfile"
|
||||
args:
|
||||
PHP_HASH_TAG: ${PHP_HASH}
|
||||
volumes:
|
||||
- ./test_dummy_name/target_2/php/src/:/var/www/html/
|
||||
<<: *healthcheck_web
|
||||
networks:
|
||||
- test-network
|
||||
```
|
||||
|
||||
To make sure Wapiti waits for the containers to be ready, add dependances :
|
||||
```yml
|
||||
depends_on:
|
||||
dummy_target:
|
||||
condition: service_healthy
|
||||
built_dummy_target:
|
||||
condition: service_healthy
|
||||
```
|
||||
<br/>
|
||||
|
||||
5. Modify the ``tests/integration/wapiti/module.json`` to define the behavior of Wapiti toward the target(s). You can supply:
|
||||
- A filter per scenario to avoid bloating the reports and the assertions. If you don't a default one will be supplied (see [this section](#creating-and-understanding-filters) for more informations).
|
||||
- Supplementary arguments per scenario or per target (supplementary arguments will sum up unless you specify you want target supplementary argument to override scenario supplementary argument)
|
||||
- Modules
|
||||
<br/>
|
||||
|
||||
As Docker relies on hostnames, you can indicate them as their names preprended by ``http(s)://`` so Wapiti can attack them.
|
||||
|
||||
Here is an example:
|
||||
```JSON
|
||||
"test_dummy_name": {
|
||||
"modules": "dummy",
|
||||
"supplementary_argument": "--auth-method digest",
|
||||
"report_filter_tree": {},
|
||||
"targets": [
|
||||
{
|
||||
"name": "http://dummy_target/endpoint1/index.php"
|
||||
},
|
||||
{
|
||||
"name": "http://dummy_target/endpoint2/index.php",
|
||||
"supplementary_argument": "--endpoint http://endpoint/"
|
||||
},
|
||||
{
|
||||
"name": "http://built_dummy_target",
|
||||
"supplementary_argument": "--auth-method basic",
|
||||
"erase_global_supplementary": true
|
||||
}
|
||||
]
|
||||
},
|
||||
```
|
||||
<br/>
|
||||
|
||||
As shown, you can also define multiples targets on a single container, which allow you to host mutliple websites on a single server. Wapiti will be launched on each target and thus, will produce as many reports as there is target for a given scenario.
|
||||
<br/>
|
||||
|
||||
``supplementary_argument`` and ``report_filter_tree`` can be omitted. All the other keys are mandatory (``modules`` should be left as an empty string when testing without any module)
|
||||
<br/>
|
||||
|
||||
__supplementary_argument__
|
||||
As you can see above, the first target will inherit from the scenario supplementary argument, the second one will have both argument and the third one runs with it own supplementary argument
|
||||
|
||||
Some arguments are already supplied by default and can't be changed. Wapiti will always be run with ``--detailed-report --flush-session --verbose 2 -f json``. The outpout path of the reports will also be supplied, supplying it here may break you scenario.
|
||||
<br/>
|
||||
|
||||
__report_filter_tree__
|
||||
The report filter tree value should be a json following strictly the same structure of a Wapiti report in json, you can find what it looks like in ``tests/integration/wapiti/templates_and_data.py``. The goal of applying a filter is not only to prevent having large reports made of useless data, but also remove data that may vary arbitrarily from one report to another.
|
||||
<br/>
|
||||
|
||||
6. Generate (or regenerate your own assertions)
|
||||
Run the tests once
|
||||
```Bash
|
||||
./run.sh
|
||||
```
|
||||
All the reports from the different targets will be generated in the ``tests/integration/.test`` directory. From here you can generate or regenerate your assertions by using the script ``regenerate_assertions.sh``, if left empty, it will erase all the assertions by the produced reports. To replace specific assertions, specify them by their names
|
||||
```Bash
|
||||
./regenerate_assertions.sh test_dummy_name
|
||||
```
|
||||
(This script doesn't have any checking system, supplying unknown or mistyped arguments may lead to unexpected behavior, use it carefully)
|
||||
Or you can copy it yourself:
|
||||
```Bash
|
||||
cp tests/integrations/.test/test_dummy_name/dummy_target_endpoint1_index.php.out tests/integrations/test_dummy_name/assertions/dummy_target_endpoint1_index.php.json
|
||||
cp tests/integrations/.test/test_dummy_name/dummy_target_endpoint2_index.php.out tests/integrations/test_dummy_name/assertions/dummy_target_endpoint2_index.php.json
|
||||
cp tests/integrations/.test/test_dummy_name/built_dummy_target.out tests/integrations/test_dummy_name/assertions/built_dummy_target.json
|
||||
```
|
||||
<br/>
|
||||
You can finally, re-run the tests and observe if the assertions are respected or not.
|
||||
### Creating and understanding filters
|
||||
|
||||
The default filter can be found in ``tests\integration\wapiti\templates_and_data.py``. It will remove every WSTG code explanations shipped by default on each report:
|
||||
```JSON
|
||||
{
|
||||
"vulnerabilities": {},
|
||||
"anomalies": {},
|
||||
"additionals": {},
|
||||
"infos": {}
|
||||
}
|
||||
```
|
||||
<br/>
|
||||
|
||||
If you want to create your own filter, you can look at the general template in ``tests\integration\wapiti\templates_and_data.py``. Any key with a corresponding empty object in the filter will indicate to the system that everything produced in the report inside this key will be copied. Non-written keys will be ignored.
|
||||
For arrays, you can indicate in the filter, a single element and the system will treat every elements of the arrays in the output report as the first occurence of the report.
|
||||
<br/>
|
||||
As an example, for this dummy raw output:
|
||||
|
||||
```JSON
|
||||
{
|
||||
"vulnerabilities": {
|
||||
"A dummy vuln":{
|
||||
"wanted_data": 34,
|
||||
"wanted_array": [
|
||||
{
|
||||
"wanted_array_data": "blablabla",
|
||||
"bloat_array_data": 455
|
||||
},
|
||||
{
|
||||
"wanted_array_data": "blebleble",
|
||||
"bloat_array_data": 456
|
||||
},
|
||||
{
|
||||
"wanted_array_data": "bliblibli",
|
||||
"bloat_array_data": 457
|
||||
}
|
||||
],
|
||||
"bloat_data": "blablabla"
|
||||
}
|
||||
},
|
||||
"info":{
|
||||
"value1": 1,
|
||||
"value2": 2,
|
||||
"value3": 3,
|
||||
"value4": 4,
|
||||
"value5": 5
|
||||
}
|
||||
}
|
||||
```
|
||||
If we want to keep the wanted data, the infos and get rid of the bloat data, we should write the following filter:
|
||||
```JSON
|
||||
{
|
||||
"vulnerabilities":{
|
||||
"A dummy vuln":{
|
||||
"wanted_data": 0,
|
||||
"wanted_array":[
|
||||
{
|
||||
"wanted_array_data": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"info":{}
|
||||
}
|
||||
```
|
||||
The produced output will be:
|
||||
```JSON
|
||||
{
|
||||
"vulnerabilities": {
|
||||
"A dummy vuln":{
|
||||
"wanted_data": 34,
|
||||
"wanted_array": [
|
||||
{
|
||||
"wanted_array_data": "blablabla"
|
||||
},
|
||||
{
|
||||
"wanted_array_data": "blebleble"
|
||||
},
|
||||
{
|
||||
"wanted_array_data": "bliblibli"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"info":{
|
||||
"value1": 1,
|
||||
"value2": 2,
|
||||
"value3": 3,
|
||||
"value4": 4,
|
||||
"value5": 5
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Miscellaneous and notes
|
||||
|
||||
- As modules are added to Wapiti, the constant ``EXISTING_MODULES`` in ``tests/integrations/wapiti/templates_and_data.py`` should be updated in consequences, not having a new module in this variable will make the system crash. This is a security to prevent you from launching tests with modules that doesn't exist or with a typo
|
|
@ -0,0 +1,44 @@
|
|||
# The following hash corresponds to the image php:8.1.18-apache
|
||||
# php@sha256:5d5f0dbea68afab7e6fa7649fb818e078680e338a3265ec5cf237a6a791dd471
|
||||
# it can be found here: https://hub.docker.com/layers/library/php/8.1.18-apache/images/sha256-5d5f0dbea68afab7e6fa7649fb818e078680e338a3265ec5cf237a6a791dd471?context=explore
|
||||
PHP_HASH='@sha256:5d5f0dbea68afab7e6fa7649fb818e078680e338a3265ec5cf237a6a791dd471'
|
||||
|
||||
# The following hash corresponds to the image mysql:8
|
||||
# mysql@sha256:13e429971e970ebcb7bc611de52d71a3c444247dc67cf7475a02718f6a5ef559
|
||||
# it can be found here: https://hub.docker.com/layers/library/mysql/8/images/sha256-13e429971e970ebcb7bc611de52d71a3c444247dc67cf7475a02718f6a5ef559?context=explore
|
||||
MYSQL_HASH='@sha256:13e429971e970ebcb7bc611de52d71a3c444247dc67cf7475a02718f6a5ef559'
|
||||
|
||||
# The following hashes corresponds respectively to the images, drupal:9-apache, drupal:10-apache
|
||||
# drupal@sha256:18692a0792c882957024f4086cadbc966778c5593850dfa89edb7780ba8b794d
|
||||
# drupal@sha256:d85280f104d6c8e1eff7e2613b5ee584d0a4105d54f4ffe352f945e38d095514
|
||||
# They can respectively be found at:
|
||||
# https://hub.docker.com/layers/library/drupal/9-apache/images/sha256-18692a0792c882957024f4086cadbc966778c5593850dfa89edb7780ba8b794d?context=explore
|
||||
# https://hub.docker.com/layers/library/drupal/10-apache/images/sha256-d85280f104d6c8e1eff7e2613b5ee584d0a4105d54f4ffe352f945e38d095514?context=explore
|
||||
DRUPAL9_HASH='@sha256:18692a0792c882957024f4086cadbc966778c5593850dfa89edb7780ba8b794d'
|
||||
DRUPAL10_HASH='@sha256:d85280f104d6c8e1eff7e2613b5ee584d0a4105d54f4ffe352f945e38d095514'
|
||||
# Some variables allowing the drupal instances and their DB to run
|
||||
DRUPAL_MYSQL_DB='drupal'
|
||||
DRUPAL_MYSQL_USER='drupal'
|
||||
DRUPAL_MYSQL_PASSWORD='drupalpasswd'
|
||||
DRUPAL_MYSQL_ROOT_PASSWORD='rootpasswd'
|
||||
|
||||
# This wordpress image refers to the 6.2.0 tag, it can be found here:
|
||||
# https://hub.docker.com/layers/library/wordpress/6.2.0/images/sha256-c3d6df13e49ed4039fbbe5bd1ec172b166a7a4df603716fd06f5bd66b7e60f90?context=explore
|
||||
WP_HASH='@sha256:c3d6df13e49ed4039fbbe5bd1ec172b166a7a4df603716fd06f5bd66b7e60f90'
|
||||
# Credentials for the wordpress database
|
||||
WP_MYSQL_ROOT_PASSWORD="somewordpress"
|
||||
WP_MYSQL_DATABASE="wordpress"
|
||||
WP_MYSQL_USER="wordpress"
|
||||
WP_MYSQL_PASSWORD="wordpress"
|
||||
|
||||
|
||||
# VARIABLES USED IN HEALTHCHECK SYSTEM
|
||||
# Various commands to check if a container is healthy or not
|
||||
DEFAULT_MYSQL_HEALTHCHECK_COMMAND='mysqladmin ping --silent'
|
||||
DEFAULT_WEB_HEALTHCHECK_COMMAND='curl --silent --fail http://localhost/'
|
||||
HTTPS_WEB_HEALTHCHECK_COMMAND='curl --silent --fail --insecure https://localhost/'
|
||||
# Some other default variables related to the healthcheck conditions
|
||||
DEFAULT_HEALTHCHECKS_RETRIES=30
|
||||
DEFAULT_HEALTHCHECKS_TIMEOUT=3s
|
||||
DEFAULT_HEALTHCHECKS_INTERVAL=5s
|
||||
DEFAULT_HEALTHCHECKS_START_PERIOD=5s
|
|
@ -0,0 +1,77 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Symlink this file to each module so it can
|
||||
# check the assertions.
|
||||
|
||||
#define some colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
NC='\033[0m' # no colors
|
||||
|
||||
# exit upon any error
|
||||
set -o errexit
|
||||
|
||||
# exit upon using undeclared variables
|
||||
set -o nounset
|
||||
|
||||
die(){
|
||||
echo >&2 "$@"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Ensure we are on the good execution directory
|
||||
cd "$(dirname "$0")"
|
||||
MODULE_NAME=$(basename "$(realpath ..)")
|
||||
|
||||
# get all the assertions (paths to text files that we want to
|
||||
# find in the report).
|
||||
# 1 assertion per target, one or more targets per module,
|
||||
# assertions can be found in the same directory than this file
|
||||
declare -a assertions
|
||||
mapfile -t assertions < <(find . -name "*.json")
|
||||
if [[ ${#assertions[@]} -eq 0 ]]; then
|
||||
die "Error: No assertions found in module $MODULE_NAME"
|
||||
fi
|
||||
|
||||
# Since the assertions and the target reports must have the same name
|
||||
# (except their extention names, .json and .out), we extract the targets names
|
||||
# from the assertion array. We also use their names to log some informations
|
||||
# so keeping an array is somewhat useful
|
||||
declare -a targets_name
|
||||
for i in "${assertions[@]}"; do
|
||||
targets_name+=("$(basename "$i" .json)")
|
||||
done
|
||||
|
||||
# We store the target reports paths in an array with respect to the target_name
|
||||
# array order. That way, our third array is ordered with the 2 other ones and
|
||||
# we can safely use indexes to compare
|
||||
declare -a outputs
|
||||
for target in "${targets_name[@]}"; do
|
||||
outputs+=("$(realpath "../../.test/$MODULE_NAME/$target.out")")
|
||||
done
|
||||
if [[ ${#outputs[@]} -eq 0 ]]; then
|
||||
die "Error: No targets found in module $MODULE_NAME"
|
||||
fi
|
||||
|
||||
# A special case is if we don't get the same number of reports as they are targets.
|
||||
# Wapiti may not detect some targets or it can be due to various misconfigurations
|
||||
# so we check the size of each array to be sure they match
|
||||
if [[ ${#assertions[@]} -ne ${#targets_name[@]} || ${#targets_name[@]} -ne ${#outputs[@]} ]] ; then
|
||||
die "Error: different number of reports/assertion files, found ${#outputs[@]} outputs for ${#assertions[@]} assertions"
|
||||
fi
|
||||
|
||||
EXIT_CODE=0
|
||||
# Comparing outputs and assertions :
|
||||
for i in "${!outputs[@]}"; do
|
||||
if [[ "$(cat "${outputs[$i]}")" != "$(cat "${assertions[$i]}")" ]]; then
|
||||
echo -e "Assertion $(basename "${assertions[$i]}" .json) of module $MODULE_NAME is ${RED}not respected:${NC}"
|
||||
echo "< : assertion"
|
||||
echo "> : output"
|
||||
diff <(jq --sort-keys . "${outputs[$i]}") <(jq --sort-keys . "${assertions[$i]}") || echo "---End of diff of assertion $(basename "${assertions[$i]}" .json) module ${MODULE_NAME}---"
|
||||
EXIT_CODE=1
|
||||
else
|
||||
echo -e "Assertion $(basename "${assertions[$i]}" .json) of module $MODULE_NAME is ${GREEN}respected${NC}"
|
||||
fi
|
||||
done
|
||||
|
||||
exit $EXIT_CODE
|
|
@ -0,0 +1,49 @@
|
|||
version: '3.9'
|
||||
|
||||
# Following the DRY philosophy
|
||||
x-default_php_setup:
|
||||
&default_php_setup
|
||||
image: php${PHP_HASH}
|
||||
networks:
|
||||
- test-network
|
||||
|
||||
x-healthcheck_web:
|
||||
&healthcheck_web
|
||||
healthcheck:
|
||||
test: ${DEFAULT_WEB_HEALTHCHECK_COMMAND}
|
||||
interval: ${DEFAULT_HEALTHCHECKS_INTERVAL}
|
||||
timeout: ${DEFAULT_HEALTHCHECKS_TIMEOUT}
|
||||
start_period: ${DEFAULT_HEALTHCHECKS_START_PERIOD}
|
||||
retries: ${DEFAULT_HEALTHCHECKS_RETRIES}
|
||||
|
||||
x-default_mysql_setup:
|
||||
&default_mysql_setup
|
||||
image: mysql${MYSQL_HASH}
|
||||
networks:
|
||||
- test-network
|
||||
|
||||
x-healthcheck_mysql:
|
||||
&healthcheck_mysql
|
||||
healthcheck:
|
||||
test: ${DEFAULT_MYSQL_HEALTHCHECK_COMMAND}
|
||||
start_period: ${DEFAULT_HEALTHCHECKS_START_PERIOD}
|
||||
interval: ${DEFAULT_HEALTHCHECKS_INTERVAL}
|
||||
timeout: ${DEFAULT_HEALTHCHECKS_TIMEOUT}
|
||||
retries: ${DEFAULT_HEALTHCHECKS_RETRIES}
|
||||
|
||||
services:
|
||||
# Wapiti container
|
||||
# requires all the targets containers to work perfectly
|
||||
wapiti:
|
||||
build:
|
||||
context: "../../"
|
||||
dockerfile: "./tests/integration/wapiti/Dockerfile.integration"
|
||||
no_cache: true
|
||||
container_name: wapiti
|
||||
volumes:
|
||||
- ./.test:/home/
|
||||
networks:
|
||||
- test-network
|
||||
command: "${TESTS}"
|
||||
networks:
|
||||
test-network:
|
|
@ -0,0 +1,29 @@
|
|||
#!/bin/bash
|
||||
|
||||
# exit upon any error
|
||||
set -o errexit
|
||||
|
||||
# exit upon using undeclared variables
|
||||
set -o nounset
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
if [[ $# -eq 0 ]]; then
|
||||
mapfile -t modules_dirs < <(find .test -maxdepth 1 -mindepth 1 -type d)
|
||||
else
|
||||
tests=$*
|
||||
mapfile -t modules_dirs < <(echo -e "${tests// /\\n}" | sed 's/^/.test\//')
|
||||
fi
|
||||
|
||||
for module in "${modules_dirs[@]}";do
|
||||
if [[ -d "./$(basename "${module}")/assertions" ]]; then
|
||||
cp "${module}/"* "./$(basename "${module}")/"
|
||||
mapfile -t assertion_files < <(find "./$(basename "${module}")/" -name "*.out")
|
||||
for assertion_file in "${assertion_files[@]}";do
|
||||
mv -- "${assertion_file}" "$(dirname "${assertion_file}")/assertions/$(basename "${assertion_file}" .out).json"
|
||||
done
|
||||
echo "assertions of module $(basename "${module}") copied"
|
||||
else
|
||||
echo "directory ./$(basename "${module}")/assertions/ does not exist, skipping..."
|
||||
fi
|
||||
done
|
|
@ -0,0 +1,81 @@
|
|||
#!/bin/bash
|
||||
|
||||
# List of modules to be tested
|
||||
TESTS=""
|
||||
|
||||
# Normalize spaces for shell substitution
|
||||
if [[ ! -z "$TESTS" ]]; then
|
||||
export TESTS="$(echo "$TESTS" | xargs) "
|
||||
fi
|
||||
|
||||
# exit upon any error
|
||||
set -o errexit
|
||||
|
||||
# exit upon using undeclared variables
|
||||
set -o nounset
|
||||
|
||||
# Placing ourselves in the right directory
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
# Parsing script arguments
|
||||
declare -A args
|
||||
for arg in "$@"; do
|
||||
args[$arg]=1;
|
||||
done;
|
||||
|
||||
if [[ -v args["--help"] ]]; then
|
||||
# Printing some help
|
||||
printf "%s\n" \
|
||||
"Entrypoint to run integration tests" \
|
||||
"Usage: ./run.sh [options]" \
|
||||
"Options:" \
|
||||
" --help Display this message and exit"\
|
||||
" --docker-clean Kill containers, remove and prune all docker images, volumes, and system, be carefull when using this option"\
|
||||
" --verbose-build Print the build messages before running the tests";
|
||||
exit 0;
|
||||
fi
|
||||
|
||||
if [[ -v args["--docker-clean"] ]]; then
|
||||
# Cleaning docker
|
||||
echo "Cleaning docker..."
|
||||
docker kill $(docker ps -q) 2> /dev/null || echo "No containers to kill"
|
||||
docker container prune -f 2> /dev/null || echo "No containers to prune"
|
||||
docker volume prune -f 2> /dev/null || echo "No volumes to prune"
|
||||
docker volume rm $(docker volume ls -q) 2> /dev/null || echo "No volume to remove"
|
||||
docker rmi $(docker images -a -q) 2> /dev/null || echo "No images to remove"
|
||||
(docker system prune -f && docker network create test-network) 2> /dev/null || echo "No need to prune the system"
|
||||
fi
|
||||
|
||||
# Fallback to create the test-network in case it doesn't exist
|
||||
docker network inspect test-network > /dev/null || docker network create test-network > /dev/null
|
||||
|
||||
echo "Building images..."
|
||||
if [[ ! -v args["--verbose-build"] ]];then
|
||||
# Quietly build all Dockerfiles
|
||||
docker compose -f docker-compose.setup.yml build --quiet
|
||||
fi
|
||||
|
||||
# Start the tests
|
||||
docker compose --progress quiet -f docker-compose.setup.yml up --abort-on-container-exit
|
||||
|
||||
declare -a asserters=()
|
||||
# If the TESTS env variable is supplied, we will only check the specified tests
|
||||
if [[ ! -z "$TESTS" ]]; then
|
||||
# Assuming all the tests in the TESTS variable are well written and exist
|
||||
mapfile -t asserters < <(echo -e "${TESTS// /\/assertions\/check.sh\\n}" | head -n -1)
|
||||
else
|
||||
# Otherwise, we take all the tests
|
||||
mapfile -t asserters < <(find . -mindepth 2 -type l,f -name check.sh)
|
||||
fi
|
||||
EXIT_CODE=0
|
||||
for path in "${asserters[@]}"; do
|
||||
cd "$(dirname "${path}")"
|
||||
bash "check.sh" | tee -a ../../.dump_diff_file.txt
|
||||
# Workaround to check if check.sh succeed, may not work on zsh
|
||||
if [[ "${PIPESTATUS[0]}" -eq 1 ]]; then
|
||||
EXIT_CODE=1
|
||||
fi
|
||||
cd - > /dev/null
|
||||
done
|
||||
|
||||
exit $EXIT_CODE
|
|
@ -0,0 +1,42 @@
|
|||
FROM debian:bullseye-slim as build
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive \
|
||||
LANG=en_US.UTF-8
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
RUN apt-get -y update &&\
|
||||
apt-get -y install --no-install-recommends\
|
||||
python3 python3-pip python3-setuptools ca-certificates &&\
|
||||
apt-get -y clean &&\
|
||||
apt-get -y autoremove &&\
|
||||
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* &&\
|
||||
truncate -s 0 /var/log/*log
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN pip3 install . requests
|
||||
|
||||
FROM debian:bullseye-slim
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive \
|
||||
LANG=en_US.UTF-8 \
|
||||
PYTHONDONTWRITEBYTECODE=1
|
||||
|
||||
RUN apt-get -y update &&\
|
||||
apt-get -y install --no-install-recommends \
|
||||
python3 python3-setuptools curl &&\
|
||||
apt-get -y clean &&\
|
||||
apt-get -y autoremove &&\
|
||||
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* &&\
|
||||
truncate -s 0 /var/log/*log
|
||||
|
||||
COPY --from=build /usr/local/lib/python3.9/dist-packages/ /usr/local/lib/python3.9/dist-packages/
|
||||
COPY --from=build /usr/local/bin/wapiti /usr/local/bin/wapiti-getcookie /usr/local/bin/
|
||||
|
||||
COPY ./tests/integration/wapiti/test.py /usr/local/bin/test.py
|
||||
COPY ./tests/integration/wapiti/templates_and_data.py /usr/local/bin/templates_and_data.py
|
||||
COPY ./tests/integration/wapiti/misc_functions.py /usr/local/bin/misc_functions.py
|
||||
COPY ./tests/integration/wapiti/modules.json /usr/local/bin/modules.json
|
||||
|
||||
ENTRYPOINT [ "python3","-u","/usr/local/bin/test.py"]
|
|
@ -0,0 +1,78 @@
|
|||
from collections import defaultdict
|
||||
from re import findall, MULTILINE
|
||||
from json import dumps
|
||||
|
||||
from templates_and_data import TREE_CHECKER
|
||||
|
||||
|
||||
def purge_irrelevant_data(data) -> None:
|
||||
"""
|
||||
Look recursively for any pattern matching a 2 lenght sized list with
|
||||
"date", "last-modified", "keep-alive" or "etag" in a dictionnary containing lists,
|
||||
dictionnaries, and other non-collections structures. Removing them because those
|
||||
datas can change from one test to another and aren't really relevant
|
||||
"""
|
||||
if isinstance(data, dict):
|
||||
for key in data.keys():
|
||||
purge_irrelevant_data(data[key])
|
||||
elif isinstance(data, list) and len(data) != 0:
|
||||
indexes_to_remove = []
|
||||
for i, item in enumerate(data):
|
||||
if isinstance(item, list) and len(item) == 2 and item[0] in ("date", "last-modified", "etag", "keep-alive"):
|
||||
indexes_to_remove.append(i)
|
||||
elif isinstance(item, dict) or (isinstance(item, list) and len(item) > 2):
|
||||
purge_irrelevant_data(item)
|
||||
for i in indexes_to_remove[::-1]:
|
||||
data.pop(i)
|
||||
else:
|
||||
return
|
||||
|
||||
|
||||
def filter_data(data, filter):
|
||||
"""
|
||||
Filter recursively data from report using a filter, is sensitive to report changes and don't check if the filter is correct
|
||||
make sure to write filter correctly or reinforce this function
|
||||
"""
|
||||
# Another check, type based, also considering if filter and data order match
|
||||
assert (type(data) is type(filter)) or (type(data) is type(None)), \
|
||||
f"Mismatch content, filter element is {type(filter)} and data element is {type(data)}"
|
||||
if isinstance(data, dict):
|
||||
filtered_tree = defaultdict()
|
||||
for data_key, data_content in data.items():
|
||||
if data_key in filter:
|
||||
nested_content = \
|
||||
filter[data_key] and (isinstance(filter[data_key], dict) or isinstance(filter[data_key], list))
|
||||
filtered_tree[data_key] = \
|
||||
filter_data(data_content, filter[data_key]) if nested_content else data_content
|
||||
return dict(filtered_tree)
|
||||
elif isinstance(data, list) and filter:
|
||||
filtered_list = list()
|
||||
for element in data:
|
||||
filtered_list.append(filter_data(element, filter[0]))
|
||||
return filtered_list
|
||||
|
||||
|
||||
def all_keys_dicts(data: dict) -> set:
|
||||
"""
|
||||
Function to return a set of every keys in a nested dictionary
|
||||
"""
|
||||
return set(findall(r"^[ ]*\"(.+?)\"\s*:", dumps(data, indent=4), MULTILINE))
|
||||
|
||||
|
||||
def sort_lists_in_dict(data):
|
||||
"""
|
||||
Function that recursively sort every lists in a dictionary to normalize them
|
||||
Mainly used because requests in the detailed reports aren't always in the same order
|
||||
"""
|
||||
if data:
|
||||
if isinstance(data, dict):
|
||||
for key, value in data.items():
|
||||
if isinstance(value, dict):
|
||||
sort_lists_in_dict(data[key])
|
||||
elif isinstance(value, list):
|
||||
sort_lists_in_dict(data[key])
|
||||
# sort the array here
|
||||
data[key] = sorted(data[key], key=str)
|
||||
elif isinstance(data, list):
|
||||
for item in data:
|
||||
sort_lists_in_dict(item)
|
|
@ -0,0 +1 @@
|
|||
{}
|
|
@ -0,0 +1,463 @@
|
|||
DEFAULT_FILTER_TREE = {
|
||||
"vulnerabilities": {},
|
||||
"anomalies": {},
|
||||
"additionals": {},
|
||||
"infos": {}
|
||||
}
|
||||
|
||||
EXISTING_MODULES = {
|
||||
"backup", "brute_login_form",
|
||||
"buster", "cookieflags",
|
||||
"crlf", "csp",
|
||||
"csrf", "drupal_enum",
|
||||
"exec", "file",
|
||||
"htaccess", "htp",
|
||||
"http_headers", "log4shell",
|
||||
"methods", "nikto",
|
||||
"permanentxss", "redirect",
|
||||
"shellshock", "sql",
|
||||
"ssl", "ssrf",
|
||||
"takeover", "timesql",
|
||||
"wapp", "wp_enum",
|
||||
"xss", "xxe", ""
|
||||
}
|
||||
|
||||
TREE_CHECKER = {
|
||||
"vulnerabilities": {'Backup file': [{'curl_command': '',
|
||||
'detail': {
|
||||
'response': {
|
||||
'body': '',
|
||||
'headers': [],
|
||||
'status_code': 0
|
||||
}
|
||||
},
|
||||
'http_request': '',
|
||||
'info': '',
|
||||
'level': 0,
|
||||
'method': '',
|
||||
'module': '',
|
||||
'parameter': '',
|
||||
'path': '',
|
||||
'referer': '',
|
||||
'wstg': []}],
|
||||
'Blind SQL Injection': [{'curl_command': '',
|
||||
'detail': {
|
||||
'response': {
|
||||
'body': '',
|
||||
'headers': [],
|
||||
'status_code': 0
|
||||
}
|
||||
},
|
||||
'http_request': '',
|
||||
'info': '',
|
||||
'level': 0,
|
||||
'method': '',
|
||||
'module': '',
|
||||
'parameter': '',
|
||||
'path': '',
|
||||
'referer': '',
|
||||
'wstg': []}],
|
||||
'CRLF Injection': [{'curl_command': '',
|
||||
'detail': {
|
||||
'response': {
|
||||
'body': '',
|
||||
'headers': [],
|
||||
'status_code': 0
|
||||
}
|
||||
},
|
||||
'http_request': '',
|
||||
'info': '',
|
||||
'level': 0,
|
||||
'method': '',
|
||||
'module': '',
|
||||
'parameter': '',
|
||||
'path': '',
|
||||
'referer': '',
|
||||
'wstg': []}],
|
||||
'Command execution': [{'curl_command': '',
|
||||
'detail': {
|
||||
'response': {
|
||||
'body': '',
|
||||
'headers': [],
|
||||
'status_code': 0
|
||||
}
|
||||
},
|
||||
'http_request': '',
|
||||
'info': '',
|
||||
'level': 0,
|
||||
'method': '',
|
||||
'module': '',
|
||||
'parameter': '',
|
||||
'path': '',
|
||||
'referer': '',
|
||||
'wstg': []}],
|
||||
'Content Security Policy Configuration': [{'curl_command': '',
|
||||
'detail': {
|
||||
'response': {
|
||||
'body': '',
|
||||
'headers': [],
|
||||
'status_code': 0
|
||||
}
|
||||
},
|
||||
'http_request': '',
|
||||
'info': '',
|
||||
'level': 0,
|
||||
'method': '',
|
||||
'module': '',
|
||||
'parameter': '',
|
||||
'path': '',
|
||||
'referer': '',
|
||||
'wstg': []}],
|
||||
'Cross Site Request Forgery': [{'curl_command': '',
|
||||
'detail': {
|
||||
'response': {
|
||||
'body': '',
|
||||
'headers': [],
|
||||
'status_code': 0
|
||||
}
|
||||
},
|
||||
'http_request': '',
|
||||
'info': '',
|
||||
'level': 0,
|
||||
'method': '',
|
||||
'module': '',
|
||||
'parameter': '',
|
||||
'path': '',
|
||||
'referer': '',
|
||||
'wstg': []}],
|
||||
'Fingerprint web application framework': [{'curl_command': '',
|
||||
'detail': {
|
||||
'response': {
|
||||
'body': '',
|
||||
'headers': [],
|
||||
'status_code': 0
|
||||
}
|
||||
},
|
||||
'http_request': '',
|
||||
'info': '',
|
||||
'level': 0,
|
||||
'method': '',
|
||||
'module': '',
|
||||
'parameter': '',
|
||||
'path': '',
|
||||
'referer': '',
|
||||
'wstg': []}],
|
||||
'Fingerprint web server': [{'curl_command': '',
|
||||
'detail': {
|
||||
'response': {
|
||||
'body': '',
|
||||
'headers': [],
|
||||
'status_code': 0
|
||||
}
|
||||
},
|
||||
'http_request': '',
|
||||
'info': '',
|
||||
'level': 0,
|
||||
'method': '',
|
||||
'module': '',
|
||||
'parameter': '',
|
||||
'path': '',
|
||||
'referer': '',
|
||||
'wstg': []}],
|
||||
'HTTP Secure Headers': [{'curl_command': '',
|
||||
'detail': {
|
||||
'response': {
|
||||
'body': '',
|
||||
'headers': [],
|
||||
'status_code': 0
|
||||
}
|
||||
},
|
||||
'http_request': '',
|
||||
'info': '',
|
||||
'level': 0,
|
||||
'method': '',
|
||||
'module': '',
|
||||
'parameter': '',
|
||||
'path': '',
|
||||
'referer': '',
|
||||
'wstg': []}],
|
||||
'Htaccess Bypass': [{'curl_command': '',
|
||||
'detail': {
|
||||
'response': {
|
||||
'body': '',
|
||||
'headers': [],
|
||||
'status_code': 0
|
||||
}
|
||||
},
|
||||
'http_request': '',
|
||||
'info': '',
|
||||
'level': 0,
|
||||
'method': '',
|
||||
'module': '',
|
||||
'parameter': '',
|
||||
'path': '',
|
||||
'referer': '',
|
||||
'wstg': []}],
|
||||
'HttpOnly Flag cookie': [{'curl_command': '',
|
||||
'detail': {
|
||||
'response': {
|
||||
'body': '',
|
||||
'headers': [],
|
||||
'status_code': 0
|
||||
}
|
||||
},
|
||||
'http_request': '',
|
||||
'info': '',
|
||||
'level': 0,
|
||||
'method': '',
|
||||
'module': '',
|
||||
'parameter': '',
|
||||
'path': '',
|
||||
'referer': '',
|
||||
'wstg': []}],
|
||||
'Log4Shell': [{'curl_command': '',
|
||||
'detail': {
|
||||
'response': {
|
||||
'body': '',
|
||||
'headers': [],
|
||||
'status_code': 0
|
||||
}
|
||||
},
|
||||
'http_request': '',
|
||||
'info': '',
|
||||
'level': 0,
|
||||
'method': '',
|
||||
'module': '',
|
||||
'parameter': '',
|
||||
'path': '',
|
||||
'referer': '',
|
||||
'wstg': []}],
|
||||
'Open Redirect': [{'curl_command': '',
|
||||
'detail': {
|
||||
'response': {
|
||||
'body': '',
|
||||
'headers': [],
|
||||
'status_code': 0
|
||||
}
|
||||
},
|
||||
'http_request': '',
|
||||
'info': '',
|
||||
'level': 0,
|
||||
'method': '',
|
||||
'module': '',
|
||||
'parameter': '',
|
||||
'path': '',
|
||||
'referer': '',
|
||||
'wstg': []}],
|
||||
'Path Traversal': [{'curl_command': '',
|
||||
'detail': {
|
||||
'response': {
|
||||
'body': '',
|
||||
'headers': [],
|
||||
'status_code': 0
|
||||
}
|
||||
},
|
||||
'http_request': '',
|
||||
'info': '',
|
||||
'level': 0,
|
||||
'method': '',
|
||||
'module': '',
|
||||
'parameter': '',
|
||||
'path': '',
|
||||
'referer': '',
|
||||
'wstg': []}],
|
||||
'Potentially dangerous file': [{'curl_command': '',
|
||||
'detail': {
|
||||
'response': {
|
||||
'body': '',
|
||||
'headers': [],
|
||||
'status_code': 0
|
||||
}
|
||||
},
|
||||
'http_request': '',
|
||||
'info': '',
|
||||
'level': 0,
|
||||
'method': '',
|
||||
'module': '',
|
||||
'parameter': '',
|
||||
'path': '',
|
||||
'referer': '',
|
||||
'wstg': []}],
|
||||
'Reflected Cross Site Scripting': [{'curl_command': '',
|
||||
'detail': {
|
||||
'response': {
|
||||
'body': '',
|
||||
'headers': [],
|
||||
'status_code': 0
|
||||
}
|
||||
},
|
||||
'http_request': '',
|
||||
'info': '',
|
||||
'level': 0,
|
||||
'method': '',
|
||||
'module': '',
|
||||
'parameter': '',
|
||||
'path': '',
|
||||
'referer': '',
|
||||
'wstg': []}],
|
||||
'SQL Injection': [{'curl_command': '',
|
||||
'detail': {
|
||||
'response': {
|
||||
'body': '',
|
||||
'headers': [],
|
||||
'status_code': 0
|
||||
}
|
||||
},
|
||||
'http_request': '',
|
||||
'info': '',
|
||||
'level': 0,
|
||||
'method': '',
|
||||
'module': '',
|
||||
'parameter': '',
|
||||
'path': '',
|
||||
'referer': '',
|
||||
'wstg': []}],
|
||||
'Secure Flag cookie': [{'curl_command': '',
|
||||
'detail': {
|
||||
'response': {
|
||||
'body': '',
|
||||
'headers': [],
|
||||
'status_code': 0
|
||||
}
|
||||
},
|
||||
'http_request': '',
|
||||
'info': '',
|
||||
'level': 0,
|
||||
'method': '',
|
||||
'module': '',
|
||||
'parameter': '',
|
||||
'path': '',
|
||||
'referer': '',
|
||||
'wstg': []}],
|
||||
'Server Side Request Forgery': [{'curl_command': '',
|
||||
'detail': {
|
||||
'response': {
|
||||
'body': '',
|
||||
'headers': [],
|
||||
'status_code': 0
|
||||
}
|
||||
},
|
||||
'http_request': '',
|
||||
'info': '',
|
||||
'level': 0,
|
||||
'method': '',
|
||||
'module': '',
|
||||
'parameter': '',
|
||||
'path': '',
|
||||
'referer': '',
|
||||
'wstg': []}],
|
||||
'Stored Cross Site Scripting': [{'curl_command': '',
|
||||
'detail': {
|
||||
'response': {
|
||||
'body': '',
|
||||
'headers': [],
|
||||
'status_code': 0
|
||||
}
|
||||
},
|
||||
'http_request': '',
|
||||
'info': '',
|
||||
'level': 0,
|
||||
'method': '',
|
||||
'module': '',
|
||||
'parameter': '',
|
||||
'path': '',
|
||||
'referer': '',
|
||||
'wstg': []}],
|
||||
'Subdomain takeover': [{'curl_command': '',
|
||||
'detail': {
|
||||
'response': {
|
||||
'body': '',
|
||||
'headers': [],
|
||||
'status_code': 0
|
||||
}
|
||||
},
|
||||
'http_request': '',
|
||||
'info': '',
|
||||
'level': 0,
|
||||
'method': '',
|
||||
'module': '',
|
||||
'parameter': '',
|
||||
'path': '',
|
||||
'referer': '',
|
||||
'wstg': []}],
|
||||
'TLS/SSL misconfigurations': [{'curl_command': '',
|
||||
'detail': {
|
||||
'response': {
|
||||
'body': '',
|
||||
'headers': [],
|
||||
'status_code': 0
|
||||
}
|
||||
},
|
||||
'http_request': '',
|
||||
'info': '',
|
||||
'level': 0,
|
||||
'method': '',
|
||||
'module': '',
|
||||
'parameter': '',
|
||||
'path': '',
|
||||
'referer': '',
|
||||
'wstg': []}],
|
||||
'Weak credentials': [{'curl_command': '',
|
||||
'detail': {
|
||||
'response': {
|
||||
'body': '',
|
||||
'headers': [],
|
||||
'status_code': 0
|
||||
}
|
||||
},
|
||||
'http_request': '',
|
||||
'info': '',
|
||||
'level': 0,
|
||||
'method': '',
|
||||
'module': '',
|
||||
'parameter': '',
|
||||
'path': '',
|
||||
'referer': '',
|
||||
'wstg': []}],
|
||||
'XML External Entity': [{'curl_command': '',
|
||||
'detail': {
|
||||
'response': {
|
||||
'body': '',
|
||||
'headers': [],
|
||||
'status_code': 0
|
||||
}
|
||||
},
|
||||
'http_request': '',
|
||||
'info': '',
|
||||
'level': 0,
|
||||
'method': '',
|
||||
'module': '',
|
||||
'parameter': '',
|
||||
'path': '',
|
||||
'referer': '',
|
||||
'wstg': []}]},
|
||||
"anomalies": {
|
||||
'Internal Server Error': [],
|
||||
'Resource consumption': []},
|
||||
"additionals": {
|
||||
'Fingerprint web technology': [],
|
||||
'HTTP Methods': [],
|
||||
'Review Webserver Metafiles for Information Leakage': []},
|
||||
"infos": {'auth': None,
|
||||
'crawled_pages': [{
|
||||
'request': {
|
||||
'depth': 0,
|
||||
'encoding': "",
|
||||
'enctype': "",
|
||||
'headers': [],
|
||||
'methods': "",
|
||||
'referer': "",
|
||||
'url': ""},
|
||||
'response': {
|
||||
'body': "",
|
||||
'headers': [],
|
||||
'status_code': 0
|
||||
}
|
||||
}],
|
||||
'crawled_pages_nbr': 0,
|
||||
'detailed_report': False,
|
||||
'scope': "",
|
||||
'target': "",
|
||||
'version': ""
|
||||
}
|
||||
}
|
|
@ -0,0 +1,118 @@
|
|||
import json
|
||||
import os
|
||||
import sys
|
||||
import requests
|
||||
import re
|
||||
from itertools import cycle
|
||||
from collections import defaultdict
|
||||
from itertools import chain
|
||||
from uuid import uuid4
|
||||
from time import sleep
|
||||
|
||||
from misc_functions import purge_irrelevant_data, filter_data, all_keys_dicts, sort_lists_in_dict
|
||||
from templates_and_data import DEFAULT_FILTER_TREE, EXISTING_MODULES, TREE_CHECKER
|
||||
|
||||
# parsing and checking the json file containing the modules
|
||||
with open('/usr/local/bin/modules.json', 'r') as integration_file:
|
||||
integration_data = json.load(integration_file)
|
||||
wanted_modules = set(chain.from_iterable([test["modules"].split(",")
|
||||
for _, test in integration_data.items()]))
|
||||
assert wanted_modules.issubset(EXISTING_MODULES), f"{wanted_modules-EXISTING_MODULES} modules not existing"
|
||||
|
||||
# Adding on-the-fly uuid for each target for each test
|
||||
# and using quotes for empty modules
|
||||
for _, integ_test in integration_data.items():
|
||||
if not len(integ_test["modules"]):
|
||||
integ_test["modules"] = "\"\""
|
||||
for target in integ_test['targets']:
|
||||
target.update({"uid": str(uuid4())})
|
||||
|
||||
# Eventually filter arguments if any
|
||||
if len(sys.argv) > 1:
|
||||
# first checking unknown tests/typo errors
|
||||
assert set(sys.argv[1:]).issubset(set(key for key, _ in integration_data.items()))
|
||||
# then filtering the wanted integration tests
|
||||
integration_data = {key: test for key, test in integration_data.items() if key in sys.argv[1:]}
|
||||
|
||||
# creating folders for the logs
|
||||
for test_key, _ in integration_data.items():
|
||||
if not os.path.exists(f"/home/{test_key}"):
|
||||
os.mkdir(f"/home/{test_key}")
|
||||
|
||||
# All keys available in a general default report
|
||||
# to check syntax of filters on the fly
|
||||
KEYS_AVAILABLE = all_keys_dicts(TREE_CHECKER)
|
||||
|
||||
# data structures to count and cycle through targets
|
||||
targets_done = set()
|
||||
iter_tests = cycle(integration_data.items())
|
||||
total_targets = sum([len(test["targets"]) for _, test in integration_data.items()])
|
||||
|
||||
# If any target recieve too many requests, it might not have
|
||||
# started well, this is another way to fill the set to break
|
||||
# the loop
|
||||
requests_counter = defaultdict(int)
|
||||
MAX_REQ = 25
|
||||
|
||||
# Running wapiti for each module for each target
|
||||
# If a target isn't set up, passing to another and so on
|
||||
# That way we don't have a strict order and spare testing time
|
||||
for key_test, content_test in iter_tests:
|
||||
if len(targets_done) == total_targets:
|
||||
break
|
||||
for target in content_test["targets"]:
|
||||
if target['uid'] not in targets_done:
|
||||
sys.stdout.write(f"Querying target {target['name']}...\n")
|
||||
requests_counter[target['name']] += 1
|
||||
try:
|
||||
requests.get(f"{target['name']}", verify=False)
|
||||
json_output_path = f"/home/{key_test}/{re.sub('/','_',re.sub(r'^https?://', '', target['name']))}.out"
|
||||
|
||||
# We define supplementary arguments globally and for each target:
|
||||
more_args = target.get('supplementary_argument', '') + \
|
||||
('' if target.get("erase_global_supplementary", False)
|
||||
else content_test.get('supplementary_argument', ''))
|
||||
|
||||
# We then call wapiti on each target of each module, generating a detailed JSON report
|
||||
os.system(f"wapiti -u {target['name']} -m {content_test['modules']} "
|
||||
f"-f json -o {json_output_path} "
|
||||
f"{more_args} "
|
||||
f"--detailed-report --flush-session --verbose 2 ")
|
||||
# Now we reparse the JSON to get only useful tests informations:
|
||||
with open(json_output_path, "r") as bloated_output_file:
|
||||
bloated_output_data = json.load(bloated_output_file)
|
||||
with open(json_output_path, "w") as output_file:
|
||||
|
||||
# is a filter_tree supplied for this test ?
|
||||
if "report_filter_tree" in content_test and len(content_test["report_filter_tree"]):
|
||||
filter_tree = content_test["report_filter_tree"]
|
||||
# We look for key that CANNOT exist at all,
|
||||
# not even with a full report
|
||||
filter_keys = all_keys_dicts(filter_tree)
|
||||
assert filter_keys.issubset(KEYS_AVAILABLE), \
|
||||
f"Keys not existing at all: {filter_keys - KEYS_AVAILABLE}"
|
||||
else:
|
||||
filter_tree = DEFAULT_FILTER_TREE
|
||||
|
||||
filtered_data = filter_data(bloated_output_data, filter_tree)
|
||||
|
||||
# Some dates and other non determinist data
|
||||
# still exists somewhere in the detailed report
|
||||
bloated_output_data.get("infos", {}).pop("date", None)
|
||||
purge_irrelevant_data(filtered_data)
|
||||
|
||||
# Arrays in JSONs needs to be ordered to prevent false positive
|
||||
sort_lists_in_dict(filtered_data)
|
||||
|
||||
# Rewriting the file
|
||||
json.dump(filtered_data, output_file, indent=4)
|
||||
targets_done.add(target['uid'])
|
||||
except requests.exceptions.ConnectionError:
|
||||
sys.stdout.write(f"Target {target['name']} from test {key_test} is not read...(yet ?)\n")
|
||||
# 0.5 seconds penalty in case of no response to avoid requests spamming and being
|
||||
# too fast at blacklisting targets
|
||||
sleep(0.5)
|
||||
if requests_counter[target['name']] > MAX_REQ:
|
||||
sys.stdout.write(
|
||||
f"Target {target['name']} from test {key_test} takes too long to respond\nSkipping...\n")
|
||||
targets_done.add(target['uid'])
|
Loading…
Reference in New Issue