├── .dockerignore ├── .env ├── .github ├── FUNDING.yml └── workflows │ ├── codeql-analysis.yml │ ├── github-sync.sh │ ├── main.yml │ └── secret.env.pgp ├── .gitignore ├── .gitlab-ci-old.yml ├── .gitlab-ci.yml ├── .travis.yml ├── AUTHORS ├── Dockerfile ├── Dockerfile_nginx ├── LICENSE ├── README.md ├── SECURITY.md ├── VERSION ├── app ├── __init__.py ├── assets_detection_rules.py.sample ├── celery.py ├── context_processors.py ├── settings.py.sample ├── urls.py ├── views.py └── wsgi.py ├── assets ├── __init__.py ├── admin.py ├── apis.py ├── apps.py ├── forms.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20200616_1735.py │ ├── 0003_asset_owner.py │ ├── 0004_auto_20200617_0958.py │ ├── 0005_auto_20200617_1011.py │ ├── 0006_assetgroup_teams.py │ ├── 0007_auto_20200709_0007.py │ ├── 0008_asset_exposure.py │ ├── 0009_auto_20200921_1151.py │ ├── 0010_auto_20210402_1439.py │ ├── 0011_auto_20211123_1140.py │ ├── 0012_dynamicassetgroup.py │ └── __init__.py ├── models.py ├── serializers.py ├── templates │ ├── add-asset-group.html │ ├── add-asset-owner.html │ ├── add-asset.html │ ├── add-assets-bulk.html │ ├── add-dyn-asset-group.html │ ├── delete-asset-group.html │ ├── delete-asset-owner.html │ ├── delete-asset.html │ ├── details-asset-group.html │ ├── details-asset-owner.html │ ├── details-asset.html │ ├── details-dyn-asset-group.html │ ├── edit-asset-group.html │ ├── edit-asset.html │ ├── edit-dyn-asset-group.html │ ├── list-asset-owners.html │ ├── list-assets.html │ ├── report-asset-findings.html │ └── report-assetgroup-findings.html ├── tests.py ├── urls.py ├── utils.py └── views.py ├── common ├── __init__.py └── utils │ ├── __init__.py │ ├── cpe.py │ ├── date.py │ ├── encoding.py │ ├── net.py │ ├── pagination.py │ ├── password.py │ └── settings.py ├── docker-compose.with-engines.yml ├── docker-compose.yml ├── docker-entrypoint.sh ├── docker-entrypoint.with-engines.sh ├── engines ├── __init__.py ├── admin.py ├── apis.py ├── apps.py ├── forms.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── parsers │ └── nessus.py ├── serializers.py ├── tasks.py ├── templates │ ├── add-engine-policy.html │ ├── add-engine.html │ ├── add-scan-engine.html │ ├── delete-engine-policy.html │ ├── delete-engine.html │ ├── delete-scan-engine.html │ ├── edit-engine-policy.html │ ├── edit-engine.html │ ├── edit-scan-engine.html │ ├── import-engine-policies.html │ ├── info-scan-engine.html │ ├── list-engine-policies.html │ ├── list-engines.html │ └── list-scan-engines.html ├── tests.py ├── urls.py ├── utils.py └── views.py ├── events ├── __init__.py ├── admin.py ├── apis.py ├── apps.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_alert.py │ ├── 0003_alert_metadata.py │ ├── 0004_auto_20200602_1813.py │ ├── 0005_auto_20200616_1631.py │ ├── 0006_auto_20200624_0923.py │ ├── 0007_auditlog.py │ ├── 0008_auditlog_owner_username.py │ ├── 0009_auditlog_type.py │ ├── 0010_auditlog_metadata.py │ ├── 0011_auto_20200723_1528.py │ ├── 0012_alert_type.py │ ├── 0013_alertoverride.py │ ├── 0014_alertoverride_teams.py │ └── __init__.py ├── models.py ├── serializers.py ├── templates │ ├── delete-event.html │ └── list-alerts.html ├── tests.py ├── urls.py ├── utils.py └── views.py ├── findings ├── __init__.py ├── admin.py ├── apis.py ├── apps.py ├── forms.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20200306_0822.py │ ├── 0003_auto_20200602_2323.py │ ├── 0004_auto_20200709_0024.py │ ├── 0005_auto_20200709_0910.py │ ├── 0006_auto_20200720_1829.py │ ├── 0007_auto_20210113_1644.py │ ├── 0008_findingoverride.py │ ├── 0009_auto_20210504_1320.py │ ├── 0010_auto_20211104_1152.py │ └── __init__.py ├── models.py ├── serializers.py ├── templates │ ├── add-finding.html │ ├── compare-findings.html │ ├── delete-findings.html │ ├── details-finding.html │ ├── edit-finding.html │ ├── import-findings.html │ ├── list-findings.html │ └── report-finding.html ├── tests.py ├── urls.py ├── utils.py └── views.py ├── install-patrowl.sh ├── manage.py ├── media └── .gitignore ├── nginx.conf ├── nginx_docker.conf ├── reportings ├── __init__.py ├── admin.py ├── api.py ├── apps.py ├── migrations │ └── __init__.py ├── models.py ├── templates │ ├── home-dashboard.html │ └── patch-management-dashboard.html ├── urls.py └── views.py ├── requirements.macos.txt ├── requirements.txt ├── rules ├── __init__.py ├── admin.py ├── apis.py ├── apps.py ├── forms.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20200720_1829.py │ ├── 0003_auto_20210113_1644.py │ ├── 0004_auto_20211104_1152.py │ └── __init__.py ├── models.py ├── templates │ └── list-rules.html ├── tests.py ├── urls.py └── views.py ├── scans ├── __init__.py ├── admin.py ├── apis.py ├── apps.py ├── forms.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_scandefinition_teams.py │ ├── 0003_scanjob.py │ ├── 0004_auto_20201202_1448.py │ ├── 0005_auto_20201202_1909.py │ ├── 0006_scandefinition_taggroups_list.py │ ├── 0007_auto_20220114_1547.py │ └── __init__.py ├── models.py ├── templates │ ├── add-scan-campaign.html │ ├── add-scan-definition.html │ ├── compare-scans.html │ ├── delete-scan-campaign.html │ ├── delete-scan-definition.html │ ├── details-scan-def.html │ ├── details-scan.html │ ├── edit-scan-campaign.html │ ├── edit-scan-definition.html │ ├── email_send_report.html │ ├── email_send_report.txt │ ├── list-scan-campaigns.html │ ├── list-scan-definitions.html │ ├── list-scans-performed.html │ └── report-scan.html ├── tests.py ├── urls.py ├── utils.py └── views.py ├── search ├── __init__.py ├── admin.py ├── apis.py ├── apps.py ├── migrations │ └── __init__.py ├── models.py ├── templates │ └── search-results.html ├── tests.py ├── urls.py └── views.py ├── settings ├── __init__.py ├── admin.py ├── apis.py ├── apps.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── templates │ ├── menu-settings.html │ ├── menu-users.html │ └── support.html ├── tests.py ├── urls.py └── views.py ├── start-server.sh ├── static ├── css │ ├── bootstrap-4.0.0.min.css │ ├── bootstrap-tagsinput.css │ ├── bootstrap.min.css │ ├── cal-heatmap.css │ ├── dataTables.bootstrap4.min.css │ ├── family-Lato.css │ ├── images │ │ └── ui-bg_flat_75_ffffff_40x100.png │ ├── jquery-ui.css │ ├── jquery.dataTables.min.css │ ├── patrowlmanager.css │ ├── select.dataTables.min.css │ └── selectize.bootstrap4.css ├── favicon.ico ├── fonts │ ├── S6u8w4BMUTPHjxsAXC-v.ttf │ ├── S6u9w4BMUTPHh6UVSwiPHA.ttf │ ├── S6uyw4BMUTPHjx4wWw.ttf │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.svg │ ├── glyphicons-halflings-regular.ttf │ ├── glyphicons-halflings-regular.woff │ └── glyphicons-halflings-regular.woff2 ├── images │ ├── sort_asc.png │ ├── sort_asc_disabled.png │ ├── sort_both.png │ └── sort_desc.png ├── js │ ├── Chart.bundle.min.js │ ├── bootstrap-tagsinput.min.js │ ├── bootstrap-tagsinput.min.js.map │ ├── bootstrap.min.js │ ├── cal-heatmap.min.js │ ├── cal-heatmap.source-map.js │ ├── d3.min.js │ ├── dataTables.buttons.min.js │ ├── dataTables.select.min.js │ ├── fontawesome-all.min.js │ ├── jquery-3.2.0.min.js │ ├── jquery-ui.min.js │ ├── jquery.dataTables.min.js │ ├── jquery.min.js │ ├── selectize.js │ ├── tether.min.js │ └── typeahead │ │ └── typeahead.bundle.min.js └── tmpl │ ├── assets_bulk_import_template.csv │ └── users_bulk_import_template.csv ├── templates ├── base.html ├── errors │ ├── 400.html │ ├── 403.html │ ├── 404.html │ └── 500.html ├── home.html ├── login.html └── signup.html ├── templatetags ├── __init__.py └── common_tags.py ├── tests └── test_sample.py ├── users ├── __init__.py ├── admin.py ├── apis.py ├── apps.py ├── forms.py ├── middleware.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20200616_1704.py │ ├── 0003_auto_20200624_0037.py │ ├── 0004_remove_profile_role.py │ ├── 0005_profile_is_delegated.py │ └── __init__.py ├── models.py ├── permissions.py ├── serializers.py ├── templates │ ├── add-user.html │ ├── details-user.html │ ├── edit-user-password.html │ └── list-users.html ├── tests.py ├── urls.py └── views.py └── var ├── bin ├── create_default_admin.py ├── create_default_team.py └── update_db_migrations.sh ├── data ├── assets.AssetCategory.json ├── engines.Engine.json ├── engines.EnginePolicy.json └── engines.EnginePolicyScope.json ├── db ├── create_db.sql ├── create_user_and_db.sql └── init_db.sql ├── etc ├── supervisord-celery-pro.conf ├── supervisord-celery.conf ├── supervisord-pro.conf └── supervisord.conf ├── log ├── .dockerignore └── .gitignore ├── migrations └── assets │ └── 0002_asset_exposure.py └── tmp └── .gitignore /.dockerignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | app/settings.py 3 | app/assets_detection_rules.py 4 | var/log/*.log 5 | var/log/*.log.[0-9]* 6 | var/tmp/*.pid 7 | # **/migrations/[0-9]*.py 8 | media/* 9 | *.pyc 10 | __pycache__ 11 | env 12 | env3 13 | 14 | pro/* 15 | env3pro 16 | .env 17 | .travis.yml 18 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | PATROWL_MANAGER_VERSION=latest 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [MaKyOtOx] 2 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | name: "CodeQL" 7 | 8 | on: 9 | push: 10 | branches: [master] 11 | pull_request: 12 | # The branches below must be a subset of the branches above 13 | branches: [master] 14 | schedule: 15 | - cron: '0 23 * * 3' 16 | 17 | jobs: 18 | analyze: 19 | name: Analyze 20 | runs-on: ubuntu-latest 21 | 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | # Override automatic language detection by changing the below list 26 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] 27 | language: ['python'] 28 | # Learn more... 29 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection 30 | 31 | steps: 32 | - name: Checkout repository 33 | uses: actions/checkout@v2 34 | with: 35 | # We must fetch at least the immediate parents so that if this is 36 | # a pull request then we can checkout the head. 37 | fetch-depth: 2 38 | 39 | # If this run was triggered by a pull request event, then checkout 40 | # the head of the pull request instead of the merge commit. 41 | - run: git checkout HEAD^2 42 | if: ${{ github.event_name == 'pull_request' }} 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v1 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v1 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v1 72 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # Gitlab CI automation 2 | 3 | name: CI mirroring 4 | 5 | # Controls when the action will run. 6 | on: 7 | push: 8 | branches: 9 | - '**' 10 | tags-ignore: 11 | - '*.*' 12 | pull_request: 13 | # schedule: 14 | # # * is a special character in YAML so you have to quote this string 15 | # - cron: '30 2 * * *' 16 | 17 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 18 | jobs: 19 | # This workflow contains a single job called "build" 20 | build: 21 | # The type of runner that the job will run on 22 | #runs-on: ubuntu-latest 23 | runs-on: ubuntu-latest 24 | timeout-minutes: 15 25 | 26 | # Steps represent a sequence of tasks that will be executed as part of the job 27 | steps: 28 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 29 | - uses: actions/checkout@v2 30 | 31 | - name: Import environment variables from a file 32 | id: import-env 33 | shell: bash 34 | run: | 35 | gpg --quiet --batch --yes --decrypt --passphrase="$PATROWL_SECRET_GITHUB_KEY" --output ./.github/workflows/secret.env ./.github/workflows/secret.env.pgp 36 | while read line; do 37 | echo "$line" >> $GITHUB_ENV 38 | done < ./.github/workflows/secret.env 39 | env: 40 | PATROWL_SECRET_GITHUB_KEY: ${{ secrets.PATROWL_SECRET_GITHUB_KEY }} 41 | 42 | - name: Get branch name (merge) 43 | if: github.event_name != 'pull_request' 44 | shell: bash 45 | run: echo "BRANCH=$(echo ${GITHUB_REF#refs/heads/})" >> $GITHUB_ENV 46 | 47 | - name: Get branch name (pull request) 48 | if: github.event_name == 'pull_request' 49 | shell: bash 50 | run: echo "BRANCH=$(echo ${GITHUB_HEAD_REF})" >> $GITHUB_ENV 51 | 52 | - name: github-sync and wait for gitlab pipeline 53 | shell: bash 54 | run: | 55 | chmod +x .github/workflows/github-sync.sh 56 | ./.github/workflows/github-sync.sh 57 | env: 58 | SRC_REPO: ${{ env.SRC_REPO }} 59 | DST_REPO: ${{ env.DST_REPO }} 60 | SRC_REPO_TOKEN_USER: ${{ env.SRC_REPO_TOKEN_USER }} 61 | DST_REPO_TOKEN_USER: ${{ env.DST_REPO_TOKEN_USER }} 62 | SRC_REPO_TOKEN: ${{ env.SRC_REPO_TOKEN }} 63 | DST_REPO_TOKEN: ${{ env.DST_REPO_TOKEN }} 64 | -------------------------------------------------------------------------------- /.github/workflows/secret.env.pgp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Patrowl/PatrowlManager/d5f0a23cf8eebd7a393dfa2b73e4274f34891dce/.github/workflows/secret.env.pgp -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.pyc 3 | __pycache__ 4 | env 5 | env3 6 | db.sqlite3 7 | .DS_Store 8 | dump.rdb 9 | app/settings.py 10 | app/settings-*.py 11 | app/assets_detection_rules.py 12 | media/imports/* 13 | media/owners_docs/* 14 | media/policies/* 15 | media/reports/* 16 | var/tmp/db.json 17 | # **/migrations/[0-9]*.py 18 | .scannerwork 19 | .vscode 20 | staticfiles 21 | docker-compose.cloud.yml* 22 | docker-entrypoint.cloud.sh* 23 | 24 | # Exclude PRO files :) 25 | pro 26 | env3pro/ 27 | .env-* 28 | -------------------------------------------------------------------------------- /.gitlab-ci-old.yml: -------------------------------------------------------------------------------- 1 | --- 2 | stages: 3 | - build 4 | - test 5 | - clean 6 | 7 | build-job: 8 | stage: build 9 | before_script: 10 | - docker-compose --version 11 | script: 12 | - echo "Building Patrowl Manager on branch $CI_COMMIT_BRANCH" 13 | - docker-compose build --force-rm 14 | 15 | run-job: 16 | stage: test 17 | script: 18 | - echo "Running Patrowl Manager on branch $CI_COMMIT_BRANCH" 19 | - docker-compose up --detach 20 | needs: ["build-job"] 21 | after_script: 22 | - docker ps -a 23 | 24 | clean-job: 25 | stage: clean 26 | script: 27 | - echo "Stopping Patrowl Manager on branch $CI_COMMIT_BRANCH" 28 | - docker-compose stop 29 | needs: ["run-job"] 30 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | variables: 3 | CI_DEBUG_TRACE: "false" 4 | REGISTRY_URL: "registry.local:5000/patrowl/docker-images" 5 | STACK: "patrowl-manager" 6 | FOLDER_PATH: "." 7 | stages: 8 | - build 9 | - test 10 | - run 11 | 12 | build-push-manager: 13 | stage: build 14 | image: 15 | name: gcr.io/kaniko-project/executor:debug 16 | entrypoint: [""] 17 | script: 18 | - VERSION=$(cat $CI_PROJECT_DIR/$FOLDER_PATH/VERSION | cut -d " " -f1) 19 | - echo "Building Dockerfile $REGISTRY_URL/$STACK:$VERSION on branch $CI_COMMIT_BRANCH" 20 | - mkdir -p /kaniko/.docker 21 | - echo "{\"auths\":{$(echo \"${REGISTRY_URL}\" | cut -d '/' -f1)\":{\"username\":\"$REGISTRY_USER\",\"password\":\"$REGISTRY_PASSWORD\"}}}" > /kaniko/.docker/config.json 22 | - cat /kaniko/.docker/config.json 23 | - /kaniko/executor --insecure --cleanup --context $CI_PROJECT_DIR/$FOLDER_PATH --dockerfile $CI_PROJECT_DIR/$FOLDER_PATH/Dockerfile --destination $REGISTRY_URL/$STACK:$VERSION 24 | rules: 25 | - if: $CI_COMMIT_BRANCH == "master" 26 | changes: 27 | - VERSION 28 | 29 | test-build-manager: 30 | stage: test 31 | image: 32 | name: gcr.io/kaniko-project/executor:debug 33 | entrypoint: [""] 34 | script: 35 | - echo "Building Dockerfile $REGISTRY_URL/$STACK:$VERSION on branch $CI_COMMIT_BRANCH" 36 | - /kaniko/executor --insecure --cleanup --context $CI_PROJECT_DIR/$FOLDER_PATH --dockerfile $CI_PROJECT_DIR/$FOLDER_PATH/Dockerfile --no-push 37 | rules: 38 | - if: $CI_COMMIT_BRANCH == "master" 39 | when: never 40 | - when: always 41 | 42 | # run-job-manager: 43 | # stage: run 44 | # image: 45 | # name: $REGISTRY_URL/$STACK:$VERSION 46 | # script: 47 | # - echo "Running $STACK on branch $CI_COMMIT_BRANCH" 48 | # rules: 49 | # - if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH 50 | # changes: 51 | # - $FOLDER_PATH/* 52 | # after_script: 53 | # - docker images | grep $STACK 54 | # - docker stop $(docker ps -a -q --filter ancestor=$REGISTRY_URL/$STACK:$VERSION) 55 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | language: python 3 | python: 4 | - '3.7' 5 | install: 6 | - pip3 install -r requirements.txt 7 | script: 8 | - docker --version 9 | # - docker build --tag patrowl-manager . 10 | - docker-compose up -d 11 | # - docker-compose up -f docker-compose.with-engines.yml 12 | - pytest 13 | after_script: 14 | - docker images 15 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Authors 2 | ------- 3 | * Nicolas Mattiocco - Patrowl (@MaKyOtOx - nicolas@patrowl.io) 4 | 5 | Crontributors 6 | ------- 7 | * CERT Banque de France (CERT-BDF) 8 | * Nicolas Béguier 9 | 10 | Copyright (C) 2018-2021 Nicolas MATTIOCCO 11 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7-slim 2 | MAINTAINER Patrowl.io "getsupport@patrowl.io" 3 | LABEL Name="PatrowlManager" Version="1.8.4" 4 | 5 | ENV PYTHONUNBUFFERED 1 6 | ARG arg_http_proxy 7 | ENV http_proxy $arg_http_proxy 8 | ENV https_proxy $arg_http_proxy 9 | 10 | RUN mkdir -p /opt/patrowl-manager/ 11 | WORKDIR /opt/patrowl-manager/ 12 | 13 | RUN apt-get update -yq \ 14 | && apt-get install -yq --no-install-recommends apt-utils python3 python3-pip libmagic-dev python3-dev gcc wget \ 15 | && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \ 16 | && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 17 | 18 | ADD ./requirements.txt /root/ 19 | 20 | #RUN python --version \ 21 | # && pip3 install virtualenv \ 22 | # && virtualenv env3 \ 23 | # && /opt/patrowl-manager/env3/bin/pip3 install --no-cache-dir -r /root/requirements.txt 24 | 25 | RUN python --version \ 26 | && pip3 install --trusted-host pypi.org --trusted-host files.pythonhosted.org --trusted-host pypi.python.org --default-timeout=100 virtualenv \ 27 | && virtualenv env3 \ 28 | && /opt/patrowl-manager/env3/bin/pip3 install --trusted-host pypi.org --trusted-host files.pythonhosted.org --trusted-host pypi.python.org --default-timeout=100 --no-cache-dir -r /root/requirements.txt 29 | 30 | COPY . /opt/patrowl-manager/ 31 | COPY app/settings.py.sample /opt/patrowl-manager/app/settings.py 32 | COPY app/assets_detection_rules.py.sample /opt/patrowl-manager/app/assets_detection_rules.py 33 | 34 | EXPOSE 8003 35 | ENTRYPOINT ["/opt/patrowl-manager/docker-entrypoint.sh"] 36 | CMD ["run"] 37 | -------------------------------------------------------------------------------- /Dockerfile_nginx: -------------------------------------------------------------------------------- 1 | FROM nginx:latest 2 | 3 | MAINTAINER Patrowl.io "getsupport@patrowl.io" 4 | LABEL Name="Patrowl Manager - Nginx" Version="1.0.0" 5 | 6 | ADD /static /static 7 | ADD /media /media 8 | 9 | WORKDIR / 10 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | - [Security Policy](#security-policy) 2 | - [Supported Versions](#supported-versions) 3 | - [Reporting a Vulnerability](#reporting-a-vulnerability) 4 | 5 | # Security Policy 6 | 7 | ## Supported Versions 8 | 9 | We release patches for security vulnerabilities. Which versions are eligible 10 | receiving such patches depend on the CVSS v3.0 Rating: 11 | 12 | | CVSS v3.0 | Supported Versions | 13 | | --------- | ----------------------------------------- | 14 | | 9.0-10.0 | Releases within the previous three months | 15 | | 4.0-8.9 | Most recent release | 16 | 17 | ## Reporting a Vulnerability 18 | 19 | Please report (suspected) security vulnerabilities to 20 | **[security@patrowl.io](mailto:security@patrowl.io)**. You will receive a response from 21 | us within 48 hours. If the issue is confirmed, we will release a patch as soon 22 | as possible depending on complexity but historically within a few days. 23 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 1.8.4 // Community Edition 2 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import absolute_import, unicode_literals 3 | from .celery import app as celery_app 4 | 5 | __all__ = ('celery_app',) 6 | -------------------------------------------------------------------------------- /app/assets_detection_rules.py.sample: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ASSET_DETECTION_RULES = [ 3 | { 4 | "name": "New urls from NMAP report (HTTP)", 5 | "datatype": "url", 6 | "group_name": "New URLS", 7 | "filters": [ 8 | { 9 | "asset_name__startswith": "http://", 10 | }, { 11 | "engine_type": "NMAP", 12 | "title__startswith": "Service 'http' is running" 13 | }, { 14 | "engine_type": "NMAP", 15 | "title__startswith": "Port 'tcp/80' is open" 16 | }, 17 | ], 18 | "allowed_datatypes": ["ip", "domain", "fqdn"], 19 | "output_pattern": "http://__asset__" 20 | }, 21 | { 22 | "name": "New urls from NMAP report (HTTPS)", 23 | "datatype": "url", 24 | "group_name": "New URLS", 25 | "filters": [ 26 | { 27 | "asset_name__startswith": "https://", 28 | }, { 29 | "engine_type": "NMAP", 30 | "title__startswith": "Service 'https' is running" 31 | }, { 32 | "engine_type": "NMAP", 33 | "title__startswith": "Port 'tcp/443' is open" 34 | }, 35 | ], 36 | "allowed_datatypes": ["ip", "domain", "fqdn"], 37 | "output_pattern": "https://__asset__" 38 | }, 39 | ] 40 | -------------------------------------------------------------------------------- /app/celery.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | from celery import Celery 5 | from django.conf import settings 6 | from kombu import Exchange, Queue 7 | 8 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'app.settings') 9 | 10 | # set the default Django settings module for the 'celery' program. 11 | app = Celery('app', broker=settings.BROKER_URL) 12 | app.config_from_object('django.conf:settings', namespace='CELERY') 13 | app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) 14 | 15 | # app.conf.broker_transport_options = { 16 | # 'max_retries': 3, 17 | # 'interval_start': 0, 18 | # 'interval_step': 0.2, 19 | # 'interval_max': 0.2, 20 | # } 21 | 22 | #app.conf.timezone = os.environ.get('PATROWL_TZ', 'Europe/Paris') 23 | 24 | app.conf.task_queues = ( 25 | # Default Queue / administrative purposes 26 | Queue('default', Exchange('default'), routing_key='default'), 27 | Queue('scan', Exchange('scan'), routing_key='scan'), 28 | Queue('scanmgt', Exchange('scanmgt'), routing_key='scanmgt'), 29 | ) 30 | app.conf.task_default_queue = 'default' 31 | app.conf.task_default_exchange = 'default' 32 | app.conf.task_default_exchange_type = 'direct' 33 | app.conf.task_default_routing_key = 'default' 34 | 35 | app.conf.enable_utc = False 36 | -------------------------------------------------------------------------------- /app/context_processors.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | 4 | def selected_settings(request): 5 | r = { 6 | 'PATROWL_VERSION': settings.PATROWL_VERSION, 7 | 'PATROWL_REFRESH_ENGINE': settings.PATROWL_REFRESH_ENGINE, 8 | 'PRO_EDITION': settings.PRO_EDITION, 9 | 'LOGOUT_URL': settings.LOGOUT_URL 10 | } 11 | 12 | if hasattr(settings, 'LOGIN_SSO_URL'): 13 | r.update({'LOGIN_SSO_URL': settings.LOGIN_SSO_URL}) 14 | 15 | return r 16 | -------------------------------------------------------------------------------- /app/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | 4 | # HTTP Error 400 5 | def custom_bad_request(request, exception=None): 6 | response = render(request, 'errors/400.html', {}) 7 | response.status_code = 400 8 | 9 | return response 10 | 11 | 12 | # HTTP Error 403 13 | def custom_permission_denied(request, exception=None): 14 | response = render(request, 'errors/403.html', {}) 15 | response.status_code = 403 16 | 17 | return response 18 | 19 | 20 | # HTTP Error 404 21 | def custom_page_not_found(request, exception): 22 | response = render(request, 'errors/404.html', {}) 23 | response.status_code = 404 24 | 25 | return response 26 | 27 | 28 | # HTTP Error 500 29 | def custom_error(request, exception=None): 30 | response = render(request, 'errors/500.html', {}) 31 | response.status_code = 500 32 | 33 | return response 34 | -------------------------------------------------------------------------------- /app/wsgi.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | from django.core.wsgi import get_wsgi_application 5 | 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "app.settings") 7 | 8 | application = get_wsgi_application() 9 | -------------------------------------------------------------------------------- /assets/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Patrowl/PatrowlManager/d5f0a23cf8eebd7a393dfa2b73e4274f34891dce/assets/__init__.py -------------------------------------------------------------------------------- /assets/admin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.contrib import admin 4 | 5 | # Register your models here. 6 | from .models import ( 7 | Asset, AssetGroup, DynamicAssetGroup, 8 | AssetOwner, AssetOwnerContact, AssetOwnerDocument, AssetCategory 9 | ) 10 | 11 | admin.site.register(Asset) 12 | admin.site.register(AssetGroup) 13 | admin.site.register(DynamicAssetGroup) 14 | admin.site.register(AssetOwner) 15 | admin.site.register(AssetOwnerContact) 16 | admin.site.register(AssetOwnerDocument) 17 | admin.site.register(AssetCategory) 18 | -------------------------------------------------------------------------------- /assets/apps.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.apps import AppConfig 4 | 5 | 6 | class AssetsConfig(AppConfig): 7 | name = 'assets' 8 | -------------------------------------------------------------------------------- /assets/migrations/0002_auto_20200616_1735.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.13 on 2020-06-16 15:35 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('users', '0002_auto_20200616_1704'), 11 | ('assets', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.RemoveField( 16 | model_name='asset', 17 | name='owner', 18 | ), 19 | migrations.AddField( 20 | model_name='asset', 21 | name='team', 22 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='users.Team'), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /assets/migrations/0003_asset_owner.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.13 on 2020-06-16 15:38 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 12 | ('assets', '0002_auto_20200616_1735'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AddField( 17 | model_name='asset', 18 | name='owner', 19 | field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /assets/migrations/0004_auto_20200617_0958.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.13 on 2020-06-17 07:58 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('users', '0002_auto_20200616_1704'), 10 | ('assets', '0003_asset_owner'), 11 | ] 12 | 13 | operations = [ 14 | migrations.RemoveField( 15 | model_name='asset', 16 | name='team', 17 | ), 18 | migrations.AddField( 19 | model_name='asset', 20 | name='teams', 21 | field=models.ManyToManyField(to='users.Team'), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /assets/migrations/0005_auto_20200617_1011.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.13 on 2020-06-17 08:11 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('assets', '0004_auto_20200617_0958'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='asset', 17 | name='owner', 18 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL), 19 | ), 20 | migrations.AlterField( 21 | model_name='asset', 22 | name='teams', 23 | field=models.ManyToManyField(blank=True, to='users.Team'), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /assets/migrations/0006_assetgroup_teams.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.13 on 2020-06-17 08:27 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('users', '0002_auto_20200616_1704'), 10 | ('assets', '0005_auto_20200617_1011'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='assetgroup', 16 | name='teams', 17 | field=models.ManyToManyField(blank=True, to='users.Team'), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /assets/migrations/0007_auto_20200709_0007.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.13 on 2020-07-08 22:07 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('assets', '0006_assetgroup_teams'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='asset', 15 | name='name', 16 | field=models.TextField(max_length=2048), 17 | ), 18 | migrations.AlterField( 19 | model_name='asset', 20 | name='value', 21 | field=models.TextField(max_length=2048, unique=True), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /assets/migrations/0008_asset_exposure.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.16 on 2020-09-21 09:40 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('assets', '0007_auto_20200709_0007'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='asset', 15 | name='exposure', 16 | field=models.CharField(choices=[('external', 'External'), ('internal', 'Internal'), ('restricted', 'Restricted')], default='external', max_length=16), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /assets/migrations/0009_auto_20200921_1151.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.16 on 2020-09-21 09:51 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('assets', '0008_asset_exposure'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='asset', 15 | name='exposure', 16 | field=models.CharField(choices=[('unknown', 'Unknown'), ('external', 'External'), ('internal', 'Internal'), ('restricted', 'Restricted')], default='unknown', max_length=16), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /assets/migrations/0010_auto_20210402_1439.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.18 on 2021-04-02 12:39 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('assets', '0009_auto_20200921_1151'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='assetgroup', 15 | name='categories', 16 | field=models.ManyToManyField(blank=True, to='assets.AssetCategory'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /assets/migrations/0011_auto_20211123_1140.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.24 on 2021-11-23 10:40 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('assets', '0010_auto_20210402_1439'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='assetgroup', 15 | name='criticity', 16 | field=models.CharField(choices=[('low', 'low'), ('medium', 'medium'), ('high', 'high')], default='low', max_length=10), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /assets/migrations/0012_dynamicassetgroup.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.25 on 2021-12-21 15:00 2 | 3 | import assets.models 4 | from django.conf import settings 5 | import django.contrib.postgres.fields.jsonb 6 | from django.db import migrations, models 7 | import django.db.models.deletion 8 | import django.utils.timezone 9 | 10 | 11 | class Migration(migrations.Migration): 12 | 13 | dependencies = [ 14 | ('users', '0005_profile_is_delegated'), 15 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 16 | ('assets', '0011_auto_20211123_1140'), 17 | ] 18 | 19 | operations = [ 20 | migrations.CreateModel( 21 | name='DynamicAssetGroup', 22 | fields=[ 23 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 24 | ('name', models.CharField(max_length=256, unique=True)), 25 | ('criticity', models.CharField(choices=[('low', 'low'), ('medium', 'medium'), ('high', 'high')], default='low', max_length=10)), 26 | ('risk_level', django.contrib.postgres.fields.jsonb.JSONField(default=assets.models.get_default_risk_level)), 27 | ('description', models.CharField(blank=True, max_length=256, null=True)), 28 | ('created_at', models.DateTimeField(default=django.utils.timezone.now)), 29 | ('updated_at', models.DateTimeField(default=django.utils.timezone.now)), 30 | ('owner', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), 31 | ('tags', models.ManyToManyField(blank=True, to='assets.AssetCategory')), 32 | ('teams', models.ManyToManyField(blank=True, to='users.Team')), 33 | ], 34 | options={ 35 | 'db_table': 'asset_groups_dynamic', 36 | }, 37 | ), 38 | ] 39 | -------------------------------------------------------------------------------- /assets/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Patrowl/PatrowlManager/d5f0a23cf8eebd7a393dfa2b73e4274f34891dce/assets/migrations/__init__.py -------------------------------------------------------------------------------- /assets/serializers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from rest_framework import serializers, generics 4 | from common.utils.pagination import StandardResultsSetPagination 5 | from django.utils.translation import gettext_lazy as _ 6 | from django_filters import rest_framework as filters 7 | from django_filters import FilterSet, OrderingFilter 8 | from .models import Asset, AssetGroup 9 | 10 | 11 | class AssetSerializer(serializers.ModelSerializer): 12 | class Meta: 13 | model = Asset 14 | fields = ('id', 'value', 'name', 'type', 'owner', 'description', 15 | 'criticity', 'status', 'created_at', 'updated_at', 'teams', 'exposure') 16 | 17 | 18 | class AssetFilter(FilterSet): 19 | sorted_by = OrderingFilter( 20 | # tuple-mapping retains order 21 | choices=( 22 | ('value', _('Value')), 23 | ('-value', _('Value (desc)')), 24 | ('name', _('Name')), 25 | ('-name', _('Name (desc)')), 26 | ('criticity', _('Criticity')), 27 | ('-criticity', _('Criticity (desc)')), 28 | ('type', _('Type')), 29 | ('-type', _('Type (desc)')), 30 | ('exposure', _('Exposure')), 31 | ('-exposure', _('Exposure (desc)')), 32 | ) 33 | ) 34 | 35 | class Meta: 36 | model = Asset 37 | fields = { 38 | 'name': ['icontains'], 39 | 'value': ['icontains'], 40 | 'description': ['icontains'], 41 | } 42 | 43 | 44 | class AssetList(generics.ListAPIView): 45 | serializer_class = AssetSerializer 46 | filter_backends = (filters.DjangoFilterBackend,) 47 | filterset_class = AssetFilter 48 | filterset_fields = ('id', 'name', 'value', 'criticity', 'type') 49 | pagination_class = StandardResultsSetPagination 50 | 51 | def get_queryset(self): 52 | return Asset.objects.for_user(self.request.user).all().order_by('value') 53 | 54 | 55 | class AssetGroupSerializer(serializers.ModelSerializer): 56 | class Meta: 57 | model = AssetGroup 58 | fields = ('id', 'name', 'owner', 'description', 'assets', 59 | 'criticity', 'created_at', 'updated_at', 'teams') 60 | 61 | 62 | class AssetGroupFilter(FilterSet): 63 | sorted_by = OrderingFilter( 64 | choices=( 65 | ('name', _('Name')), 66 | ('-name', _('Name (desc)')), 67 | ('criticity', _('Criticity')), 68 | ('-criticity', _('Criticity (desc)')), 69 | ) 70 | ) 71 | 72 | class Meta: 73 | model = AssetGroup 74 | fields = { 75 | 'name': ['icontains'], 76 | 'description': ['icontains'], 77 | } 78 | 79 | 80 | class AssetGroupList(generics.ListAPIView): 81 | serializer_class = AssetGroupSerializer 82 | filter_backends = (filters.DjangoFilterBackend,) 83 | filterset_class = AssetGroupFilter 84 | filterset_fields = ('id', 'name', 'criticity') 85 | pagination_class = StandardResultsSetPagination 86 | 87 | def get_queryset(self): 88 | return AssetGroup.objects.for_user(self.request.user).all().order_by('name') 89 | -------------------------------------------------------------------------------- /assets/templates/add-asset.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 | 4 | 8 | 9 |
10 |
11 | Add an asset 12 | {% csrf_token %} 13 | {% for field in form %} 14 | {% if field.errors %} 15 |
16 | 17 |
18 | {{ field }} 19 | 20 | {% for error in field.errors %}{{ error }}{% endfor %} 21 | 22 |
23 |
24 | {% elif not field.is_hidden %} 25 |
26 | 27 |
28 | {{ field }} 29 | {% if field.help_text %} 30 |

