Codebase list python-faraday / b391855
New upstream version 3.14.0 Sophie Brun 3 years ago
182 changed file(s) with 6338 addition(s) and 2927 deletion(s). Raw diff Collapse all Expand all
+0
-25
.gitlab/ci/.set-tag-gitlab-ci.yml less more
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
-32
.gitlab/ci/.storage-gitlab-ci.yml less more
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
1818 when: never
1919 - when: always
2020
21
22
2321 cache:
2422 paths:
2523 - "$CI_PROJECT_DIR/.cache/pip"
2927 - mkdir -pv $APT_CACHE_DIR
3028
3129 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
3447
3548 stages:
3649 - pre_testing
3750 - testing
3851 - post_testing
39 - build_faraday
52 - pre_build
4053 - build
41 - distro_testing
54 - build_testing
55 - upload
56 - upload_testing
4257 - deploy
43 - post_deploy_testing
4458 - publish
4559
4660 services:
4761 - 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
77 - id: check-json
88 - id: check-yaml
99 - 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 ]
1020 - repo: local
1121 hooks:
1222 - id: sanity-check
2939 language: python
3040 verbose: true
3141 pass_filenames: false
32 args: [--mode=ls]
42 args: [--mode=ls, --local]
3343 stages: [push]
166166 logging-format-truncated,
167167 logging-too-many-args,
168168 logging-too-few-args,
169 logging-fstring-interpolation,
169170 bad-format-character,
170171 truncated-format-string,
171172 mixed-format-string,
275276 redundant-unittest-assert,
276277 deprecated-method,
277278 bad-thread-instantiation,
279 map-builtin-not-iterating,
280 unused-import,
281 comparison-with-callable,
282 unused-variable
283
278284
279285
280286 # Enable the message, report, category or checker with the given id(s). You can
99 * Add creator information when uploading reports or using de bulk create api
1010 * Add feature to disable rules in the searcher
1111 * Add API endpoint to export Faraday data to Metasploit XML format
12 * Change websocket url route from / to /websockets
1213 * Use run date instead of creation date when plugins report specifies it
1314 * Improve knowledge base UX
1415 * 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
70 New features in the latest update
81 =====================================
92
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
1034
1135 3.12 [Sep 3rd, 2020]:
1236 ---
5175 * Add creator information when uploading reports or using de bulk create api
5276 * Add feature to disable rules in the searcher
5377 * Add API endpoint to export Faraday data to Metasploit XML format
78 * Change websocket url route from / to /websockets
5479 * Use run date instead of creation date when plugins report specifies it
5580 * Improve knowledge base UX
5681 * Improve workspace table and status report table UX.
1818
1919
2020 if __name__ == '__main__':
21 version = os.environ.get("IMAGE_TAG", args.version)
21 version = os.environ.get("FARADAY_VERSION", args.version)
2222 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
70 New features in the latest update
81 =====================================
+0
-1
CHANGELOG/websockets_route_change.md less more
0 Change url route from / to /websockets
44 include faraday/server/default.ini
55 include requirements.txt
66 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
70 New features in the latest update
81 =====================================
92
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
1034
1135 3.12 [Sep 3rd, 2020]:
1236 ---
5175 * Add creator information when uploading reports or using de bulk create api
5276 * Add feature to disable rules in the searcher
5377 * Add API endpoint to export Faraday data to Metasploit XML format
78 * Change websocket url route from / to /websockets
5479 * Use run date instead of creation date when plugins report specifies it
5580 * Improve knowledge base UX
5681 * Improve workspace table and status report table UX.
1010
1111 Send us a email with all relevant information about your discovery at:
1212
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)
1414
1515 To encrypt your communications, or to verify signed messages you receive from us you can use the PGP key below.
1616
1717 **Key ID:** 3A48E3A9FC5DE068 **Key type:** RSA **Key size:** 4096
18
18
1919 Fingerprint: `841D C247 7544 1625 5533 7BC8 3A48 E3A9 FC5D E068`
2020
2121 -----BEGIN PGP PUBLIC KEY BLOCK-----
11 # Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/)
22 # See the file 'doc/LICENSE' for the license information
33
4 __version__ = '3.12'
4 __version__ = '3.14.0'
55 __license_version__ = __version__
1919 #The current user may be different from the logged user
2020 current_user = getuser()
2121 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.")
2323 print("After adding the user to the group, please logout and login again.")
2424 sys.exit(1)
2525 except KeyError:
4444 from faraday.server.commands.custom_fields import add_custom_field_main, delete_custom_field_main
4545 from faraday.server.commands import support as support_zip
4646 from faraday.server.commands import change_username
47 from faraday.server.commands import nginx_config
48 from faraday.server.commands import import_vulnerability_template
4749 from faraday.server.models import db, User
4850 from faraday.server.web import app
4951 from faraday_plugins.plugins.manager import PluginsManager
6971
7072
7173 @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)
7488
7589
7690 @click.command(help="Create Faraday DB in Postgresql, also tables and indexes")
199213 is_ldap=False)
200214 db.session.commit()
201215 click.echo(click.style(
202 'User {} created successfully!'.format(username),
216 f'User {username} created successfully!',
203217 fg='green', bold=True))
204218
205219
243257 def migrate(downgrade, revision):
244258 try:
245259 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")
247261 os.chdir(FARADAY_BASE)
248262 if downgrade:
249263 alembic.command.downgrade(config, revision)
250264 else:
251265 alembic.command.upgrade(config, revision)
266 # TODO Return to prev dir
252267 except OperationalError as e:
253268 logger = logging.getLogger(__name__)
254269 logger.error("Migration Error: %s", e)
281296 else:
282297 change_username.change_username(current_username, new_username)
283298
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)
284309
285310 cli.add_command(show_urls)
286311 cli.add_command(initdb)
297322 cli.add_command(list_plugins)
298323 cli.add_command(rename_user)
299324 cli.add_command(openapi_yaml)
325 cli.add_command(generate_nginx_config)
326 cli.add_command(import_vulnerability_templates)
327
300328
301329 if __name__ == '__main__':
302330
00
11 import logging
2 import sys
3 import os
4 sys.path.append(os.getcwd())
52 import faraday.server.config
63 from faraday.server.web import app
74 from faraday.server.models import db
2320 # from myapp import mymodel
2421 target_metadata = db.metadata
2522 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'
2925 fh = logging.FileHandler(LOG_FILE)
3026 fh.setLevel(logging.INFO)
3127 alembic_logger.addHandler(fh)
8177 run_migrations_offline()
8278 else:
8379 run_migrations_online()
84 # I'm Py3
80 # I'm Py3
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')
6868 op.drop_table('notification')
6969 #op.drop_constraint(None, 'notification_user_id_fkey', type_='foreignkey')
7070 #op.drop_constraint(None, 'notification_workspace_id_fkey', type_='foreignkey')
71 # I'm Py3
71 # 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')
66 """
77 from alembic import op
88 import sqlalchemy as sa
9 from faraday.server.models import Command
10
119
1210 # revision identifiers, used by Alembic.
1311 revision = '282ac9b6569f'
1513 branch_labels = None
1614 depends_on = None
1715
16 IMPORT_SOURCE = [
17 'report',
18 'shell'
19 ]
1820
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']))
2123 new_options = sorted(new_types)
2224
23 old_type = sa.Enum(*Command.IMPORT_SOURCE, name='import_source_enum')
25 old_type = sa.Enum(*IMPORT_SOURCE, name='import_source_enum')
2426 new_type = sa.Enum(*new_options, name='import_source_enum')
2527 tmp_type = sa.Enum(*new_options, name='_import_source_enum')
2628
3333
3434
3535 def downgrade():
36 op.drop_table('search_filter')
36 op.drop_table('search_filter')
2121
2222 def downgrade():
2323 op.drop_column('workspace', 'readonly')
24 # I'm Py3
24 # I'm Py3
2323 def downgrade():
2424 conn = op.get_bind()
2525 conn.execute('ALTER TABLE custom_fields_schema DROP CONSTRAINT custom_fields_schema_field_name_key;')
26 # I'm Py3
26 # I'm Py3
2121
2222 def downgrade():
2323 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')
2929 op.drop_column('faraday_user', 'otp_secret')
3030 op.drop_column('faraday_user', 'state_otp')
3131 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')
3333 conn.execute('ALTER TABLE vulnerability DROP COLUMN custom_fields')
3434 conn.execute('ALTER TABLE vulnerability_template DROP COLUMN custom_fields')
3535 conn.execute('DROP TABLE custom_fields_schema')
36 # I'm Py3
36 # I'm Py3
88 import sqlalchemy as sa
99
1010
11 # revision identifiers, used by Alembic.
12 from faraday.server.models import User
13
1411 revision = 'f8a44acd0e41'
1512 down_revision = '526aa91cac98'
1613 branch_labels = None
1714 depends_on = None
1815
16 ROLES = ['admin', 'pentester', 'client']
17
1918
2019 def upgrade():
21 old_type = sa.Enum(*User.ROLES, name='user_roles')
20 old_type = sa.Enum(*ROLES, name='user_roles')
2221
23 new_types = list(set(User.ROLES + ['asset_owner']))
22 new_types = list(set(ROLES + ['asset_owner']))
2423 new_options = sorted(new_types)
2524 new_type = sa.Enum(*new_options, name='user_roles')
2625
3837
3938
4039 def downgrade():
41 new_types = list(set(User.ROLES + ['asset_owner']))
40 new_types = list(set(ROLES + ['asset_owner']))
4241 new_options = sorted(new_types)
4342 new_type = sa.Enum(*new_options, name='user_roles')
4443
4746 tcr = sa.sql.table('faraday_user',
4847 sa.Column('role', new_type, nullable=False))
4948
50 old_type = sa.Enum(*User.ROLES, name='user_roles')
49 old_type = sa.Enum(*ROLES, name='user_roles')
5150
5251 # Convert 'asset_owner' status into 'client'
5352 op.execute(tcr.update().where(tcr.c.role == u'asset_owner')
5959 url = self.base + 'v2/' + path
6060 if self.command_id and 'commands' not in url and not url.endswith('}') and not is_get:
6161 if '?' in url:
62 url += '&command_id={}'.format(self.command_id)
62 url += f'&command_id={self.command_id}'
6363 elif url.endswith('/'):
64 url += '?command_id={}'.format(self.command_id)
64 url += f'?command_id={self.command_id}'
6565 else:
66 url += '/?command_id={}'.format(self.command_id)
66 url += f'/?command_id={self.command_id}'
6767 return url
6868
6969 def _get(self, url, object_name):
70 logger.debug('Getting url {}'.format(url))
70 logger.debug(f'Getting url {url}')
7171 if self.headers:
7272 response = self.requests.get(url, headers=self.headers)
7373 else:
7474 response = self.requests.get(url, cookies=self.cookies)
7575 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}')
7777 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}')
7979 if isinstance(response.json, dict):
8080 return response.json
8181 return json.loads(response.content)
8686 else:
8787 response = self.requests.post(url, json=data, cookies=self.cookies)
8888 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}')
9090 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)}")
9292 if isinstance(response.json, dict):
9393 return response.json
9494 return json.loads(response.content)
9999 else:
100100 response = self.requests.put(url, json=data, cookies=self.cookies)
101101 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}')
103103 if response.status_code != 200:
104 raise ApiError('Unable to update {}'.format(object_name))
104 raise ApiError(f'Unable to update {object_name}')
105105 if isinstance(response.json, dict):
106106 return response.json
107107 return json.loads(response.content)
112112 else:
113113 response = self.requests.delete(url, cookies=self.cookies)
114114 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}')
116116 if response.status_code != 204:
117 raise ApiError('Unable to delete {}'.format(object_name))
117 raise ApiError(f'Unable to delete {object_name}')
118118 return True
119119
120120 def login(self, username, password):
133133 else:
134134 token = self.requests.get(self.base + 'v2/token/').json
135135
136 header = {'Authorization': 'Token {}'.format(token)}
136 header = {'Authorization': f'Token {token}'}
137137
138138 return header, cookies
139139 except ConnectionError as ex:
148148 self.params = params
149149 self.tool_name = tool_name
150150 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')
152152 return res["_id"]
153153
154154 def _command_info(self, duration=None):
170170
171171 def close_command(self, command_id, duration):
172172 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')
174174
175175 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),
177177 'vulnerabilities')['vulnerabilities']]
178178
179179 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),
181181 'services')['services']]
182182
183183 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),
185185 'hosts')['rows']]
186186
187187 def fetch_templates(self):
191191 def filter_vulnerabilities(self, **kwargs):
192192 if len(list(kwargs.keys())) > 1:
193193 params = urlencode(kwargs)
194 url = self._url('ws/{}/vulns/?{}'.format(self.workspace, params))
194 url = self._url(f'ws/{self.workspace}/vulns/?{params}')
195195 else:
196196 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)
198198 return [Structure(**item['value']) for item in
199199 self._get(url, 'vulnerabilities')['vulnerabilities']]
200200
201201 def filter_services(self, **kwargs):
202202 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)
204204 return [Structure(**item['value']) for item in
205205 self._get(url, 'services')['services']]
206206
207207 def filter_hosts(self, **kwargs):
208208 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)
210210 return [Structure(**item['value']) for item in
211211 self._get(url, 'hosts')['rows']]
212212
221221 return filtered_templates
222222
223223 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}/'),
225225 vulnerability.__dict__, 'vulnerability'))
226226
227227 def update_service(self, service):
229229 service.ports = [service.ports]
230230 else:
231231 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}/'),
233233 service.__dict__, 'service'))
234234
235235 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}/'),
237237 host.__dict__, 'hosts'))
238238
239239 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')
241241
242242 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')
244244
245245 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')
247247
248248 @staticmethod
249249 def parse_args(**kwargs):
250250 if len(list(kwargs.keys())) > 0:
251251 key = list(kwargs.keys())[0]
252252 value = kwargs.get(key, '')
253 item = '"name":"{}","op":"eq","val":"{}"'.format(key, value)
253 item = f'"name":"{key}","op":"eq","val":"{value}"'
254254 params = 'filter?q={"filters":[{' + item + '}]}'
255255 return params
256256 return ''
1313 import os
1414 import re
1515 import signal
16 import smtplib
1716 import sys
1817 import time
1918 from datetime import datetime
2019 from difflib import SequenceMatcher
21 from email.mime.multipart import MIMEMultipart
22 from email.mime.text import MIMEText
20 from pathlib import Path
2321
2422 import click
2523 import requests
2725 from faraday.searcher.api import Api
2826 from faraday.searcher.validator import validate_rules
2927 from faraday.server.models import Service, Host
28 from faraday.utils.smtp import MailNotification
3029
3130 logger = logging.getLogger('Faraday searcher')
3231
3332 threshold = 0.75
3433 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
6334
6435 def compare(a, b):
6536 return SequenceMatcher(None, a, b).ratio()
9061
9162
9263 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}'")
9465 match = True
9566 total_ratio = 0
9667 count_fields = 0
12596 percent = (total_ratio * 100.0) / count_fields
12697 else:
12798 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']}:")
129100
130101 if match and total_ratio >= (threshold * count_fields):
131102 logger.info("MATCH")
139110 if is_same_level(model, md):
140111 environment.append(md)
141112 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")
163113
164114
165115 def get_field(obj, field):
170120 return getattr(obj, 'references')
171121 return None
172122 except AttributeError:
173 logger.error("ERROR: Field %s is invalid" % field)
123 logger.error(f"ERROR: Field {field} is invalid")
174124 return None
175125
176126
188138 if key == 'template':
189139 cwe = get_cwe(api, value)
190140 if cwe is None:
191 logger.error("%s: cwe not found" % value)
141 logger.error(f"{value}: cwe not found")
192142 return False
193143
194144 vuln.name = cwe.name
196146 vuln.desc = cwe.description
197147 vuln.resolution = cwe.resolution
198148
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}'")
200150
201151 elif key == 'confirmed':
202152 value = value == 'True'
203153 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}")
205155 elif key == 'owned':
206156 value = value == 'True'
207157 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}")
209159 else:
210160 to_add = True
211161 if key.startswith('-'):
223173 if isinstance(field, str):
224174 setattr(vuln, key, value)
225175 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}")
227177 else:
228178 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}'
230180 if not to_add:
231181 action = 'Removing %s from %s list in vulnerability %s with id %s' % (
232182 value, key, vuln.name, vuln.id)
236186 if field is not None and is_custom_field is True:
237187 vuln.custom_fields[key] = value
238188 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}")
240190
241191 api.update_vulnerability(vuln)
242192
248198 if key == 'owned':
249199 value = value == 'True'
250200 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}")
252202 else:
253203 to_add = True
254204 if key.startswith('-'):
260210 if isinstance(field, str):
261211 setattr(service, key, value)
262212 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}")
264214 else:
265215 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}'
267217 if not to_add:
268218 action = 'Removing %s from %s list in service %s with id %s' % (
269219 value, key, service.name, service.id)
280230 if key == 'owned':
281231 value = value == 'True'
282232 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}")
284234 else:
285235 to_add = True
286236 if key.startswith('-'):
291241 if field is not None:
292242 if isinstance(field, str):
293243 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}")
295245 else:
296246 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}'
298248 if not to_add:
299249 action = 'Removing %s from %s list in host %s with id %s' % (
300250 value, key, host.name, host.id)
458408 def _process_vulnerabilities(self, rules):
459409 logger.debug("--> Start Process vulnerabilities")
460410 for rule_item in rules:
461 logger.debug('Processing rule {}'.format(rule_item['id']))
411 logger.debug(f"Processing rule {rule_item['id']}")
462412 if rule_item['model'].lower() == 'vulnerability':
463413 count_values = 1
464414 values = [None]
483433 def _process_services(self, rules):
484434 logger.debug("--> Start Process services")
485435 for rule_item in rules:
486 logger.debug('Processing rule {}'.format(rule_item['id']))
436 logger.debug(f"Processing rule {rule_item['id']}")
487437 if rule_item['model'].lower() == 'service':
488438 count_values = 1
489439 values = [None]
508458 def _process_hosts(self, rules):
509459 logger.debug("--> Start Process Hosts")
510460 for rule_item in rules:
511 logger.debug('Processing rule {}'.format(rule_item['id']))
461 logger.debug(f"Processing rule {rule_item['id']}")
512462 if rule_item['model'].lower() == 'host':
513463 count_values = 1
514464 values = [None]
551501 if 'parent' in rule:
552502 parent = self._get_parent(rule['parent'])
553503 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']} ")
555505 return self._fetch_objects(rule['model']), None
556506 return self._get_objects_by_parent(parent, rule['model']), parent
557507 return self._fetch_objects(rule['model']), None
625575 return len(self._filter_objects(rule['model'], **kwargs)) > 0
626576
627577 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']}' :")
629579 actions = rule['actions']
630580 _objs_value = None
631581 if 'object' in rule:
665615 elif command == 'DELETE':
666616 if object_type in ['Vulnerabilityweb', 'Vulnerability_web', 'Vulnerability']:
667617 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}':")
669619
670620 elif object_type == 'Service':
671621 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}':")
673623
674624 elif object_type == 'Host':
675625 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}':")
677627 else:
678628 if self.mail_notification:
679629 subject = 'Faraday searcher alert'
680630 body = '%s %s have been modified by rule %s at %s' % (
681631 object_type, obj.name, rule['id'], str(datetime.now()))
682632 self.mail_notification.send_mail(expression, subject, body)
683 logger.info("Sending mail to: '%s'" % expression)
633 logger.info(f"Sending mail to: '{expression}'")
684634 else:
685635 logger.warn("Searcher needs SMTP configuration to send mails")
686636
693643 if key == 'template':
694644 cwe = get_cwe(self.api, value)
695645 if cwe is None:
696 logger.error("%s: cwe not found" % value)
646 logger.error(f"{value}: cwe not found")
697647 return False
698648
699649 vuln.name = cwe.name
701651 vuln.desc = cwe.description
702652 vuln.resolution = cwe.resolution
703653
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}'")
705655
706656 elif key == 'confirmed':
707657 value = value == 'True'
708658 vuln.confirmed = value
709659 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}")
711661 elif key == 'owned':
712662 value = value == 'True'
713663 vuln.owned = value
714664 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}")
716666 else:
717667 to_add = True
718668 if key.startswith('-'):
765715 value = value == 'True'
766716 service.owned = value
767717 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}")
769719 else:
770720 to_add = True
771721 if key.startswith('-'):
781731 key, value, service.name, service.id))
782732 else:
783733 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}'
785735 if not to_add:
786736 action = 'Removing %s from %s list in service %s with id %s' % (
787737 value, key, service.name, service.id)
797747 if key == 'owned':
798748 value = value == 'True'
799749 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}")
801751 else:
802752 to_add = True
803753 if key.startswith('-'):
808758 if field is not None:
809759 if isinstance(field, str):
810760 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}")
812762 else:
813763 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}'
815765 if not to_add:
816766 action = 'Removing %s from %s list in host %s with id %s' % (
817767 value, key, host.ip, host.id)
849799 @click.option('--user', required=True, prompt=True, help='')
850800 @click.option('--password', required=True, prompt=True, hide_input=True, help='')
851801 @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)
854805 @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)
856808 @click.option('--log', required=False, default='debug')
857809 @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):
860813 signal.signal(signal.SIGINT, signal_handler)
861814
862815 loglevel = log
868821 sys.exit(1)
869822
870823 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
876830 )
877831
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)
881835
882836 numeric_level = getattr(logging, loglevel.upper(), None)
883837 if not isinstance(numeric_level, int):
884 raise ValueError('Invalid log level: %s' % loglevel)
838 raise ValueError(f'Invalid log level: {loglevel}')
885839
886840 if not logger.handlers:
887841 logger.propagate = 0
903857
904858 try:
905859 logger.info('Started')
906 logger.info('Searching objects into workspace %s ' % workspace)
860 logger.info(f'Searching objects into workspace {workspace} ')
907861
908862 if not server_address.endswith('/'):
909863 server_address += '/'
2121 if len(workspace) > 0:
2222 self.workspace = workspace[0]
2323 else:
24 raise ApiError("Workspace %s doesn't exist" % workspace_name)
24 raise ApiError(f"Workspace {workspace_name} doesn't exist")
2525
2626 def create_command(self, itime, params, tool_name):
2727 self.itime = itime
4141 if model in vfields and len(fields) != 0:
4242 for field in fields:
4343 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}'")
4646 return False
4747 return True
4848 else:
5555 array = item.split('=')
5656 if allow_old_option:
5757 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'")
5959 return False
6060
6161 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' ")
6363 return False
6464
6565 return True
8888 for index, item in enumerate(values):
8989 if index != 0:
9090 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}")
9292 return False
9393 keys = item.keys()
9494
9595 for var in _vars:
9696 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}")
9898 return False
9999 return True
100100
137137 def validate(key, dictionary, validate_function=None, rule_id=None, mandatory=True, **args):
138138 if rule_id is None:
139139 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")
141141 return False
142142 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")
144144 return False
145145 else:
146146 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}")
148148 return False
149149 if key in dictionary:
150150 if key == 'fields':
151151 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}")
153153 return False
154154 return True
155155
157157 return validate_function(dictionary[key], dictionary, rule_id)
158158
159159 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}")
161161 return False
162162
163163 return True
196196
197197 logger.info('<-- Rules OK')
198198 return True
199 # I'm Py3
199 # I'm Py3
55 """
66 import json
77 import logging
8 from json import JSONDecodeError
9 from typing import Tuple
810
911 import flask
1012 import sqlalchemy
2022 from marshmallow.validate import Length
2123 from marshmallow_sqlalchemy import ModelConverter
2224 from marshmallow_sqlalchemy.schema import ModelSchemaMeta, ModelSchemaOpts
25 from sqlalchemy.sql.elements import BooleanClauseList
2326 from webargs.flaskparser import FlaskParser
2427 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
2631 from faraday.server.schemas import NullToBlankString
2732 from faraday.server.utils.database import (
2833 get_conflict_object,
2934 is_unique_constraint_violation
3035 )
36 from faraday.server.utils.filters import FlaskRestlessSchema
37 from faraday.server.utils.search import search
3138
3239 from faraday.server.config import faraday_server
3340
271278 try:
272279 obj = query.filter(self._get_lookup_field() == object_id).one()
273280 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')
275282 return obj
276283
277284 def _dump(self, obj, route_kwargs, **kwargs):
291298 data. It a ``Marshmallow.Schema`` instance to perform the
292299 deserialization
293300 """
294 return FlaskParser().parse(schema, request, location="json",
301 return FlaskParser(unknown=EXCLUDE).parse(schema, request, location="json",
295302 *args, **kwargs)
296303
297304 @classmethod
340347 response = {'success': False, 'message': f"Exception: {err.original_exception}" if faraday_server.debug else 'Internal Server Error'}
341348 return flask.jsonify(response), 500
342349
350
343351 class GenericWorkspacedView(GenericView):
344352 """Abstract class for a view that depends on the workspace, that is
345353 passed in the URL
359367 try:
360368 ws = Workspace.query.filter_by(name=workspace_name).one()
361369 if not ws.active:
362 flask.abort(403, "Disabled workspace: %s" % workspace_name)
370 flask.abort(403, f"Disabled workspace: {workspace_name}")
363371 except NoResultFound:
364 flask.abort(404, "No such workspace: %s" % workspace_name)
372 flask.abort(404, f"No such workspace: {workspace_name}")
365373 return ws
366374
367375 def _get_base_query(self, workspace_name):
378386 try:
379387 obj = query.filter(self._get_lookup_field() == object_id).one()
380388 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')
382390 return obj
383391
384392 def _set_schema_context(self, context, **kwargs):
452460 return self.order_field
453461
454462 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 """
455474 query = self._filter_query(self._get_eagerloaded_query(**kwargs))
456475 order_field = self._get_order_field(**kwargs)
457476 if order_field is not None:
503522 field_instance = schema.fields[order_field]
504523 except KeyError:
505524 if self.sort_pass_silently:
506 logger.warn("Unknown field: %s" % order_field)
525 logger.warn(f"Unknown field: {order_field}")
507526 return self.order_field
508 raise InvalidUsage("Unknown field: %s" % order_field)
527 raise InvalidUsage(f"Unknown field: {order_field}")
509528
510529 # Translate from the field name in the schema to the database field
511530 # name
516535 model_class = self.sort_model_class or self.model_class
517536 if order_field not in inspect(model_class).attrs:
518537 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}")
520539 return self.order_field
521540 # 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}")
523542
524543 if hasattr(model_class, order_field + '_id'):
525544 # Ugly hack to allow sorting by a parent
530549 self.default_sort_direction)
531550 if sort_dir not in ('asc', 'desc'):
532551 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}")
535553 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}")
538555 try:
539556 if self.order_field is not None:
540557 if not isinstance(self.order_field, tuple):
544561 return getattr(field, sort_dir)()
545562 except NotImplementedError:
546563 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")
550565 return self.order_field
551566 # 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")
555568
556569
557570 class PaginatedMixin:
591604 def _filter_query(self, query):
592605 assert self.filterset_class is not None, 'You must define a filterset'
593606 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)
594786
595787
596788 class ListWorkspacedMixin(ListMixin):
10761268 count_extra_filters = []
10771269
10781270 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 """
10791284 res = {
10801285 'groups': [],
10811286 'total_count': 0
10981303 # using format is not a great practice.
10991304 # the user input is group_by, however it's filtered by column name.
11001305 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}'
11021307
11031308 count = self._filter_query(
11041309 db.session.query(self.model_class)
11461351 count_extra_filters = []
11471352
11481353 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 # """
11491378 res = {
11501379 'groups': defaultdict(dict),
11511380 'total_count': 0
00 # Faraday Penetration Test IDE
11 # Copyright (C) 2019 Infobyte LLC (http://www.infobytesec.com/)
22 # See the file 'doc/LICENSE' for the license information
3 from datetime import datetime
4
35 import flask
46 import logging
57
2022 )
2123 from faraday.server.api.modules.workspaces import WorkspaceSchema
2224 from faraday.server.models import Agent, Executor, AgentExecution, db, \
23 Workspace
25 Workspace, Command
2426 from faraday.server.schemas import PrimaryKeyRelatedField
2527 from faraday.server.config import faraday_server
2628 from faraday.server.events import changes_queue
117119 try:
118120 ws = Workspace.query.filter_by(name=workspace_name).one()
119121 if not ws.active:
120 flask.abort(403, "Disabled workspace: %s" % workspace_name)
122 flask.abort(403, f"Disabled workspace: {workspace_name}")
121123 return ws
122124 except NoResultFound:
123 flask.abort(404, "No such workspace: %s" % workspace_name)
125 flask.abort(404, f"No such workspace: {workspace_name}")
124126
125127 def _perform_create(self, data, **kwargs):
126128 token = data.pop('token')
176178 required=True
177179 )
178180
181 def __init__(self, *args, **kwargs):
182 super(AgentRunSchema, self).__init__(*args, **kwargs)
183 self.unknown = EXCLUDE
184
179185
180186 class AgentWithWorkspacesView(UpdateMixin,
181187 DeleteMixin,
189195 try:
190196 ws = Workspace.query.filter_by(name=workspace_name).one()
191197 if not ws.active:
192 flask.abort(403, "Disabled workspace: %s" % workspace_name)
198 flask.abort(403, f"Disabled workspace: {workspace_name}")
193199 return ws
194200 except NoResultFound:
195 flask.abort(404, "No such workspace: %s" % workspace_name)
201 flask.abort(404, f"No such workspace: {workspace_name}")
196202
197203 def _update_object(self, obj, data):
198204 """Perform changes in the selected object
289295 try:
290296 executor = Executor.query.filter(Executor.name == executor_data['executor'],
291297 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 )
292309
293310 agent_execution = AgentExecution(
294311 running=None,
296313 message='',
297314 executor=executor,
298315 workspace_id=workspace.id,
299 parameters_data=executor_data["args"]
316 parameters_data=executor_data["args"],
317 command=command
300318 )
301319 db.session.add(agent_execution)
302320 db.session.commit()
312330 except NoResultFound as e:
313331 logger.exception(e)
314332 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 })
319337
320338
321339 AgentWithWorkspacesView.register(agent_api)
2323 schema_class = AgentAuthTokenSchema
2424
2525 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 """
2642 return AgentAuthTokenSchema().dump(
2743 {'token': faraday_server.agent_token})
2844
2945 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 """
3058 from faraday.server.app import save_new_agent_creation_token # pylint:disable=import-outside-toplevel
3159 try:
3260 validate_csrf(flask.request.form.get('csrf_token'))
00 import logging
11 from datetime import datetime, timedelta
2 from typing import Type, Optional
3
24 import flask
35 import sqlalchemy
46 from sqlalchemy.orm.exc import NoResultFound
810 Schema,
911 utils,
1012 ValidationError,
13 validates_schema,
1114 )
1215 from marshmallow.validate import Range
1316 from faraday.server.models import (
2023 Service,
2124 Vulnerability,
2225 VulnerabilityWeb,
23 AgentExecution)
26 AgentExecution,
27 Workspace,
28 Metadata
29 )
2430 from faraday.server.utils.database import (
2531 get_conflict_object,
2632 is_unique_constraint_violation,
141147 class Meta(hosts.HostSchema.Meta):
142148 fields = hosts.HostSchema.Meta.fields + ('services', 'vulnerabilities')
143149
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
144156
145157 class BulkCommandSchema(AutoSchema):
146158 """The schema of faraday/server/api/modules/commandsrun.py has a lot
148160
149161 I don't need that here, so I'll write a schema from scratch."""
150162
151 duration = fields.TimeDelta('microseconds', required=True)
163 duration = fields.TimeDelta('microseconds', required=False)
152164
153165 class Meta:
154166 model = Command
159171
160172 @post_load
161173 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
164177 return data
165178
166179
177190 execution_id = fields.Integer(attribute='execution_id')
178191
179192
180 def get_or_create(ws, model_class, data):
193 def get_or_create(ws: Workspace, model_class: Type[Metadata], data: dict):
181194 """Check for conflicts and create a new object
182195
183196 Is is passed the data parsed by the marshmallow schema (it
202215 return (True, obj)
203216
204217
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):
206223 if not data_already_deserialized:
207224 schema = BulkCreateSchema()
208225 data = schema.load(data)
209226 if 'command' in data:
210 command = _create_command(ws, data['command'])
211 else:
212 command = None
227 command = _update_command(command, data['command'])
213228 for host in data['hosts']:
214229 _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()
220239 return command
221240
222241
376395 content:
377396 application/json:
378397 schema: BulkCreateSchema
398 401:
399 $ref: "#/components/responses/UnauthorizedError"
379400 403:
380401 description: Disabled workspace
381402 404:
388409 workspace = self._get_workspace(workspace_name)
389410
390411 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}")
392413
393414 if "execution_id" not in data:
394415 flask.abort(400, "'execution_id' argument expected")
395416
396417 execution_id = data["execution_id"]
397418
398 agent_execution = AgentExecution.query.filter(
419 agent_execution: AgentExecution = AgentExecution.query.filter(
399420 AgentExecution.id == execution_id
400421 ).one_or_none()
401422
413434 )
414435 flask.abort(400, "Trying to write to the incorrect workspace")
415436
416 now = datetime.now()
417
418437 params_data = agent_execution.parameters_data
419438 params = ', '.join([f'{key}={value}' for (key, value) in params_data.items()])
420439
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
421444
422445 data["command"] = {
446 'id': agent_execution.command.id,
423447 'tool': agent.name, # Agent name
424448 'command': agent_execution.executor.name,
425449 'user': '',
426450 'hostname': '',
427451 'params': params,
428452 '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
431454 }
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
432470 else:
433471 workspace = self._get_workspace(workspace_name)
434472 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
439491
440492 post.is_public = True
441493
442494 BulkCreateView.register(bulk_create_api)
443
444
7979
8080 @route('/activity_feed/')
8181 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 """
8293 res = []
8394 query = Command.query.join(Workspace).filter_by(name=workspace_name)
8495 for command in query.all():
97108 })
98109 return res
99110
100 @route('/last/')
111 @route('/last/', methods=['GET'])
101112 def last_command(self, workspace_name):
102113 """
103114 ---
104 get:
105 tags: ["Commands"]
115 tags: ["Command"]
106116 description: Gets the last executed command
107117 responses:
108118 200:
109 description: Last executed command or an empty json
119 description: Last executed command or an empty json
110120 """
111121 command = Command.query.join(Workspace).filter_by(name=workspace_name).order_by(Command.start_date.desc()).first()
112122 command_obj = {}
4848 route_base = 'comment'
4949 model_class = Comment
5050 schema_class = CommentSchema
51
51 order_field = 'create_date'
5252
5353 class UniqueCommentView(GenericWorkspacedView, CommentCreateMixing):
5454 """
7979
8080 CommentView.register(comment_api)
8181 UniqueCommentView.register(comment_api)
82 # I'm Py3
82 # I'm Py3
7979 not_parent_field = 'host_id'
8080 else:
8181 raise ValidationError(
82 'Unknown parent type: {}'.format(parent_type))
82 f'Unknown parent type: {parent_type}')
8383 try:
8484 parent = db.session.query(parent_class).join(Workspace).filter(
8585 Workspace.name == self.context['workspace_name'],
8686 parent_class.id == parent_id).one()
8787 except NoResultFound:
88 raise InvalidUsage('Parent id not found: {}'.format(parent_id))
88 raise InvalidUsage(f'Parent id not found: {parent_id}')
8989 data[parent_field] = parent.id
9090 data[not_parent_field] = None
9191 return data
1616 workspace = Workspace.query.filter_by(name=workspace_name).first()
1717 if not workspace:
1818 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}")
2020
2121 export_format = request.args.get('format', '')
2222 if not export_format:
2727 memory_file = xml_metasploit_format(workspace)
2828 return send_file(
2929 memory_file,
30 attachment_filename="Faraday-%s-data.xml" % workspace_name,
30 attachment_filename=f"Faraday-{workspace_name}-data.xml",
3131 as_attachment=True,
3232 cache_timeout=-1
3333 )
1919 """
2020
2121 logger.debug(
22 "Request parameters: {!r}".format(flask.request.args))
22 f"Request parameters: {flask.request.args!r}")
2323
2424 headers = {
2525 "Content-Type": "application/json",
6363 json_response["exploitdb"].append(obj_module)
6464
6565 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))
6767
6868 return flask.jsonify(json_response)
69 # I'm Py3
69 # I'm Py3
2323 AutoSchema,
2424 FilterAlchemyMixin,
2525 FilterSetMeta,
26 )
26 FilterWorkspacedMixin)
2727 from faraday.server.schemas import (
2828 MetadataSchema,
2929 MutableField,
3737 host_api = Blueprint('host_api', __name__)
3838
3939 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')
4060
4161
4262 class HostSchema(AutoSchema):
6282 fields.List(fields.String))
6383 metadata = SelfNestedField(MetadataSchema())
6484 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)
6989
7090 class Meta:
7191 model = Host
7292 fields = ('id', '_id', '_rev', 'ip', 'description', 'mac',
7393 'credentials', 'default_gateway', 'metadata',
7494 'name', 'os', 'owned', 'owner', 'services', 'vulns',
75 'hostnames', 'type', 'service_summaries', 'versions'
95 'hostnames', 'type', 'service_summaries', 'versions',
96 'important', 'severity_counts'
7697 )
7798
7899 def get_service_summaries(self, obj):
112133 port = ServicePortFilter(fields.Str())
113134
114135
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')
130136
131137 class HostsView(PaginatedMixin,
132138 FilterAlchemyMixin,
133 ReadWriteWorkspacedView):
139 ReadWriteWorkspacedView,
140 FilterWorkspacedMixin):
134141 route_base = 'hosts'
135142 model_class = Host
136143 order_field = desc(Host.vulnerability_critical_generic_count),\
147154 Host.vulnerability_count]
148155 get_joinedloads = [Host.hostnames, Host.services, Host.update_user]
149156
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
150194 @route('/bulk_create/', methods=['POST'])
151195 def bulk_create(self, workspace_name):
152196 """
153197 ---
154198 post:
155 tags: ["Vulns"]
199 tags: ["Bulk", "Host"]
156200 description: Creates hosts in bulk
157201 responses:
158202 201:
164208 description: Bad request
165209 403:
166210 description: Forbidden
211 tags: ["Bulk", "Host"]
212 responses:
213 200:
214 description: Ok
167215 """
168216 try:
169217 validate_csrf(flask.request.form.get('csrf_token'))
186234 hosts_reader = csv.DictReader(stream)
187235 if set(hosts_reader.fieldnames) != FILE_HEADERS:
188236 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})")
190238 hosts_created_count = 0
191239 hosts_with_errors_count = 0
192240 for host_dict in hosts_reader:
208256 return make_response(jsonify(hosts_created=hosts_created_count, hosts_with_errors=hosts_with_errors_count), 200)
209257 except Exception as e:
210258 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})")
212260
213261
214262 @route('/<host_id>/services/')
215263 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 """
216280 services = self._get_object(host_id, workspace_name).services
217281 return ServiceSchema(many=True).dump(services)
218282
221285 """
222286 ---
223287 get:
224 tags: ["Hosts"]
288 tags: ["Host"]
225289 summary: Counts Vulnerabilities per host
226290 responses:
227291 200:
229293 content:
230294 application/json:
231295 schema: HostCountSchema
296 tags: ["Host"]
297 responses:
298 200:
299 description: Ok
232300 """
233301 host_ids = flask.request.args.get('hosts', None)
234302 if host_ids:
249317
250318 @route('/<host_id>/tools_history/')
251319 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 """
252336 workspace = self._get_workspace(workspace_name)
253337 query = db.session.query(Host, Command).filter(Host.id == CommandObject.object_id,
254338 CommandObject.object_type == 'host',
302386
303387 def _envelope_list(self, objects, pagination_metadata=None):
304388 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
306392 hosts.append({
307 'id': host['id'],
308 'key': host['id'],
393 'id': host.get('_id', index),
394 'key': host.get('_id', index),
309395 'value': host
310396 })
311397 return {
314400 or len(hosts)),
315401 }
316402
403 # TODO SCHEMA
317404 @route('bulk_delete/', methods=['DELETE'])
318405 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 """
319423 workspace = self._get_workspace(workspace_name)
320424 json_request = flask.request.get_json()
321425 if not json_request:
8484 Host.id == host_id
8585 ).one()
8686 except NoResultFound:
87 raise ValidationError('Host with id {} not found'.format(host_id))
87 raise ValidationError(f'Host with id {host_id} not found')
8888
8989 return data
9090
1818 data['preferences'] = user.preferences
1919 data['permissions'] = get_user_permissions(user)
2020 return jsonify(data)
21
1717 serializer = TimedJSONWebSignatureSerializer(
1818 app.config['SECRET_KEY'],
1919 salt="api_token",
20 expires_in=faraday_server.api_token_expiration
20 expires_in=int(faraday_server.api_token_expiration)
2121 )
2222 hashed_data = hash_data(g.user.password) if g.user.password else None
2323 return serializer.dumps({'user_id': user_id, "validation_check": hashed_data}).decode('utf-8')
2424
2525
2626 TokenAuthView.register(token_api)
27
28 # I'm Py3
00 # Faraday Penetration Test IDE
11 # Copyright (C) 2018 Infobyte LLC (http://www.infobytesec.com/)
22 # See the file 'doc/LICENSE' for the license information
3 import os
43 import string
54 import random
65 import logging
6 from datetime import datetime
77
88 from faraday.server.config import CONST_FARADAY_HOME_PATH
99 from faraday.server.threads.reports_processor import REPORTS_QUEUE
2121 from wtforms import ValidationError
2222
2323 from faraday.server.utils.web import gzipped
24 from faraday.server.models import Workspace
24 from faraday.server.models import Workspace, Command, db
2525 from faraday.server import config
2626
2727 from faraday_plugins.plugins.manager import PluginsManager, ReportAnalyzer
4444 ws = Workspace.query.filter_by(name=workspace).first()
4545 if not ws or not ws.active:
4646 # Don't raise a 403 to prevent workspace name enumeration
47 abort(404, "Workspace disabled: %s" % workspace)
47 abort(404, f"Workspace disabled: {workspace}")
4848
4949 if 'file' not in request.files:
5050 abort(400)
6060
6161 chars = string.ascii_uppercase + string.digits
6262 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)}'
6464
6565 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:
6869 output.write(report_file.read())
6970 except AttributeError:
7071 logger.warning(
7172 "Upload reports in WEB-UI not configurated, run Faraday client and try again...")
7273 abort(make_response(jsonify(message="Upload reports not configurated: Run faraday client and start Faraday server again"), 500))
7374 else:
74 logger.info("Get plugin for file: %s", file_path)
75 logger.info(f"Get plugin for file: {file_path}")
7576 plugin = report_analyzer.get_plugin(file_path)
7677 if not plugin:
7778 logger.info("Could not get plugin for file")
7879 abort(make_response(jsonify(message="Invalid report file"), 400))
7980 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 )
83110 else:
84111 abort(make_response(jsonify(message="Missing report file"), 400))
85 # I'm Py3
33 from builtins import str, bytes
44 from io import TextIOWrapper
55
6 import json
67 import threading
78 import logging
89 import csv
910
11 from werkzeug.exceptions import Conflict
1012 from flask import Blueprint, request, abort, jsonify, make_response
1113 from flask_classful import route
1214 from filteralchemy import (
2527 FilterSetMeta,
2628 PaginatedMixin,
2729 ReadWriteView,
28 )
30 FilterMixin)
2931
3032 from faraday.server.schemas import (
3133 PrimaryKeyRelatedField,
7072 policyviolations = fields.List(fields.String,
7173 attribute='policy_violations')
7274 creator = PrimaryKeyRelatedField('username', dump_only=True, attribute='creator')
75 creator_id = fields.Integer(dump_only=True, attribute='creator_id')
76
7377 create_at = fields.DateTime(attribute='create_date',
7478 dump_only=True)
7579
8488 fields = ('id', '_id', '_rev', 'cwe', 'description', 'desc',
8589 'exploitation', 'name', 'references', 'refs', 'resolution',
8690 'impact', 'easeofresolution', 'policyviolations', 'data',
87 'external_id', 'creator', 'create_at',
91 'external_id', 'creator', 'create_at', 'creator_id',
8892 'customfields')
8993
9094 def get_references(self, obj):
129133
130134 class VulnerabilityTemplateView(PaginatedMixin,
131135 FilterAlchemyMixin,
132 ReadWriteView):
136 ReadWriteView,
137 FilterMixin):
133138 route_base = 'vulnerability_template'
134139 model_class = VulnerabilityTemplate
135140 schema_class = VulnerabilityTemplateSchema
162167
163168 @route('/bulk_create/', methods=['POST'])
164169 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', '')
165193 try:
166 validate_csrf(request.form.get('csrf_token'))
194 validate_csrf(csrf_token)
167195 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 = []
184236 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 = []
185267 for index, vuln_dict in enumerate(vulns_reader):
186268 vuln_dict['customfields'] = {}
187269 vuln_dict['impact'] = {}
188270 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]
191290
192291 vuln_dict['impact']['accountability'] = vuln_dict.get('accountability', False)
193292 vuln_dict['impact']['availability'] = vuln_dict.get('availability', False)
194293 vuln_dict['impact']['confidentiality'] = vuln_dict.get('confidentiality', False)
195294 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
206299
207300 VulnerabilityTemplateView.register(vulnerability_template_api)
11 # Copyright (C) 2016 Infobyte LLC (http://www.infobytesec.com/)
22 # See the file 'doc/LICENSE' for the license information
33
4 import os
54 import io
65 import json
76 import logging
87 from base64 import b64encode, b64decode
98 from json.decoder import JSONDecodeError
9 from pathlib import Path
1010
1111 import flask
1212 import wtforms
2323 from werkzeug.datastructures import ImmutableMultiDict
2424 from depot.manager import DepotManager
2525
26 from faraday.server.utils.search import search, QueryBuilder, Filter as RestLessFilter
26 from faraday.server.utils.search import (
27 search,
28 )
29
2730 from faraday.server.api.base import (
2831 AutoSchema,
2932 FilterAlchemyMixin,
252255 parent_class.id == parent_id
253256 ).one()
254257 except NoResultFound:
255 raise ValidationError('Parent id not found: {}'.format(parent_id))
258 raise ValidationError(f'Parent id not found: {parent_id}')
256259 data[parent_field] = parent.id
257260 # TODO migration: check what happens when updating the parent from
258261 # service to host or viceverse
520523 File,
521524 object_id=obj.id,
522525 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,
525528 content=faraday_file,
526529 )
527530
618621 }
619622
620623 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 """
622642 res = super(VulnerabilityView, self).count(**kwargs)
623643
624644 def convert_group(group):
638658
639659 @route('/<int:vuln_id>/attachment/', methods=['POST'])
640660 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 """
641674
642675 try:
643676 validate_csrf(request.form.get('csrf_token'))
672705 def filter(self, workspace_name):
673706 """
674707 ---
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
689727 """
690728 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)
692737
693738 def _hostname_filters(self, filters):
694739 res_filters = []
724769
725770 return res_filters, hostname_filters
726771
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):
728773 hosts_os_filter = [host_os_filter for host_os_filter in filters.get('filters', []) if host_os_filter.get('name') == 'host__os']
729774
730775 if hosts_os_filter:
748793
749794 if hosts_os_filter:
750795 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()
761842 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 )
769850 column_names = ['count'] + [field['field'] for field in filters.get('group_by',[])]
770851 rows = [list(zip(column_names, row)) for row in vulns.all()]
771852 vulns_data = []
772853 for row in rows:
773854 vulns_data.append({field[0]:field[1] for field in row})
774855
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)
799857
800858 @route('/<int:vuln_id>/attachment/<attachment_filename>/', methods=['GET'])
801859 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 """
802873 vuln_workspace_check = db.session.query(VulnerabilityGeneric, Workspace.id).join(
803874 Workspace).filter(VulnerabilityGeneric.id == vuln_id,
804875 Workspace.name == workspace_name).first()
832903 """
833904 ---
834905 get:
835 tags: ["Vulns"]
906 tags: ["Vulnerability", "File"]
836907 description: Gets an attachment for a vulnerability
837908 responses:
838909 200:
844915 description: Workspace disabled or no permission
845916 404:
846917 description: Not Found
918 tags: ["Vulnerability", "File"]
919 responses:
920 200:
921 description: Ok
847922 """
848923 workspace = self._get_workspace(workspace_name)
849924 vuln_workspace_check = db.session.query(VulnerabilityGeneric, Workspace.id).join(
864939
865940 @route('/<int:vuln_id>/attachment/<attachment_filename>/', methods=['DELETE'])
866941 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 """
867951 vuln_workspace_check = db.session.query(VulnerabilityGeneric, Workspace.id).join(
868952 Workspace).filter(
869953 VulnerabilityGeneric.id == vuln_id, Workspace.name == workspace_name).first()
885969
886970 @route('export_csv/', methods=['GET'])
887971 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 """
888985 confirmed = bool(request.args.get('confirmed'))
889986 filters = request.args.get('q', '{}')
890987 custom_fields_columns = []
891988 for custom_field in db.session.query(CustomFieldsSchema).order_by(CustomFieldsSchema.field_order):
892989 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)
8941001 memory_file = export_vulns_to_csv(vulns_query, custom_fields_columns)
8951002 return send_file(memory_file,
896 attachment_filename="Faraday-SR-%s.csv" % workspace_name,
1003 attachment_filename=f"Faraday-SR-{workspace_name}.csv",
8971004 as_attachment=True,
8981005 cache_timeout=-1)
8991006
9001007
9011008 @route('bulk_delete/', methods=['DELETE'])
9021009 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 """
9031027 workspace = self._get_workspace(workspace_name)
9041028 json_quest = request.get_json()
9051029 vulnerability_ids = json_quest.get('vulnerability_ids', [])
9281052 """
9291053 ---
9301054 get:
931 tags: ["Vulns"]
1055 tags: ["Vulnerability"]
9321056 params: limit
9331057 description: Gets a list of top users having account its uploaded vulns
9341058 responses:
9351059 200:
9361060 description: List of top users
1061 tags: ["Vulnerability"]
1062 responses:
1063 200:
1064 description: Ok
9371065 """
9381066 limit = flask.request.args.get('limit', 1)
9391067 workspace = self._get_workspace(workspace_name)
1515 from sqlalchemy.orm.exc import NoResultFound
1616
1717
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
1920 from faraday.server.schemas import (
2021 JSTimestampField,
2122 MutableField,
2223 PrimaryKeyRelatedField,
2324 SelfNestedField,
2425 )
25 from faraday.server.api.base import ReadWriteView, AutoSchema
26 from faraday.server.api.base import ReadWriteView, AutoSchema, FilterMixin
2627
2728 logger = logging.getLogger(__name__)
2829
4142 attribute='vulnerability_code_count')
4243 std_vulns = fields.Integer(dump_only=True, allow_none=False,
4344 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')
4457 total_vulns = fields.Integer(dump_only=True, allow_none=False,
4558 attribute='vulnerability_total_count')
4659
5366 class WorkspaceSchema(AutoSchema):
5467
5568 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\\_\\$\\(\\)\\+\\-\\/]*$"))
5772 stats = SelfNestedField(WorkspaceSummarySchema())
5873 duration = SelfNestedField(WorkspaceDurationSchema())
5974 _id = fields.Integer(dump_only=True, attribute='id')
6984 update_date = fields.DateTime(attribute='update_date',
7085 dump_only=True)
7186
87 active_agents_count = fields.Integer(dump_only=True)
88
7289
7390 class Meta:
7491 model = Workspace
7592 fields = ('_id', 'id', 'customer', 'description', 'active',
7693 'duration', 'name', 'public', 'scope', 'stats',
77 'create_date', 'update_date', 'readonly')
94 'create_date', 'update_date', 'readonly',
95 'active_agents_count')
7896
7997 @post_load
8098 def post_load_duration(self, data, **kwargs):
88106 return data
89107
90108
91 class WorkspaceView(ReadWriteView):
109 class WorkspaceView(ReadWriteView, FilterMixin):
92110 route_base = 'ws'
93111 lookup_field = 'name'
94112 lookup_field_type = str
97115 order_field = Workspace.name.asc()
98116
99117 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 """
100134 query = self._get_base_query()
101135 objects = []
102136 for workspace_stat in query:
112146 workspace_stat_dict['scope'].append({'name': scope})
113147 objects.append(workspace_stat_dict)
114148 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)
115180
116181 def _get_querystring_boolean_field(self, field_name, default=None):
117182 try:
174239 Workspace.vulnerability_code_count,
175240 _make_vuln_count_property('vulnerability_code',
176241 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)
182250
183251 try:
184252 obj = query.one()
185253 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')
187255 return obj
188256
189257 def _perform_create(self, data, **kwargs):
214282
215283 @route('/<workspace_id>/activate/', methods=["PUT"])
216284 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 """
217298 changed = self._get_object(workspace_id).activate()
218299 db.session.commit()
219300 return changed
220301
221302 @route('/<workspace_id>/deactivate/', methods=["PUT"])
222303 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 """
223317 changed = self._get_object(workspace_id).deactivate()
224318 db.session.commit()
225319 return changed
226320
227321 @route('/<workspace_id>/change_readonly/', methods=["PUT"])
228322 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 """
229336 self._get_object(workspace_id).change_readonly()
230337 db.session.commit()
231338 return self._get_object(workspace_id).readonly
11 # Copyright (C) 2016 Infobyte LLC (http://www.infobytesec.com/)
22 # See the file 'doc/LICENSE' for the license information
33 import logging
4 import os
54 import string
65 import datetime
76
87 import requests
98 from itsdangerous import TimedJSONWebSignatureSerializer, SignatureExpired, BadSignature
10 from os.path import join
119 from random import SystemRandom
1210
1311 from faraday.server.config import LOCAL_CONFIG_FILE, copy_default_config_to_local
4745
4846
4947 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()
5452 config = ConfigParser()
5553 config.read(faraday.server.config.LOCAL_CONFIG_FILE)
5654 try:
5755 config.add_section('storage')
58 config.set('storage', 'path', default_path)
56 config.set('storage', 'path', str(default_path))
5957 except DuplicateSectionError:
6058 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:
6260 config.write(configfile)
6361
6462 return default_path
185183 if logged_in:
186184 assert user
187185
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':
189188 flask.abort(401)
190189
191190 g.user = None
192191 if logged_in:
193192 g.user = user
194193 if user is None:
195 logger.warn("Unknown user id {}".format(session["_user_id"]))
194 logger.warn(f"Unknown user id {session['_user_id']}")
196195 del flask.session['_user_id']
197196 flask.abort(401) # 403 would be better but breaks the web ui
198197 return
220219
221220
222221 def save_new_secret_key(app):
223 if not os.path.exists(LOCAL_CONFIG_FILE):
222 if not LOCAL_CONFIG_FILE.exists():
224223 copy_default_config_to_local()
225224 config = ConfigParser()
226225 config.read(LOCAL_CONFIG_FILE)
237236
238237
239238 def save_new_agent_creation_token():
240 assert os.path.exists(LOCAL_CONFIG_FILE)
239 assert LOCAL_CONFIG_FILE.exists()
241240 config = ConfigParser()
242241 config.read(LOCAL_CONFIG_FILE)
243242 rng = SystemRandom()
270269 KVSessionExtension(app=app).cleanup_sessions(app)
271270
272271 def create_app(db_connection_string=None, testing=None):
273 app = Flask(__name__)
272 app = Flask(__name__, static_folder=None)
274273
275274 try:
276275 secret_key = faraday.server.config.faraday_server.secret_key
340339 if not DepotManager.get('default'):
341340 if testing:
342341 DepotManager.configure('default', {
343 'depot.storage_path': '/tmp'
342 'depot.storage_path': '/tmp' # nosec
344343 })
345344 else:
346345 DepotManager.configure('default', {
432431 self.email.errors.append(get_message('DISABLED_ACCOUNT')[0])
433432 return False
434433 return True
435
55 """
66 from apispec import APISpec
77 from apispec.ext.marshmallow import MarshmallowPlugin
8 from apispec_webframeworks.flask import FlaskPlugin
89 from faraday.server.web import app
910 from faraday import __version__ as f_version
1011 import json
1213 from faraday.utils.faraday_openapi_plugin import FaradayAPIPlugin
1314
1415
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'}]
1628
1729 spec = APISpec(
1830 title="Faraday API",
1931 version="2",
2032 openapi_version="3.0.2",
21 plugins=[FaradayAPIPlugin(), MarshmallowPlugin()],
22 info={'description': 'The Faraday server API'},
33 plugins=[FaradayAPIPlugin(), FlaskPlugin(), MarshmallowPlugin()],
34 **extra_specs
2335 )
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)
2448
2549 with app.test_request_context():
2650 for endpoint in app.view_functions:
2222 print('This wizard will guide you to DELETE custom field to the vulneraiblity model.')
2323 print('All available custom fields are:')
2424 for custom_field in db.session.query(CustomFieldsSchema):
25 print('* {0}'.format(custom_field.field_name))
25 print(f'* {custom_field.field_name}')
2626 print('End of custom fields')
2727 field_name = click.prompt('Field name')
2828 custom_field = db.session.query(CustomFieldsSchema).filter_by(field_name=field_name).first()
5353 print('Custom field current order')
5454 for custom_field in custom_fields:
5555 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}')
5757 field_order = click.prompt('Field order index')
5858 invalid_field_order = False
5959 try:
8888 custom_field_data.field_display_name = field_display_name
8989 custom_field_data.field_type = field_type
9090 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)
4747 config.get('database', 'connection_string')
4848 reconfigure = None
4949 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) ')
5151 if reconfigure.lower() == 'no':
5252 return False
5353 elif reconfigure.lower() == 'yes':
7272 config.read(LOCAL_CONFIG_FILE)
7373 if not self._check_current_config(config):
7474 return
75 faraday_path_conf = os.path.expanduser(CONST_FARADAY_HOME_PATH)
75 faraday_path_conf = CONST_FARADAY_HOME_PATH
7676 # we use psql_log_filename for historical saving. we will ask faraday users this file.
7777 # 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'
7979 current_psql_output = TemporaryFile()
8080 with open(psql_log_filename, 'ab+') as psql_log_file:
8181 hostname = 'localhost'
122122
123123 statement = text("""
124124 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,
127127 current_login_ip, role, state_otp
128128 ) VALUES (
129129 'faraday', 'Administrator', :password,
164164 current_psql_output_file.seek(0)
165165 psql_output = current_psql_output_file.read().decode('utf-8')
166166 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}?')
168168 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.')
170170 elif process_status > 0:
171171 current_psql_output_file.seek(0)
172172 print('ERROR: ' + psql_output)
188188 username = os.environ.get("FARADAY_DATABASE_USER", 'faraday_postgresql')
189189 postgres_command = ['sudo', '-u', 'postgres', 'psql']
190190 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}')
192192 postgres_command = ['psql', 'postgres']
193193 password = self.generate_random_pw(25)
194194 command = postgres_command + [ '-c', 'CREATE ROLE {0} WITH LOGIN PASSWORD \'{1}\';'.format(username, password)]
198198 output = psql_log_file.read()
199199 if isinstance(output, bytes):
200200 output = output.decode('utf-8')
201 already_exists_error = 'role "{0}" already exists'.format(username)
201 already_exists_error = f'role "{username}" already exists'
202202 return_code = p.returncode
203203 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 ")
205205
206206 try:
207207 if not getattr(faraday.server.config, 'database', None):
242242 if sys.platform == 'darwin':
243243 postgres_command = []
244244
245 print('Creating database {0}'.format(database_name))
245 print(f'Creating database {database_name}')
246246 command = postgres_command + ['createdb', '-E', 'utf8', '-O', username, database_name]
247247 p = Popen(command, stderr=psql_log_file, stdout=psql_log_file, cwd='/tmp') # nosec
248248 p.wait()
249249 return_code = p.returncode
250250 psql_log_file.seek(0)
251251 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'
253253 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.')
255255 return_code = 0
256256 return database_name, return_code
257257
259259 """
260260 This step saves database configuration to server.ini
261261 """
262 print('Saving database credentials file in {0}'.format(LOCAL_CONFIG_FILE))
262 print(f'Saving database credentials file in {LOCAL_CONFIG_FILE}')
263263
264264 conn_string = 'postgresql+psycopg2://{username}:{password}@{server}/{database_name}'.format(
265265 username=username,
292292 db.create_all()
293293 except OperationalError as ex:
294294 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.')
296296 sys.exit(1)
297297 elif 'password authentication failed' in str(ex):
298298 print('ERROR: ')
306306 except ImportError as ex:
307307 if 'psycopg2' in str(ex):
308308 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')
310310 sys.exit(1)
311311 else:
312312 raise
313313 else:
314 alembic_cfg = Config(os.path.join(FARADAY_BASE, 'alembic.ini'))
314 alembic_cfg = Config(FARADAY_BASE / 'alembic.ini')
315315 os.chdir(FARADAY_BASE)
316316 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)
44 See the file 'doc/LICENSE' for the license information
55
66 """
7 import sys
8 import os
9 sys.path.append(os.getcwd())
10
117 import click
128
139 from faraday.server.models import db
2218 for table in ('vulnerability', 'vulnerability_template', 'comment',
2319 'faraday_user'):
2420 try:
25 db.engine.execute('DROP TABLE {} CASCADE'.format(table))
21 db.engine.execute(f'DROP TABLE {table} CASCADE')
2622 except Exception as ex:
2723 print(ex)
2824 db.drop_all()
4743
4844 if __name__ == '__main__':
4945 main()
50 # I'm Py3
33 See the file 'doc/LICENSE' for the license information
44
55 """
6 import os
76 import socket
87
98 import sqlalchemy
9695
9796 def check_storage_permission():
9897
99 path = os.path.join(CONST_FARADAY_HOME_PATH, 'storage', 'test')
98 path = CONST_FARADAY_HOME_PATH / 'storage' / 'test'
10099
101100 try:
102 os.mkdir(path)
103 os.rmdir(path)
101 path.mkdir()
102 path.rmdir()
104103 return True
105104 except OSError:
106105 return None
107106
108107
109108 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__}")
113111
114112 data_keys = ['bind_address', 'port', 'websocket_port', 'debug']
115113 for key in data_keys:
116114 print('{blue} {KEY}: {white}{VALUE}'.
117115 format(KEY=key, VALUE=getattr(faraday.server.config.faraday_server, key), white=Fore.WHITE, blue=Fore.BLUE))
118116
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')
124121 data_keys = ['show_vulns_by_price']
125122 for key in data_keys:
126123 print('{blue} {KEY}: {white}{VALUE}'.
127124 format(KEY=key, VALUE=getattr(faraday.server.config.dashboard, key), white=Fore.WHITE, blue=Fore.BLUE))
128125
129 print('\n{white}Showing storage configuration'.format(white=Fore.WHITE))
126 print(f'\n{Fore.WHITE}Showing storage configuration')
130127 data_keys = ['path']
131128 for key in data_keys:
132129 print('{blue} {KEY}: {white}{VALUE}'.
153150 print('[{red}-{white}] PostgreSQL is running, but needs to be 9.4 or newer, please update PostgreSQL'.\
154151 format(red=Fore.RED, white=Fore.WHITE))
155152 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')
158154 return exit_code
159155
160156
164160
165161 lock_status = check_locks_postgresql()
166162 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.')
169164 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. ')
172166 elif lock_status == None:
173167 pass
174168
175169 encoding = check_postgresql_encoding()
176170 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}')
179172 elif encoding == None:
180173 pass
181174
200193 check_postgres()
201194
202195 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')
208199
209200 if check_open_ports():
210201 print("[{green}+{white}] Port {PORT} in {ad} is open"\
217208 def full_status_check():
218209 print_config_info()
219210
220 print('\n{white}Checking if postgreSQL is running...'.format(white=Fore.WHITE))
211 print(f'\n{Fore.WHITE}Checking if postgreSQL is running...')
221212 print_postgresql_status()
222213 print_postgresql_other_status()
223214
224 print('\n{white}Checking if Faraday is running...'.format(white=Fore.WHITE))
215 print(f'\n{Fore.WHITE}Checking if Faraday is running...')
225216 print_faraday_status()
226217
227218 print('\n{white}Checking Faraday config...{white}'.format(white=Fore.WHITE))
0 import os
10 import sys
21 import shutil
32 import tempfile
3 from pathlib import Path
4
45 from tqdm import tqdm
56 from colorama import init
67 from colorama import Fore, Style
1314
1415 init()
1516
17
1618 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())
2021
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
2428 original_stdout = sys.stdout
2529
26 sys.stdout = open(path + '/status_check.txt','wt')
30 sys.stdout = (path / 'status_check.txt').open('wt')
2731 status_check.full_status_check()
28
32
2933 sys.stdout.close()
3034 sys.stdout = original_stdout
3135
3236
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
3843
3944
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))
4349
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
4653 shutil.rmtree(path)
4754
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
5160
5261 def all_for_support():
5362 with tqdm(total=6) as pbar:
6574 pbar.update(1)
6675
6776 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 }
55 import shutil
66 import errno
77 from configparser import ConfigParser
8 import logging
89
910 from logging import (
1011 DEBUG,
1112 INFO,
13
1214 )
15 from pathlib import Path
16
1317 from faraday import __license_version__ as license_version
1418
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'
1622
1723 LOGGING_LEVEL = INFO
1824
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'
3538
3639 CONFIG_FILES = [DEFAULT_CONFIG_FILE, LOCAL_CONFIG_FILE]
3740 CONST_LICENSES_DB = 'faraday_licenses'
3841 CONST_VULN_MODEL_DB = 'cwe'
3942
40 if not os.path.exists(LOCAL_REPORTS_FOLDER):
43 logger = logging.getLogger(__name__)
44
45 if not LOCAL_REPORTS_FOLDER.exists():
4146 try:
42 os.makedirs(LOCAL_REPORTS_FOLDER)
47 LOCAL_REPORTS_FOLDER.mkdir(parents=True)
4348 except OSError as e:
4449 if e.errno != errno.EEXIST:
4550 raise
4651
4752
4853 def copy_default_config_to_local():
49 if os.path.exists(LOCAL_CONFIG_FILE):
54 if LOCAL_CONFIG_FILE.exists():
5055 return
5156
5257 # Create directory if it doesn't exist
5358 try:
54 os.makedirs(os.path.dirname(LOCAL_CONFIG_FILE))
59 LOCAL_CONFIG_FILE.parent.mkdir(parents=True)
5560 except OSError as e:
5661 if e.errno != errno.EEXIST:
5762 raise
5964 # Copy default config file into faraday local config
6065 shutil.copyfile(DEFAULT_CONFIG_FILE, LOCAL_CONFIG_FILE)
6166
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}")
6468
6569
6670 def parse_and_bind_configuration():
110114 section = storage
111115 elif section_name == 'logger':
112116 section = logger_config
117 elif section_name == 'smtp':
118 section = smtp
113119 else:
114120 return
115121 section.parse(__parser)
132138 self.secret_key = None
133139 self.websocket_port = None
134140 self.session_timeout = 12
135 self.api_token_expiration = 2592000
141 self.api_token_expiration = 43200 # Default as 12 hs
136142 self.agent_token = None
137143 self.debug = False
138144 self.custom_plugins_folder = None
166172 self.certificate = None
167173 self.enabled = False
168174
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
169186
170187 class StorageConfigObject(ConfigSection):
171188 def __init__(self):
184201 websocket_ssl = WebsocketSSLConfigObject()
185202 storage = StorageConfigObject()
186203 logger_config = LoggerConfig()
204 smtp = SmtpConfigObject()
187205
188206 parse_and_bind_configuration()
189207
22 bind_address=localhost
33 websocket_port=9000
44 debug=false
5 session_timeout=12
6 api_token_expiration=43200
7 ;custom_plugins_folder=/path/to/custom/plugins
58
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
1218
1319 [dashboard]
1420 show_vulns_by_price = false
1521
1622 [logger]
17 use_rfc5424_formatter = false
23 use_rfc5424_formatter = false
4949 content = content.encode('utf-8')
5050 image_format = imghdr.what(None, h=content[:32])
5151 if image_format:
52 content_type = 'image/{0}'.format(image_format)
52 content_type = f'image/{image_format}'
5353 self.generate_thumbnail(content)
5454 return super(FaradayUploadedFile, self).process_content(
5555 content, filename, content_type)
8282 output.seek(0)
8383
8484 thumb_path, thumb_id = self.store_content(output,
85 'thumb.%s' % self.thumbnail_format.lower())
85 f'thumb.{self.thumbnail_format.lower()}')
8686 self['thumb_id'] = thumb_id
8787 self['thumb_path'] = thumb_path
8888
124124 if value is not None:
125125 value = json.loads(value)
126126 return value
127 # I'm Py3
127 # I'm Py3
00 # Faraday Penetration Test IDE
11 # Copyright (C) 2016 Infobyte LLC (http://www.infobytesec.com/)
22 # See the file 'doc/LICENSE' for the license information
3 import logging
34 import operator
45 import string
56 from datetime import datetime
4344
4445 from depot.fields.sqlalchemy import UploadedFileField
4546
46 from faraday.server.fields import FaradayUploadedFile, JSONType
47 from faraday.server.fields import JSONType
4748 from flask_security import (
4849 UserMixin,
4950 )
51
52 from faraday.server.fields import FaradayUploadedFile
5053 from faraday.server.utils.database import (
5154 BooleanToIntColumn,
5255 get_object_type_for,
5356 is_unique_constraint_violation)
57
58 logger = logging.getLogger(__name__)
5459
5560 NonBlankColumn = partial(Column, nullable=False,
5661 info={'allow_blank': False})
118123
119124 db = SQLAlchemy()
120125
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
122146
123147
124148 def _make_generic_count_property(parent_table, children_table, where=None):
125149 """Make a deferred by default column property that counts the
126150 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'
130154 query = (select([func.count(text(children_id_field))]).
131155 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}')))
134157 if where is not None:
135158 query = query.where(where)
136159 return column_property(query, deferred=True)
171194 # Don't do queries using this style!
172195 # This can cause SQL injection vulnerabilities
173196 # 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_}'"))
175198 if confirmed:
176199 if db.session.bind.dialect.name == 'sqlite':
177200 # SQLite has no "true" expression, we have to use the integer 1
199222 return query
200223
201224
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"):
203298 assert severity in ['critical', 'high', 'medium', 'low', 'informational', 'unclassified']
204299
205300 vuln_count = (
273368
274369 __table_args__ = (
275370 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"
276787 )
277788
278789 @property
337848 UniqueConstraint(ip, workspace_id, name='uix_host_ip_workspace'),
338849 )
339850
340 vulnerability_info_count = query_expression()
341 vulnerability_med_count = query_expression()
851 vulnerability_informational_count = query_expression()
852 vulnerability_medium_count = query_expression()
342853 vulnerability_high_count = query_expression()
343854 vulnerability_critical_count = query_expression()
344855 vulnerability_low_count = query_expression()
351862 vulnerability_low_generic_count = _make_vuln_generic_count_by_severity('low')
352863 vulnerability_info_generic_count = _make_vuln_generic_count_by_severity('informational')
353864 vulnerability_unclassified_generic_count = _make_vuln_generic_count_by_severity('unclassified')
865
866 important = Column(Boolean, nullable=False, default=False)
354867
355868 @classmethod
356869 def query_with_count(cls, confirmed, host_ids, workspace_name):
359872 query = query.filter(cls.id.in_(host_ids))
360873 return query.options(
361874 with_expression(
362 cls.vulnerability_info_count,
875 cls.vulnerability_informational_count,
363876 _make_vuln_count_property(
364877 type_=None,
365878 confirmed = confirmed,
369882 )
370883 ),
371884 with_expression(
372 cls.vulnerability_med_count,
885 cls.vulnerability_medium_count,
373886 _make_vuln_count_property(
374887 type_ = None,
375888 confirmed = confirmed,
446959 child_field='name')
447960
448961
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
515962 class Service(Metadata):
516963 STATUSES = [
517964 'open',
5621009 version = " (" + self.version + ")"
5631010 else:
5641011 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 ''}"
9101013
9111014
9121015 class VulnerabilityGeneric(VulnerabilityABC):
9311034 association_date = Column(DateTime, nullable=True)
9321035 disassociated_manually = Column(Boolean, nullable=False, default=False)
9331036 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(
9361049 Integer,
9371050 ForeignKey('vulnerability.id'),
9381051 index=True,
9421055 backref=backref('vulnerability_duplicate', remote_side=[id])
9431056 )
9441057
945 vulnerability_template_id = Column(
1058 vulnerability_template_id = Column(
9461059 Integer,
9471060 ForeignKey('vulnerability_template.id'),
9481061 index=True,
10501163 .where(text('vulnerability.service_id = service.id and '
10511164 'host_inner.id = service.host_id'))
10521165 )
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
10531174 target_host_os = column_property(
10541175 case([
10551176 (text('vulnerability.host_id IS NOT null'),
10791200 def has_duplicate(self):
10801201 return self.vulnerability_duplicate_id == None
10811202
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
11011203 @property
11021204 def hostnames(self):
11031205 if self.host is not None:
11061208 return self.service.host.hostnames
11071209 raise ValueError("Vulnerability has no service nor host")
11081210
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
11091229 @property
11101230 def parent(self):
11111231 return self.host or self.service
11171237
11181238 class VulnerabilityWeb(VulnerabilityGeneric):
11191239 __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)
11291240
11301241 @declared_attr
11311242 def service_id(cls):
11361247 @declared_attr
11371248 def service(cls):
11381249 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
11501250
11511251 @property
11521252 def parent(self):
14481548 vulnerability_code_count = query_expression()
14491549 vulnerability_standard_count = query_expression()
14501550 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()
14511559
14521560 workspace_permission_instances = relationship(
14531561 "WorkspacePermission",
14771585 FROM host
14781586 WHERE host.workspace_id = workspace.id
14791587 ) 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,
14801593 p_4.count_3 as open_services,
14811594 p_4.count_4 as total_service_count,
14821595 p_5.count_5 as vulnerability_web_count,
14831596 p_5.count_6 as vulnerability_code_count,
14841597 p_5.count_7 as vulnerability_standard_count,
14851598 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,
14861605 workspace.create_date AS workspace_create_date,
14871606 workspace.update_date AS workspace_update_date,
14881607 workspace.id AS workspace_id,
14981617 workspace.creator_id AS workspace_creator_id,
14991618 (SELECT {concat_func}(scope.name, ',') FROM scope where scope.workspace_id=workspace.id) as scope_raw
15001619 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
15021623 FROM service
15031624 RIGHT JOIN workspace w ON service.workspace_id = w.id
15041625 GROUP BY w.id
15051626 ) 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
15071638 FROM vulnerability
15081639 RIGHT JOIN workspace w ON vulnerability.workspace_id = w.id
15091640 WHERE 1=1 {0}
15351666 params['workspace_name'] = workspace_name
15361667 if filters:
15371668 query += ' WHERE ' + ' AND '.join(filters)
1538 #query += " GROUP BY workspace.id "
1669 # query += " GROUP BY workspace.id "
15391670 query += " ORDER BY workspace.name ASC"
1671
15401672 return db.session.execute(text(query), params)
15411673
15421674 def set_scope(self, new_scope):
16541786 super(User, self).__init__(*args, **kwargs)
16551787
16561788 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}>"
16591790
16601791 def get_security_payload(self):
16611792 return {
16621793 "username": self.username,
1663 "name": self.email
1794 "name": self.username,
1795 "email": self.email,
1796 "role": self.role,
1797 "roles": [self.role],
16641798 }
16651799
16661800
17401874 __mapper_args__ = {
17411875 'concrete': True
17421876 }
1743
17441877 template = relationship(
17451878 'MethodologyTemplate',
17461879 backref=backref('tasks', cascade="all, delete-orphan"))
18541987
18551988 object_id = Column(Integer, nullable=False)
18561989 object_type = Column(Enum(*OBJECT_TYPES, name='object_types'), nullable=False)
1990
18571991 tag = relationship('Tag', backref='tagged_objects')
18581992 tag_id = Column(Integer, ForeignKey('tag.id'), index=True)
18591993
19112045 confirmed = Column(Boolean, nullable=False, default=False)
19122046 vuln_count = Column(Integer, default=0) # saves the amount of vulns when the report was generated.
19132047 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="")
19142050
19152051 workspace_id = Column(Integer, ForeignKey('workspace.id'), index=True, nullable=False)
19162052 workspace = relationship(
19852121
19862122 faraday_kb_id = Column(Text, nullable=False)
19872123 reference_id = Column(Integer, nullable=False)
1988
19892124 script_name = Column(Text, nullable=False)
19902125 external_identifier = Column(Text, nullable=False)
19912126 tool_name = Column(Text, nullable=False)
21352270 backref=backref('agent_executions', cascade="all, delete-orphan"),
21362271 )
21372272 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
21382280
21392281 @property
21402282 def parent(self):
294294
295295 def get_command(self, obj):
296296 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}"
298298 if obj.command in ['DELETE', 'REMOVE']:
299299 return "--DELETE:"
300300 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.")
304304
305305
306306 class WorkerConditionSchema(Schema):
310310 if obj.operator == "equals":
311311 operator = "="
312312 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}'
315315
316316
317317 class WorkerRuleSchema(Schema):
337337 value = 'info'
338338 if value == 'medium':
339339 value = 'med'
340 return '{}={}'.format(object_rule_name, value)
340 return f'{object_rule_name}={value}'
341341
342342 @post_dump
343343 def remove_none_values(self, data, **kwargs):
00 import logging
11 import threading
2 from pathlib import Path
23 from threading import Thread
34 from queue import Queue, Empty
4 import os
5 from typing import Tuple
6
57 from faraday_plugins.plugins.manager import PluginsManager
68 from faraday.server.api.modules.bulk_create import bulk_create, BulkCreateSchema
79 from faraday.server import config
810
9 from faraday.server.models import Workspace
11 from faraday.server.models import Workspace, Command, User
1012 from faraday.server.utils.bulk_create import add_creator
1113
1214 logger = logging.getLogger(__name__)
2729 logger.debug("Stop Reports Manager")
2830 self.__event.set()
2931
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):
3137 logger.info("Send Report data to workspace [%s]", workspace_name)
3238 from faraday.server.web import app # pylint:disable=import-outside-toplevel
3339 with app.app_context():
3440 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()
3543 schema = BulkCreateSchema()
3644 data = schema.load(report_json)
3745 data = add_creator(data, user)
38 bulk_create(ws, data, True)
46 bulk_create(ws, command, data, True, True)
3947
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):
4154 plugin = self.plugins_manager.get_plugin(plugin_id)
4255 if plugin:
4356 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))
4660 vulns_data = plugin.get_data()
61 del vulns_data['command']['duration']
4762 except Exception as e:
4863 logger.error("Processing Error: %s", e)
4964 logger.exception(e)
5065 else:
5166 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 )
5370 logger.info("Report processing finished")
5471 except Exception as e:
5572 logger.exception(e)
5673 logger.error("Save Error: %s", e)
5774 else:
58 logger.info("No plugin detected for report [%s]", file_path)
75 logger.info(f"No plugin detected for report [{file_path}]")
5976
6077 def run(self):
6178 logger.debug("Start Reports Manager")
6279 while not self.__event.is_set():
6380 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 )
6895 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)
7098 except Empty:
7199 self.__event.wait(0.1)
72100 except KeyboardInterrupt:
1212 import signal
1313 import logging
1414 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 )
1722
1823 logger = logging.getLogger(__name__)
1924
136141
137142 def start_server():
138143 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
140145 createDaemon()
141146
142147
148153 return False
149154
150155 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}')
152157 os.kill(pid, signal.SIGTERM)
153158 logger.info("Faraday Server stopped successfully")
154159 except OSError as err:
184189 else:
185190 return pid
186191
187
188192 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():
190194 return None
191195
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:
193197 # If PID file is badly written, delete it and
194198 # assume server is not running
195199 try:
205209
206210
207211 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()}')
210214 atexit.register(partial(remove_pid_file, port))
211215
212216
213217 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))
215219
216220
217221 def get_ports_running():
218222 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)))
228230
229231 return ports
201201
202202 @compiler.compiles(BooleanToIntColumn, 'postgresql')
203203 def _integer_to_boolean_postgresql(element, compiler, **kw):
204 return '{0}::int'.format(element.expression_str)
204 return f'{element.expression_str}::int'
205205
206206
207207 @compiler.compiles(BooleanToIntColumn, 'sqlite')
217217 'VulnerabilityCode']:
218218 object_type = 'vulnerability'
219219 else:
220 raise RuntimeError("Unknown table for object: {}".format(
221 instance))
220 raise RuntimeError(f"Unknown table for object: {instance}")
222221 return object_type
223222
224223
55 import pstats
66 import contextlib
77 from io import StringIO
8 import logging
89
9 import faraday.server.utils.logger
1010
11 debug_logger = faraday.server.utils.logger.get_logger(__name__)
11 debug_logger = logging.getLogger(__name__)
1212
1313 class Timer:
1414 def __init__(self, tag, logger=None):
2222 def __exit__(self, *args):
2323 self.__end = time.time()
2424 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')
2626
2727 #
2828 # Debug utility extracted from http://docs.sqlalchemy.org/en/latest/faq/performance.html
4040 # ps.print_callers()
4141 debug_logger.debug(s.getvalue())
4242
43 # I'm Py3
43 # I'm Py3
127127 vuln_date = vuln['metadata']['create_time']
128128 if vuln['service']:
129129 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]
131131 vuln_service = " - ".join(service_fields_values)
132132 else:
133133 vuln_service = ""
33 See the file 'doc/LICENSE' for the license information
44
55 """
6 import logging
67 import re
78 import typing
89 import numbers
910 import datetime
11
12 import marshmallow_sqlalchemy
1013 from distutils.util import strtobool
1114
1215 from dateutil.parser import parse
13 from sqlalchemy import inspect
1416 from collections.abc import Iterable
1517 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
1719 from marshmallow_sqlalchemy.convert import ModelConverter
1820
19 from faraday.server.models import VulnerabilityWeb, Host, Service
21 from faraday.server.models import VulnerabilityWeb, Host, Service, VulnerabilityTemplate
2022 from faraday.server.utils.search import OPERATORS
2123 from faraday.server.fields import JSONType
2224
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
4725
4826 VALID_OPERATORS = set(OPERATORS.keys()) - set(['desc', 'asc'])
4927
28 logger = logging.getLogger(__name__)
5029
5130 class FlaskRestlessFilterSchema(Schema):
52 name = fields.String(validate=validate.OneOf(VULNERABILITY_FIELDS), required=True)
31 name = fields.String(required=True)
5332 val = fields.Raw(required=True)
5433 op = fields.String(validate=validate.OneOf(list(OPERATORS.keys())), required=True)
5534 valid_relationship = {
5635 'host': Host,
57 'service': Service
36 'services': Service
5837 }
5938
6039 def load(
7756 res += self._validate_filter_types(filter_)
7857 return res
7958
59 def _model_class(self):
60 raise NotImplementedError
61
8062 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 """
8168 if isinstance(filter_['val'], str) and '\x00' in filter_['val']:
8269 raise ValidationError('Value can\'t containt null chars')
8370 converter = ModelConverter()
9077 raise ValidationError('Invalid Relationship')
9178 column = getattr(model, column_name)
9279 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')
9484
9585 if not getattr(column, 'type', None) and filter_['op'].lower():
9686 if filter_['op'].lower() in ['eq', '==']:
9989 if not isinstance(filter_['val'], str):
10090 raise ValidationError('Relationship attribute to compare to must be a string')
10191 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_]
10295 else:
10396 raise ValidationError('Field does not support in operator')
10497
107100 if not isinstance(filter_['val'], Iterable):
108101 filter_['val'] = [filter_['val']]
109102
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
111112 if filter_['op'].lower() in ['ilike', 'like']:
112113 # like muse be used with string
113114 if isinstance(filter_['val'], numbers.Number) or isinstance(field, fields.Number):
169170 return [filter_]
170171
171172
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
172186 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 ]
175195
176196 def load(
177197 self,
192212 for search_filter in data:
193213 # we try to validate against filter schema since the list could contain
194214 # 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:
198225 res.append(self._do_load(
199226 search_filter, many=False, partial=partial, unknown=unknown, postprocess=True
200227 ))
203230
204231
205232 class FlaskRestlessGroupFieldSchema(Schema):
206 field = fields.String(validate=validate.OneOf(VULNERABILITY_FIELDS), required=True)
233 field = fields.String(required=True)
207234
208235
209236 class FlaskRestlessOrderFieldSchema(Schema):
210 field = fields.String(validate=validate.OneOf(VULNERABILITY_FIELDS), required=True)
237 field = fields.String(required=True)
211238 direction = fields.String(validate=validate.OneOf(["asc", "desc"]), required=False)
212239
213240
214241 class FilterSchema(Schema):
215 filters = fields.List(fields.Nested("FlaskRestlessSchema"))
242 filters = fields.Nested("FlaskRestlessSchema")
216243 order_by = fields.List(fields.Nested(FlaskRestlessOrderFieldSchema))
217244 group_by = fields.List(fields.Nested(FlaskRestlessGroupFieldSchema))
218245 limit = fields.Integer()
219246 offset = fields.Integer()
220247
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
221269
222270 class FlaskRestlessSchema(Schema):
223271 valid_schemas = [
224272 FilterSchema,
225 FlaskRestlessFilterSchema,
226273 FlaskRestlessOperator,
274 FlaskRestlessVulnerabilityFilterSchema,
275 FlaskRestlessVulnerabilityTemplateFilterSchema,
276 FlaskRestlessHostFilterSchema,
227277 ]
228278
229279 def load(
245295 return schema(many=many).load(data)
246296 except ValidationError:
247297 continue
248 raise ValidationError('No valid schema found. data {}'.format(data))
298 raise ValidationError(f'No valid schema found. data {data}')
33 See the file 'doc/LICENSE' for the license information
44
55 """
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
1076 def remove_null_caracters(string):
1087 string = string.replace('\x00', '')
1098 string = string.replace('\00', '')
00 # Faraday Penetration Test IDE
11 # Copyright (C) 2016 Infobyte LLC (http://www.infobytesec.com/)
22 # See the file 'doc/LICENSE' for the license information
3 import os
43 import logging
54 import logging.handlers
65 import faraday.server.config
76 import errno
87
98 from syslog_rfc5424_formatter import RFC5424Formatter
9 from faraday.server.config import CONST_FARADAY_HOME_PATH
1010
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'
1512
1613 MAX_LOG_FILE_SIZE = 5 * 1024 * 1024 # 5 MB
1714 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'
1916 LOG_DATE_FORMAT = '%Y-%m-%dT%H:%M:%S%z'
20 ROOT_LOGGER = u'faraday'
2117 LOGGING_HANDLERS = []
2218 LVL_SETTABLE_HANDLERS = []
2319
2420
2521 def setup_logging():
26 logger = logging.getLogger(ROOT_LOGGER)
27 logger.propagate = False
22 logger = logging.getLogger()
2823 logger.setLevel(logging.DEBUG)
2924
3025 if faraday.server.config.logger_config.use_rfc5424_formatter:
5550
5651
5752 def add_handler(handler):
58 logger = logging.getLogger(ROOT_LOGGER)
53 logger = logging.getLogger()
5954 logger.addHandler(handler)
6055 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
7856
7957
8058 def set_logging_level(level):
8563
8664 def create_logging_path():
8765 try:
88 os.makedirs(os.path.dirname(LOG_FILE))
66 LOG_FILE.parent.mkdir(parents=True)
8967 except OSError as e:
9068 if e.errno != errno.EEXIST:
9169 raise
9371 setup_logging()
9472
9573
96 # I'm Py3
74 # I'm Py3
1414
1515 """
1616 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_
1921 from sqlalchemy import inspect as sqlalchemy_inspect
2022 from sqlalchemy.ext.associationproxy import AssociationProxy
2123 from sqlalchemy.orm.attributes import InstrumentedAttribute
2224 from sqlalchemy.orm.attributes import QueryableAttribute
2325 from sqlalchemy.orm import ColumnProperty
26
27
28 logger = logging.getLogger(__name__)
2429
2530
2631 def session_query(session, model):
159164
160165 def __repr__(self):
161166 """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}>'
163168
164169
165170 class GroupBy:
175180
176181 def __repr__(self):
177182 """Returns a string representation of this object."""
178 return '<GroupBy {0}>'.format(self.field)
183 return f'<GroupBy {self.field}>'
179184
180185
181186 class Filter:
280285
281286 class ConjunctionFilter(JunctionFilter):
282287 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)}'
284289
285290
286291 class DisjunctionFilter(JunctionFilter):
287292 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)}'
289294
290295
291296 class SearchParameters:
484489 return or_(create_filt(model, f) for f in filt)
485490
486491 @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
487507 def create_query(session, model, search_params, _ignore_order_by=False):
488508 """Builds an SQLAlchemy query instance based on the search parameters
489509 present in ``search_params``, an instance of :class:`SearchParameters`.
521541 query = session.query(*select_fields)
522542 else:
523543 query = session.query(model)
524 # For the sake of brevity, rename this method.
525 create_filt = QueryBuilder._create_filter
526544 # This function call may raise an exception.
527545 valid_model_fields = [str(algo).split('.')[1] for algo in sqlalchemy_inspect(model).attrs]
528546
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]
537552
538553 # Multiple filter criteria at the top level of the provided search
539554 # parameters are interpreted as a conjunction (AND).
650665 # may raise NoResultFound or MultipleResultsFound
651666 return query.one()
652667 return query
653
00 # Faraday Penetration Test IDE
11 # Copyright (C) 2016 Infobyte LLC (http://www.infobytesec.com/)
22 # See the file 'doc/LICENSE' for the license information
3 import os
43 import sys
54 import functools
65 import logging
7170 class WebServer:
7271 UI_URL_PATH = b'_ui'
7372 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'
7574
7675 def __init__(self):
7776 self.__ssl_enabled = faraday.server.config.ssl.enabled
116115
117116 def __build_websockets_resource(self):
118117 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'
120119 if self.__websocket_ssl_enabled:
121120 url = 'wss://' + url
122121 else:
146145 self.raw_report_processor.stop()
147146 self.ping_home_thread.stop()
148147
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'
150149 site = twisted.web.server.Site(self.__root_resource,
151150 logPath=log_path,
152151 logFormatter=proxiedLogFormatter)
186185 except error.CannotListenError:
187186 logger.warn('Could not start websockets, address already open. This is ok is you wan to run multiple instances.')
188187 except Exception as ex:
189 logger.warn('Could not start websocket, error: {}'.format(ex))
188 logger.warn(f'Could not start websocket, error: {ex}')
190189 else:
191190 try:
192191 listenWS(self.__build_websockets_resource(), interface=self.__bind_address)
193192 except error.CannotListenError:
194193 logger.warn('Could not start websockets, address already open. This is ok is you wan to run multiple instances.')
195194 except Exception as ex:
196 logger.warn('Could not start websocket, error: {}'.format(ex))
195 logger.warn(f'Could not start websocket, error: {ex}')
197196 logger.info('Faraday Server is ready')
198197 reactor.addSystemEventTrigger('before', 'shutdown', signal_handler)
199198 reactor.run()
4141 def onConnect(self, request):
4242 protocol, headers = None, {}
4343 # see if there already is a cookie set ..
44 logger.debug('Websocket request {0}'.format(request))
44 logger.debug(f'Websocket request {request}')
4545 if 'cookie' in request.headers:
4646 try:
4747 cookie = http.cookies.SimpleCookie()
6363 message = json.loads(payload)
6464 if message['action'] == 'JOIN_WORKSPACE':
6565 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}')
6867 self.sendClose()
6968 return
7069 signer = itsdangerous.TimestampSigner(app.config['SECRET_KEY'],
119118 if message['action'] == 'RUN_STATUS':
120119 with app.app_context():
121120 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}')
123122 return True
124123
125124 (agent_id,) = [
229228 reactor.callLater(0.5, self.tick)
230229
231230 def join_workspace(self, client, workspace):
232 logger.debug('Join workspace {0}'.format(workspace))
231 logger.debug(f'Join workspace {workspace}')
233232 if client not in self.workspace_clients[workspace]:
234 logger.debug("registered client {}".format(client.peer))
233 logger.debug(f"registered client {client.peer}")
235234 self.workspace_clients[workspace].append(client)
236235
237236 def leave_workspace(self, client, workspace_name):
238 logger.debug('Leave workspace {0}'.format(workspace_name))
237 logger.debug(f'Leave workspace {workspace_name}')
239238 self.workspace_clients[workspace_name].remove(client)
240239
241240 def join_agent(self, agent_connection, agent):
242 logger.info("Agent {} joined!".format(agent.id))
241 logger.info(f"Agent {agent.id} joined!")
243242 connected_agents[agent.id] = agent_connection
244243 return True
245244
246245 def leave_agent(self, agent_connection, agent):
247 logger.info("Agent {} left".format(agent.id))
246 logger.info(f"Agent {agent.id} left")
248247 connected_agents.pop(agent.id)
249248 return True
250249
255254 for workspace_name, clients in self.workspace_clients.items():
256255 for client in clients:
257256 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}")
259258 self.leave_workspace(client, workspace_name)
260259 return
261260
263262 for (key, value) in connected_agents.copy().items():
264263 if value == protocol:
265264 del connected_agents[key]
266 logger.info("Agent {} disconnected!".format(key))
265 logger.info(f"Agent {key} disconnected!")
267266
268267 def broadcast(self, msg):
269268 if isinstance(msg, str):
270269 msg = msg.encode('utf-8')
271 logger.debug("broadcasting prepared message '{}' ..".format(msg))
270 logger.debug(f"broadcasting prepared message '{msg}' ..")
272271 prepared_msg = json.loads(self.prepareMessage(msg).payload)
273272 if b'agent_id' not in msg:
274273 for client in self.workspace_clients[prepared_msg['workspace']]:
275274 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}")
277276
278277 if b'agent_id' in msg:
279278 agent_id = prepared_msg['agent_id']
283282 # The agent is offline
284283 return
285284 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}")
10471047 font-size: 16px;
10481048 font-weight: bold;
10491049 }
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 }
210210 <script type="text/javascript" src="scripts/vulndb/controllers/modalNew.js"></script>
211211 <script type="text/javascript" src="scripts/vulndb/providers/vulnModel.js"></script>
212212 <script type="text/javascript" src="scripts/vulndb/providers/vulnModels.js"></script>
213 <script type="text/javascript" src="scripts/vulndb/directives/customField.js"></script>
213214 <script type="text/javascript" src="scripts/admin/admin.js"></script>
214215 <script type="text/javascript" src="scripts/admin/customFields/controllers/customFields.js"></script>
215216 <script type="text/javascript" src="scripts/admin/customFields/providers/customFields.js"></script>
3838 $scope.workspaces = [];
3939
4040 wss.forEach(function (ws) {
41 $scope.workspaces.push(ws);
41 if (ws.active && !ws.readonly) {
42 $scope.workspaces.push(ws);
43 }
4244 });
4345
4446 $scope.workspace = $scope.workspaces[0].name;
124124 controller: 'workspacesCtrl',
125125 title: 'Dashboard | '
126126 }).
127 when('/extras', {
128 templateUrl: 'scripts/extras/partials/extras.html',
129 title: 'Extras | '
130 }).
127131 when('/help', {
128132 templateUrl: 'scripts/help/partials/help.html',
129133 title: 'Help | '
77
88 $scope.data = {
99 "user": null,
10 "pass": null
10 "pass": null,
11 "remember": false
1112 };
1213
1314 $scope.errorLoginFlag = false;
1920
2021 $scope.login = function(){
2122 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){
2324 var currentUrl = "/workspaces";
2425 if($cookies.currentUrl != undefined) {
2526 currentUrl = $cookies.currentUrl;
2121 </span>
2222 </label>
2323 </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>
2432 <p class="font-xs font-bold fg-red pull-left margin-top-18px">{{errorMessage}}</p>
2533 </div><!-- .form-signin -->
2634 <button class="btn-frd btn-xl bg-blue btn-block" style="background-color: #00a8e1" type="submit" ng-click="login()">Login</button>
22 // See the file 'doc/LICENSE' for the license information
33
44 angular.module('faradayApp')
5 .service('loginSrv', ['BASEURL', '$q', function(BASEURL, $q) {
6
5 .service('loginSrv', ['BASEURL', '$q', '$cookies', function(BASEURL, $q, $cookies) {
6
77 loginSrv = {
88 is_authenticated: false,
99 user_obj: null,
1010 last_time_checked: new Date(0),
1111
12 login: function(user, pass){
12 login: function(user, pass, remember){
1313 var deferred = $q.defer();
1414 $.ajax({
1515 type: 'POST',
1616 url: BASEURL + '_api/login',
17 data: JSON.stringify({"email": user, "password": pass}),
17 data: JSON.stringify({"email": user, "password": pass, "remember": remember}),
1818 dataType: 'json',
1919 contentType: 'application/json'
2020 })
8383 loginSrv.user_obj = null;
8484 deferred.resolve();
8585 }
86 $cookies.remove('remember_token');
8687 $.ajax({
8788 url: BASEURL + '_api/logout',
8889 type: 'GET',
6464 <li role="menuitem"><a href="" ng-click="logout()">Logout</a></li>
6565 <hr class="hr_divider"></hr>
6666 <li role="menuitem"><a href="#/help">Help</a></li>
67 <li role="menuitem"><a href="#/extras">Extras</a></li>
6768 <li role="menuitem"><a href="" ng-click="about()">About</a></li>
6869 </ul>
6970 </div>
1111 </div><!-- .modal-body -->
1212 <div class="modal-footer container-fluid" style="text-align: center;">
1313 <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>
1515 </div>
1616 <div class="col-md-4">
1717 <a href="http://github.com/infobyte/faraday/blob/master/AUTHORS" target="_blank">Authors</a>
1818 </div>
1919 <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>
2121 </div>
2222 </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>
55 <h3 class="modal-title"><span class="glyphicon glyphicon-exclamation-sign"></span>Oops!</h3>
66 </div>
77 <div class="modal-body">
8 <h5>{{ msg }}</h5>
8 <h5 class="line-breaks">{{ msg }}</h5>
99 </div><!-- .modal-body -->
1010 <div class="modal-footer">
1111 <button class="btn btn-success" ng-click="ok()">OK</button>
55 <h3 class="modal-title"><span class="glyphicon glyphicon-ok"></span>Great!</h3>
66 </div>
77 <div class="modal-body">
8 <h5>{{ msg }}</h5>
8 <h5 class="line-breaks">{{ msg }}</h5>
99 </div><!-- .modal-body -->
1010 <div class="modal-footer">
1111 <button class="btn btn-success" ng-click="ok()">OK</button>
541541 return modVulnerabilityTemplate(createNonWorkspacedObject, vulnerabilityTemplate);
542542 };
543543
544 ServerAPI.bulkCreateVulnerabilityTemplate = function (vulns) {
545 var bulkCreateURL = APIURL + 'vulnerability_template/bulk_create/';
546 return send_data(bulkCreateURL, vulns, false, "POST");
547 };
548
544549 ServerAPI.updateVulnerabilityTemplate = function (vulnerabilityTemplate) {
545550 return modVulnerabilityTemplate(updateNonWorkspacedObject, vulnerabilityTemplate);
546551 };
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 -->
22 // See the file 'doc/LICENSE' for the license information
33
44 angular.module('faradayApp')
5 .controller('indexCtrl',
5 .controller('indexCtrl',
66 ['$scope', '$uibModal', 'indexFact', 'BASEURL',
77 function($scope, $uibModal, indexFact, BASEURL) {
88 indexFact.getConf().then(function(conf) {
797797 $scope.saveAsModel = function() {
798798 var self = this;
799799 var selected = $scope.getCurrentSelection();
800 var promises = [];
801800 try {
801 var vulnsToSend = [];
802802 selected.forEach(function(vuln) {
803803 let vulnCopy = angular.copy(vuln);
804804 vulnCopy.data = '';
806806 vulnCopy.description = vuln.desc;
807807 vulnCopy.desc_summary = vuln.desc;
808808 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 }
816836 } catch(err) {
817837 commonsFact.showMessage("Something failed when creating some of the templates.");
818838 }
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
819878 };
820879
821880 $scope.selectAll = function() {
3434 <div ng-if="cf.field_type === \'choice\'"> \n\
3535 <div class="tab-pane-header">{{cf.field_display_name}}</div> \n\
3636 <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\
3838 <span ng-if="modal.data.custom_fields[cf.field_name] !== null">{{modal.data.custom_fields[cf.field_name]}}</span>\n\
3939 <span ng-if="modal.data.custom_fields[cf.field_name] === null">Select {{cf.field_display_name}}</span>\n\
4040 </button>\n\
4343 </button> \n\
4444 <ul class="dropdown-menu dropdown-menu-right col-md-12 dropd-cf-choice" role="menu"> \n\
4545 <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\
4747 </li>\n\
4848 </ul>\n\
4949 </div> \n\
3636 <div ng-if="cf.field_type === \'choice\'"> \n\
3737 <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\
3838 <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\
4040 <span ng-if="lastClickedVuln.custom_fields[cf.field_name] !== null" > {{lastClickedVuln.custom_fields[cf.field_name]}}</span>\n\
4141 <span ng-if="lastClickedVuln.custom_fields[cf.field_name] === null">Select {{cf.field_display_name}}</span> \n\
4242 </button> \n\
4545 </button> \n\
4646 <ul class="dropdown-menu dropdown-menu-right col-md-12 dropd-cf-choice" role="menu"> \n\
4747 <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\
4949 </li> \n\
5050 </ul> \n\
5151 </div> \n\
4444 $scope.data.policyviolations = angular.copy($scope.policyviolations);
4545 $scope.data.refs = angular.copy($scope.references);
4646 $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 }
5354
5455 $modalInstance.close($scope.data);
5556 };
4646 $scope.data.model = $scope.other_model;
4747 }
4848
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 }
5456
5557 if ($scope.data.easeofresolution === ""){
5658 $scope.data.easeofresolution = null;
163163 }).then(
164164 function(d) {
165165 $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);
167175 $route.reload();
168176 },
169177 function(d){
170178 $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);
172185 }
173186 );
174187 }
175188 );
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;
176211 };
177212
178213 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 }]);
143143 </p>
144144 </div>
145145 <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>
153148 </div>
149 </div>
154150 </div>
155151 </div>
156152 </div><!-- .form-horizontal -->
149149 </div>
150150 <div class="col-md-12 margin-bottom-15px">
151151 <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>
157153 </div>
158154 </div>
159155 </div>
113113 }
114114 var message;
115115 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;
117118 } else {
118119 message = "Unable to save the Vuln Model. " + msg;
119120 }
99 vulnModelsManager.models = [];
1010 vulnModelsManager.totalNumberOfModels = 0;
1111
12 vulnModelsManager.create = function(data) {
12 vulnModelsManager.create = function(data) {
1313 var deferred = $q.defer();
1414 var self = this;
1515 try {
2727
2828 return deferred.promise;
2929 };
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 }
3051
3152 vulnModelsManager.delete = function(vulnModel) {
3253 var deferred = $q.defer();
5757 <th class="ui-grid-cell-contents ui-grid-header-cell">Vulns</th>
5858 <th class="ui-grid-cell-contents ui-grid-header-cell">Hosts</th>
5959 <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>
6061 <th class="ui-grid-cell-contents ui-grid-header-cell">Last Modified</th>
6162 <th class="ui-grid-cell-contents ui-grid-header-cell">Active</th>
6263 <th class="ui-grid-cell-contents ui-grid-header-cell">Read only</th>
7778 <td class="ui-grid-cell-contents"><a href="#/status/ws/{{ws.name}}">{{objects[ws.name]['total_vulns']}}</a></td>
7879 <td class="ui-grid-cell-contents"><a href="#/hosts/ws/{{ws.name}}">{{objects[ws.name]['hosts']}}</a></td>
7980 <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>
8082 <td class="ui-grid-cell-contents" ng-bind="ws.update_date | amTimeAgo"></td>
8183 <td class="ui-grid-cell-contents active-toggle">
8284 <div class="toogle-img-container">
22 # See the file 'doc/LICENSE' for the license information
33 import os
44 import sys
5 import glob
65 import socket
76 import argparse
7 import logging
88
99 from alembic.runtime.migration import MigrationContext
1010
1919 from alembic.script import ScriptDirectory
2020 from alembic.config import Config
2121
22 logger = faraday.server.utils.logger.get_logger(faraday.server.utils.logger.ROOT_LOGGER)
22 logger = logging.getLogger(__name__)
2323
2424 init()
2525
3434 def is_server_running(port):
3535 pid = daemonize.is_server_running(port)
3636 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}")
3838 return True
3939 else:
4040 return False
5050 with app.app_context():
5151 try:
5252 if not db.session.query(Workspace).count():
53 logger.warn('No workspaces found')
53 logger.warning('No workspaces found')
5454 except sqlalchemy.exc.ArgumentError:
5555 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'
5757 )
5858 sys.exit(1)
5959 except sqlalchemy.exc.OperationalError:
6262 sys.exit(1)
6363 except sqlalchemy.exc.ProgrammingError:
6464 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')
6666 sys.exit(1)
6767
6868
8787
8888 current_revision = context.get_current_revision()
8989 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')):
9293 print('--' * 20)
9394 print('Missing migrations, please execute: \n\n')
9495 print('faraday-manage migrate')
104105 def main():
105106 os.chdir(faraday.server.config.FARADAY_BASE)
106107 check_alembic_version()
108 # TODO RETURN TO prev CWD
107109 check_postgresql()
108110 parser = argparse.ArgumentParser()
109111 parser.add_argument('--debug', action='store_true', help='run Faraday Server in debug mode')
113115 parser.add_argument('--websocket_port', help='Overides server.ini websocket port configuration')
114116 parser.add_argument('--bind_address', help='Overides server.ini bind_address configuration')
115117 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}')
117119 args = parser.parse_args()
118120 if args.debug or faraday.server.config.faraday_server.debug:
119121 faraday.server.utils.logger.set_logging_level(faraday.server.config.DEBUG)
6565
6666
6767 """
68 import os
6869 import re
69
70 import logging
7071 from flask import current_app
7172
7273 from apispec import BasePlugin, yaml_utils
7879
7980 RE_URL = re.compile(r"<(?:[^:<>]+:)?([^<>]+)>")
8081
82 logger = logging.getLogger(__name__)
8183
8284 class FaradayAPIPlugin(BasePlugin):
8385 """APISpec plugin for Flask"""
117119 return self.flaskpath2openapi(rule.rule)
118120 view_instance = next(cl.cell_contents for cl in view.__closure__ if isinstance(cl.cell_contents, GenericView))
119121 if view_name in ['get', 'put', 'post', 'delete']:
120
121122 if view.__doc__:
122123 if hasattr(view_instance.model_class, "__name__"):
123124 class_model = view_instance.model_class.__name__
124125 else:
125126 class_model = 'No name'
127 logger.debug(f'{view_name} / {class_model} / {rule.methods} / {view_name} / {view_instance._get_schema_class().__name__}')
126128 operations[view_name] = yaml_utils.load_yaml_from_docstring(
127129 view.__doc__.format(schema_class=view_instance._get_schema_class().__name__, class_model=class_model, tag_name=class_model)
128130 )
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 )
130144 if hasattr(view, "view_class") and issubclass(view.view_class, MethodView):
131145 for method in view.methods:
132146 if method in rule.methods:
135149 operations[method_name] = yaml_utils.load_yaml_from_docstring(
136150 method.__doc__
137151 )
152
138153 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)
11 # If you run pynixify again, the file will be either overwritten or
22 # deleted, and you will lose the changes you made to it.
33
4 { overlays ? [ ], ... }@args:
4 { overlays ?
5 [ ]
6 , ...
7 }@args:
58 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 };
1547
1648 nixpkgs =
1749
1850 builtins.fetchTarball {
1951 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";
2255 };
2356
24 packageOverrides = self: super: {
25 anyascii = self.callPackage ./packages/anyascii { };
57 packageOverrides =
58 self: super: {
59 anyascii =
60 self.callPackage
61 ./packages/anyascii
62 { };
2663
27 apispec-webframeworks =
28 self.callPackage ./packages/apispec-webframeworks { };
64 apispec-webframeworks =
65 self.callPackage
66 ./packages/apispec-webframeworks
67 { };
2968
30 faraday-plugins = self.callPackage ./packages/faraday-plugins { };
69 faraday-plugins =
70 self.callPackage
71 ./packages/faraday-plugins
72 { };
3173
32 faradaysec = self.callPackage ./packages/faradaysec { };
74 faradaysec =
75 self.callPackage
76 ./packages/faradaysec
77 { };
3378
34 filedepot = self.callPackage ./packages/filedepot { };
79 filedepot =
80 self.callPackage
81 ./packages/filedepot
82 { };
3583
36 filteralchemy-fork = self.callPackage ./packages/filteralchemy-fork { };
84 filteralchemy-fork =
85 self.callPackage
86 ./packages/filteralchemy-fork
87 { };
3788
38 flask-classful = self.callPackage ./packages/flask-classful { };
89 flask-classful =
90 self.callPackage
91 ./packages/flask-classful
92 { };
3993
40 flask-kvsession-fork = self.callPackage ./packages/flask-kvsession-fork { };
94 flask-kvsession-fork =
95 self.callPackage
96 ./packages/flask-kvsession-fork
97 { };
4198
42 flask-login = self.callPackage ./packages/flask-login { };
99 flask-security =
100 self.callPackage
101 ./packages/flask-security
102 { };
43103
44 flask-security = self.callPackage ./packages/flask-security { };
104 simplekv =
105 self.callPackage
106 ./packages/simplekv
107 { };
45108
46 nplusone = self.callPackage ./packages/nplusone { };
109 syslog-rfc5424-formatter =
110 self.callPackage
111 ./packages/syslog-rfc5424-formatter
112 { };
47113
48 pytest-factoryboy = self.callPackage ./packages/pytest-factoryboy { };
114 webargs =
115 self.callPackage
116 ./packages/webargs
117 { };
49118
50 simplekv = self.callPackage ./packages/simplekv { };
119 };
51120
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 })
11 # If you run pynixify again, the file will be either overwritten or
22 # deleted, and you will lose the changes you made to it.
33
4 { buildPythonPackage, fetchPypi, lib }:
4 { buildPythonPackage
5 , fetchPypi
6 , lib
7 }:
58
69 buildPythonPackage rec {
7 pname = "anyascii";
8 version = "0.1.6";
10 pname =
11 "anyascii";
12 version =
13 "0.1.7";
914
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 };
1423
1524 # TODO FIXME
16 doCheck = false;
25 doCheck =
26 false;
1727
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 { };
2231 }
11 # If you run pynixify again, the file will be either overwritten or
22 # deleted, and you will lose the changes you made to it.
33
4 { apispec, buildPythonPackage, fetchPypi, lib }:
4 { apispec
5 , buildPythonPackage
6 , fetchPypi
7 , lib
8 }:
59
610 buildPythonPackage rec {
7 pname = "apispec-webframeworks";
8 version = "0.5.2";
11 pname =
12 "apispec-webframeworks";
13 version =
14 "0.5.2";
915
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 };
1424
15 propagatedBuildInputs = [ apispec ];
25 propagatedBuildInputs =
26 [
27 apispec
28 ];
1629
1730 # TODO FIXME
18 doCheck = false;
31 doCheck =
32 false;
1933
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 };
2441 }
11 # If you run pynixify again, the file will be either overwritten or
22 # deleted, and you will lose the changes you made to it.
33
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 }:
617
718 buildPythonPackage rec {
8 pname = "faraday-plugins";
9 version = "1.3.0";
19 pname =
20 "faraday-plugins";
21 version =
22 "1.4.0";
1023
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 };
1532
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 ];
2745
2846 # TODO FIXME
29 doCheck = false;
47 doCheck =
48 false;
3049
31 meta = with lib; { description = "Faraday plugins package"; };
50 meta =
51 with lib; {
52 description =
53 "Faraday plugins package";
54 };
3255 }
11 # If you run pynixify again, the file will be either overwritten or
22 # deleted, and you will lose the changes you made to it.
33
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 }:
1356
1457 buildPythonPackage rec {
15 pname = "faradaysec";
16 version = "3.11.1";
58 pname =
59 "faradaysec";
60 version =
61 "3.14.0";
1762
18 src = lib.cleanSource ../../..;
63 src =
64 lib.cleanSource
65 ../../..;
1966
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 ];
74125
75 checkPhase = "true # TODO fill with the real command for testing";
126 checkPhase =
127 "true # TODO fill with the real command for testing";
76128
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 };
82136 }
11 # If you run pynixify again, the file will be either overwritten or
22 # deleted, and you will lose the changes you made to it.
33
4 { anyascii, buildPythonPackage, fetchPypi, lib }:
4 { anyascii
5 , buildPythonPackage
6 , fetchPypi
7 , lib
8 }:
59
610 buildPythonPackage rec {
7 pname = "filedepot";
8 version = "0.8.0";
11 pname =
12 "filedepot";
13 version =
14 "0.8.0";
915
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 };
1424
15 propagatedBuildInputs = [ anyascii ];
25 propagatedBuildInputs =
26 [
27 anyascii
28 ];
1629
1730 # TODO FIXME
18 doCheck = false;
31 doCheck =
32 false;
1933
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 };
2541 }
11 # If you run pynixify again, the file will be either overwritten or
22 # deleted, and you will lose the changes you made to it.
33
4 { buildPythonPackage, fetchPypi, lib, marshmallow-sqlalchemy, six, webargs }:
4 { buildPythonPackage
5 , fetchPypi
6 , lib
7 , marshmallow-sqlalchemy
8 , six
9 , webargs
10 }:
511
612 buildPythonPackage rec {
7 pname = "filteralchemy-fork";
8 version = "0.1.0";
13 pname =
14 "filteralchemy-fork";
15 version =
16 "0.1.0";
917
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 };
1426
15 propagatedBuildInputs = [ six webargs marshmallow-sqlalchemy ];
27 propagatedBuildInputs =
28 [
29 six
30 webargs
31 marshmallow-sqlalchemy
32 ];
1633
1734 # TODO FIXME
18 doCheck = false;
35 doCheck =
36 false;
1937
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 };
2545 }
11 # If you run pynixify again, the file will be either overwritten or
22 # deleted, and you will lose the changes you made to it.
33
4 { buildPythonPackage, fetchPypi, flask, lib }:
4 { buildPythonPackage
5 , fetchPypi
6 , flask
7 , lib
8 }:
59
610 buildPythonPackage rec {
7 pname = "flask-classful";
8 version = "0.14.2";
11 pname =
12 "flask-classful";
13 version =
14 "0.14.2";
915
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 };
1525
16 propagatedBuildInputs = [ flask ];
26 propagatedBuildInputs =
27 [
28 flask
29 ];
1730
1831 # TODO FIXME
19 doCheck = false;
32 doCheck =
33 false;
2034
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 };
2542 }
11 # If you run pynixify again, the file will be either overwritten or
22 # deleted, and you will lose the changes you made to it.
33
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 }:
613
714 buildPythonPackage rec {
8 pname = "flask-kvsession-fork";
9 version = "0.6.3";
15 pname =
16 "flask-kvsession-fork";
17 version =
18 "0.6.3";
1019
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 };
1629
17 propagatedBuildInputs = [ flask simplekv werkzeug itsdangerous six ];
30 propagatedBuildInputs =
31 [
32 flask
33 simplekv
34 werkzeug
35 itsdangerous
36 six
37 ];
1838
1939 # TODO FIXME
20 doCheck = false;
40 doCheck =
41 false;
2142
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 };
2650 }
11 # If you run pynixify again, the file will be either overwritten or
22 # deleted, and you will lose the changes you made to it.
33
4 { buildPythonPackage, fetchPypi, flask, lib }:
4 { buildPythonPackage
5 , fetchPypi
6 , flask
7 , lib
8 }:
59
610 buildPythonPackage rec {
7 pname = "flask-login";
8 version = "0.5.0";
11 pname =
12 "flask-login";
13 version =
14 "0.5.0";
915
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 };
1525
16 propagatedBuildInputs = [ flask ];
26 propagatedBuildInputs =
27 [
28 flask
29 ];
1730
1831 # TODO FIXME
19 doCheck = false;
32 doCheck =
33 false;
2034
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 };
2542 }
11 # If you run pynixify again, the file will be either overwritten or
22 # deleted, and you will lose the changes you made to it.
33
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 }:
718
819 buildPythonPackage rec {
9 pname = "flask-security";
10 version = "3.0.0";
20 pname =
21 "flask-security";
22 version =
23 "3.0.0";
1124
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 };
1734
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 ];
2951
3052 # TODO FIXME
31 doCheck = false;
53 doCheck =
54 false;
3255
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 };
3763 }
11 # If you run pynixify again, the file will be either overwritten or
22 # deleted, and you will lose the changes you made to it.
33
4 { blinker, buildPythonPackage, fetchPypi, lib, six }:
4 { blinker
5 , buildPythonPackage
6 , fetchPypi
7 , lib
8 , six
9 }:
510
611 buildPythonPackage rec {
7 pname = "nplusone";
8 version = "1.0.0";
12 pname =
13 "nplusone";
14 version =
15 "1.0.0";
916
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 };
1425
15 propagatedBuildInputs = [ six blinker ];
26 propagatedBuildInputs =
27 [
28 six
29 blinker
30 ];
1631
1732 # TODO FIXME
18 doCheck = false;
33 doCheck =
34 false;
1935
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 };
2443 }
11 # If you run pynixify again, the file will be either overwritten or
22 # deleted, and you will lose the changes you made to it.
33
4 { buildPythonPackage, factory_boy, fetchPypi, inflection, lib, pytest }:
4 { buildPythonPackage
5 , factory_boy
6 , fetchPypi
7 , inflection
8 , lib
9 , pytest
10 }:
511
612 buildPythonPackage rec {
7 pname = "pytest-factoryboy";
8 version = "2.0.3";
13 pname =
14 "pytest-factoryboy";
15 version =
16 "2.0.3";
917
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 };
1426
15 propagatedBuildInputs = [ inflection factory_boy pytest ];
27 propagatedBuildInputs =
28 [
29 inflection
30 factory_boy
31 pytest
32 ];
1633
1734 # TODO FIXME
18 doCheck = false;
35 doCheck =
36 false;
1937
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 };
2445 }
11 # If you run pynixify again, the file will be either overwritten or
22 # deleted, and you will lose the changes you made to it.
33
4 { buildPythonPackage, fetchPypi, lib }:
4 { buildPythonPackage
5 , fetchPypi
6 , lib
7 }:
58
69 buildPythonPackage rec {
7 pname = "simplekv";
8 version = "0.14.1";
10 pname =
11 "simplekv";
12 version =
13 "0.14.1";
914
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 };
1423
1524 # TODO FIXME
16 doCheck = false;
25 doCheck =
26 false;
1727
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 };
2235 }
11 # If you run pynixify again, the file will be either overwritten or
22 # deleted, and you will lose the changes you made to it.
33
4 { buildPythonPackage, fetchPypi, lib }:
4 { buildPythonPackage
5 , fetchPypi
6 , lib
7 }:
58
69 buildPythonPackage rec {
7 pname = "syslog-rfc5424-formatter";
8 version = "1.2.2";
10 pname =
11 "syslog-rfc5424-formatter";
12 version =
13 "1.2.2";
914
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 };
1423
1524 # TODO FIXME
16 doCheck = false;
25 doCheck =
26 false;
1727
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 };
2335 }
11 # If you run pynixify again, the file will be either overwritten or
22 # deleted, and you will lose the changes you made to it.
33
4 { buildPythonPackage, fetchPypi, lib, marshmallow }:
4 { buildPythonPackage
5 , fetchPypi
6 , lib
7 , marshmallow
8 }:
59
610 buildPythonPackage rec {
7 pname = "webargs";
8 version = "6.1.0";
11 pname =
12 "webargs";
13 version =
14 "7.0.1";
915
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 };
1424
15 propagatedBuildInputs = [ marshmallow ];
25 propagatedBuildInputs =
26 [
27 marshmallow
28 ];
1629
1730 # TODO FIXME
18 doCheck = false;
31 doCheck =
32 false;
1933
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 };
2541 }
11 # If you run pynixify again, the file will be either overwritten or
22 # deleted, and you will lose the changes you made to it.
33
4 { buildPythonPackage, fetchPypi, lib }:
4 { buildPythonPackage
5 , fetchPypi
6 , lib
7 }:
58
69 buildPythonPackage rec {
7 pname = "werkzeug";
8 version = "1.0.1";
10 pname =
11 "werkzeug";
12 version =
13 "1.0.1";
914
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 };
1524
1625 # TODO FIXME
17 doCheck = false;
26 doCheck =
27 false;
1828
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 };
2336 }
11 # If you run pynixify again, the file will be either overwritten or
22 # deleted, and you will lose the changes you made to it.
33
4 { python ? "python3" }:
4 { python ?
5 "python3"
6 }:
57 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;
816 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 ];
1128 }
11 let
22 version = builtins.head (builtins.match ".*'([0-9]+.[0-9]+(.[0-9]+)?)'.*"
33 (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 '';
48
59 in { dockerName ? "registry.gitlab.com/faradaysec/faraday", dockerTag ? version
610 , systemUser ? "faraday", systemGroup ? "faraday", systemHome ? null
1115 , useLastCommit ? true }: rec {
1216
1317 faraday-server = python38.pkgs.faradaysec.overrideAttrs (old:
14 {
18 assert !builtins.hasAttr "checkInputs" old; {
19 name = "faraday-server-${version}";
1520 doCheck = true;
1621 checkPhase = "true";
22 checkInputs = [ pynixify runPynixify ];
1723 } // lib.optionalAttrs useLastCommit {
1824 src = builtins.fetchGit {
1925 url = ./.;
7177 [Install]
7278 WantedBy=multi-user.target
7379 '';
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 '';
74119 }
1919 requests>=2.18.4
2020 pyasn1
2121 service_identity>=17.0.0
22 SQLAlchemy>=1.2.0b2
22 SQLAlchemy>=1.2.0,<1.4.0
2323 tqdm>=4.15.0
2424 twisted>=18.9.0
25 webargs>=6.0.0
25 webargs>=7.0.0
2626 marshmallow-sqlalchemy
2727 filteralchemy-fork
2828 filedepot>=0.5.0
3232 Flask-KVSession-fork>=0.6.3
3333 distro>=1.4.0
3434 faraday-plugins>=1.0.1,<2.0.0
35 apispec>=3.0.0
35 apispec>=4.0.0
3636 apispec-webframeworks>=0.5.0
3737 pyyaml
00 flask # required to have flask shell inside nix-shell
11 factory-boy>=2.10.0
22 pylint
3 pytest>=3.5.1
3 pytest<6
44 pytest-cov
55 pytest-factoryboy>=2.0.1
66 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
99 import sys
1010 import argparse
1111 import os
12 from faraday.config.constant import CONST_FARADAY_HOME_PATH
12 from faraday.server.config import CONST_FARADAY_HOME_PATH
1313 from faraday.server.config import FARADAY_BASE
1414
1515 my_env = os.environ
5858 print(e['error'])
5959 sys.exit()
6060
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.
6262 if 'download' in resource:
6363 return r.content
6464 else:
186186
187187 Get the historical information for the particular scan and hid. Return
188188 the status if available. If not return unknown.
189 """
189 """
190190
191191 d = get_scan_history(sid, hid)
192192 return d['status']
238238 # For version 7, use the nessus scan Id to avoid overwrite the output file
239239 if not output:
240240 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:
244245 print('Saving scan results to {0}.'.format(report_path))
245246 report.write(data)
246247
279280 scans_info['id'] = scans['id']
280281 scans_info['creation_date'] = scans['creation_date']
281282 scan_list.append(scans_info)
282
283
283284 scan_list = sorted(scan_list,key=lambda scan:scan['creation_date'])
284285 return scan_list
285286
286287 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:
288289 date = date_file.read()
289290 try:
290291 date = int(date)
295296 return date
296297
297298 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:
299300 date_file.write(str(date))
300301
301302 def get_version():
304305
305306
306307 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()
311312
312313 if __name__ == "__main__":
313314 parser = argparse.ArgumentParser(description='nessus_client is develop for automating security testing')
361362 if scan['creation_date'] > date:
362363 set_date(scan['creation_date'])
363364 print('Downloading scan. Id: {0}'.format(scan['id']))
364 file_id = export(scan['id'])
365 file_id = export(scan['id'])
365366 download(scan['id'], file_id)
366367 else:
367368 print('Scan up to date. Id: {0}'.format(scan['id']))
368369
369370 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")
44
55 parser = argparse.ArgumentParser()
66 parser.add_argument('--mode', choices=['diff', 'ls'], default='diff')
7 parser.add_argument('--local', action='store_true', default=False)
78 args = parser.parse_args()
89
910 ACTUAL_BRANCH = subprocess.run(
1213 ).stdout.decode().strip()
1314
1415 BRANCH_NAME = os.environ.get("CI_COMMIT_REF_NAME", ACTUAL_BRANCH)
16 if not args.local:
17 BRANCH_NAME = f"origin/{BRANCH_NAME}"
18
1519 PINK_FILE = "faraday/server/api/modules/reports.py"
1620 BLACK_FILE = "faraday/server/api/modules/jira.py"
1721
4246 intersection = git_diff_intersection({BLACK_FILE})
4347 assert len(intersection) == 0, f"The {intersection} should not be in " \
4448 f"{BRANCH_NAME}"
49 assert child.returncode == 0, (child.stdout, child.returncode)
22
33 [tool:pytest]
44 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
66
77 # Always prefer setuptools over distutils
88 from setuptools import setup, find_packages
9 from os import path
109 # io.open is needed for projects that support Python 2.7
1110 # It ensures open() defaults to text mode with universal newlines,
1211 # and accepts an argument to specify the text encoding
1312 # Python 3 only projects can skip this import
1413 from io import open
1514 from re import search
16
17 here = path.abspath(path.dirname(__file__))
1815
1916 # Get the long description from the README file
2017 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.
250247 'Source': 'https://github.com/infobyte/faraday/',
251248 },
252249 setup_requires=['pytest-runner'],
253 tests_require=['pytest', 'flask'] + dev_required,
250 tests_require=dev_required,
254251 )
255
256
257 # I'm Py3
66
77 from tempfile import NamedTemporaryFile
88
9 import os
10 import sys
119 import json
1210 import inspect
1311 import pytest
1412 from factory import Factory
1513 from flask.testing import FlaskClient
1614 from flask_principal import Identity, identity_changed
15 from pathlib import Path
16 from pytest_factoryboy import register
1717 from sqlalchemy import event
18 from pytest_factoryboy import register
19
20 sys.path.append(os.path.abspath(os.getcwd()))
18
2119 from faraday.server.app import create_app
2220 from faraday.server.models import db
2321 from tests import factories
2422
2523
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'
2825
2926 TEMPORATY_SQLITE = NamedTemporaryFile()
3027 # Discover factories to automatically register them to pytest-factoryboy and to
7976 def pytest_addoption(parser):
8077 # currently for tests using sqlite and memory have problem while using transactions
8178 # 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}',
8380 help="Database connection string. Defaults to in-memory "
8481 "sqlite if not specified:")
8582 parser.addoption('--ignore-nplusone', action='store_true',
228225 from flask import g
229226 try:
230227 del g.csrf_token
231 except:
228 except (NameError, AttributeError):
232229 pass
233230
234231 return app.test_client()
293290 dialect = db.session.bind.dialect.name
294291 if request.node.get_closest_marker('skip_sql_dialect'):
295292 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}')
297294
298295
299296 @pytest.fixture
+0
-203
tests/dont_run_but_update_updates.py less more
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
5353 RuleAction)
5454
5555 # 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(
5859 datetime.datetime.now() - datetime.timedelta(days=40),
5960 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 )
6871
6972 all_unicode = ''.join(chr(i) for i in range(65536))
7073 UNICODE_LETTERS = ''.join(c for c in all_unicode if unicodedata.category(c) == 'Lu' or unicodedata.category(c) == 'Ll')
493496 workspace = factory.LazyAttribute(
494497 lambda agent_execution: agent_execution.executor.agent.workspaces[0]
495498 )
499 command = factory.SubFactory(
500 CommandFactory,
501 workspace=factory.SelfAttribute("..workspace"),
502 end_date=None
503 )
496504
497505 class Meta:
498506 model = AgentExecution
33 See the file 'doc/LICENSE' for the license information
44
55 '''
6 import os
76 import pytest
87 from faraday.server.models import File
98 from depot.manager import DepotManager
109
11 CURRENT_PATH = os.path.dirname(os.path.abspath(__file__))
10 from tests.conftest import TEST_DATA_PATH
1211
1312
1413 @pytest.fixture
1514 def depotfile():
1615 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:
1919 fileid = depot.create(fp, 'faraday.png', 'image/png')
2020 return fileid
2121
5555 assert len(vulnerability_web.evidence) == 1
5656 assert vulnerability_web.evidence[0].object_type == 'vulnerability'
5757 assert vulnerability_web.evidence[0].object_id == vulnerability_web.id
58 # I'm Py3
58 # I'm Py3
2424 session.commit()
2525
2626 res = test_client.get(
27 '/v2/ws/{ws_name}/activities/'
28 .format(ws_name=ws.name)
27 f'/v2/ws/{ws.name}/activities/'
2928 )
3029
3130 assert res.status_code == 200
4645 itime = 1544745600.0
4746 data = {
4847 'command': command.command,
49 'tool' : command.tool,
48 'tool': command.tool,
5049 'itime': itime
5150
5251 }
5352
5453 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}/',
5755 data=data,
5856 )
5957 assert res.status_code == 200
77 import pytest
88
99 from faraday.server.api.modules.agent import AgentWithWorkspacesView, AgentView
10 from faraday.server.models import Agent
10 from faraday.server.models import Agent, Command
1111 from tests.factories import AgentFactory, WorkspaceFactory, ExecutorFactory
1212 from tests.test_api_non_workspaced_base import ReadOnlyAPITests
1313 from tests.test_api_workspaced_base import ReadOnlyMultiWorkspacedAPITests
520520 json=payload
521521 )
522522 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
523526
524527 def test_invalid_json_on_executorData_breaks_the_api(self, csrf_token,
525528 session, test_client):
1111 Vulnerability,
1212 VulnerabilityGeneric,
1313 VulnerabilityWeb,
14 Workspace
1415 )
1516 from faraday.server.api.modules import bulk_create as bc
17 from tests.factories import CustomFieldsSchemaFactory
1618
1719 host_data = {
1820 "ip": "127.0.0.1",
6466 'user': 'root',
6567 'hostname': 'pc',
6668 'start_date': '2014-12-22T03:12:58.019077+00:00',
67 'duration': 30,
6869 }
6970
7071
7273 return model.query.filter(model.workspace == workspace).count()
7374
7475
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
7587 def test_create_host(session, workspace):
7688 assert count(Host, workspace) == 0
77 bc.bulk_create(workspace, dict(hosts=[host_data]))
89 bc.bulk_create(workspace, None, dict(hosts=[host_data]))
7890 db.session.commit()
7991 host = Host.query.filter(Host.workspace == workspace).one()
8092 assert host.ip == "127.0.0.1"
8395
8496 def test_create_duplicated_hosts(session, workspace):
8597 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]))
8799 db.session.commit()
88100 assert count(Host, workspace) == 1
89101
90102
91103 def test_create_host_add_hostnames(session, workspace):
92104 assert count(Host, workspace) == 0
93 bc.bulk_create(workspace, dict(hosts=[host_data]))
105 bc.bulk_create(workspace, None, dict(hosts=[host_data]))
94106 db.session.commit()
95107 host_copy = host_data.copy()
96108 host_copy['hostnames'] = ["test3.org"]
97 bc.bulk_create(workspace, dict(hosts=[host_copy]))
109 bc.bulk_create(workspace, None, dict(hosts=[host_copy]))
98110 db.session.commit()
99111 host = Host.query.filter(Host.workspace == workspace).one()
100112 assert host.ip == "127.0.0.1"
109121 "description": host.description,
110122 "hostnames": [hn.name for hn in host.hostnames]
111123 }
112 bc.bulk_create(host.workspace, dict(hosts=[data]))
124 bc.bulk_create(host.workspace, None, dict(hosts=[data]))
113125 assert count(Host, host.workspace) == 1
114126
115127
116128 def test_create_host_with_services(session, workspace):
117129 host_data_ = host_data.copy()
118130 host_data_['services'] = [service_data]
119 bc.bulk_create(workspace, dict(hosts=[host_data_]))
131 bc.bulk_create(workspace, None, dict(hosts=[host_data_]))
120132 assert count(Host, workspace) == 1
121133 assert count(Service, workspace) == 1
122134 service = Service.query.filter(Service.workspace == workspace).one()
193205 vuln_web_data_ = vuln_data.copy()
194206 service_data_['vulnerabilities'] = [vuln_web_data_]
195207 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 )
197217 assert count(Vulnerability, service.workspace) == 1
198218 vuln = service.workspace.vulnerabilities[0]
199219 assert vuln.tool == vuln_data['tool']
206226 vuln_web_data_.pop('tool')
207227 service_data_['vulnerabilities'] = [vuln_web_data_]
208228 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 )
210235 assert count(Vulnerability, service.workspace) == 1
211236 vuln = service.workspace.vulnerabilities[0]
212237 assert vuln.tool == command_data['tool']
262287 def test_create_host_with_vuln(session, workspace):
263288 host_data_ = host_data.copy()
264289 host_data_['vulnerabilities'] = [vuln_data]
265 bc.bulk_create(workspace, dict(hosts=[host_data_]))
290 bc.bulk_create(workspace, None, dict(hosts=[host_data_]))
266291 assert count(Host, workspace) == 1
267292 host = workspace.hosts[0]
268293 assert count(Vulnerability, workspace) == 1
274299 def test_create_host_with_cred(session, workspace):
275300 host_data_ = host_data.copy()
276301 host_data_['credentials'] = [credential_data]
277 bc.bulk_create(workspace, dict(hosts=[host_data_]))
302 bc.bulk_create(workspace, None, dict(hosts=[host_data_]))
278303 assert count(Host, workspace) == 1
279304 host = workspace.hosts[0]
280305 assert count(Credential, workspace) == 1
359384 assert vuln.status_code == 200
360385
361386
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=[]))
364395 assert count(Command, workspace) == 1
365396 command = workspace.commands[0]
366397 assert command.tool == 'pytest'
367398 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):
372406 host_data_ = host_data.copy()
373407 service_data_ = service_data.copy()
374408 vuln_web_data_ = vuln_data.copy()
378412 host_data_['services'] = [service_data_]
379413 host_data_['vulnerabilities'] = [vuln_data]
380414 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 )
382421
383422 command = workspace.commands[0]
384423 host = workspace.hosts[0]
511550
512551 data['command'] = command_data.copy()
513552
514 bc.bulk_create(command.workspace, data)
553 command2 = new_empty_command(command.workspace)
554 bc.bulk_create(command.workspace, command2, data)
515555 assert count(Command, command.workspace) == 2
516556
517557 new_command = Command.query.filter_by(tool='pytest').one()
531571 def test_bulk_create_endpoint(session, workspace, test_client, logged_user):
532572 assert count(Host, workspace) == 0
533573 assert count(VulnerabilityGeneric, workspace) == 0
534 url = 'v2/ws/{}/bulk_create/'.format(workspace.name)
574 url = f'v2/ws/{workspace.name}/bulk_create/'
535575 host_data_ = host_data.copy()
536576 service_data_ = service_data.copy()
537577 service_data_['vulnerabilities'] = [vuln_data]
538578 host_data_['services'] = [service_data_]
539579 host_data_['credentials'] = [credential_data]
540580 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 )
542585 assert res.status_code == 201, res.json
543586 assert count(Host, workspace) == 1
544587 assert count(Service, workspace) == 1
545588 assert count(Vulnerability, workspace) == 2
589 assert count(Command, workspace) == 1
546590 host = Host.query.filter(Host.workspace == workspace).one()
547591 assert host.ip == "127.0.0.1"
548592 assert host.creator_id == logged_user.id
554598 assert service.creator_id == logged_user.id
555599 credential = Credential.query.filter(Credential.workspace == workspace).one()
556600 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
557604
558605
559606 @pytest.mark.usefixtures('logged_user')
591638
592639 @pytest.mark.usefixtures('logged_user')
593640 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/'
595642 host_data_ = host_data.copy()
596643 host_data_.pop('ip')
597644 res = test_client.post(url, data=dict(hosts=[host_data_]))
599646
600647
601648 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/'
603650 res = test_client.post(url, data=dict(hosts=[host_data]))
604651 assert res.status_code == 401
605652 assert count(Host, workspace) == 0
608655 @pytest.mark.parametrize('token_type', ['agent', 'token'])
609656 def test_bulk_create_endpoints_fails_with_invalid_token(
610657 session, token_type, workspace, test_client):
611 url = 'v2/ws/{}/bulk_create/'.format(workspace.name)
658 url = f'v2/ws/{workspace.name}/bulk_create/'
612659 res = test_client.post(
613660 url,
614661 data=dict(hosts=[host_data]),
615 headers=[("authorization", "{} 1234".format(token_type))]
662 headers=[("authorization", f"{token_type} 1234")]
616663 )
617664 if token_type == 'token':
618665 # TODO change expected status code to 403
630677 session.add(agent)
631678 session.commit()
632679 assert agent.token
633 url = 'v2/ws/{}/bulk_create/'.format(second_workspace.name)
680 url = f'v2/ws/{second_workspace.name}/bulk_create/'
634681 res = test_client.post(
635682 url,
636683 data=dict(hosts=[host_data]),
637 headers=[("authorization", "agent {}".format(agent.token))]
684 headers=[("authorization", f"agent {agent.token}")]
638685 )
639686 assert res.status_code == 404
640687 assert b'No such workspace' in res.data
647694 session.add(agent)
648695 session.commit()
649696 assert agent.token
650 url = 'v2/ws/{}/bulk_create/'.format("im_a_incorrect_ws")
697 url = "v2/ws/im_a_incorrect_ws/bulk_create/"
651698 res = test_client.post(
652699 url,
653700 data=dict(hosts=[host_data]),
654 headers=[("authorization", "agent {}".format(agent.token))]
701 headers=[("authorization", f"agent {agent.token}")]
655702 )
656703 assert res.status_code == 404
657704 assert b'No such workspace' in res.data
664711 session.commit()
665712 for workspace in agent.workspaces:
666713 assert count(Host, workspace) == 0
667 url = 'v2/ws/{}/bulk_create/'.format(workspace.name)
714 url = f'v2/ws/{workspace.name}/bulk_create/'
668715 res = test_client.post(
669716 url,
670717 data=dict(hosts=[host_data]),
671 headers=[("authorization", "agent {}".format(agent.token))]
718 headers=[("authorization", f"agent {agent.token}")]
672719 )
673720 assert res.status_code == 400
674721 assert b"\'execution_id\' argument expected" in res.data
676723 assert count(Command, workspace) == 0
677724
678725
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,
680729 test_client,
681 agent_execution_factory):
730 agent_execution_factory,
731 start_date, duration):
732 agent_execution = agent_execution_factory.create()
682733 agent = agent_execution.executor.agent
683734 extra_agent_execution = agent_execution_factory.create()
684735
686737 agent_execution.executor.parameters_metadata = {}
687738 agent_execution.parameters_data = {}
688739 agent_execution.workspace = workspace
740 agent_execution.command.workspace = workspace
689741 session.add(agent_execution)
690742 session.add(extra_agent_execution)
691743 session.commit()
692744
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/'
695777 res = test_client.post(
696778 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}")]
699781 )
700782 assert res.status_code == 400
701783
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
704787 res = test_client.post(
705788 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}")]
708791 )
709792 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()
710834 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/'
712837 res = test_client.post(
713838 url,
714839 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}")]
743841 )
744842 assert res.status_code == 201
745843 assert count(Host, workspace) == 1
752850 params = ', '.join([f'{key}={value}' for (key, value) in agent_execution.parameters_data.items()])
753851 assert command.params == str(params)
754852 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
755856
756857
757858 def test_bulk_create_endpoint_with_agent_token_readonly_workspace(
763864 session.commit()
764865 for workspace in agent.workspaces:
765866
766 url = 'v2/ws/{}/bulk_create/'.format(workspace.name)
867 url = f'v2/ws/{workspace.name}/bulk_create/'
767868 res = test_client.post(
768869 url,
769870 data=dict(hosts=[host_data]),
770 headers=[("authorization", "agent {}".format(agent.token))]
871 headers=[("authorization", f"agent {agent.token}")]
771872 )
772873 assert res.status_code == 403
773874
780881 session.add(workspace)
781882 session.commit()
782883 for workspace in agent.workspaces:
783 url = 'v2/ws/{}/bulk_create/'.format(workspace.name)
884 url = f'v2/ws/{workspace.name}/bulk_create/'
784885 res = test_client.post(
785886 url,
786887 data=dict(hosts=[host_data]),
787 headers=[("authorization", "agent {}".format(agent.token))]
888 headers=[("authorization", f"agent {agent.token}")]
788889 )
789890 assert res.status_code == 403
790891
791892 @pytest.mark.usefixtures('logged_user')
792893 def test_bulk_create_endpoint_raises_400_with_no_data(
793894 session, test_client, workspace):
794 url = 'v2/ws/{}/bulk_create/'.format(workspace.name)
895 url = f'v2/ws/{workspace.name}/bulk_create/'
795896 res = test_client.post(
796897 url,
797898 data="",
804905 def test_bulk_create_endpoint_with_vuln_run_date(session, workspace, test_client):
805906 assert count(Host, workspace) == 0
806907 assert count(VulnerabilityGeneric, workspace) == 0
807 url = 'v2/ws/{}/bulk_create/'.format(workspace.name)
908 url = f'v2/ws/{workspace.name}/bulk_create/'
808909 run_date = datetime.now(timezone.utc) - timedelta(days=30)
809910 host_data_copy = host_data.copy()
810911 vuln_data_copy = vuln_data.copy()
821922 def test_bulk_create_endpoint_with_vuln_future_run_date(session, workspace, test_client):
822923 assert count(Host, workspace) == 0
823924 assert count(VulnerabilityGeneric, workspace) == 0
824 url = 'v2/ws/{}/bulk_create/'.format(workspace.name)
925 url = f'v2/ws/{workspace.name}/bulk_create/'
825926 run_date = datetime.now(timezone.utc) + timedelta(days=10)
826927 host_data_copy = host_data.copy()
827928 vuln_data_copy = vuln_data.copy()
839940 def test_bulk_create_endpoint_with_invalid_vuln_run_date(session, workspace, test_client):
840941 assert count(Host, workspace) == 0
841942 assert count(VulnerabilityGeneric, workspace) == 0
842 url = 'v2/ws/{}/bulk_create/'.format(workspace.name)
943 url = f'v2/ws/{workspace.name}/bulk_create/'
843944 host_data_copy = host_data.copy()
844945 vuln_data_copy = vuln_data.copy()
845946 vuln_data_copy['run_date'] = "INVALID_VALUE"
849950 assert count(VulnerabilityGeneric, workspace) == 0
850951
851952
852
853
854953 @pytest.mark.usefixtures('logged_user')
855954 def test_bulk_create_endpoint_fails_with_list_in_NullToBlankString(session, workspace, test_client, logged_user):
856955 assert count(Host, workspace) == 0
857956 assert count(VulnerabilityGeneric, workspace) == 0
858 url = 'v2/ws/{}/bulk_create/'.format(workspace.name)
957 url = f'v2/ws/{workspace.name}/bulk_create/'
859958 host_data_ = host_data.copy()
860959 host_data_['services'] = [service_data]
861960 host_data_['credentials'] = [credential_data]
868967 assert count(Credential, workspace) == 0
869968 assert count(Vulnerability, workspace) == 0
870969
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
404404 )
405405 session.commit()
406406
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}/')
411411 assert res.status_code == 204
412412
413413 res = test_client.get(self.url(workspace=command.workspace) + 'activity_feed/')
112112 assert 'object' in res.json
113113 assert type(res.json) == dict
114114
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]
103103 credential = self.factory.create(host=host, service=None,
104104 workspace=self.workspace)
105105 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}')
107107 assert res.status_code == 200
108108 assert [cred['value']['parent'] for cred in res.json['rows']] == [credential.host.id]
109109 assert [cred['value']['parent_type'] for cred in res.json['rows']] == [u'Host']
112112 service = ServiceFactory.create()
113113 credential = self.factory.create(service=service, host=None, workspace=service.workspace)
114114 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}')
116116 assert res.status_code == 200
117117 assert [cred['value']['parent'] for cred in res.json['rows']] == [credential.service.id]
118118 assert [cred['value']['parent_type'] for cred in res.json['rows']] == [u'Service']
248248 credential4 = self.factory.create(service=service2, host=None, workspace=second_workspace)
249249
250250 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}",
255255 ]
256256
257257 # Desc order
44
55 '''
66
7 import os
87 import pytest
98 from lxml.etree import fromstring, tostring
109
10 from tests.conftest import TEST_DATA_PATH
1111 from tests.factories import (
1212 WorkspaceFactory,
1313 HostFactory,
2121 class TestExportData():
2222 def test_export_data_without_format(self, test_client):
2323 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'
2525 response = test_client.get(url)
2626 assert response.status_code == 400
2727
8484 session.add(vuln_web)
8585 session.commit()
8686
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'
8888 response = test_client.get(url)
8989 assert response.status_code == 200
9090 response_xml = response.data
9191
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:
9795 xml_file = output.read()
9896
9997 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
1111 class TestGetExploits():
1212 def test_get_exploit(self, test_client):
1313 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}')
1515 assert res.status_code == 200
1616
1717 @pytest.mark.skip()
1818 def test_key_error(self, test_client):
1919 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}')
2121 assert res.status_code == 400
2222
2323 @pytest.mark.skip()
2424 def test_get_exploit_with_modules(self, test_client):
2525 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}')
2727 assert res.status_code == 200
2828 assert res.json.get('metasploit') != []
2929 assert res.json.get('exploitdb') != []
1212 try:
1313 import urlparse
1414 from urllib import urlencode
15 except: # For Python 3
15 except ImportError: # For Python 3
1616 import urllib.parse as urlparse
1717 from urllib.parse import urlencode
1818 from random import choice
136136 assert host.ip == "127.0.0.1"
137137 assert host.description == "aaaaa"
138138 assert host.os == ''
139 assert host.workspace == self.workspace
140
139 assert host.workspace == self.workspace
140
141141 def test_create_a_host_fails_with_missing_desc(self, test_client):
142142 res = test_client.post(self.url(), data={
143143 "ip": "127.0.0.1",
288288 assert res.status_code == 200
289289 self.compare_results(hosts, res)
290290
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
291324 def test_filter_by_os_like_ilike(self, test_client, session, workspace,
292325 second_workspace, host_factory):
293326 # The hosts that should be shown
313346 assert res.status_code == 200
314347 self.compare_results(hosts + [case_insensitive_host], res)
315348
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
316375 def test_filter_by_service(self, test_client, session, workspace,
317376 service_factory, host_factory):
318377 services = service_factory.create_batch(10, workspace=workspace,
329388 expected_host_ids = set(host.id for host in hosts)
330389 assert shown_hosts_ids == expected_host_ids
331390
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
332411 def test_filter_by_service_port(self, test_client, session, workspace,
333412 service_factory, host_factory):
334413 services = service_factory.create_batch(10, workspace=workspace, port=25)
344423 expected_host_ids = set(host.id for host in hosts)
345424 assert shown_hosts_ids == expected_host_ids
346425
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
347473 def test_filter_by_invalid_service_port(self, test_client, session, workspace,
348474 service_factory, host_factory):
349475 services = service_factory.create_batch(10, workspace=workspace, port=25)
356482 res = test_client.get(self.url() + '?port=invalid_port')
357483 assert res.status_code == 200
358484 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
359514
360515 def test_search_ip(self, test_client, session, workspace, host_factory):
361516 host = host_factory.create(ip="longname",
399554
400555 host = host_factory.create(workspace=workspace)
401556 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")
404559
405560 session.commit()
406561
409564 json_host = list(filter(lambda json_host: json_host['value']['id'] == host.id, res.json['rows']))[0]
410565 # the host has one vuln associated. another one via service.
411566 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
412573
413574 def test_host_services_vuln_count_verification(self, test_client, session,
414575 workspace, host_factory, vulnerability_factory,
497658 "os":"Microsoft Windows Server 2008 R2 Standard Service Pack 1",
498659 "id": 4000,
499660 "icon":"windows",
500 "versions": []}
661 "versions": [],
662 "important": False,
663 }
501664
502665 res = test_client.put(self.url(host, workspace=host.workspace), data=raw_data)
503666 assert res.status_code == 200
529692 u'services': 0,
530693 u'service_summaries': [],
531694 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 }
533708
534709 def test_add_hosts_from_csv(self, session, test_client, csrf_token):
535710 ws = WorkspaceFactory.create(name='abc')
545720 'csrf_token': csrf_token
546721 }
547722 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/',
549724 data=data, headers=headers, use_json_data=False)
550725 assert res.status_code == 200
551726 assert res.json['hosts_created'] == expected_created_hosts
560735 hosts_ids = [host_1.id, host_2.id]
561736 request_data = {'hosts_ids': hosts_ids}
562737
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)
564739
565740 deleted_hosts = delete_response.json['deleted_hosts']
566741 host_count_after_delete = db.session.query(Host).filter(
575750 ws = WorkspaceFactory.create(name="abc")
576751 request_data = {'hosts_ids': []}
577752
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)
579754
580755 assert delete_response.status_code == 400
581756
588763
589764 # Try to delete workspace_2's host from workspace_1
590765 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/'
592767 delete_response = test_client.delete(url, data=request_data)
593768
594769 assert delete_response.json['deleted_hosts'] == 0
596771 def test_bulk_delete_hosts_invalid_characters_in_request(self, test_client):
597772 ws = WorkspaceFactory.create(name="abc")
598773 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)
600775
601776 assert delete_response.json['deleted_hosts'] == 0
602777
611786 headers = [('content-type', 'text/xml')]
612787
613788 delete_response = test_client.delete(
614 '/v2/ws/{0}/hosts/bulk_delete/'.format(ws.name),
789 f'/v2/ws/{ws.name}/hosts/bulk_delete/',
615790 data=request_data,
616791 headers=headers)
617792
799974 "os":"Unknown",
800975 }
801976
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)
803978 assert res.status_code == 200
804979
805980 assert session.query(Hostname).filter_by(host=host).count() == 1
9301105 def send_api_request(raw_data):
9311106
9321107 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/',
9341109 data=raw_data)
9351110 assert res.status_code in [201, 400, 409]
9361111
44
55 '''
66
7 import os
87 import pytest
98
109
1211 class TestAPIInfoEndpoint:
1312
1413 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
2314 response = test_client.get('v2/info')
2415 assert response.status_code == 200
2516 assert response.json['Faraday Server'] == 'Running'
26 # to avoid side effects
27 os.chdir(current_dir)
2817
2918 def test_get_config(self, test_client):
3019 res = test_client.get('/config')
88 from tests.conftest import logged_user, login_as
99
1010
11 class TestLogin():
11 class TestLogin:
1212 def test_case_bug_with_username(self, test_client, session):
1313 """
1414 When the user case does not match the one in database,
103103 test_client.cookie_jar.clear()
104104 res = test_client.get('/v2/ws/wonderland/', headers=headers)
105105 assert res.status_code == 200
106 assert res.headers.has_key('Set-Cookie') is False
106 assert 'Set-Cookie' not in res.headers
107107 cookies = [cookie.name for cookie in test_client.cookie_jar]
108108 assert "faraday_session_2" not in cookies
109109
164164
165165 ws = test_client.get('/v2/ws/wonderland/', headers=headers)
166166 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
55 '''
66
77 import pytest
8 from tests.conftest import login_as
89
910 @pytest.mark.usefixtures('logged_user')
1011 class TestSessionLogged():
1112 def test_session_when_user_is_logged(self, test_client):
1213 res = test_client.get('/session')
1314 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
1424
1525 class TestSessionNotLogged():
1626 def test_session_when_user_is_not_logged(self, test_client):
44
55 '''
66
7 import os
87 import pytest
98 from io import BytesIO
109
10 from tests.conftest import TEST_DATA_PATH
1111 from tests.factories import WorkspaceFactory
1212
1313 from faraday.server.threads.reports_processor import REPORTS_QUEUE
1414
15 from faraday.server.models import Host, Vulnerability, Service, Command
15 from faraday.server.models import Host, Service, Command
1616
1717
1818 @pytest.mark.usefixtures('logged_user')
2222 ws = WorkspaceFactory.create(name="abc")
2323 session.add(ws)
2424 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'
2926
30 with open(path,'rb') as report:
27 with path.open('rb') as report:
3128 file_contents = report.read()
3229 data = {
33 'file' : (BytesIO(file_contents), 'nmap_report.xml'),
30 'file': (BytesIO(file_contents), 'nmap_report.xml'),
3431 'csrf_token': csrf_token
3532 }
3633
3734 res = test_client.post(
38 '/v2/ws/{ws_name}/upload_report'.format(ws_name=ws.name),
35 f'/v2/ws/{ws.name}/upload_report',
3936 data=data,
4037 use_json_data=False)
4138
4340 assert len(REPORTS_QUEUE.queue) == 1
4441 queue_elem = REPORTS_QUEUE.queue[0]
4542 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
4845
4946 # I'm testing a method which lost referene of workspace and logged_user within the test
5047 ws_id = ws.id
5249
5350 from faraday.server.threads.reports_processor import ReportsManager
5451 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])
5655 command = Command.query.filter(Command.workspace_id == ws_id).one()
5756 assert command
5857 assert command.creator_id == logged_user_id
58 assert command.id == res.json["command_id"]
59 assert command.end_date
5960 host = Host.query.filter(Host.workspace_id == ws_id).first()
6061 assert host
6162 assert host.creator_id == logged_user_id
7071 session.commit()
7172
7273 res = test_client.post(
73 '/v2/ws/{ws_name}/upload_report'.format(ws_name=ws.name))
74 f'/v2/ws/{ws.name}/upload_report')
7475
7576 assert res.status_code == 400
7677
7980 ws = WorkspaceFactory.create(name="abc")
8081 session.add(ws)
8182 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'
8684
87 with open(path,'r') as report:
85 with path.open('r') as report:
8886 file_contents = report.read().encode('utf-8')
8987
9088 data = {
91 'file' : (BytesIO(file_contents), 'nmap_report.xml'),
89 'file': (BytesIO(file_contents), 'nmap_report.xml'),
9290 }
9391
9492 res = test_client.post(
95 '/v2/ws/{ws_name}/upload_report'.format(ws_name=ws.name),
93 f'/v2/ws/{ws.name}/upload_report',
9694 data=data,
9795 use_json_data=False)
9896
9997 assert res.status_code == 403
100
10198
10299 def test_request_with_workspace_deactivate(self, test_client, session, csrf_token):
103100 ws = WorkspaceFactory.create(name="abc")
104101 ws.active = False
105102 session.add(ws)
106103 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'
111105
112 with open(path,'r') as report:
106 with path.open('r') as report:
113107 file_contents = report.read().encode('utf-8')
114108
115109 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
118112 }
119113 res = test_client.post(
120 '/v2/ws/{ws_name}/upload_report'.format(ws_name=ws.name),
114 f'/v2/ws/{ws.name}/upload_report',
121115 data=data,
122116 use_json_data=False)
123117
44 See the file 'doc/LICENSE' for the license information
55
66 '''
7 import os
87 import csv
98 import json
109 import urllib
1110 import datetime
1211 from builtins import str
12 from pathlib import Path
1313 from tempfile import NamedTemporaryFile
1414 from base64 import b64encode
1515 from io import BytesIO, StringIO
2626
2727 from hypothesis import given, settings, strategies as st
2828
29 from sqlalchemy import inspect
2930 from faraday.server.api.modules.vulns import (
3031 VulnerabilityFilterSet,
3132 VulnerabilitySchema,
3233 VulnerabilityView
3334 )
3435 from faraday.server.fields import FaradayUploadedFile
35 from faraday.server.utils.filters import WHITE_LIST
3636 from faraday.server.schemas import NullToBlankString
3737 from tests import factories
38 from tests.conftest import TEST_DATA
38 from tests.conftest import TEST_DATA_PATH
3939 from tests.test_api_workspaced_base import (
4040 ReadOnlyAPITests
4141 )
6666 WorkspaceFactory,
6767 CustomFieldsSchemaFactory
6868 )
69
70 CURRENT_PATH = os.path.dirname(os.path.abspath(__file__))
71
7269
7370 def _create_post_data_vulnerability(name, vuln_type, parent_id,
7471 parent_type, refs, policyviolations,
261258 ))),
262259 ], ids=['standard_vuln_with_host', 'standard_vuln_with_service',
263260 'web_vuln_with_service'])
264
265261 def test_hostnames(self, host_with_hostnames, test_client, session,
266262 creator_func):
267263 vuln = creator_func(host_with_hostnames)
296292 )
297293 ws_name = host_with_hostnames.workspace.name
298294 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)
300296 assert res.status_code == 201
301297 assert vuln_count_previous + 1 == session.query(Vulnerability).count()
302298 assert res.json['name'] == 'New vulns'
369365 assert filename in res.json['_attachments']
370366 attachment.close()
371367 # 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}/')
373369 assert res.status_code == 200
374370 assert res.data == file_content
375371
376372 res = test_client.get(
377373 self.url() +
378 '{0}/attachment/notexistingattachment.png/'.format(vuln_id))
374 f'{vuln_id}/attachment/notexistingattachment.png/')
379375 assert res.status_code == 404
380376
381377 @pytest.mark.usefixtures('ignore_nplusone')
400396 assert res.status_code == 200
401397 filename = attachment.name.split('/')[-1]
402398 res = test_client.get(
403 self.url() + '{0}/attachment/{1}/'.format(vuln.id, filename))
399 self.url() + f'{vuln.id}/attachment/{filename}/')
404400 assert res.status_code == 200
405401 assert res.data == file_content
406402
423419
424420 # verify that the old file was deleted and the new one exists
425421 res = test_client.get(
426 self.url() + '{0}/attachment/{1}/'.format(vuln.id, filename))
422 self.url() + f'{vuln.id}/attachment/{filename}/')
427423 assert res.status_code == 404
428424 res = test_client.get(
429 self.url() + '{0}/attachment/{1}/'.format(vuln.id, new_filename))
425 self.url() + f'{vuln.id}/attachment/{new_filename}/')
430426 assert res.status_code == 200
431427 assert res.data == file_content
432428
434430 vuln = VulnerabilityFactory.create(workspace=workspace)
435431 session.add(vuln)
436432 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:
439436 new_file = FaradayUploadedFile(file_obj.read())
440437
441438 new_attach = File(object_type='vulnerability', object_id=vuln.id, name='Faraday', filename='faraday.png',
443440 session.add(new_attach)
444441 session.commit()
445442
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/')
447444 assert res.status_code == 200
448445 assert new_attach.filename in res.json
449446 assert 'image/png' in res.json[new_attach.filename]['content_type']
480477 )
481478 ws_name = host_with_hostnames.workspace.name
482479 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)
484481 assert res.status_code == 201
485482 for prop, value in vuln_props.items():
486483 if prop not in vuln_props_excluded:
507504 )
508505 ws_name = host_with_hostnames.workspace.name
509506 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)
511508 assert res.status_code == 201
512509 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)
514511 assert res.status_code == 409
515512 assert vuln_count_previous + 1 == session.query(Vulnerability).count()
516513
527524 )
528525 ws_name = host_with_hostnames.workspace.name
529526 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)
531528 assert res.status_code == 201
532529 assert vuln_count_previous + 1 == session.query(Vulnerability).count()
533530 assert res.json['status'] == 'closed'
644641 ws_name = host_with_hostnames.workspace.name
645642 vuln_count_previous = session.query(Vulnerability).count()
646643 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)
648645 assert res.status_code == 201
649646 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()
651648 assert res.json['name'] == 'New vulns'
652649 assert res.json['owner'] == 'test'
653650 assert res.json['type'] == 'VulnerabilityWeb'
715712 expected_ids.update(vuln.id for vuln in medium_vulns_web)
716713
717714 res = test_client.get(self.url(
718 workspace=second_workspace) + '?severity=%s' % medium_name)
715 workspace=second_workspace) + f'?severity={medium_name}')
719716 assert res.status_code == 200
720717 for vuln in res.json['data']:
721718 assert vuln['severity'] == 'med'
824821 assert vuln['target'] == '9.9.9.9'
825822 assert set(vuln['_id'] for vuln in res.json['data']) == expected_ids
826823
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
827901 @pytest.mark.usefixtures('mock_envelope_list')
828902 def test_sort_by_method(self, session, test_client, second_workspace,
829903 vulnerability_factory, vulnerability_web_factory):
858932 session):
859933 session.commit() # flush host_with_hostnames
860934 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')
863937 ]
864938 raw_data = _create_post_data_vulnerability(
865939 name='New vulns',
872946 )
873947 ws_name = host_with_hostnames.workspace.name
874948 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)
876950
877951 assert res.status_code == 201
878952 assert len(res.json['_attachments']) == 2
891965 )
892966 ws_name = host_with_hostnames.workspace.name
893967 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)
895969 assert res.status_code == 201
896970 assert session.query(Reference).count() == 2
897971 assert vuln_count_previous + 1 == session.query(Vulnerability).count()
909983 )
910984 ws_name = host_with_hostnames.workspace.name
911985 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)
913987 assert res.status_code == 201
914988 assert session.query(PolicyViolation).count() == 1
915989 assert vuln_count_previous + 1 == session.query(Vulnerability).count()
9321006 )
9331007 ws_name = host_with_hostnames.workspace.name
9341008 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)
9361010 assert res.status_code == 201
9371011 assert vuln_count_previous + 1 == session.query(Vulnerability).count()
9381012 assert res.json['name'] == 'New vulns'
9591033 )
9601034 ws_name = host_with_hostnames.workspace.name
9611035 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)
9631037 assert res.status_code == 400
9641038
9651039 def test_create_vuln_with_invalid_type(self,
9781052 ws_name = host_with_hostnames.workspace.name
9791053 vuln_count_previous = session.query(Vulnerability).count()
9801054 res = test_client.post(
981 '/v2/ws/{0}/vulns/'.format(ws_name),
1055 f'/v2/ws/{ws_name}/vulns/',
9821056 data=raw_data,
9831057 )
9841058 assert res.status_code == 400
10071081 raw_data.pop("type")
10081082 ws_name = host_with_hostnames.workspace.name
10091083 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)
10111085 assert res.status_code == 400
10121086 assert vuln_count_previous == session.query(Vulnerability).count()
10131087 assert res.json['message'] == 'Type is required.'
10271101 )
10281102 ws_name = host_with_hostnames.workspace.name
10291103 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)
10311105 assert res.status_code == 400
10321106 assert vuln_count_previous == session.query(Vulnerability).count()
10331107 assert b'Invalid severity type.' in res.data
10481122 )
10491123 ws_name = host_with_hostnames.workspace.name
10501124 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)
10521126 assert res.status_code == 400
10531127 assert vuln_count_previous == session.query(Vulnerability).count()
10541128 assert list(res.json['messages']['json'].keys()) == ['easeofresolution']
10691143 easeofresolution=None,
10701144 )
10711145 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/',
10731147 data=raw_data)
10741148 assert res.status_code == 201, (res.status_code, res.data)
10751149 created_vuln = Vulnerability.query.get(res.json['_id'])
13651439 web_expected_ids.update(vuln.id for vuln in high_vulns_web)
13661440
13671441 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}')
13691443 assert res.status_code == 200
13701444 for vuln in res.json['data']:
13711445 command_object = CommandObject.query.filter_by(
13781452
13791453 # Check for web vulns
13801454 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}')
13821456 assert res.status_code == 200
13831457 for vuln in res.json['data']:
13841458 command_object = CommandObject.query.filter_by(
13911465
13921466 # Check for cross-workspace bugs
13931467 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}')
13951469 assert res.status_code == 200
13961470 assert len(res.json['data']) == 0
13971471
15071581 severity='low',
15081582 )
15091583 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)
15111585 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/',
15131587 data=raw_data)
15141588 assert res.status_code == 409
15151589
15341608 severity='low',
15351609 )
15361610 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)
15381612 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/',
15401614 data=raw_data)
15411615 assert res.status_code == 409
15421616
16031677 severity='low',
16041678 )
16051679 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)
16071681 assert res.status_code == 201
16081682 raw_data = _create_post_data_vulnerability(
16091683 name='Update vulnsweb',
16151689 description='Update helloworld',
16161690 severity='high',
16171691 )
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}",
16191693 data=raw_data)
16201694 assert res.status_code == 200
16211695
17441818 session.add(vuln)
17451819 session.add(vuln2)
17461820 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}')
17481822 assert res.json['count'] == 1
17491823 assert res.json['vulnerabilities'][0]['value']['name'] == vuln.name
17501824
17611835 session.add(service)
17621836 session.add(hostname)
17631837 session.commit()
1764 url = self.url(workspace=workspace) + '?hostnames={0}'.format(hostname.name)
1838 url = self.url(workspace=workspace) + f'?hostnames={hostname.name}'
17651839 res = test_client.get(url)
17661840
17671841 assert res.status_code == 200
17801854 session.add(host)
17811855 session.add(hostname)
17821856 session.commit()
1783 url = self.url(workspace=workspace) + '?hostnames={0}'.format(hostname.name)
1857 url = self.url(workspace=workspace) + f'?hostnames={hostname.name}'
17841858 res = test_client.get(url)
17851859 assert res.status_code == 200
17861860 assert res.json['count'] == 1
18041878 session.commit()
18051879
18061880 #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}')
18081882 assert res.status_code == 200
18091883 assert res.json['count'] == 2
18101884
18511925 session.commit()
18521926 file_contents = b'my file contents'
18531927 data = {
1854 'file' : (BytesIO(file_contents), 'borrar.txt')
1928 'file': (BytesIO(file_contents), 'borrar.txt')
18551929 }
18561930 headers = {'Content-type': 'multipart/form-data'}
18571931
18581932 res = test_client.post(
1859 '/v2/ws/abc/vulns/{0}/attachment/'.format(vuln.id),
1933 f'/v2/ws/abc/vulns/{vuln.id}/attachment/',
18601934 data=data, headers=headers, use_json_data=False)
18611935 assert res.status_code == 403 # Missing CSRF protection
18621936
18631937 data = {
1864 'file' : (BytesIO(file_contents), 'borrar.txt'),
1938 'file': (BytesIO(file_contents), 'borrar.txt'),
18651939 'csrf_token': csrf_token
18661940 }
18671941 res = test_client.post(
1868 '/v2/ws/abc/vulns/{0}/attachment/'.format(vuln.id),
1942 f'/v2/ws/abc/vulns/{vuln.id}/attachment/',
18691943 data=data, headers=headers, use_json_data=False)
18701944 assert res.status_code == 200 # Now it should work
18711945
18811955 session.commit()
18821956 file_contents = b'my file contents'
18831957 data = {
1884 'file' : (BytesIO(file_contents), 'borrar.txt')
1958 'file': (BytesIO(file_contents), 'borrar.txt')
18851959 }
18861960 headers = {'Content-type': 'multipart/form-data'}
18871961
18891963 session.commit()
18901964
18911965 res = test_client.post(
1892 '/v2/ws/abc/vulns/{0}/attachment/'.format(vuln.id),
1966 f'/v2/ws/abc/vulns/{vuln.id}/attachment/',
18931967 data=data, headers=headers, use_json_data=False)
18941968 assert res.status_code == 403
18951969 query_test = session.query(Vulnerability).filter_by(id=vuln.id).first().evidence
19111985 policyviolations=[],
19121986 attachments=[attachment]
19131987 )
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)
19161989 assert res.status_code == 201
19171990
19181991 filename = attachment.name.split('/')[-1]
19191992 vuln_id = res.json['_id']
19201993 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}/'
19231995 )
19241996 assert res.status_code == 200
19251997
19422014 policyviolations=[],
19432015 attachments=[attachment]
19442016 )
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)
19472018 assert res.status_code == 201
19482019
19492020 self.workspace.readonly = True
19522023 filename = attachment.name.split('/')[-1]
19532024 vuln_id = res.json['_id']
19542025 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}/'
19572027 )
19582028 assert res.status_code == 403
19592029
19612031 assert len(query_test) == 1
19622032 assert query_test[0].filename == filename
19632033
1964 @pytest.mark.usefixtures('ignore_nplusone')
19652034 def test_vuln_filter(self, test_client, session, workspace):
19662035 new_host = HostFactory.create(workspace=workspace)
19672036 session.commit()
19762045 description='helloworld',
19772046 severity='medium',
19782047 )
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)
19812049
19822050 data = {
19832051 'q': '{"filters":[{"name":"severity","op":"eq","val":"medium"}]}'
19932061 data = {
19942062 "q": {"filters":[{"name":"severity","op":"eq","val":"medium"}]}
19952063 }
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)
19972065 assert res.status_code == 400
19982066
1999 @pytest.mark.usefixtures('ignore_nplusone')
20002067 def test_vuln_filter_exception(self, test_client, workspace, session):
20012068 vuln = VulnerabilityFactory.create(workspace=workspace, severity="medium")
20022069 session.add(vuln)
20042071 data = {
20052072 'q': '{"filters":[{"name":"severity","op":"eq","val":"medium"}]}'
20062073 }
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)
20082075 assert res.status_code == 200
20092076 assert res.json['count'] == 1
20102077
2011 @pytest.mark.usefixtures('ignore_nplusone')
20122078 def test_vuln_restless_group_same_creator(self, test_client, session):
20132079 workspace = WorkspaceFactory.create()
20142080 creator = UserFactory.create()
20282094 data = {
20292095 'q': '{"group_by":[{"field":"creator_id"}]}'
20302096 }
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)
20322098 assert res.status_code == 200
20332099 assert res.json['count'] == 1 # all vulns created by the same creator
20342100 expected = [{'count': 2, 'creator_id': creator.id}]
20352101 assert [vuln['value'] for vuln in res.json['vulnerabilities']] == expected
20362102
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
20382190 def test_vuln_restless_group_same_name_description(self, test_client, session):
20392191 workspace = WorkspaceFactory.create()
20402192 creator = UserFactory.create()
20662218 data = {
20672219 'q': '{"group_by":[{"field":"name"}, {"field":"description"}]}'
20682220 }
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)
20702222 assert res.status_code == 200
20712223 assert res.json['count'] == 2
20722224 expected = [{'count': 2, 'name': 'test', 'description': 'test'}, {'count': 1, 'name': 'test2', 'description': 'test'}]
20732225 assert [vuln['value'] for vuln in res.json['vulnerabilities']] == expected
20742226
2075 @pytest.mark.usefixtures('ignore_nplusone')
20762227 def test_vuln_restless_sort_by_(self, test_client, session):
20772228 workspace = WorkspaceFactory.create()
20782229 host = HostFactory.create(workspace=workspace)
21322283 data = {
21332284 'q': json.dumps(query)
21342285 }
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)
21362287 assert res.status_code == 200
21372288 assert res.json['count'] == 12
21382289 expected_order = ['critical', 'critical', 'med', 'med', 'med', 'med', 'med', 'med', 'med', 'med', 'med', 'med']
21462297 data = {
21472298 'q': json.dumps({"filters":[{"name":"creator","op":"eq","val": vuln.creator.username}]})
21482299 }
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
21532303 def test_vuln_web_filter_exception(self, test_client, workspace, session):
21542304 vuln = VulnerabilityWebFactory.create(workspace=workspace, severity="medium")
21552305 session.add(vuln)
21572307 data = {
21582308 'q': '{"filters":[{"name":"severity","op":"eq","val":"medium"}]}'
21592309 }
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)
21612311 assert res.status_code == 200
21622312 assert res.json['count'] == 1
21632313
21932343 session.commit()
21942344
21952345 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/',
21982347 data={'csrf_token': csrf_token},
21992348 headers={'Content-Type': 'multipart/form-data'},
22002349 use_json_data=False)
22022351
22032352 def test_get_attachment_with_invalid_workspace_and_vuln(self, test_client):
22042353 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/")
22072355 assert res.status_code == 404
22082356
22092357 def test_delete_attachment_with_invalid_workspace_and_vuln(self, test_client):
22102358 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/")
22132360 assert res.status_code == 404
22142361
22152362 def test_delete_invalid_attachment(self, test_client, workspace, session):
22172364 session.add(vuln)
22182365 session.commit()
22192366 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/")
22222368 assert res.status_code == 404
22232369
22242370 def test_export_vuln_csv_empty_workspace(self, test_client, session):
22252371 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/')
22272373 expected_headers = [
22282374 "confirmed", "id", "date", "name", "severity", "service",
22292375 "target", "desc", "status", "hostnames", "comments", "owner",
22402386 assert res.status_code == 200
22412387 assert expected_headers == res.data.decode('utf-8').strip('\r\n').split(',')
22422388
2243 @pytest.mark.usefixtures('ignore_nplusone')
22442389 def test_export_vuln_csv_filters_confirmed_using_filters_query(self, test_client, session):
22452390 workspace = WorkspaceFactory.create()
22462391 confirmed_vulns = VulnerabilityFactory.create(confirmed=True, workspace=workspace)
23032448 session.add(vuln)
23042449 session.commit()
23052450
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/')
23072452 assert res.status_code == 200
23082453
23092454 csv_data = csv.DictReader(StringIO(res.data.decode('utf-8')), delimiter=',')
26162761 )
26172762 ws_name = host_with_hostnames.workspace.name
26182763 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)
26212766 vuln_1_id = res_1.json['obj_id']
26222767 vuln_2_id = res_2.json['obj_id']
26232768 vulns_to_delete = [vuln_1_id, vuln_2_id]
26242769 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)
26262771 vuln_count_after = session.query(Vulnerability).count()
26272772 deleted_vulns = delete_response.json['deleted_vulns']
26282773 assert delete_response.status_code == 200
26612806 )
26622807 ws_name = host_with_hostnames.workspace.name
26632808 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)
26662811 vuln_1_id = res_1.json['obj_id']
26672812 vuln_2_id = res_2.json['obj_id']
26682813 vulns_to_delete = [vuln_1_id, vuln_2_id]
26692814 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)
26712816 vuln_count_after = session.query(Vulnerability).count()
26722817 deleted_vulns = delete_response.json['deleted_vulns']
26732818 assert delete_response.status_code == 200
26972842 )
26982843 ws_name = host_with_hostnames.workspace.name
26992844 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)
27012846 assert res.status_code == 201
27022847 assert vuln_count_previous + 1 == session.query(Vulnerability).count()
27032848 assert res.json['tool'] == tool_name
27232868 )
27242869 ws_name = host_with_hostnames.workspace.name
27252870 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)
27272872 assert res.status_code == 201
27282873 assert vuln_count_previous + 1 == session.query(Vulnerability).count()
27292874 assert res.json['tool'] == "Web UI"
27992944 @pytest.mark.usefixtures('logged_user')
28002945 class TestVulnerabilitySearch():
28012946
2802 @pytest.mark.usefixtures('ignore_nplusone')
28032947 @pytest.mark.skip_sql_dialect('sqlite')
28042948 def test_search_by_hostname_vulns(self, test_client, session):
28052949 workspace = WorkspaceFactory.create()
28142958 [{"name":"hostnames","op":"eq","val":"pepe"}]
28152959 }
28162960 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)}')
28182962 assert res.status_code == 200
28192963 assert res.json['count'] == 1
28202964 assert res.json['vulnerabilities'][0]['id'] == vuln.id
28212965
2822 @pytest.mark.usefixtures('ignore_nplusone')
28232966 @pytest.mark.skip_sql_dialect('sqlite')
28242967 def test_search_by_hostname_vulns_with_service(self, test_client, session):
28252968 workspace = WorkspaceFactory.create()
28352978 [{"name":"hostnames","op":"eq","val":"pepe"}]
28362979 }
28372980 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)}')
28392982 assert res.status_code == 200
28402983 assert res.json['count'] == 1
28412984 assert res.json['vulnerabilities'][0]['id'] == vuln.id
28422985
2986 @pytest.mark.skip_sql_dialect('sqlite')
28432987 @pytest.mark.usefixtures('ignore_nplusone')
2844 @pytest.mark.skip_sql_dialect('sqlite')
28452988 def test_search_hostname_web_vulns(self, test_client, session):
28462989 workspace = WorkspaceFactory.create()
28472990 host = HostFactory.create(workspace=workspace)
28562999 [{"name": "hostnames", "op": "eq", "val": "pepe"}]
28573000 }
28583001 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)}')
28603003 assert res.status_code == 200
28613004 assert res.json['count'] == 1
28623005 assert res.json['vulnerabilities'][0]['id'] == vuln.id
28633006
2864 @pytest.mark.usefixtures('ignore_nplusone')
28653007 def test_search_empty_filters(self, workspace, test_client, session):
28663008 query_filter = {"filters":
28673009 []
28683010 }
28693011 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)}')
28713013 assert res.status_code == 200
28723014 assert res.json['count'] == 0
28733015
2874 @pytest.mark.usefixtures('ignore_nplusone')
28753016 def test_search_code_attribute_bug(self, workspace, test_client, session):
28763017 query_filter = {"filters":
28773018 [{"name":"code", "op": "eq", "val": "test"}]
28783019 }
28793020 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
28953025 @pytest.mark.skip_sql_dialect('sqlite')
28963026 def test_search_by_hostname_multiple_logic(self, test_client, session):
28973027 workspace = WorkspaceFactory.create()
29063036 {"and": [{"name": "hostnames","op": "eq", "val": "pepe"}]}
29073037 ]}
29083038 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)}')
29103040 assert res.status_code == 200
29113041 assert res.json['count'] == 1
29123042 assert res.json['vulnerabilities'][0]['id'] == vuln.id
29133043
3044 @pytest.mark.skip_sql_dialect('sqlite')
29143045 @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):
29173047 workspace = WorkspaceFactory.create()
29183048 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,
29243050 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)
29273059 session.add(host)
29283060 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')
29423079 @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
29433106 @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.")
29443147 def test_search_by_host_os_with_vulnerability_web_bug(self, test_client, session):
29453148 """
29463149 When searching by the host os an error was raised when a vuln web exists in the ws
29473150 """
29483151 workspace = WorkspaceFactory.create()
29493152 host = HostFactory.create(workspace=workspace, os='Linux')
2950 host.hostnames.append(HostnameFactory.create(name='pepe', workspace=workspace))
29513153 service = ServiceFactory.create(host=host, workspace=workspace)
2952 vuln = VulnerabilityWebFactory.create(
3154 vuln = VulnerabilityFactory.create(
29533155 service=service,
29543156 confirmed=True,
29553157 workspace=workspace,
29703172 {"name": "host__os", "op": "has", "val": "Linux"}
29713173 ]}
29723174 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)}')
29743176 assert res.status_code == 200
29753177 assert res.json['count'] == 1
29763178 assert res.json['vulnerabilities'][0]['id'] == vuln.id
29773179
2978
3180 @pytest.mark.skip_sql_dialect('sqlite')
29793181 @pytest.mark.usefixtures('ignore_nplusone')
2980 @pytest.mark.skip_sql_dialect('sqlite')
29813182 def test_search_by_date_equals(self, test_client, session):
29823183 """
29833184 When searching by the host os an error was raised when a vuln web exists in the ws
30183219 {"name": "create_date", "op": "eq", "val": vuln.create_date.strftime("%Y-%m-%d")}
30193220 ]}
30203221 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)}')
30223223 assert res.status_code == 200
30233224 assert res.json['count'] == 3
30243225
3025 @pytest.mark.usefixtures('ignore_nplusone')
30263226 @pytest.mark.skip_sql_dialect('sqlite')
30273227 def test_search_by_date_equals_invalid_date(self, test_client, session):
30283228 """
30333233 {"name": "create_date", "op": "eq", "val": "30/01/2020"}
30343234 ]}
30353235 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
30403239 @pytest.mark.skip_sql_dialect('sqlite')
30413240 def test_search_hypothesis_test_found_case(self, test_client, session, workspace):
30423241 query_filter = {'filters': [{'name': 'host_id', 'op': 'not_in', 'val': '\U0010a1a7\U00093553\U000eb46a\x1e\x10\r\x18%\U0005ddfa0\x05\U000fdeba\x08\x04絮'}]}
30433242 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)}')
30453244 assert res.status_code == 400
30463245
3047 @pytest.mark.usefixtures('ignore_nplusone')
30483246 @pytest.mark.skip_sql_dialect('sqlite')
30493247 def test_search_hypothesis_test_found_case_2(self, test_client, session, workspace):
30503248 query_filter = {'filters': [{'name': 'host__os', 'op': 'ilike', 'val': -1915870387}]}
30513249 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)}')
30533251 assert res.status_code == 400
30543252
3055 @pytest.mark.usefixtures('ignore_nplusone')
30563253 @pytest.mark.skip_sql_dialect('sqlite')
30573254 @pytest.mark.parametrize('query_filter', [
30583255 {'filters': [{'name': 'workspace_id', 'op': '==', 'val': ''}]},
30613258 ])
30623259 def test_search_hypothesis_test_found_case_3(self, query_filter, test_client, session, workspace):
30633260 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)}')
30653262 assert res.status_code == 400
30663263
3067 @pytest.mark.usefixtures('ignore_nplusone')
30683264 @pytest.mark.skip_sql_dialect('sqlite')
30693265 @pytest.mark.parametrize('query_filter', [
30703266 {'filters': [{'name': 'workspace_id', 'op': 'in', 'val': 56}]},
30733269 ])
30743270 def test_search_hypothesis_test_found_case_4(self, query_filter, test_client, session, workspace):
30753271 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)}')
30773273 assert res.status_code == 400
30783274
3079 @pytest.mark.usefixtures('ignore_nplusone')
30803275 @pytest.mark.skip_sql_dialect('sqlite')
30813276 @pytest.mark.parametrize('query_filter', [
30823277 {'filters': [{'name': 'creator', 'op': 'geq', 'val': 27576}, {'name': 'name', 'op': 'eq', 'val': None}]},
30853280 ])
30863281 def test_search_hypothesis_test_found_case_5(self, query_filter, test_client, session, workspace):
30873282 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)}')
30893284 assert res.status_code == 400
30903285
3091 @pytest.mark.usefixtures('ignore_nplusone')
30923286 @pytest.mark.skip_sql_dialect('sqlite')
30933287 def test_search_hypothesis_test_found_case_6(self, test_client, session, workspace):
30943288 query_filter = {'filters': [{'name': 'resolution', 'op': 'any', 'val': ''}]}
30953289 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
31003293 @pytest.mark.skip_sql_dialect('sqlite')
31013294 def test_search_hypothesis_test_found_case_7(self, test_client, session, workspace):
31023295 query_filter = {'filters': [{'name': 'name', 'op': '>', 'val': '\U0004e755\U0007a789\U000e02d1\U000b3d32\x10\U000ad0e2,\x05\x1a'}, {'name': 'creator', 'op': 'eq', 'val': 21883}]}
31033296 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)}')
31053298 assert res.status_code == 400
31063299
3107 @pytest.mark.usefixtures('ignore_nplusone')
31083300 @pytest.mark.skip_sql_dialect('sqlite')
31093301 @pytest.mark.parametrize('query_filter', [
31103302 {'filters': [{'name': 'id', 'op': '>', 'val': 3}]},
31123304 ])
31133305 def test_search_hypothesis_test_found_case_7_valid(self, query_filter, test_client, session, workspace):
31143306 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
31193310 @pytest.mark.skip_sql_dialect('sqlite')
31203311 def test_search_hypothesis_test_found_case_8(self, test_client, session, workspace):
31213312 query_filter = {'filters': [{'name': 'hostnames', 'op': '==', 'val': ''}]}
31223313 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
31273317 @pytest.mark.skip_sql_dialect('sqlite')
31283318 def test_search_hypothesis_test_found_case_9(self, test_client, session, workspace):
31293319 query_filter = {'filters': [{'name': 'issuetracker', 'op': 'not_equal_to', 'val': '0\x00\U00034383$\x13-\U000375fb\U0007add2\x01\x01\U0010c23a'}]}
31303320
31313321 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)}')
31333323 assert res.status_code == 400
31343324
3135 @pytest.mark.usefixtures('ignore_nplusone')
31363325 @pytest.mark.skip_sql_dialect('sqlite')
31373326 def test_search_hypothesis_test_found_case_10(self, test_client, session, workspace):
31383327 query_filter = {'filters': [{'name': 'impact_integrity', 'op': 'neq', 'val': 0}]}
31393328
31403329 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)}')
31423331 assert res.status_code == 400
31433332
3144 @pytest.mark.usefixtures('ignore_nplusone')
31453333 @pytest.mark.skip_sql_dialect('sqlite')
31463334 def test_search_hypothesis_test_found_case_11(self, test_client, session, workspace):
31473335 query_filter = {'filters': [{'name': 'host_id', 'op': 'like', 'val': '0'}]}
31483336
31493337 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)}')
31513339 assert res.status_code == 400
31523340
3153 @pytest.mark.usefixtures('ignore_nplusone')
31543341 @pytest.mark.skip_sql_dialect('sqlite')
31553342 def test_search_hypothesis_test_found_case_12(self, test_client, session, workspace):
31563343 query_filter = {'filters': [{'name': 'custom_fields', 'op': 'like', 'val': ''}]}
31573344
31583345 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)}')
31603347 assert res.status_code == 400
31613348
3162 @pytest.mark.usefixtures('ignore_nplusone')
31633349 @pytest.mark.skip_sql_dialect('sqlite')
31643350 def test_search_hypothesis_test_found_case_13(self, test_client, session, workspace):
31653351 query_filter = {'filters': [{'name': 'impact_accountability', 'op': 'ilike', 'val': '0'}]}
31663352
31673353 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)}')
31693355 assert res.status_code == 400
31703356
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
31713482
31723483 def test_type_filter(workspace, session,
31733484 vulnerability_factory,
33413652 def send_api_create_request(raw_data):
33423653
33433654 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/',
33453656 data=raw_data)
33463657 assert res.status_code in [201, 400, 409]
33473658
33493660 def send_api_update_request(raw_data):
33503661
33513662 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']}/",
33533664 data=raw_data)
33543665 assert res.status_code in [200, 400, 409, 405]
33553666
33623673 'filters': st.lists(
33633674 st.fixed_dictionaries({
33643675 'name': st.sampled_from(
3365 [col.name for col in VulnerabilityWeb.__table__.columns] + WHITE_LIST
3676 [col.name for col in VulnerabilityWeb.__table__.columns]
33663677 ),
33673678 'op': st.sampled_from([
33683679 'is_null', 'is_not_null',
44 See the file 'doc/LICENSE' for the license information
55
66 '''
7
8 import os
7 from datetime import datetime
98 from io import BytesIO
109 import pytest
1110
2019 from tests.factories import (
2120 VulnerabilityTemplateFactory,
2221 ReferenceTemplateFactory,
23 CustomFieldsSchemaFactory
22 CustomFieldsSchemaFactory,
23 UserFactory,
24 VulnerabilityFactory
2425 )
2526
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 ]
2843
2944 @pytest.mark.usefixtures('logged_user')
3045 class TestListVulnerabilityTemplateView(ReadOnlyAPITests):
7893 vuln_template = VulnerabilityTemplate.query.get(res.json['_id'])
7994 assert vuln_template.references == set()
8095
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
81250 def test_update_vulnerability_template(self, session, test_client):
82251 template = self.factory.create()
83252 session.commit()
84253 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)
86255 assert res.status_code == 200
87256 updated_template = session.query(VulnerabilityTemplate).filter_by(id=template.id).first()
88257 assert updated_template.name == raw_data['name']
115284 self.first_object.reference_template_instances.add(ref)
116285 session.commit()
117286 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)
119288 assert res.status_code == 200
120289 updated_template = session.query(VulnerabilityTemplate).filter_by(id=template.id).first()
121290 assert updated_template.name == raw_data['name']
138307 def test_delete_vuln_template(self, session, test_client):
139308 template = self.factory.create()
140309 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}/')
142311
143312 assert res.status_code == 204
144313 assert vuln_count_previous - 1 == session.query(VulnerabilityTemplate).count()
247416
248417 def test_add_vuln_template_from_csv(self, session, test_client, csrf_token):
249418 expected_created_vuln_template = 1
419 vuln_template_name = "EN-Improper Restriction of Operations within the Bounds of a Memory Buffer (Type: Class)"
250420 file_contents = b"""cwe,name,description,resolution,exploitation,references\n
251421 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
252422 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
268438 res = test_client.post('/v2/vulnerability_template/bulk_create/',
269439 data=data, headers=headers, use_json_data=False)
270440 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
272443
273444 def test_add_unicode_vuln_template_from_csv(self, session, test_client, csrf_token):
274445 expected_created_vuln_template = 1
446 vuln_template_name = "ES-Exposición de información a través del listado de directorios"
275447 file_contents = """cwe,name,description,resolution,exploitation,references
276448 ,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
277449 ",Siempre evitar que se puedan listar directorios de manera externa y sin permisos,high,
284456 res = test_client.post('/v2/vulnerability_template/bulk_create/',
285457 data=data, headers=headers, use_json_data=False)
286458 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
288461
289462 def test_add_vuln_template_only_required_fields(self, session, test_client, csrf_token):
290463 expected_created_vuln_template = 1
464 vuln_template_name = "test"
291465 file_contents = b"""name,exploitation\n
292466 "test",high
293467
300474 res = test_client.post('/v2/vulnerability_template/bulk_create/',
301475 data=data, headers=headers, use_json_data=False)
302476 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
304480
305481 def test_add_vuln_template_missing_required_fields(self, session, test_client, csrf_token):
306482 expected_created_vuln_template = 1
340516 res = test_client.post('/v2/vulnerability_template/bulk_create/',
341517 data=data, headers=headers, use_json_data=False)
342518 assert res.status_code == 200
343 assert res.json['vulns_created'] == 1
519 assert len(res.json['vulns_created']) == 1
344520 inserted_template = session.query(VulnerabilityTemplate).filter_by(name='test').first()
345521 assert inserted_template.resolution == 'resolution'
346522 assert inserted_template.severity == 'high'
347523 assert inserted_template.data == 'technical details'
348524 assert 'cvss' in inserted_template.custom_fields
349525 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
1212 class TestWebsocketAuthEndpoint:
1313
1414 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/')
1716 assert res.status_code == 401
1817
1918 @pytest.mark.usefixtures('logged_user')
2019 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/')
2321 assert res.status_code == 405
2422
2523 @pytest.mark.usefixtures('logged_user')
2624 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/')
2926 assert res.status_code == 200
3027
3128 # A token for that workspace should be generated,
1919 lookup_field = 'name'
2020 view_class = WorkspaceView
2121
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
2283 def test_host_count(self, host_factory, test_client, session):
2384 host_factory.create(workspace=self.first_object)
2485 session.commit()
3798 session,
3899 querystring):
39100 vulns = vulnerability_factory.create_batch(8, workspace=self.first_object,
40 confirmed=False, status='open')
101 confirmed=False, status='open', severity='critical')
41102
42103 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')
47108
48109
49110
54115 assert res.json['stats']['code_vulns'] == 0
55116 assert res.json['stats']['web_vulns'] == 2
56117 assert res.json['stats']['std_vulns'] == 0
118 assert res.json['stats']['critical_vulns'] == 0
119 assert res.json['stats']['info_vulns'] == 2
57120 assert res.json['stats']['total_vulns'] == 2
58121
59122
67130 session,
68131 querystring):
69132 vulns = vulnerability_factory.create_batch(8, workspace=self.first_object,
70 confirmed=False, status='open')
133 confirmed=False, status='open', severity='informational')
71134
72135 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')
77140
78141 session.add_all(vulns)
79142 session.commit()
82145 assert res.json['stats']['code_vulns'] == 0
83146 assert res.json['stats']['web_vulns'] == 0
84147 assert res.json['stats']['std_vulns'] == 3
148 assert res.json['stats']['critical_vulns'] == 0
149 assert res.json['stats']['info_vulns'] == 3
85150 assert res.json['stats']['total_vulns'] == 3
86151
87152 @pytest.mark.parametrize('querystring', [
121186 '?confirmed=1',
122187 '?confirmed=true'
123188 ])
124
125189 def test_vuln_count_confirmed(self,
126190 vulnerability_factory,
127191 test_client,
141205 '?confirmed=0',
142206 '?confirmed=false'
143207 ])
144
145208 def test_vuln_count_confirmed(self,
146209 vulnerability_factory,
147210 test_client,
240303 session.add_all(vulns)
241304 session.commit()
242305 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}/',
244307 data=raw_data)
245308 assert res.status_code == 200
246309 assert res.json['stats']['web_vulns'] == 5
269332 ]
270333 raw_data = {'name': 'something', 'description': 'test',
271334 '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)
273336 assert res.status_code == 200
274337 assert set(res.json['scope']) == set(desired_scope)
275338 assert set(s.name for s in workspace.scope) == set(desired_scope)
282345 workspace.active = False
283346 session.add(workspace)
284347 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}/')
291352 active = res.json.get('active')
292353 assert active == True
293354
298359 workspace.active = True
299360 session.add(workspace)
300361 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}/')
307366 active = res.json.get('active')
308367 assert active == False
309368
6464 def mock_envelope_list(self, monkeypatch):
6565 assert self.view_class is not None, 'You must define view_class ' \
6666 '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):
6869 return {"data": objects}
6970 monkeypatch.setattr(self.view_class, '_envelope_list', _envelope_list)
7071
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)
22 import subprocess
33
44 from configparser import SafeConfigParser, DuplicateSectionError
5 from pathlib import Path
56
67
78 def test_manage_migrate():
1819 database=os.environ['POSTGRES_DB'],
1920 )
2021 faraday_config = SafeConfigParser()
21 config_path = os.path.expanduser('~/.faraday/config/server.ini')
22 config_path = Path('~/.faraday/config/server.ini').expanduser()
2223 faraday_config.read(config_path)
2324 try:
2425 faraday_config.add_section('database')
2526 except DuplicateSectionError:
2627 pass
2728 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:
2930 faraday_config.write(faraday_config_file)
3031
3132 command = ['faraday-manage', 'create-tables']
00 from alembic.script import ScriptDirectory
11 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
92
103 from faraday.server.config import FARADAY_BASE
114
12 class TestMigrations():
135
6 class TestMigrations:
147
158 def test_migrations_check_revision_hashes(self):
169 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 )
1814 script = ScriptDirectory.from_config(config)
1915
2016 alembic_hashes = []
2218 alembic_hashes.append(revision.revision)
2319
2420 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
2723 migrations_hashes.append(filename.split('_')[0])
2824
2925 assert set(alembic_hashes) == set(migrations_hashes)
44
55 '''
66
7 import os
7 from pathlib import Path
88
99 from faraday.server.fields import FaradayUploadedFile
1010
11 CURRENT_PATH = os.path.dirname(os.path.abspath(__file__))
11 TEST_DATA_PATH = Path(__file__).parent / 'data'
1212
1313
1414 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:
1616 field = FaradayUploadedFile(image_data.read())
1717 assert field['content_type'] == 'application/octet-stream'
1818 assert len(field['files']) == 1
2020
2121 def test_image_is_detected_correctly():
2222
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:
2424 field = FaradayUploadedFile(image_data.read())
2525 assert field['content_type'] == 'image/png'
2626 assert 'thumb_id' in field.keys()
2929
3030
3131 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:
3333 field = FaradayUploadedFile(image_data.read())
3434 assert field['content_type'] == 'application/octet-stream'
3535 assert len(field['files']) == 1
36
37
38 # I'm Py3
33 import pytest
44
55 from faraday.searcher.api import Api
6 from faraday.searcher.searcher import Searcher, MailNotification
6 from faraday.searcher.searcher import Searcher
77 from faraday.searcher.sqlapi import SqlApi
88 from faraday.server.models import Service, Host, VulnerabilityWeb
99 from faraday.server.models import Vulnerability, CommandObject
1010 from faraday.server.schemas import WorkerRuleSchema
11 from faraday.utils.smtp import MailNotification
1112 from tests.factories import (
1213 VulnerabilityTemplateFactory,
1314 ServiceFactory,
256257 'id': 'APPLY_TEMPLATE',
257258 'model': 'Vulnerability',
258259 'object': "severity=low",
259 'actions': ["--UPDATE:template={}".format(template_id)]
260 'actions': [f"--UPDATE:template={template_id}"]
260261 }]
261262
262263 searcher.process(rules)
318319 session.commit()
319320
320321 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
325326 )
326327 _api = api(workspace, test_client, session)
327328 searcher = Searcher(_api, mail_notification=mail_notification)
876877 searcher.process(rules_data)
877878 vulns_count = session.query(Vulnerability).filter_by(workspace=workspace).count()
878879 assert vulns_count == 5
879
880
44
55 '''
66
7 import os
8 import sys
97 import unittest
108 import pytest
11
12 sys.path.append(os.path.abspath(os.getcwd()))
139
1410 from faraday.server.models import db
1511
66 import re
77 import random
88 import string
9 import tempfile
10 from pathlib import Path
911 from unittest import mock
1012
1113 from faraday import __version__
1517 )
1618
1719
18 @mock.patch('os.makedirs')
1920 @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
2327
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):
2629 assert copy_default_config_to_local() is None
27 assert makedirs.called
2830 assert copyfile.called
2931
3032 # the second call will re use the file just created.
31 makedirs.reset_mock()
3233 copyfile.reset_mock()
3334 assert copy_default_config_to_local() is None
34 assert not makedirs.called
3535 assert not copyfile.called
3636
3737 VERSION_PATTERN = r"""
33
44 from faraday.server.utils.filters import FilterSchema
55 from faraday.server.utils.filters import FlaskRestlessSchema
6 from faraday.server.models import VulnerabilityWeb
67
78
89 class TestFilters:
238239
239240 res = FilterSchema().load(filters)
240241 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)
1111 session.add(agent)
1212 session.commit()
1313
14 headers = {"Authorization": "Agent {}".format(agent.token)}
14 headers = {"Authorization": f"Agent {agent.token}"}
1515 token = test_client.post('v2/agent_websocket_token/', headers=headers).json['token']
1616 return token
1717
4646
4747 def test_join_agent_message_with_valid_token(self, session, proto, workspace, test_client):
4848 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": [] }}'
5050 assert proto.onMessage(message, False)
5151
5252 def test_leave_agent_happy_path(self, session, proto, workspace, test_client):
5353 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": [] }}'
5555 assert proto.onMessage(message, False)
5656
57 message = '{{"action": "LEAVE_AGENT" }}'.format(token)
57 message = '{"action": "LEAVE_AGENT" }'
5858 assert proto.onMessage(message, False)
5959
6060 def test_agent_status(self, session, proto, workspace, test_client):
6161 token = _join_agent(test_client, session)
6262 agent = Agent.query.one()
6363 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": [] }}'
6565 assert proto.onMessage(message, False)
6666 assert agent.is_online
6767
68 message = '{{"action": "LEAVE_AGENT"}}'.format(token)
68 message = '{"action": "LEAVE_AGENT"}'
6969 assert proto.onMessage(message, False)
7070 assert not agent.is_online
7171