New upstream version 3.14.0
Sophie Brun
3 years ago
0 | tag_on_github: | |
1 | image: python:3 | |
2 | stage: publish | |
3 | before_script: | |
4 | script: | |
5 | - git config remote.github.url >/dev/null || git remote add github https://${GH_TOKEN}@github.com/infobyte/faraday.git | |
6 | - CHANGELOG/check_pre_tag.py | |
7 | - git push github white/dev:master | |
8 | - git tag v$IMAGE_TAG -m "$(cat CHANGELOG/$IMAGE_TAG/white.md)" | |
9 | - git push github v$IMAGE_TAG | |
10 | rules: | |
11 | - if: '$CI_COMMIT_TAG =~ /^white-v[0-9.]+$/' | |
12 | when: on_success | |
13 | ||
14 | publish_pypi: | |
15 | image: python:3 | |
16 | stage: publish | |
17 | script: | |
18 | - apt-get update -qy | |
19 | - apt-get install twine -y | |
20 | - python setup.py sdist bdist_wheel | |
21 | - twine upload -u $PYPI_USER -p $PYPI_PASS dist/* --verbose | |
22 | rules: | |
23 | - if: '$CI_COMMIT_TAG =~ /^white-v[0-9.]+$/' | |
24 | when: on_success |
0 | .google_storage_deb_rpm_base: &google_storage_deb_rpm_base | |
1 | - check_not_skipped(){ grep -v 'Skipping'; } | |
2 | - gcloud auth activate-service-account --key-file auth_file.json | |
3 | - "gsutil cp faraday-server_amd64.deb ${GCLOUD_FILE_PATH}.deb 2>&1 | check_not_skipped" | |
4 | - "gsutil cp faraday-server_amd64.rpm ${GCLOUD_FILE_PATH}.rpm 2>&1 | check_not_skipped" | |
5 | - "gsutil setmeta -h x-goog-meta-commit:${CI_COMMIT_SHA} ${GCLOUD_FILE_PATH}.*" | |
6 | ||
7 | ||
8 | .google_storage_deb_rpm: | |
9 | stage: deploy | |
10 | image: 'google/cloud-sdk:latest' | |
11 | variables: | |
12 | FILE_BASENAME: faraday-server-community_amd64 | |
13 | GCLOUD_FILE_PATH: ${STORAGE_SPACE_BASE}${STORAGE_SPACE_ROUTE}/${FILE_BASENAME} | |
14 | dependencies: | |
15 | - generate_deb | |
16 | - generate_rpm | |
17 | ||
18 | google_storage_deb_rpm_dev: | |
19 | extends: .google_storage_deb_rpm | |
20 | variables: | |
21 | STORAGE_SPACE_BASE: gs://faraday-dev | |
22 | script: | |
23 | - cp $GCLOUD_STORAGE_KEY_FILE auth_file.json | |
24 | - *google_storage_deb_rpm_base | |
25 | - "gsutil setmeta -h x-goog-meta-branch:${CI_COMMIT_BRANCH} ${GCLOUD_FILE_PATH}.*" | |
26 | rules: | |
27 | - if: '$CI_COMMIT_REF_NAME =~ /^.*\/(master)$/' | |
28 | when: on_success | |
29 | - if: '$BUILD_TEST || $FULL_TEST || $DAILY_TEST' | |
30 | when: on_success | |
31 | - when: never |
0 | generate_deb: | |
1 | image: registry.gitlab.com/faradaysec/devops | |
2 | stage: build | |
3 | before_script: | |
4 | - git clone https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com/faradaysec/faraday-linux-installers-builder.git | |
5 | - mv py3.tar / | |
6 | - cd /; tar xf py3.tar; cd - | |
7 | ||
8 | script: | |
9 | - mkdir build_installer | |
10 | - cp -a faraday.tar.gz build_installer/. | |
11 | - cd build_installer | |
12 | - /bin/tar zxf faraday.tar.gz | |
13 | - cd faraday_copy | |
14 | - cp -r /nix . | |
15 | - mv ../../faraday-linux-installers-builder . | |
16 | - cd faraday-linux-installers-builder | |
17 | - git rev-parse HEAD | |
18 | - git clone https://github.com/jordansissel/fpm.git | |
19 | - cd fpm | |
20 | - git checkout d7b466787d17581bc723e474ecf6e18f48226031 | |
21 | - git apply ../fpm-patchs/fpm.virtualenv.patch | |
22 | - make gem | |
23 | - gem install --no-ri --no-rdoc fpm-1.11.0.gem | |
24 | - cd ../../ | |
25 | - POSTFIX=$(echo "$CI_COMMIT_BRANCH" | awk '{split($1,a,"_");split($1,b,"/"); if (a[3]!="y2k") if (b[2]=="dev"||b[2]=="master") print ""; else print "~"a[3]; else exit 1;}') | |
26 | - sh faraday-linux-installers-builder/build.sh $(eval $IMAGE_TAG)~$((`date '+%s%N'`/1000))$POSTFIX server deb | |
27 | - mv faraday-server_amd64.deb ../../faraday-server_amd64.deb | |
28 | needs: | |
29 | - job: generate_build_file | |
30 | artifacts: true | |
31 | - job: build_nix_python3 | |
32 | artifacts: true | |
33 | artifacts: | |
34 | name: 'faraday_$CI_COMMIT_REF_NAME.deb' | |
35 | paths: | |
36 | - "faraday-server_amd64.deb" | |
37 | expire_in: 15 days | |
38 | rules: | |
39 | - if: '$CI_COMMIT_REF_NAME =~ /^.*\/(master)$/' | |
40 | when: on_success | |
41 | - if: '$CI_COMMIT_TAG || $BUILD_TEST || $FULL_TEST || $DAILY_TEST' | |
42 | when: on_success | |
43 | - when: never | |
44 | ||
45 | ||
46 | generate_rpm: | |
47 | stage: build | |
48 | image: centos:7 | |
49 | before_script: | |
50 | - yum -y upgrade | |
51 | - yum -y install which git epel-release centos-release-scl | |
52 | - git clone https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com/faradaysec/faraday-linux-installers-builder.git | |
53 | - mv py3.tar / | |
54 | - cd /; tar xf py3.tar; cd - | |
55 | - yum -y install curl zsh mailcap libffi-devel openssl-devel openldap-devel libjpeg-devel postgresql-devel | |
56 | - yum groups -y install "Development Tools" | |
57 | - yum -y install rh-python36 rh-ruby23 rh-ruby23-ruby-devel | |
58 | - source /opt/rh/rh-ruby23/enable | |
59 | - export X_SCLS="`scl enable rh-ruby23 'echo $X_SCLS'`" | |
60 | - source /opt/rh/rh-python36/enable | |
61 | - pip install virtualenv | |
62 | - pip install virtualenv-tools3 | |
63 | script: | |
64 | - mkdir build_installer | |
65 | - cp -a faraday.tar.gz build_installer/. | |
66 | - cd build_installer | |
67 | - /bin/tar zxf faraday.tar.gz | |
68 | - cd faraday_copy | |
69 | - cp -r /nix . | |
70 | - mv ../../faraday-linux-installers-builder . | |
71 | - cd faraday-linux-installers-builder | |
72 | - git rev-parse HEAD | |
73 | - git clone https://github.com/jordansissel/fpm.git | |
74 | - cd fpm | |
75 | - git checkout d7b466787d17581bc723e474ecf6e18f48226031 | |
76 | - git apply ../fpm-patchs/fpm.virtualenv.patch | |
77 | - make gem | |
78 | - gem install --no-ri --no-rdoc fpm-1.11.0.gem | |
79 | - cd ../../ | |
80 | - sh faraday-linux-installers-builder/build.sh $(eval $IMAGE_TAG) server rpm | |
81 | - mv faraday-server_amd64.rpm ../../faraday-server_amd64.rpm | |
82 | needs: | |
83 | - job: generate_build_file | |
84 | artifacts: true | |
85 | - job: build_nix_python3 | |
86 | artifacts: true | |
87 | artifacts: | |
88 | name: 'faraday_$CI_COMMIT_REF_NAME.rpm' | |
89 | paths: | |
90 | - "faraday-server_amd64.rpm" | |
91 | expire_in: 15 days | |
92 | rules: | |
93 | - if: '$CI_COMMIT_REF_NAME =~ /^.*\/(master)$/' | |
94 | when: on_success | |
95 | - if: '$CI_COMMIT_TAG || $BUILD_TEST || $FULL_TEST || $DAILY_TEST' | |
96 | when: on_success | |
97 | - when: never | |
98 | ||
99 | generate_docker_tar_gz: | |
100 | stage: build | |
101 | image: nixorg/nix | |
102 | script: | |
103 | - nix-env -if pynixify/nixpkgs.nix -A cachix | |
104 | - mkdir -p ~/.config/cachix | |
105 | - export USER=$(whoami) | |
106 | - echo "$CACHIX_CONFG" >~/.config/cachix/cachix.dhall | |
107 | - cachix use faradaysec | |
108 | - nix-build ./release.nix -A dockerImage --argstr dockerName $CI_REGISTRY_IMAGE --argstr dockerTag latest | |
109 | - cp $(readlink result) faraday-server-docker.tar.gz | |
110 | artifacts: | |
111 | paths: | |
112 | - faraday-server-docker.tar.gz | |
113 | rules: | |
114 | - if: '$CI_COMMIT_REF_NAME =~ /^.*\/(master)$/' | |
115 | when: on_success | |
116 | - if: '$CI_COMMIT_TAG || $BUILD_TEST || $FULL_TEST || $DAILY_TEST' | |
117 | when: on_success |
0 | generate_build_file: | |
1 | image: registry.gitlab.com/faradaysec/devops | |
2 | stage: pre_build | |
3 | script: | |
4 | - "/bin/mkdir faraday_copy" | |
5 | - "/usr/bin/rsync -aq --exclude 'faraday_copy' --exclude '.cache' . faraday_copy" | |
6 | - "/bin/tar -zcf faraday.tar.gz faraday_copy" | |
7 | rules: | |
8 | - if: '$CI_COMMIT_REF_NAME =~ /^.*\/(master)$/' | |
9 | when: on_success | |
10 | - if: '$CI_COMMIT_TAG || $BUILD_TEST || $FULL_TEST || $DAILY_TEST' | |
11 | when: on_success | |
12 | - when: never | |
13 | artifacts: | |
14 | name: 'faraday' | |
15 | paths: | |
16 | - "faraday.tar.gz" | |
17 | expire_in: 15 days | |
18 | ||
19 | build_nix_python3: | |
20 | image: nixorg/nix | |
21 | stage: pre_build | |
22 | script: | |
23 | - nix-env -if pynixify/nixpkgs.nix -A cachix | |
24 | - mkdir -p ~/.config/cachix | |
25 | - export USER=$(whoami) | |
26 | - echo "$CACHIX_CONFG" >~/.config/cachix/cachix.dhall | |
27 | - cachix use faradaysec | |
28 | - nix-build | |
29 | - tar cf /py3.tar $(nix-store --query --requisites $(readlink result)) | |
30 | - mkdir -p /opt/faraday | |
31 | - cp -r $(readlink result)/* /opt/faraday | |
32 | - tar rvf /py3.tar /opt/faraday | |
33 | - mv /py3.tar $CI_PROJECT_DIR | |
34 | artifacts: | |
35 | name: python3 | |
36 | paths: | |
37 | - py3.tar | |
38 | expire_in: 15 days # in the future we don't need to expire this. | |
39 | rules: | |
40 | - if: '$CI_COMMIT_REF_NAME =~ /^.*\/(master)$/' | |
41 | when: on_success | |
42 | - if: '$CI_COMMIT_TAG || $BUILD_TEST || $FULL_TEST || $DAILY_TEST' | |
43 | when: on_success | |
44 | - when: never |
0 | smoke_test_deb: | |
1 | allow_failure: false | |
2 | stage: build_testing # TODO improve | |
3 | image: ubuntu:18.04 | |
4 | needs: | |
5 | - job: generate_deb | |
6 | artifacts: true | |
7 | script: | |
8 | - apt-get update -y | |
9 | - apt install -y sudo curl | |
10 | - apt-get install -y ./faraday-server_amd64.deb | |
11 | - which faraday-manage | |
12 | - faraday-manage show-urls | |
13 | - export FARADAY_HOME=/home/faraday | |
14 | - /opt/faraday/bin/faraday-server || true # create .faraday | |
15 | - "echo '[database]' >>~faraday/.faraday/config/server.ini" | |
16 | - echo "connection_string = postgresql+psycopg2://$POSTGRES_USER:$POSTGRES_PASSWORD@postgres/$POSTGRES_DB" >>~faraday/.faraday/config/server.ini | |
17 | - cat ~faraday/.faraday/config/server.ini | |
18 | - faraday-manage create-tables | |
19 | - /opt/faraday/bin/faraday-server & | |
20 | - sleep 5 | |
21 | - curl -v http://localhost:5985/_api/v2/info | |
22 | - faraday-manage status-check | |
23 | - kill $(cat ~faraday/.faraday/faraday-server-port-5985.pid) | |
24 | - jobs | |
25 | rules: | |
26 | - if: '$CI_COMMIT_REF_NAME =~ /^.*\/(master)$/' | |
27 | when: on_success | |
28 | - if: '$CI_COMMIT_TAG || $BUILD_TEST || $FULL_TEST || $DAILY_TEST' | |
29 | when: on_success | |
30 | - when: never |
0 | deploy_to_dev_environment: | |
1 | stage: deploy | |
2 | variables: | |
3 | FARADAY_DEB_NAME: faraday-server-community_amd64.deb | |
4 | DEPLOY_TO_DEV_ENV: "True" | |
5 | trigger: | |
6 | project: faradaysec/devops | |
7 | strategy: depend | |
8 | rules: | |
9 | - if: $DEPLOY_TO_DEV | |
10 | when: on_success | |
11 | - if: '$BUILD_TEST || $FULL_TEST || $DAILY_TEST' | |
12 | when: manual | |
13 | allow_failure: true | |
14 | - when: never |
0 | .docker-publish: | |
1 | stage: publish | |
2 | before_script: | |
3 | - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY | |
4 | - gzip -d faraday-server-docker.tar.gz | |
5 | - docker load -i faraday-server-docker.tar | |
6 | - export VERSION=$(eval $IMAGE_TAG) | |
7 | tags: | |
8 | - shell | |
9 | ||
10 | docker-publish-dev: | |
11 | extends: .docker-publish | |
12 | script: | |
13 | - docker image tag $CI_REGISTRY_IMAGE:latest $CI_REGISTRY_IMAGE:$VERSION | |
14 | - docker push "$CI_REGISTRY_IMAGE" | |
15 | rules: | |
16 | - if: '$CI_COMMIT_REF_NAME =~ /^.*\/(master)$/' | |
17 | when: on_success | |
18 | needs: # dev won't wait for any previous stage, it will deploy instantly, and | |
19 | # then run tests in docker image (To be done) | |
20 | - job: generate_docker_tar_gz | |
21 | artifacts: true | |
22 | ||
23 | docker-publish-prod: | |
24 | extends: .docker-publish | |
25 | variables: | |
26 | CI_REGISTRY_USER: $DOCKER_USER | |
27 | CI_REGISTRY_PASSWORD: $DOCKER_PASS | |
28 | CI_REGISTRY: docker.io | |
29 | CI_REGISTRY_IMAGE: index.docker.io/faradaysec/faraday | |
30 | script: | |
31 | - docker image tag registry.gitlab.com/faradaysec/faraday:latest $CI_REGISTRY_IMAGE:latest | |
32 | - docker push $CI_REGISTRY_IMAGE:latest | |
33 | - docker image tag $CI_REGISTRY_IMAGE:latest $CI_REGISTRY_IMAGE:$VERSION | |
34 | - docker push $CI_REGISTRY_IMAGE:$VERSION | |
35 | rules: | |
36 | - if: '$CI_COMMIT_TAG =~ /^white-v[0-9.]+$/' | |
37 | when: on_success | |
38 | dependencies: # prod will wait for any previous stage | |
39 | - generate_docker_tar_gz |
0 | tag_on_github: | |
1 | image: python:3 | |
2 | stage: publish | |
3 | before_script: | |
4 | script: | |
5 | - git config remote.github.url >/dev/null || git remote add github https://${GH_TOKEN}@github.com/infobyte/faraday.git | |
6 | - export FARADAY_VERSION=$(eval $IMAGE_TAG) | |
7 | - CHANGELOG/check_pre_tag.py | |
8 | - git push github white/dev:master | |
9 | - git tag v$FARADAY_VERSION -m "$(cat CHANGELOG/$FARADAY_VERSION/white.md)" | |
10 | - git push github v$FARADAY_VERSION | |
11 | - scripts/github_release.py | |
12 | rules: | |
13 | - if: '$CI_COMMIT_TAG =~ /^white-v[0-9.]+$/' | |
14 | when: on_success | |
15 | dependencies: | |
16 | - generate_deb | |
17 | - generate_rpm | |
18 | ||
19 | publish_pypi: | |
20 | image: python:3 | |
21 | stage: publish | |
22 | script: | |
23 | - apt-get update -qy | |
24 | - apt-get install twine -y | |
25 | - python setup.py sdist bdist_wheel | |
26 | - twine upload -u $PYPI_USER -p $PYPI_PASS dist/* --verbose | |
27 | rules: | |
28 | - if: '$CI_COMMIT_TAG =~ /^white-v[0-9.]+$/' | |
29 | when: on_success |
0 | test_hypothesis: | |
1 | tags: | |
2 | - hypothesis | |
3 | image: nixorg/nix | |
4 | stage: testing | |
5 | allow_failure: true | |
6 | script: | |
7 | - nix-env -if pynixify/nixpkgs.nix -A cachix | |
8 | - mkdir -p ~/.config/cachix | |
9 | - export USER=$(whoami) | |
10 | - echo "$CACHIX_CONFG" >~/.config/cachix/cachix.dhall | |
11 | - cachix use faradaysec | |
12 | - "echo 'hosts: files dns' >/etc/nsswitch.conf" | |
13 | - export LC_ALL=C.UTF-8 | |
14 | - export LANG=C.UTF-8 | |
15 | - mkdir -p ~/.faraday/config | |
16 | - cp tests/data/server.ini ~/.faraday/config | |
17 | - mkdir run_from | |
18 | - nix-shell --command "pytest tests -v --cov=faraday/server/api --disable-warnings --connection-string=postgresql+psycopg2://$POSTGRES_USER:$POSTGRES_PASSWORD@postgres/$POSTGRES_DB -m hypothesis" | |
19 | rules: | |
20 | - if: '$HYPO_TEST || $FULL_TEST || $DAILY_TEST' | |
21 | when: on_success | |
22 | - when: never |
0 | ||
1 | pylint: | |
2 | tags: | |
3 | - faradaytests | |
4 | image: nixorg/nix | |
5 | stage: testing # This should be after build_and_push_to_cachix to improve performance | |
6 | script: | |
7 | - nix-env -if pynixify/nixpkgs.nix -A cachix | |
8 | - mkdir -p ~/.config/cachix | |
9 | - export USER=$(whoami) | |
10 | - echo "$CACHIX_CONFG" >~/.config/cachix/cachix.dhall | |
11 | - cachix use faradaysec | |
12 | - nix-shell --command "pylint --rcfile=.pylintrc faraday" | tee pylint.txt | |
13 | - nix-env -if pynixify/nixpkgs.nix -A gnused | |
14 | - score=$(sed -n 's/^Your code has been rated at \([-0-9.]*\)\/.*/\1/p' pylint.txt) | |
15 | #- anybadge --label pylint --value=$score --file pylint.svg 4=red 6=orange 8=yellow 10=green | |
16 | artifacts: | |
17 | paths: | |
18 | - pylint.svg | |
19 | - pylint3.svg | |
20 | rules: | |
21 | - if: $BUILD_TEST | |
22 | when: never | |
23 | - if: '$CI_COMMIT_TAG' | |
24 | when: never | |
25 | - if: '$FULL_TEST || $DAILY_TEST' | |
26 | when: on_success | |
27 | - when: on_success | |
28 | ||
29 | .postgresql_test_nix_base: | |
30 | tags: | |
31 | - faradaytests | |
32 | stage: testing | |
33 | coverage: '/TOTAL\s+\d+\s+\d+\s+(\d+%)/' | |
34 | script: | |
35 | - nix-env -if pynixify/nixpkgs.nix -A cachix | |
36 | - mkdir -p ~/.config/cachix | |
37 | - export USER=$(whoami) | |
38 | - echo "$CACHIX_CONFG" >~/.config/cachix/cachix.dhall | |
39 | - cachix use faradaysec | |
40 | - "echo 'hosts: files dns' >/etc/nsswitch.conf" | |
41 | - export LC_ALL=C.UTF-8 | |
42 | - export LANG=C.UTF-8 | |
43 | - mkdir -p ~/.faraday/config | |
44 | - cp tests/data/server.ini ~/.faraday/config | |
45 | - mkdir run_from | |
46 | - nix-shell --command "cd run_from && pytest ../tests -v --capture=sys --cov=../faraday/server --color=yes --disable-warnings --connection-string=postgresql+psycopg2://$POSTGRES_USER:$POSTGRES_PASSWORD@postgres/$POSTGRES_DB" | |
47 | artifacts: | |
48 | when: on_failure | |
49 | paths: | |
50 | - ~/.faraday/logs/faraday-server.log | |
51 | needs: | |
52 | - job: build_and_push_to_cachix | |
53 | artifacts: false | |
54 | # Speed up tests | |
55 | rules: | |
56 | - if: $BUILD_TEST | |
57 | when: never | |
58 | - if: '$FULL_TEST || $DAILY_TEST' | |
59 | when: on_success | |
60 | - if: '$CI_COMMIT_TAG' | |
61 | when: never | |
62 | - when: on_success | |
63 | ||
64 | .sqlite_test_nix_base: | |
65 | tags: | |
66 | - faradaytests | |
67 | stage: testing | |
68 | coverage: '/TOTAL\s+\d+\s+\d+\s+(\d+%)/' | |
69 | script: | |
70 | - nix-env -if pynixify/nixpkgs.nix -A cachix | |
71 | - mkdir -p ~/.config/cachix | |
72 | - export USER=$(whoami) | |
73 | - echo "$CACHIX_CONFG" >~/.config/cachix/cachix.dhall | |
74 | - cachix use faradaysec | |
75 | - "echo 'hosts: files dns' >/etc/nsswitch.conf" | |
76 | - export LC_ALL=C.UTF-8 | |
77 | - export LANG=C.UTF-8 | |
78 | - mkdir -p ~/.faraday/config | |
79 | - cp tests/data/server.ini ~/.faraday/config | |
80 | - mkdir run_from | |
81 | - nix-shell --command "cd run_from && pytest ../tests --capture=sys -v --cov=../faraday/server --color=yes --disable-warnings" | |
82 | artifacts: | |
83 | when: on_failure | |
84 | paths: | |
85 | - dist/* | |
86 | needs: | |
87 | - job: build_and_push_to_cachix | |
88 | artifacts: false | |
89 | rules: | |
90 | - if: $BUILD_TEST | |
91 | when: never | |
92 | - if: '$CI_COMMIT_TAG' | |
93 | when: never | |
94 | - if: '$FULL_TEST || $DAILY_TEST' | |
95 | when: on_success | |
96 | - when: on_success | |
97 | ||
98 | sqlite_test_nix: | |
99 | extends: .sqlite_test_nix_base | |
100 | image: nixorg/nix | |
101 | ||
102 | ||
103 | postgresql_test_nix: | |
104 | extends: .postgresql_test_nix_base | |
105 | image: nixorg/nix |
0 | agent_integration_and_db_regression: | |
1 | stage: post_testing | |
2 | variables: | |
3 | FARADAY_REF: $CI_COMMIT_REF_NAME | |
4 | FARADAY_REGRESSION: "True" | |
5 | trigger: | |
6 | project: faradaysec/devops | |
7 | strategy: depend | |
8 | rules: | |
9 | - if: '$CI_COMMIT_REF_NAME =~ /^.*\/(master)$/' | |
10 | when: on_success | |
11 | - if: '$INTEGRATION || $FULL_TEST || $DAILY_TEST' | |
12 | when: on_success | |
13 | - when: never |
0 | merge_conflict_check: | |
1 | tags: | |
2 | - faradaytests | |
3 | image: python:3 | |
4 | stage: pre_testing | |
5 | allow_failure: true | |
6 | script: | |
7 | - git config --global user.email "[email protected]" | |
8 | - git config --global user.name "Mergerbot" | |
9 | - python3 scripts/merge-conflict-detector.py | |
10 | rules: | |
11 | - if: '$CI_COMMIT_REF_NAME =~ /^.*\/(master|dev)$/' | |
12 | when: on_success | |
13 | - if: '$CI_COMMIT_TAG' | |
14 | when: never | |
15 | - when: never | |
16 | ||
17 | sanity_check: | |
18 | tags: | |
19 | - faradaytests | |
20 | image: python:3 | |
21 | stage: pre_testing | |
22 | script: | |
23 | - bash scripts/sanity_check_commit.sh | |
24 | - scripts/sanity_check_file.py --mode=ls | |
25 | rules: | |
26 | - if: '$CI_COMMIT_REF_NAME =~ /^.*\/(master|dev)$/' | |
27 | when: on_success | |
28 | - if: '$CI_COMMIT_TAG' | |
29 | when: never | |
30 | - when: never | |
31 | ||
32 | migration_sanity_check: | |
33 | tags: | |
34 | - faradaytests | |
35 | image: python:3 | |
36 | stage: pre_testing | |
37 | script: | |
38 | - scripts/model_check.py | |
39 | - pip install . | |
40 | - cd faraday | |
41 | - $(alembic branches) | |
42 | rules: | |
43 | - if: '$CI_COMMIT_REF_NAME =~ /^.*\/(master|dev)$/' | |
44 | when: always | |
45 | - if: '$CI_COMMIT_TAG' | |
46 | when: never | |
47 | - when: never | |
48 | ||
49 | bandit: | |
50 | tags: | |
51 | - faradaytests | |
52 | image: python:3 | |
53 | stage: pre_testing | |
54 | script: | |
55 | - pip3 install virtualenv | |
56 | - virtualenv -p python3 faraday_venv | |
57 | - source faraday_venv/bin/activate | |
58 | - pip3 install bandit | |
59 | - "bandit -r ${CI_PROJECT_DIR}/faraday --format custom --skip B101 --msg-template \ | |
60 | \"{abspath}:{line}: {test_id}[bandit]: {severity}: {msg}\"" | |
61 | rules: | |
62 | - if: '$CI_COMMIT_TAG' | |
63 | when: never | |
64 | - when: on_success | |
65 | ||
66 | build_and_push_to_cachix: | |
67 | tags: | |
68 | - faradaytests | |
69 | image: nixorg/nix | |
70 | stage: pre_testing | |
71 | variables: | |
72 | # Note: this size has to fit both our community, professional and corporate versions | |
73 | MAX_CLOSURE_SIZE_IN_MB: 850 | |
74 | script: | |
75 | - nix-env -if pynixify/nixpkgs.nix -A cachix | |
76 | - nix-env -if pynixify/nixpkgs.nix -A gawk | |
77 | - mkdir -p ~/.config/cachix | |
78 | - export USER=$(whoami) | |
79 | - echo "$CACHIX_CONFG" >~/.config/cachix/cachix.dhall | |
80 | - cachix use faradaysec | |
81 | - nix-build | cachix push faradaysec | |
82 | - ./scripts/check-closure-size ./result | |
83 | rules: | |
84 | - if: '$FULL_TEST || $DAILY_TEST' | |
85 | when: on_success | |
86 | - when: always | |
87 | ||
88 | flake8: | |
89 | image: python:3 | |
90 | stage: pre_testing | |
91 | script: | |
92 | - pip install flake8 | |
93 | - flake8 . | |
94 | rules: | |
95 | - if: '$CI_COMMIT_TAG' | |
96 | when: never | |
97 | - when: on_success | |
98 | ||
99 | no-format-str: | |
100 | image: python:3 | |
101 | stage: pre_testing | |
102 | script: | |
103 | - pip install flynt | |
104 | - flynt -df faraday tests | |
105 | rules: | |
106 | - if: '$CI_COMMIT_TAG' | |
107 | when: never | |
108 | - when: on_success |
0 | .unit_tests_base: | |
1 | tags: | |
2 | - faradaytests | |
3 | stage: testing | |
4 | coverage: '/TOTAL\s+\d+\s+\d+\s+(\d+%)/' | |
5 | script: | |
6 | - export LC_ALL=C.UTF-8 | |
7 | - export LANG=C.UTF-8 | |
8 | - mkdir -p ~/.faraday/config | |
9 | - cp tests/data/server.ini ~/.faraday/config | |
10 | - mkdir run_from | |
11 | - source faraday_venv/bin/activate | |
12 | - cd run_from && pytest ../tests -v --capture=sys --cov=../faraday/server --color=yes --disable-warnings --connection-string=postgresql+psycopg2://$POSTGRES_USER:$POSTGRES_PASSWORD@postgres/$POSTGRES_DB | |
13 | needs: [] # Speed up tests | |
14 | artifacts: | |
15 | when: on_failure | |
16 | paths: | |
17 | - ~/.faraday/logs/faraday-server.log | |
18 | ||
19 | .latest_unit_test_base: | |
20 | extends: .unit_tests_base | |
21 | before_script: | |
22 | - pip install virtualenv | |
23 | - virtualenv faraday_venv | |
24 | - source faraday_venv/bin/activate | |
25 | - pip install . | |
26 | - pip install -r requirements_dev.txt | |
27 | - pip install pytest-cov | |
28 | - pip install pyyaml | |
29 | after_script: | |
30 | - source faraday_venv/bin/activate | |
31 | - pip freeze | |
32 | allow_failure: true | |
33 | rules: | |
34 | #- if: '$FULL_TEST || $DAILY_TEST || $ALPHA_TEST' | |
35 | - if: '$ALPHA_TEST' # FOR NOW, ASKED TO NOT CHARGE CI WORKER | |
36 | when: on_success | |
37 | - if: '$CI_COMMIT_REF_NAME =~ /^.*\/(master|dev)$/' | |
38 | when: never # FOR NOW, ASKED TO NOT CHARGE CI WORKER | |
39 | #when: on_success | |
40 | - when: never | |
41 | ||
42 | ||
43 | .alpha_unit_test_base: | |
44 | extends: .unit_tests_base | |
45 | before_script: | |
46 | - pip install virtualenv | |
47 | - virtualenv faraday_venv | |
48 | - source faraday_venv/bin/activate | |
49 | - pip install --pre . | |
50 | - pip install --pre -r requirements_dev.txt | |
51 | - pip install --pre pytest-cov | |
52 | - pip install --pre pyyaml | |
53 | after_script: | |
54 | - source faraday_venv/bin/activate | |
55 | - pip freeze | |
56 | allow_failure: true | |
57 | rules: | |
58 | #- if: '$FULL_TEST || $DAILY_TEST || $ALPHA_TEST' | |
59 | - if: '$ALPHA_TEST' # FOR NOW, ASKED TO FIX #6474 first | |
60 | when: on_success | |
61 | - if: '$CI_COMMIT_REF_NAME =~ /^.*\/(master|dev)$/' | |
62 | when: never # FOR NOW, ASKED TO FIX #6474 first | |
63 | #when: on_success | |
64 | - when: never | |
65 | ||
66 | unit_test 3.7: | |
67 | extends: .latest_unit_test_base | |
68 | image: python:3.7 | |
69 | ||
70 | unit_test 3.8: | |
71 | extends: .latest_unit_test_base | |
72 | image: python:3.8 | |
73 | ||
74 | unit_test 3.9: | |
75 | extends: .latest_unit_test_base | |
76 | image: python:3.9-rc | |
77 | ||
78 | alpha_unit_test 3.7: | |
79 | extends: .alpha_unit_test_base | |
80 | image: python:3.7 | |
81 | ||
82 | alpha_unit_test 3.8: | |
83 | extends: .alpha_unit_test_base | |
84 | image: python:3.8 | |
85 | rules: | |
86 | #- if: '$FULL_TEST || $DAILY_TEST || $ALPHA_TEST' | |
87 | - if: '$ALPHA_TEST' | |
88 | when: on_success | |
89 | - if: '$CI_COMMIT_REF_NAME =~ /^.*\/(master|dev)$/' | |
90 | when: never #on_success REACTIVATE WHEN HAVE TIME TO CHECK | |
91 | - when: never | |
92 | ||
93 | alpha_unit_test 3.9: | |
94 | extends: .alpha_unit_test_base | |
95 | image: python:3.9-rc |
0 | .google_storage_deb_rpm_base: &google_storage_deb_rpm_base | |
1 | - check_not_skipped(){ grep -v 'Skipping'; } | |
2 | - gcloud auth activate-service-account --key-file auth_file.json | |
3 | - "gsutil cp faraday-server_amd64.deb ${GCLOUD_FILE_PATH}_amd64.deb 2>&1 | check_not_skipped" | |
4 | - "gsutil cp faraday-server_amd64.rpm ${GCLOUD_FILE_PATH}_amd64.rpm 2>&1 | check_not_skipped" | |
5 | - "gsutil cp faraday-server-docker.tar.gz ${GCLOUD_FILE_PATH}-docker.tar.gz 2>&1 | check_not_skipped" | |
6 | - "gsutil setmeta -h x-goog-meta-commit:${CI_COMMIT_SHA} ${GCLOUD_FILE_PATH}*.*" | |
7 | - export DEB_HASH=$(sha256sum faraday-server_amd64.deb | awk '{print $1}') | |
8 | - export RPM_HASH=$(sha256sum faraday-server_amd64.rpm | awk '{print $1}') | |
9 | - export DOCKER_HASH=$(sha256sum faraday-server-docker.tar.gz| awk '{print $1}') | |
10 | - "gsutil setmeta -h x-goog-meta-sha256_hash:${DEB_HASH} ${GCLOUD_FILE_PATH}_amd64.deb 2>&1 | check_not_skipped" | |
11 | - "gsutil setmeta -h x-goog-meta-sha256_hash:${RPM_HASH} ${GCLOUD_FILE_PATH}_amd64.rpm 2>&1 | check_not_skipped" | |
12 | - "gsutil setmeta -h x-goog-meta-sha256_hash:${DOCKER_HASH} ${GCLOUD_FILE_PATH}-docker.tar.gz 2>&1 | check_not_skipped" | |
13 | ||
14 | ||
15 | .google_storage_deb_rpm: | |
16 | stage: upload | |
17 | image: 'google/cloud-sdk:latest' | |
18 | variables: | |
19 | FILE_BASENAME: faraday-server-community | |
20 | GCLOUD_FILE_PATH: ${STORAGE_SPACE_BASE}${STORAGE_SPACE_ROUTE}/${FILE_BASENAME} | |
21 | ||
22 | google_storage_deb_rpm_dev: | |
23 | extends: .google_storage_deb_rpm | |
24 | variables: | |
25 | STORAGE_SPACE_BASE: gs://faraday-dev | |
26 | script: | |
27 | - cp $GCLOUD_STORAGE_KEY_FILE auth_file.json | |
28 | - *google_storage_deb_rpm_base | |
29 | - "gsutil setmeta -h x-goog-meta-branch:${CI_COMMIT_BRANCH} ${GCLOUD_FILE_PATH}*.*" | |
30 | rules: | |
31 | - if: '$CI_COMMIT_REF_NAME =~ /^.*\/(master)$/' | |
32 | when: on_success | |
33 | - if: '$BUILD_TEST || $FULL_TEST || $DAILY_TEST' | |
34 | when: on_success | |
35 | - when: never | |
36 | needs: | |
37 | - job: generate_deb | |
38 | artifacts: true | |
39 | - job: generate_rpm | |
40 | artifacts: true | |
41 | - job: generate_docker_tar_gz | |
42 | artifacts: true |
0 | qa_integration: | |
1 | stage: upload_testing | |
2 | variables: | |
3 | REMOTE_BRANCH: $CI_COMMIT_BRANCH | |
4 | MAIN_COMMIT_SHA: $CI_COMMIT_SHA | |
5 | trigger: | |
6 | project: faradaysec/qa/automation | |
7 | strategy: depend | |
8 | branch: develop | |
9 | rules: | |
10 | - if: '$CI_COMMIT_REF_NAME =~ /^.*\/(master)$/' | |
11 | when: on_success | |
12 | - if: '$CI_COMMIT_TAG || $BUILD_TEST || $FULL_TEST || $DAILY_TEST' | |
13 | when: on_success |
18 | 18 | when: never |
19 | 19 | - when: always |
20 | 20 | |
21 | ||
22 | ||
23 | 21 | cache: |
24 | 22 | paths: |
25 | 23 | - "$CI_PROJECT_DIR/.cache/pip" |
29 | 27 | - mkdir -pv $APT_CACHE_DIR |
30 | 28 | |
31 | 29 | include: |
32 | - local: .gitlab/ci/.set-tag-gitlab-ci.yml | |
33 | - local: .gitlab/ci/.storage-gitlab-ci.yml | |
30 | - local: .gitlab/ci/testing/.pretesting-gitlab-ci.yml | |
31 | - local: .gitlab/ci/testing/.nix-testing-gitlab-ci.yml | |
32 | - local: .gitlab/ci/testing/.venv-testing-gitlab-ci.yml | |
33 | - local: .gitlab/ci/testing/.hypothesis-nix-gitlab-ci.yml | |
34 | - local: .gitlab/ci/testing/.posttesting-gitlab-ci.yml | |
35 | ||
36 | - local: .gitlab/ci/build-ci/.prebuild-gitlab-ci.yml | |
37 | - local: .gitlab/ci/build-ci/.build-gitlab-ci.yml | |
38 | - local: .gitlab/ci/build-ci/.testing-gitlab-ci.yml | |
39 | ||
40 | - local: .gitlab/ci/upload/.storage-gitlab-ci.yml | |
41 | - local: .gitlab/ci/upload/.testing-gitlab-ci.yml | |
42 | ||
43 | - local: .gitlab/ci/deploy/deploy-gitlab-ci.yml | |
44 | ||
45 | - local: .gitlab/ci/publish/.set-tag-gitlab-ci.yml | |
46 | - local: .gitlab/ci/publish/.docker-publish-gitlab-ci.yml | |
34 | 47 | |
35 | 48 | stages: |
36 | 49 | - pre_testing |
37 | 50 | - testing |
38 | 51 | - post_testing |
39 | - build_faraday | |
52 | - pre_build | |
40 | 53 | - build |
41 | - distro_testing | |
54 | - build_testing | |
55 | - upload | |
56 | - upload_testing | |
42 | 57 | - deploy |
43 | - post_deploy_testing | |
44 | 58 | - publish |
45 | 59 | |
46 | 60 | services: |
47 | 61 | - postgres:latest |
48 | ||
49 | merge_conflict_check: | |
50 | tags: | |
51 | - faradaytests | |
52 | image: python:3 | |
53 | stage: pre_testing | |
54 | allow_failure: true | |
55 | script: | |
56 | - git config --global user.email "[email protected]" | |
57 | - git config --global user.name "Mergerbot" | |
58 | - python3 scripts/merge-conflict-detector.py | |
59 | rules: | |
60 | - if: '$CI_COMMIT_REF_NAME =~ /^.*\/(master|dev)$/' | |
61 | when: on_success | |
62 | - when: never | |
63 | ||
64 | sanity_check: | |
65 | tags: | |
66 | - faradaytests | |
67 | image: python:3 | |
68 | stage: pre_testing | |
69 | script: | |
70 | - bash scripts/sanity_check_commit.sh | |
71 | - scripts/sanity_check_file.py --mode=ls | |
72 | rules: | |
73 | - when: on_success | |
74 | ||
75 | bandit: | |
76 | tags: | |
77 | - faradaytests | |
78 | image: python:3 | |
79 | stage: pre_testing | |
80 | script: | |
81 | - pip3 install virtualenv | |
82 | - virtualenv -p python3 faraday_venv | |
83 | - source faraday_venv/bin/activate | |
84 | - pip3 install bandit | |
85 | - "bandit -r ${CI_PROJECT_DIR}/faraday --format custom --skip B101 --msg-template \ | |
86 | \"{abspath}:{line}: {test_id}[bandit]: {severity}: {msg}\"" | |
87 | ||
88 | build_and_push_to_cachix: | |
89 | tags: | |
90 | - faradaytests | |
91 | image: nixorg/nix | |
92 | stage: pre_testing | |
93 | script: | |
94 | - nix-env -if pynixify/nixpkgs.nix -A cachix | |
95 | - mkdir -p ~/.config/cachix | |
96 | - export USER=$(whoami) | |
97 | - echo "$CACHIX_CONFG" >~/.config/cachix/cachix.dhall | |
98 | - cachix use faradaysec | |
99 | - nix-build | cachix push faradaysec | |
100 | rules: | |
101 | - if: '$FULL_TEST || $DAILY_TEST' | |
102 | when: on_success | |
103 | - when: always | |
104 | ||
105 | pylint: | |
106 | tags: | |
107 | - faradaytests | |
108 | image: nixorg/nix | |
109 | stage: testing # This should be after build_and_push_to_cachix to improve performance | |
110 | script: | |
111 | - nix-env -if pynixify/nixpkgs.nix -A cachix | |
112 | - mkdir -p ~/.config/cachix | |
113 | - export USER=$(whoami) | |
114 | - echo "$CACHIX_CONFG" >~/.config/cachix/cachix.dhall | |
115 | - cachix use faradaysec | |
116 | - nix-shell --command "pylint --rcfile=.pylintrc faraday" | tee pylint.txt | |
117 | - nix-env -if pynixify/nixpkgs.nix -A gnused | |
118 | - score=$(sed -n 's/^Your code has been rated at \([-0-9.]*\)\/.*/\1/p' pylint.txt) | |
119 | #- anybadge --label pylint --value=$score --file pylint.svg 4=red 6=orange 8=yellow 10=green | |
120 | artifacts: | |
121 | paths: | |
122 | - pylint.svg | |
123 | - pylint3.svg | |
124 | rules: | |
125 | - if: $BUILD_TEST | |
126 | when: never | |
127 | - if: '$FULL_TEST || $DAILY_TEST' | |
128 | when: on_success | |
129 | - when: on_success | |
130 | ||
131 | .postgresql_test_nix_base: | |
132 | tags: | |
133 | - faradaytests | |
134 | stage: testing | |
135 | coverage: '/TOTAL\s+\d+\s+\d+\s+(\d+%)/' | |
136 | script: | |
137 | - nix-env -if pynixify/nixpkgs.nix -A cachix | |
138 | - mkdir -p ~/.config/cachix | |
139 | - export USER=$(whoami) | |
140 | - echo "$CACHIX_CONFG" >~/.config/cachix/cachix.dhall | |
141 | - cachix use faradaysec | |
142 | - "echo 'hosts: files dns' >/etc/nsswitch.conf" | |
143 | - export LC_ALL=C.UTF-8 | |
144 | - export LANG=C.UTF-8 | |
145 | - mkdir -p ~/.faraday/config | |
146 | - cp tests/data/server.ini ~/.faraday/config | |
147 | - mkdir run_from | |
148 | - nix-shell --command "cd run_from && pytest ../tests -v --capture=sys --cov=../faraday/server --color=yes --disable-warnings --connection-string=postgresql+psycopg2://$POSTGRES_USER:$POSTGRES_PASSWORD@postgres/$POSTGRES_DB" | |
149 | artifacts: | |
150 | when: on_failure | |
151 | paths: | |
152 | - ~/.faraday/logs/faraday-server.log | |
153 | rules: | |
154 | - if: $BUILD_TEST | |
155 | when: never | |
156 | - if: '$FULL_TEST || $DAILY_TEST' | |
157 | when: on_success | |
158 | - when: on_success | |
159 | ||
160 | .sqlite_test_nix_base: | |
161 | tags: | |
162 | - faradaytests | |
163 | stage: testing | |
164 | coverage: '/TOTAL\s+\d+\s+\d+\s+(\d+%)/' | |
165 | script: | |
166 | - nix-env -if pynixify/nixpkgs.nix -A cachix | |
167 | - mkdir -p ~/.config/cachix | |
168 | - export USER=$(whoami) | |
169 | - echo "$CACHIX_CONFG" >~/.config/cachix/cachix.dhall | |
170 | - cachix use faradaysec | |
171 | - "echo 'hosts: files dns' >/etc/nsswitch.conf" | |
172 | - export LC_ALL=C.UTF-8 | |
173 | - export LANG=C.UTF-8 | |
174 | - mkdir -p ~/.faraday/config | |
175 | - cp tests/data/server.ini ~/.faraday/config | |
176 | - mkdir run_from | |
177 | - nix-shell --command "cd run_from && pytest ../tests --capture=sys -v --cov=../faraday/server --color=yes --disable-warnings" | |
178 | artifacts: | |
179 | when: on_failure | |
180 | paths: | |
181 | - dist/* | |
182 | rules: | |
183 | - if: $BUILD_TEST | |
184 | when: never | |
185 | - if: '$FULL_TEST || $DAILY_TEST' | |
186 | when: on_success | |
187 | - when: on_success | |
188 | ||
189 | sqlite_test_nix: | |
190 | extends: .sqlite_test_nix_base | |
191 | image: nixorg/nix | |
192 | ||
193 | ||
194 | postgresql_test_nix: | |
195 | extends: .postgresql_test_nix_base | |
196 | image: nixorg/nix | |
197 | ||
198 | ||
199 | .unit_tests_base: | |
200 | tags: | |
201 | - faradaytests | |
202 | stage: testing | |
203 | coverage: '/TOTAL\s+\d+\s+\d+\s+(\d+%)/' | |
204 | script: | |
205 | - export LC_ALL=C.UTF-8 | |
206 | - export LANG=C.UTF-8 | |
207 | - mkdir -p ~/.faraday/config | |
208 | - cp tests/data/server.ini ~/.faraday/config | |
209 | - mkdir run_from | |
210 | - source faraday_venv/bin/activate | |
211 | - cd run_from && pytest ../tests -v --capture=sys --cov=../faraday/server --color=yes --disable-warnings --connection-string=postgresql+psycopg2://$POSTGRES_USER:$POSTGRES_PASSWORD@postgres/$POSTGRES_DB | |
212 | artifacts: | |
213 | when: on_failure | |
214 | paths: | |
215 | - ~/.faraday/logs/faraday-server.log | |
216 | ||
217 | .latest_unit_test_base: | |
218 | extends: .unit_tests_base | |
219 | before_script: | |
220 | - pip install virtualenv | |
221 | - virtualenv faraday_venv | |
222 | - source faraday_venv/bin/activate | |
223 | - pip install . | |
224 | - pip install -r requirements_dev.txt | |
225 | - pip install pytest-cov | |
226 | - pip install pyyaml | |
227 | after_script: | |
228 | - source faraday_venv/bin/activate | |
229 | - pip freeze | |
230 | allow_failure: true | |
231 | rules: | |
232 | #- if: '$FULL_TEST || $DAILY_TEST || $ALPHA_TEST' | |
233 | - if: '$ALPHA_TEST' # FOR NOW, ASKED TO NOT CHARGE CI WORKER | |
234 | when: on_success | |
235 | - if: '$CI_COMMIT_REF_NAME =~ /^.*\/(master|dev)$/' | |
236 | when: never # FOR NOW, ASKED TO NOT CHARGE CI WORKER | |
237 | #when: on_success | |
238 | - when: never | |
239 | ||
240 | ||
241 | .alpha_unit_test_base: | |
242 | extends: .unit_tests_base | |
243 | before_script: | |
244 | - pip install virtualenv | |
245 | - virtualenv faraday_venv | |
246 | - source faraday_venv/bin/activate | |
247 | - pip install --pre . | |
248 | - pip install --pre -r requirements_dev.txt | |
249 | - pip install --pre pytest-cov | |
250 | - pip install --pre pyyaml | |
251 | after_script: | |
252 | - source faraday_venv/bin/activate | |
253 | - pip freeze | |
254 | allow_failure: true | |
255 | rules: | |
256 | #- if: '$FULL_TEST || $DAILY_TEST || $ALPHA_TEST' | |
257 | - if: '$ALPHA_TEST' # FOR NOW, ASKED TO FIX #6474 first | |
258 | when: on_success | |
259 | - if: '$CI_COMMIT_REF_NAME =~ /^.*\/(master|dev)$/' | |
260 | when: never # FOR NOW, ASKED TO FIX #6474 first | |
261 | #when: on_success | |
262 | - when: never | |
263 | ||
264 | unit_test 3.7: | |
265 | extends: .latest_unit_test_base | |
266 | image: python:3.7 | |
267 | ||
268 | unit_test 3.8: | |
269 | extends: .latest_unit_test_base | |
270 | image: python:3.8 | |
271 | ||
272 | unit_test 3.9: | |
273 | extends: .latest_unit_test_base | |
274 | image: python:3.9-rc | |
275 | ||
276 | alpha_unit_test 3.7: | |
277 | extends: .alpha_unit_test_base | |
278 | image: python:3.7 | |
279 | ||
280 | alpha_unit_test 3.8: | |
281 | extends: .alpha_unit_test_base | |
282 | image: python:3.8 | |
283 | rules: | |
284 | - if: '$FULL_TEST || $DAILY_TEST || $ALPHA_TEST' | |
285 | when: on_success | |
286 | - if: '$CI_COMMIT_REF_NAME =~ /^.*\/(master|dev)$/' | |
287 | when: on_success | |
288 | - when: never | |
289 | ||
290 | alpha_unit_test 3.9: | |
291 | extends: .alpha_unit_test_base | |
292 | image: python:3.9-rc | |
293 | ||
294 | build_nix_python3: | |
295 | image: nixorg/nix | |
296 | stage: build_faraday | |
297 | script: | |
298 | - nix-env -if pynixify/nixpkgs.nix -A cachix | |
299 | - mkdir -p ~/.config/cachix | |
300 | - export USER=$(whoami) | |
301 | - echo "$CACHIX_CONFG" >~/.config/cachix/cachix.dhall | |
302 | - cachix use faradaysec | |
303 | - nix-build | |
304 | - tar cf /py3.tar $(nix-store --query --requisites $(readlink result)) | |
305 | - mkdir -p /opt/faraday | |
306 | - cp -r $(readlink result)/* /opt/faraday | |
307 | - tar rvf /py3.tar /opt/faraday | |
308 | - mv /py3.tar $CI_PROJECT_DIR | |
309 | artifacts: | |
310 | name: python3 | |
311 | paths: | |
312 | - py3.tar | |
313 | expire_in: 15 days # in the future we don't need to expire this. | |
314 | rules: | |
315 | - if: '$CI_COMMIT_REF_NAME =~ /^.*\/(master)$/' | |
316 | when: on_success | |
317 | - if: '$CI_COMMIT_TAG || $BUILD_TEST || $FULL_TEST || $DAILY_TEST' | |
318 | when: on_success | |
319 | - when: never | |
320 | ||
321 | generate_build_file: | |
322 | image: registry.gitlab.com/faradaysec/integrationrepo | |
323 | stage: build_faraday | |
324 | script: | |
325 | - "/bin/mkdir faraday_copy" | |
326 | - "/usr/bin/rsync -aq --exclude 'faraday_copy' --exclude '.cache' . faraday_copy" | |
327 | - "/bin/tar -zcf faraday.tar.gz faraday_copy" | |
328 | rules: | |
329 | - if: '$CI_COMMIT_REF_NAME =~ /^.*\/(master)$/' | |
330 | when: on_success | |
331 | - if: '$CI_COMMIT_TAG || $BUILD_TEST || $FULL_TEST || $DAILY_TEST' | |
332 | when: on_success | |
333 | - when: never | |
334 | artifacts: | |
335 | name: 'faraday' | |
336 | paths: | |
337 | - "faraday.tar.gz" | |
338 | expire_in: 15 days | |
339 | ||
340 | ||
341 | generate_deb: | |
342 | image: registry.gitlab.com/faradaysec/integrationrepo | |
343 | stage: build | |
344 | before_script: | |
345 | - git clone https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com/faradaysec/faraday-linux-installers-builder.git | |
346 | - mv py3.tar / | |
347 | - cd /; tar xf py3.tar; cd - | |
348 | ||
349 | script: | |
350 | - mkdir build_installer | |
351 | - cp -a faraday.tar.gz build_installer/. | |
352 | - cd build_installer | |
353 | - /bin/tar zxf faraday.tar.gz | |
354 | - cd faraday_copy | |
355 | - cp -r /nix . | |
356 | - mv ../../faraday-linux-installers-builder . | |
357 | - cd faraday-linux-installers-builder | |
358 | - git rev-parse HEAD | |
359 | - git clone https://github.com/jordansissel/fpm.git | |
360 | - cd fpm | |
361 | - git checkout d7b466787d17581bc723e474ecf6e18f48226031 | |
362 | - git apply ../fpm-patchs/fpm.virtualenv.patch | |
363 | - make gem | |
364 | - gem install --no-ri --no-rdoc fpm-1.11.0.gem | |
365 | - cd ../../ | |
366 | - sh faraday-linux-installers-builder/build.sh $(eval $IMAGE_TAG) server deb | |
367 | - mv faraday-server_amd64.deb ../../faraday-server_amd64.deb | |
368 | dependencies: | |
369 | - generate_build_file | |
370 | - build_nix_python3 | |
371 | artifacts: | |
372 | name: 'faraday_$CI_COMMIT_REF_NAME.deb' | |
373 | paths: | |
374 | - "faraday-server_amd64.deb" | |
375 | expire_in: 15 days | |
376 | rules: | |
377 | - if: '$CI_COMMIT_REF_NAME =~ /^.*\/(master)$/' | |
378 | when: on_success | |
379 | - if: '$CI_COMMIT_TAG || $BUILD_TEST || $FULL_TEST || $DAILY_TEST' | |
380 | when: on_success | |
381 | - when: never | |
382 | ||
383 | ||
384 | smoke_test_deb: | |
385 | allow_failure: false | |
386 | stage: distro_testing # TODO improve | |
387 | image: ubuntu:18.04 | |
388 | dependencies: | |
389 | - generate_deb | |
390 | script: | |
391 | - apt-get update -y | |
392 | - apt install -y sudo curl | |
393 | - apt-get install -y ./faraday-server_amd64.deb | |
394 | - which faraday-manage | |
395 | - faraday-manage show-urls | |
396 | - export FARADAY_HOME=/home/faraday | |
397 | - /opt/faraday/bin/faraday-server || true # create .faraday | |
398 | - "echo '[database]' >>~faraday/.faraday/config/server.ini" | |
399 | - echo "connection_string = postgresql+psycopg2://$POSTGRES_USER:$POSTGRES_PASSWORD@postgres/$POSTGRES_DB" >>~faraday/.faraday/config/server.ini | |
400 | - cat ~faraday/.faraday/config/server.ini | |
401 | - faraday-manage create-tables | |
402 | - /opt/faraday/bin/faraday-server & | |
403 | - sleep 5 | |
404 | - curl -v http://localhost:5985/_api/v2/info | |
405 | - faraday-manage status-check | |
406 | - kill $(cat ~faraday/.faraday/faraday-server-port-5985.pid) | |
407 | - jobs | |
408 | rules: | |
409 | - if: '$CI_COMMIT_REF_NAME =~ /^.*\/(master)$/' | |
410 | when: on_success | |
411 | - if: '$CI_COMMIT_TAG || $BUILD_TEST || $FULL_TEST || $DAILY_TEST' | |
412 | when: on_success | |
413 | - when: never | |
414 | ||
415 | generate_rpm: | |
416 | stage: build | |
417 | image: centos:7 | |
418 | before_script: | |
419 | - yum -y upgrade | |
420 | - yum -y install which git epel-release centos-release-scl | |
421 | - git clone https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com/faradaysec/faraday-linux-installers-builder.git | |
422 | - mv py3.tar / | |
423 | - cd /; tar xf py3.tar; cd - | |
424 | - yum -y install curl zsh mailcap libffi-devel openssl-devel openldap-devel libjpeg-devel postgresql-devel | |
425 | - yum groups -y install "Development Tools" | |
426 | - yum -y install rh-python36 rh-ruby23 rh-ruby23-ruby-devel | |
427 | - source /opt/rh/rh-ruby23/enable | |
428 | - export X_SCLS="`scl enable rh-ruby23 'echo $X_SCLS'`" | |
429 | - source /opt/rh/rh-python36/enable | |
430 | - pip install virtualenv | |
431 | - pip install virtualenv-tools3 | |
432 | script: | |
433 | - mkdir build_installer | |
434 | - cp -a faraday.tar.gz build_installer/. | |
435 | - cd build_installer | |
436 | - /bin/tar zxf faraday.tar.gz | |
437 | - cd faraday_copy | |
438 | - cp -r /nix . | |
439 | - mv ../../faraday-linux-installers-builder . | |
440 | - cd faraday-linux-installers-builder | |
441 | - git rev-parse HEAD | |
442 | - git clone https://github.com/jordansissel/fpm.git | |
443 | - cd fpm | |
444 | - git checkout d7b466787d17581bc723e474ecf6e18f48226031 | |
445 | - git apply ../fpm-patchs/fpm.virtualenv.patch | |
446 | - make gem | |
447 | - gem install --no-ri --no-rdoc fpm-1.11.0.gem | |
448 | - cd ../../ | |
449 | - sh faraday-linux-installers-builder/build.sh $(eval $IMAGE_TAG) server rpm | |
450 | - mv faraday-server_amd64.rpm ../../faraday-server_amd64.rpm | |
451 | dependencies: | |
452 | - generate_build_file | |
453 | - build_nix_python3 | |
454 | artifacts: | |
455 | name: 'faraday_$CI_COMMIT_REF_NAME.rpm' | |
456 | paths: | |
457 | - "faraday-server_amd64.rpm" | |
458 | expire_in: 15 days | |
459 | rules: | |
460 | - if: '$CI_COMMIT_REF_NAME =~ /^.*\/(master)$/' | |
461 | when: on_success | |
462 | - if: '$CI_COMMIT_TAG || $BUILD_TEST || $FULL_TEST || $DAILY_TEST' | |
463 | when: on_success | |
464 | - when: never | |
465 | ||
466 | test_hypothesis: | |
467 | tags: | |
468 | - hypothesis | |
469 | image: nixorg/nix | |
470 | stage: testing | |
471 | allow_failure: true | |
472 | script: | |
473 | - nix-env -if pynixify/nixpkgs.nix -A cachix | |
474 | - mkdir -p ~/.config/cachix | |
475 | - export USER=$(whoami) | |
476 | - echo "$CACHIX_CONFG" >~/.config/cachix/cachix.dhall | |
477 | - cachix use faradaysec | |
478 | - "echo 'hosts: files dns' >/etc/nsswitch.conf" | |
479 | - export LC_ALL=C.UTF-8 | |
480 | - export LANG=C.UTF-8 | |
481 | - mkdir -p ~/.faraday/config | |
482 | - cp tests/data/server.ini ~/.faraday/config | |
483 | - mkdir run_from | |
484 | - nix-shell --command "pytest tests -v --cov=faraday/server/api --disable-warnings --connection-string=postgresql+psycopg2://$POSTGRES_USER:$POSTGRES_PASSWORD@postgres/$POSTGRES_DB -m hypothesis" | |
485 | rules: | |
486 | - if: '$HYPO_TEST || $FULL_TEST || $DAILY_TEST' | |
487 | when: on_success | |
488 | - when: never | |
489 | ||
490 | agent_integration: | |
491 | stage: post_testing | |
492 | variables: | |
493 | FARADAY_REF: $CI_COMMIT_REF_NAME | |
494 | trigger: | |
495 | project: faradaysec/integrationrepo | |
496 | strategy: depend | |
497 | rules: | |
498 | - if: '$CI_COMMIT_REF_NAME =~ /^.*\/(master)$/' | |
499 | when: on_success | |
500 | - if: '$INTEGRATION || $FULL_TEST || $DAILY_TEST' | |
501 | when: on_success | |
502 | - when: never | |
503 | ||
504 | qa_integration: | |
505 | stage: post_deploy_testing | |
506 | variables: | |
507 | REMOTE_BRANCH: $CI_COMMIT_BRANCH | |
508 | MAIN_COMMIT_SHA: $CI_COMMIT_SHA | |
509 | trigger: | |
510 | project: faradaysec/qa/automation | |
511 | strategy: depend | |
512 | branch: develop | |
513 | rules: | |
514 | - if: '$CI_COMMIT_REF_NAME =~ /^.*\/(master)$/' | |
515 | when: on_success | |
516 | - if: '$CI_COMMIT_TAG || $BUILD_TEST || $FULL_TEST || $DAILY_TEST' | |
517 | when: on_success |
7 | 7 | - id: check-json |
8 | 8 | - id: check-yaml |
9 | 9 | - id: debug-statements |
10 | - repo: https://gitlab.com/pycqa/flake8 | |
11 | rev: 3.8.3 | |
12 | hooks: | |
13 | - id: flake8 | |
14 | additional_dependencies: [flake8-typing-imports==1.9.0] | |
15 | - repo: https://github.com/ikamensh/flynt/ | |
16 | rev: '0.56' | |
17 | hooks: | |
18 | - id: flynt | |
19 | args: [ -df ] | |
10 | 20 | - repo: local |
11 | 21 | hooks: |
12 | 22 | - id: sanity-check |
29 | 39 | language: python |
30 | 40 | verbose: true |
31 | 41 | pass_filenames: false |
32 | args: [--mode=ls] | |
42 | args: [--mode=ls, --local] | |
33 | 43 | stages: [push] |
166 | 166 | logging-format-truncated, |
167 | 167 | logging-too-many-args, |
168 | 168 | logging-too-few-args, |
169 | logging-fstring-interpolation, | |
169 | 170 | bad-format-character, |
170 | 171 | truncated-format-string, |
171 | 172 | mixed-format-string, |
275 | 276 | redundant-unittest-assert, |
276 | 277 | deprecated-method, |
277 | 278 | bad-thread-instantiation, |
279 | map-builtin-not-iterating, | |
280 | unused-import, | |
281 | comparison-with-callable, | |
282 | unused-variable | |
283 | ||
278 | 284 | |
279 | 285 | |
280 | 286 | # Enable the message, report, category or checker with the given id(s). You can |
9 | 9 | * Add creator information when uploading reports or using de bulk create api |
10 | 10 | * Add feature to disable rules in the searcher |
11 | 11 | * Add API endpoint to export Faraday data to Metasploit XML format |
12 | * Change websocket url route from / to /websockets | |
12 | 13 | * Use run date instead of creation date when plugins report specifies it |
13 | 14 | * Improve knowledge base UX |
14 | 15 | * Improve workspace table and status report table UX. |
0 | Dec 23th, 2020 |
0 | * ADD RESTless filter to multiples views, improving the searchs | |
1 | * ADD "extras" modal in options menu, linking to other Faraday resources | |
2 | * ADD `import vulnerability templates` command to faraday-manage | |
3 | * ADD `generate nginx config` command to faraday-manage | |
4 | * ADD vulnerabilities severities count to host | |
5 | * ADD Active Agent columns to workspace | |
6 | * ADD critical vulns count to workspace | |
7 | * ADD `Remember me` login option | |
8 | * ADD distinguish host flag | |
9 | * ADD a create_date field to comments | |
10 | * FIX to use new webargs version | |
11 | * FIX Custom Fields view in KB (Vulnerability Templates) | |
12 | * FIX bug on filter endpoint for vulnerabilities with offset and limit parameters | |
13 | * FIX bug raising `403 Forbidden` HTTP error when the first workspace was not active | |
14 | * FIX bug when changing the token expiration change | |
15 | * FIX bug in Custom Fields type Choice when choice name is too long. | |
16 | * FIX Vulnerability Filter endpoint Performance improvement using joinedload. Removed several nplusone uses | |
17 | * MOD Updating the template.ini for new installations | |
18 | * MOD Improve SMTP configuration | |
19 | * MOD The agent now indicates how much time it had run (faraday-agent-dispatcher v1.4.0) | |
20 | * MOD Type "Vulnerability Web" cannot have "Host" type as a parent when creating data in bulk | |
21 | * MOD Expiration default time from 1 month to 12 hour | |
22 | * MOD Improve data reference when uploading a new report | |
23 | * MOD Refactor Knowledge Base's bulk create to take to take also multiple creation from vulns in status report. | |
24 | * MOD All HTTP OPTIONS endpoints are now public | |
25 | * MOD Change documentation and what's new links in about | |
26 | * REMOVE Flask static endpoint | |
27 | * REMOVE of our custom logger |
0 | IMPORTANT | |
1 | =========== | |
2 | ||
3 | Please be kind to remove all your pyc files before running faraday if you are updating this piece of software. | |
4 | Make sure you run ```./manage.py migrate``` the first time after an update! | |
5 | ||
6 | ||
7 | 0 | New features in the latest update |
8 | 1 | ===================================== |
9 | 2 | |
3 | ||
4 | 3.14.0 [Dec 23th, 2020]: | |
5 | --- | |
6 | * ADD RESTless filter to multiples views, improving the searchs | |
7 | * ADD "extras" modal in options menu, linking to other Faraday resources | |
8 | * ADD `import vulnerability templates` command to faraday-manage | |
9 | * ADD `generate nginx config` command to faraday-manage | |
10 | * ADD vulnerabilities severities count to host | |
11 | * ADD Active Agent columns to workspace | |
12 | * ADD critical vulns count to workspace | |
13 | * ADD `Remember me` login option | |
14 | * ADD distinguish host flag | |
15 | * ADD a create_date field to comments | |
16 | * FIX to use new webargs version | |
17 | * FIX Custom Fields view in KB (Vulnerability Templates) | |
18 | * FIX bug on filter endpoint for vulnerabilities with offset and limit parameters | |
19 | * FIX bug raising `403 Forbidden` HTTP error when the first workspace was not active | |
20 | * FIX bug when changing the token expiration change | |
21 | * FIX bug in Custom Fields type Choice when choice name is too long. | |
22 | * FIX Vulnerability Filter endpoint Performance improvement using joinedload. Removed several nplusone uses | |
23 | * MOD Updating the template.ini for new installations | |
24 | * MOD Improve SMTP configuration | |
25 | * MOD The agent now indicates how much time it had run (faraday-agent-dispatcher v1.4.0) | |
26 | * MOD Type "Vulnerability Web" cannot have "Host" type as a parent when creating data in bulk | |
27 | * MOD Expiration default time from 1 month to 12 hour | |
28 | * MOD Improve data reference when uploading a new report | |
29 | * MOD Refactor Knowledge Base's bulk create to take to take also multiple creation from vulns in status report. | |
30 | * MOD All HTTP OPTIONS endpoints are now public | |
31 | * MOD Change documentation and what's new links in about | |
32 | * REMOVE Flask static endpoint | |
33 | * REMOVE of our custom logger | |
10 | 34 | |
11 | 35 | 3.12 [Sep 3rd, 2020]: |
12 | 36 | --- |
51 | 75 | * Add creator information when uploading reports or using de bulk create api |
52 | 76 | * Add feature to disable rules in the searcher |
53 | 77 | * Add API endpoint to export Faraday data to Metasploit XML format |
78 | * Change websocket url route from / to /websockets | |
54 | 79 | * Use run date instead of creation date when plugins report specifies it |
55 | 80 | * Improve knowledge base UX |
56 | 81 | * Improve workspace table and status report table UX. |
18 | 18 | |
19 | 19 | |
20 | 20 | if __name__ == '__main__': |
21 | version = os.environ.get("IMAGE_TAG", args.version) | |
21 | version = os.environ.get("FARADAY_VERSION", args.version) | |
22 | 22 | main(version) |
0 | IMPORTANT | |
1 | =========== | |
2 | ||
3 | Please be kind to remove all your pyc files before running faraday if you are updating this piece of software. | |
4 | Make sure you run ```./manage.py migrate``` the first time after an update! | |
5 | ||
6 | ||
7 | 0 | New features in the latest update |
8 | 1 | ===================================== |
4 | 4 | include faraday/server/default.ini |
5 | 5 | include requirements.txt |
6 | 6 | include requirements_dev.txt |
7 | include faraday/server/commands/templates/nginx_config.j2 |
0 | IMPORTANT | |
1 | =========== | |
2 | ||
3 | Please be kind to remove all your pyc files before running faraday if you are updating this piece of software. | |
4 | Make sure you run ```./manage.py migrate``` the first time after an update! | |
5 | ||
6 | ||
7 | 0 | New features in the latest update |
8 | 1 | ===================================== |
9 | 2 | |
3 | ||
4 | 3.14.0 [Dec 23th, 2020]: | |
5 | --- | |
6 | * ADD RESTless filter to multiples views, improving the searchs | |
7 | * ADD "extras" modal in options menu, linking to other Faraday resources | |
8 | * ADD `import vulnerability templates` command to faraday-manage | |
9 | * ADD `generate nginx config` command to faraday-manage | |
10 | * ADD vulnerabilities severities count to host | |
11 | * ADD Active Agent columns to workspace | |
12 | * ADD critical vulns count to workspace | |
13 | * ADD `Remember me` login option | |
14 | * ADD distinguish host flag | |
15 | * ADD a create_date field to comments | |
16 | * FIX to use new webargs version | |
17 | * FIX Custom Fields view in KB (Vulnerability Templates) | |
18 | * FIX bug on filter endpoint for vulnerabilities with offset and limit parameters | |
19 | * FIX bug raising `403 Forbidden` HTTP error when the first workspace was not active | |
20 | * FIX bug when changing the token expiration change | |
21 | * FIX bug in Custom Fields type Choice when choice name is too long. | |
22 | * FIX Vulnerability Filter endpoint Performance improvement using joinedload. Removed several nplusone uses | |
23 | * MOD Updating the template.ini for new installations | |
24 | * MOD Improve SMTP configuration | |
25 | * MOD The agent now indicates how much time it had run (faraday-agent-dispatcher v1.4.0) | |
26 | * MOD Type "Vulnerability Web" cannot have "Host" type as a parent when creating data in bulk | |
27 | * MOD Expiration default time from 1 month to 12 hour | |
28 | * MOD Improve data reference when uploading a new report | |
29 | * MOD Refactor Knowledge Base's bulk create to take to take also multiple creation from vulns in status report. | |
30 | * MOD All HTTP OPTIONS endpoints are now public | |
31 | * MOD Change documentation and what's new links in about | |
32 | * REMOVE Flask static endpoint | |
33 | * REMOVE of our custom logger | |
10 | 34 | |
11 | 35 | 3.12 [Sep 3rd, 2020]: |
12 | 36 | --- |
51 | 75 | * Add creator information when uploading reports or using de bulk create api |
52 | 76 | * Add feature to disable rules in the searcher |
53 | 77 | * Add API endpoint to export Faraday data to Metasploit XML format |
78 | * Change websocket url route from / to /websockets | |
54 | 79 | * Use run date instead of creation date when plugins report specifies it |
55 | 80 | * Improve knowledge base UX |
56 | 81 | * Improve workspace table and status report table UX. |
10 | 10 | |
11 | 11 | Send us a email with all relevant information about your discovery at: |
12 | 12 | |
13 | ![](https://raw.github.com/wiki/infobyte/faraday/images/extras/security-email.png) | |
13 | ![](https://raw.github.com/wiki/infobyte/faraday/images/extras/security-email.png) | |
14 | 14 | |
15 | 15 | To encrypt your communications, or to verify signed messages you receive from us you can use the PGP key below. |
16 | 16 | |
17 | 17 | **Key ID:** 3A48E3A9FC5DE068 **Key type:** RSA **Key size:** 4096 |
18 | ||
18 | ||
19 | 19 | Fingerprint: `841D C247 7544 1625 5533 7BC8 3A48 E3A9 FC5D E068` |
20 | 20 | |
21 | 21 | -----BEGIN PGP PUBLIC KEY BLOCK----- |
1 | 1 | # Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/) |
2 | 2 | # See the file 'doc/LICENSE' for the license information |
3 | 3 | |
4 | __version__ = '3.12' | |
4 | __version__ = '3.14.0' | |
5 | 5 | __license_version__ = __version__ |
19 | 19 | #The current user may be different from the logged user |
20 | 20 | current_user = getuser() |
21 | 21 | if current_user != 'root' and faraday_group.gr_gid not in os.getgroups(): |
22 | print("\n\nUser (%s) must be in the '%s' group." % (os.getlogin(), FARADAY_GROUP)) | |
22 | print(f"\n\nUser ({os.getlogin()}) must be in the '{FARADAY_GROUP}' group.") | |
23 | 23 | print("After adding the user to the group, please logout and login again.") |
24 | 24 | sys.exit(1) |
25 | 25 | except KeyError: |
44 | 44 | from faraday.server.commands.custom_fields import add_custom_field_main, delete_custom_field_main |
45 | 45 | from faraday.server.commands import support as support_zip |
46 | 46 | from faraday.server.commands import change_username |
47 | from faraday.server.commands import nginx_config | |
48 | from faraday.server.commands import import_vulnerability_template | |
47 | 49 | from faraday.server.models import db, User |
48 | 50 | from faraday.server.web import app |
49 | 51 | from faraday_plugins.plugins.manager import PluginsManager |
69 | 71 | |
70 | 72 | |
71 | 73 | @click.command(help="Show all URLs in OPENAPI format") |
72 | def openapi_yaml(): | |
73 | openapi_format() | |
74 | @click.option('--no-servers', default=False, is_flag=True, | |
75 | help="Avoids adding servers tag") | |
76 | @click.option('--server', required=False, prompt=False, default="localhost", | |
77 | help="Server host/ip where to test api docs.") | |
78 | def openapi_yaml(server, no_servers): | |
79 | openapi_format(format="yaml", server=server, no_servers=no_servers) | |
80 | ||
81 | ||
82 | ||
83 | @click.command(help="Import Vulnerability templates") | |
84 | @click.option('--language', required=False, default='en') | |
85 | @click.option('--list-languages', is_flag=True) | |
86 | def import_vulnerability_templates(language, list_languages): | |
87 | import_vulnerability_template.run(language, list_languages) | |
74 | 88 | |
75 | 89 | |
76 | 90 | @click.command(help="Create Faraday DB in Postgresql, also tables and indexes") |
199 | 213 | is_ldap=False) |
200 | 214 | db.session.commit() |
201 | 215 | click.echo(click.style( |
202 | 'User {} created successfully!'.format(username), | |
216 | f'User {username} created successfully!', | |
203 | 217 | fg='green', bold=True)) |
204 | 218 | |
205 | 219 | |
243 | 257 | def migrate(downgrade, revision): |
244 | 258 | try: |
245 | 259 | revision = revision or ("-1" if downgrade else "head") |
246 | config = Config(os.path.join(FARADAY_BASE,"alembic.ini")) | |
260 | config = Config(FARADAY_BASE / "alembic.ini") | |
247 | 261 | os.chdir(FARADAY_BASE) |
248 | 262 | if downgrade: |
249 | 263 | alembic.command.downgrade(config, revision) |
250 | 264 | else: |
251 | 265 | alembic.command.upgrade(config, revision) |
266 | # TODO Return to prev dir | |
252 | 267 | except OperationalError as e: |
253 | 268 | logger = logging.getLogger(__name__) |
254 | 269 | logger.error("Migration Error: %s", e) |
281 | 296 | else: |
282 | 297 | change_username.change_username(current_username, new_username) |
283 | 298 | |
299 | @click.command(help="Generate nginx config") | |
300 | @click.option('--fqdn', prompt='Server FQDN', help='The FQDN of your faraday server', type=str, show_default=True) | |
301 | @click.option('--port', prompt='Faraday port', help='Faraday listening port', type=int, default=5985) | |
302 | @click.option('--ws-port', prompt='Faraday Websocket port', help='Faraday websocket listening port', type=int, | |
303 | default=9000, show_default=True) | |
304 | @click.option('--ssl-certificate', prompt='SSL Certificate Path', help='SSL Certificate Path', type=click.Path(exists=True)) | |
305 | @click.option('--ssl-key', prompt='SSL Key Path', help='SSL Key Path', type=click.Path(exists=True)) | |
306 | @click.option('--multitenant-url', help='URL for multitenant config', type=str) | |
307 | def generate_nginx_config(fqdn, port, ws_port, ssl_certificate, ssl_key, multitenant_url): | |
308 | nginx_config.generate_nginx_config(fqdn, port, ws_port, ssl_certificate, ssl_key, multitenant_url) | |
284 | 309 | |
285 | 310 | cli.add_command(show_urls) |
286 | 311 | cli.add_command(initdb) |
297 | 322 | cli.add_command(list_plugins) |
298 | 323 | cli.add_command(rename_user) |
299 | 324 | cli.add_command(openapi_yaml) |
325 | cli.add_command(generate_nginx_config) | |
326 | cli.add_command(import_vulnerability_templates) | |
327 | ||
300 | 328 | |
301 | 329 | if __name__ == '__main__': |
302 | 330 |
0 | 0 | |
1 | 1 | import logging |
2 | import sys | |
3 | import os | |
4 | sys.path.append(os.getcwd()) | |
5 | 2 | import faraday.server.config |
6 | 3 | from faraday.server.web import app |
7 | 4 | from faraday.server.models import db |
23 | 20 | # from myapp import mymodel |
24 | 21 | target_metadata = db.metadata |
25 | 22 | alembic_logger = logging.getLogger('alembic.runtime.migration') |
26 | LOG_FILE = os.path.expanduser(os.path.join( | |
27 | faraday.server.config.CONST_FARADAY_HOME_PATH, | |
28 | 'logs', 'alembic.log')) | |
23 | LOG_FILE = faraday.server.config.CONST_FARADAY_HOME_PATH / 'logs' \ | |
24 | / 'alembic.log' | |
29 | 25 | fh = logging.FileHandler(LOG_FILE) |
30 | 26 | fh.setLevel(logging.INFO) |
31 | 27 | alembic_logger.addHandler(fh) |
81 | 77 | run_migrations_offline() |
82 | 78 | else: |
83 | 79 | run_migrations_online() |
84 | # I'm Py3⏎ | |
80 | # I'm Py3 |
+24
-0
0 | """alter executive_report table | |
1 | ||
2 | Revision ID: 08d02214aedc | |
3 | Revises: b49d8efbd0c2 | |
4 | Create Date: 2020-09-21 20:42:57.190002+00:00 | |
5 | ||
6 | """ | |
7 | from alembic import op | |
8 | import sqlalchemy as sa | |
9 | ||
10 | ||
11 | # revision identifiers, used by Alembic. | |
12 | revision = '08d02214aedc' | |
13 | down_revision = 'b49d8efbd0c2' | |
14 | branch_labels = None | |
15 | depends_on = None | |
16 | ||
17 | ||
18 | def upgrade(): | |
19 | op.add_column('executive_report', sa.Column('advanced_filter', sa.Boolean(), nullable=False, server_default='False')) | |
20 | ||
21 | ||
22 | def downgrade(): | |
23 | op.drop_column('executive_report', 'advanced_filter') |
68 | 68 | op.drop_table('notification') |
69 | 69 | #op.drop_constraint(None, 'notification_user_id_fkey', type_='foreignkey') |
70 | 70 | #op.drop_constraint(None, 'notification_workspace_id_fkey', type_='foreignkey') |
71 | # I'm Py3⏎ | |
71 | # I'm Py3 |
+1
-1
74 | 74 | 'vuln_id': vuln_id |
75 | 75 | }) |
76 | 76 | |
77 | # I'm Py3⏎ | |
77 | # I'm Py3 |
0 | """Alter table executive_report add advanced_filter_parsed field | |
1 | ||
2 | Revision ID: 20f3d0c2f71f | |
3 | Revises: 08d02214aedc | |
4 | Create Date: 2020-10-04 15:42:43.511069+00:00 | |
5 | ||
6 | """ | |
7 | from alembic import op | |
8 | import sqlalchemy as sa | |
9 | ||
10 | ||
11 | # revision identifiers, used by Alembic. | |
12 | revision = '20f3d0c2f71f' | |
13 | down_revision = '08d02214aedc' | |
14 | branch_labels = None | |
15 | depends_on = None | |
16 | ||
17 | ||
18 | def upgrade(): | |
19 | op.add_column('executive_report', | |
20 | sa.Column( | |
21 | 'advanced_filter_parsed', | |
22 | sa.String(255), | |
23 | nullable = False, | |
24 | server_default = "" | |
25 | ) | |
26 | ) | |
27 | ||
28 | ||
29 | def downgrade(): | |
30 | op.drop_column('executive_report','advanced_filter_parsed') |
6 | 6 | """ |
7 | 7 | from alembic import op |
8 | 8 | import sqlalchemy as sa |
9 | from faraday.server.models import Command | |
10 | ||
11 | 9 | |
12 | 10 | # revision identifiers, used by Alembic. |
13 | 11 | revision = '282ac9b6569f' |
15 | 13 | branch_labels = None |
16 | 14 | depends_on = None |
17 | 15 | |
16 | IMPORT_SOURCE = [ | |
17 | 'report', | |
18 | 'shell' | |
19 | ] | |
18 | 20 | |
19 | old_types = Command.IMPORT_SOURCE.remove('agent') | |
20 | new_types = list(set(Command.IMPORT_SOURCE + ['agent'])) | |
21 | old_types = IMPORT_SOURCE | |
22 | new_types = list(set(IMPORT_SOURCE + ['agent'])) | |
21 | 23 | new_options = sorted(new_types) |
22 | 24 | |
23 | old_type = sa.Enum(*Command.IMPORT_SOURCE, name='import_source_enum') | |
25 | old_type = sa.Enum(*IMPORT_SOURCE, name='import_source_enum') | |
24 | 26 | new_type = sa.Enum(*new_options, name='import_source_enum') |
25 | 27 | tmp_type = sa.Enum(*new_options, name='_import_source_enum') |
26 | 28 |
33 | 33 | |
34 | 34 | |
35 | 35 | def downgrade(): |
36 | op.drop_table('search_filter')⏎ | |
36 | op.drop_table('search_filter') |
23 | 23 | def downgrade(): |
24 | 24 | conn = op.get_bind() |
25 | 25 | conn.execute('ALTER TABLE custom_fields_schema DROP CONSTRAINT custom_fields_schema_field_name_key;') |
26 | # I'm Py3⏎ | |
26 | # I'm Py3 |
+1
-1
21 | 21 | |
22 | 22 | def downgrade(): |
23 | 23 | op.drop_column('executive_report', 'markdown') |
24 | # I'm Py3⏎ | |
24 | # I'm Py3 |
0 | """Add command_id to Agent Execution | |
1 | ||
2 | Revision ID: 5658775e113f | |
3 | Revises: e03a13c41a67 | |
4 | Create Date: 2020-12-07 22:21:28.005670+00:00 | |
5 | ||
6 | """ | |
7 | from alembic import op | |
8 | import sqlalchemy as sa | |
9 | ||
10 | ||
11 | # revision identifiers, used by Alembic. | |
12 | revision = '5658775e113f' | |
13 | down_revision = 'e03a13c41a67' | |
14 | branch_labels = None | |
15 | depends_on = None | |
16 | ||
17 | ||
18 | def upgrade(): | |
19 | op.add_column( | |
20 | 'agent_execution', | |
21 | sa.Column('command_id', sa.Integer), | |
22 | ) | |
23 | op.create_foreign_key( | |
24 | 'agent_execution_command_id_fkey', | |
25 | 'agent_execution', | |
26 | 'command', | |
27 | ['command_id'], | |
28 | ['id'], | |
29 | ) | |
30 | ||
31 | ||
32 | def downgrade(): | |
33 | op.drop_constraint('agent_execution_command_id_fkey', 'agent_execution') | |
34 | op.drop_column('agent_execution', 'command_id') |
29 | 29 | op.drop_column('faraday_user', 'otp_secret') |
30 | 30 | op.drop_column('faraday_user', 'state_otp') |
31 | 31 | op.execute('DROP TYPE user_otp_states') |
32 | # I'm Py3⏎ | |
32 | # I'm Py3 |
0 | """check important host | |
1 | ||
2 | Revision ID: e03a13c41a67 | |
3 | Revises: 20f3d0c2f71f | |
4 | Create Date: 2020-10-06 02:03:17.966397+00:00 | |
5 | ||
6 | """ | |
7 | from alembic import op | |
8 | import sqlalchemy as sa | |
9 | ||
10 | ||
11 | # revision identifiers, used by Alembic. | |
12 | revision = 'e03a13c41a67' | |
13 | down_revision = '20f3d0c2f71f' | |
14 | branch_labels = None | |
15 | depends_on = None | |
16 | ||
17 | ||
18 | def upgrade(): | |
19 | op.add_column('host', sa.Column('important', sa.Boolean(), nullable=False, server_default='False')) | |
20 | ||
21 | ||
22 | def downgrade(): | |
23 | op.drop_column('host', 'important') |
33 | 33 | conn.execute('ALTER TABLE vulnerability DROP COLUMN custom_fields') |
34 | 34 | conn.execute('ALTER TABLE vulnerability_template DROP COLUMN custom_fields') |
35 | 35 | conn.execute('DROP TABLE custom_fields_schema') |
36 | # I'm Py3⏎ | |
36 | # I'm Py3 |
8 | 8 | import sqlalchemy as sa |
9 | 9 | |
10 | 10 | |
11 | # revision identifiers, used by Alembic. | |
12 | from faraday.server.models import User | |
13 | ||
14 | 11 | revision = 'f8a44acd0e41' |
15 | 12 | down_revision = '526aa91cac98' |
16 | 13 | branch_labels = None |
17 | 14 | depends_on = None |
18 | 15 | |
16 | ROLES = ['admin', 'pentester', 'client'] | |
17 | ||
19 | 18 | |
20 | 19 | def upgrade(): |
21 | old_type = sa.Enum(*User.ROLES, name='user_roles') | |
20 | old_type = sa.Enum(*ROLES, name='user_roles') | |
22 | 21 | |
23 | new_types = list(set(User.ROLES + ['asset_owner'])) | |
22 | new_types = list(set(ROLES + ['asset_owner'])) | |
24 | 23 | new_options = sorted(new_types) |
25 | 24 | new_type = sa.Enum(*new_options, name='user_roles') |
26 | 25 | |
38 | 37 | |
39 | 38 | |
40 | 39 | def downgrade(): |
41 | new_types = list(set(User.ROLES + ['asset_owner'])) | |
40 | new_types = list(set(ROLES + ['asset_owner'])) | |
42 | 41 | new_options = sorted(new_types) |
43 | 42 | new_type = sa.Enum(*new_options, name='user_roles') |
44 | 43 | |
47 | 46 | tcr = sa.sql.table('faraday_user', |
48 | 47 | sa.Column('role', new_type, nullable=False)) |
49 | 48 | |
50 | old_type = sa.Enum(*User.ROLES, name='user_roles') | |
49 | old_type = sa.Enum(*ROLES, name='user_roles') | |
51 | 50 | |
52 | 51 | # Convert 'asset_owner' status into 'client' |
53 | 52 | op.execute(tcr.update().where(tcr.c.role == u'asset_owner') |
59 | 59 | url = self.base + 'v2/' + path |
60 | 60 | if self.command_id and 'commands' not in url and not url.endswith('}') and not is_get: |
61 | 61 | if '?' in url: |
62 | url += '&command_id={}'.format(self.command_id) | |
62 | url += f'&command_id={self.command_id}' | |
63 | 63 | elif url.endswith('/'): |
64 | url += '?command_id={}'.format(self.command_id) | |
64 | url += f'?command_id={self.command_id}' | |
65 | 65 | else: |
66 | url += '/?command_id={}'.format(self.command_id) | |
66 | url += f'/?command_id={self.command_id}' | |
67 | 67 | return url |
68 | 68 | |
69 | 69 | def _get(self, url, object_name): |
70 | logger.debug('Getting url {}'.format(url)) | |
70 | logger.debug(f'Getting url {url}') | |
71 | 71 | if self.headers: |
72 | 72 | response = self.requests.get(url, headers=self.headers) |
73 | 73 | else: |
74 | 74 | response = self.requests.get(url, cookies=self.cookies) |
75 | 75 | if response.status_code == 401: |
76 | raise ApiError('Unauthorized operation trying to get {}'.format(object_name)) | |
76 | raise ApiError(f'Unauthorized operation trying to get {object_name}') | |
77 | 77 | if response.status_code != 200: |
78 | raise ApiError('Cannot fetch {}. Url {}'.format(object_name, url)) | |
78 | raise ApiError(f'Cannot fetch {object_name}. Url {url}') | |
79 | 79 | if isinstance(response.json, dict): |
80 | 80 | return response.json |
81 | 81 | return json.loads(response.content) |
86 | 86 | else: |
87 | 87 | response = self.requests.post(url, json=data, cookies=self.cookies) |
88 | 88 | if response.status_code == 401: |
89 | raise ApiError('Unauthorized operation trying to get {}'.format(object_name)) | |
89 | raise ApiError(f'Unauthorized operation trying to get {object_name}') | |
90 | 90 | if response.status_code != 201: |
91 | raise ApiError('Cannot fetch {}, api response: {}'.format(object_name, getattr(response, 'text', None))) | |
91 | raise ApiError(f"Cannot fetch {object_name}, api response: {getattr(response, 'text', None)}") | |
92 | 92 | if isinstance(response.json, dict): |
93 | 93 | return response.json |
94 | 94 | return json.loads(response.content) |
99 | 99 | else: |
100 | 100 | response = self.requests.put(url, json=data, cookies=self.cookies) |
101 | 101 | if response.status_code == 401: |
102 | raise ApiError('Unauthorized operation trying to upFdate {}'.format(object_name)) | |
102 | raise ApiError(f'Unauthorized operation trying to upFdate {object_name}') | |
103 | 103 | if response.status_code != 200: |
104 | raise ApiError('Unable to update {}'.format(object_name)) | |
104 | raise ApiError(f'Unable to update {object_name}') | |
105 | 105 | if isinstance(response.json, dict): |
106 | 106 | return response.json |
107 | 107 | return json.loads(response.content) |
112 | 112 | else: |
113 | 113 | response = self.requests.delete(url, cookies=self.cookies) |
114 | 114 | if response.status_code == 401: |
115 | raise ApiError('Unauthorized operation trying to delete {}'.format(object_name)) | |
115 | raise ApiError(f'Unauthorized operation trying to delete {object_name}') | |
116 | 116 | if response.status_code != 204: |
117 | raise ApiError('Unable to delete {}'.format(object_name)) | |
117 | raise ApiError(f'Unable to delete {object_name}') | |
118 | 118 | return True |
119 | 119 | |
120 | 120 | def login(self, username, password): |
133 | 133 | else: |
134 | 134 | token = self.requests.get(self.base + 'v2/token/').json |
135 | 135 | |
136 | header = {'Authorization': 'Token {}'.format(token)} | |
136 | header = {'Authorization': f'Token {token}'} | |
137 | 137 | |
138 | 138 | return header, cookies |
139 | 139 | except ConnectionError as ex: |
148 | 148 | self.params = params |
149 | 149 | self.tool_name = tool_name |
150 | 150 | data = self._command_info() |
151 | res = self._post(self._url('ws/{}/commands/'.format(self.workspace)), data, 'command') | |
151 | res = self._post(self._url(f'ws/{self.workspace}/commands/'), data, 'command') | |
152 | 152 | return res["_id"] |
153 | 153 | |
154 | 154 | def _command_info(self, duration=None): |
170 | 170 | |
171 | 171 | def close_command(self, command_id, duration): |
172 | 172 | data = self._command_info(duration) |
173 | self._put(self._url('ws/{}/commands/{}/'.format(self.workspace, command_id)), data, 'command') | |
173 | self._put(self._url(f'ws/{self.workspace}/commands/{command_id}/'), data, 'command') | |
174 | 174 | |
175 | 175 | def fetch_vulnerabilities(self): |
176 | return [Structure(**item['value']) for item in self._get(self._url('ws/{}/vulns/'.format(self.workspace), True), | |
176 | return [Structure(**item['value']) for item in self._get(self._url(f'ws/{self.workspace}/vulns/', True), | |
177 | 177 | 'vulnerabilities')['vulnerabilities']] |
178 | 178 | |
179 | 179 | def fetch_services(self): |
180 | return [Structure(**item['value']) for item in self._get(self._url('ws/{}/services/'.format(self.workspace), True), | |
180 | return [Structure(**item['value']) for item in self._get(self._url(f'ws/{self.workspace}/services/', True), | |
181 | 181 | 'services')['services']] |
182 | 182 | |
183 | 183 | def fetch_hosts(self): |
184 | return [Structure(**item['value']) for item in self._get(self._url('ws/{}/hosts/'.format(self.workspace), True), | |
184 | return [Structure(**item['value']) for item in self._get(self._url(f'ws/{self.workspace}/hosts/', True), | |
185 | 185 | 'hosts')['rows']] |
186 | 186 | |
187 | 187 | def fetch_templates(self): |
191 | 191 | def filter_vulnerabilities(self, **kwargs): |
192 | 192 | if len(list(kwargs.keys())) > 1: |
193 | 193 | params = urlencode(kwargs) |
194 | url = self._url('ws/{}/vulns/?{}'.format(self.workspace, params)) | |
194 | url = self._url(f'ws/{self.workspace}/vulns/?{params}') | |
195 | 195 | else: |
196 | 196 | params = self.parse_args(**kwargs) |
197 | url = self._url('ws/{}/vulns/{}'.format(self.workspace, params), True) | |
197 | url = self._url(f'ws/{self.workspace}/vulns/{params}', True) | |
198 | 198 | return [Structure(**item['value']) for item in |
199 | 199 | self._get(url, 'vulnerabilities')['vulnerabilities']] |
200 | 200 | |
201 | 201 | def filter_services(self, **kwargs): |
202 | 202 | params = urlencode(kwargs) |
203 | url = self._url('ws/{}/services/?{}'.format(self.workspace, params), True) | |
203 | url = self._url(f'ws/{self.workspace}/services/?{params}', True) | |
204 | 204 | return [Structure(**item['value']) for item in |
205 | 205 | self._get(url, 'services')['services']] |
206 | 206 | |
207 | 207 | def filter_hosts(self, **kwargs): |
208 | 208 | params = urlencode(kwargs) |
209 | url = self._url('ws/{}/hosts/?{}'.format(self.workspace, params), True) | |
209 | url = self._url(f'ws/{self.workspace}/hosts/?{params}', True) | |
210 | 210 | return [Structure(**item['value']) for item in |
211 | 211 | self._get(url, 'hosts')['rows']] |
212 | 212 | |
221 | 221 | return filtered_templates |
222 | 222 | |
223 | 223 | def update_vulnerability(self, vulnerability): |
224 | return Structure(**self._put(self._url('ws/{}/vulns/{}/'.format(self.workspace, vulnerability.id)), | |
224 | return Structure(**self._put(self._url(f'ws/{self.workspace}/vulns/{vulnerability.id}/'), | |
225 | 225 | vulnerability.__dict__, 'vulnerability')) |
226 | 226 | |
227 | 227 | def update_service(self, service): |
229 | 229 | service.ports = [service.ports] |
230 | 230 | else: |
231 | 231 | service.ports = [] |
232 | return Structure(**self._put(self._url('ws/{}/services/{}/'.format(self.workspace, service.id)), | |
232 | return Structure(**self._put(self._url(f'ws/{self.workspace}/services/{service.id}/'), | |
233 | 233 | service.__dict__, 'service')) |
234 | 234 | |
235 | 235 | def update_host(self, host): |
236 | return Structure(**self._put(self._url('ws/{}/hosts/{}/'.format(self.workspace, host.id)), | |
236 | return Structure(**self._put(self._url(f'ws/{self.workspace}/hosts/{host.id}/'), | |
237 | 237 | host.__dict__, 'hosts')) |
238 | 238 | |
239 | 239 | def delete_vulnerability(self, vulnerability_id): |
240 | return self._delete(self._url('ws/{}/vulns/{}/'.format(self.workspace, vulnerability_id)), 'vulnerability') | |
240 | return self._delete(self._url(f'ws/{self.workspace}/vulns/{vulnerability_id}/'), 'vulnerability') | |
241 | 241 | |
242 | 242 | def delete_service(self, service_id): |
243 | return self._delete(self._url('ws/{}/services/{}/'.format(self.workspace, service_id)), 'service') | |
243 | return self._delete(self._url(f'ws/{self.workspace}/services/{service_id}/'), 'service') | |
244 | 244 | |
245 | 245 | def delete_host(self, host_id): |
246 | return self._delete(self._url('ws/{}/hosts/{}/'.format(self.workspace, host_id)), 'host') | |
246 | return self._delete(self._url(f'ws/{self.workspace}/hosts/{host_id}/'), 'host') | |
247 | 247 | |
248 | 248 | @staticmethod |
249 | 249 | def parse_args(**kwargs): |
250 | 250 | if len(list(kwargs.keys())) > 0: |
251 | 251 | key = list(kwargs.keys())[0] |
252 | 252 | value = kwargs.get(key, '') |
253 | item = '"name":"{}","op":"eq","val":"{}"'.format(key, value) | |
253 | item = f'"name":"{key}","op":"eq","val":"{value}"' | |
254 | 254 | params = 'filter?q={"filters":[{' + item + '}]}' |
255 | 255 | return params |
256 | 256 | return '' |
13 | 13 | import os |
14 | 14 | import re |
15 | 15 | import signal |
16 | import smtplib | |
17 | 16 | import sys |
18 | 17 | import time |
19 | 18 | from datetime import datetime |
20 | 19 | from difflib import SequenceMatcher |
21 | from email.mime.multipart import MIMEMultipart | |
22 | from email.mime.text import MIMEText | |
20 | from pathlib import Path | |
23 | 21 | |
24 | 22 | import click |
25 | 23 | import requests |
27 | 25 | from faraday.searcher.api import Api |
28 | 26 | from faraday.searcher.validator import validate_rules |
29 | 27 | from faraday.server.models import Service, Host |
28 | from faraday.utils.smtp import MailNotification | |
30 | 29 | |
31 | 30 | logger = logging.getLogger('Faraday searcher') |
32 | 31 | |
33 | 32 | threshold = 0.75 |
34 | 33 | min_weight = 0.3 |
35 | ||
36 | ||
37 | class MailNotification: | |
38 | def __init__(self, mail_from, mail_password, mail_protocol, mail_port): | |
39 | self.mail_from = mail_from | |
40 | self.mail_password = mail_password | |
41 | self.mail_protocol = mail_protocol | |
42 | self.mail_port = mail_port | |
43 | ||
44 | def send_mail(self, to_addr, subject, body): | |
45 | from_addr = self.mail_from | |
46 | msg = MIMEMultipart() | |
47 | msg['From'] = from_addr | |
48 | msg['To'] = to_addr | |
49 | msg['Subject'] = subject | |
50 | ||
51 | msg.attach(MIMEText(body, 'plain')) | |
52 | try: | |
53 | server_mail = smtplib.SMTP(self.mail_protocol, self.mail_port) | |
54 | server_mail.starttls() | |
55 | server_mail.login(from_addr, self.mail_password) | |
56 | text = msg.as_string() | |
57 | server_mail.sendmail(from_addr, to_addr, text) | |
58 | server_mail.quit() | |
59 | except Exception as error: | |
60 | logger.error("Error: unable to send email") | |
61 | logger.exception(error) | |
62 | ||
63 | 34 | |
64 | 35 | def compare(a, b): |
65 | 36 | return SequenceMatcher(None, a, b).ratio() |
90 | 61 | |
91 | 62 | |
92 | 63 | def equals(m1, m2, rule): |
93 | logger.debug("Comparing by similarity '%s' and '%s'" % (m1.name, m2.name)) | |
64 | logger.debug(f"Comparing by similarity '{m1.name}' and '{m2.name}'") | |
94 | 65 | match = True |
95 | 66 | total_ratio = 0 |
96 | 67 | count_fields = 0 |
125 | 96 | percent = (total_ratio * 100.0) / count_fields |
126 | 97 | else: |
127 | 98 | percent = 0.0 |
128 | logger.debug("Verify result with %.2f %% evaluating rule %s:" % (percent, rule['id'])) | |
99 | logger.debug(f"Verify result with {percent:.2f} % evaluating rule {rule['id']}:") | |
129 | 100 | |
130 | 101 | if match and total_ratio >= (threshold * count_fields): |
131 | 102 | logger.info("MATCH") |
139 | 110 | if is_same_level(model, md): |
140 | 111 | environment.append(md) |
141 | 112 | return environment |
142 | ||
143 | ||
144 | def process_models_by_similarity(api, _models, rule, mail_notificacion): | |
145 | logger.debug("--> Start Process models by similarity") | |
146 | for index_m1, m1 in zip(list(range(len(_models) - 1)), _models): | |
147 | for _, m2 in zip(list(range(index_m1 + 1, len(_models))), _models[index_m1 + 1:]): | |
148 | if m1.id != m2.id and is_same_level(m1, m2): | |
149 | if equals(m1, m2, rule): | |
150 | environment = [m1, m2] | |
151 | _objs_value = None | |
152 | if 'object' in rule: | |
153 | _objs_value = rule['object'] | |
154 | _object = get_object(environment, _objs_value) | |
155 | if _object is not None: | |
156 | if 'conditions' in rule: | |
157 | environment = get_model_environment(m2, _models) | |
158 | if can_execute_action(environment, rule['conditions']): | |
159 | execute_action(api, _object, rule, mail_notificacion) | |
160 | else: | |
161 | execute_action(api, _object, rule, mail_notificacion) | |
162 | logger.debug("<-- Finish Process models by similarity") | |
163 | 113 | |
164 | 114 | |
165 | 115 | def get_field(obj, field): |
170 | 120 | return getattr(obj, 'references') |
171 | 121 | return None |
172 | 122 | except AttributeError: |
173 | logger.error("ERROR: Field %s is invalid" % field) | |
123 | logger.error(f"ERROR: Field {field} is invalid") | |
174 | 124 | return None |
175 | 125 | |
176 | 126 | |
188 | 138 | if key == 'template': |
189 | 139 | cwe = get_cwe(api, value) |
190 | 140 | if cwe is None: |
191 | logger.error("%s: cwe not found" % value) | |
141 | logger.error(f"{value}: cwe not found") | |
192 | 142 | return False |
193 | 143 | |
194 | 144 | vuln.name = cwe.name |
196 | 146 | vuln.desc = cwe.description |
197 | 147 | vuln.resolution = cwe.resolution |
198 | 148 | |
199 | logger.info("Applying template '%s' to vulnerability '%s' with id '%s'" % (value, vuln.name, vuln.id)) | |
149 | logger.info(f"Applying template '{value}' to vulnerability '{vuln.name}' with id '{vuln.id}'") | |
200 | 150 | |
201 | 151 | elif key == 'confirmed': |
202 | 152 | value = value == 'True' |
203 | 153 | vuln.confirmed = value |
204 | logger.info("Changing property %s to %s in vulnerability '%s' with id %s" % (key, value, vuln.name, vuln.id)) | |
154 | logger.info(f"Changing property {key} to {value} in vulnerability '{vuln.name}' with id {vuln.id}") | |
205 | 155 | elif key == 'owned': |
206 | 156 | value = value == 'True' |
207 | 157 | vuln.owned = value |
208 | logger.info("Changing property %s to %s in vulnerability '%s' with id %s" % (key, value, vuln.name, vuln.id)) | |
158 | logger.info(f"Changing property {key} to {value} in vulnerability '{vuln.name}' with id {vuln.id}") | |
209 | 159 | else: |
210 | 160 | to_add = True |
211 | 161 | if key.startswith('-'): |
223 | 173 | if isinstance(field, str): |
224 | 174 | setattr(vuln, key, value) |
225 | 175 | logger.info( |
226 | "Changing property %s to %s in vulnerability '%s' with id %s" % (key, value, vuln.name, vuln.id)) | |
176 | f"Changing property {key} to {value} in vulnerability '{vuln.name}' with id {vuln.id}") | |
227 | 177 | else: |
228 | 178 | set_array(field, value, add=to_add) |
229 | action = 'Adding %s to %s list in vulnerability %s with id %s' % (value, key, vuln.name, vuln.id) | |
179 | action = f'Adding {value} to {key} list in vulnerability {vuln.name} with id {vuln.id}' | |
230 | 180 | if not to_add: |
231 | 181 | action = 'Removing %s from %s list in vulnerability %s with id %s' % ( |
232 | 182 | value, key, vuln.name, vuln.id) |
236 | 186 | if field is not None and is_custom_field is True: |
237 | 187 | vuln.custom_fields[key] = value |
238 | 188 | logger.info( |
239 | "Changing custom field %s to %s in vulnerability '%s' with id %s" % (key, value, vuln.name, vuln.id)) | |
189 | f"Changing custom field {key} to {value} in vulnerability '{vuln.name}' with id {vuln.id}") | |
240 | 190 | |
241 | 191 | api.update_vulnerability(vuln) |
242 | 192 | |
248 | 198 | if key == 'owned': |
249 | 199 | value = value == 'True' |
250 | 200 | service.owned = value |
251 | logger.info("Changing property %s to %s in service '%s' with id %s" % (key, value, service.name, service.id)) | |
201 | logger.info(f"Changing property {key} to {value} in service '{service.name}' with id {service.id}") | |
252 | 202 | else: |
253 | 203 | to_add = True |
254 | 204 | if key.startswith('-'): |
260 | 210 | if isinstance(field, str): |
261 | 211 | setattr(service, key, value) |
262 | 212 | logger.info( |
263 | "Changing property %s to %s in service '%s' with id %s" % (key, value, service.name, service.id)) | |
213 | f"Changing property {key} to {value} in service '{service.name}' with id {service.id}") | |
264 | 214 | else: |
265 | 215 | set_array(field, value, add=to_add) |
266 | action = 'Adding %s to %s list in service %s with id %s' % (value, key, service.name, service.id) | |
216 | action = f'Adding {value} to {key} list in service {service.name} with id {service.id}' | |
267 | 217 | if not to_add: |
268 | 218 | action = 'Removing %s from %s list in service %s with id %s' % ( |
269 | 219 | value, key, service.name, service.id) |
280 | 230 | if key == 'owned': |
281 | 231 | value = value == 'True' |
282 | 232 | host.owned = value |
283 | logger.info("Changing property %s to %s in host '%s' with id %s" % (key, value, host.name, host.id)) | |
233 | logger.info(f"Changing property {key} to {value} in host '{host.name}' with id {host.id}") | |
284 | 234 | else: |
285 | 235 | to_add = True |
286 | 236 | if key.startswith('-'): |
291 | 241 | if field is not None: |
292 | 242 | if isinstance(field, str): |
293 | 243 | setattr(host, key, value) |
294 | logger.info("Changing property %s to %s in host '%s' with id %s" % (key, value, host.name, host.id)) | |
244 | logger.info(f"Changing property {key} to {value} in host '{host.name}' with id {host.id}") | |
295 | 245 | else: |
296 | 246 | set_array(field, value, add=to_add) |
297 | action = 'Adding %s to %s list in host %s with id %s' % (value, key, host.name, host.id) | |
247 | action = f'Adding {value} to {key} list in host {host.name} with id {host.id}' | |
298 | 248 | if not to_add: |
299 | 249 | action = 'Removing %s from %s list in host %s with id %s' % ( |
300 | 250 | value, key, host.name, host.id) |
458 | 408 | def _process_vulnerabilities(self, rules): |
459 | 409 | logger.debug("--> Start Process vulnerabilities") |
460 | 410 | for rule_item in rules: |
461 | logger.debug('Processing rule {}'.format(rule_item['id'])) | |
411 | logger.debug(f"Processing rule {rule_item['id']}") | |
462 | 412 | if rule_item['model'].lower() == 'vulnerability': |
463 | 413 | count_values = 1 |
464 | 414 | values = [None] |
483 | 433 | def _process_services(self, rules): |
484 | 434 | logger.debug("--> Start Process services") |
485 | 435 | for rule_item in rules: |
486 | logger.debug('Processing rule {}'.format(rule_item['id'])) | |
436 | logger.debug(f"Processing rule {rule_item['id']}") | |
487 | 437 | if rule_item['model'].lower() == 'service': |
488 | 438 | count_values = 1 |
489 | 439 | values = [None] |
508 | 458 | def _process_hosts(self, rules): |
509 | 459 | logger.debug("--> Start Process Hosts") |
510 | 460 | for rule_item in rules: |
511 | logger.debug('Processing rule {}'.format(rule_item['id'])) | |
461 | logger.debug(f"Processing rule {rule_item['id']}") | |
512 | 462 | if rule_item['model'].lower() == 'host': |
513 | 463 | count_values = 1 |
514 | 464 | values = [None] |
551 | 501 | if 'parent' in rule: |
552 | 502 | parent = self._get_parent(rule['parent']) |
553 | 503 | if parent is None: |
554 | logger.warning("WARNING: Parent %s not found in rule %s " % (rule['parent'], rule['id'])) | |
504 | logger.warning(f"WARNING: Parent {rule['parent']} not found in rule {rule['id']} ") | |
555 | 505 | return self._fetch_objects(rule['model']), None |
556 | 506 | return self._get_objects_by_parent(parent, rule['model']), parent |
557 | 507 | return self._fetch_objects(rule['model']), None |
625 | 575 | return len(self._filter_objects(rule['model'], **kwargs)) > 0 |
626 | 576 | |
627 | 577 | def _execute_action(self, objects, rule): |
628 | logger.info("Running actions of rule '%s' :" % rule['id']) | |
578 | logger.info(f"Running actions of rule '{rule['id']}' :") | |
629 | 579 | actions = rule['actions'] |
630 | 580 | _objs_value = None |
631 | 581 | if 'object' in rule: |
665 | 615 | elif command == 'DELETE': |
666 | 616 | if object_type in ['Vulnerabilityweb', 'Vulnerability_web', 'Vulnerability']: |
667 | 617 | self.api.delete_vulnerability(obj.id) |
668 | logger.info("Deleting vulnerability '%s' with id '%s':" % (obj.name, obj.id)) | |
618 | logger.info(f"Deleting vulnerability '{obj.name}' with id '{obj.id}':") | |
669 | 619 | |
670 | 620 | elif object_type == 'Service': |
671 | 621 | self.api.delete_service(obj.id) |
672 | logger.info("Deleting service '%s' with id '%s':" % (obj.name, obj.id)) | |
622 | logger.info(f"Deleting service '{obj.name}' with id '{obj.id}':") | |
673 | 623 | |
674 | 624 | elif object_type == 'Host': |
675 | 625 | self.api.delete_host(obj.id) |
676 | logger.info("Deleting host '%s' with id '%s':" % (obj.ip, obj.id)) | |
626 | logger.info(f"Deleting host '{obj.ip}' with id '{obj.id}':") | |
677 | 627 | else: |
678 | 628 | if self.mail_notification: |
679 | 629 | subject = 'Faraday searcher alert' |
680 | 630 | body = '%s %s have been modified by rule %s at %s' % ( |
681 | 631 | object_type, obj.name, rule['id'], str(datetime.now())) |
682 | 632 | self.mail_notification.send_mail(expression, subject, body) |
683 | logger.info("Sending mail to: '%s'" % expression) | |
633 | logger.info(f"Sending mail to: '{expression}'") | |
684 | 634 | else: |
685 | 635 | logger.warn("Searcher needs SMTP configuration to send mails") |
686 | 636 | |
693 | 643 | if key == 'template': |
694 | 644 | cwe = get_cwe(self.api, value) |
695 | 645 | if cwe is None: |
696 | logger.error("%s: cwe not found" % value) | |
646 | logger.error(f"{value}: cwe not found") | |
697 | 647 | return False |
698 | 648 | |
699 | 649 | vuln.name = cwe.name |
701 | 651 | vuln.desc = cwe.description |
702 | 652 | vuln.resolution = cwe.resolution |
703 | 653 | |
704 | logger.info("Applying template '%s' to vulnerability '%s' with id '%s'" % (value, vuln.name, vuln.id)) | |
654 | logger.info(f"Applying template '{value}' to vulnerability '{vuln.name}' with id '{vuln.id}'") | |
705 | 655 | |
706 | 656 | elif key == 'confirmed': |
707 | 657 | value = value == 'True' |
708 | 658 | vuln.confirmed = value |
709 | 659 | logger.info( |
710 | "Changing property %s to %s in vulnerability '%s' with id %s" % (key, value, vuln.name, vuln.id)) | |
660 | f"Changing property {key} to {value} in vulnerability '{vuln.name}' with id {vuln.id}") | |
711 | 661 | elif key == 'owned': |
712 | 662 | value = value == 'True' |
713 | 663 | vuln.owned = value |
714 | 664 | logger.info( |
715 | "Changing property %s to %s in vulnerability '%s' with id %s" % (key, value, vuln.name, vuln.id)) | |
665 | f"Changing property {key} to {value} in vulnerability '{vuln.name}' with id {vuln.id}") | |
716 | 666 | else: |
717 | 667 | to_add = True |
718 | 668 | if key.startswith('-'): |
765 | 715 | value = value == 'True' |
766 | 716 | service.owned = value |
767 | 717 | logger.info( |
768 | "Changing property %s to %s in service '%s' with id %s" % (key, value, service.name, service.id)) | |
718 | f"Changing property {key} to {value} in service '{service.name}' with id {service.id}") | |
769 | 719 | else: |
770 | 720 | to_add = True |
771 | 721 | if key.startswith('-'): |
781 | 731 | key, value, service.name, service.id)) |
782 | 732 | else: |
783 | 733 | self.api.set_array(field, value, add=to_add, key=key, object=service) |
784 | action = 'Adding %s to %s list in service %s with id %s' % (value, key, service.name, service.id) | |
734 | action = f'Adding {value} to {key} list in service {service.name} with id {service.id}' | |
785 | 735 | if not to_add: |
786 | 736 | action = 'Removing %s from %s list in service %s with id %s' % ( |
787 | 737 | value, key, service.name, service.id) |
797 | 747 | if key == 'owned': |
798 | 748 | value = value == 'True' |
799 | 749 | host.owned = value |
800 | logger.info("Changing property %s to %s in host '%s' with id %s" % (key, value, host.ip, host.id)) | |
750 | logger.info(f"Changing property {key} to {value} in host '{host.ip}' with id {host.id}") | |
801 | 751 | else: |
802 | 752 | to_add = True |
803 | 753 | if key.startswith('-'): |
808 | 758 | if field is not None: |
809 | 759 | if isinstance(field, str): |
810 | 760 | setattr(host, key, value) |
811 | logger.info("Changing property %s to %s in host '%s' with id %s" % (key, value, host.ip, host.id)) | |
761 | logger.info(f"Changing property {key} to {value} in host '{host.ip}' with id {host.id}") | |
812 | 762 | else: |
813 | 763 | self.api.set_array(field, value, add=to_add, key=key, object=host) |
814 | action = 'Adding %s to %s list in host %s with id %s' % (value, key, host.ip, host.id) | |
764 | action = f'Adding {value} to {key} list in host {host.ip} with id {host.id}' | |
815 | 765 | if not to_add: |
816 | 766 | action = 'Removing %s from %s list in host %s with id %s' % ( |
817 | 767 | value, key, host.ip, host.id) |
849 | 799 | @click.option('--user', required=True, prompt=True, help='') |
850 | 800 | @click.option('--password', required=True, prompt=True, hide_input=True, help='') |
851 | 801 | @click.option('--output', required=False, help='Choose a custom output directory', default='output') |
852 | @click.option('--email', required=False) | |
853 | @click.option('--email_password', required=False) | |
802 | @click.option('--email_sender', required=False) | |
803 | @click.option('--smtp_username', required=False) | |
804 | @click.option('--smtp_password', required=False) | |
854 | 805 | @click.option('--mail_protocol', required=False) |
855 | @click.option('--port_protocol', required=False, default=587) | |
806 | @click.option('--port_protocol', required=False) | |
807 | @click.option('--ssl', required=False) | |
856 | 808 | @click.option('--log', required=False, default='debug') |
857 | 809 | @click.option('--rules', required=True, prompt=True, help='Filename with rules') |
858 | def main(workspace, server_address, user, password, output, email, email_password, mail_protocol, port_protocol, log, | |
859 | rules): | |
810 | def main(workspace, server_address, user, password, output, email_sender, | |
811 | smtp_username, smtp_password, mail_protocol, port_protocol, ssl, | |
812 | log, rules): | |
860 | 813 | signal.signal(signal.SIGINT, signal_handler) |
861 | 814 | |
862 | 815 | loglevel = log |
868 | 821 | sys.exit(1) |
869 | 822 | |
870 | 823 | mail_notification = MailNotification( |
871 | email, | |
872 | email_password, | |
873 | mail_protocol, | |
874 | port_protocol, | |
875 | ||
824 | smtp_host=mail_protocol, | |
825 | smtp_sender=email_sender, | |
826 | smtp_username=smtp_username, | |
827 | smtp_password=smtp_password, | |
828 | smtp_port=port_protocol, | |
829 | smtp_ssl=ssl | |
876 | 830 | ) |
877 | 831 | |
878 | for d in [output, 'log/']: | |
879 | if not os.path.isdir(d): | |
880 | os.makedirs(d) | |
832 | for d in [output, 'log/']: # TODO CHANGE THIS | |
833 | if not Path(d): | |
834 | Path(d).mkdir(parents=True) | |
881 | 835 | |
882 | 836 | numeric_level = getattr(logging, loglevel.upper(), None) |
883 | 837 | if not isinstance(numeric_level, int): |
884 | raise ValueError('Invalid log level: %s' % loglevel) | |
838 | raise ValueError(f'Invalid log level: {loglevel}') | |
885 | 839 | |
886 | 840 | if not logger.handlers: |
887 | 841 | logger.propagate = 0 |
903 | 857 | |
904 | 858 | try: |
905 | 859 | logger.info('Started') |
906 | logger.info('Searching objects into workspace %s ' % workspace) | |
860 | logger.info(f'Searching objects into workspace {workspace} ') | |
907 | 861 | |
908 | 862 | if not server_address.endswith('/'): |
909 | 863 | server_address += '/' |
21 | 21 | if len(workspace) > 0: |
22 | 22 | self.workspace = workspace[0] |
23 | 23 | else: |
24 | raise ApiError("Workspace %s doesn't exist" % workspace_name) | |
24 | raise ApiError(f"Workspace {workspace_name} doesn't exist") | |
25 | 25 | |
26 | 26 | def create_command(self, itime, params, tool_name): |
27 | 27 | self.itime = itime |
41 | 41 | if model in vfields and len(fields) != 0: |
42 | 42 | for field in fields: |
43 | 43 | if field not in vfields[model]: |
44 | print("ERROR: The field '%s' doesn't exist in model '%s'" % (field, model)) | |
45 | logger.error("The field '%s' doesn't exist in model '%s'" % (field, model)) | |
44 | print(f"ERROR: The field '{field}' doesn't exist in model '{model}'") | |
45 | logger.error(f"The field '{field}' doesn't exist in model '{model}'") | |
46 | 46 | return False |
47 | 47 | return True |
48 | 48 | else: |
55 | 55 | array = item.split('=') |
56 | 56 | if allow_old_option: |
57 | 57 | if item != '--old' and len(array) != 2 or '' in array: |
58 | logger.error("ERROR: '%s' must have 'field=value' or '--old'" % item) | |
58 | logger.error(f"ERROR: '{item}' must have 'field=value' or '--old'") | |
59 | 59 | return False |
60 | 60 | |
61 | 61 | elif len(array) != 2 or '' in array: |
62 | logger.error("ERROR: '%s' must have 'field=value' " % item) | |
62 | logger.error(f"ERROR: '{item}' must have 'field=value' ") | |
63 | 63 | return False |
64 | 64 | |
65 | 65 | return True |
88 | 88 | for index, item in enumerate(values): |
89 | 89 | if index != 0: |
90 | 90 | if len(values[index - 1]) != len(values[index]): |
91 | logger.error("Each value item must be equal in rule: %s" % rule_id) | |
91 | logger.error(f"Each value item must be equal in rule: {rule_id}") | |
92 | 92 | return False |
93 | 93 | keys = item.keys() |
94 | 94 | |
95 | 95 | for var in _vars: |
96 | 96 | if var not in keys: |
97 | logger.error("Variable '%s' should has a value in rule: %s" % (var, rule_id)) | |
97 | logger.error(f"Variable '{var}' should has a value in rule: {rule_id}") | |
98 | 98 | return False |
99 | 99 | return True |
100 | 100 | |
137 | 137 | def validate(key, dictionary, validate_function=None, rule_id=None, mandatory=True, **args): |
138 | 138 | if rule_id is None: |
139 | 139 | if key not in dictionary: |
140 | logger.error("ERROR: Key %s doesn't exist" % key) | |
140 | logger.error(f"ERROR: Key {key} doesn't exist") | |
141 | 141 | return False |
142 | 142 | if not validate_function(args['id_list'], dictionary[key]): |
143 | logger.error("ERROR: Key %s is repeated" % key) | |
143 | logger.error(f"ERROR: Key {key} is repeated") | |
144 | 144 | return False |
145 | 145 | else: |
146 | 146 | if key not in dictionary and mandatory: |
147 | logger.error("ERROR: Key %s doesn't exist in rule: %s" % (key, rule_id)) | |
147 | logger.error(f"ERROR: Key {key} doesn't exist in rule: {rule_id}") | |
148 | 148 | return False |
149 | 149 | if key in dictionary: |
150 | 150 | if key == 'fields': |
151 | 151 | if not validate_function(args['model'], dictionary[key]): |
152 | logger.error("ERROR: Key %s has an invalid value in rule: %s" % (key, rule_id)) | |
152 | logger.error(f"ERROR: Key {key} has an invalid value in rule: {rule_id}") | |
153 | 153 | return False |
154 | 154 | return True |
155 | 155 | |
157 | 157 | return validate_function(dictionary[key], dictionary, rule_id) |
158 | 158 | |
159 | 159 | if not validate_function(dictionary[key]): |
160 | logger.error("ERROR: Key %s has an invalid value in rule: %s" % (key, rule_id)) | |
160 | logger.error(f"ERROR: Key {key} has an invalid value in rule: {rule_id}") | |
161 | 161 | return False |
162 | 162 | |
163 | 163 | return True |
196 | 196 | |
197 | 197 | logger.info('<-- Rules OK') |
198 | 198 | return True |
199 | # I'm Py3⏎ | |
199 | # I'm Py3 |
5 | 5 | """ |
6 | 6 | import json |
7 | 7 | import logging |
8 | from json import JSONDecodeError | |
9 | from typing import Tuple | |
8 | 10 | |
9 | 11 | import flask |
10 | 12 | import sqlalchemy |
20 | 22 | from marshmallow.validate import Length |
21 | 23 | from marshmallow_sqlalchemy import ModelConverter |
22 | 24 | from marshmallow_sqlalchemy.schema import ModelSchemaMeta, ModelSchemaOpts |
25 | from sqlalchemy.sql.elements import BooleanClauseList | |
23 | 26 | from webargs.flaskparser import FlaskParser |
24 | 27 | from webargs.core import ValidationError |
25 | from faraday.server.models import Workspace, db, Command, CommandObject | |
28 | from flask_classful import route | |
29 | ||
30 | from faraday.server.models import Workspace, db, Command, CommandObject, count_vulnerability_severities | |
26 | 31 | from faraday.server.schemas import NullToBlankString |
27 | 32 | from faraday.server.utils.database import ( |
28 | 33 | get_conflict_object, |
29 | 34 | is_unique_constraint_violation |
30 | 35 | ) |
36 | from faraday.server.utils.filters import FlaskRestlessSchema | |
37 | from faraday.server.utils.search import search | |
31 | 38 | |
32 | 39 | from faraday.server.config import faraday_server |
33 | 40 | |
271 | 278 | try: |
272 | 279 | obj = query.filter(self._get_lookup_field() == object_id).one() |
273 | 280 | except NoResultFound: |
274 | flask.abort(404, 'Object with id "%s" not found' % object_id) | |
281 | flask.abort(404, f'Object with id "{object_id}" not found') | |
275 | 282 | return obj |
276 | 283 | |
277 | 284 | def _dump(self, obj, route_kwargs, **kwargs): |
291 | 298 | data. It a ``Marshmallow.Schema`` instance to perform the |
292 | 299 | deserialization |
293 | 300 | """ |
294 | return FlaskParser().parse(schema, request, location="json", | |
301 | return FlaskParser(unknown=EXCLUDE).parse(schema, request, location="json", | |
295 | 302 | *args, **kwargs) |
296 | 303 | |
297 | 304 | @classmethod |
340 | 347 | response = {'success': False, 'message': f"Exception: {err.original_exception}" if faraday_server.debug else 'Internal Server Error'} |
341 | 348 | return flask.jsonify(response), 500 |
342 | 349 | |
350 | ||
343 | 351 | class GenericWorkspacedView(GenericView): |
344 | 352 | """Abstract class for a view that depends on the workspace, that is |
345 | 353 | passed in the URL |
359 | 367 | try: |
360 | 368 | ws = Workspace.query.filter_by(name=workspace_name).one() |
361 | 369 | if not ws.active: |
362 | flask.abort(403, "Disabled workspace: %s" % workspace_name) | |
370 | flask.abort(403, f"Disabled workspace: {workspace_name}") | |
363 | 371 | except NoResultFound: |
364 | flask.abort(404, "No such workspace: %s" % workspace_name) | |
372 | flask.abort(404, f"No such workspace: {workspace_name}") | |
365 | 373 | return ws |
366 | 374 | |
367 | 375 | def _get_base_query(self, workspace_name): |
378 | 386 | try: |
379 | 387 | obj = query.filter(self._get_lookup_field() == object_id).one() |
380 | 388 | except NoResultFound: |
381 | flask.abort(404, 'Object with id "%s" not found' % object_id) | |
389 | flask.abort(404, f'Object with id "{object_id}" not found') | |
382 | 390 | return obj |
383 | 391 | |
384 | 392 | def _set_schema_context(self, context, **kwargs): |
452 | 460 | return self.order_field |
453 | 461 | |
454 | 462 | def index(self, **kwargs): |
463 | """ | |
464 | --- | |
465 | tags: [{tag_name}] | |
466 | summary: "Get a list of {class_model}." | |
467 | responses: | |
468 | 200: | |
469 | description: Ok | |
470 | content: | |
471 | application/json: | |
472 | schema: {schema_class} | |
473 | """ | |
455 | 474 | query = self._filter_query(self._get_eagerloaded_query(**kwargs)) |
456 | 475 | order_field = self._get_order_field(**kwargs) |
457 | 476 | if order_field is not None: |
503 | 522 | field_instance = schema.fields[order_field] |
504 | 523 | except KeyError: |
505 | 524 | if self.sort_pass_silently: |
506 | logger.warn("Unknown field: %s" % order_field) | |
525 | logger.warn(f"Unknown field: {order_field}") | |
507 | 526 | return self.order_field |
508 | raise InvalidUsage("Unknown field: %s" % order_field) | |
527 | raise InvalidUsage(f"Unknown field: {order_field}") | |
509 | 528 | |
510 | 529 | # Translate from the field name in the schema to the database field |
511 | 530 | # name |
516 | 535 | model_class = self.sort_model_class or self.model_class |
517 | 536 | if order_field not in inspect(model_class).attrs: |
518 | 537 | if self.sort_pass_silently: |
519 | logger.warn("Field not in the DB: %s" % order_field) | |
538 | logger.warn(f"Field not in the DB: {order_field}") | |
520 | 539 | return self.order_field |
521 | 540 | # It could be something like fields.Method |
522 | raise InvalidUsage("Field not in the DB: %s" % order_field) | |
541 | raise InvalidUsage(f"Field not in the DB: {order_field}") | |
523 | 542 | |
524 | 543 | if hasattr(model_class, order_field + '_id'): |
525 | 544 | # Ugly hack to allow sorting by a parent |
530 | 549 | self.default_sort_direction) |
531 | 550 | if sort_dir not in ('asc', 'desc'): |
532 | 551 | if self.sort_pass_silently: |
533 | logger.warn("Invalid value for sorting direction: %s" % | |
534 | sort_dir) | |
552 | logger.warn(f"Invalid value for sorting direction: {sort_dir}") | |
535 | 553 | return self.order_field |
536 | raise InvalidUsage("Invalid value for sorting direction: %s" % | |
537 | sort_dir) | |
554 | raise InvalidUsage(f"Invalid value for sorting direction: {sort_dir}") | |
538 | 555 | try: |
539 | 556 | if self.order_field is not None: |
540 | 557 | if not isinstance(self.order_field, tuple): |
544 | 561 | return getattr(field, sort_dir)() |
545 | 562 | except NotImplementedError: |
546 | 563 | if self.sort_pass_silently: |
547 | logger.warn("field {} doesn't support sorting".format( | |
548 | order_field | |
549 | )) | |
564 | logger.warn(f"field {order_field} doesn't support sorting") | |
550 | 565 | return self.order_field |
551 | 566 | # There are some fields that can't be used for sorting |
552 | raise InvalidUsage("field {} doesn't support sorting".format( | |
553 | order_field | |
554 | )) | |
567 | raise InvalidUsage(f"field {order_field} doesn't support sorting") | |
555 | 568 | |
556 | 569 | |
557 | 570 | class PaginatedMixin: |
591 | 604 | def _filter_query(self, query): |
592 | 605 | assert self.filterset_class is not None, 'You must define a filterset' |
593 | 606 | return self.filterset_class(query).filter() |
607 | ||
608 | ||
609 | class FilterWorkspacedMixin(ListMixin): | |
610 | """Add filter endpoint for searching on any workspaced objects columns | |
611 | """ | |
612 | @route('/filter') | |
613 | def filter(self, workspace_name): | |
614 | """ | |
615 | --- | |
616 | tags: [Filter, {tag_name}] | |
617 | description: Filters, sorts and groups workspaced objects using a json with parameters. These parameters must be part of the model. | |
618 | parameters: | |
619 | - in: query | |
620 | name: q | |
621 | description: recursive json with filters that supports operators. The json could also contain sort and group. | |
622 | responses: | |
623 | 200: | |
624 | description: returns filtered, sorted and grouped results | |
625 | content: | |
626 | application/json: | |
627 | schema: FlaskRestlessSchema | |
628 | 400: | |
629 | description: invalid q was sent to the server | |
630 | """ | |
631 | filters = flask.request.args.get('q', '{"filters": []}') | |
632 | filtered_objs, count = self._filter(filters, workspace_name) | |
633 | ||
634 | class PageMeta: | |
635 | total = 0 | |
636 | pagination_metadata = PageMeta() | |
637 | pagination_metadata.total = count | |
638 | return self._envelope_list(filtered_objs, pagination_metadata) | |
639 | ||
640 | def _generate_filter_query(self, filters, workspace, severity_count=False): | |
641 | filter_query = search(db.session, | |
642 | self.model_class, | |
643 | filters) | |
644 | ||
645 | filter_query = filter_query.filter(self.model_class.workspace == workspace) | |
646 | ||
647 | if severity_count and 'group_by' not in filters: | |
648 | filter_query = count_vulnerability_severities(filter_query, self.model_class, | |
649 | all_severities=True, host_vulns=True) | |
650 | ||
651 | return filter_query | |
652 | ||
653 | def _filter(self, filters, workspace_name, severity_count=False): | |
654 | marshmallow_params = {'many': True, 'context': {}} | |
655 | try: | |
656 | filters = FlaskRestlessSchema().load(json.loads(filters)) or {} | |
657 | except (ValidationError, JSONDecodeError) as ex: | |
658 | logger.exception(ex) | |
659 | flask.abort(400, "Invalid filters") | |
660 | ||
661 | workspace = self._get_workspace(workspace_name) | |
662 | if 'group_by' not in filters: | |
663 | offset = None | |
664 | limit = None | |
665 | if 'offset' in filters: | |
666 | offset = filters.pop('offset') | |
667 | if 'limit' in filters: | |
668 | limit = filters.pop('limit') # we need to remove pagination, since | |
669 | ||
670 | filter_query = self._generate_filter_query( | |
671 | filters, | |
672 | workspace, | |
673 | severity_count=severity_count | |
674 | ) | |
675 | ||
676 | if limit: | |
677 | filter_query = filter_query.limit(limit) | |
678 | if offset: | |
679 | filter_query = filter_query.offset(offset) | |
680 | count = filter_query.count() | |
681 | objs = self.schema_class(**marshmallow_params).dumps(filter_query.all()) | |
682 | return json.loads(objs), count | |
683 | else: | |
684 | filter_query = self._generate_filter_query( | |
685 | filters, | |
686 | workspace, | |
687 | ) | |
688 | column_names = ['count'] + [field['field'] for field in filters.get('group_by', [])] | |
689 | rows = [list(zip(column_names, row)) for row in filter_query.all()] | |
690 | data = [] | |
691 | for row in rows: | |
692 | data.append({field[0]: field[1] for field in row}) | |
693 | ||
694 | return data, len(rows) | |
695 | ||
696 | ||
697 | class FilterMixin(ListMixin): | |
698 | """Add filter endpoint for searching on any non workspaced objects columns | |
699 | """ | |
700 | ||
701 | @route('/filter') | |
702 | def filter(self): | |
703 | """ | |
704 | --- | |
705 | tags: ["Filter", {tag_name}] | |
706 | description: Filters, sorts and groups non workspaced objects using a json with parameters. These parameters must be part of the model. | |
707 | parameters: | |
708 | - in: query | |
709 | name: q | |
710 | description: Recursive json with filters that supports operators. The json could also contain sort and group. | |
711 | responses: | |
712 | 200: | |
713 | description: Returns filtered, sorted and grouped results | |
714 | content: | |
715 | application/json: | |
716 | schema: FlaskRestlessSchema | |
717 | 400: | |
718 | description: Invalid q was sent to the server | |
719 | """ | |
720 | filters = flask.request.args.get('q', '{"filters": []}') | |
721 | filtered_objs, count = self._filter(filters) | |
722 | ||
723 | class PageMeta: | |
724 | total = 0 | |
725 | pagination_metadata = PageMeta() | |
726 | pagination_metadata.total = count | |
727 | return self._envelope_list(filtered_objs, pagination_metadata) | |
728 | ||
729 | def _generate_filter_query(self, filters, severity_count=False, host_vulns=False): | |
730 | filter_query = search(db.session, | |
731 | self.model_class, | |
732 | filters) | |
733 | ||
734 | if severity_count and 'group_by' not in filters: | |
735 | filter_query = count_vulnerability_severities(filter_query, self.model_class, | |
736 | all_severities=True, host_vulns=host_vulns) | |
737 | ||
738 | return filter_query | |
739 | ||
740 | def _filter(self, filters: str, extra_alchemy_filters: BooleanClauseList = None, | |
741 | severity_count=False, host_vulns=False) -> Tuple[list, int]: | |
742 | marshmallow_params = {'many': True, 'context': {}} | |
743 | try: | |
744 | filters = FlaskRestlessSchema().load(json.loads(filters)) or {} | |
745 | except (ValidationError, JSONDecodeError) as ex: | |
746 | logger.exception(ex) | |
747 | flask.abort(400, "Invalid filters") | |
748 | ||
749 | if 'group_by' not in filters: | |
750 | offset = None | |
751 | limit = None | |
752 | if 'offset' in filters: | |
753 | offset = filters.pop('offset') | |
754 | if 'limit' in filters: | |
755 | limit = filters.pop('limit') # we need to remove pagination, since | |
756 | ||
757 | filter_query = self._generate_filter_query( | |
758 | filters, | |
759 | severity_count=severity_count, | |
760 | host_vulns=host_vulns | |
761 | ) | |
762 | ||
763 | if extra_alchemy_filters is not None: | |
764 | filter_query = filter_query.filter(extra_alchemy_filters) | |
765 | if limit: | |
766 | filter_query = filter_query.limit(limit) | |
767 | if offset: | |
768 | filter_query = filter_query.offset(offset) | |
769 | count = filter_query.count() | |
770 | objs = self.schema_class(**marshmallow_params).dumps(filter_query.all()) | |
771 | return json.loads(objs), count | |
772 | else: | |
773 | filter_query = self._generate_filter_query( | |
774 | filters, | |
775 | ) | |
776 | if extra_alchemy_filters is not None: | |
777 | filter_query += filter_query.filter(extra_alchemy_filters) | |
778 | ||
779 | column_names = ['count'] + [field['field'] for field in filters.get('group_by', [])] | |
780 | rows = [list(zip(column_names, row)) for row in filter_query.all()] | |
781 | data = [] | |
782 | for row in rows: | |
783 | data.append({field[0]: field[1] for field in row}) | |
784 | ||
785 | return data, len(rows) | |
594 | 786 | |
595 | 787 | |
596 | 788 | class ListWorkspacedMixin(ListMixin): |
1076 | 1268 | count_extra_filters = [] |
1077 | 1269 | |
1078 | 1270 | def count(self, **kwargs): |
1271 | """ | |
1272 | --- | |
1273 | tags: [{tag_name}] | |
1274 | summary: "Group {class_model} by the field set in the group_by GET parameter." | |
1275 | responses: | |
1276 | 200: | |
1277 | description: Ok | |
1278 | content: | |
1279 | application/json: | |
1280 | schema: {schema_class} | |
1281 | 404: | |
1282 | description: group_by is not specified | |
1283 | """ | |
1079 | 1284 | res = { |
1080 | 1285 | 'groups': [], |
1081 | 1286 | 'total_count': 0 |
1098 | 1303 | # using format is not a great practice. |
1099 | 1304 | # the user input is group_by, however it's filtered by column name. |
1100 | 1305 | table_name = inspect(self.model_class).tables[0].name |
1101 | group_by = '{0}.{1}'.format(table_name, group_by) | |
1306 | group_by = f'{table_name}.{group_by}' | |
1102 | 1307 | |
1103 | 1308 | count = self._filter_query( |
1104 | 1309 | db.session.query(self.model_class) |
1146 | 1351 | count_extra_filters = [] |
1147 | 1352 | |
1148 | 1353 | def count_multi_workspace(self, **kwargs): |
1354 | """ | |
1355 | --- | |
1356 | tags: [{tag_name}] | |
1357 | summary: "Count {class_model} by multiples workspaces" | |
1358 | responses: | |
1359 | 200: | |
1360 | description: Ok | |
1361 | content: | |
1362 | application/json: | |
1363 | schema: {schema_class} | |
1364 | 400: | |
1365 | description: No workspace passed or group_by is not specified | |
1366 | """ | |
1367 | #"""head: | |
1368 | # tags: [{tag_name}] | |
1369 | # responses: | |
1370 | # 200: | |
1371 | # description: Ok | |
1372 | # options: | |
1373 | # tags: [{tag_name}] | |
1374 | # responses: | |
1375 | # 200: | |
1376 | # description: Ok | |
1377 | # """ | |
1149 | 1378 | res = { |
1150 | 1379 | 'groups': defaultdict(dict), |
1151 | 1380 | 'total_count': 0 |
0 | 0 | # Faraday Penetration Test IDE |
1 | 1 | # Copyright (C) 2019 Infobyte LLC (http://www.infobytesec.com/) |
2 | 2 | # See the file 'doc/LICENSE' for the license information |
3 | from datetime import datetime | |
4 | ||
3 | 5 | import flask |
4 | 6 | import logging |
5 | 7 | |
20 | 22 | ) |
21 | 23 | from faraday.server.api.modules.workspaces import WorkspaceSchema |
22 | 24 | from faraday.server.models import Agent, Executor, AgentExecution, db, \ |
23 | Workspace | |
25 | Workspace, Command | |
24 | 26 | from faraday.server.schemas import PrimaryKeyRelatedField |
25 | 27 | from faraday.server.config import faraday_server |
26 | 28 | from faraday.server.events import changes_queue |
117 | 119 | try: |
118 | 120 | ws = Workspace.query.filter_by(name=workspace_name).one() |
119 | 121 | if not ws.active: |
120 | flask.abort(403, "Disabled workspace: %s" % workspace_name) | |
122 | flask.abort(403, f"Disabled workspace: {workspace_name}") | |
121 | 123 | return ws |
122 | 124 | except NoResultFound: |
123 | flask.abort(404, "No such workspace: %s" % workspace_name) | |
125 | flask.abort(404, f"No such workspace: {workspace_name}") | |
124 | 126 | |
125 | 127 | def _perform_create(self, data, **kwargs): |
126 | 128 | token = data.pop('token') |
176 | 178 | required=True |
177 | 179 | ) |
178 | 180 | |
181 | def __init__(self, *args, **kwargs): | |
182 | super(AgentRunSchema, self).__init__(*args, **kwargs) | |
183 | self.unknown = EXCLUDE | |
184 | ||
179 | 185 | |
180 | 186 | class AgentWithWorkspacesView(UpdateMixin, |
181 | 187 | DeleteMixin, |
189 | 195 | try: |
190 | 196 | ws = Workspace.query.filter_by(name=workspace_name).one() |
191 | 197 | if not ws.active: |
192 | flask.abort(403, "Disabled workspace: %s" % workspace_name) | |
198 | flask.abort(403, f"Disabled workspace: {workspace_name}") | |
193 | 199 | return ws |
194 | 200 | except NoResultFound: |
195 | flask.abort(404, "No such workspace: %s" % workspace_name) | |
201 | flask.abort(404, f"No such workspace: {workspace_name}") | |
196 | 202 | |
197 | 203 | def _update_object(self, obj, data): |
198 | 204 | """Perform changes in the selected object |
289 | 295 | try: |
290 | 296 | executor = Executor.query.filter(Executor.name == executor_data['executor'], |
291 | 297 | Executor.agent_id == agent_id).one() |
298 | params = ', '.join([f'{key}={value}' for (key, value) in executor_data["args"].items()]) | |
299 | command = Command( | |
300 | import_source="agent", | |
301 | tool=agent.name, | |
302 | command=executor.name, | |
303 | user='', | |
304 | hostname='', | |
305 | params=params, | |
306 | start_date=datetime.now(), | |
307 | workspace=workspace | |
308 | ) | |
292 | 309 | |
293 | 310 | agent_execution = AgentExecution( |
294 | 311 | running=None, |
296 | 313 | message='', |
297 | 314 | executor=executor, |
298 | 315 | workspace_id=workspace.id, |
299 | parameters_data=executor_data["args"] | |
316 | parameters_data=executor_data["args"], | |
317 | command=command | |
300 | 318 | ) |
301 | 319 | db.session.add(agent_execution) |
302 | 320 | db.session.commit() |
312 | 330 | except NoResultFound as e: |
313 | 331 | logger.exception(e) |
314 | 332 | abort(400, "Can not find an agent execution with that id") |
315 | ||
316 | return flask.jsonify({ | |
317 | 'successful': True, | |
318 | }) | |
333 | else: | |
334 | return flask.jsonify({ | |
335 | 'command_id': command.id, | |
336 | }) | |
319 | 337 | |
320 | 338 | |
321 | 339 | AgentWithWorkspacesView.register(agent_api) |
23 | 23 | schema_class = AgentAuthTokenSchema |
24 | 24 | |
25 | 25 | def index(self): |
26 | """ | |
27 | --- | |
28 | get: | |
29 | summary: "Get a token to register new agents." | |
30 | tags: ["Agent"] | |
31 | responses: | |
32 | 200: | |
33 | description: Ok | |
34 | content: | |
35 | application/json: | |
36 | schema: AgentAuthTokenSchema | |
37 | tags: ["Agent"] | |
38 | responses: | |
39 | 200: | |
40 | description: Ok | |
41 | """ | |
26 | 42 | return AgentAuthTokenSchema().dump( |
27 | 43 | {'token': faraday_server.agent_token}) |
28 | 44 | |
29 | 45 | def post(self): |
46 | """ | |
47 | --- | |
48 | post: | |
49 | summary: "Generate a new token to register new agents." | |
50 | tags: ["Agent"] | |
51 | responses: | |
52 | 200: | |
53 | description: Ok | |
54 | content: | |
55 | application/json: | |
56 | schema: AgentAuthTokenSchema | |
57 | """ | |
30 | 58 | from faraday.server.app import save_new_agent_creation_token # pylint:disable=import-outside-toplevel |
31 | 59 | try: |
32 | 60 | validate_csrf(flask.request.form.get('csrf_token')) |
0 | 0 | import logging |
1 | 1 | from datetime import datetime, timedelta |
2 | from typing import Type, Optional | |
3 | ||
2 | 4 | import flask |
3 | 5 | import sqlalchemy |
4 | 6 | from sqlalchemy.orm.exc import NoResultFound |
8 | 10 | Schema, |
9 | 11 | utils, |
10 | 12 | ValidationError, |
13 | validates_schema, | |
11 | 14 | ) |
12 | 15 | from marshmallow.validate import Range |
13 | 16 | from faraday.server.models import ( |
20 | 23 | Service, |
21 | 24 | Vulnerability, |
22 | 25 | VulnerabilityWeb, |
23 | AgentExecution) | |
26 | AgentExecution, | |
27 | Workspace, | |
28 | Metadata | |
29 | ) | |
24 | 30 | from faraday.server.utils.database import ( |
25 | 31 | get_conflict_object, |
26 | 32 | is_unique_constraint_violation, |
141 | 147 | class Meta(hosts.HostSchema.Meta): |
142 | 148 | fields = hosts.HostSchema.Meta.fields + ('services', 'vulnerabilities') |
143 | 149 | |
150 | @validates_schema | |
151 | def validate_schema(self, data, **kwargs): | |
152 | for vulnerability in data['vulnerabilities']: | |
153 | if vulnerability['type'] != 'vulnerability': | |
154 | raise ValidationError('Type "Vulnerability Web" cannot have "Host" type as a parent') | |
155 | ||
144 | 156 | |
145 | 157 | class BulkCommandSchema(AutoSchema): |
146 | 158 | """The schema of faraday/server/api/modules/commandsrun.py has a lot |
148 | 160 | |
149 | 161 | I don't need that here, so I'll write a schema from scratch.""" |
150 | 162 | |
151 | duration = fields.TimeDelta('microseconds', required=True) | |
163 | duration = fields.TimeDelta('microseconds', required=False) | |
152 | 164 | |
153 | 165 | class Meta: |
154 | 166 | model = Command |
159 | 171 | |
160 | 172 | @post_load |
161 | 173 | def load_end_date(self, data, **kwargs): |
162 | duration = data.pop('duration') | |
163 | data['end_date'] = data['start_date'] + duration | |
174 | if 'duration' in data: | |
175 | duration = data.pop('duration') | |
176 | data['end_date'] = data['start_date'] + duration | |
164 | 177 | return data |
165 | 178 | |
166 | 179 | |
177 | 190 | execution_id = fields.Integer(attribute='execution_id') |
178 | 191 | |
179 | 192 | |
180 | def get_or_create(ws, model_class, data): | |
193 | def get_or_create(ws: Workspace, model_class: Type[Metadata], data: dict): | |
181 | 194 | """Check for conflicts and create a new object |
182 | 195 | |
183 | 196 | Is is passed the data parsed by the marshmallow schema (it |
202 | 215 | return (True, obj) |
203 | 216 | |
204 | 217 | |
205 | def bulk_create(ws, data, data_already_deserialized=False): | |
218 | def bulk_create(ws: Workspace, | |
219 | command: Optional[Command], | |
220 | data: dict, | |
221 | data_already_deserialized: bool = False, | |
222 | set_end_date: bool = True): | |
206 | 223 | if not data_already_deserialized: |
207 | 224 | schema = BulkCreateSchema() |
208 | 225 | data = schema.load(data) |
209 | 226 | if 'command' in data: |
210 | command = _create_command(ws, data['command']) | |
211 | else: | |
212 | command = None | |
227 | command = _update_command(command, data['command']) | |
213 | 228 | for host in data['hosts']: |
214 | 229 | _create_host(ws, host, command) |
215 | ||
216 | ||
217 | def _create_command(ws, command_data): | |
218 | (created, command) = get_or_create(ws, Command, command_data) | |
219 | assert created # There isn't an unique constraint in command | |
230 | if 'command' in data and set_end_date: | |
231 | command.end_date = datetime.now() if command.end_date is None else \ | |
232 | command.end_date | |
233 | db.session.commit() | |
234 | ||
235 | ||
236 | def _update_command(command: Command, command_data: dict): | |
237 | db.session.query(Command).filter(Command.id == command.id).update(command_data) | |
238 | db.session.commit() | |
220 | 239 | return command |
221 | 240 | |
222 | 241 | |
376 | 395 | content: |
377 | 396 | application/json: |
378 | 397 | schema: BulkCreateSchema |
398 | 401: | |
399 | $ref: "#/components/responses/UnauthorizedError" | |
379 | 400 | 403: |
380 | 401 | description: Disabled workspace |
381 | 402 | 404: |
388 | 409 | workspace = self._get_workspace(workspace_name) |
389 | 410 | |
390 | 411 | if not workspace or workspace not in agent.workspaces: |
391 | flask.abort(404, "No such workspace: %s" % workspace_name) | |
412 | flask.abort(404, f"No such workspace: {workspace_name}") | |
392 | 413 | |
393 | 414 | if "execution_id" not in data: |
394 | 415 | flask.abort(400, "'execution_id' argument expected") |
395 | 416 | |
396 | 417 | execution_id = data["execution_id"] |
397 | 418 | |
398 | agent_execution = AgentExecution.query.filter( | |
419 | agent_execution: AgentExecution = AgentExecution.query.filter( | |
399 | 420 | AgentExecution.id == execution_id |
400 | 421 | ).one_or_none() |
401 | 422 | |
413 | 434 | ) |
414 | 435 | flask.abort(400, "Trying to write to the incorrect workspace") |
415 | 436 | |
416 | now = datetime.now() | |
417 | ||
418 | 437 | params_data = agent_execution.parameters_data |
419 | 438 | params = ', '.join([f'{key}={value}' for (key, value) in params_data.items()]) |
420 | 439 | |
440 | start_date = (data["command"].get("start_date") or agent_execution.command.start_date) \ | |
441 | if "command" in data else agent_execution.command.start_date | |
442 | ||
443 | end_date = data["command"].get("end_date", None) if "command" in data else None | |
421 | 444 | |
422 | 445 | data["command"] = { |
446 | 'id': agent_execution.command.id, | |
423 | 447 | 'tool': agent.name, # Agent name |
424 | 448 | 'command': agent_execution.executor.name, |
425 | 449 | 'user': '', |
426 | 450 | 'hostname': '', |
427 | 451 | 'params': params, |
428 | 452 | 'import_source': 'agent', |
429 | 'start_date': (data["command"].get("start_date") or now) if "command" in data else now, #Now or when received run | |
430 | 'end_date': (data["command"].get("start_date") or now) if "command" in data else now, #Now or when received run | |
453 | 'start_date': start_date | |
431 | 454 | } |
455 | ||
456 | if end_date is not None: | |
457 | data["command"]["end_date"] = end_date | |
458 | ||
459 | command = Command.query.filter(Command.id == agent_execution.command.id).one_or_none() | |
460 | if command is None: | |
461 | logger.exception( | |
462 | ValueError(f"There is no command with {agent_execution.command.id}") | |
463 | ) | |
464 | flask.abort(400, "Trying to update a not existent command") | |
465 | ||
466 | _update_command(command, data['command']) | |
467 | db.session.flush() | |
468 | ||
469 | ||
432 | 470 | else: |
433 | 471 | workspace = self._get_workspace(workspace_name) |
434 | 472 | creator_user = flask.g.user |
435 | data = add_creator(data,creator_user) | |
436 | ||
437 | bulk_create(workspace, data, True) | |
438 | return "Created", 201 | |
473 | data = add_creator(data, creator_user) | |
474 | ||
475 | if 'command' in data: | |
476 | command = Command(**(data['command'])) | |
477 | command.workspace = workspace | |
478 | db.session.add(command) | |
479 | db.session.commit() | |
480 | else: | |
481 | # Here the data won't appear in the activity field | |
482 | command = None | |
483 | ||
484 | bulk_create(workspace, command, data, True, False) | |
485 | return flask.jsonify( | |
486 | { | |
487 | "message": "Created", | |
488 | "command_id": None if command is None else command.id | |
489 | } | |
490 | ), 201 | |
439 | 491 | |
440 | 492 | post.is_public = True |
441 | 493 | |
442 | 494 | BulkCreateView.register(bulk_create_api) |
443 | ||
444 |
79 | 79 | |
80 | 80 | @route('/activity_feed/') |
81 | 81 | def activity_feed(self, workspace_name): |
82 | """ | |
83 | --- | |
84 | tags: ["Command"] | |
85 | description: Gets a summary of the lastest executed commands | |
86 | responses: | |
87 | 200: | |
88 | description: Ok | |
89 | content: | |
90 | application/json: | |
91 | schema: CommandSchema | |
92 | """ | |
82 | 93 | res = [] |
83 | 94 | query = Command.query.join(Workspace).filter_by(name=workspace_name) |
84 | 95 | for command in query.all(): |
97 | 108 | }) |
98 | 109 | return res |
99 | 110 | |
100 | @route('/last/') | |
111 | @route('/last/', methods=['GET']) | |
101 | 112 | def last_command(self, workspace_name): |
102 | 113 | """ |
103 | 114 | --- |
104 | get: | |
105 | tags: ["Commands"] | |
115 | tags: ["Command"] | |
106 | 116 | description: Gets the last executed command |
107 | 117 | responses: |
108 | 118 | 200: |
109 | description: Last executed command or an empty json | |
119 | description: Last executed command or an empty json | |
110 | 120 | """ |
111 | 121 | command = Command.query.join(Workspace).filter_by(name=workspace_name).order_by(Command.start_date.desc()).first() |
112 | 122 | command_obj = {} |
48 | 48 | route_base = 'comment' |
49 | 49 | model_class = Comment |
50 | 50 | schema_class = CommentSchema |
51 | ||
51 | order_field = 'create_date' | |
52 | 52 | |
53 | 53 | class UniqueCommentView(GenericWorkspacedView, CommentCreateMixing): |
54 | 54 | """ |
79 | 79 | |
80 | 80 | CommentView.register(comment_api) |
81 | 81 | UniqueCommentView.register(comment_api) |
82 | # I'm Py3⏎ | |
82 | # I'm Py3 |
79 | 79 | not_parent_field = 'host_id' |
80 | 80 | else: |
81 | 81 | raise ValidationError( |
82 | 'Unknown parent type: {}'.format(parent_type)) | |
82 | f'Unknown parent type: {parent_type}') | |
83 | 83 | try: |
84 | 84 | parent = db.session.query(parent_class).join(Workspace).filter( |
85 | 85 | Workspace.name == self.context['workspace_name'], |
86 | 86 | parent_class.id == parent_id).one() |
87 | 87 | except NoResultFound: |
88 | raise InvalidUsage('Parent id not found: {}'.format(parent_id)) | |
88 | raise InvalidUsage(f'Parent id not found: {parent_id}') | |
89 | 89 | data[parent_field] = parent.id |
90 | 90 | data[not_parent_field] = None |
91 | 91 | return data |
16 | 16 | workspace = Workspace.query.filter_by(name=workspace_name).first() |
17 | 17 | if not workspace: |
18 | 18 | logger.error("No such workspace. Please, specify a valid workspace.") |
19 | abort(404, "No such workspace: %s" % workspace_name) | |
19 | abort(404, f"No such workspace: {workspace_name}") | |
20 | 20 | |
21 | 21 | export_format = request.args.get('format', '') |
22 | 22 | if not export_format: |
27 | 27 | memory_file = xml_metasploit_format(workspace) |
28 | 28 | return send_file( |
29 | 29 | memory_file, |
30 | attachment_filename="Faraday-%s-data.xml" % workspace_name, | |
30 | attachment_filename=f"Faraday-{workspace_name}-data.xml", | |
31 | 31 | as_attachment=True, |
32 | 32 | cache_timeout=-1 |
33 | 33 | ) |
19 | 19 | """ |
20 | 20 | |
21 | 21 | logger.debug( |
22 | "Request parameters: {!r}".format(flask.request.args)) | |
22 | f"Request parameters: {flask.request.args!r}") | |
23 | 23 | |
24 | 24 | headers = { |
25 | 25 | "Content-Type": "application/json", |
63 | 63 | json_response["exploitdb"].append(obj_module) |
64 | 64 | |
65 | 65 | except KeyError as ex: |
66 | abort(make_response(jsonify(message='Could not find {0}'.format(str(ex))), 400)) | |
66 | abort(make_response(jsonify(message=f'Could not find {str(ex)}'), 400)) | |
67 | 67 | |
68 | 68 | return flask.jsonify(json_response) |
69 | # I'm Py3⏎ | |
69 | # I'm Py3 |
23 | 23 | AutoSchema, |
24 | 24 | FilterAlchemyMixin, |
25 | 25 | FilterSetMeta, |
26 | ) | |
26 | FilterWorkspacedMixin) | |
27 | 27 | from faraday.server.schemas import ( |
28 | 28 | MetadataSchema, |
29 | 29 | MutableField, |
37 | 37 | host_api = Blueprint('host_api', __name__) |
38 | 38 | |
39 | 39 | logger = logging.getLogger(__name__) |
40 | ||
41 | ||
42 | ||
43 | class HostCountSchema(Schema): | |
44 | host_id = fields.Integer(dump_only=True, allow_none=False, | |
45 | attribute='id') | |
46 | critical = fields.Integer(dump_only=True, allow_none=False, | |
47 | attribute='vulnerability_critical_count') | |
48 | high = fields.Integer(dump_only=True, allow_none=False, | |
49 | attribute='vulnerability_high_count') | |
50 | med = fields.Integer(dump_only=True, allow_none=False, | |
51 | attribute='vulnerability_medium_count') | |
52 | low = fields.Integer(dump_only=True, allow_none=False, | |
53 | attribute='vulnerability_low_count') | |
54 | info = fields.Integer(dump_only=True, allow_none=False, | |
55 | attribute='vulnerability_informational_count') | |
56 | unclassified = fields.Integer(dump_only=True, allow_none=False, | |
57 | attribute='vulnerability_unclassified_count') | |
58 | total = fields.Integer(dump_only=True, allow_none=False, | |
59 | attribute='vulnerability_total_count') | |
40 | 60 | |
41 | 61 | |
42 | 62 | class HostSchema(AutoSchema): |
62 | 82 | fields.List(fields.String)) |
63 | 83 | metadata = SelfNestedField(MetadataSchema()) |
64 | 84 | type = fields.Function(lambda obj: 'Host', dump_only=True) |
65 | service_summaries = fields.Method('get_service_summaries', | |
66 | dump_only=True) | |
67 | versions = fields.Method('get_service_version', | |
68 | dump_only=True) | |
85 | service_summaries = fields.Method('get_service_summaries', dump_only=True) | |
86 | versions = fields.Method('get_service_version', dump_only=True) | |
87 | important = fields.Boolean(default=False) | |
88 | severity_counts = SelfNestedField(HostCountSchema(), dump_only=True) | |
69 | 89 | |
70 | 90 | class Meta: |
71 | 91 | model = Host |
72 | 92 | fields = ('id', '_id', '_rev', 'ip', 'description', 'mac', |
73 | 93 | 'credentials', 'default_gateway', 'metadata', |
74 | 94 | 'name', 'os', 'owned', 'owner', 'services', 'vulns', |
75 | 'hostnames', 'type', 'service_summaries', 'versions' | |
95 | 'hostnames', 'type', 'service_summaries', 'versions', | |
96 | 'important', 'severity_counts' | |
76 | 97 | ) |
77 | 98 | |
78 | 99 | def get_service_summaries(self, obj): |
112 | 133 | port = ServicePortFilter(fields.Str()) |
113 | 134 | |
114 | 135 | |
115 | class HostCountSchema(Schema): | |
116 | host_id = fields.Integer(dump_only=True, allow_none=False, | |
117 | attribute='id') | |
118 | critical = fields.Integer(dump_only=True, allow_none=False, | |
119 | attribute='vulnerability_critical_count') | |
120 | high = fields.Integer(dump_only=True, allow_none=False, | |
121 | attribute='vulnerability_high_count') | |
122 | med = fields.Integer(dump_only=True, allow_none=False, | |
123 | attribute='vulnerability_med_count') | |
124 | info = fields.Integer(dump_only=True, allow_none=False, | |
125 | attribute='vulnerability_info_count') | |
126 | unclassified = fields.Integer(dump_only=True, allow_none=False, | |
127 | attribute='vulnerability_unclassified_count') | |
128 | total = fields.Integer(dump_only=True, allow_none=False, | |
129 | attribute='vulnerability_total_count') | |
130 | 136 | |
131 | 137 | class HostsView(PaginatedMixin, |
132 | 138 | FilterAlchemyMixin, |
133 | ReadWriteWorkspacedView): | |
139 | ReadWriteWorkspacedView, | |
140 | FilterWorkspacedMixin): | |
134 | 141 | route_base = 'hosts' |
135 | 142 | model_class = Host |
136 | 143 | order_field = desc(Host.vulnerability_critical_generic_count),\ |
147 | 154 | Host.vulnerability_count] |
148 | 155 | get_joinedloads = [Host.hostnames, Host.services, Host.update_user] |
149 | 156 | |
157 | def _get_base_query(self, workspace_name): | |
158 | return Host.query_with_count(None, None, workspace_name) | |
159 | ||
160 | @route('/filter') | |
161 | def filter(self, workspace_name): | |
162 | """ | |
163 | --- | |
164 | get: | |
165 | tags: ["Filter", "Host"] | |
166 | description: Filters, sorts and groups hosts using a json with parameters. These parameters must be part of the model. | |
167 | parameters: | |
168 | - in: query | |
169 | name: q | |
170 | description: Recursive json with filters that supports operators. The json could also contain sort and group. | |
171 | responses: | |
172 | 200: | |
173 | description: Returns filtered, sorted and grouped results | |
174 | content: | |
175 | application/json: | |
176 | schema: FlaskRestlessSchema | |
177 | 400: | |
178 | description: Invalid q was sent to the server | |
179 | tags: ["Filter", "Host"] | |
180 | responses: | |
181 | 200: | |
182 | description: Ok | |
183 | """ | |
184 | filters = flask.request.args.get('q', '{"filters": []}') | |
185 | filtered_objs, count = self._filter(filters, workspace_name, severity_count=True) | |
186 | ||
187 | class PageMeta: | |
188 | total = 0 | |
189 | ||
190 | pagination_metadata = PageMeta() | |
191 | pagination_metadata.total = count | |
192 | return self._envelope_list(filtered_objs, pagination_metadata) | |
193 | ||
150 | 194 | @route('/bulk_create/', methods=['POST']) |
151 | 195 | def bulk_create(self, workspace_name): |
152 | 196 | """ |
153 | 197 | --- |
154 | 198 | post: |
155 | tags: ["Vulns"] | |
199 | tags: ["Bulk", "Host"] | |
156 | 200 | description: Creates hosts in bulk |
157 | 201 | responses: |
158 | 202 | 201: |
164 | 208 | description: Bad request |
165 | 209 | 403: |
166 | 210 | description: Forbidden |
211 | tags: ["Bulk", "Host"] | |
212 | responses: | |
213 | 200: | |
214 | description: Ok | |
167 | 215 | """ |
168 | 216 | try: |
169 | 217 | validate_csrf(flask.request.form.get('csrf_token')) |
186 | 234 | hosts_reader = csv.DictReader(stream) |
187 | 235 | if set(hosts_reader.fieldnames) != FILE_HEADERS: |
188 | 236 | logger.error("Missing Required headers in CSV (%s)", FILE_HEADERS) |
189 | abort(400, "Missing Required headers in CSV (%s)" % FILE_HEADERS) | |
237 | abort(400, f"Missing Required headers in CSV ({FILE_HEADERS})") | |
190 | 238 | hosts_created_count = 0 |
191 | 239 | hosts_with_errors_count = 0 |
192 | 240 | for host_dict in hosts_reader: |
208 | 256 | return make_response(jsonify(hosts_created=hosts_created_count, hosts_with_errors=hosts_with_errors_count), 200) |
209 | 257 | except Exception as e: |
210 | 258 | logger.error("Error parsing hosts CSV (%s)", e) |
211 | abort(400, "Error parsing hosts CSV (%s)" % e) | |
259 | abort(400, f"Error parsing hosts CSV ({e})") | |
212 | 260 | |
213 | 261 | |
214 | 262 | @route('/<host_id>/services/') |
215 | 263 | def service_list(self, workspace_name, host_id): |
264 | """ | |
265 | --- | |
266 | get: | |
267 | tags: ["Host", "Service"] | |
268 | summary: Get the services of a host | |
269 | responses: | |
270 | 200: | |
271 | description: Ok | |
272 | content: | |
273 | application/json: | |
274 | schema: ServiceSchema | |
275 | tags: ["Host", "Service"] | |
276 | responses: | |
277 | 200: | |
278 | description: Ok | |
279 | """ | |
216 | 280 | services = self._get_object(host_id, workspace_name).services |
217 | 281 | return ServiceSchema(many=True).dump(services) |
218 | 282 | |
221 | 285 | """ |
222 | 286 | --- |
223 | 287 | get: |
224 | tags: ["Hosts"] | |
288 | tags: ["Host"] | |
225 | 289 | summary: Counts Vulnerabilities per host |
226 | 290 | responses: |
227 | 291 | 200: |
229 | 293 | content: |
230 | 294 | application/json: |
231 | 295 | schema: HostCountSchema |
296 | tags: ["Host"] | |
297 | responses: | |
298 | 200: | |
299 | description: Ok | |
232 | 300 | """ |
233 | 301 | host_ids = flask.request.args.get('hosts', None) |
234 | 302 | if host_ids: |
249 | 317 | |
250 | 318 | @route('/<host_id>/tools_history/') |
251 | 319 | def tool_impacted_by_host(self, workspace_name, host_id): |
320 | """ | |
321 | --- | |
322 | get: | |
323 | tags: ["Host", "Command"] | |
324 | summary: "Get the command impacted by a host" | |
325 | responses: | |
326 | 200: | |
327 | description: Ok | |
328 | content: | |
329 | application/json: | |
330 | schema: CommandSchema | |
331 | tags: ["Host", "Command"] | |
332 | responses: | |
333 | 200: | |
334 | description: Ok | |
335 | """ | |
252 | 336 | workspace = self._get_workspace(workspace_name) |
253 | 337 | query = db.session.query(Host, Command).filter(Host.id == CommandObject.object_id, |
254 | 338 | CommandObject.object_type == 'host', |
302 | 386 | |
303 | 387 | def _envelope_list(self, objects, pagination_metadata=None): |
304 | 388 | hosts = [] |
305 | for host in objects: | |
389 | for index, host in enumerate(objects): | |
390 | # we use index when the filter endpoint uses group by and | |
391 | # the _id was not used in the group by | |
306 | 392 | hosts.append({ |
307 | 'id': host['id'], | |
308 | 'key': host['id'], | |
393 | 'id': host.get('_id', index), | |
394 | 'key': host.get('_id', index), | |
309 | 395 | 'value': host |
310 | 396 | }) |
311 | 397 | return { |
314 | 400 | or len(hosts)), |
315 | 401 | } |
316 | 402 | |
403 | # TODO SCHEMA | |
317 | 404 | @route('bulk_delete/', methods=['DELETE']) |
318 | 405 | def bulk_delete(self, workspace_name): |
406 | """ | |
407 | --- | |
408 | delete: | |
409 | tags: ["Bulk", "Host"] | |
410 | description: Delete hosts in bulk | |
411 | responses: | |
412 | 200: | |
413 | description: Ok | |
414 | 400: | |
415 | description: Bad request | |
416 | 403: | |
417 | description: Forbidden | |
418 | tags: ["Bulk", "Host"] | |
419 | responses: | |
420 | 200: | |
421 | description: Ok | |
422 | """ | |
319 | 423 | workspace = self._get_workspace(workspace_name) |
320 | 424 | json_request = flask.request.get_json() |
321 | 425 | if not json_request: |
84 | 84 | Host.id == host_id |
85 | 85 | ).one() |
86 | 86 | except NoResultFound: |
87 | raise ValidationError('Host with id {} not found'.format(host_id)) | |
87 | raise ValidationError(f'Host with id {host_id} not found') | |
88 | 88 | |
89 | 89 | return data |
90 | 90 |
18 | 18 | data['preferences'] = user.preferences |
19 | 19 | data['permissions'] = get_user_permissions(user) |
20 | 20 | return jsonify(data) |
21 |
17 | 17 | serializer = TimedJSONWebSignatureSerializer( |
18 | 18 | app.config['SECRET_KEY'], |
19 | 19 | salt="api_token", |
20 | expires_in=faraday_server.api_token_expiration | |
20 | expires_in=int(faraday_server.api_token_expiration) | |
21 | 21 | ) |
22 | 22 | hashed_data = hash_data(g.user.password) if g.user.password else None |
23 | 23 | return serializer.dumps({'user_id': user_id, "validation_check": hashed_data}).decode('utf-8') |
24 | 24 | |
25 | 25 | |
26 | 26 | TokenAuthView.register(token_api) |
27 | ||
28 | # I'm Py3 |
0 | 0 | # Faraday Penetration Test IDE |
1 | 1 | # Copyright (C) 2018 Infobyte LLC (http://www.infobytesec.com/) |
2 | 2 | # See the file 'doc/LICENSE' for the license information |
3 | import os | |
4 | 3 | import string |
5 | 4 | import random |
6 | 5 | import logging |
6 | from datetime import datetime | |
7 | 7 | |
8 | 8 | from faraday.server.config import CONST_FARADAY_HOME_PATH |
9 | 9 | from faraday.server.threads.reports_processor import REPORTS_QUEUE |
21 | 21 | from wtforms import ValidationError |
22 | 22 | |
23 | 23 | from faraday.server.utils.web import gzipped |
24 | from faraday.server.models import Workspace | |
24 | from faraday.server.models import Workspace, Command, db | |
25 | 25 | from faraday.server import config |
26 | 26 | |
27 | 27 | from faraday_plugins.plugins.manager import PluginsManager, ReportAnalyzer |
44 | 44 | ws = Workspace.query.filter_by(name=workspace).first() |
45 | 45 | if not ws or not ws.active: |
46 | 46 | # Don't raise a 403 to prevent workspace name enumeration |
47 | abort(404, "Workspace disabled: %s" % workspace) | |
47 | abort(404, f"Workspace disabled: {workspace}") | |
48 | 48 | |
49 | 49 | if 'file' not in request.files: |
50 | 50 | abort(400) |
60 | 60 | |
61 | 61 | chars = string.ascii_uppercase + string.digits |
62 | 62 | random_prefix = ''.join(random.choice(chars) for x in range(12)) # nosec |
63 | raw_report_filename = '{0}_{1}'.format(random_prefix, secure_filename(report_file.filename)) | |
63 | raw_report_filename = f'{random_prefix}_{secure_filename(report_file.filename)}' | |
64 | 64 | |
65 | 65 | try: |
66 | file_path = os.path.join(CONST_FARADAY_HOME_PATH, 'uploaded_reports', raw_report_filename) | |
67 | with open(file_path, 'wb') as output: | |
66 | file_path = CONST_FARADAY_HOME_PATH / 'uploaded_reports' \ | |
67 | / raw_report_filename | |
68 | with file_path.open('wb') as output: | |
68 | 69 | output.write(report_file.read()) |
69 | 70 | except AttributeError: |
70 | 71 | logger.warning( |
71 | 72 | "Upload reports in WEB-UI not configurated, run Faraday client and try again...") |
72 | 73 | abort(make_response(jsonify(message="Upload reports not configurated: Run faraday client and start Faraday server again"), 500)) |
73 | 74 | else: |
74 | logger.info("Get plugin for file: %s", file_path) | |
75 | logger.info(f"Get plugin for file: {file_path}") | |
75 | 76 | plugin = report_analyzer.get_plugin(file_path) |
76 | 77 | if not plugin: |
77 | 78 | logger.info("Could not get plugin for file") |
78 | 79 | abort(make_response(jsonify(message="Invalid report file"), 400)) |
79 | 80 | else: |
80 | logger.info("Plugin for file: %s Plugin: %s", file_path, plugin.id) | |
81 | REPORTS_QUEUE.put((workspace, file_path, plugin.id, flask.g.user)) | |
82 | return make_response(jsonify(message="ok"), 200) | |
81 | logger.info( | |
82 | f"Plugin for file: {file_path} Plugin: {plugin.id}" | |
83 | ) | |
84 | workspace_instance = Workspace.query.filter_by( | |
85 | name=workspace).one() | |
86 | command = Command() | |
87 | command.workspace = workspace_instance | |
88 | command.start_date = datetime.now() | |
89 | command.import_source = 'report' | |
90 | # The data will be updated in the bulk_create function | |
91 | command.tool = "In progress" | |
92 | command.command = "In progress" | |
93 | ||
94 | db.session.add(command) | |
95 | db.session.commit() | |
96 | ||
97 | REPORTS_QUEUE.put( | |
98 | ( | |
99 | workspace_instance.name, | |
100 | command.id, | |
101 | file_path, | |
102 | plugin.id, | |
103 | flask.g.user.id | |
104 | ) | |
105 | ) | |
106 | return make_response( | |
107 | jsonify(message="ok", command_id=command.id), | |
108 | 200 | |
109 | ) | |
83 | 110 | else: |
84 | 111 | abort(make_response(jsonify(message="Missing report file"), 400)) |
85 | # I'm Py3 |
3 | 3 | from builtins import str, bytes |
4 | 4 | from io import TextIOWrapper |
5 | 5 | |
6 | import json | |
6 | 7 | import threading |
7 | 8 | import logging |
8 | 9 | import csv |
9 | 10 | |
11 | from werkzeug.exceptions import Conflict | |
10 | 12 | from flask import Blueprint, request, abort, jsonify, make_response |
11 | 13 | from flask_classful import route |
12 | 14 | from filteralchemy import ( |
25 | 27 | FilterSetMeta, |
26 | 28 | PaginatedMixin, |
27 | 29 | ReadWriteView, |
28 | ) | |
30 | FilterMixin) | |
29 | 31 | |
30 | 32 | from faraday.server.schemas import ( |
31 | 33 | PrimaryKeyRelatedField, |
70 | 72 | policyviolations = fields.List(fields.String, |
71 | 73 | attribute='policy_violations') |
72 | 74 | creator = PrimaryKeyRelatedField('username', dump_only=True, attribute='creator') |
75 | creator_id = fields.Integer(dump_only=True, attribute='creator_id') | |
76 | ||
73 | 77 | create_at = fields.DateTime(attribute='create_date', |
74 | 78 | dump_only=True) |
75 | 79 | |
84 | 88 | fields = ('id', '_id', '_rev', 'cwe', 'description', 'desc', |
85 | 89 | 'exploitation', 'name', 'references', 'refs', 'resolution', |
86 | 90 | 'impact', 'easeofresolution', 'policyviolations', 'data', |
87 | 'external_id', 'creator', 'create_at', | |
91 | 'external_id', 'creator', 'create_at', 'creator_id', | |
88 | 92 | 'customfields') |
89 | 93 | |
90 | 94 | def get_references(self, obj): |
129 | 133 | |
130 | 134 | class VulnerabilityTemplateView(PaginatedMixin, |
131 | 135 | FilterAlchemyMixin, |
132 | ReadWriteView): | |
136 | ReadWriteView, | |
137 | FilterMixin): | |
133 | 138 | route_base = 'vulnerability_template' |
134 | 139 | model_class = VulnerabilityTemplate |
135 | 140 | schema_class = VulnerabilityTemplateSchema |
162 | 167 | |
163 | 168 | @route('/bulk_create/', methods=['POST']) |
164 | 169 | def bulk_create(self): |
170 | """ | |
171 | --- | |
172 | post: | |
173 | tags: ["Bulk", "VulnerabilityTemplate"] | |
174 | description: Creates Vulnerability templates in bulk | |
175 | responses: | |
176 | 201: | |
177 | description: Created | |
178 | content: | |
179 | application/json: | |
180 | schema: VulnerabilityTemplateSchema | |
181 | 400: | |
182 | description: Bad request | |
183 | 403: | |
184 | description: Forbidden | |
185 | tags: ["Bulk", "VulnerabilityTemplate"] | |
186 | responses: | |
187 | 200: | |
188 | description: Ok | |
189 | """ | |
190 | csrf_token = request.form.get('csrf_token', '') | |
191 | if not csrf_token: | |
192 | csrf_token = request.json.get('csrf_token', '') | |
165 | 193 | try: |
166 | validate_csrf(request.form.get('csrf_token')) | |
194 | validate_csrf(csrf_token) | |
167 | 195 | except wtforms.ValidationError: |
168 | abort(403) | |
169 | logger.info("Create vulns template from CSV") | |
170 | if 'file' not in request.files: | |
171 | abort(400, "Missing File in request") | |
172 | vulns_file = request.files['file'] | |
173 | required_headers = {'name', 'exploitation'} | |
174 | io_wrapper = TextIOWrapper(vulns_file, encoding=request.content_encoding or "utf8") | |
175 | ||
176 | vulns_reader = csv.DictReader(io_wrapper, skipinitialspace=True) | |
177 | intersect_required = set(vulns_reader.fieldnames).intersection(required_headers) | |
178 | if intersect_required != required_headers: | |
179 | logger.error("Missing Required headers in CSV (%s)", required_headers - intersect_required) | |
180 | abort(400, "Missing Required headers in CSV (%s)" % (required_headers - intersect_required)) | |
181 | custom_fields = [custom_field_schema.field_name for custom_field_schema in db.session.query(CustomFieldsSchema).all()] | |
182 | vulns_created_count = 0 | |
183 | vulns_with_errors_count = 0 | |
196 | logger.error("Invalid CSRF token.") | |
197 | abort(make_response({"message": "Invalid CSRF token."}, 403)) | |
198 | ||
199 | if 'file' in request.files: | |
200 | logger.info("Create vulns template from CSV") | |
201 | vulns_file = request.files['file'] | |
202 | ||
203 | io_wrapper = TextIOWrapper(vulns_file, encoding=request.content_encoding or "utf8") | |
204 | vulns_reader = csv.DictReader(io_wrapper, skipinitialspace=True) | |
205 | ||
206 | required_headers = {'name', 'exploitation'} | |
207 | diff_required = required_headers.difference(set(vulns_reader.fieldnames)) | |
208 | if diff_required: | |
209 | logger.error(f"Missing required headers in CSV: {diff_required}") | |
210 | abort( | |
211 | make_response( | |
212 | {"message": f"Missing required headers in CSV: {diff_required}"}, 400 | |
213 | ) | |
214 | ) | |
215 | ||
216 | vulns_to_create = self._parse_vuln_from_file(vulns_reader) | |
217 | elif request.json.get('vulns'): | |
218 | logger.info("Create vulns template from vulnerabilities in Status Report") | |
219 | ||
220 | vulns_to_create = request.json.get('vulns') | |
221 | for vuln in vulns_to_create: | |
222 | # Due to the definition in the model, we need to | |
223 | # rename 'custom_fields' attribute to 'customfields' | |
224 | vuln['customfields'] = vuln.get('custom_fields', {}) | |
225 | else: | |
226 | logger.error("Missing data to create vulnerabilities templates.") | |
227 | abort(make_response({"message": "Missing data to create vulnerabilities templates."}, 400)) | |
228 | ||
229 | if not vulns_to_create: | |
230 | logger.error("Missing data to create vulnerabilities templates.") | |
231 | abort(make_response({"message": "Missing data to create vulnerabilities templates."}, 400)) | |
232 | ||
233 | vulns_created = [] | |
234 | vulns_with_errors = [] | |
235 | vulns_with_conflict = [] | |
184 | 236 | schema = self.schema_class() |
237 | for vuln in vulns_to_create: | |
238 | try: | |
239 | vuln_schema = schema.load(vuln) | |
240 | super(VulnerabilityTemplateView, self)._perform_create(vuln_schema) | |
241 | db.session.commit() | |
242 | except ValidationError as e: | |
243 | vulns_with_errors.append((vuln.get('_id', ''), vuln['name'])) | |
244 | except Conflict: | |
245 | vulns_with_conflict.append((vuln.get('_id', ''), vuln['name'])) | |
246 | else: | |
247 | vulns_created.append((vuln.get('_id', ''), vuln['name'])) | |
248 | ||
249 | if vulns_created: | |
250 | status_code = 200 | |
251 | elif not vulns_created and vulns_with_conflict: | |
252 | status_code = 409 | |
253 | elif not vulns_created and vulns_with_errors: | |
254 | status_code = 400 | |
255 | ||
256 | return make_response( | |
257 | jsonify(vulns_created=vulns_created, | |
258 | vulns_with_errors=vulns_with_errors, | |
259 | vulns_with_conflict=vulns_with_conflict), | |
260 | status_code | |
261 | ) | |
262 | ||
263 | ||
264 | def _parse_vuln_from_file(self, vulns_reader): | |
265 | custom_fields = {cf_schema.field_name: cf_schema for cf_schema in db.session.query(CustomFieldsSchema).all()} | |
266 | vulns_list = [] | |
185 | 267 | for index, vuln_dict in enumerate(vulns_reader): |
186 | 268 | vuln_dict['customfields'] = {} |
187 | 269 | vuln_dict['impact'] = {} |
188 | 270 | for key in vuln_dict.keys(): |
189 | if key in custom_fields: | |
190 | vuln_dict['customfields'][key] = vuln_dict[key] | |
271 | if key in custom_fields.keys(): | |
272 | if custom_fields[key].field_type == 'list' and vuln_dict[key]: | |
273 | custom_field_value = vuln_dict[key].replace('‘', '"').replace('’', '"') | |
274 | try: | |
275 | vuln_dict['customfields'][key] = json.loads(custom_field_value) | |
276 | except ValueError: | |
277 | logger.warning(f'Invalid list for custom field {key}. ' | |
278 | f'Faraday will skip this custom field.') | |
279 | elif custom_fields[key].field_type == 'choice' and vuln_dict[key]: | |
280 | cf_choices = custom_fields[key].field_metadata | |
281 | if isinstance(cf_choices, str): | |
282 | cf_choices = json.loads(cf_choices) | |
283 | if vuln_dict[key] not in cf_choices: | |
284 | logger.warning(f'Invalid choice for custom field {key}. ' | |
285 | f'Faraday will skip this custom field.') | |
286 | else: | |
287 | vuln_dict['customfields'][key] = vuln_dict[key] | |
288 | else: | |
289 | vuln_dict['customfields'][key] = vuln_dict[key] | |
191 | 290 | |
192 | 291 | vuln_dict['impact']['accountability'] = vuln_dict.get('accountability', False) |
193 | 292 | vuln_dict['impact']['availability'] = vuln_dict.get('availability', False) |
194 | 293 | vuln_dict['impact']['confidentiality'] = vuln_dict.get('confidentiality', False) |
195 | 294 | vuln_dict['impact']['integrity'] = vuln_dict.get('integrity', False) |
196 | try: | |
197 | vuln_schema = schema.load(vuln_dict) | |
198 | super(VulnerabilityTemplateView, self)._perform_create(vuln_schema) | |
199 | db.session.commit() | |
200 | except ValidationError as e: | |
201 | logger.error("Error creating vuln from line (%s): (%s)", str(index), e) | |
202 | vulns_with_errors_count += 1 | |
203 | else: | |
204 | vulns_created_count += 1 | |
205 | return make_response(jsonify(vulns_created=vulns_created_count, vulns_with_errors=vulns_with_errors_count), 200) | |
295 | vulns_list.append(vuln_dict) | |
296 | ||
297 | return vulns_list | |
298 | ||
206 | 299 | |
207 | 300 | VulnerabilityTemplateView.register(vulnerability_template_api) |
1 | 1 | # Copyright (C) 2016 Infobyte LLC (http://www.infobytesec.com/) |
2 | 2 | # See the file 'doc/LICENSE' for the license information |
3 | 3 | |
4 | import os | |
5 | 4 | import io |
6 | 5 | import json |
7 | 6 | import logging |
8 | 7 | from base64 import b64encode, b64decode |
9 | 8 | from json.decoder import JSONDecodeError |
9 | from pathlib import Path | |
10 | 10 | |
11 | 11 | import flask |
12 | 12 | import wtforms |
23 | 23 | from werkzeug.datastructures import ImmutableMultiDict |
24 | 24 | from depot.manager import DepotManager |
25 | 25 | |
26 | from faraday.server.utils.search import search, QueryBuilder, Filter as RestLessFilter | |
26 | from faraday.server.utils.search import ( | |
27 | search, | |
28 | ) | |
29 | ||
27 | 30 | from faraday.server.api.base import ( |
28 | 31 | AutoSchema, |
29 | 32 | FilterAlchemyMixin, |
252 | 255 | parent_class.id == parent_id |
253 | 256 | ).one() |
254 | 257 | except NoResultFound: |
255 | raise ValidationError('Parent id not found: {}'.format(parent_id)) | |
258 | raise ValidationError(f'Parent id not found: {parent_id}') | |
256 | 259 | data[parent_field] = parent.id |
257 | 260 | # TODO migration: check what happens when updating the parent from |
258 | 261 | # service to host or viceverse |
520 | 523 | File, |
521 | 524 | object_id=obj.id, |
522 | 525 | object_type='vulnerability', |
523 | name=os.path.splitext(os.path.basename(filename))[0], | |
524 | filename=os.path.basename(filename), | |
526 | name=Path(filename).stem, | |
527 | filename=Path(filename).name, | |
525 | 528 | content=faraday_file, |
526 | 529 | ) |
527 | 530 | |
618 | 621 | } |
619 | 622 | |
620 | 623 | def count(self, **kwargs): |
621 | """Override to change severity values""" | |
624 | """ | |
625 | --- | |
626 | get: | |
627 | tags: ["Vulnerability"] | |
628 | summary: "Group vulnerabilities by the field set in the group_by GET parameter." | |
629 | responses: | |
630 | 200: | |
631 | description: Ok | |
632 | content: | |
633 | application/json: | |
634 | schema: VulnerabilityWeb | |
635 | 404: | |
636 | description: group_by is not specified | |
637 | tags: ["Vulnerability"] | |
638 | responses: | |
639 | 200: | |
640 | description: Ok | |
641 | """ | |
622 | 642 | res = super(VulnerabilityView, self).count(**kwargs) |
623 | 643 | |
624 | 644 | def convert_group(group): |
638 | 658 | |
639 | 659 | @route('/<int:vuln_id>/attachment/', methods=['POST']) |
640 | 660 | def post_attachment(self, workspace_name, vuln_id): |
661 | """ | |
662 | --- | |
663 | post: | |
664 | tags: ["Vulnerability", "File"] | |
665 | description: Creates a new attachment in the vuln | |
666 | responses: | |
667 | 201: | |
668 | description: Created | |
669 | tags: ["Vulnerability", "File"] | |
670 | responses: | |
671 | 200: | |
672 | description: Ok | |
673 | """ | |
641 | 674 | |
642 | 675 | try: |
643 | 676 | validate_csrf(request.form.get('csrf_token')) |
672 | 705 | def filter(self, workspace_name): |
673 | 706 | """ |
674 | 707 | --- |
675 | tags: ["vulnerability", "filter"] | |
676 | summary: Filters, sorts and groups vulnerabilities using a json with parameters. | |
677 | parameters: | |
678 | - q: recursive json with filters that supports operators. The json could also contain sort and group | |
679 | ||
680 | responses: | |
681 | 200: | |
682 | description: return filtered, sorted and grouped vulnerabilities | |
683 | content: | |
684 | application/json: | |
685 | schema: FlaskRestlessSchema | |
686 | 400: | |
687 | description: invalid q was sent to the server | |
688 | ||
708 | get: | |
709 | tags: ["Filter", "Vulnerability"] | |
710 | description: Filters, sorts and groups vulnerabilities using a json with parameters. These parameters must be part of the model. | |
711 | parameters: | |
712 | - in: query | |
713 | name: q | |
714 | description: Recursive json with filters that supports operators. The json could also contain sort and group. | |
715 | responses: | |
716 | 200: | |
717 | description: Returns filtered, sorted and grouped results | |
718 | content: | |
719 | application/json: | |
720 | schema: FlaskRestlessSchema | |
721 | 400: | |
722 | description: Invalid q was sent to the server | |
723 | tags: ["Filter", "Vulnerability"] | |
724 | responses: | |
725 | 200: | |
726 | description: Ok | |
689 | 727 | """ |
690 | 728 | filters = request.args.get('q') |
691 | return self._envelope_list(self._filter(filters, workspace_name)) | |
729 | filtered_vulns, count = self._filter(filters, workspace_name) | |
730 | ||
731 | class PageMeta: | |
732 | total = 0 | |
733 | ||
734 | pagination_metadata = PageMeta() | |
735 | pagination_metadata.total = count | |
736 | return self._envelope_list(filtered_vulns, pagination_metadata) | |
692 | 737 | |
693 | 738 | def _hostname_filters(self, filters): |
694 | 739 | res_filters = [] |
724 | 769 | |
725 | 770 | return res_filters, hostname_filters |
726 | 771 | |
727 | def _filter_vulns(self, vulnerability_class, filters, hostname_filters, workspace, marshmallow_params, is_web): | |
772 | def _generate_filter_query(self, vulnerability_class, filters, hostname_filters, workspace, marshmallow_params): | |
728 | 773 | hosts_os_filter = [host_os_filter for host_os_filter in filters.get('filters', []) if host_os_filter.get('name') == 'host__os'] |
729 | 774 | |
730 | 775 | if hosts_os_filter: |
748 | 793 | |
749 | 794 | if hosts_os_filter: |
750 | 795 | os_value = hosts_os_filter['val'] |
751 | if is_web: | |
752 | exists_part = vulnerability_class.query.join(Service).join(Host).filter(Host.os == os_value).exists() | |
753 | else: | |
754 | filt = RestLessFilter('host__os', 'has', os_value) | |
755 | exists_part = QueryBuilder._create_filter(vulnerability_class, filt) | |
756 | vulns = vulns.filter(exists_part) | |
757 | ||
758 | if is_web: | |
759 | _type = 'VulnerabilityWeb' | |
760 | ||
796 | vulns = vulns.join(Host).join(Service).filter(Host.os==os_value) | |
797 | ||
798 | if 'group_by' not in filters: | |
799 | vulns = vulns.options( | |
800 | joinedload(VulnerabilityGeneric.tags), | |
801 | joinedload(Vulnerability.host), | |
802 | joinedload(Vulnerability.service), | |
803 | joinedload(VulnerabilityWeb.service), | |
804 | ) | |
805 | return vulns | |
806 | ||
807 | def _filter(self, filters, workspace_name): | |
808 | try: | |
809 | filters = FlaskRestlessSchema().load(json.loads(filters)) or {} | |
810 | hostname_filters = [] | |
811 | if filters: | |
812 | _, hostname_filters = self._hostname_filters(filters.get('filters', [])) | |
813 | except (ValidationError, JSONDecodeError) as ex: | |
814 | logger.exception(ex) | |
815 | flask.abort(400, "Invalid filters") | |
816 | ||
817 | workspace = self._get_workspace(workspace_name) | |
818 | marshmallow_params = {'many': True, 'context': {}} | |
819 | if 'group_by' not in filters: | |
820 | offset = None | |
821 | limit = None | |
822 | if 'offset' in filters: | |
823 | offset = filters.pop('offset') | |
824 | if 'limit' in filters: | |
825 | limit = filters.pop('limit') # we need to remove pagination, since | |
826 | ||
827 | vulns = self._generate_filter_query( | |
828 | VulnerabilityGeneric, | |
829 | filters, | |
830 | hostname_filters, | |
831 | workspace, | |
832 | marshmallow_params) | |
833 | total_vulns = vulns | |
834 | if limit: | |
835 | vulns = vulns.limit(limit) | |
836 | if offset: | |
837 | vulns = vulns.offset(offset) | |
838 | ||
839 | vulns = self.schema_class_dict['VulnerabilityWeb'](**marshmallow_params).dumps( | |
840 | vulns.all()) | |
841 | return json.loads(vulns), total_vulns.count() | |
761 | 842 | else: |
762 | _type = 'Vulnerability' | |
763 | ||
764 | if 'group_by' not in filters: | |
765 | vulns = self.schema_class_dict[_type](**marshmallow_params).dumps( | |
766 | vulns.all()) | |
767 | vulns_data = json.loads(vulns) | |
768 | else: | |
843 | vulns = self._generate_filter_query( | |
844 | VulnerabilityGeneric, | |
845 | filters, | |
846 | hostname_filters, | |
847 | workspace, | |
848 | marshmallow_params, | |
849 | ) | |
769 | 850 | column_names = ['count'] + [field['field'] for field in filters.get('group_by',[])] |
770 | 851 | rows = [list(zip(column_names, row)) for row in vulns.all()] |
771 | 852 | vulns_data = [] |
772 | 853 | for row in rows: |
773 | 854 | vulns_data.append({field[0]:field[1] for field in row}) |
774 | 855 | |
775 | return vulns_data | |
776 | ||
777 | def _filter(self, filters, workspace_name, confirmed=False): | |
778 | try: | |
779 | filters = FlaskRestlessSchema().load(json.loads(filters)) | |
780 | _, hostname_filters = self._hostname_filters(filters.get('filters', [])) | |
781 | except (ValidationError, JSONDecodeError) as ex: | |
782 | logger.exception(ex) | |
783 | flask.abort(400, "Invalid filters") | |
784 | if confirmed: | |
785 | if 'filters' not in filters: | |
786 | filters = {} | |
787 | filters['filters'] = [] | |
788 | filters['filters'].append({ | |
789 | "name": "confirmed", | |
790 | "op": "==", | |
791 | "val": "true" | |
792 | }) | |
793 | ||
794 | workspace = self._get_workspace(workspace_name) | |
795 | marshmallow_params = {'many': True, 'context': {}} | |
796 | normal_vulns_data = self._filter_vulns(Vulnerability, filters, hostname_filters, workspace, marshmallow_params, False) | |
797 | web_vulns_data = self._filter_vulns(VulnerabilityWeb, filters, hostname_filters, workspace, marshmallow_params, True) | |
798 | return normal_vulns_data + web_vulns_data | |
856 | return vulns_data, len(rows) | |
799 | 857 | |
800 | 858 | @route('/<int:vuln_id>/attachment/<attachment_filename>/', methods=['GET']) |
801 | 859 | def get_attachment(self, workspace_name, vuln_id, attachment_filename): |
860 | """ | |
861 | --- | |
862 | get: | |
863 | tags: ["Vulnerability", "File"] | |
864 | description: Get a vuln attachment | |
865 | responses: | |
866 | 200: | |
867 | description: Ok | |
868 | tags: ["Vulnerability", "File"] | |
869 | responses: | |
870 | 200: | |
871 | description: Ok | |
872 | """ | |
802 | 873 | vuln_workspace_check = db.session.query(VulnerabilityGeneric, Workspace.id).join( |
803 | 874 | Workspace).filter(VulnerabilityGeneric.id == vuln_id, |
804 | 875 | Workspace.name == workspace_name).first() |
832 | 903 | """ |
833 | 904 | --- |
834 | 905 | get: |
835 | tags: ["Vulns"] | |
906 | tags: ["Vulnerability", "File"] | |
836 | 907 | description: Gets an attachment for a vulnerability |
837 | 908 | responses: |
838 | 909 | 200: |
844 | 915 | description: Workspace disabled or no permission |
845 | 916 | 404: |
846 | 917 | description: Not Found |
918 | tags: ["Vulnerability", "File"] | |
919 | responses: | |
920 | 200: | |
921 | description: Ok | |
847 | 922 | """ |
848 | 923 | workspace = self._get_workspace(workspace_name) |
849 | 924 | vuln_workspace_check = db.session.query(VulnerabilityGeneric, Workspace.id).join( |
864 | 939 | |
865 | 940 | @route('/<int:vuln_id>/attachment/<attachment_filename>/', methods=['DELETE']) |
866 | 941 | def delete_attachment(self, workspace_name, vuln_id, attachment_filename): |
942 | """ | |
943 | --- | |
944 | delete: | |
945 | tags: ["Vulnerability", "File"] | |
946 | description: Remove a vuln attachment | |
947 | responses: | |
948 | 200: | |
949 | description: Ok | |
950 | """ | |
867 | 951 | vuln_workspace_check = db.session.query(VulnerabilityGeneric, Workspace.id).join( |
868 | 952 | Workspace).filter( |
869 | 953 | VulnerabilityGeneric.id == vuln_id, Workspace.name == workspace_name).first() |
885 | 969 | |
886 | 970 | @route('export_csv/', methods=['GET']) |
887 | 971 | def export_csv(self, workspace_name): |
972 | """ | |
973 | --- | |
974 | get: | |
975 | tags: ["Vulnerability", "File"] | |
976 | description: Get a CSV file with all vulns from a workspace | |
977 | responses: | |
978 | 200: | |
979 | description: Ok | |
980 | tags: ["Vulnerability", "File"] | |
981 | responses: | |
982 | 200: | |
983 | description: Ok | |
984 | """ | |
888 | 985 | confirmed = bool(request.args.get('confirmed')) |
889 | 986 | filters = request.args.get('q', '{}') |
890 | 987 | custom_fields_columns = [] |
891 | 988 | for custom_field in db.session.query(CustomFieldsSchema).order_by(CustomFieldsSchema.field_order): |
892 | 989 | custom_fields_columns.append(custom_field.field_name) |
893 | vulns_query = self._filter(filters, workspace_name, confirmed) | |
990 | if confirmed: | |
991 | if 'filters' not in filters: | |
992 | filters = {} | |
993 | filters['filters'] = [] | |
994 | filters['filters'].append({ | |
995 | "name": "confirmed", | |
996 | "op": "==", | |
997 | "val": "true" | |
998 | }) | |
999 | filters = json.dumps(filters) | |
1000 | vulns_query, _ = self._filter(filters, workspace_name) | |
894 | 1001 | memory_file = export_vulns_to_csv(vulns_query, custom_fields_columns) |
895 | 1002 | return send_file(memory_file, |
896 | attachment_filename="Faraday-SR-%s.csv" % workspace_name, | |
1003 | attachment_filename=f"Faraday-SR-{workspace_name}.csv", | |
897 | 1004 | as_attachment=True, |
898 | 1005 | cache_timeout=-1) |
899 | 1006 | |
900 | 1007 | |
901 | 1008 | @route('bulk_delete/', methods=['DELETE']) |
902 | 1009 | def bulk_delete(self, workspace_name): |
1010 | """ | |
1011 | --- | |
1012 | delete: | |
1013 | tags: ["Bulk", "Vulnerability"] | |
1014 | description: Delete vulnerabilities in bulk | |
1015 | responses: | |
1016 | 200: | |
1017 | description: Ok | |
1018 | 400: | |
1019 | description: Bad request | |
1020 | 403: | |
1021 | description: Forbidden | |
1022 | tags: ["Bulk", "Vulnerability"] | |
1023 | responses: | |
1024 | 200: | |
1025 | description: Ok | |
1026 | """ | |
903 | 1027 | workspace = self._get_workspace(workspace_name) |
904 | 1028 | json_quest = request.get_json() |
905 | 1029 | vulnerability_ids = json_quest.get('vulnerability_ids', []) |
928 | 1052 | """ |
929 | 1053 | --- |
930 | 1054 | get: |
931 | tags: ["Vulns"] | |
1055 | tags: ["Vulnerability"] | |
932 | 1056 | params: limit |
933 | 1057 | description: Gets a list of top users having account its uploaded vulns |
934 | 1058 | responses: |
935 | 1059 | 200: |
936 | 1060 | description: List of top users |
1061 | tags: ["Vulnerability"] | |
1062 | responses: | |
1063 | 200: | |
1064 | description: Ok | |
937 | 1065 | """ |
938 | 1066 | limit = flask.request.args.get('limit', 1) |
939 | 1067 | workspace = self._get_workspace(workspace_name) |
15 | 15 | from sqlalchemy.orm.exc import NoResultFound |
16 | 16 | |
17 | 17 | |
18 | from faraday.server.models import db, Workspace, _make_vuln_count_property, Vulnerability | |
18 | from faraday.server.models import db, Workspace, _make_vuln_count_property, Vulnerability, \ | |
19 | _make_active_agents_count_property, count_vulnerability_severities | |
19 | 20 | from faraday.server.schemas import ( |
20 | 21 | JSTimestampField, |
21 | 22 | MutableField, |
22 | 23 | PrimaryKeyRelatedField, |
23 | 24 | SelfNestedField, |
24 | 25 | ) |
25 | from faraday.server.api.base import ReadWriteView, AutoSchema | |
26 | from faraday.server.api.base import ReadWriteView, AutoSchema, FilterMixin | |
26 | 27 | |
27 | 28 | logger = logging.getLogger(__name__) |
28 | 29 | |
41 | 42 | attribute='vulnerability_code_count') |
42 | 43 | std_vulns = fields.Integer(dump_only=True, allow_none=False, |
43 | 44 | attribute='vulnerability_standard_count') |
45 | critical_vulns = fields.Integer(dump_only=True, allow_none=False, | |
46 | attribute='vulnerability_critical_count') | |
47 | info_vulns = fields.Integer(dump_only=True, allow_none=False, | |
48 | attribute='vulnerability_informational_count') | |
49 | high_vulns = fields.Integer(dump_only=True, allow_none=False, | |
50 | attribute='vulnerability_high_count') | |
51 | medium_vulns = fields.Integer(dump_only=True, allow_none=False, | |
52 | attribute='vulnerability_medium_count') | |
53 | low_vulns = fields.Integer(dump_only=True, allow_none=False, | |
54 | attribute='vulnerability_low_count') | |
55 | unclassified_vulns = fields.Integer(dump_only=True, allow_none=False, | |
56 | attribute='vulnerability_unclassified_count') | |
44 | 57 | total_vulns = fields.Integer(dump_only=True, allow_none=False, |
45 | 58 | attribute='vulnerability_total_count') |
46 | 59 | |
53 | 66 | class WorkspaceSchema(AutoSchema): |
54 | 67 | |
55 | 68 | name = fields.String(required=True, |
56 | validate=validate.Regexp(r"^[a-z0-9][a-z0-9\_\$\(\)\+\-\/]*$", 0, error="ERORROROR")) | |
69 | validate=validate.Regexp(r"^[a-z0-9][a-z0-9\_\$\(\)\+\-\/]*$", 0, | |
70 | error="The workspace name must validate with the regex " | |
71 | "^[a-z0-9][a-z0-9\\_\\$\\(\\)\\+\\-\\/]*$")) | |
57 | 72 | stats = SelfNestedField(WorkspaceSummarySchema()) |
58 | 73 | duration = SelfNestedField(WorkspaceDurationSchema()) |
59 | 74 | _id = fields.Integer(dump_only=True, attribute='id') |
69 | 84 | update_date = fields.DateTime(attribute='update_date', |
70 | 85 | dump_only=True) |
71 | 86 | |
87 | active_agents_count = fields.Integer(dump_only=True) | |
88 | ||
72 | 89 | |
73 | 90 | class Meta: |
74 | 91 | model = Workspace |
75 | 92 | fields = ('_id', 'id', 'customer', 'description', 'active', |
76 | 93 | 'duration', 'name', 'public', 'scope', 'stats', |
77 | 'create_date', 'update_date', 'readonly') | |
94 | 'create_date', 'update_date', 'readonly', | |
95 | 'active_agents_count') | |
78 | 96 | |
79 | 97 | @post_load |
80 | 98 | def post_load_duration(self, data, **kwargs): |
88 | 106 | return data |
89 | 107 | |
90 | 108 | |
91 | class WorkspaceView(ReadWriteView): | |
109 | class WorkspaceView(ReadWriteView, FilterMixin): | |
92 | 110 | route_base = 'ws' |
93 | 111 | lookup_field = 'name' |
94 | 112 | lookup_field_type = str |
97 | 115 | order_field = Workspace.name.asc() |
98 | 116 | |
99 | 117 | def index(self, **kwargs): |
118 | """ | |
119 | --- | |
120 | get: | |
121 | summary: "Get a list of workspaces." | |
122 | tags: ["Workspace"] | |
123 | responses: | |
124 | 200: | |
125 | description: Ok | |
126 | content: | |
127 | application/json: | |
128 | schema: WorkspaceSchema | |
129 | tags: ["Workspace"] | |
130 | responses: | |
131 | 200: | |
132 | description: Ok | |
133 | """ | |
100 | 134 | query = self._get_base_query() |
101 | 135 | objects = [] |
102 | 136 | for workspace_stat in query: |
112 | 146 | workspace_stat_dict['scope'].append({'name': scope}) |
113 | 147 | objects.append(workspace_stat_dict) |
114 | 148 | return self._envelope_list(self._dump(objects, kwargs, many=True)) |
149 | ||
150 | @route('/filter') | |
151 | def filter(self): | |
152 | """ | |
153 | --- | |
154 | tags: ["Filter"] | |
155 | summary: Filters, sorts and groups objects using a json with parameters. | |
156 | parameters: | |
157 | - in: query | |
158 | name: q | |
159 | description: recursive json with filters that supports operators. The json could also contain sort and group | |
160 | ||
161 | responses: | |
162 | 200: | |
163 | description: return filtered, sorted and grouped results | |
164 | content: | |
165 | application/json: | |
166 | schema: FlaskRestlessSchema | |
167 | 400: | |
168 | description: invalid q was sent to the server | |
169 | ||
170 | """ | |
171 | filters = flask.request.args.get('q', '{"filters": []}') | |
172 | filtered_objs, count = self._filter(filters, severity_count=True, host_vulns=False) | |
173 | ||
174 | class PageMeta: | |
175 | total = 0 | |
176 | ||
177 | pagination_metadata = PageMeta() | |
178 | pagination_metadata.total = count | |
179 | return self._envelope_list(filtered_objs, pagination_metadata) | |
115 | 180 | |
116 | 181 | def _get_querystring_boolean_field(self, field_name, default=None): |
117 | 182 | try: |
174 | 239 | Workspace.vulnerability_code_count, |
175 | 240 | _make_vuln_count_property('vulnerability_code', |
176 | 241 | extra_query=extra_query, |
177 | use_column_property=False) | |
178 | ), | |
179 | ||
180 | ||
181 | ) | |
242 | use_column_property=False), | |
243 | ), | |
244 | with_expression( | |
245 | Workspace.active_agents_count, | |
246 | _make_active_agents_count_property(), | |
247 | ), | |
248 | ) | |
249 | query = count_vulnerability_severities(query, Workspace, status=status, confirmed=confirmed, all_severities=True) | |
182 | 250 | |
183 | 251 | try: |
184 | 252 | obj = query.one() |
185 | 253 | except NoResultFound: |
186 | flask.abort(404, 'Object with name "%s" not found' % object_id) | |
254 | flask.abort(404, f'Object with name "{object_id}" not found') | |
187 | 255 | return obj |
188 | 256 | |
189 | 257 | def _perform_create(self, data, **kwargs): |
214 | 282 | |
215 | 283 | @route('/<workspace_id>/activate/', methods=["PUT"]) |
216 | 284 | def activate(self, workspace_id): |
285 | """ | |
286 | --- | |
287 | put: | |
288 | tags: ["Workspace"] | |
289 | description: Activate a workspace | |
290 | responses: | |
291 | 200: | |
292 | description: Ok | |
293 | tags: ["Workspace"] | |
294 | responses: | |
295 | 200: | |
296 | description: Ok | |
297 | """ | |
217 | 298 | changed = self._get_object(workspace_id).activate() |
218 | 299 | db.session.commit() |
219 | 300 | return changed |
220 | 301 | |
221 | 302 | @route('/<workspace_id>/deactivate/', methods=["PUT"]) |
222 | 303 | def deactivate(self, workspace_id): |
304 | """ | |
305 | --- | |
306 | put: | |
307 | tags: ["Workspace"] | |
308 | description: Deactivate a workspace | |
309 | responses: | |
310 | 200: | |
311 | description: Ok | |
312 | tags: ["Workspace"] | |
313 | responses: | |
314 | 200: | |
315 | description: Ok | |
316 | """ | |
223 | 317 | changed = self._get_object(workspace_id).deactivate() |
224 | 318 | db.session.commit() |
225 | 319 | return changed |
226 | 320 | |
227 | 321 | @route('/<workspace_id>/change_readonly/', methods=["PUT"]) |
228 | 322 | def change_readonly(self, workspace_id): |
323 | """ | |
324 | --- | |
325 | put: | |
326 | tags: ["Workspace"] | |
327 | description: Change readonly workspace's status | |
328 | responses: | |
329 | 200: | |
330 | description: Ok | |
331 | tags: ["Workspace"] | |
332 | responses: | |
333 | 200: | |
334 | description: Ok | |
335 | """ | |
229 | 336 | self._get_object(workspace_id).change_readonly() |
230 | 337 | db.session.commit() |
231 | 338 | return self._get_object(workspace_id).readonly |
1 | 1 | # Copyright (C) 2016 Infobyte LLC (http://www.infobytesec.com/) |
2 | 2 | # See the file 'doc/LICENSE' for the license information |
3 | 3 | import logging |
4 | import os | |
5 | 4 | import string |
6 | 5 | import datetime |
7 | 6 | |
8 | 7 | import requests |
9 | 8 | from itsdangerous import TimedJSONWebSignatureSerializer, SignatureExpired, BadSignature |
10 | from os.path import join | |
11 | 9 | from random import SystemRandom |
12 | 10 | |
13 | 11 | from faraday.server.config import LOCAL_CONFIG_FILE, copy_default_config_to_local |
47 | 45 | |
48 | 46 | |
49 | 47 | def setup_storage_path(): |
50 | default_path = join(CONST_FARADAY_HOME_PATH, 'storage') | |
51 | if not os.path.exists(default_path): | |
52 | logger.info('Creating directory {0}'.format(default_path)) | |
53 | os.mkdir(default_path) | |
48 | default_path = CONST_FARADAY_HOME_PATH / 'storage' | |
49 | if not default_path.exists(): | |
50 | logger.info(f'Creating directory {default_path}') | |
51 | default_path.mkdir() | |
54 | 52 | config = ConfigParser() |
55 | 53 | config.read(faraday.server.config.LOCAL_CONFIG_FILE) |
56 | 54 | try: |
57 | 55 | config.add_section('storage') |
58 | config.set('storage', 'path', default_path) | |
56 | config.set('storage', 'path', str(default_path)) | |
59 | 57 | except DuplicateSectionError: |
60 | 58 | logger.info('Duplicate section storage. skipping.') |
61 | with open(faraday.server.config.LOCAL_CONFIG_FILE, 'w') as configfile: | |
59 | with faraday.server.config.LOCAL_CONFIG_FILE.open('w') as configfile: | |
62 | 60 | config.write(configfile) |
63 | 61 | |
64 | 62 | return default_path |
185 | 183 | if logged_in: |
186 | 184 | assert user |
187 | 185 | |
188 | if not logged_in and not getattr(view, 'is_public', False): | |
186 | if not logged_in and not getattr(view, 'is_public', False) \ | |
187 | and flask.request.method != 'OPTIONS': | |
189 | 188 | flask.abort(401) |
190 | 189 | |
191 | 190 | g.user = None |
192 | 191 | if logged_in: |
193 | 192 | g.user = user |
194 | 193 | if user is None: |
195 | logger.warn("Unknown user id {}".format(session["_user_id"])) | |
194 | logger.warn(f"Unknown user id {session['_user_id']}") | |
196 | 195 | del flask.session['_user_id'] |
197 | 196 | flask.abort(401) # 403 would be better but breaks the web ui |
198 | 197 | return |
220 | 219 | |
221 | 220 | |
222 | 221 | def save_new_secret_key(app): |
223 | if not os.path.exists(LOCAL_CONFIG_FILE): | |
222 | if not LOCAL_CONFIG_FILE.exists(): | |
224 | 223 | copy_default_config_to_local() |
225 | 224 | config = ConfigParser() |
226 | 225 | config.read(LOCAL_CONFIG_FILE) |
237 | 236 | |
238 | 237 | |
239 | 238 | def save_new_agent_creation_token(): |
240 | assert os.path.exists(LOCAL_CONFIG_FILE) | |
239 | assert LOCAL_CONFIG_FILE.exists() | |
241 | 240 | config = ConfigParser() |
242 | 241 | config.read(LOCAL_CONFIG_FILE) |
243 | 242 | rng = SystemRandom() |
270 | 269 | KVSessionExtension(app=app).cleanup_sessions(app) |
271 | 270 | |
272 | 271 | def create_app(db_connection_string=None, testing=None): |
273 | app = Flask(__name__) | |
272 | app = Flask(__name__, static_folder=None) | |
274 | 273 | |
275 | 274 | try: |
276 | 275 | secret_key = faraday.server.config.faraday_server.secret_key |
340 | 339 | if not DepotManager.get('default'): |
341 | 340 | if testing: |
342 | 341 | DepotManager.configure('default', { |
343 | 'depot.storage_path': '/tmp' | |
342 | 'depot.storage_path': '/tmp' # nosec | |
344 | 343 | }) |
345 | 344 | else: |
346 | 345 | DepotManager.configure('default', { |
432 | 431 | self.email.errors.append(get_message('DISABLED_ACCOUNT')[0]) |
433 | 432 | return False |
434 | 433 | return True |
435 |
5 | 5 | """ |
6 | 6 | from apispec import APISpec |
7 | 7 | from apispec.ext.marshmallow import MarshmallowPlugin |
8 | from apispec_webframeworks.flask import FlaskPlugin | |
8 | 9 | from faraday.server.web import app |
9 | 10 | from faraday import __version__ as f_version |
10 | 11 | import json |
12 | 13 | from faraday.utils.faraday_openapi_plugin import FaradayAPIPlugin |
13 | 14 | |
14 | 15 | |
15 | def openapi_format(format="yaml"): | |
16 | def openapi_format(format="yaml", server="localhost", no_servers=False): | |
17 | extra_specs = {'info': { | |
18 | 'description': 'The Faraday REST API enables you to interact with ' | |
19 | '[our server](https://github.com/infobyte/faraday).\n' | |
20 | 'Use this API to interact or integrate with Faraday' | |
21 | ' server. This page documents the REST API, with HTTP' | |
22 | ' response codes and example requests and responses.'}, | |
23 | 'security': {"ApiKeyAuth": []} | |
24 | } | |
25 | ||
26 | if not no_servers: | |
27 | extra_specs['servers'] = [{'url': f'https://{server}/_api'}] | |
16 | 28 | |
17 | 29 | spec = APISpec( |
18 | 30 | title="Faraday API", |
19 | 31 | version="2", |
20 | 32 | openapi_version="3.0.2", |
21 | plugins=[FaradayAPIPlugin(), MarshmallowPlugin()], | |
22 | info={'description': 'The Faraday server API'}, | |
33 | plugins=[FaradayAPIPlugin(), FlaskPlugin(), MarshmallowPlugin()], | |
34 | **extra_specs | |
23 | 35 | ) |
36 | api_key_scheme = { | |
37 | "type": "apiKey", | |
38 | "in": "header", | |
39 | "name": "Authorization" | |
40 | } | |
41 | ||
42 | spec.components.security_scheme("API_KEY", api_key_scheme) | |
43 | response_401_unauthorized = { | |
44 | "description": "You are not authenticated or your API key is missing " | |
45 | "or invalid" | |
46 | } | |
47 | spec.components.response("UnauthorizedError", response_401_unauthorized) | |
24 | 48 | |
25 | 49 | with app.test_request_context(): |
26 | 50 | for endpoint in app.view_functions: |
22 | 22 | print('This wizard will guide you to DELETE custom field to the vulneraiblity model.') |
23 | 23 | print('All available custom fields are:') |
24 | 24 | for custom_field in db.session.query(CustomFieldsSchema): |
25 | print('* {0}'.format(custom_field.field_name)) | |
25 | print(f'* {custom_field.field_name}') | |
26 | 26 | print('End of custom fields') |
27 | 27 | field_name = click.prompt('Field name') |
28 | 28 | custom_field = db.session.query(CustomFieldsSchema).filter_by(field_name=field_name).first() |
53 | 53 | print('Custom field current order') |
54 | 54 | for custom_field in custom_fields: |
55 | 55 | current_used_orders.add(custom_field.field_order) |
56 | print('Field {0}, order {1}'.format(custom_field.field_display_name, custom_field.field_order)) | |
56 | print(f'Field {custom_field.field_display_name}, order {custom_field.field_order}') | |
57 | 57 | field_order = click.prompt('Field order index') |
58 | 58 | invalid_field_order = False |
59 | 59 | try: |
88 | 88 | custom_field_data.field_display_name = field_display_name |
89 | 89 | custom_field_data.field_type = field_type |
90 | 90 | db.session.commit() |
91 | # I'm Py3⏎ | |
91 | # I'm Py3 |
0 | from __future__ import absolute_import | |
1 | ||
2 | import csv | |
3 | import tempfile | |
4 | ||
5 | import requests | |
6 | from colorama import init | |
7 | from colorama import Fore, Style | |
8 | import logging | |
9 | ||
10 | from sqlalchemy.exc import IntegrityError | |
11 | ||
12 | from faraday.server.web import app | |
13 | from faraday.server.models import ( | |
14 | db, | |
15 | VulnerabilityTemplate, | |
16 | ) | |
17 | ||
18 | CWE_URL = "https://raw.githubusercontent.com/infobyte/faraday_templates/master/vulnerability_templates" | |
19 | ||
20 | CWE_LANGS = ['en', 'es'] | |
21 | ||
22 | logger = logging.getLogger(__name__) | |
23 | ||
24 | init() | |
25 | ||
26 | ||
27 | def import_vulnerability_templates(language): | |
28 | imported_rows = 0 | |
29 | duplicated_rows = 0 | |
30 | with app.app_context(): | |
31 | try: | |
32 | res = requests.get(f'{CWE_URL}/cwe_{language}.csv') | |
33 | except Exception as e: | |
34 | print(f'[{Fore.RED}-{Style.RESET_ALL}] An error has occurred downloading the file.\n{e}') | |
35 | return None | |
36 | ||
37 | if res.status_code != 200: | |
38 | print(f'[{Fore.RED}-{Style.RESET_ALL}] An error has occurred downloading the file.' | |
39 | f' Response was {res.status_code}') | |
40 | return None | |
41 | ||
42 | cwe_file = tempfile.TemporaryFile(mode="w+t") | |
43 | cwe_file.write(res.content.decode('utf8')) | |
44 | cwe_file.seek(0) | |
45 | ||
46 | vulnerability_templates = csv.DictReader(cwe_file) | |
47 | for vulnerability_template in vulnerability_templates: | |
48 | vulnerability_template = dict(vulnerability_template) | |
49 | ||
50 | references = [ref.strip() for ref in vulnerability_template['references'].split(',')] | |
51 | try: | |
52 | v = VulnerabilityTemplate(name=vulnerability_template['name'], | |
53 | description=vulnerability_template['description'], | |
54 | severity=vulnerability_template['exploitation'], | |
55 | resolution=vulnerability_template['resolution'], | |
56 | references=references, | |
57 | shipped=True) | |
58 | db.session.add(v) | |
59 | db.session.flush() | |
60 | imported_rows += 1 | |
61 | except IntegrityError: | |
62 | duplicated_rows += 1 | |
63 | db.session.rollback() | |
64 | db.session.commit() | |
65 | ||
66 | if imported_rows > 0: | |
67 | print(f'[{Fore.GREEN}+{Style.RESET_ALL}] {imported_rows} new vulnerability templates were imported') | |
68 | else: | |
69 | print(f'[{Fore.YELLOW}+{Style.RESET_ALL}] {duplicated_rows} vulnerability templates were already imported') | |
70 | ||
71 | ||
72 | def available_languages(): | |
73 | print(f'[{Fore.GREEN}+{Style.RESET_ALL}] Available languages') | |
74 | for lang in CWE_LANGS: | |
75 | print(f'[{Fore.GREEN}*{Style.RESET_ALL}] {lang}') | |
76 | ||
77 | ||
78 | def run(language='en', list_languages=False): | |
79 | ||
80 | if list_languages: | |
81 | available_languages() | |
82 | return None | |
83 | ||
84 | if language not in CWE_LANGS: | |
85 | print(f'[{Fore.RED}-{Style.RESET_ALL}] Language not available') | |
86 | return None | |
87 | ||
88 | import_vulnerability_templates(language) |
47 | 47 | config.get('database', 'connection_string') |
48 | 48 | reconfigure = None |
49 | 49 | while not reconfigure: |
50 | reconfigure = input('Database section {yellow} already found{white}. Do you want to reconfigure database? (yes/no) '.format(yellow=Fore.YELLOW, white=Fore.WHITE)) | |
50 | reconfigure = input(f'Database section {Fore.YELLOW} already found{Fore.WHITE}. Do you want to reconfigure database? (yes/no) ') | |
51 | 51 | if reconfigure.lower() == 'no': |
52 | 52 | return False |
53 | 53 | elif reconfigure.lower() == 'yes': |
72 | 72 | config.read(LOCAL_CONFIG_FILE) |
73 | 73 | if not self._check_current_config(config): |
74 | 74 | return |
75 | faraday_path_conf = os.path.expanduser(CONST_FARADAY_HOME_PATH) | |
75 | faraday_path_conf = CONST_FARADAY_HOME_PATH | |
76 | 76 | # we use psql_log_filename for historical saving. we will ask faraday users this file. |
77 | 77 | # current_psql_output is for checking psql command already known errors for each execution. |
78 | psql_log_filename = os.path.join(faraday_path_conf, 'logs', 'psql_log.log') | |
78 | psql_log_filename = faraday_path_conf / 'logs' / 'psql_log.log' | |
79 | 79 | current_psql_output = TemporaryFile() |
80 | 80 | with open(psql_log_filename, 'ab+') as psql_log_file: |
81 | 81 | hostname = 'localhost' |
122 | 122 | |
123 | 123 | statement = text(""" |
124 | 124 | INSERT INTO faraday_user ( |
125 | username, name, password, | |
126 | is_ldap, active, last_login_ip, | |
125 | username, name, password, | |
126 | is_ldap, active, last_login_ip, | |
127 | 127 | current_login_ip, role, state_otp |
128 | 128 | ) VALUES ( |
129 | 129 | 'faraday', 'Administrator', :password, |
164 | 164 | current_psql_output_file.seek(0) |
165 | 165 | psql_output = current_psql_output_file.read().decode('utf-8') |
166 | 166 | if 'unknown user: postgres' in psql_output: |
167 | print('ERROR: Postgres user not found. Did you install package {blue}postgresql{white}?'.format(blue=Fore.BLUE, white=Fore.WHITE)) | |
167 | print(f'ERROR: Postgres user not found. Did you install package {Fore.BLUE}postgresql{Fore.WHITE}?') | |
168 | 168 | elif 'could not connect to server' in psql_output: |
169 | print('ERROR: {red}PostgreSQL service{white} is not running. Please verify that it is running in port 5432 before executing setup script.'.format(red=Fore.RED, white=Fore.WHITE)) | |
169 | print(f'ERROR: {Fore.RED}PostgreSQL service{Fore.WHITE} is not running. Please verify that it is running in port 5432 before executing setup script.') | |
170 | 170 | elif process_status > 0: |
171 | 171 | current_psql_output_file.seek(0) |
172 | 172 | print('ERROR: ' + psql_output) |
188 | 188 | username = os.environ.get("FARADAY_DATABASE_USER", 'faraday_postgresql') |
189 | 189 | postgres_command = ['sudo', '-u', 'postgres', 'psql'] |
190 | 190 | if sys.platform == 'darwin': |
191 | print('{blue}MAC OS detected{white}'.format(blue=Fore.BLUE, white=Fore.WHITE)) | |
191 | print(f'{Fore.BLUE}MAC OS detected{Fore.WHITE}') | |
192 | 192 | postgres_command = ['psql', 'postgres'] |
193 | 193 | password = self.generate_random_pw(25) |
194 | 194 | command = postgres_command + [ '-c', 'CREATE ROLE {0} WITH LOGIN PASSWORD \'{1}\';'.format(username, password)] |
198 | 198 | output = psql_log_file.read() |
199 | 199 | if isinstance(output, bytes): |
200 | 200 | output = output.decode('utf-8') |
201 | already_exists_error = 'role "{0}" already exists'.format(username) | |
201 | already_exists_error = f'role "{username}" already exists' | |
202 | 202 | return_code = p.returncode |
203 | 203 | if already_exists_error in output: |
204 | print("{yellow}WARNING{white}: Role {username} already exists, skipping creation ".format(yellow=Fore.YELLOW, white=Fore.WHITE, username=username)) | |
204 | print(f"{Fore.YELLOW}WARNING{Fore.WHITE}: Role {username} already exists, skipping creation ") | |
205 | 205 | |
206 | 206 | try: |
207 | 207 | if not getattr(faraday.server.config, 'database', None): |
242 | 242 | if sys.platform == 'darwin': |
243 | 243 | postgres_command = [] |
244 | 244 | |
245 | print('Creating database {0}'.format(database_name)) | |
245 | print(f'Creating database {database_name}') | |
246 | 246 | command = postgres_command + ['createdb', '-E', 'utf8', '-O', username, database_name] |
247 | 247 | p = Popen(command, stderr=psql_log_file, stdout=psql_log_file, cwd='/tmp') # nosec |
248 | 248 | p.wait() |
249 | 249 | return_code = p.returncode |
250 | 250 | psql_log_file.seek(0) |
251 | 251 | output = psql_log_file.read().decode('utf-8') |
252 | already_exists_error = 'database creation failed: ERROR: database "{0}" already exists'.format(database_name) | |
252 | already_exists_error = f'database creation failed: ERROR: database "{database_name}" already exists' | |
253 | 253 | if already_exists_error in output: |
254 | print('{yellow}WARNING{white}: Database already exists.'.format(yellow=Fore.YELLOW, white=Fore.WHITE)) | |
254 | print(f'{Fore.YELLOW}WARNING{Fore.WHITE}: Database already exists.') | |
255 | 255 | return_code = 0 |
256 | 256 | return database_name, return_code |
257 | 257 | |
259 | 259 | """ |
260 | 260 | This step saves database configuration to server.ini |
261 | 261 | """ |
262 | print('Saving database credentials file in {0}'.format(LOCAL_CONFIG_FILE)) | |
262 | print(f'Saving database credentials file in {LOCAL_CONFIG_FILE}') | |
263 | 263 | |
264 | 264 | conn_string = 'postgresql+psycopg2://{username}:{password}@{server}/{database_name}'.format( |
265 | 265 | username=username, |
292 | 292 | db.create_all() |
293 | 293 | except OperationalError as ex: |
294 | 294 | if 'could not connect to server' in str(ex): |
295 | print('ERROR: {red}PostgreSQL service{white} is not running. Please verify that it is running in port 5432 before executing setup script.'.format(red=Fore.RED, white=Fore.WHITE)) | |
295 | print(f'ERROR: {Fore.RED}PostgreSQL service{Fore.WHITE} is not running. Please verify that it is running in port 5432 before executing setup script.') | |
296 | 296 | sys.exit(1) |
297 | 297 | elif 'password authentication failed' in str(ex): |
298 | 298 | print('ERROR: ') |
306 | 306 | except ImportError as ex: |
307 | 307 | if 'psycopg2' in str(ex): |
308 | 308 | print( |
309 | 'ERROR: Missing python depency {red}psycopg2{white}. Please install it with {blue}pip install psycopg2'.format(red=Fore.RED, white=Fore.WHITE, blue=Fore.BLUE)) | |
309 | f'ERROR: Missing python depency {Fore.RED}psycopg2{Fore.WHITE}. Please install it with {Fore.BLUE}pip install psycopg2') | |
310 | 310 | sys.exit(1) |
311 | 311 | else: |
312 | 312 | raise |
313 | 313 | else: |
314 | alembic_cfg = Config(os.path.join(FARADAY_BASE, 'alembic.ini')) | |
314 | alembic_cfg = Config(FARADAY_BASE / 'alembic.ini') | |
315 | 315 | os.chdir(FARADAY_BASE) |
316 | 316 | command.stamp(alembic_cfg, "head") |
317 | # I'm Py3 | |
317 | # TODO ADD RETURN TO PREV DIR |
0 | import sys | |
1 | import click | |
2 | from jinja2 import Environment, FileSystemLoader | |
3 | from pathlib import Path | |
4 | ||
5 | ||
6 | def generate_nginx_config(fqdn, port, ws_port, ssl_certificate, ssl_key, multitenant_url): | |
7 | click.echo(f"Generating Faraday nginx config for server: {fqdn}") | |
8 | click.echo("Faraday") | |
9 | if multitenant_url: | |
10 | click.echo(f"- Multitenant URL: /{multitenant_url}/") | |
11 | click.echo(f"- Port: {port}") | |
12 | click.echo(f"- Websocket Port: {ws_port}") | |
13 | click.echo(f"SSL: certificate [{ssl_certificate}] - key [{ssl_key}]") | |
14 | confirm = click.prompt('Confirm [Y/n]', type=bool) | |
15 | if confirm: | |
16 | version = sys.version_info | |
17 | static_path = f"/opt/faraday/lib/python{version.major}.{version.minor}/site-packages/faraday/server/www/" | |
18 | templates_path = Path(__file__).parent / 'templates' | |
19 | file_loader = FileSystemLoader(templates_path) | |
20 | env = Environment(loader=file_loader, autoescape=True) | |
21 | template = env.get_template('nginx_config.j2') | |
22 | output = template.render(**{'fqdn': fqdn, 'static_path': static_path, 'faraday_port': port, | |
23 | 'faraday_ws_port': ws_port, 'ssl_certificate': ssl_certificate, 'ssl_key': ssl_key, | |
24 | 'multitenant_url': multitenant_url}) | |
25 | click.echo("NGINX Config\n#####################################\n\n") | |
26 | click.echo(output) |
4 | 4 | See the file 'doc/LICENSE' for the license information |
5 | 5 | |
6 | 6 | """ |
7 | import sys | |
8 | import os | |
9 | sys.path.append(os.getcwd()) | |
10 | ||
11 | 7 | import click |
12 | 8 | |
13 | 9 | from faraday.server.models import db |
22 | 18 | for table in ('vulnerability', 'vulnerability_template', 'comment', |
23 | 19 | 'faraday_user'): |
24 | 20 | try: |
25 | db.engine.execute('DROP TABLE {} CASCADE'.format(table)) | |
21 | db.engine.execute(f'DROP TABLE {table} CASCADE') | |
26 | 22 | except Exception as ex: |
27 | 23 | print(ex) |
28 | 24 | db.drop_all() |
47 | 43 | |
48 | 44 | if __name__ == '__main__': |
49 | 45 | main() |
50 | # I'm Py3⏎ |
3 | 3 | See the file 'doc/LICENSE' for the license information |
4 | 4 | |
5 | 5 | """ |
6 | import os | |
7 | 6 | import socket |
8 | 7 | |
9 | 8 | import sqlalchemy |
96 | 95 | |
97 | 96 | def check_storage_permission(): |
98 | 97 | |
99 | path = os.path.join(CONST_FARADAY_HOME_PATH, 'storage', 'test') | |
98 | path = CONST_FARADAY_HOME_PATH / 'storage' / 'test' | |
100 | 99 | |
101 | 100 | try: |
102 | os.mkdir(path) | |
103 | os.rmdir(path) | |
101 | path.mkdir() | |
102 | path.rmdir() | |
104 | 103 | return True |
105 | 104 | except OSError: |
106 | 105 | return None |
107 | 106 | |
108 | 107 | |
109 | 108 | def print_config_info(): |
110 | print('\n{white}Showing faraday server configuration'.format(white=Fore.WHITE)) | |
111 | print('{blue} {KEY}: {white}{VALUE}'. | |
112 | format(KEY='version', VALUE=faraday.__version__, white=Fore.WHITE, blue=Fore.BLUE)) | |
109 | print(f'\n{Fore.WHITE}Showing faraday server configuration') | |
110 | print(f"{Fore.BLUE} version: {Fore.WHITE}{faraday.__version__}") | |
113 | 111 | |
114 | 112 | data_keys = ['bind_address', 'port', 'websocket_port', 'debug'] |
115 | 113 | for key in data_keys: |
116 | 114 | print('{blue} {KEY}: {white}{VALUE}'. |
117 | 115 | format(KEY=key, VALUE=getattr(faraday.server.config.faraday_server, key), white=Fore.WHITE, blue=Fore.BLUE)) |
118 | 116 | |
119 | print('\n{white}Showing faraday plugins data'.format(white=Fore.WHITE)) | |
120 | print('{blue} {KEY}: {white}{VALUE}'. | |
121 | format(KEY='version', VALUE=faraday_plugins.__version__, white=Fore.WHITE, blue=Fore.BLUE)) | |
122 | ||
123 | print('\n{white}Showing dashboard configuration'.format(white=Fore.WHITE)) | |
117 | print(f'\n{Fore.WHITE}Showing faraday plugins data') | |
118 | print(f"{Fore.BLUE} version: {Fore.WHITE}{faraday_plugins.__version__}") | |
119 | ||
120 | print(f'\n{Fore.WHITE}Showing dashboard configuration') | |
124 | 121 | data_keys = ['show_vulns_by_price'] |
125 | 122 | for key in data_keys: |
126 | 123 | print('{blue} {KEY}: {white}{VALUE}'. |
127 | 124 | format(KEY=key, VALUE=getattr(faraday.server.config.dashboard, key), white=Fore.WHITE, blue=Fore.BLUE)) |
128 | 125 | |
129 | print('\n{white}Showing storage configuration'.format(white=Fore.WHITE)) | |
126 | print(f'\n{Fore.WHITE}Showing storage configuration') | |
130 | 127 | data_keys = ['path'] |
131 | 128 | for key in data_keys: |
132 | 129 | print('{blue} {KEY}: {white}{VALUE}'. |
153 | 150 | print('[{red}-{white}] PostgreSQL is running, but needs to be 9.4 or newer, please update PostgreSQL'.\ |
154 | 151 | format(red=Fore.RED, white=Fore.WHITE)) |
155 | 152 | elif result: |
156 | print('[{green}+{white}] PostgreSQL is running and up to date'.\ | |
157 | format(green=Fore.GREEN, white=Fore.WHITE)) | |
153 | print(f'[{Fore.GREEN}+{Fore.WHITE}] PostgreSQL is running and up to date') | |
158 | 154 | return exit_code |
159 | 155 | |
160 | 156 | |
164 | 160 | |
165 | 161 | lock_status = check_locks_postgresql() |
166 | 162 | if lock_status: |
167 | print('[{yellow}-{white}] Warning: PostgreSQL lock detected.' \ | |
168 | .format(yellow=Fore.YELLOW, white=Fore.WHITE)) | |
163 | print(f'[{Fore.YELLOW}-{Fore.WHITE}] Warning: PostgreSQL lock detected.') | |
169 | 164 | elif lock_status == False: |
170 | print('[{green}+{white}] PostgreSQL lock not detected. '.\ | |
171 | format(green=Fore.GREEN, white=Fore.WHITE)) | |
165 | print(f'[{Fore.GREEN}+{Fore.WHITE}] PostgreSQL lock not detected. ') | |
172 | 166 | elif lock_status == None: |
173 | 167 | pass |
174 | 168 | |
175 | 169 | encoding = check_postgresql_encoding() |
176 | 170 | if encoding: |
177 | print('[{green}+{white}] PostgreSQL encoding: {db_encoding}'.\ | |
178 | format(green=Fore.GREEN, white=Fore.WHITE, db_encoding=encoding)) | |
171 | print(f'[{Fore.GREEN}+{Fore.WHITE}] PostgreSQL encoding: {encoding}') | |
179 | 172 | elif encoding == None: |
180 | 173 | pass |
181 | 174 | |
200 | 193 | check_postgres() |
201 | 194 | |
202 | 195 | if check_storage_permission(): |
203 | print('[{green}+{white}] /.faraday/storage -> Permission accepted' \ | |
204 | .format(green=Fore.GREEN, white=Fore.WHITE)) | |
205 | else: | |
206 | print('[{red}-{white}] /.faraday/storage -> Permission denied'\ | |
207 | .format(red=Fore.RED, white=Fore.WHITE)) | |
196 | print(f'[{Fore.GREEN}+{Fore.WHITE}] /.faraday/storage -> Permission accepted') | |
197 | else: | |
198 | print(f'[{Fore.RED}-{Fore.WHITE}] /.faraday/storage -> Permission denied') | |
208 | 199 | |
209 | 200 | if check_open_ports(): |
210 | 201 | print("[{green}+{white}] Port {PORT} in {ad} is open"\ |
217 | 208 | def full_status_check(): |
218 | 209 | print_config_info() |
219 | 210 | |
220 | print('\n{white}Checking if postgreSQL is running...'.format(white=Fore.WHITE)) | |
211 | print(f'\n{Fore.WHITE}Checking if postgreSQL is running...') | |
221 | 212 | print_postgresql_status() |
222 | 213 | print_postgresql_other_status() |
223 | 214 | |
224 | print('\n{white}Checking if Faraday is running...'.format(white=Fore.WHITE)) | |
215 | print(f'\n{Fore.WHITE}Checking if Faraday is running...') | |
225 | 216 | print_faraday_status() |
226 | 217 | |
227 | 218 | print('\n{white}Checking Faraday config...{white}'.format(white=Fore.WHITE)) |
0 | import os | |
1 | 0 | import sys |
2 | 1 | import shutil |
3 | 2 | import tempfile |
3 | from pathlib import Path | |
4 | ||
4 | 5 | from tqdm import tqdm |
5 | 6 | from colorama import init |
6 | 7 | from colorama import Fore, Style |
13 | 14 | |
14 | 15 | init() |
15 | 16 | |
17 | ||
16 | 18 | def init_config(): |
17 | #Creates the directory where all the info will go to | |
18 | path = tempfile.mkdtemp() | |
19 | return path | |
19 | # Creates the directory where all the info will go to | |
20 | return Path(tempfile.mkdtemp()) | |
20 | 21 | |
21 | def get_status_check(path): | |
22 | #Executes status check from with-in the code and uses stdout to save info to file | |
23 | #stdout was the only way to get this information without doing a big refactor | |
22 | ||
23 | def get_status_check(path: Path): | |
24 | # Executes status check from with-in the code and uses stdout to save | |
25 | # info to file | |
26 | # stdout was the only way to get this information without doing a big | |
27 | # refactor | |
24 | 28 | original_stdout = sys.stdout |
25 | 29 | |
26 | sys.stdout = open(path + '/status_check.txt','wt') | |
30 | sys.stdout = (path / 'status_check.txt').open('wt') | |
27 | 31 | status_check.full_status_check() |
28 | ||
32 | ||
29 | 33 | sys.stdout.close() |
30 | 34 | sys.stdout = original_stdout |
31 | 35 | |
32 | 36 | |
33 | def get_logs(path): | |
34 | #Copies the logs using the logs path saved on constants | |
35 | orig_path = os.path.join(CONST_FARADAY_HOME_PATH, 'logs') | |
36 | dst_path = os.path.join(path, 'logs') | |
37 | shutil.copytree(orig_path, dst_path, ignore=shutil.ignore_patterns('access*.*')) | |
37 | def get_logs(path: Path): | |
38 | # Copies the logs using the logs path saved on constants | |
39 | orig_path = CONST_FARADAY_HOME_PATH / 'logs' | |
40 | dst_path = path / 'logs' | |
41 | shutil.copytree(str(orig_path), str(dst_path), # I would do this in other | |
42 | ignore=shutil.ignore_patterns('access*.*')) # way. by Eric | |
38 | 43 | |
39 | 44 | |
40 | def make_zip(path): | |
41 | #Makes a zip file of the new folder with all the information obtained inside | |
42 | shutil.make_archive('faraday_support', 'zip', path) | |
45 | def make_zip(path: Path): | |
46 | # Makes a zip file of the new folder with all the information obtained | |
47 | # inside | |
48 | shutil.make_archive('faraday_support', 'zip', str(path)) | |
43 | 49 | |
44 | def end_config(path): | |
45 | #Deletes recursively the directory created on the init_config | |
50 | ||
51 | def end_config(path: Path): | |
52 | # Deletes recursively the directory created on the init_config | |
46 | 53 | shutil.rmtree(path) |
47 | 54 | |
48 | def revise_os(path): | |
49 | with open(path + '/os_distro.txt','wt') as os_file: | |
50 | os_file.write("{}".format(distro.linux_distribution())) | |
55 | ||
56 | def revise_os(path: Path): | |
57 | with (path / 'os_distro.txt').open('wt') as os_file: | |
58 | os_file.write(f"{distro.linux_distribution()}") | |
59 | ||
51 | 60 | |
52 | 61 | def all_for_support(): |
53 | 62 | with tqdm(total=6) as pbar: |
65 | 74 | pbar.update(1) |
66 | 75 | |
67 | 76 | print('[{green}+{white}] Process Completed. A {bright}faraday_support.zip{normal} was generated' |
68 | .format(green=Fore.GREEN, white=Fore.WHITE, bright=Style.BRIGHT, normal=Style.NORMAL))# I'm Py3 | |
77 | .format(green=Fore.GREEN, white=Fore.WHITE, bright=Style.BRIGHT, normal=Style.NORMAL)) |
0 | server { | |
1 | server_name {{ fqdn }}; | |
2 | listen 443 ssl http2; | |
3 | ||
4 | client_max_body_size 150M; | |
5 | ||
6 | ssl on; | |
7 | ssl_session_cache shared:SSL:50m; | |
8 | ssl_certificate {{ ssl_certificate }}; | |
9 | ssl_certificate_key {{ ssl_key }}; | |
10 | ||
11 | location /{% if multitenant_url %}{{ multitenant_url }}/{% endif %} { | |
12 | alias {{ static_path }}; | |
13 | } | |
14 | ||
15 | location {% if multitenant_url %}/{{ multitenant_url }}{% endif %}/_api/ { | |
16 | proxy_pass http://localhost:{{ faraday_port }}/_api/; | |
17 | proxy_redirect http:// $scheme://; | |
18 | proxy_read_timeout 300; | |
19 | ||
20 | proxy_set_header Host $host; | |
21 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; | |
22 | proxy_set_header X-Forwarded-Ssl on; | |
23 | proxy_set_header X-Forwarded-Proto $scheme; | |
24 | } | |
25 | ||
26 | location {% if multitenant_url %}/{{ multitenant_url }}{% endif %}/websockets { | |
27 | proxy_http_version 1.1; | |
28 | proxy_pass http://localhost:{{ faraday_ws_port }}/websockets; | |
29 | ||
30 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; | |
31 | proxy_set_header Upgrade $http_upgrade; | |
32 | proxy_set_header Connection "upgrade"; | |
33 | } | |
34 | } | |
35 | ||
36 | server { | |
37 | server_name {{ fqdn }}; | |
38 | listen 80 ; | |
39 | ||
40 | # https redirect | |
41 | if ($host = {{ fqdn }}) { | |
42 | return 301 https://$host$request_uri; | |
43 | } | |
44 | ||
45 | return 404; | |
46 | } |
5 | 5 | import shutil |
6 | 6 | import errno |
7 | 7 | from configparser import ConfigParser |
8 | import logging | |
8 | 9 | |
9 | 10 | from logging import ( |
10 | 11 | DEBUG, |
11 | 12 | INFO, |
13 | ||
12 | 14 | ) |
15 | from pathlib import Path | |
16 | ||
13 | 17 | from faraday import __license_version__ as license_version |
14 | 18 | |
15 | CONST_FARADAY_HOME_PATH = os.path.join(os.getenv('FARADAY_HOME', os.path.expanduser('~/')), '.faraday') | |
19 | CONST_FARADAY_HOME_PATH = Path( | |
20 | os.getenv('FARADAY_HOME', Path('~/').expanduser()) | |
21 | ) / '.faraday' | |
16 | 22 | |
17 | 23 | LOGGING_LEVEL = INFO |
18 | 24 | |
19 | FARADAY_BASE = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) | |
20 | FARADAY_SERVER_SESSIONS_DIR = os.path.join(CONST_FARADAY_HOME_PATH, 'session') | |
21 | if not os.path.exists(CONST_FARADAY_HOME_PATH): | |
22 | os.mkdir(CONST_FARADAY_HOME_PATH) | |
23 | if not os.path.exists(FARADAY_SERVER_SESSIONS_DIR): | |
24 | # Temporary hack, remove me | |
25 | os.mkdir(FARADAY_SERVER_SESSIONS_DIR) | |
26 | FARADAY_SERVER_PID_FILE = os.path.join( | |
27 | CONST_FARADAY_HOME_PATH, 'faraday-server-port-{0}.pid') | |
28 | REQUIREMENTS_FILE = os.path.join(FARADAY_BASE, 'requirements.txt') | |
29 | DEFAULT_CONFIG_FILE = os.path.join(FARADAY_BASE, 'server/default.ini') | |
30 | REPORTS_VIEWS_DIR = os.path.join(FARADAY_BASE, 'views/reports') | |
31 | LOCAL_CONFIG_FILE = os.path.expanduser( | |
32 | os.path.join(CONST_FARADAY_HOME_PATH, 'config/server.ini')) | |
33 | LOCAL_REPORTS_FOLDER = os.path.expanduser( | |
34 | os.path.join(CONST_FARADAY_HOME_PATH, 'uploaded_reports/')) | |
25 | FARADAY_BASE = Path(__file__).parent.parent | |
26 | FARADAY_SERVER_SESSIONS_DIR = CONST_FARADAY_HOME_PATH / 'session' | |
27 | if not CONST_FARADAY_HOME_PATH.exists(): | |
28 | CONST_FARADAY_HOME_PATH.mkdir() | |
29 | if not FARADAY_SERVER_SESSIONS_DIR.exists(): | |
30 | FARADAY_SERVER_SESSIONS_DIR.mkdir() | |
31 | FARADAY_SERVER_PID_FILE = CONST_FARADAY_HOME_PATH / \ | |
32 | 'faraday-server-port-{0}.pid' | |
33 | REQUIREMENTS_FILE = FARADAY_BASE / 'requirements.txt' | |
34 | DEFAULT_CONFIG_FILE = FARADAY_BASE / 'server' / 'default.ini' | |
35 | REPORTS_VIEWS_DIR = FARADAY_BASE / 'views' / 'reports' | |
36 | LOCAL_CONFIG_FILE = CONST_FARADAY_HOME_PATH / 'config' / 'server.ini' | |
37 | LOCAL_REPORTS_FOLDER = CONST_FARADAY_HOME_PATH / 'uploaded_reports' | |
35 | 38 | |
36 | 39 | CONFIG_FILES = [DEFAULT_CONFIG_FILE, LOCAL_CONFIG_FILE] |
37 | 40 | CONST_LICENSES_DB = 'faraday_licenses' |
38 | 41 | CONST_VULN_MODEL_DB = 'cwe' |
39 | 42 | |
40 | if not os.path.exists(LOCAL_REPORTS_FOLDER): | |
43 | logger = logging.getLogger(__name__) | |
44 | ||
45 | if not LOCAL_REPORTS_FOLDER.exists(): | |
41 | 46 | try: |
42 | os.makedirs(LOCAL_REPORTS_FOLDER) | |
47 | LOCAL_REPORTS_FOLDER.mkdir(parents=True) | |
43 | 48 | except OSError as e: |
44 | 49 | if e.errno != errno.EEXIST: |
45 | 50 | raise |
46 | 51 | |
47 | 52 | |
48 | 53 | def copy_default_config_to_local(): |
49 | if os.path.exists(LOCAL_CONFIG_FILE): | |
54 | if LOCAL_CONFIG_FILE.exists(): | |
50 | 55 | return |
51 | 56 | |
52 | 57 | # Create directory if it doesn't exist |
53 | 58 | try: |
54 | os.makedirs(os.path.dirname(LOCAL_CONFIG_FILE)) | |
59 | LOCAL_CONFIG_FILE.parent.mkdir(parents=True) | |
55 | 60 | except OSError as e: |
56 | 61 | if e.errno != errno.EEXIST: |
57 | 62 | raise |
59 | 64 | # Copy default config file into faraday local config |
60 | 65 | shutil.copyfile(DEFAULT_CONFIG_FILE, LOCAL_CONFIG_FILE) |
61 | 66 | |
62 | from faraday.server.utils.logger import get_logger # pylint:disable=import-outside-toplevel | |
63 | get_logger(__name__).info(u"Local faraday-server configuration created at {}".format(LOCAL_CONFIG_FILE)) | |
67 | logger.info(f"Local faraday-server configuration created at {LOCAL_CONFIG_FILE}") | |
64 | 68 | |
65 | 69 | |
66 | 70 | def parse_and_bind_configuration(): |
110 | 114 | section = storage |
111 | 115 | elif section_name == 'logger': |
112 | 116 | section = logger_config |
117 | elif section_name == 'smtp': | |
118 | section = smtp | |
113 | 119 | else: |
114 | 120 | return |
115 | 121 | section.parse(__parser) |
132 | 138 | self.secret_key = None |
133 | 139 | self.websocket_port = None |
134 | 140 | self.session_timeout = 12 |
135 | self.api_token_expiration = 2592000 | |
141 | self.api_token_expiration = 43200 # Default as 12 hs | |
136 | 142 | self.agent_token = None |
137 | 143 | self.debug = False |
138 | 144 | self.custom_plugins_folder = None |
166 | 172 | self.certificate = None |
167 | 173 | self.enabled = False |
168 | 174 | |
175 | ||
176 | class SmtpConfigObject(ConfigSection): | |
177 | def __init__(self): | |
178 | self.username = None | |
179 | self.password = None | |
180 | self.host = None | |
181 | self.port = None | |
182 | self.sender = None | |
183 | self.ssl = False | |
184 | self.certfile = None | |
185 | self.keyfile = None | |
169 | 186 | |
170 | 187 | class StorageConfigObject(ConfigSection): |
171 | 188 | def __init__(self): |
184 | 201 | websocket_ssl = WebsocketSSLConfigObject() |
185 | 202 | storage = StorageConfigObject() |
186 | 203 | logger_config = LoggerConfig() |
204 | smtp = SmtpConfigObject() | |
187 | 205 | |
188 | 206 | parse_and_bind_configuration() |
189 | 207 |
2 | 2 | bind_address=localhost |
3 | 3 | websocket_port=9000 |
4 | 4 | debug=false |
5 | session_timeout=12 | |
6 | api_token_expiration=43200 | |
7 | ;custom_plugins_folder=/path/to/custom/plugins | |
5 | 8 | |
6 | [ssl] | |
7 | port=6985 | |
8 | certificate= | |
9 | keyfile= | |
10 | ;keyfile_pwd='' | |
11 | ; | |
9 | [smtp] | |
10 | ;username=username | |
11 | ;password=password | |
12 | ;host=localhost | |
13 | ;port=25 | |
14 | ;[email protected] | |
15 | ;ssl=false | |
16 | ;certfile=/path/to/custom/certfile | |
17 | ;keyfile=/path/to/custom/keyfile | |
12 | 18 | |
13 | 19 | [dashboard] |
14 | 20 | show_vulns_by_price = false |
15 | 21 | |
16 | 22 | [logger] |
17 | use_rfc5424_formatter = false⏎ | |
23 | use_rfc5424_formatter = false |
49 | 49 | content = content.encode('utf-8') |
50 | 50 | image_format = imghdr.what(None, h=content[:32]) |
51 | 51 | if image_format: |
52 | content_type = 'image/{0}'.format(image_format) | |
52 | content_type = f'image/{image_format}' | |
53 | 53 | self.generate_thumbnail(content) |
54 | 54 | return super(FaradayUploadedFile, self).process_content( |
55 | 55 | content, filename, content_type) |
82 | 82 | output.seek(0) |
83 | 83 | |
84 | 84 | thumb_path, thumb_id = self.store_content(output, |
85 | 'thumb.%s' % self.thumbnail_format.lower()) | |
85 | f'thumb.{self.thumbnail_format.lower()}') | |
86 | 86 | self['thumb_id'] = thumb_id |
87 | 87 | self['thumb_path'] = thumb_path |
88 | 88 | |
124 | 124 | if value is not None: |
125 | 125 | value = json.loads(value) |
126 | 126 | return value |
127 | # I'm Py3⏎ | |
127 | # I'm Py3 |
0 | 0 | # Faraday Penetration Test IDE |
1 | 1 | # Copyright (C) 2016 Infobyte LLC (http://www.infobytesec.com/) |
2 | 2 | # See the file 'doc/LICENSE' for the license information |
3 | import logging | |
3 | 4 | import operator |
4 | 5 | import string |
5 | 6 | from datetime import datetime |
43 | 44 | |
44 | 45 | from depot.fields.sqlalchemy import UploadedFileField |
45 | 46 | |
46 | from faraday.server.fields import FaradayUploadedFile, JSONType | |
47 | from faraday.server.fields import JSONType | |
47 | 48 | from flask_security import ( |
48 | 49 | UserMixin, |
49 | 50 | ) |
51 | ||
52 | from faraday.server.fields import FaradayUploadedFile | |
50 | 53 | from faraday.server.utils.database import ( |
51 | 54 | BooleanToIntColumn, |
52 | 55 | get_object_type_for, |
53 | 56 | is_unique_constraint_violation) |
57 | ||
58 | logger = logging.getLogger(__name__) | |
54 | 59 | |
55 | 60 | NonBlankColumn = partial(Column, nullable=False, |
56 | 61 | info={'allow_blank': False}) |
118 | 123 | |
119 | 124 | db = SQLAlchemy() |
120 | 125 | |
121 | SCHEMA_VERSION = 'W.3.0.0' | |
126 | ||
127 | def _make_active_agents_count_property(): | |
128 | query = select([func.count(text('1'))]) | |
129 | ||
130 | from_clause = table('association_workspace_and_agents_table').join( | |
131 | Agent, text('agent.id = association_workspace_and_agents_table.agent_id ' | |
132 | 'and association_workspace_and_agents_table.workspace_id = workspace.id') | |
133 | ) | |
134 | query = query.select_from(from_clause) | |
135 | ||
136 | if db.session.bind.dialect.name == 'sqlite': | |
137 | # SQLite has no "true" expression, we have to use the integer 1 | |
138 | # instead | |
139 | query = query.where(text('agent.active = 1')) | |
140 | elif db.session.bind.dialect.name == 'postgresql': | |
141 | # I suppose that we're using PostgreSQL, that can't compare | |
142 | # booleans with integers | |
143 | query = query.where(text('agent.active = true')) | |
144 | ||
145 | return query | |
122 | 146 | |
123 | 147 | |
124 | 148 | def _make_generic_count_property(parent_table, children_table, where=None): |
125 | 149 | """Make a deferred by default column property that counts the |
126 | 150 | amount of childrens of some parent object""" |
127 | children_id_field = '{}.id'.format(children_table) | |
128 | parent_id_field = '{}.id'.format(parent_table) | |
129 | children_rel_field = '{}.{}_id'.format(children_table, parent_table) | |
151 | children_id_field = f'{children_table}.id' | |
152 | parent_id_field = f'{parent_table}.id' | |
153 | children_rel_field = f'{children_table}.{parent_table}_id' | |
130 | 154 | query = (select([func.count(text(children_id_field))]). |
131 | 155 | select_from(table(children_table)). |
132 | where(text('{} = {}'.format( | |
133 | children_rel_field, parent_id_field)))) | |
156 | where(text(f'{children_rel_field} = {parent_id_field}'))) | |
134 | 157 | if where is not None: |
135 | 158 | query = query.where(where) |
136 | 159 | return column_property(query, deferred=True) |
171 | 194 | # Don't do queries using this style! |
172 | 195 | # This can cause SQL injection vulnerabilities |
173 | 196 | # In this case type_ is supplied from a whitelist so this is safe |
174 | query = query.where(text("vulnerability.type = '%s'" % type_)) | |
197 | query = query.where(text(f"vulnerability.type = '{type_}'")) | |
175 | 198 | if confirmed: |
176 | 199 | if db.session.bind.dialect.name == 'sqlite': |
177 | 200 | # SQLite has no "true" expression, we have to use the integer 1 |
199 | 222 | return query |
200 | 223 | |
201 | 224 | |
202 | def _make_vuln_generic_count_by_severity(severity): | |
225 | def count_vulnerability_severities(query: str, | |
226 | model: db.Model, | |
227 | status: str = None, | |
228 | confirmed: bool = None, | |
229 | all_severities: bool = False, | |
230 | critical: bool = False, | |
231 | informational: bool = False, | |
232 | high: bool = False, | |
233 | medium: bool = False, | |
234 | low: bool = False, | |
235 | unclassified: bool = False, | |
236 | host_vulns: bool = False): | |
237 | """ | |
238 | We assume that vulnerability_SEVERITYNAME_count attr exists in the model passed by param | |
239 | :param query: Alchemy query to append options | |
240 | :param model: model class | |
241 | :param status: vulnerability status | |
242 | :param confirmed: if vuln is confirmed or not | |
243 | :param all_severities: All severities will be counted | |
244 | :param critical: Critical severities will be counted if True | |
245 | :param informational: Informational severities will be counted if True | |
246 | :param high: High severities will be counted if True | |
247 | :param medium: Medium severities will be counted if True | |
248 | :param low: Low severities will be counted if True | |
249 | :param unclassified: Unclassified severities will be counted if True | |
250 | :return: Query with options added | |
251 | """ | |
252 | ||
253 | severities = { | |
254 | 'informational': all_severities or informational, | |
255 | 'critical': all_severities or critical, | |
256 | 'high': all_severities or high, | |
257 | 'medium': all_severities or medium, | |
258 | 'low': all_severities or low, | |
259 | 'unclassified': all_severities or unclassified | |
260 | } | |
261 | ||
262 | extra_query = None | |
263 | if status: | |
264 | if status in Vulnerability.STATUSES: | |
265 | extra_query = f"status = '{status}'" | |
266 | else: | |
267 | logging.warning(f"Incorrect status ({status}) requested ") | |
268 | ||
269 | for severity_name, filter_severity in severities.items(): | |
270 | if filter_severity: | |
271 | _extra_query = f"{extra_query} AND severity = '{severity_name}'" \ | |
272 | if extra_query else f"severity = '{severity_name}'" | |
273 | query = query.options( | |
274 | with_expression( | |
275 | getattr(model, f'vulnerability_{severity_name}_count'), | |
276 | _make_vuln_count_property(None, | |
277 | extra_query=_extra_query, | |
278 | use_column_property=False, | |
279 | get_hosts_vulns=host_vulns, | |
280 | confirmed=confirmed) | |
281 | ) | |
282 | ) | |
283 | ||
284 | query = query.options( | |
285 | with_expression( | |
286 | getattr(model, 'vulnerability_total_count'), | |
287 | _make_vuln_count_property(None, | |
288 | extra_query=extra_query, | |
289 | use_column_property=False, | |
290 | get_hosts_vulns=host_vulns, | |
291 | confirmed=confirmed) | |
292 | ) | |
293 | ) | |
294 | return query | |
295 | ||
296 | ||
297 | def _make_vuln_generic_count_by_severity(severity, tablename="host"): | |
203 | 298 | assert severity in ['critical', 'high', 'medium', 'low', 'informational', 'unclassified'] |
204 | 299 | |
205 | 300 | vuln_count = ( |
273 | 368 | |
274 | 369 | __table_args__ = ( |
275 | 370 | UniqueConstraint(filename, workspace_id, name='uix_source_code_filename_workspace'), |
371 | ) | |
372 | ||
373 | @property | |
374 | def parent(self): | |
375 | return | |
376 | ||
377 | ||
378 | def set_children_objects(instance, value, parent_field, child_field='id', | |
379 | workspaced=True): | |
380 | """ | |
381 | Override some kind of children of instance. This is useful in one | |
382 | to many relationships. It takes care of deleting not used children, | |
383 | adding new objects, and keeping the not modified ones the same. | |
384 | ||
385 | :param instance: instance of the parent object | |
386 | :param value: list of childs (values of the child_field) | |
387 | :param parent_field: the parent field's relationship to the children name | |
388 | :param child_field: the "lookup field" of the children model | |
389 | :param workspaced: indicates if the parent model has a workspace | |
390 | """ | |
391 | # Get the class of the children. Inspired in | |
392 | # https://stackoverflow.com/questions/6843144/how-to-find-sqlalchemy-remote-side-objects-class-or-class-name-without-db-queri | |
393 | children_model = getattr( | |
394 | type(instance), parent_field).property.mapper.class_ | |
395 | ||
396 | value = set(value) | |
397 | current_value = getattr(instance, parent_field) | |
398 | current_value_fields = set(map(operator.attrgetter(child_field), | |
399 | current_value)) | |
400 | ||
401 | for existing_child in current_value_fields: | |
402 | if existing_child not in value: | |
403 | # It was removed | |
404 | removed_instance = next( | |
405 | inst for inst in current_value | |
406 | if getattr(inst, child_field) == existing_child) | |
407 | db.session.delete(removed_instance) | |
408 | ||
409 | for new_child in value: | |
410 | if new_child in current_value_fields: | |
411 | # it already exists | |
412 | continue | |
413 | kwargs = {child_field: new_child} | |
414 | if workspaced: | |
415 | kwargs['workspace'] = instance.workspace | |
416 | current_value.append(children_model(**kwargs)) | |
417 | ||
418 | ||
419 | class Hostname(Metadata): | |
420 | __tablename__ = 'hostname' | |
421 | id = Column(Integer, primary_key=True) | |
422 | name = NonBlankColumn(Text) | |
423 | ||
424 | host_id = Column(Integer, ForeignKey('host.id'), index=True, nullable=False) | |
425 | host = relationship('Host', backref=backref("hostnames", cascade="all, delete-orphan")) | |
426 | workspace_id = Column(Integer, ForeignKey('workspace.id'), index=True, nullable=False) | |
427 | workspace = relationship( | |
428 | 'Workspace', | |
429 | backref='hostnames', | |
430 | foreign_keys=[workspace_id] | |
431 | ) | |
432 | __table_args__ = ( | |
433 | UniqueConstraint(name, host_id, workspace_id, name='uix_hostname_host_workspace'), | |
434 | ) | |
435 | ||
436 | def __str__(self): | |
437 | return self.name | |
438 | ||
439 | @property | |
440 | def parent(self): | |
441 | return self.host | |
442 | ||
443 | ||
444 | ||
445 | class CustomFieldsSchema(db.Model): | |
446 | __tablename__ = 'custom_fields_schema' | |
447 | ||
448 | id = Column(Integer, primary_key=True) | |
449 | field_name = Column(Text, unique=True) | |
450 | field_type = Column(Text) | |
451 | field_metadata = Column(JSONType, nullable=True) | |
452 | field_display_name = Column(Text) | |
453 | field_order = Column(Integer) | |
454 | table_name = Column(Text) | |
455 | ||
456 | ||
457 | class VulnerabilityABC(Metadata): | |
458 | # revisar plugin nexpose, netspark para terminar de definir uniques. asegurar que se carguen bien | |
459 | EASE_OF_RESOLUTIONS = [ | |
460 | 'trivial', | |
461 | 'simple', | |
462 | 'moderate', | |
463 | 'difficult', | |
464 | 'infeasible' | |
465 | ] | |
466 | SEVERITIES = [ | |
467 | 'critical', | |
468 | 'high', | |
469 | 'medium', | |
470 | 'low', | |
471 | 'informational', | |
472 | 'unclassified', | |
473 | ] | |
474 | ||
475 | __abstract__ = True | |
476 | id = Column(Integer, primary_key=True) | |
477 | ||
478 | data = BlankColumn(Text) | |
479 | description = BlankColumn(Text) | |
480 | ease_of_resolution = Column(Enum(*EASE_OF_RESOLUTIONS, name='vulnerability_ease_of_resolution'), nullable=True) | |
481 | name = NonBlankColumn(Text, nullable=False) | |
482 | resolution = BlankColumn(Text) | |
483 | severity = Column(Enum(*SEVERITIES, name='vulnerability_severity'), nullable=False) | |
484 | risk = Column(Float(3, 1), nullable=True) | |
485 | ||
486 | impact_accountability = Column(Boolean, default=False, nullable=False) | |
487 | impact_availability = Column(Boolean, default=False, nullable=False) | |
488 | impact_confidentiality = Column(Boolean, default=False, nullable=False) | |
489 | impact_integrity = Column(Boolean, default=False, nullable=False) | |
490 | ||
491 | external_id = BlankColumn(Text) | |
492 | ||
493 | __table_args__ = ( | |
494 | CheckConstraint('1.0 <= risk AND risk <= 10.0', | |
495 | name='check_vulnerability_risk'), | |
496 | ) | |
497 | ||
498 | custom_fields = Column(JSONType) | |
499 | ||
500 | @property | |
501 | def parent(self): | |
502 | raise NotImplementedError('ABC property called') | |
503 | ||
504 | ||
505 | class CustomAssociationSet(_AssociationSet): | |
506 | """ | |
507 | A custom associacion set that passes the creator method the both | |
508 | the value and the instance of the parent object | |
509 | """ | |
510 | ||
511 | # def __init__(self, lazy_collection, creator, getter, setter, parent): | |
512 | def __init__(self, lazy_collection, creator, value_attr, parent): | |
513 | """I have to override this method because the proxy_factory | |
514 | class takes different arguments than the hardcoded | |
515 | _AssociationSet one. | |
516 | In particular, the getter and the setter aren't passed, but | |
517 | since I have an instance of the parent (AssociationProxy | |
518 | instance) I do the logic here. | |
519 | The value_attr argument isn't relevant to this implementation | |
520 | """ | |
521 | ||
522 | if getattr(parent, 'getset_factory', False): | |
523 | getter, setter = parent.getset_factory( | |
524 | parent.collection_class, parent) | |
525 | else: | |
526 | getter, setter = parent._default_getset(parent.collection_class) | |
527 | ||
528 | super(CustomAssociationSet, self).__init__( | |
529 | lazy_collection, creator, getter, setter, parent) | |
530 | ||
531 | def _create(self, value): | |
532 | if getattr(self.lazy_collection, 'ref', False): | |
533 | # for sqlalchemy previous to 1.3.0b1 | |
534 | parent_instance = self.lazy_collection.ref() | |
535 | else: | |
536 | parent_instance = self.lazy_collection.parent | |
537 | session = db.session | |
538 | conflict_objs = session.new | |
539 | try: | |
540 | yield self.creator(value, parent_instance) | |
541 | except IntegrityError as ex: | |
542 | if not is_unique_constraint_violation(ex): | |
543 | raise | |
544 | # unique constraint failed at database | |
545 | # other process/thread won us on the commit | |
546 | # we need to fetch already created objs. | |
547 | session.rollback() | |
548 | for conflict_obj in conflict_objs: | |
549 | if not hasattr(conflict_obj, 'name'): | |
550 | # The session can hold elements without a name (altough it shouldn't) | |
551 | continue | |
552 | if conflict_obj.name == value: | |
553 | continue | |
554 | persisted_conclict_obj = session.query(conflict_obj.__class__).filter_by(name=conflict_obj.name).first() | |
555 | if persisted_conclict_obj: | |
556 | self.col.add(persisted_conclict_obj) | |
557 | yield self.creator(value, parent_instance) | |
558 | ||
559 | def add(self, value): | |
560 | if value not in self: | |
561 | for new_value in self._create(value): | |
562 | self.col.add(new_value) | |
563 | ||
564 | def _build_associationproxy_creator(model_class_name): | |
565 | def creator(name, vulnerability): | |
566 | """Get or create a reference/policyviolation with the | |
567 | corresponding name. This must be workspace aware""" | |
568 | ||
569 | # Ugly hack to avoid the fact that Reference is defined after | |
570 | # Vulnerability | |
571 | model_class = globals()[model_class_name] | |
572 | ||
573 | assert (vulnerability.workspace and vulnerability.workspace.id | |
574 | is not None), "Unknown workspace id" | |
575 | child = model_class.query.filter( | |
576 | getattr(model_class, 'workspace') == vulnerability.workspace, | |
577 | getattr(model_class, 'name') == name, | |
578 | ).first() | |
579 | if child is None: | |
580 | # Doesn't exist | |
581 | child = model_class(name, vulnerability.workspace.id) | |
582 | return child | |
583 | ||
584 | return creator | |
585 | ||
586 | ||
587 | def _build_associationproxy_creator_non_workspaced(model_class_name): | |
588 | def creator(name, vulnerability): | |
589 | """Get or create a reference/policyviolation with the | |
590 | corresponding name. This must be workspace aware""" | |
591 | ||
592 | # Ugly hack to avoid the fact that Reference is defined after | |
593 | # Vulnerability | |
594 | model_class = globals()[model_class_name] | |
595 | child = model_class.query.filter( | |
596 | getattr(model_class, 'name') == name, | |
597 | ).first() | |
598 | if child is None: | |
599 | # Doesn't exist | |
600 | child = model_class(name) | |
601 | return child | |
602 | ||
603 | return creator | |
604 | ||
605 | ||
606 | class VulnerabilityTemplate(VulnerabilityABC): | |
607 | __tablename__ = 'vulnerability_template' | |
608 | ||
609 | __table_args__ = ( | |
610 | UniqueConstraint('name', name='uix_vulnerability_template_name'), | |
611 | ) | |
612 | ||
613 | # We use ReferenceTemplate and not Reference since Templates does not have workspace. | |
614 | ||
615 | reference_template_instances = relationship( | |
616 | "ReferenceTemplate", | |
617 | secondary="reference_template_vulnerability_association", | |
618 | lazy="joined", | |
619 | collection_class=set | |
620 | ) | |
621 | ||
622 | references = association_proxy( | |
623 | 'reference_template_instances', | |
624 | 'name', | |
625 | proxy_factory=CustomAssociationSet, | |
626 | creator=_build_associationproxy_creator_non_workspaced('ReferenceTemplate') | |
627 | ) | |
628 | ||
629 | policy_violation_template_instances = relationship( | |
630 | "PolicyViolationTemplate", | |
631 | secondary="policy_violation_template_vulnerability_association", | |
632 | lazy="joined", | |
633 | collection_class=set | |
634 | ) | |
635 | ||
636 | policy_violations = association_proxy( | |
637 | 'policy_violation_template_instances', | |
638 | 'name', | |
639 | proxy_factory=CustomAssociationSet, | |
640 | creator=_build_associationproxy_creator_non_workspaced('PolicyViolationTemplate') | |
641 | ) | |
642 | custom_fields = Column(JSONType) | |
643 | shipped = Column(Boolean, nullable=False, default=False) | |
644 | ||
645 | ||
646 | class CommandObject(db.Model): | |
647 | __tablename__ = 'command_object' | |
648 | id = Column(Integer, primary_key=True) | |
649 | ||
650 | object_id = Column(Integer, nullable=False) | |
651 | object_type = Column(Enum(*OBJECT_TYPES, name='object_types'), nullable=False) | |
652 | ||
653 | command = relationship('Command', backref='command_objects') | |
654 | command_id = Column(Integer, ForeignKey('command.id'), index=True) | |
655 | ||
656 | workspace_id = Column(Integer, ForeignKey('workspace.id'), index=True, nullable=False) | |
657 | workspace = relationship( | |
658 | 'Workspace', | |
659 | foreign_keys=[workspace_id], | |
660 | backref = backref('command_objects', cascade="all, delete-orphan") | |
661 | ) | |
662 | ||
663 | create_date = Column(DateTime, default=datetime.utcnow) | |
664 | ||
665 | # the following properties are used to know if the command created the specified objects_type | |
666 | # remeber that this table has a row instances per relationship. | |
667 | # this created integer can be used to obtain the total object_type objects created. | |
668 | created = _make_command_created_related_object() | |
669 | ||
670 | # We are currently using the column property created. however to avoid losing information | |
671 | # we also store the a boolean to know if at the moment of created the object related to the | |
672 | # Command was created. | |
673 | created_persistent = Column(Boolean, nullable=False) | |
674 | ||
675 | __table_args__ = ( | |
676 | UniqueConstraint('object_id', 'object_type', 'command_id', 'workspace_id', | |
677 | name='uix_command_object_objid_objtype_command_id_ws'), | |
678 | ) | |
679 | ||
680 | @property | |
681 | def parent(self): | |
682 | return self.command | |
683 | ||
684 | @classmethod | |
685 | def create(cls, obj, command, add_to_session=True, **kwargs): | |
686 | co = cls(obj, workspace=command.workspace, command=command, | |
687 | created_persistent=True, **kwargs) | |
688 | if add_to_session: | |
689 | db.session.add(co) | |
690 | return co | |
691 | ||
692 | def __init__(self, object_=None, **kwargs): | |
693 | ||
694 | if object_ is not None: | |
695 | assert 'object_type' not in kwargs | |
696 | assert 'object_id' not in kwargs | |
697 | object_type = get_object_type_for(object_) | |
698 | ||
699 | # db.session.flush() | |
700 | assert object_.id is not None, "object must have an ID. Try " \ | |
701 | "flushing the session" | |
702 | kwargs['object_id'] = object_.id | |
703 | kwargs['object_type'] = object_type | |
704 | return super(CommandObject, self).__init__(**kwargs) | |
705 | ||
706 | ||
707 | def _make_created_objects_sum(object_type_filter): | |
708 | where_conditions = [f"command_object.object_type= '{object_type_filter}'"] | |
709 | where_conditions.append("command_object.command_id = command.id") | |
710 | where_conditions.append("command_object.workspace_id = command.workspace_id") | |
711 | return column_property( | |
712 | select([func.sum(CommandObject.created)]). | |
713 | select_from(table('command_object')). | |
714 | where(text(' and '.join(where_conditions))) | |
715 | ) | |
716 | ||
717 | ||
718 | def _make_created_objects_sum_joined(object_type_filter, join_filters): | |
719 | """ | |
720 | ||
721 | :param object_type_filter: can be any host, service, vulnerability, credential or any object created from commands. | |
722 | :param join_filters: Filter for vulnerability fields. | |
723 | :return: column property with sum of created objects. | |
724 | """ | |
725 | where_conditions = [f"command_object.object_type= '{object_type_filter}'"] | |
726 | where_conditions.append("command_object.command_id = command.id") | |
727 | where_conditions.append("vulnerability.id = command_object.object_id ") | |
728 | where_conditions.append("command_object.workspace_id = vulnerability.workspace_id") | |
729 | for attr, filter_value in join_filters.items(): | |
730 | where_conditions.append(f"vulnerability.{attr} = {filter_value}") | |
731 | return column_property( | |
732 | select([func.sum(CommandObject.created)]). \ | |
733 | select_from(table('command_object')). \ | |
734 | select_from(table('vulnerability')). \ | |
735 | where(text(' and '.join(where_conditions))) | |
736 | ) | |
737 | ||
738 | ||
739 | class Command(Metadata): | |
740 | ||
741 | IMPORT_SOURCE = [ | |
742 | 'report', # all the files the tools export and faraday imports it from the resports directory, gtk manual import or web import. | |
743 | 'shell', # command executed on the shell or webshell with hooks connected to faraday. | |
744 | 'agent' | |
745 | ] | |
746 | ||
747 | __tablename__ = 'command' | |
748 | id = Column(Integer, primary_key=True) | |
749 | command = NonBlankColumn(Text) | |
750 | tool = NonBlankColumn(Text) | |
751 | start_date = Column(DateTime, nullable=False) | |
752 | end_date = Column(DateTime, nullable=True) | |
753 | ip = BlankColumn(String(250)) # where the command was executed | |
754 | hostname = BlankColumn(String(250)) # where the command was executed | |
755 | params = BlankColumn(Text) | |
756 | user = BlankColumn(String(250)) # os username where the command was executed | |
757 | import_source = Column(Enum(*IMPORT_SOURCE, name='import_source_enum')) | |
758 | ||
759 | workspace_id = Column(Integer, ForeignKey('workspace.id'), index=True, nullable=False) | |
760 | workspace = relationship( | |
761 | 'Workspace', | |
762 | foreign_keys=[workspace_id], | |
763 | backref=backref('commands', cascade="all, delete-orphan") | |
764 | ) | |
765 | ||
766 | sum_created_vulnerabilities = _make_created_objects_sum('vulnerability') | |
767 | ||
768 | sum_created_vulnerabilities_web = _make_created_objects_sum_joined('vulnerability', {'type': '\'vulnerability_web\''}) | |
769 | ||
770 | sum_created_hosts = _make_created_objects_sum('host') | |
771 | ||
772 | sum_created_services = _make_created_objects_sum('service') | |
773 | ||
774 | sum_created_vulnerability_critical = _make_created_objects_sum_joined('vulnerability', {'severity': '\'critical\''}) | |
775 | sum_created_vulnerability_high = _make_created_objects_sum_joined('vulnerability', {'severity': '\'high\''}) | |
776 | sum_created_vulnerability_medium = _make_created_objects_sum_joined('vulnerability', {'severity': '\'medium\''}) | |
777 | sum_created_vulnerability_low = _make_created_objects_sum_joined('vulnerability', {'severity': '\'low\''}) | |
778 | sum_created_vulnerability_info = _make_created_objects_sum_joined('vulnerability', | |
779 | {'severity': '\'informational\''}) | |
780 | sum_created_vulnerability_unclassified = _make_created_objects_sum_joined('vulnerability', | |
781 | {'severity': '\'unclassified\''}) | |
782 | ||
783 | agent_execution = relationship( | |
784 | 'AgentExecution', | |
785 | uselist=False, | |
786 | back_populates="command" | |
276 | 787 | ) |
277 | 788 | |
278 | 789 | @property |
337 | 848 | UniqueConstraint(ip, workspace_id, name='uix_host_ip_workspace'), |
338 | 849 | ) |
339 | 850 | |
340 | vulnerability_info_count = query_expression() | |
341 | vulnerability_med_count = query_expression() | |
851 | vulnerability_informational_count = query_expression() | |
852 | vulnerability_medium_count = query_expression() | |
342 | 853 | vulnerability_high_count = query_expression() |
343 | 854 | vulnerability_critical_count = query_expression() |
344 | 855 | vulnerability_low_count = query_expression() |
351 | 862 | vulnerability_low_generic_count = _make_vuln_generic_count_by_severity('low') |
352 | 863 | vulnerability_info_generic_count = _make_vuln_generic_count_by_severity('informational') |
353 | 864 | vulnerability_unclassified_generic_count = _make_vuln_generic_count_by_severity('unclassified') |
865 | ||
866 | important = Column(Boolean, nullable=False, default=False) | |
354 | 867 | |
355 | 868 | @classmethod |
356 | 869 | def query_with_count(cls, confirmed, host_ids, workspace_name): |
359 | 872 | query = query.filter(cls.id.in_(host_ids)) |
360 | 873 | return query.options( |
361 | 874 | with_expression( |
362 | cls.vulnerability_info_count, | |
875 | cls.vulnerability_informational_count, | |
363 | 876 | _make_vuln_count_property( |
364 | 877 | type_=None, |
365 | 878 | confirmed = confirmed, |
369 | 882 | ) |
370 | 883 | ), |
371 | 884 | with_expression( |
372 | cls.vulnerability_med_count, | |
885 | cls.vulnerability_medium_count, | |
373 | 886 | _make_vuln_count_property( |
374 | 887 | type_ = None, |
375 | 888 | confirmed = confirmed, |
446 | 959 | child_field='name') |
447 | 960 | |
448 | 961 | |
449 | def set_children_objects(instance, value, parent_field, child_field='id', | |
450 | workspaced=True): | |
451 | """ | |
452 | Override some kind of children of instance. This is useful in one | |
453 | to many relationships. It takes care of deleting not used children, | |
454 | adding new objects, and keeping the not modified ones the same. | |
455 | ||
456 | :param instance: instance of the parent object | |
457 | :param value: list of childs (values of the child_field) | |
458 | :param parent_field: the parent field's relationship to the children name | |
459 | :param child_field: the "lookup field" of the children model | |
460 | :param workspaced: indicates if the parent model has a workspace | |
461 | """ | |
462 | # Get the class of the children. Inspired in | |
463 | # https://stackoverflow.com/questions/6843144/how-to-find-sqlalchemy-remote-side-objects-class-or-class-name-without-db-queri | |
464 | children_model = getattr( | |
465 | type(instance), parent_field).property.mapper.class_ | |
466 | ||
467 | value = set(value) | |
468 | current_value = getattr(instance, parent_field) | |
469 | current_value_fields = set(map(operator.attrgetter(child_field), | |
470 | current_value)) | |
471 | ||
472 | for existing_child in current_value_fields: | |
473 | if existing_child not in value: | |
474 | # It was removed | |
475 | removed_instance = next( | |
476 | inst for inst in current_value | |
477 | if getattr(inst, child_field) == existing_child) | |
478 | db.session.delete(removed_instance) | |
479 | ||
480 | for new_child in value: | |
481 | if new_child in current_value_fields: | |
482 | # it already exists | |
483 | continue | |
484 | kwargs = {child_field: new_child} | |
485 | if workspaced: | |
486 | kwargs['workspace'] = instance.workspace | |
487 | current_value.append(children_model(**kwargs)) | |
488 | ||
489 | ||
490 | class Hostname(Metadata): | |
491 | __tablename__ = 'hostname' | |
492 | id = Column(Integer, primary_key=True) | |
493 | name = NonBlankColumn(Text) | |
494 | ||
495 | host_id = Column(Integer, ForeignKey('host.id'), index=True, nullable=False) | |
496 | host = relationship('Host', backref=backref("hostnames", cascade="all, delete-orphan")) | |
497 | workspace_id = Column(Integer, ForeignKey('workspace.id'), index=True, nullable=False) | |
498 | workspace = relationship( | |
499 | 'Workspace', | |
500 | backref='hostnames', | |
501 | foreign_keys=[workspace_id] | |
502 | ) | |
503 | __table_args__ = ( | |
504 | UniqueConstraint(name, host_id, workspace_id, name='uix_hostname_host_workspace'), | |
505 | ) | |
506 | ||
507 | def __str__(self): | |
508 | return self.name | |
509 | ||
510 | @property | |
511 | def parent(self): | |
512 | return self.host | |
513 | ||
514 | ||
515 | 962 | class Service(Metadata): |
516 | 963 | STATUSES = [ |
517 | 964 | 'open', |
562 | 1009 | version = " (" + self.version + ")" |
563 | 1010 | else: |
564 | 1011 | version = "" |
565 | return "(%s/%s) %s%s" % (self.port, self.protocol, self.name, | |
566 | version or "") | |
567 | ||
568 | ||
569 | class CustomFieldsSchema(db.Model): | |
570 | __tablename__ = 'custom_fields_schema' | |
571 | ||
572 | id = Column(Integer, primary_key=True) | |
573 | field_name = Column(Text, unique=True) | |
574 | field_type = Column(Text) | |
575 | field_metadata = Column(JSONType, nullable=True) | |
576 | field_display_name = Column(Text) | |
577 | field_order = Column(Integer) | |
578 | table_name = Column(Text) | |
579 | ||
580 | ||
581 | class VulnerabilityABC(Metadata): | |
582 | # revisar plugin nexpose, netspark para terminar de definir uniques. asegurar que se carguen bien | |
583 | EASE_OF_RESOLUTIONS = [ | |
584 | 'trivial', | |
585 | 'simple', | |
586 | 'moderate', | |
587 | 'difficult', | |
588 | 'infeasible' | |
589 | ] | |
590 | SEVERITIES = [ | |
591 | 'critical', | |
592 | 'high', | |
593 | 'medium', | |
594 | 'low', | |
595 | 'informational', | |
596 | 'unclassified', | |
597 | ] | |
598 | ||
599 | __abstract__ = True | |
600 | id = Column(Integer, primary_key=True) | |
601 | ||
602 | data = BlankColumn(Text) | |
603 | description = BlankColumn(Text) | |
604 | ease_of_resolution = Column(Enum(*EASE_OF_RESOLUTIONS, name='vulnerability_ease_of_resolution'), nullable=True) | |
605 | name = NonBlankColumn(Text, nullable=False) | |
606 | resolution = BlankColumn(Text) | |
607 | severity = Column(Enum(*SEVERITIES, name='vulnerability_severity'), nullable=False) | |
608 | risk = Column(Float(3, 1), nullable=True) | |
609 | ||
610 | impact_accountability = Column(Boolean, default=False, nullable=False) | |
611 | impact_availability = Column(Boolean, default=False, nullable=False) | |
612 | impact_confidentiality = Column(Boolean, default=False, nullable=False) | |
613 | impact_integrity = Column(Boolean, default=False, nullable=False) | |
614 | ||
615 | external_id = BlankColumn(Text) | |
616 | ||
617 | __table_args__ = ( | |
618 | CheckConstraint('1.0 <= risk AND risk <= 10.0', | |
619 | name='check_vulnerability_risk'), | |
620 | ) | |
621 | ||
622 | custom_fields = Column(JSONType) | |
623 | ||
624 | @property | |
625 | def parent(self): | |
626 | raise NotImplementedError('ABC property called') | |
627 | ||
628 | ||
629 | class CustomAssociationSet(_AssociationSet): | |
630 | """ | |
631 | A custom associacion set that passes the creator method the both | |
632 | the value and the instance of the parent object | |
633 | """ | |
634 | ||
635 | # def __init__(self, lazy_collection, creator, getter, setter, parent): | |
636 | def __init__(self, lazy_collection, creator, value_attr, parent): | |
637 | """I have to override this method because the proxy_factory | |
638 | class takes different arguments than the hardcoded | |
639 | _AssociationSet one. | |
640 | In particular, the getter and the setter aren't passed, but | |
641 | since I have an instance of the parent (AssociationProxy | |
642 | instance) I do the logic here. | |
643 | The value_attr argument isn't relevant to this implementation | |
644 | """ | |
645 | ||
646 | if getattr(parent, 'getset_factory', False): | |
647 | getter, setter = parent.getset_factory( | |
648 | parent.collection_class, parent) | |
649 | else: | |
650 | getter, setter = parent._default_getset(parent.collection_class) | |
651 | ||
652 | super(CustomAssociationSet, self).__init__( | |
653 | lazy_collection, creator, getter, setter, parent) | |
654 | ||
655 | def _create(self, value): | |
656 | if getattr(self.lazy_collection, 'ref', False): | |
657 | # for sqlalchemy previous to 1.3.0b1 | |
658 | parent_instance = self.lazy_collection.ref() | |
659 | else: | |
660 | parent_instance = self.lazy_collection.parent | |
661 | session = db.session | |
662 | conflict_objs = session.new | |
663 | try: | |
664 | yield self.creator(value, parent_instance) | |
665 | except IntegrityError as ex: | |
666 | if not is_unique_constraint_violation(ex): | |
667 | raise | |
668 | # unique constraint failed at database | |
669 | # other process/thread won us on the commit | |
670 | # we need to fetch already created objs. | |
671 | session.rollback() | |
672 | for conflict_obj in conflict_objs: | |
673 | if not hasattr(conflict_obj, 'name'): | |
674 | # The session can hold elements without a name (altough it shouldn't) | |
675 | continue | |
676 | if conflict_obj.name == value: | |
677 | continue | |
678 | persisted_conclict_obj = session.query(conflict_obj.__class__).filter_by(name=conflict_obj.name).first() | |
679 | if persisted_conclict_obj: | |
680 | self.col.add(persisted_conclict_obj) | |
681 | yield self.creator(value, parent_instance) | |
682 | ||
683 | def add(self, value): | |
684 | if value not in self: | |
685 | for new_value in self._create(value): | |
686 | self.col.add(new_value) | |
687 | ||
688 | def _build_associationproxy_creator(model_class_name): | |
689 | def creator(name, vulnerability): | |
690 | """Get or create a reference/policyviolation with the | |
691 | corresponding name. This must be workspace aware""" | |
692 | ||
693 | # Ugly hack to avoid the fact that Reference is defined after | |
694 | # Vulnerability | |
695 | model_class = globals()[model_class_name] | |
696 | ||
697 | assert (vulnerability.workspace and vulnerability.workspace.id | |
698 | is not None), "Unknown workspace id" | |
699 | child = model_class.query.filter( | |
700 | getattr(model_class, 'workspace') == vulnerability.workspace, | |
701 | getattr(model_class, 'name') == name, | |
702 | ).first() | |
703 | if child is None: | |
704 | # Doesn't exist | |
705 | child = model_class(name, vulnerability.workspace.id) | |
706 | return child | |
707 | ||
708 | return creator | |
709 | ||
710 | ||
711 | def _build_associationproxy_creator_non_workspaced(model_class_name): | |
712 | def creator(name, vulnerability): | |
713 | """Get or create a reference/policyviolation with the | |
714 | corresponding name. This must be workspace aware""" | |
715 | ||
716 | # Ugly hack to avoid the fact that Reference is defined after | |
717 | # Vulnerability | |
718 | model_class = globals()[model_class_name] | |
719 | child = model_class.query.filter( | |
720 | getattr(model_class, 'name') == name, | |
721 | ).first() | |
722 | if child is None: | |
723 | # Doesn't exist | |
724 | child = model_class(name) | |
725 | return child | |
726 | ||
727 | return creator | |
728 | ||
729 | ||
730 | class VulnerabilityTemplate(VulnerabilityABC): | |
731 | __tablename__ = 'vulnerability_template' | |
732 | ||
733 | __table_args__ = ( | |
734 | UniqueConstraint('name', name='uix_vulnerability_template_name'), | |
735 | ) | |
736 | ||
737 | # We use ReferenceTemplate and not Reference since Templates does not have workspace. | |
738 | ||
739 | reference_template_instances = relationship( | |
740 | "ReferenceTemplate", | |
741 | secondary="reference_template_vulnerability_association", | |
742 | lazy="joined", | |
743 | collection_class=set | |
744 | ) | |
745 | ||
746 | references = association_proxy( | |
747 | 'reference_template_instances', | |
748 | 'name', | |
749 | proxy_factory=CustomAssociationSet, | |
750 | creator=_build_associationproxy_creator_non_workspaced('ReferenceTemplate') | |
751 | ) | |
752 | ||
753 | policy_violation_template_instances = relationship( | |
754 | "PolicyViolationTemplate", | |
755 | secondary="policy_violation_template_vulnerability_association", | |
756 | lazy="joined", | |
757 | collection_class=set | |
758 | ) | |
759 | ||
760 | policy_violations = association_proxy( | |
761 | 'policy_violation_template_instances', | |
762 | 'name', | |
763 | proxy_factory=CustomAssociationSet, | |
764 | creator=_build_associationproxy_creator_non_workspaced('PolicyViolationTemplate') | |
765 | ) | |
766 | custom_fields = Column(JSONType) | |
767 | shipped = Column(Boolean, nullable=False, default=False) | |
768 | ||
769 | ||
770 | class CommandObject(db.Model): | |
771 | __tablename__ = 'command_object' | |
772 | id = Column(Integer, primary_key=True) | |
773 | ||
774 | object_id = Column(Integer, nullable=False) | |
775 | object_type = Column(Enum(*OBJECT_TYPES, name='object_types'), nullable=False) | |
776 | ||
777 | command = relationship('Command', backref='command_objects') | |
778 | command_id = Column(Integer, ForeignKey('command.id'), index=True) | |
779 | ||
780 | workspace_id = Column(Integer, ForeignKey('workspace.id'), index=True, nullable=False) | |
781 | workspace = relationship( | |
782 | 'Workspace', | |
783 | foreign_keys=[workspace_id], | |
784 | backref = backref('command_objects', cascade="all, delete-orphan") | |
785 | ) | |
786 | ||
787 | create_date = Column(DateTime, default=datetime.utcnow) | |
788 | ||
789 | # the following properties are used to know if the command created the specified objects_type | |
790 | # remeber that this table has a row instances per relationship. | |
791 | # this created integer can be used to obtain the total object_type objects created. | |
792 | created = _make_command_created_related_object() | |
793 | ||
794 | # We are currently using the column property created. however to avoid losing information | |
795 | # we also store the a boolean to know if at the moment of created the object related to the | |
796 | # Command was created. | |
797 | created_persistent = Column(Boolean, nullable=False) | |
798 | ||
799 | __table_args__ = ( | |
800 | UniqueConstraint('object_id', 'object_type', 'command_id', 'workspace_id', | |
801 | name='uix_command_object_objid_objtype_command_id_ws'), | |
802 | ) | |
803 | ||
804 | @property | |
805 | def parent(self): | |
806 | return self.command | |
807 | ||
808 | @classmethod | |
809 | def create(cls, obj, command, add_to_session=True, **kwargs): | |
810 | co = cls(obj, workspace=command.workspace, command=command, | |
811 | created_persistent=True, **kwargs) | |
812 | if add_to_session: | |
813 | db.session.add(co) | |
814 | return co | |
815 | ||
816 | def __init__(self, object_=None, **kwargs): | |
817 | ||
818 | if object_ is not None: | |
819 | assert 'object_type' not in kwargs | |
820 | assert 'object_id' not in kwargs | |
821 | object_type = get_object_type_for(object_) | |
822 | ||
823 | # db.session.flush() | |
824 | assert object_.id is not None, "object must have an ID. Try " \ | |
825 | "flushing the session" | |
826 | kwargs['object_id'] = object_.id | |
827 | kwargs['object_type'] = object_type | |
828 | return super(CommandObject, self).__init__(**kwargs) | |
829 | ||
830 | ||
831 | def _make_created_objects_sum(object_type_filter): | |
832 | where_conditions = ["command_object.object_type= '%s'" % object_type_filter] | |
833 | where_conditions.append("command_object.command_id = command.id") | |
834 | where_conditions.append("command_object.workspace_id = command.workspace_id") | |
835 | return column_property( | |
836 | select([func.sum(CommandObject.created)]). | |
837 | select_from(table('command_object')). | |
838 | where(text(' and '.join(where_conditions))) | |
839 | ) | |
840 | ||
841 | ||
842 | def _make_created_objects_sum_joined(object_type_filter, join_filters): | |
843 | """ | |
844 | ||
845 | :param object_type_filter: can be any host, service, vulnerability, credential or any object created from commands. | |
846 | :param join_filters: Filter for vulnerability fields. | |
847 | :return: column property with sum of created objects. | |
848 | """ | |
849 | where_conditions = ["command_object.object_type= '%s'" % object_type_filter] | |
850 | where_conditions.append("command_object.command_id = command.id") | |
851 | where_conditions.append("vulnerability.id = command_object.object_id ") | |
852 | where_conditions.append("command_object.workspace_id = vulnerability.workspace_id") | |
853 | for attr, filter_value in join_filters.items(): | |
854 | where_conditions.append("vulnerability.{0} = {1}".format(attr, filter_value)) | |
855 | return column_property( | |
856 | select([func.sum(CommandObject.created)]). \ | |
857 | select_from(table('command_object')). \ | |
858 | select_from(table('vulnerability')). \ | |
859 | where(text(' and '.join(where_conditions))) | |
860 | ) | |
861 | ||
862 | ||
863 | class Command(Metadata): | |
864 | ||
865 | IMPORT_SOURCE = [ | |
866 | 'report', # all the files the tools export and faraday imports it from the resports directory, gtk manual import or web import. | |
867 | 'shell', # command executed on the shell or webshell with hooks connected to faraday. | |
868 | 'agent' | |
869 | ] | |
870 | ||
871 | __tablename__ = 'command' | |
872 | id = Column(Integer, primary_key=True) | |
873 | command = NonBlankColumn(Text) | |
874 | tool = NonBlankColumn(Text) | |
875 | start_date = Column(DateTime, nullable=False) | |
876 | end_date = Column(DateTime, nullable=True) | |
877 | ip = BlankColumn(String(250)) # where the command was executed | |
878 | hostname = BlankColumn(String(250)) # where the command was executed | |
879 | params = BlankColumn(Text) | |
880 | user = BlankColumn(String(250)) # os username where the command was executed | |
881 | import_source = Column(Enum(*IMPORT_SOURCE, name='import_source_enum')) | |
882 | ||
883 | workspace_id = Column(Integer, ForeignKey('workspace.id'), index=True, nullable=False) | |
884 | workspace = relationship( | |
885 | 'Workspace', | |
886 | foreign_keys=[workspace_id], | |
887 | backref=backref('commands', cascade="all, delete-orphan") | |
888 | ) | |
889 | ||
890 | sum_created_vulnerabilities = _make_created_objects_sum('vulnerability') | |
891 | ||
892 | sum_created_vulnerabilities_web = _make_created_objects_sum_joined('vulnerability', {'type': '\'vulnerability_web\''}) | |
893 | ||
894 | sum_created_hosts = _make_created_objects_sum('host') | |
895 | ||
896 | sum_created_services = _make_created_objects_sum('service') | |
897 | ||
898 | sum_created_vulnerability_critical = _make_created_objects_sum_joined('vulnerability', {'severity': '\'critical\''}) | |
899 | sum_created_vulnerability_high = _make_created_objects_sum_joined('vulnerability', {'severity': '\'high\''}) | |
900 | sum_created_vulnerability_medium = _make_created_objects_sum_joined('vulnerability', {'severity': '\'medium\''}) | |
901 | sum_created_vulnerability_low = _make_created_objects_sum_joined('vulnerability', {'severity': '\'low\''}) | |
902 | sum_created_vulnerability_info = _make_created_objects_sum_joined('vulnerability', | |
903 | {'severity': '\'informational\''}) | |
904 | sum_created_vulnerability_unclassified = _make_created_objects_sum_joined('vulnerability', | |
905 | {'severity': '\'unclassified\''}) | |
906 | ||
907 | @property | |
908 | def parent(self): | |
909 | return | |
1012 | return f"({self.port}/{self.protocol}) {self.name}{version or ''}" | |
910 | 1013 | |
911 | 1014 | |
912 | 1015 | class VulnerabilityGeneric(VulnerabilityABC): |
931 | 1034 | association_date = Column(DateTime, nullable=True) |
932 | 1035 | disassociated_manually = Column(Boolean, nullable=False, default=False) |
933 | 1036 | tool = BlankColumn(Text, nullable=False) |
934 | ||
935 | vulnerability_duplicate_id = Column( | |
1037 | method = BlankColumn(Text) | |
1038 | parameters = BlankColumn(Text) | |
1039 | parameter_name = BlankColumn(Text) | |
1040 | path = BlankColumn(Text) | |
1041 | query_string = BlankColumn(Text) | |
1042 | request = BlankColumn(Text) | |
1043 | response = BlankColumn(Text) | |
1044 | website = BlankColumn(Text) | |
1045 | status_code = Column(Integer, nullable=True) | |
1046 | ||
1047 | ||
1048 | vulnerability_duplicate_id = Column( | |
936 | 1049 | Integer, |
937 | 1050 | ForeignKey('vulnerability.id'), |
938 | 1051 | index=True, |
942 | 1055 | backref=backref('vulnerability_duplicate', remote_side=[id]) |
943 | 1056 | ) |
944 | 1057 | |
945 | vulnerability_template_id = Column( | |
1058 | vulnerability_template_id = Column( | |
946 | 1059 | Integer, |
947 | 1060 | ForeignKey('vulnerability_template.id'), |
948 | 1061 | index=True, |
1050 | 1163 | .where(text('vulnerability.service_id = service.id and ' |
1051 | 1164 | 'host_inner.id = service.host_id')) |
1052 | 1165 | ) |
1166 | ||
1167 | host_id = Column(Integer, ForeignKey(Host.id), index=True) | |
1168 | host = relationship( | |
1169 | 'Host', | |
1170 | backref=backref("vulnerabilities", cascade="all, delete-orphan"), | |
1171 | foreign_keys=[host_id], | |
1172 | ) | |
1173 | ||
1053 | 1174 | target_host_os = column_property( |
1054 | 1175 | case([ |
1055 | 1176 | (text('vulnerability.host_id IS NOT null'), |
1079 | 1200 | def has_duplicate(self): |
1080 | 1201 | return self.vulnerability_duplicate_id == None |
1081 | 1202 | |
1082 | ||
1083 | class Vulnerability(VulnerabilityGeneric): | |
1084 | __tablename__ = None | |
1085 | host_id = Column(Integer, ForeignKey(Host.id), index=True) | |
1086 | host = relationship( | |
1087 | 'Host', | |
1088 | backref=backref("vulnerabilities", cascade="all, delete-orphan"), | |
1089 | foreign_keys=[host_id], | |
1090 | ) | |
1091 | ||
1092 | @declared_attr | |
1093 | def service_id(cls): | |
1094 | return VulnerabilityGeneric.__table__.c.get('service_id', Column(Integer, db.ForeignKey('service.id'), | |
1095 | index=True)) | |
1096 | ||
1097 | @declared_attr | |
1098 | def service(cls): | |
1099 | return relationship('Service', backref=backref("vulnerabilities", cascade="all, delete-orphan")) | |
1100 | ||
1101 | 1203 | @property |
1102 | 1204 | def hostnames(self): |
1103 | 1205 | if self.host is not None: |
1106 | 1208 | return self.service.host.hostnames |
1107 | 1209 | raise ValueError("Vulnerability has no service nor host") |
1108 | 1210 | |
1211 | @declared_attr | |
1212 | def service(cls): | |
1213 | return relationship('Service', backref=backref("vulnerabilitiesGeneric", cascade="all, delete-orphan")) | |
1214 | ||
1215 | ||
1216 | class Vulnerability(VulnerabilityGeneric): | |
1217 | __tablename__ = None | |
1218 | ||
1219 | @declared_attr | |
1220 | def service_id(cls): | |
1221 | return VulnerabilityGeneric.__table__.c.get('service_id', Column(Integer, db.ForeignKey('service.id'), | |
1222 | index=True)) | |
1223 | ||
1224 | @declared_attr | |
1225 | def service(cls): | |
1226 | return relationship('Service', backref=backref("vulnerabilities", cascade="all, delete-orphan")) | |
1227 | ||
1228 | ||
1109 | 1229 | @property |
1110 | 1230 | def parent(self): |
1111 | 1231 | return self.host or self.service |
1117 | 1237 | |
1118 | 1238 | class VulnerabilityWeb(VulnerabilityGeneric): |
1119 | 1239 | __tablename__ = None |
1120 | method = BlankColumn(Text) | |
1121 | parameters = BlankColumn(Text) | |
1122 | parameter_name = BlankColumn(Text) | |
1123 | path = BlankColumn(Text) | |
1124 | query_string = BlankColumn(Text) | |
1125 | request = BlankColumn(Text) | |
1126 | response = BlankColumn(Text) | |
1127 | website = BlankColumn(Text) | |
1128 | status_code = Column(Integer, nullable=True) | |
1129 | 1240 | |
1130 | 1241 | @declared_attr |
1131 | 1242 | def service_id(cls): |
1136 | 1247 | @declared_attr |
1137 | 1248 | def service(cls): |
1138 | 1249 | return relationship('Service', backref=backref("vulnerabilities_web", cascade="all, delete-orphan")) |
1139 | ||
1140 | @declared_attr | |
1141 | def host_id(cls): | |
1142 | return VulnerabilityGeneric.__table__.c.get( | |
1143 | 'host_id', Column(Integer, db.ForeignKey('host.id'), | |
1144 | nullable=False)) | |
1145 | ||
1146 | @declared_attr | |
1147 | def host(cls): | |
1148 | return relationship('Host', backref=backref("vulnerabilities_web", cascade="all, delete-orphan")) | |
1149 | ||
1150 | 1250 | |
1151 | 1251 | @property |
1152 | 1252 | def parent(self): |
1448 | 1548 | vulnerability_code_count = query_expression() |
1449 | 1549 | vulnerability_standard_count = query_expression() |
1450 | 1550 | vulnerability_total_count = query_expression() |
1551 | active_agents_count = query_expression() | |
1552 | ||
1553 | vulnerability_informational_count = query_expression() | |
1554 | vulnerability_medium_count = query_expression() | |
1555 | vulnerability_high_count = query_expression() | |
1556 | vulnerability_critical_count = query_expression() | |
1557 | vulnerability_low_count = query_expression() | |
1558 | vulnerability_unclassified_count = query_expression() | |
1451 | 1559 | |
1452 | 1560 | workspace_permission_instances = relationship( |
1453 | 1561 | "WorkspacePermission", |
1477 | 1585 | FROM host |
1478 | 1586 | WHERE host.workspace_id = workspace.id |
1479 | 1587 | ) AS host_count, |
1588 | (SELECT count(*) | |
1589 | FROM association_workspace_and_agents_table as assoc | |
1590 | JOIN agent ON agent.id = assoc.agent_id and assoc.workspace_id = workspace.id | |
1591 | WHERE agent.active is TRUE | |
1592 | ) AS active_agents_count, | |
1480 | 1593 | p_4.count_3 as open_services, |
1481 | 1594 | p_4.count_4 as total_service_count, |
1482 | 1595 | p_5.count_5 as vulnerability_web_count, |
1483 | 1596 | p_5.count_6 as vulnerability_code_count, |
1484 | 1597 | p_5.count_7 as vulnerability_standard_count, |
1485 | 1598 | p_5.count_8 as vulnerability_total_count, |
1599 | p_5.count_9 as vulnerability_critical_count, | |
1600 | p_5.count_10 as vulnerability_high_count, | |
1601 | p_5.count_11 as vulnerability_medium_count, | |
1602 | p_5.count_12 as vulnerability_low_count, | |
1603 | p_5.count_13 as vulnerability_informational_count, | |
1604 | p_5.count_14 as vulnerability_unclassified_count, | |
1486 | 1605 | workspace.create_date AS workspace_create_date, |
1487 | 1606 | workspace.update_date AS workspace_update_date, |
1488 | 1607 | workspace.id AS workspace_id, |
1498 | 1617 | workspace.creator_id AS workspace_creator_id, |
1499 | 1618 | (SELECT {concat_func}(scope.name, ',') FROM scope where scope.workspace_id=workspace.id) as scope_raw |
1500 | 1619 | FROM workspace |
1501 | LEFT JOIN (SELECT w.id as wid, COUNT(case when service.id IS NOT NULL and service.status = 'open' then 1 else null end) as count_3, COUNT(case when service.id IS NOT NULL then 1 else null end) AS count_4 | |
1620 | LEFT JOIN (SELECT w.id as wid, | |
1621 | COUNT(case when service.id IS NOT NULL and service.status = 'open' then 1 else null end) as count_3, | |
1622 | COUNT(case when service.id IS NOT NULL then 1 else null end) AS count_4 | |
1502 | 1623 | FROM service |
1503 | 1624 | RIGHT JOIN workspace w ON service.workspace_id = w.id |
1504 | 1625 | GROUP BY w.id |
1505 | 1626 | ) AS p_4 ON p_4.wid = workspace.id |
1506 | LEFT JOIN (SELECT w.id as w_id, COUNT(case when vulnerability.type = 'vulnerability_web' then 1 else null end) as count_5, COUNT(case when vulnerability.type = 'vulnerability_code' then 1 else null end) AS count_6, COUNT(case when vulnerability.type = 'vulnerability' then 1 else null end) as count_7, COUNT(case when vulnerability.id IS NOT NULL then 1 else null end) AS count_8 | |
1627 | LEFT JOIN (SELECT w.id as w_id, | |
1628 | COUNT(case when vulnerability.type = 'vulnerability_web' then 1 else null end) as count_5, | |
1629 | COUNT(case when vulnerability.type = 'vulnerability_code' then 1 else null end) AS count_6, | |
1630 | COUNT(case when vulnerability.type = 'vulnerability' then 1 else null end) as count_7, | |
1631 | COUNT(case when vulnerability.id IS NOT NULL then 1 else null end) AS count_8, | |
1632 | COUNT(case when vulnerability.severity = 'critical' then 1 else null end) as count_9, | |
1633 | COUNT(case when vulnerability.severity = 'high' then 1 else null end) as count_10, | |
1634 | COUNT(case when vulnerability.severity = 'medium' then 1 else null end) as count_11, | |
1635 | COUNT(case when vulnerability.severity = 'low' then 1 else null end) as count_12, | |
1636 | COUNT(case when vulnerability.severity = 'informational' then 1 else null end) as count_13, | |
1637 | COUNT(case when vulnerability.severity = 'unclassified' then 1 else null end) as count_14 | |
1507 | 1638 | FROM vulnerability |
1508 | 1639 | RIGHT JOIN workspace w ON vulnerability.workspace_id = w.id |
1509 | 1640 | WHERE 1=1 {0} |
1535 | 1666 | params['workspace_name'] = workspace_name |
1536 | 1667 | if filters: |
1537 | 1668 | query += ' WHERE ' + ' AND '.join(filters) |
1538 | #query += " GROUP BY workspace.id " | |
1669 | # query += " GROUP BY workspace.id " | |
1539 | 1670 | query += " ORDER BY workspace.name ASC" |
1671 | ||
1540 | 1672 | return db.session.execute(text(query), params) |
1541 | 1673 | |
1542 | 1674 | def set_scope(self, new_scope): |
1654 | 1786 | super(User, self).__init__(*args, **kwargs) |
1655 | 1787 | |
1656 | 1788 | def __repr__(self): |
1657 | return '<%sUser: %s>' % ('LDAP ' if self.is_ldap else '', | |
1658 | self.username) | |
1789 | return f"<{'LDAP ' if self.is_ldap else ''}User: {self.username}>" | |
1659 | 1790 | |
1660 | 1791 | def get_security_payload(self): |
1661 | 1792 | return { |
1662 | 1793 | "username": self.username, |
1663 | "name": self.email | |
1794 | "name": self.username, | |
1795 | "email": self.email, | |
1796 | "role": self.role, | |
1797 | "roles": [self.role], | |
1664 | 1798 | } |
1665 | 1799 | |
1666 | 1800 | |
1740 | 1874 | __mapper_args__ = { |
1741 | 1875 | 'concrete': True |
1742 | 1876 | } |
1743 | ||
1744 | 1877 | template = relationship( |
1745 | 1878 | 'MethodologyTemplate', |
1746 | 1879 | backref=backref('tasks', cascade="all, delete-orphan")) |
1854 | 1987 | |
1855 | 1988 | object_id = Column(Integer, nullable=False) |
1856 | 1989 | object_type = Column(Enum(*OBJECT_TYPES, name='object_types'), nullable=False) |
1990 | ||
1857 | 1991 | tag = relationship('Tag', backref='tagged_objects') |
1858 | 1992 | tag_id = Column(Integer, ForeignKey('tag.id'), index=True) |
1859 | 1993 | |
1911 | 2045 | confirmed = Column(Boolean, nullable=False, default=False) |
1912 | 2046 | vuln_count = Column(Integer, default=0) # saves the amount of vulns when the report was generated. |
1913 | 2047 | markdown = Column(Boolean, default=False, nullable=False) |
2048 | advanced_filter = Column(Boolean, default=False, nullable=False) | |
2049 | advanced_filter_parsed = Column(String, nullable=False, default="") | |
1914 | 2050 | |
1915 | 2051 | workspace_id = Column(Integer, ForeignKey('workspace.id'), index=True, nullable=False) |
1916 | 2052 | workspace = relationship( |
1985 | 2121 | |
1986 | 2122 | faraday_kb_id = Column(Text, nullable=False) |
1987 | 2123 | reference_id = Column(Integer, nullable=False) |
1988 | ||
1989 | 2124 | script_name = Column(Text, nullable=False) |
1990 | 2125 | external_identifier = Column(Text, nullable=False) |
1991 | 2126 | tool_name = Column(Text, nullable=False) |
2135 | 2270 | backref=backref('agent_executions', cascade="all, delete-orphan"), |
2136 | 2271 | ) |
2137 | 2272 | parameters_data = Column(JSONType, nullable=False) |
2273 | command_id = Column(Integer, ForeignKey('command.id'), index=True) | |
2274 | command = relationship( | |
2275 | 'Command', | |
2276 | foreign_keys=[command_id], | |
2277 | backref=backref('agent_execution_id', cascade="all, delete-orphan") | |
2278 | ) | |
2279 | ||
2138 | 2280 | |
2139 | 2281 | @property |
2140 | 2282 | def parent(self): |
294 | 294 | |
295 | 295 | def get_command(self, obj): |
296 | 296 | if obj.command == 'UPDATE': |
297 | return "--{command}:{field}={value}".format(command=obj.command, field=obj.field, value=obj.value) | |
297 | return f"--{obj.command}:{obj.field}={obj.value}" | |
298 | 298 | if obj.command in ['DELETE', 'REMOVE']: |
299 | 299 | return "--DELETE:" |
300 | 300 | if obj.command == 'ALERT': |
301 | return "--{command}:{value}".format(command=obj.command, value=obj.value) | |
302 | ||
303 | raise ValidationError("Command {} not supported.".format(obj.command)) | |
301 | return f"--{obj.command}:{obj.value}" | |
302 | ||
303 | raise ValidationError(f"Command {obj.command} not supported.") | |
304 | 304 | |
305 | 305 | |
306 | 306 | class WorkerConditionSchema(Schema): |
310 | 310 | if obj.operator == "equals": |
311 | 311 | operator = "=" |
312 | 312 | else: |
313 | raise ValidationError("Condition operator {} not support.".format(obj.operator)) | |
314 | return '{field}{operator}{value}'.format(field=obj.field, operator=operator, value=obj.value) | |
313 | raise ValidationError(f"Condition operator {obj.operator} not support.") | |
314 | return f'{obj.field}{operator}{obj.value}' | |
315 | 315 | |
316 | 316 | |
317 | 317 | class WorkerRuleSchema(Schema): |
337 | 337 | value = 'info' |
338 | 338 | if value == 'medium': |
339 | 339 | value = 'med' |
340 | return '{}={}'.format(object_rule_name, value) | |
340 | return f'{object_rule_name}={value}' | |
341 | 341 | |
342 | 342 | @post_dump |
343 | 343 | def remove_none_values(self, data, **kwargs): |
0 | 0 | import logging |
1 | 1 | import threading |
2 | from pathlib import Path | |
2 | 3 | from threading import Thread |
3 | 4 | from queue import Queue, Empty |
4 | import os | |
5 | from typing import Tuple | |
6 | ||
5 | 7 | from faraday_plugins.plugins.manager import PluginsManager |
6 | 8 | from faraday.server.api.modules.bulk_create import bulk_create, BulkCreateSchema |
7 | 9 | from faraday.server import config |
8 | 10 | |
9 | from faraday.server.models import Workspace | |
11 | from faraday.server.models import Workspace, Command, User | |
10 | 12 | from faraday.server.utils.bulk_create import add_creator |
11 | 13 | |
12 | 14 | logger = logging.getLogger(__name__) |
27 | 29 | logger.debug("Stop Reports Manager") |
28 | 30 | self.__event.set() |
29 | 31 | |
30 | def send_report_request(self, workspace_name, report_json, user): | |
32 | def send_report_request(self, | |
33 | workspace_name: str, | |
34 | command_id: int, | |
35 | report_json: dict, | |
36 | user_id: int): | |
31 | 37 | logger.info("Send Report data to workspace [%s]", workspace_name) |
32 | 38 | from faraday.server.web import app # pylint:disable=import-outside-toplevel |
33 | 39 | with app.app_context(): |
34 | 40 | ws = Workspace.query.filter_by(name=workspace_name).one() |
41 | command = Command.query.filter_by(id=command_id).one() | |
42 | user = User.query.filter_by(id=user_id).one() | |
35 | 43 | schema = BulkCreateSchema() |
36 | 44 | data = schema.load(report_json) |
37 | 45 | data = add_creator(data, user) |
38 | bulk_create(ws, data, True) | |
46 | bulk_create(ws, command, data, True, True) | |
39 | 47 | |
40 | def process_report(self, workspace, file_path, plugin_id, user): | |
48 | def process_report(self, | |
49 | workspace_name: str, | |
50 | command_id: int, | |
51 | file_path: Path, | |
52 | plugin_id: int, | |
53 | user_id: int): | |
41 | 54 | plugin = self.plugins_manager.get_plugin(plugin_id) |
42 | 55 | if plugin: |
43 | 56 | try: |
44 | logger.info("Processing report [%s] with plugin [%s]", file_path, plugin.id) | |
45 | plugin.processReport(file_path) | |
57 | logger.info(f"Processing report [{file_path}] with plugin [" | |
58 | f"{plugin.id}") | |
59 | plugin.processReport(str(file_path)) | |
46 | 60 | vulns_data = plugin.get_data() |
61 | del vulns_data['command']['duration'] | |
47 | 62 | except Exception as e: |
48 | 63 | logger.error("Processing Error: %s", e) |
49 | 64 | logger.exception(e) |
50 | 65 | else: |
51 | 66 | try: |
52 | self.send_report_request(workspace, vulns_data, user) | |
67 | self.send_report_request( | |
68 | workspace_name, command_id, vulns_data, user_id | |
69 | ) | |
53 | 70 | logger.info("Report processing finished") |
54 | 71 | except Exception as e: |
55 | 72 | logger.exception(e) |
56 | 73 | logger.error("Save Error: %s", e) |
57 | 74 | else: |
58 | logger.info("No plugin detected for report [%s]", file_path) | |
75 | logger.info(f"No plugin detected for report [{file_path}]") | |
59 | 76 | |
60 | 77 | def run(self): |
61 | 78 | logger.debug("Start Reports Manager") |
62 | 79 | while not self.__event.is_set(): |
63 | 80 | try: |
64 | workspace, file_path, plugin_id, user = self.upload_reports_queue.get(False, timeout=0.1) | |
65 | logger.info("Processing raw report %s", file_path) | |
66 | if os.path.isfile(file_path): | |
67 | self.process_report(workspace, file_path, plugin_id, user) | |
81 | tpl: Tuple[str, int, Path, int, int] = \ | |
82 | self.upload_reports_queue.get(False, timeout=0.1) | |
83 | ||
84 | workspace_name, command_id, file_path, plugin_id, user_id = tpl | |
85 | ||
86 | logger.info(f"Processing raw report {file_path}") | |
87 | if file_path.is_file(): | |
88 | self.process_report( | |
89 | workspace_name, | |
90 | command_id, | |
91 | file_path, | |
92 | plugin_id, | |
93 | user_id | |
94 | ) | |
68 | 95 | else: |
69 | logger.warning("Report file [%s] don't exists", file_path) | |
96 | logger.warning(f"Report file [{file_path}] don't exists", | |
97 | file_path) | |
70 | 98 | except Empty: |
71 | 99 | self.__event.wait(0.1) |
72 | 100 | except KeyboardInterrupt: |
12 | 12 | import signal |
13 | 13 | import logging |
14 | 14 | from functools import partial |
15 | ||
16 | import faraday.server.config | |
15 | from pathlib import Path | |
16 | ||
17 | from faraday.server.config import ( | |
18 | CONST_FARADAY_HOME_PATH, | |
19 | FARADAY_SERVER_PID_FILE, | |
20 | FARADAY_BASE | |
21 | ) | |
17 | 22 | |
18 | 23 | logger = logging.getLogger(__name__) |
19 | 24 | |
136 | 141 | |
137 | 142 | def start_server(): |
138 | 143 | logger.info('Running as a daemon') |
139 | WORKDIR = faraday.server.config.FARADAY_BASE # pylint:disable=unused-variable | |
144 | WORKDIR = FARADAY_BASE # pylint:disable=unused-variable | |
140 | 145 | createDaemon() |
141 | 146 | |
142 | 147 | |
148 | 153 | return False |
149 | 154 | |
150 | 155 | try: |
151 | logger.info('Sending SIGTERM to pid {0}, in port {1}'.format(pid, port)) | |
156 | logger.info(f'Sending SIGTERM to pid {pid}, in port {port}') | |
152 | 157 | os.kill(pid, signal.SIGTERM) |
153 | 158 | logger.info("Faraday Server stopped successfully") |
154 | 159 | except OSError as err: |
184 | 189 | else: |
185 | 190 | return pid |
186 | 191 | |
187 | ||
188 | 192 | def get_server_pid(port): |
189 | if not os.path.isfile(faraday.server.config.FARADAY_SERVER_PID_FILE.format(port)): | |
193 | if not Path(str(FARADAY_SERVER_PID_FILE).format(port)).exists(): | |
190 | 194 | return None |
191 | 195 | |
192 | with open(faraday.server.config.FARADAY_SERVER_PID_FILE.format(port), 'r') as pid_file: | |
196 | with open(str(FARADAY_SERVER_PID_FILE).format(port), 'r') as pid_file: | |
193 | 197 | # If PID file is badly written, delete it and |
194 | 198 | # assume server is not running |
195 | 199 | try: |
205 | 209 | |
206 | 210 | |
207 | 211 | def create_pid_file(port): |
208 | with open(faraday.server.config.FARADAY_SERVER_PID_FILE.format(port), 'w') as pid_file: | |
209 | pid_file.write('{}'.format(os.getpid())) | |
212 | with open(str(FARADAY_SERVER_PID_FILE).format(port), 'w') as pid_file: | |
213 | pid_file.write(f'{os.getpid()}') | |
210 | 214 | atexit.register(partial(remove_pid_file, port)) |
211 | 215 | |
212 | 216 | |
213 | 217 | def remove_pid_file(port): |
214 | os.remove(faraday.server.config.FARADAY_SERVER_PID_FILE.format(port)) | |
218 | os.remove(str(FARADAY_SERVER_PID_FILE).format(port)) | |
215 | 219 | |
216 | 220 | |
217 | 221 | def get_ports_running(): |
218 | 222 | ports = [] |
219 | re_string = re.escape(faraday.server.config.FARADAY_SERVER_PID_FILE) | |
220 | re_string = re_string.replace("\{0\}", "[0-9]+") | |
221 | home_dir = os.listdir(faraday.server.config.CONST_FARADAY_HOME_PATH) | |
222 | ||
223 | for path in home_dir: | |
224 | path = faraday.server.config.CONST_FARADAY_HOME_PATH + "/" + path | |
225 | if re.match(re_string, path): | |
226 | port = path.split("-")[-1].split(".")[0] | |
227 | ports.append(int(port)) | |
223 | home_dir = CONST_FARADAY_HOME_PATH | |
224 | ||
225 | for path in home_dir.iterdir(): | |
226 | match = re.match(r"faraday\-server\-port\-(?P<last_name>[0-9]+)\.pid", | |
227 | path.name) | |
228 | if match: | |
229 | ports.append(int(match.group(1))) | |
228 | 230 | |
229 | 231 | return ports |
201 | 201 | |
202 | 202 | @compiler.compiles(BooleanToIntColumn, 'postgresql') |
203 | 203 | def _integer_to_boolean_postgresql(element, compiler, **kw): |
204 | return '{0}::int'.format(element.expression_str) | |
204 | return f'{element.expression_str}::int' | |
205 | 205 | |
206 | 206 | |
207 | 207 | @compiler.compiles(BooleanToIntColumn, 'sqlite') |
217 | 217 | 'VulnerabilityCode']: |
218 | 218 | object_type = 'vulnerability' |
219 | 219 | else: |
220 | raise RuntimeError("Unknown table for object: {}".format( | |
221 | instance)) | |
220 | raise RuntimeError(f"Unknown table for object: {instance}") | |
222 | 221 | return object_type |
223 | 222 | |
224 | 223 |
5 | 5 | import pstats |
6 | 6 | import contextlib |
7 | 7 | from io import StringIO |
8 | import logging | |
8 | 9 | |
9 | import faraday.server.utils.logger | |
10 | 10 | |
11 | debug_logger = faraday.server.utils.logger.get_logger(__name__) | |
11 | debug_logger = logging.getLogger(__name__) | |
12 | 12 | |
13 | 13 | class Timer: |
14 | 14 | def __init__(self, tag, logger=None): |
22 | 22 | def __exit__(self, *args): |
23 | 23 | self.__end = time.time() |
24 | 24 | diff = (self.__end - self.__start) * 1000 |
25 | self.__logger.debug('elapsed time in {}: {} ms'.format(self.__tag, diff)) | |
25 | self.__logger.debug(f'elapsed time in {self.__tag}: {diff} ms') | |
26 | 26 | |
27 | 27 | # |
28 | 28 | # Debug utility extracted from http://docs.sqlalchemy.org/en/latest/faq/performance.html |
40 | 40 | # ps.print_callers() |
41 | 41 | debug_logger.debug(s.getvalue()) |
42 | 42 | |
43 | # I'm Py3⏎ | |
43 | # I'm Py3 |
127 | 127 | vuln_date = vuln['metadata']['create_time'] |
128 | 128 | if vuln['service']: |
129 | 129 | service_fields = ["status", "protocol", "name", "summary", "version", "ports"] |
130 | service_fields_values = ["%s:%s" % (field, vuln['service'][field]) for field in service_fields] | |
130 | service_fields_values = [f"{field}:{vuln['service'][field]}" for field in service_fields] | |
131 | 131 | vuln_service = " - ".join(service_fields_values) |
132 | 132 | else: |
133 | 133 | vuln_service = "" |
3 | 3 | See the file 'doc/LICENSE' for the license information |
4 | 4 | |
5 | 5 | """ |
6 | import logging | |
6 | 7 | import re |
7 | 8 | import typing |
8 | 9 | import numbers |
9 | 10 | import datetime |
11 | ||
12 | import marshmallow_sqlalchemy | |
10 | 13 | from distutils.util import strtobool |
11 | 14 | |
12 | 15 | from dateutil.parser import parse |
13 | from sqlalchemy import inspect | |
14 | 16 | from collections.abc import Iterable |
15 | 17 | from dateutil.parser._parser import ParserError |
16 | from marshmallow import Schema, fields, ValidationError, types, validate | |
18 | from marshmallow import Schema, fields, ValidationError, types, validate, post_load | |
17 | 19 | from marshmallow_sqlalchemy.convert import ModelConverter |
18 | 20 | |
19 | from faraday.server.models import VulnerabilityWeb, Host, Service | |
21 | from faraday.server.models import VulnerabilityWeb, Host, Service, VulnerabilityTemplate | |
20 | 22 | from faraday.server.utils.search import OPERATORS |
21 | 23 | from faraday.server.fields import JSONType |
22 | 24 | |
23 | WHITE_LIST = [ | |
24 | 'tags__name', | |
25 | 'service__name', | |
26 | 'type', | |
27 | 'policy_violations__name', | |
28 | 'host__os', | |
29 | 'references__name', | |
30 | 'evidence__filename', | |
31 | 'service__port', | |
32 | 'hostnames', | |
33 | 'creator' | |
34 | ] | |
35 | ||
36 | ||
37 | COUNT_FIELDS = [ | |
38 | 'host__vulnerability_critical_generic_count', | |
39 | 'host__vulnerability_high_generic_count', | |
40 | 'host__vulnerability_medium_generic_count', | |
41 | 'host__vulnerability_low_generic_count', | |
42 | 'host__vulnerability_info_generic_count', | |
43 | ] | |
44 | ||
45 | VULNERABILITY_FIELDS = [str(algo).split('.')[1] for algo in inspect(VulnerabilityWeb).attrs] + WHITE_LIST + COUNT_FIELDS | |
46 | ||
47 | 25 | |
48 | 26 | VALID_OPERATORS = set(OPERATORS.keys()) - set(['desc', 'asc']) |
49 | 27 | |
28 | logger = logging.getLogger(__name__) | |
50 | 29 | |
51 | 30 | class FlaskRestlessFilterSchema(Schema): |
52 | name = fields.String(validate=validate.OneOf(VULNERABILITY_FIELDS), required=True) | |
31 | name = fields.String(required=True) | |
53 | 32 | val = fields.Raw(required=True) |
54 | 33 | op = fields.String(validate=validate.OneOf(list(OPERATORS.keys())), required=True) |
55 | 34 | valid_relationship = { |
56 | 35 | 'host': Host, |
57 | 'service': Service | |
36 | 'services': Service | |
58 | 37 | } |
59 | 38 | |
60 | 39 | def load( |
77 | 56 | res += self._validate_filter_types(filter_) |
78 | 57 | return res |
79 | 58 | |
59 | def _model_class(self): | |
60 | raise NotImplementedError | |
61 | ||
80 | 62 | def _validate_filter_types(self, filter_): |
63 | """ | |
64 | Compares the filter_ list against the model field and the value to be compared. | |
65 | PostgreSQL is very hincha con los types. | |
66 | Return a list of filters (filters are dicts) | |
67 | """ | |
81 | 68 | if isinstance(filter_['val'], str) and '\x00' in filter_['val']: |
82 | 69 | raise ValidationError('Value can\'t containt null chars') |
83 | 70 | converter = ModelConverter() |
90 | 77 | raise ValidationError('Invalid Relationship') |
91 | 78 | column = getattr(model, column_name) |
92 | 79 | else: |
93 | column = getattr(VulnerabilityWeb, column_name) | |
80 | try: | |
81 | column = getattr(self._model_class(), column_name) | |
82 | except AttributeError: | |
83 | raise ValidationError('Field does not exists') | |
94 | 84 | |
95 | 85 | if not getattr(column, 'type', None) and filter_['op'].lower(): |
96 | 86 | if filter_['op'].lower() in ['eq', '==']: |
99 | 89 | if not isinstance(filter_['val'], str): |
100 | 90 | raise ValidationError('Relationship attribute to compare to must be a string') |
101 | 91 | return [filter_] |
92 | # has and any should be used with fields that has a relationship with other table | |
93 | if filter_['op'].lower() in ['has', 'any']: | |
94 | return [filter_] | |
102 | 95 | else: |
103 | 96 | raise ValidationError('Field does not support in operator') |
104 | 97 | |
107 | 100 | if not isinstance(filter_['val'], Iterable): |
108 | 101 | filter_['val'] = [filter_['val']] |
109 | 102 | |
110 | field = converter.column2field(column) | |
103 | try: | |
104 | field = converter.column2field(column) | |
105 | except AttributeError as e: | |
106 | logger.warning(f"Column {column_name} could not be converted. {e}") | |
107 | return [filter_] | |
108 | except marshmallow_sqlalchemy.exceptions.ModelConversionError as e: | |
109 | logger.warning(f"Column {column_name} could not be converted. {e}") | |
110 | return [filter_] | |
111 | ||
111 | 112 | if filter_['op'].lower() in ['ilike', 'like']: |
112 | 113 | # like muse be used with string |
113 | 114 | if isinstance(filter_['val'], numbers.Number) or isinstance(field, fields.Number): |
169 | 170 | return [filter_] |
170 | 171 | |
171 | 172 | |
173 | class FlaskRestlessVulnerabilityFilterSchema(FlaskRestlessFilterSchema): | |
174 | def _model_class(self): | |
175 | return VulnerabilityWeb | |
176 | ||
177 | class FlaskRestlessVulnerabilityTemplateFilterSchema(FlaskRestlessFilterSchema): | |
178 | def _model_class(self): | |
179 | return VulnerabilityTemplate | |
180 | ||
181 | class FlaskRestlessHostFilterSchema(FlaskRestlessFilterSchema): | |
182 | def _model_class(self): | |
183 | return Host | |
184 | ||
185 | ||
172 | 186 | class FlaskRestlessOperator(Schema): |
173 | _or = fields.List(fields.Nested("self"), attribute='or', data_key='or') | |
174 | _and = fields.List(fields.Nested("self"), attribute='and', data_key='and') | |
187 | _or = fields.Nested("self", attribute='or', data_key='or') | |
188 | _and = fields.Nested("self", attribute='and', data_key='and') | |
189 | ||
190 | model_filter_schemas = [ | |
191 | FlaskRestlessHostFilterSchema, | |
192 | FlaskRestlessVulnerabilityFilterSchema, | |
193 | FlaskRestlessVulnerabilityTemplateFilterSchema, | |
194 | ] | |
175 | 195 | |
176 | 196 | def load( |
177 | 197 | self, |
192 | 212 | for search_filter in data: |
193 | 213 | # we try to validate against filter schema since the list could contain |
194 | 214 | # operatores mixed with filters in the list |
195 | try: | |
196 | res += FlaskRestlessFilterSchema(many=False).load(search_filter) | |
197 | except ValidationError: | |
215 | valid_count = 0 | |
216 | for schema in self.model_filter_schemas: | |
217 | try: | |
218 | res += schema(many=False).load(search_filter) | |
219 | valid_count += 1 | |
220 | break | |
221 | except ValidationError: | |
222 | continue | |
223 | ||
224 | if valid_count == 0: | |
198 | 225 | res.append(self._do_load( |
199 | 226 | search_filter, many=False, partial=partial, unknown=unknown, postprocess=True |
200 | 227 | )) |
203 | 230 | |
204 | 231 | |
205 | 232 | class FlaskRestlessGroupFieldSchema(Schema): |
206 | field = fields.String(validate=validate.OneOf(VULNERABILITY_FIELDS), required=True) | |
233 | field = fields.String(required=True) | |
207 | 234 | |
208 | 235 | |
209 | 236 | class FlaskRestlessOrderFieldSchema(Schema): |
210 | field = fields.String(validate=validate.OneOf(VULNERABILITY_FIELDS), required=True) | |
237 | field = fields.String(required=True) | |
211 | 238 | direction = fields.String(validate=validate.OneOf(["asc", "desc"]), required=False) |
212 | 239 | |
213 | 240 | |
214 | 241 | class FilterSchema(Schema): |
215 | filters = fields.List(fields.Nested("FlaskRestlessSchema")) | |
242 | filters = fields.Nested("FlaskRestlessSchema") | |
216 | 243 | order_by = fields.List(fields.Nested(FlaskRestlessOrderFieldSchema)) |
217 | 244 | group_by = fields.List(fields.Nested(FlaskRestlessGroupFieldSchema)) |
218 | 245 | limit = fields.Integer() |
219 | 246 | offset = fields.Integer() |
220 | 247 | |
248 | @post_load | |
249 | def validate_order_and_group_by(self, data, **kwargs): | |
250 | """ | |
251 | We need to validate that if group_by is used, all the field | |
252 | in the order_by are the same. | |
253 | When using different order_by fields with group, it will cause | |
254 | an error on PostgreSQL | |
255 | """ | |
256 | if 'group_by' in data and 'order_by' in data: | |
257 | group_by_fields = set() | |
258 | order_by_fields = set() | |
259 | for group_field in data['group_by']: | |
260 | group_by_fields.add(group_field['field']) | |
261 | for order_field in data['order_by']: | |
262 | order_by_fields.add(order_field['field']) | |
263 | ||
264 | if order_by_fields != group_by_fields: | |
265 | raise ValidationError('Can\'t group and order by with different fields. ') | |
266 | ||
267 | return data | |
268 | ||
221 | 269 | |
222 | 270 | class FlaskRestlessSchema(Schema): |
223 | 271 | valid_schemas = [ |
224 | 272 | FilterSchema, |
225 | FlaskRestlessFilterSchema, | |
226 | 273 | FlaskRestlessOperator, |
274 | FlaskRestlessVulnerabilityFilterSchema, | |
275 | FlaskRestlessVulnerabilityTemplateFilterSchema, | |
276 | FlaskRestlessHostFilterSchema, | |
227 | 277 | ] |
228 | 278 | |
229 | 279 | def load( |
245 | 295 | return schema(many=many).load(data) |
246 | 296 | except ValidationError: |
247 | 297 | continue |
248 | raise ValidationError('No valid schema found. data {}'.format(data)) | |
298 | raise ValidationError(f'No valid schema found. data {data}') |
3 | 3 | See the file 'doc/LICENSE' for the license information |
4 | 4 | |
5 | 5 | """ |
6 | from builtins import chr #In py3 this is unicode! | |
7 | ||
8 | import re | |
9 | import sys | |
10 | import binascii | |
11 | ||
12 | ||
13 | def clean_dict(d): | |
14 | if not isinstance(d, dict): | |
15 | return d | |
16 | else: | |
17 | new_dict = dict() | |
18 | for key, value in d.items(): | |
19 | if isinstance(value, str): | |
20 | new_dict[key] = clean_string(value) | |
21 | elif isinstance(value, dict): | |
22 | new_dict[key] = clean_dict(value) | |
23 | elif isinstance(value, list): | |
24 | new_dict[key] = clean_list(value) | |
25 | else: | |
26 | new_dict[key] = value | |
27 | return new_dict | |
28 | ||
29 | ||
30 | def clean_list(l): | |
31 | if not isinstance(l, list): | |
32 | return l | |
33 | else: | |
34 | new_list = list() | |
35 | for item in l: | |
36 | if isinstance(item, str): | |
37 | new_list.append(clean_string(item)) | |
38 | elif isinstance(item, dict): | |
39 | new_list.append(clean_dict(item)) | |
40 | elif isinstance(item, list): | |
41 | new_list.append(clean_list(item)) | |
42 | else: | |
43 | new_list.append(item) | |
44 | return new_list | |
45 | ||
46 | ||
47 | def clean_string(s): | |
48 | return ''.join([clean_char(x) for x in s]) | |
49 | ||
50 | ||
51 | def clean_char(char): | |
52 | try: | |
53 | #Get rid of the ctrl characters first. | |
54 | #http://stackoverflow.com/questions/1833873/python-regex-escape-characters | |
55 | char = re.sub('\x1b[^m]*m', '', char) | |
56 | #Clean up invalid xml | |
57 | char = remove_invalid_chars(char) | |
58 | replacements = [ | |
59 | (u'\u201c', '\"'), | |
60 | (u'\u201d', '\"'), | |
61 | (u"\u001B", ' '), #http://www.fileformat.info/info/unicode/char/1b/index.htm | |
62 | (u"\u0019", ' '), #http://www.fileformat.info/info/unicode/char/19/index.htm | |
63 | (u"\u0016", ' '), #http://www.fileformat.info/info/unicode/char/16/index.htm | |
64 | (u"\u001C", ' '), #http://www.fileformat.info/info/unicode/char/1c/index.htm | |
65 | (u"\u0003", ' '), #http://www.utf8-chartable.de/unicode-utf8-table.pl?utf8=0x | |
66 | (u"\u000C", ' ') | |
67 | ] | |
68 | for rep, new_char in replacements: | |
69 | if char == rep: | |
70 | #print ord(char), char.encode('ascii', 'ignore') | |
71 | return new_char | |
72 | #get here if pass all controls, so try to encode or throw UnicodeEncodeError | |
73 | char.encode() | |
74 | ||
75 | return char | |
76 | except UnicodeEncodeError: | |
77 | # Ugly hack triggered when importing some strange objects | |
78 | return '' | |
79 | ||
80 | ||
81 | def remove_invalid_chars(c): | |
82 | #http://stackoverflow.com/questions/1707890/fast-way-to-filter-illegal-xml-unicode-chars-in-python | |
83 | illegal_unichrs = [ (0x00, 0x08), (0x0B, 0x1F), (0x7F, 0x84), (0x86, 0x9F), | |
84 | (0xD800, 0xDFFF), (0xFDD0, 0xFDDF), (0xFFFE, 0xFFFF), | |
85 | (0x1FFFE, 0x1FFFF), (0x2FFFE, 0x2FFFF), (0x3FFFE, 0x3FFFF), | |
86 | (0x4FFFE, 0x4FFFF), (0x5FFFE, 0x5FFFF), (0x6FFFE, 0x6FFFF), | |
87 | (0x7FFFE, 0x7FFFF), (0x8FFFE, 0x8FFFF), (0x9FFFE, 0x9FFFF), | |
88 | (0xAFFFE, 0xAFFFF), (0xBFFFE, 0xBFFFF), (0xCFFFE, 0xCFFFF), | |
89 | (0xDFFFE, 0xDFFFF), (0xEFFFE, 0xEFFFF), (0xFFFFE, 0xFFFFF), | |
90 | (0x10FFFE, 0x10FFFF) ] | |
91 | ||
92 | illegal_ranges = ["%s-%s" % (chr(low), chr(high)) | |
93 | for (low, high) in illegal_unichrs | |
94 | if low < sys.maxunicode] | |
95 | ||
96 | illegal_xml_re = re.compile(u'[%s]' % u''.join(illegal_ranges)) | |
97 | if illegal_xml_re.search(c) is not None: | |
98 | # ret = hex(ord(c)) | |
99 | # ret = binascii.b2a_uu(c) | |
100 | # ret_final = ret[1:-1] | |
101 | ret = '\\x'+binascii.hexlify(c) | |
102 | return ret | |
103 | else: | |
104 | return c | |
105 | ||
106 | ||
107 | 6 | def remove_null_caracters(string): |
108 | 7 | string = string.replace('\x00', '') |
109 | 8 | string = string.replace('\00', '') |
0 | 0 | # Faraday Penetration Test IDE |
1 | 1 | # Copyright (C) 2016 Infobyte LLC (http://www.infobytesec.com/) |
2 | 2 | # See the file 'doc/LICENSE' for the license information |
3 | import os | |
4 | 3 | import logging |
5 | 4 | import logging.handlers |
6 | 5 | import faraday.server.config |
7 | 6 | import errno |
8 | 7 | |
9 | 8 | from syslog_rfc5424_formatter import RFC5424Formatter |
9 | from faraday.server.config import CONST_FARADAY_HOME_PATH | |
10 | 10 | |
11 | LOG_FILE = os.path.expanduser(os.path.join( | |
12 | faraday.server.config.CONST_FARADAY_HOME_PATH, | |
13 | 'logs', | |
14 | 'faraday-server.log')) | |
11 | LOG_FILE = CONST_FARADAY_HOME_PATH / 'logs' / 'faraday-server.log' | |
15 | 12 | |
16 | 13 | MAX_LOG_FILE_SIZE = 5 * 1024 * 1024 # 5 MB |
17 | 14 | MAX_LOG_FILE_BACKUP_COUNT = 5 |
18 | LOG_FORMAT = '%(asctime)s - %(name)s - %(levelname)s {%(threadName)s} [%(filename)s:%(lineno)s - %(funcName)s()] %(message)s' | |
15 | LOG_FORMAT = '%(asctime)s - %(name)s - %(levelname)s {%(threadName)s} [pid:%(process)d] [%(filename)s:%(lineno)s - %(funcName)s()] %(message)s' | |
19 | 16 | LOG_DATE_FORMAT = '%Y-%m-%dT%H:%M:%S%z' |
20 | ROOT_LOGGER = u'faraday' | |
21 | 17 | LOGGING_HANDLERS = [] |
22 | 18 | LVL_SETTABLE_HANDLERS = [] |
23 | 19 | |
24 | 20 | |
25 | 21 | def setup_logging(): |
26 | logger = logging.getLogger(ROOT_LOGGER) | |
27 | logger.propagate = False | |
22 | logger = logging.getLogger() | |
28 | 23 | logger.setLevel(logging.DEBUG) |
29 | 24 | |
30 | 25 | if faraday.server.config.logger_config.use_rfc5424_formatter: |
55 | 50 | |
56 | 51 | |
57 | 52 | def add_handler(handler): |
58 | logger = logging.getLogger(ROOT_LOGGER) | |
53 | logger = logging.getLogger() | |
59 | 54 | logger.addHandler(handler) |
60 | 55 | LOGGING_HANDLERS.append(handler) |
61 | ||
62 | ||
63 | def get_logger(obj=None): | |
64 | """Creates a logger named by a string or an object's class name. | |
65 | Allowing logger to additionally accept strings as names | |
66 | for non-class loggings.""" | |
67 | if obj is None: | |
68 | logger = logging.getLogger(ROOT_LOGGER) | |
69 | elif isinstance(obj, str): | |
70 | if obj != ROOT_LOGGER: | |
71 | logger = logging.getLogger(u'{}.{}'.format(ROOT_LOGGER, obj)) | |
72 | else: | |
73 | logger = logging.getLogger(obj) | |
74 | else: | |
75 | cls_name = obj.__class__.__name__ | |
76 | logger = logging.getLogger(u'{}.{}'.format(ROOT_LOGGER, cls_name)) | |
77 | return logger | |
78 | 56 | |
79 | 57 | |
80 | 58 | def set_logging_level(level): |
85 | 63 | |
86 | 64 | def create_logging_path(): |
87 | 65 | try: |
88 | os.makedirs(os.path.dirname(LOG_FILE)) | |
66 | LOG_FILE.parent.mkdir(parents=True) | |
89 | 67 | except OSError as e: |
90 | 68 | if e.errno != errno.EEXIST: |
91 | 69 | raise |
93 | 71 | setup_logging() |
94 | 72 | |
95 | 73 | |
96 | # I'm Py3⏎ | |
74 | # I'm Py3 |
14 | 14 | |
15 | 15 | """ |
16 | 16 | import inspect |
17 | ||
18 | from sqlalchemy import and_, or_, func | |
17 | import logging | |
18 | ||
19 | from sqlalchemy import func | |
20 | from sqlalchemy import and_, or_ | |
19 | 21 | from sqlalchemy import inspect as sqlalchemy_inspect |
20 | 22 | from sqlalchemy.ext.associationproxy import AssociationProxy |
21 | 23 | from sqlalchemy.orm.attributes import InstrumentedAttribute |
22 | 24 | from sqlalchemy.orm.attributes import QueryableAttribute |
23 | 25 | from sqlalchemy.orm import ColumnProperty |
26 | ||
27 | ||
28 | logger = logging.getLogger(__name__) | |
24 | 29 | |
25 | 30 | |
26 | 31 | def session_query(session, model): |
159 | 164 | |
160 | 165 | def __repr__(self): |
161 | 166 | """Returns a string representation of this object.""" |
162 | return '<OrderBy {0}, {1}>'.format(self.field, self.direction) | |
167 | return f'<OrderBy {self.field}, {self.direction}>' | |
163 | 168 | |
164 | 169 | |
165 | 170 | class GroupBy: |
175 | 180 | |
176 | 181 | def __repr__(self): |
177 | 182 | """Returns a string representation of this object.""" |
178 | return '<GroupBy {0}>'.format(self.field) | |
183 | return f'<GroupBy {self.field}>' | |
179 | 184 | |
180 | 185 | |
181 | 186 | class Filter: |
280 | 285 | |
281 | 286 | class ConjunctionFilter(JunctionFilter): |
282 | 287 | def __repr__(self): |
283 | return 'and_{0}'.format(tuple(repr(f) for f in self)) | |
288 | return f'and_{tuple(repr(f) for f in self)}' | |
284 | 289 | |
285 | 290 | |
286 | 291 | class DisjunctionFilter(JunctionFilter): |
287 | 292 | def __repr__(self): |
288 | return 'or_{0}'.format(tuple(repr(f) for f in self)) | |
293 | return f'or_{tuple(repr(f) for f in self)}' | |
289 | 294 | |
290 | 295 | |
291 | 296 | class SearchParameters: |
484 | 489 | return or_(create_filt(model, f) for f in filt) |
485 | 490 | |
486 | 491 | @staticmethod |
492 | def create_filters_func(model, valid_model_fields): | |
493 | create_filt = QueryBuilder._create_filter | |
494 | ||
495 | def create_filters(filt): | |
496 | if not getattr(filt, 'fieldname', False) or filt.fieldname.split('__')[0] in valid_model_fields: | |
497 | try: | |
498 | return create_filt(model, filt) | |
499 | except AttributeError: | |
500 | # Can't create the filter since the model or submodel does not have the attribute (usually mapper) | |
501 | return None | |
502 | return None | |
503 | ||
504 | return create_filters | |
505 | ||
506 | @staticmethod | |
487 | 507 | def create_query(session, model, search_params, _ignore_order_by=False): |
488 | 508 | """Builds an SQLAlchemy query instance based on the search parameters |
489 | 509 | present in ``search_params``, an instance of :class:`SearchParameters`. |
521 | 541 | query = session.query(*select_fields) |
522 | 542 | else: |
523 | 543 | query = session.query(model) |
524 | # For the sake of brevity, rename this method. | |
525 | create_filt = QueryBuilder._create_filter | |
526 | 544 | # This function call may raise an exception. |
527 | 545 | valid_model_fields = [str(algo).split('.')[1] for algo in sqlalchemy_inspect(model).attrs] |
528 | 546 | |
529 | filters = [] | |
530 | for filt in search_params.filters: | |
531 | if not getattr(filt, 'fieldname', False) or filt.fieldname in valid_model_fields: | |
532 | try: | |
533 | filters.append(create_filt(model, filt)) | |
534 | except AttributeError: | |
535 | # Can't create the filter since the model or submodel does not have the attribute (usually mapper) | |
536 | pass | |
547 | filters_generator = map( # pylint: disable=W1636 | |
548 | QueryBuilder.create_filters_func(model, valid_model_fields), | |
549 | search_params.filters | |
550 | ) | |
551 | filters = [filt for filt in filters_generator if filt is not None] | |
537 | 552 | |
538 | 553 | # Multiple filter criteria at the top level of the provided search |
539 | 554 | # parameters are interpreted as a conjunction (AND). |
650 | 665 | # may raise NoResultFound or MultipleResultsFound |
651 | 666 | return query.one() |
652 | 667 | return query |
653 |
0 | 0 | # Faraday Penetration Test IDE |
1 | 1 | # Copyright (C) 2016 Infobyte LLC (http://www.infobytesec.com/) |
2 | 2 | # See the file 'doc/LICENSE' for the license information |
3 | import os | |
4 | 3 | import sys |
5 | 4 | import functools |
6 | 5 | import logging |
71 | 70 | class WebServer: |
72 | 71 | UI_URL_PATH = b'_ui' |
73 | 72 | API_URL_PATH = b'_api' |
74 | WEB_UI_LOCAL_PATH = os.path.join(faraday.server.config.FARADAY_BASE, 'server/www') | |
73 | WEB_UI_LOCAL_PATH = faraday.server.config.FARADAY_BASE / 'server/www' | |
75 | 74 | |
76 | 75 | def __init__(self): |
77 | 76 | self.__ssl_enabled = faraday.server.config.ssl.enabled |
116 | 115 | |
117 | 116 | def __build_websockets_resource(self): |
118 | 117 | websocket_port = int(faraday.server.config.faraday_server.websocket_port) |
119 | url = '{0}:{1}/websockets'.format(self.__bind_address, websocket_port) | |
118 | url = f'{self.__bind_address}:{websocket_port}/websockets' | |
120 | 119 | if self.__websocket_ssl_enabled: |
121 | 120 | url = 'wss://' + url |
122 | 121 | else: |
146 | 145 | self.raw_report_processor.stop() |
147 | 146 | self.ping_home_thread.stop() |
148 | 147 | |
149 | log_path = os.path.join(CONST_FARADAY_HOME_PATH, 'logs', 'access-logging.log') | |
148 | log_path = CONST_FARADAY_HOME_PATH / 'logs' / 'access-logging.log' | |
150 | 149 | site = twisted.web.server.Site(self.__root_resource, |
151 | 150 | logPath=log_path, |
152 | 151 | logFormatter=proxiedLogFormatter) |
186 | 185 | except error.CannotListenError: |
187 | 186 | logger.warn('Could not start websockets, address already open. This is ok is you wan to run multiple instances.') |
188 | 187 | except Exception as ex: |
189 | logger.warn('Could not start websocket, error: {}'.format(ex)) | |
188 | logger.warn(f'Could not start websocket, error: {ex}') | |
190 | 189 | else: |
191 | 190 | try: |
192 | 191 | listenWS(self.__build_websockets_resource(), interface=self.__bind_address) |
193 | 192 | except error.CannotListenError: |
194 | 193 | logger.warn('Could not start websockets, address already open. This is ok is you wan to run multiple instances.') |
195 | 194 | except Exception as ex: |
196 | logger.warn('Could not start websocket, error: {}'.format(ex)) | |
195 | logger.warn(f'Could not start websocket, error: {ex}') | |
197 | 196 | logger.info('Faraday Server is ready') |
198 | 197 | reactor.addSystemEventTrigger('before', 'shutdown', signal_handler) |
199 | 198 | reactor.run() |
41 | 41 | def onConnect(self, request): |
42 | 42 | protocol, headers = None, {} |
43 | 43 | # see if there already is a cookie set .. |
44 | logger.debug('Websocket request {0}'.format(request)) | |
44 | logger.debug(f'Websocket request {request}') | |
45 | 45 | if 'cookie' in request.headers: |
46 | 46 | try: |
47 | 47 | cookie = http.cookies.SimpleCookie() |
63 | 63 | message = json.loads(payload) |
64 | 64 | if message['action'] == 'JOIN_WORKSPACE': |
65 | 65 | if 'workspace' not in message or 'token' not in message: |
66 | logger.warning('Invalid join workspace message: ' | |
67 | '{}'.format(message)) | |
66 | logger.warning(f'Invalid join workspace message: {message}') | |
68 | 67 | self.sendClose() |
69 | 68 | return |
70 | 69 | signer = itsdangerous.TimestampSigner(app.config['SECRET_KEY'], |
119 | 118 | if message['action'] == 'RUN_STATUS': |
120 | 119 | with app.app_context(): |
121 | 120 | if 'executor_name' not in message: |
122 | logger.warning('Missing executor_name param in message: ''{}'.format(message)) | |
121 | logger.warning(f'Missing executor_name param in message: {message}') | |
123 | 122 | return True |
124 | 123 | |
125 | 124 | (agent_id,) = [ |
229 | 228 | reactor.callLater(0.5, self.tick) |
230 | 229 | |
231 | 230 | def join_workspace(self, client, workspace): |
232 | logger.debug('Join workspace {0}'.format(workspace)) | |
231 | logger.debug(f'Join workspace {workspace}') | |
233 | 232 | if client not in self.workspace_clients[workspace]: |
234 | logger.debug("registered client {}".format(client.peer)) | |
233 | logger.debug(f"registered client {client.peer}") | |
235 | 234 | self.workspace_clients[workspace].append(client) |
236 | 235 | |
237 | 236 | def leave_workspace(self, client, workspace_name): |
238 | logger.debug('Leave workspace {0}'.format(workspace_name)) | |
237 | logger.debug(f'Leave workspace {workspace_name}') | |
239 | 238 | self.workspace_clients[workspace_name].remove(client) |
240 | 239 | |
241 | 240 | def join_agent(self, agent_connection, agent): |
242 | logger.info("Agent {} joined!".format(agent.id)) | |
241 | logger.info(f"Agent {agent.id} joined!") | |
243 | 242 | connected_agents[agent.id] = agent_connection |
244 | 243 | return True |
245 | 244 | |
246 | 245 | def leave_agent(self, agent_connection, agent): |
247 | logger.info("Agent {} left".format(agent.id)) | |
246 | logger.info(f"Agent {agent.id} left") | |
248 | 247 | connected_agents.pop(agent.id) |
249 | 248 | return True |
250 | 249 | |
255 | 254 | for workspace_name, clients in self.workspace_clients.items(): |
256 | 255 | for client in clients: |
257 | 256 | if client == client_to_unregister: |
258 | logger.debug("unregistered client from workspace {0}".format(workspace_name)) | |
257 | logger.debug(f"unregistered client from workspace {workspace_name}") | |
259 | 258 | self.leave_workspace(client, workspace_name) |
260 | 259 | return |
261 | 260 | |
263 | 262 | for (key, value) in connected_agents.copy().items(): |
264 | 263 | if value == protocol: |
265 | 264 | del connected_agents[key] |
266 | logger.info("Agent {} disconnected!".format(key)) | |
265 | logger.info(f"Agent {key} disconnected!") | |
267 | 266 | |
268 | 267 | def broadcast(self, msg): |
269 | 268 | if isinstance(msg, str): |
270 | 269 | msg = msg.encode('utf-8') |
271 | logger.debug("broadcasting prepared message '{}' ..".format(msg)) | |
270 | logger.debug(f"broadcasting prepared message '{msg}' ..") | |
272 | 271 | prepared_msg = json.loads(self.prepareMessage(msg).payload) |
273 | 272 | if b'agent_id' not in msg: |
274 | 273 | for client in self.workspace_clients[prepared_msg['workspace']]: |
275 | 274 | reactor.callFromThread(client.sendPreparedMessage, self.prepareMessage(msg)) |
276 | logger.debug("prepared message sent to {}".format(client.peer)) | |
275 | logger.debug(f"prepared message sent to {client.peer}") | |
277 | 276 | |
278 | 277 | if b'agent_id' in msg: |
279 | 278 | agent_id = prepared_msg['agent_id'] |
283 | 282 | # The agent is offline |
284 | 283 | return |
285 | 284 | reactor.callFromThread(agent_connection.sendPreparedMessage, self.prepareMessage(msg)) |
286 | logger.debug("prepared message sent to agent id: {}".format( | |
287 | agent_id)) | |
285 | logger.debug(f"prepared message sent to agent id: {agent_id}") |
1047 | 1047 | font-size: 16px; |
1048 | 1048 | font-weight: bold; |
1049 | 1049 | } |
1050 | ||
1051 | .remember-me span{ | |
1052 | display: inline-block !important; | |
1053 | width: fit-content; | |
1054 | margin-left: 20px; | |
1055 | } | |
1056 | ||
1057 | .remember-me input{ | |
1058 | display: inline-block; | |
1059 | width: fit-content; | |
1060 | width: -moz-max-content; | |
1061 | margin-top: 0; | |
1062 | top: -2.5px; | |
1063 | left: 0; | |
1064 | } | |
1065 | ||
1066 | .no-overflow { | |
1067 | overflow: hidden; | |
1068 | text-overflow: ellipsis; | |
1069 | white-space: nowrap; | |
1070 | } |
210 | 210 | <script type="text/javascript" src="scripts/vulndb/controllers/modalNew.js"></script> |
211 | 211 | <script type="text/javascript" src="scripts/vulndb/providers/vulnModel.js"></script> |
212 | 212 | <script type="text/javascript" src="scripts/vulndb/providers/vulnModels.js"></script> |
213 | <script type="text/javascript" src="scripts/vulndb/directives/customField.js"></script> | |
213 | 214 | <script type="text/javascript" src="scripts/admin/admin.js"></script> |
214 | 215 | <script type="text/javascript" src="scripts/admin/customFields/controllers/customFields.js"></script> |
215 | 216 | <script type="text/javascript" src="scripts/admin/customFields/providers/customFields.js"></script> |
38 | 38 | $scope.workspaces = []; |
39 | 39 | |
40 | 40 | wss.forEach(function (ws) { |
41 | $scope.workspaces.push(ws); | |
41 | if (ws.active && !ws.readonly) { | |
42 | $scope.workspaces.push(ws); | |
43 | } | |
42 | 44 | }); |
43 | 45 | |
44 | 46 | $scope.workspace = $scope.workspaces[0].name; |
124 | 124 | controller: 'workspacesCtrl', |
125 | 125 | title: 'Dashboard | ' |
126 | 126 | }). |
127 | when('/extras', { | |
128 | templateUrl: 'scripts/extras/partials/extras.html', | |
129 | title: 'Extras | ' | |
130 | }). | |
127 | 131 | when('/help', { |
128 | 132 | templateUrl: 'scripts/help/partials/help.html', |
129 | 133 | title: 'Help | ' |
7 | 7 | |
8 | 8 | $scope.data = { |
9 | 9 | "user": null, |
10 | "pass": null | |
10 | "pass": null, | |
11 | "remember": false | |
11 | 12 | }; |
12 | 13 | |
13 | 14 | $scope.errorLoginFlag = false; |
19 | 20 | |
20 | 21 | $scope.login = function(){ |
21 | 22 | if ($scope.data.user && $scope.data.pass){ |
22 | loginSrv.login($scope.data.user, $scope.data.pass).then(function(user){ | |
23 | loginSrv.login($scope.data.user, $scope.data.pass, $scope.data.remember).then(function(user){ | |
23 | 24 | var currentUrl = "/workspaces"; |
24 | 25 | if($cookies.currentUrl != undefined) { |
25 | 26 | currentUrl = $cookies.currentUrl; |
21 | 21 | </span> |
22 | 22 | </label> |
23 | 23 | </div> |
24 | <div class="form-input margin-top-30px"> | |
25 | <label class="remember-me"> | |
26 | <span class=""> | |
27 | Remember me | |
28 | </span> | |
29 | <input id="remember" type="checkbox" ng-model="data.remember" ng-change="checkResetError()"> | |
30 | </label> | |
31 | </div> | |
24 | 32 | <p class="font-xs font-bold fg-red pull-left margin-top-18px">{{errorMessage}}</p> |
25 | 33 | </div><!-- .form-signin --> |
26 | 34 | <button class="btn-frd btn-xl bg-blue btn-block" style="background-color: #00a8e1" type="submit" ng-click="login()">Login</button> |
2 | 2 | // See the file 'doc/LICENSE' for the license information |
3 | 3 | |
4 | 4 | angular.module('faradayApp') |
5 | .service('loginSrv', ['BASEURL', '$q', function(BASEURL, $q) { | |
6 | ||
5 | .service('loginSrv', ['BASEURL', '$q', '$cookies', function(BASEURL, $q, $cookies) { | |
6 | ||
7 | 7 | loginSrv = { |
8 | 8 | is_authenticated: false, |
9 | 9 | user_obj: null, |
10 | 10 | last_time_checked: new Date(0), |
11 | 11 | |
12 | login: function(user, pass){ | |
12 | login: function(user, pass, remember){ | |
13 | 13 | var deferred = $q.defer(); |
14 | 14 | $.ajax({ |
15 | 15 | type: 'POST', |
16 | 16 | url: BASEURL + '_api/login', |
17 | data: JSON.stringify({"email": user, "password": pass}), | |
17 | data: JSON.stringify({"email": user, "password": pass, "remember": remember}), | |
18 | 18 | dataType: 'json', |
19 | 19 | contentType: 'application/json' |
20 | 20 | }) |
83 | 83 | loginSrv.user_obj = null; |
84 | 84 | deferred.resolve(); |
85 | 85 | } |
86 | $cookies.remove('remember_token'); | |
86 | 87 | $.ajax({ |
87 | 88 | url: BASEURL + '_api/logout', |
88 | 89 | type: 'GET', |
64 | 64 | <li role="menuitem"><a href="" ng-click="logout()">Logout</a></li> |
65 | 65 | <hr class="hr_divider"></hr> |
66 | 66 | <li role="menuitem"><a href="#/help">Help</a></li> |
67 | <li role="menuitem"><a href="#/extras">Extras</a></li> | |
67 | 68 | <li role="menuitem"><a href="" ng-click="about()">About</a></li> |
68 | 69 | </ul> |
69 | 70 | </div> |
11 | 11 | </div><!-- .modal-body --> |
12 | 12 | <div class="modal-footer container-fluid" style="text-align: center;"> |
13 | 13 | <div class="col-md-4"> |
14 | <a href="http://github.com/infobyte/faraday/blob/master/RELEASE.md" target="_blank">What's new</a> | |
14 | <a href="https://medium.com/faraday/faraday-changelog-cd35dfcf37dd" target="_blank">What's new</a> | |
15 | 15 | </div> |
16 | 16 | <div class="col-md-4"> |
17 | 17 | <a href="http://github.com/infobyte/faraday/blob/master/AUTHORS" target="_blank">Authors</a> |
18 | 18 | </div> |
19 | 19 | <div class="col-md-4"> |
20 | <a href="http://github.com/infobyte/faraday/wiki" target="_blank">Documentation</a> | |
20 | <a href="https://support.faradaysec.com/" target="_blank">Documentation</a> | |
21 | 21 | </div> |
22 | 22 | </div> |
0 | <div class="modal-header"> | |
1 | <h4 class="modal-title"><img src="images/faraday-iso.svg" height="30" | |
2 | style="display: inline; padding: 5px; vertical-align:middle;"> Extras</h4> | |
3 | </div> | |
4 | <div class="modal-body"> | |
5 | <h4>Want to level up your Faraday usage? You can download any of the following enhancers to connect with your | |
6 | instance:</h4> | |
7 | <br/> | |
8 | ||
9 | Faraday CLI | |
10 | <br/> | |
11 | Link: <a | |
12 | href="https://github.com/infobyte/faraday-cli" target="_blank">https://github.com/infobyte/faraday-cli</a> | |
13 | <br/> | |
14 | Use Faraday directly from your favorite terminal. This is a common replacement for our GTK Client. | |
15 | ||
16 | <br/><br/> | |
17 | ||
18 | Agent Dispatcher | |
19 | <br/> | |
20 | Link: <a href="https://github.com/infobyte/faraday_agent_dispatcher" target="_blank">https://github.com/infobyte/faraday_agent_dispatcher</a> | |
21 | <br/> | |
22 | Integrate Faraday with your scanning tools and automate all your scans. Our Dispatcher allows you to create custom | |
23 | integrations for your instance. | |
24 | ||
25 | <br/><br/> | |
26 | ||
27 | Faraday Client | |
28 | <br/> | |
29 | DEB Link: <a href="https://storage.googleapis.com/faraday-community/faraday-client_amd64.deb" target="_blank">https://storage.googleapis.com/faraday-community/faraday-client_amd64.deb</a> | |
30 | <br/> | |
31 | RPM Link: <a href="https://storage.googleapis.com/faraday-community/faraday-client_amd64.rpm" target="_blank">https://storage.googleapis.com/faraday-community/faraday-client_amd64.rpm</a> | |
32 | <br/> | |
33 | Run commands directly from our GTK Client and get all results imported automatically into your instance. | |
34 | ||
35 | <br/><br/> | |
36 | ||
37 | Methodology Templates | |
38 | <br/> | |
39 | Link: <a href="https://github.com/infobyte/faraday_templates/tree/master/methodologies" target="_blank">https://github.com/infobyte/faraday_templates/tree/master/methodologies</a> | |
40 | <br/> | |
41 | Common security methodologies ready to be used within your Faraday projects. | |
42 | ||
43 | <br/><br/> | |
44 | ||
45 | Vulnerability KB Templates (CWE) | |
46 | <br/> | |
47 | Link: <a href="https://github.com/infobyte/faraday_templates/tree/master/vulnerability_templates" target="_blank">https://github.com/infobyte/faraday_templates/tree/master/vulnerability_templates</a> | |
48 | <br/> | |
49 | Populate your Faraday KB with default CWE information. | |
50 | ||
51 | <br/><br/> | |
52 | ||
53 | Burp Extender | |
54 | <br/> | |
55 | Link: <a href="https://portswigger.net/bappstore/82f3cbaea46c4f158fd85bbccc90c31c" target="_blank">https://portswigger.net/bappstore/82f3cbaea46c4f158fd85bbccc90c31c</a> | |
56 | <br/> | |
57 | Send findings directly from Burp into your Faraday instance. Automatically populate your project or choose exactly what you want to share. | |
58 | ||
59 | <br/><br/> | |
60 | </div> |
5 | 5 | <h3 class="modal-title"><span class="glyphicon glyphicon-exclamation-sign"></span>Oops!</h3> |
6 | 6 | </div> |
7 | 7 | <div class="modal-body"> |
8 | <h5>{{ msg }}</h5> | |
8 | <h5 class="line-breaks">{{ msg }}</h5> | |
9 | 9 | </div><!-- .modal-body --> |
10 | 10 | <div class="modal-footer"> |
11 | 11 | <button class="btn btn-success" ng-click="ok()">OK</button> |
5 | 5 | <h3 class="modal-title"><span class="glyphicon glyphicon-ok"></span>Great!</h3> |
6 | 6 | </div> |
7 | 7 | <div class="modal-body"> |
8 | <h5>{{ msg }}</h5> | |
8 | <h5 class="line-breaks">{{ msg }}</h5> | |
9 | 9 | </div><!-- .modal-body --> |
10 | 10 | <div class="modal-footer"> |
11 | 11 | <button class="btn btn-success" ng-click="ok()">OK</button> |
541 | 541 | return modVulnerabilityTemplate(createNonWorkspacedObject, vulnerabilityTemplate); |
542 | 542 | }; |
543 | 543 | |
544 | ServerAPI.bulkCreateVulnerabilityTemplate = function (vulns) { | |
545 | var bulkCreateURL = APIURL + 'vulnerability_template/bulk_create/'; | |
546 | return send_data(bulkCreateURL, vulns, false, "POST"); | |
547 | }; | |
548 | ||
544 | 549 | ServerAPI.updateVulnerabilityTemplate = function (vulnerabilityTemplate) { |
545 | 550 | return modVulnerabilityTemplate(updateNonWorkspacedObject, vulnerabilityTemplate); |
546 | 551 | }; |
0 | <!-- Faraday Penetration Test IDE --> | |
1 | <!-- Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/) --> | |
2 | <!-- See the file 'doc/LICENSE' for the license information --> | |
3 | ||
4 | <section id="main" class="seccion"> | |
5 | <div class="faraday-header-border-fix"> | |
6 | <div ng-controller="headerCtrl" ng-include="'scripts/commons/partials/header.html'"></div> | |
7 | <div id="reports-main" class="fila"> | |
8 | <h2 class="ws-label"> | |
9 | <span id="ws-name" title="Help">Extras</span> | |
10 | </h2><!-- .ws-label --> | |
11 | <div class="reports col-md-10 col-sm-10 col-xs-12"> | |
12 | Want to level up your Faraday usage? You can download any of the following enhancers to connect with | |
13 | your instance: | |
14 | <br/><br/> | |
15 | ||
16 | <a href="https://github.com/infobyte/faraday-cli" target="_blank">Faraday CLI</a> | |
17 | <br> | |
18 | Use Faraday directly from your favorite terminal. This is a common replacement for our GTK Client. | |
19 | <br><br> | |
20 | ||
21 | <a href="https://github.com/infobyte/faraday_agent_dispatcher" target="_blank">Agent Dispatcher</a> | |
22 | <br> | |
23 | Integrate Faraday with your scanning tools and automate all your scans. Our Dispatcher allows you to | |
24 | create custom integrations for your instance. | |
25 | <br><br> | |
26 | ||
27 | Faraday Client | |
28 | <br> | |
29 | <a href="https://storage.googleapis.com/faraday-community/faraday-client_amd64.deb" target="_blank">DEB | |
30 | Link</a> | |
31 | <br> | |
32 | <a href="https://storage.googleapis.com/faraday-community/faraday-client_amd64.rpm" target="_blank">RPM | |
33 | Link</a> | |
34 | <br> | |
35 | Run commands directly from our GTK Client and get all results imported automatically into your instance. | |
36 | <br><br> | |
37 | ||
38 | <a href="https://github.com/infobyte/faraday_templates/tree/master/methodologies" target="_blank">Methodology | |
39 | Templates</a> | |
40 | <br> | |
41 | Common security methodologies ready to be used within your Faraday projects. | |
42 | <br><br> | |
43 | ||
44 | <a href="https://github.com/infobyte/faraday_templates/tree/master/vulnerability_templates" | |
45 | target="_blank">Vulnerability KB Templates (CWE)</a> | |
46 | <br> | |
47 | Populate your Faraday KB with default CWE information. | |
48 | <br><br> | |
49 | ||
50 | <a href="https://portswigger.net/bappstore/82f3cbaea46c4f158fd85bbccc90c31c" target="_blank">Burp | |
51 | Extender</a> | |
52 | <br> | |
53 | Send findings directly from Burp into your Faraday instance. Automatically populate your project or | |
54 | choose exactly what you want to share. | |
55 | <br><br> | |
56 | ||
57 | </div><!-- .reports --> | |
58 | </div><!-- #reports-main --> | |
59 | </div><!-- .right-main --> | |
60 | </section><!-- #main --> |
2 | 2 | // See the file 'doc/LICENSE' for the license information |
3 | 3 | |
4 | 4 | angular.module('faradayApp') |
5 | .controller('indexCtrl', | |
5 | .controller('indexCtrl', | |
6 | 6 | ['$scope', '$uibModal', 'indexFact', 'BASEURL', |
7 | 7 | function($scope, $uibModal, indexFact, BASEURL) { |
8 | 8 | indexFact.getConf().then(function(conf) { |
797 | 797 | $scope.saveAsModel = function() { |
798 | 798 | var self = this; |
799 | 799 | var selected = $scope.getCurrentSelection(); |
800 | var promises = []; | |
801 | 800 | try { |
801 | var vulnsToSend = []; | |
802 | 802 | selected.forEach(function(vuln) { |
803 | 803 | let vulnCopy = angular.copy(vuln); |
804 | 804 | vulnCopy.data = ''; |
806 | 806 | vulnCopy.description = vuln.desc; |
807 | 807 | vulnCopy.desc_summary = vuln.desc; |
808 | 808 | vulnCopy.references = vuln.refs; |
809 | promises.push(self.vulnModelsManager.create(vulnCopy, true)); | |
810 | }); | |
811 | $q.all(promises).then(function(success) { | |
812 | commonsFact.showMessage("Created " + selected.length + " templates successfully.", true); | |
813 | }, function(failedMessage) { | |
814 | commonsFact.showMessage(failedMessage); | |
815 | }); | |
809 | vulnsToSend.push(vulnCopy); | |
810 | }); | |
811 | if(vulnsToSend.length > 1) { | |
812 | self.vulnModelsManager.bulkCreate(vulnsToSend).then( | |
813 | function(response) { | |
814 | var message = _saveAsModelMessage(response.data) | |
815 | commonsFact.showMessage(message, true); | |
816 | }, function(response) { | |
817 | var message = "Error creating vulnerability templates.\n\n"; | |
818 | if(response.status === 400 && response.data.message) | |
819 | message += response.data.message; | |
820 | else | |
821 | message += _saveAsModelMessage(response.data); | |
822 | commonsFact.showMessage(message); | |
823 | } | |
824 | ); | |
825 | } else { | |
826 | self.vulnModelsManager.create(vulnsToSend[0], true).then( | |
827 | function(vuln) { | |
828 | var message = "The following vulnerability was created as template:\n\n"; | |
829 | message += "\tId: " + vuln.id.toString() + ". " + "Name: " + vuln.name; | |
830 | commonsFact.showMessage(message, true); | |
831 | }, function(response) { | |
832 | commonsFact.showMessage(response); | |
833 | } | |
834 | ); | |
835 | } | |
816 | 836 | } catch(err) { |
817 | 837 | commonsFact.showMessage("Something failed when creating some of the templates."); |
818 | 838 | } |
839 | }; | |
840 | ||
841 | var _saveAsModelMessage = function(data) { | |
842 | var message = ""; | |
843 | var vulnsCreated = data.vulns_created; | |
844 | if(vulnsCreated.length > 0) { | |
845 | message += "The following vulnerabilities were created as templates:\n"; | |
846 | vulnsCreated.forEach(function (vuln) { | |
847 | if (vuln[0]) | |
848 | message += "\n\tId: " + vuln[0].toString() + ". " | |
849 | message += "Name: " + vuln[1] | |
850 | }) | |
851 | message += "\n\n" | |
852 | } | |
853 | ||
854 | var vulnsWithErrors = data.vulns_with_errors; | |
855 | if(vulnsWithErrors.length > 0) { | |
856 | message += "The following vulnerabilities couldn't be created as templates:\n"; | |
857 | vulnsWithErrors.forEach(function (vuln) { | |
858 | if (vuln[0]) | |
859 | message += "\n\tId: " + vuln[0].toString() + ". " | |
860 | message += "Name: " + vuln[1] | |
861 | }) | |
862 | message += "\n\n" | |
863 | } | |
864 | ||
865 | var vulnsWithConflict = data.vulns_with_conflict; | |
866 | if(vulnsWithConflict.length > 0) { | |
867 | message += "The following vulnerabilities generated conflicts when Faraday tried " + | |
868 | "to create them as templates, this means that their vulnerability " + | |
869 | "templates already exist:\n"; | |
870 | vulnsWithConflict.forEach(function (vuln) { | |
871 | if (vuln[0]) | |
872 | message += "\n\tId: " + vuln[0].toString() + ". "; | |
873 | message += "Name: " + vuln[1]; | |
874 | }) | |
875 | message += "\n\n" | |
876 | } | |
877 | return message | |
819 | 878 | }; |
820 | 879 | |
821 | 880 | $scope.selectAll = function() { |
34 | 34 | <div ng-if="cf.field_type === \'choice\'"> \n\ |
35 | 35 | <div class="tab-pane-header">{{cf.field_display_name}}</div> \n\ |
36 | 36 | <div class="btn-group col-md-6 col-sm-6 col-xs-6 btn-cf-choice" ng-if="cf.field_type === \'choice\'"> \n\ |
37 | <button type = "button" class="dropdown-toggle btn-change-property primary-btn btn-primary-white" data-toggle = "dropdown" id="btn-chg-choice" title="Choices">\n\ | |
37 | <button type = "button" class="dropdown-toggle btn-change-property primary-btn btn-primary-white no-overflow" data-toggle = "dropdown" id="btn-chg-choice" title="Choices">\n\ | |
38 | 38 | <span ng-if="modal.data.custom_fields[cf.field_name] !== null">{{modal.data.custom_fields[cf.field_name]}}</span>\n\ |
39 | 39 | <span ng-if="modal.data.custom_fields[cf.field_name] === null">Select {{cf.field_display_name}}</span>\n\ |
40 | 40 | </button>\n\ |
43 | 43 | </button> \n\ |
44 | 44 | <ul class="dropdown-menu dropdown-menu-right col-md-12 dropd-cf-choice" role="menu"> \n\ |
45 | 45 | <li ng-repeat="choice in parserOptions(cf.field_metadata)">\n\ |
46 | <a class="ws" ng-click="modal.data.custom_fields[cf.field_name] = choice">{{choice}}</a> \n\ | |
46 | <a class="ws no-overflow" ng-click="modal.data.custom_fields[cf.field_name] = choice">{{choice}}</a> \n\ | |
47 | 47 | </li>\n\ |
48 | 48 | </ul>\n\ |
49 | 49 | </div> \n\ |
36 | 36 | <div ng-if="cf.field_type === \'choice\'"> \n\ |
37 | 37 | <div class="tab-pane-header"><i class="fa fa-spinner fa-spin" ng-show="isUpdatingVuln === true && fieldToEdit === cf.field_name"></i> {{cf.field_display_name}}</div> \n\ |
38 | 38 | <div class="btn-group col-md-6 col-sm-6 col-xs-6 btn-cf-choice" ng-if="cf.field_type === \'choice\'"> \n\ |
39 | <button type="button" class="dropdown-toggle btn-change-property primary-btn btn-primary-white" data-toggle = "dropdown" id = "btn-chg-choice" title = "Choices">\n\ | |
39 | <button type="button" class="dropdown-toggle btn-change-property primary-btn btn-primary-white no-overflow" data-toggle = "dropdown" id = "btn-chg-choice" title = "Choices">\n\ | |
40 | 40 | <span ng-if="lastClickedVuln.custom_fields[cf.field_name] !== null" > {{lastClickedVuln.custom_fields[cf.field_name]}}</span>\n\ |
41 | 41 | <span ng-if="lastClickedVuln.custom_fields[cf.field_name] === null">Select {{cf.field_display_name}}</span> \n\ |
42 | 42 | </button> \n\ |
45 | 45 | </button> \n\ |
46 | 46 | <ul class="dropdown-menu dropdown-menu-right col-md-12 dropd-cf-choice" role="menu"> \n\ |
47 | 47 | <li ng-repeat="choice in parserOptions(cf.field_metadata)"> \n\ |
48 | <a class="ws" href="javascript:;" ng-click="onChangeChoiceCf(choice)">{{choice}}</a> \n\ | |
48 | <a class="ws no-overflow" href="javascript:;" ng-click="onChangeChoiceCf(choice)">{{choice}}</a> \n\ | |
49 | 49 | </li> \n\ |
50 | 50 | </ul> \n\ |
51 | 51 | </div> \n\ |
44 | 44 | $scope.data.policyviolations = angular.copy($scope.policyviolations); |
45 | 45 | $scope.data.refs = angular.copy($scope.references); |
46 | 46 | $scope.data.references = $scope.data.refs.join(','); |
47 | $scope.customFields.forEach(function(cf){ | |
48 | if(cf.value){ | |
49 | $scope.data.customfields[cf.field_name] = cf.value; | |
50 | } | |
51 | }) | |
52 | ||
47 | for (const fieldName in $scope.data.customfields) { | |
48 | $scope.customFields.forEach(function(cf){ | |
49 | if(cf.field_name === fieldName){ | |
50 | cf.value = $scope.data.customfields[fieldName]; | |
51 | } | |
52 | }) | |
53 | } | |
53 | 54 | |
54 | 55 | $modalInstance.close($scope.data); |
55 | 56 | }; |
46 | 46 | $scope.data.model = $scope.other_model; |
47 | 47 | } |
48 | 48 | |
49 | $scope.customFields.forEach(function(cf){ | |
50 | if(cf.value){ | |
51 | $scope.data.customfields[cf.field_name] = cf.value; | |
52 | } | |
53 | }) | |
49 | for (const fieldName in $scope.data.customfields) { | |
50 | $scope.customFields.forEach(function(cf){ | |
51 | if(cf.field_name === fieldName){ | |
52 | cf.value = $scope.data.customfields[fieldName]; | |
53 | } | |
54 | }) | |
55 | } | |
54 | 56 | |
55 | 57 | if ($scope.data.easeofresolution === ""){ |
56 | 58 | $scope.data.easeofresolution = null; |
163 | 163 | }).then( |
164 | 164 | function(d) { |
165 | 165 | $scope.loading = false; |
166 | commonsFact.showMessage("Vulnerability templates created: " + d.data.vulns_created + " Vulnerability templates with error: " + d.data.vulns_with_errors + "", true); | |
166 | var message = "Vulnerability templates created:\n"; | |
167 | d.data.vulns_created.forEach(function(vuln) { | |
168 | message += "\t" + vuln[1] + "\n"; | |
169 | }); | |
170 | ||
171 | message += "\n\n"; | |
172 | message += loadCSVErrorMessage(d.data); | |
173 | ||
174 | commonsFact.showMessage(message, true); | |
167 | 175 | $route.reload(); |
168 | 176 | }, |
169 | 177 | function(d){ |
170 | 178 | $scope.loading = false; |
171 | commonsFact.showMessage("Error uploading vulnerability templates"); | |
179 | var message = "Error uploading vulnerability templates.\n\n"; | |
180 | if(d.status === 400 && d.data.message) | |
181 | message += d.data.message; | |
182 | else | |
183 | message += loadCSVErrorMessage(d.data) | |
184 | commonsFact.showMessage(message); | |
172 | 185 | } |
173 | 186 | ); |
174 | 187 | } |
175 | 188 | ); |
189 | }; | |
190 | ||
191 | var loadCSVErrorMessage = function(data) { | |
192 | var message = ""; | |
193 | if(data.vulns_with_conflict.length > 0) { | |
194 | message += "Vulnerability templates that were " + | |
195 | "not created due to conflict error:\n"; | |
196 | data.vulns_with_conflict.forEach(function(vuln) { | |
197 | message += "\t" + vuln[1] + "\n"; | |
198 | }); | |
199 | } | |
200 | message += "\n\n"; | |
201 | ||
202 | if(data.vulns_with_errors.length > 0) { | |
203 | message += "Vulnerability templates that were " + | |
204 | "not create due to an error:\n"; | |
205 | data.vulns_with_errors.forEach(function(vuln) { | |
206 | message += "\t" + vuln[1] + "\n"; | |
207 | }); | |
208 | } | |
209 | ||
210 | return message; | |
176 | 211 | }; |
177 | 212 | |
178 | 213 | modal.result.then(function(data) { |
0 | // Faraday Penetration Test IDE | |
1 | // Copyright (C) 2018 Infobyte LLC (http://www.infobytesec.com/) | |
2 | // See the file 'doc/LICENSE' for the license information | |
3 | ||
4 | angular.module('faradayApp') | |
5 | .directive('customFieldsVulnDb', [function () { | |
6 | return { | |
7 | restrict: 'E', | |
8 | scope: true, | |
9 | replace: true, | |
10 | template: '<div>\ | |
11 | <div ng-if="cf.field_type === \'str\'" ng-init="isEditable = true"> \n\ | |
12 | <div class="tab-pane-header" ng-dblclick="isEditable = true" title="Double click to edit">{{cf.field_display_name}} <span class="glyphicon glyphicon-question-sign" title="Edit using markdown code"></span></div> \n\ | |
13 | <div class="form-group"> \n\ | |
14 | <label class="sr-only" for="{{cf.field_name}}">{{cf.field_display_name}}</label> \n\ | |
15 | <textarea class="form-control" rows="5" id="vuln-desc" name="desc" ng-show="isEditable === true" \n\ | |
16 | ng-model="data.customfields[cf.field_name]" ng-bind-html="data.customfields[cf.field_name] | markdown" \n\ | |
17 | style="margin: 0 2px 0 0;" ng-blur="isEditable = isEditable.length==0 || !data.customfields[cf.field_name]" autofocus> \n\ | |
18 | </textarea> \n\ | |
19 | <div class="col-md-12" ng-cloak ng-show="data.customfields[cf.field_name].length > 0 && isEditable === false"> \n\ | |
20 | <div class="markdown-preview" style="height: 100px;!important;" ng-bind-html="data.customfields[cf.field_name] | markdown" ng-dblclick="isEditable = true">{{data.customfields[cf.field_name] | markdown}}</div> \n\ | |
21 | </div> \n\ | |
22 | </div> \n\ | |
23 | </div> \n\ | |
24 | <div ng-if="cf.field_type === \'int\'"> \n\ | |
25 | <div class="tab-pane-header">{{cf.field_display_name}}</div> \n\ | |
26 | <div class="form-group"> \n\ | |
27 | <label class="sr-only" for="{{cf.field_name}}">{{cf.field_display_name}}</label> \n\ | |
28 | <input type="text" class="form-control input-sm" id="{{cf.field_name}}" name="{{cf.field_name}}" \n\ | |
29 | placeholder="{{cf.field_display_name}}" \n\ | |
30 | ng-model="data.customfields[cf.field_name]" check-custom-type="{{cf.field_type}}" \n\ | |
31 | uib-tooltip="Type only numbers"/> \n\ | |
32 | </div> \n\ | |
33 | </div> \n\ \ | |
34 | <div ng-if="cf.field_type === \'choice\'"> \n\ | |
35 | <div class="tab-pane-header">{{cf.field_display_name}}</div> \n\ | |
36 | <div class="btn-group col-md-6 col-sm-6 col-xs-6 btn-cf-choice" ng-if="cf.field_type === \'choice\'"> \n\ | |
37 | <button type = "button" class="dropdown-toggle btn-change-property primary-btn btn-primary-white no-overflow" data-toggle = "dropdown" id="btn-chg-choice" title="Choices">\n\ | |
38 | <span ng-if="data.customfields[cf.field_name] !== null">{{data.customfields[cf.field_name]}}</span>\n\ | |
39 | <span ng-if="data.customfields[cf.field_name] === null">Select {{cf.field_display_name}}</span>\n\ | |
40 | </button>\n\ | |
41 | <button type="button" class="dropdown-toggle secondary-btn btn-change-property btn-secondary-white" data-toggle="dropdown" id="caret-choice" title="Choices">\n\ | |
42 | <span> <i class="fa fa-angle-down fa-lg" aria-hidden="true"></i> </span> \n\ | |
43 | </button> \n\ | |
44 | <ul class="dropdown-menu dropdown-menu-right col-md-12 dropd-cf-choice" role="menu"> \n\ | |
45 | <li ng-repeat="choice in parserOptions(cf.field_metadata)">\n\ | |
46 | <a class="ws no-overflow" ng-click="data.customfields[cf.field_name] = choice">{{choice}}</a> \n\ | |
47 | </li>\n\ | |
48 | </ul>\n\ | |
49 | </div> \n\ | |
50 | </div> \n\ | |
51 | <div ng-if="cf.field_type === \'list\'"> \n\ | |
52 | <div class="tab-pane-header">{{cf.field_display_name}}</div> \n\ | |
53 | <div class="form-group" ng-class="data.customfields[cf.field_name].length > 0 ? \'no-margin-bottom\' : \'\'"> \n\ | |
54 | <div class="input-group"> \n\ | |
55 | <label class="sr-only" for="{{cf.field_name}}">{{cf.field_display_name}}</label> \n\ | |
56 | <input type="text" class="form-control input-sm" id="{{cf.field_name}}_list" name="{{cf.field_name}}" \n\ | |
57 | placeholder="{{cf.field_display_name}}" \n\ | |
58 | ng-model="valueField" \n\ | |
59 | uib-tooltip="Input type list"/> \n\ | |
60 | <span class="input-group-addon cursor" ng-click="newValueField(valueField)"><i class="fa fa-plus-circle"></i></span> \n\ | |
61 | </div> \n\ | |
62 | </div> \n\ | |
63 | <div class="reference" ng-repeat="item in data.customfields[cf.field_name] track by $index" ng-class="{\'last-item-field\':$last}" ng-if="cf.field_type === \'list\'"> \n\ | |
64 | <div class="input-group margin-bottom-sm"> \n\ | |
65 | <label class="sr-only" for="vuln-refs-create">{{cf.field_display_name}}</label> \n\ | |
66 | <input ng-if="item.value" type="text" class="form-control input-sm" id="vuln-refs-create" placeholder="{{cf.field_display_name}}" \n\ | |
67 | ng-model="item.value" \n\ | |
68 | role="button" readonly/> \n\ | |
69 | <input ng-if="!item.value" type="text" class="form-control input-sm" id="vuln-refs-create" placeholder="{{cf.field_display_name}}" \n\ | |
70 | ng-model="item" \n\ | |
71 | role="button" readonly/> \n\ | |
72 | <span class="input-group-addon cursor" ng-click="data.customfields[cf.field_name].splice($index, 1)"> \n\ | |
73 | <i class="fa fa-minus-circle"></i></span> \n\ | |
74 | </div> \n\ | |
75 | </div> \n\ | |
76 | </div> \n\ | |
77 | </div></div>', | |
78 | link: function (scope, element, attrs) { | |
79 | ||
80 | scope.newValueField = function (valueField) { | |
81 | if (valueField !== "" && valueField !== undefined) { | |
82 | if(scope.data.customfields[scope.cf.field_name] == null ) | |
83 | scope.data.customfields[scope.cf.field_name] = []; | |
84 | ||
85 | // we need to check if the ref already exists | |
86 | if (scope.data.customfields[scope.cf.field_name].filter(function(field) {return field.value === valueField}).length === 0) { | |
87 | scope.data.customfields[scope.cf.field_name].push({value: valueField}); | |
88 | scope.valueField = ""; | |
89 | } | |
90 | angular.element('#'+scope.cf.field_name+'_list').val(""); | |
91 | } | |
92 | } | |
93 | ||
94 | scope.parserOptions = function (rawOptions) { | |
95 | return JSON.parse(rawOptions) | |
96 | } | |
97 | } | |
98 | } | |
99 | }]); |
143 | 143 | </p> |
144 | 144 | </div> |
145 | 145 | <div class="col-md-12 margin-bottom-15px"> |
146 | <div class="col-md-12" ng-repeat="cf in customFields | orderBy : 'field_order'"> | |
147 | <!--<custom-field field="{{cf}}"></custom-field>--> | |
148 | <div class="col-md-6"> | |
149 | <label for="model">{{cf.field_display_name}}</label> | |
150 | <input type="text" name="name" class="form-control" placeholder="Name" ng-model="cf.value"> | |
151 | </div> | |
152 | </div> | |
146 | <div class="col-md-12" ng-repeat="cf in customFields | orderBy : 'field_order'"> | |
147 | <custom-fields-vuln-db field="{{cf}}"></custom-fields-vuln-db> | |
153 | 148 | </div> |
149 | </div> | |
154 | 150 | </div> |
155 | 151 | </div> |
156 | 152 | </div><!-- .form-horizontal --> |
149 | 149 | </div> |
150 | 150 | <div class="col-md-12 margin-bottom-15px"> |
151 | 151 | <div class="col-md-12" ng-repeat="cf in customFields | orderBy : 'field_order'"> |
152 | <!--<custom-field field="{{cf}}"></custom-field>--> | |
153 | <div class="col-md-6"> | |
154 | <label for="model">{{cf.field_display_name}}</label> | |
155 | <input type="text" name="name" class="form-control" placeholder="Name" ng-model="cf.value"> | |
156 | </div> | |
152 | <custom-fields-vuln-db field="{{cf}}"></custom-fields-vuln-db> | |
157 | 153 | </div> |
158 | 154 | </div> |
159 | 155 | </div> |
113 | 113 | } |
114 | 114 | var message; |
115 | 115 | if (res.status == 409) { |
116 | message = "Vulnerability template already exists. " + res.data.message + " ID: " + res.data.object._id; | |
116 | message = "Vulnerability template \"" + res.data.object.name + | |
117 | "\" already exists with vulnerability template ID: " + res.data.object.id; | |
117 | 118 | } else { |
118 | 119 | message = "Unable to save the Vuln Model. " + msg; |
119 | 120 | } |
9 | 9 | vulnModelsManager.models = []; |
10 | 10 | vulnModelsManager.totalNumberOfModels = 0; |
11 | 11 | |
12 | vulnModelsManager.create = function(data) { | |
12 | vulnModelsManager.create = function(data) { | |
13 | 13 | var deferred = $q.defer(); |
14 | 14 | var self = this; |
15 | 15 | try { |
27 | 27 | |
28 | 28 | return deferred.promise; |
29 | 29 | }; |
30 | ||
31 | ||
32 | vulnModelsManager.bulkCreate = function(vulns){ | |
33 | var deferred = $q.defer(); | |
34 | var self = this; | |
35 | $http.get(BASEURL + '_api/session') | |
36 | .then(function(d) { | |
37 | var dataToSend = {} | |
38 | dataToSend.csrf_token = d.data.csrf_token; | |
39 | dataToSend.vulns = vulns; | |
40 | ServerAPI.bulkCreateVulnerabilityTemplate(dataToSend) | |
41 | .then(function (response) { | |
42 | var vulnsCreated = response.data.vulns_created; | |
43 | self.updateState(self.totalNumberOfModels + vulnsCreated.length); | |
44 | deferred.resolve(response); | |
45 | }, function (response) { | |
46 | deferred.reject(response) | |
47 | }) | |
48 | }) | |
49 | return deferred.promise; | |
50 | } | |
30 | 51 | |
31 | 52 | vulnModelsManager.delete = function(vulnModel) { |
32 | 53 | var deferred = $q.defer(); |
57 | 57 | <th class="ui-grid-cell-contents ui-grid-header-cell">Vulns</th> |
58 | 58 | <th class="ui-grid-cell-contents ui-grid-header-cell">Hosts</th> |
59 | 59 | <th class="ui-grid-cell-contents ui-grid-header-cell">Services</th> |
60 | <th class="ui-grid-cell-contents ui-grid-header-cell">Active Agents</th> | |
60 | 61 | <th class="ui-grid-cell-contents ui-grid-header-cell">Last Modified</th> |
61 | 62 | <th class="ui-grid-cell-contents ui-grid-header-cell">Active</th> |
62 | 63 | <th class="ui-grid-cell-contents ui-grid-header-cell">Read only</th> |
77 | 78 | <td class="ui-grid-cell-contents"><a href="#/status/ws/{{ws.name}}">{{objects[ws.name]['total_vulns']}}</a></td> |
78 | 79 | <td class="ui-grid-cell-contents"><a href="#/hosts/ws/{{ws.name}}">{{objects[ws.name]['hosts']}}</a></td> |
79 | 80 | <td class="ui-grid-cell-contents" ng-bind="objects[ws.name]['services']"></td> |
81 | <td class="ui-grid-cell-contents" ng-bind="ws.active_agents_count"></td> | |
80 | 82 | <td class="ui-grid-cell-contents" ng-bind="ws.update_date | amTimeAgo"></td> |
81 | 83 | <td class="ui-grid-cell-contents active-toggle"> |
82 | 84 | <div class="toogle-img-container"> |
2 | 2 | # See the file 'doc/LICENSE' for the license information |
3 | 3 | import os |
4 | 4 | import sys |
5 | import glob | |
6 | 5 | import socket |
7 | 6 | import argparse |
7 | import logging | |
8 | 8 | |
9 | 9 | from alembic.runtime.migration import MigrationContext |
10 | 10 | |
19 | 19 | from alembic.script import ScriptDirectory |
20 | 20 | from alembic.config import Config |
21 | 21 | |
22 | logger = faraday.server.utils.logger.get_logger(faraday.server.utils.logger.ROOT_LOGGER) | |
22 | logger = logging.getLogger(__name__) | |
23 | 23 | |
24 | 24 | init() |
25 | 25 | |
34 | 34 | def is_server_running(port): |
35 | 35 | pid = daemonize.is_server_running(port) |
36 | 36 | if pid is not None: |
37 | logger.warn("Faraday Server is already running. PID: {}".format(pid)) | |
37 | logger.warning(f"Faraday Server is already running. PID: {pid}") | |
38 | 38 | return True |
39 | 39 | else: |
40 | 40 | return False |
50 | 50 | with app.app_context(): |
51 | 51 | try: |
52 | 52 | if not db.session.query(Workspace).count(): |
53 | logger.warn('No workspaces found') | |
53 | logger.warning('No workspaces found') | |
54 | 54 | except sqlalchemy.exc.ArgumentError: |
55 | 55 | logger.error( |
56 | '\n\b{RED}Please check your PostgreSQL connection string in the file ~/.faraday/config/server.ini on your home directory.{WHITE} \n'.format(RED=Fore.RED, WHITE=Fore.WHITE) | |
56 | f'\n{Fore.RED}Please check your PostgreSQL connection string in the file ~/.faraday/config/server.ini on your home directory.{Fore.WHITE} \n' | |
57 | 57 | ) |
58 | 58 | sys.exit(1) |
59 | 59 | except sqlalchemy.exc.OperationalError: |
62 | 62 | sys.exit(1) |
63 | 63 | except sqlalchemy.exc.ProgrammingError: |
64 | 64 | logger.error( |
65 | '\n\nn{WHITE}Missing migrations, please execute: \n\nfaraday-manage migrate'.format(WHITE=Fore.WHITE)) | |
65 | f'\n\nn{Fore.WHITE}Missing migrations, please execute: \n\nfaraday-manage migrate') | |
66 | 66 | sys.exit(1) |
67 | 67 | |
68 | 68 | |
87 | 87 | |
88 | 88 | current_revision = context.get_current_revision() |
89 | 89 | if head_revision != current_revision: |
90 | if glob.glob(os.path.join(faraday.server.config.FARADAY_BASE, 'migrations', 'versions', | |
91 | '{}_*.py'.format(current_revision))): | |
90 | version_path = faraday.server.config.FARADAY_BASE / 'migrations'\ | |
91 | / 'versions' | |
92 | if list(version_path.glob(f'{current_revision}_*.py')): | |
92 | 93 | print('--' * 20) |
93 | 94 | print('Missing migrations, please execute: \n\n') |
94 | 95 | print('faraday-manage migrate') |
104 | 105 | def main(): |
105 | 106 | os.chdir(faraday.server.config.FARADAY_BASE) |
106 | 107 | check_alembic_version() |
108 | # TODO RETURN TO prev CWD | |
107 | 109 | check_postgresql() |
108 | 110 | parser = argparse.ArgumentParser() |
109 | 111 | parser.add_argument('--debug', action='store_true', help='run Faraday Server in debug mode') |
113 | 115 | parser.add_argument('--websocket_port', help='Overides server.ini websocket port configuration') |
114 | 116 | parser.add_argument('--bind_address', help='Overides server.ini bind_address configuration') |
115 | 117 | f_version = faraday.__version__ |
116 | parser.add_argument('-v', '--version', action='version', version='Faraday v{version}'.format(version=f_version)) | |
118 | parser.add_argument('-v', '--version', action='version', version=f'Faraday v{f_version}') | |
117 | 119 | args = parser.parse_args() |
118 | 120 | if args.debug or faraday.server.config.faraday_server.debug: |
119 | 121 | faraday.server.utils.logger.set_logging_level(faraday.server.config.DEBUG) |
65 | 65 | |
66 | 66 | |
67 | 67 | """ |
68 | import os | |
68 | 69 | import re |
69 | ||
70 | import logging | |
70 | 71 | from flask import current_app |
71 | 72 | |
72 | 73 | from apispec import BasePlugin, yaml_utils |
78 | 79 | |
79 | 80 | RE_URL = re.compile(r"<(?:[^:<>]+:)?([^<>]+)>") |
80 | 81 | |
82 | logger = logging.getLogger(__name__) | |
81 | 83 | |
82 | 84 | class FaradayAPIPlugin(BasePlugin): |
83 | 85 | """APISpec plugin for Flask""" |
117 | 119 | return self.flaskpath2openapi(rule.rule) |
118 | 120 | view_instance = next(cl.cell_contents for cl in view.__closure__ if isinstance(cl.cell_contents, GenericView)) |
119 | 121 | if view_name in ['get', 'put', 'post', 'delete']: |
120 | ||
121 | 122 | if view.__doc__: |
122 | 123 | if hasattr(view_instance.model_class, "__name__"): |
123 | 124 | class_model = view_instance.model_class.__name__ |
124 | 125 | else: |
125 | 126 | class_model = 'No name' |
127 | logger.debug(f'{view_name} / {class_model} / {rule.methods} / {view_name} / {view_instance._get_schema_class().__name__}') | |
126 | 128 | operations[view_name] = yaml_utils.load_yaml_from_docstring( |
127 | 129 | view.__doc__.format(schema_class=view_instance._get_schema_class().__name__, class_model=class_model, tag_name=class_model) |
128 | 130 | ) |
129 | ||
131 | elif hasattr(view, "__doc__"): | |
132 | if not view.__doc__: | |
133 | view.__doc__ = "" | |
134 | if hasattr(view_instance.model_class, "__name__"): | |
135 | class_model = view_instance.model_class.__name__ | |
136 | else: | |
137 | class_model = 'No name' | |
138 | for method in rule.methods: | |
139 | logger.debug(f'{view_name} / {class_model} / {rule.methods} / {method} / {view_instance._get_schema_class().__name__}') | |
140 | if method not in ['HEAD', 'OPTIONS'] or os.environ.get("FULL_API_DOC", None): | |
141 | operations[method.lower()] = yaml_utils.load_yaml_from_docstring( | |
142 | view.__doc__.format(schema_class=view_instance._get_schema_class().__name__, class_model=class_model, tag_name=class_model) | |
143 | ) | |
130 | 144 | if hasattr(view, "view_class") and issubclass(view.view_class, MethodView): |
131 | 145 | for method in view.methods: |
132 | 146 | if method in rule.methods: |
135 | 149 | operations[method_name] = yaml_utils.load_yaml_from_docstring( |
136 | 150 | method.__doc__ |
137 | 151 | ) |
152 | ||
138 | 153 | return self.flaskpath2openapi(rule.rule) |
0 | import logging | |
1 | import smtplib | |
2 | import ssl | |
3 | from email.mime.multipart import MIMEMultipart | |
4 | from email.mime.text import MIMEText | |
5 | ||
6 | from faraday.server.config import smtp | |
7 | ||
8 | logger = logging.getLogger(__name__) | |
9 | ||
10 | ||
11 | class MailNotification: | |
12 | def __init__(self, smtp_host: str, smtp_sender: str, | |
13 | smtp_username: str = None, smtp_password: str = None, | |
14 | smtp_port: int = 0, smtp_ssl: bool = False): | |
15 | self.smtp_username = smtp_username or smtp.username | |
16 | self.smtp_sender = smtp_sender or smtp.sender | |
17 | self.smtp_password = smtp_password or smtp.password | |
18 | self.smtp_host = smtp_host or smtp.host | |
19 | self.smtp_port = smtp_port or smtp.port | |
20 | if smtp.keyfile is not None and smtp.certfile is not None: | |
21 | self.smtp_ssl = True | |
22 | self.smtp_keyfile = smtp.keyfile | |
23 | self.smtp_certfile = smtp.certfile | |
24 | else: | |
25 | self.smtp_ssl = smtp_ssl or smtp.ssl | |
26 | self.smtp_keyfile = None | |
27 | self.smtp_certfile = None | |
28 | ||
29 | def send_mail(self, to_addr: str, subject: str, body: str): | |
30 | msg = MIMEMultipart() | |
31 | msg['From'] = self.smtp_sender | |
32 | msg['To'] = to_addr | |
33 | msg['Subject'] = subject | |
34 | ||
35 | msg.attach(MIMEText(body, 'plain')) | |
36 | SMTP = smtplib.SMTP | |
37 | try: | |
38 | with SMTP(host=self.smtp_host, port=self.smtp_port) as server_mail: | |
39 | if self.smtp_ssl: | |
40 | server_mail.starttls(keyfile=smtp.keyfile, | |
41 | certfile=smtp.certfile) | |
42 | if self.smtp_username and self.smtp_password: | |
43 | server_mail.login(self.smtp_username, self.smtp_password) | |
44 | text = msg.as_string() | |
45 | server_mail.sendmail(msg['From'], msg['To'], text) | |
46 | except (smtplib.SMTPException, ssl.SSLError) as error: | |
47 | logger.error("Error: unable to send email") | |
48 | logger.exception(error) |
1 | 1 | # If you run pynixify again, the file will be either overwritten or |
2 | 2 | # deleted, and you will lose the changes you made to it. |
3 | 3 | |
4 | { overlays ? [ ], ... }@args: | |
4 | { overlays ? | |
5 | [ ] | |
6 | , ... | |
7 | }@args: | |
5 | 8 | let |
6 | pynixifyOverlay = self: super: { | |
7 | python2 = super.python2.override { inherit packageOverrides; }; | |
8 | python27 = super.python27.override { inherit packageOverrides; }; | |
9 | python3 = super.python3.override { inherit packageOverrides; }; | |
10 | python35 = super.python35.override { inherit packageOverrides; }; | |
11 | python36 = super.python36.override { inherit packageOverrides; }; | |
12 | python37 = super.python37.override { inherit packageOverrides; }; | |
13 | python38 = super.python38.override { inherit packageOverrides; }; | |
14 | }; | |
9 | pynixifyOverlay = | |
10 | self: super: { | |
11 | python2 = | |
12 | super.python2.override { | |
13 | inherit | |
14 | packageOverrides; | |
15 | }; | |
16 | python27 = | |
17 | super.python27.override { | |
18 | inherit | |
19 | packageOverrides; | |
20 | }; | |
21 | python3 = | |
22 | super.python3.override { | |
23 | inherit | |
24 | packageOverrides; | |
25 | }; | |
26 | python35 = | |
27 | super.python35.override { | |
28 | inherit | |
29 | packageOverrides; | |
30 | }; | |
31 | python36 = | |
32 | super.python36.override { | |
33 | inherit | |
34 | packageOverrides; | |
35 | }; | |
36 | python37 = | |
37 | super.python37.override { | |
38 | inherit | |
39 | packageOverrides; | |
40 | }; | |
41 | python38 = | |
42 | super.python38.override { | |
43 | inherit | |
44 | packageOverrides; | |
45 | }; | |
46 | }; | |
15 | 47 | |
16 | 48 | nixpkgs = |
17 | 49 | |
18 | 50 | builtins.fetchTarball { |
19 | 51 | url = |
20 | "https://github.com/infobyte/nixpkgs/archive/6edef530cb277f73587bfc7cb8ef17175abd306d.tar.gz"; | |
21 | sha256 = "034d88hgni68bghpbrmakgbiyrr8m0zwlgag6a0zqpzf6pdl818x"; | |
52 | "https://github.com/infobyte/nixpkgs/archive/98720fe237de55ca5779af5ee07407d0947b8deb.tar.gz"; | |
53 | sha256 = | |
54 | "1zfc84xg7xa70v3gfqn1wgzq0rn8fwna9bmmyi9720vs0bzkdj86"; | |
22 | 55 | }; |
23 | 56 | |
24 | packageOverrides = self: super: { | |
25 | anyascii = self.callPackage ./packages/anyascii { }; | |
57 | packageOverrides = | |
58 | self: super: { | |
59 | anyascii = | |
60 | self.callPackage | |
61 | ./packages/anyascii | |
62 | { }; | |
26 | 63 | |
27 | apispec-webframeworks = | |
28 | self.callPackage ./packages/apispec-webframeworks { }; | |
64 | apispec-webframeworks = | |
65 | self.callPackage | |
66 | ./packages/apispec-webframeworks | |
67 | { }; | |
29 | 68 | |
30 | faraday-plugins = self.callPackage ./packages/faraday-plugins { }; | |
69 | faraday-plugins = | |
70 | self.callPackage | |
71 | ./packages/faraday-plugins | |
72 | { }; | |
31 | 73 | |
32 | faradaysec = self.callPackage ./packages/faradaysec { }; | |
74 | faradaysec = | |
75 | self.callPackage | |
76 | ./packages/faradaysec | |
77 | { }; | |
33 | 78 | |
34 | filedepot = self.callPackage ./packages/filedepot { }; | |
79 | filedepot = | |
80 | self.callPackage | |
81 | ./packages/filedepot | |
82 | { }; | |
35 | 83 | |
36 | filteralchemy-fork = self.callPackage ./packages/filteralchemy-fork { }; | |
84 | filteralchemy-fork = | |
85 | self.callPackage | |
86 | ./packages/filteralchemy-fork | |
87 | { }; | |
37 | 88 | |
38 | flask-classful = self.callPackage ./packages/flask-classful { }; | |
89 | flask-classful = | |
90 | self.callPackage | |
91 | ./packages/flask-classful | |
92 | { }; | |
39 | 93 | |
40 | flask-kvsession-fork = self.callPackage ./packages/flask-kvsession-fork { }; | |
94 | flask-kvsession-fork = | |
95 | self.callPackage | |
96 | ./packages/flask-kvsession-fork | |
97 | { }; | |
41 | 98 | |
42 | flask-login = self.callPackage ./packages/flask-login { }; | |
99 | flask-security = | |
100 | self.callPackage | |
101 | ./packages/flask-security | |
102 | { }; | |
43 | 103 | |
44 | flask-security = self.callPackage ./packages/flask-security { }; | |
104 | simplekv = | |
105 | self.callPackage | |
106 | ./packages/simplekv | |
107 | { }; | |
45 | 108 | |
46 | nplusone = self.callPackage ./packages/nplusone { }; | |
109 | syslog-rfc5424-formatter = | |
110 | self.callPackage | |
111 | ./packages/syslog-rfc5424-formatter | |
112 | { }; | |
47 | 113 | |
48 | pytest-factoryboy = self.callPackage ./packages/pytest-factoryboy { }; | |
114 | webargs = | |
115 | self.callPackage | |
116 | ./packages/webargs | |
117 | { }; | |
49 | 118 | |
50 | simplekv = self.callPackage ./packages/simplekv { }; | |
119 | }; | |
51 | 120 | |
52 | syslog-rfc5424-formatter = | |
53 | self.callPackage ./packages/syslog-rfc5424-formatter { }; | |
54 | ||
55 | webargs = self.callPackage ./packages/webargs { }; | |
56 | ||
57 | werkzeug = self.callPackage ./packages/werkzeug { }; | |
58 | ||
59 | }; | |
60 | ||
61 | in import nixpkgs (args // { overlays = [ pynixifyOverlay ] ++ overlays; }) | |
121 | in import | |
122 | nixpkgs | |
123 | (args | |
124 | // { | |
125 | overlays = | |
126 | [ | |
127 | pynixifyOverlay | |
128 | ] | |
129 | ++ overlays; | |
130 | }) |
1 | 1 | # If you run pynixify again, the file will be either overwritten or |
2 | 2 | # deleted, and you will lose the changes you made to it. |
3 | 3 | |
4 | { buildPythonPackage, fetchPypi, lib }: | |
4 | { buildPythonPackage | |
5 | , fetchPypi | |
6 | , lib | |
7 | }: | |
5 | 8 | |
6 | 9 | buildPythonPackage rec { |
7 | pname = "anyascii"; | |
8 | version = "0.1.6"; | |
10 | pname = | |
11 | "anyascii"; | |
12 | version = | |
13 | "0.1.7"; | |
9 | 14 | |
10 | src = fetchPypi { | |
11 | inherit pname version; | |
12 | sha256 = "112z1jlqngcqdnpb7amb1r2yvd4n0h1748jjsfsy35qx3y32ij6r"; | |
13 | }; | |
15 | src = | |
16 | fetchPypi { | |
17 | inherit | |
18 | pname | |
19 | version; | |
20 | sha256 = | |
21 | "1xcrhmgpv8da34sg62r0yfxzyq2kwgiaardkih9z3sm96dlhgsyh"; | |
22 | }; | |
14 | 23 | |
15 | 24 | # TODO FIXME |
16 | doCheck = false; | |
25 | doCheck = | |
26 | false; | |
17 | 27 | |
18 | meta = with lib; { | |
19 | description = "Unicode to ASCII transliteration"; | |
20 | homepage = "https://github.com/hunterwb/any-ascii"; | |
21 | }; | |
28 | meta = | |
29 | with lib; | |
30 | { }; | |
22 | 31 | } |
1 | 1 | # If you run pynixify again, the file will be either overwritten or |
2 | 2 | # deleted, and you will lose the changes you made to it. |
3 | 3 | |
4 | { apispec, buildPythonPackage, fetchPypi, lib }: | |
4 | { apispec | |
5 | , buildPythonPackage | |
6 | , fetchPypi | |
7 | , lib | |
8 | }: | |
5 | 9 | |
6 | 10 | buildPythonPackage rec { |
7 | pname = "apispec-webframeworks"; | |
8 | version = "0.5.2"; | |
11 | pname = | |
12 | "apispec-webframeworks"; | |
13 | version = | |
14 | "0.5.2"; | |
9 | 15 | |
10 | src = fetchPypi { | |
11 | inherit pname version; | |
12 | sha256 = "1wyw30402xq2a8icrsjmy9v43jyvawcjd85ccb2zicqlg4k5pcqd"; | |
13 | }; | |
16 | src = | |
17 | fetchPypi { | |
18 | inherit | |
19 | pname | |
20 | version; | |
21 | sha256 = | |
22 | "1wyw30402xq2a8icrsjmy9v43jyvawcjd85ccb2zicqlg4k5pcqd"; | |
23 | }; | |
14 | 24 | |
15 | propagatedBuildInputs = [ apispec ]; | |
25 | propagatedBuildInputs = | |
26 | [ | |
27 | apispec | |
28 | ]; | |
16 | 29 | |
17 | 30 | # TODO FIXME |
18 | doCheck = false; | |
31 | doCheck = | |
32 | false; | |
19 | 33 | |
20 | meta = with lib; { | |
21 | description = "Web framework plugins for apispec."; | |
22 | homepage = "https://github.com/marshmallow-code/apispec-webframeworks"; | |
23 | }; | |
34 | meta = | |
35 | with lib; { | |
36 | description = | |
37 | "Web framework plugins for apispec."; | |
38 | homepage = | |
39 | "https://github.com/marshmallow-code/apispec-webframeworks"; | |
40 | }; | |
24 | 41 | } |
1 | 1 | # If you run pynixify again, the file will be either overwritten or |
2 | 2 | # deleted, and you will lose the changes you made to it. |
3 | 3 | |
4 | { beautifulsoup4, buildPythonPackage, click, colorama, dateutil, fetchPypi | |
5 | , html2text, lib, lxml, pytz, requests, simplejson }: | |
4 | { beautifulsoup4 | |
5 | , buildPythonPackage | |
6 | , click | |
7 | , colorama | |
8 | , dateutil | |
9 | , fetchPypi | |
10 | , html2text | |
11 | , lib | |
12 | , lxml | |
13 | , pytz | |
14 | , requests | |
15 | , simplejson | |
16 | }: | |
6 | 17 | |
7 | 18 | buildPythonPackage rec { |
8 | pname = "faraday-plugins"; | |
9 | version = "1.3.0"; | |
19 | pname = | |
20 | "faraday-plugins"; | |
21 | version = | |
22 | "1.4.0"; | |
10 | 23 | |
11 | src = fetchPypi { | |
12 | inherit pname version; | |
13 | sha256 = "10jcajizwaql39sbaa72aymnh9r9p4y6yd9sgqg5j6i919d57i37"; | |
14 | }; | |
24 | src = | |
25 | fetchPypi { | |
26 | inherit | |
27 | pname | |
28 | version; | |
29 | sha256 = | |
30 | "18s4r487c8agn5yd3mn0d2gyig5xb6r7kaiyhwkjpzj3nwlh9kw2"; | |
31 | }; | |
15 | 32 | |
16 | propagatedBuildInputs = [ | |
17 | click | |
18 | simplejson | |
19 | requests | |
20 | lxml | |
21 | html2text | |
22 | beautifulsoup4 | |
23 | pytz | |
24 | dateutil | |
25 | colorama | |
26 | ]; | |
33 | propagatedBuildInputs = | |
34 | [ | |
35 | click | |
36 | simplejson | |
37 | requests | |
38 | lxml | |
39 | html2text | |
40 | beautifulsoup4 | |
41 | pytz | |
42 | dateutil | |
43 | colorama | |
44 | ]; | |
27 | 45 | |
28 | 46 | # TODO FIXME |
29 | doCheck = false; | |
47 | doCheck = | |
48 | false; | |
30 | 49 | |
31 | meta = with lib; { description = "Faraday plugins package"; }; | |
50 | meta = | |
51 | with lib; { | |
52 | description = | |
53 | "Faraday plugins package"; | |
54 | }; | |
32 | 55 | } |
1 | 1 | # If you run pynixify again, the file will be either overwritten or |
2 | 2 | # deleted, and you will lose the changes you made to it. |
3 | 3 | |
4 | { alembic, apispec, apispec-webframeworks, autobahn, bcrypt, buildPythonPackage | |
5 | , click, colorama, dateutil, distro, email_validator, factory_boy | |
6 | , faraday-plugins, fetchPypi, filedepot, filteralchemy-fork, flask | |
7 | , flask-classful, flask-kvsession-fork, flask-login, flask-security | |
8 | , flask_sqlalchemy, hypothesis, lib, marshmallow, marshmallow-sqlalchemy | |
9 | , nplusone, pgcli, pillow, psycopg2, pyasn1, pylint, pyopenssl, pytest | |
10 | , pytest-factoryboy, pytestcov, pytestrunner, requests, responses | |
11 | , service-identity, simplekv, sphinx, sqlalchemy, syslog-rfc5424-formatter, tqdm | |
12 | , twine, twisted, webargs, werkzeug, wtforms }: | |
4 | { alembic | |
5 | , apispec | |
6 | , apispec-webframeworks | |
7 | , autobahn | |
8 | , bcrypt | |
9 | , buildPythonPackage | |
10 | , click | |
11 | , colorama | |
12 | , dateutil | |
13 | , distro | |
14 | , email_validator | |
15 | , factory_boy | |
16 | , faraday-plugins | |
17 | , fetchPypi | |
18 | , filedepot | |
19 | , filteralchemy-fork | |
20 | , flask | |
21 | , flask-classful | |
22 | , flask-kvsession-fork | |
23 | , flask-security | |
24 | , flask_login | |
25 | , flask_sqlalchemy | |
26 | , hypothesis | |
27 | , lib | |
28 | , marshmallow | |
29 | , marshmallow-sqlalchemy | |
30 | , nplusone | |
31 | , pgcli | |
32 | , pillow | |
33 | , psycopg2 | |
34 | , pyasn1 | |
35 | , pylint | |
36 | , pyopenssl | |
37 | , pytest | |
38 | , pytest-factoryboy | |
39 | , pytestcov | |
40 | , pytestrunner | |
41 | , pyyaml | |
42 | , requests | |
43 | , responses | |
44 | , service-identity | |
45 | , simplekv | |
46 | , sphinx | |
47 | , sqlalchemy | |
48 | , syslog-rfc5424-formatter | |
49 | , tqdm | |
50 | , twine | |
51 | , twisted | |
52 | , webargs | |
53 | , werkzeug | |
54 | , wtforms | |
55 | }: | |
13 | 56 | |
14 | 57 | buildPythonPackage rec { |
15 | pname = "faradaysec"; | |
16 | version = "3.11.1"; | |
58 | pname = | |
59 | "faradaysec"; | |
60 | version = | |
61 | "3.14.0"; | |
17 | 62 | |
18 | src = lib.cleanSource ../../..; | |
63 | src = | |
64 | lib.cleanSource | |
65 | ../../..; | |
19 | 66 | |
20 | buildInputs = [ pytestrunner ]; | |
21 | propagatedBuildInputs = [ | |
22 | werkzeug | |
23 | autobahn | |
24 | alembic | |
25 | bcrypt | |
26 | colorama | |
27 | click | |
28 | flask | |
29 | flask_sqlalchemy | |
30 | flask-classful | |
31 | email_validator | |
32 | wtforms | |
33 | flask-login | |
34 | flask-security | |
35 | marshmallow | |
36 | pillow | |
37 | psycopg2 | |
38 | pgcli | |
39 | pyopenssl | |
40 | dateutil | |
41 | requests | |
42 | pyasn1 | |
43 | service-identity | |
44 | sqlalchemy | |
45 | tqdm | |
46 | twisted | |
47 | webargs | |
48 | marshmallow-sqlalchemy | |
49 | filteralchemy-fork | |
50 | filedepot | |
51 | nplusone | |
52 | syslog-rfc5424-formatter | |
53 | simplekv | |
54 | flask-kvsession-fork | |
55 | distro | |
56 | faraday-plugins | |
57 | apispec | |
58 | apispec-webframeworks | |
59 | ]; | |
60 | checkInputs = [ | |
61 | pytest | |
62 | flask | |
63 | flask | |
64 | factory_boy | |
65 | pylint | |
66 | pytest | |
67 | pytestcov | |
68 | pytest-factoryboy | |
69 | responses | |
70 | hypothesis | |
71 | sphinx | |
72 | twine | |
73 | ]; | |
67 | buildInputs = | |
68 | [ | |
69 | pytestrunner | |
70 | ]; | |
71 | propagatedBuildInputs = | |
72 | [ | |
73 | werkzeug | |
74 | autobahn | |
75 | alembic | |
76 | bcrypt | |
77 | colorama | |
78 | click | |
79 | flask | |
80 | flask_sqlalchemy | |
81 | flask-classful | |
82 | email_validator | |
83 | wtforms | |
84 | flask_login | |
85 | flask-security | |
86 | marshmallow | |
87 | pillow | |
88 | psycopg2 | |
89 | pgcli | |
90 | pyopenssl | |
91 | dateutil | |
92 | requests | |
93 | pyasn1 | |
94 | service-identity | |
95 | sqlalchemy | |
96 | tqdm | |
97 | twisted | |
98 | webargs | |
99 | marshmallow-sqlalchemy | |
100 | filteralchemy-fork | |
101 | filedepot | |
102 | nplusone | |
103 | syslog-rfc5424-formatter | |
104 | simplekv | |
105 | flask-kvsession-fork | |
106 | distro | |
107 | faraday-plugins | |
108 | apispec | |
109 | apispec-webframeworks | |
110 | pyyaml | |
111 | ]; | |
112 | checkInputs = | |
113 | [ | |
114 | flask | |
115 | factory_boy | |
116 | pylint | |
117 | pytest | |
118 | pytestcov | |
119 | pytest-factoryboy | |
120 | responses | |
121 | hypothesis | |
122 | sphinx | |
123 | twine | |
124 | ]; | |
74 | 125 | |
75 | checkPhase = "true # TODO fill with the real command for testing"; | |
126 | checkPhase = | |
127 | "true # TODO fill with the real command for testing"; | |
76 | 128 | |
77 | meta = with lib; { | |
78 | description = | |
79 | "Collaborative Penetration Test and Vulnerability Management Platform https://www.faradaysec.com"; | |
80 | homepage = "https://github.com/infobyte/faraday"; | |
81 | }; | |
129 | meta = | |
130 | with lib; { | |
131 | description = | |
132 | "Collaborative Penetration Test and Vulnerability Management Platform https://www.faradaysec.com"; | |
133 | homepage = | |
134 | "https://github.com/infobyte/faraday"; | |
135 | }; | |
82 | 136 | } |
1 | 1 | # If you run pynixify again, the file will be either overwritten or |
2 | 2 | # deleted, and you will lose the changes you made to it. |
3 | 3 | |
4 | { anyascii, buildPythonPackage, fetchPypi, lib }: | |
4 | { anyascii | |
5 | , buildPythonPackage | |
6 | , fetchPypi | |
7 | , lib | |
8 | }: | |
5 | 9 | |
6 | 10 | buildPythonPackage rec { |
7 | pname = "filedepot"; | |
8 | version = "0.8.0"; | |
11 | pname = | |
12 | "filedepot"; | |
13 | version = | |
14 | "0.8.0"; | |
9 | 15 | |
10 | src = fetchPypi { | |
11 | inherit pname version; | |
12 | sha256 = "0j8q05fyrnhjscz7yi75blymdswviv8bl79j9m5m45if6p6nwc95"; | |
13 | }; | |
16 | src = | |
17 | fetchPypi { | |
18 | inherit | |
19 | pname | |
20 | version; | |
21 | sha256 = | |
22 | "0j8q05fyrnhjscz7yi75blymdswviv8bl79j9m5m45if6p6nwc95"; | |
23 | }; | |
14 | 24 | |
15 | propagatedBuildInputs = [ anyascii ]; | |
25 | propagatedBuildInputs = | |
26 | [ | |
27 | anyascii | |
28 | ]; | |
16 | 29 | |
17 | 30 | # TODO FIXME |
18 | doCheck = false; | |
31 | doCheck = | |
32 | false; | |
19 | 33 | |
20 | meta = with lib; { | |
21 | description = | |
22 | "Toolkit for storing files and attachments in web applications"; | |
23 | homepage = "https://github.com/amol-/depot"; | |
24 | }; | |
34 | meta = | |
35 | with lib; { | |
36 | description = | |
37 | "Toolkit for storing files and attachments in web applications"; | |
38 | homepage = | |
39 | "https://github.com/amol-/depot"; | |
40 | }; | |
25 | 41 | } |
1 | 1 | # If you run pynixify again, the file will be either overwritten or |
2 | 2 | # deleted, and you will lose the changes you made to it. |
3 | 3 | |
4 | { buildPythonPackage, fetchPypi, lib, marshmallow-sqlalchemy, six, webargs }: | |
4 | { buildPythonPackage | |
5 | , fetchPypi | |
6 | , lib | |
7 | , marshmallow-sqlalchemy | |
8 | , six | |
9 | , webargs | |
10 | }: | |
5 | 11 | |
6 | 12 | buildPythonPackage rec { |
7 | pname = "filteralchemy-fork"; | |
8 | version = "0.1.0"; | |
13 | pname = | |
14 | "filteralchemy-fork"; | |
15 | version = | |
16 | "0.1.0"; | |
9 | 17 | |
10 | src = fetchPypi { | |
11 | inherit pname version; | |
12 | sha256 = "1lssfgz7vlsvyl9kpcmdjndfklyb3nkxyyqwf2jwzd8zpv9cbwvs"; | |
13 | }; | |
18 | src = | |
19 | fetchPypi { | |
20 | inherit | |
21 | pname | |
22 | version; | |
23 | sha256 = | |
24 | "1lssfgz7vlsvyl9kpcmdjndfklyb3nkxyyqwf2jwzd8zpv9cbwvs"; | |
25 | }; | |
14 | 26 | |
15 | propagatedBuildInputs = [ six webargs marshmallow-sqlalchemy ]; | |
27 | propagatedBuildInputs = | |
28 | [ | |
29 | six | |
30 | webargs | |
31 | marshmallow-sqlalchemy | |
32 | ]; | |
16 | 33 | |
17 | 34 | # TODO FIXME |
18 | doCheck = false; | |
35 | doCheck = | |
36 | false; | |
19 | 37 | |
20 | meta = with lib; { | |
21 | description = | |
22 | "Declarative query builder for SQLAlchemy. This is a fork of the original project with the changes of https://github.com/jmcarp/filteralchemy/pull/2 applied"; | |
23 | homepage = "https://github.com/infobyte/filteralchemy"; | |
24 | }; | |
38 | meta = | |
39 | with lib; { | |
40 | description = | |
41 | "Declarative query builder for SQLAlchemy. This is a fork of the original project with the changes of https://github.com/jmcarp/filteralchemy/pull/2 applied"; | |
42 | homepage = | |
43 | "https://github.com/infobyte/filteralchemy"; | |
44 | }; | |
25 | 45 | } |
1 | 1 | # If you run pynixify again, the file will be either overwritten or |
2 | 2 | # deleted, and you will lose the changes you made to it. |
3 | 3 | |
4 | { buildPythonPackage, fetchPypi, flask, lib }: | |
4 | { buildPythonPackage | |
5 | , fetchPypi | |
6 | , flask | |
7 | , lib | |
8 | }: | |
5 | 9 | |
6 | 10 | buildPythonPackage rec { |
7 | pname = "flask-classful"; | |
8 | version = "0.14.2"; | |
11 | pname = | |
12 | "flask-classful"; | |
13 | version = | |
14 | "0.14.2"; | |
9 | 15 | |
10 | src = fetchPypi { | |
11 | inherit version; | |
12 | pname = "Flask-Classful"; | |
13 | sha256 = "1xxzwhv09l8j8qmww2ps9cj7fm9s5n3507zk7gdic7lyyv9sn35f"; | |
14 | }; | |
16 | src = | |
17 | fetchPypi { | |
18 | inherit | |
19 | version; | |
20 | pname = | |
21 | "Flask-Classful"; | |
22 | sha256 = | |
23 | "1xxzwhv09l8j8qmww2ps9cj7fm9s5n3507zk7gdic7lyyv9sn35f"; | |
24 | }; | |
15 | 25 | |
16 | propagatedBuildInputs = [ flask ]; | |
26 | propagatedBuildInputs = | |
27 | [ | |
28 | flask | |
29 | ]; | |
17 | 30 | |
18 | 31 | # TODO FIXME |
19 | doCheck = false; | |
32 | doCheck = | |
33 | false; | |
20 | 34 | |
21 | meta = with lib; { | |
22 | description = "Class based views for Flask"; | |
23 | homepage = "https://github.com/teracyhq/flask-classful"; | |
24 | }; | |
35 | meta = | |
36 | with lib; { | |
37 | description = | |
38 | "Class based views for Flask"; | |
39 | homepage = | |
40 | "https://github.com/teracyhq/flask-classful"; | |
41 | }; | |
25 | 42 | } |
1 | 1 | # If you run pynixify again, the file will be either overwritten or |
2 | 2 | # deleted, and you will lose the changes you made to it. |
3 | 3 | |
4 | { buildPythonPackage, fetchPypi, flask, itsdangerous, lib, simplekv, six | |
5 | , werkzeug }: | |
4 | { buildPythonPackage | |
5 | , fetchPypi | |
6 | , flask | |
7 | , itsdangerous | |
8 | , lib | |
9 | , simplekv | |
10 | , six | |
11 | , werkzeug | |
12 | }: | |
6 | 13 | |
7 | 14 | buildPythonPackage rec { |
8 | pname = "flask-kvsession-fork"; | |
9 | version = "0.6.3"; | |
15 | pname = | |
16 | "flask-kvsession-fork"; | |
17 | version = | |
18 | "0.6.3"; | |
10 | 19 | |
11 | src = fetchPypi { | |
12 | inherit version; | |
13 | pname = "Flask-KVSession-fork"; | |
14 | sha256 = "0j5ncqb2kwigs2h12vd5jwhj11ma2igw35yz9l79h2q2gg38nn8l"; | |
15 | }; | |
20 | src = | |
21 | fetchPypi { | |
22 | inherit | |
23 | version; | |
24 | pname = | |
25 | "Flask-KVSession-fork"; | |
26 | sha256 = | |
27 | "0j5ncqb2kwigs2h12vd5jwhj11ma2igw35yz9l79h2q2gg38nn8l"; | |
28 | }; | |
16 | 29 | |
17 | propagatedBuildInputs = [ flask simplekv werkzeug itsdangerous six ]; | |
30 | propagatedBuildInputs = | |
31 | [ | |
32 | flask | |
33 | simplekv | |
34 | werkzeug | |
35 | itsdangerous | |
36 | six | |
37 | ]; | |
18 | 38 | |
19 | 39 | # TODO FIXME |
20 | doCheck = false; | |
40 | doCheck = | |
41 | false; | |
21 | 42 | |
22 | meta = with lib; { | |
23 | description = "Transparent server-side session support for flask"; | |
24 | homepage = "https://github.com/mbr/flask-kvsession"; | |
25 | }; | |
43 | meta = | |
44 | with lib; { | |
45 | description = | |
46 | "Transparent server-side session support for flask"; | |
47 | homepage = | |
48 | "https://github.com/mbr/flask-kvsession"; | |
49 | }; | |
26 | 50 | } |
1 | 1 | # If you run pynixify again, the file will be either overwritten or |
2 | 2 | # deleted, and you will lose the changes you made to it. |
3 | 3 | |
4 | { buildPythonPackage, fetchPypi, flask, lib }: | |
4 | { buildPythonPackage | |
5 | , fetchPypi | |
6 | , flask | |
7 | , lib | |
8 | }: | |
5 | 9 | |
6 | 10 | buildPythonPackage rec { |
7 | pname = "flask-login"; | |
8 | version = "0.5.0"; | |
11 | pname = | |
12 | "flask-login"; | |
13 | version = | |
14 | "0.5.0"; | |
9 | 15 | |
10 | src = fetchPypi { | |
11 | inherit version; | |
12 | pname = "Flask-Login"; | |
13 | sha256 = "0jqb3jfm92yyz4f8n3f92f7y59p8m9j98cyc19wavkjvbgqswcvd"; | |
14 | }; | |
16 | src = | |
17 | fetchPypi { | |
18 | inherit | |
19 | version; | |
20 | pname = | |
21 | "Flask-Login"; | |
22 | sha256 = | |
23 | "0jqb3jfm92yyz4f8n3f92f7y59p8m9j98cyc19wavkjvbgqswcvd"; | |
24 | }; | |
15 | 25 | |
16 | propagatedBuildInputs = [ flask ]; | |
26 | propagatedBuildInputs = | |
27 | [ | |
28 | flask | |
29 | ]; | |
17 | 30 | |
18 | 31 | # TODO FIXME |
19 | doCheck = false; | |
32 | doCheck = | |
33 | false; | |
20 | 34 | |
21 | meta = with lib; { | |
22 | description = "User session management for Flask"; | |
23 | homepage = "https://github.com/maxcountryman/flask-login"; | |
24 | }; | |
35 | meta = | |
36 | with lib; { | |
37 | description = | |
38 | "User session management for Flask"; | |
39 | homepage = | |
40 | "https://github.com/maxcountryman/flask-login"; | |
41 | }; | |
25 | 42 | } |
1 | 1 | # If you run pynixify again, the file will be either overwritten or |
2 | 2 | # deleted, and you will lose the changes you made to it. |
3 | 3 | |
4 | { Babel, buildPythonPackage, fetchPypi, flask, flask-babelex, flask-login | |
5 | , flask_mail, flask_principal, flask_wtf, itsdangerous, lib, passlib | |
6 | , pytestrunner }: | |
4 | { Babel | |
5 | , buildPythonPackage | |
6 | , fetchPypi | |
7 | , flask | |
8 | , flask-babelex | |
9 | , flask_login | |
10 | , flask_mail | |
11 | , flask_principal | |
12 | , flask_wtf | |
13 | , itsdangerous | |
14 | , lib | |
15 | , passlib | |
16 | , pytestrunner | |
17 | }: | |
7 | 18 | |
8 | 19 | buildPythonPackage rec { |
9 | pname = "flask-security"; | |
10 | version = "3.0.0"; | |
20 | pname = | |
21 | "flask-security"; | |
22 | version = | |
23 | "3.0.0"; | |
11 | 24 | |
12 | src = fetchPypi { | |
13 | inherit version; | |
14 | pname = "Flask-Security"; | |
15 | sha256 = "0ck4ybpppka56cqv0s26h1jjq6sqvwmqfm85ylq9zy28b9gsl7fn"; | |
16 | }; | |
25 | src = | |
26 | fetchPypi { | |
27 | inherit | |
28 | version; | |
29 | pname = | |
30 | "Flask-Security"; | |
31 | sha256 = | |
32 | "0ck4ybpppka56cqv0s26h1jjq6sqvwmqfm85ylq9zy28b9gsl7fn"; | |
33 | }; | |
17 | 34 | |
18 | buildInputs = [ Babel pytestrunner ]; | |
19 | propagatedBuildInputs = [ | |
20 | flask | |
21 | flask-login | |
22 | flask_mail | |
23 | flask_principal | |
24 | flask_wtf | |
25 | flask-babelex | |
26 | itsdangerous | |
27 | passlib | |
28 | ]; | |
35 | buildInputs = | |
36 | [ | |
37 | Babel | |
38 | pytestrunner | |
39 | ]; | |
40 | propagatedBuildInputs = | |
41 | [ | |
42 | flask | |
43 | flask_login | |
44 | flask_mail | |
45 | flask_principal | |
46 | flask_wtf | |
47 | flask-babelex | |
48 | itsdangerous | |
49 | passlib | |
50 | ]; | |
29 | 51 | |
30 | 52 | # TODO FIXME |
31 | doCheck = false; | |
53 | doCheck = | |
54 | false; | |
32 | 55 | |
33 | meta = with lib; { | |
34 | description = "Simple security for Flask apps."; | |
35 | homepage = "https://github.com/mattupstate/flask-security"; | |
36 | }; | |
56 | meta = | |
57 | with lib; { | |
58 | description = | |
59 | "Simple security for Flask apps."; | |
60 | homepage = | |
61 | "https://github.com/mattupstate/flask-security"; | |
62 | }; | |
37 | 63 | } |
1 | 1 | # If you run pynixify again, the file will be either overwritten or |
2 | 2 | # deleted, and you will lose the changes you made to it. |
3 | 3 | |
4 | { blinker, buildPythonPackage, fetchPypi, lib, six }: | |
4 | { blinker | |
5 | , buildPythonPackage | |
6 | , fetchPypi | |
7 | , lib | |
8 | , six | |
9 | }: | |
5 | 10 | |
6 | 11 | buildPythonPackage rec { |
7 | pname = "nplusone"; | |
8 | version = "1.0.0"; | |
12 | pname = | |
13 | "nplusone"; | |
14 | version = | |
15 | "1.0.0"; | |
9 | 16 | |
10 | src = fetchPypi { | |
11 | inherit pname version; | |
12 | sha256 = "0lanbbpi5gfwjy6rlwlxw9z6nyzr5y4b4kg20jxym9qa1jhw09hp"; | |
13 | }; | |
17 | src = | |
18 | fetchPypi { | |
19 | inherit | |
20 | pname | |
21 | version; | |
22 | sha256 = | |
23 | "0lanbbpi5gfwjy6rlwlxw9z6nyzr5y4b4kg20jxym9qa1jhw09hp"; | |
24 | }; | |
14 | 25 | |
15 | propagatedBuildInputs = [ six blinker ]; | |
26 | propagatedBuildInputs = | |
27 | [ | |
28 | six | |
29 | blinker | |
30 | ]; | |
16 | 31 | |
17 | 32 | # TODO FIXME |
18 | doCheck = false; | |
33 | doCheck = | |
34 | false; | |
19 | 35 | |
20 | meta = with lib; { | |
21 | description = "Detecting the n+1 queries problem in Python"; | |
22 | homepage = "https://github.com/jmcarp/nplusone"; | |
23 | }; | |
36 | meta = | |
37 | with lib; { | |
38 | description = | |
39 | "Detecting the n+1 queries problem in Python"; | |
40 | homepage = | |
41 | "https://github.com/jmcarp/nplusone"; | |
42 | }; | |
24 | 43 | } |
1 | 1 | # If you run pynixify again, the file will be either overwritten or |
2 | 2 | # deleted, and you will lose the changes you made to it. |
3 | 3 | |
4 | { buildPythonPackage, factory_boy, fetchPypi, inflection, lib, pytest }: | |
4 | { buildPythonPackage | |
5 | , factory_boy | |
6 | , fetchPypi | |
7 | , inflection | |
8 | , lib | |
9 | , pytest | |
10 | }: | |
5 | 11 | |
6 | 12 | buildPythonPackage rec { |
7 | pname = "pytest-factoryboy"; | |
8 | version = "2.0.3"; | |
13 | pname = | |
14 | "pytest-factoryboy"; | |
15 | version = | |
16 | "2.0.3"; | |
9 | 17 | |
10 | src = fetchPypi { | |
11 | inherit pname version; | |
12 | sha256 = "06js78jshf81i2nqgf2svb8z68wh4m34hcqdvz9rj4pcvnvkzvzz"; | |
13 | }; | |
18 | src = | |
19 | fetchPypi { | |
20 | inherit | |
21 | pname | |
22 | version; | |
23 | sha256 = | |
24 | "06js78jshf81i2nqgf2svb8z68wh4m34hcqdvz9rj4pcvnvkzvzz"; | |
25 | }; | |
14 | 26 | |
15 | propagatedBuildInputs = [ inflection factory_boy pytest ]; | |
27 | propagatedBuildInputs = | |
28 | [ | |
29 | inflection | |
30 | factory_boy | |
31 | pytest | |
32 | ]; | |
16 | 33 | |
17 | 34 | # TODO FIXME |
18 | doCheck = false; | |
35 | doCheck = | |
36 | false; | |
19 | 37 | |
20 | meta = with lib; { | |
21 | description = "Factory Boy support for pytest."; | |
22 | homepage = "https://github.com/pytest-dev/pytest-factoryboy"; | |
23 | }; | |
38 | meta = | |
39 | with lib; { | |
40 | description = | |
41 | "Factory Boy support for pytest."; | |
42 | homepage = | |
43 | "https://github.com/pytest-dev/pytest-factoryboy"; | |
44 | }; | |
24 | 45 | } |
1 | 1 | # If you run pynixify again, the file will be either overwritten or |
2 | 2 | # deleted, and you will lose the changes you made to it. |
3 | 3 | |
4 | { buildPythonPackage, fetchPypi, lib }: | |
4 | { buildPythonPackage | |
5 | , fetchPypi | |
6 | , lib | |
7 | }: | |
5 | 8 | |
6 | 9 | buildPythonPackage rec { |
7 | pname = "simplekv"; | |
8 | version = "0.14.1"; | |
10 | pname = | |
11 | "simplekv"; | |
12 | version = | |
13 | "0.14.1"; | |
9 | 14 | |
10 | src = fetchPypi { | |
11 | inherit pname version; | |
12 | sha256 = "1xnh5k7bhvi6almfsv3zj8dzxxiv66sn46fyr4hsh7klndna6lw9"; | |
13 | }; | |
15 | src = | |
16 | fetchPypi { | |
17 | inherit | |
18 | pname | |
19 | version; | |
20 | sha256 = | |
21 | "1xnh5k7bhvi6almfsv3zj8dzxxiv66sn46fyr4hsh7klndna6lw9"; | |
22 | }; | |
14 | 23 | |
15 | 24 | # TODO FIXME |
16 | doCheck = false; | |
25 | doCheck = | |
26 | false; | |
17 | 27 | |
18 | meta = with lib; { | |
19 | description = "A key-value storage for binary data, support many backends."; | |
20 | homepage = "http://github.com/mbr/simplekv"; | |
21 | }; | |
28 | meta = | |
29 | with lib; { | |
30 | description = | |
31 | "A key-value storage for binary data, support many backends."; | |
32 | homepage = | |
33 | "http://github.com/mbr/simplekv"; | |
34 | }; | |
22 | 35 | } |
1 | 1 | # If you run pynixify again, the file will be either overwritten or |
2 | 2 | # deleted, and you will lose the changes you made to it. |
3 | 3 | |
4 | { buildPythonPackage, fetchPypi, lib }: | |
4 | { buildPythonPackage | |
5 | , fetchPypi | |
6 | , lib | |
7 | }: | |
5 | 8 | |
6 | 9 | buildPythonPackage rec { |
7 | pname = "syslog-rfc5424-formatter"; | |
8 | version = "1.2.2"; | |
10 | pname = | |
11 | "syslog-rfc5424-formatter"; | |
12 | version = | |
13 | "1.2.2"; | |
9 | 14 | |
10 | src = fetchPypi { | |
11 | inherit pname version; | |
12 | sha256 = "113fc9wbsbb63clw74f7riyv37ar1131x8lc32q2cvqd523jqsns"; | |
13 | }; | |
15 | src = | |
16 | fetchPypi { | |
17 | inherit | |
18 | pname | |
19 | version; | |
20 | sha256 = | |
21 | "113fc9wbsbb63clw74f7riyv37ar1131x8lc32q2cvqd523jqsns"; | |
22 | }; | |
14 | 23 | |
15 | 24 | # TODO FIXME |
16 | doCheck = false; | |
25 | doCheck = | |
26 | false; | |
17 | 27 | |
18 | meta = with lib; { | |
19 | description = | |
20 | "Logging formatter which produces well-formatted RFC5424 Syslog Protocol messages"; | |
21 | homepage = "https://github.com/easypost/syslog-rfc5424-formatter"; | |
22 | }; | |
28 | meta = | |
29 | with lib; { | |
30 | description = | |
31 | "Logging formatter which produces well-formatted RFC5424 Syslog Protocol messages"; | |
32 | homepage = | |
33 | "https://github.com/easypost/syslog-rfc5424-formatter"; | |
34 | }; | |
23 | 35 | } |
1 | 1 | # If you run pynixify again, the file will be either overwritten or |
2 | 2 | # deleted, and you will lose the changes you made to it. |
3 | 3 | |
4 | { buildPythonPackage, fetchPypi, lib, marshmallow }: | |
4 | { buildPythonPackage | |
5 | , fetchPypi | |
6 | , lib | |
7 | , marshmallow | |
8 | }: | |
5 | 9 | |
6 | 10 | buildPythonPackage rec { |
7 | pname = "webargs"; | |
8 | version = "6.1.0"; | |
11 | pname = | |
12 | "webargs"; | |
13 | version = | |
14 | "7.0.1"; | |
9 | 15 | |
10 | src = fetchPypi { | |
11 | inherit pname version; | |
12 | sha256 = "0gxvd1k5czch2l3jpvgbb53wbzl2drld25rs45jcfkrwbjrpzd7b"; | |
13 | }; | |
16 | src = | |
17 | fetchPypi { | |
18 | inherit | |
19 | pname | |
20 | version; | |
21 | sha256 = | |
22 | "07fr5hzffnqaw05ykrabn1kpqzi03g60yi49924glj7kx4y8hg9g"; | |
23 | }; | |
14 | 24 | |
15 | propagatedBuildInputs = [ marshmallow ]; | |
25 | propagatedBuildInputs = | |
26 | [ | |
27 | marshmallow | |
28 | ]; | |
16 | 29 | |
17 | 30 | # TODO FIXME |
18 | doCheck = false; | |
31 | doCheck = | |
32 | false; | |
19 | 33 | |
20 | meta = with lib; { | |
21 | description = | |
22 | "Declarative parsing and validation of HTTP request objects, with built-in support for popular web frameworks, including Flask, Django, Bottle, Tornado, Pyramid, webapp2, Falcon, and aiohttp."; | |
23 | homepage = "https://github.com/marshmallow-code/webargs"; | |
24 | }; | |
34 | meta = | |
35 | with lib; { | |
36 | description = | |
37 | "Declarative parsing and validation of HTTP request objects, with built-in support for popular web frameworks, including Flask, Django, Bottle, Tornado, Pyramid, Falcon, and aiohttp."; | |
38 | homepage = | |
39 | "https://github.com/marshmallow-code/webargs"; | |
40 | }; | |
25 | 41 | } |
1 | 1 | # If you run pynixify again, the file will be either overwritten or |
2 | 2 | # deleted, and you will lose the changes you made to it. |
3 | 3 | |
4 | { buildPythonPackage, fetchPypi, lib }: | |
4 | { buildPythonPackage | |
5 | , fetchPypi | |
6 | , lib | |
7 | }: | |
5 | 8 | |
6 | 9 | buildPythonPackage rec { |
7 | pname = "werkzeug"; | |
8 | version = "1.0.1"; | |
10 | pname = | |
11 | "werkzeug"; | |
12 | version = | |
13 | "1.0.1"; | |
9 | 14 | |
10 | src = fetchPypi { | |
11 | inherit version; | |
12 | pname = "Werkzeug"; | |
13 | sha256 = "0z74sa1xw5h20yin9faj0vvdbq713cgbj84klc72jr9nmpjv303c"; | |
14 | }; | |
15 | src = | |
16 | fetchPypi { | |
17 | inherit | |
18 | version; | |
19 | pname = | |
20 | "Werkzeug"; | |
21 | sha256 = | |
22 | "0z74sa1xw5h20yin9faj0vvdbq713cgbj84klc72jr9nmpjv303c"; | |
23 | }; | |
15 | 24 | |
16 | 25 | # TODO FIXME |
17 | doCheck = false; | |
26 | doCheck = | |
27 | false; | |
18 | 28 | |
19 | meta = with lib; { | |
20 | description = "The comprehensive WSGI web application library."; | |
21 | homepage = "https://palletsprojects.com/p/werkzeug/"; | |
22 | }; | |
29 | meta = | |
30 | with lib; { | |
31 | description = | |
32 | "The comprehensive WSGI web application library."; | |
33 | homepage = | |
34 | "https://palletsprojects.com/p/werkzeug/"; | |
35 | }; | |
23 | 36 | } |
1 | 1 | # If you run pynixify again, the file will be either overwritten or |
2 | 2 | # deleted, and you will lose the changes you made to it. |
3 | 3 | |
4 | { python ? "python3" }: | |
4 | { python ? | |
5 | "python3" | |
6 | }: | |
5 | 7 | let |
6 | pkgs = import ./nixpkgs.nix { }; | |
7 | pythonPkg = builtins.getAttr python pkgs; | |
8 | pkgs = | |
9 | import | |
10 | ./nixpkgs.nix | |
11 | { }; | |
12 | pythonPkg = | |
13 | builtins.getAttr | |
14 | python | |
15 | pkgs; | |
8 | 16 | in pkgs.mkShell { |
9 | name = "pynixify-env"; | |
10 | buildInputs = [ (pythonPkg.withPackages (ps: with ps; [ faradaysec ])) ]; | |
17 | name = | |
18 | "pynixify-env"; | |
19 | buildInputs = | |
20 | [ | |
21 | (pythonPkg.withPackages | |
22 | (ps: | |
23 | with ps; | |
24 | [ | |
25 | faradaysec | |
26 | ])) | |
27 | ]; | |
11 | 28 | } |
1 | 1 | let |
2 | 2 | version = builtins.head (builtins.match ".*'([0-9]+.[0-9]+(.[0-9]+)?)'.*" |
3 | 3 | (builtins.readFile ./faraday/__init__.py)); |
4 | ||
5 | pynixifyCommand = '' | |
6 | pynixify --nixpkgs https://github.com/infobyte/nixpkgs/archive/98720fe237de55ca5779af5ee07407d0947b8deb.tar.gz --local faradaysec --tests faradaysec | |
7 | ''; | |
4 | 8 | |
5 | 9 | in { dockerName ? "registry.gitlab.com/faradaysec/faraday", dockerTag ? version |
6 | 10 | , systemUser ? "faraday", systemGroup ? "faraday", systemHome ? null |
11 | 15 | , useLastCommit ? true }: rec { |
12 | 16 | |
13 | 17 | faraday-server = python38.pkgs.faradaysec.overrideAttrs (old: |
14 | { | |
18 | assert !builtins.hasAttr "checkInputs" old; { | |
19 | name = "faraday-server-${version}"; | |
15 | 20 | doCheck = true; |
16 | 21 | checkPhase = "true"; |
22 | checkInputs = [ pynixify runPynixify ]; | |
17 | 23 | } // lib.optionalAttrs useLastCommit { |
18 | 24 | src = builtins.fetchGit { |
19 | 25 | url = ./.; |
71 | 77 | [Install] |
72 | 78 | WantedBy=multi-user.target |
73 | 79 | ''; |
80 | ||
81 | pynixify = let | |
82 | src = builtins.fetchGit { | |
83 | url = "https://github.com/cript0nauta/pynixify.git"; | |
84 | ref = "refs/heads/main"; | |
85 | }; | |
86 | ||
87 | original = | |
88 | # TODO: use python 3.8 after migrating to 20.09 | |
89 | python37Packages.callPackage "${src}/nix/packages/pynixify" { }; | |
90 | ||
91 | in original.overridePythonAttrs (drv: { | |
92 | # based in https://github.com/cript0nauta/pynixify/blob/main/default.nix | |
93 | checkInputs = drv.checkInputs ++ [ nix nixfmtCustom bats ]; | |
94 | ||
95 | checkPhase = '' | |
96 | mypy pynixify/ tests/ acceptance_tests/ | |
97 | pytest tests/ -m 'not usesnix' # We can't run Nix inside Nix builds | |
98 | ''; | |
99 | ||
100 | postInstall = '' | |
101 | # Add nixfmt to pynixify's PATH | |
102 | wrapProgram $out/bin/pynixify --prefix PATH : "${nixfmtCustom}/bin" | |
103 | ''; | |
104 | }); | |
105 | ||
106 | nixfmtCustom = | |
107 | # custom wrapper of nixfmt that sets the column width to 1. This will force | |
108 | # splitting function arguments into separate lines and prevent merge | |
109 | # conflicts with our commercial versions. | |
110 | writeShellScriptBin "nixfmt" '' | |
111 | exec ${nixfmt}/bin/nixfmt --width=1 $@ | |
112 | ''; | |
113 | ||
114 | runPynixify = | |
115 | writeShellScriptBin "run-pynixify" '' | |
116 | export PATH=${pynixify}/bin:$PATH | |
117 | ${pynixifyCommand} | |
118 | ''; | |
74 | 119 | } |
19 | 19 | requests>=2.18.4 |
20 | 20 | pyasn1 |
21 | 21 | service_identity>=17.0.0 |
22 | SQLAlchemy>=1.2.0b2 | |
22 | SQLAlchemy>=1.2.0,<1.4.0 | |
23 | 23 | tqdm>=4.15.0 |
24 | 24 | twisted>=18.9.0 |
25 | webargs>=6.0.0 | |
25 | webargs>=7.0.0 | |
26 | 26 | marshmallow-sqlalchemy |
27 | 27 | filteralchemy-fork |
28 | 28 | filedepot>=0.5.0 |
32 | 32 | Flask-KVSession-fork>=0.6.3 |
33 | 33 | distro>=1.4.0 |
34 | 34 | faraday-plugins>=1.0.1,<2.0.0 |
35 | apispec>=3.0.0 | |
35 | apispec>=4.0.0 | |
36 | 36 | apispec-webframeworks>=0.5.0 |
37 | 37 | pyyaml |
0 | 0 | flask # required to have flask shell inside nix-shell |
1 | 1 | factory-boy>=2.10.0 |
2 | 2 | pylint |
3 | pytest>=3.5.1 | |
3 | pytest<6 | |
4 | 4 | pytest-cov |
5 | 5 | pytest-factoryboy>=2.0.1 |
6 | 6 | responses>=0.9.0 |
0 | #!/usr/bin/env bash | |
1 | ||
2 | set -euo pipefail | |
3 | ||
4 | MAX="$((MAX_CLOSURE_SIZE_IN_MB * 1024 * 1024))" | |
5 | OUT_PATH="${1?Usage: $0 PATH}" | |
6 | ||
7 | CLOSURE_SIZE="$(nix path-info --closure-size "${OUT_PATH}" | awk '{ print $2 }')" | |
8 | ||
9 | if [ "$CLOSURE_SIZE" -gt "$MAX" ]; then | |
10 | echo "ERROR: closure size too big!" | |
11 | echo "Consider changing your dependencies or increasing MAX_CLOSURE_SIZE_IN_MB in .gitlab-ci.yml" | |
12 | nix path-info --human-readable --closure-size "${OUT_PATH}" | |
13 | exit 1 | |
14 | fi |
9 | 9 | import sys |
10 | 10 | import argparse |
11 | 11 | import os |
12 | from faraday.config.constant import CONST_FARADAY_HOME_PATH | |
12 | from faraday.server.config import CONST_FARADAY_HOME_PATH | |
13 | 13 | from faraday.server.config import FARADAY_BASE |
14 | 14 | |
15 | 15 | my_env = os.environ |
58 | 58 | print(e['error']) |
59 | 59 | sys.exit() |
60 | 60 | |
61 | # When downloading a scan we need the raw contents not the JSON data. | |
61 | # When downloading a scan we need the raw contents not the JSON data. | |
62 | 62 | if 'download' in resource: |
63 | 63 | return r.content |
64 | 64 | else: |
186 | 186 | |
187 | 187 | Get the historical information for the particular scan and hid. Return |
188 | 188 | the status if available. If not return unknown. |
189 | """ | |
189 | """ | |
190 | 190 | |
191 | 191 | d = get_scan_history(sid, hid) |
192 | 192 | return d['status'] |
238 | 238 | # For version 7, use the nessus scan Id to avoid overwrite the output file |
239 | 239 | if not output: |
240 | 240 | print('Using Nessus 7. Ignore --output. This is normal.') |
241 | report_path = os.path.join(FARADAY_BASE,'scripts','cscan','output','nessus_{0}.xml'.format(sid)) | |
242 | if not os.path.exists(report_path): | |
243 | with open(report_path,'w') as report: | |
241 | report_path = FARADAY_BASE / 'scripts' / 'cscan' / 'output' / \ | |
242 | f'nessus_{sid}.xml' | |
243 | if not report_path.exists(): | |
244 | with report_path.open('w') as report: | |
244 | 245 | print('Saving scan results to {0}.'.format(report_path)) |
245 | 246 | report.write(data) |
246 | 247 | |
279 | 280 | scans_info['id'] = scans['id'] |
280 | 281 | scans_info['creation_date'] = scans['creation_date'] |
281 | 282 | scan_list.append(scans_info) |
282 | ||
283 | ||
283 | 284 | scan_list = sorted(scan_list,key=lambda scan:scan['creation_date']) |
284 | 285 | return scan_list |
285 | 286 | |
286 | 287 | def get_date(): |
287 | with open(os.path.join(CONST_FARADAY_HOME_PATH,'cscan','date.txt'),'r') as date_file: | |
288 | with (CONST_FARADAY_HOME_PATH/'cscan'/'date.txt').open('r') as date_file: | |
288 | 289 | date = date_file.read() |
289 | 290 | try: |
290 | 291 | date = int(date) |
295 | 296 | return date |
296 | 297 | |
297 | 298 | def set_date(date): |
298 | with open(os.path.join(CONST_FARADAY_HOME_PATH,'cscan','date.txt'),'w') as date_file: | |
299 | with (CONST_FARADAY_HOME_PATH/'cscan'/'date.txt').open('w') as date_file: | |
299 | 300 | date_file.write(str(date)) |
300 | 301 | |
301 | 302 | def get_version(): |
304 | 305 | |
305 | 306 | |
306 | 307 | def create_directory(): |
307 | if not os.path.exists(os.path.join(CONST_FARADAY_HOME_PATH,'cscan')): | |
308 | os.mkdir(os.path.join(CONST_FARADAY_HOME_PATH,'cscan')) | |
309 | if not os.path.exists(os.path.join(CONST_FARADAY_HOME_PATH,'cscan','date.txt')): | |
310 | open(os.path.join(CONST_FARADAY_HOME_PATH,'cscan','date.txt'),'w').close() | |
308 | if not (CONST_FARADAY_HOME_PATH / 'cscan').exists(): | |
309 | (CONST_FARADAY_HOME_PATH / 'cscan').mkdir() | |
310 | if not (CONST_FARADAY_HOME_PATH / 'cscan' / 'date.txt').exists(): | |
311 | (CONST_FARADAY_HOME_PATH /'cscan' / 'date.txt').open('w').close() | |
311 | 312 | |
312 | 313 | if __name__ == "__main__": |
313 | 314 | parser = argparse.ArgumentParser(description='nessus_client is develop for automating security testing') |
361 | 362 | if scan['creation_date'] > date: |
362 | 363 | set_date(scan['creation_date']) |
363 | 364 | print('Downloading scan. Id: {0}'.format(scan['id'])) |
364 | file_id = export(scan['id']) | |
365 | file_id = export(scan['id']) | |
365 | 366 | download(scan['id'], file_id) |
366 | 367 | else: |
367 | 368 | print('Scan up to date. Id: {0}'.format(scan['id'])) |
368 | 369 | |
369 | 370 | print('Logout') |
370 | logout()# I'm Py3⏎ | |
371 | logout()# I'm Py3 |
0 | from pathlib import Path | |
1 | import os | |
2 | import requests | |
3 | ||
4 | ||
5 | VERSION = os.environ.get('FARADAY_VERSION') | |
6 | ||
7 | ||
8 | def main(): | |
9 | release_data = dict() | |
10 | release_data["tag_name"] = f"v{VERSION}" | |
11 | release_data["name"] = f"v{VERSION}" | |
12 | with open( | |
13 | Path(__file__).parent.parent / 'CHANGELOG' / VERSION / 'white.md' | |
14 | ) as body_file: | |
15 | release_data["body"] = body_file.read() | |
16 | ||
17 | headers = {'Accept': 'application/vnd.github.v3+json'} | |
18 | res = requests.post( | |
19 | "https://api.github.com/repos/infobyte/faraday/releases", | |
20 | json=release_data, | |
21 | headers=headers | |
22 | ) | |
23 | res.raise_for_status() | |
24 | release_id = res.json()['id'] | |
25 | # TODO ADD THIS | |
26 | # for asset_file in ["rpm", "deb"]: | |
27 | # | |
28 | # res = requests.post( | |
29 | # "https://api.github.com/repos/infobyte/faraday/releases/" | |
30 | # f"{release_id}/assets", | |
31 | # headers=headers, | |
32 | # files={ | |
33 | # 'file': ( | |
34 | # asset_file, # TODO FIX NAME | |
35 | # open(asset_file, mode="rb"), # TODO FIX NAME | |
36 | # asset_file # TODO FIX TYPE | |
37 | # ) | |
38 | # } | |
39 | # ) | |
40 | # res.raise_for_status() | |
41 | ||
42 | ||
43 | if __name__ == '__main__': | |
44 | main() |
0 | #!/usr/bin/env python3 | |
1 | import subprocess | |
2 | ||
3 | ||
4 | def check(source_branch: str, target_branch: str) -> None: | |
5 | child = subprocess.run( | |
6 | [ | |
7 | "git", "diff", "--compact-summary", | |
8 | f"{source_branch}..{target_branch}", "faraday/migrations/" | |
9 | ], | |
10 | stdout=subprocess.PIPE | |
11 | ) | |
12 | assert child.returncode == 0, (child.stdout, child.returncode) | |
13 | assert b"insertion" not in child.stdout | |
14 | ||
15 | ||
16 | if __name__ == '__main__': | |
17 | check("origin/white/dev", "origin/pink/dev") | |
18 | check("origin/pink/dev", "origin/black/dev") |
4 | 4 | |
5 | 5 | parser = argparse.ArgumentParser() |
6 | 6 | parser.add_argument('--mode', choices=['diff', 'ls'], default='diff') |
7 | parser.add_argument('--local', action='store_true', default=False) | |
7 | 8 | args = parser.parse_args() |
8 | 9 | |
9 | 10 | ACTUAL_BRANCH = subprocess.run( |
12 | 13 | ).stdout.decode().strip() |
13 | 14 | |
14 | 15 | BRANCH_NAME = os.environ.get("CI_COMMIT_REF_NAME", ACTUAL_BRANCH) |
16 | if not args.local: | |
17 | BRANCH_NAME = f"origin/{BRANCH_NAME}" | |
18 | ||
15 | 19 | PINK_FILE = "faraday/server/api/modules/reports.py" |
16 | 20 | BLACK_FILE = "faraday/server/api/modules/jira.py" |
17 | 21 | |
42 | 46 | intersection = git_diff_intersection({BLACK_FILE}) |
43 | 47 | assert len(intersection) == 0, f"The {intersection} should not be in " \ |
44 | 48 | f"{BRANCH_NAME}" |
49 | assert child.returncode == 0, (child.stdout, child.returncode) |
2 | 2 | |
3 | 3 | [tool:pytest] |
4 | 4 | collect_ignore = ["_install/"] |
5 | ||
6 | [flake8] | |
7 | min_python_version = 3.7.0 | |
8 | ignore = | |
9 | # The list of error ignored is ordered by priority/easiness of the fix | |
10 | # First to fix | |
11 | ||
12 | ## Logic improve | |
13 | ### comparison to None should be 'if cond is None:' | |
14 | E711 | |
15 | ### ambiguous variable name 'x' | |
16 | E741 | |
17 | ### the backslash is redundant between brackets | |
18 | E502 | |
19 | ### 'x' imported but unused | |
20 | F401 | |
21 | ### comparison to False should be 'if cond is False:' or 'if not cond:' | |
22 | E712 | |
23 | ### redefinition of unused 'logger' from line 26 | |
24 | F811 | |
25 | ||
26 | ## Invalid escape sequence; probably fixed by adding r to specify regex str | |
27 | ### invalid escape sequence | |
28 | W605 | |
29 | ||
30 | ## Undefined uses, Not used, etc | |
31 | ### local variable 'x' is assigned to but never used | |
32 | F841 | |
33 | ||
34 | ## New lines | |
35 | ### no newline at end of file | |
36 | W292 | |
37 | ### Blank line at end of file | |
38 | W391 | |
39 | ### expected 1 blank line, found 0 | |
40 | E302 | |
41 | ### expected 2 blank line, found 1 | |
42 | E301 | |
43 | ### line break before binary operator | |
44 | W503 | |
45 | ### line break after binary operator | |
46 | W504 | |
47 | ### expected 2 blank lines after class or function definition, found 1 | |
48 | E305 | |
49 | ### too many blank lines (N) | |
50 | E303 | |
51 | ||
52 | ## Spaces | |
53 | ### whitespace after '[' | |
54 | E201 | |
55 | ### whitespace before ']' | |
56 | E202 | |
57 | ### missing whitespace after ',' | |
58 | E231 | |
59 | ### multiple spaces after operator | |
60 | E222 | |
61 | ### missing whitespace around arithmetic operator | |
62 | E226 | |
63 | ### unexpected spaces around keyword / parameter equals | |
64 | E251 | |
65 | ### missing whitespace around operator | |
66 | E225 | |
67 | ### blank line contains whitespace | |
68 | W293 | |
69 | ### trailing whitespace | |
70 | W291 | |
71 | ### multiple spaces after ',' | |
72 | E241 | |
73 | ||
74 | ## Block comment | |
75 | ### at least two spaces before inline comment | |
76 | E261 | |
77 | ### inline comment should start with '# ' | |
78 | E262 | |
79 | ### block comment should start with '# ' | |
80 | E265 | |
81 | ### E266 too many leading '#' for block comment | |
82 | E266 | |
83 | ||
84 | ## Visual | |
85 | ### continuation line missing indentation or outdented | |
86 | E122 | |
87 | ### continuation line over-indented for hanging indent | |
88 | E126 | |
89 | ### continuation line over-indented for visual indent | |
90 | E127 | |
91 | ### continuation line under-indented for visual indent | |
92 | E128 | |
93 | ### visually indented line with same indent as next logical line | |
94 | E129 | |
95 | ### continuation line unaligned for hanging indent | |
96 | E131 | |
97 | ### continuation line with same indent as next logical line | |
98 | E125 | |
99 | ### closing bracket does not match visual indentation | |
100 | E124 | |
101 | ### closing bracket does not match indentation of opening bracket's line | |
102 | E123 | |
103 | ### continuation line under-indented for hanging indent | |
104 | E121 | |
105 | ### > 79 characters lines | |
106 | E501 | |
107 | ||
108 | ## Imports | |
109 | ### module level import not at top of file | |
110 | E402 | |
111 | ||
112 | # Last to fix | |
113 | exclude = | |
114 | # tests was not originally in the pylint scope | |
115 | # tests | |
116 | # scripts has cscan for now, when deleted, we should add it | |
117 | CHANGELOG | |
118 | doc | |
119 | # scripts has cscan for now, when deleted, we should add it | |
120 | scripts |
6 | 6 | |
7 | 7 | # Always prefer setuptools over distutils |
8 | 8 | from setuptools import setup, find_packages |
9 | from os import path | |
10 | 9 | # io.open is needed for projects that support Python 2.7 |
11 | 10 | # It ensures open() defaults to text mode with universal newlines, |
12 | 11 | # and accepts an argument to specify the text encoding |
13 | 12 | # Python 3 only projects can skip this import |
14 | 13 | from io import open |
15 | 14 | from re import search |
16 | ||
17 | here = path.abspath(path.dirname(__file__)) | |
18 | 15 | |
19 | 16 | # Get the long description from the README file |
20 | 17 | long_description = """Faraday introduces a new concept - IPE (Integrated Penetration-Test Environment) a multiuser Penetration test IDE. Designed for distributing, indexing, and analyzing the data generated during a security audit. |
250 | 247 | 'Source': 'https://github.com/infobyte/faraday/', |
251 | 248 | }, |
252 | 249 | setup_requires=['pytest-runner'], |
253 | tests_require=['pytest', 'flask'] + dev_required, | |
250 | tests_require=dev_required, | |
254 | 251 | ) |
255 | ||
256 | ||
257 | # I'm Py3 |
6 | 6 | |
7 | 7 | from tempfile import NamedTemporaryFile |
8 | 8 | |
9 | import os | |
10 | import sys | |
11 | 9 | import json |
12 | 10 | import inspect |
13 | 11 | import pytest |
14 | 12 | from factory import Factory |
15 | 13 | from flask.testing import FlaskClient |
16 | 14 | from flask_principal import Identity, identity_changed |
15 | from pathlib import Path | |
16 | from pytest_factoryboy import register | |
17 | 17 | from sqlalchemy import event |
18 | from pytest_factoryboy import register | |
19 | ||
20 | sys.path.append(os.path.abspath(os.getcwd())) | |
18 | ||
21 | 19 | from faraday.server.app import create_app |
22 | 20 | from faraday.server.models import db |
23 | 21 | from tests import factories |
24 | 22 | |
25 | 23 | |
26 | TEST_BASE = os.path.abspath(os.path.dirname(__file__)) | |
27 | TEST_DATA = os.path.join(TEST_BASE, 'data') | |
24 | TEST_DATA_PATH = Path(__file__).parent / 'data' | |
28 | 25 | |
29 | 26 | TEMPORATY_SQLITE = NamedTemporaryFile() |
30 | 27 | # Discover factories to automatically register them to pytest-factoryboy and to |
79 | 76 | def pytest_addoption(parser): |
80 | 77 | # currently for tests using sqlite and memory have problem while using transactions |
81 | 78 | # we need to review sqlite configuraitons for persistence using PRAGMA. |
82 | parser.addoption('--connection-string', default='sqlite:////{0}'.format(TEMPORATY_SQLITE.name), | |
79 | parser.addoption('--connection-string', default=f'sqlite:////{TEMPORATY_SQLITE.name}', | |
83 | 80 | help="Database connection string. Defaults to in-memory " |
84 | 81 | "sqlite if not specified:") |
85 | 82 | parser.addoption('--ignore-nplusone', action='store_true', |
228 | 225 | from flask import g |
229 | 226 | try: |
230 | 227 | del g.csrf_token |
231 | except: | |
228 | except (NameError, AttributeError): | |
232 | 229 | pass |
233 | 230 | |
234 | 231 | return app.test_client() |
293 | 290 | dialect = db.session.bind.dialect.name |
294 | 291 | if request.node.get_closest_marker('skip_sql_dialect'): |
295 | 292 | if request.node.get_closest_marker('skip_sql_dialect').args[0] == dialect: |
296 | pytest.skip('Skipped dialect is {}'.format(dialect)) | |
293 | pytest.skip(f'Skipped dialect is {dialect}') | |
297 | 294 | |
298 | 295 | |
299 | 296 | @pytest.fixture |
0 | #!/usr/bin/python | |
1 | ''' | |
2 | Faraday Penetration Test IDE | |
3 | Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/) | |
4 | See the file 'doc/LICENSE' for the license information | |
5 | ||
6 | ''' | |
7 | ||
8 | import unittest | |
9 | import sys | |
10 | sys.path.append('.') | |
11 | import faraday.client.model.controller | |
12 | import faraday.client.managers.mapper_manager | |
13 | from mockito import mock | |
14 | from faraday.client.persistence.mappers.abstract_mapper import NullPersistenceManager | |
15 | from faraday.client.model.hosts import Host, ModelObjectVuln | |
16 | from faraday.client.model.diff import ModelObjectDiff | |
17 | ||
18 | import tests.common as test_utils | |
19 | ||
20 | ||
21 | class DiffTests(unittest.TestCase): | |
22 | ||
23 | def setUp(self): | |
24 | pass | |
25 | ||
26 | def tearDown(self): | |
27 | pass | |
28 | ||
29 | def test_diff_between_equal_hosts(self): | |
30 | """ | |
31 | This test case creates a host and the compares it | |
32 | with another equal host using the ModelObjectDiff class | |
33 | """ | |
34 | h1 = Host(name='host1', os='Windows') | |
35 | h2 = Host(name='host1', os='Windows') | |
36 | ||
37 | diff = ModelObjectDiff(h1, h2) | |
38 | ||
39 | self.assertFalse(diff.existDiff()) | |
40 | ||
41 | def test_diff_between_different_hosts(self): | |
42 | """ | |
43 | This test case creates a host and the compares it | |
44 | with another different host using the ModelObjectDiff class | |
45 | """ | |
46 | h1 = Host(name='host1', os='Windows') | |
47 | h2 = Host(name='host1', os='Linux') | |
48 | ||
49 | diff = ModelObjectDiff(h1, h2) | |
50 | ||
51 | self.assertTrue(diff.existDiff()) | |
52 | ||
53 | def test_diff_between_equal_vulns_with_different_confirmed(self): | |
54 | v1 = ModelObjectVuln(name="vuln1", | |
55 | desc="description", | |
56 | severity="high", | |
57 | confirmed=True) | |
58 | v2 = ModelObjectVuln(name="vuln1", | |
59 | desc="description", severity="high") | |
60 | ||
61 | self.assertFalse(v1.addUpdate(v2), | |
62 | "The conflict should be resolved automatically") | |
63 | self.assertTrue(v1.confirmed, | |
64 | "The vuln should be still confirmed") | |
65 | ||
66 | ||
67 | class UpdatesTests(unittest.TestCase): | |
68 | ||
69 | def setUp(self): | |
70 | self._mappers_manager = faraday.client.managers.mapper_manager.MapperManager() | |
71 | self._persistence_manager = NullPersistenceManager() | |
72 | self._mappers_manager.createMappers(self._persistence_manager) | |
73 | self.faraday.client.model.controller = model.controller.ModelController( | |
74 | mock(), self._mappers_manager) | |
75 | ||
76 | def tearDown(self): | |
77 | pass | |
78 | ||
79 | def test_add_host_and_generate_solvable_update(self): | |
80 | """ | |
81 | This test case creates a host within the Model Controller context | |
82 | and then creates another with the same key elements, but different | |
83 | non-key attributes with default value to generate an automatic | |
84 | solvable update | |
85 | """ | |
86 | # When | |
87 | hostname = 'host' | |
88 | host1a = test_utils.create_host(self, host_name=hostname, os='windows') | |
89 | ||
90 | host = self._mappers_manager.find(host1a.getID()) | |
91 | self.assertEquals( | |
92 | host.getOS(), | |
93 | 'windows', | |
94 | 'Host\'s OS should be windows') | |
95 | ||
96 | # Then, we generate an update | |
97 | host1b = test_utils.create_host(self, host_name=hostname, os='unknown') | |
98 | ||
99 | self.assertEquals( | |
100 | host1a.getID(), | |
101 | host1b.getID(), | |
102 | 'Both hosts should have the same id') | |
103 | ||
104 | self.assertEquals( | |
105 | len(self.faraday.client.model.controller.getConflicts()), | |
106 | 0, | |
107 | 'Update was generated') | |
108 | ||
109 | host = self._mappers_manager.find(host1a.getID()) | |
110 | ||
111 | self.assertEquals( | |
112 | host.getOS(), | |
113 | 'windows', | |
114 | 'Host\'s OS should still be windows') | |
115 | ||
116 | def test_add_host_and_generate_solvable_update_with_edition(self): | |
117 | """ | |
118 | This test case creates a host with a default value in a non-key | |
119 | attrribute within the Model Controller context and then creates | |
120 | another with the same key elements, but different non-key | |
121 | attributes to generate an automatic solvable update | |
122 | """ | |
123 | # When | |
124 | hostname = 'host' | |
125 | host1a = test_utils.create_host(self, host_name=hostname, os='unknown') | |
126 | ||
127 | host = self._mappers_manager.find(host1a.getID()) | |
128 | ||
129 | self.assertEquals( | |
130 | host.getOS(), | |
131 | 'unknown', | |
132 | 'Host\'s OS should be unknown') | |
133 | ||
134 | # Then, we generate an update | |
135 | host1b = test_utils.create_host(self, host_name=hostname, os='windows') | |
136 | ||
137 | self.assertEquals( | |
138 | host1a.getID(), | |
139 | host1b.getID(), | |
140 | 'Both hosts should have the same id') | |
141 | ||
142 | self.assertEquals( | |
143 | len(self.faraday.client.model.controller.getConflicts()), | |
144 | 0, | |
145 | 'Update was generated') | |
146 | ||
147 | host = self._mappers_manager.find(host1a.getID()) | |
148 | ||
149 | self.assertEquals( | |
150 | host.getOS(), | |
151 | 'windows', | |
152 | 'Host\'s OS should now be windows') | |
153 | ||
154 | def test_add_host_and_generate_unsolvable_update(self): | |
155 | """ | |
156 | This test case creates a host within the Model Controller | |
157 | context and then creates another with the same key elements, | |
158 | but different non-key attributes to generate an update to | |
159 | be resolved by the user | |
160 | """ | |
161 | # When | |
162 | hostname = 'host' | |
163 | host1a = test_utils.create_host(self, host_name=hostname, os='windows') | |
164 | ||
165 | host = self._mappers_manager.find(host1a.getID()) | |
166 | ||
167 | self.assertEquals( | |
168 | host.getOS(), | |
169 | 'windows', | |
170 | 'Host\'s OS should be windows') | |
171 | ||
172 | # Then, we generate an update | |
173 | host1b = test_utils.create_host(self, host_name=hostname, os='linux') | |
174 | ||
175 | self.assertEquals( | |
176 | host1a.getID(), | |
177 | host1b.getID(), | |
178 | 'Both hosts should have the same id') | |
179 | ||
180 | self.assertEquals( | |
181 | len(self.faraday.client.model.controller.getConflicts()), | |
182 | 1, | |
183 | 'Update was not generated') | |
184 | ||
185 | host = self._mappers_manager.find(host1a.getID()) | |
186 | ||
187 | self.assertEquals( | |
188 | host.getOS(), | |
189 | 'windows', | |
190 | 'Host\'s OS should still be windows') | |
191 | ||
192 | self.assertEquals( | |
193 | len(host.getUpdates()), | |
194 | 1, | |
195 | 'The host should have a pending update') | |
196 | ||
197 | ||
198 | if __name__ == '__main__': | |
199 | unittest.main() | |
200 | ||
201 | ||
202 | # I'm Py3 |
53 | 53 | RuleAction) |
54 | 54 | |
55 | 55 | # Make partials for start and end date. End date must be after start date |
56 | FuzzyStartTime = lambda: ( | |
57 | FuzzyNaiveDateTime( | |
56 | def FuzzyStartTime(): | |
57 | return ( | |
58 | FuzzyNaiveDateTime( | |
58 | 59 | datetime.datetime.now() - datetime.timedelta(days=40), |
59 | 60 | datetime.datetime.now() - datetime.timedelta(days=20), |
60 | ) | |
61 | ) | |
62 | FuzzyEndTime = lambda: ( | |
63 | FuzzyNaiveDateTime( | |
64 | datetime.datetime.now() - datetime.timedelta(days=19), | |
65 | datetime.datetime.now() | |
66 | ) | |
67 | ) | |
61 | ) | |
62 | ) | |
63 | ||
64 | def FuzzyEndTime(): | |
65 | return ( | |
66 | FuzzyNaiveDateTime( | |
67 | datetime.datetime.now() - datetime.timedelta(days=19), | |
68 | datetime.datetime.now() | |
69 | ) | |
70 | ) | |
68 | 71 | |
69 | 72 | all_unicode = ''.join(chr(i) for i in range(65536)) |
70 | 73 | UNICODE_LETTERS = ''.join(c for c in all_unicode if unicodedata.category(c) == 'Lu' or unicodedata.category(c) == 'Ll') |
493 | 496 | workspace = factory.LazyAttribute( |
494 | 497 | lambda agent_execution: agent_execution.executor.agent.workspaces[0] |
495 | 498 | ) |
499 | command = factory.SubFactory( | |
500 | CommandFactory, | |
501 | workspace=factory.SelfAttribute("..workspace"), | |
502 | end_date=None | |
503 | ) | |
496 | 504 | |
497 | 505 | class Meta: |
498 | 506 | model = AgentExecution |
3 | 3 | See the file 'doc/LICENSE' for the license information |
4 | 4 | |
5 | 5 | ''' |
6 | import os | |
7 | 6 | import pytest |
8 | 7 | from faraday.server.models import File |
9 | 8 | from depot.manager import DepotManager |
10 | 9 | |
11 | CURRENT_PATH = os.path.dirname(os.path.abspath(__file__)) | |
10 | from tests.conftest import TEST_DATA_PATH | |
12 | 11 | |
13 | 12 | |
14 | 13 | @pytest.fixture |
15 | 14 | def depotfile(): |
16 | 15 | depot = DepotManager.get('default') |
17 | path = os.path.join(CURRENT_PATH, '../data', 'faraday.png') | |
18 | with open(path, 'rb') as fp: | |
16 | png_file_path = TEST_DATA_PATH / 'faraday.png' | |
17 | ||
18 | with png_file_path.open('rb') as fp: | |
19 | 19 | fileid = depot.create(fp, 'faraday.png', 'image/png') |
20 | 20 | return fileid |
21 | 21 | |
55 | 55 | assert len(vulnerability_web.evidence) == 1 |
56 | 56 | assert vulnerability_web.evidence[0].object_type == 'vulnerability' |
57 | 57 | assert vulnerability_web.evidence[0].object_id == vulnerability_web.id |
58 | # I'm Py3⏎ | |
58 | # I'm Py3 |
24 | 24 | session.commit() |
25 | 25 | |
26 | 26 | res = test_client.get( |
27 | '/v2/ws/{ws_name}/activities/' | |
28 | .format(ws_name=ws.name) | |
27 | f'/v2/ws/{ws.name}/activities/' | |
29 | 28 | ) |
30 | 29 | |
31 | 30 | assert res.status_code == 200 |
46 | 45 | itime = 1544745600.0 |
47 | 46 | data = { |
48 | 47 | 'command': command.command, |
49 | 'tool' : command.tool, | |
48 | 'tool': command.tool, | |
50 | 49 | 'itime': itime |
51 | 50 | |
52 | 51 | } |
53 | 52 | |
54 | 53 | res = test_client.put( |
55 | '/v2/ws/{ws_name}/activities/{id}/' | |
56 | .format(ws_name=ws.name, id=command.id), | |
54 | f'/v2/ws/{ws.name}/activities/{command.id}/', | |
57 | 55 | data=data, |
58 | 56 | ) |
59 | 57 | assert res.status_code == 200 |
7 | 7 | import pytest |
8 | 8 | |
9 | 9 | from faraday.server.api.modules.agent import AgentWithWorkspacesView, AgentView |
10 | from faraday.server.models import Agent | |
10 | from faraday.server.models import Agent, Command | |
11 | 11 | from tests.factories import AgentFactory, WorkspaceFactory, ExecutorFactory |
12 | 12 | from tests.test_api_non_workspaced_base import ReadOnlyAPITests |
13 | 13 | from tests.test_api_workspaced_base import ReadOnlyMultiWorkspacedAPITests |
520 | 520 | json=payload |
521 | 521 | ) |
522 | 522 | assert res.status_code == 200 |
523 | command_id = res.json["command_id"] | |
524 | command = Command.query.filter(Command.workspace_id == self.workspace.id).one() | |
525 | assert command_id == command.id | |
523 | 526 | |
524 | 527 | def test_invalid_json_on_executorData_breaks_the_api(self, csrf_token, |
525 | 528 | session, test_client): |
11 | 11 | Vulnerability, |
12 | 12 | VulnerabilityGeneric, |
13 | 13 | VulnerabilityWeb, |
14 | Workspace | |
14 | 15 | ) |
15 | 16 | from faraday.server.api.modules import bulk_create as bc |
17 | from tests.factories import CustomFieldsSchemaFactory | |
16 | 18 | |
17 | 19 | host_data = { |
18 | 20 | "ip": "127.0.0.1", |
64 | 66 | 'user': 'root', |
65 | 67 | 'hostname': 'pc', |
66 | 68 | 'start_date': '2014-12-22T03:12:58.019077+00:00', |
67 | 'duration': 30, | |
68 | 69 | } |
69 | 70 | |
70 | 71 | |
72 | 73 | return model.query.filter(model.workspace == workspace).count() |
73 | 74 | |
74 | 75 | |
76 | def new_empty_command(workspace: Workspace): | |
77 | command = Command() | |
78 | command.workspace = workspace | |
79 | command.start_date = datetime.now() | |
80 | command.import_source = 'report' | |
81 | command.tool = "In progress" | |
82 | command.command = "In progress" | |
83 | db.session.commit() | |
84 | return command | |
85 | ||
86 | ||
75 | 87 | def test_create_host(session, workspace): |
76 | 88 | assert count(Host, workspace) == 0 |
77 | bc.bulk_create(workspace, dict(hosts=[host_data])) | |
89 | bc.bulk_create(workspace, None, dict(hosts=[host_data])) | |
78 | 90 | db.session.commit() |
79 | 91 | host = Host.query.filter(Host.workspace == workspace).one() |
80 | 92 | assert host.ip == "127.0.0.1" |
83 | 95 | |
84 | 96 | def test_create_duplicated_hosts(session, workspace): |
85 | 97 | assert count(Host, workspace) == 0 |
86 | bc.bulk_create(workspace, dict(hosts=[host_data, host_data])) | |
98 | bc.bulk_create(workspace, None, dict(hosts=[host_data, host_data])) | |
87 | 99 | db.session.commit() |
88 | 100 | assert count(Host, workspace) == 1 |
89 | 101 | |
90 | 102 | |
91 | 103 | def test_create_host_add_hostnames(session, workspace): |
92 | 104 | assert count(Host, workspace) == 0 |
93 | bc.bulk_create(workspace, dict(hosts=[host_data])) | |
105 | bc.bulk_create(workspace, None, dict(hosts=[host_data])) | |
94 | 106 | db.session.commit() |
95 | 107 | host_copy = host_data.copy() |
96 | 108 | host_copy['hostnames'] = ["test3.org"] |
97 | bc.bulk_create(workspace, dict(hosts=[host_copy])) | |
109 | bc.bulk_create(workspace, None, dict(hosts=[host_copy])) | |
98 | 110 | db.session.commit() |
99 | 111 | host = Host.query.filter(Host.workspace == workspace).one() |
100 | 112 | assert host.ip == "127.0.0.1" |
109 | 121 | "description": host.description, |
110 | 122 | "hostnames": [hn.name for hn in host.hostnames] |
111 | 123 | } |
112 | bc.bulk_create(host.workspace, dict(hosts=[data])) | |
124 | bc.bulk_create(host.workspace, None, dict(hosts=[data])) | |
113 | 125 | assert count(Host, host.workspace) == 1 |
114 | 126 | |
115 | 127 | |
116 | 128 | def test_create_host_with_services(session, workspace): |
117 | 129 | host_data_ = host_data.copy() |
118 | 130 | host_data_['services'] = [service_data] |
119 | bc.bulk_create(workspace, dict(hosts=[host_data_])) | |
131 | bc.bulk_create(workspace, None, dict(hosts=[host_data_])) | |
120 | 132 | assert count(Host, workspace) == 1 |
121 | 133 | assert count(Service, workspace) == 1 |
122 | 134 | service = Service.query.filter(Service.workspace == workspace).one() |
193 | 205 | vuln_web_data_ = vuln_data.copy() |
194 | 206 | service_data_['vulnerabilities'] = [vuln_web_data_] |
195 | 207 | host_data_['services'] = [service_data_] |
196 | bc.bulk_create(service.workspace, dict(command=command_data, hosts=[host_data_])) | |
208 | command = new_empty_command(service.workspace) | |
209 | bc.bulk_create( | |
210 | service.workspace, | |
211 | command, | |
212 | dict( | |
213 | command=command_data, | |
214 | hosts=[host_data_] | |
215 | ) | |
216 | ) | |
197 | 217 | assert count(Vulnerability, service.workspace) == 1 |
198 | 218 | vuln = service.workspace.vulnerabilities[0] |
199 | 219 | assert vuln.tool == vuln_data['tool'] |
206 | 226 | vuln_web_data_.pop('tool') |
207 | 227 | service_data_['vulnerabilities'] = [vuln_web_data_] |
208 | 228 | host_data_['services'] = [service_data_] |
209 | bc.bulk_create(service.workspace, dict(command=command_data, hosts=[host_data_])) | |
229 | command = new_empty_command(service.workspace) | |
230 | bc.bulk_create( | |
231 | service.workspace, | |
232 | command, | |
233 | dict(command=command_data, hosts=[host_data_]) | |
234 | ) | |
210 | 235 | assert count(Vulnerability, service.workspace) == 1 |
211 | 236 | vuln = service.workspace.vulnerabilities[0] |
212 | 237 | assert vuln.tool == command_data['tool'] |
262 | 287 | def test_create_host_with_vuln(session, workspace): |
263 | 288 | host_data_ = host_data.copy() |
264 | 289 | host_data_['vulnerabilities'] = [vuln_data] |
265 | bc.bulk_create(workspace, dict(hosts=[host_data_])) | |
290 | bc.bulk_create(workspace, None, dict(hosts=[host_data_])) | |
266 | 291 | assert count(Host, workspace) == 1 |
267 | 292 | host = workspace.hosts[0] |
268 | 293 | assert count(Vulnerability, workspace) == 1 |
274 | 299 | def test_create_host_with_cred(session, workspace): |
275 | 300 | host_data_ = host_data.copy() |
276 | 301 | host_data_['credentials'] = [credential_data] |
277 | bc.bulk_create(workspace, dict(hosts=[host_data_])) | |
302 | bc.bulk_create(workspace, None, dict(hosts=[host_data_])) | |
278 | 303 | assert count(Host, workspace) == 1 |
279 | 304 | host = workspace.hosts[0] |
280 | 305 | assert count(Credential, workspace) == 1 |
359 | 384 | assert vuln.status_code == 200 |
360 | 385 | |
361 | 386 | |
362 | def test_create_command(session, workspace): | |
363 | bc.bulk_create(workspace, dict(command=command_data, hosts=[])) | |
387 | @pytest.mark.parametrize("duration", [None, "30"]) | |
388 | def test_update_command(session, workspace, duration): | |
389 | command = new_empty_command(workspace) | |
390 | assert count(Command, workspace) == 1 | |
391 | command_data_ = command_data.copy() | |
392 | if duration is not None: | |
393 | command_data_["duration"] = duration | |
394 | bc.bulk_create(workspace, command, dict(command=command_data_, hosts=[])) | |
364 | 395 | assert count(Command, workspace) == 1 |
365 | 396 | command = workspace.commands[0] |
366 | 397 | assert command.tool == 'pytest' |
367 | 398 | assert command.user == 'root' |
368 | assert (command.end_date - command.start_date).microseconds == 30 | |
369 | ||
370 | ||
371 | def test_creates_command_object(session, workspace): | |
399 | if duration is not None: | |
400 | assert (command.end_date - command.start_date).microseconds == 30 | |
401 | else: | |
402 | assert command.end_date is not None | |
403 | ||
404 | ||
405 | def test_updates_command_object(session, workspace): | |
372 | 406 | host_data_ = host_data.copy() |
373 | 407 | service_data_ = service_data.copy() |
374 | 408 | vuln_web_data_ = vuln_data.copy() |
378 | 412 | host_data_['services'] = [service_data_] |
379 | 413 | host_data_['vulnerabilities'] = [vuln_data] |
380 | 414 | host_data_['credentials'] = [credential_data] |
381 | bc.bulk_create(workspace, dict(command=command_data, hosts=[host_data_])) | |
415 | command = new_empty_command(workspace) | |
416 | bc.bulk_create( | |
417 | workspace, | |
418 | command, | |
419 | dict(command=command_data, hosts=[host_data_]) | |
420 | ) | |
382 | 421 | |
383 | 422 | command = workspace.commands[0] |
384 | 423 | host = workspace.hosts[0] |
511 | 550 | |
512 | 551 | data['command'] = command_data.copy() |
513 | 552 | |
514 | bc.bulk_create(command.workspace, data) | |
553 | command2 = new_empty_command(command.workspace) | |
554 | bc.bulk_create(command.workspace, command2, data) | |
515 | 555 | assert count(Command, command.workspace) == 2 |
516 | 556 | |
517 | 557 | new_command = Command.query.filter_by(tool='pytest').one() |
531 | 571 | def test_bulk_create_endpoint(session, workspace, test_client, logged_user): |
532 | 572 | assert count(Host, workspace) == 0 |
533 | 573 | assert count(VulnerabilityGeneric, workspace) == 0 |
534 | url = 'v2/ws/{}/bulk_create/'.format(workspace.name) | |
574 | url = f'v2/ws/{workspace.name}/bulk_create/' | |
535 | 575 | host_data_ = host_data.copy() |
536 | 576 | service_data_ = service_data.copy() |
537 | 577 | service_data_['vulnerabilities'] = [vuln_data] |
538 | 578 | host_data_['services'] = [service_data_] |
539 | 579 | host_data_['credentials'] = [credential_data] |
540 | 580 | host_data_['vulnerabilities'] = [vuln_data] |
541 | res = test_client.post(url, data=dict(hosts=[host_data_])) | |
581 | res = test_client.post( | |
582 | url, | |
583 | data=dict(hosts=[host_data_], command=command_data) | |
584 | ) | |
542 | 585 | assert res.status_code == 201, res.json |
543 | 586 | assert count(Host, workspace) == 1 |
544 | 587 | assert count(Service, workspace) == 1 |
545 | 588 | assert count(Vulnerability, workspace) == 2 |
589 | assert count(Command, workspace) == 1 | |
546 | 590 | host = Host.query.filter(Host.workspace == workspace).one() |
547 | 591 | assert host.ip == "127.0.0.1" |
548 | 592 | assert host.creator_id == logged_user.id |
554 | 598 | assert service.creator_id == logged_user.id |
555 | 599 | credential = Credential.query.filter(Credential.workspace == workspace).one() |
556 | 600 | assert credential.creator_id == logged_user.id |
601 | command = Command.query.filter(Credential.workspace == workspace).one() | |
602 | assert command.creator_id == logged_user.id | |
603 | assert res.json["command_id"] == command.id | |
557 | 604 | |
558 | 605 | |
559 | 606 | @pytest.mark.usefixtures('logged_user') |
591 | 638 | |
592 | 639 | @pytest.mark.usefixtures('logged_user') |
593 | 640 | def test_bulk_create_endpoint_without_host_ip(session, workspace, test_client): |
594 | url = 'v2/ws/{}/bulk_create/'.format(workspace.name) | |
641 | url = f'v2/ws/{workspace.name}/bulk_create/' | |
595 | 642 | host_data_ = host_data.copy() |
596 | 643 | host_data_.pop('ip') |
597 | 644 | res = test_client.post(url, data=dict(hosts=[host_data_])) |
599 | 646 | |
600 | 647 | |
601 | 648 | def test_bulk_create_endpoints_fails_without_auth(session, workspace, test_client): |
602 | url = 'v2/ws/{}/bulk_create/'.format(workspace.name) | |
649 | url = f'v2/ws/{workspace.name}/bulk_create/' | |
603 | 650 | res = test_client.post(url, data=dict(hosts=[host_data])) |
604 | 651 | assert res.status_code == 401 |
605 | 652 | assert count(Host, workspace) == 0 |
608 | 655 | @pytest.mark.parametrize('token_type', ['agent', 'token']) |
609 | 656 | def test_bulk_create_endpoints_fails_with_invalid_token( |
610 | 657 | session, token_type, workspace, test_client): |
611 | url = 'v2/ws/{}/bulk_create/'.format(workspace.name) | |
658 | url = f'v2/ws/{workspace.name}/bulk_create/' | |
612 | 659 | res = test_client.post( |
613 | 660 | url, |
614 | 661 | data=dict(hosts=[host_data]), |
615 | headers=[("authorization", "{} 1234".format(token_type))] | |
662 | headers=[("authorization", f"{token_type} 1234")] | |
616 | 663 | ) |
617 | 664 | if token_type == 'token': |
618 | 665 | # TODO change expected status code to 403 |
630 | 677 | session.add(agent) |
631 | 678 | session.commit() |
632 | 679 | assert agent.token |
633 | url = 'v2/ws/{}/bulk_create/'.format(second_workspace.name) | |
680 | url = f'v2/ws/{second_workspace.name}/bulk_create/' | |
634 | 681 | res = test_client.post( |
635 | 682 | url, |
636 | 683 | data=dict(hosts=[host_data]), |
637 | headers=[("authorization", "agent {}".format(agent.token))] | |
684 | headers=[("authorization", f"agent {agent.token}")] | |
638 | 685 | ) |
639 | 686 | assert res.status_code == 404 |
640 | 687 | assert b'No such workspace' in res.data |
647 | 694 | session.add(agent) |
648 | 695 | session.commit() |
649 | 696 | assert agent.token |
650 | url = 'v2/ws/{}/bulk_create/'.format("im_a_incorrect_ws") | |
697 | url = "v2/ws/im_a_incorrect_ws/bulk_create/" | |
651 | 698 | res = test_client.post( |
652 | 699 | url, |
653 | 700 | data=dict(hosts=[host_data]), |
654 | headers=[("authorization", "agent {}".format(agent.token))] | |
701 | headers=[("authorization", f"agent {agent.token}")] | |
655 | 702 | ) |
656 | 703 | assert res.status_code == 404 |
657 | 704 | assert b'No such workspace' in res.data |
664 | 711 | session.commit() |
665 | 712 | for workspace in agent.workspaces: |
666 | 713 | assert count(Host, workspace) == 0 |
667 | url = 'v2/ws/{}/bulk_create/'.format(workspace.name) | |
714 | url = f'v2/ws/{workspace.name}/bulk_create/' | |
668 | 715 | res = test_client.post( |
669 | 716 | url, |
670 | 717 | data=dict(hosts=[host_data]), |
671 | headers=[("authorization", "agent {}".format(agent.token))] | |
718 | headers=[("authorization", f"agent {agent.token}")] | |
672 | 719 | ) |
673 | 720 | assert res.status_code == 400 |
674 | 721 | assert b"\'execution_id\' argument expected" in res.data |
676 | 723 | assert count(Command, workspace) == 0 |
677 | 724 | |
678 | 725 | |
679 | def test_bulk_create_endpoint_with_agent_token(session, agent_execution, | |
726 | @pytest.mark.parametrize('start_date', [None, datetime.now()]) | |
727 | @pytest.mark.parametrize('duration', [None, 1200]) | |
728 | def test_bulk_create_endpoint_with_agent_token(session, | |
680 | 729 | test_client, |
681 | agent_execution_factory): | |
730 | agent_execution_factory, | |
731 | start_date, duration): | |
732 | agent_execution = agent_execution_factory.create() | |
682 | 733 | agent = agent_execution.executor.agent |
683 | 734 | extra_agent_execution = agent_execution_factory.create() |
684 | 735 | |
686 | 737 | agent_execution.executor.parameters_metadata = {} |
687 | 738 | agent_execution.parameters_data = {} |
688 | 739 | agent_execution.workspace = workspace |
740 | agent_execution.command.workspace = workspace | |
689 | 741 | session.add(agent_execution) |
690 | 742 | session.add(extra_agent_execution) |
691 | 743 | session.commit() |
692 | 744 | |
693 | assert count(Host, workspace) == 0 | |
694 | url = 'v2/ws/{}/bulk_create/'.format(workspace.name) | |
745 | command_data = {} | |
746 | if start_date: | |
747 | command_data.update({ | |
748 | 'tool': agent.name, # Agent name | |
749 | 'command': agent_execution.executor.name, | |
750 | 'user': '', | |
751 | 'hostname': '', | |
752 | 'params': '', | |
753 | 'import_source': 'agent', | |
754 | 'start_date': str(start_date) | |
755 | }) | |
756 | if duration: | |
757 | command_data.update({ | |
758 | 'tool': agent.name, # Agent name | |
759 | 'command': agent_execution.executor.name, | |
760 | 'user': '', | |
761 | 'hostname': '', | |
762 | 'params': '', | |
763 | 'import_source': 'agent', | |
764 | 'duration': str(duration) | |
765 | }) | |
766 | ||
767 | data_kwargs = { | |
768 | "hosts": [host_data], | |
769 | "execution_id": -1 | |
770 | } | |
771 | if command_data: | |
772 | data_kwargs["command"] = command_data | |
773 | ||
774 | initial_host_count = Host.query.filter(Host.workspace == workspace and Host.creator_id is None).count() | |
775 | assert count(Command, workspace) == 1 | |
776 | url = f'v2/ws/{workspace.name}/bulk_create/' | |
695 | 777 | res = test_client.post( |
696 | 778 | url, |
697 | data=dict(hosts=[host_data], execution_id=-1), | |
698 | headers=[("authorization", "agent {}".format(agent.token))] | |
779 | data=dict(**data_kwargs), | |
780 | headers=[("authorization", f"agent {agent.token}")] | |
699 | 781 | ) |
700 | 782 | assert res.status_code == 400 |
701 | 783 | |
702 | assert count(Host, workspace) == 0 | |
703 | assert count(Command, workspace) == 0 | |
784 | assert Host.query.filter(Host.workspace == workspace and Host.creator_id is None).count() == initial_host_count | |
785 | assert count(Command, workspace) == 1 | |
786 | data_kwargs["execution_id"] = extra_agent_execution.id | |
704 | 787 | res = test_client.post( |
705 | 788 | url, |
706 | data=dict(hosts=[host_data], execution_id=extra_agent_execution.id), | |
707 | headers=[("authorization", "agent {}".format(agent.token))] | |
789 | data=dict(**data_kwargs), | |
790 | headers=[("authorization", f"agent {agent.token}")] | |
708 | 791 | ) |
709 | 792 | assert res.status_code == 400 |
793 | assert Host.query.filter(Host.workspace == workspace and Host.creator_id is None).count() == initial_host_count | |
794 | assert count(Command, workspace) == 1 | |
795 | data_kwargs["execution_id"] = agent_execution.id | |
796 | res = test_client.post( | |
797 | url, | |
798 | data=dict(**data_kwargs), | |
799 | headers=[("authorization", f"agent {agent.token}")] | |
800 | ) | |
801 | ||
802 | if start_date or duration is None: | |
803 | assert res.status_code == 201, res.json | |
804 | assert Host.query.filter(Host.workspace == workspace and Host.creator_id is None).count() == \ | |
805 | initial_host_count + 1 | |
806 | assert count(Command, workspace) == 1 | |
807 | command = Command.query.filter(Command.workspace == workspace).one() | |
808 | assert command.tool == agent.name | |
809 | assert command.command == agent_execution.executor.name | |
810 | assert command.params == "" | |
811 | assert command.import_source == 'agent' | |
812 | command_id = res.json["command_id"] | |
813 | assert command.id == command_id | |
814 | assert command.id == agent_execution.command.id | |
815 | assert command.start_date is not None | |
816 | if duration is None: | |
817 | assert command.end_date is None | |
818 | else: | |
819 | assert command.end_date == command.start_date + timedelta(microseconds=duration) | |
820 | else: | |
821 | assert res.status_code == 400, res.json | |
822 | ||
823 | ||
824 | ||
825 | def test_bulk_create_endpoint_with_agent_token_with_param(session, agent_execution, test_client): | |
826 | agent = agent_execution.executor.agent | |
827 | session.add(agent_execution) | |
828 | session.commit() | |
829 | for workspace in agent.workspaces: | |
830 | agent_execution.workspace = workspace | |
831 | agent_execution.command.workspace = workspace | |
832 | session.add(agent_execution) | |
833 | session.commit() | |
710 | 834 | assert count(Host, workspace) == 0 |
711 | assert count(Command, workspace) == 0 | |
835 | assert count(Command, workspace) == 1 | |
836 | url = f'v2/ws/{workspace.name}/bulk_create/' | |
712 | 837 | res = test_client.post( |
713 | 838 | url, |
714 | 839 | data=dict(hosts=[host_data], execution_id=agent_execution.id), |
715 | headers=[("authorization", "agent {}".format(agent.token))] | |
716 | ) | |
717 | assert res.status_code == 201, res.json | |
718 | assert count(Host, workspace) == 1 | |
719 | host = Host.query.filter(Host.workspace == workspace).one() | |
720 | assert host.creator_id is None | |
721 | assert count(Command, workspace) == 1 | |
722 | command = Command.query.filter(Command.workspace == workspace).one() | |
723 | assert command.tool == agent.name | |
724 | assert command.command == agent_execution.executor.name | |
725 | assert command.params == "" | |
726 | assert command.import_source == 'agent' | |
727 | ||
728 | ||
729 | def test_bulk_create_endpoint_with_agent_token_with_param(session, agent_execution, test_client): | |
730 | agent = agent_execution.executor.agent | |
731 | session.add(agent_execution) | |
732 | session.commit() | |
733 | for workspace in agent.workspaces: | |
734 | agent_execution.workspace = workspace | |
735 | session.add(agent_execution) | |
736 | session.commit() | |
737 | assert count(Host, workspace) == 0 | |
738 | url = 'v2/ws/{}/bulk_create/'.format(workspace.name) | |
739 | res = test_client.post( | |
740 | url, | |
741 | data=dict(hosts=[host_data], execution_id=agent_execution.id), | |
742 | headers=[("authorization", "agent {}".format(agent.token))] | |
840 | headers=[("authorization", f"agent {agent.token}")] | |
743 | 841 | ) |
744 | 842 | assert res.status_code == 201 |
745 | 843 | assert count(Host, workspace) == 1 |
752 | 850 | params = ', '.join([f'{key}={value}' for (key, value) in agent_execution.parameters_data.items()]) |
753 | 851 | assert command.params == str(params) |
754 | 852 | assert command.import_source == 'agent' |
853 | command_id = res.json["command_id"] | |
854 | assert command.id == command_id | |
855 | assert command.id == agent_execution.command.id | |
755 | 856 | |
756 | 857 | |
757 | 858 | def test_bulk_create_endpoint_with_agent_token_readonly_workspace( |
763 | 864 | session.commit() |
764 | 865 | for workspace in agent.workspaces: |
765 | 866 | |
766 | url = 'v2/ws/{}/bulk_create/'.format(workspace.name) | |
867 | url = f'v2/ws/{workspace.name}/bulk_create/' | |
767 | 868 | res = test_client.post( |
768 | 869 | url, |
769 | 870 | data=dict(hosts=[host_data]), |
770 | headers=[("authorization", "agent {}".format(agent.token))] | |
871 | headers=[("authorization", f"agent {agent.token}")] | |
771 | 872 | ) |
772 | 873 | assert res.status_code == 403 |
773 | 874 | |
780 | 881 | session.add(workspace) |
781 | 882 | session.commit() |
782 | 883 | for workspace in agent.workspaces: |
783 | url = 'v2/ws/{}/bulk_create/'.format(workspace.name) | |
884 | url = f'v2/ws/{workspace.name}/bulk_create/' | |
784 | 885 | res = test_client.post( |
785 | 886 | url, |
786 | 887 | data=dict(hosts=[host_data]), |
787 | headers=[("authorization", "agent {}".format(agent.token))] | |
888 | headers=[("authorization", f"agent {agent.token}")] | |
788 | 889 | ) |
789 | 890 | assert res.status_code == 403 |
790 | 891 | |
791 | 892 | @pytest.mark.usefixtures('logged_user') |
792 | 893 | def test_bulk_create_endpoint_raises_400_with_no_data( |
793 | 894 | session, test_client, workspace): |
794 | url = 'v2/ws/{}/bulk_create/'.format(workspace.name) | |
895 | url = f'v2/ws/{workspace.name}/bulk_create/' | |
795 | 896 | res = test_client.post( |
796 | 897 | url, |
797 | 898 | data="", |
804 | 905 | def test_bulk_create_endpoint_with_vuln_run_date(session, workspace, test_client): |
805 | 906 | assert count(Host, workspace) == 0 |
806 | 907 | assert count(VulnerabilityGeneric, workspace) == 0 |
807 | url = 'v2/ws/{}/bulk_create/'.format(workspace.name) | |
908 | url = f'v2/ws/{workspace.name}/bulk_create/' | |
808 | 909 | run_date = datetime.now(timezone.utc) - timedelta(days=30) |
809 | 910 | host_data_copy = host_data.copy() |
810 | 911 | vuln_data_copy = vuln_data.copy() |
821 | 922 | def test_bulk_create_endpoint_with_vuln_future_run_date(session, workspace, test_client): |
822 | 923 | assert count(Host, workspace) == 0 |
823 | 924 | assert count(VulnerabilityGeneric, workspace) == 0 |
824 | url = 'v2/ws/{}/bulk_create/'.format(workspace.name) | |
925 | url = f'v2/ws/{workspace.name}/bulk_create/' | |
825 | 926 | run_date = datetime.now(timezone.utc) + timedelta(days=10) |
826 | 927 | host_data_copy = host_data.copy() |
827 | 928 | vuln_data_copy = vuln_data.copy() |
839 | 940 | def test_bulk_create_endpoint_with_invalid_vuln_run_date(session, workspace, test_client): |
840 | 941 | assert count(Host, workspace) == 0 |
841 | 942 | assert count(VulnerabilityGeneric, workspace) == 0 |
842 | url = 'v2/ws/{}/bulk_create/'.format(workspace.name) | |
943 | url = f'v2/ws/{workspace.name}/bulk_create/' | |
843 | 944 | host_data_copy = host_data.copy() |
844 | 945 | vuln_data_copy = vuln_data.copy() |
845 | 946 | vuln_data_copy['run_date'] = "INVALID_VALUE" |
849 | 950 | assert count(VulnerabilityGeneric, workspace) == 0 |
850 | 951 | |
851 | 952 | |
852 | ||
853 | ||
854 | 953 | @pytest.mark.usefixtures('logged_user') |
855 | 954 | def test_bulk_create_endpoint_fails_with_list_in_NullToBlankString(session, workspace, test_client, logged_user): |
856 | 955 | assert count(Host, workspace) == 0 |
857 | 956 | assert count(VulnerabilityGeneric, workspace) == 0 |
858 | url = 'v2/ws/{}/bulk_create/'.format(workspace.name) | |
957 | url = f'v2/ws/{workspace.name}/bulk_create/' | |
859 | 958 | host_data_ = host_data.copy() |
860 | 959 | host_data_['services'] = [service_data] |
861 | 960 | host_data_['credentials'] = [credential_data] |
868 | 967 | assert count(Credential, workspace) == 0 |
869 | 968 | assert count(Vulnerability, workspace) == 0 |
870 | 969 | |
970 | ||
971 | @pytest.mark.usefixtures('logged_user') | |
972 | def test_bulk_create_with_custom_fields_list(test_client, workspace, session, logged_user): | |
973 | custom_field_schema = CustomFieldsSchemaFactory( | |
974 | field_name='changes', | |
975 | field_type='list', | |
976 | field_display_name='Changes', | |
977 | table_name='vulnerability' | |
978 | ) | |
979 | session.add(custom_field_schema) | |
980 | session.commit() | |
981 | ||
982 | assert count(Host, workspace) == 0 | |
983 | assert count(VulnerabilityGeneric, workspace) == 0 | |
984 | url = f'v2/ws/{workspace.name}/bulk_create/' | |
985 | host_data_ = host_data.copy() | |
986 | service_data_ = service_data.copy() | |
987 | vuln_data_ = vuln_data.copy() | |
988 | vuln_data_['custom_fields'] = {'changes': ['1', '2', '3']} | |
989 | service_data_['vulnerabilities'] = [vuln_data_] | |
990 | host_data_['services'] = [service_data_] | |
991 | host_data_['credentials'] = [credential_data] | |
992 | host_data_['vulnerabilities'] = [vuln_data_] | |
993 | res = test_client.post( | |
994 | url, | |
995 | data=dict(hosts=[host_data_], command=command_data) | |
996 | ) | |
997 | assert res.status_code == 201, res.json | |
998 | assert count(Host, workspace) == 1 | |
999 | assert count(Service, workspace) == 1 | |
1000 | assert count(Vulnerability, workspace) == 2 | |
1001 | assert count(Command, workspace) == 1 | |
1002 | host = Host.query.filter(Host.workspace == workspace).one() | |
1003 | assert host.ip == "127.0.0.1" | |
1004 | assert host.creator_id == logged_user.id | |
1005 | assert set({hn.name for hn in host.hostnames}) == {"test.com", "test2.org"} | |
1006 | assert len(host.services) == 1 | |
1007 | assert len(host.vulnerabilities) == 1 | |
1008 | assert len(host.services[0].vulnerabilities) == 1 | |
1009 | service = Service.query.filter(Service.workspace == workspace).one() | |
1010 | assert service.creator_id == logged_user.id | |
1011 | credential = Credential.query.filter(Credential.workspace == workspace).one() | |
1012 | assert credential.creator_id == logged_user.id | |
1013 | command = Command.query.filter(Credential.workspace == workspace).one() | |
1014 | assert command.creator_id == logged_user.id | |
1015 | assert res.json["command_id"] == command.id | |
1016 | for vuln in Vulnerability.query.filter(Vulnerability.workspace == workspace): | |
1017 | assert vuln.custom_fields['changes'] == ['1', '2', '3'] | |
1018 | ||
1019 | ||
1020 | @pytest.mark.usefixtures('logged_user') | |
1021 | def test_vuln_web_cannot_have_host_parent(session, workspace, test_client, logged_user): | |
1022 | url = f'v2/ws/{workspace.name}/bulk_create/' | |
1023 | host_data_ = host_data.copy() | |
1024 | vuln_web_data_ = vuln_web_data.copy() | |
1025 | vuln_web_data_['severity'] = "high" | |
1026 | vuln_web_data_['name'] = "test" | |
1027 | host_data_['vulnerabilities'] = [vuln_web_data_] | |
1028 | res = test_client.post( | |
1029 | url, | |
1030 | data=dict(hosts=[host_data_], command=command_data) | |
1031 | ) | |
1032 | assert res.status_code == 400 |
404 | 404 | ) |
405 | 405 | session.commit() |
406 | 406 | |
407 | res = test_client.get(u'/v2/ws/{0}/hosts/{1}/'.format(host.workspace.name, host.id)) | |
408 | assert res.status_code == 200 | |
409 | ||
410 | res = test_client.delete(u'/v2/ws/{0}/hosts/{1}/'.format(host.workspace.name, host.id)) | |
407 | res = test_client.get(f'/v2/ws/{host.workspace.name}/hosts/{host.id}/') | |
408 | assert res.status_code == 200 | |
409 | ||
410 | res = test_client.delete(f'/v2/ws/{host.workspace.name}/hosts/{host.id}/') | |
411 | 411 | assert res.status_code == 204 |
412 | 412 | |
413 | 413 | res = test_client.get(self.url(workspace=command.workspace) + 'activity_feed/') |
112 | 112 | assert 'object' in res.json |
113 | 113 | assert type(res.json) == dict |
114 | 114 | |
115 | ||
116 | # I'm Py3 | |
115 | def test_default_order_field(self, session, test_client): | |
116 | workspace = factories.WorkspaceFactory.create() | |
117 | factories.CommentFactory.create(workspace=workspace, text='first') | |
118 | factories.CommentFactory.create(workspace=workspace, text='second') | |
119 | factories.CommentFactory.create(workspace=workspace, text='third') | |
120 | factories.CommentFactory.create(workspace=workspace, text='fourth') | |
121 | get_comments = test_client.get(self.url(workspace=workspace)) | |
122 | expected = ['first', 'second', 'third','fourth'] | |
123 | assert expected == [comment['text'] for comment in get_comments.json] |
103 | 103 | credential = self.factory.create(host=host, service=None, |
104 | 104 | workspace=self.workspace) |
105 | 105 | session.commit() |
106 | res = test_client.get(self.url(workspace=credential.workspace) + '?host_id={0}'.format(credential.host.id)) | |
106 | res = test_client.get(self.url(workspace=credential.workspace) + f'?host_id={credential.host.id}') | |
107 | 107 | assert res.status_code == 200 |
108 | 108 | assert [cred['value']['parent'] for cred in res.json['rows']] == [credential.host.id] |
109 | 109 | assert [cred['value']['parent_type'] for cred in res.json['rows']] == [u'Host'] |
112 | 112 | service = ServiceFactory.create() |
113 | 113 | credential = self.factory.create(service=service, host=None, workspace=service.workspace) |
114 | 114 | session.commit() |
115 | res = test_client.get(self.url(workspace=credential.workspace) + '?service={0}'.format(credential.service.id)) | |
115 | res = test_client.get(self.url(workspace=credential.workspace) + f'?service={credential.service.id}') | |
116 | 116 | assert res.status_code == 200 |
117 | 117 | assert [cred['value']['parent'] for cred in res.json['rows']] == [credential.service.id] |
118 | 118 | assert [cred['value']['parent_type'] for cred in res.json['rows']] == [u'Service'] |
248 | 248 | credential4 = self.factory.create(service=service2, host=None, workspace=second_workspace) |
249 | 249 | |
250 | 250 | credentials_target = [ |
251 | "{}/{}".format(credential.service.host.ip, credential.service.name), | |
252 | "{}".format(credential2.host.ip), | |
253 | "{}".format(credential3.host.ip), | |
254 | "{}/{}".format(credential4.service.host.ip, credential4.service.name), | |
251 | f"{credential.service.host.ip}/{credential.service.name}", | |
252 | f"{credential2.host.ip}", | |
253 | f"{credential3.host.ip}", | |
254 | f"{credential4.service.host.ip}/{credential4.service.name}", | |
255 | 255 | ] |
256 | 256 | |
257 | 257 | # Desc order |
4 | 4 | |
5 | 5 | ''' |
6 | 6 | |
7 | import os | |
8 | 7 | import pytest |
9 | 8 | from lxml.etree import fromstring, tostring |
10 | 9 | |
10 | from tests.conftest import TEST_DATA_PATH | |
11 | 11 | from tests.factories import ( |
12 | 12 | WorkspaceFactory, |
13 | 13 | HostFactory, |
21 | 21 | class TestExportData(): |
22 | 22 | def test_export_data_without_format(self, test_client): |
23 | 23 | workspace = WorkspaceFactory.create() |
24 | url = '/v2/ws/{ws_name}/export_data'.format(ws_name=workspace.name) | |
24 | url = f'/v2/ws/{workspace.name}/export_data' | |
25 | 25 | response = test_client.get(url) |
26 | 26 | assert response.status_code == 400 |
27 | 27 | |
84 | 84 | session.add(vuln_web) |
85 | 85 | session.commit() |
86 | 86 | |
87 | url = '/v2/ws/{ws_name}/export_data?format=xml_metasploit'.format(ws_name=workspace.name) | |
87 | url = f'/v2/ws/{workspace.name}/export_data?format=xml_metasploit' | |
88 | 88 | response = test_client.get(url) |
89 | 89 | assert response.status_code == 200 |
90 | 90 | response_xml = response.data |
91 | 91 | |
92 | xml_file_path = os.path.join( | |
93 | os.path.dirname(os.path.realpath(__file__)), | |
94 | 'data', | |
95 | 'faraday_export_data_xml_metasploit.xml') | |
96 | with open(xml_file_path, 'rb') as output: | |
92 | xml_file_path = TEST_DATA_PATH / \ | |
93 | 'faraday_export_data_xml_metasploit.xml' | |
94 | with xml_file_path.open('rb') as output: | |
97 | 95 | xml_file = output.read() |
98 | 96 | |
99 | 97 | response_tree = fromstring(response_xml) |
0 | import re | |
1 | from faraday.server.web import app | |
2 | ||
3 | placeholders = { | |
4 | r".*(<int:.*>).*": "1" | |
5 | } | |
6 | ||
7 | ||
8 | def replace_placeholders(rule: str): | |
9 | for key, value in placeholders.items(): | |
10 | match = re.match(key, rule) | |
11 | if match: | |
12 | for placeholder in match.groups(): | |
13 | rule = rule.replace(placeholder, value) | |
14 | return rule | |
15 | ||
16 | ||
17 | def test_options(test_client): | |
18 | for rule in app.url_map.iter_rules(): | |
19 | if 'OPTIONS' in rule.methods: | |
20 | res = test_client.options(replace_placeholders(rule.rule)) | |
21 | assert res.status_code == 200, rule.rule |
11 | 11 | class TestGetExploits(): |
12 | 12 | def test_get_exploit(self, test_client): |
13 | 13 | cve_id = "CVE-2018-1999045" |
14 | res = test_client.get('v2/vulners/exploits/{id}'.format(id=cve_id)) | |
14 | res = test_client.get(f'v2/vulners/exploits/{cve_id}') | |
15 | 15 | assert res.status_code == 200 |
16 | 16 | |
17 | 17 | @pytest.mark.skip() |
18 | 18 | def test_key_error(self, test_client): |
19 | 19 | cve_id = "CVE-2018-1999035ERROR" |
20 | res = test_client.get('v2/vulners/exploits/{id}'.format(id=cve_id)) | |
20 | res = test_client.get(f'v2/vulners/exploits/{cve_id}') | |
21 | 21 | assert res.status_code == 400 |
22 | 22 | |
23 | 23 | @pytest.mark.skip() |
24 | 24 | def test_get_exploit_with_modules(self, test_client): |
25 | 25 | cve_id = "CVE-2016-9299" |
26 | res = test_client.get('v2/vulners/exploits/{id}'.format(id=cve_id)) | |
26 | res = test_client.get(f'v2/vulners/exploits/{cve_id}') | |
27 | 27 | assert res.status_code == 200 |
28 | 28 | assert res.json.get('metasploit') != [] |
29 | 29 | assert res.json.get('exploitdb') != [] |
12 | 12 | try: |
13 | 13 | import urlparse |
14 | 14 | from urllib import urlencode |
15 | except: # For Python 3 | |
15 | except ImportError: # For Python 3 | |
16 | 16 | import urllib.parse as urlparse |
17 | 17 | from urllib.parse import urlencode |
18 | 18 | from random import choice |
136 | 136 | assert host.ip == "127.0.0.1" |
137 | 137 | assert host.description == "aaaaa" |
138 | 138 | assert host.os == '' |
139 | assert host.workspace == self.workspace | |
140 | ||
139 | assert host.workspace == self.workspace | |
140 | ||
141 | 141 | def test_create_a_host_fails_with_missing_desc(self, test_client): |
142 | 142 | res = test_client.post(self.url(), data={ |
143 | 143 | "ip": "127.0.0.1", |
288 | 288 | assert res.status_code == 200 |
289 | 289 | self.compare_results(hosts, res) |
290 | 290 | |
291 | @pytest.mark.usefixtures('ignore_nplusone') | |
292 | def test_filter_restless_by_os_exact(self, test_client, session, workspace, | |
293 | second_workspace, host_factory): | |
294 | # The hosts that should be shown | |
295 | hosts = host_factory.create_batch(10, workspace=workspace, os='Unix') | |
296 | ||
297 | # Search should be case sensitive so this shouln't be shown | |
298 | host_factory.create_batch(1, workspace=workspace, os='UNIX') | |
299 | ||
300 | # This shouldn't be shown, they are from other workspace | |
301 | host_factory.create_batch(5, workspace=second_workspace, os='Unix') | |
302 | ||
303 | session.commit() | |
304 | res = test_client.get(f'{self.url()}filter?q={{"filters":[{{"name": "os", "op":"eq", "val":"Unix"}}]}}') | |
305 | assert res.status_code == 200 | |
306 | self.compare_results(hosts, res) | |
307 | ||
308 | ||
309 | @pytest.mark.usefixtures('ignore_nplusone') | |
310 | def test_filter_restless_filter_and_group_by_os(self, test_client, session, workspace, host_factory): | |
311 | host_factory.create_batch(10, workspace=workspace, os='Unix') | |
312 | host_factory.create_batch(1, workspace=workspace, os='unix') | |
313 | session.commit() | |
314 | res = test_client.get(f'{self.url()}filter?q={{"filters":[{{"name": "os", "op": "like", "val": "%nix"}}], ' | |
315 | f'"group_by":[{{"field": "os"}}], ' | |
316 | f'"order_by":[{{"field": "os", "direction": "desc"}}]}}') | |
317 | assert res.status_code == 200 | |
318 | assert len(res.json['rows']) == 2 | |
319 | assert res.json['total_rows'] == 2 | |
320 | assert 'unix' in [row['value']['os'] for row in res.json['rows']] | |
321 | assert 'Unix' in [row['value']['os'] for row in res.json['rows']] | |
322 | ||
323 | ||
291 | 324 | def test_filter_by_os_like_ilike(self, test_client, session, workspace, |
292 | 325 | second_workspace, host_factory): |
293 | 326 | # The hosts that should be shown |
313 | 346 | assert res.status_code == 200 |
314 | 347 | self.compare_results(hosts + [case_insensitive_host], res) |
315 | 348 | |
349 | @pytest.mark.usefixtures('ignore_nplusone') | |
350 | def test_filter_restless_by_os_like_ilike(self, test_client, session, workspace, | |
351 | second_workspace, host_factory): | |
352 | # The hosts that should be shown | |
353 | hosts = host_factory.create_batch(5, workspace=workspace, os='Unix 1') | |
354 | hosts += host_factory.create_batch(5, workspace=workspace, os='Unix 2') | |
355 | ||
356 | # This should only be shown when using ilike, not when using like | |
357 | case_insensitive_host = host_factory.create( | |
358 | workspace=workspace, os='UNIX 3') | |
359 | ||
360 | # This doesn't match the like expression | |
361 | host_factory.create(workspace=workspace, os="Test Unix 1") | |
362 | ||
363 | # This shouldn't be shown anywhere, they are from other workspace | |
364 | host_factory.create_batch(5, workspace=second_workspace, os='Unix') | |
365 | ||
366 | session.commit() | |
367 | res = test_client.get(f'{self.url()}filter?q={{"filters":[{{"name": "os", "op":"like", "val":"Unix %"}}]}}') | |
368 | assert res.status_code == 200 | |
369 | self.compare_results(hosts, res) | |
370 | ||
371 | res = test_client.get(f'{self.url()}filter?q={{"filters":[{{"name": "os", "op":"ilike", "val":"Unix %"}}]}}') | |
372 | assert res.status_code == 200 | |
373 | self.compare_results(hosts + [case_insensitive_host], res) | |
374 | ||
316 | 375 | def test_filter_by_service(self, test_client, session, workspace, |
317 | 376 | service_factory, host_factory): |
318 | 377 | services = service_factory.create_batch(10, workspace=workspace, |
329 | 388 | expected_host_ids = set(host.id for host in hosts) |
330 | 389 | assert shown_hosts_ids == expected_host_ids |
331 | 390 | |
391 | @pytest.mark.usefixtures('ignore_nplusone') | |
392 | def test_filter_restless_by_service_name(self, test_client, session, workspace, | |
393 | service_factory, host_factory): | |
394 | services = service_factory.create_batch(10, workspace=workspace, | |
395 | name="IRC") | |
396 | hosts = [service.host for service in services] | |
397 | ||
398 | # Hosts that shouldn't be shown | |
399 | host_factory.create_batch(5, workspace=workspace) | |
400 | ||
401 | session.commit() | |
402 | ||
403 | res = test_client.get(f'{self.url()}' | |
404 | f'filter?q={{"filters":[{{"name": "services__name", "op":"any", "val":"IRC"}}]}}') | |
405 | assert res.status_code == 200 | |
406 | shown_hosts_ids = set(obj['id'] for obj in res.json['rows']) | |
407 | expected_host_ids = set(host.id for host in hosts) | |
408 | assert shown_hosts_ids == expected_host_ids | |
409 | ||
410 | ||
332 | 411 | def test_filter_by_service_port(self, test_client, session, workspace, |
333 | 412 | service_factory, host_factory): |
334 | 413 | services = service_factory.create_batch(10, workspace=workspace, port=25) |
344 | 423 | expected_host_ids = set(host.id for host in hosts) |
345 | 424 | assert shown_hosts_ids == expected_host_ids |
346 | 425 | |
426 | ||
427 | @pytest.mark.usefixtures('ignore_nplusone') | |
428 | def test_filter_restless_by_service_port(self, test_client, session, workspace, | |
429 | service_factory, host_factory): | |
430 | services = service_factory.create_batch(10, workspace=workspace, port=25) | |
431 | hosts = [service.host for service in services] | |
432 | ||
433 | # Hosts that shouldn't be shown | |
434 | host_factory.create_batch(5, workspace=workspace) | |
435 | ||
436 | session.commit() | |
437 | res = test_client.get(f'{self.url()}' | |
438 | f'filter?q={{"filters":[{{"name": "services__port", "op":"any", "val":"25"}}]}}') | |
439 | assert res.status_code == 200 | |
440 | shown_hosts_ids = set(obj['id'] for obj in res.json['rows']) | |
441 | expected_host_ids = set(host.id for host in hosts) | |
442 | assert shown_hosts_ids == expected_host_ids | |
443 | ||
444 | @pytest.mark.usefixtures('ignore_nplusone') | |
445 | def test_filter_verify_severity_counts(self, test_client, session, workspace, host_factory, vulnerability_factory): | |
446 | host = host_factory.create(workspace=workspace) | |
447 | vulnerability_factory.create(service=None, host=host, workspace=workspace, severity='critical') | |
448 | vulnerability_factory.create(service=None, host=host, workspace=workspace, severity='critical') | |
449 | vulnerability_factory.create(service=None, host=host, workspace=workspace, severity='high') | |
450 | vulnerability_factory.create(service=None, host=host, workspace=workspace, severity='low') | |
451 | vulnerability_factory.create(service=None, host=host, workspace=workspace, severity='informational') | |
452 | ||
453 | host2 = host_factory.create(workspace=workspace) | |
454 | vulnerability_factory.create(service=None, host=host2, workspace=workspace, severity='critical') | |
455 | vulnerability_factory.create(service=None, host=host2, workspace=workspace, severity='critical') | |
456 | ||
457 | session.commit() | |
458 | ||
459 | res = test_client.get(f'{self.url()}' | |
460 | f'filter?q={{"filters":[{{"name": "ip", "op":"eq", "val":"{host.ip}"}}]}}') | |
461 | ||
462 | assert res.status_code == 200 | |
463 | ||
464 | severities = res.json['rows'][0]['value']['severity_counts'] | |
465 | assert severities['info'] == 1 | |
466 | assert severities['critical'] == 2 | |
467 | assert severities['high'] == 1 | |
468 | assert severities['med'] == 0 | |
469 | assert severities['low'] == 1 | |
470 | assert severities['unclassified'] == 0 | |
471 | assert severities['total'] == 5 | |
472 | ||
347 | 473 | def test_filter_by_invalid_service_port(self, test_client, session, workspace, |
348 | 474 | service_factory, host_factory): |
349 | 475 | services = service_factory.create_batch(10, workspace=workspace, port=25) |
356 | 482 | res = test_client.get(self.url() + '?port=invalid_port') |
357 | 483 | assert res.status_code == 200 |
358 | 484 | assert res.json['total_rows'] == 0 |
485 | ||
486 | def test_filter_restless_by_invalid_service_port(self, test_client, session, workspace, | |
487 | service_factory, host_factory): | |
488 | services = service_factory.create_batch(10, workspace=workspace, port=25) | |
489 | hosts = [service.host for service in services] | |
490 | ||
491 | # Hosts that shouldn't be shown | |
492 | host_factory.create_batch(5, workspace=workspace) | |
493 | ||
494 | session.commit() | |
495 | res = test_client.get(f'{self.url()}' | |
496 | f'filter?q={{"filters":[{{"name": "services__port", "op":"any", "val":"sarasa"}}]}}') | |
497 | assert res.status_code == 400 | |
498 | ||
499 | def test_filter_restless_by_invalid_field(self, test_client): | |
500 | res = test_client.get(f'{self.url()}' | |
501 | f'filter?q={{"filters":[{{"name": "severity", "op":"any", "val":"sarasa"}}]}}') | |
502 | assert res.status_code == 400 | |
503 | ||
504 | @pytest.mark.usefixtures('ignore_nplusone') | |
505 | def test_filter_restless_with_no_q_param(self, test_client, session, workspace, host_factory): | |
506 | res = test_client.get(f'{self.url()}filter') | |
507 | assert res.status_code == 200 | |
508 | assert len(res.json['rows']) == HOSTS_COUNT | |
509 | ||
510 | @pytest.mark.usefixtures('ignore_nplusone') | |
511 | def test_filter_restless_with_empty_q_param(self, test_client, session, workspace, host_factory): | |
512 | res = test_client.get(f'{self.url()}filter?q') | |
513 | assert res.status_code == 400 | |
359 | 514 | |
360 | 515 | def test_search_ip(self, test_client, session, workspace, host_factory): |
361 | 516 | host = host_factory.create(ip="longname", |
399 | 554 | |
400 | 555 | host = host_factory.create(workspace=workspace) |
401 | 556 | service = service_factory.create(host=host, workspace=workspace) |
402 | vulnerability_factory.create(service=service, host=None, workspace=workspace) | |
403 | vulnerability_factory.create(service=None, host=host, workspace=workspace) | |
557 | vulnerability_factory.create(service=service, host=None, workspace=workspace, severity="low") | |
558 | vulnerability_factory.create(service=None, host=host, workspace=workspace, severity="critical") | |
404 | 559 | |
405 | 560 | session.commit() |
406 | 561 | |
409 | 564 | json_host = list(filter(lambda json_host: json_host['value']['id'] == host.id, res.json['rows']))[0] |
410 | 565 | # the host has one vuln associated. another one via service. |
411 | 566 | assert json_host['value']['vulns'] == 2 |
567 | assert json_host['value']['severity_counts']['critical'] == 1 | |
568 | assert json_host['value']['severity_counts']['low'] == 1 | |
569 | assert json_host['value']['severity_counts']['info'] == 0 | |
570 | assert json_host['value']['severity_counts']['unclassified'] == 0 | |
571 | assert json_host['value']['severity_counts']['med'] == 0 | |
572 | assert json_host['value']['severity_counts']['high'] == 0 | |
412 | 573 | |
413 | 574 | def test_host_services_vuln_count_verification(self, test_client, session, |
414 | 575 | workspace, host_factory, vulnerability_factory, |
497 | 658 | "os":"Microsoft Windows Server 2008 R2 Standard Service Pack 1", |
498 | 659 | "id": 4000, |
499 | 660 | "icon":"windows", |
500 | "versions": []} | |
661 | "versions": [], | |
662 | "important": False, | |
663 | } | |
501 | 664 | |
502 | 665 | res = test_client.put(self.url(host, workspace=host.workspace), data=raw_data) |
503 | 666 | assert res.status_code == 200 |
529 | 692 | u'services': 0, |
530 | 693 | u'service_summaries': [], |
531 | 694 | u'vulns': 0, |
532 | u"versions": []} | |
695 | u"versions": [], | |
696 | u'important': False, | |
697 | u'severity_counts': { | |
698 | u'critical': None, | |
699 | u'high': None, | |
700 | u'host_id': host.id, | |
701 | u'info': None, | |
702 | u'med': None, | |
703 | u'low': None, | |
704 | u'total': None, | |
705 | u'unclassified': None | |
706 | } | |
707 | } | |
533 | 708 | |
534 | 709 | def test_add_hosts_from_csv(self, session, test_client, csrf_token): |
535 | 710 | ws = WorkspaceFactory.create(name='abc') |
545 | 720 | 'csrf_token': csrf_token |
546 | 721 | } |
547 | 722 | headers = {'Content-type': 'multipart/form-data'} |
548 | res = test_client.post('/v2/ws/{0}/hosts/bulk_create/'.format(ws.name), | |
723 | res = test_client.post(f'/v2/ws/{ws.name}/hosts/bulk_create/', | |
549 | 724 | data=data, headers=headers, use_json_data=False) |
550 | 725 | assert res.status_code == 200 |
551 | 726 | assert res.json['hosts_created'] == expected_created_hosts |
560 | 735 | hosts_ids = [host_1.id, host_2.id] |
561 | 736 | request_data = {'hosts_ids': hosts_ids} |
562 | 737 | |
563 | delete_response = test_client.delete('/v2/ws/{0}/hosts/bulk_delete/'.format(ws.name), data=request_data) | |
738 | delete_response = test_client.delete(f'/v2/ws/{ws.name}/hosts/bulk_delete/', data=request_data) | |
564 | 739 | |
565 | 740 | deleted_hosts = delete_response.json['deleted_hosts'] |
566 | 741 | host_count_after_delete = db.session.query(Host).filter( |
575 | 750 | ws = WorkspaceFactory.create(name="abc") |
576 | 751 | request_data = {'hosts_ids': []} |
577 | 752 | |
578 | delete_response = test_client.delete('/v2/ws/{0}/hosts/bulk_delete/'.format(ws.name), data=request_data) | |
753 | delete_response = test_client.delete(f'/v2/ws/{ws.name}/hosts/bulk_delete/', data=request_data) | |
579 | 754 | |
580 | 755 | assert delete_response.status_code == 400 |
581 | 756 | |
588 | 763 | |
589 | 764 | # Try to delete workspace_2's host from workspace_1 |
590 | 765 | request_data = {'hosts_ids': [host_of_ws_2.id]} |
591 | url = '/v2/ws/{0}/hosts/bulk_delete/'.format(workspace_1.name) | |
766 | url = f'/v2/ws/{workspace_1.name}/hosts/bulk_delete/' | |
592 | 767 | delete_response = test_client.delete(url, data=request_data) |
593 | 768 | |
594 | 769 | assert delete_response.json['deleted_hosts'] == 0 |
596 | 771 | def test_bulk_delete_hosts_invalid_characters_in_request(self, test_client): |
597 | 772 | ws = WorkspaceFactory.create(name="abc") |
598 | 773 | request_data = {'hosts_ids': [-1, 'test']} |
599 | delete_response = test_client.delete('/v2/ws/{0}/hosts/bulk_delete/'.format(ws.name), data=request_data) | |
774 | delete_response = test_client.delete(f'/v2/ws/{ws.name}/hosts/bulk_delete/', data=request_data) | |
600 | 775 | |
601 | 776 | assert delete_response.json['deleted_hosts'] == 0 |
602 | 777 | |
611 | 786 | headers = [('content-type', 'text/xml')] |
612 | 787 | |
613 | 788 | delete_response = test_client.delete( |
614 | '/v2/ws/{0}/hosts/bulk_delete/'.format(ws.name), | |
789 | f'/v2/ws/{ws.name}/hosts/bulk_delete/', | |
615 | 790 | data=request_data, |
616 | 791 | headers=headers) |
617 | 792 | |
799 | 974 | "os":"Unknown", |
800 | 975 | } |
801 | 976 | |
802 | res = test_client.put('v2/ws/{0}/hosts/{1}/'.format(host.workspace.name, host.id), data=data) | |
977 | res = test_client.put(f'v2/ws/{host.workspace.name}/hosts/{host.id}/', data=data) | |
803 | 978 | assert res.status_code == 200 |
804 | 979 | |
805 | 980 | assert session.query(Hostname).filter_by(host=host).count() == 1 |
930 | 1105 | def send_api_request(raw_data): |
931 | 1106 | |
932 | 1107 | ws_name = host_with_hostnames.workspace.name |
933 | res = test_client.post('/v2/ws/{0}/vulns/'.format(ws_name), | |
1108 | res = test_client.post(f'/v2/ws/{ws_name}/vulns/', | |
934 | 1109 | data=raw_data) |
935 | 1110 | assert res.status_code in [201, 400, 409] |
936 | 1111 |
4 | 4 | |
5 | 5 | ''' |
6 | 6 | |
7 | import os | |
8 | 7 | import pytest |
9 | 8 | |
10 | 9 | |
12 | 11 | class TestAPIInfoEndpoint: |
13 | 12 | |
14 | 13 | def test_api_info(self, test_client): |
15 | current_dir = os.getcwd() | |
16 | # this is a bug on the info api! | |
17 | # we require faraday to be a package since we can't import | |
18 | # from base path when our current working dir is tests. | |
19 | if 'tests' in current_dir: | |
20 | faraday_base = os.path.join(current_dir, '..') | |
21 | os.chdir(faraday_base) | |
22 | ||
23 | 14 | response = test_client.get('v2/info') |
24 | 15 | assert response.status_code == 200 |
25 | 16 | assert response.json['Faraday Server'] == 'Running' |
26 | # to avoid side effects | |
27 | os.chdir(current_dir) | |
28 | 17 | |
29 | 18 | def test_get_config(self, test_client): |
30 | 19 | res = test_client.get('/config') |
8 | 8 | from tests.conftest import logged_user, login_as |
9 | 9 | |
10 | 10 | |
11 | class TestLogin(): | |
11 | class TestLogin: | |
12 | 12 | def test_case_bug_with_username(self, test_client, session): |
13 | 13 | """ |
14 | 14 | When the user case does not match the one in database, |
103 | 103 | test_client.cookie_jar.clear() |
104 | 104 | res = test_client.get('/v2/ws/wonderland/', headers=headers) |
105 | 105 | assert res.status_code == 200 |
106 | assert res.headers.has_key('Set-Cookie') is False | |
106 | assert 'Set-Cookie' not in res.headers | |
107 | 107 | cookies = [cookie.name for cookie in test_client.cookie_jar] |
108 | 108 | assert "faraday_session_2" not in cookies |
109 | 109 | |
164 | 164 | |
165 | 165 | ws = test_client.get('/v2/ws/wonderland/', headers=headers) |
166 | 166 | assert ws.status_code == 200 |
167 | ||
168 | def test_login_remember_me(self, test_client, session): | |
169 | """ | |
170 | When the remember me option is true, flask stores a remember_token | |
171 | """ | |
172 | test_client.cookie_jar.clear() | |
173 | susan = factories.UserFactory.create( | |
174 | active=True, | |
175 | username='susan', | |
176 | password=hash_password('pepito'), | |
177 | role='pentester') | |
178 | session.add(susan) | |
179 | session.commit() | |
180 | ||
181 | login_payload = { | |
182 | 'email': 'susan', | |
183 | 'password': 'pepito', | |
184 | 'remember': True | |
185 | } | |
186 | res = test_client.post('/login', data=login_payload) | |
187 | assert res.status_code == 200 | |
188 | cookies = [cookie.name for cookie in test_client.cookie_jar] | |
189 | assert "remember_token" in cookies | |
190 | ||
191 | def test_login_not_remember_me(self, test_client, session): | |
192 | """ | |
193 | When the remember me option is false, flask dont stores a remember_token | |
194 | """ | |
195 | ||
196 | test_client.cookie_jar.clear() | |
197 | susan = factories.UserFactory.create( | |
198 | active=True, | |
199 | username='susan', | |
200 | password=hash_password('pepito'), | |
201 | role='pentester') | |
202 | session.add(susan) | |
203 | session.commit() | |
204 | login_payload = { | |
205 | 'email': 'susan', | |
206 | 'password': 'pepito', | |
207 | 'remember': False | |
208 | } | |
209 | res = test_client.post('/login', data=login_payload) | |
210 | assert res.status_code == 200 | |
211 | cookies = [cookie.name for cookie in test_client.cookie_jar] | |
212 | assert "remember_token" not in cookies | |
213 | ||
214 | def test_login_without_remember_me(self, test_client, session): | |
215 | """ | |
216 | When the remember me option is missing, flask dont stores a remember_token | |
217 | """ | |
218 | ||
219 | test_client.cookie_jar.clear() | |
220 | susan = factories.UserFactory.create( | |
221 | active=True, | |
222 | username='susan', | |
223 | password=hash_password('pepito'), | |
224 | role='pentester') | |
225 | session.add(susan) | |
226 | session.commit() | |
227 | login_payload = { | |
228 | 'email': 'susan', | |
229 | 'password': 'pepito' | |
230 | } | |
231 | res = test_client.post('/login', data=login_payload) | |
232 | assert res.status_code == 200 | |
233 | cookies = [cookie.name for cookie in test_client.cookie_jar] | |
234 | assert "remember_token" not in cookies |
5 | 5 | ''' |
6 | 6 | |
7 | 7 | import pytest |
8 | from tests.conftest import login_as | |
8 | 9 | |
9 | 10 | @pytest.mark.usefixtures('logged_user') |
10 | 11 | class TestSessionLogged(): |
11 | 12 | def test_session_when_user_is_logged(self, test_client): |
12 | 13 | res = test_client.get('/session') |
13 | 14 | assert res.status_code == 200 |
15 | ||
16 | @pytest.mark.parametrize('role', ['admin', 'pentester', 'client', 'asset_owner']) | |
17 | def test_session_when_user_is_logged_with_different_roles(self, test_client, session, user, role): | |
18 | user.role = role | |
19 | session.commit() | |
20 | login_as(test_client, user) | |
21 | res = test_client.get('/session') | |
22 | assert res.json['role'] == role | |
23 | ||
14 | 24 | |
15 | 25 | class TestSessionNotLogged(): |
16 | 26 | def test_session_when_user_is_not_logged(self, test_client): |
4 | 4 | |
5 | 5 | ''' |
6 | 6 | |
7 | import os | |
8 | 7 | import pytest |
9 | 8 | from io import BytesIO |
10 | 9 | |
10 | from tests.conftest import TEST_DATA_PATH | |
11 | 11 | from tests.factories import WorkspaceFactory |
12 | 12 | |
13 | 13 | from faraday.server.threads.reports_processor import REPORTS_QUEUE |
14 | 14 | |
15 | from faraday.server.models import Host, Vulnerability, Service, Command | |
15 | from faraday.server.models import Host, Service, Command | |
16 | 16 | |
17 | 17 | |
18 | 18 | @pytest.mark.usefixtures('logged_user') |
22 | 22 | ws = WorkspaceFactory.create(name="abc") |
23 | 23 | session.add(ws) |
24 | 24 | session.commit() |
25 | path = os.path.join( | |
26 | os.path.dirname(os.path.realpath(__file__)), | |
27 | 'data', | |
28 | 'nmap_plugin_with_api.xml') | |
25 | path = TEST_DATA_PATH / 'nmap_plugin_with_api.xml' | |
29 | 26 | |
30 | with open(path,'rb') as report: | |
27 | with path.open('rb') as report: | |
31 | 28 | file_contents = report.read() |
32 | 29 | data = { |
33 | 'file' : (BytesIO(file_contents), 'nmap_report.xml'), | |
30 | 'file': (BytesIO(file_contents), 'nmap_report.xml'), | |
34 | 31 | 'csrf_token': csrf_token |
35 | 32 | } |
36 | 33 | |
37 | 34 | res = test_client.post( |
38 | '/v2/ws/{ws_name}/upload_report'.format(ws_name=ws.name), | |
35 | f'/v2/ws/{ws.name}/upload_report', | |
39 | 36 | data=data, |
40 | 37 | use_json_data=False) |
41 | 38 | |
43 | 40 | assert len(REPORTS_QUEUE.queue) == 1 |
44 | 41 | queue_elem = REPORTS_QUEUE.queue[0] |
45 | 42 | assert queue_elem[0] == ws.name |
46 | assert queue_elem[2].lower() == "nmap" | |
47 | assert queue_elem[3].id == logged_user.id | |
43 | assert queue_elem[3].lower() == "nmap" | |
44 | assert queue_elem[4] == logged_user.id | |
48 | 45 | |
49 | 46 | # I'm testing a method which lost referene of workspace and logged_user within the test |
50 | 47 | ws_id = ws.id |
52 | 49 | |
53 | 50 | from faraday.server.threads.reports_processor import ReportsManager |
54 | 51 | false_thread = ReportsManager(None) |
55 | false_thread.process_report(queue_elem[0], queue_elem[1], queue_elem[2], queue_elem[3]) | |
52 | false_thread.process_report(queue_elem[0], queue_elem[1], | |
53 | queue_elem[2], queue_elem[3], | |
54 | queue_elem[4]) | |
56 | 55 | command = Command.query.filter(Command.workspace_id == ws_id).one() |
57 | 56 | assert command |
58 | 57 | assert command.creator_id == logged_user_id |
58 | assert command.id == res.json["command_id"] | |
59 | assert command.end_date | |
59 | 60 | host = Host.query.filter(Host.workspace_id == ws_id).first() |
60 | 61 | assert host |
61 | 62 | assert host.creator_id == logged_user_id |
70 | 71 | session.commit() |
71 | 72 | |
72 | 73 | res = test_client.post( |
73 | '/v2/ws/{ws_name}/upload_report'.format(ws_name=ws.name)) | |
74 | f'/v2/ws/{ws.name}/upload_report') | |
74 | 75 | |
75 | 76 | assert res.status_code == 400 |
76 | 77 | |
79 | 80 | ws = WorkspaceFactory.create(name="abc") |
80 | 81 | session.add(ws) |
81 | 82 | session.commit() |
82 | path = os.path.join( | |
83 | os.path.dirname(os.path.realpath(__file__)), | |
84 | 'data', | |
85 | 'nmap_plugin_with_api.xml') | |
83 | path = TEST_DATA_PATH / 'nmap_plugin_with_api.xml' | |
86 | 84 | |
87 | with open(path,'r') as report: | |
85 | with path.open('r') as report: | |
88 | 86 | file_contents = report.read().encode('utf-8') |
89 | 87 | |
90 | 88 | data = { |
91 | 'file' : (BytesIO(file_contents), 'nmap_report.xml'), | |
89 | 'file': (BytesIO(file_contents), 'nmap_report.xml'), | |
92 | 90 | } |
93 | 91 | |
94 | 92 | res = test_client.post( |
95 | '/v2/ws/{ws_name}/upload_report'.format(ws_name=ws.name), | |
93 | f'/v2/ws/{ws.name}/upload_report', | |
96 | 94 | data=data, |
97 | 95 | use_json_data=False) |
98 | 96 | |
99 | 97 | assert res.status_code == 403 |
100 | ||
101 | 98 | |
102 | 99 | def test_request_with_workspace_deactivate(self, test_client, session, csrf_token): |
103 | 100 | ws = WorkspaceFactory.create(name="abc") |
104 | 101 | ws.active = False |
105 | 102 | session.add(ws) |
106 | 103 | session.commit() |
107 | path = os.path.join( | |
108 | os.path.dirname(os.path.realpath(__file__)), | |
109 | 'data', | |
110 | 'nmap_plugin_with_api.xml') | |
104 | path = TEST_DATA_PATH / 'nmap_plugin_with_api.xml' | |
111 | 105 | |
112 | with open(path,'r') as report: | |
106 | with path.open('r') as report: | |
113 | 107 | file_contents = report.read().encode('utf-8') |
114 | 108 | |
115 | 109 | data = { |
116 | 'file' : (BytesIO(file_contents), 'nmap_report.xml'), | |
117 | 'csrf_token' : csrf_token | |
110 | 'file': (BytesIO(file_contents), 'nmap_report.xml'), | |
111 | 'csrf_token': csrf_token | |
118 | 112 | } |
119 | 113 | res = test_client.post( |
120 | '/v2/ws/{ws_name}/upload_report'.format(ws_name=ws.name), | |
114 | f'/v2/ws/{ws.name}/upload_report', | |
121 | 115 | data=data, |
122 | 116 | use_json_data=False) |
123 | 117 |
4 | 4 | See the file 'doc/LICENSE' for the license information |
5 | 5 | |
6 | 6 | ''' |
7 | import os | |
8 | 7 | import csv |
9 | 8 | import json |
10 | 9 | import urllib |
11 | 10 | import datetime |
12 | 11 | from builtins import str |
12 | from pathlib import Path | |
13 | 13 | from tempfile import NamedTemporaryFile |
14 | 14 | from base64 import b64encode |
15 | 15 | from io import BytesIO, StringIO |
26 | 26 | |
27 | 27 | from hypothesis import given, settings, strategies as st |
28 | 28 | |
29 | from sqlalchemy import inspect | |
29 | 30 | from faraday.server.api.modules.vulns import ( |
30 | 31 | VulnerabilityFilterSet, |
31 | 32 | VulnerabilitySchema, |
32 | 33 | VulnerabilityView |
33 | 34 | ) |
34 | 35 | from faraday.server.fields import FaradayUploadedFile |
35 | from faraday.server.utils.filters import WHITE_LIST | |
36 | 36 | from faraday.server.schemas import NullToBlankString |
37 | 37 | from tests import factories |
38 | from tests.conftest import TEST_DATA | |
38 | from tests.conftest import TEST_DATA_PATH | |
39 | 39 | from tests.test_api_workspaced_base import ( |
40 | 40 | ReadOnlyAPITests |
41 | 41 | ) |
66 | 66 | WorkspaceFactory, |
67 | 67 | CustomFieldsSchemaFactory |
68 | 68 | ) |
69 | ||
70 | CURRENT_PATH = os.path.dirname(os.path.abspath(__file__)) | |
71 | ||
72 | 69 | |
73 | 70 | def _create_post_data_vulnerability(name, vuln_type, parent_id, |
74 | 71 | parent_type, refs, policyviolations, |
261 | 258 | ))), |
262 | 259 | ], ids=['standard_vuln_with_host', 'standard_vuln_with_service', |
263 | 260 | 'web_vuln_with_service']) |
264 | ||
265 | 261 | def test_hostnames(self, host_with_hostnames, test_client, session, |
266 | 262 | creator_func): |
267 | 263 | vuln = creator_func(host_with_hostnames) |
296 | 292 | ) |
297 | 293 | ws_name = host_with_hostnames.workspace.name |
298 | 294 | vuln_count_previous = session.query(Vulnerability).count() |
299 | res = test_client.post('/v2/ws/{0}/vulns/'.format(ws_name), data=raw_data) | |
295 | res = test_client.post(f'/v2/ws/{ws_name}/vulns/', data=raw_data) | |
300 | 296 | assert res.status_code == 201 |
301 | 297 | assert vuln_count_previous + 1 == session.query(Vulnerability).count() |
302 | 298 | assert res.json['name'] == 'New vulns' |
369 | 365 | assert filename in res.json['_attachments'] |
370 | 366 | attachment.close() |
371 | 367 | # check the attachment can be downloaded |
372 | res = test_client.get(self.url() + '{0}/attachment/{1}/'.format(vuln_id, filename)) | |
368 | res = test_client.get(self.url() + f'{vuln_id}/attachment/{filename}/') | |
373 | 369 | assert res.status_code == 200 |
374 | 370 | assert res.data == file_content |
375 | 371 | |
376 | 372 | res = test_client.get( |
377 | 373 | self.url() + |
378 | '{0}/attachment/notexistingattachment.png/'.format(vuln_id)) | |
374 | f'{vuln_id}/attachment/notexistingattachment.png/') | |
379 | 375 | assert res.status_code == 404 |
380 | 376 | |
381 | 377 | @pytest.mark.usefixtures('ignore_nplusone') |
400 | 396 | assert res.status_code == 200 |
401 | 397 | filename = attachment.name.split('/')[-1] |
402 | 398 | res = test_client.get( |
403 | self.url() + '{0}/attachment/{1}/'.format(vuln.id, filename)) | |
399 | self.url() + f'{vuln.id}/attachment/{filename}/') | |
404 | 400 | assert res.status_code == 200 |
405 | 401 | assert res.data == file_content |
406 | 402 | |
423 | 419 | |
424 | 420 | # verify that the old file was deleted and the new one exists |
425 | 421 | res = test_client.get( |
426 | self.url() + '{0}/attachment/{1}/'.format(vuln.id, filename)) | |
422 | self.url() + f'{vuln.id}/attachment/{filename}/') | |
427 | 423 | assert res.status_code == 404 |
428 | 424 | res = test_client.get( |
429 | self.url() + '{0}/attachment/{1}/'.format(vuln.id, new_filename)) | |
425 | self.url() + f'{vuln.id}/attachment/{new_filename}/') | |
430 | 426 | assert res.status_code == 200 |
431 | 427 | assert res.data == file_content |
432 | 428 | |
434 | 430 | vuln = VulnerabilityFactory.create(workspace=workspace) |
435 | 431 | session.add(vuln) |
436 | 432 | session.commit() |
437 | ||
438 | with open(os.path.join(TEST_DATA,'faraday.png'), 'rb') as file_obj: | |
433 | png_file = Path(__file__).parent / 'data' / 'faraday.png' | |
434 | ||
435 | with open(png_file, 'rb') as file_obj: | |
439 | 436 | new_file = FaradayUploadedFile(file_obj.read()) |
440 | 437 | |
441 | 438 | new_attach = File(object_type='vulnerability', object_id=vuln.id, name='Faraday', filename='faraday.png', |
443 | 440 | session.add(new_attach) |
444 | 441 | session.commit() |
445 | 442 | |
446 | res = test_client.get(self.url(workspace=workspace) + '{0}/attachments/'.format(vuln.id)) | |
443 | res = test_client.get(self.url(workspace=workspace) + f'{vuln.id}/attachments/') | |
447 | 444 | assert res.status_code == 200 |
448 | 445 | assert new_attach.filename in res.json |
449 | 446 | assert 'image/png' in res.json[new_attach.filename]['content_type'] |
480 | 477 | ) |
481 | 478 | ws_name = host_with_hostnames.workspace.name |
482 | 479 | vuln_count_previous = session.query(Vulnerability).count() |
483 | res = test_client.post('/v2/ws/{0}/vulns/'.format(ws_name), data=raw_data) | |
480 | res = test_client.post(f'/v2/ws/{ws_name}/vulns/', data=raw_data) | |
484 | 481 | assert res.status_code == 201 |
485 | 482 | for prop, value in vuln_props.items(): |
486 | 483 | if prop not in vuln_props_excluded: |
507 | 504 | ) |
508 | 505 | ws_name = host_with_hostnames.workspace.name |
509 | 506 | vuln_count_previous = session.query(Vulnerability).count() |
510 | res = test_client.post('/v2/ws/{0}/vulns/'.format(ws_name), data=raw_data) | |
507 | res = test_client.post(f'/v2/ws/{ws_name}/vulns/', data=raw_data) | |
511 | 508 | assert res.status_code == 201 |
512 | 509 | assert vuln_count_previous + 1 == session.query(Vulnerability).count() |
513 | res = test_client.post('/v2/ws/{0}/vulns/'.format(ws_name), data=raw_data) | |
510 | res = test_client.post(f'/v2/ws/{ws_name}/vulns/', data=raw_data) | |
514 | 511 | assert res.status_code == 409 |
515 | 512 | assert vuln_count_previous + 1 == session.query(Vulnerability).count() |
516 | 513 | |
527 | 524 | ) |
528 | 525 | ws_name = host_with_hostnames.workspace.name |
529 | 526 | vuln_count_previous = session.query(Vulnerability).count() |
530 | res = test_client.post('/v2/ws/{0}/vulns/'.format(ws_name), data=raw_data) | |
527 | res = test_client.post(f'/v2/ws/{ws_name}/vulns/', data=raw_data) | |
531 | 528 | assert res.status_code == 201 |
532 | 529 | assert vuln_count_previous + 1 == session.query(Vulnerability).count() |
533 | 530 | assert res.json['status'] == 'closed' |
644 | 641 | ws_name = host_with_hostnames.workspace.name |
645 | 642 | vuln_count_previous = session.query(Vulnerability).count() |
646 | 643 | vuln_web_count_previous = session.query(VulnerabilityWeb).count() |
647 | res = test_client.post('/v2/ws/{0}/vulns/'.format(ws_name), data=raw_data) | |
644 | res = test_client.post(f'/v2/ws/{ws_name}/vulns/', data=raw_data) | |
648 | 645 | assert res.status_code == 201 |
649 | 646 | assert vuln_web_count_previous + 1 == session.query(VulnerabilityWeb).count() |
650 | assert vuln_count_previous == session.query(Vulnerability).count() | |
647 | assert vuln_count_previous == session.query(Vulnerability).count() | |
651 | 648 | assert res.json['name'] == 'New vulns' |
652 | 649 | assert res.json['owner'] == 'test' |
653 | 650 | assert res.json['type'] == 'VulnerabilityWeb' |
715 | 712 | expected_ids.update(vuln.id for vuln in medium_vulns_web) |
716 | 713 | |
717 | 714 | res = test_client.get(self.url( |
718 | workspace=second_workspace) + '?severity=%s' % medium_name) | |
715 | workspace=second_workspace) + f'?severity={medium_name}') | |
719 | 716 | assert res.status_code == 200 |
720 | 717 | for vuln in res.json['data']: |
721 | 718 | assert vuln['severity'] == 'med' |
824 | 821 | assert vuln['target'] == '9.9.9.9' |
825 | 822 | assert set(vuln['_id'] for vuln in res.json['data']) == expected_ids |
826 | 823 | |
824 | @pytest.mark.usefixtures('ignore_nplusone') | |
825 | def test_filter_restless_by_target(self, test_client, session, workspace, host_factory): | |
826 | ||
827 | host_factory.create(workspace=workspace, ip="192.168.0.1") | |
828 | host_factory.create(workspace=workspace, ip="192.168.0.2") | |
829 | ||
830 | session.commit() | |
831 | res = test_client.get(f'{self.url()}' | |
832 | f'filter?q={{"filters":[{{"name": "target", "op":"eq", "val":"192.168.0.2"}}]}}') | |
833 | assert res.status_code == 200 | |
834 | ||
835 | @pytest.mark.usefixtures('ignore_nplusone') | |
836 | def test_filter_restless_by_target_host_ip(self, test_client, session, workspace, | |
837 | host_factory, vulnerability_factory): | |
838 | ||
839 | Vulnerability.query.delete() | |
840 | host = host_factory.create(workspace=workspace, ip="192.168.0.2") | |
841 | host_vulns = vulnerability_factory.create_batch( | |
842 | 1, workspace=self.workspace, host=host, service=None) | |
843 | ||
844 | host2 = host_factory.create(workspace=workspace, ip="192.168.0.1") | |
845 | host_vulns2 = vulnerability_factory.create_batch( | |
846 | 10, workspace=self.workspace, host=host2, service=None) | |
847 | ||
848 | session.commit() | |
849 | res = test_client.get(f'{self.url()}' | |
850 | f'filter?q={{"filters":[{{"name": "target_host_ip", "op":"eq", "val":"192.168.0.2"}}]}}') | |
851 | assert res.status_code == 200 | |
852 | assert len(res.json['vulnerabilities']) == 1 | |
853 | assert res.json['vulnerabilities'][0]['value']['target'] == '192.168.0.2' | |
854 | ||
855 | ||
856 | @pytest.mark.usefixtures('ignore_nplusone') | |
857 | def test_filter_restless_by_service_port(self, test_client, session, workspace, | |
858 | host_factory, vulnerability_factory, | |
859 | vulnerability_web_factory, service_factory): | |
860 | ||
861 | service = service_factory.create(port=9098, name="ssh", workspace=self.workspace) | |
862 | vulns = vulnerability_factory.create_batch( | |
863 | 1, workspace=self.workspace, service=service, host=None) | |
864 | ||
865 | ||
866 | service = service_factory.create(port=8956, name="443", workspace=self.workspace) | |
867 | ||
868 | vulns_web = vulnerability_web_factory.create_batch( | |
869 | 10, workspace=self.workspace, host=None, service=service) | |
870 | ||
871 | session.commit() | |
872 | res = test_client.get(f'{self.url()}' | |
873 | f'filter?q={{"filters":[{{"name": "service", "op":"has", "val":{{"name": "port", "op":"eq", "val":"8956"}} }}]}}') | |
874 | assert res.status_code == 200 | |
875 | assert len(res.json['vulnerabilities']) == 10 | |
876 | assert res.json['count'] == 10 | |
877 | ||
878 | ||
879 | @pytest.mark.usefixtures('ignore_nplusone') | |
880 | def test_filter_restless_by_service_name(self, test_client, session, workspace, | |
881 | host_factory, vulnerability_factory, | |
882 | vulnerability_web_factory, service_factory): | |
883 | ||
884 | service = service_factory.create(port=9098, name="ssh", workspace=self.workspace) | |
885 | vulns = vulnerability_factory.create_batch( | |
886 | 1, workspace=self.workspace, service=service, host=None) | |
887 | ||
888 | ||
889 | service = service_factory.create(port=8956, name="443", workspace=self.workspace) | |
890 | ||
891 | vulns_web = vulnerability_web_factory.create_batch( | |
892 | 10, workspace=self.workspace, host=None, service=service) | |
893 | ||
894 | session.commit() | |
895 | res = test_client.get(f'{self.url()}' | |
896 | f'filter?q={{"filters":[{{"name": "service", "op":"has", "val":{{"name": "name", "op":"eq", "val":"ssh"}} }}]}}') | |
897 | assert res.status_code == 200 | |
898 | assert len(res.json['vulnerabilities']) == 1 | |
899 | assert res.json['count'] == 1 | |
900 | ||
827 | 901 | @pytest.mark.usefixtures('mock_envelope_list') |
828 | 902 | def test_sort_by_method(self, session, test_client, second_workspace, |
829 | 903 | vulnerability_factory, vulnerability_web_factory): |
858 | 932 | session): |
859 | 933 | session.commit() # flush host_with_hostnames |
860 | 934 | attachments = [ |
861 | open(os.path.join(CURRENT_PATH, 'data', 'faraday.png'), 'rb'), | |
862 | open(os.path.join(CURRENT_PATH, 'data', 'test.html'), 'rb') | |
935 | (TEST_DATA_PATH / 'faraday.png').open('rb'), | |
936 | (TEST_DATA_PATH / 'test.html').open('rb') | |
863 | 937 | ] |
864 | 938 | raw_data = _create_post_data_vulnerability( |
865 | 939 | name='New vulns', |
872 | 946 | ) |
873 | 947 | ws_name = host_with_hostnames.workspace.name |
874 | 948 | vuln_count_previous = session.query(Vulnerability).count() |
875 | res = test_client.post('/v2/ws/{0}/vulns/'.format(ws_name), data=raw_data) | |
949 | res = test_client.post(f'/v2/ws/{ws_name}/vulns/', data=raw_data) | |
876 | 950 | |
877 | 951 | assert res.status_code == 201 |
878 | 952 | assert len(res.json['_attachments']) == 2 |
891 | 965 | ) |
892 | 966 | ws_name = host_with_hostnames.workspace.name |
893 | 967 | vuln_count_previous = session.query(Vulnerability).count() |
894 | res = test_client.post('/v2/ws/{0}/vulns/'.format(ws_name), data=raw_data) | |
968 | res = test_client.post(f'/v2/ws/{ws_name}/vulns/', data=raw_data) | |
895 | 969 | assert res.status_code == 201 |
896 | 970 | assert session.query(Reference).count() == 2 |
897 | 971 | assert vuln_count_previous + 1 == session.query(Vulnerability).count() |
909 | 983 | ) |
910 | 984 | ws_name = host_with_hostnames.workspace.name |
911 | 985 | vuln_count_previous = session.query(Vulnerability).count() |
912 | res = test_client.post('/v2/ws/{0}/vulns/'.format(ws_name), data=raw_data) | |
986 | res = test_client.post(f'/v2/ws/{ws_name}/vulns/', data=raw_data) | |
913 | 987 | assert res.status_code == 201 |
914 | 988 | assert session.query(PolicyViolation).count() == 1 |
915 | 989 | assert vuln_count_previous + 1 == session.query(Vulnerability).count() |
932 | 1006 | ) |
933 | 1007 | ws_name = host_with_hostnames.workspace.name |
934 | 1008 | vuln_count_previous = session.query(Vulnerability).count() |
935 | res = test_client.post('/v2/ws/{0}/vulns/'.format(ws_name), data=raw_data) | |
1009 | res = test_client.post(f'/v2/ws/{ws_name}/vulns/', data=raw_data) | |
936 | 1010 | assert res.status_code == 201 |
937 | 1011 | assert vuln_count_previous + 1 == session.query(Vulnerability).count() |
938 | 1012 | assert res.json['name'] == 'New vulns' |
959 | 1033 | ) |
960 | 1034 | ws_name = host_with_hostnames.workspace.name |
961 | 1035 | vuln_count_previous = session.query(Vulnerability).count() |
962 | res = test_client.post('/v2/ws/{0}/vulns/'.format(ws_name), data=raw_data) | |
1036 | res = test_client.post(f'/v2/ws/{ws_name}/vulns/', data=raw_data) | |
963 | 1037 | assert res.status_code == 400 |
964 | 1038 | |
965 | 1039 | def test_create_vuln_with_invalid_type(self, |
978 | 1052 | ws_name = host_with_hostnames.workspace.name |
979 | 1053 | vuln_count_previous = session.query(Vulnerability).count() |
980 | 1054 | res = test_client.post( |
981 | '/v2/ws/{0}/vulns/'.format(ws_name), | |
1055 | f'/v2/ws/{ws_name}/vulns/', | |
982 | 1056 | data=raw_data, |
983 | 1057 | ) |
984 | 1058 | assert res.status_code == 400 |
1007 | 1081 | raw_data.pop("type") |
1008 | 1082 | ws_name = host_with_hostnames.workspace.name |
1009 | 1083 | vuln_count_previous = session.query(Vulnerability).count() |
1010 | res = test_client.post('/v2/ws/{0}/vulns/'.format(ws_name), data=raw_data) | |
1084 | res = test_client.post(f'/v2/ws/{ws_name}/vulns/', data=raw_data) | |
1011 | 1085 | assert res.status_code == 400 |
1012 | 1086 | assert vuln_count_previous == session.query(Vulnerability).count() |
1013 | 1087 | assert res.json['message'] == 'Type is required.' |
1027 | 1101 | ) |
1028 | 1102 | ws_name = host_with_hostnames.workspace.name |
1029 | 1103 | vuln_count_previous = session.query(Vulnerability).count() |
1030 | res = test_client.post('/v2/ws/{0}/vulns/'.format(ws_name), data=raw_data) | |
1104 | res = test_client.post(f'/v2/ws/{ws_name}/vulns/', data=raw_data) | |
1031 | 1105 | assert res.status_code == 400 |
1032 | 1106 | assert vuln_count_previous == session.query(Vulnerability).count() |
1033 | 1107 | assert b'Invalid severity type.' in res.data |
1048 | 1122 | ) |
1049 | 1123 | ws_name = host_with_hostnames.workspace.name |
1050 | 1124 | vuln_count_previous = session.query(Vulnerability).count() |
1051 | res = test_client.post('/v2/ws/{0}/vulns/'.format(ws_name), data=raw_data) | |
1125 | res = test_client.post(f'/v2/ws/{ws_name}/vulns/', data=raw_data) | |
1052 | 1126 | assert res.status_code == 400 |
1053 | 1127 | assert vuln_count_previous == session.query(Vulnerability).count() |
1054 | 1128 | assert list(res.json['messages']['json'].keys()) == ['easeofresolution'] |
1069 | 1143 | easeofresolution=None, |
1070 | 1144 | ) |
1071 | 1145 | ws_name = host_with_hostnames.workspace.name |
1072 | res = test_client.post('/v2/ws/{0}/vulns/'.format(ws_name), | |
1146 | res = test_client.post(f'/v2/ws/{ws_name}/vulns/', | |
1073 | 1147 | data=raw_data) |
1074 | 1148 | assert res.status_code == 201, (res.status_code, res.data) |
1075 | 1149 | created_vuln = Vulnerability.query.get(res.json['_id']) |
1365 | 1439 | web_expected_ids.update(vuln.id for vuln in high_vulns_web) |
1366 | 1440 | |
1367 | 1441 | res = test_client.get(self.url( |
1368 | workspace=second_workspace) + '?command_id={0}'.format(command.id)) | |
1442 | workspace=second_workspace) + f'?command_id={command.id}') | |
1369 | 1443 | assert res.status_code == 200 |
1370 | 1444 | for vuln in res.json['data']: |
1371 | 1445 | command_object = CommandObject.query.filter_by( |
1378 | 1452 | |
1379 | 1453 | # Check for web vulns |
1380 | 1454 | res = test_client.get(self.url( |
1381 | workspace=second_workspace) + '?command_id={0}'.format(web_command.id)) | |
1455 | workspace=second_workspace) + f'?command_id={web_command.id}') | |
1382 | 1456 | assert res.status_code == 200 |
1383 | 1457 | for vuln in res.json['data']: |
1384 | 1458 | command_object = CommandObject.query.filter_by( |
1391 | 1465 | |
1392 | 1466 | # Check for cross-workspace bugs |
1393 | 1467 | res = test_client.get(self.url( |
1394 | workspace=workspace) + '?command_id={0}'.format(web_command.id)) | |
1468 | workspace=workspace) + f'?command_id={web_command.id}') | |
1395 | 1469 | assert res.status_code == 200 |
1396 | 1470 | assert len(res.json['data']) == 0 |
1397 | 1471 | |
1507 | 1581 | severity='low', |
1508 | 1582 | ) |
1509 | 1583 | ws_name = host_with_hostnames.workspace.name |
1510 | res = test_client.post('/v2/ws/{0}/vulns/'.format(ws_name), data=raw_data) | |
1584 | res = test_client.post(f'/v2/ws/{ws_name}/vulns/', data=raw_data) | |
1511 | 1585 | assert res.status_code == 201 |
1512 | res = test_client.post('/v2/ws/{0}/vulns/'.format(ws_name), | |
1586 | res = test_client.post(f'/v2/ws/{ws_name}/vulns/', | |
1513 | 1587 | data=raw_data) |
1514 | 1588 | assert res.status_code == 409 |
1515 | 1589 | |
1534 | 1608 | severity='low', |
1535 | 1609 | ) |
1536 | 1610 | ws_name = host_with_hostnames.workspace.name |
1537 | res = test_client.post('/v2/ws/{0}/vulns/'.format(ws_name), data=raw_data) | |
1611 | res = test_client.post(f'/v2/ws/{ws_name}/vulns/', data=raw_data) | |
1538 | 1612 | assert res.status_code == 201 |
1539 | res = test_client.post('/v2/ws/{0}/vulns/'.format(ws_name), | |
1613 | res = test_client.post(f'/v2/ws/{ws_name}/vulns/', | |
1540 | 1614 | data=raw_data) |
1541 | 1615 | assert res.status_code == 409 |
1542 | 1616 | |
1603 | 1677 | severity='low', |
1604 | 1678 | ) |
1605 | 1679 | ws_name = host_with_hostnames.workspace.name |
1606 | res = test_client.post('/v2/ws/{0}/vulns/?command_id={1}'.format(ws_name, command.id), data=raw_data) | |
1680 | res = test_client.post(f'/v2/ws/{ws_name}/vulns/?command_id={command.id}', data=raw_data) | |
1607 | 1681 | assert res.status_code == 201 |
1608 | 1682 | raw_data = _create_post_data_vulnerability( |
1609 | 1683 | name='Update vulnsweb', |
1615 | 1689 | description='Update helloworld', |
1616 | 1690 | severity='high', |
1617 | 1691 | ) |
1618 | res = test_client.put('/v2/ws/{0}/vulns/{1}/?command_id={2}'.format(ws_name, res.json['_id'], command.id), | |
1692 | res = test_client.put(f"/v2/ws/{ws_name}/vulns/{res.json['_id']}/?command_id={command.id}", | |
1619 | 1693 | data=raw_data) |
1620 | 1694 | assert res.status_code == 200 |
1621 | 1695 | |
1744 | 1818 | session.add(vuln) |
1745 | 1819 | session.add(vuln2) |
1746 | 1820 | session.commit() |
1747 | res = test_client.get(self.url(workspace=vuln.workspace) + '?id={0}'.format(vuln.id)) | |
1821 | res = test_client.get(self.url(workspace=vuln.workspace) + f'?id={vuln.id}') | |
1748 | 1822 | assert res.json['count'] == 1 |
1749 | 1823 | assert res.json['vulnerabilities'][0]['value']['name'] == vuln.name |
1750 | 1824 | |
1761 | 1835 | session.add(service) |
1762 | 1836 | session.add(hostname) |
1763 | 1837 | session.commit() |
1764 | url = self.url(workspace=workspace) + '?hostnames={0}'.format(hostname.name) | |
1838 | url = self.url(workspace=workspace) + f'?hostnames={hostname.name}' | |
1765 | 1839 | res = test_client.get(url) |
1766 | 1840 | |
1767 | 1841 | assert res.status_code == 200 |
1780 | 1854 | session.add(host) |
1781 | 1855 | session.add(hostname) |
1782 | 1856 | session.commit() |
1783 | url = self.url(workspace=workspace) + '?hostnames={0}'.format(hostname.name) | |
1857 | url = self.url(workspace=workspace) + f'?hostnames={hostname.name}' | |
1784 | 1858 | res = test_client.get(url) |
1785 | 1859 | assert res.status_code == 200 |
1786 | 1860 | assert res.json['count'] == 1 |
1804 | 1878 | session.commit() |
1805 | 1879 | |
1806 | 1880 | #Search with hosnames=HA,HB |
1807 | res = test_client.get(self.url(workspace=vuln.workspace) + '?hostname={0},{1}'.format(hostnameA,hostnameB)) | |
1881 | res = test_client.get(self.url(workspace=vuln.workspace) + f'?hostname={hostnameA},{hostnameB}') | |
1808 | 1882 | assert res.status_code == 200 |
1809 | 1883 | assert res.json['count'] == 2 |
1810 | 1884 | |
1851 | 1925 | session.commit() |
1852 | 1926 | file_contents = b'my file contents' |
1853 | 1927 | data = { |
1854 | 'file' : (BytesIO(file_contents), 'borrar.txt') | |
1928 | 'file': (BytesIO(file_contents), 'borrar.txt') | |
1855 | 1929 | } |
1856 | 1930 | headers = {'Content-type': 'multipart/form-data'} |
1857 | 1931 | |
1858 | 1932 | res = test_client.post( |
1859 | '/v2/ws/abc/vulns/{0}/attachment/'.format(vuln.id), | |
1933 | f'/v2/ws/abc/vulns/{vuln.id}/attachment/', | |
1860 | 1934 | data=data, headers=headers, use_json_data=False) |
1861 | 1935 | assert res.status_code == 403 # Missing CSRF protection |
1862 | 1936 | |
1863 | 1937 | data = { |
1864 | 'file' : (BytesIO(file_contents), 'borrar.txt'), | |
1938 | 'file': (BytesIO(file_contents), 'borrar.txt'), | |
1865 | 1939 | 'csrf_token': csrf_token |
1866 | 1940 | } |
1867 | 1941 | res = test_client.post( |
1868 | '/v2/ws/abc/vulns/{0}/attachment/'.format(vuln.id), | |
1942 | f'/v2/ws/abc/vulns/{vuln.id}/attachment/', | |
1869 | 1943 | data=data, headers=headers, use_json_data=False) |
1870 | 1944 | assert res.status_code == 200 # Now it should work |
1871 | 1945 | |
1881 | 1955 | session.commit() |
1882 | 1956 | file_contents = b'my file contents' |
1883 | 1957 | data = { |
1884 | 'file' : (BytesIO(file_contents), 'borrar.txt') | |
1958 | 'file': (BytesIO(file_contents), 'borrar.txt') | |
1885 | 1959 | } |
1886 | 1960 | headers = {'Content-type': 'multipart/form-data'} |
1887 | 1961 | |
1889 | 1963 | session.commit() |
1890 | 1964 | |
1891 | 1965 | res = test_client.post( |
1892 | '/v2/ws/abc/vulns/{0}/attachment/'.format(vuln.id), | |
1966 | f'/v2/ws/abc/vulns/{vuln.id}/attachment/', | |
1893 | 1967 | data=data, headers=headers, use_json_data=False) |
1894 | 1968 | assert res.status_code == 403 |
1895 | 1969 | query_test = session.query(Vulnerability).filter_by(id=vuln.id).first().evidence |
1911 | 1985 | policyviolations=[], |
1912 | 1986 | attachments=[attachment] |
1913 | 1987 | ) |
1914 | res = test_client.post('/v2/ws/{workspace}/vulns/'\ | |
1915 | .format(workspace=ws_name), data=vuln) | |
1988 | res = test_client.post(f'/v2/ws/{ws_name}/vulns/', data=vuln) | |
1916 | 1989 | assert res.status_code == 201 |
1917 | 1990 | |
1918 | 1991 | filename = attachment.name.split('/')[-1] |
1919 | 1992 | vuln_id = res.json['_id'] |
1920 | 1993 | res = test_client.delete( |
1921 | '/v2/ws/{workspace}/vulns/{vulnerability}/attachment/{file_name}/' | |
1922 | .format(workspace=ws_name, vulnerability=vuln_id, file_name=filename) | |
1994 | f'/v2/ws/{ws_name}/vulns/{vuln_id}/attachment/{filename}/' | |
1923 | 1995 | ) |
1924 | 1996 | assert res.status_code == 200 |
1925 | 1997 | |
1942 | 2014 | policyviolations=[], |
1943 | 2015 | attachments=[attachment] |
1944 | 2016 | ) |
1945 | res = test_client.post('/v2/ws/{workspace}/vulns/'\ | |
1946 | .format(workspace=ws_name), data=vuln) | |
2017 | res = test_client.post(f'/v2/ws/{ws_name}/vulns/', data=vuln) | |
1947 | 2018 | assert res.status_code == 201 |
1948 | 2019 | |
1949 | 2020 | self.workspace.readonly = True |
1952 | 2023 | filename = attachment.name.split('/')[-1] |
1953 | 2024 | vuln_id = res.json['_id'] |
1954 | 2025 | res = test_client.delete( |
1955 | '/v2/ws/{workspace}/vulns/{vulnerability}/attachment/{file_name}/' | |
1956 | .format(workspace=ws_name, vulnerability=vuln_id, file_name=filename) | |
2026 | f'/v2/ws/{ws_name}/vulns/{vuln_id}/attachment/{filename}/' | |
1957 | 2027 | ) |
1958 | 2028 | assert res.status_code == 403 |
1959 | 2029 | |
1961 | 2031 | assert len(query_test) == 1 |
1962 | 2032 | assert query_test[0].filename == filename |
1963 | 2033 | |
1964 | @pytest.mark.usefixtures('ignore_nplusone') | |
1965 | 2034 | def test_vuln_filter(self, test_client, session, workspace): |
1966 | 2035 | new_host = HostFactory.create(workspace=workspace) |
1967 | 2036 | session.commit() |
1976 | 2045 | description='helloworld', |
1977 | 2046 | severity='medium', |
1978 | 2047 | ) |
1979 | res = test_client.post('/v2/ws/{workspace}/vulns/' | |
1980 | .format(workspace=workspace.name), data=raw_data) | |
2048 | res = test_client.post(f'/v2/ws/{workspace.name}/vulns/', data=raw_data) | |
1981 | 2049 | |
1982 | 2050 | data = { |
1983 | 2051 | 'q': '{"filters":[{"name":"severity","op":"eq","val":"medium"}]}' |
1993 | 2061 | data = { |
1994 | 2062 | "q": {"filters":[{"name":"severity","op":"eq","val":"medium"}]} |
1995 | 2063 | } |
1996 | res = test_client.get('/v2/ws/{name}/vulns/filter'.format(name=workspace.name), query_string=data) | |
2064 | res = test_client.get(f'/v2/ws/{workspace.name}/vulns/filter', query_string=data) | |
1997 | 2065 | assert res.status_code == 400 |
1998 | 2066 | |
1999 | @pytest.mark.usefixtures('ignore_nplusone') | |
2000 | 2067 | def test_vuln_filter_exception(self, test_client, workspace, session): |
2001 | 2068 | vuln = VulnerabilityFactory.create(workspace=workspace, severity="medium") |
2002 | 2069 | session.add(vuln) |
2004 | 2071 | data = { |
2005 | 2072 | 'q': '{"filters":[{"name":"severity","op":"eq","val":"medium"}]}' |
2006 | 2073 | } |
2007 | res = test_client.get('/v2/ws/{name}/vulns/filter'.format(name=workspace.name), query_string=data) | |
2074 | res = test_client.get(f'/v2/ws/{workspace.name}/vulns/filter', query_string=data) | |
2008 | 2075 | assert res.status_code == 200 |
2009 | 2076 | assert res.json['count'] == 1 |
2010 | 2077 | |
2011 | @pytest.mark.usefixtures('ignore_nplusone') | |
2012 | 2078 | def test_vuln_restless_group_same_creator(self, test_client, session): |
2013 | 2079 | workspace = WorkspaceFactory.create() |
2014 | 2080 | creator = UserFactory.create() |
2028 | 2094 | data = { |
2029 | 2095 | 'q': '{"group_by":[{"field":"creator_id"}]}' |
2030 | 2096 | } |
2031 | res = test_client.get('/v2/ws/{name}/vulns/filter'.format(name=workspace.name), query_string=data) | |
2097 | res = test_client.get(f'/v2/ws/{workspace.name}/vulns/filter', query_string=data) | |
2032 | 2098 | assert res.status_code == 200 |
2033 | 2099 | assert res.json['count'] == 1 # all vulns created by the same creator |
2034 | 2100 | expected = [{'count': 2, 'creator_id': creator.id}] |
2035 | 2101 | assert [vuln['value'] for vuln in res.json['vulnerabilities']] == expected |
2036 | 2102 | |
2037 | @pytest.mark.usefixtures('ignore_nplusone') | |
2103 | def test_vuln_group_by_severity_does_not_duplicate_groups(self, test_client, session): | |
2104 | workspace = WorkspaceFactory.create() | |
2105 | creator = UserFactory.create() | |
2106 | vuln = VulnerabilityFactory.create_batch(size=10, | |
2107 | workspace=workspace, | |
2108 | severity="critical", | |
2109 | creator=creator, | |
2110 | ) | |
2111 | vuln2 = VulnerabilityWebFactory.create_batch(size=10, | |
2112 | workspace=workspace, | |
2113 | severity="critical", | |
2114 | creator=creator, | |
2115 | ) | |
2116 | session.add_all(vuln) | |
2117 | session.add_all(vuln2) | |
2118 | session.commit() | |
2119 | data = { | |
2120 | 'q': '{"group_by":[{"field":"severity"}]}' | |
2121 | } | |
2122 | res = test_client.get(f'/v2/ws/{workspace.name}/vulns/filter', query_string=data) | |
2123 | assert res.status_code == 200, res.json | |
2124 | assert res.json['count'] == 1, res.json # all vulns created by the same creator | |
2125 | expected = { | |
2126 | 'count': 1, | |
2127 | 'vulnerabilities': [ | |
2128 | {'id': 0, 'key': 0, 'value': {'count': 20, 'severity': 'critical'}} | |
2129 | ] | |
2130 | } | |
2131 | assert res.json == expected, res.json | |
2132 | ||
2133 | def test_vuln_group_by_multiple_fields(self, test_client, session): | |
2134 | workspace = WorkspaceFactory.create() | |
2135 | creator = UserFactory.create() | |
2136 | vuln = VulnerabilityFactory.create_batch(size=10, | |
2137 | name='name 1', | |
2138 | workspace=workspace, | |
2139 | severity="critical", | |
2140 | creator=creator, | |
2141 | ) | |
2142 | vuln2 = VulnerabilityWebFactory.create_batch(size=10, | |
2143 | name='name 2', | |
2144 | workspace=workspace, | |
2145 | severity="critical", | |
2146 | creator=creator, | |
2147 | ) | |
2148 | session.add_all(vuln) | |
2149 | session.add_all(vuln2) | |
2150 | session.commit() | |
2151 | data = { | |
2152 | 'q': '{"group_by":[{"field":"severity"}, {"field": "name"}]}' | |
2153 | } | |
2154 | res = test_client.get(f'/v2/ws/{workspace.name}/vulns/filter', query_string=data) | |
2155 | assert res.status_code == 200, res.json | |
2156 | assert res.json['count'] == 2, res.json # all vulns created by the same creator | |
2157 | expected ={'vulnerabilities': [ | |
2158 | {'id': 0, 'key': 0, 'value': {'count': 10, 'severity': 'critical', 'name': 'name 1'}}, {'id': 1, 'key': 1, 'value': {'count': 10, 'severity': 'critical', 'name': 'name 2'}}], 'count': 2} | |
2159 | ||
2160 | assert res.json == expected, res.json | |
2161 | ||
2162 | @pytest.mark.parametrize('col_name', [ | |
2163 | 'severity', | |
2164 | 'name', | |
2165 | 'status', | |
2166 | 'description', | |
2167 | ]) | |
2168 | def test_vuln_group_by_all_columns(self, col_name, test_client, session): | |
2169 | workspace = WorkspaceFactory.create() | |
2170 | creator = UserFactory.create() | |
2171 | vuln = VulnerabilityFactory.create_batch(size=10, | |
2172 | workspace=workspace, | |
2173 | severity="critical", | |
2174 | creator=creator, | |
2175 | ) | |
2176 | vuln2 = VulnerabilityWebFactory.create_batch(size=10, | |
2177 | workspace=workspace, | |
2178 | severity="critical", | |
2179 | creator=creator, | |
2180 | ) | |
2181 | session.add_all(vuln) | |
2182 | session.add_all(vuln2) | |
2183 | session.commit() | |
2184 | data = { | |
2185 | 'q': json.dumps({"group_by":[{"field":col_name}]}) | |
2186 | } | |
2187 | res = test_client.get(f'/v2/ws/{workspace.name}/vulns/filter', query_string=data) | |
2188 | assert res.status_code == 200, res.json | |
2189 | ||
2038 | 2190 | def test_vuln_restless_group_same_name_description(self, test_client, session): |
2039 | 2191 | workspace = WorkspaceFactory.create() |
2040 | 2192 | creator = UserFactory.create() |
2066 | 2218 | data = { |
2067 | 2219 | 'q': '{"group_by":[{"field":"name"}, {"field":"description"}]}' |
2068 | 2220 | } |
2069 | res = test_client.get('/v2/ws/{name}/vulns/filter'.format(name=workspace.name), query_string=data) | |
2221 | res = test_client.get(f'/v2/ws/{workspace.name}/vulns/filter', query_string=data) | |
2070 | 2222 | assert res.status_code == 200 |
2071 | 2223 | assert res.json['count'] == 2 |
2072 | 2224 | expected = [{'count': 2, 'name': 'test', 'description': 'test'}, {'count': 1, 'name': 'test2', 'description': 'test'}] |
2073 | 2225 | assert [vuln['value'] for vuln in res.json['vulnerabilities']] == expected |
2074 | 2226 | |
2075 | @pytest.mark.usefixtures('ignore_nplusone') | |
2076 | 2227 | def test_vuln_restless_sort_by_(self, test_client, session): |
2077 | 2228 | workspace = WorkspaceFactory.create() |
2078 | 2229 | host = HostFactory.create(workspace=workspace) |
2132 | 2283 | data = { |
2133 | 2284 | 'q': json.dumps(query) |
2134 | 2285 | } |
2135 | res = test_client.get('/v2/ws/{name}/vulns/filter'.format(name=workspace.name), query_string=data) | |
2286 | res = test_client.get(f'/v2/ws/{workspace.name}/vulns/filter', query_string=data) | |
2136 | 2287 | assert res.status_code == 200 |
2137 | 2288 | assert res.json['count'] == 12 |
2138 | 2289 | expected_order = ['critical', 'critical', 'med', 'med', 'med', 'med', 'med', 'med', 'med', 'med', 'med', 'med'] |
2146 | 2297 | data = { |
2147 | 2298 | 'q': json.dumps({"filters":[{"name":"creator","op":"eq","val": vuln.creator.username}]}) |
2148 | 2299 | } |
2149 | res = test_client.get('/v2/ws/{name}/vulns/filter'.format(name=workspace.name), query_string=data) | |
2150 | assert res.status_code == 200 | |
2151 | ||
2152 | @pytest.mark.usefixtures('ignore_nplusone') | |
2300 | res = test_client.get(f'/v2/ws/{workspace.name}/vulns/filter', query_string=data) | |
2301 | assert res.status_code == 200 | |
2302 | ||
2153 | 2303 | def test_vuln_web_filter_exception(self, test_client, workspace, session): |
2154 | 2304 | vuln = VulnerabilityWebFactory.create(workspace=workspace, severity="medium") |
2155 | 2305 | session.add(vuln) |
2157 | 2307 | data = { |
2158 | 2308 | 'q': '{"filters":[{"name":"severity","op":"eq","val":"medium"}]}' |
2159 | 2309 | } |
2160 | res = test_client.get('/v2/ws/{name}/vulns/filter'.format(name=workspace.name), query_string=data) | |
2310 | res = test_client.get(f'/v2/ws/{workspace.name}/vulns/filter', query_string=data) | |
2161 | 2311 | assert res.status_code == 200 |
2162 | 2312 | assert res.json['count'] == 1 |
2163 | 2313 | |
2193 | 2343 | session.commit() |
2194 | 2344 | |
2195 | 2345 | res = test_client.post( |
2196 | '/v2/ws/{ws_name}/vulns/{id}/attachment/' | |
2197 | .format(ws_name=workspace.name,id=vuln.id), | |
2346 | f'/v2/ws/{workspace.name}/vulns/{vuln.id}/attachment/', | |
2198 | 2347 | data={'csrf_token': csrf_token}, |
2199 | 2348 | headers={'Content-Type': 'multipart/form-data'}, |
2200 | 2349 | use_json_data=False) |
2202 | 2351 | |
2203 | 2352 | def test_get_attachment_with_invalid_workspace_and_vuln(self, test_client): |
2204 | 2353 | res = test_client.get( |
2205 | '/v2/ws/{ws_name}/vulns/{vuln_id}/attachment/{name}/' | |
2206 | .format(ws_name="invalid_ws", vuln_id="invalid_vuln", name="random_name")) | |
2354 | "/v2/ws/invalid_ws/vulns/invalid_vuln/attachment/random_name/") | |
2207 | 2355 | assert res.status_code == 404 |
2208 | 2356 | |
2209 | 2357 | def test_delete_attachment_with_invalid_workspace_and_vuln(self, test_client): |
2210 | 2358 | res = test_client.delete( |
2211 | '/v2/ws/{ws_name}/vulns/{vuln_id}/attachment/{name}/' | |
2212 | .format(ws_name="invalid_ws",vuln_id="invalid_vuln", name="random_name")) | |
2359 | "/v2/ws/invalid_ws/vulns/invalid_vuln/attachment/random_name/") | |
2213 | 2360 | assert res.status_code == 404 |
2214 | 2361 | |
2215 | 2362 | def test_delete_invalid_attachment(self, test_client, workspace, session): |
2217 | 2364 | session.add(vuln) |
2218 | 2365 | session.commit() |
2219 | 2366 | res = test_client.delete( |
2220 | '/v2/ws/{ws_name}/vulns/{vuln_id}/attachment/{name}/' | |
2221 | .format(ws_name=workspace.name,vuln_id=vuln.id, name="random_name")) | |
2367 | f"/v2/ws/{workspace.name}/vulns/{vuln.id}/attachment/random_name/") | |
2222 | 2368 | assert res.status_code == 404 |
2223 | 2369 | |
2224 | 2370 | def test_export_vuln_csv_empty_workspace(self, test_client, session): |
2225 | 2371 | ws = WorkspaceFactory(name='abc') |
2226 | res = test_client.get('/v2/ws/{ws_name}/vulns/export_csv/'.format(ws_name=ws.name)) | |
2372 | res = test_client.get(f'/v2/ws/{ws.name}/vulns/export_csv/') | |
2227 | 2373 | expected_headers = [ |
2228 | 2374 | "confirmed", "id", "date", "name", "severity", "service", |
2229 | 2375 | "target", "desc", "status", "hostnames", "comments", "owner", |
2240 | 2386 | assert res.status_code == 200 |
2241 | 2387 | assert expected_headers == res.data.decode('utf-8').strip('\r\n').split(',') |
2242 | 2388 | |
2243 | @pytest.mark.usefixtures('ignore_nplusone') | |
2244 | 2389 | def test_export_vuln_csv_filters_confirmed_using_filters_query(self, test_client, session): |
2245 | 2390 | workspace = WorkspaceFactory.create() |
2246 | 2391 | confirmed_vulns = VulnerabilityFactory.create(confirmed=True, workspace=workspace) |
2303 | 2448 | session.add(vuln) |
2304 | 2449 | session.commit() |
2305 | 2450 | |
2306 | res = test_client.get('v2/ws/{}/vulns/export_csv/'.format(workspace.name)) | |
2451 | res = test_client.get(f'v2/ws/{workspace.name}/vulns/export_csv/') | |
2307 | 2452 | assert res.status_code == 200 |
2308 | 2453 | |
2309 | 2454 | csv_data = csv.DictReader(StringIO(res.data.decode('utf-8')), delimiter=',') |
2616 | 2761 | ) |
2617 | 2762 | ws_name = host_with_hostnames.workspace.name |
2618 | 2763 | vuln_count_previous = session.query(Vulnerability).count() |
2619 | res_1 = test_client.post('/v2/ws/{0}/vulns/'.format(ws_name), data=raw_data_vuln_1) | |
2620 | res_2 = test_client.post('/v2/ws/{0}/vulns/'.format(ws_name), data=raw_data_vuln_2) | |
2764 | res_1 = test_client.post(f'/v2/ws/{ws_name}/vulns/', data=raw_data_vuln_1) | |
2765 | res_2 = test_client.post(f'/v2/ws/{ws_name}/vulns/', data=raw_data_vuln_2) | |
2621 | 2766 | vuln_1_id = res_1.json['obj_id'] |
2622 | 2767 | vuln_2_id = res_2.json['obj_id'] |
2623 | 2768 | vulns_to_delete = [vuln_1_id, vuln_2_id] |
2624 | 2769 | request_data = {'vulnerability_ids': vulns_to_delete} |
2625 | delete_response = test_client.delete('/v2/ws/{0}/vulns/bulk_delete/'.format(ws_name), data=request_data) | |
2770 | delete_response = test_client.delete(f'/v2/ws/{ws_name}/vulns/bulk_delete/', data=request_data) | |
2626 | 2771 | vuln_count_after = session.query(Vulnerability).count() |
2627 | 2772 | deleted_vulns = delete_response.json['deleted_vulns'] |
2628 | 2773 | assert delete_response.status_code == 200 |
2661 | 2806 | ) |
2662 | 2807 | ws_name = host_with_hostnames.workspace.name |
2663 | 2808 | vuln_count_previous = session.query(Vulnerability).count() |
2664 | res_1 = test_client.post('/v2/ws/{0}/vulns/'.format(ws_name), data=raw_data_vuln_1) | |
2665 | res_2 = test_client.post('/v2/ws/{0}/vulns/'.format(ws_name), data=raw_data_vuln_2) | |
2809 | res_1 = test_client.post(f'/v2/ws/{ws_name}/vulns/', data=raw_data_vuln_1) | |
2810 | res_2 = test_client.post(f'/v2/ws/{ws_name}/vulns/', data=raw_data_vuln_2) | |
2666 | 2811 | vuln_1_id = res_1.json['obj_id'] |
2667 | 2812 | vuln_2_id = res_2.json['obj_id'] |
2668 | 2813 | vulns_to_delete = [vuln_1_id, vuln_2_id] |
2669 | 2814 | request_data = {'severities': ['low']} |
2670 | delete_response = test_client.delete('/v2/ws/{0}/vulns/bulk_delete/'.format(ws_name), data=request_data) | |
2815 | delete_response = test_client.delete(f'/v2/ws/{ws_name}/vulns/bulk_delete/', data=request_data) | |
2671 | 2816 | vuln_count_after = session.query(Vulnerability).count() |
2672 | 2817 | deleted_vulns = delete_response.json['deleted_vulns'] |
2673 | 2818 | assert delete_response.status_code == 200 |
2697 | 2842 | ) |
2698 | 2843 | ws_name = host_with_hostnames.workspace.name |
2699 | 2844 | vuln_count_previous = session.query(Vulnerability).count() |
2700 | res = test_client.post('/v2/ws/{0}/vulns/'.format(ws_name), data=raw_data) | |
2845 | res = test_client.post(f'/v2/ws/{ws_name}/vulns/', data=raw_data) | |
2701 | 2846 | assert res.status_code == 201 |
2702 | 2847 | assert vuln_count_previous + 1 == session.query(Vulnerability).count() |
2703 | 2848 | assert res.json['tool'] == tool_name |
2723 | 2868 | ) |
2724 | 2869 | ws_name = host_with_hostnames.workspace.name |
2725 | 2870 | vuln_count_previous = session.query(Vulnerability).count() |
2726 | res = test_client.post('/v2/ws/{0}/vulns/'.format(ws_name), data=raw_data) | |
2871 | res = test_client.post(f'/v2/ws/{ws_name}/vulns/', data=raw_data) | |
2727 | 2872 | assert res.status_code == 201 |
2728 | 2873 | assert vuln_count_previous + 1 == session.query(Vulnerability).count() |
2729 | 2874 | assert res.json['tool'] == "Web UI" |
2799 | 2944 | @pytest.mark.usefixtures('logged_user') |
2800 | 2945 | class TestVulnerabilitySearch(): |
2801 | 2946 | |
2802 | @pytest.mark.usefixtures('ignore_nplusone') | |
2803 | 2947 | @pytest.mark.skip_sql_dialect('sqlite') |
2804 | 2948 | def test_search_by_hostname_vulns(self, test_client, session): |
2805 | 2949 | workspace = WorkspaceFactory.create() |
2814 | 2958 | [{"name":"hostnames","op":"eq","val":"pepe"}] |
2815 | 2959 | } |
2816 | 2960 | res = test_client.get( |
2817 | '/v2/ws/{}/vulns/?q={}'.format(workspace.name, json.dumps(query_filter))) | |
2961 | f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
2818 | 2962 | assert res.status_code == 200 |
2819 | 2963 | assert res.json['count'] == 1 |
2820 | 2964 | assert res.json['vulnerabilities'][0]['id'] == vuln.id |
2821 | 2965 | |
2822 | @pytest.mark.usefixtures('ignore_nplusone') | |
2823 | 2966 | @pytest.mark.skip_sql_dialect('sqlite') |
2824 | 2967 | def test_search_by_hostname_vulns_with_service(self, test_client, session): |
2825 | 2968 | workspace = WorkspaceFactory.create() |
2835 | 2978 | [{"name":"hostnames","op":"eq","val":"pepe"}] |
2836 | 2979 | } |
2837 | 2980 | res = test_client.get( |
2838 | '/v2/ws/{}/vulns/?q={}'.format(workspace.name, json.dumps(query_filter))) | |
2981 | f'/v2/ws/{workspace.name}/vulns/?q={json.dumps(query_filter)}') | |
2839 | 2982 | assert res.status_code == 200 |
2840 | 2983 | assert res.json['count'] == 1 |
2841 | 2984 | assert res.json['vulnerabilities'][0]['id'] == vuln.id |
2842 | 2985 | |
2986 | @pytest.mark.skip_sql_dialect('sqlite') | |
2843 | 2987 | @pytest.mark.usefixtures('ignore_nplusone') |
2844 | @pytest.mark.skip_sql_dialect('sqlite') | |
2845 | 2988 | def test_search_hostname_web_vulns(self, test_client, session): |
2846 | 2989 | workspace = WorkspaceFactory.create() |
2847 | 2990 | host = HostFactory.create(workspace=workspace) |
2856 | 2999 | [{"name": "hostnames", "op": "eq", "val": "pepe"}] |
2857 | 3000 | } |
2858 | 3001 | res = test_client.get( |
2859 | '/v2/ws/{}/vulns/?q={}'.format(workspace.name, json.dumps(query_filter))) | |
3002 | f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
2860 | 3003 | assert res.status_code == 200 |
2861 | 3004 | assert res.json['count'] == 1 |
2862 | 3005 | assert res.json['vulnerabilities'][0]['id'] == vuln.id |
2863 | 3006 | |
2864 | @pytest.mark.usefixtures('ignore_nplusone') | |
2865 | 3007 | def test_search_empty_filters(self, workspace, test_client, session): |
2866 | 3008 | query_filter = {"filters": |
2867 | 3009 | [] |
2868 | 3010 | } |
2869 | 3011 | res = test_client.get( |
2870 | '/v2/ws/{}/vulns/?q={}'.format(workspace.name, json.dumps(query_filter))) | |
3012 | f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
2871 | 3013 | assert res.status_code == 200 |
2872 | 3014 | assert res.json['count'] == 0 |
2873 | 3015 | |
2874 | @pytest.mark.usefixtures('ignore_nplusone') | |
2875 | 3016 | def test_search_code_attribute_bug(self, workspace, test_client, session): |
2876 | 3017 | query_filter = {"filters": |
2877 | 3018 | [{"name":"code", "op": "eq", "val": "test"}] |
2878 | 3019 | } |
2879 | 3020 | res = test_client.get( |
2880 | '/v2/ws/{}/vulns/?q={}'.format(workspace.name, json.dumps(query_filter))) | |
2881 | assert res.status_code == 200 | |
2882 | assert res.json['count'] == 0 | |
2883 | ||
2884 | @pytest.mark.usefixtures('ignore_nplusone') | |
2885 | def test_search_code_attribute_bug(self, workspace, test_client, session): | |
2886 | query_filter = {"filters": | |
2887 | [{"name":"code", "op": "eq", "val": "test"}] | |
2888 | } | |
2889 | res = test_client.get( | |
2890 | '/v2/ws/{}/vulns/?q={}'.format(workspace.name, json.dumps(query_filter))) | |
2891 | assert res.status_code == 200 | |
2892 | assert res.json['count'] == 0 | |
2893 | ||
2894 | @pytest.mark.usefixtures('ignore_nplusone') | |
3021 | f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3022 | ||
3023 | assert res.status_code == 400, res.json | |
3024 | ||
2895 | 3025 | @pytest.mark.skip_sql_dialect('sqlite') |
2896 | 3026 | def test_search_by_hostname_multiple_logic(self, test_client, session): |
2897 | 3027 | workspace = WorkspaceFactory.create() |
2906 | 3036 | {"and": [{"name": "hostnames","op": "eq", "val": "pepe"}]} |
2907 | 3037 | ]} |
2908 | 3038 | res = test_client.get( |
2909 | '/v2/ws/{}/vulns/?q={}'.format(workspace.name, json.dumps(query_filter))) | |
3039 | f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
2910 | 3040 | assert res.status_code == 200 |
2911 | 3041 | assert res.json['count'] == 1 |
2912 | 3042 | assert res.json['vulnerabilities'][0]['id'] == vuln.id |
2913 | 3043 | |
3044 | @pytest.mark.skip_sql_dialect('sqlite') | |
2914 | 3045 | @pytest.mark.usefixtures('ignore_nplusone') |
2915 | @pytest.mark.skip_sql_dialect('sqlite') | |
2916 | def test_search_by_hostname_and_confirmed(self, test_client, session): | |
3046 | def test_search_filter_offset_and_limit_mixed_vulns_type_bug(self, test_client, session): | |
2917 | 3047 | workspace = WorkspaceFactory.create() |
2918 | 3048 | host = HostFactory.create(workspace=workspace) |
2919 | host.hostnames.append(HostnameFactory.create(name='pepe', workspace=workspace)) | |
2920 | vuln = VulnerabilityFactory.create( | |
2921 | host=host, | |
2922 | service=None, | |
2923 | confirmed=True, | |
3049 | vulns = VulnerabilityFactory.create_batch(10, | |
2924 | 3050 | workspace=workspace, |
2925 | ) | |
2926 | session.add(vuln) | |
3051 | severity='high' | |
3052 | ) | |
3053 | session.add_all(vulns) | |
3054 | web_vulns = VulnerabilityWebFactory.create_batch(10, | |
3055 | workspace=workspace, | |
3056 | severity='high' | |
3057 | ) | |
3058 | session.add_all(web_vulns) | |
2927 | 3059 | session.add(host) |
2928 | 3060 | session.commit() |
2929 | ||
2930 | query_filter = {"filters": [ | |
2931 | {"and": [ | |
2932 | {"name": "hostnames", "op": "eq", "val": "pepe"}, | |
2933 | {"name": "confirmed", "op": "==", "val": "true"} | |
2934 | ]} | |
2935 | ]} | |
2936 | res = test_client.get( | |
2937 | '/v2/ws/{}/vulns/?q={}'.format(workspace.name, json.dumps(query_filter))) | |
2938 | assert res.status_code == 200 | |
2939 | assert res.json['count'] == 1 | |
2940 | assert res.json['vulnerabilities'][0]['id'] == vuln.id | |
2941 | ||
3061 | paginated_vulns = set() | |
3062 | expected_vulns = set([vuln.id for vuln in vulns] + [vuln.id for vuln in web_vulns]) | |
3063 | for offset in range(0, 2): | |
3064 | query_filter = { | |
3065 | "filters":[{"name":"severity","op":"eq","val":"high"}], | |
3066 | "limit": 10, | |
3067 | "offset": offset * 10, | |
3068 | } | |
3069 | res = test_client.get( | |
3070 | f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3071 | assert res.status_code == 200 | |
3072 | assert res.json['count'] == 20, query_filter | |
3073 | assert len(res.json['vulnerabilities']) == 10 | |
3074 | for vuln in res.json['vulnerabilities']: | |
3075 | paginated_vulns.add(vuln['id']) | |
3076 | assert expected_vulns == paginated_vulns | |
3077 | ||
3078 | @pytest.mark.skip_sql_dialect('sqlite') | |
2942 | 3079 | @pytest.mark.usefixtures('ignore_nplusone') |
3080 | def test_search_filter_offset_and_limit_page_size_10(self, test_client, session): | |
3081 | workspace = WorkspaceFactory.create() | |
3082 | host = HostFactory.create(workspace=workspace) | |
3083 | vulns = VulnerabilityWebFactory.create_batch(100, | |
3084 | workspace=workspace, | |
3085 | severity='high' | |
3086 | ) | |
3087 | session.add_all(vulns) | |
3088 | session.add(host) | |
3089 | session.commit() | |
3090 | paginated_vulns = set() | |
3091 | expected_vulns = set([vuln.id for vuln in vulns]) | |
3092 | for offset in range(0, 10): | |
3093 | query_filter = { | |
3094 | "filters":[{"name":"severity","op":"eq","val":"high"}], | |
3095 | "limit": 10, | |
3096 | "offset": 10 * offset, | |
3097 | } | |
3098 | res = test_client.get( | |
3099 | f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3100 | assert res.status_code == 200 | |
3101 | assert res.json['count'] == 100 | |
3102 | for vuln in res.json['vulnerabilities']: | |
3103 | paginated_vulns.add(vuln['id']) | |
3104 | assert expected_vulns == paginated_vulns | |
3105 | ||
2943 | 3106 | @pytest.mark.skip_sql_dialect('sqlite') |
3107 | @pytest.mark.usefixtures('ignore_nplusone') | |
3108 | def test_search_filter_offset_and_limit(self, test_client, session): | |
3109 | workspace = WorkspaceFactory.create() | |
3110 | host = HostFactory.create(workspace=workspace) | |
3111 | vulns = VulnerabilityWebFactory.create_batch(10, | |
3112 | workspace=workspace, | |
3113 | severity='high' | |
3114 | ) | |
3115 | session.add_all(vulns) | |
3116 | vulns = VulnerabilityFactory.create_batch(10, | |
3117 | workspace=workspace, | |
3118 | severity='low' | |
3119 | ) | |
3120 | session.add_all(vulns) | |
3121 | med_vulns = VulnerabilityFactory.create_batch(10, | |
3122 | workspace=workspace, | |
3123 | severity='medium' | |
3124 | ) | |
3125 | session.add_all(med_vulns) | |
3126 | session.add(host) | |
3127 | session.commit() | |
3128 | paginated_vulns = set() | |
3129 | expected_vulns = set([vuln.id for vuln in med_vulns]) | |
3130 | for offset in range(0, 10): | |
3131 | query_filter = { | |
3132 | "filters":[{"name":"severity","op":"eq","val":"medium"}], | |
3133 | "limit":"1", | |
3134 | "offset": offset, | |
3135 | } | |
3136 | res = test_client.get( | |
3137 | f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3138 | assert res.status_code == 200 | |
3139 | assert res.json['count'] == 10 | |
3140 | paginated_vulns.add(res.json['vulnerabilities'][0]['id']) | |
3141 | ||
3142 | assert expected_vulns == paginated_vulns | |
3143 | ||
3144 | @pytest.mark.skip_sql_dialect('sqlite') | |
3145 | @pytest.mark.usefixtures('ignore_nplusone') | |
3146 | @pytest.mark.skip(reason="We need a better solution for searching in the model.") | |
2944 | 3147 | def test_search_by_host_os_with_vulnerability_web_bug(self, test_client, session): |
2945 | 3148 | """ |
2946 | 3149 | When searching by the host os an error was raised when a vuln web exists in the ws |
2947 | 3150 | """ |
2948 | 3151 | workspace = WorkspaceFactory.create() |
2949 | 3152 | host = HostFactory.create(workspace=workspace, os='Linux') |
2950 | host.hostnames.append(HostnameFactory.create(name='pepe', workspace=workspace)) | |
2951 | 3153 | service = ServiceFactory.create(host=host, workspace=workspace) |
2952 | vuln = VulnerabilityWebFactory.create( | |
3154 | vuln = VulnerabilityFactory.create( | |
2953 | 3155 | service=service, |
2954 | 3156 | confirmed=True, |
2955 | 3157 | workspace=workspace, |
2970 | 3172 | {"name": "host__os", "op": "has", "val": "Linux"} |
2971 | 3173 | ]} |
2972 | 3174 | res = test_client.get( |
2973 | '/v2/ws/{}/vulns/filter?q={}'.format(workspace.name, json.dumps(query_filter))) | |
3175 | f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
2974 | 3176 | assert res.status_code == 200 |
2975 | 3177 | assert res.json['count'] == 1 |
2976 | 3178 | assert res.json['vulnerabilities'][0]['id'] == vuln.id |
2977 | 3179 | |
2978 | ||
3180 | @pytest.mark.skip_sql_dialect('sqlite') | |
2979 | 3181 | @pytest.mark.usefixtures('ignore_nplusone') |
2980 | @pytest.mark.skip_sql_dialect('sqlite') | |
2981 | 3182 | def test_search_by_date_equals(self, test_client, session): |
2982 | 3183 | """ |
2983 | 3184 | When searching by the host os an error was raised when a vuln web exists in the ws |
3018 | 3219 | {"name": "create_date", "op": "eq", "val": vuln.create_date.strftime("%Y-%m-%d")} |
3019 | 3220 | ]} |
3020 | 3221 | res = test_client.get( |
3021 | '/v2/ws/{}/vulns/filter?q={}'.format(workspace.name, json.dumps(query_filter))) | |
3222 | f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3022 | 3223 | assert res.status_code == 200 |
3023 | 3224 | assert res.json['count'] == 3 |
3024 | 3225 | |
3025 | @pytest.mark.usefixtures('ignore_nplusone') | |
3026 | 3226 | @pytest.mark.skip_sql_dialect('sqlite') |
3027 | 3227 | def test_search_by_date_equals_invalid_date(self, test_client, session): |
3028 | 3228 | """ |
3033 | 3233 | {"name": "create_date", "op": "eq", "val": "30/01/2020"} |
3034 | 3234 | ]} |
3035 | 3235 | res = test_client.get( |
3036 | '/v2/ws/{}/vulns/filter?q={}'.format(workspace.name, json.dumps(query_filter))) | |
3037 | assert res.status_code == 200 | |
3038 | ||
3039 | @pytest.mark.usefixtures('ignore_nplusone') | |
3236 | f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3237 | assert res.status_code == 200 | |
3238 | ||
3040 | 3239 | @pytest.mark.skip_sql_dialect('sqlite') |
3041 | 3240 | def test_search_hypothesis_test_found_case(self, test_client, session, workspace): |
3042 | 3241 | query_filter = {'filters': [{'name': 'host_id', 'op': 'not_in', 'val': '\U0010a1a7\U00093553\U000eb46a\x1e\x10\r\x18%\U0005ddfa0\x05\U000fdeba\x08\x04絮'}]} |
3043 | 3242 | res = test_client.get( |
3044 | '/v2/ws/{}/vulns/filter?q={}'.format(workspace.name, json.dumps(query_filter))) | |
3243 | f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3045 | 3244 | assert res.status_code == 400 |
3046 | 3245 | |
3047 | @pytest.mark.usefixtures('ignore_nplusone') | |
3048 | 3246 | @pytest.mark.skip_sql_dialect('sqlite') |
3049 | 3247 | def test_search_hypothesis_test_found_case_2(self, test_client, session, workspace): |
3050 | 3248 | query_filter = {'filters': [{'name': 'host__os', 'op': 'ilike', 'val': -1915870387}]} |
3051 | 3249 | res = test_client.get( |
3052 | '/v2/ws/{}/vulns/filter?q={}'.format(workspace.name, json.dumps(query_filter))) | |
3250 | f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3053 | 3251 | assert res.status_code == 400 |
3054 | 3252 | |
3055 | @pytest.mark.usefixtures('ignore_nplusone') | |
3056 | 3253 | @pytest.mark.skip_sql_dialect('sqlite') |
3057 | 3254 | @pytest.mark.parametrize('query_filter', [ |
3058 | 3255 | {'filters': [{'name': 'workspace_id', 'op': '==', 'val': ''}]}, |
3061 | 3258 | ]) |
3062 | 3259 | def test_search_hypothesis_test_found_case_3(self, query_filter, test_client, session, workspace): |
3063 | 3260 | res = test_client.get( |
3064 | '/v2/ws/{}/vulns/filter?q={}'.format(workspace.name, json.dumps(query_filter))) | |
3261 | f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3065 | 3262 | assert res.status_code == 400 |
3066 | 3263 | |
3067 | @pytest.mark.usefixtures('ignore_nplusone') | |
3068 | 3264 | @pytest.mark.skip_sql_dialect('sqlite') |
3069 | 3265 | @pytest.mark.parametrize('query_filter', [ |
3070 | 3266 | {'filters': [{'name': 'workspace_id', 'op': 'in', 'val': 56}]}, |
3073 | 3269 | ]) |
3074 | 3270 | def test_search_hypothesis_test_found_case_4(self, query_filter, test_client, session, workspace): |
3075 | 3271 | res = test_client.get( |
3076 | '/v2/ws/{}/vulns/filter?q={}'.format(workspace.name, json.dumps(query_filter))) | |
3272 | f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3077 | 3273 | assert res.status_code == 400 |
3078 | 3274 | |
3079 | @pytest.mark.usefixtures('ignore_nplusone') | |
3080 | 3275 | @pytest.mark.skip_sql_dialect('sqlite') |
3081 | 3276 | @pytest.mark.parametrize('query_filter', [ |
3082 | 3277 | {'filters': [{'name': 'creator', 'op': 'geq', 'val': 27576}, {'name': 'name', 'op': 'eq', 'val': None}]}, |
3085 | 3280 | ]) |
3086 | 3281 | def test_search_hypothesis_test_found_case_5(self, query_filter, test_client, session, workspace): |
3087 | 3282 | res = test_client.get( |
3088 | '/v2/ws/{}/vulns/filter?q={}'.format(workspace.name, json.dumps(query_filter))) | |
3283 | f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3089 | 3284 | assert res.status_code == 400 |
3090 | 3285 | |
3091 | @pytest.mark.usefixtures('ignore_nplusone') | |
3092 | 3286 | @pytest.mark.skip_sql_dialect('sqlite') |
3093 | 3287 | def test_search_hypothesis_test_found_case_6(self, test_client, session, workspace): |
3094 | 3288 | query_filter = {'filters': [{'name': 'resolution', 'op': 'any', 'val': ''}]} |
3095 | 3289 | res = test_client.get( |
3096 | '/v2/ws/{}/vulns/filter?q={}'.format(workspace.name, json.dumps(query_filter))) | |
3097 | assert res.status_code == 200 | |
3098 | ||
3099 | @pytest.mark.usefixtures('ignore_nplusone') | |
3290 | f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3291 | assert res.status_code == 200 | |
3292 | ||
3100 | 3293 | @pytest.mark.skip_sql_dialect('sqlite') |
3101 | 3294 | def test_search_hypothesis_test_found_case_7(self, test_client, session, workspace): |
3102 | 3295 | query_filter = {'filters': [{'name': 'name', 'op': '>', 'val': '\U0004e755\U0007a789\U000e02d1\U000b3d32\x10\U000ad0e2,\x05\x1a'}, {'name': 'creator', 'op': 'eq', 'val': 21883}]} |
3103 | 3296 | res = test_client.get( |
3104 | '/v2/ws/{}/vulns/filter?q={}'.format(workspace.name, json.dumps(query_filter))) | |
3297 | f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3105 | 3298 | assert res.status_code == 400 |
3106 | 3299 | |
3107 | @pytest.mark.usefixtures('ignore_nplusone') | |
3108 | 3300 | @pytest.mark.skip_sql_dialect('sqlite') |
3109 | 3301 | @pytest.mark.parametrize('query_filter', [ |
3110 | 3302 | {'filters': [{'name': 'id', 'op': '>', 'val': 3}]}, |
3112 | 3304 | ]) |
3113 | 3305 | def test_search_hypothesis_test_found_case_7_valid(self, query_filter, test_client, session, workspace): |
3114 | 3306 | res = test_client.get( |
3115 | '/v2/ws/{}/vulns/filter?q={}'.format(workspace.name, json.dumps(query_filter))) | |
3116 | assert res.status_code == 200 | |
3117 | ||
3118 | @pytest.mark.usefixtures('ignore_nplusone') | |
3307 | f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3308 | assert res.status_code == 200 | |
3309 | ||
3119 | 3310 | @pytest.mark.skip_sql_dialect('sqlite') |
3120 | 3311 | def test_search_hypothesis_test_found_case_8(self, test_client, session, workspace): |
3121 | 3312 | query_filter = {'filters': [{'name': 'hostnames', 'op': '==', 'val': ''}]} |
3122 | 3313 | res = test_client.get( |
3123 | '/v2/ws/{}/vulns/filter?q={}'.format(workspace.name, json.dumps(query_filter))) | |
3124 | assert res.status_code == 200 | |
3125 | ||
3126 | @pytest.mark.usefixtures('ignore_nplusone') | |
3314 | f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3315 | assert res.status_code == 200 | |
3316 | ||
3127 | 3317 | @pytest.mark.skip_sql_dialect('sqlite') |
3128 | 3318 | def test_search_hypothesis_test_found_case_9(self, test_client, session, workspace): |
3129 | 3319 | query_filter = {'filters': [{'name': 'issuetracker', 'op': 'not_equal_to', 'val': '0\x00\U00034383$\x13-\U000375fb\U0007add2\x01\x01\U0010c23a'}]} |
3130 | 3320 | |
3131 | 3321 | res = test_client.get( |
3132 | '/v2/ws/{}/vulns/filter?q={}'.format(workspace.name, json.dumps(query_filter))) | |
3322 | f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3133 | 3323 | assert res.status_code == 400 |
3134 | 3324 | |
3135 | @pytest.mark.usefixtures('ignore_nplusone') | |
3136 | 3325 | @pytest.mark.skip_sql_dialect('sqlite') |
3137 | 3326 | def test_search_hypothesis_test_found_case_10(self, test_client, session, workspace): |
3138 | 3327 | query_filter = {'filters': [{'name': 'impact_integrity', 'op': 'neq', 'val': 0}]} |
3139 | 3328 | |
3140 | 3329 | res = test_client.get( |
3141 | '/v2/ws/{}/vulns/filter?q={}'.format(workspace.name, json.dumps(query_filter))) | |
3330 | f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3142 | 3331 | assert res.status_code == 400 |
3143 | 3332 | |
3144 | @pytest.mark.usefixtures('ignore_nplusone') | |
3145 | 3333 | @pytest.mark.skip_sql_dialect('sqlite') |
3146 | 3334 | def test_search_hypothesis_test_found_case_11(self, test_client, session, workspace): |
3147 | 3335 | query_filter = {'filters': [{'name': 'host_id', 'op': 'like', 'val': '0'}]} |
3148 | 3336 | |
3149 | 3337 | res = test_client.get( |
3150 | '/v2/ws/{}/vulns/filter?q={}'.format(workspace.name, json.dumps(query_filter))) | |
3338 | f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3151 | 3339 | assert res.status_code == 400 |
3152 | 3340 | |
3153 | @pytest.mark.usefixtures('ignore_nplusone') | |
3154 | 3341 | @pytest.mark.skip_sql_dialect('sqlite') |
3155 | 3342 | def test_search_hypothesis_test_found_case_12(self, test_client, session, workspace): |
3156 | 3343 | query_filter = {'filters': [{'name': 'custom_fields', 'op': 'like', 'val': ''}]} |
3157 | 3344 | |
3158 | 3345 | res = test_client.get( |
3159 | '/v2/ws/{}/vulns/filter?q={}'.format(workspace.name, json.dumps(query_filter))) | |
3346 | f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3160 | 3347 | assert res.status_code == 400 |
3161 | 3348 | |
3162 | @pytest.mark.usefixtures('ignore_nplusone') | |
3163 | 3349 | @pytest.mark.skip_sql_dialect('sqlite') |
3164 | 3350 | def test_search_hypothesis_test_found_case_13(self, test_client, session, workspace): |
3165 | 3351 | query_filter = {'filters': [{'name': 'impact_accountability', 'op': 'ilike', 'val': '0'}]} |
3166 | 3352 | |
3167 | 3353 | res = test_client.get( |
3168 | '/v2/ws/{}/vulns/filter?q={}'.format(workspace.name, json.dumps(query_filter))) | |
3354 | f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3169 | 3355 | assert res.status_code == 400 |
3170 | 3356 | |
3357 | @pytest.mark.usefixtures('ignore_nplusone') | |
3358 | def test_filter_count(self, test_client, session, workspace): | |
3359 | vulns_web = VulnerabilityWebFactory.create_batch(10, workspace=workspace, severity='high') | |
3360 | vulns = VulnerabilityFactory.create_batch(10, workspace=workspace, severity='high') | |
3361 | another_workspace = WorkspaceFactory.create() | |
3362 | more_vulns = VulnerabilityFactory.create_batch(10, workspace=another_workspace, severity='high') | |
3363 | session.add_all(more_vulns) | |
3364 | session.add_all(vulns_web) | |
3365 | session.add_all(vulns) | |
3366 | session.commit() | |
3367 | query_filter = {'filters': [{'name': 'severity', 'op': 'eq', 'val': 'high'}]} | |
3368 | ||
3369 | res = test_client.get( | |
3370 | f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3371 | assert res.status_code == 200 | |
3372 | assert res.json['count'] == 20 | |
3373 | ||
3374 | @pytest.mark.usefixtures('ignore_nplusone') | |
3375 | def test_filter_group_and_sort(self, test_client, session, workspace): | |
3376 | vulns_web = VulnerabilityWebFactory.create_batch(10, workspace=workspace, severity='high') | |
3377 | vulns = VulnerabilityFactory.create_batch(10, workspace=workspace, severity='high') | |
3378 | another_workspace = WorkspaceFactory.create() | |
3379 | more_vulns = VulnerabilityFactory.create_batch(10, workspace=another_workspace, severity='high') | |
3380 | session.add_all(more_vulns) | |
3381 | session.add_all(vulns_web) | |
3382 | session.add_all(vulns) | |
3383 | session.commit() | |
3384 | query_filter = { | |
3385 | "group_by": | |
3386 | [{"field":"severity"}], | |
3387 | "order_by": | |
3388 | [{"field":"name","direction":"asc"}] | |
3389 | } | |
3390 | ||
3391 | res = test_client.get( | |
3392 | f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3393 | assert res.status_code == 400 | |
3394 | ||
3395 | @pytest.mark.skip_sql_dialect('sqlite') | |
3396 | @pytest.mark.parametrize("sort_order", [ | |
3397 | {"direction":"asc", "expected": ['a', 'A', 'b', 'B']}, | |
3398 | {"direction":"desc", "expected": ['B', 'b', 'A', 'a']} | |
3399 | ]) | |
3400 | def test_filter_order_by_name_directions(self, sort_order, test_client, session, workspace): | |
3401 | vuln_1 = VulnerabilityWebFactory.create(name='a', workspace=workspace, severity='high') | |
3402 | vuln_2 = VulnerabilityWebFactory.create(name='b', workspace=workspace, severity='high') | |
3403 | vuln_3 = VulnerabilityWebFactory.create(name='A', workspace=workspace, severity='high') | |
3404 | vuln_4 = VulnerabilityWebFactory.create(name='B', workspace=workspace, severity='high') | |
3405 | ||
3406 | session.add_all([vuln_1, vuln_2, vuln_3, vuln_4]) | |
3407 | session.commit() | |
3408 | query_filter = { | |
3409 | "order_by": | |
3410 | [{"field":"name","direction": sort_order["direction"]}], | |
3411 | "limit": 10, | |
3412 | "offset": 0 | |
3413 | } | |
3414 | ||
3415 | res = test_client.get( | |
3416 | f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3417 | assert res.status_code == 200 | |
3418 | expected_order = sort_order["expected"] | |
3419 | ||
3420 | assert expected_order == [vuln['value']['name'] for vuln in res.json['vulnerabilities']] | |
3421 | ||
3422 | @pytest.mark.skip_sql_dialect('sqlite') | |
3423 | def test_filter_order_by_severity(self, test_client, session, workspace): | |
3424 | vuln_4 = VulnerabilityWebFactory.create(name='B', workspace=workspace, severity='low') | |
3425 | vuln_1 = VulnerabilityWebFactory.create(name='a', workspace=workspace, severity='critical') | |
3426 | vuln_3 = VulnerabilityWebFactory.create(name='A', workspace=workspace, severity='medium') | |
3427 | vuln_2 = VulnerabilityWebFactory.create(name='b', workspace=workspace, severity='high') | |
3428 | ||
3429 | session.add_all([vuln_1, vuln_2, vuln_3, vuln_4]) | |
3430 | session.commit() | |
3431 | query_filter = { | |
3432 | "order_by": | |
3433 | [{"field":"severity","direction":"asc"}], | |
3434 | "limit": 10, | |
3435 | "offset": 0 | |
3436 | } | |
3437 | ||
3438 | res = test_client.get( | |
3439 | f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3440 | assert res.status_code == 200 | |
3441 | expected_order = ['critical', 'high', 'med', 'low'] | |
3442 | ||
3443 | assert expected_order == [vuln['value']['severity'] for vuln in res.json['vulnerabilities']] | |
3444 | ||
3445 | def test_filter_by_creator_command_id(self, | |
3446 | test_client, | |
3447 | session, | |
3448 | workspace, | |
3449 | command_object_factory, | |
3450 | empty_command_factory): | |
3451 | ||
3452 | command = empty_command_factory.create(workspace=workspace, | |
3453 | tool="metasploit") | |
3454 | session.commit() | |
3455 | vulns_web = VulnerabilityWebFactory.create_batch(10, workspace=workspace, severity='high') | |
3456 | vulns = VulnerabilityFactory.create_batch(100, workspace=workspace, severity='high') | |
3457 | another_workspace = WorkspaceFactory.create() | |
3458 | more_vulns = VulnerabilityFactory.create_batch(10, workspace=another_workspace, severity='high') | |
3459 | session.add_all(more_vulns) | |
3460 | session.add_all(vulns_web) | |
3461 | session.add_all(vulns) | |
3462 | session.commit() | |
3463 | for vuln in vulns: | |
3464 | command_object_factory.create(command=command, | |
3465 | object_type='vulnerability', | |
3466 | object_id=vuln.id, | |
3467 | workspace=workspace) | |
3468 | session.commit() | |
3469 | ||
3470 | query_filter ={ | |
3471 | "filters":[{"and":[ | |
3472 | {"name":"creator_command_id","op":"==","val":command.id}] | |
3473 | }], | |
3474 | "offset":0, | |
3475 | "limit":40 | |
3476 | } | |
3477 | ||
3478 | res = test_client.get( | |
3479 | f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3480 | assert res.status_code == 200 | |
3481 | assert res.json['count'] == 100 | |
3171 | 3482 | |
3172 | 3483 | def test_type_filter(workspace, session, |
3173 | 3484 | vulnerability_factory, |
3341 | 3652 | def send_api_create_request(raw_data): |
3342 | 3653 | |
3343 | 3654 | ws_name = host_with_hostnames.workspace.name |
3344 | res = test_client.post('/v2/ws/{0}/vulns/'.format(ws_name), | |
3655 | res = test_client.post(f'/v2/ws/{ws_name}/vulns/', | |
3345 | 3656 | data=raw_data) |
3346 | 3657 | assert res.status_code in [201, 400, 409] |
3347 | 3658 | |
3349 | 3660 | def send_api_update_request(raw_data): |
3350 | 3661 | |
3351 | 3662 | ws_name = host_with_hostnames.workspace.name |
3352 | res = test_client.put('/v2/ws/{0}/vulns/{1}/'.format(ws_name, raw_data['_id']), | |
3663 | res = test_client.put(f"/v2/ws/{ws_name}/vulns/{raw_data['_id']}/", | |
3353 | 3664 | data=raw_data) |
3354 | 3665 | assert res.status_code in [200, 400, 409, 405] |
3355 | 3666 | |
3362 | 3673 | 'filters': st.lists( |
3363 | 3674 | st.fixed_dictionaries({ |
3364 | 3675 | 'name': st.sampled_from( |
3365 | [col.name for col in VulnerabilityWeb.__table__.columns] + WHITE_LIST | |
3676 | [col.name for col in VulnerabilityWeb.__table__.columns] | |
3366 | 3677 | ), |
3367 | 3678 | 'op': st.sampled_from([ |
3368 | 3679 | 'is_null', 'is_not_null', |
4 | 4 | See the file 'doc/LICENSE' for the license information |
5 | 5 | |
6 | 6 | ''' |
7 | ||
8 | import os | |
7 | from datetime import datetime | |
9 | 8 | from io import BytesIO |
10 | 9 | import pytest |
11 | 10 | |
20 | 19 | from tests.factories import ( |
21 | 20 | VulnerabilityTemplateFactory, |
22 | 21 | ReferenceTemplateFactory, |
23 | CustomFieldsSchemaFactory | |
22 | CustomFieldsSchemaFactory, | |
23 | UserFactory, | |
24 | VulnerabilityFactory | |
24 | 25 | ) |
25 | 26 | |
26 | CURRENT_PATH = os.path.dirname(os.path.abspath(__file__)) | |
27 | ||
27 | TEMPLATES_DATA = [ | |
28 | {'name': 'XML Injection (aka Blind XPath Injection) (Type: Base)', | |
29 | 'description': 'The software does not properly neutralize special elements that are u', | |
30 | 'resolution': 'resolved', | |
31 | 'severity': 'medium', | |
32 | 'create_date': datetime(2020, 5, 1, 11, 00), | |
33 | 'creator': 'testuser' | |
34 | }, | |
35 | {'name': 'xml InjectioN (aka Blind XPath Injection) (Type: Base)', | |
36 | 'description': 'THE SOFtware does not properly neutralize special elements that are', | |
37 | 'resolution': 'not resolved', | |
38 | 'severity': 'high', | |
39 | 'create_date': datetime(2020, 6, 1), | |
40 | 'creator': 'testuser2' | |
41 | } | |
42 | ] | |
28 | 43 | |
29 | 44 | @pytest.mark.usefixtures('logged_user') |
30 | 45 | class TestListVulnerabilityTemplateView(ReadOnlyAPITests): |
78 | 93 | vuln_template = VulnerabilityTemplate.query.get(res.json['_id']) |
79 | 94 | assert vuln_template.references == set() |
80 | 95 | |
96 | @pytest.mark.usefixtures('ignore_nplusone') | |
97 | @pytest.mark.parametrize('filters', [ | |
98 | {'field': 'name', 'op': 'eq', 'count': 0, | |
99 | 'filtered_value': 'xml Injection (aka Blind XPath Injection) (Type: Base)', | |
100 | 'expected_template_name': None, | |
101 | 'templates': TEMPLATES_DATA}, | |
102 | {'field': 'name', 'op': 'eq', 'count': 1, | |
103 | 'filtered_value': 'XML Injection (aka Blind XPath Injection) (Type: Base)', | |
104 | 'expected_template_name':TEMPLATES_DATA[0]['name'], | |
105 | 'templates': TEMPLATES_DATA}, | |
106 | {'field': 'name', 'op': 'like', 'count': 1, | |
107 | 'filtered_value': '% Injection (aka Blind XPath Injection)%', | |
108 | 'expected_template_name': TEMPLATES_DATA[0]['name'], | |
109 | 'templates': TEMPLATES_DATA}, | |
110 | {'field': 'name', 'op': 'ilike', 'count': 2, | |
111 | 'filtered_value': '% Injection (aka Blind XPath Injection)%', | |
112 | 'expected_template_name': None, | |
113 | 'templates': TEMPLATES_DATA}, | |
114 | {'field': 'description', 'op': 'eq', 'count': 1, | |
115 | 'filtered_value': 'The software does not properly neutralize special elements that are u', | |
116 | 'expected_template_name': TEMPLATES_DATA[0]['name'], | |
117 | 'templates': TEMPLATES_DATA}, | |
118 | {'field': 'description', 'op': 'like', 'count': 1, | |
119 | 'filtered_value': '% software does not properly neutralize special elements that are u', | |
120 | 'expected_template_name': TEMPLATES_DATA[0]['name'], | |
121 | 'templates': TEMPLATES_DATA}, | |
122 | {'field': 'description', 'op': 'ilike', 'count': 2, | |
123 | 'filtered_value': '% software does not properly neutralize special elements that are%', | |
124 | 'expected_template_name': None, | |
125 | 'templates': TEMPLATES_DATA}, | |
126 | {'field': 'resolution', 'op': 'eq', 'count': 1, | |
127 | 'filtered_value': 'resolved', | |
128 | 'expected_template_name': TEMPLATES_DATA[0]['name'], | |
129 | 'templates': TEMPLATES_DATA}, | |
130 | {'field': 'resolution', 'op': 'like', 'count': 1, | |
131 | 'filtered_value': 'resolv%', | |
132 | 'expected_template_name': TEMPLATES_DATA[0]['name'], | |
133 | 'templates': TEMPLATES_DATA}, | |
134 | {'field': 'resolution', 'op': 'ilike', 'count': 2, | |
135 | 'filtered_value': '%REsolve%', | |
136 | 'expected_template_name': None, | |
137 | 'templates': TEMPLATES_DATA}, | |
138 | {'field': 'severity', 'op': 'eq', 'count': 1, | |
139 | 'filtered_value': 'medium', | |
140 | 'expected_template_name': TEMPLATES_DATA[0]['name'], | |
141 | 'templates': TEMPLATES_DATA}, | |
142 | ]) | |
143 | def test_filter_vuln_template(self, session, test_client, filters): | |
144 | templates = [] | |
145 | ||
146 | VulnerabilityTemplate.query.delete() | |
147 | session.commit() | |
148 | ||
149 | for template in filters['templates']: | |
150 | user = UserFactory.create(username=template['creator']) | |
151 | session.commit() | |
152 | templates.append(self.factory.create( | |
153 | name=template['name'], | |
154 | description=template['description'], | |
155 | resolution=template['resolution'], | |
156 | severity=template['severity'], | |
157 | create_date=template['create_date'], | |
158 | creator=user | |
159 | )) | |
160 | session.commit() | |
161 | ||
162 | query = f'/v2/vulnerability_template/filter?q={{"filters": [' \ | |
163 | f'{{ "name": "{filters["field"]}",' \ | |
164 | f' "op": "{filters["op"]}", ' \ | |
165 | f' "val": "{filters["filtered_value"]}" }}]}}' | |
166 | ||
167 | res = test_client.get(query) | |
168 | assert res.status_code == 200 | |
169 | assert len(res.json['rows']) == filters['count'] | |
170 | if filters['count'] == 1: | |
171 | assert res.json['rows'][0]['doc']['name'] == templates[0].name | |
172 | ||
173 | @pytest.mark.usefixtures('ignore_nplusone') | |
174 | @pytest.mark.parametrize('filters', [ | |
175 | {'field': 'creator_id', 'op': 'eq', 'count': 1, | |
176 | 'filtered_value': TEMPLATES_DATA[0]['creator'], | |
177 | 'expected_template_name': TEMPLATES_DATA[0]['name'], | |
178 | 'templates': TEMPLATES_DATA} | |
179 | ]) | |
180 | # TODO: fix filter restless to filter by username | |
181 | def test_filter_vuln_template_by_creator(self, session, test_client, filters): | |
182 | templates = [] | |
183 | ||
184 | VulnerabilityTemplate.query.delete() | |
185 | session.commit() | |
186 | ||
187 | for template in filters['templates']: | |
188 | user = UserFactory.create(username=template['creator']) | |
189 | session.commit() | |
190 | templates.append(self.factory.create( | |
191 | name=template['name'], | |
192 | description=template['description'], | |
193 | resolution=template['resolution'], | |
194 | severity=template['severity'], | |
195 | create_date=template['create_date'], | |
196 | creator=user | |
197 | )) | |
198 | session.commit() | |
199 | ||
200 | query = f'/v2/vulnerability_template/filter?q={{"filters": [' \ | |
201 | f'{{ "name": "{filters["field"]}",' \ | |
202 | f' "op": "{filters["op"]}", ' \ | |
203 | f' "val": "{templates[0].creator.id}" }}]}}' | |
204 | ||
205 | res = test_client.get(query) | |
206 | assert res.status_code == 200 | |
207 | assert len(res.json['rows']) == filters['count'] | |
208 | if filters['count'] == 1: | |
209 | assert res.json['rows'][0]['doc']['name'] == templates[0].name | |
210 | ||
211 | ||
212 | @pytest.mark.skip_sql_dialect('sqlite') | |
213 | @pytest.mark.usefixtures('ignore_nplusone') | |
214 | @pytest.mark.parametrize('filters', [ | |
215 | {'field': 'create_date', 'op': 'eq', 'count': 1, | |
216 | 'filtered_value': "2020-05-01", | |
217 | 'expected_template_name': TEMPLATES_DATA[0]['name'], | |
218 | 'templates': TEMPLATES_DATA} | |
219 | ]) | |
220 | def test_filter_vuln_template_by_create_date(self, session, test_client, filters): | |
221 | templates = [] | |
222 | ||
223 | VulnerabilityTemplate.query.delete() | |
224 | session.commit() | |
225 | ||
226 | for template in filters['templates']: | |
227 | user = UserFactory.create(username=template['creator']) | |
228 | session.commit() | |
229 | templates.append(self.factory.create( | |
230 | name=template['name'], | |
231 | description=template['description'], | |
232 | resolution=template['resolution'], | |
233 | severity=template['severity'], | |
234 | create_date=template['create_date'], | |
235 | creator=user | |
236 | )) | |
237 | session.commit() | |
238 | ||
239 | query = f'/v2/vulnerability_template/filter?q={{"filters": [' \ | |
240 | f'{{ "name": "{filters["field"]}",' \ | |
241 | f' "op": "{filters["op"]}", ' \ | |
242 | f' "val": "{filters["filtered_value"]}" }}]}}' | |
243 | ||
244 | res = test_client.get(query) | |
245 | assert res.status_code == 200 | |
246 | assert len(res.json['rows']) == filters['count'] | |
247 | if filters['count'] == 1: | |
248 | assert res.json['rows'][0]['doc']['name'] == templates[0].name | |
249 | ||
81 | 250 | def test_update_vulnerability_template(self, session, test_client): |
82 | 251 | template = self.factory.create() |
83 | 252 | session.commit() |
84 | 253 | raw_data = self._create_post_data_vulnerability_template(references='') |
85 | res = test_client.put('/v2/vulnerability_template/{0}/'.format(template.id), data=raw_data) | |
254 | res = test_client.put(f'/v2/vulnerability_template/{template.id}/', data=raw_data) | |
86 | 255 | assert res.status_code == 200 |
87 | 256 | updated_template = session.query(VulnerabilityTemplate).filter_by(id=template.id).first() |
88 | 257 | assert updated_template.name == raw_data['name'] |
115 | 284 | self.first_object.reference_template_instances.add(ref) |
116 | 285 | session.commit() |
117 | 286 | raw_data = self._create_post_data_vulnerability_template(references='new_ref,another_ref') |
118 | res = test_client.put('/v2/vulnerability_template/{0}/'.format(template.id), data=raw_data) | |
287 | res = test_client.put(f'/v2/vulnerability_template/{template.id}/', data=raw_data) | |
119 | 288 | assert res.status_code == 200 |
120 | 289 | updated_template = session.query(VulnerabilityTemplate).filter_by(id=template.id).first() |
121 | 290 | assert updated_template.name == raw_data['name'] |
138 | 307 | def test_delete_vuln_template(self, session, test_client): |
139 | 308 | template = self.factory.create() |
140 | 309 | vuln_count_previous = session.query(VulnerabilityTemplate).count() |
141 | res = test_client.delete('/v2/vulnerability_template/{0}/'.format(template.id)) | |
310 | res = test_client.delete(f'/v2/vulnerability_template/{template.id}/') | |
142 | 311 | |
143 | 312 | assert res.status_code == 204 |
144 | 313 | assert vuln_count_previous - 1 == session.query(VulnerabilityTemplate).count() |
247 | 416 | |
248 | 417 | def test_add_vuln_template_from_csv(self, session, test_client, csrf_token): |
249 | 418 | expected_created_vuln_template = 1 |
419 | vuln_template_name = "EN-Improper Restriction of Operations within the Bounds of a Memory Buffer (Type: Class)" | |
250 | 420 | file_contents = b"""cwe,name,description,resolution,exploitation,references\n |
251 | 421 | CWE-119,EN-Improper Restriction of Operations within the Bounds of a Memory Buffer (Type: Class),"The software performs operations on a memory buffer, but it can read from or write to a memory location that is outside of the intended boundary of the buffer.\n |
252 | 422 | Certain languages allow direct addressing of memory locations and do not automatically ensure that these locations are valid for the memory buffer that is being referenced. This can cause read or write operations to be performed on memory locations that may be associated with other variables, data structures, or internal program data.\n |
268 | 438 | res = test_client.post('/v2/vulnerability_template/bulk_create/', |
269 | 439 | data=data, headers=headers, use_json_data=False) |
270 | 440 | assert res.status_code == 200 |
271 | assert res.json['vulns_created'] == expected_created_vuln_template | |
441 | assert len(res.json['vulns_created']) == expected_created_vuln_template | |
442 | assert res.json['vulns_created'][0][1] == vuln_template_name | |
272 | 443 | |
273 | 444 | def test_add_unicode_vuln_template_from_csv(self, session, test_client, csrf_token): |
274 | 445 | expected_created_vuln_template = 1 |
446 | vuln_template_name = "ES-Exposición de información a través del listado de directorios" | |
275 | 447 | file_contents = """cwe,name,description,resolution,exploitation,references |
276 | 448 | ,ES-Exposición de información a través del listado de directorios,"Estos directorios no deberian estar publicos, pues exponen información sensible del tipo de tecnología utilizada, código de programación, información sobre rutas de acceso a distintos lugares, particularmente en este caso podemos listar toda la información del servidor sin ningun tipo de restricción |
277 | 449 | ",Siempre evitar que se puedan listar directorios de manera externa y sin permisos,high, |
284 | 456 | res = test_client.post('/v2/vulnerability_template/bulk_create/', |
285 | 457 | data=data, headers=headers, use_json_data=False) |
286 | 458 | assert res.status_code == 200 |
287 | assert res.json['vulns_created'] == expected_created_vuln_template | |
459 | assert len(res.json['vulns_created']) == expected_created_vuln_template | |
460 | assert res.json['vulns_created'][0][1] == vuln_template_name | |
288 | 461 | |
289 | 462 | def test_add_vuln_template_only_required_fields(self, session, test_client, csrf_token): |
290 | 463 | expected_created_vuln_template = 1 |
464 | vuln_template_name = "test" | |
291 | 465 | file_contents = b"""name,exploitation\n |
292 | 466 | "test",high |
293 | 467 | |
300 | 474 | res = test_client.post('/v2/vulnerability_template/bulk_create/', |
301 | 475 | data=data, headers=headers, use_json_data=False) |
302 | 476 | assert res.status_code == 200 |
303 | assert res.json['vulns_created'] == expected_created_vuln_template | |
477 | assert len(res.json['vulns_created']) == expected_created_vuln_template | |
478 | assert res.json['vulns_created'][0][1] == vuln_template_name | |
479 | ||
304 | 480 | |
305 | 481 | def test_add_vuln_template_missing_required_fields(self, session, test_client, csrf_token): |
306 | 482 | expected_created_vuln_template = 1 |
340 | 516 | res = test_client.post('/v2/vulnerability_template/bulk_create/', |
341 | 517 | data=data, headers=headers, use_json_data=False) |
342 | 518 | assert res.status_code == 200 |
343 | assert res.json['vulns_created'] == 1 | |
519 | assert len(res.json['vulns_created']) == 1 | |
344 | 520 | inserted_template = session.query(VulnerabilityTemplate).filter_by(name='test').first() |
345 | 521 | assert inserted_template.resolution == 'resolution' |
346 | 522 | assert inserted_template.severity == 'high' |
347 | 523 | assert inserted_template.data == 'technical details' |
348 | 524 | assert 'cvss' in inserted_template.custom_fields |
349 | 525 | assert inserted_template.custom_fields['cvss'] == '5' |
526 | ||
527 | def test_vuln_template_bulk_create(self, test_client, csrf_token): | |
528 | vuln_1 = VulnerabilityFactory.build_dict() | |
529 | vuln_1['exploitation'] = vuln_1['severity'] | |
530 | vuln_2 = VulnerabilityFactory.build_dict() | |
531 | vuln_2['exploitation'] = vuln_2['severity'] | |
532 | ||
533 | data = { | |
534 | 'csrf_token': csrf_token, | |
535 | 'vulns': [vuln_1, vuln_2] | |
536 | } | |
537 | ||
538 | res = test_client.post('/v2/vulnerability_template/bulk_create/', json=data) | |
539 | assert res.status_code == 200 | |
540 | ||
541 | vulns_created = res.json['vulns_created'] | |
542 | assert len(vulns_created) == 2 | |
543 | assert vulns_created[0][1] == vuln_1['name'] | |
544 | assert vulns_created[1][1] == vuln_2['name'] | |
545 | ||
546 | def test_bulk_create_without_csrf_token(self, test_client): | |
547 | vuln_1 = VulnerabilityFactory.build_dict() | |
548 | vuln_1['exploitation'] = vuln_1['severity'] | |
549 | vuln_2 = VulnerabilityFactory.build_dict() | |
550 | vuln_2['exploitation'] = vuln_2['severity'] | |
551 | ||
552 | data = { | |
553 | 'vulns': [vuln_1, vuln_2] | |
554 | } | |
555 | ||
556 | res = test_client.post('/v2/vulnerability_template/bulk_create/', json=data) | |
557 | assert res.status_code == 403 | |
558 | assert res.json['message'] == 'Invalid CSRF token.' | |
559 | ||
560 | def test_bulk_create_without_data(self, test_client, csrf_token): | |
561 | data = {'csrf_token': csrf_token} | |
562 | res = test_client.post('/v2/vulnerability_template/bulk_create/', json=data) | |
563 | ||
564 | assert res.status_code == 400 | |
565 | assert res.json['message'] == 'Missing data to create vulnerabilities templates.' | |
566 | ||
567 | def test_bulk_create_with_one_conflict(self, test_client, session, csrf_token): | |
568 | vuln_template = VulnerabilityTemplate(name='conflict_vuln', severity='high') | |
569 | session.add(vuln_template) | |
570 | session.commit() | |
571 | ||
572 | vuln_1 = VulnerabilityFactory.build_dict() | |
573 | vuln_1['name'] = 'conflict_vuln' | |
574 | vuln_1['exploitation'] = vuln_1['severity'] | |
575 | vuln_2 = VulnerabilityFactory.build_dict() | |
576 | vuln_2['exploitation'] = vuln_2['severity'] | |
577 | ||
578 | data = { | |
579 | 'csrf_token': csrf_token, | |
580 | 'vulns': [vuln_1, vuln_2] | |
581 | } | |
582 | ||
583 | res = test_client.post('/v2/vulnerability_template/bulk_create/', json=data) | |
584 | assert res.status_code == 200 | |
585 | ||
586 | assert len(res.json['vulns_with_conflict']) == 1 | |
587 | assert res.json['vulns_with_conflict'][0][1] == vuln_1['name'] | |
588 | ||
589 | assert len(res.json['vulns_created']) == 1 | |
590 | assert res.json['vulns_created'][0][1] == vuln_2['name'] | |
591 | ||
592 | def test_bulk_create_with_conflict_in_every_vuln(self, test_client, session, csrf_token): | |
593 | vuln_template_1 = VulnerabilityTemplate(name='conflict_vuln_1', severity='high') | |
594 | session.add(vuln_template_1) | |
595 | vuln_template_2 = VulnerabilityTemplate(name='conflict_vuln_2', severity='high') | |
596 | session.add(vuln_template_2) | |
597 | session.commit() | |
598 | ||
599 | vuln_1 = VulnerabilityFactory.build_dict() | |
600 | vuln_1['name'] = 'conflict_vuln_1' | |
601 | vuln_1['exploitation'] = vuln_1['severity'] | |
602 | vuln_2 = VulnerabilityFactory.build_dict() | |
603 | vuln_2['name'] = 'conflict_vuln_2' | |
604 | vuln_2['exploitation'] = vuln_2['severity'] | |
605 | ||
606 | data = { | |
607 | 'csrf_token': csrf_token, | |
608 | 'vulns': [vuln_1, vuln_2] | |
609 | } | |
610 | ||
611 | res = test_client.post('/v2/vulnerability_template/bulk_create/', json=data) | |
612 | assert res.status_code == 409 | |
613 | ||
614 | assert len(res.json['vulns_with_conflict']) == 2 | |
615 | assert res.json['vulns_with_conflict'][0][1] == vuln_1['name'] | |
616 | assert res.json['vulns_with_conflict'][1][1] == vuln_2['name'] | |
617 | ||
618 | assert len(res.json['vulns_created']) == 0 |
12 | 12 | class TestWebsocketAuthEndpoint: |
13 | 13 | |
14 | 14 | def test_not_logged_in_request_fail(self, test_client, workspace): |
15 | res = test_client.post('/v2/ws/{}/websocket_token/'.format( | |
16 | workspace.name)) | |
15 | res = test_client.post(f'/v2/ws/{workspace.name}/websocket_token/') | |
17 | 16 | assert res.status_code == 401 |
18 | 17 | |
19 | 18 | @pytest.mark.usefixtures('logged_user') |
20 | 19 | def test_get_method_not_allowed(self, test_client, workspace): |
21 | res = test_client.get('/v2/ws/{}/websocket_token/'.format( | |
22 | workspace.name)) | |
20 | res = test_client.get(f'/v2/ws/{workspace.name}/websocket_token/') | |
23 | 21 | assert res.status_code == 405 |
24 | 22 | |
25 | 23 | @pytest.mark.usefixtures('logged_user') |
26 | 24 | def test_succeeds(self, test_client, workspace): |
27 | res = test_client.post('/v2/ws/{}/websocket_token/'.format( | |
28 | workspace.name)) | |
25 | res = test_client.post(f'/v2/ws/{workspace.name}/websocket_token/') | |
29 | 26 | assert res.status_code == 200 |
30 | 27 | |
31 | 28 | # A token for that workspace should be generated, |
19 | 19 | lookup_field = 'name' |
20 | 20 | view_class = WorkspaceView |
21 | 21 | |
22 | @pytest.mark.usefixtures('ignore_nplusone') | |
23 | def test_filter_restless_by_name(self, test_client): | |
24 | res = test_client.get(f'{self.url()}filter?q=' | |
25 | f'{{"filters":[{{"name": "name", "op":"eq", "val": "{self.first_object.name}"}}]}}') | |
26 | assert res.status_code == 200 | |
27 | assert len(res.json) == 1 | |
28 | assert res.json[0]['name'] == self.first_object.name | |
29 | ||
30 | @pytest.mark.usefixtures('ignore_nplusone') | |
31 | def test_filter_restless_by_name_zero_results_found(self, test_client): | |
32 | res = test_client.get(f'{self.url()}filter?q=' | |
33 | f'{{"filters":[{{"name": "name", "op":"eq", "val": "thiswsdoesnotexist"}}]}}') | |
34 | assert res.status_code == 200 | |
35 | assert len(res.json) == 0 | |
36 | ||
37 | def test_filter_restless_by_description(self, test_client): | |
38 | self.first_object.description = "this is a new description" | |
39 | res = test_client.get(f'{self.url()}filter?q=' | |
40 | f'{{"filters":[{{"name": "description", "op":"eq", "val": "{self.first_object.description}"}}]}}') | |
41 | assert res.status_code == 200 | |
42 | assert len(res.json) == 1 | |
43 | assert res.json[0]['description'] == self.first_object.description | |
44 | ||
45 | def test_filter_restless_with_vulns_stats(self, test_client, vulnerability_factory, | |
46 | vulnerability_web_factory, session): | |
47 | ||
48 | vulns = vulnerability_factory.create_batch(8, workspace=self.first_object, | |
49 | confirmed=False, status='open', severity='informational') | |
50 | ||
51 | vulns += vulnerability_factory.create_batch(3, workspace=self.first_object, | |
52 | confirmed=True, status='closed', severity='critical') | |
53 | ||
54 | vulns += vulnerability_web_factory.create_batch(2, workspace=self.first_object, | |
55 | confirmed=True, status='open', severity='low') | |
56 | ||
57 | vulns += vulnerability_web_factory.create_batch(2, workspace=self.first_object, | |
58 | confirmed=True, status='open', severity='high') | |
59 | ||
60 | vulns += vulnerability_web_factory.create_batch(2, workspace=self.first_object, | |
61 | confirmed=True, status='open', severity='unclassified') | |
62 | ||
63 | vulns += vulnerability_web_factory.create_batch(2, workspace=self.first_object, | |
64 | confirmed=True, status='open', severity='medium') | |
65 | ||
66 | session.add_all(vulns) | |
67 | session.commit() | |
68 | ||
69 | self.first_object.description = "this is a new description" | |
70 | res = test_client.get(f'{self.url()}filter?q=' | |
71 | f'{{"filters":[{{"name": "description", "op":"eq", "val": "{self.first_object.description}"}}]}}') | |
72 | assert res.status_code == 200 | |
73 | assert len(res.json) == 1 | |
74 | assert res.json[0]['description'] == self.first_object.description | |
75 | assert res.json[0]['stats']['total_vulns'] == 19 | |
76 | assert res.json[0]['stats']['info_vulns'] == 8 | |
77 | assert res.json[0]['stats']['critical_vulns'] == 3 | |
78 | assert res.json[0]['stats']['low_vulns'] == 2 | |
79 | assert res.json[0]['stats']['high_vulns'] == 2 | |
80 | assert res.json[0]['stats']['medium_vulns'] == 2 | |
81 | assert res.json[0]['stats']['unclassified_vulns'] == 2 | |
82 | ||
22 | 83 | def test_host_count(self, host_factory, test_client, session): |
23 | 84 | host_factory.create(workspace=self.first_object) |
24 | 85 | session.commit() |
37 | 98 | session, |
38 | 99 | querystring): |
39 | 100 | vulns = vulnerability_factory.create_batch(8, workspace=self.first_object, |
40 | confirmed=False, status='open') | |
101 | confirmed=False, status='open', severity='critical') | |
41 | 102 | |
42 | 103 | vulns += vulnerability_factory.create_batch(3, workspace=self.first_object, |
43 | confirmed=True, status='closed') | |
44 | ||
45 | vulns += vulnerability_web_factory.create_batch(2, workspace=self.first_object, | |
46 | confirmed=True, status='open') | |
104 | confirmed=True, status='closed', severity='critical') | |
105 | ||
106 | vulns += vulnerability_web_factory.create_batch(2, workspace=self.first_object, | |
107 | confirmed=True, status='open', severity='informational') | |
47 | 108 | |
48 | 109 | |
49 | 110 | |
54 | 115 | assert res.json['stats']['code_vulns'] == 0 |
55 | 116 | assert res.json['stats']['web_vulns'] == 2 |
56 | 117 | assert res.json['stats']['std_vulns'] == 0 |
118 | assert res.json['stats']['critical_vulns'] == 0 | |
119 | assert res.json['stats']['info_vulns'] == 2 | |
57 | 120 | assert res.json['stats']['total_vulns'] == 2 |
58 | 121 | |
59 | 122 | |
67 | 130 | session, |
68 | 131 | querystring): |
69 | 132 | vulns = vulnerability_factory.create_batch(8, workspace=self.first_object, |
70 | confirmed=False, status='open') | |
133 | confirmed=False, status='open', severity='informational') | |
71 | 134 | |
72 | 135 | vulns += vulnerability_factory.create_batch(3, workspace=self.first_object, |
73 | confirmed=True, status='closed') | |
74 | ||
75 | vulns += vulnerability_web_factory.create_batch(2, workspace=self.first_object, | |
76 | confirmed=True, status='open') | |
136 | confirmed=True, status='closed', severity='informational') | |
137 | ||
138 | vulns += vulnerability_web_factory.create_batch(2, workspace=self.first_object, | |
139 | confirmed=True, status='open', severity='informational') | |
77 | 140 | |
78 | 141 | session.add_all(vulns) |
79 | 142 | session.commit() |
82 | 145 | assert res.json['stats']['code_vulns'] == 0 |
83 | 146 | assert res.json['stats']['web_vulns'] == 0 |
84 | 147 | assert res.json['stats']['std_vulns'] == 3 |
148 | assert res.json['stats']['critical_vulns'] == 0 | |
149 | assert res.json['stats']['info_vulns'] == 3 | |
85 | 150 | assert res.json['stats']['total_vulns'] == 3 |
86 | 151 | |
87 | 152 | @pytest.mark.parametrize('querystring', [ |
121 | 186 | '?confirmed=1', |
122 | 187 | '?confirmed=true' |
123 | 188 | ]) |
124 | ||
125 | 189 | def test_vuln_count_confirmed(self, |
126 | 190 | vulnerability_factory, |
127 | 191 | test_client, |
141 | 205 | '?confirmed=0', |
142 | 206 | '?confirmed=false' |
143 | 207 | ]) |
144 | ||
145 | 208 | def test_vuln_count_confirmed(self, |
146 | 209 | vulnerability_factory, |
147 | 210 | test_client, |
240 | 303 | session.add_all(vulns) |
241 | 304 | session.commit() |
242 | 305 | raw_data = {'name': 'something', 'description': ''} |
243 | res = test_client.put('/v2/ws/{}/'.format(workspace.name), | |
306 | res = test_client.put(f'/v2/ws/{workspace.name}/', | |
244 | 307 | data=raw_data) |
245 | 308 | assert res.status_code == 200 |
246 | 309 | assert res.json['stats']['web_vulns'] == 5 |
269 | 332 | ] |
270 | 333 | raw_data = {'name': 'something', 'description': 'test', |
271 | 334 | 'scope': desired_scope} |
272 | res = test_client.put('/v2/ws/{}/'.format(workspace.name), data=raw_data) | |
335 | res = test_client.put(f'/v2/ws/{workspace.name}/', data=raw_data) | |
273 | 336 | assert res.status_code == 200 |
274 | 337 | assert set(res.json['scope']) == set(desired_scope) |
275 | 338 | assert set(s.name for s in workspace.scope) == set(desired_scope) |
282 | 345 | workspace.active = False |
283 | 346 | session.add(workspace) |
284 | 347 | session.commit() |
285 | res = test_client.put('{url}{id}/activate/' | |
286 | .format(url=self.url(), | |
287 | id=workspace.name)) | |
288 | assert res.status_code == 200 | |
289 | ||
290 | res = test_client.get('{url}{id}/'.format(url=self.url(),id=workspace.name)) | |
348 | res = test_client.put(f'{self.url()}{workspace.name}/activate/') | |
349 | assert res.status_code == 200 | |
350 | ||
351 | res = test_client.get(f'{self.url()}{workspace.name}/') | |
291 | 352 | active = res.json.get('active') |
292 | 353 | assert active == True |
293 | 354 | |
298 | 359 | workspace.active = True |
299 | 360 | session.add(workspace) |
300 | 361 | session.commit() |
301 | res = test_client.put('{url}{id}/deactivate/' | |
302 | .format(url=self.url(), | |
303 | id=workspace.name)) | |
304 | assert res.status_code == 200 | |
305 | ||
306 | res = test_client.get('{url}{id}/'.format(url=self.url(),id=workspace.name)) | |
362 | res = test_client.put(f'{self.url()}{workspace.name}/deactivate/') | |
363 | assert res.status_code == 200 | |
364 | ||
365 | res = test_client.get(f'{self.url()}{workspace.name}/') | |
307 | 366 | active = res.json.get('active') |
308 | 367 | assert active == False |
309 | 368 |
64 | 64 | def mock_envelope_list(self, monkeypatch): |
65 | 65 | assert self.view_class is not None, 'You must define view_class ' \ |
66 | 66 | 'in order to use ListTestsMixin or PaginationTestsMixin' |
67 | def _envelope_list(self, objects, pagination_metadata=None): | |
67 | ||
68 | def _envelope_list(_, objects, pagination_metadata=None): | |
68 | 69 | return {"data": objects} |
69 | 70 | monkeypatch.setattr(self.view_class, '_envelope_list', _envelope_list) |
70 | 71 |
0 | from faraday.server.commands.app_urls import openapi_format | |
1 | ||
2 | ||
3 | def test_openapi_format(session): | |
4 | openapi_format(format="yaml", no_servers=True) |
2 | 2 | import subprocess |
3 | 3 | |
4 | 4 | from configparser import SafeConfigParser, DuplicateSectionError |
5 | from pathlib import Path | |
5 | 6 | |
6 | 7 | |
7 | 8 | def test_manage_migrate(): |
18 | 19 | database=os.environ['POSTGRES_DB'], |
19 | 20 | ) |
20 | 21 | faraday_config = SafeConfigParser() |
21 | config_path = os.path.expanduser('~/.faraday/config/server.ini') | |
22 | config_path = Path('~/.faraday/config/server.ini').expanduser() | |
22 | 23 | faraday_config.read(config_path) |
23 | 24 | try: |
24 | 25 | faraday_config.add_section('database') |
25 | 26 | except DuplicateSectionError: |
26 | 27 | pass |
27 | 28 | faraday_config.set('database', 'connection_string', connection_string) |
28 | with open(config_path, 'w') as faraday_config_file: | |
29 | with config_path.open('w') as faraday_config_file: | |
29 | 30 | faraday_config.write(faraday_config_file) |
30 | 31 | |
31 | 32 | command = ['faraday-manage', 'create-tables'] |
0 | 0 | from alembic.script import ScriptDirectory |
1 | 1 | from alembic.config import Config |
2 | from alembic import command | |
3 | from os.path import ( | |
4 | split, | |
5 | join, | |
6 | ) | |
7 | import pytest | |
8 | import glob | |
9 | 2 | |
10 | 3 | from faraday.server.config import FARADAY_BASE |
11 | 4 | |
12 | class TestMigrations(): | |
13 | 5 | |
6 | class TestMigrations: | |
14 | 7 | |
15 | 8 | def test_migrations_check_revision_hashes(self): |
16 | 9 | config = Config() |
17 | config.set_main_option("script_location", join(FARADAY_BASE,"migrations")) | |
10 | config.set_main_option( | |
11 | "script_location", | |
12 | str(FARADAY_BASE / "migrations") | |
13 | ) | |
18 | 14 | script = ScriptDirectory.from_config(config) |
19 | 15 | |
20 | 16 | alembic_hashes = [] |
22 | 18 | alembic_hashes.append(revision.revision) |
23 | 19 | |
24 | 20 | migrations_hashes = [] |
25 | for migration in glob.glob(join(FARADAY_BASE, 'migrations', 'versions', '*.py')): | |
26 | path, filename = split(migration) | |
21 | for migration in (FARADAY_BASE / 'migrations' / 'versions').glob('*.py'): | |
22 | filename = migration.name | |
27 | 23 | migrations_hashes.append(filename.split('_')[0]) |
28 | 24 | |
29 | 25 | assert set(alembic_hashes) == set(migrations_hashes) |
4 | 4 | |
5 | 5 | ''' |
6 | 6 | |
7 | import os | |
7 | from pathlib import Path | |
8 | 8 | |
9 | 9 | from faraday.server.fields import FaradayUploadedFile |
10 | 10 | |
11 | CURRENT_PATH = os.path.dirname(os.path.abspath(__file__)) | |
11 | TEST_DATA_PATH = Path(__file__).parent / 'data' | |
12 | 12 | |
13 | 13 | |
14 | 14 | def test_html_content_type_is_not_html(): |
15 | with open(os.path.join(CURRENT_PATH, 'data', 'test.html'), "rb")as image_data: | |
15 | with open(TEST_DATA_PATH / 'test.html', "rb")as image_data: | |
16 | 16 | field = FaradayUploadedFile(image_data.read()) |
17 | 17 | assert field['content_type'] == 'application/octet-stream' |
18 | 18 | assert len(field['files']) == 1 |
20 | 20 | |
21 | 21 | def test_image_is_detected_correctly(): |
22 | 22 | |
23 | with open(os.path.join(CURRENT_PATH, 'data', 'faraday.png'), "rb")as image_data: | |
23 | with open(TEST_DATA_PATH / 'faraday.png', "rb")as image_data: | |
24 | 24 | field = FaradayUploadedFile(image_data.read()) |
25 | 25 | assert field['content_type'] == 'image/png' |
26 | 26 | assert 'thumb_id' in field.keys() |
29 | 29 | |
30 | 30 | |
31 | 31 | def test_normal_attach_is_not_detected_as_image(): |
32 | with open(os.path.join(CURRENT_PATH, 'data', 'report_w3af.xml'), "rb")as image_data: | |
32 | with open(TEST_DATA_PATH / 'report_w3af.xml', "rb")as image_data: | |
33 | 33 | field = FaradayUploadedFile(image_data.read()) |
34 | 34 | assert field['content_type'] == 'application/octet-stream' |
35 | 35 | assert len(field['files']) == 1 |
36 | ||
37 | ||
38 | # I'm Py3 |
3 | 3 | import pytest |
4 | 4 | |
5 | 5 | from faraday.searcher.api import Api |
6 | from faraday.searcher.searcher import Searcher, MailNotification | |
6 | from faraday.searcher.searcher import Searcher | |
7 | 7 | from faraday.searcher.sqlapi import SqlApi |
8 | 8 | from faraday.server.models import Service, Host, VulnerabilityWeb |
9 | 9 | from faraday.server.models import Vulnerability, CommandObject |
10 | 10 | from faraday.server.schemas import WorkerRuleSchema |
11 | from faraday.utils.smtp import MailNotification | |
11 | 12 | from tests.factories import ( |
12 | 13 | VulnerabilityTemplateFactory, |
13 | 14 | ServiceFactory, |
256 | 257 | 'id': 'APPLY_TEMPLATE', |
257 | 258 | 'model': 'Vulnerability', |
258 | 259 | 'object': "severity=low", |
259 | 'actions': ["--UPDATE:template={}".format(template_id)] | |
260 | 'actions': [f"--UPDATE:template={template_id}"] | |
260 | 261 | }] |
261 | 262 | |
262 | 263 | searcher.process(rules) |
318 | 319 | session.commit() |
319 | 320 | |
320 | 321 | mail_notification = MailNotification( |
321 | '[email protected]', | |
322 | 'testpass', | |
323 | 'smtp.gmail.com', | |
324 | 587 | |
322 | smtp_host='smtp.gmail.com', | |
323 | smtp_sender='[email protected]', | |
324 | smtp_password='testpass', | |
325 | smtp_port=587 | |
325 | 326 | ) |
326 | 327 | _api = api(workspace, test_client, session) |
327 | 328 | searcher = Searcher(_api, mail_notification=mail_notification) |
876 | 877 | searcher.process(rules_data) |
877 | 878 | vulns_count = session.query(Vulnerability).filter_by(workspace=workspace).count() |
878 | 879 | assert vulns_count == 5 |
879 | ||
880 |
4 | 4 | |
5 | 5 | ''' |
6 | 6 | |
7 | import os | |
8 | import sys | |
9 | 7 | import unittest |
10 | 8 | import pytest |
11 | ||
12 | sys.path.append(os.path.abspath(os.getcwd())) | |
13 | 9 | |
14 | 10 | from faraday.server.models import db |
15 | 11 |
6 | 6 | import re |
7 | 7 | import random |
8 | 8 | import string |
9 | import tempfile | |
10 | from pathlib import Path | |
9 | 11 | from unittest import mock |
10 | 12 | |
11 | 13 | from faraday import __version__ |
15 | 17 | ) |
16 | 18 | |
17 | 19 | |
18 | @mock.patch('os.makedirs') | |
19 | 20 | @mock.patch('shutil.copyfile') |
20 | def test_copy_default_config_to_local_does_not_exist(copyfile, makedirs): | |
21 | random_name = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in | |
22 | range(25)) | |
21 | def test_copy_default_config_to_local_does_not_exist(copyfile): | |
22 | random_name = ''.join( | |
23 | random.choice(string.ascii_uppercase + string.digits) for _ in | |
24 | range(25) | |
25 | ) | |
26 | filename = Path(tempfile.gettempdir()) / random_name | |
23 | 27 | |
24 | new_file = '/tmp/{0}'.format(random_name) | |
25 | with mock.patch('faraday.server.config.LOCAL_CONFIG_FILE', new_file): | |
28 | with mock.patch('faraday.server.config.LOCAL_CONFIG_FILE', filename): | |
26 | 29 | assert copy_default_config_to_local() is None |
27 | assert makedirs.called | |
28 | 30 | assert copyfile.called |
29 | 31 | |
30 | 32 | # the second call will re use the file just created. |
31 | makedirs.reset_mock() | |
32 | 33 | copyfile.reset_mock() |
33 | 34 | assert copy_default_config_to_local() is None |
34 | assert not makedirs.called | |
35 | 35 | assert not copyfile.called |
36 | 36 | |
37 | 37 | VERSION_PATTERN = r""" |
3 | 3 | |
4 | 4 | from faraday.server.utils.filters import FilterSchema |
5 | 5 | from faraday.server.utils.filters import FlaskRestlessSchema |
6 | from faraday.server.models import VulnerabilityWeb | |
6 | 7 | |
7 | 8 | |
8 | 9 | class TestFilters: |
238 | 239 | |
239 | 240 | res = FilterSchema().load(filters) |
240 | 241 | assert res == filters |
242 | ||
243 | def test_case_filter_invalid_attr(self): | |
244 | filters = {'filters': [ | |
245 | {"name": "columna_pepe", "op": "has", "val": "Linux"} | |
246 | ]} | |
247 | with pytest.raises(ValidationError): | |
248 | FilterSchema().load(filters) |
11 | 11 | session.add(agent) |
12 | 12 | session.commit() |
13 | 13 | |
14 | headers = {"Authorization": "Agent {}".format(agent.token)} | |
14 | headers = {"Authorization": f"Agent {agent.token}"} | |
15 | 15 | token = test_client.post('v2/agent_websocket_token/', headers=headers).json['token'] |
16 | 16 | return token |
17 | 17 | |
46 | 46 | |
47 | 47 | def test_join_agent_message_with_valid_token(self, session, proto, workspace, test_client): |
48 | 48 | token = _join_agent(test_client, session) |
49 | message = '{{"action": "JOIN_AGENT", "workspace": "{}", "token": "{}", "executors": [] }}'.format(workspace.name, token) | |
49 | message = f'{{"action": "JOIN_AGENT", "workspace": "{workspace.name}", "token": "{token}", "executors": [] }}' | |
50 | 50 | assert proto.onMessage(message, False) |
51 | 51 | |
52 | 52 | def test_leave_agent_happy_path(self, session, proto, workspace, test_client): |
53 | 53 | token = _join_agent(test_client, session) |
54 | message = '{{"action": "JOIN_AGENT", "workspace": "{}", "token": "{}", "executors": [] }}'.format(workspace.name, token) | |
54 | message = f'{{"action": "JOIN_AGENT", "workspace": "{workspace.name}", "token": "{token}", "executors": [] }}' | |
55 | 55 | assert proto.onMessage(message, False) |
56 | 56 | |
57 | message = '{{"action": "LEAVE_AGENT" }}'.format(token) | |
57 | message = '{"action": "LEAVE_AGENT" }' | |
58 | 58 | assert proto.onMessage(message, False) |
59 | 59 | |
60 | 60 | def test_agent_status(self, session, proto, workspace, test_client): |
61 | 61 | token = _join_agent(test_client, session) |
62 | 62 | agent = Agent.query.one() |
63 | 63 | assert not agent.is_online |
64 | message = '{{"action": "JOIN_AGENT", "workspace": "{}", "token": "{}", "executors": [] }}'.format(workspace.name, token) | |
64 | message = f'{{"action": "JOIN_AGENT", "workspace": "{workspace.name}", "token": "{token}", "executors": [] }}' | |
65 | 65 | assert proto.onMessage(message, False) |
66 | 66 | assert agent.is_online |
67 | 67 | |
68 | message = '{{"action": "LEAVE_AGENT"}}'.format(token) | |
68 | message = '{"action": "LEAVE_AGENT"}' | |
69 | 69 | assert proto.onMessage(message, False) |
70 | 70 | assert not agent.is_online |
71 | 71 |