{{ field.help_text }}

31 | {% endif %} 32 |
33 |
34 | {% endif %} 35 | {% endfor %} 36 |
37 |
38 |
39 |
40 | 41 |
42 |
43 |
44 |
45 | 46 |
47 | {% if messages %} 48 | 53 | {% endif %} 54 |
55 | 56 | {% endblock %} 57 | -------------------------------------------------------------------------------- /assets/templates/add-assets-bulk.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 | 4 | 8 | 9 |
10 |
11 | Add assets in bulk [template] 12 | {% csrf_token %} 13 | {% for field in form %} 14 | {% if field.errors %} 15 |
16 | 17 |
18 | {{ field }} 19 | 20 | {% for error in field.errors %}{{ error }}{% endfor %} 21 | 22 |
23 |
24 | {% elif not field.is_hidden %} 25 |
26 | 27 |
28 | {{ field }} 29 | {% if field.help_text %} 30 |

{{ field.help_text }}

31 | {% endif %} 32 |
33 |
34 | {% endif %} 35 | {% endfor %} 36 |
37 | 38 | 39 |
40 |
41 |
42 | 43 | {% endblock %} 44 | -------------------------------------------------------------------------------- /assets/templates/delete-asset-group.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 | 4 |
5 |
6 | 7 | Do you confirm the deletion? 8 | {% csrf_token %} 9 |

Asset Group ID: {{ asset_group.id }}

10 |

Addr: {{ asset_group.assets }}

11 |
12 | 13 | 14 |
15 |
16 | 17 | 18 | {% if messages %} 19 | 24 | {% endif %} 25 |
26 | 27 | {% endblock %} 28 | -------------------------------------------------------------------------------- /assets/templates/delete-asset-owner.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 | 4 |
5 |
6 | 7 | Do you confirm the deletion? 8 | {% csrf_token %} 9 |

Asset ID: {{ owner.id }}

10 |

Name: {{ owner.name }}

11 |
12 | 13 | 14 |
15 |
16 | 17 | 18 | {% if messages %} 19 | 24 | {% endif %} 25 |
26 | 27 | {% endblock %} 28 | -------------------------------------------------------------------------------- /assets/templates/delete-asset.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 | 4 |
5 |
6 | 7 | Do you confirm the deletion? 8 | {% csrf_token %} 9 |

Asset ID: {{ asset.id }}

10 |

Addr: {{ asset.value }}

