diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 58f0594c..cb17b29b 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,4 +1,14 @@ +variables: + VAULT_SERVER_URL: https://tluav-lb.faradaysec.com + VAULT_AUTH_ROLE: python-sast-readonly + VAULT_AUTH_PATH: jwt + + +include: + - local: .gitlab/ci/get-secrets.yml + stages: + - SAST - pre_testing - testing - post_testing @@ -14,6 +24,12 @@ workflow: when: never - when: always +.parse-secrets: &parse-secrets +- DEVSECOPS_WORKSPACE=$(cat $DEVSECOPS_WORKSPACE) +- FARADAY_PASSWORD=$(cat $FARADAY_PASSWORD) +- FARADAY_URL=$(cat $FARADAY_URL) +- FARADAY_USER=$(cat $FARADAY_USER) + .install_faraday_venv: &install_faraday_venv - pip3 install virtualenv - virtualenv -p python3 faraday_venv @@ -32,9 +48,37 @@ workflow: - git checkout $REPORT_REF - cd .. +bandit: + stage: SAST + image: python:3.11 + tags: + - faradaytests + extends: + - .get-secrets + script: + - pip3 install virtualenv + - virtualenv -p python3 faraday_venv + - source faraday_venv/bin/activate + - pip3 install bandit + - mkdir /results + - "bandit -r ${CI_PROJECT_DIR}/faraday-plugins -o /results/output.xml -f xml --skip B101,B104,B410,B405,B314,B320" + - if [[ $(grep -c testcase /results/output.xml) -gt 0 ]]; then (cat /results/output.xml); fi + after_script: + - *parse-secrets + - apt update && apt-get install lsb-release gpg wget -y + - apt-get install software-properties-common -y + - wget -O- https://apt.releases.hashicorp.com/gpg | gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg + - gpg --no-default-keyring --keyring /usr/share/keyrings/hashicorp-archive-keyring.gpg --fingerprint + - echo "deb [ signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg ] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | tee /etc/apt/sources.list.d/hashicorp.list + - apt update && apt install vault -y + - setcap cap_ipc_lock= /usr/bin/vault + - pip3 install faraday-cli + - if [[ $(grep -c testcase /results/output.xml) -gt 0 ]]; then (faraday-cli auth -f $FARADAY_URL -u $FARADAY_USER -p $FARADAY_PASSWORD && faraday-cli tool report /results/output.xml -w $DEVSECOPS_WORKSPACE --vuln-tag $CI_PROJECT_NAME --vuln-tag $CI_COMMIT_REF_NAME); else (echo 'no vulns detected' && exit 0); fi + rules: + - when: on_success flake8: - image: python:3 + image: python:3.11 stage: pre_testing before_script: - pip install flake8 @@ -57,11 +101,11 @@ flake8: tests: extends: .test_base - image: python:3 + image: python:3.11 test_performance: extends: .test_base - image: python:3 + image: python:3.11 stage: post_testing allow_failure: true variables: @@ -71,14 +115,13 @@ test_performance: when: on_success publish_pypi: - image: python:3 - stage: publish - script: - - apt-get update -qy - - apt-get install twine -y - - python setup.py sdist bdist_wheel - - twine upload -u $PYPI_USER -p $PYPI_PASS dist/* --verbose - rules: - - if: '$CI_COMMIT_TAG' - when: on_success - + image: python:3.11 + stage: publish + script: + - apt-get update -qy + - apt-get install twine -y + - python setup.py sdist bdist_wheel + - twine upload -u $PYPI_USER -p $PYPI_PASS dist/* --verbose + rules: + - if: '$CI_COMMIT_TAG' + when: on_success diff --git a/.gitlab/ci/fetch-secrets.yml b/.gitlab/ci/fetch-secrets.yml new file mode 100644 index 00000000..e12a67bd --- /dev/null +++ b/.gitlab/ci/fetch-secrets.yml @@ -0,0 +1,7 @@ +.get_secrets: + script: + - export VAULT_TOKEN="$(vault write -field=token auth/jwt/login role=python-sast-readonly jwt=$CI_JOB_JWT)"; if [ -z "$VAULT_TOKEN" ]; then exit 1; fi + - if [ -z "$DEVSECOPS_WORKSPACE" ]; then export DEVSECOPS_WORKSPACE="$(vault kv get -field=DEVSECOPS_WORKSPACE secrets/gitlab/SAST)"; fi; if [ -z "$DEVSECOPS_WORKSPACE" ]; then exit 1; fi + - if [ -z "$FARADAY_PASSWORD" ]; then export FARADAY_PASSWORD="$(vault kv get -field=FARADAY_PASSWORD secrets/gitlab/SAST)"; fi; if [ -z "$FARADAY_PASSWORD" ]; then exit 1; fi + - if [ -z "$FARADAY_URL" ]; then export FARADAY_URL="$(vault kv get -field=FARADAY_URL secrets/gitlab/SAST)"; fi; if [ -z "$FARADAY_URL" ]; then exit 1; fi + - if [ -z "$FARADAY_USER" ]; then export FARADAY_USER="$(vault kv get -field=FARADAY_USER secrets/gitlab/SAST)"; fi; if [ -z "$FARADAY_USER" ]; then exit 1; fi diff --git a/.gitlab/ci/get-secrets.yml b/.gitlab/ci/get-secrets.yml new file mode 100644 index 00000000..0bc27d32 --- /dev/null +++ b/.gitlab/ci/get-secrets.yml @@ -0,0 +1,13 @@ +.get-secrets: + id_tokens: + VAULT_ID_TOKEN: + aud: https://gitlab.com + secrets: + DEVSECOPS_WORKSPACE: + vault: gitlab/SAST/DEVSECOPS_WORKSPACE@secrets + FARADAY_PASSWORD: + vault: gitlab/SAST/FARADAY_PASSWORD@secrets + FARADAY_URL: + vault: gitlab/SAST/FARADAY_URL@secrets + FARADAY_USER: + vault: gitlab/SAST/FARADAY_USER@secrets diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..8232b668 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,26 @@ +default_stages: [commit] +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v3.1.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-json + - id: check-yaml + args: [ --unsafe ] + - id: debug-statements +- repo: https://github.com/pycqa/flake8 + rev: 3.8.3 + hooks: + - id: flake8 + additional_dependencies: [flake8-typing-imports==1.9.0] +- repo: https://github.com/ikamensh/flynt/ + rev: '0.56' + hooks: + - id: flynt + args: [ -df ] +- repo: https://github.com/asottile/pyupgrade + rev: v2.29.0 + hooks: + - id: pyupgrade + args: [ --py3-plus , --py36-plus] diff --git a/CHANGELOG/1.10.0/293.md b/CHANGELOG/1.10.0/293.md new file mode 100644 index 00000000..c3ad7ce9 --- /dev/null +++ b/CHANGELOG/1.10.0/293.md @@ -0,0 +1 @@ +[ADD] Add new acunetix360 plugin #293 diff --git a/CHANGELOG/1.10.0/date.md b/CHANGELOG/1.10.0/date.md new file mode 100644 index 00000000..0d1a0606 --- /dev/null +++ b/CHANGELOG/1.10.0/date.md @@ -0,0 +1 @@ +Jan 31th, 2023 diff --git a/CHANGELOG/1.11.0/292.md b/CHANGELOG/1.11.0/292.md new file mode 100644 index 00000000..865cfc79 --- /dev/null +++ b/CHANGELOG/1.11.0/292.md @@ -0,0 +1 @@ +[FIX] Change syhunt´s and trivy´s plugins to export cvss vector correctly #292 diff --git a/CHANGELOG/1.11.0/294.md b/CHANGELOG/1.11.0/294.md new file mode 100644 index 00000000..c721c13a --- /dev/null +++ b/CHANGELOG/1.11.0/294.md @@ -0,0 +1 @@ +[ADD] Add force flag to process-command to process the output of the command regardless of the exit code. #294 diff --git a/CHANGELOG/1.11.0/296.md b/CHANGELOG/1.11.0/296.md new file mode 100644 index 00000000..4367b9f6 --- /dev/null +++ b/CHANGELOG/1.11.0/296.md @@ -0,0 +1 @@ +[MOD] The accunetix plugin now search for CVSS and cvss #296 diff --git a/CHANGELOG/1.11.0/297.md b/CHANGELOG/1.11.0/297.md new file mode 100644 index 00000000..5d6cb4d3 --- /dev/null +++ b/CHANGELOG/1.11.0/297.md @@ -0,0 +1 @@ +[ADD] Add semgrep plugin. #297 diff --git a/CHANGELOG/1.11.0/298.md b/CHANGELOG/1.11.0/298.md new file mode 100644 index 00000000..1ad37b3d --- /dev/null +++ b/CHANGELOG/1.11.0/298.md @@ -0,0 +1 @@ +[FIX] Fix inviti's plugin, check remedial procedures before parsing it with b4f. #298 diff --git a/CHANGELOG/1.11.0/date.md b/CHANGELOG/1.11.0/date.md new file mode 100644 index 00000000..73c92d71 --- /dev/null +++ b/CHANGELOG/1.11.0/date.md @@ -0,0 +1 @@ +Apr 3rd, 2023 diff --git a/CHANGELOG/1.12.0/299.md b/CHANGELOG/1.12.0/299.md new file mode 100644 index 00000000..dc36e2d5 --- /dev/null +++ b/CHANGELOG/1.12.0/299.md @@ -0,0 +1 @@ +[ADD] Add Sarif plugin. #299 diff --git a/CHANGELOG/1.12.0/date.md b/CHANGELOG/1.12.0/date.md new file mode 100644 index 00000000..10f84e9a --- /dev/null +++ b/CHANGELOG/1.12.0/date.md @@ -0,0 +1 @@ +May 24th, 2023 diff --git a/CHANGELOG/1.12.1/302.md b/CHANGELOG/1.12.1/302.md new file mode 100644 index 00000000..511a6c99 --- /dev/null +++ b/CHANGELOG/1.12.1/302.md @@ -0,0 +1 @@ +[FIX] Fix Appscan's pluign. #302 diff --git a/CHANGELOG/1.12.1/date.md b/CHANGELOG/1.12.1/date.md new file mode 100644 index 00000000..0ff53151 --- /dev/null +++ b/CHANGELOG/1.12.1/date.md @@ -0,0 +1 @@ +July 7th, 2023 diff --git a/CHANGELOG/1.13.0/305.md b/CHANGELOG/1.13.0/305.md new file mode 100644 index 00000000..8d4c81b7 --- /dev/null +++ b/CHANGELOG/1.13.0/305.md @@ -0,0 +1 @@ +[FIX] If severity id in an appscan item is greater than 4 set it to 4. #305 diff --git a/CHANGELOG/1.13.0/306.md b/CHANGELOG/1.13.0/306.md new file mode 100644 index 00000000..dd7563c2 --- /dev/null +++ b/CHANGELOG/1.13.0/306.md @@ -0,0 +1 @@ +[FIX] Update Naabu plugin for the latest version, Semgrep create a new service for each vuln, fix Arachni bug in case the report has no vulns. #306 diff --git a/CHANGELOG/1.13.0/310.md b/CHANGELOG/1.13.0/310.md new file mode 100644 index 00000000..2a4128f7 --- /dev/null +++ b/CHANGELOG/1.13.0/310.md @@ -0,0 +1 @@ +[ADD] Add Terrascan and TFSec plugins. #310 diff --git a/CHANGELOG/1.13.0/311.md b/CHANGELOG/1.13.0/311.md new file mode 100644 index 00000000..d3dc0cb1 --- /dev/null +++ b/CHANGELOG/1.13.0/311.md @@ -0,0 +1 @@ +[FIX] Use cvss_score to calculate severity in nessus plugin. #311 diff --git a/CHANGELOG/1.13.0/date.md b/CHANGELOG/1.13.0/date.md new file mode 100644 index 00000000..0505e011 --- /dev/null +++ b/CHANGELOG/1.13.0/date.md @@ -0,0 +1 @@ +Aug 24th, 2023 diff --git a/CHANGELOG/1.13.2/307.md b/CHANGELOG/1.13.2/307.md new file mode 100644 index 00000000..e73a0060 --- /dev/null +++ b/CHANGELOG/1.13.2/307.md @@ -0,0 +1 @@ +[ADD] Extract response and request info in qualyswebapp's plugins. #307 diff --git a/CHANGELOG/1.13.2/315.md b/CHANGELOG/1.13.2/315.md new file mode 100644 index 00000000..eb3a3b7d --- /dev/null +++ b/CHANGELOG/1.13.2/315.md @@ -0,0 +1 @@ +[ADD] Create Plugin for windows defender. #315 diff --git a/CHANGELOG/1.13.2/date.md b/CHANGELOG/1.13.2/date.md new file mode 100644 index 00000000..22e2a907 --- /dev/null +++ b/CHANGELOG/1.13.2/date.md @@ -0,0 +1 @@ +Sep 6th, 2023 diff --git a/CHANGELOG/1.14.0/318.md b/CHANGELOG/1.14.0/318.md new file mode 100644 index 00000000..c577ca66 --- /dev/null +++ b/CHANGELOG/1.14.0/318.md @@ -0,0 +1 @@ +[ADD] Add Crowdstrike's plugin. #318 diff --git a/CHANGELOG/1.14.0/date.md b/CHANGELOG/1.14.0/date.md new file mode 100644 index 00000000..27869e03 --- /dev/null +++ b/CHANGELOG/1.14.0/date.md @@ -0,0 +1 @@ +Oct 10th, 2023 diff --git a/CHANGELOG/1.15.0/303.md b/CHANGELOG/1.15.0/303.md new file mode 100644 index 00000000..a29759a9 --- /dev/null +++ b/CHANGELOG/1.15.0/303.md @@ -0,0 +1 @@ +[ADD] Add PopEye's plugin. #303 diff --git a/CHANGELOG/1.15.0/304.md b/CHANGELOG/1.15.0/304.md new file mode 100644 index 00000000..02cef655 --- /dev/null +++ b/CHANGELOG/1.15.0/304.md @@ -0,0 +1 @@ +[ADD] Add Ping Castle's plugin. #304 diff --git a/CHANGELOG/1.15.0/320.md b/CHANGELOG/1.15.0/320.md new file mode 100644 index 00000000..e27c4f43 --- /dev/null +++ b/CHANGELOG/1.15.0/320.md @@ -0,0 +1 @@ +[ADD] Add Kubescape's plugin. #320 diff --git a/CHANGELOG/1.15.0/322.md b/CHANGELOG/1.15.0/322.md new file mode 100644 index 00000000..1ac043b1 --- /dev/null +++ b/CHANGELOG/1.15.0/322.md @@ -0,0 +1 @@ +[ADD] Add AWS Inspector's plugins. #322 diff --git a/CHANGELOG/1.15.0/date.md b/CHANGELOG/1.15.0/date.md new file mode 100644 index 00000000..ac7b15ea --- /dev/null +++ b/CHANGELOG/1.15.0/date.md @@ -0,0 +1 @@ +Dec 12th, 2023 diff --git a/CHANGELOG/1.15.1/323.md b/CHANGELOG/1.15.1/323.md new file mode 100644 index 00000000..df20bf7c --- /dev/null +++ b/CHANGELOG/1.15.1/323.md @@ -0,0 +1 @@ +[FIX] Filter \x00 in nuclei response. #323 diff --git a/CHANGELOG/1.15.1/date.md b/CHANGELOG/1.15.1/date.md new file mode 100644 index 00000000..7d4ea867 --- /dev/null +++ b/CHANGELOG/1.15.1/date.md @@ -0,0 +1 @@ +Dec 22th, 2023 diff --git a/CHANGELOG/1.16.0/314.md b/CHANGELOG/1.16.0/314.md new file mode 100644 index 00000000..39778581 --- /dev/null +++ b/CHANGELOG/1.16.0/314.md @@ -0,0 +1 @@ +[ADD] Add Snyk plugin. #314 diff --git a/CHANGELOG/1.16.0/322.md b/CHANGELOG/1.16.0/322.md new file mode 100644 index 00000000..431da97b --- /dev/null +++ b/CHANGELOG/1.16.0/322.md @@ -0,0 +1 @@ +[MOD] Mod AWS Inspector's plugins. #322 \ No newline at end of file diff --git a/CHANGELOG/1.16.0/324.md b/CHANGELOG/1.16.0/324.md new file mode 100644 index 00000000..a8d24cd3 --- /dev/null +++ b/CHANGELOG/1.16.0/324.md @@ -0,0 +1 @@ +[ADD] Add faraday_json plugins. #324 diff --git a/CHANGELOG/1.16.0/328.md b/CHANGELOG/1.16.0/328.md new file mode 100644 index 00000000..09326a54 --- /dev/null +++ b/CHANGELOG/1.16.0/328.md @@ -0,0 +1 @@ +[ADD] Update prowler plugin to support the latest tool output format. Also rename the oldest plugin to prowler_legacy. #328 diff --git a/CHANGELOG/1.16.0/date.md b/CHANGELOG/1.16.0/date.md new file mode 100644 index 00000000..185f0934 --- /dev/null +++ b/CHANGELOG/1.16.0/date.md @@ -0,0 +1 @@ +Feb 8th, 2024 diff --git a/CHANGELOG/1.17.0/321.md b/CHANGELOG/1.17.0/321.md new file mode 100644 index 00000000..b0a1618e --- /dev/null +++ b/CHANGELOG/1.17.0/321.md @@ -0,0 +1 @@ +[ADD] Add hotspots logic for sonarqube plugin #321 diff --git a/CHANGELOG/1.17.0/date.md b/CHANGELOG/1.17.0/date.md new file mode 100644 index 00000000..09b411bb --- /dev/null +++ b/CHANGELOG/1.17.0/date.md @@ -0,0 +1 @@ +Mar 12th, 2024 diff --git a/CHANGELOG/1.18.0/331.md b/CHANGELOG/1.18.0/331.md new file mode 100644 index 00000000..a43bc459 --- /dev/null +++ b/CHANGELOG/1.18.0/331.md @@ -0,0 +1 @@ +[FIX] Fix key error when `packageVulnerabilityDetails` key was not in the file. #331 diff --git a/CHANGELOG/1.18.0/333.md b/CHANGELOG/1.18.0/333.md new file mode 100644 index 00000000..6ea5311b --- /dev/null +++ b/CHANGELOG/1.18.0/333.md @@ -0,0 +1 @@ +[FIX] Addressed a bug where Burp plugin output would display null data in cases of encountering a malformed XML token from the report. #333 diff --git a/CHANGELOG/1.18.0/336.md b/CHANGELOG/1.18.0/336.md new file mode 100644 index 00000000..82ede73e --- /dev/null +++ b/CHANGELOG/1.18.0/336.md @@ -0,0 +1 @@ +[FIX] Previously, CSV files edited in tools like Mac Numbers would transform boolean values to uppercase. This issue has been addressed within the faraday_csv plugin, ensuring accurate comparison. #336 diff --git a/CHANGELOG/1.18.0/date.md b/CHANGELOG/1.18.0/date.md new file mode 100644 index 00000000..d7f736d5 --- /dev/null +++ b/CHANGELOG/1.18.0/date.md @@ -0,0 +1 @@ +May 22th, 2024 diff --git a/CHANGELOG/1.18.1/339.md b/CHANGELOG/1.18.1/339.md new file mode 100644 index 00000000..69f1d257 --- /dev/null +++ b/CHANGELOG/1.18.1/339.md @@ -0,0 +1 @@ +[MOD] Naabu reports changed their JSON structure, so new keys were added to detect the new report structure. #339 diff --git a/CHANGELOG/1.18.1/date.md b/CHANGELOG/1.18.1/date.md new file mode 100644 index 00000000..fee3ffd8 --- /dev/null +++ b/CHANGELOG/1.18.1/date.md @@ -0,0 +1 @@ +Jul 11th, 2024 diff --git a/CHANGELOG/1.18.2/343.md b/CHANGELOG/1.18.2/343.md new file mode 100644 index 00000000..a6d3d409 --- /dev/null +++ b/CHANGELOG/1.18.2/343.md @@ -0,0 +1 @@ +[FIX] Added validations for empty lines and multiple fields including lists. #343 diff --git a/CHANGELOG/1.18.2/date.md b/CHANGELOG/1.18.2/date.md new file mode 100644 index 00000000..2b68d6c7 --- /dev/null +++ b/CHANGELOG/1.18.2/date.md @@ -0,0 +1 @@ +Jul 24th, 2024 diff --git a/CHANGELOG/1.19.0/100.md b/CHANGELOG/1.19.0/100.md new file mode 100644 index 00000000..d00982a9 --- /dev/null +++ b/CHANGELOG/1.19.0/100.md @@ -0,0 +1 @@ +[ADD] Added owasp dependency check. #100 diff --git a/CHANGELOG/1.19.0/341.md b/CHANGELOG/1.19.0/341.md new file mode 100644 index 00000000..2452b629 --- /dev/null +++ b/CHANGELOG/1.19.0/341.md @@ -0,0 +1 @@ +[FIX] Nessus plugin crashed when parsing tenableio reports without vulnerabilities, so a check for that was added. #341 diff --git a/CHANGELOG/1.19.0/342.md b/CHANGELOG/1.19.0/342.md new file mode 100644 index 00000000..f46d0a2b --- /dev/null +++ b/CHANGELOG/1.19.0/342.md @@ -0,0 +1 @@ +[ADD] Added gitleaks plugin. #342 diff --git a/CHANGELOG/1.19.0/date.md b/CHANGELOG/1.19.0/date.md new file mode 100644 index 00000000..bb16a39c --- /dev/null +++ b/CHANGELOG/1.19.0/date.md @@ -0,0 +1 @@ +Aug 23rd, 2024 diff --git a/CHANGELOG/1.19.1/349.md b/CHANGELOG/1.19.1/349.md new file mode 100644 index 00000000..afdf3cec --- /dev/null +++ b/CHANGELOG/1.19.1/349.md @@ -0,0 +1 @@ +[MOD] Updated the SSLyze JSON parser to support and correctly process scan results from SSLyze version 6. #349 diff --git a/CHANGELOG/1.19.1/350.md b/CHANGELOG/1.19.1/350.md new file mode 100644 index 00000000..7a49e70c --- /dev/null +++ b/CHANGELOG/1.19.1/350.md @@ -0,0 +1 @@ +[FIX] Crowdstrike IP resolution for asset #350 diff --git a/CHANGELOG/1.19.1/351.md b/CHANGELOG/1.19.1/351.md new file mode 100644 index 00000000..fe3ac614 --- /dev/null +++ b/CHANGELOG/1.19.1/351.md @@ -0,0 +1 @@ +[FIX] Corrected a hostname resolution issue that was causing traceback errors and malfunctioning of the plugin. This fix ensures proper hostname resolution and stabilizes plugin performance. #351 diff --git a/CHANGELOG/1.19.1/date.md b/CHANGELOG/1.19.1/date.md new file mode 100644 index 00000000..d424556c --- /dev/null +++ b/CHANGELOG/1.19.1/date.md @@ -0,0 +1 @@ +Sep 20th, 2024 diff --git a/CHANGELOG/1.20.0/354.md b/CHANGELOG/1.20.0/354.md new file mode 100644 index 00000000..9a542836 --- /dev/null +++ b/CHANGELOG/1.20.0/354.md @@ -0,0 +1 @@ +[FIX] Duplicate detection vulnerability in the Burp plugin. #354 diff --git a/CHANGELOG/1.20.0/date.md b/CHANGELOG/1.20.0/date.md new file mode 100644 index 00000000..b2c2f718 --- /dev/null +++ b/CHANGELOG/1.20.0/date.md @@ -0,0 +1 @@ +Nov 21st, 2024 diff --git a/CHANGELOG/1.21.0/353.md b/CHANGELOG/1.21.0/353.md new file mode 100644 index 00000000..bbfbdf1f --- /dev/null +++ b/CHANGELOG/1.21.0/353.md @@ -0,0 +1 @@ +[ADD] Added Saint CSV report processor. #353 diff --git a/CHANGELOG/1.21.0/date.md b/CHANGELOG/1.21.0/date.md new file mode 100644 index 00000000..1835d233 --- /dev/null +++ b/CHANGELOG/1.21.0/date.md @@ -0,0 +1 @@ +Jan 6th, 2025 diff --git a/CHANGELOG/1.3.0/date.md b/CHANGELOG/1.3.0/date.md new file mode 100644 index 00000000..ca52a58b --- /dev/null +++ b/CHANGELOG/1.3.0/date.md @@ -0,0 +1 @@ +Sep 2nd, 2020 \ No newline at end of file diff --git a/CHANGELOG/1.4.0/date.md b/CHANGELOG/1.4.0/date.md new file mode 100644 index 00000000..70af2e91 --- /dev/null +++ b/CHANGELOG/1.4.0/date.md @@ -0,0 +1 @@ +Dec 23rd, 2020 diff --git a/CHANGELOG/1.4.0/update_nuclei_fields.md b/CHANGELOG/1.4.0/update_nuclei_fields.md new file mode 100644 index 00000000..9d67a341 --- /dev/null +++ b/CHANGELOG/1.4.0/update_nuclei_fields.md @@ -0,0 +1 @@ +Update the fields of the nuclei output used to create a vuln \ No newline at end of file diff --git a/CHANGELOG/current/add_multilinejson_format.md b/CHANGELOG/1.4.0b1/add_multilinejson_format.md similarity index 100% rename from CHANGELOG/current/add_multilinejson_format.md rename to CHANGELOG/1.4.0b1/add_multilinejson_format.md diff --git a/CHANGELOG/current/add_ncrack.md b/CHANGELOG/1.4.0b1/add_ncrack.md similarity index 100% rename from CHANGELOG/current/add_ncrack.md rename to CHANGELOG/1.4.0b1/add_ncrack.md diff --git a/CHANGELOG/current/add_nuclei.md b/CHANGELOG/1.4.0b1/add_nuclei.md similarity index 100% rename from CHANGELOG/current/add_nuclei.md rename to CHANGELOG/1.4.0b1/add_nuclei.md diff --git a/CHANGELOG/current/add_sslyze_json.md b/CHANGELOG/1.4.0b1/add_sslyze_json.md similarity index 100% rename from CHANGELOG/current/add_sslyze_json.md rename to CHANGELOG/1.4.0b1/add_sslyze_json.md diff --git a/CHANGELOG/current/add_whatweb.md b/CHANGELOG/1.4.0b1/add_whatweb.md similarity index 100% rename from CHANGELOG/current/add_whatweb.md rename to CHANGELOG/1.4.0b1/add_whatweb.md diff --git a/CHANGELOG/1.4.0b1/date.md b/CHANGELOG/1.4.0b1/date.md new file mode 100644 index 00000000..c691eb27 --- /dev/null +++ b/CHANGELOG/1.4.0b1/date.md @@ -0,0 +1 @@ +Dec 14th, 2020 \ No newline at end of file diff --git a/CHANGELOG/current/fix_arachni_ip_missing.md b/CHANGELOG/1.4.0b1/fix_arachni_ip_missing.md similarity index 100% rename from CHANGELOG/current/fix_arachni_ip_missing.md rename to CHANGELOG/1.4.0b1/fix_arachni_ip_missing.md diff --git a/CHANGELOG/current/fix_netsparker.md b/CHANGELOG/1.4.0b1/fix_netsparker.md similarity index 100% rename from CHANGELOG/current/fix_netsparker.md rename to CHANGELOG/1.4.0b1/fix_netsparker.md diff --git a/CHANGELOG/current/fix_whois.md b/CHANGELOG/1.4.0b1/fix_whois.md similarity index 100% rename from CHANGELOG/current/fix_whois.md rename to CHANGELOG/1.4.0b1/fix_whois.md diff --git a/CHANGELOG/current/modify_json_reports_detection.md b/CHANGELOG/1.4.0b1/modify_json_reports_detection.md similarity index 100% rename from CHANGELOG/current/modify_json_reports_detection.md rename to CHANGELOG/1.4.0b1/modify_json_reports_detection.md diff --git a/CHANGELOG/1.4.0b2/date.md b/CHANGELOG/1.4.0b2/date.md new file mode 100644 index 00000000..585afaf5 --- /dev/null +++ b/CHANGELOG/1.4.0b2/date.md @@ -0,0 +1 @@ +Dec 15th, 2020 \ No newline at end of file diff --git a/CHANGELOG/1.4.0b2/fix_nuclei.md b/CHANGELOG/1.4.0b2/fix_nuclei.md new file mode 100644 index 00000000..ef0a22eb --- /dev/null +++ b/CHANGELOG/1.4.0b2/fix_nuclei.md @@ -0,0 +1 @@ +Fix nuclei plugin bug when url is None \ No newline at end of file diff --git a/CHANGELOG/1.4.1/add_microsoft_baseline.md b/CHANGELOG/1.4.1/add_microsoft_baseline.md new file mode 100644 index 00000000..bf729f6a --- /dev/null +++ b/CHANGELOG/1.4.1/add_microsoft_baseline.md @@ -0,0 +1 @@ +ADD microsoft baseline security analyzer plugin diff --git a/CHANGELOG/1.4.1/add_nextnet.md b/CHANGELOG/1.4.1/add_nextnet.md new file mode 100644 index 00000000..b9f53277 --- /dev/null +++ b/CHANGELOG/1.4.1/add_nextnet.md @@ -0,0 +1 @@ +ADD nextnet plugin diff --git a/CHANGELOG/1.4.1/add_openscap.md b/CHANGELOG/1.4.1/add_openscap.md new file mode 100644 index 00000000..4cfac92f --- /dev/null +++ b/CHANGELOG/1.4.1/add_openscap.md @@ -0,0 +1 @@ +ADD openscap plugin diff --git a/CHANGELOG/1.4.1/date.md b/CHANGELOG/1.4.1/date.md new file mode 100644 index 00000000..1c7356eb --- /dev/null +++ b/CHANGELOG/1.4.1/date.md @@ -0,0 +1 @@ +Feb 26th, 2021 diff --git a/CHANGELOG/1.4.1/fix_nessus_plugin.md b/CHANGELOG/1.4.1/fix_nessus_plugin.md new file mode 100644 index 00000000..34dba3ab --- /dev/null +++ b/CHANGELOG/1.4.1/fix_nessus_plugin.md @@ -0,0 +1 @@ +FIX old versions of Nessus plugins bugs diff --git a/CHANGELOG/1.4.2/date.md b/CHANGELOG/1.4.2/date.md new file mode 100644 index 00000000..45d9b329 --- /dev/null +++ b/CHANGELOG/1.4.2/date.md @@ -0,0 +1 @@ +Mar 10th, 2021 diff --git a/CHANGELOG/1.4.2/fix_bug_in_sslyze_output_file.md b/CHANGELOG/1.4.2/fix_bug_in_sslyze_output_file.md new file mode 100644 index 00000000..ce00a3c1 --- /dev/null +++ b/CHANGELOG/1.4.2/fix_bug_in_sslyze_output_file.md @@ -0,0 +1 @@ +Fix bug with sslyze output file diff --git a/CHANGELOG/1.4.2/fix_sslyze_plugin.md b/CHANGELOG/1.4.2/fix_sslyze_plugin.md new file mode 100644 index 00000000..36cd2cc4 --- /dev/null +++ b/CHANGELOG/1.4.2/fix_sslyze_plugin.md @@ -0,0 +1 @@ +FIX change id sslyze for JSON/XML diff --git a/CHANGELOG/1.4.3/date.md b/CHANGELOG/1.4.3/date.md new file mode 100644 index 00000000..8886c2b5 --- /dev/null +++ b/CHANGELOG/1.4.3/date.md @@ -0,0 +1 @@ +Mar 17th, 2021 diff --git a/CHANGELOG/1.4.3/new_ignore_info_option.md b/CHANGELOG/1.4.3/new_ignore_info_option.md new file mode 100644 index 00000000..64a23214 --- /dev/null +++ b/CHANGELOG/1.4.3/new_ignore_info_option.md @@ -0,0 +1 @@ +Add Ignore information vulnerabilities option diff --git a/CHANGELOG/1.4.4/csv_plugin_dont_user_ignore_info.md b/CHANGELOG/1.4.4/csv_plugin_dont_user_ignore_info.md new file mode 100644 index 00000000..78c8492c --- /dev/null +++ b/CHANGELOG/1.4.4/csv_plugin_dont_user_ignore_info.md @@ -0,0 +1 @@ +Faraday CSV Plugin do not consider ignore_info diff --git a/CHANGELOG/1.4.4/date.md b/CHANGELOG/1.4.4/date.md new file mode 100644 index 00000000..2bf6cb08 --- /dev/null +++ b/CHANGELOG/1.4.4/date.md @@ -0,0 +1 @@ +Mar 30th, 2021 diff --git a/CHANGELOG/1.4.5/add_bandit_plugin.md b/CHANGELOG/1.4.5/add_bandit_plugin.md new file mode 100644 index 00000000..32347321 --- /dev/null +++ b/CHANGELOG/1.4.5/add_bandit_plugin.md @@ -0,0 +1 @@ +Add Bandit plugin diff --git a/CHANGELOG/1.4.5/change_burp_fields.md b/CHANGELOG/1.4.5/change_burp_fields.md new file mode 100644 index 00000000..63529a11 --- /dev/null +++ b/CHANGELOG/1.4.5/change_burp_fields.md @@ -0,0 +1 @@ +Use background for description and detail for data en Burp plugin. diff --git a/CHANGELOG/1.4.5/date.md b/CHANGELOG/1.4.5/date.md new file mode 100644 index 00000000..0d5ebf9e --- /dev/null +++ b/CHANGELOG/1.4.5/date.md @@ -0,0 +1 @@ +Apr 15th, 2021 diff --git a/CHANGELOG/1.4.5/fix_appscan.md b/CHANGELOG/1.4.5/fix_appscan.md new file mode 100644 index 00000000..754678a8 --- /dev/null +++ b/CHANGELOG/1.4.5/fix_appscan.md @@ -0,0 +1 @@ +Rewrite Appscan Plugin diff --git a/CHANGELOG/1.4.5/parse_nmap_vulnes.md b/CHANGELOG/1.4.5/parse_nmap_vulnes.md new file mode 100644 index 00000000..6e79a68c --- /dev/null +++ b/CHANGELOG/1.4.5/parse_nmap_vulnes.md @@ -0,0 +1 @@ +Parse Nmap vulners script data diff --git a/CHANGELOG/1.4.6/add-attribute_command_for_plugins_of_all_command.md b/CHANGELOG/1.4.6/add-attribute_command_for_plugins_of_all_command.md new file mode 100644 index 00000000..1b2533a8 --- /dev/null +++ b/CHANGELOG/1.4.6/add-attribute_command_for_plugins_of_all_command.md @@ -0,0 +1,3 @@ +- add attribute "command" for the pluggins of each command +- adding test in test_command +- change some regex in self._command_regex \ No newline at end of file diff --git a/CHANGELOG/1.4.6/add_hostnames_to_cached_host.md b/CHANGELOG/1.4.6/add_hostnames_to_cached_host.md new file mode 100644 index 00000000..69bdad0c --- /dev/null +++ b/CHANGELOG/1.4.6/add_hostnames_to_cached_host.md @@ -0,0 +1 @@ +[FIX] add hostnames if host is already cached diff --git a/CHANGELOG/1.4.6/add_naabu_plugin.md b/CHANGELOG/1.4.6/add_naabu_plugin.md new file mode 100644 index 00000000..99102b7c --- /dev/null +++ b/CHANGELOG/1.4.6/add_naabu_plugin.md @@ -0,0 +1 @@ +Add Naabu plugin diff --git a/CHANGELOG/1.4.6/add_sonarqube.md b/CHANGELOG/1.4.6/add_sonarqube.md new file mode 100644 index 00000000..330d55cb --- /dev/null +++ b/CHANGELOG/1.4.6/add_sonarqube.md @@ -0,0 +1 @@ +Add Sonarqube plugin diff --git a/CHANGELOG/1.4.6/change_list_plugins.md b/CHANGELOG/1.4.6/change_list_plugins.md new file mode 100644 index 00000000..5bf08716 --- /dev/null +++ b/CHANGELOG/1.4.6/change_list_plugins.md @@ -0,0 +1 @@ +Add version and change list_plugins style diff --git a/CHANGELOG/1.4.6/clean_code.md b/CHANGELOG/1.4.6/clean_code.md new file mode 100644 index 00000000..d306b2ee --- /dev/null +++ b/CHANGELOG/1.4.6/clean_code.md @@ -0,0 +1 @@ +FIX unused import, innecesary list compression and unused variables \ No newline at end of file diff --git a/CHANGELOG/1.4.6/date.md b/CHANGELOG/1.4.6/date.md new file mode 100644 index 00000000..8c3ac607 --- /dev/null +++ b/CHANGELOG/1.4.6/date.md @@ -0,0 +1 @@ +May 14th, 2021 diff --git a/CHANGELOG/1.4.6/error_when_import_report_of_metasploit_with_faraday_plugins.md b/CHANGELOG/1.4.6/error_when_import_report_of_metasploit_with_faraday_plugins.md new file mode 100644 index 00000000..989f5f0e --- /dev/null +++ b/CHANGELOG/1.4.6/error_when_import_report_of_metasploit_with_faraday_plugins.md @@ -0,0 +1 @@ +FIX metasploit report when the web-site-id is null \ No newline at end of file diff --git a/CHANGELOG/1.4.6/fix_nmap_port_status.md b/CHANGELOG/1.4.6/fix_nmap_port_status.md new file mode 100644 index 00000000..52a24744 --- /dev/null +++ b/CHANGELOG/1.4.6/fix_nmap_port_status.md @@ -0,0 +1 @@ +Fix port stats in nmap diff --git a/CHANGELOG/1.4.6/fixup_sslyze.md b/CHANGELOG/1.4.6/fixup_sslyze.md new file mode 100644 index 00000000..279e498c --- /dev/null +++ b/CHANGELOG/1.4.6/fixup_sslyze.md @@ -0,0 +1,2 @@ +fixup ssylze +sacar unknown de version= diff --git a/CHANGELOG/1.4.6/issue_in_netsparker.md b/CHANGELOG/1.4.6/issue_in_netsparker.md new file mode 100644 index 00000000..e4e055f1 --- /dev/null +++ b/CHANGELOG/1.4.6/issue_in_netsparker.md @@ -0,0 +1 @@ +ADD remedy into resolution \ No newline at end of file diff --git a/CHANGELOG/1.4.6/update_nuclei.md b/CHANGELOG/1.4.6/update_nuclei.md new file mode 100644 index 00000000..f0ff714a --- /dev/null +++ b/CHANGELOG/1.4.6/update_nuclei.md @@ -0,0 +1 @@ +Support for nuclei 2.3.0 diff --git a/CHANGELOG/1.4.6/when_import_report_of_nessus_we_can_get_more_data.md b/CHANGELOG/1.4.6/when_import_report_of_nessus_we_can_get_more_data.md new file mode 100644 index 00000000..3fe6ad29 --- /dev/null +++ b/CHANGELOG/1.4.6/when_import_report_of_nessus_we_can_get_more_data.md @@ -0,0 +1 @@ +ADD cve, cvss3_base_score, cvss3_vector, exploit_available when import nessus and change the structure of external_id to NESSUS-XXX \ No newline at end of file diff --git a/CHANGELOG/1.4.6/when_import_reports_of_owasp_and_zap_we_might_get_more_data.md b/CHANGELOG/1.4.6/when_import_reports_of_owasp_and_zap_we_might_get_more_data.md new file mode 100644 index 00000000..d9d98473 --- /dev/null +++ b/CHANGELOG/1.4.6/when_import_reports_of_owasp_and_zap_we_might_get_more_data.md @@ -0,0 +1 @@ +ADD more data like attack, params, uri, method, WASC, CWE and format externail_id \ No newline at end of file diff --git a/CHANGELOG/1.5.0/add_nipper_plugin.md b/CHANGELOG/1.5.0/add_nipper_plugin.md new file mode 100644 index 00000000..10aa6052 --- /dev/null +++ b/CHANGELOG/1.5.0/add_nipper_plugin.md @@ -0,0 +1 @@ +Add Nipper Plugin diff --git a/CHANGELOG/1.5.0/add_shodan_plugin.md b/CHANGELOG/1.5.0/add_shodan_plugin.md new file mode 100644 index 00000000..5b2c4c79 --- /dev/null +++ b/CHANGELOG/1.5.0/add_shodan_plugin.md @@ -0,0 +1 @@ +add shodan plugin diff --git a/CHANGELOG/1.5.0/date.md b/CHANGELOG/1.5.0/date.md new file mode 100644 index 00000000..136efa0b --- /dev/null +++ b/CHANGELOG/1.5.0/date.md @@ -0,0 +1 @@ +Jun 28th, 2021 diff --git a/CHANGELOG/1.5.0/fix_acunetix_url_parser.md b/CHANGELOG/1.5.0/fix_acunetix_url_parser.md new file mode 100644 index 00000000..89fe358b --- /dev/null +++ b/CHANGELOG/1.5.0/fix_acunetix_url_parser.md @@ -0,0 +1 @@ +fix acunetix url parser diff --git a/CHANGELOG/1.5.0/fix_netsparker_multihost.md b/CHANGELOG/1.5.0/fix_netsparker_multihost.md new file mode 100644 index 00000000..720241a3 --- /dev/null +++ b/CHANGELOG/1.5.0/fix_netsparker_multihost.md @@ -0,0 +1 @@ +FIX netsparker multi-host \ No newline at end of file diff --git a/CHANGELOG/1.5.0/fixup_ssylyze_desc_data.md b/CHANGELOG/1.5.0/fixup_ssylyze_desc_data.md new file mode 100644 index 00000000..2f3a01c3 --- /dev/null +++ b/CHANGELOG/1.5.0/fixup_ssylyze_desc_data.md @@ -0,0 +1 @@ +Add vuln details for Certificate Mismatch and move unique details to data, now vulns can be grupped diff --git a/CHANGELOG/1.5.0/many-error-when-parse_the_xml_of_arachni_and_w3af.md b/CHANGELOG/1.5.0/many-error-when-parse_the_xml_of_arachni_and_w3af.md new file mode 100644 index 00000000..b390c4e6 --- /dev/null +++ b/CHANGELOG/1.5.0/many-error-when-parse_the_xml_of_arachni_and_w3af.md @@ -0,0 +1 @@ +ADD more data to plugins arachni and w3af \ No newline at end of file diff --git a/CHANGELOG/1.5.0/run_date_utc.md b/CHANGELOG/1.5.0/run_date_utc.md new file mode 100644 index 00000000..31440832 --- /dev/null +++ b/CHANGELOG/1.5.0/run_date_utc.md @@ -0,0 +1 @@ +Use run_date in UTC diff --git a/CHANGELOG/1.5.0/we_can_get_more_data_of_the_reports_of_openvas.md b/CHANGELOG/1.5.0/we_can_get_more_data_of_the_reports_of_openvas.md new file mode 100644 index 00000000..407f217a --- /dev/null +++ b/CHANGELOG/1.5.0/we_can_get_more_data_of_the_reports_of_openvas.md @@ -0,0 +1 @@ +ADD cvss_base, cpe, threat, severity into references \ No newline at end of file diff --git a/CHANGELOG/1.5.1/add_nuclei_fields.md b/CHANGELOG/1.5.1/add_nuclei_fields.md new file mode 100644 index 00000000..cd1d287b --- /dev/null +++ b/CHANGELOG/1.5.1/add_nuclei_fields.md @@ -0,0 +1 @@ +cwe, capec, references, tags, impact, resolution, easeofresolution diff --git a/CHANGELOG/1.5.1/add_os_openvas.md b/CHANGELOG/1.5.1/add_os_openvas.md new file mode 100644 index 00000000..4938e06f --- /dev/null +++ b/CHANGELOG/1.5.1/add_os_openvas.md @@ -0,0 +1 @@ +add os openvas \ No newline at end of file diff --git a/CHANGELOG/1.5.1/date.md b/CHANGELOG/1.5.1/date.md new file mode 100644 index 00000000..8ac327c0 --- /dev/null +++ b/CHANGELOG/1.5.1/date.md @@ -0,0 +1 @@ +Jul 27th, 2021 diff --git a/CHANGELOG/1.5.1/fix_csv_big_fields_error.md b/CHANGELOG/1.5.1/fix_csv_big_fields_error.md new file mode 100644 index 00000000..110be2cc --- /dev/null +++ b/CHANGELOG/1.5.1/fix_csv_big_fields_error.md @@ -0,0 +1 @@ +[FIX] Fix improt of CSV with big fields diff --git a/CHANGELOG/1.5.1/fixx_sslyze_json_bug.md b/CHANGELOG/1.5.1/fixx_sslyze_json_bug.md new file mode 100644 index 00000000..1f207cc5 --- /dev/null +++ b/CHANGELOG/1.5.1/fixx_sslyze_json_bug.md @@ -0,0 +1 @@ +Fix sslyze json bug with port diff --git a/CHANGELOG/1.5.1/only_show_report_name.md b/CHANGELOG/1.5.1/only_show_report_name.md new file mode 100644 index 00000000..4f9cfe83 --- /dev/null +++ b/CHANGELOG/1.5.1/only_show_report_name.md @@ -0,0 +1 @@ +Only show report name in command data diff --git a/CHANGELOG/1.5.10/Nuclei_metadata.md b/CHANGELOG/1.5.10/Nuclei_metadata.md new file mode 100644 index 00000000..38d079df --- /dev/null +++ b/CHANGELOG/1.5.10/Nuclei_metadata.md @@ -0,0 +1 @@ +support cve,cwe,cvss and metadata diff --git a/CHANGELOG/1.5.10/date.md b/CHANGELOG/1.5.10/date.md new file mode 100644 index 00000000..f18710db --- /dev/null +++ b/CHANGELOG/1.5.10/date.md @@ -0,0 +1 @@ +Jan 13th, 2022 diff --git a/CHANGELOG/1.5.2/date.md b/CHANGELOG/1.5.2/date.md new file mode 100644 index 00000000..808a322f --- /dev/null +++ b/CHANGELOG/1.5.2/date.md @@ -0,0 +1 @@ +Aug 9th, 2021 diff --git a/CHANGELOG/1.5.2/new_structure_acunetix.md b/CHANGELOG/1.5.2/new_structure_acunetix.md new file mode 100644 index 00000000..eb488f1f --- /dev/null +++ b/CHANGELOG/1.5.2/new_structure_acunetix.md @@ -0,0 +1 @@ +add new structure acunetix \ No newline at end of file diff --git a/CHANGELOG/1.5.3/command_support_nuclei.md b/CHANGELOG/1.5.3/command_support_nuclei.md new file mode 100644 index 00000000..9a7bb04b --- /dev/null +++ b/CHANGELOG/1.5.3/command_support_nuclei.md @@ -0,0 +1 @@ +Adding support for running nuclei through command / faraday-cli diff --git a/CHANGELOG/1.5.3/date.md b/CHANGELOG/1.5.3/date.md new file mode 100644 index 00000000..7cbdc177 --- /dev/null +++ b/CHANGELOG/1.5.3/date.md @@ -0,0 +1 @@ +Sep 7th, 2021 diff --git a/CHANGELOG/1.5.3/fix_mising_references.md b/CHANGELOG/1.5.3/fix_mising_references.md new file mode 100644 index 00000000..dde74629 --- /dev/null +++ b/CHANGELOG/1.5.3/fix_mising_references.md @@ -0,0 +1 @@ +Fix missing references in nuclei diff --git a/CHANGELOG/1.5.4/date.md b/CHANGELOG/1.5.4/date.md new file mode 100644 index 00000000..b74ac4e0 --- /dev/null +++ b/CHANGELOG/1.5.4/date.md @@ -0,0 +1 @@ +Oct 19th, 2021 diff --git a/CHANGELOG/1.5.4/nuclei_updates.md b/CHANGELOG/1.5.4/nuclei_updates.md new file mode 100644 index 00000000..9b7dbb67 --- /dev/null +++ b/CHANGELOG/1.5.4/nuclei_updates.md @@ -0,0 +1 @@ +Update nuclei parser diff --git a/CHANGELOG/1.5.5/date.md b/CHANGELOG/1.5.5/date.md new file mode 100644 index 00000000..b5dd20c9 --- /dev/null +++ b/CHANGELOG/1.5.5/date.md @@ -0,0 +1 @@ +Oct 21st, 2021 diff --git a/CHANGELOG/1.5.5/merge_from_github.md b/CHANGELOG/1.5.5/merge_from_github.md new file mode 100644 index 00000000..5075bb61 --- /dev/null +++ b/CHANGELOG/1.5.5/merge_from_github.md @@ -0,0 +1 @@ +Merge PR from github diff --git a/CHANGELOG/1.5.6/date.md b/CHANGELOG/1.5.6/date.md new file mode 100644 index 00000000..cec46886 --- /dev/null +++ b/CHANGELOG/1.5.6/date.md @@ -0,0 +1 @@ +Nov 10th, 2021 diff --git a/CHANGELOG/1.5.6/fix_acunetix_issue.md b/CHANGELOG/1.5.6/fix_acunetix_issue.md new file mode 100644 index 00000000..bdb7ad23 --- /dev/null +++ b/CHANGELOG/1.5.6/fix_acunetix_issue.md @@ -0,0 +1,2 @@ +FIX issue with acunetix plugin + diff --git a/CHANGELOG/1.5.6/fix_nikto_typo.md b/CHANGELOG/1.5.6/fix_nikto_typo.md new file mode 100644 index 00000000..4eaf470a --- /dev/null +++ b/CHANGELOG/1.5.6/fix_nikto_typo.md @@ -0,0 +1 @@ +FIX typo in nikto plugin diff --git a/CHANGELOG/1.5.7/date.md b/CHANGELOG/1.5.7/date.md new file mode 100644 index 00000000..d3bbd39b --- /dev/null +++ b/CHANGELOG/1.5.7/date.md @@ -0,0 +1 @@ +Nov 19th, 2021 diff --git a/CHANGELOG/1.5.7/fix_netsparker.md b/CHANGELOG/1.5.7/fix_netsparker.md new file mode 100644 index 00000000..9bc70f5a --- /dev/null +++ b/CHANGELOG/1.5.7/fix_netsparker.md @@ -0,0 +1 @@ +FIX extrainfo of netsparker plugin diff --git a/CHANGELOG/1.5.7/nuclei_legacy.md b/CHANGELOG/1.5.7/nuclei_legacy.md new file mode 100644 index 00000000..a6425aeb --- /dev/null +++ b/CHANGELOG/1.5.7/nuclei_legacy.md @@ -0,0 +1 @@ +Add nuclei_legacy plugin diff --git a/CHANGELOG/1.5.8/Add CVE to plugins.md b/CHANGELOG/1.5.8/Add CVE to plugins.md new file mode 100644 index 00000000..9b30dbbc --- /dev/null +++ b/CHANGELOG/1.5.8/Add CVE to plugins.md @@ -0,0 +1,15 @@ +Add CVE to plugins +- acunetix +- appscan +- burp +- metasploit +- nessus +- netsparker +- nexpose +- nikto +- nipper +- nmap +- openscap +- qualysguard +- retina +- shodan diff --git a/CHANGELOG/1.5.8/Add_support_for_Sslyze_5.md b/CHANGELOG/1.5.8/Add_support_for_Sslyze_5.md new file mode 100644 index 00000000..9c9a3fc9 --- /dev/null +++ b/CHANGELOG/1.5.8/Add_support_for_Sslyze_5.md @@ -0,0 +1 @@ +Add support for Sslyze 5.0 resports \ No newline at end of file diff --git a/CHANGELOG/1.5.8/Fix appscan plugin.md b/CHANGELOG/1.5.8/Fix appscan plugin.md new file mode 100644 index 00000000..5252153b --- /dev/null +++ b/CHANGELOG/1.5.8/Fix appscan plugin.md @@ -0,0 +1 @@ +Fix errors while creating hosts with wrong regex \ No newline at end of file diff --git a/CHANGELOG/1.5.8/add_masscan_support.md b/CHANGELOG/1.5.8/add_masscan_support.md new file mode 100644 index 00000000..b58b735d --- /dev/null +++ b/CHANGELOG/1.5.8/add_masscan_support.md @@ -0,0 +1 @@ +ADD masscan support to nmap plugin diff --git a/CHANGELOG/1.5.8/date.md b/CHANGELOG/1.5.8/date.md new file mode 100644 index 00000000..c833416c --- /dev/null +++ b/CHANGELOG/1.5.8/date.md @@ -0,0 +1 @@ +Dec 13th, 2021 diff --git a/CHANGELOG/1.5.8/fix openvas.md b/CHANGELOG/1.5.8/fix openvas.md new file mode 100644 index 00000000..aeb726fd --- /dev/null +++ b/CHANGELOG/1.5.8/fix openvas.md @@ -0,0 +1 @@ +Fix bug in openvas plugin \ No newline at end of file diff --git a/CHANGELOG/1.5.9/add_cve_in_faraday_csv_plugin.md b/CHANGELOG/1.5.9/add_cve_in_faraday_csv_plugin.md new file mode 100644 index 00000000..e8e5a485 --- /dev/null +++ b/CHANGELOG/1.5.9/add_cve_in_faraday_csv_plugin.md @@ -0,0 +1 @@ +Add cve in faraday_csv plugin \ No newline at end of file diff --git a/CHANGELOG/1.5.9/add_grype.md b/CHANGELOG/1.5.9/add_grype.md new file mode 100644 index 00000000..43a9ad3a --- /dev/null +++ b/CHANGELOG/1.5.9/add_grype.md @@ -0,0 +1 @@ +ADD Grype plugin diff --git a/CHANGELOG/1.5.9/date.md b/CHANGELOG/1.5.9/date.md new file mode 100644 index 00000000..7f30195e --- /dev/null +++ b/CHANGELOG/1.5.9/date.md @@ -0,0 +1 @@ +Dec 27th, 2021 diff --git a/CHANGELOG/1.6.0/add_packaging.md b/CHANGELOG/1.6.0/add_packaging.md new file mode 100644 index 00000000..d48bb5fb --- /dev/null +++ b/CHANGELOG/1.6.0/add_packaging.md @@ -0,0 +1 @@ +Add packaging to requierments in setup.py diff --git a/CHANGELOG/1.6.0/add_severity_to_shodan.md b/CHANGELOG/1.6.0/add_severity_to_shodan.md new file mode 100644 index 00000000..975ecb59 --- /dev/null +++ b/CHANGELOG/1.6.0/add_severity_to_shodan.md @@ -0,0 +1 @@ +Add severity to shodan's plugins using cvss \ No newline at end of file diff --git a/CHANGELOG/1.6.0/date.md b/CHANGELOG/1.6.0/date.md new file mode 100644 index 00000000..68baa17e --- /dev/null +++ b/CHANGELOG/1.6.0/date.md @@ -0,0 +1 @@ +Feb 3rd, 2022 diff --git a/CHANGELOG/1.6.0/fix_cve.md b/CHANGELOG/1.6.0/fix_cve.md new file mode 100644 index 00000000..83faa153 --- /dev/null +++ b/CHANGELOG/1.6.0/fix_cve.md @@ -0,0 +1 @@ +check if cve exist on cve-id field diff --git a/CHANGELOG/1.6.0/fix_fortify.md b/CHANGELOG/1.6.0/fix_fortify.md new file mode 100644 index 00000000..573a3b57 --- /dev/null +++ b/CHANGELOG/1.6.0/fix_fortify.md @@ -0,0 +1 @@ +Fix Fortify's plugin diff --git a/CHANGELOG/1.6.0/fix_to_qualysguard.md b/CHANGELOG/1.6.0/fix_to_qualysguard.md new file mode 100644 index 00000000..6062b767 --- /dev/null +++ b/CHANGELOG/1.6.0/fix_to_qualysguard.md @@ -0,0 +1 @@ +Change qualysguard's plugin severity_dict to refer level 2 severities as low diff --git a/CHANGELOG/1.6.1/add_references_to_burp.md b/CHANGELOG/1.6.1/add_references_to_burp.md new file mode 100644 index 00000000..a731f356 --- /dev/null +++ b/CHANGELOG/1.6.1/add_references_to_burp.md @@ -0,0 +1 @@ +Add references tu burp plugin diff --git a/CHANGELOG/1.6.1/date.md b/CHANGELOG/1.6.1/date.md new file mode 100644 index 00000000..1c8e2ad6 --- /dev/null +++ b/CHANGELOG/1.6.1/date.md @@ -0,0 +1 @@ +Mar 18th, 2022 diff --git a/CHANGELOG/1.6.1/fix_burp_plugin.md b/CHANGELOG/1.6.1/fix_burp_plugin.md new file mode 100644 index 00000000..e202a554 --- /dev/null +++ b/CHANGELOG/1.6.1/fix_burp_plugin.md @@ -0,0 +1 @@ +Move item.detail from data to desc diff --git a/CHANGELOG/1.6.1/update_open_status.md b/CHANGELOG/1.6.1/update_open_status.md new file mode 100644 index 00000000..ca22fe80 --- /dev/null +++ b/CHANGELOG/1.6.1/update_open_status.md @@ -0,0 +1 @@ +update open status diff --git a/CHANGELOG/1.6.2/add_line_indicator_to_appscan.md b/CHANGELOG/1.6.2/add_line_indicator_to_appscan.md new file mode 100644 index 00000000..0fa6b49a --- /dev/null +++ b/CHANGELOG/1.6.2/add_line_indicator_to_appscan.md @@ -0,0 +1 @@ +Now Appscan plugin saves line and highlight of the vulns in desc and data diff --git a/CHANGELOG/1.6.2/date.md b/CHANGELOG/1.6.2/date.md new file mode 100644 index 00000000..667786ba --- /dev/null +++ b/CHANGELOG/1.6.2/date.md @@ -0,0 +1 @@ +Apr 4th, 2022 diff --git a/CHANGELOG/1.6.3/add_zap_json.md b/CHANGELOG/1.6.3/add_zap_json.md new file mode 100644 index 00000000..14c52ffb --- /dev/null +++ b/CHANGELOG/1.6.3/add_zap_json.md @@ -0,0 +1 @@ +Add Zap Json plugin. diff --git a/CHANGELOG/1.6.3/date.md b/CHANGELOG/1.6.3/date.md new file mode 100644 index 00000000..92afff5c --- /dev/null +++ b/CHANGELOG/1.6.3/date.md @@ -0,0 +1 @@ +Apr 19th, 2022 diff --git a/CHANGELOG/1.6.4/add_location_as_param.md b/CHANGELOG/1.6.4/add_location_as_param.md new file mode 100644 index 00000000..07ac2af1 --- /dev/null +++ b/CHANGELOG/1.6.4/add_location_as_param.md @@ -0,0 +1 @@ +Add location as params in burp's plugin diff --git a/CHANGELOG/1.6.4/date.md b/CHANGELOG/1.6.4/date.md new file mode 100644 index 00000000..1f8197f8 --- /dev/null +++ b/CHANGELOG/1.6.4/date.md @@ -0,0 +1 @@ +Apr 21th, 2022 diff --git a/CHANGELOG/1.6.4/fix_custom_field_regex.md b/CHANGELOG/1.6.4/fix_custom_field_regex.md new file mode 100644 index 00000000..9f653467 --- /dev/null +++ b/CHANGELOG/1.6.4/fix_custom_field_regex.md @@ -0,0 +1 @@ +Now the faraday_csv custom_fields regex match any no whitespace character. diff --git a/CHANGELOG/1.6.5/date.md b/CHANGELOG/1.6.5/date.md new file mode 100644 index 00000000..04e240fe --- /dev/null +++ b/CHANGELOG/1.6.5/date.md @@ -0,0 +1 @@ +Apr 28th, 2022 diff --git a/CHANGELOG/1.6.5/set_severity.md b/CHANGELOG/1.6.5/set_severity.md new file mode 100644 index 00000000..9379e7cf --- /dev/null +++ b/CHANGELOG/1.6.5/set_severity.md @@ -0,0 +1 @@ +Now Openvas's plugin set severity to Critical when cvss >= 9.0 diff --git a/CHANGELOG/1.6.6/add_hostname_resolution_parameter.md b/CHANGELOG/1.6.6/add_hostname_resolution_parameter.md new file mode 100644 index 00000000..7e46221a --- /dev/null +++ b/CHANGELOG/1.6.6/add_hostname_resolution_parameter.md @@ -0,0 +1 @@ +Add hostname_resolution parameter within plugins diff --git a/CHANGELOG/1.6.6/date.md b/CHANGELOG/1.6.6/date.md new file mode 100644 index 00000000..4b0e9604 --- /dev/null +++ b/CHANGELOG/1.6.6/date.md @@ -0,0 +1 @@ +May 20th, 2022 diff --git a/CHANGELOG/1.6.6/fix_openvas_external_id.md b/CHANGELOG/1.6.6/fix_openvas_external_id.md new file mode 100644 index 00000000..adcc6461 --- /dev/null +++ b/CHANGELOG/1.6.6/fix_openvas_external_id.md @@ -0,0 +1 @@ +Fix openvas external ID diff --git a/CHANGELOG/1.6.7/change_hostname_resolution.md b/CHANGELOG/1.6.7/change_hostname_resolution.md new file mode 100644 index 00000000..81f9dac5 --- /dev/null +++ b/CHANGELOG/1.6.7/change_hostname_resolution.md @@ -0,0 +1 @@ +Change hostname_restolution to dont_resolve_hostname for process-report and now test dosent resovle hostname diff --git a/CHANGELOG/1.6.7/date.md b/CHANGELOG/1.6.7/date.md new file mode 100644 index 00000000..5bf0b806 --- /dev/null +++ b/CHANGELOG/1.6.7/date.md @@ -0,0 +1 @@ +Jun 2nd, 2022 diff --git a/CHANGELOG/1.6.7/fix_qualyswebapp.md b/CHANGELOG/1.6.7/fix_qualyswebapp.md new file mode 100644 index 00000000..ccff7367 --- /dev/null +++ b/CHANGELOG/1.6.7/fix_qualyswebapp.md @@ -0,0 +1 @@ +Now QualysWebApp's plugin will diferenciate vulns from differents urlpaths diff --git a/CHANGELOG/1.6.8/add_appscancsv.md b/CHANGELOG/1.6.8/add_appscancsv.md new file mode 100644 index 00000000..55e8e77a --- /dev/null +++ b/CHANGELOG/1.6.8/add_appscancsv.md @@ -0,0 +1 @@ +Add appscan csv diff --git a/CHANGELOG/1.6.8/add_ignore_info_to_faradaycsv.md b/CHANGELOG/1.6.8/add_ignore_info_to_faradaycsv.md new file mode 100644 index 00000000..6678b1f8 --- /dev/null +++ b/CHANGELOG/1.6.8/add_ignore_info_to_faradaycsv.md @@ -0,0 +1 @@ +Now faraday_csv's plugin uses ignore_info parameter diff --git a/CHANGELOG/1.6.8/add_syhunt.md b/CHANGELOG/1.6.8/add_syhunt.md new file mode 100644 index 00000000..f4c66fc1 --- /dev/null +++ b/CHANGELOG/1.6.8/add_syhunt.md @@ -0,0 +1 @@ +Add syhunt plugin diff --git a/CHANGELOG/1.6.8/date.md b/CHANGELOG/1.6.8/date.md new file mode 100644 index 00000000..f4ce409c --- /dev/null +++ b/CHANGELOG/1.6.8/date.md @@ -0,0 +1 @@ +Jul 25th, 2022 diff --git a/CHANGELOG/1.6.8/fix_appscan.md b/CHANGELOG/1.6.8/fix_appscan.md new file mode 100644 index 00000000..30555263 --- /dev/null +++ b/CHANGELOG/1.6.8/fix_appscan.md @@ -0,0 +1 @@ +Add cve and data fields to desc for avoid duplications diff --git a/CHANGELOG/1.6.8/fix_nuclei.md b/CHANGELOG/1.6.8/fix_nuclei.md new file mode 100644 index 00000000..ca14c332 --- /dev/null +++ b/CHANGELOG/1.6.8/fix_nuclei.md @@ -0,0 +1 @@ +Now nuclei resolve hostname if the field ip is None diff --git a/CHANGELOG/1.7.0/add_cwe.md b/CHANGELOG/1.7.0/add_cwe.md new file mode 100644 index 00000000..8d2ac5f1 --- /dev/null +++ b/CHANGELOG/1.7.0/add_cwe.md @@ -0,0 +1,20 @@ +Add CWE to PluginBase. The plugins that have this implemented are the following: +"Acunetix", +"Acunetix_Json", +"AppSpider", +"Appscan", +"Arachni", +"Burp", +"Checkmarx", +"Metasploit", +"Nessus", +"Netsparker", +"NetsparkerCloud", +"Openvas", +"QualysWebapp", +"W3af", +"Wapiti", +"Zap", +"Zap_Json", +"nuclei", +"nuclei_legacy" diff --git a/CHANGELOG/1.7.0/add_support_cvss.md b/CHANGELOG/1.7.0/add_support_cvss.md new file mode 100644 index 00000000..4ee30c2d --- /dev/null +++ b/CHANGELOG/1.7.0/add_support_cvss.md @@ -0,0 +1,16 @@ +Now the nexts pluggins extracts cvss from reports: + +- Acunetix +- Acunetix_Json +- Appscan +- Nessus +- Netsparker +- NexposeFull +- Nipper +- Nmap +- Openvas +- QualysWebapp +- Qualysguard +- Retina +- shodan +- whitesource diff --git a/CHANGELOG/1.7.0/add_tags_arguments.md b/CHANGELOG/1.7.0/add_tags_arguments.md new file mode 100644 index 00000000..1089ee0f --- /dev/null +++ b/CHANGELOG/1.7.0/add_tags_arguments.md @@ -0,0 +1,3 @@ +Add arguments for add tags for vulns, services and host. + +Add test for tags and ignore_info diff --git a/CHANGELOG/1.7.0/add_trivy_plugin.md b/CHANGELOG/1.7.0/add_trivy_plugin.md new file mode 100644 index 00000000..0638bf96 --- /dev/null +++ b/CHANGELOG/1.7.0/add_trivy_plugin.md @@ -0,0 +1 @@ +Add trivy's json plugin diff --git a/CHANGELOG/1.7.0/add_wpscan_command_support.md b/CHANGELOG/1.7.0/add_wpscan_command_support.md new file mode 100644 index 00000000..8514d93e --- /dev/null +++ b/CHANGELOG/1.7.0/add_wpscan_command_support.md @@ -0,0 +1 @@ +Add command support for the wpscan plugin diff --git a/CHANGELOG/1.7.0/change_ref_structure.md b/CHANGELOG/1.7.0/change_ref_structure.md new file mode 100644 index 00000000..ba6198a1 --- /dev/null +++ b/CHANGELOG/1.7.0/change_ref_structure.md @@ -0,0 +1,2 @@ +[MOD] Now refs field is a list of dictionary with the format: + {'name': string, 'type': string}, diff --git a/CHANGELOG/1.7.0/date.md b/CHANGELOG/1.7.0/date.md new file mode 100644 index 00000000..c326d6aa --- /dev/null +++ b/CHANGELOG/1.7.0/date.md @@ -0,0 +1 @@ +Sep 5th, 2022 diff --git a/CHANGELOG/1.7.0/fix_accunetix.md b/CHANGELOG/1.7.0/fix_accunetix.md new file mode 100644 index 00000000..84b67f71 --- /dev/null +++ b/CHANGELOG/1.7.0/fix_accunetix.md @@ -0,0 +1 @@ +Fix for acunetix_json when host is ip diff --git a/CHANGELOG/1.7.0/fix_appscan_csv_duplicate_assets.md b/CHANGELOG/1.7.0/fix_appscan_csv_duplicate_assets.md new file mode 100644 index 00000000..78992d7d --- /dev/null +++ b/CHANGELOG/1.7.0/fix_appscan_csv_duplicate_assets.md @@ -0,0 +1 @@ +[FIX] - Asset duplicated on same file with multiple entries for Appscan_csv plugin. \ No newline at end of file diff --git a/CHANGELOG/1.7.0/fix_for_python3.10.md b/CHANGELOG/1.7.0/fix_for_python3.10.md new file mode 100644 index 00000000..b8623226 --- /dev/null +++ b/CHANGELOG/1.7.0/fix_for_python3.10.md @@ -0,0 +1,2 @@ +[FIX] Change import dateutil to from dateutil.parser import parse +for compatibility issues with python 3.10 diff --git a/CHANGELOG/1.7.0/fix_netsparker.md b/CHANGELOG/1.7.0/fix_netsparker.md new file mode 100644 index 00000000..f3bb8608 --- /dev/null +++ b/CHANGELOG/1.7.0/fix_netsparker.md @@ -0,0 +1 @@ +[FIX] Add case for Netsparker plugins, when the url has a number inside a parenthesis. diff --git a/CHANGELOG/1.7.0/fix_syhunt.md b/CHANGELOG/1.7.0/fix_syhunt.md new file mode 100644 index 00000000..99cde432 --- /dev/null +++ b/CHANGELOG/1.7.0/fix_syhunt.md @@ -0,0 +1 @@ +Add *args **kwargs to syhunt plugin diff --git a/CHANGELOG/1.7.0/minor_fix_for_grype.md b/CHANGELOG/1.7.0/minor_fix_for_grype.md new file mode 100644 index 00000000..a349124c --- /dev/null +++ b/CHANGELOG/1.7.0/minor_fix_for_grype.md @@ -0,0 +1 @@ +fix bug when grype report has no arifact/metadata diff --git a/CHANGELOG/1.7.0/mod_prowler.md b/CHANGELOG/1.7.0/mod_prowler.md new file mode 100644 index 00000000..83db52ba --- /dev/null +++ b/CHANGELOG/1.7.0/mod_prowler.md @@ -0,0 +1,2 @@ +[MOD] Now prowler plugin returns CAF Epic as policy violation and +remove [check#] from tittle diff --git a/CHANGELOG/1.8.0/add_invicti.md b/CHANGELOG/1.8.0/add_invicti.md new file mode 100644 index 00000000..8af2ce4d --- /dev/null +++ b/CHANGELOG/1.8.0/add_invicti.md @@ -0,0 +1 @@ +[Add] Add invicti plugin diff --git a/CHANGELOG/1.8.0/add_nessus_sc_plugin.md b/CHANGELOG/1.8.0/add_nessus_sc_plugin.md new file mode 100644 index 00000000..dc993c2c --- /dev/null +++ b/CHANGELOG/1.8.0/add_nessus_sc_plugin.md @@ -0,0 +1 @@ +[Add] Add nessus_sc plugin diff --git a/CHANGELOG/1.8.0/fix_nexpose_full.md b/CHANGELOG/1.8.0/fix_nexpose_full.md new file mode 100644 index 00000000..717e5667 --- /dev/null +++ b/CHANGELOG/1.8.0/fix_nexpose_full.md @@ -0,0 +1 @@ +[FIX] Remove cvss_vector from refs in nexpose_full diff --git a/CHANGELOG/1.8.0/fix_nikto.md b/CHANGELOG/1.8.0/fix_nikto.md new file mode 100644 index 00000000..c818fe6e --- /dev/null +++ b/CHANGELOG/1.8.0/fix_nikto.md @@ -0,0 +1 @@ +Add new identifier_tag to nikto plugin diff --git a/CHANGELOG/1.8.0/fix_plugins.md b/CHANGELOG/1.8.0/fix_plugins.md new file mode 100644 index 00000000..13747072 --- /dev/null +++ b/CHANGELOG/1.8.0/fix_plugins.md @@ -0,0 +1 @@ +[FIX] Now plugins check if ref field is already a dictionary diff --git a/CHANGELOG/1.8.0/mod_grype.md b/CHANGELOG/1.8.0/mod_grype.md new file mode 100644 index 00000000..59b26c72 --- /dev/null +++ b/CHANGELOG/1.8.0/mod_grype.md @@ -0,0 +1,3 @@ +[MOD] Improve grype plugin for dockers images and change report_belong_to method for +json plugins to check if json_keys is a list, in that case iterate the list and try if +any of them create a match. diff --git a/CHANGELOG/1.8.1/add_check_cwe_nuclei.md b/CHANGELOG/1.8.1/add_check_cwe_nuclei.md new file mode 100644 index 00000000..ff6b3fbe --- /dev/null +++ b/CHANGELOG/1.8.1/add_check_cwe_nuclei.md @@ -0,0 +1 @@ +[FIX] Nuclei's plugin check if the cwe is null and add retrocompability for newer versions for wpscan plugin diff --git a/CHANGELOG/1.8.1/add_enrichment.md b/CHANGELOG/1.8.1/add_enrichment.md new file mode 100644 index 00000000..0d44d492 --- /dev/null +++ b/CHANGELOG/1.8.1/add_enrichment.md @@ -0,0 +1 @@ +[ADD] Add cvss2/3 and cwe to faraday_csv plugin diff --git a/CHANGELOG/1.8.1/add_severity.md b/CHANGELOG/1.8.1/add_severity.md new file mode 100644 index 00000000..4f508ea5 --- /dev/null +++ b/CHANGELOG/1.8.1/add_severity.md @@ -0,0 +1 @@ +[Add] Now nexpose_full plugin use severity from reports diff --git a/CHANGELOG/1.8.1/check_if_refs_is_empty.md b/CHANGELOG/1.8.1/check_if_refs_is_empty.md new file mode 100644 index 00000000..a9582c70 --- /dev/null +++ b/CHANGELOG/1.8.1/check_if_refs_is_empty.md @@ -0,0 +1 @@ +[FIX] Now plugins check if the ref is empty diff --git a/CHANGELOG/1.8.1/date.md b/CHANGELOG/1.8.1/date.md new file mode 100644 index 00000000..964cecdc --- /dev/null +++ b/CHANGELOG/1.8.1/date.md @@ -0,0 +1 @@ +Nov 28th, 2022 diff --git a/CHANGELOG/1.9.0/add_check_protocol.md b/CHANGELOG/1.9.0/add_check_protocol.md new file mode 100644 index 00000000..dc5e8f95 --- /dev/null +++ b/CHANGELOG/1.9.0/add_check_protocol.md @@ -0,0 +1 @@ +[FIX] Now all plugins check that service protocol is not empty diff --git a/CHANGELOG/1.9.0/add_pentera.md b/CHANGELOG/1.9.0/add_pentera.md new file mode 100644 index 00000000..a488962c --- /dev/null +++ b/CHANGELOG/1.9.0/add_pentera.md @@ -0,0 +1 @@ +[ADD] New pentera plugin and now json plugins can have filter_key to filter reports with that keys diff --git a/CHANGELOG/1.9.0/change_table_view_list_plugins.md b/CHANGELOG/1.9.0/change_table_view_list_plugins.md new file mode 100644 index 00000000..ce874f77 --- /dev/null +++ b/CHANGELOG/1.9.0/change_table_view_list_plugins.md @@ -0,0 +1 @@ +[MOD] Change table format for list-plugins to github diff --git a/CHANGELOG/1.9.0/date.md b/CHANGELOG/1.9.0/date.md new file mode 100644 index 00000000..73662df8 --- /dev/null +++ b/CHANGELOG/1.9.0/date.md @@ -0,0 +1 @@ +Dic 15th, 2022 diff --git a/CHANGELOG/1.9.1/add_cis_plugin.md b/CHANGELOG/1.9.1/add_cis_plugin.md new file mode 100644 index 00000000..5d73b3b2 --- /dev/null +++ b/CHANGELOG/1.9.1/add_cis_plugin.md @@ -0,0 +1 @@ +[ADD] Add new CIS plugin diff --git a/CHANGELOG/1.9.1/date.md b/CHANGELOG/1.9.1/date.md new file mode 100644 index 00000000..69e42340 --- /dev/null +++ b/CHANGELOG/1.9.1/date.md @@ -0,0 +1 @@ +Jan 3rd, 2023 diff --git a/CHANGELOG/RELEASE.md b/CHANGELOG/RELEASE.md deleted file mode 100644 index 4b0ee7be..00000000 --- a/CHANGELOG/RELEASE.md +++ /dev/null @@ -1,22 +0,0 @@ -1.3.0: ---- - * ADD plugin AppSpider - * Add tests to faraday-plugins cli - * add a default value to plugin_version - * Add --output-file parameter to faraday-plugins process command - * Add plugins prowler - * Add plugins ssl labs - * Add support for tenable io - * delete old deprecated methods - * Bug fix: Arachni Plugin 'NoneType' object has no attribute 'find' - * Bug fix: Openvas Plugin - Import xml from OpenVas doesnt work - * Bug fix: QualysWebApp Plugin, error in get info OPERATING_SYSTEM - * Fix Hydra plugin to resolve ip address - * Fix Nessus mod severity HIGH for Low - * Bug Fix: Detect plugins AWS Prowler - * Fix broken xml on nmap plugin - * Add new rdpscan plugin - * UPDATE xml report to appscan - * Update Readme - * Fix how ZAP genereate vulns - diff --git a/COPYING b/COPYING new file mode 100644 index 00000000..f288702d --- /dev/null +++ b/COPYING @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md index c531f402..430276f1 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ pip install faraday-plugins ## Commands -> List Plugins +### List Plugins List all plugins and if its compatible with command or/and report @@ -18,7 +18,7 @@ Optional params: faraday-plugins list-plugins ``` -> Test autodetect plugin from command +### Test autodetect plugin from command ```shell script faraday-plugins detect-command "ping -c 4 www.google.com" @@ -26,7 +26,7 @@ faraday-plugins detect-command "ping -c 4 www.google.com" Faraday Plugin: ping ``` -> Test process command with plugin +### Test process command with plugin Optional params: @@ -67,7 +67,7 @@ faraday-plugins process-command "ping -c4 www.google.com" } ``` -> Test autodetect plugin from report +### Test autodetect plugin from report ```shell script faraday-plugins detect-report /path/to/report.xml @@ -75,8 +75,7 @@ faraday-plugins detect-report /path/to/report.xml Faraday Plugin: Nmap ``` - -> Test report with plugin +### Test report with plugin Optional params: @@ -136,9 +135,9 @@ faraday-plugins process-report /path/to/nmap_report.xml } ``` -> Plugin Logger +## Plugin Logger -To use it you must call ```self.logger.debug("some message")``` +To use it you must call `self.logger.debug("some message")` ```shell script export PLUGIN_DEBUG=1 @@ -156,5 +155,4 @@ faraday-plugins proces-report /path/to/report.xml ... ``` - -> More documentation here https://github.com/infobyte/faraday/wiki/Basic-plugin-development +More documentation here https://docs.faradaysec.com/Basic-plugin-development/ diff --git a/RELEASE.md b/RELEASE.md index 4b0ee7be..40d646c4 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,4 +1,372 @@ -1.3.0: +1.21.0 [Jan 6th, 2025]: +--- + * [ADD] Added Saint CSV report processor. #353 + +1.20.0 [Nov 21st, 2024]: +--- + * [FIX] Duplicate detection vulnerability in the Burp plugin. #354 + +1.19.1 [Sep 20th, 2024]: +--- + * [MOD] Updated the SSLyze JSON parser to support and correctly process scan results from SSLyze version 6. #349 + * [FIX] Crowdstrike IP resolution for asset. #350 + * [FIX] Corrected a hostname resolution issue that was causing traceback errors and malfunctioning of the plugin. This fix ensures proper hostname resolution and stabilizes plugin performance. #351 + +1.19.0 [Aug 23rd, 2024]: +--- + * [ADD] Added owasp dependency check. #100 + * [ADD] Added gitleaks plugin. #342 + * [FIX] Nessus plugin crashed when parsing tenableio reports without vulnerabilities, so a check for that was added. #341 + +1.18.2 [Jul 24th, 2024]: +--- + * [FIX] Added validations for empty lines and multiple fields including lists. #343 + +1.18.1 [Jul 11th, 2024]: +--- + * [MOD] Naabu reports changed their JSON structure, so new keys were added to detect the new report structure. #339 + +1.18.0 [May 22th, 2024]: +--- + * [FIX] Fix key error when `packageVulnerabilityDetails` key was not in the file. #331 + * [FIX] Addressed a bug where Burp plugin output would display null data in cases of encountering a malformed XML token from the report. #333 + * [FIX] Previously, CSV files edited in tools like Mac Numbers would transform boolean values to uppercase. This issue has been addressed within the faraday_csv plugin, ensuring accurate comparison. #336 + +1.17.0 [Mar 12th, 2024]: +--- + * [ADD] Add hotspots logic for sonarqube plugin #321 + +1.16.0 [Feb 8th, 2024]: +--- + * [ADD] Add Snyk plugin. #314 + * [MOD] Mod AWS Inspector's plugins. #322 + * [ADD] Add faraday_json plugins. #324 + * [ADD] Update prowler plugin to support the latest tool output format. Also rename the oldest plugin to prowler_legacy. #328 + +1.15.1 [Dec 22th, 2023]: +--- + * [FIX] Filter \x00 in nuclei response. #323 + +1.15.0 [Dec 12th, 2023]: +--- + * [ADD] Add PopEye's plugin. #303 + * [ADD] Add Ping Castle's plugin. #304 + * [ADD] Add Kubescape's plugin. #320 + * [ADD] Add AWS Inspector's plugins. #322 + +1.14.0 [Oct 10th, 2023]: +--- + * [ADD] Add Crowdstrike's plugin. #318 + +1.13.2 [Sep 6th, 2023]: +--- + * [ADD] Extract response and request info in qualyswebapp's plugins. #307 + * [ADD] Create Plugin for windows defender. #315 + +1.13.0 [Aug 24th, 2023]: +--- + * [FIX] If severity id in an appscan item is greater than 4 set it to 4. #305 + * [FIX] Update Naabu plugin for the latest version, Semgrep create a new service for each vuln, fix Arachni bug in case the report has no vulns. #306 + * [ADD] Add Terrascan and TFSec plugins. #310 + * [FIX] Use cvss_score to calculate severity in nessus plugin. #311 + +1.12.1 [July 7th, 2023]: +--- + * [FIX] Fix Appscan's pluign. #302 + +1.12.0 [May 24th, 2023]: +--- + * [ADD] Add Sarif plugin. #299 + +1.11.0 [Apr 3rd, 2023]: +--- + * [FIX] Change syhunt´s and trivy´s plugins to export cvss vector correctly #292 + * [ADD] Add force flag to process-command to process the output of the command regardless of the exit code. #294 + * [MOD] The accunetix plugin now search for CVSS and cvss #296 + * [ADD] Add semgrep plugin. #297 + * [FIX] Fix inviti's plugin, check remedial procedures before parsing it with b4f. #298 + +1.10.0 [Jan 31th, 2023]: +--- + * [ADD] Add new acunetix360 plugin #293 + +1.9.1 [Jan 3rd, 2023]: +--- + * [ADD] Add new CIS plugin + +1.9.0 [Dic 15th, 2022]: +--- + * [FIX] Now all plugins check that service protocol is not empty + * [ADD] New pentera plugin and now json plugins can have filter_key to filter reports with that keys + * [MOD] Change table format for list-plugins to github + +1.8.1 [Nov 28th, 2022]: +--- + * [FIX] Nuclei's plugin check if the cwe is null and add retrocompability for newer versions for wpscan plugin + * [ADD] Add cvss2/3 and cwe to faraday_csv plugin + * [Add] Now nexpose_full plugin use severity from reports + * [FIX] Now plugins check if the ref is empty + +1.8.0: +--- + * [Add] Add invicti plugin + * [Add] Add nessus_sc plugin + * [FIX] Remove cvss_vector from refs in nexpose_full + * Add new identifier_tag to nikto plugin + * [FIX] Now plugins check if ref field is already a dictionary + * [MOD] Improve grype plugin for dockers images and change report_belong_to method for +json plugins to check if json_keys is a list, in that case iterate the list and try if +any of them create a match. + +1.7.0 [Sep 5th, 2022]: +--- + * Add CWE to PluginBase. The plugins that have this implemented are the following: +"Acunetix", +"Acunetix_Json", +"AppSpider", +"Appscan", +"Arachni", +"Burp", +"Checkmarx", +"Metasploit", +"Nessus", +"Netsparker", +"NetsparkerCloud", +"Openvas", +"QualysWebapp", +"W3af", +"Wapiti", +"Zap", +"Zap_Json", +"nuclei", +"nuclei_legacy" + * Now the nexts pluggins extracts cvss from reports: + +- Acunetix +- Acunetix_Json +- Appscan +- Nessus +- Netsparker +- NexposeFull +- Nipper +- Nmap +- Openvas +- QualysWebapp +- Qualysguard +- Retina +- shodan +- whitesource + * Add arguments for add tags for vulns, services and host. + +Add test for tags and ignore_info + * Add trivy's json plugin + * Add command support for the wpscan plugin + * [MOD] Now refs field is a list of dictionary with the format: + {'name': string, 'type': string}, + * Fix for acunetix_json when host is ip + * [FIX] - Asset duplicated on same file with multiple entries for Appscan_csv plugin. + * [FIX] Change import dateutil to from dateutil.parser import parse +for compatibility issues with python 3.10 + * [FIX] Add case for Netsparker plugins, when the url has a number inside a parenthesis. + * Add *args **kwargs to syhunt plugin + * fix bug when grype report has no arifact/metadata + * [MOD] Now prowler plugin returns CAF Epic as policy violation and +remove [check#] from tittle + +1.6.8 [Jul 25th, 2022]: +--- + * Add appscan csv + * Now faraday_csv's plugin uses ignore_info parameter + * Add syhunt plugin + * Add cve and data fields to desc for avoid duplications + * Now nuclei resolve hostname if the field ip is None + +1.6.7 [Jun 2nd, 2022]: +--- + * Change hostname_restolution to dont_resolve_hostname for process-report and now test dosent resovle hostname + * Now QualysWebApp's plugin will diferenciate vulns from differents urlpaths + +1.6.6 [May 20th, 2022]: +--- + * Add hostname_resolution parameter within plugins + * Fix openvas external ID + +1.6.5 [Apr 28th, 2022]: +--- + * Now Openvas's plugin set severity to Critical when cvss >= 9.0 + +1.6.4 [Apr 21th, 2022]: +--- + * Add location as params in burp's plugin + * Now the faraday_csv custom_fields regex match any no whitespace character. + +1.6.3 [Apr 19th, 2022]: +--- + * Add Zap Json plugin. + +1.6.2 [Apr 4th, 2022]: +--- + * Now Appscan plugin saves line and highlight of the vulns in desc and data + +1.6.1 [Mar 18th, 2022]: +--- + * Add references tu burp plugin + * Move item.detail from data to desc + * update open status + +1.6.0 [Feb 3rd, 2022]: +--- + * Add packaging to requierments in setup.py + * Add severity to shodan's plugins using cvss + * check if cve exist on cve-id field + * Fix Fortify's plugin + * Change qualysguard's plugin severity_dict to refer level 2 severities as low + +1.5.10 [Jan 13th, 2022]: +--- + * support cve,cwe,cvss and metadata + +1.5.9 [Dec 27th, 2021]: +--- + * Add cve in faraday_csv plugin + * ADD Grype plugin + +1.5.8 [Dec 13th, 2021]: +--- + * Add CVE to plugins +- acunetix +- appscan +- burp +- metasploit +- nessus +- netsparker +- nexpose +- nikto +- nipper +- nmap +- openscap +- qualysguard +- retina +- shodan + * Add support for Sslyze 5.0 resports + * Fix errors while creating hosts with wrong regex + * ADD masscan support to nmap plugin + * Fix bug in openvas plugin + +1.5.7 [Nov 19th, 2021]: +--- + * FIX extrainfo of netsparker plugin + * Add nuclei_legacy plugin + +1.5.6 [Nov 10th, 2021]: +--- + * FIX issue with acunetix plugin + + * FIX typo in nikto plugin + +1.5.5 [Oct 21st, 2021]: +--- + * Merge PR from github + +1.5.4 [Oct 19th, 2021]: +--- + * Update nuclei parser + +1.5.3 [Sep 7th, 2021]: +--- + * Adding support for running nuclei through command / faraday-cli + * Fix missing references in nuclei + +1.5.2 [Aug 9th, 2021]: +--- + * add new structure acunetix + +1.5.1 [Jul 27th, 2021]: +--- + * cwe, capec, references, tags, impact, resolution, easeofresolution + * add os openvas + * [FIX] Fix improt of CSV with big fields + * Fix sslyze json bug with port + * Only show report name in command data + +1.5.0 [Jun 28th, 2021]: +--- + * Add Nipper Plugin + * add shodan plugin + * fix acunetix url parser + * FIX netsparker multi-host + * Add vuln details for Certificate Mismatch and move unique details to data, now vulns can be grupped + * ADD more data to plugins arachni and w3af + * Use run_date in UTC + * ADD cvss_base, cpe, threat, severity into references + +1.4.6 [May 14th, 2021]: +--- + * - add attribute "command" for the pluggins of each command +- adding test in test_command +- change some regex in self._command_regex + * [FIX] add hostnames if host is already cached + * Add Naabu plugin + * Add Sonarqube plugin + * Add version and change list_plugins style + * FIX unused import, innecesary list compression and unused variables + * FIX metasploit report when the web-site-id is null + * Fix port stats in nmap + * fixup ssylze +sacar unknown de version= + * ADD remedy into resolution + * Support for nuclei 2.3.0 + * ADD cve, cvss3_base_score, cvss3_vector, exploit_available when import nessus and change the structure of external_id to NESSUS-XXX + * ADD more data like attack, params, uri, method, WASC, CWE and format externail_id + +1.4.5 [Apr 15th, 2021]: +--- + * Add Bandit plugin + * Use background for description and detail for data en Burp plugin. + * Rewrite Appscan Plugin + * Parse Nmap vulners script data + +1.4.4 [Mar 30th, 2021]: +--- + * Faraday CSV Plugin do not consider ignore_info + +1.4.3 [Mar 17th, 2021]: +--- + * Add Ignore information vulnerabilities option + +1.4.2 [Mar 10th, 2021]: +--- + * Fix bug with sslyze output file + * FIX change id sslyze for JSON/XML + +1.4.1 [Feb 26th, 2021]: +--- + * ADD microsoft baseline security analyzer plugin + * ADD nextnet plugin + * ADD openscap plugin + * FIX old versions of Nessus plugins bugs + +1.4.0 [Dec 23rd, 2020]: +--- + * Update the fields of the nuclei output used to create a vuln + +1.4.0b2 [Dec 15th, 2020]: +--- + * Fix nuclei plugin bug when url is None + +1.4.0b1 [Dec 14th, 2020]: +--- + * Add new plugin base class, for multi line json + * New ncrack plugin + * New nuclei plugin + * New sslyze json plugin + * New WhatWeb plugin + * Fix missing ip in some arachni reports + * Fix change name vuln in Netsparker plugin + * Fix whois plugin, command whois IP not parse data + * Change the way we detect json reports when they are lists of dictionaries + +1.3.0 [Sep 2nd, 2020]: --- * ADD plugin AppSpider * Add tests to faraday-plugins cli @@ -12,11 +380,10 @@ * Bug fix: Openvas Plugin - Import xml from OpenVas doesnt work * Bug fix: QualysWebApp Plugin, error in get info OPERATING_SYSTEM * Fix Hydra plugin to resolve ip address - * Fix Nessus mod severity HIGH for Low + * Fix Nessus mod severity HIGH for Low * Bug Fix: Detect plugins AWS Prowler * Fix broken xml on nmap plugin * Add new rdpscan plugin * UPDATE xml report to appscan * Update Readme * Fix how ZAP genereate vulns - diff --git a/doc/development.md b/doc/development.md index 7413c9c8..c7b157ec 100644 --- a/doc/development.md +++ b/doc/development.md @@ -5,15 +5,15 @@ ```python class XXXPLugin(PluginXMLFormat): - def __init__(self): - super().__init__() + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) # Tags to be compared with the xml mail tag, can be a list or a string - self.identifier_tag = ["tag1", "tag2"] + self.identifier_tag = ["tag1", "tag2"] self.id = 'SOME_PLUGIN_ID' # Can't be repeated self.name = 'Some plugin name' self.plugin_version = 'X.X' # The extension is optional, only if its different than xml - self.extension = ".xxx" + self.extension = ".xxx" ``` > JSON report plugin @@ -21,8 +21,8 @@ class XXXPLugin(PluginXMLFormat): ```python class XXXPLugin(PluginJsonFormat): - def __init__(self): - super().__init__() + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) # keys of the json that identify the report # you don't need to put all the keys, just some of them # it must be a set and will be compared as a subset of the json report keys @@ -31,5 +31,5 @@ class XXXPLugin(PluginJsonFormat): self.name = 'Some plugin name' self.plugin_version = 'X.X' # The extension is optional, only if its different than json - self.extension = ".xxx" -``` \ No newline at end of file + self.extension = ".xxx" +``` diff --git a/faraday_plugins/__init__.py b/faraday_plugins/__init__.py index 19b4f1d6..2d492ca7 100644 --- a/faraday_plugins/__init__.py +++ b/faraday_plugins/__init__.py @@ -1 +1,2 @@ -__version__ = '1.3.0' +__version__ = '1.21.0' + diff --git a/faraday_plugins/commands.py b/faraday_plugins/commands.py index c4fc1cc7..c996368e 100644 --- a/faraday_plugins/commands.py +++ b/faraday_plugins/commands.py @@ -1,13 +1,17 @@ +import getpass import io +import json import logging import os +import shlex +import subprocess # nosec import sys -import json +from pathlib import Path + import click -import subprocess -import shlex -import getpass +from tabulate import tabulate +from faraday_plugins import __version__ from faraday_plugins.plugins.manager import PluginsManager, ReportAnalyzer, CommandAnalyzer from faraday_plugins.plugins.plugin import PluginByExtension @@ -21,8 +25,10 @@ root_logger.addHandler(out_hdlr) root_logger.setLevel(logging.DEBUG) +CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) -@click.group() +@click.group(context_settings=CONTEXT_SETTINGS) +@click.version_option(__version__, '-v', '--version') def cli(): pass @@ -31,8 +37,10 @@ def cli(): @click.option('-cpf', '--custom-plugins-folder', type=str) def list_plugins(custom_plugins_folder): plugins_manager = PluginsManager(custom_plugins_folder) - click.echo(click.style("Available Plugins:", fg="cyan")) + click.echo(click.style(f"Faraday Plugins v{__version__}", fg="cyan")) + click.echo(click.style("Available Plugins :", fg="cyan")) loaded_plugins = 0 + plugins_data = [] for plugin_id, plugin in plugins_manager.get_plugins(): console_enabled = plugin._command_regex is not None console_enabled_color = "green" if console_enabled else "red" @@ -40,10 +48,14 @@ def list_plugins(custom_plugins_folder): report_enabled = isinstance(plugin, PluginByExtension) report_enabled_color = "green" if report_enabled else "red" report_enabled_text = click.style(f"{'Yes' if report_enabled else 'No'}", fg=report_enabled_color) - click.echo(f"{plugin.id:15} - [Command: {console_enabled_text:>12} - Report: {report_enabled_text:>12}] - {plugin.name} ") - - loaded_plugins += 1 - click.echo(click.style(f"Loaded Plugins: {loaded_plugins}", fg="cyan")) + plugins_data.append({"Name": plugin.name, "ID": plugin.id, "Command": console_enabled_text, + "Report": report_enabled_text}) + click.echo(tabulate( + plugins_data, + headers="keys", + tablefmt="github", + )) + click.echo(click.style(f"Loaded Plugins: {len(plugins_data)}", fg="cyan")) @cli.command() @@ -52,11 +64,22 @@ def list_plugins(custom_plugins_folder): @click.option('-cpf', '--custom-plugins-folder', type=str) @click.option('--summary', is_flag=True) @click.option('-o', '--output-file', type=click.Path(exists=False)) -def process_report(report_file, plugin_id, custom_plugins_folder, summary, output_file): +@click.option('--ignore-info', is_flag=True, help="Ignore information vulnerabilities") +@click.option('-drh', '--dont-resolve-hostname', is_flag=True, help="Dont resolve hostname", default=False) +@click.option('--vuln-tag', help="Vuln tag", default=None) +@click.option('--service-tag', help="Service tag", default=None) +@click.option('--host-tag', help="Host tag", default=None) +def process_report(report_file, plugin_id, custom_plugins_folder, summary, output_file, ignore_info, + dont_resolve_hostname, vuln_tag, service_tag, host_tag): if not os.path.isfile(report_file): click.echo(click.style(f"File {report_file} Don't Exists", fg="red"), err=True) else: - plugins_manager = PluginsManager(custom_plugins_folder) + plugins_manager = PluginsManager(custom_plugins_folder, + ignore_info=ignore_info, + hostname_resolution=not dont_resolve_hostname, + vuln_tag=vuln_tag, + service_tag=service_tag, + host_tag=host_tag) analyzer = ReportAnalyzer(plugins_manager) if plugin_id: plugin = plugins_manager.get_plugin(plugin_id) @@ -68,7 +91,7 @@ def process_report(report_file, plugin_id, custom_plugins_folder, summary, outpu if not plugin: click.echo(click.style(f"Failed to detect report: {report_file}", fg="red"), err=True) return - plugin.processReport(report_file, getpass.getuser()) + plugin.processReport(Path(report_file), getpass.getuser()) if summary: click.echo(json.dumps(plugin.get_summary(), indent=4)) else: @@ -87,8 +110,20 @@ def process_report(report_file, plugin_id, custom_plugins_folder, summary, outpu @click.option('--summary', is_flag=True) @click.option('-o', '--output-file', type=click.Path(exists=False)) @click.option('-sh', '--show-output', is_flag=True) -def process_command(command, plugin_id, custom_plugins_folder, dont_run, summary, output_file, show_output): - plugins_manager = PluginsManager(custom_plugins_folder) +@click.option('--ignore-info', is_flag=True, help="Ignore information vulnerabilities") +@click.option('--hostname-resolution', is_flag=True, help="Resolve hostname") +@click.option('--vuln-tag', help="Vuln tag", default=None) +@click.option('--service-tag', help="Service tag", default=None) +@click.option('--host-tag', help="Host tag", default=None) +@click.option('--force', help="Process the output of the command regardless of the result", is_flag=True) +def process_command(command, plugin_id, custom_plugins_folder, dont_run, summary, output_file, show_output, + ignore_info, hostname_resolution, vuln_tag, service_tag, host_tag, force): + plugins_manager = PluginsManager(custom_plugins_folder, + ignore_info=ignore_info, + hostname_resolution=hostname_resolution, + vuln_tag=vuln_tag, + service_tag=service_tag, + host_tag=host_tag) analyzer = CommandAnalyzer(plugins_manager) if plugin_id: plugin = plugins_manager.get_plugin(plugin_id) @@ -108,7 +143,7 @@ def process_command(command, plugin_id, custom_plugins_folder, dont_run, summary color_message = click.style("Command: ", fg="green") click.echo(f"{color_message} {command}") else: - p = subprocess.Popen(shlex.split(command), stdout=subprocess.PIPE, stderr=subprocess.PIPE) + p = subprocess.Popen(shlex.split(command), stdout=subprocess.PIPE, stderr=subprocess.PIPE) # nosec output = io.StringIO() while True: retcode = p.poll() @@ -123,7 +158,7 @@ def process_command(command, plugin_id, custom_plugins_folder, dont_run, summary output.writelines(extra_lines) break output_value = output.getvalue() - if retcode == 0: + if retcode == 0 or force: plugin.processOutput(output_value) if summary: click.echo(json.dumps(plugin.get_summary(), indent=4)) diff --git a/faraday_plugins/plugins/manager.py b/faraday_plugins/plugins/manager.py index 9472c7dc..4c38a7de 100644 --- a/faraday_plugins/plugins/manager.py +++ b/faraday_plugins/plugins/manager.py @@ -1,26 +1,21 @@ +import csv +import json import logging -import traceback -import re import os -import sys -import json import pkgutil +import re +import sys +import traceback +import xml.etree.ElementTree as ET import zipfile from importlib import import_module from importlib.machinery import SourceFileLoader -import csv from io import StringIO from . import repo logger = logging.getLogger("faraday").getChild(__name__) -try: - import xml.etree.cElementTree as ET -except ImportError: - logger.warning("cElementTree could not be imported. Using ElementTree instead") - import xml.etree.ElementTree as ET - class ReportAnalyzer: @@ -35,7 +30,7 @@ def get_plugin(self, report_path): else: file_name = os.path.basename(report_path) plugin = self._get_plugin_by_name(file_name) - if not plugin: # Was unable to detect plugin from report file name + if not plugin: # Was unable to detect plugin from report file name logger.debug("Plugin by name not found") plugin = self._get_plugin_by_file_type(report_path) if not plugin: @@ -68,7 +63,7 @@ def _get_plugin_by_file_type(self, report_path): file_name_base, file_extension = os.path.splitext(file_name) file_extension = file_extension.lower() main_tag = None - file_json_keys = {} + main_tag_attributes = {} file_csv_headers = set() file_json_keys = set() files_in_zip = set() @@ -81,9 +76,18 @@ def _get_plugin_by_file_type(self, report_path): else: try: for event, elem in ET.iterparse(report_file, ('start',)): - main_tag = elem.tag + prefix, has_namespace, postfix = elem.tag.partition("}") + if has_namespace: + main_tag = postfix + else: + main_tag = elem.tag + try: + main_tag_attributes = elem.attrib + except AttributeError: + pass break - logger.debug("Found XML content on file: %s - Main tag: %s", report_path, main_tag) + logger.debug("Found XML content on file: %s - Main tag: %s Attributes: %s", report_path, main_tag, + main_tag_attributes) except Exception as e: logger.debug("Non XML content [%s] - %s", report_path, e) try: @@ -116,9 +120,10 @@ def _get_plugin_by_file_type(self, report_path): for _plugin_id, _plugin in self.plugin_manager.get_plugins(): logger.debug("Try plugin: %s", _plugin_id) try: - if _plugin.report_belongs_to(main_tag=main_tag, report_path=report_path, - extension=file_extension, file_json_keys=file_json_keys, - file_csv_headers=file_csv_headers, files_in_zip=files_in_zip): + if _plugin.report_belongs_to(main_tag=main_tag, main_tag_attributes=main_tag_attributes, + report_path=report_path, extension=file_extension, + file_json_keys=file_json_keys, file_csv_headers=file_csv_headers, + files_in_zip=files_in_zip): plugin = _plugin logger.debug("Plugin by File Found: %s", plugin.id) break @@ -147,7 +152,8 @@ def get_plugin(self, command_string): class PluginsManager: - def __init__(self, custom_plugins_folder=None): + def __init__(self, custom_plugins_folder=None, **kwargs): + self.kwargs = kwargs self.plugins = {} self.plugin_modules = {} self._load_plugins(custom_plugins_folder) @@ -210,7 +216,7 @@ def get_plugin(self, plugin_id): plugin = None plugin_id = plugin_id.lower() if plugin_id in self.plugin_modules: - plugin = self.plugin_modules[plugin_id].createPlugin() + plugin = self.plugin_modules[plugin_id].createPlugin(**self.kwargs) else: logger.debug("Unknown Plugin: %s", plugin_id) return plugin @@ -218,4 +224,4 @@ def get_plugin(self, plugin_id): def get_plugins(self): for plugin_id, plugin_module in self.plugin_modules.items(): logger.debug("Instance Plugin: %s", plugin_id) - yield plugin_id, plugin_module.createPlugin() + yield plugin_id, plugin_module.createPlugin(**self.kwargs) diff --git a/faraday_plugins/plugins/plugin.py b/faraday_plugins/plugins/plugin.py index 97272e0a..ac5ed6d8 100644 --- a/faraday_plugins/plugins/plugin.py +++ b/faraday_plugins/plugins/plugin.py @@ -4,35 +4,48 @@ See the file 'doc/LICENSE' for the license information """ +# Standard library imports +import hashlib +import logging import os +import re import shutil import tempfile - +import uuid +import zipfile from collections import defaultdict +from datetime import datetime +from pathlib import Path +import socket +from typing import List +# Related third party imports import pytz -import re -import uuid -import logging import simplejson as json -import zipfile -from datetime import datetime -import hashlib +# Local application imports +from faraday_plugins.plugins.plugins_utils import its_cve, its_cwe logger = logging.getLogger("faraday").getChild(__name__) VALID_SERVICE_STATUS = ("open", "closed", "filtered") VULN_SKIP_FIELDS_TO_HASH = ['run_date'] + class PluginBase: # TODO: Add class generic identifier class_signature = "PluginBase" - def __init__(self): + def __init__(self, *args, **kwargs): # Must be unique. Check that there is not - # an existant plugin with the same id. + # an existent plugin with the same id. # TODO: Make script that list current ids. + self.ignore_info = kwargs.get("ignore_info", False) + self.hostname_resolution = kwargs.get("hostname_resolution", True) + self.vuln_tag = kwargs.get("vuln_tag", None) + self.host_tag = kwargs.get("host_tag", None) + self.service_tag = kwargs.get("service_tag", None) + self.default_vuln_tag = None self.id = None self.auto_load = True self._rid = id(self) @@ -40,6 +53,7 @@ def __init__(self): self.name = None self.description = "" self._command_regex = None + self.command = None self._output_file_path = None self._use_temp_file = False self._delete_temp_file = False @@ -54,7 +68,7 @@ def __init__(self): self._hosts_cache = {} self._service_cache = {} self._vulns_cache = {} - self.start_date = datetime.now() + self.start_date = datetime.utcnow() self.logger = logger.getChild(self.__class__.__name__) self.open_options = {"mode": "r", "encoding": "utf-8"} self.plugin_version = "0.0" @@ -76,6 +90,24 @@ def _get_temp_file(self, extension="tmp"): temp_file_path = os.path.join(temp_dir, temp_filename) return temp_file_path + def resolve_hostname(self, hostname): + if not self.hostname_resolution: + return hostname + if not hostname: + self.logger.error(f"Hostname provided is None or Empty {hostname}, using 0.0.0.0 as ip") + return "0.0.0.0" + try: + socket.inet_aton(hostname) # is already an ip + return hostname + except OSError: + pass + try: + ip_address = socket.gethostbyname(hostname) + except Exception as e: + return hostname + else: + return ip_address + @staticmethod def get_utctimestamp(date): if date is not None: @@ -83,7 +115,7 @@ def get_utctimestamp(date): utc_date = date.astimezone(pytz.UTC) return utc_date.timestamp() except Exception as e: - logger.error("Error generating timestamp: %s", e) + logger.error(f"Error generating timestamp: {e}") return None else: return date @@ -101,6 +133,7 @@ def align_string_based_vulns(severity): if severity[0:3] in sev: return sev return severity + severity = align_string_based_vulns(severity) # Transform numeric severity into desc severity numeric_severities = {"0": "info", @@ -124,6 +157,9 @@ def save_host_cache(self, host): self._hosts_cache[cache_id] = obj_uuid else: obj_uuid = self._hosts_cache[cache_id] + if host['hostnames']: + chached_host = self.get_from_cache(obj_uuid) + chached_host['hostnames'] = list(set(chached_host['hostnames'] + host['hostnames'])) return obj_uuid def save_service_cache(self, host_id, service): @@ -138,32 +174,37 @@ def save_service_cache(self, host_id, service): return obj_uuid def save_service_vuln_cache(self, host_id, service_id, vuln): - cache_id = self.get_service_vuln_cache_id(host_id, service_id, vuln) - if cache_id not in self._vulns_cache: - obj_uuid = self.save_cache(vuln) - service = self.get_from_cache(service_id) - service["vulnerabilities"].append(vuln) - self._vulns_cache[cache_id] = obj_uuid + if self.ignore_info and vuln['severity'] == 'info': + return None else: - obj_uuid = self._vulns_cache[cache_id] - return obj_uuid + cache_id = self.get_service_vuln_cache_id(host_id, service_id, vuln) + if cache_id not in self._vulns_cache: + obj_uuid = self.save_cache(vuln) + service = self.get_from_cache(service_id) + service["vulnerabilities"].append(vuln) + self._vulns_cache[cache_id] = obj_uuid + else: + obj_uuid = self._vulns_cache[cache_id] + return obj_uuid def save_host_vuln_cache(self, host_id, vuln): - cache_id = self.get_host_vuln_cache_id(host_id, vuln) - if cache_id not in self._vulns_cache: - obj_uuid = self.save_cache(vuln) - host = self.get_from_cache(host_id) - host["vulnerabilities"].append(vuln) - self._vulns_cache[cache_id] = obj_uuid + if self.ignore_info and vuln['severity'] == 'info': + return None else: - obj_uuid = self._vulns_cache[cache_id] - return obj_uuid + cache_id = self.get_host_vuln_cache_id(host_id, vuln) + if cache_id not in self._vulns_cache: + obj_uuid = self.save_cache(vuln) + host = self.get_from_cache(host_id) + host["vulnerabilities"].append(vuln) + self._vulns_cache[cache_id] = obj_uuid + else: + obj_uuid = self._vulns_cache[cache_id] + return obj_uuid @staticmethod def _get_dict_hash(d, keys): return hash(frozenset(map(lambda x: (x, d.get(x, None)), keys))) - @classmethod def get_host_cache_id(cls, host): cache_id = cls._get_dict_hash(host, ['ip']) @@ -180,14 +221,17 @@ def get_host_service_cache_id(cls, host_id, service): def get_service_vuln_cache_id(cls, host_id, service_id, vuln): vuln_copy = vuln.copy() vuln_copy.update({"host_cache_id": host_id, "service_cache_id": service_id}) - cache_id = cls._get_dict_hash(vuln_copy, ['host_cache_id', 'service_cache_id', 'name', 'desc', 'website', 'path', 'pname', 'method']) + cache_id = cls._get_dict_hash(vuln_copy, + ['host_cache_id', 'service_cache_id', 'name', 'desc', 'website', 'path', 'pname', + 'method']) return cache_id @classmethod def get_host_vuln_cache_id(cls, host_id, vuln): vuln_copy = vuln.copy() vuln_copy.update({"host_cache_id": host_id}) - cache_id = cls._get_dict_hash(vuln_copy, ['host_cache_id', 'name', 'desc', 'website', 'path', 'pname', 'method']) + cache_id = cls._get_dict_hash(vuln_copy, + ['host_cache_id', 'name', 'desc', 'website', 'path', 'pname', 'method']) return cache_id def save_cache(self, obj): @@ -221,7 +265,7 @@ def getSettings(self): for param, (param_type, value) in self._settings.items(): yield param, value - def get_ws(self): # TODO Borrar + def get_ws(self): # TODO Borrar return "" def getSetting(self, name): @@ -239,11 +283,20 @@ def updateSettings(self, new_settings): def canParseCommandString(self, current_input): """ - This method can be overriden in the plugin implementation + This method can be overridden in the plugin implementation if a different kind of check is needed """ - return (self._command_regex is not None and - self._command_regex.match(current_input.strip()) is not None) + if (self._command_regex is not None and + self._command_regex.match(current_input.strip()) is not None): + self.command = self.get_command(current_input) + return True + + def get_command(self, current_input: str) -> str: + command = self._command_regex.findall(current_input)[0] + if isinstance(command, tuple): + return "".join(command).strip() + + return command.strip() def processCommandString(self, username, current_path, command_string): """ @@ -251,11 +304,11 @@ def processCommandString(self, username, current_path, command_string): command that it's going to be executed. """ self._current_path = current_path - if command_string.startswith("sudo"): + if command_string.startswith(("sudo", "python", "python3")): params = " ".join(command_string.split()[2:]) else: params = " ".join(command_string.split()[1:]) - self.vulns_data["command"]["params"] = params + self.vulns_data["command"]["params"] = params if not self.ignore_info else f"{params} (Info ignored)" self.vulns_data["command"]["user"] = username self.vulns_data["command"]["import_source"] = "shell" if self._use_temp_file: @@ -265,7 +318,7 @@ def processCommandString(self, username, current_path, command_string): def getCompletitionSuggestionsList(self, current_input): """ - This method can be overriden in the plugin implementation + This method can be overridden in the plugin implementation if a different kind of check is needed """ words = current_input.split(" ") @@ -278,25 +331,27 @@ def getCompletitionSuggestionsList(self, current_input): def processOutput(self, command_output): if self.has_custom_output(): - self._parse_filename(self.get_custom_file_path()) + self._parse_filename(Path(self.get_custom_file_path())) else: self.parseOutputString(command_output) - def _parse_filename(self, filename): - with open(filename, **self.open_options) as output: + def _parse_filename(self, filename: Path): + with filename.open(**self.open_options) as output: self.parseOutputString(output.read()) if self._delete_temp_file: try: - if os.path.isfile(filename): + if filename.is_file(): os.remove(filename) - elif os.path.isdir(filename): + elif filename.is_dir(): shutil.rmtree(filename) except Exception as e: - self.logger.error("Error on delete file: (%s) [%s]", filename, e) + self.logger.error(f"Error on delete file: ({filename}) [{e}]") - def processReport(self, filepath, user="faraday"): - if os.path.isfile(filepath): - self.vulns_data["command"]["params"] = filepath + def processReport(self, filepath: Path, user="faraday"): + if isinstance(filepath, str): # TODO workaround for compatibility, remove in the future + filepath = Path(filepath) + if filepath.is_file(): + self.vulns_data["command"]["params"] = filepath.name if not self.ignore_info else f"{filepath.name} (Info ignored)" self.vulns_data["command"]["user"] = user self.vulns_data["command"]["import_source"] = "report" self._parse_filename(filepath) @@ -327,27 +382,38 @@ def createAndAddHost(self, name, os="unknown", hostnames=None, mac=None, descrip tags = [] if isinstance(tags, str): tags = [tags] - host = {"ip": name, "os": os, "hostnames": hostnames, "description": description, "mac": mac, + if self.host_tag: + if isinstance(self.host_tag, list): + tags += self.host_tag + else: + tags.append(self.host_tag) + host = {"ip": name, "os": os, "hostnames": hostnames, "description": description, "mac": mac, "credentials": [], "services": [], "vulnerabilities": [], "tags": tags} host_id = self.save_host_cache(host) return host_id def createAndAddServiceToHost(self, host_id, name, - protocol="tcp", ports=None, - status="open", version="unknown", - description="", tags=None): + protocol="tcp", ports=None, + status="open", version="", + description="", tags=None): if ports: if isinstance(ports, list): ports = int(ports[0]) elif isinstance(ports, str): ports = int(ports) - + if not protocol: + protocol = "tcp" if status not in VALID_SERVICE_STATUS: status = 'open' if tags is None: tags = [] if isinstance(tags, str): tags = [tags] + if self.service_tag: + if isinstance(self.service_tag, list): + tags += self.service_tag + else: + tags.append(self.service_tag) service = {"name": name, "protocol": protocol, "port": ports, "status": status, "version": version, "description": description, "credentials": [], "vulnerabilities": [], "tags": tags} @@ -356,14 +422,31 @@ def createAndAddServiceToHost(self, host_id, name, return service_id + @staticmethod + def modify_refs_struct(ref: List[str]) -> List[dict]: + """ + Change reference struct from list of strings to a list of dicts with the form of {name, type} + """ + if not ref: + return [] + refs = [] + for r in ref: + if isinstance(r, dict): + refs.append(r) + else: + if r.strip(): + refs.append({'name': r.strip(), 'type': 'other'}) + return refs + def createAndAddVulnToHost(self, host_id, name, desc="", ref=None, severity="", resolution="", data="", external_id=None, run_date=None, impact=None, custom_fields=None, status="", policyviolations=None, - easeofresolution=None, confirmed=False, tags=None): - if ref is None: - ref = [] + easeofresolution=None, confirmed=False, tags=None, cve=None, cwe=None, cvss2=None, + cvss3=None): + + ref = self.modify_refs_struct(ref) if status == "": - status = "opened" + status = "open" if impact is None: impact = {} if policyviolations is None: @@ -374,10 +457,35 @@ def createAndAddVulnToHost(self, host_id, name, desc="", ref=None, tags = [] if isinstance(tags, str): tags = [tags] + if self.vuln_tag: + if isinstance(self.vuln_tag, list): + tags += self.vuln_tag + else: + tags.append(self.vuln_tag) + if self.default_vuln_tag: + if isinstance(self.default_vuln_tag, list): + tags += self.default_vuln_tag + else: + tags.append(self.default_vuln_tag) + if cve is None: + cve = [] + elif type(cve) is str: + cve = [cve] + cve = its_cve(cve) + if cwe is None: + cwe = [] + elif type(cwe) is str: + cwe = [cwe] + cwe = its_cwe(cwe) + if cvss2 is None: + cvss2 = {} + if cvss3 is None: + cvss3 = {} vulnerability = {"name": name, "desc": desc, "severity": self.normalize_severity(severity), "refs": ref, "external_id": external_id, "type": "Vulnerability", "resolution": resolution, "data": data, - "custom_fields": custom_fields, "status": status, "impact": impact, "policyviolations": policyviolations, - "confirmed": confirmed, "easeofresolution": easeofresolution, "tags": tags + "custom_fields": custom_fields, "status": status, "impact": impact, + "policyviolations": policyviolations, "cve": cve, "cvss3": cvss3, "cvss2": cvss2, + "confirmed": confirmed, "easeofresolution": easeofresolution, "tags": tags, "cwe": cwe } if run_date: vulnerability["run_date"] = self.get_utctimestamp(run_date) @@ -387,11 +495,11 @@ def createAndAddVulnToHost(self, host_id, name, desc="", ref=None, def createAndAddVulnToService(self, host_id, service_id, name, desc="", ref=None, severity="", resolution="", data="", external_id=None, run_date=None, custom_fields=None, policyviolations=None, impact=None, status="", - confirmed=False, easeofresolution=None, tags=None): - if ref is None: - ref = [] + confirmed=False, easeofresolution=None, tags=None, cve=None, cwe=None, cvss2=None, + cvss3=None): + ref = self.modify_refs_struct(ref) if status == "": - status = "opened" + status = "open" if impact is None: impact = {} if policyviolations is None: @@ -402,10 +510,35 @@ def createAndAddVulnToService(self, host_id, service_id, name, desc="", tags = [] if isinstance(tags, str): tags = [tags] + if self.vuln_tag: + if isinstance(self.vuln_tag, list): + tags += self.vuln_tag + else: + tags.append(self.vuln_tag) + if self.default_vuln_tag: + if isinstance(self.default_vuln_tag, list): + tags += self.default_vuln_tag + else: + tags.append(self.default_vuln_tag) + if cve is None: + cve = [] + elif type(cve) is str: + cve = [cve] + cve = its_cve(cve) + if cwe is None: + cwe = [] + elif type(cwe) is str: + cwe = [cwe] + cwe = its_cwe(cwe) + if cvss2 is None: + cvss2 = {} + if cvss3 is None: + cvss3 = {} vulnerability = {"name": name, "desc": desc, "severity": self.normalize_severity(severity), "refs": ref, "external_id": external_id, "type": "Vulnerability", "resolution": resolution, "data": data, - "custom_fields": custom_fields, "status": status, "impact": impact, "policyviolations": policyviolations, - "easeofresolution": easeofresolution, "confirmed": confirmed, "tags": tags + "custom_fields": custom_fields, "status": status, "impact": impact, + "policyviolations": policyviolations, "cve": cve, "cvss3": cvss3, "cvss2": cvss2, + "easeofresolution": easeofresolution, "confirmed": confirmed, "tags": tags, "cwe": cwe } if run_date: vulnerability["run_date"] = self.get_utctimestamp(run_date) @@ -418,17 +551,14 @@ def createAndAddVulnWebToService(self, host_id, service_id, name, desc="", response="", method="", pname="", params="", query="", category="", data="", external_id=None, confirmed=False, status="", easeofresolution=None, impact=None, - policyviolations=None, status_code=None, custom_fields=None, run_date=None, tags=None): + policyviolations=None, status_code=None, custom_fields=None, run_date=None, + tags=None, cve=None, cvss2=None, cvss3=None, cwe=None): if params is None: params = "" - if response is None: - response = "" if method is None: method = "" if pname is None: pname = "" - if params is None: - params = "" if query is None: query = "" if website is None: @@ -439,10 +569,9 @@ def createAndAddVulnWebToService(self, host_id, service_id, name, desc="", request = "" if response is None: response = "" - if ref is None: - ref = [] + ref = self.modify_refs_struct(ref) if status == "": - status = "opened" + status = "open" if impact is None: impact = {} if policyviolations is None: @@ -453,13 +582,38 @@ def createAndAddVulnWebToService(self, host_id, service_id, name, desc="", tags = [] if isinstance(tags, str): tags = [tags] + if self.vuln_tag: + if isinstance(self.vuln_tag, list): + tags += self.vuln_tag + else: + tags.append(self.vuln_tag) + if self.default_vuln_tag: + if isinstance(self.default_vuln_tag, list): + tags += self.default_vuln_tag + else: + tags.append(self.default_vuln_tag) + if cve is None: + cve = [] + elif type(cve) is str: + cve = [cve] + cve = its_cve(cve) + if cwe is None: + cwe = [] + elif type(cwe) is str: + cwe = [cwe] + cwe = its_cwe(cwe) + if cvss2 is None: + cvss2 = {} + if cvss3 is None: + cvss3 = {} vulnerability = {"name": name, "desc": desc, "severity": self.normalize_severity(severity), "refs": ref, "external_id": external_id, "type": "VulnerabilityWeb", "resolution": resolution, "data": data, "website": website, "path": path, "request": request, "response": response, "method": method, "pname": pname, "params": params, "query": query, "category": category, "confirmed": confirmed, "status": status, "easeofresolution": easeofresolution, - "impact": impact, "policyviolations": policyviolations, - "status_code": status_code, "custom_fields": custom_fields, "tags": tags} + "impact": impact, "policyviolations": policyviolations, "cve": cve, "cvss3": cvss3, + "cvss2": cvss2, "status_code": status_code, "custom_fields": custom_fields, "tags": tags, + "cwe": cwe} if run_date: vulnerability["run_date"] = self.get_utctimestamp(run_date) vulnerability_id = self.save_service_vuln_cache(host_id, service_id, vulnerability) @@ -468,7 +622,6 @@ def createAndAddVulnWebToService(self, host_id, service_id, name, desc="", def createAndAddNoteToHost(self, host_id, name, text): return None - def createAndAddNoteToService(self, host_id, service_id, name, text): return None @@ -484,8 +637,8 @@ def createAndAddCredToService(self, host_id, service_id, username, password): def get_data(self): self.vulns_data["command"]["tool"] = self.id - self.vulns_data["command"]["command"] = self.id - self.vulns_data["command"]["duration"] = (datetime.now() - self.start_date).microseconds + self.vulns_data["command"]["command"] = self.command if self.command else self.id + self.vulns_data["command"]["duration"] = (datetime.utcnow() - self.start_date).microseconds return self.vulns_data def get_json(self): @@ -517,14 +670,15 @@ def get_summary(self): vuln_copy = vuln.copy() for field in VULN_SKIP_FIELDS_TO_HASH: vuln_copy.pop(field, None) - dict_hash = hashlib.sha1(json.dumps(vuln_copy).encode()).hexdigest() + dict_hash = hashlib.sha1(json.dumps(vuln_copy).encode()).hexdigest() # nosec summary['vuln_hashes'].append(dict_hash) return summary + # TODO Borrar class PluginTerminalOutput(PluginBase): - def __init__(self): - super().__init__() + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) def processOutput(self, term_output): try: @@ -535,54 +689,58 @@ def processOutput(self, term_output): # TODO Borrar class PluginCustomOutput(PluginBase): - def __init__(self): - super().__init__() + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) def processOutput(self, term_output): # we discard the term_output since it's not necessary # for this type of plugins - self.processReport(self._output_file_path) + self.processReport(Path(self._output_file_path)) class PluginByExtension(PluginBase): - def __init__(self): - super().__init__() + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) self.extension = [] def report_belongs_to(self, extension="", **kwargs): match = False - if type(self.extension) == str: + if isinstance(self.extension, str): match = (self.extension == extension) - elif type(self.extension) == list: + elif isinstance(self.extension, list): match = (extension in self.extension) - self.logger.debug("Extension Match: [%s =/in %s] -> %s", extension, self.extension, match) + self.logger.debug(f"Extension Match: [{extension} =/in {self.extension}] -> {match}") return match class PluginXMLFormat(PluginByExtension): - def __init__(self): - super().__init__() + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) self.identifier_tag = [] + self.identifier_tag_attributes = {} self.extension = ".xml" self.open_options = {"mode": "rb"} - def report_belongs_to(self, main_tag="", **kwargs): + def report_belongs_to(self, main_tag="", main_tag_attributes={}, **kwargs): match = False if super().report_belongs_to(**kwargs): - if type(self.identifier_tag) == str: + if isinstance(self.identifier_tag, str): match = (main_tag == self.identifier_tag) - elif type(self.identifier_tag) == list: + elif isinstance(self.identifier_tag, list): match = (main_tag in self.identifier_tag) - self.logger.debug("Tag Match: [%s =/in %s] -> %s", main_tag, self.identifier_tag, match) + if self.identifier_tag_attributes: + match = self.identifier_tag_attributes.issubset(main_tag_attributes) + self.logger.debug(f"Tag Match: [{main_tag} =/in {self.identifier_tag}] -> {match}") return match class PluginJsonFormat(PluginByExtension): - def __init__(self): - super().__init__() + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) self.json_keys = set() + self.filter_keys = set() self.extension = ".json" def report_belongs_to(self, file_json_keys=None, **kwargs): @@ -590,15 +748,24 @@ def report_belongs_to(self, file_json_keys=None, **kwargs): if super().report_belongs_to(**kwargs): if file_json_keys is None: file_json_keys = set() - match = self.json_keys.issubset(file_json_keys) - self.logger.debug("Json Keys Match: [%s =/in %s] -> %s", file_json_keys, self.json_keys, match) + if self.filter_keys & file_json_keys: + return match + if isinstance(self.json_keys, list): + for jk in self.json_keys: + match = jk.issubset(file_json_keys) + self.logger.debug(f"Json Keys Match: [{file_json_keys} =/in {jk}] -> {match}") + if match: + break + else: + match = self.json_keys.issubset(file_json_keys) + self.logger.debug(f"Json Keys Match: [{file_json_keys} =/in {self.json_keys}] -> {match}") return match class PluginMultiLineJsonFormat(PluginByExtension): - def __init__(self): - super().__init__() + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) self.json_keys = set() self.extension = ".json" @@ -613,8 +780,7 @@ def report_belongs_to(self, file_json_keys=None, **kwargs): matched_lines = list(filter(lambda json_line: self.json_keys.issubset(json_line.keys()), json_lines)) match = len(matched_lines) == len(json_lines) - self.logger.debug("Json Keys Match: [%s =/in %s] -> %s", json_lines[0].keys(), self.json_keys, - match) + self.logger.debug(f"Json Keys Match: [{json_lines[0].keys()} =/in {self.json_keys}] -> {match}") except ValueError: return False return match @@ -622,8 +788,8 @@ def report_belongs_to(self, file_json_keys=None, **kwargs): class PluginCSVFormat(PluginByExtension): - def __init__(self): - super().__init__() + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) self.extension = ".csv" self.csv_headers = set() @@ -636,14 +802,14 @@ def report_belongs_to(self, file_csv_headers=None, **kwargs): match = bool(list(filter(lambda x: x.issubset(file_csv_headers), self.csv_headers))) else: match = self.csv_headers.issubset(file_csv_headers) - self.logger.debug("CSV Headers Match: [%s =/in %s] -> %s", file_csv_headers, self.csv_headers, match) + self.logger.debug(f"CSV Headers Match: [{file_csv_headers} =/in {self.csv_headers}] -> {match}") return match class PluginZipFormat(PluginByExtension): - def __init__(self): - super().__init__() + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) self.extension = ".zip" self.files_list = set() @@ -657,7 +823,5 @@ def report_belongs_to(self, files_in_zip=None, **kwargs): if files_in_zip is None: files_in_zip = set() match = bool(self.files_list & files_in_zip) - self.logger.debug("Files List Match: [%s =/in %s] -> %s", files_in_zip, self.files_list, match) + self.logger.debug(f"Files List Match: [{files_in_zip} =/in {self.files_list}] -> {match}") return match - - diff --git a/faraday_plugins/plugins/plugins_utils.py b/faraday_plugins/plugins/plugins_utils.py index 628ecd3a..515f88d0 100644 --- a/faraday_plugins/plugins/plugins_utils.py +++ b/faraday_plugins/plugins/plugins_utils.py @@ -1,30 +1,36 @@ """ Faraday Penetration Test IDE -Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/) +Copyright (C) 2013 Infobyte LLC (https://www.faradaysec.com/) See the file 'doc/LICENSE' for the license information """ -import os +# Standard library imports import logging -import socket -from collections import defaultdict - +import os +import re from urllib.parse import urlsplit - +from bs4 import BeautifulSoup +from markdown import markdown SERVICE_MAPPER = None - +CVE_regex = re.compile(r'CVE-\d{4}-\d{4,7}') +CWE_regex = re.compile(r'CWE-\d{1,4}') logger = logging.getLogger(__name__) - +CVSS_RANGE = [(0.0, 0.1, 'info'), + (0.1, 4.0, 'low'), + (4.0, 7.0, 'med'), + (7.0, 9.0, 'high'), + (9.0, 10.1, 'critical')] def get_vulnweb_url_fields(url): """Given a URL, return kwargs to pass to createAndAddVulnWebToService.""" parse = urlsplit(url) return { - "website": "{}://{}".format(parse.scheme, parse.netloc), + "website": f"{parse.scheme}://{parse.netloc}", "path": parse.path, "query": parse.query - } + } + def filter_services(): global SERVICE_MAPPER @@ -94,19 +100,31 @@ def get_all_protocols(): 'mobility-header' ] - for item in protocols: - yield item + yield from protocols -def resolve_hostname(hostname): +def get_severity_from_cvss(cvss): try: - socket.inet_aton(hostname) # is already an ip - return hostname - except socket.error: - pass - try: - ip_address = socket.gethostbyname(hostname) - except Exception as e: - return hostname - else: - return ip_address \ No newline at end of file + if not isinstance(cvss, float): + cvss = float(cvss) + + for (lower, upper, severity) in CVSS_RANGE: + if lower <= cvss < upper: + return severity + except ValueError: + return 'unclassified' + + +def its_cve(cves: list): + r = [cve for cve in cves if CVE_regex.match(cve)] + return r + + +def its_cwe(cwes: list): + r = [cwe for cwe in cwes if CWE_regex.match(cwe)] + return r + +def markdown2text(text): + html = markdown(text) + text = ''.join(BeautifulSoup(html, features="lxml").findAll(text=True)) + return text diff --git a/faraday_plugins/plugins/repo/acunetix/DTO.py b/faraday_plugins/plugins/repo/acunetix/DTO.py new file mode 100644 index 00000000..335992cb --- /dev/null +++ b/faraday_plugins/plugins/repo/acunetix/DTO.py @@ -0,0 +1,363 @@ +from typing import List +import re + +class Technicaldetails: + def __init__(self, node): + self.node = node + + @property + def request(self) -> str: + if self.node in ('', None): + return '' + return self.node.findtext('Request', '') + + @property + def response(self) -> str: + if self.node in ('', None): + return '' + return self.node.findtext('Response', '') + + +class Cve: + def __init__(self, node): + self.node = node + + @property + def text(self) -> str: + if self.node in ('', None): + return '' + return self.node.text + + +class CVEList: + def __init__(self, node): + self.node = node + + @property + def cve(self) -> Cve: + if self.node is None: + return '' + return Cve(self.node.find('CVE')) + + +class Cwe: + def __init__(self, node): + self.node = node + + @property + def id_attr(self) -> str: + return self.node.attrib.get('id', '') + + @property + def text(self) -> str: + return self.node.text + + +class Cwelist: + def __init__(self, node): + self.node = node + + @property + def cwe(self) -> Cwe: + return Cwe(self.node.find('CWE')) + + +class Cvss: + def __init__(self, node): + self.node = node + + @property + def descriptor(self) -> str: + return self.node.findtext('Descriptor', '') + + @property + def score(self) -> str: + if self.node is None: + return '' + return self.node.findtext('Score') + + @property + def av(self) -> str: + return self.node.findtext('AV', '') + + @property + def ac(self) -> str: + return self.node.findtext('AC', '') + + @property + def au(self) -> str: + return self.node.findtext('Au', '') + + @property + def c(self) -> str: + return self.node.findtext('C', '') + + @property + def i(self) -> str: + return self.node.findtext('I', '') + + @property + def a(self) -> str: + return self.node.findtext('A', '') + + @property + def e(self): + return self.node.find('E') + + @property + def rl(self): + return self.node.find('RL') + + @property + def rc(self): + return self.node.find('RC') + + +class Cvss3: + def __init__(self, node): + self.node = node + + @property + def descriptor(self) -> str: + return self.node.findtext('Descriptor', '') + + @property + def score(self) -> str: + return self.node.findtext('Score') + + @property + def tempscore(self): + return self.node.find('TempScore') + + @property + def envscore(self): + return self.node.find('EnvScore') + + @property + def av(self) -> str: + return self.node.find('AV', '') + + @property + def ac(self) -> str: + return self.node.find('AC', '') + + @property + def pr(self) -> str: + return self.node.find('PR', '') + + @property + def ui(self) -> str: + return self.node.find('UI', '') + + @property + def s(self) -> str: + return self.node.find('S', '') + + @property + def c(self) -> str: + return self.node.find('C', '') + + @property + def i(self) -> str: + return self.node.findtext('I', '') + + @property + def a(self) -> str: + return self.node.findtext('A', '') + + @property + def e(self): + return self.node.find('E') + + @property + def rl(self): + return self.node.find('RL') + + @property + def rc(self): + return self.node.find('RC') + + +class Reference: + def __init__(self, node): + self.node = node + + @property + def database(self) -> str: + return self.node.findtext('Database', '') + + @property + def url(self) -> str: + return self.node.findtext('URL', '') + + +class References: + def __init__(self, node): + self.node = node + + @property + def reference(self) -> List[Reference]: + return [Reference(i) for i in self.node.findall('Reference', [])] + + +class Reportitem: + def __init__(self, node): + self.node = node + + @property + def id_attr(self) -> str: + return self.node.findtext('id', '') + + @property + def color_attr(self) -> str: + return self.node.findtext('color', '') + + @property + def name(self) -> str: + return self.node.findtext('Name', '') + + @property + def modulename(self) -> str: + return self.node.findtext('ModuleName', '') + + @property + def details(self) -> str: + return self.node.findtext('Details', '') + + @property + def affects(self) -> str: + return self.node.findtext('Affects', '') + + @property + def parameter(self) -> str: + return self.node.findtext('Parameter') + + @property + def aop_sourcefile(self): + return self.node.find('AOP_SourceFile') + + @property + def aop_sourceline(self): + return self.node.find('AOP_SourceLine') + + @property + def aop_additional(self): + return self.node.find('AOP_Additional') + + @property + def isfalsepositive(self): + return self.node.find('IsFalsePositive') + + @property + def severity(self) -> str: + return self.node.findtext('Severity', '') + + @property + def type(self) -> str: + return self.node.findtext('Type', '') + + @property + def impact(self) -> str: + return self.node.findtext('Impact', '') + + @property + def description(self) -> str: + return self.node.findtext('Description', '') + + @property + def recommendation(self) -> str: + return self.node.findtext('Recommendation', '') + + @property + def technicaldetails(self) -> Technicaldetails: + return Technicaldetails(self.node.find('TechnicalDetails')) + + @property + def cwelist(self) -> Cwelist: + return Cwelist(self.node.find('CWEList')) + + @property + def cvelist(self): + return CVEList(self.node.find('CVEList')) + + @property + def cvss(self) -> Cvss: + cvss = self.node.find('CVSS') + if not cvss: + cvss = self.node.find('cvss') + return Cvss(cvss) + + @property + def cvss3(self) -> Cvss3: + cvss = self.node.find('CVSS3') + if not cvss: + cvss = self.node.find('cvss3') + return Cvss3(cvss) + + @property + def references(self) -> References: + return References(self.node.find('References')) + + +class Reportitems: + def __init__(self, node): + self.node = node + + @property + def reportitem(self) -> List[Reportitem]: + return [Reportitem(i) for i in self.node.findall('ReportItem', [])] + + +class Crawler: + def __init__(self, node): + self.node = node + + @property + def start_url_attr(self) -> str: + return self.node.get('StartUrl', '') + + +class Scan: + def __init__(self, node): + self.node = node + + @property + def reportitems(self) -> Reportitems: + return Reportitems(self.node.find('ReportItems')) + + @property + def start_url(self) -> str: + return self.node.findtext("StartURL", "") + + @property + def crawler(self) -> Crawler: + return Crawler(self.node.find('Crawler')) + + @property + def os(self) -> str: + if not self.node.findtext("Os", "unknown"): + return "unknown" + return self.node.findtext("Os", "unknown") + + @property + def banner(self) -> str: + if not self.node.findtext('Banner'): + return None + return self.node.findtext("Banner") + + @property + def start_url_new(self) -> str: + return self.node.findtext("", "") + + +class Acunetix: + def __init__(self, node): + self.node = node + + @property + def exportedon_attr(self) -> str: + return self.node.get('ExportedOn') + + @property + def scan(self) -> List[Scan]: + return [Scan(i) for i in self.node.findall('Scan', [])] diff --git a/faraday_plugins/plugins/repo/acunetix/__init__.py b/faraday_plugins/plugins/repo/acunetix/__init__.py index ea531e17..625a6e25 100644 --- a/faraday_plugins/plugins/repo/acunetix/__init__.py +++ b/faraday_plugins/plugins/repo/acunetix/__init__.py @@ -4,4 +4,4 @@ See the file 'doc/LICENSE' for the license information """ -# I'm Py3 \ No newline at end of file + diff --git a/faraday_plugins/plugins/repo/acunetix/plugin.py b/faraday_plugins/plugins/repo/acunetix/plugin.py index 644850da..b2bef048 100644 --- a/faraday_plugins/plugins/repo/acunetix/plugin.py +++ b/faraday_plugins/plugins/repo/acunetix/plugin.py @@ -4,25 +4,13 @@ See the file 'doc/LICENSE' for the license information """ +from re import findall from urllib.parse import urlsplit -import re -import os from lxml import etree -try: - import xml.etree.cElementTree as ET - import xml.etree.ElementTree as ET_ORIG - ETREE_VERSION = ET_ORIG.VERSION -except ImportError: - import xml.etree.ElementTree as ET - ETREE_VERSION = ET.VERSION - from faraday_plugins.plugins.plugin import PluginXMLFormat -from faraday_plugins.plugins.plugins_utils import resolve_hostname - -ETREE_VERSION = [int(i) for i in ETREE_VERSION.split(".")] - +from faraday_plugins.plugins.repo.acunetix.DTO import Acunetix, Scan __author__ = "Francisco Amato" __copyright__ = "Copyright (c) 2013, Infobyte LLC" @@ -47,13 +35,12 @@ class AcunetixXmlParser: """ def __init__(self, xml_output): + tree = self.parse_xml(xml_output) - if len(tree): - self.sites = list(self.get_items(tree)) - else: - self.sites = [] + self.acunetix = Acunetix(tree) - def parse_xml(self, xml_output): + @staticmethod + def parse_xml(xml_output): """ Open and parse an xml file. @@ -62,163 +49,24 @@ def parse_xml(self, xml_output): @return xml_tree An xml tree instance. None if error. """ + try: parser = etree.XMLParser(recover=True) tree = etree.fromstring(xml_output, parser=parser) except SyntaxError as err: - print("SyntaxError: %s. %s", err, xml_output) + print(f"SyntaxError: {err}. {xml_output}") return None return tree - def get_items(self, tree): - """ - @return items A list of Host instances - """ - - for node in tree.findall('Scan'): - yield Site(node) - - -def get_attrib_from_subnode(xml_node, subnode_xpath_expr, attrib_name): - """ - Finds a subnode in the item node and the retrieves a value from it - - @return An attribute value - """ - global ETREE_VERSION - node = None - - if ETREE_VERSION[0] <= 1 and ETREE_VERSION[1] < 3: - - match_obj = re.search("([^\@]+?)\[\@([^=]*?)=\'([^\']*?)\'", subnode_xpath_expr) - - if match_obj is not None: - node_to_find = match_obj.group(1) - xpath_attrib = match_obj.group(2) - xpath_value = match_obj.group(3) - for node_found in xml_node.findall(node_to_find): - if node_found.attrib[xpath_attrib] == xpath_value: - node = node_found - break - else: - node = xml_node.find(subnode_xpath_expr) - - else: - node = xml_node.find(subnode_xpath_expr) - - if node is not None: - return node.get(attrib_name) - - return None - - -class Site: - - def __init__(self, item_node): - self.node = item_node - url_data = self.get_url(self.node) - - self.protocol = url_data.scheme - if url_data.hostname: - self.host = url_data.hostname - else: - self.host = None - # Use the port in the URL if it is defined, or 80 or 443 by default - self.port = url_data.port or (443 if url_data.scheme == "https" else 80) - - self.ip = resolve_hostname(self.host) - self.os = self.get_text_from_subnode('Os') - self.banner = self.get_text_from_subnode('Banner') - self.items = [] - for alert in self.node.findall('ReportItems/ReportItem'): - self.items.append(Item(alert)) - - def get_text_from_subnode(self, subnode_xpath_expr): - """ - Finds a subnode in the host node and the retrieves a value from it. - - @return An attribute value - """ - sub_node = self.node.find(subnode_xpath_expr) - if sub_node is not None: - return sub_node.text - - return None - - def get_url(self, node): - url = self.get_text_from_subnode('StartURL') - url_data = urlsplit(url) - if not url_data.scheme: - # Getting url from subnode 'Crawler' - url_aux = get_attrib_from_subnode(node, 'Crawler', 'StartUrl') - url_data = urlsplit(url_aux) - - return url_data - - -class Item: - """ - An abstract representation of a Item - - - @param item_node A item_node taken from an acunetix xml tree - """ - - def __init__(self, item_node): - self.node = item_node - self.name = self.get_text_from_subnode('Name') - self.severity = self.get_text_from_subnode('Severity') - self.request = self.get_text_from_subnode('TechnicalDetails/Request') - self.response = self.get_text_from_subnode('TechnicalDetails/Response') - self.parameter = self.get_text_from_subnode('Parameter') - self.uri = self.get_text_from_subnode('Affects') - - if self.get_text_from_subnode('Description'): - self.desc = self.get_text_from_subnode('Description') - else: - self.desc = "" - - if self.get_text_from_subnode('Recommendation'): - self.resolution = self.get_text_from_subnode('Recommendation') - else: - self.resolution = "" - - if self.get_text_from_subnode('reference'): - self.desc += "\nDetails: " + self.get_text_from_subnode('Details') - - # Add path and params to the description to create different IDs if at - # least one of this fields is different - if self.uri: - self.desc += '\nPath: ' + self.uri - if self.parameter: - self.desc += '\nParameter: ' + self.parameter - - self.ref = [] - for n in item_node.findall('References/Reference'): - n2 = n.find('URL') - self.ref.append(n2.text) - - def get_text_from_subnode(self, subnode_xpath_expr): - """ - Finds a subnode in the host node and the retrieves a value from it. - - @return An attribute value - """ - sub_node = self.node.find(subnode_xpath_expr) - if sub_node is not None: - return sub_node.text - - return None - class AcunetixPlugin(PluginXMLFormat): """ Example plugin to parse acunetix output. """ - def __init__(self): - super().__init__() + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) self.identifier_tag = "ScanGroup" self.id = "Acunetix" self.name = "Acunetix XML Output Plugin" @@ -239,57 +87,105 @@ def parseOutputString(self, output): """ parser = AcunetixXmlParser(output) - for site in parser.sites: - if site.ip is None: + for site in parser.acunetix.scan: + url_data = self.get_domain(site) + if not url_data: continue - - if site.host != site.ip and site.host is not None: - hostnames = [site.host] + if url_data.hostname: + self.old_structure(url_data, site) else: - hostnames = None - h_id = self.createAndAddHost(site.ip, site.os, hostnames=hostnames) - s_id = self.createAndAddServiceToHost( - h_id, - "http", - "tcp", - ports=[site.port], - version=site.banner, - status='open') - for item in site.items: + self.new_structure(site) - if item.desc is None: - self.createAndAddVulnWebToService( - h_id, - s_id, - item.name, - desc="", - website=site.host, - severity=item.severity, - resolution=item.resolution, - path=item.uri, - params=item.parameter, - request=item.request, - response=item.response, - ref=item.ref) - else: - self.createAndAddVulnWebToService( - h_id, - s_id, - item.name, - item.desc, - website=site.host, - severity=item.severity, - resolution=item.resolution, - path=item.uri, - params=item.parameter, - request=item.request, - response=item.response, - ref=item.ref) - del parser + def new_structure(self, site): + for item in site.reportitems.reportitem: + if not item.technicaldetails.request: + self.logger.warning("No request data") + continue + request_host = findall('Host: (.*)', item.technicaldetails.request) + if request_host: + host = request_host[0] + url = f'http://{host}' + url_data = urlsplit(url) + site_ip = self.resolve_hostname(host) + h_id = self.createAndAddHost(site_ip, site.os, hostnames=[host]) + s_id = self.createAndAddServiceToHost( + h_id, + "http", + "tcp", + ports=['443'], + version=site.banner, + status='open') + self.create_vul(item, h_id, s_id, url_data) + else: + self.logger.warning("No host in request") - def setHost(self): - pass + def old_structure(self, url_data, site: Scan): + site_ip = self.resolve_hostname(url_data.hostname) + if url_data.hostname: + hostnames = [url_data.hostname] + else: + hostnames = None + port = url_data.port or (443 if url_data.scheme == "https" else 80) + + h_id = self.createAndAddHost(site_ip, site.os, hostnames=hostnames) + s_id = self.createAndAddServiceToHost( + h_id, + "http", + "tcp", + ports=[port], + version=site.banner, + status='open') + for item in site.reportitems.reportitem: + self.create_vul(item, h_id, s_id, url_data) + + def create_vul(self, item, h_id, s_id, url_data): + description = item.description + cvss3 = {} + if item.cvss3.node is not None: + cvss3['vector_string'] = item.cvss3.descriptor + cvss2 = {} + if item.cvss.node is not None: + cvss2['vector_string'] = item.cvss.descriptor + if item.affects: + description += f'\nPath: {item.affects}' + if item.parameter: + description += f'\nParameter: {item.parameter}' + try: + cve = [item.cvelist.cve.text if item.cvelist.cve else ""] + except Exception: + cve = [] + try: + cwe = [item.cwelist.cwe.text if item.cwelist.cwe else ""] + except: + cwe = [] + self.createAndAddVulnWebToService( + h_id, + s_id, + item.name, + description, + website=url_data.hostname, + severity=item.severity, + resolution=item.recommendation, + path=item.affects, + params=item.parameter, + request=item.technicaldetails.request, + response=item.technicaldetails.response, + ref=[i.url for i in item.references.reference], + cve=cve, + cwe=cwe, + cvss2=cvss2, + cvss3=cvss3) + + @staticmethod + def get_domain(scan: Scan): + url = scan.start_url + if not url.startswith('http'): + url = f'http://{url}' + url_data = urlsplit(url) + if not url_data.scheme: + url_data = urlsplit(scan.crawler.start_url_attr) + return url_data -def createPlugin(): - return AcunetixPlugin() \ No newline at end of file +def createPlugin(*args, **kwargs): + return AcunetixPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/acunetix360/DTO.py b/faraday_plugins/plugins/repo/acunetix360/DTO.py new file mode 100644 index 00000000..a5802454 --- /dev/null +++ b/faraday_plugins/plugins/repo/acunetix360/DTO.py @@ -0,0 +1,210 @@ +from typing import List +import re + +import bs4 + +CLEAN = re.compile("<.*?>") + +def clean_external_references(external_ref) -> List[str]: + data = bs4.BeautifulSoup(external_ref).find_all("a") + refs = [] + for i in data: + refs.append(i.get("href")) + return refs + + +class Classification: + def __init__(self, node): + self.node = node + + @property + def iso(self) -> str: + iso = self.node.get('Iso27001', "") + if iso: + return "Iso27001-" + iso + else: + return "" + + @property + def capec(self) -> str: + capec = self.node.get('Capec', "") + if capec: + return "Capec-" + capec + else: + return "" + + @property + def cvss(self) -> str: + if self.node.get('Cvss'): + return self.node.get('Cvss').get("Vector", "") + return "" + + @property + def cvss31(self) -> str: + if self.node.get('Cvss31'): + return self.node.get('Cvss31').get("Vector", "") + return "" + + @property + def cwe(self) -> str: + cwe = self.node.get('Cwe', "") + if cwe: + return "CWE-" + cwe + else: + return "" + + @property + def hipaa(self) -> str: + hippa = self.node.get('Hipaa', "") + if hippa: + return "Hipaa-" + hippa + else: + return "" + + @property + def owasp(self) -> str: + owasp = self.node.get('Owasp', "") + if owasp: + return "Owasp-" + owasp + else: + return "" + + @property + def pci(self) -> str: + pci = self.node.get('Pci32', "") + if pci: + return "Pci32-" + pci + else: + return "" + + @property + def wasc(self) -> str: + wasc = self.node.get('Wasc', "") + if wasc: + return "Wasc-" + wasc + else: + return "" + + @property + def asvs(self) -> str: + asvs = self.node.get('Asvs40', "") + if asvs: + return "Asvs40-" + asvs + else: + return "" + + @property + def nistsp(self) -> str: + nistsp = self.node.get('Nistsp80053', "") + if nistsp: + return "Nistsp80053-" + nistsp + else: + return "" + + @property + def disastig(self) -> str: + disastig = self.node.get('DisaStig', "") + if disastig: + return "DisaStig-" + disastig + else: + return "" + + +class Request: + def __init__(self, node): + self.node = node + + @property + def method(self) -> str: + return self.node.get("Method", "") + + @property + def content(self) -> str: + return self.node.get("Content", "") + + +class Vulnerability: + def __init__(self, node): + self.node = node + + @property + def name(self) -> str: + return self.node.get('Name', "") + + @property + def confirmed(self) -> bool: + return self.node.get('Confirmed', False) + + @property + def description(self) -> str: + return CLEAN.sub("", self.node.get('Description', "The tool did not provide a description")) + + @property + def remedial_procedure(self) -> str: + return CLEAN.sub("", self.node.get("RemedialProcedure","")) + + @property + def severity(self) -> str: + return self.node.get('Severity', "unclassified") + + @property + def impact(self) -> str: + return CLEAN.sub("", self.node.get('Impact', "")) + + @property + def tags(self) -> List[str]: + return self.node.get("Tags", []) + + @property + def classification(self) -> Classification: + return Classification(self.node.get("Classification", {})) + + @property + def external_id(self) -> str: + return self.node.get("LookupId", "") + + @property + def remedial_actions(self) -> str: + return CLEAN.sub("", self.node.get('RemedialActions', "")) + + @property + def proof_of_concept(self) -> str: + return CLEAN.sub("", self.node.get('ProofOfConcept', "")) + + @property + def external_references(self) -> List[str]: + return clean_external_references(self.node.get('ExternalReferences', "")) + + @property + def request(self) -> Request: + return Request(self.node.get('Request', {})) + + @property + def response(self) -> str: + return self.node.get('Response', {}).get("Content", "") + + @property + def url(self) -> str: + return self.node.get('Url', "") + + +class Target: + def __init__(self, node): + self.node = node + + @property + def url(self) -> str: + return self.node.get('Url', "") + + +class Acunetix360JsonParser: + def __init__(self, node): + self.node = node + + @property + def target(self) -> Target: + return Target(self.node.get('Target', {})) + + @property + def vulnerabilities(self) -> List[Vulnerability]: + return [Vulnerability(vuln) for vuln in self.node.get('Vulnerabilities', [])] diff --git a/faraday_plugins/plugins/repo/acunetix360/__init__.py b/faraday_plugins/plugins/repo/acunetix360/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/faraday_plugins/plugins/repo/acunetix360/plugin.py b/faraday_plugins/plugins/repo/acunetix360/plugin.py new file mode 100644 index 00000000..c984305e --- /dev/null +++ b/faraday_plugins/plugins/repo/acunetix360/plugin.py @@ -0,0 +1,95 @@ +""" +Faraday Penetration Test IDE +Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/) +See the file 'doc/LICENSE' for the license information + +""" +from urllib.parse import urlsplit +import ipaddress + +from lxml import etree + +from faraday_plugins.plugins.plugin import PluginJsonFormat +from faraday_plugins.plugins.repo.acunetix.DTO import Acunetix, Scan +from json import loads + +__author__ = "Gonzalo Martinez" +__copyright__ = "Copyright (c) 2013, Infobyte LLC" +__credits__ = ["Gonzalo Martinez"] +__version__ = "1.0.0" +__maintainer__ = "Gonzalo Martinez" +__email__ = "gmartinez@infobytesec.com" +__status__ = "Development" + +from faraday_plugins.plugins.repo.acunetix360.DTO import Acunetix360JsonParser + + +class Acunetix360Plugin(PluginJsonFormat): + + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) + self.id = "Acunetix360" + self.name = "Acunetix360 Output Plugin" + self.plugin_version = "0.1" + self.version = "9" + self.json_keys = {'Target', "Vulnerabilities", "Generated"} + self.framework_version = "1.0.0" + self._temp_file_extension = "json" + + def parseOutputString(self, output): + parser = Acunetix360JsonParser(loads(output)) + h_id = self.createAndAddHost(self.resolve_hostname(parser.target.url)) + for vuln in parser.vulnerabilities: + s_id = self.createAndAddServiceToHost( + host_id=h_id, + ports="443", + name="/"+vuln.url.split('/')[1] + ) + references = [ + ] + if vuln.classification.iso: + references.append(vuln.classification.iso) + if vuln.classification.capec: + references.append(vuln.classification.capec) + if vuln.classification.hipaa: + references.append(vuln.classification.hipaa) + if vuln.classification.pci: + references.append(vuln.classification.pci) + if vuln.classification.wasc: + references.append(vuln.classification.wasc) + if vuln.classification.asvs: + references.append(vuln.classification.asvs) + if vuln.classification.nistsp: + references.append(vuln.classification.nistsp) + if vuln.classification.disastig: + references.append(vuln.classification.disastig) + resolution = vuln.remedial_actions + "\n" + vuln.remedial_procedure + data = vuln.impact + "\n POC: " + vuln.proof_of_concept + + cvss3 = {} + if vuln.classification.cvss31: + cvss3['vector_string'] = vuln.classification.cvss31 + elif vuln.classification.cvss: + cvss3['vector_string'] = vuln.classification.cvss + self.createAndAddVulnWebToService( + host_id=h_id, + service_id=s_id, + name=vuln.name, + desc=vuln.description, + ref=references, + severity=vuln.severity, + resolution=resolution, + data=data, + external_id="Acunetix360-"+vuln.external_id, + tags=vuln.tags, + method=vuln.request.method, + request=vuln.request.content, + response=vuln.response, + cwe=vuln.classification.cwe, + cvss3=cvss3 + ) + + + +def createPlugin(*args, **kwargs): + return Acunetix360Plugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/acunetix_json/DTO.py b/faraday_plugins/plugins/repo/acunetix_json/DTO.py new file mode 100644 index 00000000..3e087cff --- /dev/null +++ b/faraday_plugins/plugins/repo/acunetix_json/DTO.py @@ -0,0 +1,154 @@ +from typing import List + + +class InfoVul: + def __init__(self, node): + self.node = node + + @property + def vt_id(self) -> str: + if not self.node: + return '' + return self.node.get('vt_id', '') + + @property + def request(self) -> str: + if not self.node: + return '' + return self.node.get('request', '') + + + + +class Vulnerabilities: + def __init__(self, node): + self.node = node + + @property + def info(self) -> InfoVul: + return InfoVul(self.node.get('info')) + + @property + def response(self) -> str: + if not self.node: + return '' + return self.node.get('response', '') + + +class VulnerabilityTypes: + def __init__(self, node): + self.node = node + + @property + def vt_id(self) -> str: + if not self.node: + return '' + return self.node.get('vt_id', '') + + @property + def name(self) -> str: + if not self.node: + return '' + return self.node.get('name', '') + + @property + def description(self) -> str: + if not self.node: + return '' + return self.node.get('description', '') + + @property + def severity(self) -> int: + return self.node.get('severity', '') + + @property + def recommendation(self) -> str: + if not self.node: + return '' + return self.node.get('recommendation', '') + + @property + def app_id(self) -> str: + if not self.node: + return '' + return self.node.get('app_id', '') + + @property + def use_ssl(self) -> bool: + if not self.node: + return '' + return self.node.get('use_ssl', '') + + @property + def tags(self) -> list: + if not self.node: + return [''] + return self.node.get('tags', ['']) + + def cvss_score(self) -> str: + return self.node.get('cvss_score') + + @property + def cvss2_vector(self) -> str: + return self.node.get('cvss2', '') + + @property + def cvss3_vector(self) -> str: + return self.node.get('cvss3', '') + + +class Info: + def __init__(self, node): + self.node = node + + @property + def host(self) -> str: + if not self.node: + return '' + return self.node.get('host', '') + + @property + def start_url(self) -> str: + if not self.node: + return '' + return self.node.get('start_url', '') + +class Scan: + def __init__(self, node): + self.node = node + + @property + def info(self) -> Info: + return Info(self.node.get('info')) + + @property + def vul_types(self) -> List[VulnerabilityTypes]: + return [VulnerabilityTypes(i) for i in self.node.get('vulnerability_types', [])] + + @property + def vulnerabilities(self) -> List[Vulnerabilities]: + return [Vulnerabilities(i) for i in self.node.get('vulnerabilities', [])] + + +class Export: + def __init__(self, node): + self.node = node + + @property + def scans(self) -> List[Scan]: + return [Scan(i) for i in self.node.get('scans', [])] + + @property + def lang(self) -> str: + if not self.node: + return '' + return self.node.get('scans', '') + + +class AcunetixJsonParser: + def __init__(self, node): + self.node = node + + @property + def export(self) -> Export: + return Export(self.node.get('export')) diff --git a/faraday_plugins/plugins/repo/fruitywifi/__init__.py b/faraday_plugins/plugins/repo/acunetix_json/__init__.py old mode 100755 new mode 100644 similarity index 94% rename from faraday_plugins/plugins/repo/fruitywifi/__init__.py rename to faraday_plugins/plugins/repo/acunetix_json/__init__.py index ea531e17..625a6e25 --- a/faraday_plugins/plugins/repo/fruitywifi/__init__.py +++ b/faraday_plugins/plugins/repo/acunetix_json/__init__.py @@ -4,4 +4,4 @@ See the file 'doc/LICENSE' for the license information """ -# I'm Py3 \ No newline at end of file + diff --git a/faraday_plugins/plugins/repo/acunetix_json/plugin.py b/faraday_plugins/plugins/repo/acunetix_json/plugin.py new file mode 100644 index 00000000..9afe0a1e --- /dev/null +++ b/faraday_plugins/plugins/repo/acunetix_json/plugin.py @@ -0,0 +1,150 @@ +""" +Faraday Penetration Test IDE +Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/) +See the file 'doc/LICENSE' for the license information + +""" +from urllib.parse import urlsplit +import ipaddress + +from lxml import etree + +from faraday_plugins.plugins.plugin import PluginJsonFormat +from faraday_plugins.plugins.repo.acunetix.DTO import Acunetix, Scan +from json import loads + +__author__ = "Francisco Amato" +__copyright__ = "Copyright (c) 2013, Infobyte LLC" +__credits__ = ["Francisco Amato"] +__version__ = "1.0.0" +__maintainer__ = "Francisco Amato" +__email__ = "famato@infobytesec.com" +__status__ = "Development" + +from faraday_plugins.plugins.repo.acunetix_json.DTO import AcunetixJsonParser, Vulnerabilities, \ + VulnerabilityTypes +from faraday_plugins.plugins.plugins_utils import its_cwe + + +class AcunetixXmlParser: + """ + The objective of this class is to parse an xml file generated by + the acunetix tool. + + TODO: Handle errors. + TODO: Test acunetix output version. Handle what happens if + the parser doesn't support it. + TODO: Test cases. + + @param acunetix_xml_filepath A proper xml generated by acunetix + """ + + def __init__(self, xml_output): + + tree = self.parse_xml(xml_output) + self.acunetix = Acunetix(tree) + + @staticmethod + def parse_xml(xml_output): + """ + Open and parse an xml file. + + TODO: Write custom parser to just read the nodes that we need instead + of reading the whole file. + + @return xml_tree An xml tree instance. None if error. + """ + + try: + parser = etree.XMLParser(recover=True) + tree = etree.fromstring(xml_output, parser=parser) + except SyntaxError as err: + print("SyntaxError: %s. %s", err, xml_output) + return None + + return tree + + +class AcunetixJsonPlugin(PluginJsonFormat): + + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) + self.id = "Acunetix_Json" + self.name = "Acunetix JSON Output Plugin" + self.plugin_version = "0.1" + self.version = "9" + self.json_keys = {'export'} + self.framework_version = "1.0.0" + self._temp_file_extension = "json" + """ + Example plugin to parse acunetix output. + """ + + def parseOutputString(self, output): + """ + This method will discard the output the shell sends, it will read it + from the xml where it expects it to be present. + + NOTE: if 'debug' is true then it is being run from a test case and the + output being sent is valid. + """ + parser = AcunetixJsonParser(loads(output)) + for site in parser.export.scans: + self.new_structure(site) + + def new_structure(self, site: Scan): + start_url = site.info.host + if site.info.start_url: + start_url = site.info.start_url + url_data = urlsplit(start_url) + site_ip = self.resolve_hostname(url_data.hostname) + ports = '443' if (url_data.scheme == 'https') else '80' + vulnerability_type = {i.vt_id: i for i in site.vul_types} + h_id = self.createAndAddHost(site_ip, None, hostnames=[url_data.hostname]) + s_id = self.createAndAddServiceToHost( + h_id, + "http", + "tcp", + ports=[ports], + version=None, + status='open') + for i in site.vulnerabilities: + vul_type = vulnerability_type[i.info.vt_id] + cwe = its_cwe(vul_type.tags) + self.create_vul(i, vul_type, h_id, s_id, url_data, cwe) + + def create_vul(self, vul: Vulnerabilities, vul_type: VulnerabilityTypes, h_id, s_id, url_data, cwe): + cvss3 = { + 'vector_string': vul_type.cvss3_vector + } + cvss2 = { + 'vector_string': vul_type.cvss2_vector + } + self.createAndAddVulnWebToService( + h_id, + s_id, + vul_type.name, + vul_type.description, + website=url_data.hostname, + severity=vul_type.severity, + resolution=vul_type.recommendation, + request=vul.info.request, + response=vul.response, + cwe=cwe, + cvss3=cvss3, + cvss2=cvss2 + ) + + @staticmethod + def get_domain(scan: Scan): + url = scan.start_url + if not url.startswith('http'): + url = f'http://{url}' + url_data = urlsplit(url) + if not url_data.scheme: + url_data = urlsplit(scan.crawler.start_url_attr) + return url_data + + +def createPlugin(*args, **kwargs): + return AcunetixJsonPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/amap/__init__.py b/faraday_plugins/plugins/repo/amap/__init__.py index ea531e17..625a6e25 100644 --- a/faraday_plugins/plugins/repo/amap/__init__.py +++ b/faraday_plugins/plugins/repo/amap/__init__.py @@ -4,4 +4,4 @@ See the file 'doc/LICENSE' for the license information """ -# I'm Py3 \ No newline at end of file + diff --git a/faraday_plugins/plugins/repo/amap/plugin.py b/faraday_plugins/plugins/repo/amap/plugin.py index abd01f47..c97b93f9 100644 --- a/faraday_plugins/plugins/repo/amap/plugin.py +++ b/faraday_plugins/plugins/repo/amap/plugin.py @@ -4,24 +4,18 @@ See the file 'doc/LICENSE' for the license information """ import argparse -import random +import re import shlex -import tempfile - -from faraday_plugins.plugins.plugin import PluginBase import socket -import re -import os - -from faraday_plugins.plugins.plugins_utils import resolve_hostname +from faraday_plugins.plugins.plugin import PluginBase class AmapPlugin(PluginBase): """ Example plugin to parse amap output.""" - def __init__(self): - super().__init__() + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) self.id = "Amap" self.name = "Amap Output Plugin" self.plugin_version = "0.0.3" @@ -31,6 +25,7 @@ def __init__(self): self._command_regex = re.compile(r'^(amap|sudo amap)\s+.*?') self._use_temp_file = True self._hosts = [] + self.args = None def parseOutputString(self, output): services = {} @@ -107,9 +102,6 @@ def get_ip_6(self, host, port=0): return ip6[0][4][0] - def setHost(self): - pass - def processCommandString(self, username, current_path, command_string): """ Adds the -m parameter to get machine readable output. @@ -136,18 +128,14 @@ def processCommandString(self, username, current_path, command_string): cmd.remove("-6") cmd.insert(1, "-6") - args = None if len(cmd) > 4: try: - args, unknown = parser.parse_known_args(cmd) + self.args, unknown = parser.parse_known_args(cmd) except SystemExit: pass - self.args = args return final -def createPlugin(): - return AmapPlugin() - -# I'm Py3 +def createPlugin(*args, **kwargs): + return AmapPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/appscan/__init__.py b/faraday_plugins/plugins/repo/appscan/__init__.py index 00dc0fca..d417d2e7 100644 --- a/faraday_plugins/plugins/repo/appscan/__init__.py +++ b/faraday_plugins/plugins/repo/appscan/__init__.py @@ -4,4 +4,4 @@ See the file 'doc/LICENSE' for the license information """ -# I'm Py3 \ No newline at end of file + diff --git a/faraday_plugins/plugins/repo/appscan/plugin.py b/faraday_plugins/plugins/repo/appscan/plugin.py index 572c61d9..4a1fa584 100644 --- a/faraday_plugins/plugins/repo/appscan/plugin.py +++ b/faraday_plugins/plugins/repo/appscan/plugin.py @@ -1,386 +1,294 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- +from urllib.parse import urlparse + from faraday_plugins.plugins.plugin import PluginXMLFormat -from faraday_plugins.plugins.plugins_utils import resolve_hostname -try: - import xml.etree.cElementTree as ET -except ImportError: - import xml.etree.ElementTree as ET +import xml.etree.ElementTree as ET -__author__ = "Alejando Parodi, Ezequiel Tavella, Blas Moyano" -__copyright__ = "Copyright (c) 2015, Infobyte LLC" -__credits__ = ["Alejando Parodi", "Ezequiel Tavella"] +__author__ = "Nicolas Rebagliati" +__copyright__ = "Copyright (c) 2021, Infobyte LLC" +__credits__ = ["Nicolas Rebagliati"] __license__ = "" __version__ = "1.0" -__maintainer__ = "Ezequiel Tavella" +__maintainer__ = "Nicolas Rebagliati" __status__ = "Development" class AppScanParser: - def __init__(self, xml_output): - self.tree = self.parse_xml(xml_output) - if self.tree: - self.operating_system = self.tree.attrib['technology'] - url_group = [tags.tag for tags in self.tree] - check_url = True if 'url-group' in url_group else False - if check_url: - self.urls = self.get_urls_info(self.tree.find('url-group')) - else: - self.urls = None - self.layout = self.get_layout_info(self.tree.find('layout')) - self.item = self.get_issue_type(self.tree.find('issue-type-group')) - self.name_scan = self.get_issue_data(self.tree.find('advisory-group')) - self.host_data = None if self.tree.find('scan-configuration/scanned-hosts/item') is None else \ - self.get_scan_conf_data(self.tree.find('scan-configuration/scanned-hosts/item')) - self.issue_group = self.get_info_issue_group(self.tree.find("issue-group")) - self.fix_recomendation = self.get_fix_info(self.tree.find('fix-recommendation-group')) - else: - self.tree = None - - def parse_xml(self, xml_output): + def __init__(self, xml_output): + tree = self.parse_xml(xml_output) + if tree: + self.scan_type = tree.attrib['technology'] + self.issue_types = self.get_issue_types(tree.find('issue-type-group')) + if self.scan_type == "SAST": + self.fixes = self.get_fixes(tree.find("fix-group-group")) + self.issues = self.get_sast_issues(tree.find("issue-group")) + elif self.scan_type == "DAST": + self.hosts = self.get_hosts(tree.find('scan-configuration/scanned-hosts')) + self.remediations = self.get_remediations(tree.find('remediation-group')) + self.entities = self.get_entity_groups(tree.find('entity-group')) + self.issues = self.get_dast_issues(tree.find("issue-group")) + + @staticmethod + def parse_xml(xml_output): try: tree = ET.fromstring(xml_output) except SyntaxError as err: - print('SyntaxError In xml: %s. %s' % (err, xml_output)) + print(f'SyntaxError In xml: {err}. {xml_output}') return None return tree - def get_fix_info(self, tree): - list_fix = [] + @staticmethod + def get_fixes(tree): + fixes = {} for item in tree: - text_info_join = [] - if item.find("general/fixRecommendation"): - for text_tag in tree.findall('text'): - text_info_join += text_tag.text - info_fix = { - "id": item.attrib.get('id', None), - "text": text_info_join - } - list_fix.append(info_fix) - return list_fix - - def get_info_issue_group(sef,tree): - data_res_req = [] + fix_id = item.attrib['id'] + library = item.find("LibraryName").text if item.find("LibraryName") else "" + location = item.find("Location").text if item.find("Location") else "" + fixes[fix_id] = {"library": library, "location": location} + return fixes + + @staticmethod + def get_issue_types(tree): + issue_types = {} + for item in tree: + type_id = item.attrib['id'] + name = item.find("name").text + issue_types[type_id] = name + cve = item.find("cve") + if cve and cve.text: + issue_types[f"{type_id}_cve"] = cve.text + return issue_types + + @staticmethod + def get_remediations(tree): + remediations = {} + for item in tree: + remediation_id = item.attrib['id'] + name = item.find("name").text + remediations[remediation_id] = name + return remediations + + @staticmethod + def get_hosts(tree): + hosts = {} + for item in tree: + host = item.find("host").text + port = item.find("port").text + operating_system = item.find("operating-system").text + if "unknown" in operating_system.lower(): + operating_system = "unknown" + web_server = item.find("web-server").text + application_server = item.find("application-server").text + service_name = f"{web_server} ({application_server})" + host_key = f"{host}-{port}" + hosts[host_key] = {"host": host, "port": port, "os": operating_system, + "service_name": service_name} + return hosts + + @staticmethod + def get_entity_groups(tree): + entity_groups = {} for item in tree: - if item.find("variant-group/item/issue-information"): - resp = item.find("variant-group/item/issue-information").text + entity_id = item.attrib['id'] + name = item.find("name").text + url = item.find("url-name").text + type = item.find("entity-type").text + url_data = urlparse(url) + website = f"{url_data.scheme}://{url_data.netloc}" + host = url_data.netloc.split(":")[0] + if url_data.port: + port = url_data.port else: - resp = "Not Response" - - json_res_req = { - "request": "Not request" if item.find("variant-group/item/test-http-traffic") is None else - item.find("variant-group/item/test-http-traffic").text, - "response": resp, - "location": "Not Location" if item.find("location") is None else item.find("location").text, - "source_file": "0.0.0.0" if item.find("source-file") is None else item.find("source-file").text, - "line": 0 if item.find("line") is None else item.find("line").text, - "id_item": item.attrib.get('id', 'Not id item'), - "severity": 0 if item.find("severity-id") is None else item.find("severity-id").text, - "cvss": "No cvss" if item.find("cvss-score") is None else item.find("cvss-score").text, - "cwe": "No cwe" if item.find("cwe") is None else item.find("cwe").text, - "remediation": "No remedation" if item.find("remediation/ref") is None else item.find( - "remediation/ref").text, - "advisory": "No advisory" if item.find("advisory/ref") is None else item.find("advisory/ref").text, - "url_id": "No url id" if item.find("url/ref") is None else item.find("url/ref").text, - "id_adv": "Not info" if item.find("issue-type/ref") is None else item.find("issue-type/ref").text - } - - data_res_req.append(json_res_req) - return data_res_req - - def get_layout_info(self, tree): - info_layout = { - "name": "Not info" if tree.find("application-name") is None else tree.find("application-name").text, - "date": "Not info" if tree.find("report-date") is None else tree.find("report-date").text, - "details": f'Departamento: {"Not info" if tree.find("department") is None else tree.find("department").text}' - f'Compania: {"Not info" if tree.find("company") is None else tree.find("company").text}' - f'Titulo Reporte: {"Not info" if tree.find("title") is None else tree.find("title").text}', - "nro_issues": None if tree.find("total-issues-in-application") is None else tree.find("total-issues-in-application").text, - } - return info_layout - - def get_issue_type(self, tree): - list_item = [] + if url_data.scheme == "http": + port = 80 + elif url_data.scheme == "https": + port = 443 + path = url_data.path + entity_groups[entity_id] = {"name": name, "host": host, "port": port, "url": url, + "type": type, "website": website, "path": path} + return entity_groups + + def get_dast_issues(self, tree): + dast_issues = [] for item in tree: - severity = item.attrib.get('severity-id', None) - if severity is None: - severity = item.attrib.get('maxIssueSeverity', None) - - item_info = { - "id": item.attrib.get('id', None), - "name": item.find("name").text, - "severity_id": severity, - "severity": item.attrib.get('severity', None), - "cwe": "Not info" if item.find("cme") is None else item.find("cwe").text, - "xfid": "Not info" if item.find("xfid") is None else item.find("xfid").text, - "advisory": "Not info" if item.find("advisory/ref") is None else item.find("advisory/ref").text + if not self.entities: + entity = list(self.hosts.values())[0] + host = entity.get("host").replace('\\', '/') + port = entity.get("port") + else: + entity = self.entities[item.find("entity/ref").text] + host = entity["host"].replace('\\','/') + port = entity["port"] + name = self.issue_types[item.find("issue-type/ref").text] + severity = 0 if item.find("severity-id") is None else int(item.find("severity-id").text) + if severity > 4: + severity = 4 + resolution = self.remediations[item.find("remediation/ref").text] + description = "" if item.find("variant-group/item/reasoning") is None \ + else item.find("variant-group/item/reasoning").text + request = "" if item.find("variant-group/item/test-http-traffic") is None \ + else item.find("variant-group/item/test-http-traffic").text + response = "" if item.find("variant-group/item/issue-information/testResponseChunk") is None \ + else item.find("variant-group/item/issue-information/testResponseChunk").text + cvss2 = item.find('cvss-score').text if item.find("cvss-score") is not None else None + cvss2_base_vector = item.find('cvss-vector/base-vector').text if item.find('cvss-vector/base-vector') \ + is not None else None + cvss_temporal_vector = None if item.find('cvss-vector/temporal-vector') is None \ + else f"CVSS-temporal-vector: {item.find('cvss-vector/temporal-vector').text}" + cvss_environmental_vector = None if item.find('cvss-vector/environmental-vector') is None \ + else f"CVSS-environmental-vector: {item.find('cvss-vector/environmental-vector').text}" + cwe = None if item.find("cwe") is None else item.find('cwe').text + if item.attrib.get("cve"): + cve = None if item.find("variant-group/item/issue-information/display-name") is None \ + else item.find('variant-group/item/issue-information/display-name').text + if "CVE" not in cve: + cve = f"CVE-{cve}" + cve_url = item.attrib["cve"] + else: + cve = None + cve_url = None + if cve is None: + cve = self.issue_types.get(f"{item.find('issue-type/ref').text}_cve", None) + host_key = f"{host}-{port}" + issue_data = { + "host": host, + "port": port, + "os": self.hosts[host_key]["os"], + "service_name": self.hosts[host_key]["service_name"], + "name": name, + "severity": severity, + "desc": description, + "ref": [], + "resolution": resolution, + "request": request, + "response": response, + "website": entity.get('website'), + "path": entity.get('path'), + "cve": [], + "cwe": [], + "cvss2": {} } - list_item.append(item_info) - - return list_item - - def get_issue_data(self, tree): - list_item_data = [] - item_data = {} + if cve: + issue_data["cve"].append(cve) + issue_data["desc"] += cve + if cve_url: + issue_data["ref"].append(cve_url) + if cwe: + issue_data["cwe"].append(f"CWE-{cwe}") + if cvss2_base_vector: + issue_data["cvss2"]["vector_string"] = cvss2_base_vector + if cvss_temporal_vector: + issue_data["ref"].append(cvss_temporal_vector) + if cvss_environmental_vector: + issue_data["ref"].append(cvss_environmental_vector) + dast_issues.append(issue_data) + return dast_issues + + def get_sast_issues(self, tree): + sast_issues = [] for item in tree: - for adivisory in item: - if adivisory.find("cwe/link"): - cwe = adivisory.find("cwe/link").text - else: - cwe = "Not Response" - - if adivisory.find("xfid/link"): - xfid = adivisory.find("xfid/link").text - else: - xfid = "Not Response" - - item_data = { - "id": item.attrib.get('id', None), - "name": "Not info" if adivisory.find("name") is None else adivisory.find("name").text, - "description": "Not info" if adivisory.find("testDescription") is None else - adivisory.find("testDescription").text, - "threatClassification": { - "name": "Not info" if adivisory.find("threatClassification/name") is None else - adivisory.find("threatClassification/name").text, - "reference": "Not info" if adivisory.find("threatClassification/reference") is None else - adivisory.find("threatClassification/reference").text, - }, - "testTechnicalDescription": "Not info" if adivisory.find("testTechnicalDescription") is None else - self.get_parser(adivisory.find("testTechnicalDescription")), - "testTechnicalDescriptionMixed": "Not info" if adivisory.find("testTechnicalDescriptionMixed") is None else - self.get_parser(adivisory.find("testTechnicalDescriptionMixed")), - - "testDescriptionMixed": "Not info" if adivisory.find("testDescriptionMixed") is None else - self.get_parser(adivisory.find("testDescriptionMixed")), - "causes": "Not info" if adivisory.find("causes/cause") is None else - adivisory.find("causes/cause").text, - "securityRisks": "Not info" if adivisory.find("securityRisks/securityRisk") is None else - adivisory.find("securityRisks/securityRisk").text, - "affectedProducts": "Not info" if adivisory.find("affectedProducts/affectedProduct") is None else - adivisory.find("affectedProducts/affectedProduct").text, - "cwe": cwe, - "xfid": xfid, - "references": "Not info" if adivisory.find("references") is None else - self.get_parser(adivisory.find("references")), - "fixRecommendations": "Not info" if adivisory.find("fixRecommendations/fixRecommendation") is None else - self.get_parser(adivisory.find("fixRecommendations/fixRecommendation")) - } - list_item_data.append(item_data) - return list_item_data - - def get_parser(self, tree): - text_join = "" - code_join = "" - link_join = "" - - if tree.tag == 'testTechnicalDescription': - - for text_info in tree.findall('text'): - text_join += text_info.text - - for code_info in tree.findall('code'): - text_join += code_info.text - - tech_data = { - "text": text_join, - "code": code_join - } - - elif tree.tag == 'testDescriptionMixed': - - for text_info in tree.findall('p'): - text_join += text_info.text - - for code_info in tree.findall('li'): - text_join += code_info.text - - tech_data = { - "text": text_join, - "items": code_join - } - - elif tree.tag == 'testTechnicalDescriptionMixed': - - for text_info in tree.findall('p'): - text_join += text_info.text - - tech_data = { - "text": text_join, - } - - elif tree.tag == 'references': - for text_info in tree.findall('text'): - text_join += "no info " if text_info.text is None else text_info.text - - for link_info in tree.findall('link'): - link_join += "no info " if link_info.text is None else link_info.text - link_join += link_info.attrib.get('target', 'not target') - - tech_data = { - "text": text_join, - "Link": link_join - } - - elif tree.tag == 'fixRecommendation': - for text_info in tree.findall('text'): - text_join += "no info " if text_info.text is None else text_info.text - - for link_info in tree.findall('link'): - link_join += "no info " if link_info.text is None else link_info.text - link_join += link_info.attrib.get('target', 'not target') - - tech_data = { - "text": text_join, - "link": link_join - } - - return tech_data - - def get_urls_info(self, tree): - list_url = [] - for url in tree: - url_info = { - "id_item": url.attrib.get('id', 'Not id item'), - "id": "Not info" if url.find("issue-type") is None else url.find("issue-type").text, - "url": "Not info" if url.find("name") is None else url.find("name").text, + name = self.issue_types[item.find("issue-type/ref").text] + source_file = item.attrib["filename"].replace('\\', '/') + severity = 0 if item.find("severity-id") is None else int(item.find("severity-id").text) + if severity > 4: + severity = 4 + description = "No description provided" \ + if item.find("fix/item/general/text") is None else item.find("fix/item/general/text").text + resolution = "" if item.find("variant-group/item/issue-information/fix-resolution-text") is None \ + else item.find("variant-group/item/issue-information/fix-resolution-text").text + fix_id = item.attrib.get("fix-group-id") + if fix_id: + fix = self.fixes[fix_id] + resolution = f"{resolution}\nLibrary: {fix['library']}\nLocation: {fix['location']}" + cvss2 = item.find('cvss-score').text if item.find("cvss-score") else None + cvss2_base_vector = None if item.find('cvss-vector/base-vector') is None \ + else item.find('cvss-vector/base-vector').text + cvss_temporal_vector = None if item.find('cvss-vector/temporal-vector') is None \ + else f"CVSS-temporal-vector: {item.find('cvss-vector/temporal-vector').text}" + cvss_environmental_vector = None if item.find('cvss-vector/environmental-vector') is None \ + else f"CVSS-environmental-vector: {item.find('cvss-vector/environmental-vector').text}" + cwe = None if item.find("cwe/ref") is None else item.find('cwe/ref').text + if item.attrib.get("cve"): + cve = None if item.find("variant-group/item/issue-information/display-name") is None \ + else item.find('variant-group/item/issue-information/display-name').text + if cve and "CVE" not in cve: + cve = f"CVE-{cve}" + cve_url = item.attrib["cve"] + else: + cve = None + cve_url = None + issue_data = { + "source_file": source_file, + "name": name, + "severity": severity, + "desc": description, + "ref": [], + "resolution": resolution, + "cve": [], + "cwe": [], + "cvss2": {} } - list_url.append(url_info) - - return list_url - def get_scan_conf_data(self, host_info): - info_host = { - "host": "Not info" if host_info.find("host") is None else host_info.find("host").text, - "port": "Not info" if host_info.find("port") is None else host_info.find("port").text, - "os": "Not info" if host_info.find("operating-system") is None else host_info.find("operating-system").text, - "webserver": "Not info" if host_info.find("web-server") is None else host_info.find("web-server").text, - "appserver": "Not info" if host_info.find("application-server") is None else host_info.find("application-server").text, - } - return info_host + if cve_url: + issue_data["ref"].append(cve_url) + if cwe: + issue_data["cwe"].append(f"CWE-{cwe}") + if cvss2_base_vector: + issue_data["cvss2"]['vector_string'] = cvss2_base_vector + if cvss_temporal_vector: + issue_data["ref"].append(cvss_temporal_vector) + if cvss_environmental_vector: + issue_data["ref"].append(cvss_environmental_vector) + if cve: + issue_data["cve"].append(cve) + issue_data["desc"] += f"\nCVE: {cve}" + # Build data + data = [] + if item.attrib.get("caller"): + data.append(f"Caller: {item.attrib.get('caller')}") + if item.find("variant-group/item/issue-information/method-signature") is not None: + data.append(f"Method: {item.find('variant-group/item/issue-information/method-signature').text}") + if item.find("variant-group/item/issue-information/method-signature2") is not None: + data.append(f"Location: {item.find('variant-group/item/issue-information/method-signature2').text}") + issue_data['data'] = "\n".join(data) + issue_data['desc'] += "\n".join(data) + sast_issues.append(issue_data) + return sast_issues class AppScanPlugin(PluginXMLFormat): - def __init__(self): - super().__init__() + + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) self.identifier_tag = "xml-report" self.id = 'Appscan' self.name = 'Appscan XML Plugin' self.plugin_version = '0.0.1' self.version = '1.0.0' self.framework_version = '1.0.0' - self.options = None - self.protocol = None - self.port = '80' - self.address = None def parseOutputString(self, output): parser = AppScanParser(output) - layout = parser.layout - operating_system = parser.operating_system - host_data = parser.host_data - urls = parser.urls - item = parser.item - name_scan = parser.name_scan - issues = parser.issue_group - recomendation = parser.fix_recomendation - - if operating_system == 'DAST': - host_id = self.createAndAddHost(resolve_hostname(host_data['host']), os=host_data['os'], - hostnames=[host_data['host']], description=layout['details']) - - service_id = self.createAndAddServiceToHost(host_id, host_data['host'], ports=host_data['port'], - protocol="tcp?HTTP", - description=f'{host_data["webserver"]} - {host_data["appserver"]}') - if layout['nro_issues'] is None: - nro_check = True - - else: - nro_check = False - check_issues = [] - - for issue in issues: - id = f"{issue['url_id']}{issue['advisory']}" - if id in check_issues and nro_check is True: - check_issues.append(id) - else: - check_issues.append(id) - for info in name_scan: - if info['id'] == issue['advisory']: - vuln_name = info['name'] - vuln_desc = info['description'] - resolution = "" - if 'text' in info['fixRecommendations']: - resolution += info['fixRecommendations']['text'] - if 'link' in info['fixRecommendations']: - resolution += info['fixRecommendations']['link'] - - vuln_data = f'xfix: {info["xfid"]} cme: {info["cwe"]}' - - for url in urls: - if url['id'] == issue['advisory']: - url_name = url['url'] - elif url['id_item'] == issue['id_item']: - url_name = url['url'] - else: - url_name = None - - for rec in recomendation: - if rec['id'] == issue['advisory']: - vuln_data = f'{vuln_data}, {rec["text"]} ' - - ref = f'cwe: {issue["cwe"]} cvss: {issue["cvss"]} remediation: {issue["remediation"]}' - self.createAndAddVulnWebToService(host_id=host_id, service_id=service_id, name=vuln_name, - desc=vuln_desc, severity=issue['severity'], ref=[ref], - website=host_data['host'], request=issue['request'], - response=issue['response'], method=issue['request'], - resolution=resolution, data=vuln_data, path=url_name) - - elif operating_system == 'SAST': - for info_loc_source in issues: - source_file = info_loc_source['source_file'] - host_id = self.createAndAddHost(source_file, os=operating_system) - ref = f'{info_loc_source["location"]} - {info_loc_source["line"]}' - for vuln_data in name_scan: - if vuln_data['id'] == info_loc_source["id_adv"]: - desc = f'desc: {vuln_data["description"]} DescMix {vuln_data["testDescriptionMixed"]}' - - resolution = f'Fix Recomendarion {vuln_data["fixRecommendations"]}' \ - f' - TestTecnical {vuln_data["testTechnicalDescriptionMixed"]}' - - self.createAndAddVulnToHost(host_id=host_id, - name=vuln_data['name'], - desc=desc, - ref=[ref], - severity=info_loc_source['severity'], - resolution=resolution, - data=f'xfix: {vuln_data["xfid"]} cme: {vuln_data["cwe"]}', - run_date=None, - ) - else: - host_id = self.createAndAddHost(layout['name'], os=operating_system) - for vulnserv in name_scan: - for sev in item: - if sev['id'] == vulnserv['id']: - info_severity = sev['severity_id'] - if vulnserv['description'] is None: - desc = "" - else: - desc = vulnserv['description'] - - text_info = vulnserv['fixRecommendations']['text'] \ - if 'text' in vulnserv['fixRecommendations'] else "Not Text" - - link_info = vulnserv['fixRecommendations']['link'] \ - if 'link' in vulnserv['fixRecommendations'] else "Not Info" - - resolution = f"Text:{text_info}. Link: {link_info}." - - self.createAndAddVulnToHost(host_id=host_id, name=vulnserv['name'], desc=desc, - severity=info_severity, resolution=resolution, - data=f'xfix: {vulnserv["xfid"]} cme: {vulnserv["cwe"]}', run_date=None) - - -def createPlugin(): - return AppScanPlugin() + scan_type = parser.scan_type + + if scan_type == 'DAST': + for issue in parser.issues: + host = issue.pop("host") + port = issue.pop("port") + service_name = issue.pop("service_name") + ip = self.resolve_hostname(host) + host_os = issue.pop("os") + host_id = self.createAndAddHost(ip, hostnames=host, os=host_os) + service_id = self.createAndAddServiceToHost(host_id, service_name, ports=port) + self.createAndAddVulnWebToService(host_id=host_id, service_id=service_id, **issue) + + elif scan_type == 'SAST': + for issue in parser.issues: + source_file = issue.pop('source_file') + host_id = self.createAndAddHost(source_file) + self.createAndAddVulnToHost(host_id=host_id, **issue) + + +def createPlugin(*args, **kwargs): + return AppScanPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/appscan_csv/__init__.py b/faraday_plugins/plugins/repo/appscan_csv/__init__.py new file mode 100644 index 00000000..f70f2304 --- /dev/null +++ b/faraday_plugins/plugins/repo/appscan_csv/__init__.py @@ -0,0 +1,6 @@ +""" +Faraday Penetration Test IDE +Copyright (C) 2017 Infobyte LLC (http://www.infobytesec.com/) +See the file 'doc/LICENSE' for the license information + +""" diff --git a/faraday_plugins/plugins/repo/appscan_csv/plugin.py b/faraday_plugins/plugins/repo/appscan_csv/plugin.py new file mode 100644 index 00000000..96536dc4 --- /dev/null +++ b/faraday_plugins/plugins/repo/appscan_csv/plugin.py @@ -0,0 +1,102 @@ +""" +Faraday Penetration Test IDE +Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/) +See the file 'doc/LICENSE' for the license information + +""" + +from faraday_plugins.plugins.plugin import PluginCSVFormat +from itertools import islice +import csv +from dateutil.parser import parse + +__author__ = "Erodriguez" +__copyright__ = "Copyright (c) 2019, Infobyte LLC" +__credits__ = ["Erodriguez"] +__license__ = "" +__version__ = "1.0.0" +__maintainer__ = "Erodriguez" +__email__ = "erodriguez@infobytesec.com" +__status__ = "Development" + + +class Appscan_CSV_Plugin(PluginCSVFormat): + """ + Example plugin to parse Appscan output. + """ + + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) + self.csv_headers = {'HCL AppScan on Cloud'} + self.id = "Appscan_CSV" + self.name = "Appscan CSV Output Plugin" + self.plugin_version = "0.0.1" + self.version = "0.0.1" + self.framework_version = "1.0.1" + + def _parse_filename(self, filename): + with open(filename) as output: + self.parseOutputString(islice(output, 15, None)) + + def parseOutputString(self, output): + try: + reader = csv.DictReader(output) + except: + print("Error parser output") + return None + + for row in reader: + #Skip Fix Group + if row["Issue Id"] == "Fix Group Attributes:": + break + path = row['Source File'] + if path == "": + path = row['Location'] + try: + run_date = parse(row['Date Created']) + except: + run_date = None + name = row["Issue Type Name"] + references = [] + if row["Cwe"]: + references.append(f"CWE-{row['Cwe']}") + if row["Cve"]: + references.append(row["Cve"]) + + data = [] + if row['Security Risk']: + data.append(f"Security Risk: {row['Security Risk']}") + desc = [row['Description']] + if row['Cve']: + desc.append(f"Cve: {row['Cve']}") + if row['Line']: + desc.append(f"Line: {row['Line']}") + if row['Cause']: + desc.append(f"Cause: {row['Cause']}") + if row['Remediation']: + desc.append(f"Resolution: {row['Resolution']}") + if row['Threat Class']: + desc.append(f"Threat Class: {row['Threat Class']}") + if row['Security Risk']: + desc.append(f"Security Risk: {row['Security Risk']}") + if row['Calling Method']: + desc.append(f"Calling Method: {row['Calling Method']}") + if row['Location']: + desc.append(f"Vulnerability Line: {row['Location']}") + + h_id = self.createAndAddHost(name=path) + self.createAndAddVulnToHost( + h_id, + name=name, + desc=" \n".join(desc), + resolution=row['Remediation'], + external_id=row['Issue Id'], + cve=row['Cve'], + run_date=run_date, + severity=row["Severity"], + ref=references, + data=" \n".join(data) + ) + +def createPlugin(*args, **kargs): + return Appscan_CSV_Plugin(*args, **kargs) diff --git a/faraday_plugins/plugins/repo/appspider/plugin.py b/faraday_plugins/plugins/repo/appspider/plugin.py index d3ebd48a..cbaf6ef0 100644 --- a/faraday_plugins/plugins/repo/appspider/plugin.py +++ b/faraday_plugins/plugins/repo/appspider/plugin.py @@ -1,16 +1,13 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Faraday Penetration Test IDE Copyright (C) 2016 Infobyte LLC (http://www.infobytesec.com/) See the file 'doc/LICENSE' for the license information """ -from faraday_plugins.plugins.plugin import PluginXMLFormat +import xml.etree.ElementTree as ET from datetime import datetime -try: - import xml.etree.cElementTree as ET -except ImportError: - import xml.etree.ElementTree as ET + +from faraday_plugins.plugins.plugin import PluginXMLFormat __author__ = 'Blas Moyano' __copyright__ = 'Copyright 2020, Faraday Project' @@ -25,22 +22,24 @@ def __init__(self, xml_output): self.tree = self.parse_xml(xml_output) if self.tree: self.vuln_list = self.tree.find('VulnList') - self.name_scan = self.tree.find('ScanName').text + self.name_scan = self.tree.findtext('ScanName') else: self.tree = None - def parse_xml(self, xml_output): + @staticmethod + def parse_xml(xml_output): try: tree = ET.fromstring(xml_output) except SyntaxError as err: - print('SyntaxError In xml: %s. %s' % (err, xml_output)) + print(f'SyntaxError In xml: {err}. {xml_output}') return None return tree class AppSpiderPlugin(PluginXMLFormat): - def __init__(self): - super().__init__() + + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) self.identifier_tag = ["VulnSummary"] self.id = 'AppSpider' self.name = 'AppSpider XML Output Plugin' @@ -71,22 +70,23 @@ def parseOutputString(self, output): data_info = [] for vulns in parser.vuln_list: - vuln_name = vulns.find('VulnType').text - vuln_desc = vulns.find('Description').text - vuln_ref = vulns.find('VulnUrl').text - severity = vulns.find('AttackScore').text - vuln_resolution = vulns.find('Recommendation').text - vuln_external_id = vulns.find('DbId').text - vuln_run_date = vulns.find('ScanDate').text - data_info.append(vulns.find('AttackClass').text) - data_info.append(vulns.find('CweId').text) - data_info.append(vulns.find('CAPEC').text) - data_info.append(vulns.find('DISSA_ASC').text) - data_info.append(vulns.find('OWASP2007').text) - data_info.append(vulns.find('OWASP2010').text) - data_info.append(vulns.find('OWASP2013').text) - data_info.append(vulns.find('OVAL').text) - data_info.append(vulns.find('WASC').text) + + vuln_name = vulns.findtext('VulnType') + vuln_desc = vulns.findtext('Description') + vuln_ref = vulns.findtext('VulnUrl') + severity = vulns.findtext('AttackScore') + vuln_resolution = vulns.findtext('Recommendation') + vuln_external_id = vulns.findtext('DbId') + vuln_run_date = vulns.findtext('ScanDate') + data_info.append(vulns.findtext('AttackClass')) + cwe = ["CWE-" + vulns.findtext('CweId')] if vulns.findtext('CweId') else [] + data_info.append(vulns.findtext('CAPEC')) + data_info.append(vulns.findtext('DISSA_ASC')) + data_info.append(vulns.findtext('OWASP2007')) + data_info.append(vulns.findtext('OWASP2010')) + data_info.append(vulns.findtext('OWASP2013')) + data_info.append(vulns.findtext('OVAL')) + data_info.append(vulns.findtext('WASC')) if severity == '1-Informational': severity = 0 @@ -99,9 +99,9 @@ def parseOutputString(self, output): else: severity = 10 - str_data = f'AttackClass: {data_info[0]}, CweId: {data_info[1]}, CAPEC: {data_info[2]}, ' \ - f'DISSA_ASC: {data_info[3]}, OWASP2007: {data_info[4]}, OWASP2010: {data_info[5]}, ' \ - f'OWASP2013: {data_info[6]}, OVAL: {data_info[7]}, WASC: {data_info[8]}' + str_data = f'AttackClass: {data_info[0]}, CAPEC: {data_info[1]}, ' \ + f'DISSA_ASC: {data_info[2]}, OWASP2007: {data_info[3]}, OWASP2010: {data_info[4]}, ' \ + f'OWASP2013: {data_info[5]}, OVAL: {data_info[6]}, WASC: {data_info[7]}' if vuln_run_date is None: vuln_run_date = None @@ -110,8 +110,8 @@ def parseOutputString(self, output): self.createAndAddVulnToHost(host_id=host_id, name=vuln_name, desc=vuln_desc, ref=[vuln_ref], severity=severity, resolution=vuln_resolution, run_date=vuln_run_date, - external_id=vuln_external_id, data=str_data) + external_id=vuln_external_id, data=str_data, cwe=cwe) -def createPlugin(): - return AppSpiderPlugin() +def createPlugin(*args, **kwargs): + return AppSpiderPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/arachni/__init__.py b/faraday_plugins/plugins/repo/arachni/__init__.py index ea531e17..625a6e25 100644 --- a/faraday_plugins/plugins/repo/arachni/__init__.py +++ b/faraday_plugins/plugins/repo/arachni/__init__.py @@ -4,4 +4,4 @@ See the file 'doc/LICENSE' for the license information """ -# I'm Py3 \ No newline at end of file + diff --git a/faraday_plugins/plugins/repo/arachni/plugin.py b/faraday_plugins/plugins/repo/arachni/plugin.py index 5b7dd838..a265c6a5 100755 --- a/faraday_plugins/plugins/repo/arachni/plugin.py +++ b/faraday_plugins/plugins/repo/arachni/plugin.py @@ -3,16 +3,12 @@ Copyright (C) 2016 Infobyte LLC (http://www.infobytesec.com/) See the file 'doc/LICENSE' for the license information """ +import os import re +import xml.etree.ElementTree as ET from urllib.parse import urlparse -import os -from faraday_plugins.plugins.plugin import PluginXMLFormat -from faraday_plugins.plugins.plugins_utils import resolve_hostname -try: - import xml.etree.cElementTree as ET -except ImportError: - import xml.etree.ElementTree as ET +from faraday_plugins.plugins.plugin import PluginXMLFormat __author__ = 'Ezequiel Tavella' __copyright__ = 'Copyright 2016, Faraday Project' @@ -24,12 +20,12 @@ class ArachniXmlParser: def __init__(self, xml_output): + self.tree = self.parse_xml(xml_output) if self.tree: self.issues = self.getIssues(self.tree) self.plugins = self.getPlugins(self.tree) self.system = self.getSystem(self.tree) - else: self.system = None self.issues = None @@ -61,7 +57,7 @@ def getSystem(self, tree): return System(system_tree, True) -class Issue(): +class Issue: def __init__(self, issue_node): self.node = issue_node @@ -77,6 +73,21 @@ def __init__(self, issue_node): self.parameters = self.getParameters() self.request = self.getRequest() self.response = self.getResponse() + self.data = self.getData() + + def getData(self): + name = self.node.findtext('check/name', '') + description = self.node.findtext('check/description', '') + author = self.node.findtext('check/description', '') + data = "" + if name: + data += f'\nname: {name}' + if description: + data += f'\ndescription: {description}' + if author: + data += f'\nauthor: {author}' + + return data def getDesc(self, tag): @@ -139,28 +150,55 @@ def getRequest(self): # Get data about request. try: - - raw_data = self.node.find('page').find('request').find('raw') + request = self.node.find('page/request') + raw_data = request.find('raw') data = raw_data.text + if not data: + data = self.contruct_request(request) return data - except: return 'None' + @staticmethod + def contruct_request(request): + data = request.findtext("method", "").upper() + data += f" {request.findtext('url', '')}" + for h in request.findall('headers/header'): + data += f"\n{h.get('name')}: {h.get('value')}" + if request.findtext('body',""): + data += f"\n{request.findtext('body','')}" + return data + + @staticmethod + def construct_response(request): + data = "" + if request.findtext("code", ""): + data += f'\ncode: {request.findtext("code", "")}' + if request.findtext("ip_address", ""): + data += f'\nip_address: {request.findtext("ip_address", "")}' + if request.findtext("time", ""): + data += f'\ntime: {request.findtext("time", "")}' + if request.findtext("return_code", ""): + data += f'\nreturn_code: {request.findtext("return_code", "")}' + if request.findtext("return_message", ""): + data += f'\nreturn_message: {request.findtext("return_message", "")}' + return data + def getResponse(self): - # Get data about response. try: - - raw_data = self.node.find('page').find('response').find('raw_headers') + request = self.node.find('page/response') + raw_data = request.find('raw_headers') data = raw_data.text + if not data: + data = self.contruct_request(request) + data += self.construct_response(request) return data - except: return 'None' -class System(): +class System: def __init__(self, node, tag_exists): self.node = node @@ -181,11 +219,13 @@ def __init__(self, node, tag_exists): self.version = self.getDesc('version') self.start_time = self.getDesc('start_datetime') self.finish_time = self.getDesc('finish_datetime') - self.note = self.getNote() def getUrl(self): sitemap = self.node.find("sitemap/entry") + #Fix for the case when no vulns in the report + if not sitemap: + return self.node.find("options").text.split("url:")[-1].strip(" \n") return sitemap.get('url') def getOptions(self): @@ -200,7 +240,6 @@ def getOptions(self): else: options_string = None - try: self.user_agent = self.node.find('user_agent').text except: @@ -231,17 +270,16 @@ def getDesc(self, tag): def getNote(self): result = ('Scan url:\n {} \nUser Agent:\n {} \nVersion Arachni:\n {} \nStart time:\n {} \nFinish time:\n {}' - '\nAudited Elements:\n {} \nModules:\n {} \nCookies:\n {}').format(self.url, self.user_agent, - self.version, self.start_time, - self.finish_time, - self.audited_elements, - self.modules, self.cookies) + '\nAudited Elements:\n {} \nModules:\n {} \nCookies:\n {}').format(self.url, self.user_agent, + self.version, self.start_time, + self.finish_time, + self.audited_elements, + self.modules, self.cookies) return result -class Plugins(): - +class Plugins: """ Support: WAF (Web Application Firewall) Detector (waf_detector) @@ -259,6 +297,14 @@ def __init__(self, plugins_node): except Exception: self.ip = None + @staticmethod + def get_value(name, node): + x = node.find(name) + if x: + return x.text + else: + return "" + def getHealthmap(self): # Get info about healthmap @@ -277,23 +323,14 @@ def getHealthmap(self): else: list_urls.append(f"Without Issues: {url.text}") - def get_value(name, node=None): - if not node: - node = healthmap_tree - x = healthmap_tree.find(name) - if x: - return x.text - else: - return "" - try: - plugin_name = get_value('name') - description = get_value('description') - results = get_value('results') - total = get_value('total', results) - with_issues = get_value('with_issues', results) - without_issues = get_value('without_issues', results) - issue_percentage = get_value('issue_percentage', results) + plugin_name = self.get_value('name', healthmap_tree) + description = self.get_value('description', healthmap_tree) + results = self.get_value('results', healthmap_tree) + total = self.get_value('total', results) + with_issues = self.get_value('with_issues', results) + without_issues = self.get_value('without_issues', results) + issue_percentage = self.get_value('issue_percentage', results) urls = '\n'.join(list_urls) result = (f"Plugin Name: {plugin_name}\nDescription: {description}\nStatistics:\nTotal: {total}" @@ -308,22 +345,12 @@ def getWaf(self): # Get info about waf plugin. waf_tree = self.plugins_node.find('waf_detector') - - def get_value(name, node=None): - if not node: - node = waf_tree - x = waf_tree.find(name) - if x: - return x.text - else: - return "" - try: - plugin_name = get_value('name') - description = get_value('description') + plugin_name = self.get_value('name', waf_tree) + description = self.get_value('description', waf_tree) results = waf_tree.find('results') - message = get_value('message', results) - status = get_value('status', results) + message = self.get_value('message', results) + status = self.get_value('status', results) result = (f"Plugin Name: {plugin_name}\nDescription: {description}\nResults:" f"\nMessage: {message}\nStatus: {status}") return result @@ -335,8 +362,8 @@ class ArachniPlugin(PluginXMLFormat): # Plugin that parses Arachni's XML report files. - def __init__(self): - super().__init__() + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) self.identifier_tag = ["report", "arachni_report"] self.id = 'Arachni' self.name = 'Arachni XML Output Plugin' @@ -395,9 +422,9 @@ def parseOutputString(self, output): self.hostname = self.getHostname(parser.system.url) if parser.plugins.ip: - self.address = resolve_hostname(parser.plugins.ip) + self.address = self.resolve_hostname(parser.plugins.ip) else: - self.address = resolve_hostname(self.hostname) + self.address = self.resolve_hostname(self.hostname) # Create host and interface host_id = self.createAndAddHost(self.address, hostnames=[self.hostname]) @@ -418,9 +445,9 @@ def parseOutputString(self, output): resol = str(issue.remedy_guidance) references = issue.references + cwe = [] if issue.cwe != 'None': - references.append('CWE-' + str(issue.cwe)) - + cwe.append('CWE-' + str(issue.cwe)) if resol == 'None': resol = '' @@ -429,6 +456,7 @@ def parseOutputString(self, output): service_id, name=issue.name, desc=description, + data=issue.data, resolution=resol, ref=references, severity=issue.severity, @@ -438,7 +466,9 @@ def parseOutputString(self, output): pname=issue.var, params=issue.parameters, request=issue.request, - response=issue.response) + response=issue.response, + cwe=cwe + ) return @@ -469,9 +499,9 @@ def processCommandString(self, username, current_path, command_string): # add reporter cmd_prefix_match = re.match(r"(^.*?)arachni ", command_string) cmd_prefix = cmd_prefix_match.group(1) - reporter_cmd = "%s%s --reporter=\"xml:outfile=%s\" \"%s\"" % (cmd_prefix, "arachni_reporter", xml_file_path, + reporter_cmd = "{}{} --reporter=\"xml:outfile={}\" \"{}\"".format(cmd_prefix, "arachni_reporter", xml_file_path, afr_file_path) - return "/usr/bin/env -- bash -c '%s 2>&1 && if [ -e \"%s\" ];then %s 2>&1;fi'" % (main_cmd, + return "/usr/bin/env -- bash -c '{} 2>&1 && if [ -e \"{}\" ];then {} 2>&1;fi'".format(main_cmd, afr_file_path, reporter_cmd) @@ -491,7 +521,5 @@ def getHostname(self, url): return self.hostname -def createPlugin(): - return ArachniPlugin() - -# I'm Py3 +def createPlugin(*args, **kwargs): + return ArachniPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/arp_scan/__init__.py b/faraday_plugins/plugins/repo/arp_scan/__init__.py index ea531e17..625a6e25 100755 --- a/faraday_plugins/plugins/repo/arp_scan/__init__.py +++ b/faraday_plugins/plugins/repo/arp_scan/__init__.py @@ -4,4 +4,4 @@ See the file 'doc/LICENSE' for the license information """ -# I'm Py3 \ No newline at end of file + diff --git a/faraday_plugins/plugins/repo/arp_scan/plugin.py b/faraday_plugins/plugins/repo/arp_scan/plugin.py index e955acb9..2e0cfd89 100644 --- a/faraday_plugins/plugins/repo/arp_scan/plugin.py +++ b/faraday_plugins/plugins/repo/arp_scan/plugin.py @@ -22,8 +22,8 @@ class CmdArpScanPlugin(PluginBase): Basically inserts into the tree the ouput of this tool """ - def __init__(self): - super().__init__() + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) self.id = "arp-scan" self.name = "arp-scan network scanner" self.plugin_version = "0.0.2" @@ -60,6 +60,5 @@ def parseOutputString(self, output): -def createPlugin(): - return CmdArpScanPlugin() - +def createPlugin(*args, **kwargs): + return CmdArpScanPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/aws_inspector/__init__.py b/faraday_plugins/plugins/repo/aws_inspector/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/faraday_plugins/plugins/repo/aws_inspector/plugin.py b/faraday_plugins/plugins/repo/aws_inspector/plugin.py new file mode 100644 index 00000000..fd4f7e78 --- /dev/null +++ b/faraday_plugins/plugins/repo/aws_inspector/plugin.py @@ -0,0 +1,104 @@ +""" +Faraday Penetration Test IDE +Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/) +See the file "doc/LICENSE" for the license information + +""" +from faraday_plugins.plugins.plugin import PluginJsonFormat +from json import loads + +__author__ = "Gonzalo Martinez" +__copyright__ = "Copyright (c) 2013, Infobyte LLC" +__credits__ = ["Gonzalo Martinez"] +__version__ = "1.0.0" +__maintainer__ = "Gonzalo Martinez" +__email__ = "gmartinez@infobytesec.com" +__status__ = "Development" + + +class AWSInspectorJsonPlugin(PluginJsonFormat): + + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) + self.id = "AWSInspector_Json" + self.name = "AWS Inspector JSON Output Plugin" + self.plugin_version = "1" + self.version = "9" + self.json_keys = {"findings"} + self.framework_version = "1.0.0" + self._temp_file_extension = "json" + + def parseOutputString(self, output): + # Useful docs about aws finding struct: https://docs.aws.amazon.com/inspector/v2/APIReference/API_Finding.html + data = loads(output) + for finding in data["findings"]: + cve = [] + refs = [] + cvss2 = {} + cvss3 = {} + status = finding.get("status", "ACTIVE") + + # AWS possible status are ACTIVE | SUPPRESSED | CLOSED + if status != 'ACTIVE': + continue + + name = finding.get("title", "") + description = finding.get("description", "") + severity = finding.get('severity', 'unclassified').lower().replace("untriaged", "unclassified") + + vulnerability_details = finding.get("packageVulnerabilityDetails") + if vulnerability_details: + cve = vulnerability_details.get("vulnerabilityId", None) + if cve != name: + name = name.replace(f"{cve} - ", "") + + refs = vulnerability_details.get("referenceUrls", []) + source_url = vulnerability_details.get("sourceUrl", "") + if isinstance(source_url, str): + refs.append(source_url) + elif isinstance(source_url, list): + refs += source_url + + if "inspectorScoreDetails" in finding and "adjustedCvss" in finding["inspectorScoreDetails"]: + version = finding["inspectorScoreDetails"]["adjustedCvss"].get("version") + if version: + vector_string = finding["inspectorScoreDetails"]["adjustedCvss"]["scoringVector"] + if "3" in version: + cvss3 = { + "vector_string": vector_string + } + elif "2" in version: + cvss2 = { + "vector_string": vector_string + } + + vulnerability = { + "name": name, + "desc": description, + "ref": refs, + "severity": severity, + "cve": cve, + "cvss2": cvss2, + "cvss3": cvss3, + } + + for resource in finding.get("resources", []): + resource_name = f"{finding.get('awsAccountId', '')} | {resource.get('id', '')}" + hostnames = [] + resource_details = resource.get("details", {}) + if "awsEc2Instance" in resource_details: + for hostname in resource_details["awsEc2Instance"].get("ipV4Addresses", []): + hostnames.append(hostname) + + host_id = self.createAndAddHost( + name=resource_name, + hostnames=hostnames + ) + self.createAndAddVulnToHost( + host_id=host_id, + **vulnerability + ) + + +def createPlugin(*args, **kwargs): + return AWSInspectorJsonPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/awsprowler/plugin.py b/faraday_plugins/plugins/repo/awsprowler/plugin.py deleted file mode 100644 index 8444467a..00000000 --- a/faraday_plugins/plugins/repo/awsprowler/plugin.py +++ /dev/null @@ -1,71 +0,0 @@ -""" -Faraday Penetration Test IDE -Copyright (C) 2020 Infobyte LLC (http://www.infobytesec.com/) -See the file 'doc/LICENSE' for the license information - -""" -import socket -import json -from datetime import datetime -import re -from faraday_plugins.plugins.plugin import PluginMultiLineJsonFormat - -__author__ = "Blas Moyano" -__copyright__ = "Copyright (c) 2020, Infobyte LLC" -__credits__ = ["Blas Moyano"] -__license__ = "" -__version__ = "0.0.1" -__maintainer__ = "Blas Moyano" -__email__ = "bmoyano@infobytesec.com" -__status__ = "Development" - - -class AwsProwlerJsonParser: - - def __init__(self, json_output): - string_manipulate = json_output.replace("}", "} #") - string_manipulate = string_manipulate[:len(string_manipulate) - 2] - self.report_aws = string_manipulate.split("#") - - -class AwsProwlerPlugin(PluginMultiLineJsonFormat): - """ Handle the AWS Prowler tool. Detects the output of the tool - and adds the information to Faraday. - """ - - def __init__(self): - super().__init__() - self.id = "awsprowler" - self.name = "AWS Prowler" - self.plugin_version = "0.1" - self.version = "0.0.1" - self.json_keys = {"Profile", "Account Number"} - - - def parseOutputString(self, output, debug=False): - parser = AwsProwlerJsonParser(output) - region_list = [] - for region in parser.report_aws: - json_reg = json.loads(region) - region_list.append(json_reg.get('Region', 'Not Info')) - - host_id = self.createAndAddHost(name=f'{self.name} - {region_list}', description="AWS Prowler") - - for vuln in parser.report_aws: - json_vuln = json.loads(vuln) - vuln_name = json_vuln.get('Control', 'Not Info') - vuln_desc = json_vuln.get('Message', 'Not Info') - vuln_severity = json_vuln.get('Level', 'Not Info') - vuln_run_date = json_vuln.get('Timestamp', 'Not Info') - vuln_external_id = json_vuln.get('Control ID', 'Not Info') - vuln_policy = f'{vuln_name}:{vuln_external_id}' - vuln_run_date = vuln_run_date.replace('T', ' ') - vuln_run_date = vuln_run_date.replace('Z', '') - self.createAndAddVulnToHost(host_id=host_id, name=vuln_name, desc=vuln_desc, - severity=self.normalize_severity(vuln_severity), - run_date=datetime.strptime(vuln_run_date, '%Y-%m-%d %H:%M:%S'), - external_id=vuln_external_id, policyviolations=[vuln_policy]) - - -def createPlugin(): - return AwsProwlerPlugin() diff --git a/faraday_plugins/plugins/repo/bandit/__init__.py b/faraday_plugins/plugins/repo/bandit/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/faraday_plugins/plugins/repo/bandit/plugin.py b/faraday_plugins/plugins/repo/bandit/plugin.py new file mode 100644 index 00000000..d3b319b9 --- /dev/null +++ b/faraday_plugins/plugins/repo/bandit/plugin.py @@ -0,0 +1,72 @@ +from faraday_plugins.plugins.plugin import PluginXMLFormat +import xml.etree.ElementTree as ET +import re + +class BanditPlugin(PluginXMLFormat): + """ + Example plugin to parse bandit output. + """ + + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) + self.identifier_tag = 'testsuite' + self.extension = ".xml" + self.id = "Bandit" + self.name = "Bandit XML Output Plugin" + self.plugin_version = "0.0.1" + + def report_belongs_to(self, **kwargs): + if super().report_belongs_to(**kwargs): + report_path = kwargs.get("report_path", "") + with open(report_path) as f: + output = f.read() + return re.search("testsuite name=\"bandit\"", output) is not None + return False + + def parseOutputString(self, output): + bp = BanditParser(output) + + for vuln in bp.vulns: + host_id = self.createAndAddHost(vuln['path']) + + self.createAndAddVulnToHost( + host_id=host_id, + name=vuln['name'], + desc=vuln['issue_text'], + ref=vuln['references'], + severity=vuln['severity'], + ) + + return True + + +class BanditParser: + """ + Parser for bandit on demand + """ + + def __init__(self, xml_output): + self.vulns = self._parse_xml(xml_output) + + + def _parse_xml(self, xml_output): + vulns = [] + tree = ET.fromstring(xml_output) + testcases = tree.findall('testcase') + + for testcase in testcases: + error = testcase.find('error') + name = testcase.attrib['name'] + path = testcase.attrib['classname'] + severity = error.attrib['type'] + issue_text = error.text + more_info = error.attrib['more_info'] + ref = [more_info] + + vulns.append({'name': name, 'path': path, 'references': ref, 'issue_text': issue_text, 'severity': severity}) + + return vulns + + +def createPlugin(*args, **kwargs): + return BanditPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/beef/__init__.py b/faraday_plugins/plugins/repo/beef/__init__.py index ea531e17..625a6e25 100644 --- a/faraday_plugins/plugins/repo/beef/__init__.py +++ b/faraday_plugins/plugins/repo/beef/__init__.py @@ -4,4 +4,4 @@ See the file 'doc/LICENSE' for the license information """ -# I'm Py3 \ No newline at end of file + diff --git a/faraday_plugins/plugins/repo/beef/plugin.py b/faraday_plugins/plugins/repo/beef/plugin.py index b4cc400a..b20cfc03 100644 --- a/faraday_plugins/plugins/repo/beef/plugin.py +++ b/faraday_plugins/plugins/repo/beef/plugin.py @@ -23,8 +23,8 @@ class BeefPlugin(PluginBase): Example plugin to parse beef output. """ - def __init__(self): - super().__init__() + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) self.id = "Beef" self.name = "BeEF Online Service Plugin" self.plugin_version = "0.0.1" @@ -49,10 +49,9 @@ def parseOutputString(self, output): output being sent is valid. """ try: - f = urlopen(self.getSetting( - "Host") + "/api/hooks?token=" + self.getSetting("Authkey")) + f = urlopen(self.getSetting("Host") + "/api/hooks?token=" + self.getSetting("Authkey")) # nosec data = json.loads(f.read()) - except: + except Exception: self.logger.info("[BeEF] - Connection with api") return @@ -95,10 +94,6 @@ def parseOutputString(self, output): severity=3) - def setHost(self): - pass - - -def createPlugin(): - return BeefPlugin() +def createPlugin(*args, **kwargs): + return BeefPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/brutexss/__init__.py b/faraday_plugins/plugins/repo/brutexss/__init__.py index 454b1c0a..729fe446 100644 --- a/faraday_plugins/plugins/repo/brutexss/__init__.py +++ b/faraday_plugins/plugins/repo/brutexss/__init__.py @@ -3,4 +3,4 @@ Copyright (C) 2018 Infobyte LLC (http://www.infobytesec.com/) See the file 'doc/LICENSE' for the license information """ -# I'm Py3 \ No newline at end of file + diff --git a/faraday_plugins/plugins/repo/brutexss/plugin.py b/faraday_plugins/plugins/repo/brutexss/plugin.py index 59cacef6..a8ca792b 100644 --- a/faraday_plugins/plugins/repo/brutexss/plugin.py +++ b/faraday_plugins/plugins/repo/brutexss/plugin.py @@ -12,13 +12,12 @@ __version__ = "1.0.0" from faraday_plugins.plugins.plugin import PluginBase -from faraday_plugins.plugins.plugins_utils import resolve_hostname class brutexss (PluginBase): - def __init__(self): - super().__init__() + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) self.id = "brutexss" self.name = "brutexss" self.plugin_version = "0.0.2" @@ -33,7 +32,7 @@ def parseOutputString(self, output): found_vuln = False for linea in lineas: if linea.find("is available! Good!") > 0: - url = re.findall('(?:[-\w.]|(?:%[\da-fA-F]{2}))+', linea)[0] + url = re.findall(r'(?:[-\w.]|(?:%[\da-fA-F]{2}))+', linea)[0] port = 80 if urlparse(url).scheme == 'https': port = 443 @@ -41,11 +40,11 @@ def parseOutputString(self, output): if len(netloc_splitted) > 1: port = netloc_splitted[1] if linea.find("Vulnerable") > 0 and "No" not in linea: - vuln_list = re.findall("\w+", linea) + vuln_list = re.findall(r"\w+", linea) if vuln_list[2] == "Vulnerable": parametro.append(vuln_list[1]) found_vuln = len(parametro) > 0 - address = resolve_hostname(url) + address = self.resolve_hostname(url) host_id = self.createAndAddHost(url, hostnames=[url]) service_id = self.createAndAddServiceToHost(host_id, self.protocol, 'tcp', ports=[port], status='Open', version="", @@ -57,7 +56,5 @@ def parseOutputString(self, output): -def createPlugin(): - return brutexss() - -# I'm Py3 +def createPlugin(*args, **kwargs): + return brutexss(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/burp/__init__.py b/faraday_plugins/plugins/repo/burp/__init__.py index ea531e17..625a6e25 100644 --- a/faraday_plugins/plugins/repo/burp/__init__.py +++ b/faraday_plugins/plugins/repo/burp/__init__.py @@ -4,4 +4,4 @@ See the file 'doc/LICENSE' for the license information """ -# I'm Py3 \ No newline at end of file + diff --git a/faraday_plugins/plugins/repo/burp/plugin.py b/faraday_plugins/plugins/repo/burp/plugin.py index 1b5b195e..93e7a2ea 100644 --- a/faraday_plugins/plugins/repo/burp/plugin.py +++ b/faraday_plugins/plugins/repo/burp/plugin.py @@ -5,28 +5,19 @@ """ import re -import os import base64 -from bs4 import BeautifulSoup, Comment -from faraday_plugins.plugins.plugin import PluginXMLFormat +import distutils.util # pylint: disable=import-error from urllib.parse import urlsplit -import distutils.util #pylint: disable=import-error - +import lxml.etree as ET -try: - import xml.etree.cElementTree as ET - import xml.etree.ElementTree as ET_ORIG - ETREE_VERSION = ET_ORIG.VERSION -except ImportError: - import xml.etree.ElementTree as ET - ETREE_VERSION = ET.VERSION - -ETREE_VERSION = [int(i) for i in ETREE_VERSION.split(".")] +from bs4 import BeautifulSoup, Comment +from faraday_plugins.plugins.plugin import PluginXMLFormat +from faraday_plugins.plugins.plugins_utils import CVE_regex, CWE_regex __author__ = "Francisco Amato" __copyright__ = "Copyright (c) 2013, Infobyte LLC" -__credits__ = ["Francisco Amato", "Micaela Ranea Sanchez"] +__credits__ = ["Francisco Amato", "Micaela Ranea Sanchez", "Dante Acosta"] __license__ = "" __version__ = "1.1.0" __maintainer__ = "Francisco Amato" @@ -52,7 +43,7 @@ def __init__(self, xml_output): self.host = None tree = self.parse_xml(xml_output) - if tree: + if tree is not None: self.items = [data for data in self.get_items(tree)] else: self.items = [] @@ -67,9 +58,10 @@ def parse_xml(self, xml_output): @return xml_tree An xml tree instance. None if error. """ try: - tree = ET.fromstring(xml_output) - except SyntaxError as err: - print("SyntaxError: %s. %s" % (err, xml_output)) + parser = ET.XMLParser(recover=True) + tree = ET.fromstring(xml_output, parser=parser) + except ET.XMLSyntaxError as err: + print(f"XMLSyntaxError: {err}. {xml_output}") return None return tree @@ -78,7 +70,6 @@ def get_items(self, tree): """ @return items A list of Host instances """ - bugtype = '' for node in tree.findall('issue'): yield Item(node) @@ -90,27 +81,7 @@ def get_attrib_from_subnode(xml_node, subnode_xpath_expr, attrib_name): @return An attribute value """ - global ETREE_VERSION - node = None - - if ETREE_VERSION[0] <= 1 and ETREE_VERSION[1] < 3: - - match_obj = re.search( - "([^\@]+?)\[\@([^=]*?)=\'([^\']*?)\'", subnode_xpath_expr) - if match_obj is not None: - - node_to_find = match_obj.group(1) - xpath_attrib = match_obj.group(2) - xpath_value = match_obj.group(3) - for node_found in xml_node.findall(node_to_find): - if node_found.attrib[xpath_attrib] == xpath_value: - node = node_found - break - else: - node = xml_node.find(subnode_xpath_expr) - - else: - node = xml_node.find(subnode_xpath_expr) + node = xml_node.find(subnode_xpath_expr) if node is not None: return node.get(attrib_name) @@ -127,18 +98,24 @@ class Item: def __init__(self, item_node): self.node = item_node - name = item_node.findall('name')[0] - host_node = item_node.findall('host')[0] - path = item_node.findall('path')[0] - location = item_node.findall('location')[0] - severity = item_node.findall('severity')[0] - external_id = item_node.findall('type')[0] - request = self.decode_binary_node('./requestresponse/request') - response = self.decode_binary_node('./requestresponse/response') - - detail = self.do_clean(item_node.findall('issueDetail')) - remediation = self.do_clean(item_node.findall('remediationBackground')) - background = self.do_clean(item_node.findall('issueBackground')) + name = item_node.find('name').text + host_node = item_node.find('host') + path = item_node.find('path').text + location = item_node.find('location').text + severity = item_node.find('severity').text + external_id = item_node.find('type').text + request = self.decode_binary_node(item_node.find('./requestresponse/request')) + response = self.decode_binary_node(item_node.find('./requestresponse/response')) + detail = self.do_clean(item_node.find('issueDetail')) + remediation = self.do_clean(item_node.find('remediationBackground')) + background = self.do_clean(item_node.find('issueBackground')) + self.references = self.do_clean(item_node.find('references')) + self.vuln_class = self.do_clean(item_node.find('vulnerabilityClassifications')) + self.cve = [] + if background: + cve = CVE_regex.search(background) + if cve: + self.cve = [cve.group()] self.url = host_node.text @@ -148,49 +125,45 @@ def __init__(self, item_node): self.host = url_data.hostname # Use the port in the URL if it is defined, or 80 or 443 by default - self.port = url_data.port or (443 if url_data.scheme == "https" - else 80) + self.port = url_data.port or (443 if url_data.scheme == "https" else 80) - self.name = name.text - self.location = location.text - self.path = path.text + self.name = name + self.path = path + loc = re.search(r"(?<=\[).+?(?=\])", location.replace(self.path, "")) + self.location = loc.group().split(" ")[0] if loc else "" self.ip = host_node.get('ip') self.url = self.node.get('url') - self.severity = severity.text + self.severity = severity self.request = request self.response = response self.detail = detail self.remediation = remediation self.background = background - self.external_id = external_id.text + self.external_id = external_id - - def do_clean(self, value): + @staticmethod + def do_clean(value): myreturn = "" if value is not None: - if len(value) > 0: - myreturn = value[0].text + myreturn = value.text return myreturn - def decode_binary_node(self, path): + def decode_binary_node(self, node): """ Finds a subnode matching `path` and returns its inner text if it has no base64 attribute or its base64 decoded inner text if it has it. """ - nodes = self.node.findall(path) - try: - subnode = nodes[0] - except IndexError: - return "" - encoded = distutils.util.strtobool(subnode.get('base64', 'false')) - if encoded: - res = base64.b64decode(subnode.text).decode('utf-8', errors="backslashreplace") - else: - res = subnode.text - return "".join([ch for ch in res if ord(ch) <= 128]) + if node is not None: + encoded = distutils.util.strtobool(node.get('base64', 'false')) + if encoded: + res = base64.b64decode(node.text).decode('utf-8', errors="backslashreplace") + else: + res = node.text + return "".join([ch for ch in res if ord(ch) <= 128]) + return "" def get_text_from_subnode(self, subnode_xpath_expr): """ @@ -210,8 +183,8 @@ class BurpPlugin(PluginXMLFormat): Example plugin to parse burp output. """ - def __init__(self): - super().__init__() + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) self.identifier_tag = "issues" self.id = "Burp" self.name = "Burp XML Output Plugin" @@ -222,7 +195,6 @@ def __init__(self): self._current_output = None self.target = None - def parseOutputString(self, output): parser = BurpXmlParser(output) @@ -236,28 +208,44 @@ def parseOutputString(self, output): ports=[str(item.port)], status="open") - desc = "Detail\n" + item.detail + desc = "" if item.background: - desc += "\nBackground\n" + item.background + desc += item.background desc = self.removeHtml(desc) + data = "" + if item.detail: + data = self.removeHtml(item.detail) + ref = [] + if item.references: + ref += self.get_url(item.references) + cwe = [] + if item.vuln_class: + for cwe_ref in self.get_ref(item.vuln_class): + if CWE_regex.search(cwe_ref): + cwe.append(CWE_regex.search(cwe_ref).group()) resolution = self.removeHtml(item.remediation) if item.remediation else "" - v_id = self.createAndAddVulnWebToService( + self.createAndAddVulnWebToService( h_id, s_id, item.name, desc=desc, + data=data, severity=item.severity, website=item.host, path=item.path, request=item.request, response=item.response, resolution=resolution, - external_id=item.external_id) + ref=ref, + params=item.location, + external_id=item.external_id, + cve=item.cve, + cwe=cwe + ) del parser - def removeHtml(self, markup): soup = BeautifulSoup(markup, "html.parser") @@ -285,11 +273,27 @@ def removeHtml(self, markup): return str(soup) - def setHost(self): - pass + def get_ref(self, markup): + soup = BeautifulSoup(markup, "html.parser") + + for tag in soup.find_all("ul"): + for item in tag.find_all("li"): + for a in item.find_all("a"): + a.unwrap() + item.unwrap() + tag.unwrap() + ref = str(soup).strip().split("\n") + return ref + def get_url(self, markup): + soup = BeautifulSoup(markup, "html.parser") + ref = [] + for tag in soup.find_all("ul"): + for item in tag.find_all("li"): + for a in item.find_all("a"): + ref += [a['href'].strip()] + return ref -def createPlugin(): - return BurpPlugin() -# I'm Py3 +def createPlugin(*args, **kwargs): + return BurpPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/checkmarx/__init__.py b/faraday_plugins/plugins/repo/checkmarx/__init__.py index ea531e17..625a6e25 100644 --- a/faraday_plugins/plugins/repo/checkmarx/__init__.py +++ b/faraday_plugins/plugins/repo/checkmarx/__init__.py @@ -4,4 +4,4 @@ See the file 'doc/LICENSE' for the license information """ -# I'm Py3 \ No newline at end of file + diff --git a/faraday_plugins/plugins/repo/checkmarx/plugin.py b/faraday_plugins/plugins/repo/checkmarx/plugin.py index fbcf549d..437fead2 100755 --- a/faraday_plugins/plugins/repo/checkmarx/plugin.py +++ b/faraday_plugins/plugins/repo/checkmarx/plugin.py @@ -3,17 +3,11 @@ Copyright (C) 2016 Infobyte LLC (http://www.infobytesec.com/) See the file 'doc/LICENSE' for the license information """ -import re +import xml.etree.ElementTree as ET from urllib.parse import urlparse from faraday_plugins.plugins.plugin import PluginXMLFormat - -try: - import xml.etree.cElementTree as ET -except ImportError: - import xml.etree.ElementTree as ET - __author__ = 'Blas Moyano' __copyright__ = 'Copyright 2020, Faraday Project' __credits__ = ['Blas Moyano'] @@ -35,7 +29,7 @@ def parse_xml(self, xml_output): try: tree = ET.fromstring(xml_output) except SyntaxError as err: - print('SyntaxError In xml: %s. %s' % (err, xml_output)) + print(f'SyntaxError In xml: {err}. {xml_output}') return None return tree @@ -70,7 +64,8 @@ def get_path_node_info(self, path_node): for info_pn in pn: if info_pn.tag == 'Snippet': valor = ( - 'Number', info_pn.find('Line').find('Number').text, 'Code', info_pn.find('Line').find('Code').text) + 'Number', info_pn.find('Line').find('Number').text, 'Code', + info_pn.find('Line').find('Code').text) else: valor = (info_pn.tag, info_pn.text) lista_v.append(valor) @@ -79,8 +74,9 @@ def get_path_node_info(self, path_node): class CheckmarxPlugin(PluginXMLFormat): - def __init__(self): - super().__init__() + + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) self.identifier_tag = ["CxXMLResults"] self.id = 'Checkmarx' self.name = 'Checkmarx XML Output Plugin' @@ -89,7 +85,6 @@ def __init__(self): self.framework_version = '1.0.0' self.options = None - def parseOutputString(self, output): parser = CheckmarxXmlParser(output) if not parser.query: @@ -117,7 +112,7 @@ def parseOutputString(self, output): vuln_name = vulns.query_attrib['name'] vuln_severity = vulns.query_attrib['Severity'] vuln_external_id = vulns.query_attrib['id'] - refs.append(f'CWE-{vulns.query_attrib["cweId"]}') + cwe = [f'CWE-{vulns.query_attrib["cweId"]}'] data = '' for files_data in vulns.path_node: for file_data in files_data: @@ -130,13 +125,12 @@ def parseOutputString(self, output): refs.append(v_result['FileName']) self.createAndAddVulnToHost(host_id, vuln_name, severity=vuln_severity, - resolution=data, external_id=vuln_external_id) + resolution=data, external_id=vuln_external_id, cwe=cwe) self.createAndAddVulnWebToService(host_id, service_to_interface, vuln_name, desc=vuln_desc, severity=vuln_severity, - resolution=data, ref=refs) - + resolution=data, ref=refs, cwe=cwe) -def createPlugin(): - return CheckmarxPlugin() +def createPlugin(*args, **kwargs): + return CheckmarxPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/cis/__init__.py b/faraday_plugins/plugins/repo/cis/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/faraday_plugins/plugins/repo/cis/plugin.py b/faraday_plugins/plugins/repo/cis/plugin.py new file mode 100644 index 00000000..9131c586 --- /dev/null +++ b/faraday_plugins/plugins/repo/cis/plugin.py @@ -0,0 +1,71 @@ +""" +Faraday Penetration Test IDE +Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/) +See the file 'doc/LICENSE' for the license information + +""" + +from faraday_plugins.plugins.plugin import PluginXMLFormat +from faraday_plugins.plugins.plugins_utils import CVE_regex, CWE_regex +from xml.etree import ElementTree as ET + +__author__ = "Gonzalo Martinez" +__copyright__ = "Copyright (c) 2013, Infobyte LLC" +__credits__ = ["Gonzalo Martinez"] +__license__ = "" +__version__ = "1.0.0" +__maintainer__ = "Gonzalo Martinez" +__email__ = "gmartinez@infobytesec.com" +__status__ = "Development" + + +class CisPlugin(PluginXMLFormat): + + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) + self.ns = { + "scap-con": "{http://scap.nist.gov/schema/scap/constructs/1.2}", + "arf": "{http://scap.nist.gov/schema/asset-reporting-format/1.1}", + "dsc": "{http://scap.nist.gov/schema/scap/source/1.2}", + "ai": "{http://scap.nist.gov/schema/asset-identification/1.1}", + "xccdf": "{http://checklists.nist.gov/xccdf/1.2}" + } + self.identifier_tag = "asset-report-collection" + self.id = "CIS" + self.name = "CIS XML Output Plugin" + self.plugin_version = "1.0.0" + + def parseOutputString(self, output): + + root = ET.fromstring(output) + rules = {} + for rule in root.findall(f".//{self.ns['xccdf']}Rule"): + rules[rule.attrib['id']] = { + "title": rule[0].text, + "description": rule[1][0].text + } + reports = root.findall(f".//{self.ns['arf']}reports") + for report in reports: + target_address = report.find(f".//{self.ns['xccdf']}target-address").text + rules_results = report.findall(f".//{self.ns['xccdf']}rule-result") + h_id = self.createAndAddHost(target_address) + for rule_result in rules_results: + result = rule_result.find(f"{self.ns['xccdf']}result").text + if result != "pass": + severity = rule_result.attrib.get("severity","unclassified") + rule_id = rule_result.attrib['idref'] + references = [] + for ident in rule_result.findall(f"{self.ns['xccdf']}ident"): + text = ident.text + if isinstance(text, str) and len(text) > 10: + references.append(text) + self.createAndAddVulnToHost( + h_id, + name=rules[rule_id]["title"], + desc=rules[rule_id]["description"], + severity=severity, + ref=references + ) + +def createPlugin(*args, **kwargs): + return CisPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/cobalt/__init__.py b/faraday_plugins/plugins/repo/cobalt/__init__.py index 81e2e0d9..cef3c4e2 100644 --- a/faraday_plugins/plugins/repo/cobalt/__init__.py +++ b/faraday_plugins/plugins/repo/cobalt/__init__.py @@ -4,4 +4,4 @@ See the file 'doc/LICENSE' for the license information """ -# I'm Py3 \ No newline at end of file + diff --git a/faraday_plugins/plugins/repo/cobalt/plugin.py b/faraday_plugins/plugins/repo/cobalt/plugin.py index e2c089fc..51823f17 100644 --- a/faraday_plugins/plugins/repo/cobalt/plugin.py +++ b/faraday_plugins/plugins/repo/cobalt/plugin.py @@ -9,7 +9,8 @@ from urllib.parse import urlparse import csv import io -import dateutil +from dateutil.parser import parse + __author__ = "Blas" @@ -21,7 +22,6 @@ __email__ = "bmoyano@infobytesec.com" __status__ = "Development" -from faraday_plugins.plugins.plugins_utils import resolve_hostname class CobaltParser: @@ -51,8 +51,8 @@ class CobaltPlugin(PluginCSVFormat): Example plugin to parse Cobalt output. """ - def __init__(self): - super().__init__() + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) self.csv_headers = [{'Token'}, {'Tag'}] self.id = "Cobalt" self.name = "Cobalt CSV Output Plugin" @@ -75,7 +75,7 @@ def parseOutputString(self, output): scheme = url_data.scheme port = url_data.port try: - run_date = dateutil.parser.parse(row['CreatedAt']) + run_date = parse(row['CreatedAt']) except: run_date = None if url_data.port is None: @@ -85,7 +85,7 @@ def parseOutputString(self, output): port = 80 else: port = url_data.port - name = resolve_hostname(url_data.netloc) + name = self.resolve_hostname(url_data.netloc) references = [] if row['RefKey']: references.append(row['RefKey']) @@ -102,5 +102,5 @@ def parseOutputString(self, output): data=row['StepsToReproduce'], external_id=row['Tag'], run_date=run_date) -def createPlugin(): - return CobaltPlugin() +def createPlugin(*args, **kwargs): + return CobaltPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/crowdstrike/__init__.py b/faraday_plugins/plugins/repo/crowdstrike/__init__.py new file mode 100644 index 00000000..98901a0b --- /dev/null +++ b/faraday_plugins/plugins/repo/crowdstrike/__init__.py @@ -0,0 +1,6 @@ +""" +Faraday Penetration Test IDE +Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/) +See the file 'doc/LICENSE' for the license information + +""" diff --git a/faraday_plugins/plugins/repo/crowdstrike/plugin.py b/faraday_plugins/plugins/repo/crowdstrike/plugin.py new file mode 100644 index 00000000..5b66ec02 --- /dev/null +++ b/faraday_plugins/plugins/repo/crowdstrike/plugin.py @@ -0,0 +1,84 @@ +""" +Faraday Penetration Test IDE +Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/) +See the file 'doc/LICENSE' for the license information + +""" +from urllib.parse import urlsplit +import ipaddress + +from lxml import etree + +from faraday_plugins.plugins.plugin import PluginJsonFormat +from faraday_plugins.plugins.repo.acunetix.DTO import Acunetix, Scan +from json import loads + +__author__ = "Gonzalo Martinez" +__copyright__ = "Copyright (c) 2013, Infobyte LLC" +__credits__ = ["Gonzalo Martinez"] +__version__ = "1.0.0" +__maintainer__ = "Gonzalo Martinez" +__email__ = "gmarintez@faradaysec.com" +__status__ = "Development" + + +class Crowdstrike(PluginJsonFormat): + + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) + self.id = "Crowdstrike_Json" + self.name = "Crowdstrike JSON Output Plugin" + self.plugin_version = "1" + self.version = "1" + self.json_keys = {'host_id', 'host_type'} + self.framework_version = "1.0.0" + self._temp_file_extension = "json" + + def parseOutputString(self, output): + parser = loads(output) + for site in parser: + site_name = site.get('local_ip') + hostname = site.get('hostname') + host_tags = site.get('host_tags') + os_version = site.get('os_version') + host_id = self.createAndAddHost( + name=site_name, + os=os_version, + hostnames=hostname, + tags=host_tags + ) + cve = site.get('cve_id') + severity = site.get('severity').lower() + cvss_vector = site.get('vector') + references = [site.get('references')] + vuln = site.get('cve_description') + remedations = [] + for rr in site.get('recommended_remediations', []): + remedations.append(rr.get('detail')) + cvss3 = {} + cvss2 = {} + if float(site.get('cvss_version', '0')) > 2: + cvss3['vector_string'] = cvss_vector + else: + cvss2['vector_string'] = cvss_vector + evaluation_logic = '' + for evaluation in site.get('evaluation_logic', []): + result = [] + for item in evaluation.get('items', []): + result.append(item.get('comparison_result')) + evaluation_logic += f'Evalutaion: {evaluation.get("title")}\n\tResults: {" ".join(result)}\n' + self.createAndAddVulnToHost( + host_id=host_id, + name=vuln[:50], + desc=vuln, + severity=severity, + cve=cve, + cvss3=cvss3, + cvss2=cvss2, + ref=references, + data=evaluation_logic + ) + + +def createPlugin(*args, **kwargs): + return Crowdstrike(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/dig/__init__.py b/faraday_plugins/plugins/repo/dig/__init__.py index f55478ec..4c2d40a8 100644 --- a/faraday_plugins/plugins/repo/dig/__init__.py +++ b/faraday_plugins/plugins/repo/dig/__init__.py @@ -3,4 +3,4 @@ Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/) See the file 'doc/LICENSE' for the license information """ -# I'm Py3 \ No newline at end of file + diff --git a/faraday_plugins/plugins/repo/dig/plugin.py b/faraday_plugins/plugins/repo/dig/plugin.py index e5c43424..55e73b4f 100644 --- a/faraday_plugins/plugins/repo/dig/plugin.py +++ b/faraday_plugins/plugins/repo/dig/plugin.py @@ -7,17 +7,16 @@ """ import re -__author__ = u"Andres Tarantini" -__copyright__ = u"Copyright (c) 2015 Andres Tarantini" -__credits__ = [u"Andres Tarantini"] -__license__ = u"MIT" -__version__ = u"0.0.1" -__maintainer__ = u"Andres Tarantini" -__email__ = u"atarantini@gmail.com" -__status__ = u"Development" +__author__ = "Andres Tarantini" +__copyright__ = "Copyright (c) 2015 Andres Tarantini" +__credits__ = ["Andres Tarantini"] +__license__ = "MIT" +__version__ = "0.0.1" +__maintainer__ = "Andres Tarantini" +__email__ = "atarantini@gmail.com" +__status__ = "Development" from faraday_plugins.plugins.plugin import PluginBase -from faraday_plugins.plugins.plugins_utils import resolve_hostname class DigPlugin(PluginBase): @@ -25,25 +24,25 @@ class DigPlugin(PluginBase): Handle DiG (http://linux.die.net/man/1/dig) output """ - def __init__(self): - super().__init__() - self.id = u"dig" - self.name = u"DiG" - self.plugin_version = u"0.0.1" - self.version = u"9.9.5-3" + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) + self.id = "dig" + self.name = "DiG" + self.plugin_version = "0.0.1" + self.version = "9.9.5-3" self._command_regex = re.compile(r'^(dig)\s+.*?') def parseOutputString(self, output): # Ignore all lines that start with ";" parsed_output = [line for line in output.splitlines() if line and line[ - 0] != u";"] + 0] != ";"] if not parsed_output: return True # Parse results results = [] - answer_section_columns = [u"domain", - u"ttl", u"class", u"type", u"data"] + answer_section_columns = ["domain", + "ttl", "class", "type", "data"] for line in parsed_output: line_split = line.split() # the first 4 elements are domain, ttl, class, type; everything else data results.append(dict(zip(answer_section_columns, line_split[:4] + [line_split[4:]] ))) @@ -51,29 +50,29 @@ def parseOutputString(self, output): # Create hosts is results information is relevant try: for result in results: - relevant_types = [u"A", u"AAAA", u"MX", u"NS", u"SOA", u"TXT"] + relevant_types = ["A", "AAAA", "MX", "NS", "SOA", "TXT"] # TODO implement more types from https://en.wikipedia.org/wiki/List_of_DNS_record_types - if result.get(u"type") in relevant_types: + if result.get("type") in relevant_types: # get domain - domain = result.get(u"domain") + domain = result.get("domain") # get IP address (special if type "A") - if result.get(u"type") == u"A": # A = IPv4 address from dig - ip_address = result.get(u"data")[0] + if result.get("type") == "A": # A = IPv4 address from dig + ip_address = result.get("data")[0] else: # if not, from socket - ip_address = resolve_hostname(domain) + ip_address = self.resolve_hostname(domain) # Create host host_id = self.createAndAddHost(ip_address, hostnames=[domain]) # all other TYPES that aren't 'A' and 'AAAA' are dealt here: - if result.get(u"type") == u"MX": # Mail exchange record - mx_priority = result.get(u"data")[0] - mx_record = result.get(u"data")[1] + if result.get("type") == "MX": # Mail exchange record + mx_priority = result.get("data")[0] + mx_record = result.get("data")[1] service_id = self.createAndAddServiceToHost( host_id=host_id, @@ -89,22 +88,22 @@ def parseOutputString(self, output): name="priority", text=text.encode('ascii', 'ignore')) - elif result.get(u"type") == u"NS": # Name server record - ns_record = result.get(u"data")[0] + elif result.get("type") == "NS": # Name server record + ns_record = result.get("data")[0] self.createAndAddServiceToHost( name=ns_record, protocol="DNS", ports=[53], description="DNS Server") - elif result.get(u"type") == u"SOA": # Start of Authority Record - ns_record = result.get(u"data")[0] # primary namer server - responsible_party = result.get(u"data")[1] # responsible party of domain - timestamp = result.get(u"data")[2] - refresh_zone_time = result.get(u"data")[3] - retry_refresh_time = result.get(u"data")[4] - upper_limit_time = result.get(u"data")[5] - negative_result_ttl = result.get(u"data")[6] + elif result.get("type") == "SOA": # Start of Authority Record + ns_record = result.get("data")[0] # primary namer server + responsible_party = result.get("data")[1] # responsible party of domain + timestamp = result.get("data")[2] + refresh_zone_time = result.get("data")[3] + retry_refresh_time = result.get("data")[4] + upper_limit_time = result.get("data")[5] + negative_result_ttl = result.get("data")[6] service_id = self.createAndAddServiceToHost( host_id=host_id, @@ -127,8 +126,8 @@ def parseOutputString(self, output): name="priority", text=text.encode('ascii', 'ignore')) - elif result.get(u"type") == u"TXT": # TXT record - text = " ".join(result.get(u"data")[:]) + elif result.get("type") == "TXT": # TXT record + text = " ".join(result.get("data")[:]) self.createAndAddNoteToHost( host_id=host_id, name="TXT Information", @@ -141,7 +140,5 @@ def parseOutputString(self, output): return True -def createPlugin(): - return DigPlugin() - -# I'm Py3 +def createPlugin(*args, **kwargs): + return DigPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/dirb/__init__.py b/faraday_plugins/plugins/repo/dirb/__init__.py index 0c98f519..fd94db9c 100644 --- a/faraday_plugins/plugins/repo/dirb/__init__.py +++ b/faraday_plugins/plugins/repo/dirb/__init__.py @@ -3,4 +3,4 @@ Copyright (C) 2016 Infobyte LLC (http://www.infobytesec.com/) See the file 'doc/LICENSE' for the license information """ -# I'm Py3 \ No newline at end of file + diff --git a/faraday_plugins/plugins/repo/dirb/plugin.py b/faraday_plugins/plugins/repo/dirb/plugin.py index 2aa305d6..b325c54c 100644 --- a/faraday_plugins/plugins/repo/dirb/plugin.py +++ b/faraday_plugins/plugins/repo/dirb/plugin.py @@ -14,20 +14,19 @@ __status__ = "Development" from faraday_plugins.plugins.plugin import PluginBase -from faraday_plugins.plugins.plugins_utils import resolve_hostname class dirbPlugin(PluginBase): - def __init__(self): - super().__init__() + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) self.id = "dirb" self.name = "Dirb" self.plugin_version = "0.0.1" self.version = "2.22" self.regexpUrl = r'((http[s]?)\:\/\/([\w\.]+)[.\S]+)' - self._command_regex = re.compile(r'^(?:sudo dirb|dirb|\.\/dirb|sudo \.\/dirb)\s+(?:(http[s]?)' - r'\:\/\/([\w\.]+)[.\S]+)') + self._command_regex = re.compile(r'^(sudo dirb|dirb|\.\/dirb|sudo \.\/dirb)\s+.*?') + self.text = [] def getPort(self, host, proto): @@ -41,8 +40,8 @@ def getPort(self, host, proto): def getIP(self, host): try: - ip = resolve_hostname(host) - except Exception: + ip = self.resolve_hostname(host) + except Exception: # nosec pass return ip @@ -113,10 +112,8 @@ def processCommandString(self, username, current_path, command_string): arg_search = re.search(silent_mode_re,command_string) if arg_search is None: extra_arg +=" -S" - return "%s%s" % (command_string, extra_arg) - + return f"{command_string}{extra_arg}" -def createPlugin(): - return dirbPlugin() -# I'm Py3 +def createPlugin(*args, **kwargs): + return dirbPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/dirsearch/__init__.py b/faraday_plugins/plugins/repo/dirsearch/__init__.py index ea531e17..625a6e25 100644 --- a/faraday_plugins/plugins/repo/dirsearch/__init__.py +++ b/faraday_plugins/plugins/repo/dirsearch/__init__.py @@ -4,4 +4,4 @@ See the file 'doc/LICENSE' for the license information """ -# I'm Py3 \ No newline at end of file + diff --git a/faraday_plugins/plugins/repo/dirsearch/plugin.py b/faraday_plugins/plugins/repo/dirsearch/plugin.py index c53cd108..d90e60d1 100644 --- a/faraday_plugins/plugins/repo/dirsearch/plugin.py +++ b/faraday_plugins/plugins/repo/dirsearch/plugin.py @@ -9,7 +9,7 @@ import argparse import urllib.parse as urlparse from faraday_plugins.plugins.plugin import PluginBase -from faraday_plugins.plugins.plugins_utils import get_vulnweb_url_fields, resolve_hostname +from faraday_plugins.plugins.plugins_utils import get_vulnweb_url_fields __author__ = "Matías Lang" @@ -52,13 +52,14 @@ class DirsearchPlugin(PluginBase): - def __init__(self): - super().__init__() + + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) self.id = "dirsearch" self.name = "dirsearch" self.plugin_version = "0.0.1" self.version = "0.0.1" - self._command_regex = re.compile(r'^(sudo )?(python[0-9\.]? )?dirsearch(\.py)\s+?') + self._command_regex = re.compile(r'^(sudo )?(python[0-9\.]? )?(dirsearch\.py)\s+?') self.addSetting("Ignore 403", str, "1") self._use_temp_file = True self._temp_file_extension = "json" @@ -66,7 +67,6 @@ def __init__(self): def parseOutputString(self, output): self.parse_json(output) - @property def should_ignore_403(self): val = self.getSetting('Ignore 403') @@ -83,7 +83,7 @@ def parse_json(self, contents): return for (base_url, items) in data.items(): base_split = urlparse.urlsplit(base_url) - ip = resolve_hostname(base_split.hostname) + ip = self.resolve_hostname(base_split.hostname) h_id = self.createAndAddHost(ip, hostnames=[base_split.hostname]) s_id = self.createAndAddServiceToHost( h_id, @@ -104,13 +104,12 @@ def parse_found_url(self, base_url, h_id, s_id, item): item['content-length']) redirect = item.get('redirect') if redirect is not None: - response += '\nLocation: {}'.format(redirect) + response += f'\nLocation: {redirect}' self.createAndAddVulnWebToService( h_id, s_id, - name='Path found: {} ({})'.format(item['path'], item['status']), - desc="Dirsearch tool found the following URL: {}".format( - url.geturl()), + name=f'Path found: {item["path"]} ({item["status"]})', + desc=f"Dirsearch tool found the following URL: {url.geturl()}", severity="info", method='GET', response=response, @@ -129,10 +128,8 @@ def processCommandString(self, username, current_path, command_string): return None else: super().processCommandString(username, current_path, command_string) - return '{} --json-report {}'.format(command_string, self._output_file_path) - + return f'{command_string} --json-report {self._output_file_path}' -def createPlugin(): - return DirsearchPlugin() -# I'm Py3 +def createPlugin(*args, **kwargs): + return DirsearchPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/dnsenum/__init__.py b/faraday_plugins/plugins/repo/dnsenum/__init__.py index ea531e17..625a6e25 100644 --- a/faraday_plugins/plugins/repo/dnsenum/__init__.py +++ b/faraday_plugins/plugins/repo/dnsenum/__init__.py @@ -4,4 +4,4 @@ See the file 'doc/LICENSE' for the license information """ -# I'm Py3 \ No newline at end of file + diff --git a/faraday_plugins/plugins/repo/dnsenum/plugin.py b/faraday_plugins/plugins/repo/dnsenum/plugin.py index 9fba1611..986729bb 100644 --- a/faraday_plugins/plugins/repo/dnsenum/plugin.py +++ b/faraday_plugins/plugins/repo/dnsenum/plugin.py @@ -4,20 +4,10 @@ See the file 'doc/LICENSE' for the license information """ -from faraday_plugins.plugins.plugin import PluginBase import re -import os - -try: - import xml.etree.cElementTree as ET - import xml.etree.ElementTree as ET_ORIG - ETREE_VERSION = ET_ORIG.VERSION -except ImportError: - import xml.etree.ElementTree as ET - ETREE_VERSION = ET.VERSION - -ETREE_VERSION = [int(i) for i in ETREE_VERSION.split(".")] +import xml.etree.ElementTree as ET +from faraday_plugins.plugins.plugin import PluginBase __author__ = "Francisco Amato" __copyright__ = "Copyright (c) 2013, Infobyte LLC" @@ -60,7 +50,7 @@ def parse_xml(self, xml_output): try: tree = ET.fromstring(xml_output) except SyntaxError as err: - print("SyntaxError: %s. %s" % (err, xml_output)) + print(f"SyntaxError: {err}. {xml_output}") return None return tree @@ -69,7 +59,6 @@ def get_items(self, tree): """ @return items A list of Host instances """ - bugtype = '' node = tree.findall('testdata')[0] for hostnode in node.findall('host'): @@ -82,26 +71,8 @@ def get_attrib_from_subnode(xml_node, subnode_xpath_expr, attrib_name): @return An attribute value """ - global ETREE_VERSION - node = None - - if ETREE_VERSION[0] <= 1 and ETREE_VERSION[1] < 3: - - match_obj = re.search( - "([^\@]+?)\[\@([^=]*?)=\'([^\']*?)\'", subnode_xpath_expr) - if match_obj is not None: - node_to_find = match_obj.group(1) - xpath_attrib = match_obj.group(2) - xpath_value = match_obj.group(3) - for node_found in xml_node.findall(node_to_find): - if node_found.attrib[xpath_attrib] == xpath_value: - node = node_found - break - else: - node = xml_node.find(subnode_xpath_expr) - else: - node = xml_node.find(subnode_xpath_expr) + node = xml_node.find(subnode_xpath_expr) if node is not None: return node.get(attrib_name) @@ -150,8 +121,8 @@ class DnsenumPlugin(PluginBase): Example plugin to parse dnsenum output. """ - def __init__(self): - super().__init__() + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) self.id = "Dnsenum" self.name = "Dnsenum XML Output Plugin" self.plugin_version = "0.0.1" @@ -176,7 +147,7 @@ def parseOutputString(self, output): parser = DnsenumXmlParser(output) for item in parser.items: - h_id = self.createAndAddHost(item.ip, hostnames=[item.hostname]) + self.createAndAddHost(item.ip, hostnames=[item.hostname]) del parser @@ -193,11 +164,6 @@ def processCommandString(self, username, current_path, command_string): else: return re.sub(arg_match.group(1), r"-o %s" % self._output_file_path, command_string) - def setHost(self): - pass - - -def createPlugin(): - return DnsenumPlugin() -# I'm Py3 +def createPlugin(*args, **kwargs): + return DnsenumPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/dnsmap/__init__.py b/faraday_plugins/plugins/repo/dnsmap/__init__.py index ea531e17..625a6e25 100644 --- a/faraday_plugins/plugins/repo/dnsmap/__init__.py +++ b/faraday_plugins/plugins/repo/dnsmap/__init__.py @@ -4,4 +4,4 @@ See the file 'doc/LICENSE' for the license information """ -# I'm Py3 \ No newline at end of file + diff --git a/faraday_plugins/plugins/repo/dnsmap/plugin.py b/faraday_plugins/plugins/repo/dnsmap/plugin.py index 4ceebf98..0936be58 100644 --- a/faraday_plugins/plugins/repo/dnsmap/plugin.py +++ b/faraday_plugins/plugins/repo/dnsmap/plugin.py @@ -3,10 +3,10 @@ Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/) See the file 'doc/LICENSE' for the license information """ -from faraday_plugins.plugins.plugin import PluginBase import re from collections import defaultdict +from faraday_plugins.plugins.plugin import PluginBase __author__ = "Francisco Amato" __copyright__ = "Copyright (c) 2013, Infobyte LLC" @@ -91,8 +91,8 @@ def add_host_info_to_items(self, ip_address, hostname): class DnsmapPlugin(PluginBase): """Example plugin to parse dnsmap output.""" - def __init__(self): - super().__init__() + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) self.id = "Dnsmap" self.name = "Dnsmap Output Plugin" self.plugin_version = "0.3" @@ -104,9 +104,9 @@ def __init__(self): self._use_temp_file = True self._temp_file_extension = "txt" - def canParseCommandString(self, current_input): if self._command_regex.match(current_input.strip()): + self.command = self.get_command(current_input) return True else: return False @@ -130,14 +130,12 @@ def processCommandString(self, username, current_path, command_string): arg_match = self.xml_arg_re.match(command_string) if arg_match is None: - return "%s -r %s \\n" % (command_string, self._output_file_path) + return f"{command_string} -r {self._output_file_path} \\n" else: return re.sub(arg_match.group(1), r"-r %s" % self._output_file_path, command_string) -def createPlugin(): - return DnsmapPlugin() - -# I'm Py3 +def createPlugin(*args, **kwargs): + return DnsmapPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/dnsrecon/__init__.py b/faraday_plugins/plugins/repo/dnsrecon/__init__.py index ea531e17..625a6e25 100644 --- a/faraday_plugins/plugins/repo/dnsrecon/__init__.py +++ b/faraday_plugins/plugins/repo/dnsrecon/__init__.py @@ -4,4 +4,4 @@ See the file 'doc/LICENSE' for the license information """ -# I'm Py3 \ No newline at end of file + diff --git a/faraday_plugins/plugins/repo/dnsrecon/plugin.py b/faraday_plugins/plugins/repo/dnsrecon/plugin.py index fd5a3c07..69fdb909 100644 --- a/faraday_plugins/plugins/repo/dnsrecon/plugin.py +++ b/faraday_plugins/plugins/repo/dnsrecon/plugin.py @@ -3,19 +3,10 @@ Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/) See the file 'doc/LICENSE' for the license information """ -from faraday_plugins.plugins.plugin import PluginBase import re +import xml.etree.ElementTree as ET -try: - import xml.etree.cElementTree as ET - import xml.etree.ElementTree as ET_ORIG - ETREE_VERSION = ET_ORIG.VERSION -except ImportError: - import xml.etree.ElementTree as ET - ETREE_VERSION = ET.VERSION - -ETREE_VERSION = [int(i) for i in ETREE_VERSION.split(".")] - +from faraday_plugins.plugins.plugin import PluginBase __author__ = "Francisco Amato" __copyright__ = "Copyright (c) 2013, Infobyte LLC" @@ -59,7 +50,7 @@ def parse_xml(self, xml_output): try: tree = ET.fromstring(xml_output) except SyntaxError as err: - print("SyntaxError: %s. %s" % (err, xml_output)) + print(f"SyntaxError: {err}. {xml_output}") return None return tree @@ -78,27 +69,8 @@ def get_attrib_from_subnode(xml_node, subnode_xpath_expr, attrib_name): @return An attribute value """ - global ETREE_VERSION - node = None - - if ETREE_VERSION[0] <= 1 and ETREE_VERSION[1] < 3: - - match_obj = re.search( - "([^\@]+?)\[\@([^=]*?)=\'([^\']*?)\'", subnode_xpath_expr) - if match_obj is not None: - - node_to_find = match_obj.group(1) - xpath_attrib = match_obj.group(2) - xpath_value = match_obj.group(3) - for node_found in xml_node.findall(node_to_find): - if node_found.attrib[xpath_attrib] == xpath_value: - node = node_found - break - else: - node = xml_node.find(subnode_xpath_expr) - else: - node = xml_node.find(subnode_xpath_expr) + node = xml_node.find(subnode_xpath_expr) if node is not None: return node.get(attrib_name) @@ -156,8 +128,8 @@ class DnsreconPlugin(PluginBase): Example plugin to parse dnsrecon output. """ - def __init__(self): - super().__init__() + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) self.id = "Dnsrecon" self.name = "Dnsrecon XML Output Plugin" self.plugin_version = "0.0.2" @@ -205,7 +177,7 @@ def parseOutputString(self, output): status="open") if host.zonetransfer == "success": - v_id = self.createAndAddVulnToService( + self.createAndAddVulnToService( h_id, s_id, name="Zone transfer", @@ -239,11 +211,6 @@ def processCommandString(self, username, current_path, command_string): r"--xml %s" % self._output_file_path, command_string) - def setHost(self): - pass - - -def createPlugin(): - return DnsreconPlugin() -# I'm Py3 +def createPlugin(*args, **kwargs): + return DnsreconPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/dnswalk/__init__.py b/faraday_plugins/plugins/repo/dnswalk/__init__.py index ea531e17..625a6e25 100644 --- a/faraday_plugins/plugins/repo/dnswalk/__init__.py +++ b/faraday_plugins/plugins/repo/dnswalk/__init__.py @@ -4,4 +4,4 @@ See the file 'doc/LICENSE' for the license information """ -# I'm Py3 \ No newline at end of file + diff --git a/faraday_plugins/plugins/repo/dnswalk/plugin.py b/faraday_plugins/plugins/repo/dnswalk/plugin.py index 8ae73dc5..43cfe241 100644 --- a/faraday_plugins/plugins/repo/dnswalk/plugin.py +++ b/faraday_plugins/plugins/repo/dnswalk/plugin.py @@ -5,11 +5,8 @@ """ import re -import os from faraday_plugins.plugins.plugin import PluginBase -from faraday_plugins.plugins.plugins_utils import resolve_hostname - __author__ = "Francisco Amato" __copyright__ = "Copyright (c) 2013, Infobyte LLC" @@ -34,15 +31,15 @@ class DnswalkParser: @param dnswalk_filepath A proper simple report generated by dnswalk """ - def __init__(self, output): + def __init__(self, output, resolve_hostName): lists = output.split("\n") self.items = [] + self.resolve_hostName = resolve_hostName for line in lists: - mregex = re.search("WARN: ([\w\.]+) ([\w]+) ([\w\.]+):", line) + mregex = re.search(r"WARN: ([\w\.]+) ([\w]+) ([\w\.]+):", line) if mregex is not None: - item = { 'host': mregex.group(1), 'ip': mregex.group(3), @@ -51,11 +48,11 @@ def __init__(self, output): self.items.append(item) mregex = re.search( - "Getting zone transfer of ([\w\.]+) from ([\w\.]+)\.\.\.done\.", + r"Getting zone transfer of ([\w\.]+) from ([\w\.]+)\.\.\.done\.", line) if mregex is not None: - ip = resolve_hostname(mregex.group(2)) + ip = self.resolve_hostname(mregex.group(2)) item = { 'host': mregex.group(1), 'ip': ip, @@ -63,14 +60,13 @@ def __init__(self, output): self.items.append(item) - class DnswalkPlugin(PluginBase): """ Example plugin to parse dnswalk output. """ - def __init__(self): - super().__init__() + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) self.id = "Dnswalk" self.name = "Dnswalk XML Output Plugin" self.plugin_version = "0.0.1" @@ -80,9 +76,9 @@ def __init__(self): self._command_regex = re.compile( r'^(sudo dnswalk|dnswalk|\.\/dnswalk)\s+.*?') - def canParseCommandString(self, current_input): if self._command_regex.match(current_input.strip()): + self.command = self.get_command(current_input) return True else: return False @@ -91,7 +87,7 @@ def parseOutputString(self, output): """ output is the shell output of command Dnswalk. """ - parser = DnswalkParser(output) + parser = DnswalkParser(output, self.resolve_hostname) for item in parser.items: if item['type'] == "A": @@ -113,7 +109,5 @@ def parseOutputString(self, output): return True -def createPlugin(): - return DnswalkPlugin() - -# I'm Py3 +def createPlugin(*args, **kwargs): + return DnswalkPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/faraday_csv/__init__.py b/faraday_plugins/plugins/repo/faraday_csv/__init__.py index f55478ec..4c2d40a8 100644 --- a/faraday_plugins/plugins/repo/faraday_csv/__init__.py +++ b/faraday_plugins/plugins/repo/faraday_csv/__init__.py @@ -3,4 +3,4 @@ Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/) See the file 'doc/LICENSE' for the license information """ -# I'm Py3 \ No newline at end of file + diff --git a/faraday_plugins/plugins/repo/faraday_csv/plugin.py b/faraday_plugins/plugins/repo/faraday_csv/plugin.py index 934313d7..77db4c57 100644 --- a/faraday_plugins/plugins/repo/faraday_csv/plugin.py +++ b/faraday_plugins/plugins/repo/faraday_csv/plugin.py @@ -1,12 +1,15 @@ """ Faraday Penetration Test IDE -Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/) +Copyright (C) 2013 Infobyte LLC (https://www.faradaysec.com/) See the file 'doc/LICENSE' for the license information """ +# Standard library imports +import sys import re import csv from ast import literal_eval +# Local application imports from faraday_plugins.plugins.plugin import PluginCSVFormat @@ -43,9 +46,15 @@ def __init__(self, csv_output, logger): "impact_availability", "impact_accountability", "policyviolations", + "cve", "custom_fields", "website", "path", + "cwe", + "cvss2_base_score", + "cvss2_vector_string", + "cvss3_base_score", + "cvss3_vector_string", "request", "response", "method", @@ -60,6 +69,7 @@ def __init__(self, csv_output, logger): def parse_csv(self, output): items = [] + csv.field_size_limit(sys.maxsize) reader = csv.DictReader(output, delimiter=',') obj_to_import = self.check_objects_to_import(reader.fieldnames) if not obj_to_import: @@ -70,9 +80,14 @@ def parse_csv(self, output): reader.fieldnames[index] = "target" custom_fields_names = self.get_custom_fields_names(reader.fieldnames) for row in reader: - self.data = {} - self.data['row_with_service'] = False - self.data['row_with_vuln'] = False + self.data = { + 'row_with_service': False, + 'row_with_vuln': False + } + # verify at least one field has data + if not any(row.values()): + self.logger.warning("Row with empty data found. Skipping...") + continue self.build_host(row) if "service" in obj_to_import: if row['port'] and row['protocol']: @@ -120,9 +135,9 @@ def check_objects_to_import(self, headers): if (port and not protocol) or (protocol and not port): self.logger.error( - ("Missing columns in CSV file. " + "Missing columns in CSV file. " "In order to import services, you need to add a column called port " - " and a column called protocol.") + " and a column called protocol." ) return None else: @@ -134,9 +149,9 @@ def check_objects_to_import(self, headers): if (vuln_name and not vuln_desc) or (vuln_desc and not vuln_name): self.logger.error( - ("Missing columns in CSV file. " + "Missing columns in CSV file. " "In order to import vulnerabilities, you need to add a " - "column called name and a column called desc.") + "column called name and a column called desc." ) return None else: @@ -144,10 +159,11 @@ def check_objects_to_import(self, headers): return obj_to_import - def get_custom_fields_names(self, headers): + @staticmethod + def get_custom_fields_names(headers): custom_fields_names = [] for header in headers: - match = re.match(r"cf_(\w+)", header) + match = re.match(r"cf_(\S+)", header) if match: custom_fields_names.append(match.group(1)) @@ -162,7 +178,7 @@ def build_host(self, row): if item in row: if item == "host_tags": - self.data[item] = literal_eval(row[item]) + self.data[item] = literal_eval(row[item] if row[item] else '[]') else: self.data[item] = row[item] else: @@ -196,7 +212,7 @@ def build_vulnerability(self, row, custom_fields_names): } if "web_vulnerability" in row: - self.data['web_vulnerability'] = True if row['web_vulnerability'] == "True" else False + self.data['web_vulnerability'] = True if row['web_vulnerability'].capitalize() == "True" else False else: self.data['web_vulnerability'] = False @@ -204,9 +220,11 @@ def build_vulnerability(self, row, custom_fields_names): if item in row: if "impact_" in item: impact = re.match(r"impact_(\w+)", item).group(1) - impact_dict[impact] = True if row[item] == "True" else False - elif item in ["refs", "policyviolations", "tags"]: - self.data[item] = literal_eval(row[item]) + impact_dict[impact] = True if row[item].capitalize() == "True" else False + elif item in ["refs", "policyviolations", "cve", "tags"]: + self.data[item] = literal_eval(row[item] if row[item] else '[]') + elif "confirmed" in item: + self.data[item] = True if row[item].capitalize() == "True" else False else: self.data[item] = row[item] else: @@ -224,7 +242,8 @@ def build_hostnames_list(self, row): self.logger.error("Hostname not valid. Faraday will set it as empty.") return hostnames - def parse_vuln_impact(self, impact): + @staticmethod + def parse_vuln_impact(impact): impacts = [ "accountability", "confidentiality", @@ -235,7 +254,10 @@ def parse_vuln_impact(self, impact): if item in impact: return item - def parse_custom_fields(self, row, custom_fields_names): + @staticmethod + def parse_custom_fields(row, custom_fields_names): + if not row: + return {} custom_fields = {} for cf_name in custom_fields_names: cf_value = row["cf_" + cf_name] @@ -248,8 +270,9 @@ def parse_custom_fields(self, row, custom_fields_names): class FaradayCSVPlugin(PluginCSVFormat): - def __init__(self): - super().__init__() + + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) self.id = "faraday_csv" self.name = "Faraday CSV Plugin" self.plugin_version = "1.0" @@ -284,6 +307,16 @@ def parseOutputString(self, output): tags=item['service_tags'] ) if item['row_with_vuln']: + cvss2 = { + "base_score": item['cvss2_base_score'], + } + if item['cvss2_vector_string']: + cvss2["vector_string"]= item['cvss2_vector_string'] + cvss3 = { + "base_score": item['cvss3_base_score'], + } + if item['cvss3_vector_string']: + cvss3["vector_string"]= item['cvss3_vector_string'] if not item['web_vulnerability'] and not s_id: self.createAndAddVulnToHost( h_id, @@ -294,11 +327,15 @@ def parseOutputString(self, output): resolution=item['resolution'], data=item['data'], external_id=item['external_id'], - confirmed=item['confirmed'] or False, + confirmed=item['confirmed'], status=item['status'] or "", easeofresolution=item['easeofresolution'] or None, impact=item['impact'], policyviolations=item['policyviolations'], + cve=item['cve'], + cwe=item['cwe'], + cvss2=cvss2, + cvss3=cvss3, custom_fields=item['custom_fields'], tags=item['tags'] ) @@ -313,11 +350,15 @@ def parseOutputString(self, output): resolution=item['resolution'], data=item['data'], external_id=item['external_id'], - confirmed=item['confirmed'] or False, + confirmed=item['confirmed'], status=item['status'] or "", easeofresolution=item['easeofresolution'] or None, impact=item['impact'], policyviolations=item['policyviolations'], + cve=item['cve'], + cwe=item['cwe'], + cvss2=cvss2, + cvss3=cvss3, custom_fields=item['custom_fields'], tags=item['tags'] ) @@ -340,16 +381,20 @@ def parseOutputString(self, output): query=item['query'], data=item['data'], external_id=item['external_id'], - confirmed=item['confirmed'] or False, + confirmed=item['confirmed'], status=item['status'] or "", easeofresolution=item['easeofresolution'] or None, impact=item['impact'], policyviolations=item['policyviolations'], + cve=item['cve'], + cwe=item['cwe'], + cvss2=cvss2, + cvss3=cvss3, status_code=item['status_code'] or None, custom_fields=item['custom_fields'], tags=item['tags'] ) -def createPlugin(): - return FaradayCSVPlugin() +def createPlugin(*args, **kwargs): + return FaradayCSVPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/faraday_json/__init__.py b/faraday_plugins/plugins/repo/faraday_json/__init__.py new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/faraday_plugins/plugins/repo/faraday_json/__init__.py @@ -0,0 +1 @@ + diff --git a/faraday_plugins/plugins/repo/faraday_json/plugin.py b/faraday_plugins/plugins/repo/faraday_json/plugin.py new file mode 100644 index 00000000..a043f463 --- /dev/null +++ b/faraday_plugins/plugins/repo/faraday_json/plugin.py @@ -0,0 +1,58 @@ +""" +Faraday Penetration Test IDE +Copyright (C) 2020 Infobyte LLC (http://www.infobytesec.com/) +See the file 'doc/LICENSE' for the license information + +""" +import json +from faraday_plugins.plugins.plugin import PluginJsonFormat + +__author__ = "Gonzalo Martinez" +__copyright__ = "Copyright (c) 2020, Infobyte LLC" +__credits__ = ["Gonzalo Martinez"] +__license__ = "" +__version__ = "1.0.0" +__maintainer__ = "Gonzalo Martinez" +__email__ = "gmartinez@faradaysec.com" +__status__ = "Development" + + +class FaradayJsonPlugin(PluginJsonFormat): + ''' + This is a plugin for Faraday's Pluin output + ''' + + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) + self.id = "Faraday_JSON" + self.name = "Faraday Json" + self.plugin_version = "1.0.0" + self.json_keys = {'hosts', 'command'} + + def parseOutputString(self, output): + parser = json.loads(output) + for host in parser.pop('hosts'): + services = host.pop('services', []) + vulns = host.pop('vulnerabilities', []) + host['name'] = host.pop('ip', '') + host.pop('credentials', '') + host_id = self.createAndAddHost(**host) + for vuln in vulns: + vuln['ref'] = vuln.pop('refs', '') + vuln.pop('type', '') + vuln.pop('run_date', '') + self.createAndAddVulnToHost(host_id=host_id, **vuln) + for service in services: + vulns = service.pop('vulnerabilities', []) + service['ports'] = service.pop('port', '') + service.pop('credentials', '') + service_id = self.createAndAddServiceToHost(host_id=host_id, **service) + for vuln in vulns: + vuln['ref'] = vuln.pop('refs', '') + vuln.pop('type', '') + vuln.pop('run_date', '') + self.createAndAddVulnWebToService(host_id=host_id, service_id=service_id, **vuln) + + +def createPlugin(*args, **kwargs): + return FaradayJsonPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/fierce/__init__.py b/faraday_plugins/plugins/repo/fierce/__init__.py index ea531e17..625a6e25 100644 --- a/faraday_plugins/plugins/repo/fierce/__init__.py +++ b/faraday_plugins/plugins/repo/fierce/__init__.py @@ -4,4 +4,4 @@ See the file 'doc/LICENSE' for the license information """ -# I'm Py3 \ No newline at end of file + diff --git a/faraday_plugins/plugins/repo/fierce/plugin.py b/faraday_plugins/plugins/repo/fierce/plugin.py index 25a7adfa..5d891254 100644 --- a/faraday_plugins/plugins/repo/fierce/plugin.py +++ b/faraday_plugins/plugins/repo/fierce/plugin.py @@ -5,11 +5,8 @@ """ import re -import os -import random from faraday_plugins.plugins.plugin import PluginBase -from faraday_plugins.plugins.plugins_utils import resolve_hostname __author__ = "Francisco Amato" @@ -42,7 +39,7 @@ def __init__(self, output): self.items = [] regex = re.search( - "DNS Servers for ([\w\.-]+):\n([^$]+)Trying zone transfer first...", + "DNS Servers for ([\\w\\.-]+):\n([^$]+)Trying zone transfer first...", output) if regex is not None: @@ -51,7 +48,7 @@ def __init__(self, output): self.dns = list(filter(None, mstr.splitlines())) regex = re.search( - "Now performing [\d]+ test\(s\)...\n([^$]+)\nSubnets found ", + "Now performing [\\d]+ test\\(s\\)...\n([^$]+)\nSubnets found ", output) if regex is not None: hosts_list = regex.group(1).splitlines() @@ -101,8 +98,8 @@ class FiercePlugin(PluginBase): Example plugin to parse fierce output. """ - def __init__(self): - super().__init__() + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) self.id = "Fierce" self.name = "Fierce Output Plugin" self.plugin_version = "0.0.1" @@ -119,16 +116,16 @@ def resolveCNAME(self, item, items): item['ip'] = i['ip'] return item try: - item['ip'] = resolve_hostname(item['ip']) - except: + item['ip'] = self.resolve_hostname(item['ip']) + except: # nosec pass return item def resolveNS(self, item, items): try: item['hosts'][0] = item['ip'] - item['ip'] = resolve_hostname(item['ip']) - except: + item['ip'] = self.resolve_hostname(item['ip']) + except: # nosec pass return item @@ -139,9 +136,9 @@ def parseOutputString(self, output): item['isResolver'] = False item['isZoneVuln'] = False - if (item['record'] == "CNAME"): + if item['record'] == "CNAME": self.resolveCNAME(item, parser.items) - if (item['record'] == "NS"): + if item['record'] == "NS": self.resolveNS(item, parser.items) item['isResolver'] = True item['isZoneVuln'] = parser.isZoneVuln @@ -175,8 +172,5 @@ def parseOutputString(self, output): ref=["CVE-1999-0532"]) - -def createPlugin(): - return FiercePlugin() - -# I'm Py3 +def createPlugin(*args, **kwargs): + return FiercePlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/fortify/plugin.py b/faraday_plugins/plugins/repo/fortify/plugin.py index 6da3afae..33ce5b87 100644 --- a/faraday_plugins/plugins/repo/fortify/plugin.py +++ b/faraday_plugins/plugins/repo/fortify/plugin.py @@ -14,8 +14,8 @@ class FortifyPlugin(PluginByExtension): Example plugin to parse nmap output. """ - def __init__(self): - super().__init__() + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) self.id = "Fortify" self.name = "Fortify XML Output Plugin" self.plugin_version = "0.0.1" @@ -58,7 +58,6 @@ def _process_webinspect_vulns(self, fp): service_name, protocol=protocol_name, ports=[vuln_data['service']['port']]) - self.createAndAddVulnWebToService( host_id, service_id, vuln_data['name'], @@ -85,7 +84,7 @@ def parseOutputString(self, output): class FortifyParser: - """ + """ Parser for fortify on demand """ @@ -222,18 +221,18 @@ def _process_webinspect(self): references.append(classification.text) # Build description - description = u'' + description = '' for report_section in issue_data.findall('./ReportSection'): - description += u'{} \n'.format(report_section.Name.text) - description += u'{} \n'.format(report_section.SectionText.text) - description += u'{} \n'.format(issue_data.get('id')) + description += f'{report_section.Name.text} \n' + description += f'{report_section.SectionText.text} \n' + description += f'{issue_data.get("id")} \n' h = html2text.HTML2Text() description = h.handle(description) for repro_step in issue_data.findall('./ReproSteps'): step = repro_step.ReproStep - + if step is not None: try: params = step.PostParams.text @@ -259,7 +258,7 @@ def _process_webinspect(self): "method": method, "query": query, "response": response, - "request": request, + "request": request.decode('utf-8'), "path": path, "params": params, "status_code": status_code, @@ -372,8 +371,8 @@ def format_description(self, vulnID): idx = match.group(1) if match.group(3): idx = match.group(3) - _filekey = "{}.file".format(idx) - _linekey = "{}.line".format(idx) + _filekey = f"{idx}.file" + _linekey = f"{idx}.line" text = text.replace(placeholder, "").replace( torepl.format(_filekey), replacements[_filekey]).replace( torepl.format(_linekey), replacements[_linekey]) @@ -387,11 +386,10 @@ def format_description(self, vulnID): text = text.replace(placeholder, replace_with) - text += '{}\n Instance ID: {} \n'.format(text, vulnID) + text += f'{text}\n Instance ID: {vulnID} \n' h = html2text.HTML2Text() return text -def createPlugin(): - return FortifyPlugin() - +def createPlugin(*args, **kwargs): + return FortifyPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/fruitywifi/fruitywifi.py b/faraday_plugins/plugins/repo/fruitywifi/fruitywifi.py deleted file mode 100755 index 93a16dd4..00000000 --- a/faraday_plugins/plugins/repo/fruitywifi/fruitywifi.py +++ /dev/null @@ -1,150 +0,0 @@ -#!/usr/bin/python -""" - Copyright (C) 2016 xtr4nge [_AT_] gmail.com - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -""" - -import sys -import getopt -import json -import requests - -requests.packages.urllib3.disable_warnings() # DISABLE SSL CHECK WARNINGS - -gVersion = "1.0" -server = "http://127.0.0.1:8000" -token = "e5dab9a69988dd65e578041416773149ea57a054" - - -def usage(): - print("\nFruityWiFi API " + gVersion + " by @xtr4nge") - - print("Usage: ./client \n") - print("Options:") - print("-x , --execute= exec the command passed as parameter.") - print("-t , --token= authentication token.") - print("-s , --server= FruityWiFi server [http{s}://ip:port].") - print("-h Print this help message.") - print("") - print("FruityWiFi: http://www.fruitywifi.com") - print("") - - -def parseOptions(argv): - - v_execute = "/log/dhcp" - v_token = token - v_server = server - - try: - opts, args = getopt.getopt(argv, "hx:t:s:", - ["help","execute=","token=","server="]) - - for opt, arg in opts: - if opt in ("-h", "--help"): - usage() - sys.exit() - elif opt in ("-x", "--execute"): - v_execute = arg - elif opt in ("-t", "--token"): - v_token = arg - elif opt in ("-s", "--server"): - v_server = arg - - return (v_execute, v_token, v_server) - - except getopt.GetoptError: - usage() - sys.exit(2) - - -(execute, token, server) = parseOptions(sys.argv[1:]) - - -class Webclient: - - def __init__(self, server, token): - - self.global_webserver = server - self.path = "/modules/api/includes/ws_action.php" - self.s = requests.session() - self.token = token - - def login(self): - - payload = { - 'action': 'login', - 'token': self.token - } - - self.s = requests.session() - self.s.get(self.global_webserver, verify=False) # DISABLE SSL CHECK - self.s.post(self.global_webserver + '/login.php', data=payload) - - def loginCheck(self): - - response = self.s.get(self.global_webserver + '/login_check.php') - - if response.text != "": - self.login() - - if response.text != "": - print(json.dumps("[FruityWiFi]: Ah, Ah, Ah! You didn't say the magic word! (check API token and server)")) - sys.exit() - - return True - - def submitPost(self, data): - response = self.s.post(self.global_webserver + data) - return response.json - - def submitGet(self, data): - response = self.s.get(self.global_webserver + self.path + "?" + data) - - return response - -try: - w = Webclient(server, token) - w.login() - w.loginCheck() -except Exception as e: - print(json.dumps("[FruityWiFi]: There is something wrong (%s)" % e)) - sys.exit(1) - -_exec = "/log/dhcp" -_exec = execute -if _exec != "": - try: - out = w.submitGet("api=" + str(_exec)) - json_output = out.json() - except Exception as e: - print(json.dumps("[FruityWiFi]: There is something wrong (%s)" % e)) - sys.exit(1) - -output = [] -if _exec == "/log/dhcp": - for item in json_output: - if item.strip() != "": - output = [item.split(" ")] -else: - output = json_output - -if len(output) > 0: - print(json.dumps(output)) -else: - print(json.dumps("No clients connected")) - - -# I'm Py3 diff --git a/faraday_plugins/plugins/repo/fruitywifi/plugin.py b/faraday_plugins/plugins/repo/fruitywifi/plugin.py deleted file mode 100644 index 86b6bfcb..00000000 --- a/faraday_plugins/plugins/repo/fruitywifi/plugin.py +++ /dev/null @@ -1,133 +0,0 @@ -""" -Faraday Penetration Test IDE -Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/) -See the file 'doc/LICENSE' for the license information - -""" -from faraday_plugins.plugins.plugin import PluginBase -import re -import json -import traceback -import os - -__author__ = "xtr4nge" -__copyright__ = "Copyright (c) 2016, FruityWiFi" -__credits__ = ["xtr4nge"] -__license__ = "" -__version__ = "1.0.0" -__maintainer__ = "xtr4nge" -__email__ = "@xtr4nge" -__status__ = "Development" - -class FruityWiFiPlugin(PluginBase): - """ - This plugin handles FruityWiFi clients. - """ - - def __init__(self): - super().__init__() - self.id = "fruitywifi" - self.name = "FruityWiFi" - self.plugin_version = "0.0.1" - self.version = "2.4" - self.description = "http://www.fruitywifi.com" - self.options = None - self._current_output = None - self.target = None - - self._command_regex = re.compile(r'^(fruitywifi)\s+.*?') - - self.addSetting("Token", str, "e5dab9a69988dd65e578041416773149ea57a054") - self.addSetting("Server", str, "http://127.0.0.1:8000") - self.addSetting("Severity", str, "high") - - def getSeverity(self, severity): - if severity.lower() == "critical" or severity == "4": - return 4 - elif severity.lower() == "high" or severity == "3": - return 3 - elif severity.lower() == "med" or severity == "2": - return 2 - elif severity.lower() == "low" or severity == "1": - return 1 - elif severity.lower() == "info" or severity == "0": - return 0 - else: - return 5 - - def createHostInterfaceVuln(self, ip_address, macaddress, hostname, desc, vuln_name, severity): - h_id = self.createAndAddHost(ip_address, hostnames=[hostname]) - - self.createAndAddVulnToHost( - h_id, - vuln_name, - desc=desc, - ref=["http://www.fruitywifi.com/"], - severity=severity - ) - - def parseOutputString(self, output): - - try: - output = json.loads(output) - - if len(output) > 0: - - if len(output[0]) == 3: - - severity = self.getSeverity(self.getSetting("Severity")) - - for item in output: - ip_address = item[0] - macaddress = item[1] - hostname = item[2] - vuln_name = "FruityWiFi" - severity = severity - - desc = "Client ip: " + ip_address + \ - " has been connected to FruityWiFi\n" - desc += "More information:" - desc += "\nname: " + hostname - - self.createHostInterfaceVuln(ip_address, macaddress, hostname, desc, vuln_name, severity) - - elif len(output[0]) == 5: - for item in output: - ip_address = item[0] - macaddress = item[1] - hostname = item[2] - vuln_name = item[3] - severity = item[4] - - desc = "Client ip: " + ip_address + \ - " has been connected to FruityWiFi\n" - desc += "More information:" - desc += "\nname: " + hostname - - self.createHostInterfaceVuln(ip_address, macaddress, hostname, desc, vuln_name, severity) - - except: - traceback.print_exc() - - return True - - def _isIPV4(self, ip): - if len(ip.split(".")) == 4: - return True - else: - return False - - def processCommandString(self, username, current_path, command_string): - """ - """ - super().processCommandString(username, current_path, command_string) - params = "-t %s -s %s" % (self.getSetting("Token"), self.getSetting("Server")) - - return "python " + os.path.dirname(__file__) + "/fruitywifi.py " + params - - - -def createPlugin(): - return FruityWiFiPlugin() - -# I'm Py3 diff --git a/faraday_plugins/plugins/repo/ftp/__init__.py b/faraday_plugins/plugins/repo/ftp/__init__.py index ea531e17..625a6e25 100755 --- a/faraday_plugins/plugins/repo/ftp/__init__.py +++ b/faraday_plugins/plugins/repo/ftp/__init__.py @@ -4,4 +4,4 @@ See the file 'doc/LICENSE' for the license information """ -# I'm Py3 \ No newline at end of file + diff --git a/faraday_plugins/plugins/repo/ftp/plugin.py b/faraday_plugins/plugins/repo/ftp/plugin.py index 81faf91e..712669c8 100644 --- a/faraday_plugins/plugins/repo/ftp/plugin.py +++ b/faraday_plugins/plugins/repo/ftp/plugin.py @@ -8,7 +8,6 @@ import os from faraday_plugins.plugins.plugin import PluginBase -from faraday_plugins.plugins.plugins_utils import resolve_hostname __author__ = "Javier Victor Mariano Bruno" @@ -27,8 +26,8 @@ class CmdFtpPlugin(PluginBase): Basically detects if user was able to connect to a device """ - def __init__(self): - super().__init__() + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) self.id = "ftp" self.name = "Ftp" self.plugin_version = "0.0.1" @@ -47,10 +46,10 @@ def __init__(self): def parseOutputString(self, output): host_info = re.search(r"Connected to (.+)\.", output) - banner = re.search("220?([\w\W]+)$", output) + banner = re.search(r"220?([\w\W]+)$", output) if re.search("Connection timed out", output) is None and host_info is not None: hostname = host_info.group(1) - ip_address = resolve_hostname(hostname) + ip_address = self.resolve_hostname(hostname) self._version = banner.groups(0) if banner else "" h_id = self.createAndAddHost(ip_address, hostnames=[hostname]) s_id = self.createAndAddServiceToHost( @@ -68,11 +67,9 @@ def processCommandString(self, username, current_path, command_string): count_args = command_string.split() c = count_args.__len__() self._port = "21" - if re.search("[\d]+", count_args[c - 1]): + if re.search(r"[\d]+", count_args[c - 1]): self._port = count_args[c - 1] -def createPlugin(): - return CmdFtpPlugin() - -# I'm Py3 +def createPlugin(*args, **kwargs): + return CmdFtpPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/gitleaks/__init__.py b/faraday_plugins/plugins/repo/gitleaks/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/faraday_plugins/plugins/repo/gitleaks/plugin.py b/faraday_plugins/plugins/repo/gitleaks/plugin.py new file mode 100644 index 00000000..8cc325c2 --- /dev/null +++ b/faraday_plugins/plugins/repo/gitleaks/plugin.py @@ -0,0 +1,50 @@ +""" +Faraday Plugins +Copyright (c) 2021 Faraday Security LLC (https://www.faradaysec.com/) +See the file 'doc/LICENSE' for the license information + +""" +import json +from faraday_plugins.plugins.plugin import PluginJsonFormat + + +class GitleaksPlugin(PluginJsonFormat): + """ + Parse gitleaks JSON output + """ + + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) + self.id = 'gitleaks' + self.name = 'Gitleaks' + self.plugin_version = '0.1' + self.version = '1.0.0' + self.json_keys = {'Match', 'Secret', 'Commit', 'Author', 'Email', 'Date', 'RuleID', 'Fingerprint', 'File', 'Description'} + + def parseOutputString(self, output, debug=False): + json_output = json.loads(output) + for leak in json_output: + + description = f"Match: {leak.get('Match')}\n" \ + f"Secret: {leak.get('Secret')}\n" \ + f"Commit: {leak.get('Commit')}\n" \ + f"Author: {leak.get('Author')}\n" \ + f"Email: {leak.get('Email')}\n" \ + f"Date: {leak.get('Date')}\n" \ + f"RuleID: {leak.get('RuleID')}\n" \ + f"Fingerprint: {leak.get('Fingerprint')}\n" + + host_id = self.createAndAddHost( + name=leak.get('File'), + ) + self.createAndAddVulnToHost( + host_id, + name=leak.get('Description'), + desc=description, + severity='informational', + status='open', + ) + + +def createPlugin(*args, **kwargs): + return GitleaksPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/goohost/__init__.py b/faraday_plugins/plugins/repo/goohost/__init__.py index ea531e17..625a6e25 100644 --- a/faraday_plugins/plugins/repo/goohost/__init__.py +++ b/faraday_plugins/plugins/repo/goohost/__init__.py @@ -4,4 +4,4 @@ See the file 'doc/LICENSE' for the license information """ -# I'm Py3 \ No newline at end of file + diff --git a/faraday_plugins/plugins/repo/goohost/plugin.py b/faraday_plugins/plugins/repo/goohost/plugin.py index 4a1ac738..94b2bc58 100644 --- a/faraday_plugins/plugins/repo/goohost/plugin.py +++ b/faraday_plugins/plugins/repo/goohost/plugin.py @@ -7,7 +7,6 @@ import os from faraday_plugins.plugins.plugin import PluginBase -from faraday_plugins.plugins.plugins_utils import resolve_hostname __author__ = "Francisco Amato" __copyright__ = "Copyright (c) 2013, Infobyte LLC" @@ -30,9 +29,10 @@ class GoohostParser: @param goohost_scantype You could select scan type ip, mail or host """ - def __init__(self, output, goohost_scantype): + def __init__(self, output, goohost_scantype, resolve_hostname): self.items = [] + self.resolve_hostname = resolve_hostname lines = list(filter(None, output.split('\n'))) for line in lines: if goohost_scantype == 'ip': @@ -41,7 +41,7 @@ def __init__(self, output, goohost_scantype): self.add_host_info_to_items(item['ip'], item['host']) elif goohost_scantype == 'host': data = line.strip() - item = {'host': data, 'ip': resolve_hostname(data)} + item = {'host': data, 'ip': self.resolve_hostname(data)} self.add_host_info_to_items(item['ip'], item['host']) else: item = {'data': line} @@ -65,8 +65,8 @@ class GoohostPlugin(PluginBase): Example plugin to parse goohost output. """ - def __init__(self): - super().__init__() + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) self.id = "Goohost" self.name = "Goohost XML Output Plugin" self.plugin_version = "0.0.1" @@ -89,7 +89,7 @@ def parseOutputString(self, output): """ scantype = self.define_scantype_by_output(output) - parser = GoohostParser(output, scantype) + parser = GoohostParser(output, scantype, self.resolve_hostname) if scantype == 'host' or scantype == 'ip': for item in parser.items: h_id = self.createAndAddHost(item['ip'], hostnames=item['hosts']) @@ -111,7 +111,7 @@ def define_scantype_by_output(self, output): return 'ip' def get_report_path_from_output(self, command_output): - report_name = re.search("Results saved in file (\S+)", command_output) + report_name = re.search(r"Results saved in file (\S+)", command_output) if not report_name: return False else: @@ -129,6 +129,5 @@ def processOutput(self, command_output): self.parseOutputString(command_output) -def createPlugin(): - return GoohostPlugin() - +def createPlugin(*args, **kwargs): + return GoohostPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/grype/__init__.py b/faraday_plugins/plugins/repo/grype/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/faraday_plugins/plugins/repo/grype/plugin.py b/faraday_plugins/plugins/repo/grype/plugin.py new file mode 100644 index 00000000..0b29083c --- /dev/null +++ b/faraday_plugins/plugins/repo/grype/plugin.py @@ -0,0 +1,98 @@ +""" +Faraday Penetration Test IDE +Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/) +See the file 'doc/LICENSE' for the license information +""" + +import json +import re +from faraday_plugins.plugins.plugin import PluginJsonFormat + +__author__ = "Joachim Bauernberger" +__license__ = "MIT" +__version__ = "1.0.0" +__maintainer__ = "Joachim Bauernberger" +__email__ = "joachim.bauernberger@protonmail.com" +__status__ = "Development" + + +class GrypePlugin(PluginJsonFormat): + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) + self.id = 'grype' + self.name = 'Grype JSON Plugin' + self.plugin_version = '1.0.0' + self._command_regex = re.compile(r'^grype\s+.*') + self._use_temp_file = True + self._temp_file_extension = "json" + self.json_keys = [{"source", "matches", "descriptor"}, {"matches", "image"}] + + def parseOutputString(self, output, debug=True): + grype_json = json.loads(output) + if "userInput" in grype_json.get("source", {"target": ""}).get("target"): + name = grype_json["source"]["target"]["userInput"] + host_type = grype_json['source']['type'] + elif "tags" in grype_json.get("image", {}): + name = " ".join(grype_json["image"]["tags"]) + host_type = "Docker Image" + else: + name = grype_json["source"]["target"] + host_type = grype_json['source']['type'] + host_id = self.createAndAddHost(name, description=f"Type: {host_type}") + for match in grype_json['matches']: + name = match.get('vulnerability').get('id') + cve = name + references = [] + if match.get("relatedVulnerabilities"): + description = match["relatedVulnerabilities"][0].get('description') + references.append(match["relatedVulnerabilities"][0]["dataSource"]) + related_vuln = match["relatedVulnerabilities"][0] + severity = related_vuln["severity"].lower().replace("negligible", "info") + if related_vuln.get("links"): + for url in related_vuln["links"]: + references.append(url) + else: + for url in related_vuln["urls"]: + references.append(url) + else: + description = match.get('vulnerability').get('description', "Issues provided no description") + severity = match.get('vulnerability').get('severity').lower().replace("negligible", "info") + if match.get('vulnerability').get("links"): + for url in match.get('vulnerability')["links"]: + references.append(url) + else: + for url in match.get('vulnerability')["urls"]: + references.append(url) + if not match['artifact'].get('metadata'): + data = f"Artifact: {match['artifact']['name']}" \ + f"Version: {match['artifact']['version']} " \ + f"Type: {match['artifact']['type']}" + else: + if "Source" in match['artifact']['metadata']: + data = f"Artifact: {match['artifact']['name']} [{match['artifact']['metadata']['Source']}] " \ + f"Version: {match['artifact']['version']} " \ + f"Type: {match['artifact']['type']}" + elif "VirtualPath" in match['artifact']['metadata']: + data = f"Artifact: {match['artifact']['name']} [{match['artifact']['metadata']['VirtualPath']}] " \ + f"Version: {match['artifact']['version']} " \ + f"Type: {match['artifact']['type']}" + else: + data = f"Artifact: {match['artifact']['name']}" \ + f"Version: {match['artifact']['version']} " \ + f"Type: {match['artifact']['type']}" + self.createAndAddVulnToHost(host_id, + name=name, + desc=description, + ref=references, + severity=severity, + data=data, + cve=cve) + + def processCommandString(self, username, current_path, command_string): + super().processCommandString(username, current_path, command_string) + command_string += f" -o json --file {self._output_file_path}" + return command_string + + +def createPlugin(*args, **kwargs): + return GrypePlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/hping3/__init__.py b/faraday_plugins/plugins/repo/hping3/__init__.py index ea531e17..625a6e25 100644 --- a/faraday_plugins/plugins/repo/hping3/__init__.py +++ b/faraday_plugins/plugins/repo/hping3/__init__.py @@ -4,4 +4,4 @@ See the file 'doc/LICENSE' for the license information """ -# I'm Py3 \ No newline at end of file + diff --git a/faraday_plugins/plugins/repo/hping3/plugin.py b/faraday_plugins/plugins/repo/hping3/plugin.py index 6ba684e9..fcf27370 100644 --- a/faraday_plugins/plugins/repo/hping3/plugin.py +++ b/faraday_plugins/plugins/repo/hping3/plugin.py @@ -14,8 +14,8 @@ class hping3(PluginBase): - def __init__(self): - super().__init__() + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) self.id = "Hping3" self.name = "hping3" self.plugin_version = "0.0.1" @@ -59,7 +59,7 @@ def parseOutputString(self, output): for linea in lineas: if (re.match(" ", linea)): - list = re.findall("\w+", linea) + list = re.findall(r"\w+", linea) service = list[1] port = [list[0]] @@ -68,7 +68,5 @@ def parseOutputString(self, output): host_id, service, protocol="tcp", ports=port, status="open") -def createPlugin(): - return hping3() - -# I'm Py3 +def createPlugin(*args, **kwargs): + return hping3(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/hydra/__init__.py b/faraday_plugins/plugins/repo/hydra/__init__.py index ea531e17..625a6e25 100644 --- a/faraday_plugins/plugins/repo/hydra/__init__.py +++ b/faraday_plugins/plugins/repo/hydra/__init__.py @@ -4,4 +4,4 @@ See the file 'doc/LICENSE' for the license information """ -# I'm Py3 \ No newline at end of file + diff --git a/faraday_plugins/plugins/repo/hydra/plugin.py b/faraday_plugins/plugins/repo/hydra/plugin.py index 3e8ca486..7d6d1495 100644 --- a/faraday_plugins/plugins/repo/hydra/plugin.py +++ b/faraday_plugins/plugins/repo/hydra/plugin.py @@ -4,7 +4,6 @@ See the file 'doc/LICENSE' for the license information """ from faraday_plugins.plugins.plugin import PluginBase -from faraday_plugins.plugins.plugins_utils import resolve_hostname import re from collections import defaultdict @@ -31,7 +30,7 @@ def __init__(self, xml_output): for line in lines: reg = re.search( - "\[([^$]+)\]\[([^$]+)\] host: ([^$]+) login: ([^$]+) password: ([^$]+)", + r"\[([^$]+)\]\[([^$]+)\] host: ([^$]+) login: ([^$]+) password: ([^$]+)", line) if reg: @@ -50,8 +49,8 @@ class HydraPlugin(PluginBase): Example plugin to parse hydra output. """ - def __init__(self): - super().__init__() + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) self.id = "Hydra" self.name = "Hydra XML Output Plugin" self.plugin_version = "0.0.1" @@ -84,7 +83,7 @@ def parseOutputString(self, output): hosts[item['ip']].append([item['login'], item['password']]) for k, v in hosts.items(): - ip = resolve_hostname(k) + ip = self.resolve_hostname(k) if ip != k: hostnames = [k] else: @@ -108,7 +107,7 @@ def parseOutputString(self, output): h_id, s_id, "Weak Credentials", - "[hydra found the following credentials]\nuser:%s\npass:%s" % (cred[0], cred[1]), + f"[hydra found the following credentials]\nuser:{cred[0]}\npass:{cred[1]}", severity="high") del parser @@ -127,11 +126,8 @@ def _isIPV4(self, ip): else: return False - def setHost(self): - pass -def createPlugin(): - return HydraPlugin() -# I'm Py3 +def createPlugin(*args, **kwargs): + return HydraPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/impact/__init__.py b/faraday_plugins/plugins/repo/impact/__init__.py index ea531e17..625a6e25 100644 --- a/faraday_plugins/plugins/repo/impact/__init__.py +++ b/faraday_plugins/plugins/repo/impact/__init__.py @@ -4,4 +4,4 @@ See the file 'doc/LICENSE' for the license information """ -# I'm Py3 \ No newline at end of file + diff --git a/faraday_plugins/plugins/repo/impact/plugin.py b/faraday_plugins/plugins/repo/impact/plugin.py index ddf72d90..a6a19fb1 100644 --- a/faraday_plugins/plugins/repo/impact/plugin.py +++ b/faraday_plugins/plugins/repo/impact/plugin.py @@ -4,19 +4,11 @@ See the file 'doc/LICENSE' for the license information """ -import re -from faraday_plugins.plugins.plugin import PluginXMLFormat -try: - import xml.etree.cElementTree as ET - import xml.etree.ElementTree as ET_ORIG - ETREE_VERSION = ET_ORIG.VERSION -except ImportError: - import xml.etree.ElementTree as ET - ETREE_VERSION = ET.VERSION - -ETREE_VERSION = [int(i) for i in ETREE_VERSION.split(".")] +import xml.etree.ElementTree as ET +from faraday_plugins.plugins.plugin import PluginXMLFormat +from faraday_plugins.plugins.plugins_utils import CVE_regex __author__ = "Francisco Amato" __copyright__ = "Copyright (c) 2013, Infobyte LLC" @@ -42,7 +34,7 @@ class ImpactXmlParser: def __init__(self, xml_output): tree = self.parse_xml(xml_output) if tree: - self.items = [data for data in self.get_items(tree)] + self.items = self.get_items(tree) else: self.items = [] @@ -104,7 +96,6 @@ def __init__(self, item_node, parent=None): agentip = node.get('name').split("/")[1] if self.ip == agentip: - self.agentip = agentip self.ipfrom = self.get_text_from_subnode( @@ -170,11 +161,17 @@ def get_text_from_subnode(self, subnode_xpath_expr): return None -class Results(): +class Results: def __init__(self, issue_node): self.node = issue_node - self.ref = [issue_node.get("key")] + match = CVE_regex.match(issue_node.get("key", "")) + if match: + self.ref = [] + self.cve = [match.group()] + else: + self.ref = [issue_node.get("key")] + self.cve = [] self.severity = "" self.port = "Unknown" self.service_name = "n/a" @@ -213,8 +210,8 @@ class ImpactPlugin(PluginXMLFormat): Example plugin to parse impact output. """ - def __init__(self): - super().__init__() + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) self.identifier_tag = "entities" self.id = "CoreImpact" self.name = "Core Impact XML Output Plugin" @@ -228,7 +225,7 @@ def parseOutputString(self, output): mapped_services = {} mapped_ports = {} for item in parser.items: - os_string = f"{item.os} {item.arch }" + os_string = f"{item.os} {item.arch}" h_id = self.createAndAddHost(item.ip, os=os_string, hostnames=[item.host]) for service in item.services: @@ -261,7 +258,8 @@ def parseOutputString(self, output): v.name, desc=v.desc, severity=v.severity, - ref=v.ref) + ref=v.ref, + cve=v.cve) else: s_id = mapped_services.get(v.service_name) or mapped_ports.get(v.port) self.createAndAddVulnToService( @@ -270,7 +268,8 @@ def parseOutputString(self, output): v.name, desc=v.desc, severity=v.severity, - ref=v.ref) + ref=v.ref, + cve=v.cve) for p in item.ports: s_id = self.createAndAddServiceToHost( @@ -282,11 +281,5 @@ def parseOutputString(self, output): del parser - def setHost(self): - pass - - -def createPlugin(): - return ImpactPlugin() - - +def createPlugin(*args, **kwargs): + return ImpactPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/invicti/DTO.py b/faraday_plugins/plugins/repo/invicti/DTO.py new file mode 100644 index 00000000..5a0c95cd --- /dev/null +++ b/faraday_plugins/plugins/repo/invicti/DTO.py @@ -0,0 +1,151 @@ +from typing import List + + +class Cvss3: + def __init__(self, node): + self.node = node + + @property + def vector(self) -> str: + return self.node.find('vector').text + + +class Reference: + def __init__(self, node): + self.node = node + + @property + def owasp(self) -> str: + return self.node.find('owasp').text + + @property + def wasc(self) -> str: + return self.node.find('wasc').text + + @property + def cwe(self) -> str: + return self.node.find('cwe').text + + @property + def capec(self) -> str: + return self.node.find('capec').text + + @property + def pci32(self) -> str: + return self.node.find('pci32').text + + @property + def hipaa(self) -> str: + return self.node.find('hipaa').text + + @property + def owasppc(self) -> str: + return self.node.find('owasppc').text + + @property + def cvss3(self) -> Cvss3: + return Cvss3(self.node.find("cvss31")) + + +class Request: + def __init__(self, node): + self.node = node + + @property + def method(self) -> str: + return self.node.find("method").text + + @property + def content(self) -> str: + return self.node.find("content").text + + +class Response: + def __init__(self, node): + self.node = node + + @property + def content(self) -> str: + return self.node.find("content").text + + +class Vulnerability: + def __init__(self, node): + self.node = node + + @property + def look_id(self) -> str: + return self.node.find('LookupId').text + + @property + def url(self) -> str: + return self.node.find("url").text + + @property + def name(self) -> str: + return self.node.find('name').text + + @property + def severity(self) -> str: + sv = self.node.find('severity').text + if sv == "BestPractice": + sv = "Information" + return sv + + @property + def confirmed(self) -> str: + return self.node.find('confirmed').text + + @property + def description(self) -> str: + return self.node.find('description').text + + @property + def http_request(self) -> Request: + return Request(self.node.find("http-request")) + + @property + def http_response(self) -> Response: + return Response(self.node.find("http-response")) + + @property + def impact(self) -> str: + return self.node.find("impact").text + + @property + def remedial_actions(self) -> str: + return self.node.find("remedial-actions").text + + @property + def remedial_procedure(self) -> str: + return self.node.find("remedial-procedure").text + + @property + def classification(self) -> Reference: + return Reference(self.node.find("classification")) + + +class Target: + def __init__(self, node): + self.node = node + + @property + def scan_id(self) -> str: + return self.node.find("scan-id").text + + @property + def url(self) -> str: + return self.node.find("url").text + + +class Invicti: + def __init__(self, node): + self.node = node + + @property + def target(self) -> Target: + return Target(self.node.find('target')) + + @property + def vulnerabilities(self) -> List[Vulnerability]: + return [Vulnerability(i) for i in self.node.findall('vulnerabilities/vulnerability')] diff --git a/faraday_plugins/plugins/repo/invicti/__init__.py b/faraday_plugins/plugins/repo/invicti/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/faraday_plugins/plugins/repo/invicti/plugin.py b/faraday_plugins/plugins/repo/invicti/plugin.py new file mode 100644 index 00000000..5405330c --- /dev/null +++ b/faraday_plugins/plugins/repo/invicti/plugin.py @@ -0,0 +1,125 @@ +""" +Faraday Penetration Test IDE +Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/) +See the file 'doc/LICENSE' for the license information + +""" +from urllib.parse import urlsplit +from bs4 import BeautifulSoup +from lxml import etree + +from faraday_plugins.plugins.plugin import PluginXMLFormat +from faraday_plugins.plugins.repo.invicti.DTO import Invicti + +__author__ = "Gonzalo Martinez" +__copyright__ = "Copyright (c) 2013, Infobyte LLC" +__credits__ = ["Gonzalo Martinez"] +__version__ = "1.0.0" +__maintainer__ = "Gonzalo Martinez" +__email__ = "gmartinez@infobytesec.com" +__status__ = "Development" + + +class InvictiXmlParser: + """ + The objective of this class is to parse a xml file generated by + the acunetix tool. + + @param invicti_xml_filepath A proper xml generated by acunetix + """ + + def __init__(self, xml_output): + + tree = self.parse_xml(xml_output) + self.invicti = Invicti(tree) + + @staticmethod + def parse_xml(xml_output): + """ + Open and parse an xml file. + + TODO: Write custom parser to just read the nodes that we need instead + of reading the whole file. + + @return xml_tree An xml tree instance. None if error. + """ + + try: + parser = etree.XMLParser(recover=True) + tree = etree.fromstring(xml_output, parser=parser) + except SyntaxError as err: + print(f"SyntaxError: {err}. {xml_output}") + return None + + return tree + + +class InvictiPlugin(PluginXMLFormat): + """ + Example plugin to parse invicti output. + """ + + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) + self.identifier_tag = "invicti-enterprise" + self.id = "Invicti" + self.name = "Invicti XML Output Plugin" + self.plugin_version = "1.0.0" + self.version = "9" + self.framework_version = "1.0.0" + self.options = None + self._current_output = None + self.target = None + + def parseOutputString(self, output): + """ + This method will discard the output the shell sends, it will read it + from the xml where it expects it to be present. + + NOTE: if 'debug' is true then it is being run from a test case and the + output being sent is valid. + """ + parser = InvictiXmlParser(output) + url = urlsplit(parser.invicti.target.url) + ip = self.resolve_hostname(url.netloc) + h_id = self.createAndAddHost(ip) + s_id = self.createAndAddServiceToHost(h_id, url.scheme, ports=433) + for vulnerability in parser.invicti.vulnerabilities: + vuln = { + "name": vulnerability.name, + "severity": vulnerability.severity, + "confirmed": vulnerability.confirmed, + "desc": BeautifulSoup(vulnerability.description, features="lxml").text, + "path": vulnerability.url.replace(parser.invicti.target.url, ""), + "external_id": vulnerability.look_id + } + if vulnerability.remedial_procedure: + vuln["resolution"] = BeautifulSoup(vulnerability.remedial_procedure, features="lxml").text + if vulnerability.classification: + references = [] + if vulnerability.classification.owasp: + references.append("OWASP" + vulnerability.classification.owasp) + if vulnerability.classification.wasc: + references.append("WASC" + vulnerability.classification.wasc) + if vulnerability.classification.cwe: + vuln["cwe"] = "CWE-" + vulnerability.classification.cwe + if vulnerability.classification.capec: + references.append("CAPEC" + vulnerability.classification.capec) + if vulnerability.classification.pci32: + references.append("PCI32" + vulnerability.classification.pci32) + if vulnerability.classification.hipaa: + references.append("HIPAA" + vulnerability.classification.hipaa) + if vulnerability.classification.owasppc: + references.append("OWASPPC" + vulnerability.classification.owasppc) + if vulnerability.classification.cvss3.node is not None: + vuln["cvss3"] = {"vector_string": vulnerability.classification.cvss3.vector} + vuln["ref"] = references + if vulnerability.http_response.node is not None: + vuln["response"] = vulnerability.http_response.content + if vulnerability.http_request.node is not None: + vuln["request"] = vulnerability.http_request.content + self.createAndAddVulnWebToService(h_id, s_id, **vuln) + + +def createPlugin(*args, **kwargs): + return InvictiPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/ip360/__init__.py b/faraday_plugins/plugins/repo/ip360/__init__.py index 454b1c0a..729fe446 100644 --- a/faraday_plugins/plugins/repo/ip360/__init__.py +++ b/faraday_plugins/plugins/repo/ip360/__init__.py @@ -3,4 +3,4 @@ Copyright (C) 2018 Infobyte LLC (http://www.infobytesec.com/) See the file 'doc/LICENSE' for the license information """ -# I'm Py3 \ No newline at end of file + diff --git a/faraday_plugins/plugins/repo/ip360/plugin.py b/faraday_plugins/plugins/repo/ip360/plugin.py index 3df01e6a..eb5159e1 100644 --- a/faraday_plugins/plugins/repo/ip360/plugin.py +++ b/faraday_plugins/plugins/repo/ip360/plugin.py @@ -68,8 +68,8 @@ class Ip360Plugin(PluginBase): Example plugin to parse Ip360 output. """ - def __init__(self): - super().__init__() + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) self.id = "Ip360" self.name = "Ip360 CSV Output Plugin" self.plugin_version = "0.0.1" @@ -103,7 +103,5 @@ def parseOutputString(self, output): ref=vulnerability.get("ref")) -def createPlugin(): - return Ip360Plugin() - -# I'm Py3 +def createPlugin(*args, **kwargs): + return Ip360Plugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/junit/__init__.py b/faraday_plugins/plugins/repo/junit/__init__.py index ea531e17..625a6e25 100644 --- a/faraday_plugins/plugins/repo/junit/__init__.py +++ b/faraday_plugins/plugins/repo/junit/__init__.py @@ -4,4 +4,4 @@ See the file 'doc/LICENSE' for the license information """ -# I'm Py3 \ No newline at end of file + diff --git a/faraday_plugins/plugins/repo/junit/plugin.py b/faraday_plugins/plugins/repo/junit/plugin.py index 958383ff..b88476ac 100644 --- a/faraday_plugins/plugins/repo/junit/plugin.py +++ b/faraday_plugins/plugins/repo/junit/plugin.py @@ -5,19 +5,10 @@ """ import os -from lxml import etree -from faraday_plugins.plugins.plugin import PluginXMLFormat +from lxml import etree -try: - import xml.etree.cElementTree as ET - import xml.etree.ElementTree as ET_ORIG - ETREE_VERSION = ET_ORIG.VERSION -except ImportError: - import xml.etree.ElementTree as ET - ETREE_VERSION = ET.VERSION - -ETREE_VERSION = [int(i) for i in ETREE_VERSION.split(".")] +from faraday_plugins.plugins.plugin import PluginXMLFormat current_path = os.path.abspath(os.getcwd()) @@ -32,7 +23,7 @@ This plugin has been designed to be used with python-unittest2/paramiko script to perform security compliancy verification. It enables to have displayed both security scans results (nmap, nessus, ..) and security verification compliancy (CIS-CAT, compagny's product security requirement) by Faraday-IPE -This plugin requires that a element "host" is added to (sed -i 's/ (sed -i 's/ @@ -67,7 +58,7 @@ def __init__(self, xml_output): self.items = [data for data in self.get_items(tree)] else: self.items = [] - + def parse_xml(self, xml_output): """ Open and parse an xml file. @@ -77,7 +68,7 @@ def parse_xml(self, xml_output): try: tree = etree.fromstring(xml_output) except SyntaxError as err: - print("SyntaxError: %s. %s" % (err, xml_output)) + print(f"SyntaxError: {err}. {xml_output}") return None return tree @@ -122,8 +113,8 @@ class JunitPlugin(PluginXMLFormat): Example plugin to parse junit output. """ - def __init__(self): - super().__init__() + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) self.id = "Junit" self.name = "Junit XML Output Plugin" self.plugin_version = "0.0.1" @@ -133,7 +124,6 @@ def __init__(self): self._current_output = None def parseOutputString(self, output): - parser = JunitXmlParser(output) for item in parser.items: h_id = self.createAndAddHost(item.host, os="Linux") @@ -141,5 +131,5 @@ def parseOutputString(self, output): del parser -def createPlugin(): - return JunitPlugin() +def createPlugin(*args, **kwargs): + return JunitPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/kubescape/__init__.py b/faraday_plugins/plugins/repo/kubescape/__init__.py new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/faraday_plugins/plugins/repo/kubescape/__init__.py @@ -0,0 +1 @@ + diff --git a/faraday_plugins/plugins/repo/kubescape/plugin.py b/faraday_plugins/plugins/repo/kubescape/plugin.py new file mode 100644 index 00000000..2234d6c5 --- /dev/null +++ b/faraday_plugins/plugins/repo/kubescape/plugin.py @@ -0,0 +1,92 @@ +""" +Faraday Penetration Test IDE +Copyright (C) 2020 Infobyte LLC (http://www.infobytesec.com/) +See the file 'doc/LICENSE' for the license information + +""" +import re +import json +from faraday_plugins.plugins.plugin import PluginJsonFormat + +__author__ = "Gonzalo Martinez" +__copyright__ = "Copyright (c) 2023, Infobyte LLC" +__credits__ = ["Gonzalo Martinez"] +__license__ = "" +__version__ = "1.0.0" +__maintainer__ = "Gonzalo Martinez" +__email__ = "gmartinez@faradaysec.com" +__status__ = "Development" + +SCORE_KUBESCAPE_RANGE = [(0, 1, 'info'), + (1, 4, 'low'), + (4, 7, 'med'), + (7, 9, 'high'), + (9, 10.1, 'critical')] + + +class KubescapePlugin(PluginJsonFormat): + + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) + self.id = "Kubescape_JSON" + self.name = "Kubescape Json" + self.plugin_version = "1.0.0" + self.version = "2.9" + self.json_keys = {'clusterAPIServerInfo', 'generationTime', 'results'} + self._use_temp_file = True + self._temp_file_extension = "json" + + @staticmethod + def get_severity_from_score(score): + try: + if not isinstance(score, float): + score = float(score) + + for (lower, upper, severity) in SCORE_KUBESCAPE_RANGE: + if lower <= score < upper: + return severity + except ValueError: + return 'unclassified' + + def parseOutputString(self, output): + data = json.loads(output) + resources_ids = {} + for resource in data['resources']: + object_data = resource["object"] + if 'name' in object_data: + name = object_data.get('name') + else: + name = object_data.get('metadata', {}).get('name') + resources_ids[resource['resourceID']] = name + controls = data['summaryDetails']['controls'] + for result in data['results']: + for control in result['controls']: + if not control.get('status', {}).get('status', '') == 'failed': + continue + h_id = self.createAndAddHost(name=resources_ids[result['resourceID']]) + desc = 'Control\' Rules:\n' + for rule in control['rules']: + desc += f'Rule Name: {rule["name"]}\n' + desc += f'Rule status: {rule["status"]}\n' + if 'paths' in rule: + desc += 'Paths:\n' + for path in rule.get('paths', []): + if 'failedPath' in path: + desc += f'Failed Path: {path["failedPath"]}\n' + if 'fixPath' in path: + desc += f'Fix Path: {path["fixPath"]["path"]}\n' + desc += f'Value: {path["fixPath"]["value"]}\n' + severity = self.get_severity_from_score( + controls[control['controlID']]['scoreFactor'] + ) + self.createAndAddVulnToHost( + host_id=h_id, + name=control['name'], + desc=desc, + severity=severity, + external_id=control['controlID'], + ) + + +def createPlugin(*args, **kwargs): + return KubescapePlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/lynis/__init__.py b/faraday_plugins/plugins/repo/lynis/__init__.py index 00dc0fca..d417d2e7 100644 --- a/faraday_plugins/plugins/repo/lynis/__init__.py +++ b/faraday_plugins/plugins/repo/lynis/__init__.py @@ -4,4 +4,4 @@ See the file 'doc/LICENSE' for the license information """ -# I'm Py3 \ No newline at end of file + diff --git a/faraday_plugins/plugins/repo/lynis/plugin.py b/faraday_plugins/plugins/repo/lynis/plugin.py index cf8c6efa..b30f97b5 100644 --- a/faraday_plugins/plugins/repo/lynis/plugin.py +++ b/faraday_plugins/plugins/repo/lynis/plugin.py @@ -37,16 +37,16 @@ def osfullname(self): return " ".join([name, version]) def ipv4(self): - ipv4s = re.findall('^network_ipv4_address\[\]=(.+)$', - self.rawcontents, re.MULTILINE) + ipv4s = re.findall(r'^network_ipv4_address\[\]=(.+)$', + self.rawcontents, re.MULTILINE) ipv4addrs = self.ipv4_filter(ipv4s) - return(ipv4addrs) + return ipv4addrs def ipv6(self): - ipv6s = re.findall('^network_ipv6_address\[\]=(.+)$', - self.rawcontents, re.MULTILINE) + ipv6s = re.findall(r'^network_ipv6_address\[\]=(.+)$', + self.rawcontents, re.MULTILINE) ipv6addrs = self.ipv6_filter(ipv6s) - return(ipv6addrs) + return ipv6addrs def ipv4_filter(self, ips): ip_list = [] @@ -75,8 +75,8 @@ def kernelVersion(self): return versions_dict def listeningservices(self): - line = re.findall('^network_listen_port\[\]=(.+)$', - self.rawcontents, re.MULTILINE) + line = re.findall(r'^network_listen_port\[\]=(.+)$', + self.rawcontents, re.MULTILINE) # To avoid local services, we will create the following list local_services = ['*', 'localhost'] @@ -91,7 +91,6 @@ def listeningservices(self): def clean_services(self, combo, local_services): add = False - #if "localhost" in combo: if combo.count("|") > 1: # Service with url, protocol and perhaps name items_service = combo.split('|') @@ -172,8 +171,8 @@ def get_protocol(self): def search_service(self, port): srv = filter_services() details_dict = { - 'name' : 'Unknown', - 'protocol' : 'Unknown' + 'name': 'Unknown', + 'protocol': 'Unknown' } for item in srv: service_tuple = item[0].split('/') @@ -197,32 +196,32 @@ def colon_count(self, count, elements_ip_port, items_service): #Ipv6 elif count == 5: port = elements_ip_port[5] - ip = items_service[0].replace(':{}'.format(port), '') + ip = items_service[0].replace(f':{port}', '') return ip, port def parse_suggestions(self): sugs = {} - m = re.findall('^suggestion\[\]=(.+)$', self.rawcontents, re.MULTILINE) + m = re.findall(r'^suggestion\[\]=(.+)$', self.rawcontents, re.MULTILINE) for combo in m: x = combo.split('|') sugs[x[0]] = x[1] - return(sugs) + return sugs def parse_warnings(self): warns = {} - m = re.findall('^warning\[\]=(.+)$', self.rawcontents, re.MULTILINE) + m = re.findall(r'^warning\[\]=(.+)$', self.rawcontents, re.MULTILINE) for combo in m: x = combo.split('|') warns[x[0]] = x[1] - return(warns) + return warns class LynisPlugin(PluginByExtension): """ Simple example plugin to parse lynis' lynis-report.dat file.""" - def __init__(self): - super().__init__() + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) self.id = "Lynis" self.name = "Lynis DAT Output Plugin" self.plugin_version = "0.4" @@ -259,8 +258,8 @@ def parseOutputString(self, output): for ipv4 in ipv4s: h_id = self.createAndAddHost(name=ipv4, - os=lde.osfullname(), - hostnames=[hostname]) + os=lde.osfullname(), + hostnames=[hostname]) self.create_services(h_id, services, ipv4) self.create_vulns_with_kernel(h_id, kernel_versions) @@ -269,8 +268,8 @@ def parseOutputString(self, output): for ipv6 in ipv6s: h_id = self.createAndAddHost(name=ipv6, - os=lde.osfullname(), - hostnames=[hostname]) + os=lde.osfullname(), + hostnames=[hostname]) self.create_services(h_id, services, ipv6) self.create_vulns_with_kernel(h_id, kernel_versions) @@ -280,16 +279,16 @@ def parseOutputString(self, output): def create_services(self, host_id, parsed_services, ip_version): for service_data in parsed_services[ip_version]: self.createAndAddServiceToHost(host_id=host_id, - name=service_data['name'], - protocol=service_data['protocol'], - ports=[service_data['port']]) + name=service_data['name'], + protocol=service_data['protocol'], + ports=[service_data['port']]) if '0.0.0.0' in parsed_services: for service_data in parsed_services['0.0.0.0']: self.createAndAddServiceToHost(host_id=host_id, - name=service_data['name'], - protocol=service_data['protocol'], - ports=[service_data['port']]) + name=service_data['name'], + protocol=service_data['protocol'], + ports=[service_data['port']]) def create_vulns_with_kernel(self, host_id, kernel_versions): for kernel, version in kernel_versions.items(): @@ -319,12 +318,10 @@ def create_vulns_with_warns(self, host_id, warns): ) def processOutput(self, command_output): - m = re.search('(\/.+\.dat)$', command_output, re.MULTILINE) + m = re.search(r'(\/.+\.dat)$', command_output, re.MULTILINE) file_path = m.group(0).strip() self._parse_filename(file_path) -def createPlugin(): - return LynisPlugin() - - +def createPlugin(*args, **kwargs): + return LynisPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/maltego/__init__.py b/faraday_plugins/plugins/repo/maltego/__init__.py index ea531e17..625a6e25 100644 --- a/faraday_plugins/plugins/repo/maltego/__init__.py +++ b/faraday_plugins/plugins/repo/maltego/__init__.py @@ -4,4 +4,4 @@ See the file 'doc/LICENSE' for the license information """ -# I'm Py3 \ No newline at end of file + diff --git a/faraday_plugins/plugins/repo/maltego/plugin.py b/faraday_plugins/plugins/repo/maltego/plugin.py index b1d2113e..dd7109e8 100755 --- a/faraday_plugins/plugins/repo/maltego/plugin.py +++ b/faraday_plugins/plugins/repo/maltego/plugin.py @@ -3,23 +3,10 @@ Copyright (C) 2015 Infobyte LLC (http://www.infobytesec.com/) See the file 'doc/LICENSE' for the license information """ -from faraday_plugins.plugins.plugin import PluginZipFormat -import re -import os +import xml.etree.ElementTree as ET import zipfile -from faraday_plugins.plugins.plugins_utils import resolve_hostname - -try: - import xml.etree.cElementTree as ET - import xml.etree.ElementTree as ET_ORIG - ETREE_VERSION = ET_ORIG.VERSION -except ImportError: - import xml.etree.ElementTree as ET - ETREE_VERSION = ET.VERSION - -ETREE_VERSION = [int(i) for i in ETREE_VERSION.split(".")] - +from faraday_plugins.plugins.plugin import PluginZipFormat __author__ = "Ezequiel Tavella" __copyright__ = "Copyright (c) 2015, Infobyte LLC" @@ -108,7 +95,7 @@ def readMtgl(mtgl_file): return check_files -class Host(): +class Host: def __init__(self): self.ip = "" @@ -121,10 +108,11 @@ def __init__(self): self.ns_record = "" -class MaltegoParser(): +class MaltegoParser: - def __init__(self, xml_file, extension): + def __init__(self, xml_file, extension, resolve_hostname): + self.resolve_hostname = resolve_hostname if extension == '.mtgx': self.xml = readMtgx(xml_file) self.nodes = self.xml.findall( @@ -171,7 +159,6 @@ def getIpAndId(self, node): "{http://graphml.graphdrawing.org/xmlns}data/" "{http://maltego.paterva.com/xml/mtgx}MaltegoEntity") - # Check if is IPv4Address if entity.get("type") not in ("maltego.IPv4Address", "maltego.Domain", "maltego.Website"): return None @@ -182,7 +169,7 @@ def getIpAndId(self, node): "{http://maltego.paterva.com/xml/mtgx}Property/" "{http://maltego.paterva.com/xml/mtgx}Value") if entity.get("type") in ("maltego.Domain", "maltego.Website"): - ip = resolve_hostname(value.text) + ip = self.resolve_hostname(value.text) hostname = value.text else: ip = value.text @@ -370,8 +357,8 @@ def getInfoMtgl(self, xml, name): class MaltegoPlugin(PluginZipFormat): - def __init__(self): - super().__init__() + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) self.identifier_tag = "maltego" self.id = "Maltego" self.name = "Maltego MTGX & MTGL Output Plugin" @@ -390,7 +377,7 @@ def __init__(self): def parseOutputString(self, output): if 'Graphs/Graph1.graphml' in output.namelist(): - maltego_parser = MaltegoParser(output, self.extension[1]) + maltego_parser = MaltegoParser(output, self.extension[1], resolve_hostname=self.resolve_hostname) hosts = maltego_parser.parse() if not hosts: self.logger.warning("No hosts data found in maltego report") @@ -419,7 +406,7 @@ def parseOutputString(self, output): try: text = f'Location:\n {host.location["name"]} \nArea:\n {host.location["area"]} ' \ f'\nArea 2:\n {host.location["area_2"]} ' \ - f'\nCountry_code:\n { host.location["country_code"]} ' \ + f'\nCountry_code:\n {host.location["country_code"]} ' \ f'\nLatitude:\n {host.location["latitude"]} \nLongitude:\n {host.location["longitude"]}' except TypeError: text = "unknown" @@ -453,7 +440,7 @@ def parseOutputString(self, output): self.createAndAddServiceToHost(host_id=host_id, name=host.ns_record["value"], protocol="DNS", ports=[53], description="DNS Server") else: - maltego_parser = MaltegoParser(output, self.extension[0]) + maltego_parser = MaltegoParser(output, self.extension[0], resolve_hostname=self.resolve_hostname) if not maltego_parser.xml.get('domain') or not maltego_parser.xml.get('ipv4'): return if maltego_parser.xml.get('domain'): @@ -467,7 +454,6 @@ def parseOutputString(self, output): host_ip = '0.0.0.0' host_id = self.createAndAddHost(name=host_ip, hostnames=hostnames) - if maltego_parser.xml.get('location'): location_name = maltego_parser.getInfoMtgl(maltego_parser.xml['location'], 'location.name') location_area = maltego_parser.getInfoMtgl(maltego_parser.xml['location'], 'location.area') @@ -510,7 +496,5 @@ def parseOutputString(self, output): description="DNS Server") - - -def createPlugin(): - return MaltegoPlugin() +def createPlugin(*args, **kwargs): + return MaltegoPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/mbsa/__init__.py b/faraday_plugins/plugins/repo/mbsa/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/faraday_plugins/plugins/repo/mbsa/plugin.py b/faraday_plugins/plugins/repo/mbsa/plugin.py new file mode 100644 index 00000000..2d95cd06 --- /dev/null +++ b/faraday_plugins/plugins/repo/mbsa/plugin.py @@ -0,0 +1,118 @@ +""" +Faraday Penetration Test IDE +Copyright (C) 2015 Infobyte LLC (http://www.infobytesec.com/) +See the file 'doc/LICENSE' for the license information +""" +from faraday_plugins.plugins.plugin import PluginByExtension +import re +from datetime import datetime + +__author__ = "Blas Moyano" +__copyright__ = "Copyright (c) 2020, Infobyte LLC" +__credits__ = ["Blas Moyano"] +__license__ = "" +__version__ = "1.0.0" +__maintainer__ = "Blas Moyano" +__status__ = "Development" + + +class MbsaParser: + def __init__(self, log_output): + self.computer_name = re.search('(Computer name:) (.*[A-Z])', log_output) + self.ip = re.search(r'(IP address:) ([0-9]+(?:\.[0-9]+){3})', log_output) + self.scan_date = re.search('(Scan date:) (.*[0-9])', log_output) + self.issues = re.findall(r'Issue: .*', log_output) + self.score = re.findall(r'Score: .*', log_output) + self.result = re.findall(r'Result: .*', log_output) + + +class MbsaPlugin(PluginByExtension): + + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) + self.id = "MBSA" + self.name = "Microsoft Baseline Security Analyzer" + self.plugin_version = "1.0.1" + self.version = "MBSA 1.0" + self.framework_version = "1.0.0" + self.extension = ".log" + + def parseOutputString(self, output): + parser = MbsaParser(output) + detail = '' + i = 0 + issues_top = len(parser.issues) + ip = '0.0.0.0' + hostname = [] + run_date = None + + if parser.ip is not None: + ip = parser.ip.group(2) + if parser.computer_name is not None: + hostname.append(parser.computer_name.group(2)) + if parser.scan_date is not None: + run_date = datetime.strptime(parser.scan_date.group(2), '%Y/%m/%d %H:%M') + + host_id = self.createAndAddHost( + ip, + 'Windows', + hostnames=hostname) + + for issue in parser.issues: + + test = re.search(parser.issues[i], output) + + if i+1 != issues_top: + test_issue = re.search(parser.issues[i+1], output) + else: + end = None + try: + start = test.end() + end = test_issue.start() + except: + start = None + + if start is not None: + if end is None: + result_info = output[start:] + else: + result_info = output[start:end] + result_info.rstrip('\n') + result_info = result_info.replace(parser.score[i], '') + result_info = result_info.replace(parser.result[i], '') + result_info = result_info.strip() + if result_info: + detail = re.search('(Detail:)', result_info) + if not None: + detail = result_info + result_info = parser.result[i] + + else: + detail = '' + result_info = parser.result[i] + score = parser.score[i].replace('Score: ', '').strip() + if score != 'Check passed': + if score == 'Best practice' or score == 'Unable to scan': + severity = "info" + elif score == 'Check failed (non-critical)': + severity = 'med' + elif score == 'Check failed': + severity = 'high' + else: + severity = 'info' + + self.createAndAddVulnToHost( + host_id, + issue.replace('Issue: ', '').strip(), + desc=result_info.replace('Result: ', '').strip(), + ref=None, + severity=severity, + data=detail, + run_date=run_date + ) + + i += 1 + + +def createPlugin(*args, **kwargs): + return MbsaPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/medusa/__init__.py b/faraday_plugins/plugins/repo/medusa/__init__.py index ea531e17..625a6e25 100644 --- a/faraday_plugins/plugins/repo/medusa/__init__.py +++ b/faraday_plugins/plugins/repo/medusa/__init__.py @@ -4,4 +4,4 @@ See the file 'doc/LICENSE' for the license information """ -# I'm Py3 \ No newline at end of file + diff --git a/faraday_plugins/plugins/repo/medusa/plugin.py b/faraday_plugins/plugins/repo/medusa/plugin.py index 7aa69d62..22a64259 100644 --- a/faraday_plugins/plugins/repo/medusa/plugin.py +++ b/faraday_plugins/plugins/repo/medusa/plugin.py @@ -5,7 +5,6 @@ """ import re from faraday_plugins.plugins.plugin import PluginBase -from faraday_plugins.plugins.plugins_utils import resolve_hostname __author__ = "Francisco Amato" @@ -25,7 +24,8 @@ class MedusaParser: @param medusa_filepath A proper simple report generated by medusa """ - def __init__(self, xml_output): + def __init__(self, xml_output, resolve_hostname): + self.resolve_hostname = resolve_hostname self.srv = { 'ftp': '21', 'http': '80', 'imap': '143', 'mssql': '1433', 'mysql': '3306', 'ncp': '524', 'nntp': '119', 'pcanywhere': '5631', 'pop3': '110', 'postgres': '5432', @@ -37,12 +37,12 @@ def __init__(self, xml_output): lines = xml_output.splitlines() self.items = [] - + for line in lines: - reg = re.search("ACCOUNT FOUND: \[([^$]+)\] Host: ([^$]+) User: ([^$]+) Password: ([^$]+) \[SUCCESS\]", line) + reg = re.search(r"ACCOUNT FOUND: \[([^$]+)\] Host: ([^$]+) User: ([^$]+) Password: ([^$]+) \[SUCCESS\]", line) if reg: - + item = { 'service': reg.group(1), 'host': reg.group(2), @@ -59,8 +59,8 @@ class MedusaPlugin(PluginBase): Example plugin to parse medusa output. """ - def __init__(self): - super().__init__() + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) self.id = "Medusa" self.name = "Medusa Output Plugin" self.plugin_version = "0.0.1" @@ -81,10 +81,10 @@ def parseOutputString(self, output): NOTE: if 'debug' is true then it is being run from a test case and the output being sent is valid. """ - parser = MedusaParser(output) - + parser = MedusaParser(output, resolve_hostname=self.resolve_hostname) + for item in parser.items: - + h_id = self.createAndAddHost(item['ip'], hostnames=[item['host']]) port = self.port if self.port else item['port'] @@ -105,7 +105,7 @@ def parseOutputString(self, output): self.createAndAddVulnToService(h_id, s_id, "Weak Credentials", - "[medusa found the following credentials]\nuser:%s\npass:%s" % (item['user'], item['pass']), + f"[medusa found the following credentials]\nuser:{item['user']}\npass:{item['pass']}", severity="high") del parser @@ -130,9 +130,8 @@ def _isIPV4(self, ip): else: return False - def setHost(self): - pass -def createPlugin(): - return MedusaPlugin() + +def createPlugin(*args, **kwargs): + return MedusaPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/metasploit/__init__.py b/faraday_plugins/plugins/repo/metasploit/__init__.py index ea531e17..625a6e25 100644 --- a/faraday_plugins/plugins/repo/metasploit/__init__.py +++ b/faraday_plugins/plugins/repo/metasploit/__init__.py @@ -4,4 +4,4 @@ See the file 'doc/LICENSE' for the license information """ -# I'm Py3 \ No newline at end of file + diff --git a/faraday_plugins/plugins/repo/metasploit/plugin.py b/faraday_plugins/plugins/repo/metasploit/plugin.py index 914d8238..78fea215 100644 --- a/faraday_plugins/plugins/repo/metasploit/plugin.py +++ b/faraday_plugins/plugins/repo/metasploit/plugin.py @@ -3,19 +3,9 @@ Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/) See the file 'doc/LICENSE' for the license information """ -from faraday_plugins.plugins.plugin import PluginXMLFormat -import re - +import xml.etree.ElementTree as ET -try: - import xml.etree.cElementTree as ET - import xml.etree.ElementTree as ET_ORIG - ETREE_VERSION = ET_ORIG.VERSION -except ImportError: - import xml.etree.ElementTree as ET - ETREE_VERSION = ET.VERSION - -ETREE_VERSION = [int(i) for i in ETREE_VERSION.split(".")] +from faraday_plugins.plugins.plugin import PluginXMLFormat __author__ = "Francisco Amato" __copyright__ = "Copyright (c) 2013, Infobyte LLC" @@ -68,7 +58,7 @@ def parse_xml(self, xml_output): try: tree = ET.fromstring(xml_output) except SyntaxError as err: - print("SyntaxError: %s. %s" % (err, xml_output)) + print(f"SyntaxError: {err}. {xml_output}") return None return tree @@ -77,7 +67,6 @@ def get_items(self, tree, webVulns): """ @return items A list of Host instances """ - bugtype = "" for node in tree.findall('hosts/host'): yield Host(node, webVulns) @@ -86,7 +75,6 @@ def get_vulns(self, tree, services): """ @return items A list of WebVuln instances """ - bugtype = "" for node in tree.findall('web_vulns/web_vuln'): yield WebVuln(node, services) @@ -97,26 +85,8 @@ def get_attrib_from_subnode(xml_node, subnode_xpath_expr, attrib_name): @return An attribute value """ - global ETREE_VERSION - node = None - - if ETREE_VERSION[0] <= 1 and ETREE_VERSION[1] < 3: - - match_obj = re.search( - "([^\@]+?)\[\@([^=]*?)=\'([^\']*?)\'", subnode_xpath_expr) - if match_obj is not None: - node_to_find = match_obj.group(1) - xpath_attrib = match_obj.group(2) - xpath_value = match_obj.group(3) - for node_found in xml_node.findall(node_to_find): - if node_found.attrib[xpath_attrib] == xpath_value: - node = node_found - break - else: - node = xml_node.find(subnode_xpath_expr) - else: - node = xml_node.find(subnode_xpath_expr) + node = xml_node.find(subnode_xpath_expr) if node is not None: return node.get(attrib_name) @@ -211,7 +181,11 @@ def __init__(self, item_node, services): self.query = self.get_text_from_subnode('query') self.request = self.get_text_from_subnode('request') self.category = self.get_text_from_subnode('category-id') - self.service_id = services[self.get_text_from_subnode('web-site-id')] + web_id = self.get_text_from_subnode('web-site-id') + self.service_id = None + if web_id: + self.service_id = services[web_id] + self.isWeb = True def get_text_from_subnode(self, subnode_xpath_expr): @@ -220,6 +194,7 @@ def get_text_from_subnode(self, subnode_xpath_expr): @return An attribute value """ + sub_node = self.node.find(subnode_xpath_expr) if sub_node is not None: if sub_node.text is not None: @@ -301,7 +276,16 @@ def __init__(self, item_node): self.service_id = self.get_text_from_subnode('service-id') self.name = self.get_text_from_subnode('name') self.desc = self.get_text_from_subnode('info') - self.refs = [r.text for r in self.node.findall('refs/ref')] + self.refs = [] + self.cve = [] + self.cwe = [] + for r in self.node.findall('refs/ref'): + if r.text.startswith('CVE'): + self.cve.append(r.text) + elif r.text.startswith('CWE'): + self.cwe.append(r.text) + else: + self.refs.append(r.text) self.exploited_date = self.get_text_from_subnode('exploited-at') self.exploited = (self.exploited_date is not None) self.isWeb = False @@ -325,8 +309,8 @@ class MetasploitPlugin(PluginXMLFormat): Example plugin to parse metasploit output. """ - def __init__(self): - super().__init__() + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) self.identifier_tag = ["MetasploitV4", "MetasploitV5"] self.id = "Metasploit" self.name = "Metasploit XML Output Plugin" @@ -336,7 +320,6 @@ def __init__(self): self.options = None self.target = None - def parseOutputString(self, output): """ This method will discard the output the shell sends, it will read it from @@ -349,7 +332,7 @@ def parseOutputString(self, output): self.hostnames = [] if item.host: self.hostnames = [item.host] - + h_id = self.createAndAddHost(item.ip, os=item.os, hostnames=self.hostnames) if item.id + "_" in item.notesByService: @@ -357,53 +340,48 @@ def parseOutputString(self, output): self.createAndAddNoteToHost(h_id, n.ntype, n.data) for v in item.vulnsByHost: - v_id = self.createAndAddVulnToHost( - h_id, v.name, v.desc, ref=v.refs) + self.createAndAddVulnToHost( + h_id, v.name, v.desc, ref=v.refs, cve=v.cve) for s in item.services: s_id = self.createAndAddServiceToHost(h_id, s['name'], - protocol=s['proto'], - ports=[s['port']], - status=s['state'], - description=s['info']) + protocol=s['proto'], + ports=[s['port']], + status=s['state'], + description=s['info']) if item.id + "_" + s['id'] in item.notesByService: for n in item.notesByService[item.id + "_" + s['id']]: self.createAndAddNoteToService( h_id, s_id, n.ntype, n.data) - if s['port'] in item.credsByService: for c in item.credsByService[s['port']]: self.createAndAddCredToService( h_id, s_id, c.user, c.passwd) - self.createAndAddVulnToService(h_id, s_id, "Weak Credentials", "[metasploit found the following credentials]\nuser:%s\npass:%s" % ( - c.user, c.passwd), severity="high") + self.createAndAddVulnToService(h_id, s_id, "Weak Credentials", + "[metasploit found the following credentials]\nuser:{}\npass:{}".format( + c.user, c.passwd), severity="high") for v in item.vulnsByService[s['id']]: if v.isWeb: - v_id = self.createAndAddVulnWebToService(h_id, s_id, v.name, v.desc, + self.createAndAddVulnWebToService(h_id, s_id, v.name, v.desc, severity=v.risk, website=v.host, path=v.path, request=v.request, method=v.method, pname=v.pname, params=v.params, query=v.query, category=v.category) else: - v_id = self.createAndAddVulnToService( - h_id, s_id, v.name, v.desc, ref=v.refs) + self.createAndAddVulnToService( + h_id, s_id, v.name, v.desc, ref=v.refs, cve=v.cve, cwe=v.cwe) del parser - def _isIPV4(self, ip): + @staticmethod + def _isIPV4(ip): if len(ip.split(".")) == 4: return True else: return False - def setHost(self): - pass - - -def createPlugin(): - return MetasploitPlugin() - -# I'm Py3 +def createPlugin(*args, **kwargs): + return MetasploitPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/naabu/__init__.py b/faraday_plugins/plugins/repo/naabu/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/faraday_plugins/plugins/repo/naabu/plugin.py b/faraday_plugins/plugins/repo/naabu/plugin.py new file mode 100644 index 00000000..28e819fe --- /dev/null +++ b/faraday_plugins/plugins/repo/naabu/plugin.py @@ -0,0 +1,73 @@ +""" +Faraday Plugins +Copyright (c) 2021 Faraday Security LLC (https://www.faradaysec.com/) +See the file 'doc/LICENSE' for the license information + +""" +import socket +import json +import re +from faraday_plugins.plugins.plugin import PluginMultiLineJsonFormat + +__author__ = 'Emilio Couto' +__copyright__ = 'Copyright (c) 2021, Faraday Security LLC' +__credits__ = ['Emilio Couto'] +__license__ = '' +__version__ = '0.0.1' +__maintainer__ = 'Emilio Couto' +__email__ = 'ecouto@faradaysec.com' +__status__ = 'Development' + + +class NaabuPlugin(PluginMultiLineJsonFormat): + """ + Parse Naabu (from Project Discovery) scanner JSON output + """ + + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) + self.id = 'naabu' + self.name = 'Naabu' + self.plugin_version = '0.1' + self.version = '2.0.3' + self.json_keys = {'tls', 'protocol', 'host', 'ip', 'port'} + self._command_regex = re.compile(r'^(sudo naabu|naabu|\.\/nmap)\s+.*?') + + def parseOutputString(self, output, debug=False): + for host_json in filter(lambda x: x != '', output.split('\n')): + host_dict = json.loads(host_json) + host = host_dict.get('host') + ip = host_dict.get('ip') + port = host_dict.get('port') + try: + if isinstance(port, dict): + port = port.get("Port") + service = socket.getservbyport(port) + except OSError: + service = 'Unknown service on port ' + str(port) + host_id = self.createAndAddHost( + name=ip, + hostnames=[host]) + self.createAndAddServiceToHost( + host_id, + name=service, + ports=port, + protocol='tcp', + status='open', + version='', + description='') + + def processCommandString(self, username, current_path, command_string): + """ + Adds the -oX parameter to get xml output to the command string that the + user has set. + """ + super().processCommandString(username, current_path, command_string) + if " -json" not in command_string: + command_string += " -json" + if " -silent" not in command_string: + command_string += " -silent" + return command_string + +def createPlugin(*args, **kwargs): + return NaabuPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/ncrack/plugin.py b/faraday_plugins/plugins/repo/ncrack/plugin.py index aaa7542a..54e71a65 100644 --- a/faraday_plugins/plugins/repo/ncrack/plugin.py +++ b/faraday_plugins/plugins/repo/ncrack/plugin.py @@ -5,12 +5,9 @@ """ -from faraday_plugins.plugins.plugin import PluginXMLFormat -try: - import xml.etree.cElementTree as ET -except ImportError: - import xml.etree.ElementTree as ET +import xml.etree.ElementTree as ET +from faraday_plugins.plugins.plugin import PluginXMLFormat __author__ = "Blas Moyano" __copyright__ = "Copyright (c) 2020, Infobyte LLC" @@ -45,7 +42,7 @@ def parse_xml(self, xml_output): try: tree = ET.fromstring(xml_output) except SyntaxError as err: - print('SyntaxError In xml: %s. %s' % (err, xml_output)) + print(f'SyntaxError In xml: {err}. {xml_output}') return None return tree @@ -92,8 +89,9 @@ def get_service(self, tree): class NcrackPlugin(PluginXMLFormat): - def __init__(self): - super().__init__() + + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) self.identifier_tag = "ncrackrun" self.id = 'ncrack' self.name = 'ncrack XML Plugin' @@ -121,5 +119,5 @@ def parseOutputString(self, output): password=service_vuln['passw']) -def createPlugin(): - return NcrackPlugin() +def createPlugin(*args, **kwargs): + return NcrackPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/ndiff/__init__.py b/faraday_plugins/plugins/repo/ndiff/__init__.py index 81e2e0d9..cef3c4e2 100644 --- a/faraday_plugins/plugins/repo/ndiff/__init__.py +++ b/faraday_plugins/plugins/repo/ndiff/__init__.py @@ -4,4 +4,4 @@ See the file 'doc/LICENSE' for the license information """ -# I'm Py3 \ No newline at end of file + diff --git a/faraday_plugins/plugins/repo/ndiff/plugin.py b/faraday_plugins/plugins/repo/ndiff/plugin.py index f7263331..bd6ecbe8 100644 --- a/faraday_plugins/plugins/repo/ndiff/plugin.py +++ b/faraday_plugins/plugins/repo/ndiff/plugin.py @@ -7,11 +7,7 @@ from faraday_plugins.plugins.plugin import PluginBase import re -try: - import xml.etree.cElementTree as ET - import xml.etree.ElementTree as ET_ORIG -except ImportError: - import xml.etree.ElementTree as ET +import xml.etree.ElementTree as ET __author__ = 'Ezequiel Tavella' __copyright__ = 'Copyright (c) 2016, Infobyte LLC' @@ -22,7 +18,7 @@ __status__ = "Development" -class NdiffXmlParser(): +class NdiffXmlParser: """ The objective of this class is to parse an xml file generated by the ndiff tool. @@ -43,7 +39,7 @@ def parse_xml(self, xmlOutput): try: return ET.fromstring(xmlOutput) except SyntaxError as err: - print("SyntaxError: %s" % err) + print(f"SyntaxError: {err}") return None def getHostsDiffs(self, tree): @@ -109,8 +105,8 @@ class CmdNdiffPlugin(PluginBase): Add a new vuln INFO if detect a new host or a new port .. """ - def __init__(self): - super().__init__() + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) self.id = "Ndiff" self.name = "ndiff" self.plugin_version = "0.0.1" @@ -124,9 +120,9 @@ def parseOutputString(self, output): continue if host.isNewHost: hostId = self.createAndAddHost(host.ip, '') - description = '%s is a NEW host active.\n' % host.ip + description = f'{host.ip} is a NEW host active.\n' for port in host.ports: - description += 'Port: %s/%s\n' % (port[0], port[1]) + description += f'Port: {port[0]}/{port[1]}\n' self.createAndAddVulnToHost( hostId, 'New host active', @@ -140,7 +136,7 @@ def parseOutputString(self, output): hostId = self.createAndAddHost(host.ip, '') description = 'New service/s found.\n' for port in host.ports: - description += 'Port: %s/%s\n' % (port[0], port[1]) + description += f'Port: {port[0]}/{port[1]}\n' self.createAndAddVulnToHost( hostId, @@ -156,7 +152,5 @@ def processCommandString(self, username, current_path, command_string): return f"{command_string} --xml " -def createPlugin(): - return CmdNdiffPlugin() - -# I'm Py3 +def createPlugin(*args, **kwargs): + return CmdNdiffPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/nessus/DTO.py b/faraday_plugins/plugins/repo/nessus/DTO.py new file mode 100644 index 00000000..ac978f31 --- /dev/null +++ b/faraday_plugins/plugins/repo/nessus/DTO.py @@ -0,0 +1,395 @@ +from typing import List + + +class Attachment: + def __init__(self, node): + self.node = node + + @property + def name_attr(self): + return self.node.get("name") + + @property + def type_attr(self): + return self.node.get("type") + + @property + def text(self): + return self.node.text + + +class ReportItem: + def __init__(self, node): + self.node = node + + @property + def port_attr(self): + return self.node.get("port") + + @property + def svc_name_attr(self): + return self.node.get("svc_name") + + @property + def protocol_attr(self): + return self.node.get("protocol") + + @property + def severity_attr(self): + return self.node.get("severity") + + @property + def plugin_id_attr(self): + plugin_id = self.node.get("pluginID") + if plugin_id: + plugin_id = f'NESSUS-{plugin_id}' + return plugin_id + + @property + def plugin_name_attr(self): + return self.node.get("pluginName") + + @property + def plugin_family_attr(self): + return self.node.get("pluginFamily") + + @property + def agent(self): + return self.node.findtext("agent") + + @property + def description(self): + return self.node.findtext("description", "Not Description") + + @property + def fname(self): + return self.node.findtext("fname") + + @property + def plugin_modification_date(self): + return self.node.findtext("plugin_modification_date") + + @property + def plugin_name(self): + + plugin_name = self.node.findtext("plugin_name") + if not plugin_name: + plugin_name = self.plugin_name_attr + return plugin_name + + @property + def plugin_publication_date(self): + return self.node.findtext("plugin_publication_date") + + @property + def plugin_type(self): + return self.node.findtext("plugin_type") + + @property + def risk_factor(self): + risk_factor = self.node.findtext("risk_factor") + if risk_factor == 'None' or risk_factor is None: + risk_factor = self.severity_attr # I checked several external id and most of them were info + return risk_factor + + @property + def script_version(self): + return self.node.findtext("script_version") + + @property + def see_also(self): + return self.node.findtext("see_also") + + @property + def solution(self): + return self.node.findtext("solution", '') + + @property + def synopsis(self): + return self.node.findtext("synopsis") + + @property + def plugin_output(self): + return self.node.findtext("plugin_output", "") + + @property + def always_run(self): + return self.node.findtext("always_run") + + @property + def asset_inventory(self): + return self.node.findtext("asset_inventory") + + @property + def canvas_package(self): + return self.node.findtext("canvas_package") + + @property + def cvss3_base_score(self): + return self.node.findtext("cvss3_base_score") + + @property + def cvss3_temporal_score(self): + return self.node.findtext("cvss3_temporal_score") + + @property + def cpe(self): + return self.node.findtext("cpe") + + @property + def cvss3_temporal_vector(self): + return self.node.findtext("cvss3_temporal_vector") + + @property + def cvss3_vector(self): + return self.node.findtext("cvss3_vector") + + @property + def cvss2_base_score(self): + return self.node.findtext("cvss_base_score") + + @property + def cvss_score_rationale(self): + return self.node.findtext("cvss_score_rationale") + + @property + def cvss_score_source(self): + return self.node.findtext("cvss_score_source") + + @property + def cvss_temporal_score(self): + return self.node.findtext("cvss_temporal_score") + + @property + def cvss_temporal_vector(self): + return self.node.findtext("cvss_temporal_vector") + + @property + def cvss_vector(self): + cvss_vector = self.node.findtext("cvss_vector") + if cvss_vector: + cvss_vector = cvss_vector.replace("CVSS2#", "") + return cvss_vector + + @property + def exploit_available(self): + exploit_avalible = self.node.findtext("exploit_available", "") + if exploit_avalible: + exploit_avalible = f"Exploit available: {exploit_avalible.capitalize()}\n" + return exploit_avalible + + @property + def exploit_framework_canvas(self): + return self.node.findtext("exploit_framework_canvas") + + @property + def exploit_framework_core(self): + return self.node.findtext("exploit_framework_core") + + @property + def exploit_framework_d2_elliot(self): + return self.node.findtext("exploit_framework_d2_elliot") + + @property + def exploit_framework_metasploit(self): + return self.node.findtext("exploit_framework_metasploit") + + @property + def exploitability_ease(self): + return self.node.findtext("exploitability_ease") + + @property + def exploited_by_malware(self): + return self.node.findtext("exploited_by_malware") + + @property + def exploited_by_nessus(self): + return self.node.findtext("exploited_by_nessus") + + @property + def hardware_inventory(self): + return self.node.findtext("hardware_inventory") + + @property + def iava(self): + return self.node.findtext("iava") + + @property + def iavb(self): + return self.node.findtext("iavb") + + @property + def iavt(self): + return self.node.findtext("iavt") + + @property + def in_the_news(self): + return self.node.findtext("in_the_news") + + @property + def metasploit_name(self): + return self.node.findtext("metasploit_name") + + @property + def os_identification(self): + return self.node.findtext("os_identification") + + @property + def owasp(self): + return self.node.findtext("owasp") + + @property + def patch_publication_date(self): + return self.node.findtext("patch_publication_date") + + + @property + def stig_severity(self): + return self.node.findtext("stig_severity") + + @property + def d2_elliot_name(self): + return self.node.findtext("d2_elliot_name") + + @property + def unsupported_by_vendor(self): + return self.node.findtext("unsupported_by_vendor") + + @property + def vuln_publication_date(self): + return self.node.findtext("vuln_publication_date") + + @property + def msft(self): + return self.node.findtext("msft") + + + @property + def cert(self) -> list: + return self.node.findall("cert") + + @property + def bid(self) -> list: + return self.node.findall("bid") + + @property + def cve(self) -> list: + return [i.text for i in self.node.findall("cve")] + + @property + def cwe(self) -> list: + return ["CWE-"+i.text for i in self.node.findall("cwe")] + + @property + def edb_id(self) -> list: + return self.node.findall("edb-id") + + @property + def mskb(self) -> list: + return self.node.findall("mskb") + + @property + def xref(self) -> str: + return self.node.findtext("xref") + + @property + def attachment(self) -> Attachment: + attachment = self.node.find("attachment") + return Attachment(attachment) if attachment else None + + def get_data(self): + item_tags = {} + for i in self.node: + item_tags.setdefault(i.tag, i.text) + return item_tags + + +class Tag: + def __init__(self, node): + self.node = node + + @property + def name_attr(self) -> str: + return self.node.get("name") + + @property + def text(self) -> str: + return self.node.text + + +class HostProperties: + def __init__(self, node): + self.node = node + + @property + def tag(self) -> list: + return [Tag(i) for i in self.node.findall('tag')] + + @property + def host_end(self) -> str: + _dict = self.dict_tags + return _dict.get("HOST_END") + + @property + def mac_address(self) -> str: + _dict = self.dict_tags + return _dict.get("mac-address", "") + + @property + def operating_system(self) -> str: + _dict = self.dict_tags + return _dict.get("operating-system", None) + + @property + def host_ip(self) -> str: + _dict = self.dict_tags + + return _dict.get("host-ip", None) + + @property + def host_fqdn(self) -> str: + _dict = self.dict_tags + return _dict.get("host-fqdn", None) + + @property + def host_rdns(self) -> str: + _dict = self.dict_tags + return _dict.get("host-rdns", None) + + @property + def dict_tags(self): + host_tags = {} + for t in self.node: + host_tags.setdefault(t.attrib.get('name'), t.text) + return host_tags + + +class ReportHost: + def __init__(self, node): + self.node = node + + @property + def name(self) -> str: + return self.node.get("name") + + @property + def host_properties(self) -> HostProperties: + return HostProperties(self.node.find("HostProperties")) + + @property + def report_items(self) -> List[ReportItem]: + return [ReportItem(i) for i in self.node.findall("ReportItem")] + + +class Report: + + def __init__(self, node): + self.node = node + + @property + def name_attr(self) -> str: + return self.node.get("name") + + @property + def report_hosts(self) -> List[ReportHost]: + return [ReportHost(i) for i in self.node.findall('ReportHost')] diff --git a/faraday_plugins/plugins/repo/nessus/__init__.py b/faraday_plugins/plugins/repo/nessus/__init__.py index ea531e17..625a6e25 100644 --- a/faraday_plugins/plugins/repo/nessus/__init__.py +++ b/faraday_plugins/plugins/repo/nessus/__init__.py @@ -4,4 +4,4 @@ See the file 'doc/LICENSE' for the license information """ -# I'm Py3 \ No newline at end of file + diff --git a/faraday_plugins/plugins/repo/nessus/plugin.py b/faraday_plugins/plugins/repo/nessus/plugin.py index af2b6cd3..9fa054a8 100644 --- a/faraday_plugins/plugins/repo/nessus/plugin.py +++ b/faraday_plugins/plugins/repo/nessus/plugin.py @@ -4,21 +4,23 @@ See the file 'doc/LICENSE' for the license information """ -import dateutil -from faraday_plugins.plugins.plugin import PluginXMLFormat import xml.etree.ElementTree as ET +from dateutil.parser import parse +from faraday_plugins.plugins.plugin import PluginXMLFormat __author__ = "Blas" __copyright__ = "Copyright (c) 2019, Infobyte LLC" -__credits__ = ["Blas"] +__credits__ = ["Blas", "Nicolas Rebagliati"] __license__ = "" __version__ = "1.0.0" __maintainer__ = "Blas" __email__ = "bmoyano@infobytesec.com" __status__ = "Development" +from faraday_plugins.plugins.repo.nessus.DTO import ReportHost, Report, ReportItem +from faraday_plugins.plugins.plugins_utils import get_severity_from_cvss class NessusParser: """ @@ -33,139 +35,13 @@ class NessusParser: def __init__(self, output): self.tree = ET.fromstring(output) - self.tag_control = [] - for x in self.tree: - self.tag_control.append(x) + self.report = [] if self.tree: - self.policy = self.getPolicy(self.tree) - self.report = self.getReport(self.tree) - else: - self.policy = None - self.report = None - - def getPolicy(self, tree): - policy_tree = tree.find('Policy') - if policy_tree: - return Policy(policy_tree) - else: - return None - - def getReport(self, tree): - report_tree = tree.find('Report') - return Report(report_tree) - - -class Policy(): - def __init__(self, policy_node): - self.node = policy_node - self.policy_name = self.node.find('policyName').text - self.preferences = self.getPreferences(self.node.find('Preferences')) - self.family_selection = self.getFamilySelection(self.node.find('FamilySelection')) - self.individual_plugin_selection = self.getIndividualPluginSelection( - self.node.find('IndividualPluginSelection')) - - def getPreferences(self, preferences): - server_preferences = preferences.find('ServerPreferences') - plugins_preferences = preferences.find('PluginsPreferences') - server_preferences_all = [] - plugins_preferences_json = {} - plugins_preferences_all = [] - for sp in server_preferences: - sp_value = sp.find('value').text - sp_name = sp.find('name').text - server_preferences_all.append("Server Preferences name: {}, Server Preferences value: {}".format(sp_name, - sp_value)) - for pp in plugins_preferences: - for pp_detail in pp: - plugins_preferences_json.setdefault(pp_detail.tag, pp_detail.text) - plugins_preferences_all.append(plugins_preferences_json) - return server_preferences_all, plugins_preferences_all - - def getFamilySelection(self, family): - family_all = [] - for f in family: - family_name = f.find('FamilyName').text - family_value = f.find('Status').text - family_all.append("Family Name: {}, Family Value: {}".format(family_name, family_value)) - return family_all - - def getIndividualPluginSelection(self, individual): - item_plugin = [] - for i in individual: - plugin_id = i.find('PluginId').text - plugin_name = i.find('PluginName').text - plugin_family = i.find('Family').text - plugin_status = i.find('Status').text - item_plugin.append("Plugin ID: {}, Plugin Name: {}, Family: {}, Status: {}".format(plugin_id, plugin_name, - plugin_family, - plugin_status)) - return item_plugin + self.report = self.__get_report() - -class Report(): - def __init__(self, report_node): - self.node = report_node - self.report_name = self.node.attrib.get('name') - self.report_host = self.node.find('ReportHost') - self.report_desc = [] - self.report_ip = [] - self.report_serv = [] - self.report_json = {} - if self.report_host is not None: - for x in self.node: - self.report_host_ip = x.attrib.get('name') - self.host_properties = self.gethosttag(x.find('HostProperties')) - self.report_item = self.getreportitems(x.findall('ReportItem')) - self.report_ip.append(self.report_host_ip) - self.report_desc.append(self.host_properties) - self.report_serv.append(self.report_item) - self.report_json['ip'] = self.report_ip - self.report_json['desc'] = self.report_desc - self.report_json['serv'] = self.report_serv - self.report_json['host_end'] = self.host_properties.get('HOST_END') - - else: - self.report_host_ip = None - self.host_properties = None - self.report_item = None - self.report_json = None - - def getreportitems(self, items): - result_item = [] - - for item in items: - self.port = item.attrib.get('port') - self.svc_name = item.attrib.get('svc_name') - self.protocol = item.attrib.get('protocol') - self.severity = item.attrib.get('severity') - self.plugin_id = item.attrib.get('pluginID') - self.plugin_name = item.attrib.get('pluginName') - self.plugin_family = item.attrib.get('pluginFamily') - if item.find('plugin_output') is not None: - self.plugin_output = item.find('plugin_output').text - else: - self.plugin_output = "Not Description" - if item.find('description') is not None: - self.description = item.find('description').text - else: - self.description = "Not Description" - - self.info = self.getinfoitem(item) - result_item.append((self.port, self.svc_name, self.protocol, self.severity, self.plugin_id, - self.plugin_name, self.plugin_family, self.description, self.plugin_output, self.info)) - return result_item - - def getinfoitem(self, item): - item_tags = {} - for i in item: - item_tags.setdefault(i.tag, i.text) - return item_tags - - def gethosttag(self, tags): - host_tags = {} - for t in tags: - host_tags.setdefault(t.attrib.get('name'), t.text) - return host_tags + def __get_report(self) -> Report: + report = self.tree.find('Report') + return Report(report) if report else None class NessusPlugin(PluginXMLFormat): @@ -173,8 +49,8 @@ class NessusPlugin(PluginXMLFormat): Example plugin to parse nessus output. """ - def __init__(self): - super().__init__() + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) self.extension = ".nessus" self.identifier_tag = "NessusClientData_v2" self.id = "Nessus" @@ -184,6 +60,76 @@ def __init__(self): self.framework_version = "1.0.1" self.options = None + @staticmethod + def parse_compliance_data(data: dict): + compliance_data = {} + for key, value in data.items(): + if 'compliance-' in key: + compliance_name = key.split("}")[-1] + compliance_data[compliance_name] = value + return compliance_data + + def map_properties(self, host: ReportHost): + if self.hostname_resolution: + name = host.host_properties.host_ip if host.host_properties.host_ip else host.name + else: + name = host.name + hostnames = [host.host_properties.host_fqdn] + if host.host_properties.host_rdns and host.host_properties.host_rdns not in hostnames: + hostnames.append(host.host_properties.host_rdns) + return { + "name": name, + "hostnames": hostnames, + "mac": host.host_properties.mac_address, + "os": host.host_properties.operating_system + } + + @staticmethod + def map_item(host_id, run_date, plugin_name, item: ReportItem) -> dict: + data = item.plugin_output + data += f'{item.exploit_available}' + return { + "host_id": host_id, + "name": plugin_name, + "severity": item.risk_factor, + "data": data, + "external_id": item.plugin_id_attr, + "run_date": run_date, + "desc": item.description, + "resolution": item.solution, + "ref": [], + } + + def map_policy_general(self, kwargs, item: ReportItem): + kwargs.update({"policyviolations": []}) + if item.plugin_family_attr == 'Policy Compliance': + data = item.get_data() + bis_benchmark_data = kwargs["desc"].split('\n') + compliance_data = self.parse_compliance_data(data) + compliance_info = compliance_data.get('compliance-info', '') + if compliance_info and not kwargs["desc"]: + kwargs["desc"] = compliance_info + compliance_reference = compliance_data.get( + 'compliance-reference', '').replace('|', ':').split(',') + compliance_result = compliance_data.get('compliance-result', '') + for reference in compliance_reference: + kwargs["ref"].append(reference) + compliance_check_name = compliance_data.get('compliance-check-name', '') + compliance_solution = compliance_data.get('compliance-solution', '') + if compliance_solution and not kwargs["resolution"]: + kwargs["resolution"] = compliance_solution + policy_item = f'{compliance_check_name} - {compliance_result}' + for policy_check_data in bis_benchmark_data: + if 'ref.' in policy_check_data: + kwargs["ref"].append(policy_check_data) + if 'compliance-see-also' in compliance_data: + kwargs["ref"].append(compliance_data.get('compliance-see-also')) + # We used this info from tenable: https://community.tenable.com/s/article/Compliance-checks-in-SecurityCenter + kwargs["policyviolations"].append(policy_item) + kwargs["name"] = f'{kwargs["name"]}: {policy_item}' + + return kwargs + def parseOutputString(self, output): """ This method will discard the output the shell sends, it will read it from @@ -192,183 +138,68 @@ def parseOutputString(self, output): NOTE: if 'debug' is true then it is being run from a test case and the output being sent is valid. """ + try: parser = NessusParser(output) except Exception as e: - print(e) + self.logger.error(str(e)) return None - - if parser.report.report_json is not None: - run_date = parser.report.report_json.get('host_end') - if run_date: - run_date = dateutil.parser.parse(run_date) - for set_info, ip in enumerate(parser.report.report_json['ip'], start=1): - if 'mac-address' in parser.report.report_json['desc'][set_info - 1]: - mac = parser.report.report_json['desc'][set_info - 1]['mac-address'] - else: - mac = '' - if 'operating-system' in parser.report.report_json['desc'][set_info - 1]: - os = parser.report.report_json['desc'][set_info - 1]['operating-system'] - else: - os = None - - if 'host-ip' in parser.report.report_json['desc'][set_info - 1]: - ip_host = parser.report.report_json['desc'][set_info - 1]['host-ip'] - else: - ip_host = "0.0.0.0" - if 'host-fqdn' in parser.report.report_json['desc'][set_info - 1]: - website = parser.report.report_json['desc'][set_info - 1]['host-fqdn'] - host_name = [] - host_name.append(parser.report.report_json['desc'][set_info - 1]['host-fqdn']) - else: - website = None - host_name = None - - host_id = self.createAndAddHost(ip_host, os=os, hostnames=host_name, mac=mac) - cve = [] - for serv in parser.report.report_json['serv'][set_info -1]: - serv_name = serv[1] - serv_port = serv[0] - serv_protocol = serv[2] - serv_status = serv[3] - external_id = serv[4] - serv_description = serv[7] - cve.append(serv[8]) - severity = serv[3] - desc = serv[8] - - if serv_name == 'general': - ref = [] - vulnerability_name = serv[5] - data = serv[9] - if not data: - continue - if 'description' in data: - desc = data['description'] - else: - desc = "No description" - if 'solution' in data: - resolution = data['solution'] - else: - resolution = "No Solution" - if 'plugin_output' in data: - data_po = data['plugin_output'] - else: - data_po = "Not data" - - risk_factor = "unclassified" - if 'risk_factor' in data: - risk_factor = data['risk_factor'] - if risk_factor == 'None': - risk_factor = "info" # I checked several external id and most of them were info - - if 'cvss_base_score' in data: - cvss_base_score = "CVSS :{}".format(data['cvss_base_score']) - ref.append(cvss_base_score) - else: - ref = [] - - policyviolations = [] - tags_info = None - if serv[6] == 'Policy Compliance': - # This condition was added to support CIS Benchmark in policy violation field. - risk_factor = 'info' - tags_info = "Passed Checks" - bis_benchmark_data = serv[7].split('\n') - policy_item = bis_benchmark_data[0] - - for policy_check_data in bis_benchmark_data: - if 'ref.' in policy_check_data: - ref.append(policy_check_data) - - if 'FAILED' in policy_item: - risk_factor = 'low' - tags_info = "Failed checks" - policyviolations.append(policy_item) - - vulnerability_name = f'{serv[6]} {vulnerability_name} {policy_item}' - self.createAndAddVulnToHost(host_id, - vulnerability_name, - desc=desc, - severity=risk_factor, - resolution=resolution, - data=data_po, - ref=ref, - policyviolations=policyviolations, - external_id=external_id, - run_date=run_date, - tags=tags_info) + if not parser.report: + self.logger.error('Parser did not find any reports') + return 1 # this value is used to exit the execution of the executor. Can't use False or None because if this function doesn't fail, it returns none + report_hosts = parser.report.report_hosts + if report_hosts: + for host in report_hosts: + run_date = host.host_properties.host_end + if run_date: + run_date = parse(run_date) + website = host.host_properties.host_fqdn + host_id = self.createAndAddHost(**self.map_properties(host)) + + for item in host.report_items: + vulnerability_name = item.plugin_name + if not vulnerability_name: + continue + item_name = item.svc_name_attr + + main_data = self.map_item( + host_id, run_date, vulnerability_name, item) + + main_data = self.map_add_ref(main_data, item) + if item_name == 'general': + main_data = self.map_policy_general(main_data, item) + self.createAndAddVulnToHost(**main_data) else: - data = serv[9] - if not data: - continue - ref = [] - vulnerability_name = serv[5] - if 'description' in data: - desc = data['description'] - else: - desc = "No description" - if 'solution' in data: - resolution = data['solution'] - else: - resolution = "No Solution" - if 'plugin_output' in data: - data_po = data['plugin_output'] - else: - data_po = "Not data" - - risk_factor = "info" - if 'risk_factor' in data: - risk_factor = data['risk_factor'] - - if risk_factor == 'None': - risk_factor = 'info' - - if 'cvss_base_score' in data: - cvss_base_score = f"CVSS:{data['cvss_base_score']}" - ref.append(cvss_base_score) - if 'cvss_vector' in data: - cvss_vector = f"CVSSVECTOR:{data['cvss_vector']}" - ref.append(cvss_vector) - if 'see_also' in data: - ref.append(data['see_also']) - if 'cpe' in data: - ref.append(data['cpe']) - if 'xref' in data: - ref.append(data['xref']) - - service_id = self.createAndAddServiceToHost(host_id, name=serv_name, protocol=serv_protocol, - ports=serv_port) - - if serv_name == 'www' or serv_name == 'http': - self.createAndAddVulnWebToService(host_id, - service_id, - name=vulnerability_name, - desc=desc, - data=data_po, - severity=risk_factor, - resolution=resolution, - ref=ref, - external_id=external_id, - website=website, - run_date=run_date) + main_data["service_id"] = self.createAndAddServiceToHost( + host_id, name=item_name, protocol=item.protocol_attr, + ports=item.port_attr) + if item_name == 'www' or item_name == 'http': + main_data.update({"website": website}) + self.createAndAddVulnWebToService(**main_data) else: - self.createAndAddVulnToService(host_id, - service_id, - name=vulnerability_name, - severity=risk_factor, - desc=desc, - ref=ref, - data=data_po, - external_id=external_id, - resolution=resolution, - run_date=run_date) - else: - ip = '0.0.0.0' - host_id = self.createAndAddHost(ip, hostnames=None) - service_id = self.createAndAddServiceToHost(host_id,name="Not Information") - self.createAndAddVulnToService(host_id, service_id, name=parser.policy.policy_name, desc="No Description") - - -def createPlugin(): - return NessusPlugin() + self.createAndAddVulnToService(**main_data) + + @staticmethod + def map_add_ref(main_data, item: ReportItem): + main_data["cvss2"] = {} + main_data["cvss3"] = {} + if item.see_also: + main_data["ref"].append(item.see_also) + if item.cpe: + main_data["ref"].append(item.cpe) + if item.cve: + main_data["cve"] = item.cve + if item.cwe: + main_data["cwe"] = item.cwe + if item.cvss3_vector: + main_data["cvss3"]["vector_string"] = item.cvss3_vector + #if item has cvss3.base_score use it for severity + if item.cvss3_base_score: + main_data["severity"] = get_severity_from_cvss(item.cvss3_base_score) + if item.cvss_vector: + main_data["cvss2"]["vector_string"] = item.cvss_vector + return main_data + + +def createPlugin(*args, **kwargs): + return NessusPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/nessus_sc/__init__.py b/faraday_plugins/plugins/repo/nessus_sc/__init__.py new file mode 100644 index 00000000..f70f2304 --- /dev/null +++ b/faraday_plugins/plugins/repo/nessus_sc/__init__.py @@ -0,0 +1,6 @@ +""" +Faraday Penetration Test IDE +Copyright (C) 2017 Infobyte LLC (http://www.infobytesec.com/) +See the file 'doc/LICENSE' for the license information + +""" diff --git a/faraday_plugins/plugins/repo/nessus_sc/plugin.py b/faraday_plugins/plugins/repo/nessus_sc/plugin.py new file mode 100644 index 00000000..fe025a26 --- /dev/null +++ b/faraday_plugins/plugins/repo/nessus_sc/plugin.py @@ -0,0 +1,82 @@ +""" +Faraday Penetration Test IDE +Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/) +See the file 'doc/LICENSE' for the license information + +""" + +from faraday_plugins.plugins.plugin import PluginCSVFormat +import csv +import io + + +__author__ = "Gonzalo Martinez" +__copyright__ = "Copyright (c) 2019, Infobyte LLC" +__credits__ = ["Gonzalo Martinez"] +__license__ = "" +__version__ = "1.0.0" +__maintainer__ = "Gonzalo Martinez" +__email__ = "gmartinez@infobytesec.com" +__status__ = "Development" + + +class NessusScPlugin(PluginCSVFormat): + """ + Example plugin to parse Nessus Sc output. + """ + + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) + self.csv_headers = [{'Plugin', 'Plugin Name'}] + self.id = "Nessus_sc" + self.name = "Nessus Sc Output Plugin" + self.plugin_version = "1.0.0" + self.version = "1.0.0" + self.framework_version = "1.0.0" + + def parseOutputString(self, output): + try: + reader = csv.DictReader(io.StringIO(output)) + except: + print("Error parser output") + return None + + for row in reader: + ip = row['IP Address'] + hostname = row['DNS Name'] + h_id = self.createAndAddHost(name=ip, hostnames=hostname) + protocol = row['Protocol'] + port = row['Port'] + data = row['Plugin Output'] + s_id = self.createAndAddServiceToHost(h_id, name=port, protocol=protocol, ports=port, status="open") + name = row['Plugin Name'] + severity = row['Severity'] + description = row['Description'] + vuln = {"name": name, "severity": severity, "desc": description} + solution = row['Solution'] + if solution: + vuln["resolution"] = solution + cvss3_vector = row['CVSS V3 Vector'] + if cvss3_vector: + if not cvss3_vector.startswith("CVSS:3.0/"): + cvss3_vector = "CVSS:3.0/"+cvss3_vector + vuln["cvss3"] = {"vector_string": cvss3_vector} + cvss2_vector = row['CVSS V2 Vector'] + if cvss2_vector: + vuln["cvss2"] = {"vector_string": cvss2_vector} + external_ref = row["See Also"] + cross_ref = row["Cross References"] + references = [] + if external_ref: + references.append(external_ref) + if cross_ref: + references.append(cross_ref) + vuln["ref"] = references + cve = row['CVE'] + if cve: + vuln["cve"] = cve + self.createAndAddVulnToService(h_id, s_id, **vuln) + + +def createPlugin(*args, **kwargs): + return NessusScPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/netdiscover/__init__.py b/faraday_plugins/plugins/repo/netdiscover/__init__.py index 0c98f519..fd94db9c 100644 --- a/faraday_plugins/plugins/repo/netdiscover/__init__.py +++ b/faraday_plugins/plugins/repo/netdiscover/__init__.py @@ -3,4 +3,4 @@ Copyright (C) 2016 Infobyte LLC (http://www.infobytesec.com/) See the file 'doc/LICENSE' for the license information """ -# I'm Py3 \ No newline at end of file + diff --git a/faraday_plugins/plugins/repo/netdiscover/plugin.py b/faraday_plugins/plugins/repo/netdiscover/plugin.py index b87e7a17..5540d7a8 100644 --- a/faraday_plugins/plugins/repo/netdiscover/plugin.py +++ b/faraday_plugins/plugins/repo/netdiscover/plugin.py @@ -16,8 +16,8 @@ class NetdiscoverPlugin(PluginBase): - def __init__(self): - super().__init__() + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) self.id = "Netdiscover" self.name = "netdiscover" self.plugin_version = "0.0.1" @@ -27,7 +27,7 @@ def __init__(self): def parseOutputString(self, output): #regexp get ip, mac and hostname reg = re.findall(r"(([0-9]+\.?){4})\s+(([0-9a-f]+\:?){6})((\s+[0-9]+){2})(.*)", output) - + if output.find('Finished!') != -1 and len(reg) > 0: for stdout in reg: @@ -39,7 +39,5 @@ def parseOutputString(self, output): -def createPlugin(): - return NetdiscoverPlugin() - -# I'm Py3 +def createPlugin(*args, **kwargs): + return NetdiscoverPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/netsparker/__init__.py b/faraday_plugins/plugins/repo/netsparker/__init__.py index ea531e17..625a6e25 100644 --- a/faraday_plugins/plugins/repo/netsparker/__init__.py +++ b/faraday_plugins/plugins/repo/netsparker/__init__.py @@ -4,4 +4,4 @@ See the file 'doc/LICENSE' for the license information """ -# I'm Py3 \ No newline at end of file + diff --git a/faraday_plugins/plugins/repo/netsparker/plugin.py b/faraday_plugins/plugins/repo/netsparker/plugin.py index 7fdd1403..1044a79e 100644 --- a/faraday_plugins/plugins/repo/netsparker/plugin.py +++ b/faraday_plugins/plugins/repo/netsparker/plugin.py @@ -4,21 +4,14 @@ See the file 'doc/LICENSE' for the license information """ +import sys import re -from bs4 import BeautifulSoup -from faraday_plugins.plugins.plugin import PluginXMLFormat -from faraday_plugins.plugins.plugins_utils import resolve_hostname - -try: - import xml.etree.cElementTree as ET - import xml.etree.ElementTree as ET_ORIG - ETREE_VERSION = ET_ORIG.VERSION -except ImportError: - import xml.etree.ElementTree as ET - ETREE_VERSION = ET.VERSION +import xml.etree.ElementTree as ET -ETREE_VERSION = [int(i) for i in ETREE_VERSION.split(".")] +from bs4 import BeautifulSoup +from faraday_plugins.plugins.plugin import PluginXMLFormat +from faraday_plugins.plugins.plugins_utils import CVE_regex __author__ = "Francisco Amato" __copyright__ = "Copyright (c) 2013, Infobyte LLC" @@ -41,8 +34,9 @@ class NetsparkerXmlParser: @param netsparker_xml_filepath A proper xml generated by netsparker """ - def __init__(self, xml_output): + def __init__(self, xml_output, plugin): self.filepath = xml_output + self.plugin = plugin tree = self.parse_xml(xml_output) if tree: @@ -62,7 +56,7 @@ def parse_xml(self, xml_output): try: tree = ET.fromstring(xml_output) except SyntaxError as err: - self.logger.error("SyntaxError: %s. %s" % (err, xml_output)) + self.plugin.logger.error(f"SyntaxError: {err}. {xml_output}") return None return tree @@ -83,23 +77,16 @@ class Item: @param item_node A item_node taken from an netsparker xml tree """ - def re_map_severity(self, severity): - if severity == "Important": - return "high" - return severity - - def __init__(self, item_node, encoding="ascii"): + def __init__(self, item_node): self.node = item_node self.url = self.get_text_from_subnode("url") - host = re.search( - "(http|https|ftp)\://([a-zA-Z0-9\.\-]+(\:[a-zA-Z0-9\.&%\$\-]+)*@)*((25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]" - "{2}|[1-9]{1}[0-9]{1}|[1-9])\.(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9]|0)\.(25[0-5]|2" - "[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9]|0)\.(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]" - "{1}|[0-9])|localhost|([a-zA-Z0-9\-]+\.)*[a-zA-Z0-9\-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|" - "pro|aero|coop|museum|[a-zA-Z]{2}))[\:]*([0-9]+)*([/]*($|[a-zA-Z0-9\.\,\?\'\\\+&%\$#\=~_\-]+)).*?$", + r"(http|https|ftp)\://([a-zA-Z0-9\.\-]+(\:[a-zA-Z0-9\.&%\$\-]+)*@)*((25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]" + r"{2}|[1-9]{1}[0-9]{1}|[1-9])\.(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9]|0)\.(25[0-5]|2" + r"[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9]|0)\.(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]" + r"{1}|[0-9])|localhost|([a-zA-Z0-9\-]+\.)*[a-zA-Z0-9\-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|" + "pro|aero|coop|museum|[a-zA-Z]{2}))[\\:]*([0-9]+)*([/]*($|[\\(\\)a-zA-Z0-9\\.\\,\\?\'\\\\+&%\\$#\\=~_\\-]+)).*?$", self.url) - self.protocol = host.group(1) self.hostname = host.group(4) self.port = 80 @@ -118,20 +105,26 @@ def __init__(self, item_node, encoding="ascii"): self.param = self.get_text_from_subnode("vulnerableparameter") self.paramval = self.get_text_from_subnode("vulnerableparametervalue") self.reference = self.get_text_from_subnode("externalReferences") - self.resolution = self.get_text_from_subnode("actionsToTake") + remedy = self.get_text_from_subnode("remedy") + self.resolution = self.get_text_from_subnode("actionsToTake") + remedy if remedy else "" self.request = self.get_text_from_subnode("rawrequest") self.response = self.get_text_from_subnode("rawresponse") self.kvulns = [] + self.cve = [] for v in self.node.findall("knownvulnerabilities/knownvulnerability"): self.node = v + recheck = CVE_regex.search(v.find('title').text) + if recheck: + self.cve.append(recheck.group()) self.kvulns.append(self.get_text_from_subnode( "severity") + "-" + self.get_text_from_subnode("title")) self.extra = [] for v in item_node.findall("extrainformation/info"): - name = v.get('name') - if name: - self.extra.append("{name}:{v.text}") + name_tag = v.find('name') + value_tag = v.find('value') + if name_tag is not None: + self.extra.append(f"{name_tag.text}:{value_tag.text}") self.node = item_node self.node = item_node.find("classification") @@ -142,18 +135,18 @@ def __init__(self, item_node, encoding="ascii"): self.pci = self.get_text_from_subnode("PCI") self.pci2 = self.get_text_from_subnode("PCI2") self.node = item_node.find("classification/CVSS") - self.cvss = self.get_text_from_subnode("vector") - + self.cvss_full_vector = self.get_text_from_subnode("vector") + self.cvss_score = self.get_text_from_subnode("score[1]/value") if self.get_text_from_subnode("score[1]/value") else None + self.cvss3 = {} self.ref = [] if self.cwe: - self.ref.append("CWE-" + self.cwe) + self.cwe = ["CWE-" + self.cwe] if self.owasp: self.ref.append("OWASP-" + self.owasp) if self.reference: - self.ref.extend(sorted(set(re.findall('https?://(?:[-\w.]|(?:%[\da-fA-F]{2}))+', self.reference)))) - if self.cvss: - self.ref.append(self.cvss) - + self.ref.extend(sorted(set(re.findall(r'https?://(?:[-\w.]|(?:%[\da-fA-F]{2}))+', self.reference)))) + if self.cvss_full_vector: + self.cvss3["vector_string"] = self.cvss_full_vector self.data = "" self.data += "\nKnowVulns: " + \ "\n".join(self.kvulns) if self.kvulns else "" @@ -167,6 +160,12 @@ def __init__(self, item_node, encoding="ascii"): repr(self.paramval) if self.paramval else "" self.data += "\nExtra: " + "\n".join(self.extra) if self.extra else "" + @staticmethod + def re_map_severity(severity): + if severity == "Important": + return "high" + return severity + def get_text_from_subnode(self, subnode_xpath_expr): """ Finds a subnode in the host node and the retrieves a value from it. @@ -185,8 +184,8 @@ class NetsparkerPlugin(PluginXMLFormat): Example plugin to parse netsparker output. """ - def __init__(self): - super().__init__() + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) self.identifier_tag = "netsparker" self.id = "Netsparker" self.name = "Netsparker XML Output Plugin" @@ -196,18 +195,22 @@ def __init__(self): self.options = None def parseOutputString(self, output): - parser = NetsparkerXmlParser(output) - first = True + parser = NetsparkerXmlParser(output, self) + host_names_resolve = {} for i in parser.items: - if first: - ip = resolve_hostname(i.hostname) - h_id = self.createAndAddHost(ip, hostnames=[ip]) - - s_id = self.createAndAddServiceToHost(h_id, str(i.port), - protocol = str(i.protocol), - ports=[str(i.port)], - status="open") - first = False + + if i.hostname not in host_names_resolve: + ip = self.resolve_hostname(i.hostname) + host_names_resolve[i.hostname] = ip + else: + ip = host_names_resolve[i.hostname] + h_id = self.createAndAddHost(ip, hostnames=[i.hostname]) + + s_id = self.createAndAddServiceToHost(h_id, str(i.port), + protocol = str(i.protocol), + ports=[str(i.port)], + status="open") + if i.resolution is not None: resolution = BeautifulSoup(i.resolution, "lxml").text else: @@ -221,15 +224,13 @@ def parseOutputString(self, output): name = i.name else: name = i.name_title - v_id = self.createAndAddVulnWebToService(h_id, s_id, name, ref=i.ref, website=i.hostname, + self.createAndAddVulnWebToService(h_id, s_id, name, ref=i.ref, website=i.hostname, severity=i.severity, desc=desc, path=i.url, method=i.method, request=i.request, response=i.response, resolution=resolution, - pname=i.param, data=i.data) + pname=i.param, data=i.data, cve=i.cve, cwe=i.cwe, cvss3=i.cvss3) del parser -def createPlugin(): - return NetsparkerPlugin() - -# I'm Py3 +def createPlugin(*args, **kwargs): + return NetsparkerPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/netsparkercloud/__init__.py b/faraday_plugins/plugins/repo/netsparkercloud/__init__.py index ea531e17..625a6e25 100644 --- a/faraday_plugins/plugins/repo/netsparkercloud/__init__.py +++ b/faraday_plugins/plugins/repo/netsparkercloud/__init__.py @@ -4,4 +4,4 @@ See the file 'doc/LICENSE' for the license information """ -# I'm Py3 \ No newline at end of file + diff --git a/faraday_plugins/plugins/repo/netsparkercloud/plugin.py b/faraday_plugins/plugins/repo/netsparkercloud/plugin.py index ce85c6b4..9fe26f6a 100644 --- a/faraday_plugins/plugins/repo/netsparkercloud/plugin.py +++ b/faraday_plugins/plugins/repo/netsparkercloud/plugin.py @@ -5,20 +5,10 @@ """ import re +import xml.etree.ElementTree as ET from urllib.parse import urlparse -from faraday_plugins.plugins.plugin import PluginXMLFormat -from faraday_plugins.plugins.plugins_utils import resolve_hostname - -try: - import xml.etree.cElementTree as ET - import xml.etree.ElementTree as ET_ORIG - ETREE_VERSION = ET_ORIG.VERSION -except ImportError: - import xml.etree.ElementTree as ET - ETREE_VERSION = ET.VERSION - -ETREE_VERSION = [int(i) for i in ETREE_VERSION.split(".")] +from faraday_plugins.plugins.plugin import PluginXMLFormat __author__ = "Francisco Amato" __copyright__ = "Copyright (c) 2013, Infobyte LLC" @@ -30,13 +20,12 @@ __status__ = "Development" - def get_urls(string): if isinstance(string, bytes): string_decode = string.decode("utf-8") - urls = re.findall('(?:(?:https?|ftp):\/\/)?[\w/\-?=%.]+\.[\w/\-?=%.]+', string_decode) + urls = re.findall(r'(?:(?:https?|ftp):\/\/)?[\w/\-?=%.]+\.[\w/\-?=%.]+', string_decode) else: - urls = re.findall('(?:(?:https?|ftp):\/\/)?[\w/\-?=%.]+\.[\w/\-?=%.]+', string) + urls = re.findall(r'(?:(?:https?|ftp):\/\/)?[\w/\-?=%.]+\.[\w/\-?=%.]+', string) return urls @@ -55,7 +44,7 @@ def __init__(self, xml_output): self.filepath = xml_output tree = self.parse_xml(xml_output) if tree: - self.items = [data for data in self.get_items(tree)] + self.items = self.get_items(tree) else: self.items = [] @@ -71,7 +60,7 @@ def parse_xml(self, xml_output): try: tree = ET.fromstring(xml_output) except SyntaxError as err: - self.logger.error("SyntaxError: %s. %s" % (err, xml_output)) + self.logger.error(f"SyntaxError: {err}. {xml_output}") return None return tree @@ -90,6 +79,7 @@ class Item: @param item_node A item_node taken from an netsparkercloud xml tree """ + def re_map_severity(self, severity): if severity == "Important": return "high" @@ -120,7 +110,7 @@ def __init__(self, item_node, encoding="ascii"): self.response = self.get_text_from_subnode("content") self.extra = [] for v in item_node.findall("extra-information/info"): - self.extra.append(v.get('name') + ":" + v.get('value') ) + self.extra.append(v.get('name') + ":" + v.get('value')) self.node = item_node.find("classification") self.owasp = self.get_text_from_subnode("owasp") @@ -133,9 +123,9 @@ def __init__(self, item_node, encoding="ascii"): self.ref = [] if self.cwe: - self.ref.append("CWE-{}".format(self.cwe)) + self.cwe = [f"CWE-{self.cwe}"] if self.owasp: - self.ref.append("OWASP-{}".format(self.owasp)) + self.ref.append(f"OWASP-{self.owasp}") self.node = item_node self.remedyreferences = self.get_text_from_subnode("remedy-references") @@ -180,8 +170,8 @@ class NetsparkerCloudPlugin(PluginXMLFormat): Example plugin to parse netsparkercloud output. """ - def __init__(self): - super().__init__() + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) self.identifier_tag = "netsparker-cloud" self.id = "NetsparkerCloud" self.name = "NetsparkerCloud XML Output Plugin" @@ -195,20 +185,16 @@ def parseOutputString(self, output): first = True for i in parser.items: if first: - ip = resolve_hostname(i.hostname) + ip = self.resolve_hostname(i.hostname) h_id = self.createAndAddHost(ip, hostnames=[i.hostname]) s_id = self.createAndAddServiceToHost(h_id, i.protocol, ports=[i.port], status="open") first = False v_id = self.createAndAddVulnWebToService(h_id, s_id, i.name, ref=i.ref, website=i.hostname, severity=i.severity, desc=i.desc, path=i.url.path, method=i.method, request=i.request, response=i.response, resolution=i.resolution, - pname=i.param) + pname=i.param, cwe=i.cwe) del parser - def setHost(self): - pass - - -def createPlugin(): - return NetsparkerCloudPlugin() +def createPlugin(*args, **kwargs): + return NetsparkerCloudPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/nexpose_full/__init__.py b/faraday_plugins/plugins/repo/nexpose_full/__init__.py index ea531e17..625a6e25 100644 --- a/faraday_plugins/plugins/repo/nexpose_full/__init__.py +++ b/faraday_plugins/plugins/repo/nexpose_full/__init__.py @@ -4,4 +4,4 @@ See the file 'doc/LICENSE' for the license information """ -# I'm Py3 \ No newline at end of file + diff --git a/faraday_plugins/plugins/repo/nexpose_full/plugin.py b/faraday_plugins/plugins/repo/nexpose_full/plugin.py index c565809e..dd09fa39 100644 --- a/faraday_plugins/plugins/repo/nexpose_full/plugin.py +++ b/faraday_plugins/plugins/repo/nexpose_full/plugin.py @@ -4,21 +4,11 @@ See the file 'doc/LICENSE' for the license information """ -from faraday_plugins.plugins.plugin import PluginXMLFormat import re +import xml.etree.ElementTree as ET -try: - import xml.etree.cElementTree as ET - import xml.etree.ElementTree as ET_ORIG - - ETREE_VERSION = ET_ORIG.VERSION -except ImportError: - import xml.etree.ElementTree as ET - - ETREE_VERSION = ET.VERSION - -ETREE_VERSION = [int(i) for i in ETREE_VERSION.split(".")] - +from faraday_plugins.plugins.plugin import PluginXMLFormat +from faraday_plugins.plugins.plugins_utils import CVE_regex __author__ = "Micaela Ranea Sanchez" __copyright__ = "Copyright (c) 2013, Infobyte LLC" @@ -51,7 +41,23 @@ def __init__(self, xml_output): else: self.items = [] - def parse_xml(self, xml_output): + @staticmethod + def get_severity_from_report(score): + try: + if not isinstance(score, float): + score = float(score) + + cvss_ranges = [(0.0, 3.4, 'med'), + (3.5, 7.4, 'high'), + (7.5, 10.1, 'critical')] + for (lower, upper, severity) in cvss_ranges: + if lower <= score < upper: + return severity + except ValueError: + return 'unclassified' + + @staticmethod + def parse_xml(xml_output): """ Open and parse an xml file. @@ -122,7 +128,6 @@ def parse_tests_type(self, node, vulnsDefinitions): for tests in node.findall('tests'): for test in tests.iter('test'): - vuln = dict() if test.get('id').lower() in vulnsDefinitions: vuln = vulnsDefinitions[test.get('id').lower()].copy() key = test.get('key', '') @@ -143,9 +148,6 @@ def get_vuln_definitions(self, tree): @returns vulns A dict of Vulnerability Definitions """ vulns = dict() - # CVSS V3 - SEVERITY_MAPPING_DICT = {'0': 'info', '1': 'low', '2': 'low', '3': 'low', '4': 'med', '5': 'med', '6': 'med', - '7': 'high', '8': 'high', '9': 'critical', '10': 'critical'} for vulnsDef in tree.iter('VulnerabilityDefinitions'): for vulnDef in vulnsDef.iter('vulnerability'): @@ -154,12 +156,16 @@ def get_vuln_definitions(self, tree): vuln = { 'desc': "", 'name': vulnDef.get('title'), - 'refs': ["vector: " + vector, vid], + 'refs': [], 'resolution': "", - 'severity': SEVERITY_MAPPING_DICT[vulnDef.get('severity')], + 'severity': self.get_severity_from_report(vulnDef.get('severity')), 'tags': list(), 'is_web': vid.startswith('http-'), 'risk': vulnDef.get('riskScore'), + 'CVE': [], + 'cvss2': { + "vector_string": vector.replace("(", "").replace(")", "") if vector else None + } } for item in list(vulnDef): @@ -181,18 +187,29 @@ def get_vuln_definitions(self, tree): vuln['refs'].append(nameMalware) if item.tag == 'references': for ref in list(item): - if ref.text: - rf = ref.text.strip() + if not ref.text: + continue + source = "" + if "source" in ref.attrib: + source = ref.attrib['source'] + ": " + rf = ref.text.strip() + check = CVE_regex.search(rf.upper()) + if check: + vuln["CVE"].append(check.group()) + else: + if rf.isnumeric(): + rf = source + rf vuln['refs'].append(rf) if item.tag == 'solution': for htmlType in list(item): vuln['resolution'] += self.parse_html_type(htmlType) - """ - # there is currently no method to register tags in vulns - if item.tag == 'tags': - for tag in list(item): - vuln['tags'].append(tag.text.lower()) - """ + + """ + # there is currently no method to register tags in vulns + if item.tag == 'tags': + for tag in list(item): + vuln['tags'].append(tag.text.lower()) + """ vulns[vid] = vuln return vulns @@ -249,8 +266,8 @@ class NexposeFullPlugin(PluginXMLFormat): Example plugin to parse nexpose output. """ - def __init__(self): - super().__init__() + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) self.identifier_tag = "NexposeReport" self.id = "NexposeFull" self.name = "Nexpose XML 2.0 Report Plugin" @@ -275,17 +292,18 @@ def parseOutputString(self, output): h_id = self.createAndAddHost(item['name'], item['os'], hostnames=item['hostnames'], mac=mac) for v in item['vulns']: v['data'] = {"vulnerable_since": v['vulnerable_since'], "scan_id": v['scan_id'], "PCI": v['pci']} - v_id = self.createAndAddVulnToHost( + self.createAndAddVulnToHost( h_id, v['name'], v['desc'], v['refs'], v['severity'], - v['resolution'] + v['resolution'], + cve=v.get('CVE'), + cvss2=v.get('cvss2') ) for s in item['services']: - web = False version = s.get("version", "") s_id = self.createAndAddServiceToHost( h_id, @@ -298,7 +316,7 @@ def parseOutputString(self, output): for v in s['vulns']: if v['is_web']: - v_id = self.createAndAddVulnWebToService( + self.createAndAddVulnWebToService( h_id, s_id, v['name'], @@ -306,26 +324,25 @@ def parseOutputString(self, output): v['refs'], v['severity'], v['resolution'], - path=v.get('path', '')) + cve=v.get('CVE'), + path=v.get('path', ''), + cvss2=v.get('cvss2') + ) else: - v_id = self.createAndAddVulnToService( + self.createAndAddVulnToService( h_id, s_id, v['name'], v['desc'], v['refs'], v['severity'], - v['resolution'] + v['resolution'], + cve=v.get('CVE'), + cvss2=v.get('cvss2') ) del parser - def setHost(self): - pass - - -def createPlugin(): - return NexposeFullPlugin() - -# I'm Py3 +def createPlugin(*args, **kwargs): + return NexposeFullPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/nextnet/__init__.py b/faraday_plugins/plugins/repo/nextnet/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/faraday_plugins/plugins/repo/nextnet/plugin.py b/faraday_plugins/plugins/repo/nextnet/plugin.py new file mode 100644 index 00000000..2ae94898 --- /dev/null +++ b/faraday_plugins/plugins/repo/nextnet/plugin.py @@ -0,0 +1,67 @@ +""" +Faraday Penetration Test IDE +Copyright (C) 2020 Infobyte LLC (http://www.infobytesec.com/) +See the file 'doc/LICENSE' for the license information + +""" +import re +import json +from faraday_plugins.plugins.plugin import PluginBase + + +__author__ = "Blas Moyano" +__copyright__ = "Copyright (c) 2020, Infobyte LLC" +__credits__ = ["Blas Moyano"] +__license__ = "" +__version__ = "1.0.0" +__maintainer__ = "Blas Moyano" +__email__ = "bmoyano@infobytesec.com" +__status__ = "Development" + + +class CmdNextNetin(PluginBase): + + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) + self.id = "nextnet" + self.name = "nextnet" + self.plugin_version = "0.0.1" + self.version = "5.0.20" + self.framework_version = "1.0.0" + self.options = None + self._current_output = None + self._command_regex = re.compile(r'^[.]*?[/]*?nextnet\s+.*?') + self._host_ip = None + self._info = 0 + + def parseOutputString(self, output): + output_lines = output.split('\n') + output_lines = output_lines[:-1] + + for line in output_lines: + json_line = json.loads(line) + info_data = json_line.get("info", None) + desc = "" + mac = None + if info_data is not None: + desc = f'Domain Tag: {info_data.get("domain", "Not tag info")}' + mac = info_data.get("hwaddr") + + h_id = self.createAndAddHost( + json_line.get("host", "0.0.0.0"), + os=json_line.get("name", "unknown"), + hostnames=json_line.get("nets"), + mac=mac + ) + self.createAndAddServiceToHost( + h_id, + name=json_line.get("probe", "unknown"), + protocol=json_line.get("proto", "tcp"), + ports=json_line.get("port", None), + description=desc + ) + return True + + +def createPlugin(*args, **kwargs): + return CmdNextNetin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/nikto/__init__.py b/faraday_plugins/plugins/repo/nikto/__init__.py index ea531e17..625a6e25 100644 --- a/faraday_plugins/plugins/repo/nikto/__init__.py +++ b/faraday_plugins/plugins/repo/nikto/__init__.py @@ -4,4 +4,4 @@ See the file 'doc/LICENSE' for the license information """ -# I'm Py3 \ No newline at end of file + diff --git a/faraday_plugins/plugins/repo/nikto/plugin.py b/faraday_plugins/plugins/repo/nikto/plugin.py index ee556abf..cdc4b520 100644 --- a/faraday_plugins/plugins/repo/nikto/plugin.py +++ b/faraday_plugins/plugins/repo/nikto/plugin.py @@ -4,21 +4,11 @@ See the file 'doc/LICENSE' for the license information """ import re +import xml.etree.ElementTree as ET from html.parser import HTMLParser -from faraday_plugins.plugins.plugin import PluginXMLFormat -from faraday_plugins.plugins import plugins_utils - - -try: - import xml.etree.cElementTree as ET - import xml.etree.ElementTree as ET_ORIG - ETREE_VERSION = ET_ORIG.VERSION -except ImportError: - import xml.etree.ElementTree as ET - ETREE_VERSION = ET.VERSION - -ETREE_VERSION = [int(i) for i in ETREE_VERSION.split(".")] +from faraday_plugins.plugins import plugins_utils +from faraday_plugins.plugins.plugin import PluginXMLFormat __author__ = "Francisco Amato" __copyright__ = "Copyright (c) 2013, Infobyte LLC" @@ -46,7 +36,7 @@ def __init__(self, xml_output): tree = self.parse_xml(xml_output) if tree: - self.hosts = [host for host in self.get_hosts(tree)] + self.hosts = self.get_hosts(tree) else: self.hosts = [] @@ -62,7 +52,7 @@ def parse_xml(self, xml_output): try: tree = ET.fromstring(xml_output) except SyntaxError as err: - print("SyntaxError: %s. %s" % (err, xml_output)) + print(f"SyntaxError: {err}. {xml_output}") return None return tree @@ -85,28 +75,8 @@ def get_attrib_from_subnode(xml_node, subnode_xpath_expr, attrib_name): @return An attribute value """ - global ETREE_VERSION - node = None - - if ETREE_VERSION[0] <= 1 and ETREE_VERSION[1] < 3: - - match_obj = re.search( - "([^\@]+?)\[\@([^=]*?)=\'([^\']*?)\'", subnode_xpath_expr) - if match_obj is not None: - node_to_find = match_obj.group(1) - xpath_attrib = match_obj.group(2) - xpath_value = match_obj.group(3) - for node_found in xml_node.findall(node_to_find): - - if node_found.attrib[xpath_attrib] == xpath_value: - node = node_found - break - else: - node = xml_node.find(subnode_xpath_expr) - - else: - node = xml_node.find(subnode_xpath_expr) + node = xml_node.find(subnode_xpath_expr) if node is not None: return node.get(attrib_name) @@ -130,7 +100,7 @@ def __init__(self, item_node): self.node = item_node self.osvdbid = [ - "OSVDB-ID: " + self.node.get('osvdbid')] if self.node.get('osvdbid') != "0" else [] + "OSVDB-ID: " + self.node.get('osvdbid')] if self.node.get('osvdbid', "0") != "0" else [] self.namelink = self.get_text_from_subnode('namelink') self.iplink = self.get_text_from_subnode('iplink') @@ -142,6 +112,7 @@ def __init__(self, item_node): self.uri = self.get_uri() self.desc = self.get_desc() self.params = self.get_params(self.uri) + self.CVE = self.get_cve(self.desc) def get_uri(self): @@ -194,15 +165,11 @@ def get_text_from_subnode(self, subnode_xpath_expr): return None - def __str__(self): - ports = [] - for port in self.ports: - var = " %s" % port - ports.append(var) - ports = "\n".join(ports) - - return "%s, %s, %s [%s], %s\n%s" % (self.hostnames, self.status, - self.ipv4_address, self.mac_address, self.os, ports) + def get_cve(self, desc): + match = plugins_utils.CVE_regex.search(desc) + if match: + return [match.group()] + return [] class Host: @@ -222,7 +189,7 @@ def __init__(self, host_node): self.starttime = self.node.get('starttime') self.sitename = self.node.get('sitename') self.siteip = self.node.get('hostheader') - self.items = [item for item in self.get_items()] + self.items = self.get_items() def get_items(self): """ @@ -231,15 +198,6 @@ def get_items(self): for item_node in self.node.findall('item'): yield Item(item_node) - def __str__(self): - ports = [] - for port in self.ports: - var = " %s" % port - ports.append(var) - ports = "\n".join(ports) - - return "%s, %s, %s [%s], %s\n%s" % (self.hostnames, self.status, - self.ipv4_address, self.mac_address, self.os, ports) class NiktoPlugin(PluginXMLFormat): @@ -247,9 +205,9 @@ class NiktoPlugin(PluginXMLFormat): Example plugin to parse nikto output. """ - def __init__(self): - super().__init__() - self.identifier_tag = "niktoscan" + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) + self.identifier_tag = ["niktoscan", "niktoscans"] self.id = "Nikto" self.name = "Nikto XML Output Plugin" self.plugin_version = "0.0.2" @@ -301,8 +259,6 @@ def __init__(self): "-vhost+": "Virtual host (for Host header)", } - - def parseOutputString(self, output): """ This method will discard the output the shell sends, it will read it from @@ -330,21 +286,19 @@ def parseOutputString(self, output): ) for item in host.items: - - v_id = self.createAndAddVulnWebToService( + self.createAndAddVulnWebToService( h_id, s_id, name=item.desc, ref=item.osvdbid, method=item.method, params=', '.join(item.params), + cve=item.CVE, **plugins_utils.get_vulnweb_url_fields(item.namelink) ) del parser - - def processCommandString(self, username, current_path, command_string): """ Adds the -oX parameter to get xml output to the command string that the @@ -357,14 +311,9 @@ def processCommandString(self, username, current_path, command_string): if arg_match is None: return re.sub(r"(^.*?nikto(\.pl)?)", r"\1 -output %s -Format XML" % self._output_file_path, command_string) else: - data = re.sub(" \-Format XML", "", command_string) + data = re.sub(r" \-Format XML", "", command_string) return re.sub(arg_match.group(1), r"-output %s -Format XML" % self._output_file_path, data) - def setHost(self): - pass - - -def createPlugin(): - return NiktoPlugin() -# I'm Py3 +def createPlugin(*args, **kwargs): + return NiktoPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/nipper/__init__.py b/faraday_plugins/plugins/repo/nipper/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/faraday_plugins/plugins/repo/nipper/plugin.py b/faraday_plugins/plugins/repo/nipper/plugin.py new file mode 100644 index 00000000..60d2cee3 --- /dev/null +++ b/faraday_plugins/plugins/repo/nipper/plugin.py @@ -0,0 +1,167 @@ +import xml.etree.ElementTree as ET + +from faraday_plugins.plugins.plugin import PluginXMLFormat + +__author__ = "@rfocke and @pasta <3" +__copyright__ = "Copyright (c) 2021, Faradaysec LLC" +__credits__ = ["Roberto Focke", "Javier Aguinaga"] +__license__ = "GPL" +__version__ = "0.8" +__mantainer__ = "@rfocke" +__status__ = "Development" + + +class VulnSoftNipper: + def __init__(self, **kwargs): + self.name = '' + self.data = '' + self.device = '' + self.cvss2 = {} + self.refs = [] + + +class VulnerabilityNipper: + def __init__(self, **kwargs): + self.name = '' + self.rating = '' + self.recommendation = '' + self.affected_devices = [] + self.section = '' + self.name2 = '' + self.data = '' + self.recommendation2 = '' + + +class NipperParser: + def __init__(self, output, debug=False): + self.vulns_first = [] + self.vulns_audit = [] + self.vulns_thirds = [] + self.debug = debug + + self.tree = ET.fromstring(output) + self.report_tree = self.tree.find( + "report/part/[@index='2']/section/[@title='Recommendations']/table/[@title='Security Audit recommendations list']/tablebody") + self.process_xml() + + def process_xml(self): + if not self.report_tree: + return + + for tablerow in self.report_tree: + for i, tablecell in enumerate(tablerow.findall('tablecell')): + if len(tablecell.findall('item')) == 1: + if i == 0: # Item + vuln = VulnerabilityNipper() + vuln.name = tablecell.find('item').text + elif i == 1: # Rating + vuln.rating = tablecell.find('item').text + elif i == 2: # Recommendations + vuln.recommendation = tablecell.find('item').text + elif i == 3: # Affected devices (with 1 element only) + vuln.affected_devices = [] + vuln.affected_devices.append(tablecell.find('item').text) + elif i == 4: # Section + subdetail = tablecell.find('item').text + vuln.section = subdetail + + path = "./report/part/[@index='2']/section/[@index='" + subdetail + "']" + for detail in self.tree.findall(path): + # nombre de la vuln + vuln.name2 = detail.attrib.get('title') + + if vuln.name2 != vuln.name: + pass + + path = "./report/part/[@index='2']/section/[@index='" + subdetail + "']/section/[@index='" + subdetail + ".2']" + for detail in self.tree.findall(path): + # data de la vuln + vuln.data = detail.find('text').text + + path = "./report/part/[@index='2']/section/[@index='" + subdetail + "']/section/[@index='" + subdetail + ".5']" + for detail in self.tree.findall(path): + # recomendacion de la vuln + vuln.recommendation2 = detail.find('text').text + + self.vulns_first.append(vuln) # <- GUARDADO + elif len(tablecell.findall('item')) > 1 and i == 3: + # affected devices + vuln.affected_devices = [] + for item in tablecell.findall('item'): + vuln.affected_devices.append(item.text) + + # parseo vuln de software + report_tree = self.tree.find("./report/part/[@title='Vulnerability Audit']") + for itemv in report_tree: + vuln_soft = VulnSoftNipper() + # nombre de la vuln + + vuln_soft.name = itemv.attrib.get('title') + cvss2_vector = itemv.find('infobox/infodata/[@label="CVSSv2 Base"]') + vuln_soft.cvss2["vector_string"] = cvss2_vector.text.split(' ')[0] if cvss2_vector is not None else None + for itemvv in itemv: + if itemvv.attrib.get('title') == 'Summary': + for i in itemvv: + # data de la vuln + vuln_soft.data = i.text + if itemvv.attrib.get('title') == 'Affected Device': + for i in itemvv: + # data del device + aux = i.text.split('The')[1] + vuln_soft.device = aux.split(' may be affected by this security vulnerability')[0] + if itemvv.attrib.get('title') == 'References': + # referencias de la vuln + vuln_soft.refs = [] + for texto in itemvv.findall('list/listitem/weblink'): + vuln_soft.refs.append(texto.text) + + self.vulns_audit.append(vuln_soft) + + +class NipperPlugin(PluginXMLFormat): + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) + self.extension = ".xml" + self.identifier_tag = "document" + self.identifier_tag_attributes = {'nipperstudio'} + self.id = "Nipper" + self.name = "Nipper XML Output Plugin" + self.plugin_version = "0.9" + self.version = "0.9" + self.framewor_version = "1.0.1" + self.options = None + + def parseOutputString(self, output): + parser = NipperParser(output, debug=False) + for vuln in parser.vulns_first: + for device in vuln.affected_devices: + ip = self.resolve_hostname(device) + h_id = self.createAndAddHost(ip, hostnames=device) + self.createAndAddVulnToHost(h_id, + name=vuln.name, + desc=vuln.data, + severity=vuln.rating, + resolution=vuln.recommendation, + data=vuln.data, + ref=[], + policyviolations=[], + cve=[vuln.name] + ) + for vuln in parser.vulns_audit: + if vuln.data: + ip = self.resolve_hostname(device) + h_id = self.createAndAddHost(ip, hostnames=vuln.device) + self.createAndAddVulnToHost(h_id, + name=vuln.name, + desc=vuln.data, + severity='', + resolution='', + data=vuln.data, + ref=vuln.refs, + cve=[vuln.name], + cvss2=vuln.cvss2 + ) + + +def createPlugin(*args, **kwargs): + return NipperPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/nmap/__init__.py b/faraday_plugins/plugins/repo/nmap/__init__.py index ea531e17..625a6e25 100644 --- a/faraday_plugins/plugins/repo/nmap/__init__.py +++ b/faraday_plugins/plugins/repo/nmap/__init__.py @@ -4,4 +4,4 @@ See the file 'doc/LICENSE' for the license information """ -# I'm Py3 \ No newline at end of file + diff --git a/faraday_plugins/plugins/repo/nmap/plugin.py b/faraday_plugins/plugins/repo/nmap/plugin.py index 0c493b56..b7558917 100644 --- a/faraday_plugins/plugins/repo/nmap/plugin.py +++ b/faraday_plugins/plugins/repo/nmap/plugin.py @@ -1,30 +1,20 @@ +import os +import re +from io import BytesIO + """ Faraday Penetration Test IDE Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/) See the file 'doc/LICENSE' for the license information """ - -import re -import os -from io import BytesIO - -try: - import xml.etree.cElementTree as ET - import xml.etree.ElementTree as ET_ORIG - ETREE_VERSION = ET_ORIG.VERSION -except ImportError: - import xml.etree.ElementTree as ET - ETREE_VERSION = ET.VERSION from lxml import etree from lxml.etree import XMLParser from faraday_plugins.plugins.plugin import PluginXMLFormat -ETREE_VERSION = [int(i) for i in ETREE_VERSION.split(".")] current_path = os.path.abspath(os.getcwd()) - class NmapXmlParser: """ The objective of this class is to parse an xml file generated by @@ -60,7 +50,7 @@ def parse_xml(self, xml_output): magical_parser = XMLParser(recover=True) return etree.parse(BytesIO(xml_output), magical_parser) except SyntaxError as err: - #logger.error("SyntaxError: %s." % (err)) + # logger.error("SyntaxError: %s." % (err)) return None def get_hosts(self, tree): @@ -77,30 +67,8 @@ def get_attrib_from_subnode(xml_node, subnode_xpath_expr, attrib_name): @return An attribute value """ - global ETREE_VERSION - node = None - if ETREE_VERSION[0] <= 1 and ETREE_VERSION[1] < 3: - - match_obj = re.search( - "([^\@]+?)\[\@([^=]*?)=\'([^\']*?)\'", - subnode_xpath_expr) - - if match_obj is not None: - - node_to_find = match_obj.group(1) - xpath_attrib = match_obj.group(2) - xpath_value = match_obj.group(3) - - for node_found in xml_node.findall(node_to_find): - if node_found.attrib[xpath_attrib] == xpath_value: - node = node_found - break - else: - node = xml_node.find(subnode_xpath_expr) - - else: - node = xml_node.find(subnode_xpath_expr) + node = xml_node.find(subnode_xpath_expr) if node is not None: return node.get(attrib_name) @@ -233,15 +201,14 @@ def get_os_guesses(self): os_gen = osclass.get("osgen", "unknown") accuracy = osclass.get("accuracy", "unknown") - yield ("%s %s %s" % (os_vendor, os_family, os_gen), accuracy) + yield (f"{os_vendor} {os_family} {os_gen}", accuracy) # Os information in services, bad acurracy. if osclasses == []: services = self.node.findall("ports/port/service") for service in services: ostype = service.get("ostype", "unknown") - yield ("%s" % ostype, 0) - + yield (f"{ostype}", 0) def top_os_guess(self): """ @@ -276,16 +243,11 @@ def is_up(self): def __str__(self): ports = [] for port in self.ports: - var = " %s" % port + var = f" {port}" ports.append(var) ports = "\n".join(ports) - return "%s, %s, %s [%s], %s\n%s" % ( - self.hostnames, - self.status, - self.ipv4_address, - self.mac_address, - self.os, ports) + return f"{self.hostnames}, {self.status}, {self.ipv4_address} [{self.mac_address}], {self.os}\n{ports}" class Port: @@ -295,6 +257,8 @@ class Port: @param port_node A port_node taken from an nmap xml tree """ + PORT_STATUS_FIX = {"filtered": "closed", "open|filtered": "closed"} + def __init__(self, port_node): self.node = port_node @@ -322,7 +286,8 @@ def get_state(self): @return (state, reason, reason_ttl) or ('unknown','unknown','unknown') """ - state = self.get_attrib_from_subnode('state', 'state') + state = self.PORT_STATUS_FIX.get(self.get_attrib_from_subnode('state', 'state'), + self.get_attrib_from_subnode('state', 'state')) reason = self.get_attrib_from_subnode('state', 'reason') reason_ttl = self.get_attrib_from_subnode('state', 'reason_ttl') @@ -345,10 +310,54 @@ def get_scripts(self): Expects to find a scripts in the node. """ for s in self.node.findall('script'): - yield Script(s) + if s.get("id") == 'vulners': + for service_table in s.findall('table'): + for vulnerability_table in service_table.findall('table'): + yield ScriptVulners(s, service_table, vulnerability_table) + else: + yield Script(s) + + def __str__(self): + return f"{self.number}, {self.state}, Service: {self.service}" + + +class ScriptVulners: + """ + An abstract representation of a script table when script is 'vulners'. + https://nmap.org/nsedoc/scripts/vulners.html + + '' + + @param script_node The reference to the node of the 'vulners' script + @param service_table The outer table taken from an nmap 'vulners' script xml tree + @param vulnerability_table The inner table taken from an nmap 'vulners' script xml tree + """ + + def __init__(self, script_node, service_table, vulnerability_table): + self.node = script_node + self.table = {} + for e in vulnerability_table.findall('elem'): + self.table[e.get("key")] = str(e.text) + + self.name = self.table["id"] + + self.desc = script_node.get("id") + "-" + self.table["id"] + if self.table["is_exploit"] == 'true': + self.desc += " *EXPLOIT*" + + self.refs = ["https://vulners.com/" + self.table["type"] + "/" + self.table["id"]] + self.response = "" + self.web = "" + self.cvss2 = {} def __str__(self): - return "%s, %s, Service: %s" % (self.number, self.state, self.service) + return f"{self.name}, {self.product}, {self.version}" class Script: @@ -364,7 +373,7 @@ class Script: """ def parse_output(self, output): - block_re = re.compile('^\s{4}References:((?:.|[\r\n])+[\r\n](?:\s{4}\w|\s*$))', re.MULTILINE) + block_re = re.compile('^\\s{4}References:((?:.|[\r\n])+[\r\n](?:\\s{4}\\w|\\s*$))', re.MULTILINE) m1 = block_re.findall(output) if len(m1) > 0: links_re = re.compile('[ \t]+([^ \t\n\r]+)[ \t]*') @@ -377,14 +386,15 @@ def __init__(self, script_node): self.name = script_node.get("id") self.desc = script_node.get("output") - self.refs = self.parse_output(self.desc) + self.refs = self.parse_output(self.desc) self.response = "" for k in script_node.findall("elem"): self.response += "\n" + str(k.get('key')) + ": " + str(k.text) self.web = re.search("(http-|https-)", self.name) + self.cvss2 = {} def __str__(self): - return "%s, %s, %s" % (self.name, self.product, self.version) + return f"{self.name}, {self.product}, {self.version}" class Service: @@ -423,7 +433,7 @@ def __init__(self, service_node): self.ostype = self.node.get("ostype") def __str__(self): - return "%s, %s, %s" % (self.name, self.product, self.version) + return f"{self.name}, {self.product}, {self.version}" class NmapPlugin(PluginXMLFormat): @@ -431,8 +441,8 @@ class NmapPlugin(PluginXMLFormat): Example plugin to parse nmap output. """ - def __init__(self): - super().__init__() + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) self.identifier_tag = "nmaprun" self.id = "Nmap" self.name = "Nmap XML Output Plugin" @@ -441,7 +451,7 @@ def __init__(self): self.framework_version = "1.0.0" self.options = None self._current_output = None - self._command_regex = re.compile(r'^(sudo nmap|nmap|\.\/nmap)\s+.*?') + self._command_regex = re.compile(r'^(sudo nmap|nmap|\.\/nmap|sudo masscan|masscan)\s+.*?') self._use_temp_file = True self._temp_file_extension = "xml" self.xml_arg_re = re.compile(r"^.*(-oX\s*[^\s]+).*$") @@ -481,7 +491,8 @@ def parseOutputString(self, output): desc=v.desc, ref=v.refs, severity=0, - external_id=v.name) + cve=[v.name] + ) for port in host.ports: @@ -502,27 +513,34 @@ def parseOutputString(self, output): description=srvname) for v in port.vulns: - severity = "info" + desc = v.desc refs = v.refs - if re.search(r"(?= version.parse("2.5.3") + else: + return False + except Exception as e: + return False + + +def createPlugin(*args, **kwargs): + return NucleiPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/nuclei_legacy/__init__.py b/faraday_plugins/plugins/repo/nuclei_legacy/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/faraday_plugins/plugins/repo/nuclei_legacy/plugin.py b/faraday_plugins/plugins/repo/nuclei_legacy/plugin.py new file mode 100644 index 00000000..666a63ea --- /dev/null +++ b/faraday_plugins/plugins/repo/nuclei_legacy/plugin.py @@ -0,0 +1,153 @@ +import subprocess # nosec +import re +import json +from packaging import version +from urllib.parse import urlparse +from dateutil.parser import parse +from faraday_plugins.plugins.plugin import PluginMultiLineJsonFormat + +__author__ = "Emilio Couto" +__copyright__ = "Copyright (c) 2021, Faraday Security" +__credits__ = ["Emilio Couto"] +__license__ = "" +__version__ = "1.0.0" +__maintainer__ = "Emilio Couto" +__email__ = "ecouto@infobytesec.com" +__status__ = "Development" + + +class NucleiLegacyPlugin(PluginMultiLineJsonFormat): + """ Handle the Nuclei tool. Detects the output of the tool + and adds the information to Faraday. + """ + + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) + self.id = "nuclei_legacy" + self.name = "Nuclei" + self.plugin_version = "1.0.0" + self.version = "2.5.2" + self._command_regex = re.compile(r'^(sudo nuclei|nuclei|\.\/nuclei|^.*?nuclei)\s+.*?') + self.xml_arg_re = re.compile(r"^.*(-o\s*[^\s]+).*$") + self._use_temp_file = True + self._temp_file_extension = "json" + self.json_keys = {"matched", "templateID", "host"} + + def parseOutputString(self, output, debug=False): + for vuln_json in filter(lambda x: x != '', output.split("\n")): + vuln_dict = json.loads(vuln_json) + host = vuln_dict.get('host') + url_data = urlparse(host) + ip = vuln_dict.get("ip", self.resolve_hostname(url_data.hostname)) + host_id = self.createAndAddHost( + name=ip, + hostnames=[url_data.hostname]) + port = url_data.port + if not port: + if url_data.scheme == 'https': + port = 443 + else: + port = 80 + service_id = self.createAndAddServiceToHost( + host_id, + name=url_data.scheme, + ports=port, + protocol="tcp", + status='open', + version='', + description='web server') + matched = vuln_dict.get('matched') + matched_data = urlparse(matched) + reference = vuln_dict["info"].get('reference', []) + if not reference: + reference = [] + else: + if isinstance(reference, str): + if re.match('^- ', reference): + reference = list(filter(None, [re.sub('^- ', '', elem) for elem in reference.split('\n')])) + else: + reference = [reference] + references = vuln_dict["info"].get('references', []) + if references: + if isinstance(references, str): + if re.match('^- ', references): + references = list(filter(None, [re.sub('^- ', '', elem) for elem in references.split('\n')])) + else: + references = [references] + else: + references = [] + cwe = vuln_dict['info'].get('cwe', []) + capec = vuln_dict['info'].get('capec', []) + refs = sorted(list(set(reference + references + capec))) + tags = vuln_dict['info'].get('tags', []) + if isinstance(tags, str): + tags = tags.split(',') + impact = vuln_dict['info'].get('impact') + resolution = vuln_dict['info'].get('resolution', '') + easeofresolution = vuln_dict['info'].get('easeofresolution') + request = vuln_dict.get('request', '') + if request: + method = request.split(" ")[0] + else: + method = "" + data = [f"Matched: {vuln_dict.get('matched', '')}", + f"Tags: {vuln_dict['info'].get('tags', '')}", + f"Template ID: {vuln_dict.get('templateID', '')}"] + + name = vuln_dict["info"].get("name") + run_date = vuln_dict.get('timestamp') + if run_date: + run_date = parse(run_date) + self.createAndAddVulnWebToService( + host_id, + service_id, + name=name, + desc=vuln_dict["info"].get("description", name), + ref=refs, + severity=vuln_dict["info"].get('severity'), + tags=tags, + impact=impact, + resolution=resolution, + easeofresolution=easeofresolution, + website=host, + request=request, + response=vuln_dict.get('response', ''), + method=method, + query=matched_data.query, + params=matched_data.params, + path=matched_data.path, + data="\n".join(data), + external_id=f"NUCLEI-{vuln_dict.get('templateID', '')}", + run_date=run_date, + cwe=cwe + ) + + def processCommandString(self, username, current_path, command_string): + super().processCommandString(username, current_path, command_string) + arg_match = self.xml_arg_re.match(command_string) + if arg_match is None: + return re.sub(r"(^.*?nuclei)", + r"\1 --json -irr -o %s" % self._output_file_path, + command_string) + else: + return re.sub(arg_match.group(1), + r"--json -irr -o %s" % self._output_file_path, + command_string) + + def canParseCommandString(self, current_input): + can_parse = super().canParseCommandString(current_input) + if can_parse: + try: + proc = subprocess.Popen([self.command, '-version'], stderr=subprocess.PIPE) # nosec + output = proc.stderr.read() + match = re.search(r"Current Version: ([0-9.]+)", output.decode('UTF-8')) + if match: + nuclei_version = match.groups()[0] + return version.parse(nuclei_version) <= version.parse("2.5.2") + else: + return False + except Exception as e: + return False + +def createPlugin(*args, **kwargs): + return NucleiLegacyPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/openscap/__init__.py b/faraday_plugins/plugins/repo/openscap/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/faraday_plugins/plugins/repo/openscap/plugin.py b/faraday_plugins/plugins/repo/openscap/plugin.py new file mode 100644 index 00000000..cfe6772f --- /dev/null +++ b/faraday_plugins/plugins/repo/openscap/plugin.py @@ -0,0 +1,211 @@ +#!/usr/bin/env python +""" +Faraday Penetration Test IDE +Copyright (C) 2020 Infobyte LLC (http://www.infobytesec.com/) +See the file 'doc/LICENSE' for the license information +""" +import ipaddress +from datetime import datetime + +from lxml import etree + +from faraday_plugins.plugins.plugin import PluginXMLFormat + +__author__ = 'Blas Moyano' +__copyright__ = 'Copyright 2020, Faraday Project' +__credits__ = ['Blas Moyano'] +__license__ = '' +__version__ = '1.0.0' +__status__ = 'Development' + + +class OpenScapParser: + def __init__(self, xml_output): + self.tree = self.parse_xml(xml_output) + + if self.tree is not None: + self.rule_date = self.get_parser_rule(self.tree.findall('Rule', self.tree.nsmap)) + self.result_data = self.get_parser_result(self.tree.findall('TestResult', self.tree.nsmap)) + self.tree = None + + def parse_xml(self, xml_output): + try: + parser = etree.XMLParser(recover=True) + tree = etree.fromstring(xml_output, parser=parser) + except SyntaxError as err: + print(f'SyntaxError In xml: {err}. {xml_output}') + return None + return tree + + def get_parser_rule(self, tree): + list_rules = [] + for rule in tree: + title = rule.find('title', self.tree.nsmap) + ident = rule.find('ident', self.tree.nsmap) + check = rule.find('check', self.tree.nsmap) + check_ref = rule.find('check/check-content-ref', self.tree.nsmap) + try: + ident_result = ident.text + except AttributeError: + ident_result = "" + json_rule = { + "rule_id": rule.attrib.get('id', None), + "rule_sev": rule.attrib.get('severity', None), + "rule_title": title.text, + "rule_ident": ident_result, + "rule_check": check.attrib.get('system', None), + "rule_ref_name": check_ref.attrib.get('name', None), + "rule_ref_href": check_ref.attrib.get('href', None) + } + list_rules.append(json_rule) + return list_rules + + def get_parser_result(self, tree): + list_result = [] + list_ip = [] + list_mac = [] + list_data = [] + for result in tree: + title = result.find('title', self.tree.nsmap) + target = result.find('target', self.tree.nsmap) + ips = result.findall('target-address', self.tree.nsmap) + target_facts = result.findall('target-facts/fact', self.tree.nsmap) + rule_result = result.findall('rule-result', self.tree.nsmap) + + for ip in ips: + list_ip.append(ip.text) + + for mac in target_facts: + fact_name = mac.attrib.get('name', None) + if fact_name == 'urn:xccdf:fact:ethernet:MAC': + list_mac.append(mac.text) + + for data in rule_result: + list_ident = [] + idents = data.findall('ident', self.tree.nsmap) + check = data.find('check', self.tree.nsmap) + check_ref = data.find('check/check-content-ref', self.tree.nsmap) + + for ident in idents: + json_ident = { + "system": data.attrib.get('system', None), + "text": ident.text + } + list_ident.append(json_ident) + + json_data = { + "id": data.attrib.get('idref', None), + "time": data.attrib.get('time', None), + "severity": data.attrib.get('severity', None), + "ident": list_ident, + "check": check.attrib.get('system', None), + "ref_name": check_ref.attrib.get('name', None), + "ref_href": check_ref.attrib.get('href', None) + } + + status = data.find('result', self.tree.nsmap) + if status.text == 'fail': + list_data.append(json_data) + + json_result = { + "id": result.attrib.get('id', None), + "start_time": result.attrib.get('start-time', None), + "end_time": result.attrib.get('end-time', None), + "result_title": title.text, + "target": target.text, + "ips": list_ip, + "mac": list_mac, + "rule_result": list_data + } + list_result.append(json_result) + return list_result + + +class OpenScapPlugin(PluginXMLFormat): + + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) + self.identifier_tag = "Benchmark" + self.id = 'OpenScap' + self.name = 'OpenScap XML Output Plugin' + self.plugin_version = '1.0.0' + self.version = '1.0.0' + self.framework_version = '1.0.0' + self.options = None + self.protocol = None + self.port = '80' + + def parseOutputString(self, output): + parser = OpenScapParser(output) + ips = [] + + for ip in parser.result_data[0]['ips']: + len_start_port = ip.find(":") + if len_start_port > -1: + ip = ip[:len_start_port] + try: + ipaddress.ip_address(ip) + ips.append(ip) + except ValueError: + pass + for ip in ips: + if ip != '127.0.0.1': + ip = ip + ips.remove(ip) + break + + list_mac = parser.result_data[0]['mac'] + for mac in list_mac: + if mac != '00:00:00:00:00:00': + mac_address = mac + list_mac.remove(mac_address) + break + + description = f'Title: {parser.result_data[0]["result_title"]} ' \ + f'Ips: {ips} ' \ + f'Macs: {list_mac}' + host_id = self.createAndAddHost( + name=ip, + hostnames=[parser.result_data[0]['target']], + description=description, + mac=mac_address + ) + + rules_fail = parser.result_data[0]['rule_result'] + if rules_fail: + info_rules = parser.rule_date + severity = 0 + + for rule in rules_fail: + vuln_run_date = datetime.strptime( + rule['time'].replace('T', ' '), + '%Y-%m-%d %H:%M:%S') + + if rule['severity'] == 'low': + severity = 1 + elif rule['severity'] == 'medium': + severity = 2 + elif rule['severity'] == 'high': + severity = 3 + + desc = f'name: {rule["ref_name"]} - link: {rule["ref_href"]}' + + for info in info_rules: + if rule['id'] == info['rule_id']: + vuln_name = info['rule_title'] + vuln_data = info['rule_check'] + vuln_cve = info['rule_ident'] + + self.createAndAddVulnToHost( + host_id, + vuln_name, + desc=desc, + severity=severity, + data=vuln_data, + external_id=rule['id'], + run_date=vuln_run_date, + cve=[vuln_cve]) + + +def createPlugin(*args, **kwargs): + return OpenScapPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/openvas/__init__.py b/faraday_plugins/plugins/repo/openvas/__init__.py index ea531e17..625a6e25 100644 --- a/faraday_plugins/plugins/repo/openvas/__init__.py +++ b/faraday_plugins/plugins/repo/openvas/__init__.py @@ -4,4 +4,4 @@ See the file 'doc/LICENSE' for the license information """ -# I'm Py3 \ No newline at end of file + diff --git a/faraday_plugins/plugins/repo/openvas/plugin.py b/faraday_plugins/plugins/repo/openvas/plugin.py index ebbd768c..734551e7 100644 --- a/faraday_plugins/plugins/repo/openvas/plugin.py +++ b/faraday_plugins/plugins/repo/openvas/plugin.py @@ -1,26 +1,17 @@ +import re +from collections import defaultdict +from copy import copy """ Faraday Penetration Test IDE Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/) See the file 'doc/LICENSE' for the license information """ -import re -from collections import defaultdict -from copy import copy - -try: - import xml.etree.cElementTree as ET - import xml.etree.ElementTree as ET_ORIG - ETREE_VERSION = ET_ORIG.VERSION -except ImportError: - import xml.etree.ElementTree as ET - ETREE_VERSION = ET.VERSION from faraday_plugins.plugins.plugin import PluginXMLFormat from faraday_plugins.plugins.plugins_utils import filter_services -ETREE_VERSION = [int(i) for i in ETREE_VERSION.split(".")] - +import xml.etree.ElementTree as ET __author__ = "Francisco Amato" __copyright__ = "Copyright (c) 2013, Infobyte LLC" @@ -90,10 +81,10 @@ def get_items(self, tree, hosts): try: yield Item(node, hosts) except Exception as e: - self.logger.error("Error generating Iteem from %s [%s]", node.attrib, e) + self.logger.error(f"Error generating Iteem from {node.attrib} [{e}]") except Exception as e: - self.logger.error("Tag not found: %s", e) + self.logger.error(f"Tag not found: {e}") def get_hosts(self, tree): # Hosts are located in: /report/report/host @@ -126,7 +117,7 @@ def get_data_from_detail(self, details): def do_clean(self, value): myreturn = "" if value is not None: - myreturn = re.sub("\s+", " ", value) + myreturn = re.sub(r"\s+", " ", value) return myreturn.strip() @@ -136,28 +127,8 @@ def get_attrib_from_subnode(xml_node, subnode_xpath_expr, attrib_name): @return An attribute value """ - global ETREE_VERSION - node = None - - if ETREE_VERSION[0] <= 1 and ETREE_VERSION[1] < 3: - - match_obj = re.search( - "([^\@]+?)\[\@([^=]*?)=\'([^\']*?)\'", - subnode_xpath_expr) - - if match_obj is not None: - node_to_find = match_obj.group(1) - xpath_attrib = match_obj.group(2) - xpath_value = match_obj.group(3) - for node_found in xml_node.findall(node_to_find): - if node_found.attrib[xpath_attrib] == xpath_value: - node = node_found - break - else: - node = xml_node.find(subnode_xpath_expr) - else: - node = xml_node.find(subnode_xpath_expr) + node = xml_node.find(subnode_xpath_expr) if node is not None: return node.get(attrib_name) @@ -174,15 +145,21 @@ class Item: def __init__(self, item_node, hosts): self.node = item_node self.host = self.get_text_from_subnode('host') + self.threat = self.get_text_from_subnode('threat') self.subnet = self.get_text_from_subnode('subnet') if self.subnet == '': self.subnet = self.host self.port = None self.severity = self.severity_mapper() + self.severity_nr = self.get_text_from_subnode("severity") self.service = "Unknown" self.protocol = "" + details = self.node.findall("detection/result/details/detail") + self.cpe = details[0].findtext("value") if details else None port_string = self.get_text_from_subnode('port') info = port_string.split("/") + if len(info) < 2: + info = details[1].findtext("value").split("/") if details else ["", ""] self.protocol = "".join(filter(lambda x: x.isalpha() or x in ("-", "_"), info[1])) self.port = "".join(filter(lambda x: x.isdigit(), info[0])) or None if not self.port: @@ -196,22 +173,28 @@ def __init__(self, item_node, hosts): self.nvt = self.node.findall('nvt')[0] self.node = self.nvt self.id = self.node.get('oid') + self.cvss_base = self.get_text_from_subnode("cvss_base") self.name = self.get_text_from_subnode('name') self.cve = self.get_text_from_subnode('cve') if self.get_text_from_subnode('cve') != "NOCVE" else "" self.bid = self.get_text_from_subnode('bid') if self.get_text_from_subnode('bid') != "NOBID" else "" self.xref = self.get_text_from_subnode('xref') if self.get_text_from_subnode('xref') != "NOXREF" else "" + self.cwe = [] + if "URL:https://cwe.mitre.org/data/definitions/" in self.xref: + self.cwe.append("CWE-"+self.xref.split("URL:https://cwe.mitre.org/data/definitions/")[1] + .replace("html", "")) self.description = '' self.resolution = '' self.cvss_vector = '' self.tags = self.get_text_from_subnode('tags') self.data = self.get_text_from_subnode('description') + self.data += f'\n\nid {item_node.attrib.get("id")}' if self.tags: tags_data = self.get_data_from_tags(self.tags) self.description = tags_data['description'] self.resolution = tags_data['solution'] self.cvss_vector = tags_data['cvss_base_vector'] if tags_data['impact']: - self.data += '\n\nImpact: {}'.format(tags_data['impact']) + self.data += f'\n\nImpact: {tags_data["impact"]}' def get_text_from_subnode(self, subnode_xpath_expr): """ @@ -255,7 +238,7 @@ def get_service(self, port_string, port, details_from_host): def do_clean(self, value): myreturn = "" if value is not None: - myreturn = re.sub("\s+", " ", value) + myreturn = re.sub(r"\s+", " ", value) return myreturn.strip() @@ -320,8 +303,8 @@ class OpenvasPlugin(PluginXMLFormat): Example plugin to parse openvas output. """ - def __init__(self): - super().__init__() + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) self.identifier_tag = ["report", "get_results_response"] self.id = "Openvas" self.name = "Openvas XML Output Plugin" @@ -336,7 +319,7 @@ def report_belongs_to(self, **kwargs): with open(report_path) as f: output = f.read() return re.search("OpenVAS", output) is not None \ - or re.search('', output) is not None\ + or re.search('', output) is not None \ or re.search('', output) is not None return False @@ -346,14 +329,15 @@ def parseOutputString(self, output): from the xml where it expects it to be present. """ parser = OpenvasXmlParser(output, self.logger) - web = False ids = {} # The following threats values will not be taken as vulns self.ignored_severities = ['Log', 'Debug'] for ip, values in parser.hosts.items(): # values contains: ip details and ip hostnames + os_report = values['details'].get('best_os_txt') h_id = self.createAndAddHost( ip, + os_report[0] if os_report else '', hostnames=values['hostnames'] ) ids[ip] = h_id @@ -361,18 +345,29 @@ def parseOutputString(self, output): if item.name is not None: ref = [] + cve = [] + cvss2 = {} if item.cve: cves = item.cve.split(',') - for cve in cves: - ref.append(cve.strip()) + for i in cves: + cve.append(i.strip()) if item.bid: bids = item.bid.split(',') for bid in bids: - ref.append("BID-%s" % bid.strip()) + ref.append(f"BID-{bid.strip()}") if item.xref: ref.append(item.xref) if item.tags and item.cvss_vector: - ref.append(item.cvss_vector) + cvss2["vector_string"] = item.cvss_vector + if item.cpe: + ref.append(f"{item.cpe}") + if item.severity_nr: + if float(item.severity_nr) >= 9.0: + item.threat = "Critical" + item.severity = "Critical" + ref.append(f"SEVERITY NUMBER: {item.severity_nr}") + if item.threat: + ref.append(f"THREAT: {item.threat}") if item.subnet in ids: h_id = ids[item.host] @@ -391,8 +386,12 @@ def parseOutputString(self, output): severity=item.severity, resolution=item.resolution, ref=ref, - external_id=item.id, - data=item.data) + external_id=f"OPENVAS-{item.id}", + data=item.data, + cve=cve, + cwe=item.cwe, + cvss2=cvss2 + ) else: if item.service: web = re.search( @@ -422,8 +421,12 @@ def parseOutputString(self, output): severity=item.severity, ref=ref, resolution=item.resolution, - external_id=item.id, - data=item.data) + external_id=f"OPENVAS-{item.id}", + data=item.data, + cve=cve, + cwe=item.cwe, + cvss2=cvss2 + ) elif item.severity not in self.ignored_severities: self.createAndAddVulnToService( h_id, @@ -433,21 +436,21 @@ def parseOutputString(self, output): severity=item.severity, ref=ref, resolution=item.resolution, - external_id=item.id, - data=item.data) + external_id=f"OPENVAS-{item.id}", + data=item.data, + cve=cve, + cwe=item.cwe, + cvss2=cvss2 + ) del parser - def _isIPV4(self, ip): + @staticmethod + def _isIPV4(ip): if len(ip.split(".")) == 4: return True else: return False - def setHost(self): - pass - - -def createPlugin(): - return OpenvasPlugin() -# I'm Py3 +def createPlugin(*args, **kwargs): + return OpenvasPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/owasp/__init__.py b/faraday_plugins/plugins/repo/owasp/__init__.py new file mode 100644 index 00000000..625a6e25 --- /dev/null +++ b/faraday_plugins/plugins/repo/owasp/__init__.py @@ -0,0 +1,7 @@ +""" +Faraday Penetration Test IDE +Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/) +See the file 'doc/LICENSE' for the license information + +""" + diff --git a/faraday_plugins/plugins/repo/owasp/plugin.py b/faraday_plugins/plugins/repo/owasp/plugin.py new file mode 100644 index 00000000..c3ed7a04 --- /dev/null +++ b/faraday_plugins/plugins/repo/owasp/plugin.py @@ -0,0 +1,73 @@ +import csv +import io +import re +from collections import defaultdict +from copy import copy +""" +Faraday Penetration Test IDE +Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/) +See the file 'doc/LICENSE' for the license information + +""" + +from faraday_plugins.plugins.plugin import PluginCSVFormat + + +severity_map = { + 'CRITICAL': 'critical', + 'HIGH': 'high', + 'MEDIUM': 'medium', + 'LOW': 'low' +} + + +class owaspDependencyCheckPlugin(PluginCSVFormat): + """ + Parses OWASP Dependency Check reports in CSV format. + """ + + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) + self.id = "owaspDependencyCheck" + self.name = "OWASP Dependency Check Plugin" + self.plugin_version = "1.0.0" + self.version = "1.0.0" + self.framework_version = "1.0.0" + self.csv_headers = {"Project", "ScanDate", "DependencyName", "DependencyPath", "Description"} + + def parseOutputString(self, output): + try: + reader = csv.DictReader(io.StringIO(output)) + except Exception as e: + print(f"Error parsing output: {e}") + return None + + for row in reader: + ip = row.get("Project") + description = row.get("Description") + dep_name = row.get('DependencyName') + dep_path = row.get('DependencyPath') + cve = row.get('CVE') + cwe = row.get('CWE') + severity = row.get('CVSSv3_BaseSeverity') + cvss3 = row.get('CVSSv3') + cvss2 = row.get('CVSSv2') + vulnerability_name = row.get('Vulnerability') + + # Create host + host_id = self.createAndAddHost(name=ip) + + # Create vulnerability + self.createAndAddVulnToHost(host_id, + name=vulnerability_name, + desc=f"{description}\nDependencyName: {dep_name}\nDependencyPath: {dep_path}", + severity=severity_map.get(severity, 'info'), + cve=cve, + cwe=cwe, + cvss3={"vector_string": cvss3}, + cvss2={"vector_string": cvss2}, + ) + + +def createPlugin(*args, **kwargs): + return owaspDependencyCheckPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/pasteanalyzer/__init__.py b/faraday_plugins/plugins/repo/pasteanalyzer/__init__.py index ea531e17..625a6e25 100644 --- a/faraday_plugins/plugins/repo/pasteanalyzer/__init__.py +++ b/faraday_plugins/plugins/repo/pasteanalyzer/__init__.py @@ -4,4 +4,4 @@ See the file 'doc/LICENSE' for the license information """ -# I'm Py3 \ No newline at end of file + diff --git a/faraday_plugins/plugins/repo/pasteanalyzer/plugin.py b/faraday_plugins/plugins/repo/pasteanalyzer/plugin.py index 1e7fd647..eea14a6d 100644 --- a/faraday_plugins/plugins/repo/pasteanalyzer/plugin.py +++ b/faraday_plugins/plugins/repo/pasteanalyzer/plugin.py @@ -6,7 +6,6 @@ """ # Author: @EzequielTBH -from builtins import str from faraday_plugins.plugins.plugin import PluginBase import json @@ -21,8 +20,8 @@ class pasteAnalyzerPlugin(PluginBase): - def __init__(self): - super().__init__() + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) self.id = "pasteAnalyzer" self.name = "pasteAnalyzer JSON Output Plugin" self.plugin_version = "1.0.0" @@ -37,7 +36,7 @@ def parseOutputString(self, output): indexStart:self.command_string.find(" ", indexStart)] fileJson = self._current_path + "/" + fileJson try: - with open(fileJson, "r") as fileJ: + with open(fileJson) as fileJ: results = json.loads(fileJ.read()) except Exception as e: return @@ -61,7 +60,7 @@ def parseOutputString(self, output): for element in data: # Is Category - if type(element) == str: #TODO bte arrray decode + if isinstance(element, str): #TODO bte arrray decode description += element + ": " # Is a list with results! @@ -86,7 +85,5 @@ def processCommandString(self, username, current_path, command_string): return command_string -def createPlugin(): - return pasteAnalyzerPlugin() - -# I'm Py3 +def createPlugin(*args, **kwargs): + return pasteAnalyzerPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/peepingtom/__init__.py b/faraday_plugins/plugins/repo/peepingtom/__init__.py index ea531e17..625a6e25 100644 --- a/faraday_plugins/plugins/repo/peepingtom/__init__.py +++ b/faraday_plugins/plugins/repo/peepingtom/__init__.py @@ -4,4 +4,4 @@ See the file 'doc/LICENSE' for the license information """ -# I'm Py3 \ No newline at end of file + diff --git a/faraday_plugins/plugins/repo/peepingtom/plugin.py b/faraday_plugins/plugins/repo/peepingtom/plugin.py index cc50f3fd..b46accdf 100644 --- a/faraday_plugins/plugins/repo/peepingtom/plugin.py +++ b/faraday_plugins/plugins/repo/peepingtom/plugin.py @@ -17,7 +17,6 @@ __status__ = "Development" from faraday_plugins.plugins.plugin import PluginBase -from faraday_plugins.plugins.plugins_utils import resolve_hostname class PeepingTomPlugin(PluginBase): @@ -25,8 +24,8 @@ class PeepingTomPlugin(PluginBase): Handle PeepingTom (https://bitbucket.org/LaNMaSteR53/peepingtom) output """ - def __init__(self): - super().__init__() + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) self.id = "peepingtom" self.name = "PeepingTom" self.plugin_version = "0.0.1" @@ -49,7 +48,7 @@ def parseOutputString(self, output): for url in re.findall(r'href=[\'"]?([^\'" >]+)', html): if "://" in url: url_parsed = urlparse(url) - address = resolve_hostname(url_parsed.netloc) + address = self.resolve_hostname(url_parsed.netloc) host = self.createAndAddHost(address) service = self.createAndAddServiceToHost(host, "http", protocol="tcp", ports=[80]) self.createAndAddNoteToService( @@ -71,7 +70,5 @@ def processCommandString(self, username, current_path, command_string): self._path = current_path -def createPlugin(): - return PeepingTomPlugin() - -# I'm Py3 +def createPlugin(*args, **kwargs): + return PeepingTomPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/pentera/DTO.py b/faraday_plugins/plugins/repo/pentera/DTO.py new file mode 100644 index 00000000..b0c27731 --- /dev/null +++ b/faraday_plugins/plugins/repo/pentera/DTO.py @@ -0,0 +1,137 @@ +from typing import List +import re + +from faraday_plugins.plugins.plugins_utils import CVE_regex + +CVE_WITH_P_regex = re.compile(r'\(CVE-\d{4}-\d{4,7}\)') + + +class Service: + def __init__(self, node): + self.node = node + + @property + def name(self): + return self.node.get("name", "") + + @property + def port(self): + return self.node.get("port", "") + + @property + def protocol(self): + return self.node.get("transport", "") + + @property + def status(self) -> str: + return self.node.get("status", "") + + +class Host: + def __init__(self, node): + self.node = node + + @property + def host_id(self) -> str: + return self.node.get("id", "") + + @property + def hostname(self) -> str: + return self.node.get("hostname", "") + + @property + def name(self) -> str: + return self.node.get("ip", "") + + @property + def os(self) -> str: + return self.node.get("os_name", "") + + @property + def services(self) -> List[Service]: + return [Service(ser) for ser in self.node.get("services", "")] + + +class Vulneravility: + def __init__(self, node): + self.node = node + + @property + def external_id(self) -> str: + return "Pentera-"+self.node.get("id", "") + + @property + def name(self) -> str: + return CVE_WITH_P_regex.sub("", self.node.get("name", "")).strip() + + @property + def description(self) -> str: + desc = self.node.get("summary", "") + if not desc: + desc = self.name + return desc + + @property + def found_on(self) -> str: + return self.node.get("found_on", "") + + @property + def host(self) -> str: + return self.node.get("target", "") + + @property + def host_id(self) -> str: + return self.node.get("target_id", "") + + @property + def port(self) -> str: + return self.node.get("port", "") + + @property + def protocol(self) -> str: + return self.node.get("protocol", "") + + @property + def severity(self) -> float: + return float(self.node.get("severity", 0)) + + @property + def data(self) -> str: + return self.node.get("insight", "") + + @property + def resolution(self) -> str: + return self.node.get("remediation", "") + + @property + def cve(self) -> str: + return CVE_regex.sub("", self.node.get("name", "")).strip() +class Achievment: + def __init__(self, node): + self.node = node + + +class Meta: + def __init__(self, node): + self.node = node + + +class PenteraJsonParser: + def __init__(self, node): + self.node = node + + @property + def meta(self) -> Meta: + return Meta(self.node.get('meta', {})) + + @property + def achievements(self) -> List[Achievment]: + return [Achievment(i) for i in self.node.get('Achievement', [])] + + @property + def vulneravilities(self) -> List[Vulneravility]: + return [Vulneravility(i) for i in self.node.get('vulnerabilities', [])] + + @property + def hosts(self) -> List[Host]: + return [Host(i) for i in self.node.get('hosts')] diff --git a/faraday_plugins/plugins/repo/pentera/__init__.py b/faraday_plugins/plugins/repo/pentera/__init__.py new file mode 100644 index 00000000..98901a0b --- /dev/null +++ b/faraday_plugins/plugins/repo/pentera/__init__.py @@ -0,0 +1,6 @@ +""" +Faraday Penetration Test IDE +Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/) +See the file 'doc/LICENSE' for the license information + +""" diff --git a/faraday_plugins/plugins/repo/pentera/plugin.py b/faraday_plugins/plugins/repo/pentera/plugin.py new file mode 100644 index 00000000..87ad5cc0 --- /dev/null +++ b/faraday_plugins/plugins/repo/pentera/plugin.py @@ -0,0 +1,107 @@ +""" +Faraday Penetration Test IDE +Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/) +See the file 'doc/LICENSE' for the license information +""" +from json import loads + +from faraday_plugins.plugins.plugin import PluginJsonFormat +from faraday_plugins.plugins.repo.pentera.DTO import PenteraJsonParser + +__author__ = "Gonzalo Martinez" +__copyright__ = "Copyright (c) 2013, Infobyte LLC" +__credits__ = ["Gonzalo Martinez"] +__license__ = "" +__version__ = "1.0.0" +__maintainer__ = "Gonzalo Martinez" +__email__ = "gmartinez@infobytesec.com" +__status__ = "Development" + + + + + +class PenteraJsonPlugin(PluginJsonFormat): + + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) + self.id = "Pentera_Json" + self.name = "Pentera Json Output Plugin" + self.plugin_version = "1.0.0" + self.version = "2.11.1" + self.framework_version = "1.0.0" + self.options = None + self._temp_file_extension = "json" + self.json_keys = {'vulnerabilities', 'hosts'} + + @staticmethod + def pentera_to_severity_level(pentera_score): + pentera_ranges = [(0.0, 0.01, 'info'), + (0.01, 2.5, 'low'), + (2.5, 4.5, 'med'), + (5, 7.49, 'high'), + (7.5, 10.1, 'critical')] + for (lower, upper, severity) in pentera_ranges: + if lower <= pentera_score < upper: + return severity + + def parseOutputString(self, output): + """ + This method will discard the output the shell sends, it will read it + from the json where it expects it to be present. + """ + + parser = PenteraJsonParser(loads(output)) + host_dict = {} + + for host in parser.hosts: + host_id = self.createAndAddHost( + name=host.name, + os=host.os, + hostnames=[host.hostname] + ) + host_dict[host.host_id] = {} + for service in host.services: + service_id = self.createAndAddServiceToHost( + host_id=host_id, + name=service.name, + protocol=service.protocol, + ports=service.port, + status=service.status + ) + host_dict[host.host_id][service.port] = [host_id, service_id] + for vuln in parser.vulneravilities: + try: + self.createAndAddVulnToService( + host_id=host_dict[vuln.host_id][vuln.port][0], + service_id=host_dict[vuln.host_id][vuln.port][1], + name=vuln.name, + desc=vuln.description, + severity=self.pentera_to_severity_level(vuln.severity), + resolution=vuln.resolution, + data=vuln.data, + external_id=vuln.external_id, + cve=vuln.cve + ) + except KeyError: + if "host" in vuln.found_on.lower(): + host = vuln.found_on.lower().replace("host:", "").strip().split(",")[0] + else: + host = "NOT-PROVIDED" + host_id = self.createAndAddHost( + name=host, + ) + self.createAndAddVulnToHost( + host_id=host_id, + name=vuln.name, + desc=vuln.description, + severity=self.pentera_to_severity_level(vuln.severity), + resolution=vuln.resolution, + data=vuln.data, + external_id=vuln.external_id, + cve=vuln.cve + ) + + +def createPlugin(*args, **kwargs): + return PenteraJsonPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/ping/__init__.py b/faraday_plugins/plugins/repo/ping/__init__.py index ea531e17..625a6e25 100755 --- a/faraday_plugins/plugins/repo/ping/__init__.py +++ b/faraday_plugins/plugins/repo/ping/__init__.py @@ -4,4 +4,4 @@ See the file 'doc/LICENSE' for the license information """ -# I'm Py3 \ No newline at end of file + diff --git a/faraday_plugins/plugins/repo/ping/plugin.py b/faraday_plugins/plugins/repo/ping/plugin.py index 89bb6216..ab75495c 100644 --- a/faraday_plugins/plugins/repo/ping/plugin.py +++ b/faraday_plugins/plugins/repo/ping/plugin.py @@ -4,9 +4,10 @@ See the file 'doc/LICENSE' for the license information """ -from faraday_plugins.plugins.plugin import PluginBase import re +from faraday_plugins.plugins.plugin import PluginBase + __author__ = "Facundo de Guzmán, Esteban Guillardoy" __copyright__ = "Copyright (c) 2013, Infobyte LLC" __credits__ = ["Facundo de Guzmán", "Esteban Guillardoy"] @@ -23,8 +24,8 @@ class CmdPingPlugin(PluginBase): Basically detects if user was able to connect to a device """ - def __init__(self): - super().__init__() + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) self.id = "ping" self.name = "Ping" self.plugin_version = "0.0.1" @@ -35,10 +36,9 @@ def parseOutputString(self, output): reg = re.search(r"PING ([\w\.-:]+)( |)\(([\w\.:]+)\)", output) if re.search("0 received|unknown host", output) is None and reg is not None: - ip_address = reg.group(3) hostname = reg.group(1) - h_id = self.createAndAddHost(ip_address, hostnames=[hostname]) + self.createAndAddHost(ip_address, hostnames=[hostname]) return True def _isIPV4(self, ip): @@ -48,9 +48,5 @@ def _isIPV4(self, ip): return False - -def createPlugin(): - return CmdPingPlugin() - - -# I'm Py3 +def createPlugin(*args, **kwargs): + return CmdPingPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/pingcastle/__init__.py b/faraday_plugins/plugins/repo/pingcastle/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/faraday_plugins/plugins/repo/pingcastle/plugin.py b/faraday_plugins/plugins/repo/pingcastle/plugin.py new file mode 100644 index 00000000..c4835060 --- /dev/null +++ b/faraday_plugins/plugins/repo/pingcastle/plugin.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python +""" +Faraday Penetration Test IDE +Copyright (C) 2016 Infobyte LLC (http://www.infobytesec.com/) +See the file 'doc/LICENSE' for the license information +""" +import xml.etree.ElementTree as ET +from datetime import datetime + +from faraday_plugins.plugins.plugin import PluginXMLFormat + +__author__ = 'Gonzalo Martinez' +__copyright__ = 'Copyright 2023, Faraday Project' +__credits__ = ['Gonzalo Martinez'] +__license__ = '' +__version__ = '1.0.0' +__status__ = 'Development' + + +class PingCastlePlugin(PluginXMLFormat): + + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) + self.identifier_tag = ["HealthcheckData"] + self.id = 'PingCastle' + self.name = 'Ping Castle XML Output Plugin' + self.plugin_version = '1.0.0' + self.version = '1.0.0' + self.framework_version = '1.0.0' + + @staticmethod + def map_severity(score): + if score == 0: + return "low" + elif score < 11: + return "medium" + elif score < 31: + return "high" + else: + return "critical" + + def parseOutputString(self, output): + tree = ET.fromstring(output) + url = tree.find("DomainFQDN").text + + host_id = self.createAndAddHost(name=url) + for risk in tree.find("RiskRules"): + vuln = {} + vuln["severity"] = self.map_severity(int(risk.find("Points").text)) + name = risk.find("Rationale").text + vuln["name"] = risk.find('Model').text + desc = "" + desc += f"Category: {risk.find('Category').text}\n" + desc += f"Model: {risk.find('Model').text}\n" + desc += f"RiskId: {risk.find('RiskId').text}\n" + desc += f"Rationale: {risk.find('Rationale').text}" + vuln["desc"] = desc + self.createAndAddVulnToHost(host_id=host_id, **vuln) + + +def createPlugin(*args, **kwargs): + return PingCastlePlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/popeye/__init__.py b/faraday_plugins/plugins/repo/popeye/__init__.py new file mode 100644 index 00000000..625a6e25 --- /dev/null +++ b/faraday_plugins/plugins/repo/popeye/__init__.py @@ -0,0 +1,7 @@ +""" +Faraday Penetration Test IDE +Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/) +See the file 'doc/LICENSE' for the license information + +""" + diff --git a/faraday_plugins/plugins/repo/popeye/plugin.py b/faraday_plugins/plugins/repo/popeye/plugin.py new file mode 100644 index 00000000..a886a498 --- /dev/null +++ b/faraday_plugins/plugins/repo/popeye/plugin.py @@ -0,0 +1,60 @@ +""" +Faraday Penetration Test IDE +Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/) +See the file 'doc/LICENSE' for the license information + +""" +import json +from faraday_plugins.plugins.plugin import PluginJsonFormat + +__author__ = "Gonzalo Martinez" +__copyright__ = "Copyright (c) 2013, Faradaysec LLC" +__credits__ = ["Gonzalo Martinez"] +__version__ = "1.0.0" +__maintainer__ = "Gonzalo Martinez" +__email__ = "gmartinez@faradaysec.com" +__status__ = "Development" + + +class PopeyeJsonPlugin(PluginJsonFormat): + map_level = { + '1': 'low', + '2': 'med', + '3': 'high' + } + + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) + self.id = "Popeye_Json" + self.name = "Popeye JSON Output Plugin" + self.plugin_version = "1" + self.json_keys = {'popeye'} + self.framework_version = "1.0.0" + self._temp_file_extension = "json" + + def parseOutputString(self, output): + data = json.loads(output) + for test in data['popeye'].get('sanitizers'): + path = test.get('gvr') + sanitizer = test.get('sanitizer') + h_id = False + for issue in test.get('issues', []): + for group in test['issues'][issue]: + level = group.get('level', 0) + if level == 0: + continue + if not h_id: + h_id = self.createAndAddHost(issue) + external_id = group.get("message").split(']')[0].replace('[', '') + desc = f'{group.get("message").split("]")[1]}\nPATH: {path}' + self.createAndAddVulnToHost( + host_id=h_id, + name=sanitizer, + desc=desc, + severity=self.map_level[str(level)], + external_id=external_id + ) + + +def createPlugin(*args, **kwargs): + return PopeyeJsonPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/propecia/__init__.py b/faraday_plugins/plugins/repo/propecia/__init__.py index ea531e17..625a6e25 100755 --- a/faraday_plugins/plugins/repo/propecia/__init__.py +++ b/faraday_plugins/plugins/repo/propecia/__init__.py @@ -4,4 +4,4 @@ See the file 'doc/LICENSE' for the license information """ -# I'm Py3 \ No newline at end of file + diff --git a/faraday_plugins/plugins/repo/propecia/plugin.py b/faraday_plugins/plugins/repo/propecia/plugin.py index b762c62f..13e984d4 100644 --- a/faraday_plugins/plugins/repo/propecia/plugin.py +++ b/faraday_plugins/plugins/repo/propecia/plugin.py @@ -24,8 +24,8 @@ class CmdPropeciaPlugin(PluginBase): Basically inserts into the tree the ouput of this tool """ - def __init__(self): - super().__init__() + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) self.id = "propecia" self.name = "propecia port scanner" self.plugin_version = "0.0.1" @@ -61,7 +61,5 @@ def processCommandString(self, username, current_path, command_string): self._port = count_args[2] -def createPlugin(): - return CmdPropeciaPlugin() - -# I'm Py3 +def createPlugin(*args, **kwargs): + return CmdPropeciaPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/awsprowler/__init__.py b/faraday_plugins/plugins/repo/prowler/__init__.py similarity index 100% rename from faraday_plugins/plugins/repo/awsprowler/__init__.py rename to faraday_plugins/plugins/repo/prowler/__init__.py diff --git a/faraday_plugins/plugins/repo/prowler/plugin.py b/faraday_plugins/plugins/repo/prowler/plugin.py new file mode 100644 index 00000000..b5a939f9 --- /dev/null +++ b/faraday_plugins/plugins/repo/prowler/plugin.py @@ -0,0 +1,164 @@ +""" +Faraday Penetration Test IDE +Copyright (C) 2020 Infobyte LLC (http://www.infobytesec.com/) +See the file 'doc/LICENSE' for the license information + +""" +from dateutil.parser import parse +import json +from datetime import datetime +from dataclasses import dataclass +from faraday_plugins.plugins.plugin import PluginJsonFormat + +__author__ = "Diego Nadares" +__copyright__ = "Copyright (c) 2020, Infobyte LLC" +__credits__ = ["Diego Nadares", "Nicolas Rebagliati"] +__license__ = "" +__version__ = "0.0.1" +__maintainer__ = "Diego Nadares" +__email__ = "dnadares@faradaysec.com" +__status__ = "Development" + + +@dataclass +class Issue: + region: str + profile: str + severity: str + scored: str + account: str + description: str + status: str + status_extended: str + check_title: str + check_id: str + timestamp: datetime + compliance: str + categories: str + service: str + risk: str + doc_link: str + remediation: str + resource_arn: str + resource_id: str + + +class ProwlerJsonParser: + + def parse_issues(self, records): + records = json.loads(records) + for record in records: + region = record.get("Region", "AWS_REGION") + profile = record.get("Profile", "") + severity = record.get("Severity", "info").lower() + scored = record.get("Status", "") + account = record.get("AccountId", "") + description = record.get("Description", "") + status = record.get("Status", "") + status_extended = record.get("StatusExtended", "") + check_title = record.get("CheckTitle", "") + check_id = record.get("CheckID", "") + timestamp = record.get("AssessmentStartTime", None) + if timestamp: + timestamp = parse(timestamp) + compliance = record.get("Compliance", "") + categories = record.get("Categories", "") + service = record.get("ServiceName", "") + risk = record.get("Risk", "") + doc_link = record.get("RelatedUrl", "") + remediation = record.get("Remediation", "") + resource_arn = record.get("ResourceArn", "") + resource_id = record.get("ResourceId", "") + if status == "FAIL": + self.issues.append(Issue(region=region, profile=profile, severity=severity, + scored=scored, categories=categories, account=account, + description=description, status=status, status_extended=status_extended, + check_title=check_title, check_id=check_id, timestamp=timestamp, + compliance=compliance, service=service, risk=risk, + doc_link=doc_link, remediation=remediation, resource_id=resource_id, + resource_arn=resource_arn) + ) + + def __init__(self, json_output): + self.issues = [] + self.parse_issues(json_output) + + +def parse_remediation(remediation): + recommendation = remediation.get("Recommendation", None) + if recommendation: + resolution_text = recommendation.get("Text", "") + resolution_url = recommendation.get("Url", "") + + code = remediation.get("Code", None) + if code: + NativeIaC = code.get("NativeIaC", "") + Terraform = code.get("Terraform", "") + CLI = code.get("CLI", "") + Other = code.get("Other", "") + + resolution = f"{resolution_text}" + if resolution_url: + resolution = f"{resolution}\n{resolution_url}" + if NativeIaC: + resolution = f"{resolution}\n{NativeIaC}" + if Terraform: + resolution = f"{resolution}\n{Terraform}" + if CLI: + resolution = f"{resolution}\n{CLI}" + if Other: + resolution = f"{resolution}\n{Other}" + + return resolution + + +def parse_compliance(compliance: dict) -> list: + compliance_str_list = [] + for key, value in compliance.items(): + for item in value: + compliance_str_list.append(f"{key}:{item}") + return compliance_str_list + + +class ProwlerPlugin(PluginJsonFormat): + """ Handle the AWS Prowler tool. Detects the output of the tool + and adds the information to Faraday. + """ + + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) + self.id = "prowler" + self.name = "Prowler" + self.plugin_version = "0.1" + self.version = "0.0.1" + self.json_keys = {"Profile", "AccountId", "OrganizationsInfo", "Region"} + + def parseOutputString(self, output, debug=False): + parser = ProwlerJsonParser(output) + for issue in parser.issues: + host_name = f"{issue.resource_id}" + host_id = self.createAndAddHost(name=host_name, + description=f"AWS Service: {issue.service} " + f"- Account: {issue.account} " + f"- Region: {issue.region}\n" + f"ARN: {issue.resource_arn}") + + vuln_desc = f"{issue.description}\n{issue.risk}" + resolution = parse_remediation(issue.remediation) + self.createAndAddVulnToHost( + host_id=host_id, + name=issue.check_title, + desc=vuln_desc, + data=f"{issue.status_extended}", + severity=self.normalize_severity(issue.severity), + resolution=resolution, + run_date=issue.timestamp, + external_id=f"{self.name.upper()}-{issue.check_id}", + ref=[issue.doc_link], + policyviolations=parse_compliance(issue.compliance), + tags=issue.categories, + ) + + +def createPlugin(*args, **kwargs): + return ProwlerPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/prowler_legacy/__init__.py b/faraday_plugins/plugins/repo/prowler_legacy/__init__.py new file mode 100644 index 00000000..17af5f5e --- /dev/null +++ b/faraday_plugins/plugins/repo/prowler_legacy/__init__.py @@ -0,0 +1,6 @@ +""" +Faraday Penetration Test IDE +Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/) +See the file 'doc/LICENSE' for the license information + +""" \ No newline at end of file diff --git a/faraday_plugins/plugins/repo/prowler_legacy/plugin.py b/faraday_plugins/plugins/repo/prowler_legacy/plugin.py new file mode 100644 index 00000000..e38dbfaa --- /dev/null +++ b/faraday_plugins/plugins/repo/prowler_legacy/plugin.py @@ -0,0 +1,114 @@ +""" +Faraday Penetration Test IDE +Copyright (C) 2020 Infobyte LLC (http://www.infobytesec.com/) +See the file 'doc/LICENSE' for the license information + +""" +from dateutil.parser import parse +import re +import json +from datetime import datetime +from dataclasses import dataclass +from faraday_plugins.plugins.plugin import PluginMultiLineJsonFormat + +__author__ = "Nicolas Rebagliati" +__copyright__ = "Copyright (c) 2020, Infobyte LLC" +__credits__ = ["Nicolas Rebagliati"] +__license__ = "" +__version__ = "0.0.1" +__maintainer__ = "Nicolas Rebagliati" +__email__ = "nrebagliati@faradaysec.com" +__status__ = "Development" +CHECK_NUMBER_REGEX = re.compile(r"^(\[check\d\])") + +@dataclass +class Issue: + region: str + profile: str + severity: str + scored: str + account: str + message: str + control: str + status: str + level: str + control_id: str + timestamp: datetime + compliance: str + service: str + caf_epic: str + risk: str + doc_link: str + remediation: str + resource_id: str + + +class ProwlerJsonParser: + + def parse_issues(self, records): + for record in records: + json_data = json.loads(record) + region = json_data.get("Region", "AWS_REGION") + profile = json_data.get("Profile", "") + severity = json_data.get("Severity", "info").lower() + scored = json_data.get("Status", "") + account = json_data.get("Account Number", "") + message = json_data.get("Message", "") + control = CHECK_NUMBER_REGEX.sub("", json_data.get("Control", "")).strip() + status = json_data.get("Status", "") + level = json_data.get("Level", "") + control_id = json_data.get("Control ID", "") + timestamp = json_data.get("Timestamp", None) + if timestamp: + timestamp = parse(timestamp) + compliance = json_data.get("Compliance", "") + service = json_data.get("Service", "") + caf_epic = [json_data.get("CAF Epic", "")] + risk = json_data.get("Risk", "") + doc_link = json_data.get("Doc link", "") + remediation = json_data.get("Remediation", "") + resource_id = json_data.get("Resource ID", "") + if status == "FAIL": + self.issues.append(Issue(region=region, profile=profile, severity=severity, scored=scored, + account=account, message=message, control=control, status=status, + level=level, control_id=control_id, timestamp=timestamp, compliance=compliance, + service=service, caf_epic=caf_epic, risk=risk, doc_link=doc_link, + remediation=remediation, resource_id=resource_id)) + + def __init__(self, json_output): + self.issues = [] + self.parse_issues(json_output.splitlines()) + + +class ProwlerLegacyPlugin(PluginMultiLineJsonFormat): + """ Handle the AWS Prowler tool. Detects the output of the tool + and adds the information to Faraday. + """ + + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) + self.id = "prowler_legacy" + self.name = "Prowler" + self.plugin_version = "0.1" + self.version = "0.0.1" + self.json_keys = {"Profile", "Account Number", "Region"} + + def parseOutputString(self, output, debug=False): + parser = ProwlerJsonParser(output) + for issue in parser.issues: + host_name = f"{issue.service}-{issue.account}-{issue.region}" + host_id = self.createAndAddHost(name=host_name, + description=f"AWS Service: {issue.service} - Account: {issue.account}" + f" - Region: {issue.region}") + + vuln_desc = f"{issue.risk}\nCompliance: {issue.compliance}\nMessage: {issue.message}" + self.createAndAddVulnToHost(host_id=host_id, name=issue.control, desc=vuln_desc, + data=f"Resource ID: {issue.resource_id}", + severity=self.normalize_severity(issue.severity), resolution=issue.remediation, + run_date=issue.timestamp, external_id=f"{self.name.upper()}-{issue.control_id}", + ref=[issue.doc_link], + policyviolations=issue.caf_epic) + + +def createPlugin(*args, **kwargs): + return ProwlerLegacyPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/qualysguard/__init__.py b/faraday_plugins/plugins/repo/qualysguard/__init__.py index ea531e17..625a6e25 100644 --- a/faraday_plugins/plugins/repo/qualysguard/__init__.py +++ b/faraday_plugins/plugins/repo/qualysguard/__init__.py @@ -4,4 +4,4 @@ See the file 'doc/LICENSE' for the license information """ -# I'm Py3 \ No newline at end of file + diff --git a/faraday_plugins/plugins/repo/qualysguard/plugin.py b/faraday_plugins/plugins/repo/qualysguard/plugin.py index 46658a8c..85d9f027 100644 --- a/faraday_plugins/plugins/repo/qualysguard/plugin.py +++ b/faraday_plugins/plugins/repo/qualysguard/plugin.py @@ -4,13 +4,9 @@ See the file 'doc/LICENSE' for the license information """ import re -from faraday_plugins.plugins.plugin import PluginXMLFormat - import xml.etree.ElementTree as ET -ETREE_VERSION = ET.VERSION - -ETREE_VERSION = [int(i) for i in ETREE_VERSION.split('.')] +from faraday_plugins.plugins.plugin import PluginXMLFormat __author__ = 'Francisco Amato' __copyright__ = 'Copyright (c) 2013, Infobyte LLC' @@ -150,7 +146,7 @@ def __init__(self, issue_node, glossary): self.severity_dict = { '1': 'info', - '2': 'info', + '2': 'low', '3': 'med', '4': 'high', '5': 'critical'} @@ -159,7 +155,7 @@ def __init__(self, issue_node, glossary): self.glossary = glossary self.severity = self.severity_dict.get(self.get_text_from_glossary('SEVERITY'), 'info') self.title = self.get_text_from_glossary('TITLE') - self.cvss = self.get_text_from_glossary('CVSS_SCORE/CVSS_BASE') + self.cvss2 = {} self.pci = self.get_text_from_glossary('PCI_FLAG') self.solution = self.get_text_from_glossary('SOLUTION') self.impact = self.get_text_from_glossary('IMPACT') @@ -177,16 +173,14 @@ def __init__(self, issue_node, glossary): # References self.ref = [] + self.cve = [] cve_id = self.get_text_from_glossary('CVE_ID_LIST/CVE_ID/ID') if cve_id: - self.ref.append(cve_id) - - if self.cvss: - self.ref.append('CVSS SCORE: {}'.format(self.cvss)) + self.cve.append(cve_id) if self.pci: - self.ref.append('PCI: {}'.format(self.pci)) + self.ref.append(f'PCI: {self.pci}') def get_text_from_glossary(self, tag): """ @@ -281,7 +275,8 @@ def __init__(self, issue_node, parent): self.name = self.node.get('number') self.external_id = self.node.get('number') self.title = self.get_text_from_subnode('TITLE') - self.cvss = self.get_text_from_subnode('CVSS_BASE') + self.cvss2 = {} + self.diagnosis = self.get_text_from_subnode('DIAGNOSIS') self.solution = self.get_text_from_subnode('SOLUTION') self.result = self.get_text_from_subnode('RESULT') @@ -289,7 +284,7 @@ def __init__(self, issue_node, parent): self.severity_dict = { '1': 'info', - '2': 'info', + '2': 'low', '3': 'med', '4': 'high', '5': 'critical'} @@ -308,16 +303,14 @@ def __init__(self, issue_node, parent): self.desc += '' self.ref = [] + self.cve = [] for r in issue_node.findall('CVE_ID_LIST/CVE_ID'): self.node = r - self.ref.append(self.get_text_from_subnode('ID')) + self.cve.append(self.get_text_from_subnode('ID')) for r in issue_node.findall('BUGTRAQ_ID_LIST/BUGTRAQ_ID'): self.node = r self.ref.append('bid-' + self.get_text_from_subnode('ID')) - if self.cvss: - self.ref.append('CVSS BASE: ' + self.cvss) - def get_text_from_subnode(self, subnode_xpath_expr): """ Finds a subnode in the host node and the retrieves a value from it. @@ -335,8 +328,8 @@ class QualysguardPlugin(PluginXMLFormat): Example plugin to parse qualysguard output. """ - def __init__(self): - super().__init__() + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) self.identifier_tag = ["ASSET_DATA_REPORT", "SCAN"] self.id = 'Qualysguard' self.name = 'Qualysguard XML Output Plugin' @@ -350,7 +343,6 @@ def parseOutputString(self, output): parser = QualysguardXmlParser(output) - for item in parser.items: h_id = self.createAndAddHost( item.ip, @@ -366,7 +358,10 @@ def parseOutputString(self, output): severity=v.severity, resolution=v.solution if v.solution else '', desc=v.desc, - external_id=v.external_id) + external_id=v.external_id, + cve=v.cve, + cvss2=v.cvss2 + ) else: web = False @@ -399,7 +394,10 @@ def parseOutputString(self, output): severity=v.severity, desc=v.desc, resolution=v.solution if v.solution else '', - external_id=v.external_id) + external_id=v.external_id, + cve=v.cve, + cvss2=v.cvss2 + ) else: self.createAndAddVulnToService( @@ -410,16 +408,13 @@ def parseOutputString(self, output): severity=v.severity, desc=v.desc, resolution=v.solution if v.solution else '', - external_id=v.external_id) + external_id=v.external_id, + cve=v.cve, + cvss2=v.cvss2 + ) del parser - def setHost(self): - pass - - -def createPlugin(): - return QualysguardPlugin() - - +def createPlugin(*args, **kwargs): + return QualysguardPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/qualyswebapp/plugin.py b/faraday_plugins/plugins/repo/qualyswebapp/plugin.py index 76eae4de..1eaa918a 100644 --- a/faraday_plugins/plugins/repo/qualyswebapp/plugin.py +++ b/faraday_plugins/plugins/repo/qualyswebapp/plugin.py @@ -1,22 +1,17 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Faraday Penetration Test IDE Copyright (C) 2016 Infobyte LLC (http://www.infobytesec.com/) See the file 'doc/LICENSE' for the license information """ -import re +import xml.etree.ElementTree as ET from urllib.parse import urlparse +import base64 from dateutil.parser import parse from faraday_plugins.plugins.plugin import PluginXMLFormat -try: - import xml.etree.cElementTree as ET -except ImportError: - import xml.etree.ElementTree as ET - __author__ = 'Blas Moyano' __copyright__ = 'Copyright 2020, Faraday Project' __credits__ = ['Blas Moyano'] @@ -29,7 +24,10 @@ class QualysWebappParser: def __init__(self, xml_output): self.tree = self.parse_xml(xml_output) if self.tree: - self.info_results = self.get_results_vul(self.tree.find('RESULTS')) + if self.tree.find('RESULTS/WEB_APPLICATION'): + self.info_results = self.get_results_vul(self.tree.find('RESULTS/WEB_APPLICATION')) + else: + self.info_results = self.get_results_vul(self.tree.find('RESULTS')) self.info_glossary = self.get_glossary_qid(self.tree.find('GLOSSARY')) self.info_appendix = self.get_appendix(self.tree.find('APPENDIX')) else: @@ -39,24 +37,27 @@ def parse_xml(self, xml_output): try: tree = ET.fromstring(xml_output) except SyntaxError as err: - print('SyntaxError In xml: %s. %s' % (err, xml_output)) + print(f'SyntaxError In xml: {err} {xml_output}') return None return tree - def get_appendix(self, tree): - for self.appendix_tags in tree: - yield Appendix(self.appendix_tags) + @staticmethod + def get_appendix(tree): + for appendix_tags in tree: + yield Appendix(appendix_tags) - def get_glossary_qid(self, tree): - for self.glossary_tags in tree.find('QID_LIST'): - yield Glossary(self.glossary_tags) + @staticmethod + def get_glossary_qid(tree): + for glossary_tags in tree.find('QID_LIST'): + yield Glossary(glossary_tags) - def get_results_vul(self, tree): - for self.results_tags in tree.find('VULNERABILITY_LIST'): - yield Results(self.results_tags) + @staticmethod + def get_results_vul(tree): + for results_tags in tree.find('VULNERABILITY_LIST'): + yield Results(results_tags) -class Appendix(): +class Appendix: def __init__(self, appendix_tags): if appendix_tags.tag == 'SCAN_LIST': self.lista_scan = self.get_scan(appendix_tags.find('SCAN')) @@ -64,24 +65,25 @@ def __init__(self, appendix_tags): elif appendix_tags.tag == 'WEBAPP': self.lista_webapp = self.get_webapp(appendix_tags) - def get_scan(self, appendix_tags): - self.result_scan = {} + @staticmethod + def get_scan(appendix_tags): + result_scan = {} for scan in appendix_tags: - self.result_scan[scan.tag] = scan.text - return self.result_scan + result_scan[scan.tag] = scan.text + return result_scan - def get_webapp(self, appendix_tags): - self.result_webapp = {} + @staticmethod + def get_webapp(appendix_tags): + result_webapp = {} for webapp in appendix_tags: - self.result_webapp[webapp.tag] = webapp.text - return self.result_webapp + result_webapp[webapp.tag] = webapp.text + return result_webapp -class Glossary(): +class Glossary: def __init__(self, glossary_tags): self.lista_qid = self.get_qid_list(glossary_tags) - def get_qid_list(self, qid_list_tags): self.dict_result_qid = {} for qid in qid_list_tags: @@ -89,21 +91,46 @@ def get_qid_list(self, qid_list_tags): return self.dict_result_qid -class Results(): +class Results: def __init__(self, glossary_tags): self.lista_vul = self.get_qid_list(glossary_tags) + @staticmethod + def build_request(request): + request_data = [] + for url in request.find("URL"): + request_data.append(f'URL: {url.text}') + for header in request.findall('HEADERS/HEADER'): + request_data.append(f'{header.find("key").text}: {header.find("value").text}') + return '\n'.join(request_data) + + @staticmethod + def build_response(response): + response_data = [] + for contents in response.findall('CONTENTS'): + response_data.append(base64.b64decode(contents.text).decode('utf-8')) + for evidence in response.findall('EVIDENCE'): + response_data.append(base64.b64decode(evidence.text).decode('utf-8')) + return '\n'.join(response_data) + def get_qid_list(self, vul_list_tags): self.dict_result_vul = {} for vul in vul_list_tags: - self.dict_result_vul[vul.tag] = vul.text + if vul.tag == "PAYLOADS" and vul.find("PAYLOAD"): + #TODO chequear que no se pueda hacer un html injection decodeando RESPONSE + self.dict_result_vul["REQUEST"] = self.build_request(vul.find("PAYLOAD/REQUEST")) + self.dict_result_vul["METHOD"] = vul.find("PAYLOAD/REQUEST/METHOD").text + self.dict_result_vul["RESPONSE"] = self.build_response(vul.find("PAYLOAD/RESPONSE")) + else: + self.dict_result_vul[vul.tag] = vul.text return self.dict_result_vul class QualysWebappPlugin(PluginXMLFormat): - def __init__(self): - super().__init__() - self.identifier_tag = ["WAS_SCAN_REPORT"] + + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) + self.identifier_tag = ["WAS_WEBAPP_REPORT", "WAS_SCAN_REPORT"] self.id = 'QualysWebapp' self.name = 'QualysWebapp XML Output Plugin' self.plugin_version = '1.0.0' @@ -126,6 +153,7 @@ def parseOutputString(self, output): for host_create in parser.info_appendix: self.scan_list_result.append(host_create) + operating_system = "" for k in self.scan_list_result: if 'result_scan' in k.__dict__: self.credential = k.lista_scan.get('AUTHENTICATION_RECORD') @@ -142,16 +170,15 @@ def parseOutputString(self, output): for v in parser.info_results: url = urlparse(v.dict_result_vul.get('URL')) - - host_id = self.createAndAddHost(name=url.netloc, os=operating_system, hostnames=hostnames) - vuln_scan_id = v.dict_result_vul.get('QID') + vuln_data = next((item for item in glossary if item["QID"] == vuln_scan_id), None) # Data in the xml is in different parts, we look into the glossary - vuln_data = next((item for item in glossary if item["QID"] == vuln_scan_id), None) + + vuln_name = vuln_data.get('TITLE') vuln_desc = vuln_data.get('DESCRIPTION') - + vuln_CWE = [vuln_data.get('CWE', '')] raw_severity = int(vuln_data.get('SEVERITY', 0)) vuln_severity = raw_severity - 1 @@ -162,18 +189,24 @@ def parseOutputString(self, output): vuln_resolution = vuln_data.get('SOLUTION') - vuln_ref = [] - if vuln_data.get('CVSS_BASE'): - vuln_ref = ["CVSS: {}".format(vuln_data.get('CVSS_BASE'))] + vuln_data_add = f"ID: {v.dict_result_vul.get('ID')}, DETECTION_ID: {v.dict_result_vul.get('DETECTION_ID')}" \ + f", CATEGORY: {vuln_data.get('CATEGORY')}, GROUP: {vuln_data.get('GROUP')}" \ + f", URL: {v.dict_result_vul.get('URL')}, IMPACT: {vuln_data.get('IMPACT')}" - vuln_data_add = "ID: {}, DETECTION_ID: {}, CATEGORY: {}, GROUP: {}, URL: {}, IMPACT: {}".format( - v.dict_result_vul.get('ID'), v.dict_result_vul.get('DETECTION_ID'), vuln_data.get('CATEGORY'), - vuln_data.get('GROUP'), v.dict_result_vul.get('URL'), vuln_data.get('IMPACT')) - - self.createAndAddVulnToHost(host_id=host_id, name=vuln_name, desc=vuln_desc, ref=vuln_ref, + host_id = self.createAndAddHost(name=url.netloc, os=operating_system, hostnames=hostnames) + if v.dict_result_vul.get('REQUEST'): + vuln_request = v.dict_result_vul.get('REQUEST') + vuln_response = v.dict_result_vul.get('RESPONSE') + vuln_method = v.dict_result_vul.get('METHOD') + service_id = self.createAndAddServiceToHost(host_id=host_id, name=url.path, protocol='tcp', ports=0) + self.createAndAddVulnWebToService(host_id=host_id, service_id=service_id, name=vuln_name, desc=vuln_desc, + severity=vuln_severity, resolution=vuln_resolution, run_date=run_date, + external_id="QUALYS-" + vuln_scan_id, data=vuln_data_add, cwe=vuln_CWE, + method=vuln_method, response=vuln_response, request=vuln_request, path=url.path) + else: + self.createAndAddVulnToHost(host_id=host_id, name=vuln_name, desc=vuln_desc, severity=vuln_severity, resolution=vuln_resolution, run_date=run_date, - external_id=vuln_scan_id, data=vuln_data_add) - + external_id="QUALYS-"+vuln_scan_id, data=vuln_data_add, cwe=vuln_CWE) -def createPlugin(): - return QualysWebappPlugin() +def createPlugin(*args, **kwargs): + return QualysWebappPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/rdpscan/plugin.py b/faraday_plugins/plugins/repo/rdpscan/plugin.py index 473ead5a..00927e47 100644 --- a/faraday_plugins/plugins/repo/rdpscan/plugin.py +++ b/faraday_plugins/plugins/repo/rdpscan/plugin.py @@ -2,13 +2,11 @@ from collections import defaultdict from faraday_plugins.plugins.plugin import PluginBase -from faraday_plugins.plugins.plugins_utils import resolve_hostname - class RDPScanPlugin(PluginBase): - def __init__(self): - super().__init__() + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) self.identifier_tag = "rdpscan" self.id = "rdpscan" self.name = "rdpscan" @@ -43,5 +41,5 @@ def parseOutputString(self, output): ) -def createPlugin(): - return RDPScanPlugin() +def createPlugin(*args, **kwargs): + return RDPScanPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/reconng/__init__.py b/faraday_plugins/plugins/repo/reconng/__init__.py index 308ccd2c..8b137891 100644 --- a/faraday_plugins/plugins/repo/reconng/__init__.py +++ b/faraday_plugins/plugins/repo/reconng/__init__.py @@ -1 +1 @@ -# I'm Py3 \ No newline at end of file + diff --git a/faraday_plugins/plugins/repo/reconng/plugin.py b/faraday_plugins/plugins/repo/reconng/plugin.py index 04042fea..d227b829 100644 --- a/faraday_plugins/plugins/repo/reconng/plugin.py +++ b/faraday_plugins/plugins/repo/reconng/plugin.py @@ -13,7 +13,6 @@ import xml.etree.ElementTree as ET from faraday_plugins.plugins.plugin import PluginXMLFormat -from faraday_plugins.plugins.plugins_utils import resolve_hostname __author__ = 'Leonardo Lazzaro' __copyright__ = 'Copyright (c) 2017, Infobyte LLC' @@ -128,8 +127,8 @@ class ReconngPlugin(PluginXMLFormat): Example plugin to parse qualysguard output. """ - def __init__(self): - super().__init__() + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) self.identifier_tag = "reconng" self.id = 'Reconng' self.name = 'Reconng XML Output Plugin' @@ -150,7 +149,7 @@ def parseOutputString(self, output): self.host_mapper[host['host']] = h_id for vuln in parser.vulns: if vuln['host'] not in list(self.host_mapper.keys()): - ip = resolve_hostname(vuln['host']) + ip = self.resolve_hostname(vuln['host']) h_id = self.createAndAddHost( ip, hostnames=[vuln['host']] @@ -171,7 +170,5 @@ def parseOutputString(self, output): -def createPlugin(): - return ReconngPlugin() - -# I'm Py3 +def createPlugin(*args, **kwargs): + return ReconngPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/retina/__init__.py b/faraday_plugins/plugins/repo/retina/__init__.py index ea531e17..625a6e25 100644 --- a/faraday_plugins/plugins/repo/retina/__init__.py +++ b/faraday_plugins/plugins/repo/retina/__init__.py @@ -4,4 +4,4 @@ See the file 'doc/LICENSE' for the license information """ -# I'm Py3 \ No newline at end of file + diff --git a/faraday_plugins/plugins/repo/retina/plugin.py b/faraday_plugins/plugins/repo/retina/plugin.py index ff196cc5..15b5bfdf 100644 --- a/faraday_plugins/plugins/repo/retina/plugin.py +++ b/faraday_plugins/plugins/repo/retina/plugin.py @@ -5,19 +5,9 @@ """ import re -from faraday_plugins.plugins.plugin import PluginXMLFormat - - -try: - import xml.etree.cElementTree as ET - import xml.etree.ElementTree as ET_ORIG - ETREE_VERSION = ET_ORIG.VERSION -except ImportError: - import xml.etree.ElementTree as ET - ETREE_VERSION = ET.VERSION - -ETREE_VERSION = [int(i) for i in ETREE_VERSION.split(".")] +import xml.etree.ElementTree as ET +from faraday_plugins.plugins.plugin import PluginXMLFormat __author__ = "Francisco Amato" __copyright__ = "Copyright (c) 2013, Infobyte LLC" @@ -43,7 +33,7 @@ class RetinaXmlParser: def __init__(self, xml_output): tree = self.parse_xml(xml_output) if tree: - self.items = [data for data in self.get_items(tree)] + self.items = self.get_items(tree) else: self.items = [] @@ -59,7 +49,7 @@ def parse_xml(self, xml_output): try: tree = ET.fromstring(xml_output) except SyntaxError as err: - print("SyntaxError: %s. %s" % (err, xml_output)) + print(f"SyntaxError: {err}. {xml_output}") return None return tree @@ -117,7 +107,7 @@ def get_text_from_subnode(self, subnode_xpath_expr): return None -class Results(): +class Results: def __init__(self, issue_node): self.node = issue_node @@ -133,7 +123,7 @@ def __init__(self, issue_node): self.pciLevel = self.get_text_from_subnode('pciLevel') self.pciReason = self.get_text_from_subnode('pciReason') self.pciPassFail = self.get_text_from_subnode('pciPassFail') - self.cvssScore = self.get_text_from_subnode('cvssScore') + self.cvss2Score = self.get_text_from_subnode('cvssScore') self.exploit = self.get_text_from_subnode('exploit') self.context = self.get_text_from_subnode('context') val = self.context.split(":") @@ -147,12 +137,12 @@ def __init__(self, issue_node): self.desc = self.get_text_from_subnode('description') self.solution = self.solution if self.solution else "" self.desc += "\nExploit: " + self.exploit if self.exploit else "" - self.desc += "\ncvssScore: " + self.cvssScore if self.cvssScore else "" self.desc += "\nContext: " + self.context if self.context else "" self.ref = [] - if self.cve: - self.ref = self.cve.split(",") + self.cvss2 = {} + if self.cvss2Score != "N/A": + self.cvss2["vector_string"] = self.cvss2Score.split(' ')[1].replace('[', '').replace(']', '') def get_text_from_subnode(self, subnode_xpath_expr): """ @@ -172,8 +162,8 @@ class RetinaPlugin(PluginXMLFormat): Example plugin to parse retina output. """ - def __init__(self): - super().__init__() + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) self.identifier_tag = "scanJob" self.id = "Retina" self.name = "Retina XML Output Plugin" @@ -182,13 +172,12 @@ def __init__(self): self.framework_version = "1.0.0" self.options = None - def parseOutputString(self, output): parser = RetinaXmlParser(output) for item in parser.items: hostname = item.hostname if item.hostname else None - h_id = self.createAndAddHost(item.ip, item.os,hostnames=[hostname]) + h_id = self.createAndAddHost(item.ip, item.os, hostnames=[hostname]) if not item.netbiosname == 'N/A': self.createAndAddNoteToHost( @@ -201,6 +190,7 @@ def parseOutputString(self, output): for k, vulns in item.ports.items(): if k: for v in vulns: + cve = v.cve.split(",") if v.cve else [] web = False s_id = self.createAndAddServiceToHost(h_id, 'unknown', v.protocol.lower(), ports=[str(v.port)], status="open") @@ -211,23 +201,18 @@ def parseOutputString(self, output): if web: v_id = self.createAndAddVulnWebToService(h_id, s_id, v.name, ref=v.ref, website=hostname, severity=v.severity, - resolution=v.solution, desc=v.desc) + resolution=v.solution, desc=v.desc, cve=cve, cvss2=v.cvss2) else: v_id = self.createAndAddVulnToService(h_id, s_id, v.name, ref=v.ref, severity=v.severity, resolution=v.solution, - desc=v.desc) + desc=v.desc, cve=cve, cvss2=v.cvss2) else: for v in vulns: + cve = v.cve.split(",") if v.cve else [] v_id = self.createAndAddVulnToHost(h_id, v.name, ref=v.ref, severity=v.severity, - resolution=v.solution, desc=v.desc) + resolution=v.solution, desc=v.desc, cve=cve, cvss2=v.cvss2) del parser - def setHost(self): - pass - - -def createPlugin(): - return RetinaPlugin() - -# I'm Py3 +def createPlugin(*args, **kwargs): + return RetinaPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/reverseraider/__init__.py b/faraday_plugins/plugins/repo/reverseraider/__init__.py index ea531e17..625a6e25 100644 --- a/faraday_plugins/plugins/repo/reverseraider/__init__.py +++ b/faraday_plugins/plugins/repo/reverseraider/__init__.py @@ -4,4 +4,4 @@ See the file 'doc/LICENSE' for the license information """ -# I'm Py3 \ No newline at end of file + diff --git a/faraday_plugins/plugins/repo/reverseraider/plugin.py b/faraday_plugins/plugins/repo/reverseraider/plugin.py index decad09c..13f1f739 100644 --- a/faraday_plugins/plugins/repo/reverseraider/plugin.py +++ b/faraday_plugins/plugins/repo/reverseraider/plugin.py @@ -36,11 +36,11 @@ def __init__(self, output): for line in lists: if line != "": - print("(%s)" % line) + print(f"({line})") info = line.split("\t") if info.__len__() > 0: item = {'host': info[0], 'ip': info[1]} - print("host = %s, ip = %s" % (info[0], info[1])) + print(f"host = {info[0]}, ip = {info[1]}") self.items.append(item) @@ -49,8 +49,8 @@ class ReverseraiderPlugin(PluginBase): Example plugin to parse reverseraider output. """ - def __init__(self): - super().__init__() + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) self.id = "Reverseraider" self.name = "Reverseraider XML Output Plugin" self.plugin_version = "0.0.1" @@ -78,7 +78,5 @@ def parseOutputString(self, output): -def createPlugin(): - return ReverseraiderPlugin() - -# I'm Py3 +def createPlugin(*args, **kwargs): + return ReverseraiderPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/saint/__init__.py b/faraday_plugins/plugins/repo/saint/__init__.py new file mode 100755 index 00000000..f70f2304 --- /dev/null +++ b/faraday_plugins/plugins/repo/saint/__init__.py @@ -0,0 +1,6 @@ +""" +Faraday Penetration Test IDE +Copyright (C) 2017 Infobyte LLC (http://www.infobytesec.com/) +See the file 'doc/LICENSE' for the license information + +""" diff --git a/faraday_plugins/plugins/repo/saint/plugin.py b/faraday_plugins/plugins/repo/saint/plugin.py new file mode 100644 index 00000000..12251324 --- /dev/null +++ b/faraday_plugins/plugins/repo/saint/plugin.py @@ -0,0 +1,58 @@ +""" +Faraday Penetration Test IDE +Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/) +See the file 'doc/LICENSE' for the license information + +""" + +from faraday_plugins.plugins.plugin import PluginCSVFormat +from faraday_plugins.plugins.plugins_utils import get_severity_from_cvss +import csv +import io + + +__author__ = "Dante Acosta" +__copyright__ = "Copyright (c) 2013, Infobyte LLC" +__credits__ = ["Dante Acosta"] +__license__ = "" +__version__ = "1.0.0" +__maintainer__ = "Dante Acosta" +__email__ = "dacosta@infobytesec.com" +__status__ = "Development" + + +class SaintPlugin(PluginCSVFormat): + + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) + self.id = "saint" + self.name = "Saint" + self.plugin_version = "1.0.0" + self.version = "1.0.0" + self.framework_version = "1.0.0" + self.csv_headers = {'IP Address', 'Hostname', 'System Type', 'Severity Level', 'Severity'} + + def parseOutputString(self, output): + try: + reader = csv.DictReader(io.StringIO(output)) + except Exception as e: + print(f"Error parsing output {e}") + return None + + for row in reader: + host_id = self.createAndAddHost( + name=row.get("IP Address", ""), + os=row.get("System Class") or "unknown", + hostnames=[row.get("Hostname")] or [] + ) + self.createAndAddVulnToHost( + host_id, + name=row.get("Tutorial"), + desc=row.get("Description"), + severity=get_severity_from_cvss(row.get("CVSS Score", "")), + confirmed=True if row.get("Confirmed", "").lower() == "yes" else False + ) + + +def createPlugin(*args, **kwargs): + return SaintPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/sarif/__init__.py b/faraday_plugins/plugins/repo/sarif/__init__.py new file mode 100644 index 00000000..98901a0b --- /dev/null +++ b/faraday_plugins/plugins/repo/sarif/__init__.py @@ -0,0 +1,6 @@ +""" +Faraday Penetration Test IDE +Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/) +See the file 'doc/LICENSE' for the license information + +""" diff --git a/faraday_plugins/plugins/repo/sarif/plugin.py b/faraday_plugins/plugins/repo/sarif/plugin.py new file mode 100644 index 00000000..234fb3b8 --- /dev/null +++ b/faraday_plugins/plugins/repo/sarif/plugin.py @@ -0,0 +1,101 @@ +""" +Faraday Penetration Test IDE +Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/) +See the file 'doc/LICENSE' for the license information + +""" +from faraday_plugins.plugins.plugin import PluginJsonFormat +from faraday_plugins.plugins.plugins_utils import markdown2text + +from json import loads + +__author__ = "Gonzalo Martinez" +__copyright__ = "Copyright (c) 2013, Infobyte LLC" +__credits__ = ["Gonzalo Martinez"] +__version__ = "1.0.0" +__maintainer__ = "Gonzalo Martinez" +__email__ = "gmartinez@infobytesec.com" +__status__ = "Development" + + + +class SarifPlugin(PluginJsonFormat): + + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) + self.id = "Sarif" + self.name = "Sarif Plugin" + self.plugin_version = "1" + self.version = "1" + self.json_keys = {'version'} + self.framework_version = "1.0.0" + self.extension = ".sarif" + + def map_severity(self, level): + mapping = { + "none": "unclassified", + "note": "low", + "warning": "medium", + "error": "high" + } + return mapping.get(level, "") + + + def parseOutputString(self, output): + """ + This method will discard the output the shell sends, it will read it + from the xml where it expects it to be present. + + NOTE: if 'debug' is true then it is being run from a test case and the + output being sent is valid. + """ + runs = loads(output).get("runs", "") + for run in runs: + rules = {} + tool = run.get("tool",{}).get("driver",{}).get("name", "") + for rule in run.get("tool",{}).get("driver",{}).get("rules", []): + rules[rule["id"]] = rule + for result in run.get("results", []): + locations = result.get("locations",[]) + for location in locations: + loc = location.get("physicalLocation", {}).get("artifactLocation").get("uri") + if not loc: + loc = "/" + h_id = self.createAndAddHost(loc) + rule = rules[result.get("ruleId")] + name = rule.get("name") + short_description = rule.get("shortDescription").get("text") if \ + rule.get("shortDescription", {}).get("text") else \ + markdown2text(rule.get("shortDescription", {}).get("markdown", "")) + desc = rule.get("fullDescription").get("text") if rule.get("fullDescription", {}).get("text") \ + else markdown2text(rule.get("fullDescription").get("markdown", "")) + help = rule.get("help",{}).get("text") if rule.get("help",{}).get("text") \ + else markdown2text(rule.get("help",{}).get("markdown", "")) + tags_to_check = rule.get("properties",{}).get("tags", []) + cwe = [] + tags = [] + severity = self.map_severity(result.get("level", "")) + for tag in tags_to_check: + if "cwe" in tag.lower(): + cwe.append(tag) + else: + tags.append(tag) + if "Snyk Open Source" == tool.strip(): + external_id = result.get('ruleId') + else: + external_id = f"{tool} {result.get('ruleId')}" + + self.createAndAddVulnToHost( + host_id=h_id, + name=name if name else short_description, + desc=desc, + data=help, + tags=tags, + cwe=cwe, + severity=severity, + external_id=external_id + ) + + +def createPlugin(*args, **kwargs): + return SarifPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/semgrep/__init__.py b/faraday_plugins/plugins/repo/semgrep/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/faraday_plugins/plugins/repo/semgrep/plugin.py b/faraday_plugins/plugins/repo/semgrep/plugin.py new file mode 100644 index 00000000..1adae7ae --- /dev/null +++ b/faraday_plugins/plugins/repo/semgrep/plugin.py @@ -0,0 +1,94 @@ +""" +Faraday Penetration Test IDE +Copyright (C) 2020 Infobyte LLC (http://www.infobytesec.com/) +See the file 'doc/LICENSE' for the license information + +""" +import json + +from faraday_plugins.plugins.plugin import PluginJsonFormat + +__author__ = "Gonzalo Martinez" +__copyright__ = "Copyright (c) 2020, Infobyte LLC" +__credits__ = ["Gonzalo Martinez"] +__license__ = "" +__version__ = "1.0.0" +__maintainer__ = "Gonzalo Martinez" +__email__ = "gmartinez@infobytesec.com" +__status__ = "Development" + + +class SemgrepPlugin(PluginJsonFormat): + + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) + self.id = "Semgrep_JSON" + self.name = "Semgrep Json" + self.plugin_version = "1.0.0" + self.json_keys = {'errors', 'paths', 'results', 'version'} + + def parseOutputString(self, output): + json_semgrep = json.loads(output) + results = json_semgrep.get("results") + severity_mapper = { + "ERROR": "critical", + "WARNING": "high", + "INFO": "info" + } + if not results: + return + for result in results: + path = result.get('path') + host_id = self.createAndAddHost( + name=path + ) + extra = result.get('extra') + if not extra: + continue + line_start = result.get("start",{}).get("line") + service_id = self.createAndAddServiceToHost( + host_id=host_id, + name=f"Line {line_start}", + ports=int(line_start) + ) + if line_start: + path += str(line_start) + + severity = severity_mapper[extra.get("severity", "INFO")] + lines = extra.get("lines","") + refs = [] + desc = extra.get("message") + metadata = extra.get("metadata") + if not metadata: + continue + cwe = [] + for i in metadata.get("cwe",[]): + cwe.append(i.split(":")[0]) + references = metadata.get("references") + if isinstance(references,list): + refs += references + elif isinstance(references, str): + refs.append(references) + owasp = metadata.get("owasp") + if isinstance(owasp,list): + refs += owasp + elif isinstance(owasp,str): + refs.append(owasp) + bandit_code = metadata.get("bandit-code") + if bandit_code: + references.append(f"Bandit code {bandit_code}") + data = f"Path: {path}\nLines: {lines}" + self.createAndAddVulnToService( + host_id=host_id, + service_id=service_id, + name=desc[:50], + desc=desc, + severity=severity, + cwe=cwe, + ref=refs, + data=data + ) + + +def createPlugin(*args, **kwargs): + return SemgrepPlugin(*args, **kwargs) diff --git a/faraday_plugins/plugins/repo/shodan/__init__.py b/faraday_plugins/plugins/repo/shodan/__init__.py new file mode 100644 index 00000000..d8862701 --- /dev/null +++ b/faraday_plugins/plugins/repo/shodan/__init__.py @@ -0,0 +1,6 @@ +""" +Faraday Penetration Test IDE +Copyright (C) 2021 Infobyte LLC (http://www.infobytesec.com/) +See the file 'doc/LICENSE' for the license information + +""" diff --git a/faraday_plugins/plugins/repo/shodan/plugin.py b/faraday_plugins/plugins/repo/shodan/plugin.py new file mode 100644 index 00000000..20ce218e --- /dev/null +++ b/faraday_plugins/plugins/repo/shodan/plugin.py @@ -0,0 +1,108 @@ +""" +Faraday Penetration Test IDE +Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/) +See the file 'doc/LICENSE' for the license information + +""" +import re +import json +import argparse +import shlex +import gzip +import os +import shutil + +from faraday_plugins.plugins.plugin import PluginMultiLineJsonFormat +from faraday_plugins.plugins.plugins_utils import get_severity_from_cvss + +__author__ = "Valentin Vila" +__copyright__ = "Copyright (c) 2021, Faraday" +__credits__ = ["Valentin Vila"] +__license__ = "" +__version__ = "1.0.0" +__maintainer__ = "Valentin Vila" +__email__ = "vvila@faradaysec.com" +__status__ = "Development" + + +class ShodanPlugin(PluginMultiLineJsonFormat): + """ + This plugin handles the Shodan tool. + Detects the output of the tool + and adds the information to Faraday. + """ + + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) + self.id = "shodan" + self.name = "Shodan" + self.plugin_version = "0.0.1" + self.version = "1.0.0" + self._command_regex = re.compile(r'^shodan\s+(?P