11 |
12 | 13 | 14 |
15 |
16 | 17 | 18 | {% if messages %} 19 | 24 | {% endif %} 25 |
26 | 27 | {% endblock %} 28 | -------------------------------------------------------------------------------- /assets/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase, Client 2 | 3 | 4 | class AssetTestCase(TestCase): 5 | 6 | def add_asset_test(self): 7 | print("TEST CASE: add_asset_test") 8 | c = Client() 9 | 10 | print("TEST CASE: testing with GET method") 11 | r = c.get('http://127.0.0.1:8000/assets/add?value=testingasset') 12 | print(r.json()) 13 | 14 | print("TEST CASE: testing with POST method") 15 | r = c.post('http://127.0.0.1:8000/assets/add', { 16 | "value": "8.8.8.8", 17 | "name": "DNS Google (A)", 18 | "type": "ipv4", 19 | "owner": "nicolas", 20 | "description": "Google DNS Server", 21 | "status": "up" 22 | }) 23 | print(r.json()) 24 | -------------------------------------------------------------------------------- /assets/utils.py: -------------------------------------------------------------------------------- 1 | from users.models import Team 2 | from assets.models import Asset, AssetGroup 3 | 4 | 5 | def _get_allowed_team(team_name, user): 6 | team = None 7 | if user.is_superuser: 8 | team = Team.objects.filter(name__iexact=team_name).first() 9 | else: 10 | team = user.users_team.filter(name__iexact=team_name).first() 11 | 12 | return team 13 | 14 | 15 | def _add_new_asset(metadata): 16 | """ 17 | Add new asset. 18 | 19 | { 20 | "datatype": rule['datatype'], 21 | "rule_name": rule['name'], 22 | "group_name": rule['group_name'], 23 | "asset_value": asset_value, 24 | "original_asset_value": self.asset.value, 25 | "asset_teams": self.asset.teams.all(), 26 | } 27 | """ 28 | 29 | try: 30 | # Create the asset 31 | asset = Asset.objects.create( 32 | value=metadata["asset_value"], 33 | name=f"{metadata['asset_value']} (Auto-created)", 34 | type=metadata["datatype"], 35 | owner=metadata["owner"], 36 | description=f"Auto-created by finding evaluation rule ({metadata['rule_name']}) from asset {metadata['original_asset_value']}", 37 | ) 38 | 39 | # Add related Teams 40 | for team in metadata["asset_teams"]: 41 | asset.teams.add(team) 42 | 43 | # Add it to a group 44 | asset_group = AssetGroup.objects.filter(name=metadata['group_name']).first() 45 | if asset_group is None: 46 | # Create it first 47 | asset_group = AssetGroup.objects.create( 48 | name=metadata['group_name'], 49 | owner=metadata["owner"], 50 | description=f"Auto-created by finding evaluation rule ({metadata['rule_name']})" 51 | ) 52 | 53 | for team in metadata["asset_teams"]: 54 | asset_group.teams.add(team) 55 | 56 | # Add this asset to the group 57 | asset_group.assets.add(asset) 58 | 59 | # print(asset) 60 | except Exception: 61 | return None 62 | 63 | return asset.id 64 | -------------------------------------------------------------------------------- /common/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Patrowl/PatrowlManager/d5f0a23cf8eebd7a393dfa2b73e4274f34891dce/common/__init__.py -------------------------------------------------------------------------------- /common/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.decorators import user_passes_test 2 | 3 | 4 | def pro_permission_required(*args): 5 | """ 6 | Check user has any of the given permissions. 7 | 8 | Permission required can not be used in its place as that takes only a 9 | single permission. 10 | """ 11 | def test_func(user): 12 | from django.conf import settings 13 | if not settings.PRO_EDITION or user.is_superuser: 14 | return True 15 | for perm in args: 16 | if user.has_perm(perm): 17 | return True 18 | return False 19 | return user_passes_test(test_func) 20 | 21 | 22 | def pro_group_required(*group_names): 23 | """Require user membership in at least one of the groups passed in.""" 24 | def in_groups(u): 25 | from django.conf import settings 26 | if not settings.PRO_EDITION or u.is_superuser: 27 | return True 28 | if u.is_authenticated: 29 | if bool(u.groups.filter(name__in=group_names)): 30 | return True 31 | return False 32 | return user_passes_test(in_groups) 33 | 34 | 35 | def get_api_default_permissions(self): 36 | from rest_framework.decorators import permission_classes 37 | from rest_framework.permissions import IsAdminUser, IsAuthenticated 38 | if self.action == 'list': 39 | permission_classes = [IsAuthenticated] 40 | else: 41 | permission_classes = [IsAdminUser] 42 | return [permission() for permission in permission_classes] 43 | 44 | 45 | def chunked_queryset(queryset, chunk_size): 46 | """ Slice a queryset into chunks. """ 47 | 48 | start_pk = 0 49 | queryset = queryset.order_by('pk') 50 | 51 | while True: 52 | # No entry left 53 | if not queryset.filter(pk__gt=start_pk).exists(): 54 | break 55 | 56 | try: 57 | # Fetch chunk_size entries if possible 58 | end_pk = queryset.filter(pk__gt=start_pk).values_list( 59 | 'pk', flat=True)[chunk_size - 1] 60 | 61 | # Fetch rest entries if less than chunk_size left 62 | except IndexError: 63 | end_pk = queryset.values_list('pk', flat=True).last() 64 | 65 | yield queryset.filter(pk__gt=start_pk).filter(pk__lte=end_pk) 66 | 67 | start_pk = end_pk 68 | -------------------------------------------------------------------------------- /common/utils/cpe.py: -------------------------------------------------------------------------------- 1 | from cpe import CPE 2 | 3 | 4 | def extract_cpe(cpe_vector): 5 | """Extract vendor and product strings from CPE vector.""" 6 | vendor = None 7 | product = None 8 | 9 | try: 10 | c = CPE(cpe_vector) 11 | vendor = c.get_vendor()[0] 12 | product = c.get_product()[0] 13 | # print("-->", c, vendor, product) 14 | except Exception as e: 15 | print(cpe_vector, e) 16 | 17 | return vendor, product 18 | -------------------------------------------------------------------------------- /common/utils/date.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | 4 | def validate_datetime(date_text): 5 | # print(date_text) 6 | try: 7 | return datetime.datetime.strptime(date_text, '%Y-%m-%d %H:%M:%S') 8 | except ValueError: 9 | return False 10 | -------------------------------------------------------------------------------- /common/utils/encoding.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from datetime import date, datetime 4 | from uuid import UUID 5 | import html.entities 6 | 7 | 8 | def unicode_escape(unistr): 9 | """ 10 | Tidys up unicode entities into HTML friendly entities. 11 | Takes a unicode string as an argument 12 | Returns a unicode string 13 | """ 14 | 15 | escaped = "" 16 | for char in unistr: 17 | if ord(char) in html.entities.codepoint2name: 18 | name = html.entities.codepoint2name.get(ord(char)) 19 | entity = html.entities.name2codepoint.get(name) 20 | escaped += "&#" + str(entity) 21 | 22 | else: 23 | escaped += char 24 | 25 | return escaped 26 | 27 | 28 | def json_serial(obj): 29 | """JSON serializer for objects not serializable by default json code.""" 30 | if isinstance(obj, (datetime, date)): 31 | return obj.isoformat() 32 | if isinstance(obj, UUID): 33 | # if the obj is uuid, we simply return the value of uuid 34 | return obj.hex 35 | raise TypeError("Type %s not serializable" % type(obj)) 36 | -------------------------------------------------------------------------------- /common/utils/pagination.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from rest_framework.pagination import PageNumberPagination 3 | 4 | 5 | class StandardResultsSetPagination(PageNumberPagination): 6 | page_size_query_param = 'limit' 7 | -------------------------------------------------------------------------------- /common/utils/password.py: -------------------------------------------------------------------------------- 1 | import random 2 | import string 3 | 4 | 5 | def get_random_alphanumeric_string(length): 6 | letters_and_digits = string.ascii_letters + string.digits 7 | return ''.join((random.choice(letters_and_digits) for i in range(length))) 8 | -------------------------------------------------------------------------------- /common/utils/settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from app import settings 4 | 5 | # 6 | # def is_restricted(): 7 | # """Check if the instance is usage restricted.""" 8 | # if "RESTRICTED_USAGE" in dir(settings) and settings.RESTRICTED_USAGE is True: 9 | # return True 10 | # return False 11 | 12 | 13 | def is_restricted(): 14 | """Check if the instance run PRO edition.""" 15 | if "PRO_EDITION" in dir(settings) and settings.PRO_EDITION is True: 16 | return True 17 | return False 18 | -------------------------------------------------------------------------------- /docker-compose.with-engines.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | 4 | rabbitmq: 5 | image: rabbitmq:3-alpine 6 | container_name: patrowl-rabbitmq 7 | environment: 8 | - RABBITMQ_SERVER_ADDITIONAL_ERL_ARGS=-rabbit log_levels [{connection,error}] 9 | expose: 10 | - '5672' 11 | 12 | db: 13 | image: postgres:11-alpine 14 | restart: always 15 | container_name: patrowl-postgres 16 | environment: 17 | - POSTGRES_DB=patrowl_db 18 | - POSTGRES_USER=PATROWL_DB_USER 19 | - POSTGRES_PASSWORD=PATROWL_DB_PASSWD_TO_CHANGE 20 | expose: 21 | - '5432' 22 | volumes: 23 | - ./var/db/init_db.sql:/docker-entrypoint-initdb.d/init_db.sql 24 | # - ./pg_data:/var/lib/postgresql/data/ 25 | 26 | web: 27 | container_name: patrowl-django 28 | build: . 29 | image: patrowl/patrowl-manager-community-edition:${PATROWL_MANAGER_VERSION} 30 | environment: 31 | - POSTGRES_HOST=db 32 | - DEBUG=False 33 | - RABBITMQ_HOSTNAME=rabbitmq:5672 34 | depends_on: 35 | - db 36 | - rabbitmq 37 | expose: 38 | - "8003" 39 | volumes: 40 | - ./staticfiles:/opt/patrowl-manager/staticfiles 41 | - ./media:/opt/patrowl-manager/media 42 | links: 43 | - db 44 | - rabbitmq 45 | entrypoint: ./docker-entrypoint.with-engines.sh 46 | 47 | nginx: 48 | image: nginx:stable-alpine 49 | container_name: patrowl-nginx 50 | ports: 51 | - "8083:8083" 52 | volumes: 53 | - ./staticfiles:/opt/patrowl-manager/staticfiles 54 | # - ./media:/opt/patrowl-manager/media 55 | - ./var/log:/opt/patrowl-manager/var/log 56 | - ./nginx_docker.conf:/etc/nginx/conf.d/default.conf 57 | depends_on: 58 | - web 59 | 60 | engine-nmap: 61 | image: patrowl/engine-nmap 62 | restart: always 63 | container_name: patrowl-nmap 64 | expose: 65 | - '5001' 66 | 67 | engine-sslscan: 68 | image: patrowl/engine-sslscan 69 | restart: always 70 | container_name: patrowl-sslscan 71 | expose: 72 | - '5014' 73 | 74 | engine-owl_dns: 75 | image: patrowl/engine-owl_dns 76 | restart: always 77 | container_name: patrowl-owl_dns 78 | expose: 79 | - '5006' 80 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | 4 | rabbitmq: 5 | image: rabbitmq:3-alpine 6 | container_name: patrowlmanager-rabbitmq 7 | environment: 8 | - RABBITMQ_SERVER_ADDITIONAL_ERL_ARGS=-rabbit log_levels [{connection,error}] 9 | expose: 10 | - '5672' 11 | 12 | db: 13 | image: postgres:11-alpine 14 | restart: always 15 | container_name: patrowlmanager-postgres 16 | environment: 17 | - POSTGRES_DB=patrowl_db 18 | - POSTGRES_USER=PATROWL_DB_USER 19 | - POSTGRES_PASSWORD=PATROWL_DB_PASSWD_TO_CHANGE 20 | expose: 21 | - '5432' 22 | volumes: 23 | - ./var/db/init_db.sql:/docker-entrypoint-initdb.d/init_db.sql 24 | # - ./pg_data:/var/lib/postgresql/data/ 25 | 26 | web: 27 | container_name: patrowlmanager-django 28 | build: . 29 | image: patrowl/patrowl-manager-community-edition:${PATROWL_MANAGER_VERSION} 30 | environment: 31 | - POSTGRES_HOST=db 32 | - DEBUG=True 33 | - RABBITMQ_HOSTNAME=rabbitmq:5672 34 | depends_on: 35 | - db 36 | - rabbitmq 37 | expose: 38 | - "8003" 39 | volumes: 40 | - ./staticfiles:/opt/patrowl-manager/staticfiles 41 | - ./media:/opt/patrowl-manager/media 42 | links: 43 | - db 44 | - rabbitmq 45 | 46 | nginx: 47 | image: nginx:stable-alpine 48 | container_name: patrowlmanager-nginx 49 | ports: 50 | - "8083:8083" 51 | volumes: 52 | - ./staticfiles:/opt/patrowl-manager/staticfiles 53 | # - ./media:/opt/patrowl-manager/media 54 | - ./var/log:/opt/patrowl-manager/var/log 55 | - ./nginx_docker.conf:/etc/nginx/conf.d/default.conf 56 | depends_on: 57 | - web 58 | -------------------------------------------------------------------------------- /docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | export POSTGRES_HOST=${POSTGRES_HOST:-db} 3 | export POSTGRES_PORT=${POSTGRES_PORT:-5432} 4 | export RABBITMQ_HOST=${RABBITMQ_HOST:-rabbitmq} 5 | export RABBITMQ_PORT=${RABBITMQ_PORT:-5672} 6 | # export UPDATE_DB_SCHEMA=${UPDATE_DB_SCHEMA:-0} 7 | 8 | echo "[+] Wait for DB availability" 9 | while ! 5 |
  • engines
  • 6 |
  • policies
  • 7 |
  • add new policy
  • 8 | 9 |
    10 |
    11 | Add a new policy 12 | {% csrf_token %} 13 | {% for field in form %} 14 | {% if field.errors %} 15 |
    16 | 17 |
    18 | {{ field }} 19 | 20 | {% for error in field.errors %}{{ error }}{% endfor %} 21 | 22 |
    23 |
    24 | {% elif not field.is_hidden %} 25 |
    26 | 27 |
    28 | {{ field }} 29 | {% if field.help_text %} 30 |

    {{ field.help_text }}

    31 | {% endif %} 32 |
    33 |
    34 | {% endif %} 35 | {% endfor %} 36 |
    37 |
    38 |
    39 |
    40 | 41 |
    42 |
    43 |
    44 |
    45 | 46 | {% endblock %} 47 | -------------------------------------------------------------------------------- /engines/templates/add-engine.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 | 4 | 9 | 10 |
    11 |
    12 | Add a engine type 13 | {% csrf_token %} 14 | {% for field in form %} 15 | {% if field.errors %} 16 |
    17 | 18 |
    19 | {{ field }} 20 | 21 | {% for error in field.errors %}{{ error }}{% endfor %} 22 | 23 |
    24 |
    25 | {% elif not field.is_hidden %} 26 |
    27 | 28 |
    29 | {{ field }} 30 | {% if field.help_text %} 31 |

    {{ field.help_text }}

    32 | {% endif %} 33 |
    34 |
    35 | {% endif %} 36 | {% endfor %} 37 |
    38 |
    39 |
    40 |
    41 | 42 |
    43 |
    44 |
    45 |
    46 | 47 | {% endblock %} 48 | -------------------------------------------------------------------------------- /engines/templates/add-scan-engine.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 | 4 | 8 | 9 |
    10 |
    11 | Add a new scan engine 12 | {% csrf_token %} 13 | {% for field in form %} 14 | {% if field.errors %} 15 |
    16 | 17 |
    18 | {{ field }} 19 | 20 | {% for error in field.errors %}{{ error }}{% endfor %} 21 | 22 |
    23 |
    24 | {% elif not field.is_hidden %} 25 |
    26 | 27 |
    28 | {{ field }} 29 | {% if field.help_text %} 30 |

    {{ field.help_text }}

    31 | {% endif %} 32 |
    33 |
    34 | {% endif %} 35 | {% endfor %} 36 |
    37 |
    38 |
    39 |
    40 | 41 |
    42 |
    43 | 44 |
    45 |
    46 | 47 | 68 | 69 | {% endblock %} 70 | -------------------------------------------------------------------------------- /engines/templates/delete-engine-policy.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 | 4 |
    5 |
    6 | 7 | Do you confirm the deletion? 8 | {% csrf_token %} 9 |

    policy_id: {{ policy.id }}

    10 |

    name: {{ policy.name }}

    11 |
    12 | 13 | 14 |
    15 |
    16 | 17 | {% if messages %} 18 | 23 | {% endif %} 24 |
    25 | 26 | {% endblock %} 27 | -------------------------------------------------------------------------------- /engines/templates/delete-engine.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 | 4 |
    5 |
    6 | 7 | Do you confirm the deletion? 8 | {% csrf_token %} 9 |

    engine_id: {{ engine.id }}

    10 |

    name: {{ engine.name }}

    11 |
    12 | 13 | 14 |
    15 |
    16 | 17 | {% if messages %} 18 | 23 | {% endif %} 24 |
    25 | 26 | {% endblock %} 27 | -------------------------------------------------------------------------------- /engines/templates/delete-scan-engine.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 | 4 |
    5 |
    6 | 7 | Do you confirm the deletion? 8 | {% csrf_token %} 9 |

    engine_id: {{ engine.id }}

    10 |

    name: {{ engine.name }}

    11 |

    url: {{ engine.api_url }}

    12 |
    13 | 14 | 15 |
    16 |
    17 | 18 | {% if messages %} 19 | 24 | {% endif %} 25 |
    26 | 27 | {% endblock %} 28 | -------------------------------------------------------------------------------- /engines/templates/edit-engine-policy.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 | 4 | 9 | 10 |
    11 |
    12 | Edit an engine policy 13 | {% csrf_token %} 14 | {% for field in form %} 15 | {% if field.errors %} 16 |
    17 | 18 |
    19 | {{ field }} 20 | 21 | {% for error in field.errors %}{{ error }}{% endfor %} 22 | 23 |
    24 |
    25 | {% elif not field.is_hidden %} 26 |
    27 | 28 |
    29 | {{ field }} 30 | {% if field.help_text %} 31 |

    {{ field.help_text }}

    32 | {% endif %} 33 |
    34 |
    35 | {% endif %} 36 | {% endfor %} 37 |
    38 |
    39 |
    40 |
    41 | 42 |
    43 |
    44 |
    45 | {% if messages %} 46 | 51 | {% endif %} 52 |
    53 | 54 | {% endblock %} 55 | -------------------------------------------------------------------------------- /engines/templates/edit-engine.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 | 4 | 8 | 9 |
    10 |
    11 | Edit an engine type 12 | {% csrf_token %} 13 | {% for field in form %} 14 | {% if field.errors %} 15 |
    16 | 17 |
    18 | {{ field }} 19 | 20 | {% for error in field.errors %}{{ error }}{% endfor %} 21 | 22 |
    23 |
    24 | {% elif not field.is_hidden %} 25 |
    26 | 27 |
    28 | {{ field }} 29 | {% if field.help_text %} 30 |

    {{ field.help_text }}

    31 | {% endif %} 32 |
    33 |
    34 | {% endif %} 35 | {% endfor %} 36 |
    37 |
    38 |
    39 |
    40 | 41 |
    42 |
    43 |
    44 | {% if messages %} 45 | 50 | {% endif %} 51 |
    52 | 53 | {% endblock %} 54 | -------------------------------------------------------------------------------- /engines/templates/import-engine-policies.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 | 4 | 8 | 9 |
    10 |
    11 | Import engine policies (JSON-formatted) 12 | {% csrf_token %} 13 | {% for field in form %} 14 | {% if field.errors %} 15 |
    16 | 17 |
    18 | {{ field }} 19 | 20 | {% for error in field.errors %}{{ error }}{% endfor %} 21 | 22 |
    23 |
    24 | {% elif not field.is_hidden %} 25 |
    26 | 27 |
    28 | {{ field }} 29 | {% if field.help_text %} 30 |

    {{ field.help_text }}

    31 | {% endif %} 32 |
    33 |
    34 | {% endif %} 35 | {% endfor %} 36 |
    37 | 38 | 39 |
    40 |
    41 |
    42 | 43 | {% endblock %} 44 | -------------------------------------------------------------------------------- /engines/templates/info-scan-engine.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 | 4 |
    5 | Engines Info 6 | {% csrf_token %} 7 | 8 | {% for key, value in engine_infos.items %} 9 | 12 | {% endfor %} 13 | 16 | 19 |
    20 | 21 |
    22 |
    23 | 24 | {% if messages %} 25 | 30 | {% endif %} 31 | 32 | {% endblock %} 33 | -------------------------------------------------------------------------------- /engines/tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.test import TestCase, Client 4 | -------------------------------------------------------------------------------- /events/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Patrowl/PatrowlManager/d5f0a23cf8eebd7a393dfa2b73e4274f34891dce/events/__init__.py -------------------------------------------------------------------------------- /events/admin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.contrib import admin 4 | from .models import Event, Alert, AlertOverride 5 | 6 | 7 | class EventAdmin(admin.ModelAdmin): 8 | raw_id_fields = ('finding', 'rawfinding', 'scan',) 9 | 10 | 11 | admin.site.register(Event, EventAdmin) 12 | admin.site.register(Alert) 13 | admin.site.register(AlertOverride) 14 | -------------------------------------------------------------------------------- /events/apis.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """REST-API definitions for Events and Alerts.""" 3 | 4 | from django.http import JsonResponse 5 | from django.shortcuts import get_object_or_404 6 | from django.forms.models import model_to_dict 7 | # from common.utils.pagination import StandardResultsSetPagination 8 | from rest_framework.decorators import api_view 9 | # from rest_framework import viewsets 10 | # from django_filters import rest_framework as filters 11 | from .models import Event, Alert 12 | # from .serializers import AlertSerializer 13 | 14 | 15 | @api_view(['GET']) 16 | def list_events_api(request): 17 | """List last 100 events.""" 18 | events = [] 19 | for e in Event.objects.all().order_by('-id')[:100]: 20 | events.append(model_to_dict(e)) 21 | 22 | return JsonResponse(events, safe=False) 23 | 24 | 25 | @api_view(['DELETE']) 26 | def delete_event_api(request, event_id): 27 | """Delete an event.""" 28 | event = get_object_or_404(Event, id=event_id) 29 | event.delete() 30 | 31 | return JsonResponse({ 32 | "status": "deleted", 33 | "message": "event '{}' deleted.".format(event_id) 34 | }) 35 | 36 | 37 | @api_view(['POST']) 38 | def ack_alerts_api(request): 39 | """Acknowledge an alert.""" 40 | alerts = request.data 41 | for alert_id in alerts: 42 | a = Alert.objects.filter(id=alert_id).first() 43 | if a is not None: 44 | a.status = "read" 45 | a.save() 46 | 47 | return JsonResponse({'status': 'success'}) 48 | 49 | 50 | @api_view(['POST']) 51 | def archive_alerts_api(request): 52 | """Archive an alert.""" 53 | alerts = request.data 54 | for alert_id in alerts: 55 | a = Alert.objects.filter(id=alert_id).first() 56 | if a is not None: 57 | a.status = "archived" 58 | a.save() 59 | 60 | return JsonResponse({'status': 'success'}) 61 | -------------------------------------------------------------------------------- /events/apps.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | from django.apps import AppConfig 4 | 5 | 6 | class EventsConfig(AppConfig): 7 | name = 'events' 8 | -------------------------------------------------------------------------------- /events/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2019-09-17 23:07 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | import django.utils.timezone 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ('findings', '0001_initial'), 14 | ('scans', '0001_initial'), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='Event', 20 | fields=[ 21 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 22 | ('message', models.CharField(max_length=250)), 23 | ('description', models.TextField(default='n/a')), 24 | ('type', models.CharField(choices=[('CREATE', 'CREATE'), ('UPDATE', 'UPDATE'), ('DELETE', 'DELETE'), ('ERROR', 'ERROR'), ('NOTIFICATION', 'NOTIFICATION'), ('ALERT', 'ALERT'), ('UNSPECIFIED', 'UNSPECIFIED')], default='UNSPECIFIED', max_length=15)), 25 | ('severity', models.CharField(choices=[('INFO', 'INFO'), ('WARNING', 'WARNING'), ('ERROR', 'ERROR'), ('DEBUG', 'DEBUG')], default='INFO', max_length=10)), 26 | ('code', models.CharField(blank=True, max_length=10, null=True)), 27 | ('created_at', models.DateTimeField(default=django.utils.timezone.now)), 28 | ('updated_at', models.DateTimeField(default=django.utils.timezone.now)), 29 | ('finding', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='findings.Finding')), 30 | ('rawfinding', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='findings.RawFinding')), 31 | ('scan', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='scans.Scan')), 32 | ], 33 | options={ 34 | 'db_table': 'events', 35 | }, 36 | ), 37 | ] 38 | -------------------------------------------------------------------------------- /events/migrations/0002_alert.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.12 on 2020-05-29 14:43 2 | 3 | from django.db import migrations, models 4 | import django.utils.timezone 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('events', '0001_initial'), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Alert', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('message', models.CharField(max_length=250)), 19 | ('severity', models.CharField(choices=[('info', 'info'), ('low', 'low'), ('medium', 'medium'), ('high', 'high'), ('critical', 'critical')], default='info', max_length=10)), 20 | ('status', models.CharField(choices=[('new', 'New'), ('read', 'Read'), ('archived', 'Archived')], default='new', max_length=10)), 21 | ('created_at', models.DateTimeField(default=django.utils.timezone.now)), 22 | ('updated_at', models.DateTimeField(default=django.utils.timezone.now)), 23 | ], 24 | options={ 25 | 'db_table': 'alerts', 26 | }, 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /events/migrations/0003_alert_metadata.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.12 on 2020-06-02 09:29 2 | 3 | import django.contrib.postgres.fields.jsonb 4 | from django.db import migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('events', '0002_alert'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='alert', 16 | name='metadata', 17 | field=django.contrib.postgres.fields.jsonb.JSONField(default=dict), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /events/migrations/0004_auto_20200602_1813.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.12 on 2020-06-02 16:13 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('events', '0003_alert_metadata'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='event', 16 | name='finding', 17 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='event_finding', to='findings.Finding'), 18 | ), 19 | migrations.AlterField( 20 | model_name='event', 21 | name='rawfinding', 22 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='event_rawfinding', to='findings.RawFinding'), 23 | ), 24 | migrations.AlterField( 25 | model_name='event', 26 | name='scan', 27 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='event_scan', to='scans.Scan'), 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /events/migrations/0005_auto_20200616_1631.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.13 on 2020-06-16 14:31 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('events', '0004_auto_20200602_1813'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='event', 16 | name='finding', 17 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='findings.Finding'), 18 | ), 19 | migrations.AlterField( 20 | model_name='event', 21 | name='rawfinding', 22 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='findings.RawFinding'), 23 | ), 24 | migrations.AlterField( 25 | model_name='event', 26 | name='scan', 27 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='scans.Scan'), 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /events/migrations/0006_auto_20200624_0923.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.13 on 2020-06-24 07:23 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('users', '0004_remove_profile_role'), 12 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 13 | ('events', '0005_auto_20200616_1631'), 14 | ] 15 | 16 | operations = [ 17 | migrations.AddField( 18 | model_name='alert', 19 | name='owner', 20 | field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL), 21 | ), 22 | migrations.AddField( 23 | model_name='alert', 24 | name='teams', 25 | field=models.ManyToManyField(blank=True, to='users.Team'), 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /events/migrations/0007_auditlog.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.13 on 2020-07-20 16:29 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | import django.utils.timezone 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 13 | ('events', '0006_auto_20200624_0923'), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='AuditLog', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('message', models.TextField(default='n/a')), 22 | ('scope', models.CharField(choices=[('asset', 'Asset'), ('scan', 'Scan'), ('finding', 'Finding'), ('user', 'User'), ('other', 'Other')], default='other', max_length=10)), 23 | ('created_at', models.DateTimeField(default=django.utils.timezone.now)), 24 | ('owner', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), 25 | ], 26 | options={ 27 | 'db_table': 'audit_logs', 28 | }, 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /events/migrations/0008_auditlog_owner_username.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.13 on 2020-07-20 16:31 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('events', '0007_auditlog'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='auditlog', 15 | name='owner_username', 16 | field=models.TextField(default='n/a'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /events/migrations/0009_auditlog_type.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.14 on 2020-07-21 09:32 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('events', '0008_auditlog_owner_username'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='auditlog', 15 | name='type', 16 | field=models.CharField(default='n-a', max_length=250), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /events/migrations/0010_auditlog_metadata.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.14 on 2020-07-21 15:42 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('events', '0009_auditlog_type'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='auditlog', 15 | name='metadata', 16 | field=models.TextField(default='n/a'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /events/migrations/0011_auto_20200723_1528.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.14 on 2020-07-23 13:28 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('events', '0010_auditlog_metadata'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='auditlog', 15 | name='metadata', 16 | field=models.TextField(default=''), 17 | ), 18 | migrations.AlterField( 19 | model_name='auditlog', 20 | name='scope', 21 | field=models.CharField(choices=[('asset', 'Asset'), ('scan', 'Scan'), ('engine', 'Engine'), ('finding', 'Finding'), ('user', 'User'), ('rule', 'Rule'), ('setting', 'Setting'), ('other', 'Other')], default='other', max_length=10), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /events/migrations/0012_alert_type.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.24 on 2021-11-18 09:55 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('events', '0011_auto_20200723_1528'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='alert', 15 | name='type', 16 | field=models.CharField(choices=[('other', 'Other'), ('new_finding', 'New Finding'), ('missing_finding', 'Missing Finding'), ('reopened_finding', 'Reopened Finding')], default='other', max_length=20), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /events/migrations/0013_alertoverride.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.25 on 2021-12-17 08:54 2 | 3 | import django.contrib.postgres.fields.jsonb 4 | from django.db import migrations, models 5 | import django.utils.timezone 6 | import events.models 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('events', '0012_alert_type'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='AlertOverride', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('name', models.CharField(max_length=256)), 21 | ('enabled', models.BooleanField(default=True)), 22 | ('engine_type', models.CharField(max_length=20)), 23 | ('action', models.CharField(choices=[('disable-alert', 'Disable alert'), ('set-severity', 'Set custom severity')], default='disable-alert', max_length=32)), 24 | ('params', django.contrib.postgres.fields.jsonb.JSONField(default=events.models.get_default_alert_override_params)), 25 | ('created_at', models.DateTimeField(default=django.utils.timezone.now)), 26 | ('updated_at', models.DateTimeField(default=django.utils.timezone.now)), 27 | ], 28 | options={ 29 | 'db_table': 'alert_override', 30 | }, 31 | ), 32 | ] 33 | -------------------------------------------------------------------------------- /events/migrations/0014_alertoverride_teams.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.25 on 2021-12-17 08:55 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('users', '0005_profile_is_delegated'), 10 | ('events', '0013_alertoverride'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='alertoverride', 16 | name='teams', 17 | field=models.ManyToManyField(blank=True, to='users.Team'), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /events/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Patrowl/PatrowlManager/d5f0a23cf8eebd7a393dfa2b73e4274f34891dce/events/migrations/__init__.py -------------------------------------------------------------------------------- /events/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers, generics 2 | from .models import Event, Alert 3 | 4 | 5 | class EventSerializer(serializers.ModelSerializer): 6 | class Meta: 7 | model = Event 8 | # fields = '__all__' 9 | fields = [ 10 | 'id', 11 | 'message', 12 | 'description', 13 | 'type', 14 | 'severity', 15 | 'code', 16 | 'created_at', 17 | 'updated_at', 18 | 'scan_id', 19 | 'finding_id', 20 | 'rawfinding_id' 21 | ] 22 | 23 | 24 | class EventListCreate(generics.ListCreateAPIView): 25 | queryset = Event.objects.all() 26 | serializer_class = EventSerializer 27 | 28 | 29 | class AlertSerializer(serializers.ModelSerializer): 30 | class Meta: 31 | model = Alert 32 | # fields = '__all__' 33 | fields = [ 34 | 'id', 35 | 'message', 36 | 'severity', 37 | 'status', 38 | 'metadata', 39 | 'created_at', 40 | 'updated_at', 41 | ] 42 | 43 | 44 | class AlertListCreate(generics.ListCreateAPIView): 45 | # queryset = Alert.objects.all() 46 | serializer_class = AlertSerializer 47 | 48 | def get_queryset(self): 49 | return Alert.objects.for_user(self.request.user).all().order_by('-updated_at') 50 | -------------------------------------------------------------------------------- /events/templates/delete-event.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 | 4 |
    5 |
    6 | 7 | Do you confirm the deletion? 8 | {% csrf_token %} 9 |

    Event ID: {{ event.id }}

    10 |

    Message: {{ event.message }}

    11 |

    Timestamp: {{ event.created_at }}

    12 |
    13 | 14 | 15 |
    16 |
    17 | 18 | {% if messages %} 19 | 24 | {% endif %} 25 |
    26 | 27 | {% endblock %} 28 | -------------------------------------------------------------------------------- /events/tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.test import TestCase 4 | 5 | # Create your tests here. 6 | -------------------------------------------------------------------------------- /events/urls.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.urls import path 4 | from django.conf.urls import url 5 | from . import views, apis, serializers 6 | 7 | 8 | urlpatterns = [ 9 | # API Views 10 | # ex: /events/api/v1/list 11 | url(r'^api/v1/list$', 12 | apis.list_events_api, name='list_events_api'), 13 | # ex: /events/api/v1/delete/2 14 | url(r'^api/v1/delete/(?P[0-9]+)$', 15 | apis.delete_event_api, name='delete_event_api'), 16 | url(r'^api/v1/alerts/ack$', apis.ack_alerts_api, name='ack_alerts_api'), 17 | url(r'^api/v1/alerts/archive$', apis.archive_alerts_api, name='archive_alerts_api'), 18 | 19 | # WEB Views 20 | # ex: /events/delete [POST] 21 | url(r'^delete/(?P[0-9]+)$', 22 | views.delete_event_view, name='delete_event_view'), 23 | ] 24 | 25 | # Serialized data 26 | urlpatterns += [ 27 | path('alerts/list', views.list_alerts_view, name='list_alerts_view'), 28 | path('api/list', serializers.EventListCreate.as_view()), 29 | path('api/alerts/list', serializers.AlertListCreate.as_view()), 30 | ] 31 | -------------------------------------------------------------------------------- /findings/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Patrowl/PatrowlManager/d5f0a23cf8eebd7a393dfa2b73e4274f34891dce/findings/__init__.py -------------------------------------------------------------------------------- /findings/admin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.contrib import admin 4 | from .models import Finding, RawFinding, FindingOverride 5 | 6 | 7 | class FindingAdmin(admin.ModelAdmin): 8 | raw_id_fields = ('raw_finding', 'asset', 'scan',) 9 | 10 | 11 | class RawFindingAdmin(admin.ModelAdmin): 12 | raw_id_fields = ('asset', 'scan',) 13 | 14 | 15 | admin.site.register(Finding, FindingAdmin) 16 | admin.site.register(RawFinding, RawFindingAdmin) 17 | admin.site.register(FindingOverride) 18 | -------------------------------------------------------------------------------- /findings/apps.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals 4 | 5 | from django.apps import AppConfig 6 | 7 | 8 | class FindingsConfig(AppConfig): 9 | name = 'findings' 10 | -------------------------------------------------------------------------------- /findings/forms.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | from django import forms 5 | from django.core.exceptions import ValidationError 6 | from .models import Finding, FINDING_SEVERITIES 7 | 8 | ENGINE_TYPES = ( 9 | ('json', 'json'), 10 | ('nessus', 'Nessus'), 11 | ) 12 | 13 | 14 | def validate_file_extension(value): 15 | ext = os.path.splitext(value.name)[1] 16 | valid_extensions = ['.xml', '.nessus', '.json'] 17 | if ext not in valid_extensions: 18 | raise ValidationError(u'File not supported!') 19 | 20 | 21 | class ImportFindingsForm(forms.Form): 22 | class Meta: 23 | fields = ['engine', 'min_level', 'file'] 24 | 25 | engine = forms.CharField(widget=forms.Select( 26 | attrs={'class': 'form-control form-control-sm'}, 27 | choices=ENGINE_TYPES)) 28 | min_level = forms.CharField(widget=forms.Select( 29 | attrs={'class': 'form-control form-control-sm'}, 30 | choices=FINDING_SEVERITIES), 31 | label='Minimum severity') 32 | file = forms.FileField(widget=forms.FileInput( 33 | attrs={'accept': '.json,.xml,.nessus'}), 34 | validators=[validate_file_extension] 35 | ) 36 | 37 | 38 | class FindingForm(forms.ModelForm): 39 | class Meta: 40 | model = Finding 41 | fields = [ 42 | 'title', 'type', 'severity', 'status', 'description', 'tags', 43 | 'solution', 'risk_info', 'vuln_refs', 'links', 'comments', 'asset' 44 | ] 45 | widgets = { 46 | 'description': forms.Textarea( 47 | attrs={'class': 'form-control form-control-sm'}), 48 | 'solution': forms.Textarea( 49 | attrs={'class': 'form-control form-control-sm'}), 50 | 'tags': forms.Textarea( 51 | attrs={'class': 'form-control form-control-sm'}), 52 | 'risk_info': forms.Textarea( 53 | attrs={'class': 'form-control form-control-sm'}), 54 | 'title': forms.TextInput( 55 | attrs={'class': 'form-control form-control-sm'}), 56 | 'vuln_refs': forms.Textarea( 57 | attrs={'class': 'form-control form-control-sm'}), 58 | 'links': forms.Textarea( 59 | attrs={'class': 'form-control form-control-sm'}), 60 | 'type': forms.TextInput( 61 | attrs={'class': 'form-control form-control-sm'}), 62 | 'severity': forms.Select( 63 | attrs={'class': 'form-control form-control-sm'}), 64 | 'comments': forms.Textarea( 65 | attrs={'class': 'form-control form-control-sm'}), 66 | 'status': forms.Select( 67 | attrs={'class': 'form-control form-control-sm'}), 68 | 'asset': forms.Select( 69 | attrs={'class': 'form-control form-control-sm'}) 70 | } 71 | 72 | #tags = forms.CharField(widget=forms.TextInput(attrs={"data-role": "tagsinput"})) 73 | -------------------------------------------------------------------------------- /findings/migrations/0002_auto_20200306_0822.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.11 on 2020-03-06 07:22 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('findings', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='finding', 15 | name='scopes', 16 | field=models.ManyToManyField(blank=True, related_name='finding_scopes', to='engines.EnginePolicyScope'), 17 | ), 18 | migrations.AlterField( 19 | model_name='rawfinding', 20 | name='hash', 21 | field=models.CharField(default='', max_length=256), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /findings/migrations/0003_auto_20200602_2323.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.12 on 2020-06-02 21:23 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('findings', '0002_auto_20200306_0822'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='finding', 15 | name='score', 16 | field=models.IntegerField(blank=True, default=0, null=True), 17 | ), 18 | migrations.AddField( 19 | model_name='rawfinding', 20 | name='score', 21 | field=models.IntegerField(blank=True, default=0, null=True), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /findings/migrations/0004_auto_20200709_0024.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.13 on 2020-07-08 22:24 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('findings', '0003_auto_20200602_2323'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='finding', 15 | name='status', 16 | field=models.CharField(choices=[('new', 'New'), ('ack', 'Acknowledged'), ('mitigated', 'Mitigated'), ('confirmed', 'Confirmed'), ('patched', 'Patched'), ('closed', 'Closed'), ('false-positive', 'False-Positive'), ('undone', 'Undone')], default='new', max_length=16), 17 | ), 18 | migrations.AlterField( 19 | model_name='rawfinding', 20 | name='status', 21 | field=models.CharField(choices=[('new', 'New'), ('ack', 'Acknowledged'), ('mitigated', 'Mitigated'), ('confirmed', 'Confirmed'), ('patched', 'Patched'), ('closed', 'Closed'), ('false-positive', 'False-Positive'), ('undone', 'Undone')], max_length=16), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /findings/migrations/0005_auto_20200709_0910.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.13 on 2020-07-09 07:10 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('findings', '0004_auto_20200709_0024'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='finding', 17 | name='owner', 18 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to=settings.AUTH_USER_MODEL), 19 | ), 20 | migrations.AlterField( 21 | model_name='rawfinding', 22 | name='owner', 23 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to=settings.AUTH_USER_MODEL), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /findings/migrations/0006_auto_20200720_1829.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.13 on 2020-07-20 16:29 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('findings', '0005_auto_20200709_0910'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='finding', 17 | name='owner', 18 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL), 19 | ), 20 | migrations.AlterField( 21 | model_name='rawfinding', 22 | name='owner', 23 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /findings/migrations/0007_auto_20210113_1644.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.16 on 2021-01-13 15:44 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('findings', '0006_auto_20200720_1829'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='finding', 15 | name='status', 16 | field=models.CharField(choices=[('new', 'New'), ('ack', 'Acknowledged'), ('mitigated', 'Mitigated'), ('confirmed', 'Confirmed'), ('patched', 'Patched'), ('closed', 'Closed'), ('false-positive', 'False-Positive'), ('undone', 'Undone'), ('duplicate', 'Duplicate')], default='new', max_length=16), 17 | ), 18 | migrations.AlterField( 19 | model_name='rawfinding', 20 | name='status', 21 | field=models.CharField(choices=[('new', 'New'), ('ack', 'Acknowledged'), ('mitigated', 'Mitigated'), ('confirmed', 'Confirmed'), ('patched', 'Patched'), ('closed', 'Closed'), ('false-positive', 'False-Positive'), ('undone', 'Undone'), ('duplicate', 'Duplicate')], max_length=16), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /findings/migrations/0008_findingoverride.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.18 on 2021-05-04 11:19 2 | 3 | import django.contrib.postgres.fields.jsonb 4 | from django.db import migrations, models 5 | import django.utils.timezone 6 | import findings.models 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('findings', '0007_auto_20210113_1644'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='FindingOverride', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('name', models.CharField(max_length=256)), 21 | ('enabled', models.BooleanField(default=True)), 22 | ('engine_type', models.CharField(max_length=20)), 23 | ('action', models.CharField(choices=[('set-status', 'Set custom status'), ('set-severity', 'Set custom severity')], default='set-status', max_length=16)), 24 | ('params', django.contrib.postgres.fields.jsonb.JSONField(default=findings.models.get_default_override_params)), 25 | ('created_at', models.DateTimeField(default=django.utils.timezone.now)), 26 | ('updated_at', models.DateTimeField(default=django.utils.timezone.now)), 27 | ], 28 | options={ 29 | 'db_table': 'finding_override', 30 | }, 31 | ), 32 | ] 33 | -------------------------------------------------------------------------------- /findings/migrations/0009_auto_20210504_1320.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.18 on 2021-05-04 11:20 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('findings', '0008_findingoverride'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='findingoverride', 15 | name='action', 16 | field=models.CharField(choices=[('set-status', 'Set custom status'), ('set-severity', 'Set custom severity')], default='set-status', max_length=32), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /findings/migrations/0010_auto_20211104_1152.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.24 on 2021-11-04 10:52 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('findings', '0009_auto_20210504_1320'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='finding', 15 | name='status', 16 | field=models.CharField(choices=[('new', 'New'), ('ack', 'Acknowledged'), ('confirmed', 'Confirmed'), ('mitigated', 'Mitigated'), ('patched', 'Patched'), ('closed', 'Closed'), ('false-positive', 'False-Positive'), ('undone', 'Undone'), ('duplicate', 'Duplicate'), ('reopened', 'Reopened')], default='new', max_length=16), 17 | ), 18 | migrations.AlterField( 19 | model_name='rawfinding', 20 | name='status', 21 | field=models.CharField(choices=[('new', 'New'), ('ack', 'Acknowledged'), ('confirmed', 'Confirmed'), ('mitigated', 'Mitigated'), ('patched', 'Patched'), ('closed', 'Closed'), ('false-positive', 'False-Positive'), ('undone', 'Undone'), ('duplicate', 'Duplicate'), ('reopened', 'Reopened')], max_length=16), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /findings/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Patrowl/PatrowlManager/d5f0a23cf8eebd7a393dfa2b73e4274f34891dce/findings/migrations/__init__.py -------------------------------------------------------------------------------- /findings/templates/add-finding.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 | 4 | 8 | 9 |
    10 |
    11 | Add a new finding 12 | {% csrf_token %} 13 | {% for field in form %} 14 | {% if field.errors %} 15 |
    16 | 17 |
    18 | {{ field }} 19 | 20 | {% for error in field.errors %}{{ error }}{% endfor %} 21 | 22 |
    23 |
    24 | {% elif not field.is_hidden %} 25 |
    26 | 27 |
    28 | {{ field }} 29 | {% if field.help_text %} 30 |

    {{ field.help_text }}

    31 | {% endif %} 32 |
    33 |
    34 | {% endif %} 35 | {% endfor %} 36 |
    37 |
    38 |
    39 | 40 | 41 |
    42 |
    43 |
    44 |
    45 | 46 | {% endblock %} 47 | -------------------------------------------------------------------------------- /findings/templates/delete-findings.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 | 4 |
    5 |
    6 | 7 | Do you confirm the deletion? 8 | {% csrf_token %} 9 |

    finding: {{ finding.id }}

    10 |

    title: {{ finding.title }}

    11 |

    asset: {{ finding.asset.value }}

    12 |
    13 | 14 | 15 |
    16 |
    17 | 18 | {% if messages %} 19 |
      20 | {% for message in messages %} 21 |
    • {{ message }}
    • 22 | {% endfor %} 23 |
    24 | {% endif %} 25 |
    26 | 27 | {% endblock %} 28 | -------------------------------------------------------------------------------- /findings/templates/edit-finding.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 | 4 | 8 | 9 | 21 | 22 |
    23 | {% if raw == True %} 24 |
    25 | {% else %} 26 | 27 | {% endif %} 28 | Edit finding 29 | {% csrf_token %} 30 | {% for field in form %} 31 | {% if field.errors %} 32 |
    33 | 34 |
    35 | {{ field }} 36 | 37 | {% for error in field.errors %}{{ error }}{% endfor %} 38 | 39 |
    40 |
    41 | {% elif not field.is_hidden %} 42 |
    43 | 44 |
    45 | {{ field }} 46 | {% if field.help_text %} 47 |

    {{ field.help_text }}

    48 | {% endif %} 49 |
    50 |
    51 | {% endif %} 52 | {% endfor %} 53 |
    54 |
    55 | 56 |
    57 |
    58 | 59 |
    60 |
    61 |
    62 |
    63 | 64 | {% endblock %} 65 | -------------------------------------------------------------------------------- /findings/templates/import-findings.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 | 4 | 8 | 9 |
    10 |
    11 | Import findings 12 | {% csrf_token %} 13 | {% for field in form %} 14 | {% if field.errors %} 15 |
    16 | 17 |
    18 | {{ field }} 19 | 20 | {% for error in field.errors %}{{ error }}{% endfor %} 21 | 22 |
    23 |
    24 | {% elif not field.is_hidden %} 25 |
    26 | 27 |
    28 | {{ field }} 29 | {% if field.help_text %} 30 |

    {{ field.help_text }}

    31 | {% endif %} 32 |
    33 |
    34 | {% endif %} 35 | {% endfor %} 36 |
    37 |
    38 | 39 |
    40 |
    41 | 42 |
    43 |
    44 |
    45 |
    46 | 47 | {% endblock %} 48 | -------------------------------------------------------------------------------- /findings/tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.test import TestCase, Client 4 | 5 | 6 | class FindingTestCase(TestCase): 7 | # fixtures = ['tmp/db.json'] 8 | 9 | def add_finding_test(self): 10 | print("TEST CASE: add_finding_test") 11 | c = Client() 12 | 13 | print("TEST CASE: testing with GET method") 14 | r = c.get('http://127.0.0.1:8000/findings/add?title=testing') 15 | print(r.json()) 16 | 17 | 18 | print("TEST CASE: testing with POST method") 19 | r = c.post('http://127.0.0.1:8000/findings/add', { 20 | "asset_id": "1", 21 | "title": "Open port TCP/80", 22 | "type": "open_ports", 23 | "confidence": "certain", 24 | "severity": "info", 25 | "status": "new", 26 | "engine_type": "nmap" 27 | }) 28 | print(r.json()) 29 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S python3 -OO 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "app.settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError: 10 | # The above import may fail for some other reason. Ensure that the 11 | # issue is really that Django is missing to avoid masking other 12 | # exceptions on Python 2. 13 | try: 14 | import django 15 | except ImportError: 16 | raise ImportError( 17 | "Couldn't import Django. Are you sure it's installed and " 18 | "available on your PYTHONPATH environment variable? Did you " 19 | "forget to activate a virtual environment?" 20 | ) 21 | raise 22 | execute_from_command_line(sys.argv) 23 | -------------------------------------------------------------------------------- /media/.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 8080; 3 | autoindex on; 4 | server_name my.patrowl.io localhost; 5 | 6 | root .; 7 | 8 | # access_log var/log/nginx.patrowlmanager-access.log; 9 | # error_log var/log/nginx.patrowlmanager-error.log; 10 | 11 | location / { 12 | proxy_pass http://127.0.0.1:8000; 13 | 14 | proxy_set_header X-Real-IP $remote_addr; 15 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 16 | proxy_set_header X-Forwarded-Host $host; 17 | 18 | proxy_set_header Proxy ""; 19 | } 20 | 21 | location /static { 22 | alias ./staticfiles; 23 | } 24 | # location /media { 25 | # alias ./media; 26 | # } 27 | } 28 | -------------------------------------------------------------------------------- /nginx_docker.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 8083; 3 | autoindex off; 4 | server_name localhost; 5 | 6 | access_log /var/log/nginx.patrowlmanager-access.log; 7 | error_log /var/log/nginx.patrowlmanager-error.log; 8 | 9 | # Protects against Clickjacking attacks. 10 | add_header X-Frame-Options "SAMEORIGIN"; 11 | 12 | # Protects against Clickjacking attacks. 13 | add_header Strict-Transport-Security "max-age=63072000; includeSubdomains"; 14 | 15 | # Protects against XSS injections. 16 | add_header X-XSS-Protection "1; mode=block"; 17 | 18 | # Protects against MIME-type confusion attack. 19 | add_header X-Content-Type-Options "nosniff"; 20 | 21 | # CSP modern XSS directive-based defence, used since 2014. 22 | add_header Content-Security-Policy "default-src 'self'; font-src *;img-src * data:; script-src *; style-src *;"; 23 | 24 | # Prevents from leaking referrer data over insecure connections. 25 | add_header Referrer-Policy 'strict-origin'; 26 | 27 | # Disable any limits to avoid HTTP 413 for large image uploads 28 | client_max_body_size 0; 29 | 30 | location / { 31 | proxy_pass http://web:8003; 32 | 33 | proxy_set_header X-Real-IP $remote_addr; 34 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 35 | proxy_set_header X-Forwarded-Host $host; 36 | 37 | proxy_set_header Proxy ""; 38 | 39 | add_header 'Cache-Control' 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0'; 40 | expires off; 41 | 42 | # Custom headers 43 | proxy_connect_timeout 75s; 44 | proxy_read_timeout 300s; 45 | 46 | proxy_redirect off; 47 | proxy_buffering off; 48 | proxy_buffer_size "4k"; 49 | } 50 | 51 | location /static { 52 | alias /opt/patrowl-manager/staticfiles; 53 | } 54 | # location /media { 55 | # alias /opt/patrowl-manager/media; 56 | # } 57 | } 58 | -------------------------------------------------------------------------------- /reportings/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Patrowl/PatrowlManager/d5f0a23cf8eebd7a393dfa2b73e4274f34891dce/reportings/__init__.py -------------------------------------------------------------------------------- /reportings/admin.py: -------------------------------------------------------------------------------- 1 | # from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /reportings/api.py: -------------------------------------------------------------------------------- 1 | from django.http import JsonResponse 2 | 3 | 4 | def global_stats_api(request): 5 | data = None 6 | return JsonResponse(data) 7 | -------------------------------------------------------------------------------- /reportings/apps.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.apps import AppConfig 4 | 5 | 6 | class ReportingsConfig(AppConfig): 7 | name = 'reportings' 8 | -------------------------------------------------------------------------------- /reportings/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Patrowl/PatrowlManager/d5f0a23cf8eebd7a393dfa2b73e4274f34891dce/reportings/migrations/__init__.py -------------------------------------------------------------------------------- /reportings/models.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | FIELD_TYPES_CRITERIAS = { 4 | "TEXT": [ 5 | {"criteria": "is", "criteria_kw": "exact"}, 6 | {"criteria": "contains", "criteria_kw": "icontains"}, 7 | {"criteria": "start with", "criteria_kw": "startswith"}, 8 | {"criteria": "end with", "criteria_kw": "endswith"}], 9 | "NUMBER": [ 10 | {"criteria": "is equal to", "criteria_kw": "exact"}, 11 | {"criteria": "is greater than", "criteria_kw": "gt"}, 12 | {"criteria": "is lower than", "criteria_kw": "lt"}], 13 | "DATETIME": [ 14 | {"criteria": "is equal to", "criteria_kw": "exact"}], 15 | } 16 | -------------------------------------------------------------------------------- /reportings/templates/patch-management-dashboard.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load patrowl_tags %} 3 | {% block content %} 4 | 5 | 9 |
    10 | {{data}} 11 |
    12 | 13 |
    14 | 15 | {% if messages %} 16 |
      17 | {% for message in messages %} 18 |
    • {{ message }}
    • 19 | {% endfor %} 20 |
    21 | {% endif %} 22 |
    23 | 24 | 75 | 76 | {% endblock %} 77 | -------------------------------------------------------------------------------- /reportings/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | from . import views 3 | from . import api 4 | 5 | 6 | urlpatterns = [ 7 | # VIEWS 8 | # ex: /reportings/patch_management 9 | url(r'^patch_management$', views.patch_management_view, name='patch_management_view'), 10 | # ex: /reportings/dashboard 11 | url(r'^dashboard$', views.homepage_dashboard_view, name='homepage_dashboard_view'), 12 | 13 | ## API 14 | url(r'^api/v1/global_stats$', api.global_stats_api, name='global_stats_api'), 15 | ] 16 | -------------------------------------------------------------------------------- /requirements.macos.txt: -------------------------------------------------------------------------------- 1 | amqp==2.5.2 2 | Babel==2.9.1 3 | backports-abc==0.5 4 | billiard==3.6.1.0 5 | celery==4.4.0 6 | certifi==2017.4.17 7 | chardet==3.0.4 8 | coreapi==2.3.3 9 | coreschema==0.0.4 10 | Django==2.2.26 11 | #django-celery-beat==1.1.1 12 | django-celery-beat==2.0.0 13 | django-celery-results==2.0.1 14 | django-cursor-pagination==0.1.4 15 | django-datetime-widget==0.9.3 16 | django-debug-toolbar==3.2.2 17 | django-extensions==2.1.9 18 | django-rest-swagger==2.2.0 19 | django-timezone-field==4.0 20 | djangorestframework>=3.11.2 21 | djangorestframework-datatables==0.6.0 22 | flower==0.9.2 23 | future==0.16.0 24 | futures==3.1.1 25 | gunicorn==20.0.4 26 | idna==2.5 27 | importlib-metadata==0.23 28 | itypes==1.1.0 29 | Jinja2==2.11.3 30 | jsonfield==2.0.2 31 | kombu==4.6.7 32 | libmagic==1.0 33 | MarkupSafe==1.1.0 34 | meld3==1.0.2 35 | more-itertools==7.2.0 36 | netaddr==0.7.19 37 | openapi-codec==1.3.2 38 | #psycopg2-binary==2.8.6 39 | psycopg2-binary==2.7.5 40 | python-crontab==2.3.6 41 | python-dateutil==2.8.0 42 | python-magic==0.4.15 43 | python-magic-bin==0.4.14 44 | python-memcached==1.59 45 | pytz==2019.2 46 | requests==2.25.1 47 | simplejson==3.16.0 48 | singledispatch==3.4.0.3 49 | six==1.10.0 50 | sqlparse==0.4.2 51 | supervisor==4.2.1 52 | thehive4py==1.8.1 53 | tornado==5.1 54 | tzlocal==1.5.1 55 | uritemplate==3.0.0 56 | urllib3==1.26.5 57 | vine==1.3.0 58 | Werkzeug==2.0.1 59 | zipp==0.6.0 60 | django-filter==2.4.0 61 | django-cors-headers==3.2.0 62 | djangorestframework-simplejwt==4.4.0 63 | django-health-check==3.12.1 64 | psutil==5.7.0 65 | django-organizations==1.1.2 66 | django-annoying==0.10.6 67 | django-reset-migrations 68 | django-dbconn-retry 69 | cpe==1.2.1 70 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | amqp==2.5.2 2 | Babel==2.9.1 3 | backports-abc==0.5 4 | billiard==3.6.1.0 5 | celery==4.4.0 6 | certifi==2017.4.17 7 | chardet==3.0.4 8 | coreapi==2.3.3 9 | coreschema==0.0.4 10 | Django==2.2.26 11 | #django-celery-beat==1.1.1 12 | django-celery-beat==2.0.0 13 | django-celery-results==2.0.1 14 | django-cursor-pagination==0.1.4 15 | django-datetime-widget==0.9.3 16 | django-debug-toolbar==3.2.2 17 | django-extensions==2.1.9 18 | django-rest-swagger==2.2.0 19 | django-timezone-field==4.0 20 | djangorestframework>=3.11.2 21 | djangorestframework-datatables==0.6.0 22 | flower==0.9.2 23 | future==0.16.0 24 | futures==3.1.1 25 | gunicorn==20.0.4 26 | idna==2.5 27 | importlib-metadata==0.23 28 | itypes==1.1.0 29 | Jinja2==2.11.3 30 | jsonfield==2.0.2 31 | kombu==4.6.7 32 | libmagic==1.0 33 | MarkupSafe==1.1.0 34 | meld3==1.0.2 35 | more-itertools==7.2.0 36 | netaddr==0.7.19 37 | openapi-codec==1.3.2 38 | #psycopg2-binary==2.8.6 39 | psycopg2-binary==2.7.5 40 | python-crontab==2.3.6 41 | python-dateutil==2.8.0 42 | python-magic==0.4.15 43 | #python-magic-bin==0.4.14 44 | python-memcached==1.59 45 | pytz==2019.2 46 | requests==2.25.1 47 | simplejson==3.16.0 48 | singledispatch==3.4.0.3 49 | six==1.10.0 50 | sqlparse==0.4.2 51 | supervisor==4.2.1 52 | thehive4py==1.8.1 53 | tornado==5.1 54 | tzlocal==1.5.1 55 | uritemplate==3.0.0 56 | urllib3==1.26.5 57 | vine==1.3.0 58 | Werkzeug==2.0.1 59 | zipp==0.6.0 60 | django-filter==2.4.0 61 | django-cors-headers==3.2.0 62 | djangorestframework-simplejwt==4.4.0 63 | django-health-check==3.12.1 64 | psutil==5.7.0 65 | django-organizations==1.1.2 66 | django-annoying==0.10.6 67 | django-reset-migrations 68 | django-dbconn-retry 69 | cpe==1.2.1 70 | -------------------------------------------------------------------------------- /rules/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /rules/admin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.contrib import admin 4 | from .models import Rule 5 | 6 | admin.site.register(Rule) 7 | -------------------------------------------------------------------------------- /rules/apps.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | from django.apps import AppConfig 4 | 5 | 6 | class RulesConfig(AppConfig): 7 | name = 'rules' 8 | -------------------------------------------------------------------------------- /rules/forms.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /rules/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2019-09-17 23:07 2 | 3 | from django.conf import settings 4 | import django.contrib.postgres.fields.jsonb 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | import django.utils.timezone 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | initial = True 13 | 14 | dependencies = [ 15 | ('django_celery_beat', '0001_initial'), 16 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 17 | ] 18 | 19 | operations = [ 20 | migrations.CreateModel( 21 | name='Rule', 22 | fields=[ 23 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 24 | ('title', models.CharField(max_length=256)), 25 | ('comments', models.CharField(default='n/a', max_length=256)), 26 | ('scope', models.CharField(choices=[('asset', 'Asset'), ('finding', 'Finding'), ('scan', 'Scan')], default='finding', max_length=10)), 27 | ('scope_attr', models.CharField(blank=True, max_length=20, null=True)), 28 | ('condition', django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True)), 29 | ('target', models.CharField(choices=[('event', 'Patrowl event'), ('logfile', 'To logfile'), ('email', 'Send email'), ('thehive', 'To TheHive (event'), ('splunk', 'To Splunk'), ('slack', 'To Slack')], default='event', max_length=10)), 30 | ('severity', models.CharField(choices=[('Low', 'Low'), ('Medium', 'Medium'), ('High', 'High')], default='Low', max_length=10)), 31 | ('trigger', models.CharField(choices=[('ondemand', 'On-demand'), ('auto', 'Auto'), ('periodic', 'Periodic')], default='auto', max_length=10)), 32 | ('trigger_attr', models.CharField(blank=True, max_length=20, null=True)), 33 | ('summary', django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True)), 34 | ('enabled', models.BooleanField(default=False)), 35 | ('nb_matches', models.IntegerField(default=0)), 36 | ('created_at', models.DateTimeField(default=django.utils.timezone.now)), 37 | ('updated_at', models.DateTimeField(default=django.utils.timezone.now)), 38 | ('owner', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to=settings.AUTH_USER_MODEL)), 39 | ('periodic_task', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='django_celery_beat.PeriodicTask')), 40 | ], 41 | options={ 42 | 'db_table': 'rules', 43 | }, 44 | ), 45 | ] 46 | -------------------------------------------------------------------------------- /rules/migrations/0002_auto_20200720_1829.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.13 on 2020-07-20 16:29 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('rules', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='rule', 17 | name='owner', 18 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL), 19 | ), 20 | migrations.AlterField( 21 | model_name='rule', 22 | name='target', 23 | field=models.CharField(choices=[('email', 'Send email'), ('thehive', 'TheHive Event'), ('slack', 'Slack')], default='event', max_length=10), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /rules/migrations/0003_auto_20210113_1644.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.16 on 2021-01-13 15:44 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('rules', '0002_auto_20200720_1829'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='rule', 15 | name='scope', 16 | field=models.CharField(choices=[('asset', 'Asset'), ('finding', 'Finding'), ('scan', 'Scan'), ('alert', 'Alert')], default='finding', max_length=10), 17 | ), 18 | migrations.AlterField( 19 | model_name='rule', 20 | name='severity', 21 | field=models.CharField(choices=[('Info', 'Info'), ('Low', 'Low'), ('Medium', 'Medium'), ('High', 'High'), ('Critical', 'Critical')], default='Low', max_length=10), 22 | ), 23 | migrations.AlterField( 24 | model_name='rule', 25 | name='target', 26 | field=models.CharField(choices=[('email', 'Send email'), ('thehive', 'TheHive Event'), ('slack', 'Slack'), ('alert', 'Alert')], default='event', max_length=10), 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /rules/migrations/0004_auto_20211104_1152.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.24 on 2021-11-04 10:52 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('rules', '0003_auto_20210113_1644'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='rule', 15 | name='scope', 16 | field=models.CharField(choices=[('asset', 'Asset'), ('finding', 'Finding'), ('scan', 'Scan')], default='finding', max_length=10), 17 | ), 18 | migrations.AlterField( 19 | model_name='rule', 20 | name='target', 21 | field=models.CharField(choices=[('email', 'Send email'), ('thehive', 'TheHive event'), ('slack', 'Slack notification'), ('alert', 'Patrowl alert')], default='event', max_length=10), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /rules/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Patrowl/PatrowlManager/d5f0a23cf8eebd7a393dfa2b73e4274f34891dce/rules/migrations/__init__.py -------------------------------------------------------------------------------- /rules/tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.test import TestCase 4 | 5 | # Create your tests here. 6 | -------------------------------------------------------------------------------- /rules/urls.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """URL routes for alerting rules.""" 4 | 5 | from django.conf.urls import url 6 | from . import views, apis 7 | 8 | 9 | urlpatterns = [ 10 | # API Views 11 | # ex: /rules/api/v1/alerting/list 12 | url(r'^api/v1/alerting/list$', 13 | apis.list_alerting_rules_api, name='list_alerting_rules_api'), 14 | # ex: /rules/api/v1/alerting/1 15 | url(r'^api/v1/alerting/by-id/(?P[0-9]+)$', 16 | apis.get_alerting_rule_api, name='get_alerting_rule_api'), 17 | # ex: /rules/api/v1/delete 18 | url(r'^api/v1/delete$', 19 | apis.delete_rules_api, name='delete_rules_api'), 20 | # ex: /rules/api/v1/delete/1 21 | url(r'^api/v1/delete/(?P[0-9]+)$', 22 | apis.delete_rule_api, name='delete_rule_api'), 23 | # ex: /rules/api/v1/add 24 | url(r'^api/v1/add$', 25 | apis.add_rule_api, name='add_rule_api'), 26 | # ex: /rules/api/v1/duplicate/3 27 | url(r'^api/v1/alerting/duplicate/(?P[0-9]+)$', 28 | apis.duplicate_rule_api, name='duplicate_rule_api'), 29 | # ex: /rules/api/v1/change_status/3 30 | url(r'^api/v1/change_status/(?P[0-9]+)$', 31 | apis.toggle_rule_status_api, name='toggle_rule_status_api'), 32 | # ex: /rules/api/v1/send/slack 33 | # url(r'^api/v1/send/slack$', 34 | # apis.send_slack_message_api, name='send_slack_message_api'), 35 | 36 | # WEB Views 37 | # ex: /rules/list 38 | url(r'^list$', 39 | views.list_rules_view, name='list_rules_view'), 40 | 41 | ] 42 | -------------------------------------------------------------------------------- /scans/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Patrowl/PatrowlManager/d5f0a23cf8eebd7a393dfa2b73e4274f34891dce/scans/__init__.py -------------------------------------------------------------------------------- /scans/admin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.contrib import admin 4 | from .models import Scan, ScanCampaign, ScanDefinition 5 | 6 | 7 | class ScanAssetInline(admin.TabularInline): 8 | model = Scan.assets.through 9 | 10 | 11 | class ScanAdmin(admin.ModelAdmin): 12 | raw_id_fields = ('scan_definition',) 13 | exclude = ('assets',) 14 | inlines = [ 15 | ScanAssetInline, 16 | ] 17 | 18 | 19 | class ScanDefAssetInline(admin.TabularInline): 20 | model = ScanDefinition.assets_list.through 21 | 22 | 23 | class ScanDefAssetGroupInline(admin.TabularInline): 24 | model = ScanDefinition.assetgroups_list.through 25 | 26 | 27 | class ScanDefDynAssetGroupInline(admin.TabularInline): 28 | model = ScanDefinition.dynassetgroups_list.through 29 | 30 | 31 | class ScanDefAdmin(admin.ModelAdmin): 32 | exclude = ('assets_list', 'assetgroups_list', 'dynassetgroups_list',) 33 | inlines = [ 34 | ScanDefAssetInline, ScanDefAssetGroupInline, ScanDefDynAssetGroupInline 35 | ] 36 | 37 | 38 | admin.site.register(Scan, ScanAdmin) 39 | admin.site.register(ScanCampaign) 40 | admin.site.register(ScanDefinition, ScanDefAdmin) 41 | -------------------------------------------------------------------------------- /scans/apps.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals 4 | 5 | from django.apps import AppConfig 6 | 7 | 8 | class ScansConfig(AppConfig): 9 | name = 'scans' 10 | -------------------------------------------------------------------------------- /scans/migrations/0002_scandefinition_teams.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.13 on 2020-06-23 14:54 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('users', '0002_auto_20200616_1704'), 10 | ('scans', '0001_initial'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='scandefinition', 16 | name='teams', 17 | field=models.ManyToManyField(blank=True, to='users.Team'), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /scans/migrations/0003_scanjob.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.16 on 2020-12-02 13:35 2 | 3 | import django.contrib.postgres.fields.jsonb 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | import django.utils.timezone 7 | import uuid 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | dependencies = [ 13 | ('assets', '0009_auto_20200921_1151'), 14 | ('engines', '0001_initial'), 15 | ('scans', '0002_scandefinition_teams'), 16 | ] 17 | 18 | operations = [ 19 | migrations.CreateModel( 20 | name='ScanJob', 21 | fields=[ 22 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 23 | ('uuid', models.UUIDField(default=uuid.uuid4, editable=False)), 24 | ('status', models.CharField(choices=[('created', 'Created'), ('enqueued', 'Enqueued'), ('started', 'Started'), ('finished', 'Finished'), ('error', 'Error'), ('stopped', 'Stopped')], default='created', max_length=20)), 25 | ('summary', django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True)), 26 | ('options', django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True)), 27 | ('started_at', models.DateTimeField(blank=True, null=True)), 28 | ('finished_at', models.DateTimeField(blank=True, null=True)), 29 | ('created_at', models.DateTimeField(default=django.utils.timezone.now)), 30 | ('updated_at', models.DateTimeField(default=django.utils.timezone.now)), 31 | ('assets', models.ManyToManyField(to='assets.Asset')), 32 | ('engine', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='engines.EngineInstance')), 33 | ('engine_policy', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='engines.EnginePolicy')), 34 | ('engine_type', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='engines.Engine')), 35 | ('scan', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='scans.Scan')), 36 | ], 37 | options={ 38 | 'db_table': 'scan_jobs', 39 | }, 40 | ), 41 | ] 42 | -------------------------------------------------------------------------------- /scans/migrations/0004_auto_20201202_1448.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.16 on 2020-12-02 13:48 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('scans', '0003_scanjob'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='scanjob', 15 | name='uuid', 16 | ), 17 | migrations.AddField( 18 | model_name='scanjob', 19 | name='task_id', 20 | field=models.UUIDField(blank=True, null=True), 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /scans/migrations/0005_auto_20201202_1909.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.16 on 2020-12-02 18:09 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('scans', '0004_auto_20201202_1448'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='scanjob', 15 | name='position', 16 | field=models.IntegerField(blank=True, default=1, null=True), 17 | ), 18 | migrations.AlterField( 19 | model_name='scanjob', 20 | name='status', 21 | field=models.CharField(choices=[('started', 'Started'), ('finished', 'Finished'), ('error', 'Error'), ('stopped', 'Stopped')], default='started', max_length=20), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /scans/migrations/0006_scandefinition_taggroups_list.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.18 on 2021-04-02 12:39 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('assets', '0010_auto_20210402_1439'), 10 | ('scans', '0005_auto_20201202_1909'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='scandefinition', 16 | name='taggroups_list', 17 | field=models.ManyToManyField(blank=True, to='assets.AssetCategory'), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /scans/migrations/0007_auto_20220114_1547.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.25 on 2022-01-14 14:47 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('assets', '0012_dynamicassetgroup'), 10 | ('scans', '0006_scandefinition_taggroups_list'), 11 | ] 12 | 13 | operations = [ 14 | migrations.RemoveField( 15 | model_name='scandefinition', 16 | name='taggroups_list', 17 | ), 18 | migrations.AddField( 19 | model_name='scandefinition', 20 | name='dynassetgroups_list', 21 | field=models.ManyToManyField(blank=True, to='assets.DynamicAssetGroup'), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /scans/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Patrowl/PatrowlManager/d5f0a23cf8eebd7a393dfa2b73e4274f34891dce/scans/migrations/__init__.py -------------------------------------------------------------------------------- /scans/templates/add-scan-campaign.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 | {{ form.media }} 5 | 6 | 11 | 12 |
    13 |
    14 | Add a new scan campaign 15 | {% csrf_token %} 16 | {% for field in form %} 17 | {% if field.errors %} 18 |
    19 | 20 |
    21 | {{ field }} 22 | 23 | {% for error in field.errors %}{{ error }}{% endfor %} 24 | 25 |
    26 |
    27 | {% elif not field.is_hidden %} 28 |
    29 | 30 |
    31 | {{ field }} 32 | {% if field.help_text %} 33 |

    {{ field.help_text }}

    34 | {% endif %} 35 |
    36 |
    37 | {% endif %} 38 | {% endfor %} 39 |
    40 | 41 | 42 | 43 |
    44 |
    45 |
    46 | 47 | {% endblock %} 48 | -------------------------------------------------------------------------------- /scans/templates/delete-scan-campaign.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 | 4 |
    5 |
    6 | 7 | Do you confirm the deletion? 8 | {% csrf_token %} 9 |

    Scan Campaign ID: {{ scan_campaign.scan_campaign_id }}

    10 |

    Title: {{ scan_campaign.title }}

    11 |
    12 | 13 | 14 |
    15 |
    16 | 17 | {% if messages %} 18 |
      19 | {% for message in messages %} 20 |
    • {{ message }}
    • 21 | {% endfor %} 22 |
    23 | {% endif %} 24 |
    25 | 26 | {% endblock %} 27 | -------------------------------------------------------------------------------- /scans/templates/delete-scan-definition.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 | 4 |
    5 |
    6 | 7 | Do you confirm the deletion? 8 | {% csrf_token %} 9 |

    Scan Definition ID: {{ scan_def.scan_definition_id }}

    10 |

    Title: {{ scan_def.title }}

    11 |
    12 | 13 | 14 |
    15 |
    16 | 17 | {% if messages %} 18 |
      19 | {% for message in messages %} 20 |
    • {{ message }}
    • 21 | {% endfor %} 22 |
    23 | {% endif %} 24 |
    25 | 26 | {% endblock %} 27 | -------------------------------------------------------------------------------- /scans/templates/edit-scan-campaign.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 | 4 |
    5 |
    6 | Edit a scan definition 7 | {% csrf_token %} 8 | {% for field in form %} 9 | {% if field.errors %} 10 |
    11 | 12 |
    13 | {{ field }} 14 | 15 | {% for error in field.errors %}{{ error }}{% endfor %} 16 | 17 |
    18 |
    19 | {% elif not field.is_hidden %} 20 |
    21 | 22 |
    23 | {{ field }} 24 | {% if field.help_text %} 25 |

    {{ field.help_text }}

    26 | {% endif %} 27 |
    28 |
    29 | {% endif %} 30 | {% endfor %} 31 |
    32 | 33 | 34 |
    35 |
    36 | {% if messages %} 37 |
      38 | {% for message in messages %} 39 |
    • {{ message }}
    • 40 | {% endfor %} 41 |
    42 | {% endif %} 43 |
    44 | 45 | {% endblock %} 46 | -------------------------------------------------------------------------------- /scans/templates/email_send_report.html: -------------------------------------------------------------------------------- 1 | Hello, A new scan report is available.
    2 |

    Scan

    3 |
      4 |
    • Title: {{scan.id}}/{{scan.title}}
    • 5 |
    • Status: {{scan.status|title}}
    • 6 |
    • Started at: {{scan.started_at|date:"Y/m/d\-H:i:s"}}
    • 7 |
    • Finished at: {{scan.finished_at|date:"Y/m/d\-H:i:s"}}
    • 8 |
    9 |

    Findings

    10 |
      11 |
    • Critical: {{findings_stats.critical}}
    • 12 |
    • High: {{findings_stats.high}}
    • 13 |
    • Medium: {{findings_stats.medium}}
    • 14 |
    • Low: {{findings_stats.low}}
    • 15 |
    • Info: {{findings_stats.info}}
    • 16 |
    17 | 18 | -- PatrowlManager Notification Center 19 | See https://patrowl.io 20 |

    21 | -------------------------------------------------------------------------------- /scans/templates/email_send_report.txt: -------------------------------------------------------------------------------- 1 | PatrowlManager - Send Email report 2 | -------------------------------------------------------------------------------- /scans/tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.test import TestCase 4 | 5 | # Create your tests here. 6 | -------------------------------------------------------------------------------- /search/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Patrowl/PatrowlManager/d5f0a23cf8eebd7a393dfa2b73e4274f34891dce/search/__init__.py -------------------------------------------------------------------------------- /search/admin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # from django.contrib import admin 4 | 5 | # Register your models here. 6 | -------------------------------------------------------------------------------- /search/apis.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.http import JsonResponse 4 | from .views import _search 5 | from rest_framework.decorators import api_view 6 | 7 | 8 | @api_view(['GET']) 9 | def search_api(request): 10 | """REST-API: Search based on keywords.""" 11 | kw = request.GET.get('srch-term', None) 12 | 13 | if not kw: 14 | return JsonResponse([]) 15 | 16 | results = _search(kw) 17 | return JsonResponse({'results': results, 'search_term': kw}) 18 | -------------------------------------------------------------------------------- /search/apps.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals 4 | 5 | from django.apps import AppConfig 6 | 7 | 8 | class SearchConfig(AppConfig): 9 | name = 'search' 10 | -------------------------------------------------------------------------------- /search/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Patrowl/PatrowlManager/d5f0a23cf8eebd7a393dfa2b73e4274f34891dce/search/migrations/__init__.py -------------------------------------------------------------------------------- /search/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # from __future__ import unicode_literals 4 | # 5 | # from django.db import models 6 | 7 | # Create your models here. 8 | -------------------------------------------------------------------------------- /search/templates/search-results.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 | 4 |
    5 |

    Search results for '{{ search_term }}':

    6 | 7 | 8 | 9 | 10 | 11 | {% for result in results %} 12 | 13 | 14 | 15 | 16 | {% endfor %} 17 |
    TypeItem
    {{ result.type }}{{ result.value }}
    18 | 19 | 20 | {% if messages %} 21 |
      22 | {% for message in messages %} 23 |
    • {{ message }}
    • 24 | {% endfor %} 25 |
    26 | {% endif %} 27 |
    28 | 29 | {% endblock %} 30 | -------------------------------------------------------------------------------- /search/tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.test import TestCase 4 | 5 | # Create your tests here. 6 | -------------------------------------------------------------------------------- /search/urls.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.conf.urls import url 4 | from . import views, apis 5 | 6 | 7 | urlpatterns = [ 8 | ## WEB Views 9 | # ex: /search 10 | url(r'^$', views.search_view, name='search_view'), 11 | # ex: /search 12 | url(r'/api/v1/$', apis.search_api, name='search_api'), 13 | 14 | ] 15 | -------------------------------------------------------------------------------- /settings/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Patrowl/PatrowlManager/d5f0a23cf8eebd7a393dfa2b73e4274f34891dce/settings/__init__.py -------------------------------------------------------------------------------- /settings/admin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.contrib import admin 4 | from .models import Setting 5 | 6 | admin.site.register(Setting) 7 | -------------------------------------------------------------------------------- /settings/apis.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """View and API definitions for Settings.""" 3 | 4 | from django.shortcuts import get_object_or_404 5 | from django.contrib import messages 6 | from django.http import JsonResponse, HttpResponse 7 | from rest_framework.decorators import api_view 8 | from .models import Setting 9 | import csv 10 | 11 | 12 | @api_view(['POST']) 13 | def update_setting_api(request): 14 | """API: Update a setting value.""" 15 | setting_id = request.data["setting_id"] 16 | setting = get_object_or_404(Setting, id=setting_id) 17 | setting.value = request.data["setting_value"] 18 | setting.save(force_update=True) 19 | messages.success(request, 'Setting successfully updated!') 20 | 21 | return JsonResponse({'status': 'success'}) 22 | 23 | 24 | @api_view(['POST']) 25 | def add_setting_api(request): 26 | """API: Add a setting key/value.""" 27 | setting_key = request.data["setting_key"] 28 | if Setting.objects.filter(key=setting_key).count() == 0: 29 | new_settings_args = { 30 | "key": request.data["setting_key"], 31 | "value": request.data["setting_value"], 32 | "comments": "n/a", 33 | } 34 | new_setting = Setting.objects.create(**new_settings_args) 35 | new_setting.save() 36 | 37 | messages.success(request, 'Setting successfully updated!') 38 | return JsonResponse({'status': 'success'}) 39 | 40 | return JsonResponse(data={'status': 'error'}, status=403) 41 | 42 | 43 | @api_view(['GET']) 44 | def delete_setting_api(request, setting_id): 45 | """API: Delete a setting key/value.""" 46 | setting = get_object_or_404(Setting, id=setting_id) 47 | setting.delete() 48 | messages.success(request, 'Setting successfully deleted!') 49 | 50 | return JsonResponse({'status': 'success'}) 51 | 52 | 53 | @api_view(['GET']) 54 | def export_settings_api(request): 55 | """API: Export settings.""" 56 | response = HttpResponse(content_type='text/csv') 57 | filename = "patrowl_settings.csv" 58 | response['Content-Disposition'] = 'attachment; filename=' + filename 59 | writer = csv.writer(response, delimiter=';') 60 | 61 | settings = Setting.objects.all() 62 | 63 | writer.writerow(['keys', 'values', 'comments']) # headers 64 | for setting in settings: 65 | writer.writerow([setting.key, setting.value, setting.comments]) 66 | 67 | return response 68 | 69 | 70 | @api_view(['GET']) 71 | def import_settings_api(request): 72 | """API: Export settings.""" 73 | # @Todo 74 | pass 75 | -------------------------------------------------------------------------------- /settings/apps.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals 4 | from django.apps import AppConfig 5 | 6 | 7 | class SettingsConfig(AppConfig): 8 | name = 'settings' 9 | -------------------------------------------------------------------------------- /settings/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2019-09-17 23:07 2 | 3 | from django.db import migrations, models 4 | import django.utils.timezone 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Setting', 17 | fields=[ 18 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('key', models.CharField(max_length=256, unique=True)), 20 | ('value', models.CharField(default='n/a', max_length=256)), 21 | ('comments', models.CharField(default='n/a', max_length=256)), 22 | ('created_at', models.DateTimeField(default=django.utils.timezone.now)), 23 | ('updated_at', models.DateTimeField(default=django.utils.timezone.now)), 24 | ], 25 | options={ 26 | 'db_table': 'settings', 27 | }, 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /settings/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Patrowl/PatrowlManager/d5f0a23cf8eebd7a393dfa2b73e4274f34891dce/settings/migrations/__init__.py -------------------------------------------------------------------------------- /settings/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.utils import timezone 3 | from django.db.models.signals import post_save, post_delete 4 | from django.dispatch import receiver 5 | import inspect 6 | 7 | 8 | class Setting(models.Model): 9 | key = models.CharField(max_length=256, unique=True) 10 | value = models.CharField(max_length=256, default='n/a') 11 | comments = models.CharField(max_length=256, default='n/a') 12 | created_at = models.DateTimeField(default=timezone.now) 13 | updated_at = models.DateTimeField(default=timezone.now) 14 | 15 | class Meta: 16 | db_table = 'settings' 17 | 18 | def __str__(self): 19 | return "{}: {}".format(self.key, self.value) 20 | 21 | def save(self, *args, **kwargs): 22 | # update the 'updated_at' entry on each update except on creation 23 | if not self._state.adding: 24 | self.updated_at = timezone.now() 25 | return super(Setting, self).save(*args, **kwargs) 26 | 27 | 28 | @receiver(post_save, sender=Setting) 29 | def setting_create_update_log(sender, **kwargs): 30 | from events.models import Event, AuditLog 31 | message = "" 32 | if kwargs['created']: 33 | message = "[Setting] New setting created (id={}): {}".format(kwargs['instance'].id, kwargs['instance']) 34 | Event.objects.create(message=message, type="CREATE", severity="DEBUG") 35 | else: 36 | message = "[Setting] Setting '{}' modified (id={})".format(kwargs['instance'], kwargs['instance'].id) 37 | Event.objects.create(message=message, type="UPDATE", severity="DEBUG") 38 | 39 | AuditLog.objects.create( 40 | message=message, 41 | scope='setting', type='rsetting_create_update', 42 | request_context=inspect.stack()) 43 | 44 | 45 | @receiver(post_delete, sender=Setting) 46 | def setting_delete_log(sender, **kwargs): 47 | from events.models import Event, AuditLog 48 | message = "[Setting] Setting '{}' deleted (id={})".format(kwargs['instance'], kwargs['instance'].id) 49 | Event.objects.create(message=message, type="DELETE", severity="DEBUG") 50 | 51 | AuditLog.objects.create( 52 | message=message, 53 | scope='setting', type='setting_delete', 54 | request_context=inspect.stack()) 55 | -------------------------------------------------------------------------------- /settings/tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.test import TestCase 4 | 5 | # Create your tests here. 6 | -------------------------------------------------------------------------------- /settings/urls.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.conf.urls import url 4 | from . import views, apis 5 | 6 | 7 | urlpatterns = [ 8 | ## WEB Views 9 | # ex: /settings/ 10 | url(r'^$', views.show_settings_menu, name='show_settings_menu'), 11 | url(r'^support$', views.show_support_page, name='show_support_page'), 12 | 13 | ## API views 14 | # ex: /settings/api/v1/update 15 | url(r'^api/v1/update$', apis.update_setting_api, name='update_setting_api'), 16 | # ex: /settings/api/v1/add 17 | url(r'^api/v1/add$', apis.add_setting_api, name='add_setting_api'), 18 | # ex: /settings/api/v1/delete/3 19 | url(r'^api/v1/delete/(?P[0-9]+)$', apis.delete_setting_api, name='delete_setting_api'), 20 | # ex: /settings/api/v1/export 21 | url(r'^api/v1/export$', apis.export_settings_api, name='export_settings_api'), 22 | 23 | ] 24 | -------------------------------------------------------------------------------- /settings/views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """View and API definitions for Settings.""" 3 | 4 | from django.shortcuts import render 5 | # from django.contrib.auth.models import User 6 | from django.contrib.auth import get_user_model 7 | from django.db.models import F 8 | from .models import Setting 9 | from events.models import Event 10 | from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger 11 | 12 | 13 | def show_settings_menu(request): 14 | """View: List settings menus.""" 15 | users = get_user_model().objects.all().annotate(apitoken=F('auth_token')) 16 | settings = Setting.objects.all().order_by("key") 17 | events_list = Event.objects.all().order_by("-id") 18 | 19 | nb_events_rows = int(request.GET.get('n_events', 25)) 20 | # events_paginator = CursorPaginator(events_list, ordering=['-id']) 21 | page_events = request.GET.get('p_events', 1) 22 | paginator_events = Paginator(events_list, nb_events_rows) 23 | try: 24 | events = paginator_events.page(page_events) 25 | except PageNotAnInteger: 26 | events = paginator_events.page(1) 27 | except EmptyPage: 28 | events = paginator_events.page(paginator_events.num_pages) 29 | 30 | return render(request, 'menu-settings.html', { 31 | 'users': users, 32 | 'settings': settings, 33 | 'events': events 34 | }) 35 | 36 | 37 | def show_support_page(request): 38 | """View: Support page.""" 39 | return render(request, 'support.html') 40 | -------------------------------------------------------------------------------- /start-server.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ## Starting Patrowl back-office server. MacOs compliant only. 4 | source env3/bin/activate 5 | if [ `ps waxu | grep -c postgres` -ne "1" ]; then 6 | echo "postgres db started. Nothing to do" 7 | else 8 | echo -e "postgres db stopped. Start DB First !\nExiting." 9 | exit 1 10 | fi 11 | 12 | if [ `ps waxu | grep -c supervisord` -ne "1" ]; then 13 | echo "supervisord started. Nothing to do" 14 | else 15 | echo -e "supervisord stopped. Starting supervisord." 16 | supervisord -c var/etc/supervisord.conf 17 | fi 18 | python manage.py makemigrations && \ 19 | python manage.py migrate && \ 20 | python manage.py collectstatic --noinput && \ 21 | gunicorn app.wsgi:application -b :8000 --timeout 300 --access-logfile - 22 | 23 | deactivate 24 | -------------------------------------------------------------------------------- /static/css/bootstrap-tagsinput.css: -------------------------------------------------------------------------------- 1 | .bootstrap-tagsinput { 2 | background-color: #fff; 3 | border: 1px solid #ccc; 4 | box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); 5 | display: inline-block; 6 | padding: 4px 6px; 7 | color: #555; 8 | vertical-align: middle; 9 | border-radius: 4px; 10 | max-width: 100%; 11 | line-height: 22px; 12 | cursor: text; 13 | } 14 | .bootstrap-tagsinput input { 15 | border: none; 16 | box-shadow: none; 17 | outline: none; 18 | background-color: transparent; 19 | padding: 0 6px; 20 | margin: 0; 21 | width: auto; 22 | max-width: inherit; 23 | } 24 | .bootstrap-tagsinput.form-control input::-moz-placeholder { 25 | color: #777; 26 | opacity: 1; 27 | } 28 | .bootstrap-tagsinput.form-control input:-ms-input-placeholder { 29 | color: #777; 30 | } 31 | .bootstrap-tagsinput.form-control input::-webkit-input-placeholder { 32 | color: #777; 33 | } 34 | .bootstrap-tagsinput input:focus { 35 | border: none; 36 | box-shadow: none; 37 | } 38 | .bootstrap-tagsinput .tag { 39 | margin-right: 2px; 40 | color: white; 41 | } 42 | .bootstrap-tagsinput .tag [data-role="remove"] { 43 | margin-left: 8px; 44 | cursor: pointer; 45 | } 46 | .bootstrap-tagsinput .tag [data-role="remove"]:after { 47 | content: "x"; 48 | padding: 0px 2px; 49 | } 50 | .bootstrap-tagsinput .tag [data-role="remove"]:hover { 51 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); 52 | } 53 | .bootstrap-tagsinput .tag [data-role="remove"]:hover:active { 54 | box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); 55 | } 56 | -------------------------------------------------------------------------------- /static/css/cal-heatmap.css: -------------------------------------------------------------------------------- 1 | /* Cal-HeatMap CSS */ 2 | 3 | .cal-heatmap-container { 4 | display: block; 5 | } 6 | 7 | .cal-heatmap-container .graph 8 | { 9 | font-family: "Lucida Grande", Lucida, Verdana, sans-serif; 10 | } 11 | 12 | .cal-heatmap-container .graph-label 13 | { 14 | fill: #999; 15 | font-size: 10px 16 | } 17 | 18 | .cal-heatmap-container .graph, .cal-heatmap-container .graph-legend rect { 19 | shape-rendering: crispedges 20 | } 21 | 22 | .cal-heatmap-container .graph-rect 23 | { 24 | fill: #ededed 25 | } 26 | 27 | .cal-heatmap-container .graph-subdomain-group rect:hover 28 | { 29 | stroke: #000; 30 | stroke-width: 1px 31 | } 32 | 33 | .cal-heatmap-container .subdomain-text { 34 | font-size: 8px; 35 | fill: #999; 36 | pointer-events: none 37 | } 38 | 39 | .cal-heatmap-container .hover_cursor:hover { 40 | cursor: pointer 41 | } 42 | 43 | .cal-heatmap-container .qi { 44 | background-color: #999; 45 | fill: #999 46 | } 47 | 48 | /* 49 | Remove comment to apply this style to date with value equal to 0 50 | .q0 51 | { 52 | background-color: #fff; 53 | fill: #fff; 54 | stroke: #ededed 55 | } 56 | */ 57 | 58 | .cal-heatmap-container .q1 59 | { 60 | background-color: #dae289; 61 | fill: #dae289 62 | } 63 | 64 | .cal-heatmap-container .q2 65 | { 66 | background-color: #cedb9c; 67 | fill: #9cc069 68 | } 69 | 70 | .cal-heatmap-container .q3 71 | { 72 | background-color: #b5cf6b; 73 | fill: #669d45 74 | } 75 | 76 | .cal-heatmap-container .q4 77 | { 78 | background-color: #637939; 79 | fill: #637939 80 | } 81 | 82 | .cal-heatmap-container .q5 83 | { 84 | background-color: #3b6427; 85 | fill: #3b6427 86 | } 87 | 88 | .cal-heatmap-container rect.highlight 89 | { 90 | stroke:#444; 91 | stroke-width:1 92 | } 93 | 94 | .cal-heatmap-container text.highlight 95 | { 96 | fill: #444 97 | } 98 | 99 | .cal-heatmap-container rect.highlight-now 100 | { 101 | stroke: red 102 | } 103 | 104 | .cal-heatmap-container text.highlight-now 105 | { 106 | fill: red; 107 | font-weight: 800 108 | } 109 | 110 | .cal-heatmap-container .domain-background { 111 | fill: none; 112 | shape-rendering: crispedges 113 | } 114 | 115 | .ch-tooltip { 116 | padding: 10px; 117 | background: #222; 118 | color: #bbb; 119 | font-size: 12px; 120 | line-height: 1.4; 121 | width: 140px; 122 | position: absolute; 123 | z-index: 99999; 124 | text-align: center; 125 | border-radius: 2px; 126 | box-shadow: 2px 2px 2px rgba(0,0,0,0.2); 127 | display: none; 128 | box-sizing: border-box; 129 | } 130 | 131 | .ch-tooltip::after{ 132 | position: absolute; 133 | width: 0; 134 | height: 0; 135 | border-color: transparent; 136 | border-style: solid; 137 | content: ""; 138 | padding: 0; 139 | display: block; 140 | bottom: -6px; 141 | left: 50%; 142 | margin-left: -6px; 143 | border-width: 6px 6px 0; 144 | border-top-color: #222; 145 | } 146 | -------------------------------------------------------------------------------- /static/css/family-Lato.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Lato'; 3 | font-style: italic; 4 | font-weight: 400; 5 | src: local('Lato Italic'), local('Lato-Italic'), url(/static/fonts/S6u8w4BMUTPHjxsAXC-v.ttf) format('truetype'); 6 | } 7 | @font-face { 8 | font-family: 'Lato'; 9 | font-style: normal; 10 | font-weight: 400; 11 | src: local('Lato Regular'), local('Lato-Regular'), url(/static/fonts/S6uyw4BMUTPHjx4wWw.ttf) format('truetype'); 12 | } 13 | @font-face { 14 | font-family: 'Lato'; 15 | font-style: normal; 16 | font-weight: 700; 17 | src: local('Lato Bold'), local('Lato-Bold'), url(/static/fonts/S6u9w4BMUTPHh6UVSwiPHA.ttf) format('truetype'); 18 | } 19 | -------------------------------------------------------------------------------- /static/css/images/ui-bg_flat_75_ffffff_40x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Patrowl/PatrowlManager/d5f0a23cf8eebd7a393dfa2b73e4274f34891dce/static/css/images/ui-bg_flat_75_ffffff_40x100.png -------------------------------------------------------------------------------- /static/css/patrowlmanager.css: -------------------------------------------------------------------------------- 1 | /* Modal */ 2 | .modal.right .modal-dialog, .modal.left .modal-dialog { 3 | position: fixed; 4 | margin: auto; 5 | width: 700px; 6 | height: 100%; 7 | -webkit-transform: translate3d(0%, 0, 0); 8 | -ms-transform: translate3d(0%, 0, 0); 9 | -o-transform: translate3d(0%, 0, 0); 10 | transform: translate3d(0%, 0, 0); 11 | } 12 | 13 | .modal.right .modal-content, .modal.left .modal-content { 14 | /*height: 100%;*/ 15 | overflow-y: auto; 16 | } 17 | 18 | .modal.right .modal-body, .modal.left .modal-body { 19 | padding: 15px 15px 20px; 20 | } 21 | 22 | .modal.right.fade .modal-dialog { 23 | right: -320px; 24 | -webkit-transition: opacity 0.3s linear, right 0.3s ease-out; 25 | -moz-transition: opacity 0.3s linear, right 0.3s ease-out; 26 | -o-transition: opacity 0.3s linear, right 0.3s ease-out; 27 | transition: opacity 0.3s linear, right 0.3s ease-out; 28 | } 29 | 30 | .modal.right.fade.in .modal-dialog { 31 | right: 0; 32 | } 33 | 34 | .modal.left.fade .modal-dialog{ 35 | left: -320px; 36 | -webkit-transition: opacity 0.3s linear, left 0.3s ease-out; 37 | -moz-transition: opacity 0.3s linear, left 0.3s ease-out; 38 | -o-transition: opacity 0.3s linear, left 0.3s ease-out; 39 | transition: opacity 0.3s linear, left 0.3s ease-out; 40 | } 41 | 42 | .modal.left.fade.in .modal-dialog { 43 | left: 0; 44 | } 45 | 46 | /* Twitter Typehead */ 47 | .twitter-typeahead, .tt-hint, .tt-input, .tt-menu { width: 100%; } 48 | 49 | .tt-query { 50 | box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); 51 | } 52 | 53 | .tt-hint { 54 | color: #999; 55 | } 56 | 57 | .tt-menu { 58 | /* width: 422px; */ 59 | margin-top: 12px; 60 | padding: 8px 0; 61 | background-color: #fff; 62 | border: 1px solid #ccc; 63 | border: 1px solid rgba(0, 0, 0, 0.2); 64 | border-radius: 8px; 65 | box-shadow: 0 5px 10px rgba(0,0,0,.2); 66 | } 67 | 68 | .tt-suggestion { 69 | padding: 3px 20px; 70 | /* font-size: 18px; */ 71 | line-height: 24px; 72 | } 73 | 74 | .tt-suggestion.tt-is-under-cursor { 75 | color: #fff; 76 | background-color: #0097cf; 77 | 78 | } 79 | 80 | .tt-suggestion p { 81 | margin: 0; 82 | } 83 | 84 | /* Other */ 85 | /* .no_padding { 86 | padding-left: 0px; 87 | padding-right: 0px; 88 | } */ 89 | input.search-input { 90 | float: left; 91 | width: auto; 92 | margin: 5px; 93 | border-width: 2px; 94 | border-style: outset; 95 | border-color: -internal-light-dark(rgb(118, 118, 118), rgb(133, 133, 133)); 96 | border-image: initial; 97 | background-color: -internal-light-dark(rgb(239, 239, 239), rgb(59, 59, 59)); 98 | padding: 1px 6px; 99 | height: 27px; 100 | } 101 | button.search-input-button{ 102 | margin: 5px; 103 | } 104 | div.div-override{ 105 | margin: 5px 0px; 106 | } -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Patrowl/PatrowlManager/d5f0a23cf8eebd7a393dfa2b73e4274f34891dce/static/favicon.ico -------------------------------------------------------------------------------- /static/fonts/S6u8w4BMUTPHjxsAXC-v.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Patrowl/PatrowlManager/d5f0a23cf8eebd7a393dfa2b73e4274f34891dce/static/fonts/S6u8w4BMUTPHjxsAXC-v.ttf -------------------------------------------------------------------------------- /static/fonts/S6u9w4BMUTPHh6UVSwiPHA.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Patrowl/PatrowlManager/d5f0a23cf8eebd7a393dfa2b73e4274f34891dce/static/fonts/S6u9w4BMUTPHh6UVSwiPHA.ttf -------------------------------------------------------------------------------- /static/fonts/S6uyw4BMUTPHjx4wWw.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Patrowl/PatrowlManager/d5f0a23cf8eebd7a393dfa2b73e4274f34891dce/static/fonts/S6uyw4BMUTPHjx4wWw.ttf -------------------------------------------------------------------------------- /static/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Patrowl/PatrowlManager/d5f0a23cf8eebd7a393dfa2b73e4274f34891dce/static/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /static/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Patrowl/PatrowlManager/d5f0a23cf8eebd7a393dfa2b73e4274f34891dce/static/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /static/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Patrowl/PatrowlManager/d5f0a23cf8eebd7a393dfa2b73e4274f34891dce/static/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /static/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Patrowl/PatrowlManager/d5f0a23cf8eebd7a393dfa2b73e4274f34891dce/static/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /static/images/sort_asc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Patrowl/PatrowlManager/d5f0a23cf8eebd7a393dfa2b73e4274f34891dce/static/images/sort_asc.png -------------------------------------------------------------------------------- /static/images/sort_asc_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Patrowl/PatrowlManager/d5f0a23cf8eebd7a393dfa2b73e4274f34891dce/static/images/sort_asc_disabled.png -------------------------------------------------------------------------------- /static/images/sort_both.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Patrowl/PatrowlManager/d5f0a23cf8eebd7a393dfa2b73e4274f34891dce/static/images/sort_both.png -------------------------------------------------------------------------------- /static/images/sort_desc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Patrowl/PatrowlManager/d5f0a23cf8eebd7a393dfa2b73e4274f34891dce/static/images/sort_desc.png -------------------------------------------------------------------------------- /static/tmpl/assets_bulk_import_template.csv: -------------------------------------------------------------------------------- 1 | asset_value;asset_name;asset_type;asset_description;asset_criticality;asset_groupname;asset_tags;asset_teams;asset_exposure 2 | patrowl.io;Patrowl domain name;domain;Patrowl domain name;high;recently_imported;my_tag;default;external 3 | 8.8.8.8;Google DNS A IP addr;ip;Google DNS A IP addr;low;recently_imported;my_tag;default;external 4 | 8.8.4.4;Google DNS B IP addr;ip;Google DNS B IP addr;low;recently_imported;my_tag;default;external 5 | -------------------------------------------------------------------------------- /static/tmpl/users_bulk_import_template.csv: -------------------------------------------------------------------------------- 1 | username;first_name;last_name;email;is_active;role;teams 2 | user01;fn user01;ln user01;user01@patrowl.io;true;Auditor;default 3 | user02;fn user02;ln user02;user02@patrowl.io;false;Manager; 4 | user03;fn user03;ln user03;user03@patrowl.io;true;BadRole; 5 | -------------------------------------------------------------------------------- /templates/errors/400.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 | 4 | 5 |
    6 |
    7 |
    8 | 400, Error page ... 9 |
    10 |
    11 | 12 |
    13 | 14 | 15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /templates/errors/403.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 | 4 | 5 |
    6 |
    7 |
    8 | 403, Forbbiden. Please go away ... 9 |
    10 |
    11 | 12 |
    13 | 14 | 15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /templates/errors/404.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 | 4 | 5 |
    6 |
    7 |
    8 | 404, Page not found ... 9 |
    10 |
    11 | 12 |
    13 | 14 | 15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /templates/errors/500.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 | 4 | 5 |
    6 |
    7 |
    8 | 500, Server Error ... 9 |
    10 |
    11 | 12 |
    13 | 14 | 15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /templates/login.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 | 4 | {% if form.errors %} 5 |

    Your username and password didn't match. Please try again.

    6 | {% endif %} 7 | {% if next %} 8 | {% if user.is_authenticated %} 9 | 10 |

    Your account doesn't have access to this page. To proceed, please login with an account that has access.

    11 | {% else %} 12 | 13 |

    Please login to see this page.

    14 | {% endif %} 15 | {% endif %} 16 | 17 |
    18 |
    19 |
    20 | 37 |
    38 |
    39 | {% if PRO_EDITION == True and LOGIN_SSO_URL != "" %} 40 |
    41 |
    42 | 50 |
    51 |
    52 | {% endif %} 53 |
    54 | 55 | {% endblock %} 56 | {% block javascript %} 57 | 58 | 59 | 64 | 65 | {% endblock %} 66 | -------------------------------------------------------------------------------- /templates/signup.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 |

    Sign up

    5 |
    6 | {% csrf_token %} 7 | {% for field in form %} 8 |

    9 | {{ field.label_tag }}
    10 | {{ field }} 11 | {% if field.help_text %} 12 | {{ field.help_text }} 13 | {% endif %} 14 | {% for error in field.errors %} 15 |

    {{ error }}

    16 | {% endfor %} 17 |

    18 | {% endfor %} 19 | 20 |
    21 | {% endblock %} 22 | -------------------------------------------------------------------------------- /templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Patrowl/PatrowlManager/d5f0a23cf8eebd7a393dfa2b73e4274f34891dce/templatetags/__init__.py -------------------------------------------------------------------------------- /tests/test_sample.py: -------------------------------------------------------------------------------- 1 | def test_always_valid(): 2 | assert True == True 3 | -------------------------------------------------------------------------------- /users/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Patrowl/PatrowlManager/d5f0a23cf8eebd7a393dfa2b73e4274f34891dce/users/__init__.py -------------------------------------------------------------------------------- /users/admin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.contrib import admin 4 | from .models import Profile 5 | 6 | admin.site.register(Profile) 7 | -------------------------------------------------------------------------------- /users/apps.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals 4 | 5 | from django.apps import AppConfig 6 | 7 | 8 | class UsersConfig(AppConfig): 9 | name = 'users' 10 | -------------------------------------------------------------------------------- /users/forms.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.forms import AuthenticationForm 2 | from django import forms 3 | 4 | 5 | class LoginForm(AuthenticationForm): 6 | username = forms.CharField( 7 | label="Username", max_length=250, 8 | widget=forms.TextInput(attrs={ 9 | 'class': 'form-control', 'name': 'username', 'value': ''})) 10 | password = forms.CharField( 11 | label="Password", max_length=250, 12 | widget=forms.PasswordInput(attrs={ 13 | 'class': 'form-control', 14 | 'name': 'password', 15 | 'value': '', 16 | 'autocomplete': 'off', 17 | })) 18 | -------------------------------------------------------------------------------- /users/middleware.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import re 4 | from django.conf import settings 5 | from django.contrib.auth.decorators import login_required 6 | 7 | 8 | class RequireLoginMiddleware(object): 9 | """ 10 | Middleware component that wraps the login_required decorator around 11 | matching URL patterns. To use, add the class to MIDDLEWARE_CLASSES and 12 | define LOGIN_REQUIRED_URLS and LOGIN_REQUIRED_URLS_EXCEPTIONS in your 13 | settings.py. For example: 14 | ------ 15 | LOGIN_REQUIRED_URLS = ( 16 | r'/topsecret/(.*)$', 17 | ) 18 | LOGIN_REQUIRED_URLS_EXCEPTIONS = ( 19 | r'/topsecret/login(.*)$', 20 | r'/topsecret/logout(.*)$', 21 | ) 22 | ------ 23 | LOGIN_REQUIRED_URLS is where you define URL patterns; each pattern must 24 | be a valid regex. 25 | 26 | LOGIN_REQUIRED_URLS_EXCEPTIONS is, conversely, where you explicitly 27 | define any exceptions (like login and logout URLs). 28 | """ 29 | 30 | def __init__(self, get_response=None): 31 | self.required = tuple( 32 | re.compile(url) for url in settings.LOGIN_REQUIRED_URLS 33 | ) 34 | self.exceptions = tuple( 35 | re.compile(url) for url in settings.LOGIN_REQUIRED_URLS_EXCEPTIONS 36 | ) 37 | self.get_response = get_response 38 | 39 | def __call__(self, request): 40 | response = self.get_response(request) 41 | return response 42 | 43 | def process_view(self, request, view_func, view_args, view_kwargs): 44 | # No need to process URLs if user already logged in 45 | if request.user.is_authenticated: 46 | return None 47 | 48 | # An exception match should immediately return None 49 | for url in self.exceptions: 50 | if url.match(request.path): 51 | return None 52 | 53 | # Requests matching a restricted URL pattern are returned 54 | # wrapped with the login_required decorator 55 | for url in self.required: 56 | if url.match(request.path): 57 | return login_required(view_func)(request, *view_args, **view_kwargs) 58 | 59 | # Explicitly return None for all non-matching requests 60 | return None 61 | -------------------------------------------------------------------------------- /users/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2019-09-17 23:07 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | import django.utils.timezone 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='Profile', 20 | fields=[ 21 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 22 | ('status', models.CharField(choices=[('ACTIVE', 'ACTIVE'), ('DISABLED', 'DISABLED')], default='ACTIVE', max_length=10)), 23 | ('bio', models.TextField(blank=True, max_length=500)), 24 | ('department', models.CharField(blank=True, max_length=100)), 25 | ('created_at', models.DateTimeField(default=django.utils.timezone.now)), 26 | ('updated_at', models.DateTimeField(default=django.utils.timezone.now)), 27 | ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 28 | ], 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /users/migrations/0003_auto_20200624_0037.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.13 on 2020-06-23 22:37 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('users', '0002_auto_20200616_1704'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='profile', 15 | name='bio', 16 | ), 17 | migrations.RemoveField( 18 | model_name='profile', 19 | name='department', 20 | ), 21 | migrations.AddField( 22 | model_name='profile', 23 | name='role', 24 | field=models.PositiveSmallIntegerField(blank=True, choices=[(1, 'Manager'), (2, 'Analyst'), (3, 'Auditor')], null=True), 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /users/migrations/0004_remove_profile_role.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.13 on 2020-06-23 22:43 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('users', '0003_auto_20200624_0037'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='profile', 15 | name='role', 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /users/migrations/0005_profile_is_delegated.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.13 on 2020-07-08 22:56 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('users', '0004_remove_profile_role'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='profile', 15 | name='is_delegated', 16 | field=models.BooleanField(default=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /users/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Patrowl/PatrowlManager/d5f0a23cf8eebd7a393dfa2b73e4274f34891dce/users/migrations/__init__.py -------------------------------------------------------------------------------- /users/permissions.py: -------------------------------------------------------------------------------- 1 | from rest_framework import permissions 2 | 3 | 4 | class IsOwnerOrReadOnly(permissions.BasePermission): 5 | 6 | def has_object_permission(self, request, view, obj): 7 | return obj == request.user 8 | -------------------------------------------------------------------------------- /users/serializers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from rest_framework import serializers, generics, views, response, permissions 4 | from django_filters import rest_framework as filters 5 | # from django.contrib.auth.models import User 6 | from django.contrib.auth import get_user_model 7 | 8 | # from rest_framework.authentication import SessionAuthentication, BasicAuthentication 9 | # from rest_framework.decorators import api_view 10 | 11 | 12 | class UserSerializer(serializers.ModelSerializer): 13 | 14 | class Meta: 15 | model = get_user_model() 16 | fields = ('id', 'username', 'last_login', 'is_superuser', 'is_staff') 17 | 18 | 19 | class UserList(generics.ListAPIView): 20 | queryset = get_user_model().objects.all() 21 | serializer_class = UserSerializer 22 | filter_backends = (filters.DjangoFilterBackend,) 23 | # filterset_fields = ('title', 'severity', 'engine_type') 24 | 25 | 26 | class CurrentUserView(views.APIView): 27 | # authentication_classes = [SessionAuthentication, BasicAuthentication, JWTAuthentication] 28 | # permission_classes = [IsAuthenticated] 29 | # permission_classes = [permissions.IsAdminUser] 30 | 31 | # @api_view(['GET', 'OPTIONS', 'POST']) 32 | def get(self, request): 33 | serializer = UserSerializer(request.user, context={'request': request}) 34 | # print(serializer) 35 | # print(self.check_object_permissions(self.request, serializer)) 36 | return response.Response(serializer.data) 37 | -------------------------------------------------------------------------------- /users/templates/add-user.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 | 4 | 9 | 10 |
    11 |
    12 | Add a new user 13 | {% csrf_token %} 14 | {% for field in form %} 15 | {% if field.errors %} 16 |
    17 | 18 |
    19 | {{ field }} 20 | 21 | {% for error in field.errors %}{{ error }}{% endfor %} 22 | 23 |
    24 |
    25 | {% elif not field.is_hidden %} 26 |
    27 | 28 |
    29 | {{ field }} 30 | {% if field.help_text %} 31 |

    {{ field.help_text }}

    32 | {% endif %} 33 |
    34 |
    35 | {% endif %} 36 | {% endfor %} 37 |
    38 | 39 | Cancel 40 |
    41 |
    42 |
    43 | 44 | {% endblock %} 45 | -------------------------------------------------------------------------------- /users/templates/details-user.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load patrowl_tags %} 3 | {% block content %} 4 | 5 |
    6 |

    User details

    7 | 8 |
    9 |
    User ID
    {{ user.id }}
    10 |
    Username
    {{ user.username }}
    11 |
    Role
    12 | {% if user.is_superuser %} 13 | SuperAdmin 14 | {% else %} 15 | {{user.userrole.get_role_display}} 16 | {% endif %} 17 |
    18 |
    API Token
    {{ apitoken }}
    19 |
    First Name
    {{ user.first_name }}
    20 |
    Last Name
    {{ user.last_name }}
    21 |
    Email
    {{ user.email }}
    22 |
    Is Active?
    {{ user.is_active }}
    23 |
    Is Superuser?
    {{ user.is_superuser }}
    24 |
    Is Staff?
    {{ user.is_staff }}
    25 |
    Created at
    {{ user.date_joined|smartdate }}
    26 |
    Last Login
    {{ user.last_login|smartdate }}
    27 | 28 | 29 | 30 |
    31 | 32 | {% if messages %} 33 |
      34 | {% for message in messages %} 35 |
    • {{ message }}
    • 36 | {% endfor %} 37 |
    38 | {% endif %} 39 |
    40 | 41 | 42 | {% endblock %} 43 | -------------------------------------------------------------------------------- /users/templates/edit-user-password.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 | 4 | 9 | 10 |
    11 |
    12 | {% csrf_token %} 13 | {% for field in form %} 14 | {% if field.errors %} 15 |
    16 | 17 |
    18 | {{ field }} 19 | 20 | {% for error in field.errors %}{{ error }}{% endfor %} 21 | 22 |
    23 |
    24 | {% else %} 25 |
    26 | 27 |
    28 | {{ field }} 29 | {% if field.help_text %} 30 |

    {{ field.help_text }}

    31 | {% endif %} 32 |
    33 |
    34 | {% endif %} 35 | {% endfor %} 36 |
    37 | 38 | Cancel 39 |
    40 |
    41 |
    42 | 43 | {% endblock %} 44 | -------------------------------------------------------------------------------- /users/tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.test import TestCase 4 | 5 | # Create your tests here. 6 | -------------------------------------------------------------------------------- /users/urls.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.urls import path 4 | from django.conf.urls import url 5 | from . import views, apis, serializers 6 | from reportings import views as rep_views 7 | 8 | 9 | urlpatterns = [ 10 | 11 | # Views 12 | url(r'^$', rep_views.homepage_dashboard_view, 13 | name='homepage_dashboard_view'), 14 | url(r'^list$', views.list_users_view, 15 | name='list_users_view'), 16 | url(r'^dashboard$', rep_views.homepage_dashboard_view, 17 | name='homepage_dashboard_view'), 18 | url(r'^details$', views.user_details_view, 19 | name='user_details_view'), 20 | url(r'^add$', views.add_user_view, 21 | name='add_user_view'), 22 | url(r'^editpw$', views.edit_user_password_view, 23 | name='edit_user_password_view'), 24 | 25 | # REST-API Endpoints 26 | url(r'^users/api/v1/details/(?P[0-9]+)$', 27 | apis.user_details_api, name='user_details_api'), 28 | url(r'^users/api/v1/delete/(?P[0-9]+)$', 29 | apis.delete_user_api, name='delete_user_api'), 30 | url(r'^users/api/v1/list$', 31 | apis.list_users_api, name='list_users_api'), 32 | url(r'^users/api/v1/authtoken/get$', 33 | apis.get_curruser_authtoken_api, name='get_curruser_authtoken_api'), 34 | url(r'^users/api/v1/authtoken/get/(?P[0-9]+)$', 35 | apis.get_user_authtoken_api, name='get_user_authtoken_api'), 36 | url(r'^users/api/v1/authtoken/renew$', 37 | apis.renew_curruser_authtoken_api, name='renew_curruser_authtoken_api'), 38 | url(r'^users/api/v1/authtoken/renew/(?P[0-9]+)$', 39 | apis.renew_user_authtoken_api, name='renew_user_authtoken_api'), 40 | url(r'^users/api/v1/authtoken/delete$', 41 | apis.delete_curruser_authtoken_api, name='delete_curruser_authtoken_api'), 42 | url(r'^users/api/v1/authtoken/delete/(?P[0-9]+)$', 43 | apis.delete_user_authtoken_api, name='delete_user_authtoken_api'), 44 | url(r'^users/api/v1/(?P[0-9]+)/renewpassword$', 45 | apis.renew_user_password_api, name='renew_user_password_api'), 46 | ] 47 | 48 | # Serialized data 49 | urlpatterns += [ 50 | path('api/list', serializers.UserList.as_view()), 51 | path('api/current', serializers.CurrentUserView.as_view()), 52 | ] 53 | -------------------------------------------------------------------------------- /var/bin/create_default_admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | User = get_user_model() 3 | if not User.objects.filter(username='admin').exists(): 4 | User.objects.create_superuser('admin', 'admin@dev.patrowl.io', 'Bonjour1!') 5 | -------------------------------------------------------------------------------- /var/bin/create_default_team.py: -------------------------------------------------------------------------------- 1 | from users.models import Team, TeamUser, TeamOwner 2 | from django.contrib.auth import get_user_model 3 | admin_user = get_user_model().objects.get(username='admin') 4 | if admin_user.users_team.count() == 0: 5 | admin_org = Team.objects.create(name='default', is_active=True) 6 | admin_org.save() 7 | org_user = TeamUser.objects.create(user=admin_user, organization=admin_org, is_admin=True) 8 | org_user.save() 9 | org_owner = TeamOwner.objects.create(organization=admin_org, organization_user=org_user) 10 | org_owner.save() 11 | -------------------------------------------------------------------------------- /var/bin/update_db_migrations.sh: -------------------------------------------------------------------------------- 1 | # verlte() { 2 | # [ "$1" = "`echo -e "$1\n$2" | sort -V | head -n1`" ] 3 | # } 4 | # 5 | # # Check version for compatibility, otherwise exit 6 | # verlte `cat VERSION | cut -f1 -d" "` 1.5.4 && echo "Let's go" || { echo "Use current migration files. Exit now" ; exit; } 7 | 8 | echo "-- Migrate django_celery_beat" 9 | python manage.py migrate django_celery_beat 10 | 11 | echo "-- Installed tables:" 12 | echo "from django.db import connection ; print(connection.introspection.table_names()) " | python manage.py shell 13 | 14 | if echo "from django.db import connection ; print('assets' in connection.introspection.table_names()) " | python manage.py shell | grep -q 'True'; then 15 | 16 | echo "-- Clean the django_migrations table" 17 | echo "from django.db import connection; cursor = connection.cursor(); cursor.execute('delete from django_migrations')" | python manage.py shell 18 | 19 | echo "-- Remove 'migrations' folders" 20 | rm -rf events/migrations 21 | rm -rf users/migrations 22 | rm -rf scans/migrations 23 | rm -rf assets/migrations 24 | rm -rf findings/migrations 25 | rm -rf rules/migrations 26 | rm -rf settings/migrations 27 | 28 | echo "-- Apply fake migrations for built-in apps" 29 | python manage.py migrate --fake 30 | 31 | echo "-- Run syncdb on every apps" 32 | python manage.py migrate events --run-syncdb 33 | python manage.py migrate users --run-syncdb 34 | python manage.py migrate scans --run-syncdb 35 | python manage.py migrate assets --run-syncdb 36 | python manage.py migrate findings --run-syncdb 37 | python manage.py migrate rules --run-syncdb 38 | python manage.py migrate settings --run-syncdb 39 | 40 | echo "-- Make migrations on every apps" 41 | python manage.py makemigrations events 42 | python manage.py makemigrations users 43 | python manage.py makemigrations scans 44 | python manage.py makemigrations assets 45 | python manage.py makemigrations findings 46 | python manage.py makemigrations rules 47 | python manage.py makemigrations settings 48 | 49 | echo "-- Apply migration (fake initial)" 50 | python manage.py migrate --fake-initial 51 | 52 | echo "-- Apply assets migrations (from 1.6.0)" 53 | cp var/migrations/assets/0002_asset_exposure.py assets/migrations/0002_asset_exposure.py 54 | python manage.py migrate assets 0002 55 | fi 56 | -------------------------------------------------------------------------------- /var/data/engines.EnginePolicyScope.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "model": "engines.enginepolicyscope", 3 | "pk": 1, 4 | "fields": { 5 | "name": "Network Infrastructure", 6 | "priority": null, 7 | "created_at": "2017-07-09T16:42:12Z", 8 | "updated_at": "2017-07-09T16:42:12Z" 9 | } 10 | }, { 11 | "model": "engines.enginepolicyscope", 12 | "pk": 2, 13 | "fields": { 14 | "name": "System infrastructure", 15 | "priority": null, 16 | "created_at": "2017-07-16T08:51:44Z", 17 | "updated_at": "2017-07-27T10:34:54.561Z" 18 | } 19 | }, { 20 | "model": "engines.enginepolicyscope", 21 | "pk": 3, 22 | "fields": { 23 | "name": "Domain", 24 | "priority": null, 25 | "created_at": "2017-07-27T10:34:56Z", 26 | "updated_at": "2017-07-27T10:34:56Z" 27 | } 28 | }, { 29 | "model": "engines.enginepolicyscope", 30 | "pk": 4, 31 | "fields": { 32 | "name": "Web App", 33 | "priority": null, 34 | "created_at": "2017-07-27T10:35:09Z", 35 | "updated_at": "2017-07-27T10:35:09Z" 36 | } 37 | }, { 38 | "model": "engines.enginepolicyscope", 39 | "pk": 5, 40 | "fields": { 41 | "name": "HTTPS & Certificates", 42 | "priority": null, 43 | "created_at": "2017-07-27T10:35:20Z", 44 | "updated_at": "2017-07-27T10:35:20Z" 45 | } 46 | }, { 47 | "model": "engines.enginepolicyscope", 48 | "pk": 6, 49 | "fields": { 50 | "name": "E-Reputation", 51 | "priority": null, 52 | "created_at": "2017-07-27T10:35:32Z", 53 | "updated_at": "2017-07-27T10:35:32Z" 54 | } 55 | }, { 56 | "model": "engines.enginepolicyscope", 57 | "pk": 7, 58 | "fields": { 59 | "name": "Malware", 60 | "priority": null, 61 | "created_at": "2017-07-27T10:35:41Z", 62 | "updated_at": "2017-07-27T10:35:41Z" 63 | } 64 | }, { 65 | "model": "engines.enginepolicyscope", 66 | "pk": 8, 67 | "fields": { 68 | "name": "Availability", 69 | "priority": null, 70 | "created_at": "2017-07-27T10:35:47Z", 71 | "updated_at": "2017-07-27T10:35:47Z" 72 | } 73 | }] 74 | -------------------------------------------------------------------------------- /var/db/create_db.sql: -------------------------------------------------------------------------------- 1 | -- Create user if not exists 2 | DO $$ 3 | BEGIN 4 | IF NOT EXISTS ( 5 | SELECT FROM pg_catalog.pg_roles -- SELECT list can be empty for this 6 | WHERE rolname = 'PATROWL_DB_USER') THEN 7 | 8 | CREATE USER 'PATROWL_DB_USER' WITH PASSWORD 'PATROWL_DB_PASSWD_TO_CHANGE'; 9 | END IF; 10 | END 11 | $$; 12 | 13 | 14 | SELECT 'CREATE DATABASE patrowl_db WITH OWNER "PATROWL_DB_USER"' 15 | WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'patrowl_db')\gexec 16 | 17 | ALTER ROLE "PATROWL_DB_USER" SET client_encoding TO 'utf8'; 18 | ALTER ROLE "PATROWL_DB_USER" SET default_transaction_isolation TO 'read committed'; 19 | GRANT ALL PRIVILEGES ON DATABASE "patrowl_db" TO "PATROWL_DB_USER"; 20 | -------------------------------------------------------------------------------- /var/db/create_user_and_db.sql: -------------------------------------------------------------------------------- 1 | CREATE USER "PATROWL_DB_USER" WITH PASSWORD 'PATROWL_DB_PASSWD_TO_CHANGE'; 2 | CREATE DATABASE "patrowl_db" WITH OWNER "PATROWL_DB_USER"; 3 | ALTER ROLE "PATROWL_DB_USER" SET client_encoding TO 'utf8'; 4 | ALTER ROLE "PATROWL_DB_USER" SET default_transaction_isolation TO 'read committed'; 5 | -- ALTER ROLE "PATROWL_DB_USER" SET timezone TO 'UTC'; 6 | GRANT ALL PRIVILEGES ON DATABASE "patrowl_db" TO "PATROWL_DB_USER"; 7 | -------------------------------------------------------------------------------- /var/db/init_db.sql: -------------------------------------------------------------------------------- 1 | ALTER ROLE "PATROWL_DB_USER" SET client_encoding TO 'utf8'; 2 | ALTER ROLE "PATROWL_DB_USER" SET default_transaction_isolation TO 'read committed'; 3 | ALTER ROLE "PATROWL_DB_USER" SET timezone TO 'UTC'; 4 | GRANT ALL PRIVILEGES ON DATABASE "patrowl_db" TO "PATROWL_DB_USER"; 5 | -------------------------------------------------------------------------------- /var/etc/supervisord-celery-pro.conf: -------------------------------------------------------------------------------- 1 | ;[program:celery-flow] 2 | ;command=env3/bin/celery --app=app flower --port=5555 3 | ;stdout_logfile=var/log/celeryd.flower.log 4 | ;stderr_logfile=var/log/celeryderr.flower.log 5 | ;autostart=true 6 | ;autorestart=true 7 | ;startsecs=5 8 | ;stopwaitsecs=60 9 | ;stopasgroup=true 10 | ;killasgroup=true 11 | ;priority=998 12 | 13 | [program:celery-beat] 14 | command=env3/bin/celery beat --app=app -l info -S django --pidfile="var/tmp/celerybeat.pid" 15 | environment=PRO_EDITION='True' 16 | stdout_logfile=var/log/celeryd.beat.log 17 | stderr_logfile=var/log/celeryderr.beat.log 18 | autostart=true 19 | autorestart=true 20 | startsecs=5 21 | stopwaitsecs=60 22 | stopasgroup=true 23 | killasgroup=true 24 | priority=999 25 | pidfile=var/tmp/celerybeat.pid 26 | 27 | [program:celery-default] 28 | command=env3/bin/celery worker --hostname=default-node@%%n --app=app -l info -Q default --purge --without-mingle --without-gossip --without-heartbeat -Ofair 29 | environment=PRO_EDITION='True' 30 | stdout_logfile=var/log/celeryd.default.log 31 | stderr_logfile=var/log/celeryderr.default.log 32 | autostart=true 33 | autorestart=true 34 | startsecs=5 35 | stopwaitsecs=10 36 | stopasgroup=true 37 | killasgroup=true 38 | priority=990 39 | 40 | [program:celery-scan] 41 | command=env3/bin/celery worker --hostname=scan-node@%%n --app=app -l info -Q scan --purge --without-mingle --without-gossip --without-heartbeat -Ofair 42 | environment=PRO_EDITION='True' 43 | stdout_logfile=var/log/celeryd.scan.log 44 | stderr_logfile=var/log/celeryderr.scan.log 45 | autostart=true 46 | autorestart=true 47 | startsecs=5 48 | stopwaitsecs=10 49 | stopasgroup=true 50 | killasgroup=true 51 | 52 | [program:celery-scanmgt] 53 | command=env3/bin/celery worker --hostname=scanmgt-node@%%n --app=app -l info -Q scanmgt --purge --without-mingle --without-gossip --without-heartbeat -Ofair 54 | environment=PRO_EDITION='True' 55 | stdout_logfile=var/log/celeryd.scanmgt.log 56 | stderr_logfile=var/log/celeryderr.scanmgt.log 57 | autostart=true 58 | autorestart=true 59 | startsecs=5 60 | stopwaitsecs=10 61 | stopasgroup=true 62 | killasgroup=true 63 | priority=990 64 | 65 | ;[group:celery-workers] 66 | ;programs=celery-flow,celery-beat,celery-default,celery-scan,celery-scanmgt 67 | ;priority=990 68 | -------------------------------------------------------------------------------- /var/etc/supervisord-celery.conf: -------------------------------------------------------------------------------- 1 | ;[program:celery-flow] 2 | ;command=env3/bin/celery --app=app flower --port=5555 3 | ;stdout_logfile=var/log/celeryd.flower.log 4 | ;stderr_logfile=var/log/celeryderr.flower.log 5 | ;autostart=true 6 | ;autorestart=true 7 | ;startsecs=5 8 | ;stopwaitsecs=60 9 | ;stopasgroup=true 10 | ;killasgroup=true 11 | ;priority=998 12 | 13 | [program:celery-beat] 14 | command=env3/bin/celery beat --app=app -l info -S django --pidfile="var/tmp/celerybeat.pid" 15 | stdout_logfile=var/log/celeryd.beat.log 16 | stderr_logfile=var/log/celeryderr.beat.log 17 | autostart=true 18 | autorestart=true 19 | startsecs=5 20 | stopwaitsecs=60 21 | stopasgroup=true 22 | killasgroup=true 23 | priority=999 24 | pidfile=var/tmp/celerybeat.pid 25 | 26 | [program:celery-default] 27 | command=env3/bin/celery worker --hostname=default-node@%%n --app=app -l info -Q default --purge --without-mingle --without-gossip --without-heartbeat -Ofair 28 | stdout_logfile=var/log/celeryd.default.log 29 | stderr_logfile=var/log/celeryderr.default.log 30 | autostart=true 31 | autorestart=true 32 | startsecs=5 33 | stopwaitsecs=10 34 | stopasgroup=true 35 | killasgroup=true 36 | priority=990 37 | 38 | [program:celery-scan] 39 | command=env3/bin/celery worker --hostname=scan-node@%%n --app=app -l info -Q scan --purge --without-mingle --without-gossip --without-heartbeat -Ofair 40 | stdout_logfile=var/log/celeryd.scan.log 41 | stderr_logfile=var/log/celeryderr.scan.log 42 | autostart=true 43 | autorestart=true 44 | startsecs=5 45 | stopwaitsecs=10 46 | stopasgroup=true 47 | killasgroup=true 48 | priority=990 49 | 50 | [program:celery-scanmgt] 51 | command=env3/bin/celery worker --hostname=scanmgt-node@%%n --app=app -l info -Q scanmgt --purge --without-mingle --without-gossip --without-heartbeat -Ofair 52 | stdout_logfile=var/log/celeryd.scanmgt.log 53 | stderr_logfile=var/log/celeryderr.scanmgt.log 54 | autostart=true 55 | autorestart=true 56 | startsecs=5 57 | stopwaitsecs=10 58 | stopasgroup=true 59 | killasgroup=true 60 | priority=990 61 | 62 | ;[group:celery-workers] 63 | ;programs=celery-flow,celery-beat,celery-default,celery-scan,celery-scanmgt 64 | ;priority=990 65 | -------------------------------------------------------------------------------- /var/etc/supervisord-pro.conf: -------------------------------------------------------------------------------- 1 | [inet_http_server] 2 | port=127.0.0.1:9001 3 | 4 | [supervisord] 5 | logfile=var/log/supervisord.log 6 | logfile_maxbytes=10MB 7 | pidfile=var/tmp/supervisord.pid 8 | 9 | [supervisorctl] 10 | serverurl=http://localhost:9001 11 | logfile_backups=10 12 | loglevel=info 13 | 14 | [rpcinterface:supervisor] 15 | supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface 16 | 17 | [include] 18 | files=supervisord-celery-pro.conf 19 | -------------------------------------------------------------------------------- /var/etc/supervisord.conf: -------------------------------------------------------------------------------- 1 | [inet_http_server] 2 | port=127.0.0.1:9001 3 | 4 | [supervisord] 5 | logfile=var/log/supervisord.log 6 | logfile_maxbytes=10MB 7 | pidfile=var/tmp/supervisord.pid 8 | 9 | [supervisorctl] 10 | serverurl=http://localhost:9001 11 | logfile_backups=10 12 | loglevel=info 13 | 14 | [rpcinterface:supervisor] 15 | supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface 16 | 17 | [include] 18 | files=supervisord-celery.conf 19 | -------------------------------------------------------------------------------- /var/log/.dockerignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.log 3 | *.log.[0-9]* 4 | -------------------------------------------------------------------------------- /var/log/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.log 3 | *.log.[0-9]* 4 | -------------------------------------------------------------------------------- /var/migrations/assets/0002_asset_exposure.py: -------------------------------------------------------------------------------- 1 | from django.db import migrations, models 2 | 3 | 4 | class Migration(migrations.Migration): 5 | 6 | dependencies = [ 7 | ('assets', '0001_initial'), 8 | ] 9 | 10 | operations = [ 11 | migrations.AddField( 12 | model_name='asset', 13 | name='exposure', 14 | field=models.CharField(choices=[('external', 'External'), ('internal', 'Internal'), ('restricted', 'Restricted')], default='external', max_length=16), 15 | ), 16 | migrations.AlterField( 17 | model_name='asset', 18 | name='exposure', 19 | field=models.CharField(choices=[('unknown', 'Unknown'), ('external', 'External'), ('internal', 'Internal'), ('restricted', 'Restricted')], default='unknown', max_length=16), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /var/tmp/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.pid 3 | --------------------------------------------------------------------------------