New upstream version 3.19.0
Sophie Brun
2 years ago
0 | # When should run | |
1 | .dev-staging-master: | |
2 | rules: | |
3 | - if: '$CI_COMMIT_REF_NAME =~ /^.*\/(master|staging|dev)$/' | |
4 | when: on_success | |
5 | ||
6 | .on-master-staging: | |
7 | rules: | |
8 | - if: '$CI_COMMIT_REF_NAME =~ /^.*\/(master|staging)$/' | |
9 | when: on_success | |
10 | ||
11 | .on-master: | |
12 | rules: | |
13 | - if: '$CI_COMMIT_REF_NAME =~ /^.*\/(master)$/' | |
14 | when: on_success | |
15 | ||
16 | .on-community-tag: | |
17 | rules: | |
18 | - if: '$CI_COMMIT_TAG =~ /^white-v[0-9.]+$/' | |
19 | when: on_success | |
20 | ||
21 | .pipeline-control-test: | |
22 | rules: | |
23 | - if: $FULL_TEST || $DAILY_TEST | |
24 | when: on_success | |
25 | ||
26 | .be-built: | |
27 | rules: | |
28 | - if: '$CI_COMMIT_TAG || $BUILD_TEST || $FULL_TEST || $DAILY_TEST' | |
29 | when: on_success | |
30 | ||
31 | .be-uploaded: | |
32 | rules: | |
33 | - if: '$BUILD_TEST || $FULL_TEST || $DAILY_TEST' | |
34 | when: on_success | |
35 | ||
36 | ||
37 | # Ignore | |
38 | .ignore-on-tag: | |
39 | rules: | |
40 | - if: '$CI_COMMIT_TAG' | |
41 | when: never | |
42 | ||
43 | .ignore-on-master: | |
44 | rules: | |
45 | - if: '$CI_COMMIT_REF_NAME =~ /^.*\/(master)$/' | |
46 | when: never | |
47 | ||
48 | .ignore-on-build: | |
49 | rules: | |
50 | - if: $BUILD_TEST | |
51 | when: never |
36 | 36 | - "faraday-server_amd64.deb" |
37 | 37 | expire_in: 15 days |
38 | 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 | |
39 | - !reference [.on-master-staging, rules] | |
40 | - !reference [.be-built, rules] | |
43 | 41 | - when: never |
44 | 42 | |
45 | 43 | |
90 | 88 | - "faraday-server_amd64.rpm" |
91 | 89 | expire_in: 15 days |
92 | 90 | 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 | |
91 | - !reference [.on-master-staging, rules] | |
92 | - !reference [.be-built, rules] | |
97 | 93 | - when: never |
98 | 94 | |
99 | 95 | generate_docker_tar_gz: |
114 | 110 | paths: |
115 | 111 | - faraday-server-docker.tar.gz |
116 | 112 | rules: |
117 | - if: '$CI_COMMIT_REF_NAME =~ /^.*\/(master)$/' | |
118 | when: on_success | |
119 | - if: '$CI_COMMIT_TAG || $BUILD_TEST || $FULL_TEST || $DAILY_TEST' | |
120 | when: on_success | |
113 | - !reference [.on-master-staging, rules] | |
114 | - !reference [.be-built, rules] | |
115 | - when: never |
19 | 19 | - "/usr/bin/rsync -aq --exclude 'faraday_copy' --exclude '.cache' . faraday_copy" |
20 | 20 | - "/bin/tar -zcf faraday.tar.gz faraday_copy" |
21 | 21 | rules: |
22 | - if: '$CI_COMMIT_REF_NAME =~ /^.*\/(master)$/' | |
23 | when: on_success | |
24 | - if: '$CI_COMMIT_TAG || $BUILD_TEST || $FULL_TEST || $DAILY_TEST' | |
25 | when: on_success | |
22 | - !reference [.on-master-staging, rules] | |
23 | - !reference [.be-built, rules] | |
26 | 24 | - when: never |
27 | 25 | artifacts: |
28 | 26 | name: 'faraday' |
54 | 52 | - py3.tar |
55 | 53 | expire_in: 15 days # in the future we don't need to expire this. |
56 | 54 | rules: |
57 | - if: '$CI_COMMIT_REF_NAME =~ /^.*\/(master)$/' | |
58 | when: on_success | |
59 | - if: '$CI_COMMIT_TAG || $BUILD_TEST || $FULL_TEST || $DAILY_TEST' | |
60 | when: on_success | |
61 | - when: never | |
55 | - !reference [.on-master-staging, rules] | |
56 | - !reference [.be-built, rules] | |
57 | - when: never |
22 | 22 | - kill $(cat ~faraday/.faraday/faraday-server-port-5985.pid) |
23 | 23 | - jobs |
24 | 24 | rules: |
25 | - if: '$CI_COMMIT_REF_NAME =~ /^.*\/(master)$/' | |
26 | when: on_success | |
27 | - if: '$CI_COMMIT_TAG || $BUILD_TEST || $FULL_TEST || $DAILY_TEST' | |
28 | when: on_success | |
25 | - !reference [.on-master-staging, rules] | |
26 | - !reference [.be-built, rules] | |
29 | 27 | - when: never |
13 | 13 | - docker image tag $CI_REGISTRY_IMAGE:latest $CI_REGISTRY_IMAGE:$VERSION |
14 | 14 | - docker push "$CI_REGISTRY_IMAGE" |
15 | 15 | rules: |
16 | - if: '$CI_COMMIT_REF_NAME =~ /^.*\/(master)$/' | |
17 | when: on_success | |
16 | - !reference [ .on-master, rules ] | |
18 | 17 | needs: # dev won't wait for any previous stage, it will deploy instantly, and |
19 | 18 | # then run tests in docker image (To be done) |
20 | 19 | - job: generate_docker_tar_gz |
34 | 33 | - docker image tag $CI_REGISTRY_IMAGE:latest $CI_REGISTRY_IMAGE:$VERSION |
35 | 34 | - docker push $CI_REGISTRY_IMAGE:$VERSION |
36 | 35 | rules: |
37 | - if: '$CI_COMMIT_TAG =~ /^white-v[0-9.]+$/' | |
38 | when: on_success | |
36 | - !reference [ .on-community-tag, rules ] | |
39 | 37 | dependencies: # prod will wait for any previous stage |
40 | 38 | - generate_docker_tar_gz |
0 | update_github: | |
1 | image: python:3 | |
2 | stage: publish | |
3 | script: | |
4 | - git remote set-url github https://${GH_USER}:${GH_TOKEN}@github.com/infobyte/faraday.git | |
5 | - git push github $CI_COMMIT_REF_NAME:$DESTINY_BRANCH | |
6 | rules: | |
7 | - if: '$CI_COMMIT_REF_NAME == "white/master"' | |
8 | variables: | |
9 | DESTINY_BRANCH: master | |
10 | when: on_success | |
11 | - if: '$CI_COMMIT_REF_NAME == "white/staging"' | |
12 | variables: | |
13 | DESTINY_BRANCH: staging | |
14 | when: on_success | |
15 | tags: | |
16 | - faradaytests | |
17 | ||
18 | ||
19 | tag_on_github: | |
20 | image: python:3 | |
21 | stage: publish | |
22 | script: | |
23 | - git remote set-url github https://${GH_USER}:${GH_TOKEN}@github.com/infobyte/faraday.git | |
24 | - export FARADAY_VERSION=$(eval $IMAGE_TAG) | |
25 | - CHANGELOG/check_pre_tag.py | |
26 | - git push github $CI_COMMIT_TAG:master | |
27 | - git tag v$FARADAY_VERSION -m "$(cat CHANGELOG/$FARADAY_VERSION/community.md)" | |
28 | - git push github v$FARADAY_VERSION | |
29 | - scripts/github_release.py --deb-file ./faraday-server_amd64.deb --rpm-file ./faraday-server_amd64.rpm | |
30 | rules: | |
31 | - !reference [ .on-community-tag, rules ] | |
32 | dependencies: | |
33 | - generate_deb | |
34 | - generate_rpm | |
35 | tags: | |
36 | - faradaytests |
0 | tag_on_github: | |
1 | image: python:3 | |
2 | stage: publish | |
3 | before_script: | |
4 | script: | |
5 | - git remote set-url github https://${GH_USER}:${GH_TOKEN}@github.com/infobyte/faraday.git | |
6 | - export FARADAY_VERSION=$(eval $IMAGE_TAG) | |
7 | - CHANGELOG/check_pre_tag.py | |
8 | - git push github $CI_COMMIT_TAG: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 --deb-file ./faraday-server_amd64.deb --rpm-file ./faraday-server_amd64.rpm | |
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 | 0 | publish_pypi: |
20 | 1 | image: python:3 |
21 | 2 | stage: publish |
26 | 7 | - python setup.py sdist bdist_wheel |
27 | 8 | - twine upload -u $PYPI_USER -p $PYPI_PASS dist/* --verbose |
28 | 9 | rules: |
29 | - if: '$CI_COMMIT_TAG =~ /^white-v[0-9.]+$/' | |
30 | when: on_success | |
10 | - !reference [ .on-community-tag, rules ] |
19 | 19 | - mkdir run_from |
20 | 20 | - 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" |
21 | 21 | rules: |
22 | - if: '$HYPO_TEST || $FULL_TEST || $DAILY_TEST' | |
22 | - if: $HYPO_TEST | |
23 | 23 | when: on_success |
24 | - !reference [.pipeline-control-test, rules] | |
24 | 25 | - when: never |
19 | 19 | - pylint.svg |
20 | 20 | - pylint3.svg |
21 | 21 | rules: |
22 | - if: $BUILD_TEST | |
23 | when: never | |
24 | - if: '$CI_COMMIT_TAG' | |
25 | when: never | |
26 | - if: '$FULL_TEST || $DAILY_TEST' | |
27 | when: on_success | |
22 | - !reference [.ignore-on-build, rules] | |
23 | - !reference [.ignore-on-tag, rules] | |
24 | - !reference [.ignore-on-master, rules] | |
25 | - !reference [.pipeline-control-test, rules] | |
28 | 26 | - when: on_success |
29 | 27 | |
30 | 28 | .postgresql_test_nix_base: |
56 | 54 | artifacts: false |
57 | 55 | # Speed up tests |
58 | 56 | rules: |
59 | - if: $BUILD_TEST | |
60 | when: never | |
61 | - if: '$FULL_TEST || $DAILY_TEST' | |
62 | when: on_success | |
63 | - if: '$CI_COMMIT_TAG' | |
64 | when: never | |
65 | - when: on_success | |
57 | - !reference [.ignore-on-build, rules] | |
58 | - !reference [.ignore-on-tag, rules] | |
59 | - !reference [.pipeline-control-test, rules] | |
60 | - when: on_success | |
66 | 61 | |
67 | 62 | .sqlite_test_nix_base: |
68 | 63 | tags: |
92 | 87 | - job: build_and_push_to_cachix |
93 | 88 | artifacts: false |
94 | 89 | rules: |
95 | - if: $BUILD_TEST | |
96 | when: never | |
97 | - if: '$CI_COMMIT_TAG' | |
98 | when: never | |
99 | - if: '$FULL_TEST || $DAILY_TEST' | |
100 | when: on_success | |
90 | - !reference [.ignore-on-build, rules] | |
91 | - !reference [.ignore-on-tag, rules] | |
92 | - !reference [.pipeline-control-test, rules] | |
101 | 93 | - when: on_success |
102 | 94 | |
103 | 95 | sqlite_test_nix: |
7 | 7 | strategy: depend |
8 | 8 | rules: |
9 | 9 | - if: '$CI_COMMIT_REF_NAME =~ /^.*\/(master)$/' |
10 | variables: | |
11 | DISPATCHER_REF: master | |
12 | when: on_success | |
13 | - if: '$CI_COMMIT_REF_NAME =~ /^.*\/(staging)$/' | |
14 | variables: | |
15 | DISPATCHER_REF: staging | |
10 | 16 | when: on_success |
11 | 17 | - if: '$INTEGRATION || $FULL_TEST || $DAILY_TEST' |
18 | variables: | |
19 | DISPATCHER_REF: staging | |
12 | 20 | when: on_success |
13 | 21 | - when: never |
8 | 8 | - git config --global user.name "Mergerbot" |
9 | 9 | - python3 scripts/merge-conflict-detector.py |
10 | 10 | rules: |
11 | - if: '$CI_COMMIT_REF_NAME =~ /^.*\/(master|dev)$/' | |
12 | when: on_success | |
13 | - if: '$CI_COMMIT_TAG' | |
14 | when: never | |
11 | - !reference [.dev-staging-master, rules] | |
12 | - !reference [.ignore-on-tag, rules] | |
15 | 13 | - when: never |
16 | 14 | |
17 | 15 | sanity_check: |
23 | 21 | - bash scripts/sanity_check_commit.sh |
24 | 22 | - scripts/sanity_check_file.py --mode=ls |
25 | 23 | rules: |
26 | - if: '$CI_COMMIT_REF_NAME =~ /^.*\/(master|dev)$/' | |
27 | when: on_success | |
28 | - if: '$CI_COMMIT_TAG' | |
29 | when: never | |
24 | - !reference [.dev-staging-master, rules] | |
25 | - !reference [.ignore-on-tag, rules] | |
30 | 26 | - when: never |
31 | 27 | |
32 | 28 | migration_sanity_check: |
40 | 36 | - cd faraday |
41 | 37 | - $(alembic branches) |
42 | 38 | rules: |
43 | - if: '$CI_COMMIT_REF_NAME =~ /^.*\/(master|dev)$/' | |
44 | when: always | |
45 | - if: '$CI_COMMIT_TAG' | |
46 | when: never | |
39 | - !reference [.dev-staging-master, rules] | |
40 | - !reference [.ignore-on-tag, rules] | |
47 | 41 | - when: never |
48 | 42 | |
49 | 43 | bandit: |
59 | 53 | - "bandit -r ${CI_PROJECT_DIR}/faraday --format custom --skip B101 --msg-template \ |
60 | 54 | \"{abspath}:{line}: {test_id}[bandit]: {severity}: {msg}\"" |
61 | 55 | rules: |
62 | - if: '$CI_COMMIT_TAG' | |
63 | when: never | |
56 | - !reference [.ignore-on-tag, rules] | |
64 | 57 | - when: on_success |
65 | 58 | |
66 | 59 | build_and_push_to_cachix: |
83 | 76 | - nix-build | cachix push faradaysec |
84 | 77 | - ./scripts/check-closure-size ./result |
85 | 78 | rules: |
86 | - if: '$FULL_TEST || $DAILY_TEST' | |
87 | when: on_success | |
88 | - when: always | |
79 | - when: on_success | |
89 | 80 | |
90 | 81 | flake8: |
91 | 82 | image: python:3 |
94 | 85 | - pip install flake8 |
95 | 86 | - flake8 . |
96 | 87 | rules: |
97 | - if: '$CI_COMMIT_TAG' | |
98 | when: never | |
88 | - !reference [.ignore-on-tag, rules] | |
99 | 89 | - when: on_success |
100 | 90 | |
101 | 91 | no-format-str: |
102 | 92 | image: python:3 |
103 | 93 | stage: pre_testing |
104 | 94 | script: |
105 | - pip install flynt | |
95 | - pip install flynt==0.69 | |
106 | 96 | - flynt -df faraday tests |
107 | 97 | rules: |
108 | - if: '$CI_COMMIT_TAG' | |
109 | when: never | |
98 | - !reference [.ignore-on-tag, rules] | |
99 | - !reference [.ignore-on-master, rules] | |
110 | 100 | - when: on_success |
31 | 31 | - pip freeze |
32 | 32 | allow_failure: true |
33 | 33 | rules: |
34 | #- if: '$FULL_TEST || $DAILY_TEST || $ALPHA_TEST' | |
35 | - if: '$ALPHA_TEST' # FOR NOW, ASKED TO NOT CHARGE CI WORKER | |
34 | # - !reference [ .pipeline-control-test, rules ] | |
35 | # - !reference [ .dev-staging-master, rules ] | |
36 | - if: '$ALPHA_TEST' | |
36 | 37 | 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 | 38 | - when: never |
41 | 39 | |
42 | 40 | |
55 | 53 | - pip freeze |
56 | 54 | allow_failure: true |
57 | 55 | rules: |
58 | #- if: '$FULL_TEST || $DAILY_TEST || $ALPHA_TEST' | |
59 | - if: '$ALPHA_TEST' # FOR NOW, ASKED TO FIX #6474 first | |
56 | # - !reference [ .pipeline-control-test, rules ] | |
57 | # - !reference [ .dev-staging-master, rules ] | |
58 | - if: '$ALPHA_TEST' | |
60 | 59 | 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 | 60 | - when: never |
65 | 61 | |
66 | 62 | unit_test 3.7: |
73 | 69 | |
74 | 70 | unit_test 3.9: |
75 | 71 | extends: .latest_unit_test_base |
76 | image: python:3.9-rc | |
72 | image: python:3.9 | |
73 | ||
74 | unit_test 3.10: | |
75 | extends: .latest_unit_test_base | |
76 | image: python:3.10 | |
77 | 77 | |
78 | 78 | alpha_unit_test 3.7: |
79 | 79 | extends: .alpha_unit_test_base |
83 | 83 | extends: .alpha_unit_test_base |
84 | 84 | image: python:3.8 |
85 | 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 | |
86 | # - !reference [ .pipeline-control-test, rules ] | |
87 | # - !reference [ .dev-staging-master, rules ] | |
88 | - if: '$ALPHA_TEST' | |
89 | when: on_success | |
90 | - when: never | |
92 | 91 | |
93 | 92 | alpha_unit_test 3.9: |
94 | 93 | extends: .alpha_unit_test_base |
95 | image: python:3.9-rc | |
94 | image: python:3.9 | |
95 | ||
96 | ||
97 | alpha_unit_test 3.10: | |
98 | extends: .alpha_unit_test_base | |
99 | image: python:3.10 |
34 | 34 | - *google_storage_deb_rpm_base |
35 | 35 | - "gsutil setmeta -h x-goog-meta-branch:${CI_COMMIT_BRANCH} ${GCLOUD_FILE_PATH}*.*" |
36 | 36 | rules: |
37 | - if: '$CI_COMMIT_REF_NAME =~ /^.*\/(master)$/' | |
38 | when: on_success | |
39 | - if: '$BUILD_TEST || $FULL_TEST || $DAILY_TEST' | |
40 | when: on_success | |
37 | - !reference [ .on-master-staging, rules ] | |
38 | - !reference [ .be-uploaded, rules ] | |
41 | 39 | - when: never |
42 | 40 | needs: |
43 | 41 | - job: generate_deb |
0 | .qa_integration: | |
1 | stage: upload_testing | |
2 | variables: | |
3 | REMOTE_BRANCH: $CI_COMMIT_REF_NAME | |
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 |
30 | 30 | |
31 | 31 | include: |
32 | 32 | - local: .gitlab/ci/fetch-secrets.yaml |
33 | - local: .gitlab/ci/.rules-conditions.yml | |
34 | ||
35 | ||
33 | 36 | - local: .gitlab/ci/testing/.pretesting-gitlab-ci.yml |
34 | 37 | - local: .gitlab/ci/testing/.nix-testing-gitlab-ci.yml |
35 | 38 | - local: .gitlab/ci/testing/.venv-testing-gitlab-ci.yml |
41 | 44 | - local: .gitlab/ci/build-ci/.testing-gitlab-ci.yml |
42 | 45 | |
43 | 46 | - local: .gitlab/ci/upload/.storage-gitlab-ci.yml |
44 | - local: .gitlab/ci/upload/.testing-gitlab-ci.yml | |
45 | 47 | |
46 | 48 | - local: .gitlab/ci/deploy/deploy-gitlab-ci.yml |
47 | 49 | |
48 | 50 | - local: .gitlab/ci/publish/.set-tag-gitlab-ci.yml |
51 | - local: .gitlab/ci/publish/.mirror-to-github-gitlab-ci.yml | |
49 | 52 | - local: .gitlab/ci/publish/.docker-publish-gitlab-ci.yml |
50 | 53 | |
51 | 54 | - template: Security/Secret-Detection.gitlab-ci.yml |
10 | 10 | exclude: '^faraday/server/www/' |
11 | 11 | - id: check-yaml |
12 | 12 | exclude: '^faraday/server/www/' |
13 | args: [ --unsafe ] | |
13 | 14 | - id: debug-statements |
14 | 15 | exclude: '^faraday/server/www/' |
15 | 16 | - repo: https://gitlab.com/pycqa/flake8 |
47 | 48 | pass_filenames: false |
48 | 49 | args: [--mode=ls, --local] |
49 | 50 | stages: [push] |
51 | - repo: https://github.com/asottile/pyupgrade | |
52 | rev: v2.29.0 | |
53 | hooks: | |
54 | - id: pyupgrade | |
55 | args: [ --py3-plus , --py36-plus] |
0 | * ADD v3 bulks endpoints DELETE and EDIT (PATCH) | |
1 | * Add logs of loggin, logout and log error to main log | |
2 | * Fix bug in bulk update for m2m fields | |
3 | * ADD clear settings command | |
4 | * Add open medium, high and critical vulns histogram | |
5 | * Fix integrity constraint error on cve update | |
6 | * FIX static content for react | |
7 | * Add cvss within vulnerability model | |
8 | * add check to see if workspace name is longer than 250 characters. In that case raises an error | |
9 | * change concat in urlstrings for join or urljoin | |
10 | * Add cve to csv export |
0 | Dec 27th, 2021 |
1 | 1 | ===================================== |
2 | 2 | |
3 | 3 | |
4 | 3.19.0 [Dec 27th, 2021]: | |
5 | --- | |
6 | * ADD v3 bulks endpoints DELETE and EDIT (PATCH) | |
7 | * Add logs of loggin, logout and log error to main log | |
8 | * Fix bug in bulk update for m2m fields | |
9 | * ADD clear settings command | |
10 | * Add open medium, high and critical vulns histogram | |
11 | * Fix integrity constraint error on cve update | |
12 | * FIX static content for react | |
13 | * Add cvss within vulnerability model | |
14 | * add check to see if workspace name is longer than 250 characters. In that case raises an error | |
15 | * change concat in urlstrings for join or urljoin | |
16 | * Add cve to csv export | |
17 | ||
4 | 18 | 3.18.1 [Nov 5th, 2021]: |
5 | 19 | --- |
6 | 20 | Fix CVE issue |
9 | 23 | --- |
10 | 24 | * Remove attachments in vulns filter endpoint |
11 | 25 | * Add open and confirmed vulns in workspace stats |
12 | * Add migration disabling several notifications. | |
13 | 26 | * Add user id to session API endpoint |
14 | 27 | * Add cve to vulnerability model |
15 | 28 | * Change funcs to views |
16 | 29 | * FIX report import |
17 | 30 | * Add `last_run_agent_date` field to workspace endpoint |
18 | 31 | * Fix cve parsing in `vulnerability create` and `bulk create` |
19 | * ADD check if postgres db is running during server start | |
20 | 32 | * Fix order_by in filters api |
21 | 33 | * Fix 500 status code with invalid executor arguments |
22 | 34 |
0 | # -*- coding: utf-8 -*- | |
1 | 0 | # |
2 | 1 | # Faraday documentation build configuration file, created by |
3 | 2 | # sphinx-quickstart on Tue Oct 31 19:10:26 2017. |
45 | 44 | master_doc = 'index' |
46 | 45 | |
47 | 46 | # General information about the project. |
48 | project = u'Faraday' | |
49 | copyright = u'2017, Daniel Foguelman, Esteban Guillardoy, Ezequiel Tavella, Facundo de Guzmán, Federico Kirschbaum, Francisco Amato, Franco Linares, German Riera, Joaquín López Pereyra, Leonardo Lazzaro, Martín Rocha, Matias Ariel Ré Medina, Matias Lang, Micaela Ranea Sánchez, Sebastian Kulesz' | |
50 | author = u'Daniel Foguelman, Esteban Guillardoy, Ezequiel Tavella, Facundo de Guzmán, Federico Kirschbaum, Francisco Amato, Franco Linares, German Riera, Joaquín López Pereyra, Leonardo Lazzaro, Martín Rocha, Matias Ariel Ré Medina, Matias Lang, Micaela Ranea Sánchez, Sebastian Kulesz' | |
47 | project = 'Faraday' | |
48 | copyright = '2017, Daniel Foguelman, Esteban Guillardoy, Ezequiel Tavella, Facundo de Guzmán, Federico Kirschbaum, Francisco Amato, Franco Linares, German Riera, Joaquín López Pereyra, Leonardo Lazzaro, Martín Rocha, Matias Ariel Ré Medina, Matias Lang, Micaela Ranea Sánchez, Sebastian Kulesz' | |
49 | author = 'Daniel Foguelman, Esteban Guillardoy, Ezequiel Tavella, Facundo de Guzmán, Federico Kirschbaum, Francisco Amato, Franco Linares, German Riera, Joaquín López Pereyra, Leonardo Lazzaro, Martín Rocha, Matias Ariel Ré Medina, Matias Lang, Micaela Ranea Sánchez, Sebastian Kulesz' | |
51 | 50 | |
52 | 51 | # The version info for the project you're documenting, acts as replacement for |
53 | 52 | # |version| and |release|, also used in various other places throughout the |
54 | 53 | # built documents. |
55 | 54 | # |
56 | 55 | # The short X.Y version. |
57 | version = u'3.0.0' | |
56 | version = '3.0.0' | |
58 | 57 | # The full version, including alpha/beta/rc tags. |
59 | release = u'3.0.0' | |
58 | release = '3.0.0' | |
60 | 59 | |
61 | 60 | # The language for content autogenerated by Sphinx. Refer to documentation |
62 | 61 | # for a list of supported languages. |
141 | 140 | # (source start file, target name, title, |
142 | 141 | # author, documentclass [howto, manual, or own class]). |
143 | 142 | latex_documents = [ |
144 | (master_doc, 'Faraday.tex', u'Faraday Documentation', | |
145 | u'Daniel Foguelman, Esteban Guillardoy, Ezequiel Tavella, Facundo de Guzmán, Federico Kirschbaum, Francisco Amato, Franco Linares, German Riera, Joaquín López Pereyra, Leonardo Lazzaro, Martín Rocha, Matias Ariel Ré Medina, Matias Lang, Micaela Ranea Sánchez, Sebastian Kulesz', 'manual'), | |
143 | (master_doc, 'Faraday.tex', 'Faraday Documentation', | |
144 | 'Daniel Foguelman, Esteban Guillardoy, Ezequiel Tavella, Facundo de Guzmán, Federico Kirschbaum, Francisco Amato, Franco Linares, German Riera, Joaquín López Pereyra, Leonardo Lazzaro, Martín Rocha, Matias Ariel Ré Medina, Matias Lang, Micaela Ranea Sánchez, Sebastian Kulesz', 'manual'), | |
146 | 145 | ] |
147 | 146 | |
148 | 147 | |
151 | 150 | # One entry per manual page. List of tuples |
152 | 151 | # (source start file, name, description, authors, manual section). |
153 | 152 | man_pages = [ |
154 | (master_doc, 'faraday', u'Faraday Documentation', | |
153 | (master_doc, 'faraday', 'Faraday Documentation', | |
155 | 154 | [author], 1) |
156 | 155 | ] |
157 | 156 | |
162 | 161 | # (source start file, target name, title, author, |
163 | 162 | # dir menu entry, description, category) |
164 | 163 | texinfo_documents = [ |
165 | (master_doc, 'Faraday', u'Faraday Documentation', | |
164 | (master_doc, 'Faraday', 'Faraday Documentation', | |
166 | 165 | author, 'Faraday', 'One line description of project.', |
167 | 166 | 'Miscellaneous'), |
168 | 167 | ] |
169 | ||
170 | ||
171 | ||
172 | # I'm Py3⏎ |
1 | 1 | # Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/) |
2 | 2 | # See the file 'doc/LICENSE' for the license information |
3 | 3 | |
4 | __version__ = '3.18.1' | |
4 | __version__ = '3.19.0' | |
5 | 5 | __license_version__ = __version__ |
143 | 143 | except OperationalError: |
144 | 144 | logger = logging.getLogger(__name__) |
145 | 145 | logger.error( |
146 | ('Could not connect to PostgreSQL. Please check: ' | |
147 | 'if database is running or if the configuration settings are correct.') | |
146 | 'Could not connect to PostgreSQL. Please check: ' | |
147 | 'if database is running or if the configuration settings are correct.' | |
148 | 148 | ) |
149 | 149 | sys.exit(1) |
150 | 150 | |
199 | 199 | if not conn_string: |
200 | 200 | logger = logging.getLogger(__name__) |
201 | 201 | logger.error( |
202 | ('No database configuration found. Please check: ' | |
202 | 'No database configuration found. Please check: ' | |
203 | 203 | 'if the database is running or if the configuration settings are correct. ' |
204 | 'For first time installations execute: faraday-manage initdb') | |
204 | 'For first time installations execute: faraday-manage initdb' | |
205 | 205 | ) |
206 | 206 | sys.exit(1) |
207 | 207 | InitDB()._create_tables(conn_string) |
280 | 280 | |
281 | 281 | |
282 | 282 | @click.command(help="Manage settings") |
283 | @click.option('-a', '--action', type=click.Choice(['show', 'update', 'list'], case_sensitive=False), | |
283 | @click.option('-a', '--action', type=click.Choice(['show', 'update', 'list', 'clear'], case_sensitive=False), | |
284 | 284 | default='list', show_default=True, help="Action") |
285 | 285 | @click.option('--data', type=str, required=False, callback=manage_settings.settings_format_validation, |
286 | 286 | help="Settings config in json") |
308 | 308 | |
309 | 309 | if __name__ == '__main__': |
310 | 310 | cli() |
311 | ||
312 | # I'm Py3 |
0 | ||
1 | 0 | import logging |
2 | 1 | import faraday.server.config |
3 | 2 | from faraday.server.web import get_app |
78 | 77 | run_migrations_offline() |
79 | 78 | else: |
80 | 79 | run_migrations_online() |
81 | # I'm Py3 |
183 | 183 | op.drop_table('rule_action') |
184 | 184 | op.drop_table('action') |
185 | 185 | op.drop_table('rule') |
186 | ||
187 | ||
188 | # I'm Py3 |
66 | 66 | op.drop_table('notification') |
67 | 67 | # op.drop_constraint(None, 'notification_user_id_fkey', type_='foreignkey') |
68 | 68 | # op.drop_constraint(None, 'notification_workspace_id_fkey', type_='foreignkey') |
69 | # I'm Py3 |
0 | """Severities histogram model | |
1 | ||
2 | Revision ID: 15d70093d262 | |
3 | Revises: d8f0b32a5c0e | |
4 | Create Date: 2021-11-08 13:57:28.099487+00:00 | |
5 | ||
6 | """ | |
7 | from alembic import op | |
8 | import sqlalchemy as sa | |
9 | ||
10 | ||
11 | # revision identifiers, used by Alembic. | |
12 | from sqlalchemy import func, case | |
13 | ||
14 | from faraday.server.models import VulnerabilityGeneric, SeveritiesHistogram, Workspace | |
15 | ||
16 | revision = '15d70093d262' | |
17 | down_revision = 'd8f0b32a5c0e' | |
18 | branch_labels = None | |
19 | depends_on = None | |
20 | ||
21 | ||
22 | def upgrade(): | |
23 | # ### commands auto generated by Alembic - please adjust! ### | |
24 | op.create_table('severities_histogram', | |
25 | sa.Column('id', sa.Integer(), nullable=False), | |
26 | sa.Column('workspace_id', sa.Integer(), nullable=False), | |
27 | sa.Column('date', sa.Date(), nullable=False), | |
28 | sa.Column('medium', sa.Integer(), nullable=False), | |
29 | sa.Column('high', sa.Integer(), nullable=False), | |
30 | sa.Column('critical', sa.Integer(), nullable=False), | |
31 | sa.Column('confirmed', sa.Integer(), nullable=False), | |
32 | sa.ForeignKeyConstraint(['workspace_id'], ['workspace.id'], ), | |
33 | sa.PrimaryKeyConstraint('id') | |
34 | ) | |
35 | op.create_index(op.f('ix_severities_histogram_workspace_id'), 'severities_histogram', ['workspace_id'], unique=False) | |
36 | # ### end Alembic commands ### | |
37 | ||
38 | # Init histogram | |
39 | bind = op.get_bind() | |
40 | session = sa.orm.Session(bind=bind) | |
41 | workspaces = session.query(Workspace).all() | |
42 | for workspace in workspaces: | |
43 | vulnerabilities = session.query(VulnerabilityGeneric) \ | |
44 | .with_entities(func.date_trunc('day', VulnerabilityGeneric.create_date), | |
45 | VulnerabilityGeneric.severity, | |
46 | func.count(VulnerabilityGeneric.severity), | |
47 | func.sum(case([(VulnerabilityGeneric.confirmed, 1)], else_=0)))\ | |
48 | .filter(VulnerabilityGeneric.workspace_id == workspace.id, | |
49 | VulnerabilityGeneric.status.notin_(['closed', 'risk-accepted']), | |
50 | VulnerabilityGeneric.severity.in_(['medium', 'high', 'critical']))\ | |
51 | .group_by(func.date_trunc('day', VulnerabilityGeneric.create_date), VulnerabilityGeneric.severity).all() | |
52 | for histogram_date, severity_type, severity_count, confirmed_count in vulnerabilities: | |
53 | severity_histogram = session.query(SeveritiesHistogram)\ | |
54 | .filter(SeveritiesHistogram.date == histogram_date, | |
55 | SeveritiesHistogram.workspace_id == workspace.id).first() | |
56 | if severity_histogram is None: | |
57 | severity_histogram = SeveritiesHistogram(date=histogram_date, workspace=workspace, medium=0, high=0, critical=0, confirmed=0) | |
58 | session.add(severity_histogram) | |
59 | session.commit() | |
60 | if severity_type == 'medium': | |
61 | severity_histogram.medium = severity_count | |
62 | if severity_type == 'high': | |
63 | severity_histogram.high = severity_count | |
64 | if severity_type == 'critical': | |
65 | severity_histogram.critical = severity_count | |
66 | severity_histogram.confirmed += confirmed_count | |
67 | session.commit() | |
68 | ||
69 | ||
70 | def downgrade(): | |
71 | # ### commands auto generated by Alembic - please adjust! ### | |
72 | op.drop_index(op.f('ix_severities_histogram_workspace_id'), table_name='severities_histogram') | |
73 | op.drop_table('severities_histogram') | |
74 | # ### end Alembic commands ### |
+0
-2
72 | 72 | 'json_data': json.dumps(custom_fields), |
73 | 73 | 'vuln_id': vuln_id |
74 | 74 | }) |
75 | ||
76 | # I'm Py3 |
44 | 44 | |
45 | 45 | def downgrade(): |
46 | 46 | # Convert 'asset_owner' status into 'client' |
47 | op.execute(cmd.update().where(cmd.c.import_source == u'agent') | |
47 | op.execute(cmd.update().where(cmd.c.import_source == 'agent') | |
48 | 48 | .values(import_source=None)) |
49 | 49 | # Create a temporary "_role" type, convert and drop the "new" type |
50 | 50 | tmp_type.create(op.get_bind(), checkfirst=False) |
22 | 22 | def downgrade(): |
23 | 23 | conn = op.get_bind() |
24 | 24 | conn.execute('ALTER TABLE custom_fields_schema DROP CONSTRAINT custom_fields_schema_field_name_key;') |
25 | # I'm Py3 |
0 | """empty message | |
1 | ||
2 | Revision ID: 38bb251889e6 | |
3 | Revises: 15d70093d262 | |
4 | Create Date: 2021-07-30 02:12:00.706416+00:00 | |
5 | ||
6 | """ | |
7 | from alembic import op | |
8 | ||
9 | # revision identifiers, used by Alembic. | |
10 | revision = '38bb251889e6' | |
11 | down_revision = '15d70093d262' | |
12 | branch_labels = None | |
13 | depends_on = None | |
14 | ||
15 | ||
16 | def upgrade(): | |
17 | ||
18 | # Agent table | |
19 | op.drop_constraint('executor_agent_id_fkey', 'executor') | |
20 | op.create_foreign_key( | |
21 | 'executor_agent_id_fkey', | |
22 | 'executor', | |
23 | 'agent', ['agent_id'], ['id'], | |
24 | ondelete='CASCADE' | |
25 | ) | |
26 | op.drop_constraint('association_workspace_and_agents_table_agent_id_fkey', | |
27 | 'association_workspace_and_agents_table') | |
28 | op.create_foreign_key( | |
29 | 'association_workspace_and_agents_table_agent_id_fkey', | |
30 | 'association_workspace_and_agents_table', | |
31 | 'agent', ['agent_id'], ['id'], | |
32 | ondelete='CASCADE' | |
33 | ) | |
34 | ||
35 | # Vulnerability_template table | |
36 | op.drop_constraint('knowledge_base_vulnerability_template_id_fkey', | |
37 | 'knowledge_base') | |
38 | op.create_foreign_key( | |
39 | 'knowledge_base_vulnerability_template_id_fkey', 'knowledge_base', | |
40 | 'vulnerability_template', ['vulnerability_template_id'], ['id'], | |
41 | ondelete='CASCADE' | |
42 | ) | |
43 | ||
44 | # Comment table | |
45 | op.drop_constraint('comment_reply_to_id_fkey', | |
46 | 'comment') | |
47 | op.create_foreign_key( | |
48 | 'comment_reply_to_id_fkey', 'comment', | |
49 | 'comment', ['reply_to_id'], ['id'], | |
50 | ondelete='SET NULL' | |
51 | ) | |
52 | ||
53 | # Service table | |
54 | op.drop_constraint('credential_service_id_fkey', 'credential') | |
55 | op.create_foreign_key( | |
56 | 'credential_service_id_fkey', 'credential', | |
57 | 'service', ['service_id'], ['id'], | |
58 | ondelete='CASCADE' | |
59 | ) | |
60 | ||
61 | # Command table | |
62 | op.drop_constraint('command_object_command_id_fkey', 'command_object') | |
63 | op.create_foreign_key( | |
64 | 'command_object_command_id_fkey', 'command_object', | |
65 | 'command', ['command_id'], ['id'], | |
66 | ondelete='SET NULL' | |
67 | ) | |
68 | op.drop_constraint('agent_execution_command_id_fkey', 'agent_execution') | |
69 | op.create_foreign_key( | |
70 | 'agent_execution_command_id_fkey', 'agent_execution', | |
71 | 'command', ['command_id'], ['id'], | |
72 | ondelete='SET NULL' | |
73 | ) | |
74 | op.drop_constraint('rule_execution_command_id_fkey', 'rule_execution') | |
75 | op.create_foreign_key( | |
76 | 'rule_execution_command_id_fkey', 'rule_execution', | |
77 | 'command', ['command_id'], ['id'], | |
78 | ondelete='CASCADE' | |
79 | ) | |
80 | ||
81 | # Host table | |
82 | op.drop_constraint('hostname_host_id_fkey', 'hostname') | |
83 | op.create_foreign_key( | |
84 | 'hostname_host_id_fkey', 'hostname', | |
85 | 'host', ['host_id'], ['id'], | |
86 | ondelete='CASCADE' | |
87 | ) | |
88 | op.drop_constraint('service_host_id_fkey', 'service') | |
89 | op.create_foreign_key( | |
90 | 'service_host_id_fkey', 'service', | |
91 | 'host', ['host_id'], ['id'], | |
92 | ondelete='CASCADE' | |
93 | ) | |
94 | op.drop_constraint('vulnerability_host_id_fkey', 'vulnerability') | |
95 | op.create_foreign_key( | |
96 | 'vulnerability_host_id_fkey', 'vulnerability', | |
97 | 'host', ['host_id'], ['id'], | |
98 | ondelete='CASCADE' | |
99 | ) | |
100 | op.drop_constraint('credential_host_id_fkey', 'credential') | |
101 | op.create_foreign_key( | |
102 | 'credential_host_id_fkey', 'credential', | |
103 | 'host', ['host_id'], ['id'], | |
104 | ondelete='CASCADE' | |
105 | ) | |
106 | ||
107 | # Vulnerability Table | |
108 | op.drop_constraint('vulnerability_vulnerability_duplicate_id_fkey', 'vulnerability') | |
109 | op.create_foreign_key( | |
110 | 'vulnerability_vulnerability_duplicate_id_fkey', 'vulnerability', | |
111 | 'vulnerability', ['vulnerability_duplicate_id'], ['id'], | |
112 | ondelete='SET NULL' | |
113 | ) | |
114 | ||
115 | # VulnerabilityTemplate Table | |
116 | op.drop_constraint('vulnerability_vulnerability_template_id_fkey', 'vulnerability') | |
117 | op.create_foreign_key( | |
118 | 'vulnerability_vulnerability_template_id_fkey', 'vulnerability', | |
119 | 'vulnerability_template', ['vulnerability_template_id'], ['id'], | |
120 | ondelete='SET NULL' | |
121 | ) | |
122 | ||
123 | # SourceCode Table | |
124 | op.drop_constraint('vulnerability_source_code_id_fkey', 'vulnerability') | |
125 | op.create_foreign_key( | |
126 | 'vulnerability_source_code_id_fkey', 'vulnerability', | |
127 | 'source_code', ['source_code_id'], ['id'], | |
128 | ondelete='CASCADE' | |
129 | ) | |
130 | ||
131 | ||
132 | def downgrade(): | |
133 | ||
134 | # Agent table | |
135 | op.drop_constraint('executor_agent_id_fkey', | |
136 | 'executor') | |
137 | op.create_foreign_key( | |
138 | 'executor_agent_id_fkey', | |
139 | 'executor', | |
140 | 'agent', ['agent_id'], ['id'] | |
141 | ) | |
142 | op.drop_constraint('association_workspace_and_agents_table_agent_id_fkey', | |
143 | 'association_workspace_and_agents_table') | |
144 | op.create_foreign_key( | |
145 | 'association_workspace_and_agents_table_agent_id_fkey', | |
146 | 'association_workspace_and_agents_table', | |
147 | 'agent', ['agent_id'], ['id'] | |
148 | ) | |
149 | ||
150 | # Vulnerability_template table | |
151 | op.drop_constraint('knowledge_base_vulnerability_template_id_fkey', | |
152 | 'knowledge_base') | |
153 | op.create_foreign_key( | |
154 | 'knowledge_base_vulnerability_template_id_fkey', 'knowledge_base', | |
155 | 'vulnerability_template', ['vulnerability_template_id'], ['id'] | |
156 | ) | |
157 | ||
158 | # Comment table | |
159 | op.drop_constraint('comment_reply_to_id_fkey', | |
160 | 'comment') | |
161 | op.create_foreign_key( | |
162 | 'comment_reply_to_id_fkey', 'comment', | |
163 | 'comment', ['reply_to_id'], ['id'] | |
164 | ) | |
165 | ||
166 | # Service table | |
167 | op.drop_constraint('credential_service_id_fkey', 'credential') | |
168 | op.create_foreign_key( | |
169 | 'credential_service_id_fkey', 'credential', | |
170 | 'service', ['service_id'], ['id'] | |
171 | ) | |
172 | ||
173 | # Command table | |
174 | op.drop_constraint('command_object_command_id_fkey', 'command_object') | |
175 | op.create_foreign_key( | |
176 | 'command_object_command_id_fkey', 'command_object', | |
177 | 'command', ['command_id'], ['id'] | |
178 | ) | |
179 | op.drop_constraint('agent_execution_command_id_fkey', 'agent_execution') | |
180 | op.create_foreign_key( | |
181 | 'agent_execution_command_id_fkey', 'agent_execution', | |
182 | 'command', ['command_id'], ['id'] | |
183 | ) | |
184 | op.drop_constraint('rule_execution_command_id_fkey', 'rule_execution') | |
185 | op.create_foreign_key( | |
186 | 'rule_execution_command_id_fkey', 'rule_execution', | |
187 | 'command', ['command_id'], ['id'] | |
188 | ) | |
189 | ||
190 | # Host table | |
191 | op.drop_constraint('credential_host_id_fkey', 'credential') | |
192 | op.create_foreign_key( | |
193 | 'credential_host_id_fkey', 'credential', | |
194 | 'host', ['host_id'], ['id'] | |
195 | ) | |
196 | op.drop_constraint('hostname_host_id_fkey', 'hostname') | |
197 | op.create_foreign_key( | |
198 | 'hostname_host_id_fkey', 'hostname', | |
199 | 'host', ['host_id'], ['id'] | |
200 | ) | |
201 | op.drop_constraint('service_host_id_fkey', 'service') | |
202 | op.create_foreign_key( | |
203 | 'service_host_id_fkey', 'service', | |
204 | 'host', ['host_id'], ['id'] | |
205 | ) | |
206 | op.drop_constraint('vulnerability_host_id_fkey', 'vulnerability') | |
207 | op.create_foreign_key( | |
208 | 'vulnerability_host_id_fkey', 'vulnerability', | |
209 | 'host', ['host_id'], ['id'] | |
210 | ) | |
211 | ||
212 | op.drop_constraint('vulnerability_vulnerability_duplicate_id_fkey', 'vulnerability') | |
213 | op.create_foreign_key( | |
214 | 'vulnerability_vulnerability_duplicate_id_fkey', 'vulnerability', | |
215 | 'vulnerability', ['vulnerability_duplicate_id'], ['id'] | |
216 | ) | |
217 | ||
218 | # VulnerabilityTemplate Table | |
219 | op.drop_constraint('vulnerability_vulnerability_template_id_fkey', 'vulnerability') | |
220 | op.create_foreign_key( | |
221 | 'vulnerability_vulnerability_template_id_fkey', 'vulnerability', | |
222 | 'vulnerability_template', ['vulnerability_template_id'], ['id'] | |
223 | ) | |
224 | ||
225 | # SourceCode Table | |
226 | op.drop_constraint('vulnerability_source_code_id_fkey', 'vulnerability') | |
227 | op.create_foreign_key( | |
228 | 'vulnerability_source_code_id_fkey', 'vulnerability', | |
229 | 'source_code', ['source_code_id'], ['id'] | |
230 | ) |
+0
-1
21 | 21 | |
22 | 22 | def downgrade(): |
23 | 23 | op.drop_column('executive_report', 'markdown') |
24 | # I'm Py3 |
25 | 25 | conn = op.get_bind() |
26 | 26 | conn.execute('ALTER TABLE vulnerability DROP COLUMN external_id') |
27 | 27 | conn.execute('ALTER TABLE vulnerability_template DROP COLUMN external_id') |
28 | ||
29 | ||
30 | # I'm Py3 |
+46
-0
0 | """PolicyViolationVulnerabilityAssociation FK ondelete action | |
1 | ||
2 | Revision ID: 5cf9660bba80 | |
3 | Revises: 7dea3a6caf51 | |
4 | Create Date: 2021-12-01 16:09:40.318964+00:00 | |
5 | ||
6 | """ | |
7 | from alembic import op | |
8 | import sqlalchemy as sa | |
9 | ||
10 | ||
11 | # revision identifiers, used by Alembic. | |
12 | revision = '5cf9660bba80' | |
13 | down_revision = '7dea3a6caf51' | |
14 | branch_labels = None | |
15 | depends_on = None | |
16 | ||
17 | ||
18 | def upgrade(): | |
19 | # ### commands auto generated by Alembic - please adjust! ### | |
20 | op.drop_constraint('policy_violation_vulnerability_associatio_vulnerability_id_fkey', 'policy_violation_vulnerability_association', type_='foreignkey') | |
21 | op.create_foreign_key(None, 'policy_violation_vulnerability_association', 'vulnerability', ['vulnerability_id'], ['id'], ondelete='CASCADE') | |
22 | op.alter_column('vulnerability', 'risk', | |
23 | existing_type=sa.REAL(), | |
24 | type_=sa.Float(precision=3, asdecimal=1), | |
25 | existing_nullable=True) | |
26 | op.alter_column('vulnerability_template', 'risk', | |
27 | existing_type=sa.REAL(), | |
28 | type_=sa.Float(precision=3, asdecimal=1), | |
29 | existing_nullable=True) | |
30 | # ### end Alembic commands ### | |
31 | ||
32 | ||
33 | def downgrade(): | |
34 | # ### commands auto generated by Alembic - please adjust! ### | |
35 | op.alter_column('vulnerability_template', 'risk', | |
36 | existing_type=sa.Float(precision=3, asdecimal=1), | |
37 | type_=sa.REAL(), | |
38 | existing_nullable=True) | |
39 | op.alter_column('vulnerability', 'risk', | |
40 | existing_type=sa.Float(precision=3, asdecimal=1), | |
41 | type_=sa.REAL(), | |
42 | existing_nullable=True) | |
43 | op.drop_constraint('policy_violation_vulnerability_associatio_vulnerability_id_fkey', 'policy_violation_vulnerability_association', type_='foreignkey') | |
44 | op.create_foreign_key(None, 'policy_violation_vulnerability_association', 'vulnerability', ['vulnerability_id'], ['id']) | |
45 | # ### end Alembic commands ### |
0 | """cascade in vuls relation | |
1 | ||
2 | Revision ID: 7dea3a6caf51 | |
3 | Revises: 38bb251889e6 | |
4 | Create Date: 2021-11-10 21:23:24.837776+00:00 | |
5 | ||
6 | """ | |
7 | from alembic import op | |
8 | import sqlalchemy as sa | |
9 | ||
10 | ||
11 | # revision identifiers, used by Alembic. | |
12 | revision = '7dea3a6caf51' | |
13 | down_revision = '38bb251889e6' | |
14 | branch_labels = None | |
15 | depends_on = None | |
16 | ||
17 | ||
18 | def upgrade(): | |
19 | # ### commands auto generated by Alembic - please adjust! ### | |
20 | op.drop_constraint('command_workspace_id_fkey', 'command', type_='foreignkey') | |
21 | op.create_foreign_key(None, 'command', 'workspace', ['workspace_id'], ['id'], ondelete='CASCADE') | |
22 | op.drop_constraint('knowledge_base_vulnerability_template_id_fkey', 'knowledge_base', type_='foreignkey') | |
23 | op.create_foreign_key(None, 'knowledge_base', 'vulnerability_template', ['vulnerability_template_id'], ['id']) | |
24 | op.drop_constraint('reference_vulnerability_association_vulnerability_id_fkey', 'reference_vulnerability_association', type_='foreignkey') | |
25 | op.create_foreign_key(None, 'reference_vulnerability_association', 'vulnerability', ['vulnerability_id'], ['id'], ondelete='CASCADE') | |
26 | op.alter_column('vulnerability', 'risk', | |
27 | existing_type=sa.REAL(), | |
28 | type_=sa.Float(precision=3, asdecimal=1), | |
29 | existing_nullable=True) | |
30 | op.drop_constraint('vulnerability_service_id_fkey', 'vulnerability', type_='foreignkey') | |
31 | op.create_foreign_key(None, 'vulnerability', 'service', ['service_id'], ['id'], ondelete='CASCADE') | |
32 | op.alter_column('vulnerability_template', 'risk', | |
33 | existing_type=sa.REAL(), | |
34 | type_=sa.Float(precision=3, asdecimal=1), | |
35 | existing_nullable=True) | |
36 | # ### end Alembic commands ### | |
37 | ||
38 | ||
39 | def downgrade(): | |
40 | # ### commands auto generated by Alembic - please adjust! ### | |
41 | op.alter_column('vulnerability_template', 'risk', | |
42 | existing_type=sa.Float(precision=3, asdecimal=1), | |
43 | type_=sa.REAL(), | |
44 | existing_nullable=True) | |
45 | op.drop_constraint('vulnerability_service_id_fkey', 'vulnerability', type_='foreignkey') | |
46 | op.create_foreign_key(None, 'vulnerability', 'service', ['service_id'], ['id']) | |
47 | op.alter_column('vulnerability', 'risk', | |
48 | existing_type=sa.Float(precision=3, asdecimal=1), | |
49 | type_=sa.REAL(), | |
50 | existing_nullable=True) | |
51 | op.drop_constraint('reference_vulnerability_association_vulnerability_id_fkey', 'reference_vulnerability_association', type_='foreignkey') | |
52 | op.create_foreign_key(None, 'reference_vulnerability_association', 'vulnerability', ['vulnerability_id'], ['id']) | |
53 | op.drop_constraint('knowledge_base_vulnerability_template_id_fkey', 'knowledge_base', type_='foreignkey') | |
54 | op.create_foreign_key(None, 'knowledge_base', 'vulnerability_template', ['vulnerability_template_id'], ['id'], ondelete='CASCADE') | |
55 | op.drop_constraint('command_workspace_id_fkey', 'command', type_='foreignkey') | |
56 | op.create_foreign_key(None, 'command', 'workspace', ['workspace_id'], ['id']) | |
57 | # ### end Alembic commands ### |
29 | 29 | op.drop_column('faraday_user', 'otp_secret') |
30 | 30 | op.drop_column('faraday_user', 'state_otp') |
31 | 31 | op.execute('DROP TYPE user_otp_states') |
32 | # I'm Py3 |
79 | 79 | def downgrade(): |
80 | 80 | op.drop_table('agent_schedule') |
81 | 81 | op.drop_table('agent') |
82 | ||
83 | ||
84 | # I'm Py3 |
23 | 23 | def downgrade(): |
24 | 24 | conn = op.get_bind() |
25 | 25 | conn.execute('ALTER TABLE executive_report DROP COLUMN filter') |
26 | ||
27 | ||
28 | # I'm Py3 |
0 | """cvss_model | |
1 | ||
2 | Revision ID: d8f0b32a5c0e | |
3 | Revises: f28eae25416b | |
4 | Create Date: 2021-09-01 10:30:06.693843+00:00 | |
5 | ||
6 | """ | |
7 | from alembic import op | |
8 | import sqlalchemy as sa | |
9 | ||
10 | ||
11 | # revision identifiers, used by Alembic. | |
12 | revision = 'd8f0b32a5c0e' | |
13 | down_revision = 'f28eae25416b' | |
14 | branch_labels = None | |
15 | depends_on = None | |
16 | ||
17 | ||
18 | def upgrade(): | |
19 | # ### commands auto generated by Alembic - please adjust! ### | |
20 | op.create_table('cvss_base', | |
21 | sa.Column('id', sa.Integer(), nullable=False), | |
22 | sa.Column('version', sa.String(length=8), nullable=False), | |
23 | sa.Column('vector_string', sa.String(length=64)), | |
24 | sa.Column('type', sa.String(length=24), nullable=True), | |
25 | sa.Column('base_score', sa.Float(), default=0.0), | |
26 | sa.Column('fixed_base_score', sa.Float(), default=0.0), | |
27 | sa.PrimaryKeyConstraint('id') | |
28 | ) | |
29 | op.create_table('cvss_v2', | |
30 | sa.Column('id', sa.Integer(), nullable=False), | |
31 | sa.Column('access_vector', sa.Enum('N', 'A', 'L', name='cvss_access_vector')), | |
32 | sa.Column('access_complexity', sa.Enum('L', 'M', 'H', name='cvss_access_complexity')), | |
33 | sa.Column('authentication', sa.Enum('N', 'S', 'M', name='cvss_authentication')), | |
34 | sa.Column('confidentiality_impact', sa.Enum('N', 'P', 'C', name='cvss_impact_types_v2')), | |
35 | sa.Column('integrity_impact', sa.Enum('N', 'P', 'C', name='cvss_impact_types_v2')), | |
36 | sa.Column('availability_impact', sa.Enum('N', 'P', 'C', name='cvss_impact_types_v2')), | |
37 | sa.ForeignKeyConstraint(['id'], ['cvss_base.id'], ), | |
38 | sa.PrimaryKeyConstraint('id') | |
39 | ) | |
40 | op.create_table('cvss_v3', | |
41 | sa.Column('id', sa.Integer(), nullable=False), | |
42 | sa.Column('attack_vector', sa.Enum('N', 'A', 'L', 'P', name='cvss_attack_vector')), | |
43 | sa.Column('attack_complexity', sa.Enum('L', 'H', name='cvss_attack_complexity')), | |
44 | sa.Column('privileges_required', sa.Enum('N', 'L', 'H', name='cvss_privileges_required')), | |
45 | sa.Column('user_interaction', sa.Enum('N', 'R', name='cvss_user_interaction')), | |
46 | sa.Column('scope', sa.Enum('U', 'C', name='cvss_scope')), | |
47 | sa.Column('confidentiality_impact', sa.Enum('N', 'L', 'H', name='cvss_impact_types_v3')), | |
48 | sa.Column('integrity_impact', sa.Enum('N', 'L', 'H', name='cvss_impact_types_v3')), | |
49 | sa.Column('availability_impact', sa.Enum('N', 'L', 'H', name='cvss_impact_types_v3')), | |
50 | sa.ForeignKeyConstraint(['id'], ['cvss_base.id'], ), | |
51 | sa.PrimaryKeyConstraint('id') | |
52 | ) | |
53 | ||
54 | # Vuln relationship with cvss | |
55 | op.add_column('vulnerability', sa.Column('cvssv2_id', sa.Integer(), nullable=True)) | |
56 | op.add_column('vulnerability', sa.Column('cvssv3_id', sa.Integer(), nullable=True)) | |
57 | op.create_foreign_key(None, 'vulnerability', 'cvss_v2', ['cvssv2_id'], ['id']) | |
58 | op.create_foreign_key(None, 'vulnerability', 'cvss_v3', ['cvssv3_id'], ['id']) | |
59 | ||
60 | # ### end Alembic commands ### | |
61 | ||
62 | ||
63 | def downgrade(): | |
64 | # ### commands auto generated by Alembic - please adjust! ### | |
65 | # op.drop_constraint(None, 'vulnerability', type_='foreignkey') | |
66 | op.drop_column('vulnerability', 'cvssv2_id') | |
67 | op.drop_column('vulnerability', 'cvssv3_id') | |
68 | op.drop_table('cvss_v3') | |
69 | op.drop_table('cvss_v2') | |
70 | op.drop_table('cvss_base') | |
71 | op.execute('drop type cvss_attack_complexity') | |
72 | op.execute('drop type cvss_access_vector') | |
73 | op.execute('drop type cvss_access_complexity') | |
74 | op.execute('drop type cvss_attack_vector') | |
75 | op.execute('drop type cvss_authentication') | |
76 | op.execute('drop type cvss_privileges_required') | |
77 | op.execute('drop type cvss_scope') | |
78 | op.execute('drop type cvss_user_interaction') | |
79 | op.execute('drop type cvss_impact_types_v2') | |
80 | op.execute('drop type cvss_impact_types_v3') | |
81 | # | |
82 | # ### end Alembic commands ### |
33 | 33 | conn.execute('ALTER TABLE vulnerability DROP COLUMN custom_fields') |
34 | 34 | conn.execute('ALTER TABLE vulnerability_template DROP COLUMN custom_fields') |
35 | 35 | conn.execute('DROP TABLE custom_fields_schema') |
36 | # I'm Py3 |
49 | 49 | old_type = sa.Enum(*ROLES, name='user_roles') |
50 | 50 | |
51 | 51 | # Convert 'asset_owner' status into 'client' |
52 | op.execute(tcr.update().where(tcr.c.role == u'asset_owner') | |
52 | op.execute(tcr.update().where(tcr.c.role == 'asset_owner') | |
53 | 53 | .values(status='client')) |
54 | 54 | # Create a temporary "_role" type, convert and drop the "new" type |
55 | 55 | tmp_type.create(op.get_bind(), checkfirst=False) |
0 | 0 | import json |
1 | 1 | import logging |
2 | 2 | import socket |
3 | from urllib.parse import urlencode | |
4 | ||
3 | from urllib.parse import urlencode, urljoin, urlparse | |
5 | 4 | from requests.adapters import ConnectionError, ReadTimeout |
6 | 5 | |
7 | 6 | logger = logging.getLogger('Faraday searcher') |
56 | 55 | raise UserWarning('Invalid username or password') |
57 | 56 | |
58 | 57 | def _url(self, path, is_get=False): |
59 | url = self.base + 'v3/' + path | |
58 | url = urljoin(self.base, f'v3/{path}') | |
60 | 59 | if self.command_id and 'commands' not in url and not url.endswith('}') and not is_get: |
61 | if '?' in url: | |
62 | url += f'&command_id={self.command_id}' | |
63 | elif url.endswith('/'): | |
64 | url = f'{url[:-1]}?command_id={self.command_id}' | |
60 | if url.endswith('/'): | |
61 | url = urljoin(url[:-1], f'?command_id={self.command_id}') | |
65 | 62 | else: |
66 | url += f'?command_id={self.command_id}' | |
63 | q = urlparse(url).query | |
64 | url = urljoin(url, f'?{q}&command_id={self.command_id}') | |
67 | 65 | return url |
68 | 66 | |
69 | 67 | def _get(self, url, object_name): |
0 | 0 | #!/usr/bin/env python |
1 | # -*- coding: utf-8 -*- | |
2 | 1 | |
3 | 2 | ### |
4 | 3 | # Faraday Penetration Test IDE |
5 | 4 | # Copyright (C) 2018 Infobyte LLC (http://www.infobytesec.com/) |
6 | 5 | # See the file 'doc/LICENSE' for the license information |
7 | 6 | ### |
8 | from builtins import str | |
9 | 7 | |
10 | 8 | import ast |
11 | 9 | import json |
179 | 177 | set_array(field, value, add=to_add) |
180 | 178 | action = f'Adding {value} to {key} list in vulnerability {vuln.name} with id {vuln.id}' |
181 | 179 | if not to_add: |
182 | action = 'Removing %s from %s list in vulnerability %s with id %s' % ( | |
180 | action = 'Removing {} from {} list in vulnerability {} with id {}'.format( | |
183 | 181 | value, key, vuln.name, vuln.id) |
184 | 182 | |
185 | 183 | logger.info(action) |
216 | 214 | set_array(field, value, add=to_add) |
217 | 215 | action = f'Adding {value} to {key} list in service {service.name} with id {service.id}' |
218 | 216 | if not to_add: |
219 | action = 'Removing %s from %s list in service %s with id %s' % ( | |
217 | action = 'Removing {} from {} list in service {} with id {}'.format( | |
220 | 218 | value, key, service.name, service.id) |
221 | 219 | |
222 | 220 | logger.info(action) |
247 | 245 | set_array(field, value, add=to_add) |
248 | 246 | action = f'Adding {value} to {key} list in host {host.name} with id {host.id}' |
249 | 247 | if not to_add: |
250 | action = 'Removing %s from %s list in host %s with id %s' % ( | |
248 | action = 'Removing {} from {} list in host {} with id {}'.format( | |
251 | 249 | value, key, host.name, host.id) |
252 | 250 | |
253 | 251 | logger.info(action) |
366 | 364 | return rule |
367 | 365 | |
368 | 366 | rule_str = json.dumps(rule) |
369 | r = re.findall("\{\{(.*?)\}\}", rule_str) | |
367 | r = re.findall(r"\{\{(.*?)\}\}", rule_str) | |
370 | 368 | _vars = list(set(r)) |
371 | 369 | for var in _vars: |
372 | 370 | value = value_item[var] |
598 | 596 | action = action.strip('--') |
599 | 597 | array = action.split(':') |
600 | 598 | command = array[0] |
601 | expression = str(':').join(array[1:]) | |
599 | expression = ':'.join(array[1:]) | |
602 | 600 | |
603 | 601 | if command == 'UPDATE': |
604 | 602 | array_exp = expression.split('=') |
605 | 603 | key = array_exp[0] |
606 | value = str('=').join(array_exp[1:]) | |
604 | value = '='.join(array_exp[1:]) | |
607 | 605 | if object_type in ['Vulnerabilityweb', 'Vulnerability_web', 'Vulnerability']: |
608 | 606 | self._update_vulnerability(obj, key, value) |
609 | 607 | |
628 | 626 | else: |
629 | 627 | if self.mail_notification: |
630 | 628 | subject = 'Faraday searcher alert' |
631 | body = '%s %s have been modified by rule %s at %s' % ( | |
629 | body = '{} {} have been modified by rule {} at {}'.format( | |
632 | 630 | object_type, obj.name, rule['id'], str(datetime.utcnow())) |
633 | 631 | self.mail_notification.send_mail(expression, subject, body) |
634 | 632 | logger.info(f"Sending mail to: '{expression}'") |
687 | 685 | if isinstance(field, str): |
688 | 686 | setattr(vuln, key, value) |
689 | 687 | logger.info( |
690 | "Changing property %s to %s in vulnerability '%s' with id %s" % ( | |
688 | "Changing property {} to {} in vulnerability '{}' with id {}".format( | |
691 | 689 | key, value, vuln.name, vuln.id)) |
692 | 690 | else: |
693 | 691 | self.api.set_array(field, value, add=to_add, key=key, object=vuln) |
694 | action = 'Adding %s to %s list in vulnerability %s with id %s' % ( | |
692 | action = 'Adding {} to {} list in vulnerability {} with id {}'.format( | |
695 | 693 | value, key, vuln.name, vuln.id) |
696 | 694 | if not to_add: |
697 | action = 'Removing %s from %s list in vulnerability %s with id %s' % ( | |
695 | action = 'Removing {} from {} list in vulnerability {} with id {}'.format( | |
698 | 696 | value, key, vuln.name, vuln.id) |
699 | 697 | |
700 | 698 | logger.info(action) |
702 | 700 | else: |
703 | 701 | vuln.custom_fields[key] = value |
704 | 702 | logger.info( |
705 | "Changing custom field %s to %s in vulnerability '%s' with id %s" % ( | |
703 | "Changing custom field {} to {} in vulnerability '{}' with id {}".format( | |
706 | 704 | key, value, vuln.name, vuln.id)) |
707 | 705 | |
708 | 706 | result = self.api.update_vulnerability(vuln) |
728 | 726 | if isinstance(field, str): |
729 | 727 | setattr(service, key, value) |
730 | 728 | logger.info( |
731 | "Changing property %s to %s in service '%s' with id %s" % ( | |
729 | "Changing property {} to {} in service '{}' with id {}".format( | |
732 | 730 | key, value, service.name, service.id)) |
733 | 731 | else: |
734 | 732 | self.api.set_array(field, value, add=to_add, key=key, object=service) |
735 | 733 | action = f'Adding {value} to {key} list in service {service.name} with id {service.id}' |
736 | 734 | if not to_add: |
737 | action = 'Removing %s from %s list in service %s with id %s' % ( | |
735 | action = 'Removing {} from {} list in service {} with id {}'.format( | |
738 | 736 | value, key, service.name, service.id) |
739 | 737 | |
740 | 738 | logger.info(action) |
764 | 762 | self.api.set_array(field, value, add=to_add, key=key, object=host) |
765 | 763 | action = f'Adding {value} to {key} list in host {host.ip} with id {host.id}' |
766 | 764 | if not to_add: |
767 | action = 'Removing %s from %s list in host %s with id %s' % ( | |
765 | action = 'Removing {} from {} list in host {} with id {}'.format( | |
768 | 766 | value, key, host.ip, host.id) |
769 | 767 | |
770 | 768 | logger.info(action) |
814 | 812 | signal.signal(signal.SIGINT, signal_handler) |
815 | 813 | |
816 | 814 | loglevel = log |
817 | with open(rules, 'r') as rules_file: | |
815 | with open(rules) as rules_file: | |
818 | 816 | try: |
819 | 817 | rules = json.loads(rules_file.read()) |
820 | 818 | except Exception: |
876 | 874 | |
877 | 875 | if __name__ == "__main__": |
878 | 876 | main() |
879 | # I'm Py3 |
0 | 0 | #!/usr/bin/env python |
1 | # -*- coding: utf-8 -*- | |
2 | 1 | |
3 | 2 | ### |
4 | 3 | # Faraday Penetration Test IDE |
82 | 81 | |
83 | 82 | |
84 | 83 | def validate_values(values, rule, rule_id): |
85 | r = re.findall("\{\{(.*?)\}\}", json.dumps(rule)) | |
84 | r = re.findall(r"\{\{(.*?)\}\}", json.dumps(rule)) | |
86 | 85 | _vars = list(set(r)) |
87 | 86 | keys = [] |
88 | 87 | for index, item in enumerate(values): |
118 | 117 | |
119 | 118 | if action.startswith('--ALERT:'): |
120 | 119 | expression = action.strip('--ALERT:') |
121 | if expression == '' or re.match("^(.+\@.+\..+)$", expression) is None: | |
120 | if expression == '' or re.match(r"^(.+\@.+\..+)$", expression) is None: | |
122 | 121 | return False |
123 | 122 | |
124 | 123 | if action.startswith('--EXECUTE:'): |
196 | 195 | |
197 | 196 | logger.info('<-- Rules OK') |
198 | 197 | return True |
199 | # I'm Py3 |
200 | 200 | """ |
201 | 201 | return getattr(self.model_class, self.lookup_field) |
202 | 202 | |
203 | def _validate_object_id(self, object_id): | |
203 | def _validate_object_id(self, object_id, raise_error=True): | |
204 | 204 | """ |
205 | 205 | By default, it validates the value of the lookup field set by the user |
206 | 206 | in the URL by calling ``self.lookup_field_type(object_id)``. |
210 | 210 | try: |
211 | 211 | self.lookup_field_type(object_id) |
212 | 212 | except ValueError: |
213 | flask.abort(404, 'Invalid format of lookup field') | |
213 | if raise_error: | |
214 | flask.abort(404, 'Invalid format of lookup field') | |
215 | return False | |
216 | return True | |
214 | 217 | |
215 | 218 | def _get_base_query(self): |
216 | 219 | """Return the initial query all views should use |
288 | 291 | obj = query.filter(self._get_lookup_field() == object_id).one() |
289 | 292 | except NoResultFound: |
290 | 293 | flask.abort(404, f'Object with id "{object_id}" not found') |
294 | return obj | |
295 | ||
296 | def _get_objects(self, object_ids, eagerload=False, **kwargs): | |
297 | """ | |
298 | Given the object_id and extra route params, get an instance of | |
299 | ``self.model_class`` | |
300 | """ | |
301 | object_ids = [object_id for object_id in object_ids if self._validate_object_id(object_id, raise_error=False)] | |
302 | if eagerload: | |
303 | query = self._get_eagerloaded_query(**kwargs) | |
304 | else: | |
305 | query = self._get_base_query(**kwargs) | |
306 | try: | |
307 | obj = query.filter(self._get_lookup_field().in_(object_ids)).all() | |
308 | except NoResultFound: | |
309 | return [] | |
291 | 310 | return obj |
292 | 311 | |
293 | 312 | def _dump(self, obj, route_kwargs, **kwargs): |
1222 | 1241 | self._perform_update(object_id, obj, data, partial=True, **kwargs) |
1223 | 1242 | |
1224 | 1243 | return self._dump(obj, kwargs), 200 |
1244 | ||
1245 | ||
1246 | class BulkUpdateMixin: | |
1247 | # These mixin should be merged with DeleteMixin after v2 is removed | |
1248 | ||
1249 | @route('', methods=['PATCH']) | |
1250 | def bulk_update(self, **kwargs): | |
1251 | """ | |
1252 | --- | |
1253 | tags: [{tag_name}] | |
1254 | summary: "Update a group of {class_model} by ids." | |
1255 | responses: | |
1256 | 204: | |
1257 | description: Ok | |
1258 | """ | |
1259 | # TODO BULK_UPDATE_SCHEMA | |
1260 | if not flask.request.json or 'ids' not in flask.request.json: | |
1261 | flask.abort(400) | |
1262 | ids = list(filter(lambda x: type(x) == self.lookup_field_type, flask.request.json['ids'])) | |
1263 | objects = self._get_objects(ids, **kwargs) | |
1264 | context = {'updating': True, 'objects': objects} | |
1265 | data = self._parse_data(self._get_schema_instance(kwargs, context=context, partial=True), | |
1266 | flask.request) | |
1267 | # just in case an schema allows id as writable. | |
1268 | data.pop('id', None) | |
1269 | data.pop('ids', None) | |
1270 | ||
1271 | return self._perform_bulk_update(ids, data, **kwargs), 200 | |
1272 | ||
1273 | def _bulk_update_query(self, ids, **kwargs): | |
1274 | # It IS better to as is but warn of ON CASCADE | |
1275 | return self.model_class.query.filter(self.model_class.id.in_(ids)) | |
1276 | ||
1277 | def _pre_bulk_update(self, data, **kwargs): | |
1278 | return {} | |
1279 | ||
1280 | def _post_bulk_update(self, ids, extracted_data, **kwargs): | |
1281 | pass | |
1282 | ||
1283 | def _perform_bulk_update(self, ids, data, workspace_name=None, **kwargs): | |
1284 | try: | |
1285 | post_bulk_update_data = self._pre_bulk_update(data, **kwargs) | |
1286 | if (len(data) > 0 or len(post_bulk_update_data) > 0) and len(ids) > 0: | |
1287 | queryset = self._bulk_update_query(ids, workspace_name=workspace_name, **kwargs) | |
1288 | updated = queryset.update(data, synchronize_session='fetch') | |
1289 | self._post_bulk_update(ids, post_bulk_update_data, workspace_name=workspace_name) | |
1290 | else: | |
1291 | updated = 0 | |
1292 | db.session.commit() | |
1293 | response = {'updated': updated} | |
1294 | return flask.jsonify(response) | |
1295 | except ValueError as e: | |
1296 | db.session.rollback() | |
1297 | flask.abort(400, ValidationError( | |
1298 | { | |
1299 | 'message': str(e), | |
1300 | } | |
1301 | )) | |
1302 | except sqlalchemy.exc.IntegrityError as ex: | |
1303 | if not is_unique_constraint_violation(ex): | |
1304 | raise | |
1305 | db.session.rollback() | |
1306 | workspace = None | |
1307 | if workspace_name: | |
1308 | workspace = db.session.query(Workspace).filter_by(name=workspace_name).first() | |
1309 | conflict_obj = get_conflict_object(db.session, self.model_class(), data, workspace) | |
1310 | if conflict_obj is not None: | |
1311 | flask.abort(409, ValidationError( | |
1312 | { | |
1313 | 'message': 'Existing value', | |
1314 | 'object': self._get_schema_class()().dump( | |
1315 | conflict_obj), | |
1316 | } | |
1317 | )) | |
1318 | elif len(ids) >= 2: | |
1319 | flask.abort(409, ValidationError( | |
1320 | { | |
1321 | 'message': 'Updating more than one object with unique data', | |
1322 | 'data': data | |
1323 | } | |
1324 | )) | |
1325 | else: | |
1326 | raise | |
1225 | 1327 | |
1226 | 1328 | |
1227 | 1329 | class UpdateWorkspacedMixin(UpdateMixin, CommandMixin): |
1313 | 1415 | return super().patch(object_id, workspace_name=workspace_name) |
1314 | 1416 | |
1315 | 1417 | |
1418 | class BulkUpdateWorkspacedMixin(BulkUpdateMixin): | |
1419 | ||
1420 | @route('', methods=['PATCH']) | |
1421 | def bulk_update(self, workspace_name, **kwargs): | |
1422 | """ | |
1423 | --- | |
1424 | tags: [{tag_name}] | |
1425 | summary: "Delete a group of {class_model} by ids." | |
1426 | responses: | |
1427 | 204: | |
1428 | description: Ok | |
1429 | """ | |
1430 | return super().bulk_update(workspace_name=workspace_name) | |
1431 | ||
1432 | def _bulk_update_query(self, ids, **kwargs): | |
1433 | workspace = self._get_workspace(kwargs["workspace_name"]) | |
1434 | return super()._bulk_update_query(ids).filter(self.model_class.workspace_id == workspace.id) | |
1435 | ||
1436 | ||
1316 | 1437 | class DeleteMixin: |
1317 | 1438 | """Add DELETE /<id>/ route""" |
1318 | 1439 | |
1338 | 1459 | def _perform_delete(self, obj, workspace_name=None): |
1339 | 1460 | db.session.delete(obj) |
1340 | 1461 | db.session.commit() |
1462 | ||
1463 | ||
1464 | class BulkDeleteMixin: | |
1465 | # These mixin should be merged with DeleteMixin after v2 is removed | |
1466 | ||
1467 | @route('', methods=['DELETE']) | |
1468 | def bulk_delete(self, **kwargs): | |
1469 | """ | |
1470 | --- | |
1471 | tags: [{tag_name}] | |
1472 | summary: "Delete a group of {class_model} by ids." | |
1473 | responses: | |
1474 | 204: | |
1475 | description: Ok | |
1476 | """ | |
1477 | # TODO BULK_DELETE_SCHEMA | |
1478 | if not flask.request.json or 'ids' not in flask.request.json: | |
1479 | flask.abort(400) | |
1480 | # objs = self._get_objects(flask.request.json['ids'], **kwargs) | |
1481 | # self._perform_bulk_delete(objs, **kwargs) | |
1482 | ids = list(filter(lambda x: type(x) == self.lookup_field_type, flask.request.json['ids'])) | |
1483 | return self._perform_bulk_delete(ids, **kwargs), 200 | |
1484 | ||
1485 | def _bulk_delete_query(self, ids, **kwargs): | |
1486 | # It IS better to as is but warn of ON CASCADE | |
1487 | return self.model_class.query.filter(self.model_class.id.in_(ids)) | |
1488 | ||
1489 | def _perform_bulk_delete(self, ids, **kwargs): | |
1490 | deleted = self._bulk_delete_query(ids, **kwargs).delete(synchronize_session='fetch') | |
1491 | db.session.commit() | |
1492 | response = {'deleted': deleted} | |
1493 | return flask.jsonify(response) | |
1341 | 1494 | |
1342 | 1495 | |
1343 | 1496 | class DeleteWorkspacedMixin(DeleteMixin): |
1372 | 1525 | return super()._perform_delete(obj, workspace_name) |
1373 | 1526 | |
1374 | 1527 | |
1528 | class BulkDeleteWorkspacedMixin(BulkDeleteMixin): | |
1529 | # These mixin should be merged with DeleteMixin after v2 is removed | |
1530 | ||
1531 | @route('', methods=['DELETE']) | |
1532 | def bulk_delete(self, workspace_name, **kwargs): | |
1533 | """ | |
1534 | --- | |
1535 | tags: [{tag_name}] | |
1536 | summary: "Delete a group of {class_model} by ids." | |
1537 | responses: | |
1538 | 204: | |
1539 | description: Ok | |
1540 | """ | |
1541 | return super().bulk_delete(workspace_name=workspace_name) | |
1542 | ||
1543 | def _bulk_delete_query(self, ids, **kwargs): | |
1544 | workspace = self._get_workspace(kwargs.pop("workspace_name")) | |
1545 | return super()._bulk_delete_query(ids).filter(self.model_class.workspace_id == workspace.id) | |
1546 | ||
1547 | ||
1375 | 1548 | class CountWorkspacedMixin: |
1376 | 1549 | """Add GET /<workspace_name>/<route_base>/count/ route |
1377 | 1550 |
6 | 6 | from flask import Blueprint |
7 | 7 | from marshmallow import fields |
8 | 8 | |
9 | from faraday.server.api.base import AutoSchema, ReadWriteWorkspacedView, PaginatedMixin | |
9 | from faraday.server.api.base import ( | |
10 | AutoSchema, | |
11 | ReadWriteWorkspacedView, | |
12 | PaginatedMixin | |
13 | ) | |
10 | 14 | from faraday.server.models import Command |
11 | 15 | from faraday.server.schemas import PrimaryKeyRelatedField |
12 | 16 |
210 | 210 | except NoResultFound: |
211 | 211 | flask.abort(404, f"No such workspace: {workspace_name}") |
212 | 212 | |
213 | def _update_object(self, obj, data, **kwargs): | |
214 | """Perform changes in the selected object | |
215 | ||
216 | It modifies the attributes of the SQLAlchemy model to match | |
217 | the data passed by the Marshmallow schema. | |
218 | ||
219 | It is common to overwrite this method to do something strange | |
220 | with some specific field. Typically the new method should call | |
221 | this one to handle the update of the rest of the fields. | |
222 | """ | |
213 | def _get_workspaces_from_data(self, data, **kwargs): | |
223 | 214 | workspace_names = data.pop('workspaces', '') |
224 | 215 | partial = False if 'partial' not in kwargs else kwargs['partial'] |
225 | ||
226 | 216 | if len(workspace_names) == 0 and not partial: |
227 | 217 | abort( |
228 | 218 | make_response( |
237 | 227 | 400 |
238 | 228 | ) |
239 | 229 | ) |
240 | ||
241 | 230 | workspace_names = [ |
242 | 231 | dict_["name"] for dict_ in workspace_names |
243 | 232 | ] |
244 | ||
245 | workspaces = list( | |
233 | return list( | |
246 | 234 | self._get_workspace(workspace_name) |
247 | 235 | for workspace_name in workspace_names |
248 | 236 | ) |
237 | ||
238 | def _update_object(self, obj, data, **kwargs): | |
239 | """Perform changes in the selected object | |
240 | ||
241 | It modifies the attributes of the SQLAlchemy model to match | |
242 | the data passed by the Marshmallow schema. | |
243 | ||
244 | It is common to overwrite this method to do something strange | |
245 | with some specific field. Typically the new method should call | |
246 | this one to handle the update of the rest of the fields. | |
247 | """ | |
248 | workspaces = self._get_workspaces_from_data(data, **kwargs) | |
249 | 249 | |
250 | 250 | super()._update_object(obj, data) |
251 | 251 | obj.workspaces = workspaces |
2 | 2 | Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/) |
3 | 3 | See the file 'doc/LICENSE' for the license information |
4 | 4 | """ |
5 | from __future__ import print_function | |
6 | from __future__ import absolute_import | |
7 | 5 | |
8 | 6 | import flask |
9 | 7 |
9 | 9 | from flask_classful import route |
10 | 10 | from marshmallow import fields, post_load, ValidationError |
11 | 11 | |
12 | from faraday.server.api.base import AutoSchema, ReadWriteWorkspacedView, PaginatedMixin | |
12 | from faraday.server.api.base import ( | |
13 | AutoSchema, | |
14 | ReadWriteWorkspacedView, | |
15 | PaginatedMixin | |
16 | ) | |
13 | 17 | from faraday.server.models import Command, Workspace |
14 | 18 | from faraday.server.schemas import MutableField, PrimaryKeyRelatedField, SelfNestedField, MetadataSchema |
15 | 19 |
11 | 11 | ReadWriteWorkspacedView, |
12 | 12 | InvalidUsage, |
13 | 13 | CreateWorkspacedMixin, |
14 | GenericWorkspacedView | |
14 | GenericWorkspacedView, | |
15 | BulkDeleteWorkspacedMixin | |
15 | 16 | ) |
16 | 17 | from faraday.server.models import Comment |
17 | 18 | comment_api = Blueprint('comment_api', __name__) |
51 | 52 | return super()._perform_create(data, workspace_name) |
52 | 53 | |
53 | 54 | |
54 | class CommentView(CommentCreateMixing, ReadWriteWorkspacedView): | |
55 | class CommentView(CommentCreateMixing, ReadWriteWorkspacedView, BulkDeleteWorkspacedMixin): | |
55 | 56 | route_base = 'comment' |
56 | 57 | model_class = Comment |
57 | 58 | schema_class = CommentSchema |
58 | 59 | order_field = 'create_date' |
59 | 60 | |
60 | 61 | |
61 | class UniqueCommentView(GenericWorkspacedView, CommentCreateMixing): | |
62 | class UniqueCommentView(GenericWorkspacedView, | |
63 | CommentCreateMixing): | |
62 | 64 | """ |
63 | 65 | This view is used by the plugin engine to avoid duplicate comments |
64 | 66 | when the same plugin and data was ran multiple times. |
10 | 10 | ReadWriteWorkspacedView, |
11 | 11 | FilterSetMeta, |
12 | 12 | FilterAlchemyMixin, |
13 | InvalidUsage | |
13 | ||
14 | InvalidUsage, | |
15 | BulkDeleteWorkspacedMixin, | |
16 | BulkUpdateWorkspacedMixin | |
14 | 17 | ) |
15 | 18 | from faraday.server.models import Credential, Host, Service, Workspace, db |
16 | 19 | from faraday.server.schemas import MutableField, SelfNestedField, MetadataSchema |
108 | 111 | operators = (operators.Equal, ) |
109 | 112 | |
110 | 113 | |
111 | class CredentialView(FilterAlchemyMixin, ReadWriteWorkspacedView): | |
114 | class CredentialView(FilterAlchemyMixin, | |
115 | ReadWriteWorkspacedView, | |
116 | BulkDeleteWorkspacedMixin, | |
117 | BulkUpdateWorkspacedMixin): | |
112 | 118 | route_base = 'credential' |
113 | 119 | model_class = Credential |
114 | 120 | schema_class = CredentialSchema |
6 | 6 | from faraday.server.models import CustomFieldsSchema |
7 | 7 | from faraday.server.api.base import ( |
8 | 8 | AutoSchema, |
9 | ReadWriteView | |
9 | ReadWriteView, | |
10 | BulkDeleteMixin | |
10 | 11 | ) |
11 | 12 | |
12 | 13 | |
35 | 36 | ) |
36 | 37 | |
37 | 38 | |
38 | class CustomFieldsSchemaView(ReadWriteView): | |
39 | class CustomFieldsSchemaView(ReadWriteView, BulkDeleteMixin): | |
39 | 40 | route_base = 'custom_fields_schema' |
40 | 41 | model_class = CustomFieldsSchema |
41 | 42 | schema_class = CustomFieldsSchemaSchema |
43 | ||
44 | def _check_post_only_data(self, data): | |
45 | for read_only_key in ['field_name', 'table_name', 'field_type']: | |
46 | data.pop(read_only_key, None) | |
47 | return data | |
42 | 48 | |
43 | 49 | def _update_object(self, obj, data, **kwargs): |
44 | 50 | """ |
45 | 51 | Field name must be read only |
46 | 52 | """ |
47 | for read_only_key in ['field_name', 'table_name', 'field_type']: | |
48 | if read_only_key in data: | |
49 | data.pop(read_only_key) | |
53 | data = self._check_post_only_data(data) | |
50 | 54 | return super()._update_object(obj, data) |
51 | 55 | |
52 | 56 |
23 | 23 | AutoSchema, |
24 | 24 | FilterAlchemyMixin, |
25 | 25 | FilterSetMeta, |
26 | FilterWorkspacedMixin | |
26 | ||
27 | FilterWorkspacedMixin, | |
28 | BulkDeleteWorkspacedMixin, | |
29 | BulkUpdateWorkspacedMixin | |
27 | 30 | ) |
28 | 31 | from faraday.server.schemas import ( |
29 | 32 | MetadataSchema, |
136 | 139 | class HostsView(PaginatedMixin, |
137 | 140 | FilterAlchemyMixin, |
138 | 141 | ReadWriteWorkspacedView, |
139 | FilterWorkspacedMixin): | |
142 | FilterWorkspacedMixin, | |
143 | BulkDeleteWorkspacedMixin, | |
144 | BulkUpdateWorkspacedMixin): | |
140 | 145 | route_base = 'hosts' |
141 | 146 | model_class = Host |
142 | 147 | order_field = desc(Host.vulnerability_critical_generic_count),\ |
239 | 244 | for host_dict in hosts_reader: |
240 | 245 | try: |
241 | 246 | hostnames = parse_hosts(host_dict.pop('hostnames')) |
242 | other_fields = {'owned': False, 'mac': u'00:00:00:00:00:00', 'default_gateway_ip': u'None'} | |
247 | other_fields = {'owned': False, 'mac': '00:00:00:00:00:00', 'default_gateway_ip': 'None'} | |
243 | 248 | host_dict.update(other_fields) |
244 | 249 | host = super()._perform_create(host_dict, workspace_name) |
245 | 250 | host.workspace = workspace |
398 | 403 | or len(hosts)), |
399 | 404 | } |
400 | 405 | |
401 | # ### THIS WAS FROM V2 | |
402 | # TODO SCHEMA | |
403 | # @route('bulk_delete/', methods=['DELETE']) | |
404 | # def bulk_delete(self, workspace_name): | |
405 | # """ | |
406 | # --- | |
407 | # delete: | |
408 | # tags: ["Bulk", "Host"] | |
409 | # description: Delete hosts in bulk | |
410 | # responses: | |
411 | # 200: | |
412 | # description: Ok | |
413 | # 400: | |
414 | # description: Bad request | |
415 | # 403: | |
416 | # description: Forbidden | |
417 | # tags: ["Bulk", "Host"] | |
418 | # responses: | |
419 | # 200: | |
420 | # description: Ok | |
421 | # """ | |
422 | # workspace = self._get_workspace(workspace_name) | |
423 | # json_request = flask.request.get_json() | |
424 | # if not json_request: | |
425 | # flask.abort(400, 'Invalid request. Check the request data or the content type of the request') | |
426 | # hosts_ids = json_request.get('hosts_ids', []) | |
427 | # hosts_ids = [host_id for host_id in hosts_ids if isinstance(host_id, int)] | |
428 | # deleted_hosts = 0 | |
429 | # if hosts_ids: | |
430 | # deleted_hosts = Host.query.filter( | |
431 | # Host.id.in_(hosts_ids), | |
432 | # Host.workspace_id == workspace.id).delete(synchronize_session='fetch') | |
433 | # else: | |
434 | # flask.abort(400, "Invalid request") | |
435 | # | |
436 | # db.session.commit() | |
437 | # response = {'deleted_hosts': deleted_hosts} | |
438 | # return flask.jsonify(response) | |
406 | @route('', methods=['DELETE']) | |
407 | def bulk_delete(self, workspace_name, **kwargs): | |
408 | # TODO REVISE ORIGINAL METHOD TO UPDATE NEW METHOD | |
409 | return BulkDeleteWorkspacedMixin.bulk_delete(self, workspace_name, **kwargs) | |
410 | ||
411 | bulk_delete.__doc__ = BulkDeleteWorkspacedMixin.bulk_delete.__doc__ | |
412 | ||
413 | def _pre_bulk_update(self, data, **kwargs): | |
414 | hostnames = data.pop('hostnames', None) | |
415 | ans_data = super()._pre_bulk_update(data, **kwargs) | |
416 | if hostnames is not None: | |
417 | ans_data["hostnames"] = hostnames | |
418 | return ans_data | |
419 | ||
420 | def _post_bulk_update(self, ids, extracted_data, **kwargs): | |
421 | if "hostnames" in extracted_data: | |
422 | for obj in self._bulk_update_query(ids, **kwargs).all(): | |
423 | obj.set_hostnames(extracted_data["hostnames"]) | |
439 | 424 | |
440 | 425 | |
441 | 426 | HostsView.register(host_api) |
6 | 6 | from faraday.server.models import License |
7 | 7 | from faraday.server.api.base import ( |
8 | 8 | ReadWriteView, |
9 | AutoSchema | |
9 | AutoSchema, | |
10 | 10 | ) |
11 | 11 | from faraday.server.schemas import ( |
12 | 12 | StrictDateTimeField, |
7 | 7 | from faraday.server.models import SearchFilter |
8 | 8 | from faraday.server.api.base import ( |
9 | 9 | ReadWriteView, |
10 | AutoSchema | |
10 | ||
11 | AutoSchema, | |
12 | BulkDeleteMixin, | |
13 | BulkUpdateMixin | |
11 | 14 | ) |
12 | 15 | |
13 | 16 | searchfilter_api = Blueprint('searchfilter_api', __name__) |
23 | 26 | 'json_query', 'user_query') |
24 | 27 | |
25 | 28 | |
26 | class SearchFilterView(ReadWriteView): | |
29 | class SearchFilterView(ReadWriteView, BulkDeleteMixin, BulkUpdateMixin): | |
27 | 30 | route_base = 'searchfilter' |
28 | 31 | model_class = SearchFilter |
29 | 32 | schema_class = SearchFilterSchema |
10 | 10 | AutoSchema, |
11 | 11 | ReadWriteWorkspacedView, |
12 | 12 | FilterSetMeta, |
13 | FilterAlchemyMixin | |
13 | FilterAlchemyMixin, | |
14 | BulkDeleteWorkspacedMixin, | |
15 | BulkUpdateWorkspacedMixin | |
14 | 16 | ) |
15 | 17 | from faraday.server.models import Host, Service, Workspace |
16 | 18 | from faraday.server.schemas import ( |
75 | 77 | # Partial update? |
76 | 78 | return data |
77 | 79 | |
78 | if host_id != self.context['object'].parent.id: | |
79 | raise ValidationError('Can\'t change service parent.') | |
80 | if 'object' in self.context: | |
81 | if host_id != self.context['object'].parent.id: | |
82 | raise ValidationError('Can\'t change service parent.') | |
83 | else: | |
84 | if any([host_id != obj.parent.id for obj in self.context['objects']]): | |
85 | raise ValidationError('Can\'t change service parent.') | |
80 | 86 | |
81 | 87 | else: |
82 | 88 | if not host_id: |
109 | 115 | operators = (operators.Equal,) |
110 | 116 | |
111 | 117 | |
112 | class ServiceView(FilterAlchemyMixin, ReadWriteWorkspacedView): | |
118 | class ServiceView(FilterAlchemyMixin, ReadWriteWorkspacedView, BulkDeleteWorkspacedMixin, BulkUpdateWorkspacedMixin): | |
113 | 119 | |
114 | 120 | route_base = 'services' |
115 | 121 | model_class = Service |
0 | 0 | # Faraday Penetration Test IDE |
1 | 1 | # Copyright (C) 2016 Infobyte LLC (http://www.infobytesec.com/) |
2 | 2 | # See the file 'doc/LICENSE' for the license information |
3 | from builtins import str, bytes | |
4 | 3 | from io import TextIOWrapper |
5 | 4 | |
6 | 5 | import json |
26 | 25 | FilterSetMeta, |
27 | 26 | PaginatedMixin, |
28 | 27 | ReadWriteView, |
29 | FilterMixin | |
28 | FilterMixin, | |
29 | BulkDeleteMixin, | |
30 | BulkUpdateMixin | |
30 | 31 | ) |
31 | 32 | |
32 | 33 | from faraday.server.schemas import ( |
134 | 135 | class VulnerabilityTemplateView(PaginatedMixin, |
135 | 136 | FilterAlchemyMixin, |
136 | 137 | ReadWriteView, |
137 | FilterMixin): | |
138 | FilterMixin, | |
139 | BulkDeleteMixin, | |
140 | BulkUpdateMixin): | |
138 | 141 | route_base = 'vulnerability_template' |
139 | 142 | model_class = VulnerabilityTemplate |
140 | 143 | schema_class = VulnerabilityTemplateSchema |
19 | 19 | from sqlalchemy.orm import aliased, joinedload, selectin_polymorphic, undefer, noload |
20 | 20 | from sqlalchemy.orm.exc import NoResultFound |
21 | 21 | from sqlalchemy import desc, or_, func |
22 | from sqlalchemy.inspection import inspect | |
22 | 23 | from werkzeug.datastructures import ImmutableMultiDict |
23 | 24 | from depot.manager import DepotManager |
24 | 25 | |
33 | 34 | PaginatedMixin, |
34 | 35 | ReadWriteWorkspacedView, |
35 | 36 | InvalidUsage, |
36 | CountMultiWorkspacedMixin | |
37 | CountMultiWorkspacedMixin, | |
38 | BulkDeleteWorkspacedMixin, | |
39 | BulkUpdateWorkspacedMixin | |
37 | 40 | ) |
38 | 41 | from faraday.server.fields import FaradayUploadedFile |
39 | 42 | from faraday.server.models import ( |
198 | 201 | for file_obj in obj.evidence: |
199 | 202 | try: |
200 | 203 | res[file_obj.filename] = EvidenceSchema().dump(file_obj) |
201 | except IOError: | |
204 | except OSError: | |
202 | 205 | logger.warning("File not found. Did you move your server?") |
203 | 206 | |
204 | 207 | return res |
470 | 473 | class VulnerabilityView(PaginatedMixin, |
471 | 474 | FilterAlchemyMixin, |
472 | 475 | ReadWriteWorkspacedView, |
473 | CountMultiWorkspacedMixin): | |
476 | CountMultiWorkspacedMixin, | |
477 | BulkDeleteWorkspacedMixin, | |
478 | BulkUpdateWorkspacedMixin): | |
474 | 479 | route_base = 'vulns' |
475 | 480 | filterset_class = VulnerabilityFilterSet |
476 | 481 | sort_model_class = VulnerabilityWeb # It has all the fields |
1055 | 1060 | as_attachment=True, |
1056 | 1061 | cache_timeout=-1) |
1057 | 1062 | |
1058 | @route('bulk_delete/', methods=['DELETE']) | |
1059 | def bulk_delete(self, workspace_name): | |
1060 | """ | |
1061 | --- | |
1062 | delete: | |
1063 | tags: ["Bulk", "Vulnerability"] | |
1064 | description: Delete vulnerabilities in bulk | |
1065 | responses: | |
1066 | 200: | |
1067 | description: Ok | |
1068 | 400: | |
1069 | description: Bad request | |
1070 | 403: | |
1071 | description: Forbidden | |
1072 | tags: ["Bulk", "Vulnerability"] | |
1073 | responses: | |
1074 | 200: | |
1075 | description: Ok | |
1076 | """ | |
1077 | workspace = self._get_workspace(workspace_name) | |
1078 | json_quest = request.get_json() | |
1079 | vulnerability_ids = json_quest.get('vulnerability_ids', []) | |
1080 | vulnerability_severities = json_quest.get('severities', []) | |
1081 | deleted_vulns = 0 | |
1082 | vulns = [] | |
1083 | if vulnerability_ids: | |
1084 | logger.info("Delete Vuln IDs: %s", vulnerability_ids) | |
1085 | vulns = VulnerabilityGeneric.query.filter(VulnerabilityGeneric.id.in_(vulnerability_ids), | |
1086 | VulnerabilityGeneric.workspace_id == workspace.id) | |
1087 | elif vulnerability_severities: | |
1088 | logger.info("Delete Vuln Severities: %s", vulnerability_severities) | |
1089 | vulns = VulnerabilityGeneric.query.filter(VulnerabilityGeneric.severity.in_(vulnerability_severities), | |
1090 | VulnerabilityGeneric.workspace_id == workspace.id) | |
1091 | else: | |
1092 | flask.abort(400, "Invalid Request") | |
1093 | for vuln in vulns: | |
1094 | db.session.delete(vuln) | |
1095 | deleted_vulns += 1 | |
1096 | db.session.commit() | |
1097 | response = {'deleted_vulns': deleted_vulns} | |
1098 | return flask.jsonify(response) | |
1099 | ||
1100 | 1063 | @route('top_users', methods=['GET']) |
1101 | 1064 | def top_users(self, workspace_name): |
1102 | 1065 | """ |
1129 | 1092 | response = {'users': users} |
1130 | 1093 | return flask.jsonify(response) |
1131 | 1094 | |
1095 | @route('', methods=['DELETE']) | |
1096 | def bulk_delete(self, workspace_name, **kwargs): | |
1097 | # TODO BULK_DELETE_SCHEMA | |
1098 | if not flask.request.json or 'severities' not in flask.request.json: | |
1099 | return BulkDeleteWorkspacedMixin.bulk_delete(self, workspace_name, **kwargs) | |
1100 | return self._perform_bulk_delete(flask.request.json['severities'], by='severity', | |
1101 | workspace_name=workspace_name, **kwargs), 200 | |
1102 | bulk_delete.__doc__ = BulkDeleteWorkspacedMixin.bulk_delete.__doc__ | |
1103 | ||
1104 | def _bulk_update_query(self, ids, **kwargs): | |
1105 | # It IS better to as is but warn of ON CASCADE | |
1106 | query = self.model_class.query.filter(self.model_class.id.in_(ids)) | |
1107 | workspace = self._get_workspace(kwargs.pop("workspace_name")) | |
1108 | return query.filter(self.model_class.workspace_id == workspace.id) | |
1109 | ||
1110 | def _bulk_delete_query(self, ids, **kwargs): | |
1111 | # It IS better to as is but warn of ON CASCADE | |
1112 | if kwargs.get("by", "id") != "severity": | |
1113 | query = self.model_class.query.filter(self.model_class.id.in_(ids)) | |
1114 | else: | |
1115 | query = self.model_class.query.filter(self.model_class.severity.in_(ids)) | |
1116 | workspace = self._get_workspace(kwargs.pop("workspace_name")) | |
1117 | return query.filter(self.model_class.workspace_id == workspace.id) | |
1118 | ||
1119 | def _get_model_association_proxy_fields(self): | |
1120 | return [ | |
1121 | field.target_collection | |
1122 | for field in inspect(self.model_class).all_orm_descriptors | |
1123 | if field.extension_type.name == "ASSOCIATION_PROXY" | |
1124 | ] | |
1125 | ||
1126 | def _pre_bulk_update(self, data, **kwargs): | |
1127 | data.pop('type', '') # It's forbidden to change vuln type! | |
1128 | data.pop('tool', '') | |
1129 | data.pop('service_id', '') | |
1130 | data.pop('host_id', '') | |
1131 | # TODO For now, we don't want to accept multiples attachments; moreover, attachments have its own endpoint | |
1132 | data.pop('_attachments', []) | |
1133 | super()._pre_bulk_update(data, **kwargs) | |
1134 | ||
1135 | model_association_proxy_fields = self._get_model_association_proxy_fields() | |
1136 | association_proxy_fields = {} | |
1137 | for key in list(data): | |
1138 | parent = getattr(VulnerabilityWeb, key).parent | |
1139 | field_name = getattr(parent, "target_collection", None) | |
1140 | if field_name and field_name in model_association_proxy_fields: | |
1141 | association_proxy_fields[key] = data.pop(key) | |
1142 | return association_proxy_fields | |
1143 | ||
1144 | def _post_bulk_update(self, ids, extracted_data, workspace_name, **kwargs): | |
1145 | if extracted_data: | |
1146 | queryset = self._bulk_update_query( | |
1147 | ids, | |
1148 | workspace_name=workspace_name, | |
1149 | **kwargs) | |
1150 | for obj in queryset.all(): | |
1151 | for (key, value) in extracted_data.items(): | |
1152 | setattr(obj, key, value) | |
1153 | db.session.add(obj) | |
1154 | ||
1132 | 1155 | |
1133 | 1156 | VulnerabilityView.register(vulns_api) |
0 | 0 | # Faraday Penetration Test IDE |
1 | 1 | # Copyright (C) 2016 Infobyte LLC (http://www.infobytesec.com/) |
2 | 2 | # See the file 'doc/LICENSE' for the license information |
3 | from datetime import timedelta, date | |
4 | ||
3 | 5 | import re |
4 | from builtins import str | |
5 | 6 | |
6 | 7 | import json |
7 | 8 | import logging |
9 | from itertools import groupby | |
8 | 10 | |
9 | 11 | import flask |
10 | 12 | from flask import Blueprint, abort, make_response, jsonify |
15 | 17 | ) |
16 | 18 | from sqlalchemy.orm.exc import NoResultFound |
17 | 19 | |
18 | ||
19 | 20 | from faraday.server.models import (db, |
20 | 21 | Workspace, |
21 | 22 | _make_vuln_count_property, |
22 | 23 | Vulnerability, |
23 | 24 | _make_active_agents_count_property, |
24 | 25 | count_vulnerability_severities, |
25 | _last_run_agent_date) | |
26 | _last_run_agent_date, | |
27 | SeveritiesHistogram) | |
26 | 28 | from faraday.server.schemas import ( |
27 | 29 | JSTimestampField, |
28 | 30 | MutableField, |
29 | 31 | PrimaryKeyRelatedField, |
30 | 32 | SelfNestedField, |
31 | 33 | ) |
32 | from faraday.server.api.base import ReadWriteView, AutoSchema, FilterMixin | |
34 | from faraday.server.api.base import ReadWriteView, AutoSchema, FilterMixin, BulkDeleteMixin | |
33 | 35 | |
34 | 36 | logger = logging.getLogger(__name__) |
35 | 37 | |
54 | 56 | total_vulns = fields.Integer(dump_only=True, allow_none=False, attribute='vulnerability_total_count') |
55 | 57 | |
56 | 58 | |
59 | class HistogramSchema(Schema): | |
60 | date = fields.Date(dump_only=True, attribute='date') | |
61 | medium = fields.Integer(dump_only=True, attribute='medium') | |
62 | high = fields.Integer(dump_only=True, attribute='high') | |
63 | critical = fields.Integer(dump_only=True, attribute='critical') | |
64 | confirmed = fields.Integer(dump_only=True, attribute='confirmed') | |
65 | ||
66 | ||
57 | 67 | class WorkspaceDurationSchema(Schema): |
58 | 68 | start_date = JSTimestampField(attribute='start_date') |
59 | 69 | end_date = JSTimestampField(attribute='end_date') |
63 | 73 | blacklist = ["filter"] |
64 | 74 | if name in blacklist: |
65 | 75 | raise ValidationError(f"Not possible to create workspace of name: {name}") |
66 | if not re.match(r"^[a-z0-9][a-z0-9_$()+-]*$", name): | |
76 | if not re.match(r"^[a-z0-9][a-z0-9_$()+-]{0,250}$", name): | |
67 | 77 | raise ValidationError("The workspace name must validate with the regex " |
68 | "^[a-z0-9][a-z0-9_$()+-]*$") | |
78 | "^[a-z0-9][a-z0-9_$()+-]{0,250}$") | |
69 | 79 | |
70 | 80 | |
71 | 81 | class WorkspaceSchema(AutoSchema): |
84 | 94 | update_date = fields.DateTime(attribute='update_date', dump_only=True) |
85 | 95 | active_agents_count = fields.Integer(dump_only=True) |
86 | 96 | last_run_agent_date = fields.DateTime(dump_only=True, attribute='last_run_agent_date') |
97 | histogram = fields.Nested(HistogramSchema(many=True)) | |
87 | 98 | |
88 | 99 | class Meta: |
89 | 100 | model = Workspace |
90 | 101 | fields = ('_id', 'id', 'customer', 'description', 'active', |
91 | 102 | 'duration', 'name', 'public', 'scope', 'stats', |
92 | 103 | 'create_date', 'update_date', 'readonly', |
93 | 'active_agents_count', 'last_run_agent_date') | |
104 | 'active_agents_count', 'last_run_agent_date', 'histogram') | |
94 | 105 | |
95 | 106 | @post_load |
96 | 107 | def post_load_duration(self, data, **kwargs): |
104 | 115 | return data |
105 | 116 | |
106 | 117 | |
107 | class WorkspaceView(ReadWriteView, FilterMixin): | |
118 | def init_date_range(from_day, days): | |
119 | date_list = [{'date': from_day - timedelta(days=x), | |
120 | Vulnerability.SEVERITY_MEDIUM: 0, | |
121 | Vulnerability.SEVERITY_HIGH: 0, | |
122 | Vulnerability.SEVERITY_CRITICAL: 0, | |
123 | 'confirmed': 0} for x in range(days)] | |
124 | return date_list | |
125 | ||
126 | ||
127 | def generate_histogram(from_date, days_before): | |
128 | histogram_dict = dict() | |
129 | ||
130 | workspaces_histograms = SeveritiesHistogram.query \ | |
131 | .order_by(SeveritiesHistogram.workspace_id.asc(), SeveritiesHistogram.date.asc()).all() | |
132 | ||
133 | # group dates by workspace | |
134 | grouped_histograms_by_ws = groupby(workspaces_histograms, lambda x: x.workspace.name) | |
135 | ||
136 | ws_histogram = {} | |
137 | for ws_name, dates in grouped_histograms_by_ws: | |
138 | first_date = None | |
139 | ws_histogram[ws_name] = {} | |
140 | # Convert to dict | |
141 | for d in dates: | |
142 | if first_date is None: | |
143 | first_date = d.date | |
144 | ws_histogram[ws_name][d.date] = {Vulnerability.SEVERITY_MEDIUM: d.medium, | |
145 | Vulnerability.SEVERITY_HIGH: d.high, | |
146 | Vulnerability.SEVERITY_CRITICAL: d.critical, | |
147 | 'confirmed': d.confirmed} | |
148 | ||
149 | # fix histogram gaps | |
150 | if (date.today() - first_date).days < days_before: | |
151 | # move first_date to diff between first day and days required | |
152 | first_date = first_date - timedelta(days=(days_before - (date.today() - first_date).days)) | |
153 | histogram_dict[ws_name] = [{'date': first_date + timedelta(days=x), | |
154 | Vulnerability.SEVERITY_MEDIUM: 0, | |
155 | Vulnerability.SEVERITY_HIGH: 0, | |
156 | Vulnerability.SEVERITY_CRITICAL: 0, | |
157 | 'confirmed': 0} | |
158 | for x in range((date.today() - first_date).days + 1)] | |
159 | ||
160 | # merge counters with days required | |
161 | confirmed = high = medium = critical = 0 | |
162 | for current_workspace_histogram_counters in histogram_dict[ws_name]: | |
163 | current_date = current_workspace_histogram_counters['date'] | |
164 | if current_date in ws_histogram[ws_name]: | |
165 | medium += ws_histogram[ws_name][current_date][Vulnerability.SEVERITY_MEDIUM] | |
166 | high += ws_histogram[ws_name][current_date][Vulnerability.SEVERITY_HIGH] | |
167 | critical += ws_histogram[ws_name][current_date][Vulnerability.SEVERITY_CRITICAL] | |
168 | confirmed += ws_histogram[ws_name][current_date]['confirmed'] | |
169 | current_workspace_histogram_counters[Vulnerability.SEVERITY_MEDIUM] = medium | |
170 | current_workspace_histogram_counters[Vulnerability.SEVERITY_HIGH] = high | |
171 | current_workspace_histogram_counters[Vulnerability.SEVERITY_CRITICAL] = critical | |
172 | current_workspace_histogram_counters['confirmed'] = confirmed | |
173 | histogram_dict[ws_name] = histogram_dict[ws_name][-days_before:] | |
174 | ||
175 | return histogram_dict | |
176 | ||
177 | ||
178 | class WorkspaceView(ReadWriteView, FilterMixin, BulkDeleteMixin): | |
108 | 179 | route_base = 'ws' |
109 | 180 | lookup_field = 'name' |
110 | 181 | lookup_field_type = str |
129 | 200 | 200: |
130 | 201 | description: Ok |
131 | 202 | """ |
203 | histogram = flask.request.args.get('histogram', type=lambda v: v.lower() == 'true') | |
204 | ||
205 | if histogram: | |
206 | today = date.today() | |
207 | ||
208 | histogram_days = flask.request.args.get('histogram_days', | |
209 | type=lambda x: int(x) | |
210 | if x.isnumeric() and int(x) > 0 | |
211 | else SeveritiesHistogram.DEFAULT_DAYS_BEFORE, | |
212 | default=SeveritiesHistogram.DEFAULT_DAYS_BEFORE | |
213 | ) | |
214 | histogram_dict = generate_histogram(today, histogram_days) | |
215 | ||
132 | 216 | query = self._get_base_query() |
217 | ||
133 | 218 | objects = [] |
134 | 219 | for workspace_stat in query: |
135 | 220 | workspace_stat_dict = dict(workspace_stat) |
142 | 227 | workspace_stat_dict['scope_raw'] = workspace_stat_dict['scope_raw'].split(',') |
143 | 228 | for scope in workspace_stat_dict['scope_raw']: |
144 | 229 | workspace_stat_dict['scope'].append({'name': scope}) |
230 | ||
231 | if histogram: | |
232 | if workspace_stat_dict['name'] in histogram_dict: | |
233 | workspace_stat_dict['histogram'] = histogram_dict[workspace_stat_dict['name']] | |
234 | else: | |
235 | workspace_stat_dict['histogram'] = init_date_range(today, histogram_days) | |
236 | ||
145 | 237 | objects.append(workspace_stat_dict) |
146 | 238 | return self._envelope_list(self._dump(objects, kwargs, many=True)) |
147 | 239 | |
361 | 453 | db.session.commit() |
362 | 454 | return self._get_object(workspace_id).readonly |
363 | 455 | |
456 | def _bulk_delete_query(self, ids, **kwargs): | |
457 | # It IS better to as is but warn of ON CASCADE | |
458 | return self.model_class.query.filter(self.model_class.name.in_(ids)) | |
459 | ||
364 | 460 | |
365 | 461 | WorkspaceView.register(workspace_api) |
101 | 101 | # Custom reset password |
102 | 102 | from faraday.server.api.modules.auth import auth # pylint:disable=import-outside-toplevel |
103 | 103 | from faraday.server.websockets import websockets # pylint:disable=import-outside-toplevel |
104 | from faraday.server.api.modules.settings_reports import reports_settings_api # pylint:disable=import-outside-toplevel | |
104 | from faraday.server.api.modules.settings_reports import \ | |
105 | reports_settings_api # pylint:disable=import-outside-toplevel | |
105 | 106 | from faraday.server.api.modules.settings_dashboard import \ |
106 | 107 | dashboard_settings_api # pylint:disable=import-outside-toplevel |
107 | 108 | |
268 | 269 | user_ip = request.headers.get('X-Forwarded-For', request.remote_addr) |
269 | 270 | user_logout_at = datetime.datetime.utcnow() |
270 | 271 | audit_logger.info(f"User [{user.username}] logged out from IP [{user_ip}] at [{user_logout_at}]") |
272 | logger.info(f"User [{user.username}] logged out from IP [{user_ip}] at [{user_logout_at}]") | |
271 | 273 | |
272 | 274 | |
273 | 275 | def user_logged_in_succesfull(app, user): |
288 | 290 | user_ip = request.headers.get('X-Forwarded-For', request.remote_addr) |
289 | 291 | user_login_at = datetime.datetime.utcnow() |
290 | 292 | audit_logger.info(f"User [{user.username}] logged in from IP [{user_ip}] at [{user_login_at}]") |
293 | logger.info(f"User [{user.username}] logged in from IP [{user_ip}] at [{user_login_at}]") | |
291 | 294 | |
292 | 295 | |
293 | 296 | def uia_username_mapper(identity): |
484 | 487 | # want to skip the LoginForm validate logic |
485 | 488 | if not super(LoginForm, self).validate(): |
486 | 489 | audit_logger.warning(f"Invalid Login - User [{self.email.data}] from IP [{user_ip}] at [{time_now}]") |
490 | logger.warning(f"Invalid Login - User [{self.email.data}] from IP [{user_ip}] at [{time_now}]") | |
487 | 491 | return False |
488 | 492 | self.email.data = remove_null_caracters(self.email.data) |
489 | 493 | |
492 | 496 | if self.user is None: |
493 | 497 | audit_logger.warning(f"Invalid Login - User [{self.email.data}] from IP [{user_ip}] at [{time_now}] - " |
494 | 498 | f"Reason: [Invalid Username]") |
499 | logger.warning(f"Invalid Login - User [{self.email.data}] from IP [{user_ip}] at [{time_now}] - " | |
500 | f"Reason: [Invalid Username]") | |
495 | 501 | self.email.errors.append(get_message('USER_DOES_NOT_EXIST')[0]) |
496 | 502 | return False |
497 | 503 | |
499 | 505 | if not self.user.password: |
500 | 506 | audit_logger.warning(f"Invalid Login - User [{self.email.data}] from IP [{user_ip}] at [{time_now}] - " |
501 | 507 | f"Reason: [Invalid Password]") |
508 | logger.warning(f"Invalid Login - User [{self.email.data}] from IP [{user_ip}] at [{time_now}] - " | |
509 | f"Reason: [Invalid Password]") | |
502 | 510 | self.email.errors.append(get_message('USER_DOES_NOT_EXIST')[0]) |
503 | 511 | return False |
504 | 512 | self.password.data = remove_null_caracters(self.password.data) |
505 | 513 | if not verify_and_update_password(self.password.data, self.user): |
506 | 514 | audit_logger.warning(f"Invalid Login - User [{self.email.data}] from IP [{user_ip}] at [{time_now}] - " |
507 | 515 | f"Reason: [Invalid Password]") |
516 | logger.warning(f"Invalid Login - User [{self.email.data}] from IP [{user_ip}] at [{time_now}] - " | |
517 | f"Reason: [Invalid Password]") | |
508 | 518 | self.email.errors.append(get_message('USER_DOES_NOT_EXIST')[0]) |
509 | 519 | return False |
510 | 520 | # if requires_confirmation(self.user): |
513 | 523 | if not self.user.is_active: |
514 | 524 | audit_logger.warning(f"Invalid Login - User [{self.email.data}] from IP [{user_ip}] at [{time_now}] - " |
515 | 525 | f"Reason: [Disabled Account]") |
526 | logger.warning(f"Invalid Login - User [{self.email.data}] from IP [{user_ip}] at [{time_now}] - " | |
527 | f"Reason: [Disabled Account]") | |
516 | 528 | self.email.errors.append(get_message('DISABLED_ACCOUNT')[0]) |
517 | 529 | return False |
518 | 530 | return True |
22 | 22 | print(f"Username {current_username} changed to {new_username}") |
23 | 23 | else: |
24 | 24 | print("Username not changed.") |
25 | ||
26 | ||
27 | # I'm Py3 |
89 | 89 | custom_field_data.field_display_name = field_display_name |
90 | 90 | custom_field_data.field_type = field_type |
91 | 91 | db.session.commit() |
92 | # I'm Py3 |
69 | 69 | ) |
70 | 70 | graph.write_png('uml_schema.png') # write out the file |
71 | 71 | print("Graph written to fle uml_schema.png") |
72 | # I'm Py3 |
3 | 3 | See the file 'doc/LICENSE' for the license information |
4 | 4 | |
5 | 5 | """ |
6 | from builtins import input | |
7 | 6 | |
8 | 7 | import getpass |
9 | 8 | import string |
336 | 335 | print(f'{Fore.BLUE}MAC OS detected{Fore.WHITE}') |
337 | 336 | postgres_command = ['psql', 'postgres'] |
338 | 337 | password = self.generate_random_pw(25) |
339 | command = postgres_command + ['-c', 'CREATE ROLE {0} WITH LOGIN PASSWORD \'{1}\';'.format(username, password)] | |
338 | command = postgres_command + ['-c', f'CREATE ROLE {username} WITH LOGIN PASSWORD \'{password}\';'] | |
340 | 339 | p = Popen(command, stderr=psql_log_file, stdout=psql_log_file) # nosec |
341 | 340 | p.wait() |
342 | 341 | psql_log_file.seek(0) |
26 | 26 | if name: |
27 | 27 | name = name.lower() |
28 | 28 | available_settings = get_all_settings() |
29 | if action in ('show', 'update'): | |
29 | if action in ('show', 'update', 'clear'): | |
30 | 30 | if not name: |
31 | 31 | click.secho(f"You must indicate a settings name to {action}", fg="red") |
32 | 32 | sys.exit(1) |
79 | 79 | click.secho("Updated!!", fg='green') |
80 | 80 | else: |
81 | 81 | click.secho("No changes where made to the settings", fg="green") |
82 | ||
82 | elif action == "clear": | |
83 | click.secho(f"Clear settings for: {name}", fg="green") | |
84 | settings.delete_configuration() | |
83 | 85 | else: |
84 | 86 | click.secho("Available settings:", fg="green") |
85 | 87 | for i in available_settings: |
14 | 14 | confirm = click.prompt('Confirm [Y/n]', type=bool) |
15 | 15 | if confirm: |
16 | 16 | version = sys.version_info |
17 | static_path = f"/opt/faraday/lib/python{version.major}.{version.minor}/site-packages/faraday/server/www/" | |
17 | static_path = f"/opt/faraday/lib/python{version.major}.{version.minor}/site-packages/faraday/server/www" | |
18 | 18 | templates_path = Path(__file__).parent / 'templates' |
19 | 19 | file_loader = FileSystemLoader(templates_path) |
20 | 20 | env = Environment(loader=file_loader, autoescape=True) |
8 | 8 | ssl_certificate {{ ssl_certificate }}; |
9 | 9 | ssl_certificate_key {{ ssl_key }}; |
10 | 10 | |
11 | root {{ static_path }}; | |
12 | index index.html index.htm; | |
13 | ||
11 | 14 | location /{% if multitenant_url %}{{ multitenant_url }}/{% endif %} { |
12 | alias {{ static_path }}; | |
15 | try_files $uri $uri/ /index.html; | |
13 | 16 | } |
14 | 17 | |
15 | 18 | location {% if multitenant_url %}/{{ multitenant_url }}{% endif %}/_api/ { |
6 | 6 | import sys |
7 | 7 | import logging |
8 | 8 | import inspect |
9 | from datetime import date | |
9 | 10 | from queue import Queue |
10 | 11 | |
11 | 12 | from sqlalchemy import event |
13 | from sqlalchemy.dialects import postgresql | |
14 | from sqlalchemy.orm import Query | |
15 | from sqlalchemy.orm.attributes import get_history | |
12 | 16 | |
13 | 17 | from faraday.server.models import ( |
14 | 18 | Host, |
16 | 20 | TagObject, |
17 | 21 | Comment, |
18 | 22 | File, |
23 | SeveritiesHistogram, | |
24 | Vulnerability, | |
25 | VulnerabilityWeb, | |
26 | VulnerabilityGeneric, | |
19 | 27 | ) |
20 | 28 | from faraday.server.models import db |
21 | 29 | |
97 | 105 | "This should never happen!!!" |
98 | 106 | |
99 | 107 | |
108 | def _create_or_update_histogram(connection, workspace_id=None, medium=0, high=0, critical=0, confirmed=0): | |
109 | if workspace_id is None: | |
110 | logger.error("Workspace with None value. Histogram could not be updated") | |
111 | return | |
112 | ws_id = SeveritiesHistogram.query.with_entities('id').filter( | |
113 | SeveritiesHistogram.date == date.today(), | |
114 | SeveritiesHistogram.workspace_id == workspace_id).first() | |
115 | if ws_id is None: | |
116 | connection.execute( | |
117 | f"INSERT " # nosec | |
118 | f"INTO severities_histogram (workspace_id, medium, high, critical, date, confirmed) " | |
119 | f"VALUES ({workspace_id}, {medium}, {high}, {critical}, '{date.today()}', {confirmed})") | |
120 | else: | |
121 | connection.execute( | |
122 | f"UPDATE severities_histogram " # nosec | |
123 | f"SET medium = medium + {medium}, " | |
124 | f"high = high + {high}, " | |
125 | f"critical = critical + {critical}, " | |
126 | f"confirmed = confirmed + {confirmed} " | |
127 | f"WHERE id = {ws_id[0]}") | |
128 | ||
129 | ||
130 | def _dicrease_severities_histogram(instance_severity, medium=0, high=0, critical=0): | |
131 | medium = -1 if instance_severity == Vulnerability.SEVERITY_MEDIUM else medium | |
132 | high = -1 if instance_severity == Vulnerability.SEVERITY_HIGH else high | |
133 | critical = -1 if instance_severity == Vulnerability.SEVERITY_CRITICAL else critical | |
134 | ||
135 | return medium, high, critical | |
136 | ||
137 | ||
138 | def _increase_severities_histogram(instance_severity, medium=0, high=0, critical=0): | |
139 | medium = 1 if instance_severity == Vulnerability.SEVERITY_MEDIUM else medium | |
140 | high = 1 if instance_severity == Vulnerability.SEVERITY_HIGH else high | |
141 | critical = 1 if instance_severity == Vulnerability.SEVERITY_CRITICAL else critical | |
142 | ||
143 | return medium, high, critical | |
144 | ||
145 | ||
146 | def alter_histogram_on_insert(mapper, connection, instance): | |
147 | if instance.severity in SeveritiesHistogram.SEVERITIES_ALLOWED: | |
148 | medium, high, critical = _increase_severities_histogram(instance.severity) | |
149 | confirmed = 1 if instance.confirmed else 0 | |
150 | ||
151 | _create_or_update_histogram(connection, | |
152 | instance.workspace_id, | |
153 | medium=medium, | |
154 | high=high, | |
155 | critical=critical, | |
156 | confirmed=confirmed) | |
157 | ||
158 | ||
159 | def alter_histogram_on_update(mapper, connection, instance): | |
160 | alter_histogram_on_update_general(connection, | |
161 | instance.workspace_id, | |
162 | status_history=get_history(instance, 'status'), | |
163 | confirmed_history=get_history(instance, 'confirmed'), | |
164 | severity_history=get_history(instance, 'severity')) | |
165 | ||
166 | ||
167 | def alter_histogram_on_update_general(connection, workspace_id, status_history=None, | |
168 | confirmed_history=None, severity_history=None): | |
169 | ||
170 | if not status_history or not confirmed_history or not severity_history: | |
171 | logger.error("Not all history fields provided") | |
172 | return | |
173 | ||
174 | if len(confirmed_history.unchanged) > 0: | |
175 | confirmed_counter = 0 | |
176 | confirmed_counter_on_close = -1 if confirmed_history.unchanged[0] is True else 0 | |
177 | confirmed_counter_on_reopen = 1 if confirmed_history.unchanged[0] is True else 0 | |
178 | else: | |
179 | if not confirmed_history.deleted or not confirmed_history.added: | |
180 | logger.error("Confirmed history deleted or added is None. Could not update confirmed value.") | |
181 | return | |
182 | if confirmed_history.deleted[0] is True: | |
183 | confirmed_counter = -1 | |
184 | confirmed_counter_on_close = confirmed_counter | |
185 | confirmed_counter_on_reopen = 0 | |
186 | else: | |
187 | confirmed_counter = 1 | |
188 | confirmed_counter_on_close = 0 | |
189 | confirmed_counter_on_reopen = confirmed_counter | |
190 | ||
191 | if len(status_history.unchanged) > 0: | |
192 | if len(severity_history.unchanged) > 0: | |
193 | if confirmed_counter != 0 and status_history.unchanged[0] in [Vulnerability.STATUS_OPEN, Vulnerability.STATUS_RE_OPENED]: | |
194 | _create_or_update_histogram(connection, workspace_id, confirmed=confirmed_counter) | |
195 | return | |
196 | medium = high = critical = 0 | |
197 | if not severity_history.deleted or not severity_history.added: | |
198 | if confirmed_counter != 0 and status_history.unchanged[0] in [Vulnerability.STATUS_OPEN, Vulnerability.STATUS_RE_OPENED]: | |
199 | _create_or_update_histogram(connection, workspace_id, confirmed=confirmed_counter) | |
200 | logger.error("Severity history deleted or added is None. Could not update severity histogram.") | |
201 | return | |
202 | ||
203 | if severity_history.deleted[0] in SeveritiesHistogram.SEVERITIES_ALLOWED: | |
204 | medium, high, critical = _dicrease_severities_histogram(severity_history.deleted[0]) | |
205 | ||
206 | if severity_history.added[0] in SeveritiesHistogram.SEVERITIES_ALLOWED: | |
207 | medium, high, critical = _increase_severities_histogram(severity_history.added[0], | |
208 | medium=medium, | |
209 | high=high, | |
210 | critical=critical) | |
211 | _create_or_update_histogram(connection, | |
212 | workspace_id, | |
213 | medium=medium, | |
214 | high=high, | |
215 | critical=critical, | |
216 | confirmed=confirmed_counter) | |
217 | ||
218 | elif status_history.added[0] in [Vulnerability.STATUS_CLOSED, Vulnerability.STATUS_RISK_ACCEPTED]\ | |
219 | and status_history.deleted[0] in [Vulnerability.STATUS_OPEN, Vulnerability.STATUS_RE_OPENED]: | |
220 | if len(severity_history.unchanged) > 0: | |
221 | severity = severity_history.unchanged[0] | |
222 | if len(severity_history.deleted) > 0: | |
223 | severity = severity_history.deleted[0] | |
224 | if severity in SeveritiesHistogram.SEVERITIES_ALLOWED: | |
225 | medium, high, critical = _dicrease_severities_histogram(severity) | |
226 | _create_or_update_histogram(connection, workspace_id, medium=medium, high=high, | |
227 | critical=critical, confirmed=confirmed_counter_on_close) | |
228 | elif status_history.added[0] in [Vulnerability.STATUS_OPEN, Vulnerability.STATUS_RE_OPENED] \ | |
229 | and status_history.deleted[0] in [Vulnerability.STATUS_CLOSED, Vulnerability.STATUS_RISK_ACCEPTED]: | |
230 | if len(severity_history.unchanged) > 0: | |
231 | severity = severity_history.unchanged[0] | |
232 | if len(severity_history.added) > 0: | |
233 | severity = severity_history.added[0] | |
234 | if severity in SeveritiesHistogram.SEVERITIES_ALLOWED: | |
235 | medium, high, critical = _increase_severities_histogram(severity) | |
236 | _create_or_update_histogram(connection, workspace_id, medium=medium, high=high, | |
237 | critical=critical, confirmed=confirmed_counter_on_reopen) | |
238 | elif confirmed_counter != 0: | |
239 | _create_or_update_histogram(connection, workspace_id, confirmed=confirmed_counter) | |
240 | ||
241 | ||
242 | def alter_histogram_on_delete(mapper, connection, instance): | |
243 | if instance.status in [Vulnerability.STATUS_OPEN, Vulnerability.STATUS_RE_OPENED]: | |
244 | confirmed = -1 if instance.confirmed is True else 0 | |
245 | if instance.severity in SeveritiesHistogram.SEVERITIES_ALLOWED: | |
246 | medium, high, critical = _dicrease_severities_histogram(instance.severity) | |
247 | _create_or_update_histogram(connection, instance.workspace_id, | |
248 | medium=medium, | |
249 | high=high, | |
250 | critical=critical, | |
251 | confirmed=confirmed) | |
252 | ||
253 | ||
254 | def alter_histogram_on_before_compile_delete(query, delete_context): | |
255 | for desc in query.column_descriptions: | |
256 | if desc['type'] is Vulnerability or \ | |
257 | desc['type'] is VulnerabilityGeneric or\ | |
258 | desc['type'] is VulnerabilityWeb: | |
259 | instances = query.all() | |
260 | for instance in instances: | |
261 | if instance.status in [Vulnerability.STATUS_OPEN, Vulnerability.STATUS_RE_OPENED]: | |
262 | if instance.severity in SeveritiesHistogram.SEVERITIES_ALLOWED: | |
263 | medium, high, critical = _dicrease_severities_histogram(instance.severity) | |
264 | _create_or_update_histogram(delete_context.session, | |
265 | instance.workspace_id, | |
266 | medium=medium, | |
267 | high=high, | |
268 | critical=critical, | |
269 | confirmed=-1 if instance.confirmed is True else 0) | |
270 | ||
271 | ||
272 | def get_history_from_context_values(context_values, field, old_value): | |
273 | field_history = type('history_dummy_class', (object,), {'added': [], 'unchanged': [old_value], 'deleted': []})() | |
274 | if field in context_values: | |
275 | if context_values[field] != old_value: | |
276 | field_history.deleted.append(old_value) | |
277 | field_history.added.append(context_values[field]) | |
278 | field_history.unchanged.pop() | |
279 | return field_history | |
280 | ||
281 | ||
282 | def alter_histogram_on_before_compile_update(query, update_context): | |
283 | for desc in query.column_descriptions: | |
284 | if desc['type'] is Vulnerability or \ | |
285 | desc['type'] is VulnerabilityGeneric or\ | |
286 | desc['type'] is VulnerabilityWeb: | |
287 | ids = [x[1] for x in filter(lambda x: x[0].startswith("id_"), | |
288 | query.statement.compile(dialect=postgresql.dialect()).params.items())] | |
289 | if ids: | |
290 | # this can arise some issues with counters when other filters were applied to query but... | |
291 | instances = update_context.session.query(VulnerabilityGeneric).filter( | |
292 | VulnerabilityGeneric.id.in_(ids)).all() | |
293 | else: | |
294 | instances = query.all() | |
295 | ||
296 | for instance in instances: | |
297 | status_history = get_history_from_context_values(update_context.values, 'status', instance.status) | |
298 | severity_history = get_history_from_context_values(update_context.values, 'severity', instance.severity) | |
299 | confirmed_history = get_history_from_context_values(update_context.values, 'confirmed', | |
300 | instance.confirmed) | |
301 | ||
302 | alter_histogram_on_update_general(update_context.session, | |
303 | instance.workspace_id, | |
304 | status_history=status_history, | |
305 | confirmed_history=confirmed_history, | |
306 | severity_history=severity_history) | |
307 | ||
308 | ||
100 | 309 | # register the workspace verification for all objs that has workspace_id |
101 | 310 | for name, obj in inspect.getmembers(sys.modules['faraday.server.models']): |
102 | 311 | if inspect.isclass(obj) and getattr(obj, 'workspace_id', None): |
115 | 324 | # Update object bindings |
116 | 325 | event.listen(Host, 'after_update', update_object_event) |
117 | 326 | event.listen(Service, 'after_update', update_object_event) |
327 | ||
328 | # Severities Histogram | |
329 | event.listen(VulnerabilityGeneric, "before_insert", alter_histogram_on_insert, propagate=True) | |
330 | event.listen(VulnerabilityGeneric, "before_update", alter_histogram_on_update, propagate=True) | |
331 | event.listen(VulnerabilityGeneric, "after_delete", alter_histogram_on_delete, propagate=True) | |
332 | event.listen(Query, "before_compile_delete", alter_histogram_on_before_compile_delete) | |
333 | event.listen(Query, "before_compile_update", alter_histogram_on_before_compile_update) |
3 | 3 | See the file 'doc/LICENSE' for the license information |
4 | 4 | |
5 | 5 | """ |
6 | from builtins import str | |
7 | 6 | |
8 | 7 | import json |
9 | 8 | import imghdr |
124 | 123 | if value is not None: |
125 | 124 | value = json.loads(value) |
126 | 125 | return value |
127 | # I'm Py3 |
2 | 2 | # See the file 'doc/LICENSE' for the license information |
3 | 3 | import json |
4 | 4 | import logging |
5 | import math | |
5 | 6 | import operator |
7 | import re | |
6 | 8 | import string |
7 | from datetime import datetime, timedelta | |
9 | from datetime import datetime, timedelta, date | |
8 | 10 | from functools import partial |
9 | 11 | from random import SystemRandom |
12 | from typing import Callable | |
10 | 13 | |
11 | 14 | from sqlalchemy import ( |
12 | 15 | Boolean, |
23 | 26 | event, |
24 | 27 | Table, |
25 | 28 | literal, |
29 | Date, | |
26 | 30 | ) |
27 | 31 | from sqlalchemy.exc import IntegrityError |
28 | 32 | from sqlalchemy.orm import relationship |
449 | 453 | id = Column(Integer, primary_key=True) |
450 | 454 | name = NonBlankColumn(Text) |
451 | 455 | |
452 | host_id = Column(Integer, ForeignKey('host.id'), index=True, nullable=False) | |
456 | host_id = Column(Integer, ForeignKey('host.id', ondelete='CASCADE'), index=True, nullable=False) | |
453 | 457 | host = relationship('Host', backref=backref("hostnames", cascade="all, delete-orphan")) |
454 | 458 | |
455 | 459 | # 1 workspace <--> N hostnames |
494 | 498 | 'difficult', |
495 | 499 | 'infeasible' |
496 | 500 | ] |
501 | ||
502 | SEVERITY_UNCLASSIFIED = 'unclassified' | |
503 | SEVERITY_INFORMATIONAL = 'informational' | |
504 | SEVERITY_LOW = 'low' | |
505 | SEVERITY_MEDIUM = 'medium' | |
506 | SEVERITY_HIGH = 'high' | |
507 | SEVERITY_CRITICAL = 'critical' | |
508 | ||
497 | 509 | SEVERITIES = [ |
498 | 'unclassified', | |
499 | 'informational', | |
500 | 'low', | |
501 | 'medium', | |
502 | 'high', | |
503 | 'critical', | |
510 | SEVERITY_UNCLASSIFIED, | |
511 | SEVERITY_INFORMATIONAL, | |
512 | SEVERITY_LOW, | |
513 | SEVERITY_MEDIUM, | |
514 | SEVERITY_HIGH, | |
515 | SEVERITY_CRITICAL, | |
504 | 516 | ] |
505 | 517 | |
506 | 518 | __abstract__ = True |
531 | 543 | @property |
532 | 544 | def parent(self): |
533 | 545 | raise NotImplementedError('ABC property called') |
546 | ||
547 | ||
548 | class SeveritiesHistogram(db.Model): | |
549 | __tablename__ = "severities_histogram" | |
550 | ||
551 | SEVERITIES_ALLOWED = [VulnerabilityABC.SEVERITY_MEDIUM, | |
552 | VulnerabilityABC.SEVERITY_HIGH, | |
553 | VulnerabilityABC.SEVERITY_CRITICAL] | |
554 | ||
555 | DEFAULT_DAYS_BEFORE = 20 | |
556 | ||
557 | id = Column(Integer, primary_key=True) | |
558 | workspace_id = Column(Integer, ForeignKey('workspace.id'), index=True, nullable=False) | |
559 | workspace = relationship( | |
560 | 'Workspace', | |
561 | foreign_keys=[workspace_id], | |
562 | backref=backref('severities_histogram', cascade="all, delete-orphan") | |
563 | ) | |
564 | date = Column(Date, default=date.today(), nullable=False) | |
565 | medium = Column(Integer, nullable=False) | |
566 | high = Column(Integer, nullable=False) | |
567 | critical = Column(Integer, nullable=False) | |
568 | confirmed = Column(Integer, nullable=False) | |
569 | ||
570 | # This method is required by event :_( | |
571 | @property | |
572 | def parent(self): | |
573 | return | |
534 | 574 | |
535 | 575 | |
536 | 576 | class CustomAssociationSet(_AssociationSet): |
616 | 656 | return creator |
617 | 657 | |
618 | 658 | |
619 | def _build_associationproxy_creator_non_workspaced(model_class_name): | |
659 | def _build_associationproxy_creator_non_workspaced(model_class_name, preprocess_value_func: Callable = None): | |
620 | 660 | def creator(name, vulnerability): |
621 | 661 | """Get or create a reference/policyviolation/CVE with the |
622 | 662 | corresponding name. This is not workspace aware""" |
624 | 664 | # Ugly hack to avoid the fact that Reference is defined after |
625 | 665 | # Vulnerability |
626 | 666 | model_class = globals()[model_class_name] |
667 | ||
668 | if preprocess_value_func: | |
669 | name = preprocess_value_func(name) | |
670 | ||
627 | 671 | child = model_class.query.filter( |
628 | 672 | getattr(model_class, 'name') == name, |
629 | 673 | ).first() |
683 | 727 | object_type = Column(Enum(*OBJECT_TYPES, name='object_types'), nullable=False) |
684 | 728 | |
685 | 729 | command = relationship('Command', backref='command_objects') |
686 | command_id = Column(Integer, ForeignKey('command.id'), index=True) | |
730 | command_id = Column(Integer, ForeignKey('command.id', ondelete='SET NULL'), index=True) | |
687 | 731 | |
688 | 732 | # 1 workspace <--> N command_objects |
689 | 733 | # 1 to N (the FK is placed in the child) and bidirectional (backref) |
792 | 836 | |
793 | 837 | # 1 workspace <--> N commands |
794 | 838 | # 1 to N (the FK is placed in the child) and bidirectional (backref) |
795 | workspace_id = Column(Integer, ForeignKey('workspace.id'), index=True, nullable=False) | |
839 | workspace_id = Column(Integer, ForeignKey('workspace.id', ondelete="CASCADE"), index=True, nullable=False) | |
796 | 840 | workspace = relationship( |
797 | 841 | 'Workspace', |
798 | 842 | foreign_keys=[workspace_id], |
1032 | 1076 | raise ValueError("Invalid cve format. Should be CVE-YEAR-NUMBERID.") |
1033 | 1077 | |
1034 | 1078 | |
1079 | class CVSS2GeneralConfig: | |
1080 | VERSION = '2' | |
1081 | PATTERN = 'AV:(?P<access_vector>[LAN])' \ | |
1082 | '/AC:(?P<access_complexity>[HML])' \ | |
1083 | '/Au:(?P<authentication>[MSN])' \ | |
1084 | '/C:(?P<confidentiality>[NPC])' \ | |
1085 | '/I:(?P<integrity>[NPC])' \ | |
1086 | '/A:(?P<availability>[NPC])' | |
1087 | ||
1088 | # CVSSV2 ENUMS | |
1089 | ACCESS_VECTOR_TYPES = ['L', 'N', 'A'] | |
1090 | ACCESS_COMPLEXITY_TYPES = ['L', 'M', 'H'] | |
1091 | AUTHENTICATION_TYPES = ['N', 'S', 'M'] | |
1092 | IMPACT_TYPES_V2 = ['N', 'P', 'C'] | |
1093 | ||
1094 | # CVSSV2 SCORE | |
1095 | ACCESS_VECTOR_SCORE = {'L': 0.395, 'A': 0.646, 'N': 1.0} | |
1096 | ACCESS_COMPLEXITY_SCORE = {'L': 0.71, 'M': 0.61, 'H': 0.35} | |
1097 | AUTHENTICATION_SCORE = {'N': 0.704, 'S': 0.56, 'M': 0.45} | |
1098 | IMPACT_SCORES_V2 = {'N': 0.0, 'P': 0.275, 'C': 0.660} | |
1099 | ||
1100 | ||
1101 | class CVSS3GeneralConfig: | |
1102 | VERSION = '3' | |
1103 | PATTERN = 'AV:(?P<attack_vector>[LANP])' \ | |
1104 | '/AC:(?P<attack_complexity>[HL])' \ | |
1105 | '/PR:(?P<privileges_required>[NLH])' \ | |
1106 | '/UI:(?P<user_interaction>[NR])' \ | |
1107 | '/S:(?P<scope>[UC])' \ | |
1108 | '/C:(?P<confidentiality>[NLH])' \ | |
1109 | '/I:(?P<integrity>[NLH])' \ | |
1110 | '/A:(?P<availability>[NLH])' | |
1111 | ||
1112 | CHANGED = 'C' | |
1113 | UNCHANGED = 'U' | |
1114 | ||
1115 | # CVSSV3 ENUMS | |
1116 | ATTACK_VECTOR_TYPES = ['N', 'A', 'L', 'P'] | |
1117 | ATTACK_COMPLEXITY_TYPES = ['L', 'H'] | |
1118 | PRIVILEGES_REQUIRED_TYPES = ['N', 'L', 'H'] | |
1119 | USER_INTERACTION_TYPES = ['N', 'R'] | |
1120 | SCOPE_TYPES = [UNCHANGED, CHANGED] | |
1121 | IMPACT_TYPES_V3 = ['N', 'L', 'H'] | |
1122 | ||
1123 | # CVSSV3 SCORE | |
1124 | ATTACK_VECTOR_SCORES = {'N': 0.85, 'A': 0.62, 'L': 0.55, 'P': 0.2} | |
1125 | ATTACK_COMPLEXITY_SCORES = {'L': 0.77, 'H': 0.44} | |
1126 | PRIVILEGES_REQUIRED_SCORES = {'U': {'N': 0.85, 'L': 0.62, 'H': 0.27}, | |
1127 | 'C': {'N': 0.85, 'L': 0.68, 'H': 0.5}} | |
1128 | USER_INTERACTION_SCORES = {'N': 0.85, 'R': 0.62} | |
1129 | SCOPE_SCORES = {'U': 6.42, 'C': 7.52} | |
1130 | IMPACT_SCORES_V3 = {'N': 0.0, 'L': 0.22, 'H': 0.56} | |
1131 | ||
1132 | ||
1133 | class CVSSBase(db.Model): | |
1134 | __tablename__ = "cvss_base" | |
1135 | id = Column(Integer, primary_key=True) | |
1136 | version = Column(String(8), nullable=False) | |
1137 | _vector_string = Column('vector_string', String(64)) | |
1138 | _base_score = Column('base_score', Float) | |
1139 | _fixed_base_score = Column('fixed_base_score', Float) | |
1140 | ||
1141 | type = Column(String(24)) | |
1142 | ||
1143 | __mapper_args__ = { | |
1144 | 'polymorphic_on': type, | |
1145 | 'polymorphic_identity': 'base' | |
1146 | } | |
1147 | ||
1148 | @hybrid_property | |
1149 | def vector_string(self): | |
1150 | return self._vector_string | |
1151 | ||
1152 | @vector_string.setter | |
1153 | def vector_string(self, vector_string): | |
1154 | self.assign_vector_string(vector_string) | |
1155 | self.base_score = self.calculate_base_score() | |
1156 | ||
1157 | @hybrid_property | |
1158 | def base_score(self): | |
1159 | if self._base_score is not None: | |
1160 | return self._base_score | |
1161 | return self._fixed_base_score | |
1162 | ||
1163 | @base_score.setter | |
1164 | def base_score(self, base_score): | |
1165 | self._base_score = base_score | |
1166 | ||
1167 | def assign_vector_string(self, vector_string, base_score): | |
1168 | raise NotImplementedError | |
1169 | ||
1170 | def calculate_base_score(self): | |
1171 | raise NotImplementedError | |
1172 | ||
1173 | def __repr__(self): | |
1174 | return f'{self.vector_string}' | |
1175 | ||
1176 | ||
1177 | class CVSSV2(CVSSBase): | |
1178 | __tablename__ = "cvss_v2" | |
1179 | id = Column(Integer, ForeignKey('cvss_base.id'), primary_key=True) | |
1180 | access_vector = Column(Enum(*CVSS2GeneralConfig.ACCESS_VECTOR_TYPES, name="cvss_access_vector")) | |
1181 | access_complexity = Column(Enum(*CVSS2GeneralConfig.ACCESS_COMPLEXITY_TYPES, name="cvss_access_complexity")) | |
1182 | authentication = Column(Enum(*CVSS2GeneralConfig.AUTHENTICATION_TYPES, name="cvss_authentication")) | |
1183 | confidentiality_impact = Column(Enum(*CVSS2GeneralConfig.IMPACT_TYPES_V2, name="cvss_impact_types_v2")) | |
1184 | integrity_impact = Column(Enum(*CVSS2GeneralConfig.IMPACT_TYPES_V2, name="cvss_impact_types_v2")) | |
1185 | availability_impact = Column(Enum(*CVSS2GeneralConfig.IMPACT_TYPES_V2, name="cvss_impact_types_v2")) | |
1186 | ||
1187 | __mapper_args__ = { | |
1188 | 'polymorphic_identity': "v2" | |
1189 | } | |
1190 | ||
1191 | def __init__(self, base_score: Float = None, vector_string=None, **kwargs): | |
1192 | super().__init__(version=CVSS2GeneralConfig.VERSION, vector_string=vector_string, | |
1193 | _fixed_base_score=base_score, **kwargs) | |
1194 | ||
1195 | def assign_vector_string(self, vector_string): | |
1196 | self._vector_string = vector_string | |
1197 | vector_string_parsed = re.match(CVSS2GeneralConfig.PATTERN, vector_string if vector_string else '') | |
1198 | if vector_string_parsed: | |
1199 | self.access_vector = vector_string_parsed['access_vector'] | |
1200 | self.access_complexity = vector_string_parsed['access_complexity'] | |
1201 | self.authentication = vector_string_parsed['authentication'] | |
1202 | self.confidentiality_impact = vector_string_parsed['confidentiality'] | |
1203 | self.integrity_impact = vector_string_parsed['integrity'] | |
1204 | self.availability_impact = vector_string_parsed['availability'] | |
1205 | else: | |
1206 | self.access_vector = None | |
1207 | self.access_complexity = None | |
1208 | self.authentication = None | |
1209 | self.confidentiality_impact = None | |
1210 | self.integrity_impact = None | |
1211 | self.availability_impact = None | |
1212 | ||
1213 | def exploitability(self): | |
1214 | return 20 * CVSS2GeneralConfig.ACCESS_VECTOR_SCORE[self.access_vector] * CVSS2GeneralConfig.ACCESS_COMPLEXITY_SCORE[self.access_complexity] * CVSS2GeneralConfig.AUTHENTICATION_SCORE[self.authentication] | |
1215 | ||
1216 | def impact(self): | |
1217 | return 10.41 * (1 - (1 - CVSS2GeneralConfig.IMPACT_SCORES_V2[self.confidentiality_impact]) * (1 - CVSS2GeneralConfig.IMPACT_SCORES_V2[self.integrity_impact]) * (1 - CVSS2GeneralConfig.IMPACT_SCORES_V2[self.availability_impact])) | |
1218 | ||
1219 | def fimpact(self): | |
1220 | if self.impact() == 0: | |
1221 | return 0 | |
1222 | return 1.176 | |
1223 | ||
1224 | def calculate_base_score(self): | |
1225 | if re.match(CVSS2GeneralConfig.PATTERN, self.vector_string if self.vector_string else ''): | |
1226 | score = (0.6 * self.impact() + 0.4 * self.exploitability() - 1.5) * self.fimpact() | |
1227 | return round(score, 1) # pylint: disable=round-builtin | |
1228 | return None | |
1229 | ||
1230 | ||
1231 | class CVSSV3(CVSSBase): | |
1232 | __tablename__ = "cvss_v3" | |
1233 | id = Column(Integer, ForeignKey('cvss_base.id'), primary_key=True) | |
1234 | attack_vector = Column(Enum(*CVSS3GeneralConfig.ATTACK_VECTOR_TYPES, name="cvss_attack_vector")) | |
1235 | attack_complexity = Column(Enum(*CVSS3GeneralConfig.ATTACK_COMPLEXITY_TYPES, name="cvss_attack_complexity")) | |
1236 | privileges_required = Column(Enum(*CVSS3GeneralConfig.PRIVILEGES_REQUIRED_TYPES, name="cvss_privileges_required")) | |
1237 | user_interaction = Column(Enum(*CVSS3GeneralConfig.USER_INTERACTION_TYPES, name="cvss_user_interaction")) | |
1238 | scope = Column(Enum(*CVSS3GeneralConfig.SCOPE_TYPES, name="cvss_scope")) | |
1239 | confidentiality_impact = Column(Enum(*CVSS3GeneralConfig.IMPACT_TYPES_V3, name="cvss_impact_types_v3")) | |
1240 | integrity_impact = Column(Enum(*CVSS3GeneralConfig.IMPACT_TYPES_V3, name="cvss_impact_types_v3")) | |
1241 | availability_impact = Column(Enum(*CVSS3GeneralConfig.IMPACT_TYPES_V3, name="cvss_impact_types_v3")) | |
1242 | ||
1243 | __mapper_args__ = { | |
1244 | 'polymorphic_identity': "v3" | |
1245 | } | |
1246 | ||
1247 | def __init__(self, base_score: Float = None, vector_string=None, **kwargs): | |
1248 | super().__init__(version=CVSS3GeneralConfig.VERSION, vector_string=vector_string, | |
1249 | _fixed_base_score=base_score, **kwargs) | |
1250 | ||
1251 | def assign_vector_string(self, vector_string): | |
1252 | self._vector_string = vector_string | |
1253 | vector_string_parsed = re.match(CVSS3GeneralConfig.PATTERN, vector_string if vector_string else '') | |
1254 | if vector_string_parsed: | |
1255 | self.attack_vector = vector_string_parsed['attack_vector'] | |
1256 | self.attack_complexity = vector_string_parsed['attack_complexity'] | |
1257 | self.privileges_required = vector_string_parsed['privileges_required'] | |
1258 | self.user_interaction = vector_string_parsed['user_interaction'] | |
1259 | self.scope = vector_string_parsed['scope'] | |
1260 | self.confidentiality_impact = vector_string_parsed['confidentiality'] | |
1261 | self.integrity_impact = vector_string_parsed['integrity'] | |
1262 | self.availability_impact = vector_string_parsed['availability'] | |
1263 | else: | |
1264 | self.attack_vector = None | |
1265 | self.attack_complexity = None | |
1266 | self.privileges_required = None | |
1267 | self.user_interaction = None | |
1268 | self.scope = None | |
1269 | self.confidentiality_impact = None | |
1270 | self.integrity_impact = None | |
1271 | self.availability_impact = None | |
1272 | ||
1273 | def isc_base(self): | |
1274 | return 1 - ((1 - CVSS3GeneralConfig.IMPACT_SCORES_V3[self.confidentiality_impact]) * (1 - CVSS3GeneralConfig.IMPACT_SCORES_V3[self.integrity_impact]) * (1 - CVSS3GeneralConfig.IMPACT_SCORES_V3[self.availability_impact])) | |
1275 | ||
1276 | def impact(self): | |
1277 | if self.scope == CVSS3GeneralConfig.UNCHANGED: | |
1278 | return 6.42 * self.isc_base() | |
1279 | else: | |
1280 | return 7.52 * (self.isc_base() - 0.029) - 3.25 * (self.isc_base() - 0.02) ** 15 | |
1281 | ||
1282 | def exploitability(self): | |
1283 | return 8.22 * CVSS3GeneralConfig.ATTACK_VECTOR_SCORES[self.attack_vector] * CVSS3GeneralConfig.ATTACK_COMPLEXITY_SCORES[self.attack_complexity] * CVSS3GeneralConfig.PRIVILEGES_REQUIRED_SCORES[self.scope][self.privileges_required] * CVSS3GeneralConfig.USER_INTERACTION_SCORES[self.user_interaction] | |
1284 | ||
1285 | def calculate_base_score(self): | |
1286 | if re.match(CVSS3GeneralConfig.PATTERN, self.vector_string if self.vector_string else ''): | |
1287 | score = 10 | |
1288 | if self.impact() <= 0: | |
1289 | return 0.0 | |
1290 | impact_plus_exploitability = self.impact() + self.exploitability() | |
1291 | if self.scope == CVSS3GeneralConfig.UNCHANGED: | |
1292 | if impact_plus_exploitability < 10: | |
1293 | score = impact_plus_exploitability | |
1294 | else: | |
1295 | impact_plus_exploitability = impact_plus_exploitability * 1.08 | |
1296 | if impact_plus_exploitability < 10: | |
1297 | score = impact_plus_exploitability | |
1298 | ||
1299 | # round up score | |
1300 | # Where “Round up” is defined as the smallest number, specified to one decimal place, | |
1301 | # that is equal to or higher than its input. For example, Round up (4.02) is 4.1; and Round up (4.00) is 4.0. | |
1302 | return math.ceil(score * 10) / 10 | |
1303 | return None | |
1304 | ||
1305 | ||
1035 | 1306 | class Service(Metadata): |
1036 | 1307 | STATUSES = [ |
1037 | 1308 | 'open', |
1051 | 1322 | |
1052 | 1323 | banner = BlankColumn(Text) |
1053 | 1324 | |
1054 | host_id = Column(Integer, ForeignKey('host.id'), index=True, nullable=False) | |
1325 | host_id = Column(Integer, ForeignKey('host.id', ondelete='CASCADE'), index=True, nullable=False) | |
1055 | 1326 | host = relationship( |
1056 | 1327 | 'Host', |
1057 | 1328 | foreign_keys=[host_id], |
1088 | 1359 | |
1089 | 1360 | |
1090 | 1361 | class VulnerabilityGeneric(VulnerabilityABC): |
1362 | STATUS_OPEN = 'open' | |
1363 | STATUS_RE_OPENED = 're-opened' | |
1364 | STATUS_CLOSED = 'closed' | |
1365 | STATUS_RISK_ACCEPTED = 'risk-accepted' | |
1366 | ||
1091 | 1367 | STATUSES = [ |
1092 | 'open', | |
1093 | 'closed', | |
1094 | 're-opened', | |
1095 | 'risk-accepted' | |
1368 | STATUS_OPEN, | |
1369 | STATUS_CLOSED, | |
1370 | STATUS_RE_OPENED, | |
1371 | STATUS_RISK_ACCEPTED | |
1096 | 1372 | ] |
1097 | 1373 | VULN_TYPES = [ |
1098 | 1374 | 'vulnerability', |
1121 | 1397 | |
1122 | 1398 | vulnerability_duplicate_id = Column( |
1123 | 1399 | Integer, |
1124 | ForeignKey('vulnerability.id'), | |
1400 | ForeignKey('vulnerability.id', ondelete='SET NULL'), | |
1125 | 1401 | index=True, |
1126 | 1402 | nullable=True, |
1127 | 1403 | ) |
1131 | 1407 | |
1132 | 1408 | vulnerability_template_id = Column( |
1133 | 1409 | Integer, |
1134 | ForeignKey('vulnerability_template.id'), | |
1410 | ForeignKey('vulnerability_template.id', ondelete='SET NULL'), | |
1135 | 1411 | index=True, |
1136 | 1412 | nullable=True, |
1137 | 1413 | ) |
1155 | 1431 | cve = association_proxy('cve_instances', |
1156 | 1432 | 'name', |
1157 | 1433 | proxy_factory=CustomAssociationSet, |
1158 | creator=_build_associationproxy_creator_non_workspaced('CVE')) | |
1434 | creator=_build_associationproxy_creator_non_workspaced('CVE', lambda c: c.upper())) | |
1435 | ||
1436 | # TODO: Ver si el nombre deberia ser cvss_v2_id | |
1437 | cvssv2_id = Column( | |
1438 | Integer, | |
1439 | ForeignKey('cvss_v2.id'), | |
1440 | nullable=True | |
1441 | ) | |
1442 | cvssv2 = relationship('CVSSV2', backref=backref('vulnerability_cvssv2')) | |
1443 | ||
1444 | # TODO: Ver si el nombre deberia ser cvss_v3_id | |
1445 | cvssv3_id = Column( | |
1446 | Integer, | |
1447 | ForeignKey('cvss_v3.id'), | |
1448 | nullable=True | |
1449 | ) | |
1450 | cvssv3 = relationship('CVSSV3', backref=backref('vulnerability_cvssv3')) | |
1159 | 1451 | |
1160 | 1452 | reference_instances = relationship( |
1161 | 1453 | "Reference", |
1249 | 1541 | 'host_inner.id = service.host_id')) |
1250 | 1542 | ) |
1251 | 1543 | |
1252 | host_id = Column(Integer, ForeignKey(Host.id), index=True) | |
1544 | host_id = Column(Integer, ForeignKey(Host.id, ondelete='CASCADE'), index=True) | |
1253 | 1545 | host = relationship( |
1254 | 1546 | 'Host', |
1255 | 1547 | backref=backref("vulnerabilities", cascade="all, delete-orphan"), |
1310 | 1602 | |
1311 | 1603 | @declared_attr |
1312 | 1604 | def service_id(cls): |
1313 | return VulnerabilityGeneric.__table__.c.get('service_id', Column(Integer, db.ForeignKey('service.id'), | |
1314 | index=True)) | |
1605 | return VulnerabilityGeneric.__table__.c.get('service_id', | |
1606 | Column(Integer, | |
1607 | db.ForeignKey('service.id', ondelete='CASCADE'), | |
1608 | index=True)) | |
1315 | 1609 | |
1316 | 1610 | @declared_attr |
1317 | 1611 | def service(cls): |
1340 | 1634 | @declared_attr |
1341 | 1635 | def service_id(cls): |
1342 | 1636 | return VulnerabilityGeneric.__table__.c.get( |
1343 | 'service_id', Column(Integer, db.ForeignKey('service.id'), | |
1637 | 'service_id', Column(Integer, db.ForeignKey('service.id', ondelete='CASCADE'), | |
1344 | 1638 | nullable=False)) |
1345 | 1639 | |
1346 | 1640 | @declared_attr |
1366 | 1660 | start_line = Column(Integer, nullable=True) |
1367 | 1661 | end_line = Column(Integer, nullable=True) |
1368 | 1662 | |
1369 | source_code_id = Column(Integer, ForeignKey(SourceCode.id), index=True) | |
1663 | source_code_id = Column(Integer, ForeignKey(SourceCode.id, ondelete='CASCADE'), index=True) | |
1370 | 1664 | source_code = relationship( |
1371 | 1665 | SourceCode, |
1372 | 1666 | backref='vulnerabilities', |
1429 | 1723 | class ReferenceVulnerabilityAssociation(db.Model): |
1430 | 1724 | __tablename__ = 'reference_vulnerability_association' |
1431 | 1725 | |
1432 | vulnerability_id = Column(Integer, ForeignKey('vulnerability.id'), primary_key=True) | |
1726 | vulnerability_id = Column(Integer, ForeignKey('vulnerability.id', ondelete="CASCADE"), primary_key=True) | |
1433 | 1727 | reference_id = Column(Integer, ForeignKey('reference.id'), primary_key=True) |
1434 | 1728 | |
1435 | 1729 | reference = relationship("Reference", |
1446 | 1740 | class PolicyViolationVulnerabilityAssociation(db.Model): |
1447 | 1741 | __tablename__ = 'policy_violation_vulnerability_association' |
1448 | 1742 | |
1449 | vulnerability_id = Column(Integer, ForeignKey('vulnerability.id'), primary_key=True) | |
1743 | vulnerability_id = Column(Integer, ForeignKey('vulnerability.id', ondelete="CASCADE"), primary_key=True) | |
1450 | 1744 | policy_violation_id = Column(Integer, ForeignKey('policy_violation.id'), primary_key=True) |
1451 | 1745 | |
1452 | 1746 | policy_violation = relationship("PolicyViolation", backref=backref("policy_violation_associations", cascade="all, delete-orphan"), foreign_keys=[policy_violation_id]) |
1538 | 1832 | description = BlankColumn(Text) |
1539 | 1833 | name = BlankColumn(Text) |
1540 | 1834 | |
1541 | host_id = Column(Integer, ForeignKey(Host.id), index=True, nullable=True) | |
1835 | host_id = Column(Integer, ForeignKey(Host.id, ondelete='CASCADE'), index=True, nullable=True) | |
1542 | 1836 | host = relationship( |
1543 | 1837 | 'Host', |
1544 | 1838 | backref=backref("credentials", cascade="all, delete-orphan"), |
1545 | 1839 | foreign_keys=[host_id]) |
1546 | 1840 | |
1547 | service_id = Column(Integer, ForeignKey(Service.id), index=True, nullable=True) | |
1841 | service_id = Column(Integer, ForeignKey(Service.id, ondelete='CASCADE'), index=True, nullable=True) | |
1548 | 1842 | service = relationship( |
1549 | 1843 | 'Service', |
1550 | 1844 | backref=backref('credentials', cascade="all, delete-orphan"), |
1604 | 1898 | 'association_workspace_and_agents_table', |
1605 | 1899 | db.Model.metadata, |
1606 | 1900 | Column('workspace_id', Integer, ForeignKey('workspace.id')), |
1607 | Column('agent_id', Integer, ForeignKey('agent.id')) | |
1901 | Column('agent_id', Integer, ForeignKey('agent.id', ondelete='CASCADE')) | |
1608 | 1902 | ) |
1609 | 1903 | |
1610 | 1904 | |
1674 | 1968 | FROM association_workspace_and_agents_table as assoc |
1675 | 1969 | JOIN agent ON agent.id = assoc.agent_id and assoc.workspace_id = workspace.id |
1676 | 1970 | WHERE agent.active is TRUE |
1677 | ) AS active_agents_count, | |
1971 | ) AS run_agent_date, | |
1972 | (SELECT executor.last_run | |
1973 | FROM executor | |
1974 | JOIN agent ON executor.agent_id = agent.id | |
1975 | JOIN association_workspace_and_agents_table ON | |
1976 | agent.id = association_workspace_and_agents_table.agent_id | |
1977 | and association_workspace_and_agents_table.workspace_id = workspace.id | |
1978 | WHERE executor.last_run is not null | |
1979 | ORDER BY executor.last_run DESC | |
1980 | LIMIT 1 | |
1981 | ) AS last_run_agent_date, | |
1678 | 1982 | p_4.count_3 as open_services, |
1679 | 1983 | p_4.count_4 as total_service_count, |
1680 | 1984 | p_5.count_5 as vulnerability_web_count, |
1687 | 1991 | p_5.count_12 as vulnerability_low_count, |
1688 | 1992 | p_5.count_13 as vulnerability_informational_count, |
1689 | 1993 | p_5.count_14 as vulnerability_unclassified_count, |
1994 | p_5.count_15 as vulnerability_open_count, | |
1995 | p_5.count_16 as vulnerability_confirmed_count, | |
1690 | 1996 | workspace.create_date AS workspace_create_date, |
1691 | 1997 | workspace.update_date AS workspace_update_date, |
1692 | 1998 | workspace.id AS workspace_id, |
1719 | 2025 | COUNT(case when vulnerability.severity = 'medium' then 1 else null end) as count_11, |
1720 | 2026 | COUNT(case when vulnerability.severity = 'low' then 1 else null end) as count_12, |
1721 | 2027 | COUNT(case when vulnerability.severity = 'informational' then 1 else null end) as count_13, |
1722 | COUNT(case when vulnerability.severity = 'unclassified' then 1 else null end) as count_14 | |
2028 | COUNT(case when vulnerability.severity = 'unclassified' then 1 else null end) as count_14, | |
2029 | COUNT(case when vulnerability.status = 'open' OR vulnerability.status='re-opened' then 1 else null end) as count_15, | |
2030 | COUNT(case when vulnerability.confirmed is True then 1 else null end) as count_16 | |
1723 | 2031 | FROM vulnerability |
1724 | 2032 | RIGHT JOIN workspace w ON vulnerability.workspace_id = w.id |
1725 | 2033 | WHERE 1=1 {0} |
2100 | 2408 | |
2101 | 2409 | text = BlankColumn(Text) |
2102 | 2410 | |
2103 | reply_to_id = Column(Integer, ForeignKey('comment.id')) | |
2411 | reply_to_id = Column(Integer, ForeignKey('comment.id', ondelete='SET NULL')) | |
2104 | 2412 | reply_to = relationship( |
2105 | 2413 | 'Comment', |
2106 | 2414 | remote_side=[id], |
2483 | 2791 | __tablename__ = 'executor' |
2484 | 2792 | id = Column(Integer, primary_key=True) |
2485 | 2793 | name = Column(String, nullable=False) |
2486 | agent_id = Column(Integer, ForeignKey('agent.id'), index=True, nullable=False) | |
2794 | agent_id = Column(Integer, ForeignKey('agent.id', ondelete='CASCADE'), index=True, nullable=False) | |
2487 | 2795 | agent = relationship( |
2488 | 2796 | 'Agent', |
2489 | 2797 | backref=backref('executors', cascade="all, delete-orphan"), |
2604 | 2912 | backref=backref('agent_executions', cascade="all, delete-orphan") |
2605 | 2913 | ) |
2606 | 2914 | parameters_data = Column(JSONType, nullable=False) |
2607 | command_id = Column(Integer, ForeignKey('command.id'), index=True) | |
2915 | command_id = Column(Integer, ForeignKey('command.id', ondelete='SET NULL'), index=True) | |
2608 | 2916 | command = relationship( |
2609 | 2917 | 'Command', |
2610 | 2918 | foreign_keys=[command_id], |
2647 | 2955 | end = Column(DateTime, nullable=True) |
2648 | 2956 | rule_id = Column(Integer, ForeignKey('rule.id'), index=True, nullable=False) |
2649 | 2957 | rule = relationship('Rule', foreign_keys=[rule_id], backref=backref('executions', cascade="all, delete-orphan")) |
2650 | command_id = Column(Integer, ForeignKey('command.id'), index=True, nullable=False) | |
2958 | command_id = Column(Integer, ForeignKey('command.id', ondelete='CASCADE'), index=True, nullable=False) | |
2651 | 2959 | command = relationship('Command', foreign_keys=[command_id], |
2652 | 2960 | backref=backref('rule_executions', cascade="all, delete-orphan")) |
2653 | 2961 |
194 | 194 | if not Path(str(FARADAY_SERVER_PID_FILE).format(port)).exists(): |
195 | 195 | return None |
196 | 196 | |
197 | with open(str(FARADAY_SERVER_PID_FILE).format(port), 'r') as pid_file: | |
197 | with open(str(FARADAY_SERVER_PID_FILE).format(port)) as pid_file: | |
198 | 198 | # If PID file is badly written, delete it and |
199 | 199 | # assume server is not running |
200 | 200 | try: |
67 | 67 | |
68 | 68 | # Add wildcards to both ends of a search term |
69 | 69 | if is_direct_filter_search: |
70 | like_str = u'%' + field_filter.get(attribute) + u'%' | |
70 | like_str = '%' + field_filter.get(attribute) + '%' | |
71 | 71 | elif is_free_text_search: |
72 | like_str = u'%' + free_text_search + u'%' | |
72 | like_str = '%' + free_text_search + '%' | |
73 | 73 | else: |
74 | 74 | continue |
75 | 75 | |
167 | 167 | if instance: |
168 | 168 | return instance, False |
169 | 169 | else: |
170 | params = dict((k, v) for k, v in kwargs.items() if not isinstance(v, ClauseElement)) | |
170 | params = {k: v for k, v in kwargs.items() if not isinstance(v, ClauseElement)} | |
171 | 171 | params.update(defaults or {}) |
172 | 172 | instance = model(**params) |
173 | 173 | session.add(instance) |
185 | 185 | else: |
186 | 186 | separator = ',' |
187 | 187 | |
188 | res = 'array_to_string(array_agg({0}), \'{1}\')'.format( | |
188 | res = 'array_to_string(array_agg({}), \'{}\')'.format( | |
189 | 189 | compiler.process(element.clauses.clauses[0]), |
190 | 190 | separator, |
191 | 191 | ) |
42 | 42 | # uncomment this to see who's calling what |
43 | 43 | # ps.print_callers() |
44 | 44 | debug_logger.debug(s.getvalue()) |
45 | ||
46 | # I'm Py3 |
0 | # Standard library imports | |
0 | 1 | import csv |
2 | import logging | |
1 | 3 | from io import StringIO, BytesIO |
2 | import logging | |
3 | ||
4 | ||
5 | # Local application imports | |
4 | 6 | from faraday.server.models import ( |
5 | 7 | db, |
6 | 8 | Comment, |
19 | 21 | "target", "desc", "status", "hostnames", "comments", "owner", |
20 | 22 | "os", "resolution", "refs", "easeofresolution", "web_vulnerability", |
21 | 23 | "data", "website", "path", "status_code", "request", "response", "method", |
22 | "params", "pname", "query", "policyviolations", "external_id", "impact_confidentiality", | |
24 | "params", "pname", "query", "cve", "policyviolations", "external_id", "impact_confidentiality", | |
23 | 25 | "impact_integrity", "impact_availability", "impact_accountability", "update_date" |
24 | 26 | ] |
25 | 27 | |
182 | 184 | "params": vuln.get('params', None), |
183 | 185 | "pname": vuln.get('pname', None), |
184 | 186 | "query": vuln.get('query', None), |
187 | "cve": vuln.get('cve', None), | |
185 | 188 | "policyviolations": vuln.get('policyviolations', None), |
186 | 189 | "external_id": vuln.get('external_id', None), |
187 | 190 | "impact_confidentiality": vuln["impact"]["confidentiality"], |
203 | 206 | # Patch possible formula injection attacks |
204 | 207 | def csv_escape(vuln_dict): |
205 | 208 | for key, value in vuln_dict.items(): |
206 | if str(value).startswith('=') or str(value).startswith('+') or str(value).startswith('-') or str(value).startswith('@'): | |
209 | if str(value).startswith('=') or str(value).startswith('+') or str(value).startswith('-') \ | |
210 | or str(value).startswith('@'): | |
207 | 211 | # Convert value to str just in case is has another type (like a list or |
208 | 212 | # dict). This would be done anyway by the csv writer. |
209 | 213 | vuln_dict[key] = "'" + str(value) |
23 | 23 | from faraday.server.fields import JSONType |
24 | 24 | |
25 | 25 | |
26 | VALID_OPERATORS = set(OPERATORS.keys()) - set(['desc', 'asc']) | |
26 | VALID_OPERATORS = set(OPERATORS.keys()) - {'desc', 'asc'} | |
27 | 27 | |
28 | 28 | logger = logging.getLogger(__name__) |
29 | 29 | |
269 | 269 | an error on PostgreSQL |
270 | 270 | """ |
271 | 271 | if 'group_by' in data and 'order_by' in data: |
272 | group_by_fields = set(group_field['field'] for group_field in data['group_by']) | |
273 | order_by_fields = set(order_field['field'] for order_field in data['order_by']) | |
272 | group_by_fields = {group_field['field'] for group_field in data['group_by']} | |
273 | order_by_fields = {order_field['field'] for order_field in data['order_by']} | |
274 | 274 | if not order_by_fields.issubset(group_by_fields): |
275 | 275 | logger.error(f'All order fields ({order_by_fields}) must be in group by {group_by_fields}.') |
276 | 276 | raise ValidationError(f'All order fields ({order_by_fields}) must be in group by {group_by_fields}.') |
223 | 223 | |
224 | 224 | def __repr__(self): |
225 | 225 | """Returns a string representation of this object.""" |
226 | return '<Filter {0} {1} {2}>'.format(self.fieldname, self.operator, | |
226 | return '<Filter {} {} {}>'.format(self.fieldname, self.operator, | |
227 | 227 | self.argument or self.otherfield) |
228 | 228 | |
229 | 229 | @staticmethod |
9 | 9 | from twisted.web.resource import Resource, ForbiddenResource |
10 | 10 | |
11 | 11 | from twisted.internet import reactor, error |
12 | from twisted.web.server import Site | |
12 | 13 | from twisted.web.static import File |
13 | 14 | from twisted.web.util import Redirect |
14 | 15 | from twisted.web.http import proxiedLogFormatter |
32 | 33 | FARADAY_APP = None |
33 | 34 | |
34 | 35 | logger = logging.getLogger(__name__) |
36 | ||
37 | ||
38 | class FaradaySite(Site): | |
39 | def getResourceFor(self, request): | |
40 | resource = super().getResourceFor(request) | |
41 | if isinstance(resource, twisted.web.resource.NoResource): | |
42 | resource = self.resource.getChild("index.html", request) | |
43 | return resource | |
35 | 44 | |
36 | 45 | |
37 | 46 | class CleanHttpHeadersResource(Resource): |
130 | 139 | self.stop_threads() |
131 | 140 | |
132 | 141 | log_path = CONST_FARADAY_HOME_PATH / 'logs' / 'access-logging.log' |
133 | site = twisted.web.server.Site(self.root_resource, | |
134 | logPath=log_path, | |
135 | logFormatter=proxiedLogFormatter) | |
142 | site = FaradaySite(self.root_resource, logPath=log_path, logFormatter=proxiedLogFormatter) | |
136 | 143 | site.displayTracebacks = False |
137 | 144 | |
138 | 145 | try: |
105 | 105 | return self.factory.join_agent(self, agent) |
106 | 106 | if message['action'] == 'LEAVE_AGENT': |
107 | 107 | with get_app().app_context(): |
108 | (agent_id,) = [ | |
108 | (agent_id,) = ( | |
109 | 109 | k |
110 | 110 | for (k, v) in connected_agents.items() |
111 | 111 | if v == self |
112 | ] | |
112 | ) | |
113 | 113 | agent = Agent.query.get(agent_id) |
114 | 114 | assert agent is not None # TODO the agent could be deleted here |
115 | 115 | return self.factory.leave_agent(self, agent) |
119 | 119 | logger.warning(f'Missing executor_name param in message: {message}') |
120 | 120 | return True |
121 | 121 | |
122 | (agent_id,) = [ | |
122 | (agent_id,) = ( | |
123 | 123 | k |
124 | 124 | for (k, v) in connected_agents.items() |
125 | 125 | if v == self |
126 | ] | |
126 | ) | |
127 | 127 | agent = Agent.query.get(agent_id) |
128 | 128 | assert agent is not None # TODO the agent could be deleted here |
129 | 129 |
43 | 43 | settings_config.update(query.value) |
44 | 44 | settings_config = self.clear_configuration(settings_config) |
45 | 45 | return settings_config |
46 | ||
47 | def delete_configuration(self): | |
48 | from faraday.server.web import get_app # pylint: disable=import-outside-toplevel | |
49 | with get_app().app_context(): | |
50 | db.session.query(Configuration).filter(Configuration.key == self.settings_key).delete() | |
51 | db.session.commit() | |
52 | self.__class__.value.fget.cache_clear() | |
46 | 53 | |
47 | 54 | def get_default_config(self): |
48 | 55 | return {} |
0 | ||
1 | 0 | class MissingConfigurationError(Exception): |
2 | 1 | """Raised when setting configuration is missing""" |
3 | 2 | pass |
13 | 13 | pname = |
14 | 14 | "faraday-agent-parameters-types"; |
15 | 15 | version = |
16 | "1.0.2"; | |
16 | "1.0.3"; | |
17 | 17 | |
18 | 18 | src = |
19 | 19 | fetchPypi { |
22 | 22 | pname = |
23 | 23 | "faraday_agent_parameters_types"; |
24 | 24 | sha256 = |
25 | "0dw2s7lyg9s1qjj6yrn5hxpasbb32qg89pcfsv4vv47yla9djzyc"; | |
25 | "1rz0mrpgg529fd7ppi9fkpgvmfriwamlx0ah10637hvpnjfncmb1"; | |
26 | 26 | }; |
27 | 27 | |
28 | 28 | buildInputs = |
20 | 20 | pname = |
21 | 21 | "faraday-plugins"; |
22 | 22 | version = |
23 | "1.5.5"; | |
23 | "1.5.9"; | |
24 | 24 | |
25 | 25 | src = |
26 | 26 | fetchPypi { |
28 | 28 | pname |
29 | 29 | version; |
30 | 30 | sha256 = |
31 | "1dw7j8zfa8j9m0qcpyzl6k0z29k3j5i90lyfg2zapwkpalkgx0d1"; | |
31 | "0wkwan2vg7np0z1pwskpgwgnxxk9d46ffq1a3f5ai12ibwj9kwgh"; | |
32 | 32 | }; |
33 | 33 | |
34 | 34 | propagatedBuildInputs = |
64 | 64 | pname = |
65 | 65 | "faradaysec"; |
66 | 66 | version = |
67 | "3.18.1"; | |
67 | "3.19.0"; | |
68 | 68 | |
69 | 69 | src = |
70 | 70 | lib.cleanSource |
12 | 12 | pname = |
13 | 13 | "marshmallow-sqlalchemy"; |
14 | 14 | version = |
15 | "0.26.1"; | |
15 | "0.27.0"; | |
16 | 16 | |
17 | 17 | src = |
18 | 18 | fetchPypi { |
20 | 20 | pname |
21 | 21 | version; |
22 | 22 | sha256 = |
23 | "0wval5lqak31zwrzmgi9c919lqk0dw1zxvwihif4nmaivrs5ylnq"; | |
23 | "0za0zl1vyphx2pnf2zcwbjp1lzqkdi2gcf1saa668i24aqlv288m"; | |
24 | 24 | }; |
25 | 25 | |
26 | 26 | propagatedBuildInputs = |
12 | 12 | pname = |
13 | 13 | "python-socketio"; |
14 | 14 | version = |
15 | "5.4.1"; | |
15 | "5.5.0"; | |
16 | 16 | |
17 | 17 | src = |
18 | 18 | fetchPypi { |
20 | 20 | pname |
21 | 21 | version; |
22 | 22 | sha256 = |
23 | "1c17cvm91map3rbgl5156y6zwzz2wyqvm31298a23d7bvwyjfkpg"; | |
23 | "02ygri5qaw7ynqlnimn3b0arl6r5bh6wyc0dl4gq389ap2hjx5yf"; | |
24 | 24 | }; |
25 | 25 | |
26 | 26 | propagatedBuildInputs = |
32 | 32 | simplekv>=0.13.0 |
33 | 33 | Flask-KVSession-fork>=0.6.3 |
34 | 34 | distro>=1.4.0 |
35 | faraday-plugins>=1.4.4,<2.0.0 | |
36 | apispec>=4.0.0 | |
35 | faraday-plugins>=1.5.9,<2.0.0 | |
36 | apispec>=4.0.0,<5.0.0 | |
37 | 37 | apispec-webframeworks>=0.5.0 |
38 | 38 | pyyaml |
39 | 39 | Flask-SocketIO>=5.0.1 |
40 | 40 | pyotp>=2.6.0 |
41 | 41 | Flask-Limiter |
42 | 42 | Flask-Mail |
43 | faraday_agent_parameters_types>=1.0.0 | |
43 | faraday_agent_parameters_types>=1.0.3 |
10 | 10 | # It ensures open() defaults to text mode with universal newlines, |
11 | 11 | # and accepts an argument to specify the text encoding |
12 | 12 | # Python 3 only projects can skip this import |
13 | from io import open | |
14 | 13 | from re import search |
15 | 14 | |
16 | 15 | # Get the long description from the README file |
24 | 23 | |
25 | 24 | To read about the latest features check out the [release notes](https://github.com/infobyte/faraday/blob/master/RELEASE.md)!""" |
26 | 25 | |
27 | with open('faraday/__init__.py', 'rt', encoding='utf8') as f: | |
26 | with open('faraday/__init__.py', encoding='utf8') as f: | |
28 | 27 | version = search(r'__version__ = \'(.*?)\'', f.read()).group(1) |
29 | 28 | |
30 | 29 | # Taken from https://stackoverflow.com/questions/14399534/reference-requirements-txt-for-the-install-requires-kwarg-in-setuptools-setup-py/14399775#14399775 |
298 | 298 | def csrf_token(logged_user, test_client): |
299 | 299 | session_response = test_client.get('/session') |
300 | 300 | return session_response.json.get('csrf_token') |
301 | ||
302 | # I'm Py3 |
3 | 3 | See the file 'doc/LICENSE' for the license information |
4 | 4 | |
5 | 5 | ''' |
6 | from builtins import chr, range | |
7 | 6 | |
8 | 7 | import random |
9 | 8 | import string |
53 | 52 | Action, |
54 | 53 | RuleAction, |
55 | 54 | Condition, |
56 | Role | |
55 | Role, RuleExecution | |
57 | 56 | ) |
58 | 57 | |
59 | 58 | |
132 | 131 | class WorkspaceFactory(FaradayFactory): |
133 | 132 | |
134 | 133 | name = FuzzyText(chars=string.ascii_lowercase + string.digits) |
134 | description = FuzzyText() | |
135 | 135 | creator = factory.SubFactory(UserFactory) |
136 | 136 | |
137 | 137 | class Meta: |
338 | 338 | |
339 | 339 | host = factory.SubFactory(HostFactory, workspace=factory.SelfAttribute('..workspace')) |
340 | 340 | service = factory.SubFactory(ServiceFactory, workspace=factory.SelfAttribute('..workspace')) |
341 | description = FuzzyText() | |
342 | 341 | type = "vulnerability" |
343 | 342 | |
344 | 343 | @classmethod |
668 | 667 | model = RuleAction |
669 | 668 | sqlalchemy_session = db.session |
670 | 669 | |
670 | ||
671 | class RuleExecutionFactory(FaradayFactory): | |
672 | rule = factory.SubFactory(RuleFactory) | |
673 | command = factory.SubFactory(CommandFactory) | |
674 | ||
675 | class Meta: | |
676 | model = RuleExecution | |
677 | sqlalchemy_session = db.session | |
678 | ||
671 | 679 | # I'm Py3 |
55 | 55 | assert len(vulnerability_web.evidence) == 1 |
56 | 56 | assert vulnerability_web.evidence[0].object_type == 'vulnerability' |
57 | 57 | assert vulnerability_web.evidence[0].object_id == vulnerability_web.id |
58 | # I'm Py3 |
84 | 84 | session.commit() |
85 | 85 | assert len(session.new) == len(session.deleted) == len( |
86 | 86 | session.dirty) == 0 |
87 | assert set(new_value) == set(hn.name for hn in | |
88 | host_with_hostnames.hostnames) | |
87 | assert set(new_value) == {hn.name for hn in | |
88 | host_with_hostnames.hostnames} | |
89 | 89 | |
90 | 90 | def test_all(self, host, session): |
91 | 91 | a = Hostname(workspace=host.workspace, host=host, name='a') |
106 | 106 | assert c.name == 'c' |
107 | 107 | |
108 | 108 | session.commit() |
109 | assert set(hn.name for hn in host.hostnames) == {'b', 'c'} | |
109 | assert {hn.name for hn in host.hostnames} == {'b', 'c'} | |
110 | 110 | |
111 | 111 | def test_change_one(self, host, session): |
112 | 112 | hn = Hostname(workspace=host.workspace, |
250 | 250 | session.commit() |
251 | 251 | assert vulnerability.creator_command_id == command.id |
252 | 252 | assert vulnerability.creator_command_tool == command.tool |
253 | # I'm Py3 |
5 | 5 | |
6 | 6 | from unittest import mock |
7 | 7 | |
8 | from posixpath import join as urljoin | |
8 | from posixpath import join | |
9 | from urllib.parse import urljoin | |
9 | 10 | import pyotp |
10 | 11 | import pytest |
11 | 12 | |
240 | 241 | assert '405' in exc_info.value.args[0] |
241 | 242 | |
242 | 243 | def workspaced_url(self, workspace, obj=None): |
243 | url = API_PREFIX + workspace.name + '/' + self.api_endpoint | |
244 | url = urljoin(API_PREFIX, f"{workspace.name}{self.api_endpoint}") | |
244 | 245 | if obj is not None: |
245 | 246 | id_ = str(obj.id) if isinstance(obj, self.model) else str(obj) |
246 | url += u'/' + id_ | |
247 | url = urljoin(url, id_) | |
247 | 248 | return url |
248 | 249 | |
249 | 250 | def create_raw_agent(self, active=False, token="TOKEN", |
393 | 394 | }, |
394 | 395 | } |
395 | 396 | res = test_client.post( |
396 | self.url(agent) + 'run/', | |
397 | join(self.url(agent), 'run'), | |
397 | 398 | json=payload |
398 | 399 | ) |
399 | 400 | assert res.status_code == 404 |
451 | 452 | 'csrf_token': csrf_token |
452 | 453 | } |
453 | 454 | res = test_client.post( |
454 | urljoin(self.url(agent), 'run'), | |
455 | join(self.url(agent), 'run'), | |
455 | 456 | json=payload |
456 | 457 | ) |
457 | 458 | assert res.status_code == 400 |
473 | 474 | } |
474 | 475 | |
475 | 476 | res = test_client.post( |
476 | urljoin(self.url(agent), 'run'), | |
477 | join(self.url(agent), 'run'), | |
477 | 478 | json=payload |
478 | 479 | ) |
479 | 480 | |
484 | 485 | session.add(agent) |
485 | 486 | session.commit() |
486 | 487 | res = test_client.post( |
487 | urljoin(self.url(agent), 'run'), | |
488 | join(self.url(agent), 'run'), | |
488 | 489 | data='[" broken]"{' |
489 | 490 | ) |
490 | 491 | assert res.status_code == 400 |
506 | 507 | ('content-type', 'text/html'), |
507 | 508 | ] |
508 | 509 | res = test_client.post( |
509 | urljoin(self.url(agent), 'run'), | |
510 | join(self.url(agent), 'run'), | |
510 | 511 | data=payload, |
511 | 512 | headers=headers) |
512 | 513 | assert res.status_code == 400 |
525 | 526 | }, |
526 | 527 | } |
527 | 528 | res = test_client.post( |
528 | urljoin(self.url(agent), 'run'), | |
529 | join(self.url(agent), 'run'), | |
529 | 530 | json=payload |
530 | 531 | ) |
531 | 532 | assert res.status_code == 400 |
552 | 553 | }, |
553 | 554 | } |
554 | 555 | res = test_client.post( |
555 | urljoin(self.url(agent), 'run'), | |
556 | join(self.url(agent), 'run'), | |
556 | 557 | json=payload |
557 | 558 | ) |
558 | 559 | assert res.status_code == 200 |
581 | 582 | }, |
582 | 583 | } |
583 | 584 | res = test_client.post( |
584 | urljoin(self.url(agent), 'run'), | |
585 | join(self.url(agent), 'run'), | |
585 | 586 | json=payload |
586 | 587 | ) |
587 | 588 | assert res.status_code == 400 |
596 | 597 | 'executorData': '[][dassa', |
597 | 598 | } |
598 | 599 | res = test_client.post( |
599 | urljoin(self.url(agent), 'run'), | |
600 | join(self.url(agent), 'run'), | |
600 | 601 | json=payload |
601 | 602 | ) |
602 | 603 | assert res.status_code == 400 |
610 | 611 | 'executorData': '', |
611 | 612 | } |
612 | 613 | res = test_client.post( |
613 | urljoin(self.url(agent), 'run'), | |
614 | join(self.url(agent), 'run'), | |
614 | 615 | json=payload |
615 | 616 | ) |
616 | 617 | assert res.status_code == 400 |
619 | 620 | agent = AgentFactory.create(workspaces=[self.workspace]) |
620 | 621 | session.add(agent) |
621 | 622 | session.commit() |
622 | res = test_client.get(urljoin(self.url(), 'get_manifests')) | |
623 | res = test_client.get(join(self.url(), 'get_manifests')) | |
623 | 624 | assert res.status_code == 200 |
3 | 3 | See the file 'doc/LICENSE' for the license information |
4 | 4 | |
5 | 5 | ''' |
6 | from builtins import str | |
7 | 6 | import base64 |
8 | 7 | |
9 | 8 | import pytest |
0 | # -*- coding: utf8 -*- | |
1 | 0 | ''' |
2 | 1 | Faraday Penetration Test IDE |
3 | 2 | Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/) |
50 | 49 | assert res.status_code == 200 |
51 | 50 | assert 'commands' in res.json |
52 | 51 | for command in res.json['commands']: |
53 | assert set([u'id', u'key', u'value']) == set(command.keys()) | |
52 | assert {'id', 'key', 'value'} == set(command.keys()) | |
54 | 53 | object_properties = [ |
55 | u'_id', | |
56 | u'command', | |
57 | u'duration', | |
58 | u'hostname', | |
59 | u'ip', | |
60 | u'itime', | |
61 | u'params', | |
62 | u'user', | |
63 | u'workspace', | |
64 | u'tool', | |
65 | u'import_source', | |
66 | u'creator', | |
67 | u'metadata' | |
54 | '_id', | |
55 | 'command', | |
56 | 'duration', | |
57 | 'hostname', | |
58 | 'ip', | |
59 | 'itime', | |
60 | 'params', | |
61 | 'user', | |
62 | 'workspace', | |
63 | 'tool', | |
64 | 'import_source', | |
65 | 'creator', | |
66 | 'metadata' | |
68 | 67 | ] |
69 | 68 | assert command['value']['workspace'] == self.workspace.name |
70 | 69 | assert set(object_properties) == set(command['value'].keys()) |
96 | 95 | assert res.status_code == 200 |
97 | 96 | |
98 | 97 | assert list(filter(lambda stats: stats['_id'] == command.id, res.json)) == [ |
99 | {u'_id': command.id, | |
100 | u'command': command.command, | |
101 | u'import_source': u'shell', | |
102 | u'user': command.user, | |
103 | u'date': time.mktime(command.start_date.timetuple()) * 1000, | |
104 | u'params': command.params, | |
105 | u'tool': command.tool, | |
106 | u'hosts_count': 1, | |
107 | u'services_count': 0, | |
108 | u'vulnerabilities_count': 1, | |
109 | u'criticalIssue': 0}] | |
98 | {'_id': command.id, | |
99 | 'command': command.command, | |
100 | 'import_source': 'shell', | |
101 | 'user': command.user, | |
102 | 'date': time.mktime(command.start_date.timetuple()) * 1000, | |
103 | 'params': command.params, | |
104 | 'tool': command.tool, | |
105 | 'hosts_count': 1, | |
106 | 'services_count': 0, | |
107 | 'vulnerabilities_count': 1, | |
108 | 'criticalIssue': 0}] | |
110 | 109 | |
111 | 110 | assert list(filter(lambda stats: stats['_id'] == another_command.id, |
112 | 111 | res.json)) == [{ |
113 | u'_id': another_command.id, | |
114 | u'command': another_command.command, | |
115 | u'import_source': u'shell', | |
116 | u'tool': another_command.tool, | |
117 | u'user': another_command.user, | |
118 | u'date': time.mktime( | |
112 | '_id': another_command.id, | |
113 | 'command': another_command.command, | |
114 | 'import_source': 'shell', | |
115 | 'tool': another_command.tool, | |
116 | 'user': another_command.user, | |
117 | 'date': time.mktime( | |
119 | 118 | another_command.start_date.timetuple()) * 1000, |
120 | u'params': another_command.params, | |
121 | u'hosts_count': 0, | |
122 | u'services_count': 0, | |
123 | u'vulnerabilities_count': 0, | |
124 | u'criticalIssue': 0}] | |
119 | 'params': another_command.params, | |
120 | 'hosts_count': 0, | |
121 | 'services_count': 0, | |
122 | 'vulnerabilities_count': 0, | |
123 | 'criticalIssue': 0}] | |
125 | 124 | |
126 | 125 | def test_verify_created_critical_vulns_is_correctly_showing_sum_values(self, session, test_client): |
127 | 126 | workspace = WorkspaceFactory.create() |
152 | 151 | res = test_client.get(urljoin(self.url(workspace=command.workspace), 'activity_feed')) |
153 | 152 | assert res.status_code == 200 |
154 | 153 | assert res.json == [ |
155 | {u'_id': command.id, | |
156 | u'command': command.command, | |
157 | u'import_source': u'shell', | |
158 | u'tool': command.tool, | |
159 | u'user': command.user, | |
160 | u'date': time.mktime(command.start_date.timetuple()) * 1000, | |
161 | u'params': command.params, | |
162 | u'hosts_count': 1, | |
163 | u'services_count': 0, | |
164 | u'vulnerabilities_count': 2, | |
165 | u'criticalIssue': 1} | |
154 | {'_id': command.id, | |
155 | 'command': command.command, | |
156 | 'import_source': 'shell', | |
157 | 'tool': command.tool, | |
158 | 'user': command.user, | |
159 | 'date': time.mktime(command.start_date.timetuple()) * 1000, | |
160 | 'params': command.params, | |
161 | 'hosts_count': 1, | |
162 | 'services_count': 0, | |
163 | 'vulnerabilities_count': 2, | |
164 | 'criticalIssue': 1} | |
166 | 165 | ] |
167 | 166 | |
168 | 167 | def test_verify_created_vulns_with_host_and_service_verification(self, session, test_client): |
201 | 200 | res = test_client.get(urljoin(self.url(workspace=command.workspace), 'activity_feed')) |
202 | 201 | assert res.status_code == 200 |
203 | 202 | assert res.json == [{ |
204 | u'_id': command.id, | |
205 | u'command': command.command, | |
206 | u'import_source': u'shell', | |
207 | u'tool': command.tool, | |
208 | u'user': command.user, | |
209 | u'date': time.mktime(command.start_date.timetuple()) * 1000, | |
210 | u'params': command.params, | |
211 | u'hosts_count': 1, | |
212 | u'services_count': 1, | |
213 | u'vulnerabilities_count': 2, | |
214 | u'criticalIssue': 1} | |
203 | '_id': command.id, | |
204 | 'command': command.command, | |
205 | 'import_source': 'shell', | |
206 | 'tool': command.tool, | |
207 | 'user': command.user, | |
208 | 'date': time.mktime(command.start_date.timetuple()) * 1000, | |
209 | 'params': command.params, | |
210 | 'hosts_count': 1, | |
211 | 'services_count': 1, | |
212 | 'vulnerabilities_count': 2, | |
213 | 'criticalIssue': 1} | |
215 | 214 | ] |
216 | 215 | |
217 | 216 | def test_multiple_commands_executed_with_same_objects_found(self, session, test_client): |
269 | 268 | raw_first_command = list(filter(lambda comm: comm['_id'] == commands[0].id, res.json)) |
270 | 269 | |
271 | 270 | assert raw_first_command.pop() == { |
272 | u'_id': first_command.id, | |
273 | u'command': first_command.command, | |
274 | u'import_source': u'shell', | |
275 | u'user': first_command.user, | |
276 | u'date': time.mktime(first_command.start_date.timetuple()) * 1000, | |
277 | u'params': first_command.params, | |
278 | u'hosts_count': 1, | |
279 | u'services_count': 0, | |
280 | u'vulnerabilities_count': 1, | |
281 | u'tool': first_command.tool, | |
282 | u'criticalIssue': 0 | |
271 | '_id': first_command.id, | |
272 | 'command': first_command.command, | |
273 | 'import_source': 'shell', | |
274 | 'user': first_command.user, | |
275 | 'date': time.mktime(first_command.start_date.timetuple()) * 1000, | |
276 | 'params': first_command.params, | |
277 | 'hosts_count': 1, | |
278 | 'services_count': 0, | |
279 | 'vulnerabilities_count': 1, | |
280 | 'tool': first_command.tool, | |
281 | 'criticalIssue': 0 | |
283 | 282 | } |
284 | 283 | |
285 | 284 | for in_the_middle_command in in_the_middle_commands: |
286 | 285 | raw_in_the_middle_command = list(filter(lambda comm: comm['_id'] == in_the_middle_command.id, res.json)) |
287 | assert raw_in_the_middle_command.pop() == {u'_id': in_the_middle_command.id, | |
288 | u'command': in_the_middle_command.command, | |
289 | u'import_source': u'shell', | |
290 | u'user': in_the_middle_command.user, | |
291 | u'date': time.mktime( | |
286 | assert raw_in_the_middle_command.pop() == {'_id': in_the_middle_command.id, | |
287 | 'command': in_the_middle_command.command, | |
288 | 'import_source': 'shell', | |
289 | 'user': in_the_middle_command.user, | |
290 | 'date': time.mktime( | |
292 | 291 | in_the_middle_command.start_date.timetuple()) * 1000, |
293 | u'params': in_the_middle_command.params, | |
294 | u'hosts_count': 0, | |
295 | u'tool': in_the_middle_command.tool, | |
296 | u'services_count': 0, | |
297 | u'vulnerabilities_count': 0, | |
298 | u'criticalIssue': 0} | |
292 | 'params': in_the_middle_command.params, | |
293 | 'hosts_count': 0, | |
294 | 'tool': in_the_middle_command.tool, | |
295 | 'services_count': 0, | |
296 | 'vulnerabilities_count': 0, | |
297 | 'criticalIssue': 0} | |
299 | 298 | |
300 | 299 | # new command must create new service and vuln |
301 | 300 | raw_last_command = list(filter(lambda comm: comm['_id'] == last_command.id, res.json)) |
302 | assert raw_last_command.pop() == {u'_id': last_command.id, | |
303 | u'command': last_command.command, | |
304 | u'import_source': u'shell', | |
305 | u'user': last_command.user, | |
306 | u'date': time.mktime(last_command.start_date.timetuple()) * 1000, | |
307 | u'params': last_command.params, | |
308 | u'hosts_count': 0, | |
309 | u'tool': last_command.tool, | |
310 | u'services_count': 1, | |
311 | u'vulnerabilities_count': 1, | |
312 | u'criticalIssue': 0} | |
301 | assert raw_last_command.pop() == {'_id': last_command.id, | |
302 | 'command': last_command.command, | |
303 | 'import_source': 'shell', | |
304 | 'user': last_command.user, | |
305 | 'date': time.mktime(last_command.start_date.timetuple()) * 1000, | |
306 | 'params': last_command.params, | |
307 | 'hosts_count': 0, | |
308 | 'tool': last_command.tool, | |
309 | 'services_count': 1, | |
310 | 'vulnerabilities_count': 1, | |
311 | 'criticalIssue': 0} | |
313 | 312 | |
314 | 313 | @pytest.mark.usefixtures('ignore_nplusone') |
315 | 314 | def test_sub_second_command_returns_correct_duration_value(self, test_client): |
369 | 368 | 'hostname': 'mandarina', |
370 | 369 | 'ip': '192.168.20.53', |
371 | 370 | 'itime': 1511387720.048548, |
372 | 'params': u'/home/lcubo/.faraday/report/airbnb/nessus_report_Remote.nessus', | |
371 | 'params': '/home/lcubo/.faraday/report/airbnb/nessus_report_Remote.nessus', | |
373 | 372 | 'user': 'lcubo' |
374 | 373 | } |
375 | 374 | |
387 | 386 | 'hostname': 'mandarina', |
388 | 387 | 'ip': '192.168.20.53', |
389 | 388 | 'itime': start_date.timestamp(), |
390 | 'params': u'/home/lcubo/.faraday/report/airbnb/nessus_report_Remote.nessus', | |
389 | 'params': '/home/lcubo/.faraday/report/airbnb/nessus_report_Remote.nessus', | |
391 | 390 | 'user': 'lcubo' |
392 | 391 | } |
393 | 392 | |
434 | 433 | 'hostname': 'mandarina', |
435 | 434 | 'ip': '192.168.20.53', |
436 | 435 | 'itime': 1511387720000.048548, |
437 | 'params': u'/home/lcubo/.faraday/report/airbnb/nessus_report_Remote.nessus', | |
436 | 'params': '/home/lcubo/.faraday/report/airbnb/nessus_report_Remote.nessus', | |
438 | 437 | 'user': 'lcubo' |
439 | 438 | } |
440 | 439 |
7 | 7 | from faraday.server.api.modules.comments import CommentView |
8 | 8 | from faraday.server.models import Comment |
9 | 9 | from tests.factories import ServiceFactory |
10 | from tests.test_api_workspaced_base import ReadWriteAPITests | |
10 | from tests.test_api_workspaced_base import ReadWriteAPITests, BulkDeleteTestsMixin | |
11 | 11 | from tests import factories |
12 | 12 | |
13 | 13 | |
14 | class TestCommentAPIGeneric(ReadWriteAPITests): | |
14 | class TestCommentAPIGeneric(ReadWriteAPITests, BulkDeleteTestsMixin): | |
15 | 15 | model = Comment |
16 | 16 | factory = factories.CommentFactory |
17 | 17 | view_class = CommentView |
64 | 64 | raw_comment = self._create_raw_comment('service', service.id) |
65 | 65 | res = test_client.post(self.url(workspace=second_workspace), data=raw_comment) |
66 | 66 | assert res.status_code == 400 |
67 | assert res.json == {u'message': u"Can't comment object of another workspace"} | |
67 | assert res.json == {'message': "Can't comment object of another workspace"} | |
68 | 68 | |
69 | 69 | def test_cannot_create_comment_of_inexistent_object(self, test_client, session): |
70 | 70 | raw_comment = self._create_raw_comment('service', 456464556) |
71 | 71 | res = test_client.post(self.url(workspace=self.workspace), data=raw_comment) |
72 | 72 | assert res.status_code == 400 |
73 | assert res.json == {u'message': u"Can't comment inexistent object"} | |
73 | assert res.json == {'message': "Can't comment inexistent object"} | |
74 | 74 | |
75 | 75 | def test_create_unique_comment_for_plugins(self, session, test_client): |
76 | 76 | """ |
121 | 121 | get_comments = test_client.get(self.url(workspace=workspace)) |
122 | 122 | expected = ['first', 'second', 'third', 'fourth'] |
123 | 123 | assert expected == [comment['text'] for comment in get_comments.json] |
124 | ||
125 | def test_bulk_delete_with_references(self, session, test_client): | |
126 | previous_count = session.query(Comment).count() | |
127 | comment_first = factories.CommentFactory.create(workspace=self.workspace, text='first') | |
128 | comment_second = factories.CommentFactory.create(workspace=self.workspace, text='second', reply_to=comment_first) | |
129 | _ = factories.CommentFactory.create(workspace=self.workspace, text='third', reply_to=comment_second) | |
130 | comment_fourth = factories.CommentFactory.create(workspace=self.workspace, text='fourth') | |
131 | session.commit() | |
132 | ||
133 | data = {'ids': [comment_first.id, comment_fourth.id]} | |
134 | res = test_client.delete(self.url(), data=data) | |
135 | ||
136 | assert res.status_code == 200 | |
137 | assert res.json['deleted'] == 2 | |
138 | assert previous_count + 2 == session.query(Comment).count() |
3 | 3 | See the file 'doc/LICENSE' for the license information |
4 | 4 | |
5 | 5 | ''' |
6 | from urllib.parse import urljoin | |
6 | 7 | |
7 | 8 | import pytest |
8 | 9 | |
9 | 10 | from tests import factories |
10 | 11 | from tests.test_api_workspaced_base import ( |
11 | 12 | ReadWriteAPITests, |
13 | BulkUpdateTestsMixin, | |
14 | BulkDeleteTestsMixin | |
12 | 15 | ) |
13 | 16 | from faraday.server.api.modules.credentials import CredentialView |
14 | 17 | from faraday.server.models import Credential |
15 | 18 | from tests.factories import HostFactory, ServiceFactory |
16 | 19 | |
17 | 20 | |
18 | class TestCredentialsAPIGeneric(ReadWriteAPITests): | |
21 | class TestCredentialsAPIGeneric(ReadWriteAPITests, BulkUpdateTestsMixin, BulkDeleteTestsMixin): | |
19 | 22 | model = Credential |
20 | 23 | factory = factories.CredentialFactory |
21 | 24 | view_class = CredentialView |
31 | 34 | assert res.status_code == 200 |
32 | 35 | assert 'rows' in res.json |
33 | 36 | for vuln in res.json['rows']: |
34 | assert set([u'_id', u'id', u'key', u'value']) == set(vuln.keys()) | |
37 | assert {'_id', 'id', 'key', 'value'} == set(vuln.keys()) | |
35 | 38 | object_properties = [ |
36 | u'_id', | |
37 | u'couchdbid', | |
38 | u'description', | |
39 | u'metadata', | |
40 | u'name', | |
41 | u'owner', | |
42 | u'password', | |
43 | u'username', | |
44 | u'host_ip', | |
45 | u'service_name', | |
46 | u'target' | |
39 | '_id', | |
40 | 'couchdbid', | |
41 | 'description', | |
42 | 'metadata', | |
43 | 'name', | |
44 | 'owner', | |
45 | 'password', | |
46 | 'username', | |
47 | 'host_ip', | |
48 | 'service_name', | |
49 | 'target' | |
47 | 50 | ] |
48 | 51 | expected = set(object_properties) |
49 | 52 | result = set(vuln['value'].keys()) |
104 | 107 | credential = self.factory.create(host=host, service=None, |
105 | 108 | workspace=self.workspace) |
106 | 109 | session.commit() |
107 | res = test_client.get(self.url(workspace=credential.workspace) + f'?host_id={credential.host.id}') | |
110 | res = test_client.get(urljoin(self.url(workspace=credential.workspace), f'?host_id={credential.host.id}')) | |
108 | 111 | assert res.status_code == 200 |
109 | 112 | assert [cred['value']['parent'] for cred in res.json['rows']] == [credential.host.id] |
110 | assert [cred['value']['parent_type'] for cred in res.json['rows']] == [u'Host'] | |
113 | assert [cred['value']['parent_type'] for cred in res.json['rows']] == ['Host'] | |
111 | 114 | |
112 | 115 | def test_get_credentials_for_a_service_backwards_compatibility(self, session, test_client): |
113 | 116 | service = ServiceFactory.create() |
114 | 117 | credential = self.factory.create(service=service, host=None, workspace=service.workspace) |
115 | 118 | session.commit() |
116 | res = test_client.get(self.url(workspace=credential.workspace) + f'?service={credential.service.id}') | |
119 | res = test_client.get(urljoin(self.url(workspace=credential.workspace), f'?service={credential.service.id}')) | |
117 | 120 | assert res.status_code == 200 |
118 | 121 | assert [cred['value']['parent'] for cred in res.json['rows']] == [credential.service.id] |
119 | assert [cred['value']['parent_type'] for cred in res.json['rows']] == [u'Service'] | |
122 | assert [cred['value']['parent_type'] for cred in res.json['rows']] == ['Service'] | |
120 | 123 | |
121 | 124 | def _generate_raw_update_data(self, name, username, password, parent_id): |
122 | 125 | return { |
178 | 181 | |
179 | 182 | res = test_client.put(self.url(credential, workspace=credential.workspace), data=raw_data) |
180 | 183 | assert res.status_code == 200 |
181 | assert res.json['username'] == u'Username2' | |
182 | assert res.json['password'] == u'Password3' | |
183 | assert res.json['name'] == u'Name1' | |
184 | assert res.json['username'] == 'Username2' | |
185 | assert res.json['password'] == 'Password3' | |
186 | assert res.json['name'] == 'Name1' | |
184 | 187 | |
185 | 188 | @pytest.mark.parametrize("parent_type, parent_factory", [ |
186 | 189 | ("Host", HostFactory), |
254 | 257 | ] |
255 | 258 | |
256 | 259 | # Desc order |
257 | response = test_client.get(self.url(workspace=second_workspace) + "?sort=target&sort_dir=desc") | |
260 | response = test_client.get(urljoin(self.url(workspace=second_workspace), "?sort=target&sort_dir=desc")) | |
258 | 261 | assert response.status_code == 200 |
259 | 262 | assert sorted(credentials_target, reverse=True) == [v['value']['target'] for v in response.json['rows']] |
260 | 263 | |
261 | 264 | # Asc order |
262 | response = test_client.get(self.url(workspace=second_workspace) + "?sort=target&sort_dir=asc") | |
265 | response = test_client.get(urljoin(self.url(workspace=second_workspace), "?sort=target&sort_dir=asc")) | |
263 | 266 | assert response.status_code == 200 |
264 | 267 | assert sorted(credentials_target) == [v['value']['target'] for v in response.json['rows']] |
0 | 0 | import pytest |
1 | 1 | |
2 | 2 | from tests.factories import CustomFieldsSchemaFactory |
3 | from tests.test_api_non_workspaced_base import ReadWriteAPITests | |
3 | from tests.test_api_non_workspaced_base import ReadWriteAPITests, BulkDeleteTestsMixin | |
4 | 4 | |
5 | 5 | from faraday.server.api.modules.custom_fields import CustomFieldsSchemaView |
6 | 6 | from faraday.server.models import ( |
9 | 9 | |
10 | 10 | |
11 | 11 | @pytest.mark.usefixtures('logged_user') |
12 | class TestVulnerabilityCustomFields(ReadWriteAPITests): | |
12 | class TestVulnerabilityCustomFields(ReadWriteAPITests, BulkDeleteTestsMixin): | |
13 | 13 | model = CustomFieldsSchema |
14 | 14 | factory = CustomFieldsSchemaFactory |
15 | 15 | api_endpoint = 'custom_fields_schema' |
16 | 16 | # unique_fields = ['ip'] |
17 | 17 | # update_fields = ['ip', 'description', 'os'] |
18 | 18 | view_class = CustomFieldsSchemaView |
19 | patchable_fields = ['field_name'] | |
19 | patchable_fields = ['field_display_name'] | |
20 | 20 | |
21 | 21 | def test_custom_fields_data(self, session, test_client): |
22 | 22 | add_text_field = CustomFieldsSchemaFactory.create( |
31 | 31 | |
32 | 32 | res = test_client.get(self.url()) |
33 | 33 | assert res.status_code == 200 |
34 | assert {u'table_name': u'vulnerability', u'id': add_text_field.id, u'field_type': u'text', | |
35 | u'field_name': u'cvss', u'field_display_name': u'CVSS', u'field_metadata': None, | |
36 | u'field_order': 1} in res.json | |
34 | assert {'table_name': 'vulnerability', 'id': add_text_field.id, 'field_type': 'text', | |
35 | 'field_name': 'cvss', 'field_display_name': 'CVSS', 'field_metadata': None, | |
36 | 'field_order': 1} in res.json | |
37 | 37 | |
38 | 38 | def test_custom_fields_field_name_cant_be_changed(self, session, test_client): |
39 | 39 | add_text_field = CustomFieldsSchemaFactory.create( |
47 | 47 | session.commit() |
48 | 48 | |
49 | 49 | data = { |
50 | u'field_name': u'cvss 2', | |
51 | u'field_type': 'int', | |
52 | u'table_name': 'sarasa', | |
53 | u'field_display_name': u'CVSS new', | |
54 | u'field_order': 1 | |
50 | 'field_name': 'cvss 2', | |
51 | 'field_type': 'int', | |
52 | 'table_name': 'sarasa', | |
53 | 'field_display_name': 'CVSS new', | |
54 | 'field_order': 1 | |
55 | 55 | } |
56 | 56 | res = test_client.put(self.url(add_text_field.id), data=data) |
57 | 57 | assert res.status_code == 200 |
77 | 77 | |
78 | 78 | res = test_client.get(self.url()) |
79 | 79 | assert res.status_code == 200 |
80 | assert {u'table_name': u'vulnerability', u'id': add_choice_field.id, u'field_type': u'choice', | |
81 | u'field_name': u'gender', u'field_display_name': u'Gender', u'field_metadata': "['Male', 'Female']", | |
82 | u'field_order': 1} in res.json | |
80 | assert {'table_name': 'vulnerability', 'id': add_choice_field.id, 'field_type': 'choice', | |
81 | 'field_name': 'gender', 'field_display_name': 'Gender', 'field_metadata': "['Male', 'Female']", | |
82 | 'field_order': 1} in res.json |
5 | 5 | ''' |
6 | 6 | import operator |
7 | 7 | from io import BytesIO |
8 | from posixpath import join as urljoin | |
8 | from posixpath import join | |
9 | 9 | |
10 | 10 | import pytz |
11 | 11 | |
12 | from urllib.parse import urlencode | |
12 | from urllib.parse import urlencode, urljoin | |
13 | 13 | from random import choice |
14 | 14 | from sqlalchemy.orm.util import was_deleted |
15 | 15 | from hypothesis import given, strategies as st |
21 | 21 | API_PREFIX, |
22 | 22 | ReadWriteAPITests, |
23 | 23 | PaginationTestsMixin, |
24 | BulkUpdateTestsMixin, | |
25 | BulkDeleteTestsMixin | |
24 | 26 | ) |
25 | 27 | from faraday.server.models import db, Host, Hostname |
26 | 28 | from faraday.server.api.modules.hosts import HostsView |
27 | from tests.factories import HostFactory, EmptyCommandFactory, WorkspaceFactory | |
29 | from tests.factories import HostFactory, EmptyCommandFactory, WorkspaceFactory, HostnameFactory | |
28 | 30 | |
29 | 31 | HOSTS_COUNT = 5 |
30 | 32 | SERVICE_COUNT = [10, 5] # 10 services to the first host, 5 to the second |
60 | 62 | |
61 | 63 | def url(self, host=None, workspace=None): |
62 | 64 | workspace = workspace or self.workspace |
63 | url = API_PREFIX + workspace.name + '/hosts' | |
65 | url = join(API_PREFIX + workspace.name, 'hosts') | |
64 | 66 | if host is not None: |
65 | url += '/' + str(host.id) | |
67 | url = join(url, str(host.id)) | |
66 | 68 | return url |
67 | 69 | |
68 | 70 | def services_url(self, host, workspace=None): |
69 | return self.url(host, workspace) + '/services' | |
71 | return join(self.url(host, workspace), 'services') | |
70 | 72 | |
71 | 73 | def compare_results(self, hosts, response): |
72 | 74 | """ |
73 | 75 | Compare is the hosts in response are the same that in hosts. |
74 | 76 | It only compares the IDs of each one, not other fields""" |
75 | hosts_in_list = set(host.id for host in hosts) | |
76 | hosts_in_response = set(host['id'] for host in response.json['rows']) | |
77 | hosts_in_list = {host.id for host in hosts} | |
78 | hosts_in_response = {host['id'] for host in response.json['rows']} | |
77 | 79 | assert hosts_in_list == hosts_in_response |
78 | 80 | |
79 | 81 | def test_list_retrieves_all_items_from_workspace(self, test_client, |
278 | 280 | host_factory.create_batch(5, workspace=second_workspace, os='Unix') |
279 | 281 | |
280 | 282 | session.commit() |
281 | url = self.url() + '?os=Unix' | |
283 | url = urljoin(self.url(), '?os=Unix') | |
282 | 284 | res = test_client.get(url) |
283 | 285 | assert res.status_code == 200 |
284 | 286 | self.compare_results(hosts, res) |
296 | 298 | host_factory.create_batch(5, workspace=second_workspace, os='Unix') |
297 | 299 | |
298 | 300 | session.commit() |
299 | res = test_client.get(urljoin(self.url(), 'filter?q={"filters":[{"name": "os", "op":"eq", "val":"Unix"}]}')) | |
301 | res = test_client.get(join(self.url(), 'filter?q={"filters":[{"name": "os", "op":"eq", "val":"Unix"}]}')) | |
300 | 302 | assert res.status_code == 200 |
301 | 303 | self.compare_results(hosts, res) |
302 | 304 | |
310 | 312 | host_factory.create_batch(5, workspace=second_workspace, os='Unix') |
311 | 313 | |
312 | 314 | session.commit() |
313 | res = test_client.get(urljoin(self.url(), 'filter?q={"filters":[{"name": "os", "op":"eq", "val":"Unix"}],' | |
315 | res = test_client.get(join(self.url(), 'filter?q={"filters":[{"name": "os", "op":"eq", "val":"Unix"}],' | |
314 | 316 | '"offset":0, "limit":20}')) |
315 | 317 | assert res.status_code == 200 |
316 | 318 | assert res.json['count'] == 30 |
320 | 322 | host_factory.create_batch(10, workspace=workspace, os='Unix') |
321 | 323 | host_factory.create_batch(1, workspace=workspace, os='unix') |
322 | 324 | session.commit() |
323 | res = test_client.get(urljoin(self.url(), 'filter?q={"filters":[{"name": "os", "op": "like", "val": "%nix"}], ' | |
325 | res = test_client.get(join(self.url(), 'filter?q={"filters":[{"name": "os", "op": "like", "val": "%nix"}], ' | |
324 | 326 | '"group_by":[{"field": "os"}], "order_by":[{"field": "os", "direction": "desc"}]}')) |
325 | 327 | assert res.status_code == 200 |
326 | 328 | assert len(res.json['rows']) == 2 |
345 | 347 | host_factory.create_batch(5, workspace=second_workspace, os='Unix') |
346 | 348 | |
347 | 349 | session.commit() |
348 | res = test_client.get(self.url() + '?os__like=Unix %') | |
350 | res = test_client.get(urljoin(self.url(), '?os__like=Unix %')) | |
349 | 351 | assert res.status_code == 200 |
350 | 352 | self.compare_results(hosts, res) |
351 | 353 | |
352 | res = test_client.get(self.url() + '?os__ilike=Unix %') | |
354 | res = test_client.get(urljoin(self.url(), '?os__ilike=Unix %')) | |
353 | 355 | assert res.status_code == 200 |
354 | 356 | self.compare_results(hosts + [case_insensitive_host], res) |
355 | 357 | |
371 | 373 | host_factory.create_batch(5, workspace=second_workspace, os='Unix') |
372 | 374 | |
373 | 375 | session.commit() |
374 | res = test_client.get(urljoin( | |
376 | res = test_client.get(join( | |
375 | 377 | self.url(), |
376 | 378 | 'filter?q={"filters":[{"name": "os", "op":"like", "val":"Unix %"}]}' |
377 | 379 | ) |
379 | 381 | assert res.status_code == 200 |
380 | 382 | self.compare_results(hosts, res) |
381 | 383 | |
382 | res = test_client.get(urljoin( | |
384 | res = test_client.get(join( | |
383 | 385 | self.url(), |
384 | 386 | 'filter?q={"filters":[{"name": "os", "op":"ilike", "val":"Unix %"}]}' |
385 | 387 | ) |
397 | 399 | host_factory.create_batch(5, workspace=workspace) |
398 | 400 | |
399 | 401 | session.commit() |
400 | res = test_client.get(self.url() + '?service=IRC') | |
401 | assert res.status_code == 200 | |
402 | shown_hosts_ids = set(obj['id'] for obj in res.json['rows']) | |
403 | expected_host_ids = set(host.id for host in hosts) | |
402 | res = test_client.get(urljoin(self.url(), '?service=IRC')) | |
403 | assert res.status_code == 200 | |
404 | shown_hosts_ids = {obj['id'] for obj in res.json['rows']} | |
405 | expected_host_ids = {host.id for host in hosts} | |
404 | 406 | assert shown_hosts_ids == expected_host_ids |
405 | 407 | |
406 | 408 | @pytest.mark.usefixtures('ignore_nplusone') |
416 | 418 | session.commit() |
417 | 419 | |
418 | 420 | res = test_client.get( |
419 | urljoin( | |
421 | join( | |
420 | 422 | self.url(), |
421 | 423 | 'filter?q={"filters":[{"name": "services__name", "op":"any", "val":"IRC"}]}' |
422 | 424 | ) |
423 | 425 | ) |
424 | 426 | assert res.status_code == 200 |
425 | shown_hosts_ids = set(obj['id'] for obj in res.json['rows']) | |
426 | expected_host_ids = set(host.id for host in hosts) | |
427 | shown_hosts_ids = {obj['id'] for obj in res.json['rows']} | |
428 | expected_host_ids = {host.id for host in hosts} | |
427 | 429 | assert shown_hosts_ids == expected_host_ids |
428 | 430 | |
429 | 431 | def test_filter_by_service_port(self, test_client, session, workspace, |
435 | 437 | host_factory.create_batch(5, workspace=workspace) |
436 | 438 | |
437 | 439 | session.commit() |
438 | res = test_client.get(self.url() + '?port=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) | |
440 | res = test_client.get(urljoin(self.url(), '?port=25')) | |
441 | assert res.status_code == 200 | |
442 | shown_hosts_ids = {obj['id'] for obj in res.json['rows']} | |
443 | expected_host_ids = {host.id for host in hosts} | |
442 | 444 | assert shown_hosts_ids == expected_host_ids |
443 | 445 | |
444 | 446 | @pytest.mark.usefixtures('ignore_nplusone') |
452 | 454 | |
453 | 455 | session.commit() |
454 | 456 | res = test_client.get( |
455 | urljoin( | |
457 | join( | |
456 | 458 | self.url(), |
457 | 459 | 'filter?q={"filters":[{"name": "services__port", "op":"any", "val":"25"}]}' |
458 | 460 | ) |
459 | 461 | ) |
460 | 462 | assert res.status_code == 200 |
461 | shown_hosts_ids = set(obj['id'] for obj in res.json['rows']) | |
462 | expected_host_ids = set(host.id for host in hosts) | |
463 | shown_hosts_ids = {obj['id'] for obj in res.json['rows']} | |
464 | expected_host_ids = {host.id for host in hosts} | |
463 | 465 | assert shown_hosts_ids == expected_host_ids |
464 | 466 | |
465 | 467 | @pytest.mark.usefixtures('ignore_nplusone') |
478 | 480 | session.commit() |
479 | 481 | |
480 | 482 | res = test_client.get( |
481 | urljoin( | |
483 | join( | |
482 | 484 | self.url(), |
483 | 485 | f'filter?q={{"filters":[{{"name": "ip", "op":"eq", "val":"{host.ip}"}}]}}' |
484 | 486 | ) |
504 | 506 | host_factory.create_batch(5, workspace=workspace) |
505 | 507 | |
506 | 508 | session.commit() |
507 | res = test_client.get(self.url() + '?port=invalid_port') | |
509 | res = test_client.get(urljoin(self.url(), '?port=invalid_port')) | |
508 | 510 | assert res.status_code == 200 |
509 | 511 | assert res.json['count'] == 0 |
510 | 512 | |
519 | 521 | |
520 | 522 | session.commit() |
521 | 523 | res = test_client.get( |
522 | urljoin( | |
524 | join( | |
523 | 525 | self.url(), |
524 | 526 | 'filter?q={"filters":[{"name": "services__port", "op":"any", "val":"sarasa"}]}' |
525 | 527 | ) |
528 | 530 | |
529 | 531 | def test_filter_restless_by_invalid_field(self, test_client): |
530 | 532 | res = test_client.get( |
531 | urljoin( | |
533 | join( | |
532 | 534 | self.url(), |
533 | 535 | 'filter?q={"filters":[{"name": "severity", "op":"any", "val":"sarasa"}]}' |
534 | 536 | ) |
537 | 539 | |
538 | 540 | @pytest.mark.usefixtures('ignore_nplusone') |
539 | 541 | def test_filter_restless_with_no_q_param(self, test_client, session, workspace, host_factory): |
540 | res = test_client.get(urljoin(self.url(), 'filter')) | |
542 | res = test_client.get(join(self.url(), 'filter')) | |
541 | 543 | assert res.status_code == 200 |
542 | 544 | assert len(res.json['rows']) == HOSTS_COUNT |
543 | 545 | |
544 | 546 | @pytest.mark.usefixtures('ignore_nplusone') |
545 | 547 | def test_filter_restless_with_empty_q_param(self, test_client, session, workspace, host_factory): |
546 | res = test_client.get(urljoin(self.url(), 'filter?q')) | |
548 | res = test_client.get(join(self.url(), 'filter?q')) | |
547 | 549 | assert res.status_code == 400 |
548 | 550 | |
549 | 551 | def test_search_ip(self, test_client, session, workspace, host_factory): |
550 | 552 | host = host_factory.create(ip="longname", |
551 | 553 | workspace=workspace) |
552 | 554 | session.commit() |
553 | res = test_client.get(self.url() + '?search=ONGNAM') | |
555 | res = test_client.get(urljoin(self.url(), '?search=ONGNAM')) | |
554 | 556 | assert res.status_code == 200 |
555 | 557 | assert len(res.json['rows']) == 1 |
556 | 558 | assert res.json['rows'][0]['id'] == host.id |
563 | 565 | service_factory.create(host=host, name="GOPHER 5", |
564 | 566 | workspace=workspace) |
565 | 567 | session.commit() |
566 | res = test_client.get(self.url() + '?search=gopher') | |
567 | assert res.status_code == 200 | |
568 | shown_hosts_ids = set(obj['id'] for obj in res.json['rows']) | |
569 | expected_host_ids = set(host.id for host in expected_hosts) | |
568 | res = test_client.get(urljoin(self.url(), '?search=gopher')) | |
569 | assert res.status_code == 200 | |
570 | shown_hosts_ids = {obj['id'] for obj in res.json['rows']} | |
571 | expected_host_ids = {host.id for host in expected_hosts} | |
570 | 572 | assert shown_hosts_ids == expected_host_ids |
571 | 573 | |
572 | 574 | @pytest.mark.usefixtures('host_with_hostnames') |
575 | 577 | for host in expected_hosts: |
576 | 578 | host.set_hostnames(['staging.twitter.com']) |
577 | 579 | session.commit() |
578 | res = test_client.get(self.url() + '?search=twitter') | |
579 | assert res.status_code == 200 | |
580 | shown_hosts_ids = set(obj['id'] for obj in res.json['rows']) | |
581 | expected_host_ids = set(host.id for host in expected_hosts) | |
580 | res = test_client.get(urljoin(self.url(), '?search=twitter')) | |
581 | assert res.status_code == 200 | |
582 | shown_hosts_ids = {obj['id'] for obj in res.json['rows']} | |
583 | expected_host_ids = {host.id for host in expected_hosts} | |
582 | 584 | assert shown_hosts_ids == expected_host_ids |
583 | 585 | |
584 | 586 | def test_host_with_open_vuln_count_verification(self, test_client, session, |
613 | 615 | vulnerability_factory.create(service=service, host=None, workspace=workspace) |
614 | 616 | session.commit() |
615 | 617 | |
616 | res = test_client.get(urljoin(self.url(host), 'services')) | |
618 | res = test_client.get(join(self.url(host), 'services')) | |
617 | 619 | assert res.status_code == 200 |
618 | 620 | assert res.json[0]['vulns'] == 1 |
619 | 621 | |
648 | 650 | } |
649 | 651 | res = test_client.put(self.url(host_with_hostnames), data=data) |
650 | 652 | assert res.status_code == 200 |
651 | expected = set(["other.com", "test.com"]) | |
653 | expected = {"other.com", "test.com"} | |
652 | 654 | assert set(res.json['hostnames']) == expected |
653 | assert set(hn.name for hn in host_with_hostnames.hostnames) == expected | |
655 | assert {hn.name for hn in host_with_hostnames.hostnames} == expected | |
654 | 656 | |
655 | 657 | def test_create_host_with_default_gateway(self, test_client): |
656 | 658 | raw_data = { |
700 | 702 | assert res.status_code == 200 |
701 | 703 | updated_host = Host.query.filter_by(id=host.id).first() |
702 | 704 | assert res.json == { |
703 | u'_id': host.id, | |
704 | u'type': u'Host', | |
705 | u'_rev': u'', | |
706 | u'credentials': 0, | |
707 | u'default_gateway': '', | |
708 | u'description': u'', | |
709 | u'hostnames': [], | |
710 | u'id': host.id, | |
711 | u'ip': u'10.31.112.21', | |
712 | u'mac': '', | |
713 | u'metadata': { | |
714 | u'command_id': None, | |
715 | u'create_time': pytz.UTC.localize(updated_host.create_date).isoformat(), | |
716 | u'creator': u'', | |
717 | u'owner': host.creator.username, | |
718 | u'update_action': 0, | |
719 | u'update_controller_action': u'', | |
720 | u'update_time': pytz.UTC.localize(updated_host.update_date).isoformat(), | |
721 | u'update_user': None}, | |
722 | u'name': u'10.31.112.21', | |
723 | u'os': u'Microsoft Windows Server 2008 R2 Standard Service Pack 1', | |
724 | u'owned': False, | |
725 | u'owner': host.creator.username, | |
726 | u'services': 0, | |
727 | u'service_summaries': [], | |
728 | u'vulns': 0, | |
729 | u"versions": [], | |
730 | u'important': False, | |
731 | u'severity_counts': { | |
732 | u'critical': None, | |
733 | u'high': None, | |
734 | u'host_id': host.id, | |
735 | u'info': None, | |
736 | u'med': None, | |
737 | u'low': None, | |
738 | u'total': None, | |
739 | u'unclassified': None | |
705 | '_id': host.id, | |
706 | 'type': 'Host', | |
707 | '_rev': '', | |
708 | 'credentials': 0, | |
709 | 'default_gateway': '', | |
710 | 'description': '', | |
711 | 'hostnames': [], | |
712 | 'id': host.id, | |
713 | 'ip': '10.31.112.21', | |
714 | 'mac': '', | |
715 | 'metadata': { | |
716 | 'command_id': None, | |
717 | 'create_time': pytz.UTC.localize(updated_host.create_date).isoformat(), | |
718 | 'creator': '', | |
719 | 'owner': host.creator.username, | |
720 | 'update_action': 0, | |
721 | 'update_controller_action': '', | |
722 | 'update_time': pytz.UTC.localize(updated_host.update_date).isoformat(), | |
723 | 'update_user': None}, | |
724 | 'name': '10.31.112.21', | |
725 | 'os': 'Microsoft Windows Server 2008 R2 Standard Service Pack 1', | |
726 | 'owned': False, | |
727 | 'owner': host.creator.username, | |
728 | 'services': 0, | |
729 | 'service_summaries': [], | |
730 | 'vulns': 0, | |
731 | "versions": [], | |
732 | 'important': False, | |
733 | 'severity_counts': { | |
734 | 'critical': None, | |
735 | 'high': None, | |
736 | 'host_id': host.id, | |
737 | 'info': None, | |
738 | 'med': None, | |
739 | 'low': None, | |
740 | 'total': None, | |
741 | 'unclassified': None | |
740 | 742 | } |
741 | 743 | } |
742 | 744 | |
761 | 763 | assert res.json['hosts_with_errors'] == 0 |
762 | 764 | assert session.query(Host).filter_by(description="test_host").count() == expected_created_hosts |
763 | 765 | |
764 | @pytest.mark.skip("This was a v2 test, will be reimplemented") | |
765 | 766 | def test_bulk_delete_hosts(self, test_client, session): |
766 | ws = WorkspaceFactory.create(name="abc") | |
767 | host_1 = HostFactory.create(workspace=ws) | |
768 | host_2 = HostFactory.create(workspace=ws) | |
767 | host_1 = HostFactory.create(workspace=self.workspace) | |
768 | host_2 = HostFactory.create(workspace=self.workspace) | |
769 | 769 | session.commit() |
770 | 770 | hosts_ids = [host_1.id, host_2.id] |
771 | request_data = {'hosts_ids': hosts_ids} | |
772 | ||
773 | delete_response = test_client.delete(f'/v3/ws/{ws.name}/hosts/bulk_delete', data=request_data) | |
774 | ||
775 | deleted_hosts = delete_response.json['deleted_hosts'] | |
771 | request_data = {'ids': hosts_ids} | |
772 | ||
773 | delete_response = test_client.delete(self.url(), data=request_data) | |
774 | ||
775 | deleted_hosts = delete_response.json['deleted'] | |
776 | 776 | host_count_after_delete = db.session.query(Host).filter( |
777 | 777 | Host.id.in_(hosts_ids), |
778 | Host.workspace_id == ws.id).count() | |
778 | Host.workspace_id == self.workspace.id).count() | |
779 | 779 | |
780 | 780 | assert delete_response.status_code == 200 |
781 | 781 | assert deleted_hosts == len(hosts_ids) |
782 | 782 | assert host_count_after_delete == 0 |
783 | 783 | |
784 | @pytest.mark.skip("This was a v2 test, will be reimplemented") | |
785 | 784 | def test_bulk_delete_hosts_without_hosts_ids(self, test_client): |
786 | ws = WorkspaceFactory.create(name="abc") | |
787 | 785 | request_data = {'hosts_ids': []} |
788 | 786 | |
789 | delete_response = test_client.delete(f'/v3/ws/{ws.name}/hosts/bulk_delete', data=request_data) | |
787 | delete_response = test_client.delete(self.url(), data=request_data) | |
790 | 788 | |
791 | 789 | assert delete_response.status_code == 400 |
792 | 790 | |
793 | @pytest.mark.skip("This was a v2 test, will be reimplemented") | |
794 | 791 | def test_bulk_delete_hosts_from_another_workspace(self, test_client, session): |
795 | 792 | workspace_1 = WorkspaceFactory.create(name='workspace_1') |
796 | 793 | host_of_ws_1 = HostFactory.create(workspace=workspace_1) |
799 | 796 | session.commit() |
800 | 797 | |
801 | 798 | # Try to delete workspace_2's host from workspace_1 |
802 | request_data = {'hosts_ids': [host_of_ws_2.id]} | |
803 | url = f'/v3/ws/{workspace_1.name}/hosts/bulk_delete' | |
799 | request_data = {'ids': [host_of_ws_2.id]} | |
800 | url = f'/v3/ws/{workspace_1.name}/hosts' | |
804 | 801 | delete_response = test_client.delete(url, data=request_data) |
805 | 802 | |
806 | assert delete_response.json['deleted_hosts'] == 0 | |
807 | ||
808 | @pytest.mark.skip("This was a v2 test, will be reimplemented") | |
803 | assert delete_response.status_code == 200 | |
804 | assert delete_response.json['deleted'] == 0 | |
805 | ||
809 | 806 | def test_bulk_delete_hosts_invalid_characters_in_request(self, test_client): |
810 | 807 | ws = WorkspaceFactory.create(name="abc") |
811 | request_data = {'hosts_ids': [-1, 'test']} | |
812 | delete_response = test_client.delete(f'/v3/ws/{ws.name}/hosts/bulk_delete', data=request_data) | |
813 | ||
814 | assert delete_response.json['deleted_hosts'] == 0 | |
815 | ||
816 | @pytest.mark.skip("This was a v2 test, will be reimplemented") | |
808 | request_data = {'ids': [-1, 'test']} | |
809 | delete_response = test_client.delete(f'/v3/ws/{ws.name}/hosts', data=request_data) | |
810 | ||
811 | assert delete_response.json['deleted'] == 0 | |
812 | ||
817 | 813 | def test_bulk_delete_hosts_wrong_content_type(self, test_client, session): |
818 | 814 | ws = WorkspaceFactory.create(name="abc") |
819 | 815 | host_1 = HostFactory.create(workspace=ws) |
821 | 817 | session.commit() |
822 | 818 | hosts_ids = [host_1.id, host_2.id] |
823 | 819 | |
824 | request_data = {'hosts_ids': hosts_ids} | |
820 | request_data = {'ids': hosts_ids} | |
825 | 821 | headers = [('content-type', 'text/xml')] |
826 | 822 | |
827 | 823 | delete_response = test_client.delete( |
828 | f'/v3/ws/{ws.name}/hosts/bulk_delete', | |
824 | f'/v3/ws/{ws.name}/hosts', | |
829 | 825 | data=request_data, |
830 | 826 | headers=headers) |
831 | 827 | |
832 | 828 | assert delete_response.status_code == 400 |
833 | 829 | |
834 | ||
835 | class TestHostAPIGeneric(ReadWriteAPITests, PaginationTestsMixin): | |
830 | def test_bulk_delete_with_references(self, test_client, session, workspace, host_factory, vulnerability_factory, | |
831 | service_factory, credential_factory): | |
832 | host_1 = host_factory.create(workspace=workspace) | |
833 | service_factory.create(host=host_1, workspace=workspace) | |
834 | vulnerability_factory.create(service=None, host=host_1, workspace=workspace) | |
835 | host_1.hostnames.append(HostnameFactory.create(name='pepe1', workspace=workspace, host=host_1)) | |
836 | credential_factory.create(workspace=workspace, host=host_1) | |
837 | ||
838 | host_2 = host_factory.create(workspace=workspace) | |
839 | service_factory.create(host=host_2, workspace=workspace) | |
840 | vulnerability_factory.create(service=None, host=host_2, workspace=workspace) | |
841 | host_1.hostnames.append(HostnameFactory.create(name='pepe2', workspace=workspace, host=host_2)) | |
842 | credential_factory.create(workspace=workspace, host=host_2) | |
843 | ||
844 | session.commit() | |
845 | ||
846 | hosts_ids = [host_1.id, host_2.id] | |
847 | request_data = {'ids': hosts_ids} | |
848 | url = f'/v3/ws/{workspace.name}/hosts' | |
849 | delete_response = test_client.delete(url, data=request_data) | |
850 | ||
851 | assert delete_response.status_code == 200 | |
852 | assert delete_response.json['deleted'] == 2 | |
853 | ||
854 | ||
855 | class TestHostAPIGeneric(ReadWriteAPITests, PaginationTestsMixin, BulkUpdateTestsMixin, BulkDeleteTestsMixin): | |
836 | 856 | model = Host |
837 | 857 | factory = factories.HostFactory |
838 | 858 | api_endpoint = 'hosts' |
850 | 870 | expected_ids = [host.id for host in |
851 | 871 | sorted(Host.query.all(), |
852 | 872 | key=operator.attrgetter('description'))] |
853 | res = test_client.get(self.url() + '?sort=description&sort_dir=asc') | |
873 | res = test_client.get(urljoin(self.url(), '?sort=description&sort_dir=asc')) | |
854 | 874 | assert res.status_code == 200 |
855 | 875 | assert [host['_id'] for host in res.json['data']] == expected_ids |
856 | 876 | |
857 | 877 | expected_ids.reverse() # In place list reverse |
858 | res = test_client.get(self.url() + '?sort=description&sort_dir=desc') | |
878 | res = test_client.get(urljoin(self.url(), '?sort=description&sort_dir=desc')) | |
859 | 879 | assert res.status_code == 200 |
860 | 880 | assert [host['_id'] for host in res.json['data']] == expected_ids |
861 | 881 | |
870 | 890 | session.flush() |
871 | 891 | expected_ids.append(host.id) |
872 | 892 | session.commit() |
873 | res = test_client.get(self.url(workspace=second_workspace) | |
874 | + '?sort=services&sort_dir=asc') | |
893 | res = test_client.get(urljoin(self.url(workspace=second_workspace), | |
894 | '?sort=services&sort_dir=asc')) | |
875 | 895 | assert res.status_code == 200 |
876 | 896 | assert [h['_id'] for h in res.json['data']] == expected_ids |
877 | 897 | |
892 | 912 | session.add(host) |
893 | 913 | session.commit() |
894 | 914 | expected.append(host) # Put it on the end |
895 | res = test_client.get(self.url(workspace=second_workspace) | |
896 | + '?sort=metadata.update_time&sort_dir=asc') | |
915 | res = test_client.get(urljoin(self.url(workspace=second_workspace), | |
916 | '?sort=metadata.update_time&sort_dir=asc')) | |
897 | 917 | assert res.status_code == 200, res.data |
898 | 918 | assert [h['_id'] for h in res.json['data']] == [h.id for h in expected] |
899 | 919 | |
921 | 941 | command = EmptyCommandFactory.create() |
922 | 942 | session.commit() |
923 | 943 | assert len(command.command_objects) == 0 |
924 | url = self.url(workspace=command.workspace) + '?' + urlencode({'command_id': command.id}) | |
944 | url = urljoin(self.url(workspace=command.workspace), f"?{urlencode({'command_id': command.id})}") | |
925 | 945 | |
926 | 946 | res = test_client.post(url, data={ |
927 | 947 | "ip": "127.0.0.1", |
939 | 959 | new_workspace = WorkspaceFactory.create() |
940 | 960 | session.commit() |
941 | 961 | assert len(command.command_objects) == 0 |
942 | url = self.url(workspace=new_workspace) + '?' + urlencode({'command_id': command.id}) | |
962 | url = urljoin(self.url(workspace=new_workspace), f"?{urlencode({'command_id': command.id})}") | |
943 | 963 | |
944 | 964 | res = test_client.post(url, data={ |
945 | 965 | "ip": "127.0.0.1", |
947 | 967 | }) |
948 | 968 | |
949 | 969 | assert res.status_code == 400 |
950 | assert res.json == {u'message': u'Command not found.'} | |
970 | assert res.json == {'message': 'Command not found.'} | |
951 | 971 | assert len(command.command_objects) == 0 |
952 | 972 | |
953 | 973 | def test_service_summaries(self, test_client, session, service_factory): |
1102 | 1122 | index_in_response_hosts = response_hosts.index(host) |
1103 | 1123 | |
1104 | 1124 | assert index_in_hosts_ids == index_in_response_hosts |
1125 | ||
1126 | @pytest.mark.usefixtures('ignore_nplusone') | |
1127 | def test_bulk_update_host_with_hostnames(self, test_client, session, | |
1128 | host_with_hostnames): | |
1129 | session.commit() | |
1130 | data = { | |
1131 | "ids": [host_with_hostnames.id, self.first_object.id], | |
1132 | "hostnames": ["other.com", "test.com"], | |
1133 | } | |
1134 | res = test_client.patch(self.url(), data=data) | |
1135 | assert res.status_code == 200 | |
1136 | assert res.json["updated"] == 2 | |
1137 | expected = {"other.com", "test.com"} | |
1138 | assert {hn.name for hn in host_with_hostnames.hostnames} == expected | |
1139 | assert {hn.name for hn in self.first_object.hostnames} == expected | |
1140 | ||
1141 | @pytest.mark.usefixtures('ignore_nplusone') | |
1142 | def test_bulk_update_host_without_hostnames(self, test_client, session, | |
1143 | host_with_hostnames): | |
1144 | session.commit() | |
1145 | expected = {hn.name for hn in host_with_hostnames.hostnames} | |
1146 | data = { | |
1147 | "ids": [host_with_hostnames.id], | |
1148 | "os": "NotAnOS" | |
1149 | } | |
1150 | res = test_client.patch(self.url(), data=data) | |
1151 | assert res.status_code == 200 | |
1152 | assert res.json["updated"] == 1 | |
1153 | assert {hn.name for hn in host_with_hostnames.hostnames} == expected | |
1105 | 1154 | |
1106 | 1155 | |
1107 | 1156 | def host_json(): |
0 | # -*- coding: utf8 -*- | |
1 | 0 | ''' |
2 | 1 | Faraday Penetration Test IDE |
3 | 2 | Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/) |
12 | 11 | from hypothesis import given, strategies as st |
13 | 12 | |
14 | 13 | from tests import factories |
15 | from tests.test_api_non_workspaced_base import ReadWriteAPITests, API_PREFIX | |
14 | from tests.test_api_non_workspaced_base import ( | |
15 | ReadWriteAPITests, | |
16 | API_PREFIX | |
17 | ) | |
16 | 18 | from faraday.server.models import ( |
17 | 19 | License, |
18 | 20 | ) |
32 | 34 | model = License |
33 | 35 | factory = factories.LicenseFactory |
34 | 36 | api_endpoint = 'licenses' |
35 | patchable_fields = ["products"] | |
37 | view_class = LicenseView | |
38 | patchable_fields = ["product"] | |
36 | 39 | |
37 | 40 | # @pytest.mark.skip(reason="Not a license actually test") |
38 | 41 | def test_envelope_list(self, test_client, app): |
0 | ||
1 | 0 | import pytest |
2 | 1 | from flask_security.utils import hash_password |
3 | 2 | from itsdangerous import TimedJSONWebSignatureSerializer |
0 | # -*- coding: utf8 -*- | |
1 | 0 | ''' |
2 | 1 | Faraday Penetration Test IDE |
3 | 2 | Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/) |
4 | 3 | See the file 'doc/LICENSE' for the license information |
5 | 4 | |
6 | 5 | ''' |
7 | from builtins import str | |
8 | ||
6 | from posixpath import join | |
9 | 7 | """Generic tests for APIs NOT prefixed with a workspace_name""" |
10 | 8 | |
11 | 9 | import pytest |
44 | 42 | if obj is not None: |
45 | 43 | id_ = str(getattr(obj, self.lookup_field)) if isinstance( |
46 | 44 | obj, self.model) else str(obj) |
47 | url += u'/' + id_ | |
45 | url = join(url, id_) | |
48 | 46 | return url |
49 | 47 | |
50 | 48 | |
68 | 66 | assert res.status_code == 200 |
69 | 67 | assert isinstance(res.json, dict) |
70 | 68 | |
71 | @pytest.mark.parametrize('object_id', [123456789, -1, 'xxx', u'áá']) | |
69 | @pytest.mark.parametrize('object_id', [123456789, -1, 'xxx', 'áá']) | |
72 | 70 | def test_404_when_retrieving_unexistent_object(self, test_client, |
73 | 71 | object_id): |
74 | 72 | url = self.url(object_id) |
144 | 142 | |
145 | 143 | |
146 | 144 | @pytest.mark.usefixtures('logged_user') |
145 | class BulkUpdateTestsMixin: | |
146 | ||
147 | @staticmethod | |
148 | def control_data(test_suite, data: dict) -> dict: | |
149 | return UpdateTestsMixin.control_data(test_suite, data) | |
150 | ||
151 | def test_bulk_update_an_object(self, test_client, logged_user): | |
152 | all_objs = self.model.query.all() | |
153 | all_objs_id = [obj.__getattribute__(self.view_class.lookup_field) for obj in self.model.query.all()] | |
154 | all_objs, all_objs_id = all_objs[:-1], all_objs_id[:-1] | |
155 | ||
156 | data = self.factory.build_dict() | |
157 | data = BulkUpdateTestsMixin.control_data(self, data) | |
158 | ||
159 | res = test_client.patch(self.url(), data={}) | |
160 | assert res.status_code == 400 | |
161 | data["ids"] = all_objs_id | |
162 | res = test_client.patch(self.url(), data=data) | |
163 | ||
164 | assert res.status_code == 200, (res.status_code, res.json) | |
165 | assert self.model.query.count() == OBJECT_COUNT | |
166 | assert res.json['updated'] == len(all_objs) | |
167 | for obj in self.model.query.all(): | |
168 | if getattr(obj, self.view_class.lookup_field) not in all_objs_id: | |
169 | assert any( | |
170 | [ | |
171 | data[updated_field] != getattr(obj, updated_field) | |
172 | for updated_field in data if updated_field != 'ids' | |
173 | ] | |
174 | ) | |
175 | else: | |
176 | assert all( | |
177 | [ | |
178 | data[updated_field] == getattr(obj, updated_field) | |
179 | for updated_field in data if updated_field != 'ids' | |
180 | ] | |
181 | ) | |
182 | ||
183 | def test_bulk_update_fails_with_existing(self, test_client, session): | |
184 | for unique_field in self.unique_fields: | |
185 | data = self.factory.build_dict() | |
186 | data[unique_field] = getattr(self.objects[3], unique_field) | |
187 | data["ids"] = [getattr(self.objects[i], self.view_class.lookup_field) for i in range(0, 2)], | |
188 | res = test_client.patch(self.url(), data=data) | |
189 | assert res.status_code == 400 | |
190 | assert self.model.query.count() == OBJECT_COUNT | |
191 | assert res.json['updated'] == 0 | |
192 | ||
193 | def test_patch_bulk_update_an_object_does_not_fail_with_partial_data(self, test_client, logged_user): | |
194 | """To do this the user should use a PATCH request""" | |
195 | all_objs_id = [obj.__getattribute__(self.view_class.lookup_field) for obj in self.model.query.all()] | |
196 | res = test_client.patch(self.url(), data={"ids": all_objs_id}) | |
197 | assert res.status_code == 200, (res.status_code, res.json) | |
198 | assert res.json['updated'] == 0 | |
199 | ||
200 | def test_bulk_update_invalid_ids(self, test_client): | |
201 | data = self.factory.build_dict() | |
202 | data = BulkUpdateTestsMixin.control_data(self, data) | |
203 | data['ids'] = [-1, 'test'] | |
204 | res = test_client.patch(self.url(), data=data) | |
205 | assert res.status_code == 200 | |
206 | assert res.json['updated'] == 0 | |
207 | data['ids'] = [-1, 'test', self.first_object.__getattribute__(self.view_class.lookup_field)] | |
208 | res = test_client.patch(self.url(), data=data) | |
209 | assert res.status_code == 200 | |
210 | assert res.json['updated'] == 1 | |
211 | ||
212 | def test_bulk_update_wrong_content_type(self, test_client): | |
213 | all_objs = self.model.query.all() | |
214 | all_objs_id = [obj.__getattribute__(self.view_class.lookup_field) for obj in all_objs] | |
215 | ||
216 | request_data = {'ids': all_objs_id} | |
217 | headers = [('content-type', 'text/xml')] | |
218 | ||
219 | res = test_client.patch(self.url(), data=request_data, headers=headers) | |
220 | assert res.status_code == 400 | |
221 | ||
222 | ||
223 | @pytest.mark.usefixtures('logged_user') | |
147 | 224 | class DeleteTestsMixin: |
148 | 225 | |
149 | 226 | def test_delete(self, test_client, logged_user): |
152 | 229 | assert was_deleted(self.first_object) |
153 | 230 | assert self.model.query.count() == OBJECT_COUNT - 1 |
154 | 231 | |
155 | @pytest.mark.parametrize('object_id', [12300, -1, 'xxx', u'áá']) | |
232 | @pytest.mark.parametrize('object_id', [12300, -1, 'xxx', 'áá']) | |
156 | 233 | def test_delete_non_existent_raises_404(self, test_client, |
157 | 234 | object_id): |
158 | 235 | res = test_client.delete(self.url(object_id)) |
159 | 236 | assert res.status_code == 404 # No content |
160 | 237 | assert self.model.query.count() == OBJECT_COUNT |
238 | ||
239 | ||
240 | @pytest.mark.usefixtures('logged_user') | |
241 | class BulkDeleteTestsMixin: | |
242 | ||
243 | @pytest.mark.usefixtures('ignore_nplusone') | |
244 | def test_bulk_delete(self, test_client): | |
245 | ||
246 | all_objs = self.model.query.all() | |
247 | all_objs_id = [obj.__getattribute__(self.view_class.lookup_field) for obj in all_objs] | |
248 | ignored_obj = all_objs[-1] | |
249 | all_objs, all_objs_id = all_objs[:-1], all_objs_id[:-1] | |
250 | ||
251 | res = test_client.delete(self.url(), data={}) | |
252 | assert res.status_code == 400 | |
253 | data = {"ids": all_objs_id} | |
254 | res = test_client.delete(self.url(), data=data) | |
255 | assert res.status_code == 200 | |
256 | assert all([was_deleted(obj) for obj in all_objs]) | |
257 | assert res.json['deleted'] == len(all_objs) | |
258 | assert not was_deleted(ignored_obj) | |
259 | assert self.model.query.count() == 1 | |
260 | ||
261 | def test_bulk_delete_invalid_ids(self, test_client): | |
262 | request_data = {'ids': [-1, 'test']} | |
263 | count = self.model.query.count() | |
264 | res = test_client.delete(self.url(), data=request_data) | |
265 | assert res.status_code == 200 | |
266 | assert res.json['deleted'] == 0 | |
267 | assert self.model.query.count() == count | |
268 | ||
269 | def test_bulk_delete_wrong_content_type(self, test_client): | |
270 | all_objs = self.model.query.all() | |
271 | all_objs_id = [obj.__getattribute__(self.view_class.lookup_field) for obj in all_objs] | |
272 | count = self.model.query.count() | |
273 | ||
274 | request_data = {'ids': all_objs_id} | |
275 | headers = [('content-type', 'text/xml')] | |
276 | ||
277 | res = test_client.delete(self.url(), data=request_data, headers=headers) | |
278 | assert res.status_code == 400 | |
279 | assert self.model.query.count() == count | |
280 | assert all([not was_deleted(obj) for obj in all_objs]) | |
161 | 281 | |
162 | 282 | |
163 | 283 | class ReadWriteTestsMixin(ListTestsMixin, |
0 | # -*- coding: utf8 -*- | |
1 | 0 | ''' |
2 | 1 | Faraday Penetration Test IDE |
3 | 2 | Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/) |
12 | 11 | try: |
13 | 12 | from urllib import urlencode |
14 | 13 | except ImportError as e: |
15 | from urllib.parse import urlencode | |
14 | from urllib.parse import urlencode, urljoin | |
16 | 15 | |
17 | 16 | |
18 | 17 | def with_0_and_n_objects(n=10): |
45 | 44 | self.view_class.page_number_parameter_name] = page_number |
46 | 45 | if per_page is not None: |
47 | 46 | parameters[self.view_class.per_page_parameter_name] = per_page |
48 | return self.url() + '?' + urlencode(parameters) | |
47 | return urljoin(self.url(), f'?{urlencode(parameters)}') | |
49 | 48 | |
50 | 49 | @pytest.mark.parametrize("page_number", [None, 1, 2]) |
51 | 50 | @pytest.mark.usefixtures('pagination_test_logic') |
77 | 76 | self.create_many_objects(session, object_count) |
78 | 77 | res = test_client.get(self.page_url(-1, 10)) |
79 | 78 | assert res.status_code == 200 |
80 | assert res.json == {u'data': []} | |
79 | assert res.json == {'data': []} | |
81 | 80 | |
82 | 81 | @pytest.mark.usefixtures('pagination_test_logic') |
83 | 82 | @pytest.mark.pagination |
101 | 100 | self.create_many_objects(session, 5) |
102 | 101 | res = test_client.get(self.page_url(2, 5)) |
103 | 102 | assert res.status_code == 200 |
104 | assert res.json == {u'data': []} | |
103 | assert res.json == {'data': []} | |
105 | 104 | |
106 | 105 | @pytest.mark.usefixtures('pagination_test_logic') |
107 | 106 | @pytest.mark.pagination |
0 | # -*- coding: utf8 -*- | |
1 | 0 | ''' |
2 | 1 | Faraday Penetration Test IDE |
3 | 2 | Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/) |
9 | 8 | import pytest |
10 | 9 | |
11 | 10 | from tests.factories import SearchFilterFactory, UserFactory |
12 | from tests.test_api_non_workspaced_base import ReadWriteAPITests | |
11 | from tests.test_api_non_workspaced_base import ( | |
12 | ReadWriteAPITests, | |
13 | BulkUpdateTestsMixin, | |
14 | BulkDeleteTestsMixin | |
15 | ) | |
13 | 16 | from tests.test_api_agent import logout |
14 | 17 | from tests.conftest import login_as |
15 | 18 | from faraday.server.models import SearchFilter |
18 | 21 | |
19 | 22 | |
20 | 23 | @pytest.mark.usefixtures('logged_user') |
21 | class TestSearchFilterAPI(ReadWriteAPITests): | |
24 | class TestSearchFilterAPI(ReadWriteAPITests, BulkUpdateTestsMixin, BulkDeleteTestsMixin): | |
22 | 25 | model = SearchFilter |
23 | 26 | factory = SearchFilterFactory |
24 | 27 | api_endpoint = 'searchfilter' |
117 | 120 | |
118 | 121 | def test_patch_update_an_object_does_not_fail_with_partial_data(self, test_client, logged_user): |
119 | 122 | self.first_object.creator = logged_user |
120 | super().test_update_an_object_fails_with_empty_dict(test_client, logged_user) | |
123 | super().test_patch_update_an_object_does_not_fail_with_partial_data(test_client, logged_user) | |
124 | ||
125 | @pytest.mark.usefixtures('ignore_nplusone') | |
126 | def test_bulk_delete(self, test_client, logged_user): | |
127 | for obj in self.model.query.all(): | |
128 | obj.creator = logged_user | |
129 | super().test_bulk_delete(test_client) |
0 | # -*- coding: utf8 -*- | |
1 | 0 | ''' |
2 | 1 | Faraday Penetration Test IDE |
3 | 2 | Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/) |
15 | 14 | |
16 | 15 | from faraday.server.api.modules.services import ServiceView |
17 | 16 | from tests import factories |
18 | from tests.test_api_workspaced_base import ReadWriteAPITests | |
17 | from tests.test_api_workspaced_base import ReadWriteAPITests, BulkDeleteTestsMixin, BulkUpdateTestsMixin | |
19 | 18 | from faraday.server.models import ( |
20 | Service | |
19 | Service, Credential, Vulnerability | |
21 | 20 | ) |
22 | from tests.factories import HostFactory, EmptyCommandFactory | |
21 | from tests.factories import HostFactory, EmptyCommandFactory, CredentialFactory, VulnerabilityFactory | |
23 | 22 | |
24 | 23 | |
25 | 24 | @pytest.mark.usefixtures('logged_user') |
26 | class TestListServiceView(ReadWriteAPITests): | |
25 | class TestListServiceView(ReadWriteAPITests, BulkUpdateTestsMixin, BulkDeleteTestsMixin): | |
27 | 26 | model = Service |
28 | 27 | factory = factories.ServiceFactory |
29 | 28 | api_endpoint = 'services' |
43 | 42 | assert res.status_code == 200 |
44 | 43 | assert 'services' in res.json |
45 | 44 | for service in res.json['services']: |
46 | assert set([u'id', u'key', u'value']) == set(service.keys()) | |
45 | assert {'id', 'key', 'value'} == set(service.keys()) | |
47 | 46 | object_properties = [ |
48 | u'status', | |
49 | u'protocol', | |
50 | u'description', | |
51 | u'_rev', | |
52 | u'owned', | |
53 | u'owner', | |
54 | u'credentials', | |
55 | u'name', | |
56 | u'version', | |
57 | u'_id', | |
58 | u'metadata' | |
47 | 'status', | |
48 | 'protocol', | |
49 | 'description', | |
50 | '_rev', | |
51 | 'owned', | |
52 | 'owner', | |
53 | 'credentials', | |
54 | 'name', | |
55 | 'version', | |
56 | '_id', | |
57 | 'metadata' | |
59 | 58 | ] |
60 | 59 | expected = set(object_properties) |
61 | 60 | result = set(service['value'].keys()) |
334 | 333 | res = test_client.post(self.url(), data=data) |
335 | 334 | print(res.data) |
336 | 335 | assert res.status_code == 400 |
336 | ||
337 | @pytest.mark.usefixtures('ignore_nplusone') | |
338 | def test_bulk_update_cant_change_id(self, test_client): | |
339 | super().test_bulk_update_cant_change_id(test_client) | |
340 | ||
341 | def test_bulk_delete_with_references(self, test_client, session): | |
342 | previous_creds = Credential.query.count() | |
343 | previous_vulns = Vulnerability.query.count() | |
344 | previous_services = Service.query.count() | |
345 | ||
346 | service_1 = self.factory.create(workspace=self.workspace) | |
347 | service_2 = self.factory.create(workspace=self.workspace) | |
348 | service_3 = self.factory.create(workspace=self.workspace) | |
349 | ||
350 | for _ in range(3): | |
351 | CredentialFactory.create(service=service_1, workspace=self.workspace) | |
352 | VulnerabilityFactory.create(service=service_2, workspace=self.workspace) | |
353 | CredentialFactory.create(service=service_3, workspace=self.workspace) | |
354 | VulnerabilityFactory.create(service=service_3, workspace=self.workspace) | |
355 | session.commit() | |
356 | ||
357 | raw_data = {'ids': [service_1.id, service_2.id, service_3.id]} | |
358 | res = test_client.delete(self.url(), data=raw_data) | |
359 | ||
360 | assert res.status_code == 200 | |
361 | assert res.json['deleted'] == 3 |
0 | # -*- coding: utf8 -*- | |
1 | 0 | ''' |
2 | 1 | Faraday Penetration Test IDE |
3 | 2 | Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/) |
4 | 3 | See the file 'doc/LICENSE' for the license information |
5 | ||
6 | 4 | ''' |
7 | 5 | import csv |
8 | 6 | import json |
9 | 7 | import urllib |
10 | 8 | import datetime |
11 | from builtins import str | |
12 | 9 | from pathlib import Path |
13 | 10 | from tempfile import NamedTemporaryFile |
14 | 11 | from base64 import b64encode |
15 | 12 | from io import BytesIO, StringIO |
16 | from posixpath import join as urljoin | |
13 | from posixpath import join | |
14 | ||
15 | from sqlalchemy.orm.util import was_deleted | |
17 | 16 | |
18 | 17 | try: |
19 | 18 | from urllib import urlencode |
20 | 19 | except ImportError: |
21 | from urllib.parse import urlencode | |
20 | from urllib.parse import urlencode, urljoin | |
22 | 21 | |
23 | 22 | import pytz |
24 | 23 | import pytest |
37 | 36 | from tests import factories |
38 | 37 | from tests.conftest import TEST_DATA_PATH |
39 | 38 | from tests.test_api_workspaced_base import ( |
40 | ReadWriteAPITests | |
39 | ReadWriteAPITests, | |
40 | BulkDeleteTestsMixin, | |
41 | BulkUpdateTestsMixin | |
41 | 42 | ) |
42 | 43 | from faraday.server.models import ( |
43 | 44 | VulnerabilityGeneric, |
50 | 51 | File, |
51 | 52 | Host, |
52 | 53 | Service, |
53 | CVE) | |
54 | CVE, | |
55 | CVSSV2, | |
56 | CVSSV3, | |
57 | SeveritiesHistogram, | |
58 | ) | |
54 | 59 | from tests.factories import ( |
55 | 60 | ServiceFactory, |
56 | 61 | CommandFactory, |
175 | 180 | |
176 | 181 | |
177 | 182 | @pytest.mark.usefixtures('logged_user') |
178 | class TestListVulnerabilityView(ReadWriteAPITests): | |
183 | class TestListVulnerabilityView(ReadWriteAPITests, BulkUpdateTestsMixin, BulkDeleteTestsMixin): | |
179 | 184 | model = Vulnerability |
180 | 185 | factory = factories.VulnerabilityFactory |
181 | 186 | api_endpoint = 'vulns' |
182 | 187 | # unique_fields = ['ip'] |
183 | 188 | # update_fields = ['ip', 'description', 'os'] |
184 | 189 | view_class = VulnerabilityView |
185 | patchable_fields = ['description'] | |
190 | patchable_fields = ['name'] | |
186 | 191 | |
187 | 192 | def test_backward_json_compatibility(self, test_client, second_workspace, session): |
188 | 193 | new_obj = self.factory.create(workspace=second_workspace) |
192 | 197 | assert res.status_code == 200 |
193 | 198 | assert 'vulnerabilities' in res.json |
194 | 199 | for vuln in res.json['vulnerabilities']: |
195 | assert set([u'id', u'key', u'value']) == set(vuln.keys()) | |
200 | assert {'id', 'key', 'value'} == set(vuln.keys()) | |
196 | 201 | object_properties = [ |
197 | u'status', | |
198 | u'issuetracker', | |
199 | u'description', | |
200 | u'parent', | |
201 | u'tags', | |
202 | u'severity', | |
203 | u'_rev', | |
204 | u'easeofresolution', | |
205 | u'owned', | |
206 | u'hostnames', | |
207 | u'pname', | |
208 | u'query', | |
209 | u'owner', | |
210 | u'path', | |
211 | u'data', | |
212 | u'response', | |
213 | u'refs', | |
214 | u'desc', | |
215 | u'impact', | |
216 | u'confirmed', | |
217 | u'name', | |
218 | u'service', | |
219 | u'obj_id', | |
220 | u'type', | |
221 | u'policyviolations', | |
222 | u'request', | |
223 | u'_attachments', | |
224 | u'target', | |
225 | u'_id', | |
226 | u'resolution', | |
227 | u'method', | |
228 | u'metadata', | |
229 | u'website', | |
230 | u'params', | |
202 | 'status', | |
203 | 'issuetracker', | |
204 | 'description', | |
205 | 'parent', | |
206 | 'tags', | |
207 | 'severity', | |
208 | '_rev', | |
209 | 'easeofresolution', | |
210 | 'owned', | |
211 | 'hostnames', | |
212 | 'pname', | |
213 | 'query', | |
214 | 'owner', | |
215 | 'path', | |
216 | 'data', | |
217 | 'response', | |
218 | 'refs', | |
219 | 'desc', | |
220 | 'impact', | |
221 | 'confirmed', | |
222 | 'name', | |
223 | 'service', | |
224 | 'obj_id', | |
225 | 'type', | |
226 | 'policyviolations', | |
227 | 'request', | |
228 | '_attachments', | |
229 | 'target', | |
230 | '_id', | |
231 | 'resolution', | |
232 | 'method', | |
233 | 'metadata', | |
234 | 'website', | |
235 | 'params', | |
231 | 236 | ] |
232 | 237 | expected = set(object_properties) |
233 | 238 | result = set(vuln['value'].keys()) |
294 | 299 | res = test_client.get(self.url(vuln)) |
295 | 300 | assert res.status_code == 200 |
296 | 301 | assert isinstance(res.json['hostnames'], list) |
297 | assert set(res.json['hostnames']) == set(hostname.name for hostname in | |
298 | host_with_hostnames.hostnames) | |
302 | assert set(res.json['hostnames']) == {hostname.name for hostname in | |
303 | host_with_hostnames.hostnames} | |
299 | 304 | |
300 | 305 | def test_create_vuln(self, host_with_hostnames, test_client, session): |
301 | 306 | """ |
329 | 334 | assert res.json['description'] == 'helloworld' |
330 | 335 | assert res.json['severity'] == 'low' |
331 | 336 | |
337 | def test_histogram_creation(self, vulnerability_factory, second_workspace, test_client, session): | |
338 | """ | |
339 | This one should only check basic vuln properties | |
340 | :param host_with_hostnames: | |
341 | :param test_client: | |
342 | :param session: | |
343 | :return: | |
344 | """ | |
345 | ||
346 | vulns = VulnerabilityWeb.query.all() | |
347 | for vuln in vulns: | |
348 | session.delete(vuln) | |
349 | session.commit() | |
350 | ||
351 | vulns = Vulnerability.query.all() | |
352 | for vuln in vulns: | |
353 | session.delete(vuln) | |
354 | session.commit() | |
355 | ||
356 | session.query(SeveritiesHistogram).delete() | |
357 | session.commit() | |
358 | vulns_unconfirmed = vulnerability_factory.create_batch(4, confirmed=False, | |
359 | workspace=self.workspace, | |
360 | status='open', | |
361 | severity='critical') | |
362 | ||
363 | vulns_confirmed = vulnerability_factory.create_batch(4, confirmed=True, | |
364 | workspace=self.workspace, | |
365 | status='open', | |
366 | severity='critical') | |
367 | ||
368 | session.add_all(vulns_confirmed + vulns_unconfirmed) | |
369 | session.commit() | |
370 | ||
371 | histogram = SeveritiesHistogram.query.all() | |
372 | assert len(histogram) == 1 | |
373 | assert histogram[0].workspace_id == self.workspace.id | |
374 | assert histogram[0].critical == 8 | |
375 | assert histogram[0].confirmed == 4 | |
376 | assert histogram[0].date == datetime.date.today() | |
377 | ||
378 | vulns_high = vulnerability_factory.create_batch(4, | |
379 | confirmed=True, | |
380 | workspace=second_workspace, | |
381 | status='open', | |
382 | severity='high') | |
383 | ||
384 | owner = UserFactory.create() | |
385 | service = ServiceFactory.create(workspace=self.workspace) | |
386 | vuln_web = VulnerabilityWebFactory.create( | |
387 | confirmed=True, | |
388 | service=service, | |
389 | creator=owner, | |
390 | workspace=self.workspace, | |
391 | severity='medium' | |
392 | ) | |
393 | ||
394 | vulns_critical = vulnerability_factory.create_batch(4, | |
395 | confirmed=False, | |
396 | workspace=second_workspace, | |
397 | status='open', | |
398 | severity='critical') | |
399 | ||
400 | session.add_all(vulns_high + vulns_critical + [vuln_web]) | |
401 | session.commit() | |
402 | ||
403 | vhigh_id = vulns_high[0].id | |
404 | vhigh2_id = vulns_high[1].id | |
405 | vhigh3_id = vulns_high[2].id | |
406 | ||
407 | histogram = SeveritiesHistogram.query.filter(SeveritiesHistogram.workspace == self.workspace).all() | |
408 | assert len(histogram) == 1 | |
409 | assert histogram[0].workspace_id == self.workspace.id | |
410 | assert histogram[0].critical == 8 | |
411 | assert histogram[0].medium == 1 | |
412 | assert histogram[0].confirmed == 5 | |
413 | ||
414 | histogram = SeveritiesHistogram.query.filter(SeveritiesHistogram.workspace == second_workspace).all() | |
415 | assert len(histogram) == 1 | |
416 | assert histogram[0].workspace_id == second_workspace.id | |
417 | assert histogram[0].critical == 4 | |
418 | assert histogram[0].high == 4 | |
419 | assert histogram[0].medium == 0 | |
420 | assert histogram[0].confirmed == 4 | |
421 | assert histogram[0].date == datetime.date.today() | |
422 | ||
423 | v = Vulnerability.query.get(vhigh_id) | |
424 | v.confirmed = False | |
425 | session.commit() | |
426 | ||
427 | histogram = SeveritiesHistogram.query.filter(SeveritiesHistogram.workspace == second_workspace).all() | |
428 | assert len(histogram) == 1 | |
429 | assert histogram[0].workspace_id == second_workspace.id | |
430 | assert histogram[0].critical == 4 | |
431 | assert histogram[0].high == 4 | |
432 | assert histogram[0].confirmed == 3 | |
433 | assert histogram[0].date == datetime.date.today() | |
434 | ||
435 | v = Vulnerability.query.get(vhigh_id) | |
436 | v.status = 'closed' | |
437 | session.commit() | |
438 | ||
439 | histogram = SeveritiesHistogram.query.filter(SeveritiesHistogram.workspace == second_workspace).all() | |
440 | assert len(histogram) == 1 | |
441 | assert histogram[0].workspace_id == second_workspace.id | |
442 | assert histogram[0].critical == 4 | |
443 | assert histogram[0].high == 3 | |
444 | assert histogram[0].confirmed == 3 | |
445 | assert histogram[0].date == datetime.date.today() | |
446 | ||
447 | v = Vulnerability.query.get(vhigh_id) | |
448 | v.status = 'closed' | |
449 | session.commit() | |
450 | ||
451 | histogram = SeveritiesHistogram.query.filter(SeveritiesHistogram.workspace == second_workspace).all() | |
452 | assert len(histogram) == 1 | |
453 | assert histogram[0].workspace_id == second_workspace.id | |
454 | assert histogram[0].critical == 4 | |
455 | assert histogram[0].high == 3 | |
456 | assert histogram[0].confirmed == 3 | |
457 | assert histogram[0].date == datetime.date.today() | |
458 | ||
459 | v = Vulnerability.query.get(vhigh_id) | |
460 | v.status = 'open' | |
461 | v.confirmed = False | |
462 | session.commit() | |
463 | ||
464 | histogram = SeveritiesHistogram.query.filter(SeveritiesHistogram.workspace == second_workspace).all() | |
465 | assert len(histogram) == 1 | |
466 | assert histogram[0].workspace_id == second_workspace.id | |
467 | assert histogram[0].critical == 4 | |
468 | assert histogram[0].high == 4 | |
469 | assert histogram[0].confirmed == 3 | |
470 | assert histogram[0].date == datetime.date.today() | |
471 | ||
472 | v = Vulnerability.query.get(vhigh_id) | |
473 | v.status = 're-opened' | |
474 | v.confirmed = True | |
475 | session.commit() | |
476 | ||
477 | histogram = SeveritiesHistogram.query.filter(SeveritiesHistogram.workspace == second_workspace).all() | |
478 | assert len(histogram) == 1 | |
479 | assert histogram[0].workspace_id == second_workspace.id | |
480 | assert histogram[0].critical == 4 | |
481 | assert histogram[0].high == 4 | |
482 | assert histogram[0].confirmed == 4 | |
483 | assert histogram[0].date == datetime.date.today() | |
484 | ||
485 | v = Vulnerability.query.get(vhigh_id) | |
486 | v.status = 'risk-accepted' | |
487 | session.commit() | |
488 | ||
489 | histogram = SeveritiesHistogram.query.filter(SeveritiesHistogram.workspace == second_workspace).all() | |
490 | assert len(histogram) == 1 | |
491 | assert histogram[0].workspace_id == second_workspace.id | |
492 | assert histogram[0].critical == 4 | |
493 | assert histogram[0].high == 3 | |
494 | assert histogram[0].confirmed == 3 | |
495 | assert histogram[0].date == datetime.date.today() | |
496 | ||
497 | v = VulnerabilityWeb.query.get(vuln_web.id) | |
498 | v.status = 'closed' | |
499 | session.commit() | |
500 | ||
501 | histogram = SeveritiesHistogram.query.filter(SeveritiesHistogram.workspace == self.workspace).all() | |
502 | assert len(histogram) == 1 | |
503 | assert histogram[0].workspace_id == self.workspace.id | |
504 | assert histogram[0].critical == 8 | |
505 | assert histogram[0].medium == 0 | |
506 | assert histogram[0].date == datetime.date.today() | |
507 | assert histogram[0].confirmed == 4 | |
508 | ||
509 | v = VulnerabilityWeb.query.get(vuln_web.id) | |
510 | v.status = 'closed' | |
511 | session.commit() | |
512 | ||
513 | histogram = SeveritiesHistogram.query.filter(SeveritiesHistogram.workspace == self.workspace).all() | |
514 | assert len(histogram) == 1 | |
515 | assert histogram[0].workspace_id == self.workspace.id | |
516 | assert histogram[0].critical == 8 | |
517 | assert histogram[0].medium == 0 | |
518 | assert histogram[0].date == datetime.date.today() | |
519 | assert histogram[0].confirmed == 4 | |
520 | ||
521 | v = VulnerabilityWeb.query.get(vuln_web.id) | |
522 | v.status = 'open' | |
523 | session.commit() | |
524 | ||
525 | histogram = SeveritiesHistogram.query.filter(SeveritiesHistogram.workspace == self.workspace).all() | |
526 | assert len(histogram) == 1 | |
527 | assert histogram[0].workspace_id == self.workspace.id | |
528 | assert histogram[0].critical == 8 | |
529 | assert histogram[0].medium == 1 | |
530 | assert histogram[0].date == datetime.date.today() | |
531 | assert histogram[0].confirmed == 5 | |
532 | ||
533 | v = VulnerabilityWeb.query.get(vuln_web.id) | |
534 | v.status = 're-opened' | |
535 | session.commit() | |
536 | ||
537 | histogram = SeveritiesHistogram.query.filter(SeveritiesHistogram.workspace == self.workspace).all() | |
538 | assert len(histogram) == 1 | |
539 | assert histogram[0].workspace_id == self.workspace.id | |
540 | assert histogram[0].critical == 8 | |
541 | assert histogram[0].medium == 1 | |
542 | assert histogram[0].date == datetime.date.today() | |
543 | assert histogram[0].confirmed == 5 | |
544 | ||
545 | v = VulnerabilityWeb.query.get(vuln_web.id) | |
546 | v.status = 'risk-accepted' | |
547 | session.commit() | |
548 | ||
549 | histogram = SeveritiesHistogram.query.filter(SeveritiesHistogram.workspace == self.workspace).all() | |
550 | assert len(histogram) == 1 | |
551 | assert histogram[0].workspace_id == self.workspace.id | |
552 | assert histogram[0].critical == 8 | |
553 | assert histogram[0].medium == 0 | |
554 | assert histogram[0].date == datetime.date.today() | |
555 | assert histogram[0].confirmed == 4 | |
556 | ||
557 | v = VulnerabilityWeb.query.get(vuln_web.id) | |
558 | v.status = 'closed' | |
559 | session.commit() | |
560 | ||
561 | histogram = SeveritiesHistogram.query.filter(SeveritiesHistogram.workspace == self.workspace).all() | |
562 | assert len(histogram) == 1 | |
563 | assert histogram[0].workspace_id == self.workspace.id | |
564 | assert histogram[0].critical == 8 | |
565 | assert histogram[0].medium == 0 | |
566 | assert histogram[0].date == datetime.date.today() | |
567 | assert histogram[0].confirmed == 4 | |
568 | ||
569 | v = Vulnerability.query.get(vhigh_id) | |
570 | v.confirmed = False | |
571 | session.commit() | |
572 | ||
573 | histogram = SeveritiesHistogram.query.filter(SeveritiesHistogram.workspace == second_workspace).all() | |
574 | assert len(histogram) == 1 | |
575 | assert histogram[0].workspace_id == second_workspace.id | |
576 | assert histogram[0].critical == 4 | |
577 | assert histogram[0].high == 3 | |
578 | assert histogram[0].confirmed == 3 | |
579 | assert histogram[0].date == datetime.date.today() | |
580 | ||
581 | v = Vulnerability.query.get(vhigh_id) | |
582 | v.confirmed = True | |
583 | session.commit() | |
584 | ||
585 | histogram = SeveritiesHistogram.query.filter(SeveritiesHistogram.workspace == second_workspace).all() | |
586 | assert len(histogram) == 1 | |
587 | assert histogram[0].workspace_id == second_workspace.id | |
588 | assert histogram[0].critical == 4 | |
589 | assert histogram[0].high == 3 | |
590 | assert histogram[0].confirmed == 3 | |
591 | assert histogram[0].date == datetime.date.today() | |
592 | ||
593 | v = Vulnerability.query.get(vhigh_id) | |
594 | v.status = "re-opened" | |
595 | v.confirmed = True | |
596 | session.commit() | |
597 | ||
598 | histogram = SeveritiesHistogram.query.filter(SeveritiesHistogram.workspace == second_workspace).all() | |
599 | assert len(histogram) == 1 | |
600 | assert histogram[0].workspace_id == second_workspace.id | |
601 | assert histogram[0].critical == 4 | |
602 | assert histogram[0].high == 4 | |
603 | assert histogram[0].confirmed == 4 | |
604 | assert histogram[0].date == datetime.date.today() | |
605 | ||
606 | v = Vulnerability.query.get(vhigh_id) | |
607 | v.confirmed = False | |
608 | session.commit() | |
609 | ||
610 | histogram = SeveritiesHistogram.query.filter(SeveritiesHistogram.workspace == second_workspace).all() | |
611 | assert len(histogram) == 1 | |
612 | assert histogram[0].workspace_id == second_workspace.id | |
613 | assert histogram[0].critical == 4 | |
614 | assert histogram[0].high == 4 | |
615 | assert histogram[0].confirmed == 3 | |
616 | assert histogram[0].date == datetime.date.today() | |
617 | ||
618 | v = session.query(Vulnerability).filter(Vulnerability.id == vhigh_id).first() | |
619 | session.delete(v) | |
620 | session.commit() | |
621 | ||
622 | histogram = SeveritiesHistogram.query.filter(SeveritiesHistogram.workspace == second_workspace).all() | |
623 | assert len(histogram) == 1 | |
624 | assert histogram[0].workspace_id == second_workspace.id | |
625 | assert histogram[0].critical == 4 | |
626 | assert histogram[0].high == 3 | |
627 | assert histogram[0].confirmed == 3 | |
628 | assert histogram[0].date == datetime.date.today() | |
629 | ||
630 | Vulnerability.query.filter(Vulnerability.id.in_([vhigh2_id, vhigh3_id])).delete(synchronize_session=False) | |
631 | session.commit() | |
632 | ||
633 | histogram = SeveritiesHistogram.query.filter(SeveritiesHistogram.workspace == second_workspace).all() | |
634 | assert len(histogram) == 1 | |
635 | assert histogram[0].workspace_id == second_workspace.id | |
636 | assert histogram[0].critical == 4 | |
637 | assert histogram[0].high == 1 | |
638 | assert histogram[0].confirmed == 1 | |
639 | assert histogram[0].date == datetime.date.today() | |
640 | ||
641 | v = VulnerabilityWeb.query.get(vuln_web.id) | |
642 | v.status = 'open' | |
643 | session.commit() | |
644 | ||
645 | histogram = SeveritiesHistogram.query.filter(SeveritiesHistogram.workspace == self.workspace).all() | |
646 | assert len(histogram) == 1 | |
647 | assert histogram[0].workspace_id == self.workspace.id | |
648 | assert histogram[0].critical == 8 | |
649 | assert histogram[0].medium == 1 | |
650 | assert histogram[0].date == datetime.date.today() | |
651 | assert histogram[0].confirmed == 5 | |
652 | ||
653 | VulnerabilityWeb.query.filter(VulnerabilityWeb.id == vuln_web.id).delete() | |
654 | session.commit() | |
655 | ||
656 | histogram = SeveritiesHistogram.query.filter(SeveritiesHistogram.workspace == self.workspace).all() | |
657 | assert len(histogram) == 1 | |
658 | assert histogram[0].workspace_id == self.workspace.id | |
659 | assert histogram[0].critical == 8 | |
660 | assert histogram[0].medium == 0 | |
661 | assert histogram[0].confirmed == 4 | |
662 | assert histogram[0].date == datetime.date.today() | |
663 | ||
664 | Vulnerability.query.filter(Vulnerability.workspace == self.workspace, | |
665 | Vulnerability.status == 'open', | |
666 | Vulnerability.severity == 'critical', | |
667 | Vulnerability.confirmed == False).update({'status': 'closed'}) # noqa: E712 | |
668 | session.commit() | |
669 | ||
670 | histogram = SeveritiesHistogram.query.filter(SeveritiesHistogram.workspace == self.workspace).all() | |
671 | assert len(histogram) == 1 | |
672 | assert histogram[0].workspace_id == self.workspace.id | |
673 | assert histogram[0].critical == 4 | |
674 | assert histogram[0].medium == 0 | |
675 | assert histogram[0].date == datetime.date.today() | |
676 | assert histogram[0].confirmed == 4 | |
677 | ||
678 | Vulnerability.query.filter(Vulnerability.workspace == self.workspace, | |
679 | Vulnerability.status == 'open', | |
680 | Vulnerability.severity == 'critical').update({'severity': 'medium'}) | |
681 | session.commit() | |
682 | ||
683 | histogram = SeveritiesHistogram.query.filter(SeveritiesHistogram.workspace == self.workspace).all() | |
684 | assert len(histogram) == 1 | |
685 | assert histogram[0].workspace_id == self.workspace.id | |
686 | assert histogram[0].critical == 0 | |
687 | assert histogram[0].medium == 4 | |
688 | assert histogram[0].date == datetime.date.today() | |
689 | assert histogram[0].confirmed == 4 | |
690 | ||
691 | Vulnerability.query.filter(Vulnerability.workspace == self.workspace, | |
692 | Vulnerability.status == 'open', | |
693 | Vulnerability.severity == 'medium').update({'severity': 'critical', 'status': 'closed'}) | |
694 | session.commit() | |
695 | ||
696 | histogram = SeveritiesHistogram.query.filter(SeveritiesHistogram.workspace == self.workspace).all() | |
697 | assert len(histogram) == 1 | |
698 | assert histogram[0].workspace_id == self.workspace.id | |
699 | assert histogram[0].critical == 0 | |
700 | assert histogram[0].medium == 0 | |
701 | assert histogram[0].date == datetime.date.today() | |
702 | assert histogram[0].confirmed == 0 | |
703 | ||
704 | Vulnerability.query.filter(Vulnerability.workspace == self.workspace, | |
705 | Vulnerability.status == 'closed', | |
706 | Vulnerability.severity == 'critical', | |
707 | Vulnerability.confirmed == True).update({'severity': 'medium', 'status': 're-opened'}) # noqa: E712 | |
708 | session.commit() | |
709 | ||
710 | histogram = SeveritiesHistogram.query.filter(SeveritiesHistogram.workspace == self.workspace).all() | |
711 | assert len(histogram) == 1 | |
712 | assert histogram[0].workspace_id == self.workspace.id | |
713 | assert histogram[0].critical == 0 | |
714 | assert histogram[0].medium == 4 | |
715 | assert histogram[0].date == datetime.date.today() | |
716 | assert histogram[0].confirmed == 4 | |
717 | ||
332 | 718 | def test_create_cannot_create_vuln_with_empty_name_fails( |
333 | 719 | self, host, session, test_client): |
334 | 720 | # I'm using this to test the NonBlankColumn which works for |
409 | 795 | assert filename in res.json['_attachments'] |
410 | 796 | attachment.close() |
411 | 797 | # check the attachment can be downloaded |
412 | res = test_client.get(urljoin(self.url(), f'{vuln_id}/attachment/{filename}')) | |
798 | res = test_client.get(join(self.url(), f'{vuln_id}/attachment/{filename}')) | |
413 | 799 | assert res.status_code == 200 |
414 | 800 | assert res.data == file_content |
415 | 801 | |
416 | res = test_client.get(urljoin( | |
802 | res = test_client.get(join( | |
417 | 803 | self.url(), |
418 | 804 | f'{vuln_id}/attachment/notexistingattachment.png' |
419 | 805 | )) |
440 | 826 | res = test_client.put(self.url(obj=vuln, workspace=self.workspace), data=raw_data) |
441 | 827 | assert res.status_code == 200 |
442 | 828 | filename = attachment.name.split('/')[-1] |
443 | res = test_client.get(urljoin( | |
829 | res = test_client.get(join( | |
444 | 830 | self.url(), f'{vuln.id}/attachment/{filename}' |
445 | 831 | )) |
446 | 832 | assert res.status_code == 200 |
464 | 850 | assert res.status_code == 200 |
465 | 851 | |
466 | 852 | # verify that the old file was deleted and the new one exists |
467 | res = test_client.get(urljoin( | |
853 | res = test_client.get(join( | |
468 | 854 | self.url(), f'{vuln.id}/attachment/{filename}' |
469 | 855 | )) |
470 | 856 | assert res.status_code == 404 |
471 | res = test_client.get(urljoin( | |
857 | res = test_client.get(join( | |
472 | 858 | self.url(), f'{vuln.id}/attachment/{new_filename}' |
473 | 859 | )) |
474 | 860 | assert res.status_code == 200 |
488 | 874 | session.add(new_attach) |
489 | 875 | session.commit() |
490 | 876 | |
491 | res = test_client.get(urljoin(self.url(workspace=workspace), f'{vuln.id}/attachment')) | |
877 | res = test_client.get(join(self.url(workspace=workspace), f'{vuln.id}/attachment')) | |
492 | 878 | assert res.status_code == 200 |
493 | 879 | assert new_attach.filename in res.json |
494 | 880 | assert 'image/png' in res.json[new_attach.filename]['content_type'] |
582 | 968 | |
583 | 969 | def _create_put_data(self, |
584 | 970 | name, desc, status, parent, parent_type, |
585 | attachments=None, impact=None, refs=None, | |
586 | policy_violations=None): | |
587 | if not refs: | |
588 | refs = [] | |
589 | if not policy_violations: | |
590 | policy_violations = [] | |
971 | attachments=None, impact=None, refs=[], | |
972 | policy_violations=[], cve=[]): | |
591 | 973 | |
592 | 974 | if not impact: |
593 | 975 | impact = {"accountability": False, "availability": False, "confidentiality": False, "integrity": False} |
624 | 1006 | "description": "", |
625 | 1007 | "parent_type": parent_type, |
626 | 1008 | "protocol": "", |
627 | "version": ""} | |
1009 | "version": "", | |
1010 | "cve": cve | |
1011 | } | |
628 | 1012 | |
629 | 1013 | if attachments: |
630 | 1014 | raw_data['_attachments'] = {} |
675 | 1059 | res = test_client.put(self.url(vuln), data=raw_data) |
676 | 1060 | assert res.status_code in [400, 409] |
677 | 1061 | assert vuln_count_previous == session.query(Vulnerability).count() |
1062 | ||
1063 | def test_update_vuln_cve(self, test_client, session, host_with_hostnames): | |
1064 | vuln = self.factory.create(status='open', cve=['CVE-2021-1234'], host=host_with_hostnames, service=None, | |
1065 | workspace=host_with_hostnames.workspace) | |
1066 | session.add(vuln) | |
1067 | session.commit() | |
1068 | ||
1069 | vuln = self.factory.create(status='open', cve=['CVE-2021-1234'], host=host_with_hostnames, service=None, | |
1070 | workspace=host_with_hostnames.workspace) | |
1071 | session.add(vuln) | |
1072 | session.commit() | |
1073 | ||
1074 | raw_data = self._create_put_data( | |
1075 | name='New name', | |
1076 | desc='New desc', | |
1077 | status='open', | |
1078 | parent=vuln.host.id, | |
1079 | parent_type='Host', | |
1080 | policy_violations=['pv0'], | |
1081 | cve=['cve-2021-1234'] | |
1082 | ) | |
1083 | vuln_count_previous = session.query(CVE).count() | |
1084 | assert vuln_count_previous == 1 | |
1085 | res = test_client.put(self.url(vuln), data=raw_data) | |
1086 | assert res.status_code == 200 | |
1087 | assert vuln_count_previous == session.query(CVE).count() | |
678 | 1088 | |
679 | 1089 | def test_create_vuln_web(self, host_with_hostnames, test_client, session): |
680 | 1090 | service = ServiceFactory.create(host=host_with_hostnames, workspace=host_with_hostnames.workspace) |
724 | 1134 | session.commit() |
725 | 1135 | expected_ids = {vuln.id for vuln in expected_vulns} |
726 | 1136 | |
727 | res = test_client.get( | |
728 | self.url(workspace=second_workspace) + f'?{param_name}=bbb') | |
1137 | res = test_client.get(urljoin( | |
1138 | self.url(workspace=second_workspace), f'?{param_name}=bbb')) | |
729 | 1139 | assert res.status_code == 200 |
730 | 1140 | |
731 | 1141 | for vuln in res.json['data']: |
732 | 1142 | assert vuln['query'] == 'bbb' |
733 | assert set(vuln['_id'] for vuln in res.json['data']) == expected_ids | |
1143 | assert {vuln['_id'] for vuln in res.json['data']} == expected_ids | |
734 | 1144 | |
735 | 1145 | @pytest.mark.usefixtures('mock_envelope_list') |
736 | 1146 | @pytest.mark.parametrize('medium_name', ['medium', 'med']) |
757 | 1167 | expected_ids.update(vuln.id for vuln in medium_vulns) |
758 | 1168 | expected_ids.update(vuln.id for vuln in medium_vulns_web) |
759 | 1169 | |
760 | res = test_client.get(self.url( | |
761 | workspace=second_workspace) + f'?severity={medium_name}') | |
1170 | res = test_client.get(urljoin(self.url( | |
1171 | workspace=second_workspace), f'?severity={medium_name}')) | |
762 | 1172 | assert res.status_code == 200 |
763 | 1173 | for vuln in res.json['data']: |
764 | 1174 | assert vuln['severity'] == 'med' |
765 | assert set(vuln['_id'] for vuln in res.json['data']) == expected_ids | |
1175 | assert {vuln['_id'] for vuln in res.json['data']} == expected_ids | |
766 | 1176 | |
767 | 1177 | def test_filter_by_invalid_severity_fails(self, test_client): |
768 | res = test_client.get(self.url() + '?severity=131231') | |
1178 | res = test_client.get(urljoin(self.url(), '?severity=131231')) | |
769 | 1179 | assert res.status_code == 400 |
770 | 1180 | assert b'Invalid severity type' in res.data |
771 | 1181 | |
772 | 1182 | @pytest.mark.usefixtures('mock_envelope_list') |
773 | 1183 | def test_filter_by_invalid_severity(self, test_client): |
774 | res = test_client.get(self.url() + '?severity=invalid') | |
1184 | res = test_client.get(urljoin(self.url(), '?severity=invalid')) | |
775 | 1185 | assert res.status_code == 400 |
776 | 1186 | |
777 | 1187 | @pytest.mark.usefixtures('mock_envelope_list') |
795 | 1205 | expected_ids = {vuln.id for vuln in expected_vulns} |
796 | 1206 | |
797 | 1207 | # This shouldn't show any vulns with POSTT method |
798 | res = test_client.get(self.url( | |
799 | workspace=second_workspace) + '?method=POST') | |
800 | assert res.status_code == 200 | |
801 | assert set(vuln['_id'] for vuln in res.json['data']) == expected_ids, "This may fail because no presence of " \ | |
1208 | res = test_client.get(urljoin(self.url( | |
1209 | workspace=second_workspace), '?method=POST')) | |
1210 | assert res.status_code == 200 | |
1211 | assert {vuln['_id'] for vuln in res.json['data']} == expected_ids, "This may fail because no presence of " \ | |
802 | 1212 | "filter_alchemy branch" |
803 | 1213 | |
804 | 1214 | # This shouldn't show any vulns since by default method filter is |
805 | 1215 | # an exact match, not a like statement |
806 | res = test_client.get(self.url( | |
807 | workspace=second_workspace) + '?method=%25POST%25') | |
1216 | res = test_client.get(urljoin(self.url( | |
1217 | workspace=second_workspace), '?method=%25POST%25')) | |
808 | 1218 | assert res.status_code == 200 |
809 | 1219 | assert len(res.json['data']) == 0 |
810 | 1220 | |
829 | 1239 | session.commit() |
830 | 1240 | expected_ids = {vuln.id for vuln in expected_vulns} |
831 | 1241 | |
832 | res = test_client.get(self.url( | |
833 | workspace=second_workspace) + '?website=faradaysec.com') | |
1242 | res = test_client.get(urljoin(self.url( | |
1243 | workspace=second_workspace), '?website=faradaysec.com')) | |
834 | 1244 | assert res.status_code == 200 |
835 | 1245 | |
836 | 1246 | for vuln in res.json['data']: |
837 | 1247 | assert vuln['website'] == 'faradaysec.com' |
838 | assert set(vuln['_id'] for vuln in res.json['data']) == expected_ids | |
1248 | assert {vuln['_id'] for vuln in res.json['data']} == expected_ids | |
839 | 1249 | |
840 | 1250 | @pytest.mark.usefixtures('mock_envelope_list') |
841 | 1251 | def test_filter_by_target(self, test_client, session, host_factory, |
861 | 1271 | expected_ids.add(service_vuln.id) |
862 | 1272 | expected_ids.add(web_vuln.id) |
863 | 1273 | |
864 | res = test_client.get(self.url() + '?target=9.9.9.9') | |
1274 | res = test_client.get(urljoin(self.url(), '?target=9.9.9.9')) | |
865 | 1275 | assert res.status_code == 200 |
866 | 1276 | for vuln in res.json['data']: |
867 | 1277 | assert vuln['target'] == '9.9.9.9' |
868 | assert set(vuln['_id'] for vuln in res.json['data']) == expected_ids | |
1278 | assert {vuln['_id'] for vuln in res.json['data']} == expected_ids | |
869 | 1279 | |
870 | 1280 | @pytest.mark.usefixtures('ignore_nplusone') |
871 | 1281 | @pytest.mark.parametrize('filter_params', [ |
1040 | 1450 | f'"op":"{operation["filter_operation"]}",' \ |
1041 | 1451 | f'"val": {operation["filter_value"]} }}]}}' |
1042 | 1452 | |
1043 | res = test_client.get(urljoin(self.url(), qparams)) | |
1453 | res = test_client.get(join(self.url(), qparams)) | |
1044 | 1454 | |
1045 | 1455 | assert res.status_code == operation['res_status_code'] |
1046 | 1456 | assert len(res.json['vulnerabilities']) == operation['count'] |
1057 | 1467 | f'{{"name": "{filter_params["filter_field_name"]}", '\ |
1058 | 1468 | f'"op":"{operation["filter_operation"]}",'\ |
1059 | 1469 | f'"val": {operation["filter_value"]} }}], {orderparams} }}' |
1060 | res = test_client.get(urljoin(self.url(), qparams)) | |
1470 | res = test_client.get(join(self.url(), qparams)) | |
1061 | 1471 | |
1062 | 1472 | assert res.status_code == operation['res_status_code'] |
1063 | 1473 | assert len(res.json['vulnerabilities']) == operation['count'] |
1074 | 1484 | f'{{"name": "{filter_params["filter_field_name"]}", '\ |
1075 | 1485 | f'"op":"{operation["filter_operation"]}",'\ |
1076 | 1486 | f'"val": {operation["filter_value"]} }}], {groupparams} }}' |
1077 | res = test_client.get(urljoin(self.url(), qparams)) | |
1487 | res = test_client.get(join(self.url(), qparams)) | |
1078 | 1488 | |
1079 | 1489 | assert res.status_code == 200 |
1080 | 1490 | |
1227 | 1637 | res = test_client.post(self.url(workspace=ws), data=raw_data) |
1228 | 1638 | assert res.status_code == 201 |
1229 | 1639 | assert len(res.json['cve']) == 3 |
1230 | assert set(res.json['cve']) == set(['CVE-2018-1234', 'CVE-2017-0002', 'CVE-2017-0012']) | |
1640 | assert set(res.json['cve']) == {'CVE-2018-1234', 'CVE-2017-0002', 'CVE-2017-0012'} | |
1641 | ||
1642 | def test_create_vuln_with_cvssv2(self, host_with_hostnames, test_client, session): | |
1643 | cvssv2_obj = CVSSV2(vector_string='AV:L/AC:L/Au:M/C:C/I:P/A:N') | |
1644 | session.add(cvssv2_obj) | |
1645 | session.commit() | |
1646 | ||
1647 | assert session.query(CVSSV2).count() == 1 | |
1648 | assert cvssv2_obj.base_score == 5.0 | |
1649 | ||
1650 | cvssv2_obj = CVSSV2(vector_string='AV:N/AC:L/Au:M/C:C/I:P/A:N') | |
1651 | ||
1652 | session.add(cvssv2_obj) | |
1653 | session.commit() | |
1654 | ||
1655 | assert session.query(CVSSV2).count() == 2 | |
1656 | assert cvssv2_obj.base_score == 6.8 | |
1657 | ||
1658 | cvssv2_obj = CVSSV2(base_score=4.5) | |
1659 | session.add(cvssv2_obj) | |
1660 | session.commit() | |
1661 | ||
1662 | assert session.query(CVSSV2).count() == 3 | |
1663 | assert cvssv2_obj.base_score == 4.5 | |
1664 | ||
1665 | cvssv2_obj = CVSSV2() | |
1666 | session.add(cvssv2_obj) | |
1667 | session.commit() | |
1668 | ||
1669 | assert session.query(CVSSV2).count() == 4 | |
1670 | assert cvssv2_obj.base_score is None | |
1671 | ||
1672 | cvssv2_obj = CVSSV2(vector_string='AV:N/AC:H/Au:N/C:P/I:N/A:N') | |
1673 | assert cvssv2_obj.base_score == 2.6 | |
1674 | ||
1675 | cvssv2_obj = CVSSV2(vector_string='AV:N/AC:L/Au:M/C:N/I:P/A:P') | |
1676 | assert cvssv2_obj.base_score == 4.7 | |
1677 | ||
1678 | cvssv2_obj = CVSSV2(vector_string='AV:N/AC:H/Au:M/C:N/I:P/A:P') | |
1679 | assert cvssv2_obj.base_score == 3.2 | |
1680 | ||
1681 | cvssv2_obj = CVSSV2(vector_string='AV:N/AC:L/Au:M/C:N/I:P/A:P') | |
1682 | assert cvssv2_obj.base_score == 4.7 | |
1683 | ||
1684 | cvssv2_obj = CVSSV2(vector_string='AV:N/AC:L/Au:N/C:N/I:P/A:P') | |
1685 | assert cvssv2_obj.base_score == 6.4 | |
1686 | ||
1687 | cvssv2_obj = CVSSV2(vector_string='AV:A/AC:L/Au:N/C:N/I:P/A:P') | |
1688 | assert cvssv2_obj.base_score == 4.8 | |
1689 | ||
1690 | cvssv2_obj = CVSSV2(vector_string='AV:N/AC:L/Au:N/C:N/I:P/A:P') | |
1691 | assert cvssv2_obj.base_score == 6.4 | |
1692 | ||
1693 | cvssv2_obj = CVSSV2(vector_string='AV:N/AC:H/Au:N/C:P/I:P/A:P') | |
1694 | assert cvssv2_obj.base_score == 5.1 | |
1695 | ||
1696 | cvssv2_obj = CVSSV2(vector_string='AV:N/AC:H/Au:N/C:P/I:N/A:P') | |
1697 | assert cvssv2_obj.base_score == 4.0 | |
1698 | ||
1699 | cvssv2_obj = CVSSV2(vector_string='AV:N/AC:H/Au:N/C:C/I:C/A:C') | |
1700 | assert cvssv2_obj.base_score == 7.6 | |
1701 | ||
1702 | cvssv2_obj = CVSSV2(vector_string='AV:L/AC:L/Au:N/C:C/I:C/A:N') | |
1703 | assert cvssv2_obj.base_score == 6.6 | |
1704 | ||
1705 | cvssv2_obj = CVSSV2(vector_string='AV:L/AC:L/Au:N/C:C/I:C/A:P') | |
1706 | assert cvssv2_obj.base_score == 6.8 | |
1707 | ||
1708 | cvssv2_obj = CVSSV2(vector_string='AV:L/AC:L/Au:N/C:C/I:C/A:C') | |
1709 | assert cvssv2_obj.base_score == 7.2 | |
1710 | ||
1711 | cvssv2_obj = CVSSV2(vector_string='AV:L/AC:L/Au:N/C:C/I:N/A:C') | |
1712 | assert cvssv2_obj.base_score == 6.6 | |
1713 | ||
1714 | cvssv2_obj = CVSSV2(vector_string='AV:L/AC:L/Au:N/C:N/I:P/A:P') | |
1715 | assert cvssv2_obj.base_score == 3.6 | |
1716 | ||
1717 | def test_create_vuln_with_cvssv3(self, host_with_hostnames, test_client, session): | |
1718 | cvssv3_obj = CVSSV3(vector_string='AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:L') | |
1719 | session.add(cvssv3_obj) | |
1720 | session.commit() | |
1721 | ||
1722 | assert session.query(CVSSV3).count() == 1 | |
1723 | assert cvssv3_obj.base_score == 9.9 | |
1724 | ||
1725 | cvssv3_obj = CVSSV3(vector_string='AV:L/AC:L/PR:L/UI:N/S:C/C:N/I:H/A:L') | |
1726 | session.add(cvssv3_obj) | |
1727 | session.commit() | |
1728 | ||
1729 | assert session.query(CVSSV3).count() == 2 | |
1730 | assert cvssv3_obj.base_score == 7.3 | |
1731 | ||
1732 | cvssv3_obj = CVSSV3(base_score=4.5) | |
1733 | session.add(cvssv3_obj) | |
1734 | session.commit() | |
1735 | ||
1736 | assert session.query(CVSSV3).count() == 3 | |
1737 | assert cvssv3_obj.base_score == 4.5 | |
1738 | ||
1739 | cvssv3_obj = CVSSV3() | |
1740 | session.add(cvssv3_obj) | |
1741 | session.commit() | |
1742 | ||
1743 | assert session.query(CVSSV3).count() == 4 | |
1744 | assert cvssv3_obj.base_score is None | |
1745 | ||
1746 | cvssv3_obj = CVSSV3(vector_string='AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:N') | |
1747 | assert cvssv3_obj.base_score == 0.0 | |
1748 | ||
1749 | cvssv3_obj = CVSSV3(vector_string='AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N') | |
1750 | assert cvssv3_obj.base_score == 4.3 | |
1751 | ||
1752 | cvssv3_obj = CVSSV3(vector_string='AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:N') | |
1753 | assert cvssv3_obj.base_score == 7.1 | |
1754 | ||
1755 | cvssv3_obj = CVSSV3(vector_string='AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N') | |
1756 | assert cvssv3_obj.base_score == 8.1 | |
1757 | ||
1758 | cvssv3_obj = CVSSV3(vector_string='AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:L') | |
1759 | assert cvssv3_obj.base_score == 8.3 | |
1760 | ||
1761 | cvssv3_obj = CVSSV3(vector_string='AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H') | |
1762 | assert cvssv3_obj.base_score == 8.8 | |
1763 | ||
1764 | cvssv3_obj = CVSSV3(vector_string='AV:N/AC:L/PR:L/UI:N/S:C/C:N/I:N/A:N') | |
1765 | assert cvssv3_obj.base_score == 0.0 | |
1766 | ||
1767 | cvssv3_obj = CVSSV3(vector_string='AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:N/A:N') | |
1768 | assert cvssv3_obj.base_score == 5.0 | |
1769 | ||
1770 | cvssv3_obj = CVSSV3(vector_string='AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N') | |
1771 | assert cvssv3_obj.base_score == 7.7 | |
1772 | ||
1773 | cvssv3_obj = CVSSV3(vector_string='AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:L/A:N') | |
1774 | assert cvssv3_obj.base_score == 8.5 | |
1775 | ||
1776 | cvssv3_obj = CVSSV3(vector_string='AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:N') | |
1777 | assert cvssv3_obj.base_score == 9.6 | |
1778 | ||
1779 | cvssv3_obj = CVSSV3(vector_string='AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:L') | |
1780 | assert cvssv3_obj.base_score == 9.9 | |
1781 | ||
1782 | cvssv3_obj = CVSSV3(vector_string='AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H') | |
1783 | assert cvssv3_obj.base_score == 9.9 | |
1784 | ||
1785 | cvssv3_obj = CVSSV3(vector_string='AV:P/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H') | |
1786 | assert cvssv3_obj.base_score == 7.4 | |
1787 | ||
1788 | cvssv3_obj = CVSSV3(vector_string='AV:L/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H') | |
1789 | assert cvssv3_obj.base_score == 8.8 | |
1790 | ||
1791 | cvssv3_obj = CVSSV3(vector_string='AV:A/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H') | |
1792 | assert cvssv3_obj.base_score == 9.0 | |
1793 | ||
1794 | cvssv3_obj = CVSSV3(vector_string='AV:L/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:H') | |
1795 | assert cvssv3_obj.base_score == 8.2 | |
1231 | 1796 | |
1232 | 1797 | def test_create_vuln_with_policyviolations(self, host_with_hostnames, test_client, session): |
1233 | 1798 | session.commit() # flush host_with_hostnames |
1269 | 1834 | assert res.status_code == 201 |
1270 | 1835 | assert vuln_count_previous + 1 == session.query(Vulnerability).count() |
1271 | 1836 | assert res.json['name'] == 'New vulns' |
1272 | assert res.json['impact'] == {u'accountability': True, | |
1273 | u'availability': True, | |
1274 | u'confidentiality': True, | |
1275 | u'integrity': True} | |
1837 | assert res.json['impact'] == {'accountability': True, | |
1838 | 'availability': True, | |
1839 | 'confidentiality': True, | |
1840 | 'integrity': True} | |
1276 | 1841 | |
1277 | 1842 | def test_handles_invalid_impact(self, host_with_hostnames, test_client, |
1278 | 1843 | session): |
1422 | 1987 | |
1423 | 1988 | # Desc |
1424 | 1989 | res = test_client.get( |
1425 | urljoin(self.url(), "count?confirmed=1&group_by=severity&order=sc" | |
1990 | join(self.url(), "count?confirmed=1&group_by=severity&order=sc" | |
1426 | 1991 | )) |
1427 | 1992 | assert res.status_code == 400 |
1428 | 1993 | |
1429 | 1994 | # Asc |
1430 | res = test_client.get(urljoin(self.url(), "count?confirmed=1&group_by=severity&order=name,asc")) | |
1995 | res = test_client.get(join(self.url(), "count?confirmed=1&group_by=severity&order=name,asc")) | |
1431 | 1996 | assert res.status_code == 400 |
1432 | 1997 | |
1433 | 1998 | def test_count_order_by(self, test_client, session): |
1444 | 2009 | |
1445 | 2010 | # Desc |
1446 | 2011 | res = test_client.get( |
1447 | urljoin(self.url(), "count?confirmed=1&group_by=severity&order=desc" | |
2012 | join(self.url(), "count?confirmed=1&group_by=severity&order=desc" | |
1448 | 2013 | )) |
1449 | 2014 | assert res.status_code == 200 |
1450 | 2015 | assert res.json['total_count'] == 3 |
1455 | 2020 | |
1456 | 2021 | # Asc |
1457 | 2022 | res = test_client.get( |
1458 | urljoin(self.url(), "count?confirmed=1&group_by=severity&order=asc")) | |
2023 | join(self.url(), "count?confirmed=1&group_by=severity&order=asc")) | |
1459 | 2024 | assert res.status_code == 200 |
1460 | 2025 | assert res.json['total_count'] == 3 |
1461 | 2026 | assert sorted(res.json['groups'], key=lambda i: (i['name'], i['count'], i['severity']), reverse=True) == sorted( |
1476 | 2041 | session.add(vuln) |
1477 | 2042 | session.commit() |
1478 | 2043 | |
1479 | res = test_client.get(urljoin(self.url(), "count?confirmed=1&group_by=username")) | |
2044 | res = test_client.get(join(self.url(), "count?confirmed=1&group_by=username")) | |
1480 | 2045 | assert res.status_code == 400 |
1481 | 2046 | |
1482 | res = test_client.get(urljoin(self.url(), "count?confirmed=1&group_by=")) | |
2047 | res = test_client.get(join(self.url(), "count?confirmed=1&group_by=")) | |
1483 | 2048 | assert res.status_code == 400 |
1484 | 2049 | |
1485 | 2050 | def test_count_confirmed(self, test_client, session): |
1495 | 2060 | session.add(vuln) |
1496 | 2061 | session.commit() |
1497 | 2062 | |
1498 | res = test_client.get(urljoin(self.url(), 'count?confirmed=1&group_by=severity')) | |
2063 | res = test_client.get(join(self.url(), 'count?confirmed=1&group_by=severity')) | |
1499 | 2064 | assert res.status_code == 200 |
1500 | 2065 | assert res.json['total_count'] == 3 |
1501 | 2066 | assert sorted(res.json['groups'], key=lambda i: (i['count'], i['name'], i['severity'])) == sorted([ |
1514 | 2079 | session.commit() |
1515 | 2080 | |
1516 | 2081 | res = test_client.get( |
1517 | urljoin(self.url(workspace=second_workspace), 'count?group_by=severity' | |
2082 | join(self.url(workspace=second_workspace), 'count?group_by=severity' | |
1518 | 2083 | )) |
1519 | 2084 | assert res.status_code == 200 |
1520 | 2085 | assert res.json['total_count'] == 9 |
1537 | 2102 | session.commit() |
1538 | 2103 | |
1539 | 2104 | res = test_client.get( |
1540 | urljoin( | |
2105 | join( | |
1541 | 2106 | self.url(), |
1542 | 2107 | f'count_multi_workspace?workspaces={self.workspace.name}&confirmed=1&group_by=severity&order=desc' |
1543 | 2108 | ) |
1569 | 2134 | session.commit() |
1570 | 2135 | |
1571 | 2136 | res = test_client.get( |
1572 | urljoin( | |
2137 | join( | |
1573 | 2138 | self.url(), |
1574 | 2139 | f'count_multi_workspace?workspaces={self.workspace.name},' |
1575 | 2140 | f'{second_workspace.name}&confirmed=1&group_by=severity&order=desc' |
1582 | 2147 | |
1583 | 2148 | def test_count_multiworkspace_no_workspace_param(self, test_client): |
1584 | 2149 | res = test_client.get( |
1585 | urljoin(self.url(), 'count_multi_workspace?confirmed=1&group_by=severity&order=desc' | |
2150 | join(self.url(), 'count_multi_workspace?confirmed=1&group_by=severity&order=desc' | |
1586 | 2151 | )) |
1587 | 2152 | assert res.status_code == 400 |
1588 | 2153 | |
1589 | 2154 | def test_count_multiworkspace_no_groupby_param(self, test_client): |
1590 | 2155 | res = test_client.get( |
1591 | urljoin(self.url(), f'count_multi_workspace?workspaces={self.workspace.name}&confirmed=1&order=desc' | |
2156 | join(self.url(), f'count_multi_workspace?workspaces={self.workspace.name}&confirmed=1&order=desc' | |
1592 | 2157 | )) |
1593 | 2158 | assert res.status_code == 400 |
1594 | 2159 | |
1595 | 2160 | def test_count_multiworkspace_nonexistent_ws(self, test_client): |
1596 | 2161 | res = test_client.get( |
1597 | urljoin( | |
2162 | join( | |
1598 | 2163 | self.url(), |
1599 | 2164 | f'count_multi_workspace?workspaces=asdf,{self.workspace.name}&confirmed=1&group_by=severity&order=desc' |
1600 | 2165 | ) |
1700 | 2265 | expected_ids.update(vuln.id for vuln in high_vulns) |
1701 | 2266 | web_expected_ids.update(vuln.id for vuln in high_vulns_web) |
1702 | 2267 | |
1703 | res = test_client.get(self.url( | |
1704 | workspace=second_workspace) + f'?command_id={command.id}') | |
2268 | res = test_client.get(urljoin(self.url( | |
2269 | workspace=second_workspace), f'?command_id={command.id}')) | |
1705 | 2270 | assert res.status_code == 200 |
1706 | 2271 | for vuln in res.json['data']: |
1707 | 2272 | command_object = CommandObject.query.filter_by( |
1710 | 2275 | workspace=second_workspace, |
1711 | 2276 | ).first() |
1712 | 2277 | vuln['metadata']['command_id'] == command_object.command.id |
1713 | assert set(vuln['_id'] for vuln in res.json['data']) == expected_ids | |
2278 | assert {vuln['_id'] for vuln in res.json['data']} == expected_ids | |
1714 | 2279 | |
1715 | 2280 | # Check for web vulns |
1716 | res = test_client.get(self.url( | |
1717 | workspace=second_workspace) + f'?command_id={web_command.id}') | |
2281 | res = test_client.get(urljoin(self.url( | |
2282 | workspace=second_workspace), f'?command_id={web_command.id}')) | |
1718 | 2283 | assert res.status_code == 200 |
1719 | 2284 | for vuln in res.json['data']: |
1720 | 2285 | command_object = CommandObject.query.filter_by( |
1723 | 2288 | workspace=second_workspace, |
1724 | 2289 | ).first() |
1725 | 2290 | vuln['metadata']['command_id'] == command_object.command.id |
1726 | assert set(vuln['_id'] for vuln in res.json['data']) == web_expected_ids | |
2291 | assert {vuln['_id'] for vuln in res.json['data']} == web_expected_ids | |
1727 | 2292 | |
1728 | 2293 | # Check for cross-workspace bugs |
1729 | res = test_client.get(self.url( | |
1730 | workspace=workspace) + f'?command_id={web_command.id}') | |
2294 | res = test_client.get(urljoin(self.url( | |
2295 | workspace=workspace), f'?command_id={web_command.id}')) | |
1731 | 2296 | assert res.status_code == 200 |
1732 | 2297 | assert len(res.json['data']) == 0 |
1733 | 2298 | |
1764 | 2329 | res.json['vulnerabilities'])) |
1765 | 2330 | assert 'metadata' in from_json_vuln[0]['value'] |
1766 | 2331 | expected_metadata = { |
1767 | u'command_id': command.id, | |
1768 | u'create_time': pytz.UTC.localize(vuln.create_date).isoformat(), | |
1769 | u'creator': command.tool, | |
1770 | u'owner': owner.username, | |
1771 | u'update_action': 0, | |
1772 | u'update_controller_action': u'', | |
1773 | u'update_time': pytz.UTC.localize(vuln.update_date).isoformat(), | |
1774 | u'update_user': None | |
2332 | 'command_id': command.id, | |
2333 | 'create_time': pytz.UTC.localize(vuln.create_date).isoformat(), | |
2334 | 'creator': command.tool, | |
2335 | 'owner': owner.username, | |
2336 | 'update_action': 0, | |
2337 | 'update_controller_action': '', | |
2338 | 'update_time': pytz.UTC.localize(vuln.update_date).isoformat(), | |
2339 | 'update_user': None | |
1775 | 2340 | } |
1776 | 2341 | assert expected_metadata == from_json_vuln[0]['value']['metadata'] |
1777 | 2342 | |
1938 | 2503 | ) |
1939 | 2504 | ws_name = host_with_hostnames.workspace.name |
1940 | 2505 | res = test_client.post( |
1941 | urljoin(self.url(workspace=host_with_hostnames.workspace) + f'?command_id={command.id}'), | |
2506 | urljoin(self.url(workspace=host_with_hostnames.workspace), f'?command_id={command.id}'), | |
1942 | 2507 | data=raw_data |
1943 | 2508 | ) |
1944 | 2509 | assert res.status_code == 201 |
1953 | 2518 | severity='high', |
1954 | 2519 | ) |
1955 | 2520 | res = test_client.put( |
1956 | urljoin( | |
2521 | join( | |
1957 | 2522 | self.url(workspace=host_with_hostnames.workspace), f'{res.json["_id"]}?command_id={command.id}' |
1958 | 2523 | ), |
1959 | 2524 | data=raw_data |
1965 | 2530 | service = ServiceFactory.create(workspace=self.workspace) |
1966 | 2531 | session.commit() |
1967 | 2532 | assert len(command.command_objects) == 0 |
1968 | url = self.url(workspace=command.workspace) + '?' + urlencode({'command_id': command.id}) | |
2533 | url = urljoin(self.url(workspace=command.workspace), f"?{urlencode({'command_id': command.id})}") | |
1969 | 2534 | raw_data = _create_post_data_vulnerability( |
1970 | 2535 | name='Update vulnsweb', |
1971 | 2536 | vuln_type='VulnerabilityWeb', |
2055 | 2620 | |
2056 | 2621 | res = test_client.post(self.url(), data=raw_data) |
2057 | 2622 | assert res.status_code == 400 |
2058 | assert res.json == {u'messages': {'json': {u'_schema': [u'Parent id not found: 358302']}}} | |
2623 | assert res.json == {'messages': {'json': {'_schema': ['Parent id not found: 358302']}}} | |
2059 | 2624 | |
2060 | 2625 | def test_after_deleting_vuln_ref_and_policies_remains(self, session, test_client): |
2061 | 2626 | vuln = VulnerabilityFactory.create(workspace=self.workspace) |
2101 | 2666 | session.add(service) |
2102 | 2667 | session.add(hostname) |
2103 | 2668 | session.commit() |
2104 | url = self.url(workspace=workspace) + f'?hostnames={hostname.name}' | |
2669 | url = urljoin(self.url(workspace=workspace), f'?hostnames={hostname.name}') | |
2105 | 2670 | res = test_client.get(url) |
2106 | 2671 | |
2107 | 2672 | assert res.status_code == 200 |
2120 | 2685 | session.add(host) |
2121 | 2686 | session.add(hostname) |
2122 | 2687 | session.commit() |
2123 | url = self.url(workspace=workspace) + f'?hostnames={hostname.name}' | |
2688 | url = urljoin(self.url(workspace=workspace), f'?hostnames={hostname.name}') | |
2124 | 2689 | res = test_client.get(url) |
2125 | 2690 | assert res.status_code == 200 |
2126 | 2691 | assert res.json['count'] == 1 |
2144 | 2709 | session.commit() |
2145 | 2710 | |
2146 | 2711 | # Search with hosnames=HA,HB |
2147 | res = test_client.get(self.url(workspace=vuln.workspace) + f'?hostname={hostnameA},{hostnameB}') | |
2712 | res = test_client.get(urljoin(self.url(workspace=vuln.workspace), f'?hostname={hostnameA},{hostnameB}')) | |
2148 | 2713 | assert res.status_code == 200 |
2149 | 2714 | assert res.json['count'] == 2 |
2150 | 2715 | |
2646 | 3211 | "target", "desc", "status", "hostnames", "comments", "owner", |
2647 | 3212 | "os", "resolution", "refs", "easeofresolution", "web_vulnerability", |
2648 | 3213 | "data", "website", "path", "status_code", "request", "response", "method", |
2649 | "params", "pname", "query", "policyviolations", "external_id", "impact_confidentiality", | |
3214 | "params", "pname", "query", "cve", "policyviolations", "external_id", "impact_confidentiality", | |
2650 | 3215 | "impact_integrity", "impact_availability", "impact_accountability", "update_date", |
2651 | 3216 | "host_id", "host_description", "mac", |
2652 | 3217 | "host_owned", "host_creator_id", "host_date", "host_update_date", |
2663 | 3228 | session.add(confirmed_vulns) |
2664 | 3229 | session.commit() |
2665 | 3230 | res = test_client.get( |
2666 | urljoin( | |
3231 | join( | |
2667 | 3232 | self.url(workspace=workspace), |
2668 | 3233 | 'export_csv?q={"filters":[{"name":"confirmed","op":"==","val":"true"}]}' |
2669 | 3234 | ) |
2674 | 3239 | @pytest.mark.usefixtures('ignore_nplusone') |
2675 | 3240 | def test_export_vuln_csv_unicode_bug(self, test_client, session): |
2676 | 3241 | workspace = WorkspaceFactory.create() |
2677 | desc = u'Latin-1 Supplement \xa1 \xa2 \xa3 \xa4 \xa5 \xa6 \xa7 \xa8' | |
3242 | desc = 'Latin-1 Supplement \xa1 \xa2 \xa3 \xa4 \xa5 \xa6 \xa7 \xa8' | |
2678 | 3243 | confirmed_vulns = VulnerabilityFactory.create( |
2679 | 3244 | confirmed=True, |
2680 | 3245 | description=desc, |
2681 | 3246 | workspace=workspace) |
2682 | 3247 | session.add(confirmed_vulns) |
2683 | 3248 | session.commit() |
2684 | res = test_client.get(urljoin(self.url(workspace=workspace), 'export_csv')) | |
3249 | res = test_client.get(join(self.url(workspace=workspace), 'export_csv')) | |
2685 | 3250 | assert res.status_code == 200 |
2686 | 3251 | assert self._verify_csv(res.data, confirmed=True) |
2687 | 3252 | |
2692 | 3257 | session.add(confirmed_vulns) |
2693 | 3258 | session.commit() |
2694 | 3259 | res = test_client.get( |
2695 | urljoin( | |
3260 | join( | |
2696 | 3261 | self.url(workspace=workspace), |
2697 | 3262 | 'export_csv?q={"filters":[{"name":"severity","op":"==","val":"critical"}]}' |
2698 | 3263 | ) |
2706 | 3271 | session.add(self.first_object) |
2707 | 3272 | session.commit() |
2708 | 3273 | res = test_client.get( |
2709 | urljoin(self.url(), 'export_csv?confirmed=true') | |
3274 | join(self.url(), 'export_csv?confirmed=true') | |
2710 | 3275 | ) |
2711 | 3276 | assert res.status_code == 200 |
2712 | 3277 | self._verify_csv(res.data, confirmed=True) |
2771 | 3336 | session.add(vuln) |
2772 | 3337 | session.commit() |
2773 | 3338 | |
2774 | res = test_client.get(urljoin(self.url(), 'export_csv')) | |
3339 | res = test_client.get(join(self.url(), 'export_csv')) | |
2775 | 3340 | assert self._verify_csv(res.data) |
2776 | 3341 | |
2777 | 3342 | def _verify_csv(self, raw_csv_data, confirmed=False, severity=None): |
2864 | 3429 | assert new_attach.filename in res.json |
2865 | 3430 | assert 'image/png' in res.json[new_attach.filename]['content_type'] |
2866 | 3431 | |
3432 | @pytest.mark.usefixtures('ignore_nplusone') | |
3433 | def test_bulk_update_vuln_cant_change_tool_type_or_attachments(self, test_client, session): | |
3434 | host = HostFactory.create(workspace=self.workspace) | |
3435 | tool = "tool_name" | |
3436 | updated_tool = "new_tool" | |
3437 | vuln = VulnerabilityFactory.create(workspace=self.workspace, host_id=host.id, tool=tool) | |
3438 | session.add(vuln) | |
3439 | session.commit() # flush host_with_hostnames | |
3440 | type = "Vulnerability" if "web" in vuln.type.lower() else "Vulnerability" | |
3441 | # flush host_with_hostnames | |
3442 | attachment = NamedTemporaryFile() | |
3443 | file_content = b'test file' | |
3444 | attachment.write(file_content) | |
3445 | attachment.seek(0) | |
3446 | attachment_data = self._create_put_data( | |
3447 | 'Updated with attachment', | |
3448 | 'Updated vuln', | |
3449 | 'open', | |
3450 | host.id, | |
3451 | 'Host', | |
3452 | attachments=[attachment] | |
3453 | )["_attachments"] | |
3454 | raw_data = {'ids': [vuln.id], 'tool': updated_tool, "type": type, "_attachments": attachment_data} | |
3455 | res = test_client.patch(self.url(), data=raw_data) | |
3456 | assert res.status_code == 200 | |
3457 | assert res.json['updated'] == 0 | |
3458 | ||
2867 | 3459 | |
2868 | 3460 | @pytest.mark.usefixtures('logged_user') |
2869 | 3461 | class TestCustomFieldVulnerability(ReadWriteAPITests): |
2871 | 3463 | factory = factories.VulnerabilityFactory |
2872 | 3464 | api_endpoint = 'vulns' |
2873 | 3465 | view_class = VulnerabilityView |
2874 | patchable_fields = ['description'] | |
3466 | patchable_fields = ['name'] | |
2875 | 3467 | |
2876 | 3468 | def test_create_vuln_with_custom_fields_shown(self, test_client, second_workspace, session): |
2877 | 3469 | host = HostFactory.create(workspace=self.workspace) |
3038 | 3630 | assert res.status_code == 400 |
3039 | 3631 | |
3040 | 3632 | @pytest.mark.usefixtures('ignore_nplusone') |
3041 | @pytest.mark.skip(reason="To be reimplemented") | |
3042 | 3633 | def test_bulk_delete_vuln_id(self, host_with_hostnames, test_client, session): |
3043 | 3634 | """ |
3044 | 3635 | This one should only check basic vuln properties |
3072 | 3663 | vuln_count_previous = session.query(Vulnerability).count() |
3073 | 3664 | res_1 = test_client.post(f'/v3/ws/{ws_name}/vulns', data=raw_data_vuln_1) |
3074 | 3665 | res_2 = test_client.post(f'/v3/ws/{ws_name}/vulns', data=raw_data_vuln_2) |
3075 | vuln_1_id = res_1.json['obj_id'] | |
3076 | vuln_2_id = res_2.json['obj_id'] | |
3666 | vuln_1_id = int(res_1.json['obj_id']) | |
3667 | vuln_2_id = int(res_2.json['obj_id']) | |
3077 | 3668 | vulns_to_delete = [vuln_1_id, vuln_2_id] |
3078 | request_data = {'vulnerability_ids': vulns_to_delete} | |
3079 | delete_response = test_client.delete(f'/v3/ws/{ws_name}/vulns/bulk_delete', data=request_data) | |
3669 | request_data = {'ids': vulns_to_delete} | |
3670 | delete_response = test_client.delete(f'/v3/ws/{ws_name}/vulns', data=request_data) | |
3080 | 3671 | vuln_count_after = session.query(Vulnerability).count() |
3081 | deleted_vulns = delete_response.json['deleted_vulns'] | |
3672 | deleted_vulns = delete_response.json['deleted'] | |
3082 | 3673 | assert delete_response.status_code == 200 |
3083 | 3674 | assert vuln_count_previous == vuln_count_after |
3084 | 3675 | assert deleted_vulns == len(vulns_to_delete) |
3231 | 3822 | assert cmd_obj.object_id == res.json['_id'] |
3232 | 3823 | assert res.json['tool'] == command.tool |
3233 | 3824 | |
3825 | def test_custom_field_cvss(self, session, test_client): | |
3826 | add_text_field = CustomFieldsSchemaFactory.create( | |
3827 | table_name='vulnerability', | |
3828 | field_name='cvss', | |
3829 | field_type='text', | |
3830 | field_display_name='CVSS', | |
3831 | ) | |
3832 | session.add(add_text_field) | |
3833 | session.commit() | |
3834 | ||
3835 | # @pytest.mark.usefixtures('ignore_nplusone') | |
3836 | def test_bulk_delete_by_severity(self, test_client): | |
3837 | all_objs = self.model.query.all() | |
3838 | for obj in all_objs[0:2]: | |
3839 | obj.severity = 'low' # Factory just use "critical" or "high" | |
3840 | ||
3841 | data = {"severities": ["low"]} | |
3842 | res = test_client.delete(self.url(), data=data) | |
3843 | assert res.status_code == 200 | |
3844 | assert all([was_deleted(obj) for obj in all_objs[0:2]]) | |
3845 | assert res.json['deleted'] == 2 | |
3846 | assert all([not was_deleted(obj) for obj in all_objs[2:]]) | |
3847 | assert self.model.query.count() == 3 | |
3848 | ||
3234 | 3849 | @pytest.mark.parametrize('refs', [ |
3235 | 3850 | ('owasp', 'https://www.owasp.org/index.php/XSS_%28Cross_Site_Scripting%29_Prevention_Cheat_Sheet'), |
3236 | 3851 | ('cwe', 'CWE-135'), |
3257 | 3872 | assert ref_name in get_response.json |
3258 | 3873 | assert 1 == len(get_response.json[ref_name]) |
3259 | 3874 | assert ref_example == get_response.json[ref_name][0] |
3260 | ||
3261 | ||
3262 | @pytest.mark.usefixtures('logged_user') | |
3263 | class TestVulnerabilityCustomFields(ReadWriteAPITests): | |
3264 | model = Vulnerability | |
3265 | factory = factories.VulnerabilityFactory | |
3266 | api_endpoint = 'vulns' | |
3267 | view_class = VulnerabilityView | |
3268 | patchable_fields = ['description'] | |
3269 | ||
3270 | def test_custom_field_cvss(self, session, test_client): | |
3271 | add_text_field = CustomFieldsSchemaFactory.create( | |
3272 | table_name='vulnerability', | |
3273 | field_name='cvss', | |
3274 | field_type='text', | |
3275 | field_display_name='CVSS', | |
3276 | ) | |
3277 | session.add(add_text_field) | |
3278 | session.commit() | |
3279 | 3875 | |
3280 | 3876 | |
3281 | 3877 | @pytest.mark.usefixtures('logged_user') |
3432 | 4028 | session.add(host) |
3433 | 4029 | session.commit() |
3434 | 4030 | paginated_vulns = set() |
3435 | expected_vulns = set([vuln.id for vuln in vulns]) | |
4031 | expected_vulns = {vuln.id for vuln in vulns} | |
3436 | 4032 | for offset in range(0, 10): |
3437 | 4033 | query_filter = { |
3438 | 4034 | "filters": [{"name": "severity", "op": "eq", "val": "high"}], |
3471 | 4067 | session.add(host) |
3472 | 4068 | session.commit() |
3473 | 4069 | paginated_vulns = set() |
3474 | expected_vulns = set([vuln.id for vuln in med_vulns]) | |
4070 | expected_vulns = {vuln.id for vuln in med_vulns} | |
3475 | 4071 | for offset in range(0, 10): |
3476 | 4072 | query_filter = { |
3477 | 4073 | "filters": [{"name": "severity", "op": "eq", "val": "medium"}], |
0 | # -*- coding: utf8 -*- | |
1 | 0 | ''' |
2 | 1 | Faraday Penetration Test IDE |
3 | 2 | Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/) |
11 | 10 | from faraday.server.api.modules.vulnerability_template import VulnerabilityTemplateView |
12 | 11 | from tests import factories |
13 | 12 | from tests.test_api_non_workspaced_base import ( |
14 | ReadWriteAPITests | |
13 | ReadWriteAPITests, BulkUpdateTestsMixin, BulkDeleteTestsMixin | |
15 | 14 | ) |
16 | 15 | from faraday.server.models import ( |
17 | 16 | VulnerabilityTemplate, |
43 | 42 | |
44 | 43 | |
45 | 44 | @pytest.mark.usefixtures('logged_user') |
46 | class TestListVulnerabilityTemplateView(ReadWriteAPITests): | |
45 | class TestListVulnerabilityTemplateView(ReadWriteAPITests, BulkUpdateTestsMixin, BulkDeleteTestsMixin): | |
47 | 46 | model = VulnerabilityTemplate |
48 | 47 | factory = factories.VulnerabilityTemplateFactory |
49 | 48 | api_endpoint = 'vulnerability_template' |
57 | 56 | assert res.status_code == 200 |
58 | 57 | assert 'rows' in res.json |
59 | 58 | for vuln in res.json['rows']: |
60 | assert set([u'id', u'key', u'value', u'doc']) == set(vuln.keys()) | |
59 | assert {'id', 'key', 'value', 'doc'} == set(vuln.keys()) | |
61 | 60 | object_properties = [ |
62 | u'exploitation', | |
63 | u'references', | |
64 | u'refs', | |
65 | u'name', | |
66 | u'cwe', | |
67 | u'_rev', | |
68 | u'_id', | |
69 | u'resolution', | |
70 | u'description', | |
71 | u'desc' | |
61 | 'exploitation', | |
62 | 'references', | |
63 | 'refs', | |
64 | 'name', | |
65 | 'cwe', | |
66 | '_rev', | |
67 | '_id', | |
68 | 'resolution', | |
69 | 'description', | |
70 | 'desc' | |
72 | 71 | ] |
73 | 72 | |
74 | 73 | expected = set(object_properties) |
253 | 252 | assert updated_template.severity == raw_data['exploitation'] |
254 | 253 | assert updated_template.resolution == raw_data['resolution'] |
255 | 254 | assert updated_template.description == raw_data['description'] |
256 | assert updated_template.references == set([]) | |
255 | assert updated_template.references == set() | |
257 | 256 | |
258 | 257 | @pytest.mark.parametrize('references', [ |
259 | 258 | ',', |
273 | 272 | |
274 | 273 | def test_update_vulnerabiliy_template_change_refs(self, session, test_client): |
275 | 274 | template = self.factory.create() |
276 | for ref_name in set(['old1', 'old2']): | |
275 | for ref_name in {'old1', 'old2'}: | |
277 | 276 | ref = ReferenceTemplateFactory.create(name=ref_name) |
278 | 277 | self.first_object.reference_template_instances.add(ref) |
279 | 278 | session.commit() |
285 | 284 | assert updated_template.severity == raw_data['exploitation'] |
286 | 285 | assert updated_template.resolution == raw_data['resolution'] |
287 | 286 | assert updated_template.description == raw_data['description'] |
288 | assert updated_template.references == set([u'another_ref', u'new_ref']) | |
287 | assert updated_template.references == {'another_ref', 'new_ref'} | |
289 | 288 | |
290 | 289 | def test_create_new_vulnerability_template_with_references(self, session, test_client): |
291 | 290 | vuln_count_previous = session.query(VulnerabilityTemplate).count() |
293 | 292 | res = test_client.post('/v3/vulnerability_template', data=raw_data) |
294 | 293 | assert res.status_code == 201 |
295 | 294 | assert isinstance(res.json['_id'], int) |
296 | assert set(res.json['refs']) == set(['ref1', 'ref2']) | |
295 | assert set(res.json['refs']) == {'ref1', 'ref2'} | |
297 | 296 | assert vuln_count_previous + 1 == session.query(VulnerabilityTemplate).count() |
298 | 297 | new_template = session.query(VulnerabilityTemplate).filter_by(id=res.json['_id']).first() |
299 | assert new_template.references == set([u'ref1', u'ref2']) | |
298 | assert new_template.references == {'ref1', 'ref2'} | |
300 | 299 | |
301 | 300 | def test_delete_vuln_template(self, session, test_client): |
302 | 301 | template = self.factory.create() |
371 | 370 | |
372 | 371 | res = test_client.post(self.url(), data=raw_data) |
373 | 372 | assert res.status_code == 201 |
374 | assert res.json['customfields'] == {u'cvss': u'value'} | |
373 | assert res.json['customfields'] == {'cvss': 'value'} | |
375 | 374 | |
376 | 375 | def test_update_vuln_template_with_custom_fields(self, session, test_client): |
377 | 376 | |
403 | 402 | |
404 | 403 | res = test_client.put(self.url(template.id), data=raw_data) |
405 | 404 | assert res.status_code == 200 |
406 | assert res.json['customfields'] == {u'cvss': u'updated value'} | |
405 | assert res.json['customfields'] == {'cvss': 'updated value'} | |
407 | 406 | |
408 | 407 | vuln_template = session.query(VulnerabilityTemplate).filter_by(id=template.id).first() |
409 | assert vuln_template.custom_fields == {u'cvss': u'updated value'} | |
408 | assert vuln_template.custom_fields == {'cvss': 'updated value'} | |
410 | 409 | |
411 | 410 | def test_add_vuln_template_from_csv(self, session, test_client, csrf_token): |
412 | 411 | expected_created_vuln_template = 1 |
609 | 608 | assert res.json['vulns_with_conflict'][1][1] == vuln_2['name'] |
610 | 609 | |
611 | 610 | assert len(res.json['vulns_created']) == 0 |
611 | ||
612 | def test_bulk_delete_vulnerabilities_template(self, test_client, session): | |
613 | previous_count = session.query(VulnerabilityTemplate).count() | |
614 | vuln_template_1 = VulnerabilityTemplate(name='vuln_1', severity='high') | |
615 | session.add(vuln_template_1) | |
616 | vuln_template_2 = VulnerabilityTemplate(name='vuln_2', severity='high') | |
617 | session.add(vuln_template_2) | |
618 | vuln_template_3 = VulnerabilityTemplate(name='vuln_3', severity='high') | |
619 | session.add(vuln_template_3) | |
620 | session.commit() | |
621 | ||
622 | data = {'ids': [vuln_template_1.id, vuln_template_2.id, vuln_template_3.id]} | |
623 | res = test_client.delete(self.url(), data=data) | |
624 | assert res.status_code == 200 | |
625 | assert res.json['deleted'] == 3 | |
626 | assert previous_count == session.query(VulnerabilityTemplate).count() |
3 | 3 | See the file 'doc/LICENSE' for the license information |
4 | 4 | |
5 | 5 | ''' |
6 | ||
6 | from datetime import date | |
7 | 7 | import time |
8 | from urllib.parse import urljoin | |
9 | ||
8 | 10 | import pytest |
9 | from posixpath import join as urljoin | |
10 | ||
11 | from faraday.server.models import Workspace, Scope | |
11 | from posixpath import join | |
12 | ||
13 | from faraday.server.models import Workspace, Scope, SeveritiesHistogram | |
12 | 14 | from faraday.server.api.modules.workspaces import WorkspaceView |
13 | from tests.test_api_non_workspaced_base import ReadWriteAPITests | |
15 | from tests.test_api_non_workspaced_base import ReadWriteAPITests, BulkDeleteTestsMixin | |
14 | 16 | from tests import factories |
15 | 17 | |
16 | 18 | |
17 | class TestWorkspaceAPI(ReadWriteAPITests): | |
19 | class TestWorkspaceAPI(ReadWriteAPITests, BulkDeleteTestsMixin): | |
18 | 20 | model = Workspace |
19 | 21 | factory = factories.WorkspaceFactory |
20 | 22 | api_endpoint = 'ws' |
21 | 23 | lookup_field = 'name' |
22 | 24 | view_class = WorkspaceView |
23 | patchable_fields = ['name'] | |
25 | patchable_fields = ['description'] | |
24 | 26 | |
25 | 27 | @pytest.mark.usefixtures('ignore_nplusone') |
26 | 28 | def test_filter_restless_by_name(self, test_client): |
27 | 29 | res = test_client.get( |
28 | urljoin( | |
30 | join( | |
29 | 31 | self.url(), |
30 | 32 | f'filter?q={{"filters":[{{"name": "name", "op":"eq", "val": "{self.first_object.name}"}}]}}' |
31 | 33 | ) |
37 | 39 | @pytest.mark.usefixtures('ignore_nplusone') |
38 | 40 | def test_filter_restless_by_name_zero_results_found(self, test_client): |
39 | 41 | res = test_client.get( |
40 | urljoin( | |
42 | join( | |
41 | 43 | self.url(), |
42 | 44 | 'filter?q={"filters":[{"name": "name", "op":"eq", "val": "thiswsdoesnotexist"}]}' |
43 | 45 | ) |
48 | 50 | def test_filter_restless_by_description(self, test_client): |
49 | 51 | self.first_object.description = "this is a new description" |
50 | 52 | res = test_client.get( |
51 | urljoin( | |
53 | join( | |
52 | 54 | self.url(), |
53 | 55 | f'filter?q={{"filters":[{{"name": "description", "op":"eq", "val": "{self.first_object.description}"}}' |
54 | 56 | ']}' |
84 | 86 | |
85 | 87 | self.first_object.description = "this is a new description" |
86 | 88 | res = test_client.get( |
87 | urljoin( | |
89 | join( | |
88 | 90 | self.url(), |
89 | 91 | f'filter?q={{"filters":[{{"name": "description", "op":"eq", "val": "{self.first_object.description}"}}' |
90 | 92 | ']}' |
135 | 137 | |
136 | 138 | session.add_all(vulns) |
137 | 139 | session.commit() |
138 | res = test_client.get(self.url(self.first_object) + querystring) | |
140 | res = test_client.get(urljoin(self.url(self.first_object), querystring)) | |
139 | 141 | assert res.status_code == 200 |
140 | 142 | assert res.json['stats']['code_vulns'] == 0 |
141 | 143 | assert res.json['stats']['web_vulns'] == 2 |
147 | 149 | assert res.json['stats']['opened_vulns'] == 10 |
148 | 150 | assert res.json['stats']['confirmed_vulns'] == 2 |
149 | 151 | |
152 | @pytest.mark.skip_sql_dialect('sqlite') | |
153 | @pytest.mark.usefixtures('ignore_nplusone') | |
154 | def test_histogram(self, | |
155 | vulnerability_factory, | |
156 | vulnerability_web_factory, | |
157 | second_workspace, | |
158 | test_client, | |
159 | session): | |
160 | ||
161 | session.query(SeveritiesHistogram).delete() | |
162 | session.commit() | |
163 | ||
164 | vulns = vulnerability_factory.create_batch(8, workspace=self.first_object, | |
165 | confirmed=False, status='open', severity='critical') | |
166 | ||
167 | vulns += vulnerability_factory.create_batch(3, workspace=self.first_object, | |
168 | confirmed=True, status='open', severity='high') | |
169 | ||
170 | vulns += vulnerability_web_factory.create_batch(2, workspace=second_workspace, | |
171 | confirmed=True, status='open', severity='medium') | |
172 | ||
173 | vulns += vulnerability_web_factory.create_batch(2, workspace=second_workspace, | |
174 | confirmed=True, status='open', severity='low') | |
175 | ||
176 | session.add_all(vulns) | |
177 | session.commit() | |
178 | res = test_client.get('/v3/ws?histogram=true') | |
179 | assert res.status_code == 200 | |
180 | firs_ws = [ws['histogram'] for ws in res.json if ws['name'] == self.first_object.name] | |
181 | assert len(firs_ws[0]) == 20 | |
182 | ws_histogram = firs_ws[0] | |
183 | for ws_date in ws_histogram: | |
184 | if ws_date['date'] == date.today().strftime("%Y-%m-%d"): | |
185 | assert ws_date['medium'] == 0 | |
186 | assert ws_date['high'] == 3 | |
187 | assert ws_date['critical'] == 8 | |
188 | assert ws_date['confirmed'] == 3 | |
189 | else: | |
190 | assert ws_date['medium'] == 0 | |
191 | assert ws_date['high'] == 0 | |
192 | assert ws_date['critical'] == 0 | |
193 | assert ws_date['confirmed'] == 0 | |
194 | ||
195 | second_ws = [ws['histogram'] for ws in res.json if ws['name'] == second_workspace.name] | |
196 | assert len(second_ws[0]) == 20 | |
197 | ws_histogram = second_ws[0] | |
198 | for ws_date in ws_histogram: | |
199 | if ws_date['date'] == date.today().strftime("%Y-%m-%d"): | |
200 | assert ws_date['medium'] == 2 | |
201 | assert ws_date['high'] == 0 | |
202 | assert ws_date['critical'] == 0 | |
203 | assert ws_date['confirmed'] == 2 | |
204 | else: | |
205 | assert ws_date['medium'] == 0 | |
206 | assert ws_date['high'] == 0 | |
207 | assert ws_date['critical'] == 0 | |
208 | assert ws_date['confirmed'] == 0 | |
209 | ||
210 | res = test_client.get('/v3/ws?histogram=True&histogram_days=a') | |
211 | assert res.status_code == 200 | |
212 | firs_ws = [ws['histogram'] for ws in res.json if ws['name'] == self.first_object.name] | |
213 | assert len(firs_ws[0]) == 20 | |
214 | ||
215 | res = test_client.get('/v3/ws?histogram=true&histogram_days=[asdf, "adsf"]') | |
216 | assert res.status_code == 200 | |
217 | firs_ws = [ws['histogram'] for ws in res.json if ws['name'] == self.first_object.name] | |
218 | assert len(firs_ws[0]) == 20 | |
219 | ||
220 | res = test_client.get('/v3/ws?histogram=true&histogram_days=[asdf, "adsf"]') | |
221 | assert res.status_code == 200 | |
222 | firs_ws = [ws['histogram'] for ws in res.json if ws['name'] == self.first_object.name] | |
223 | assert len(firs_ws[0]) == 20 | |
224 | ||
225 | res = test_client.get('/v3/ws?histogram=true&histogram_days=5') | |
226 | assert res.status_code == 200 | |
227 | firs_ws = [ws['histogram'] for ws in res.json if ws['name'] == self.first_object.name] | |
228 | assert len(firs_ws[0]) == 5 | |
229 | ||
230 | res = test_client.get('/v3/ws?histogram=true&histogram_days=365') | |
231 | assert res.status_code == 200 | |
232 | firs_ws = [ws['histogram'] for ws in res.json if ws['name'] == self.first_object.name] | |
233 | assert len(firs_ws[0]) == 365 | |
234 | ||
235 | res = test_client.get('/v3/ws?histogram=asdf&histogram_days=365') | |
236 | assert res.status_code == 200 | |
237 | for ws in res.json: | |
238 | assert 'histogram' not in ws | |
239 | ||
150 | 240 | @pytest.mark.parametrize('querystring', [ |
151 | 241 | '?status=closed' |
152 | 242 | ]) |
167 | 257 | |
168 | 258 | session.add_all(vulns) |
169 | 259 | session.commit() |
170 | res = test_client.get(self.url(self.first_object) + querystring) | |
260 | res = test_client.get(urljoin(self.url(self.first_object), querystring)) | |
171 | 261 | assert res.status_code == 200 |
172 | 262 | assert res.json['stats']['code_vulns'] == 0 |
173 | 263 | assert res.json['stats']['web_vulns'] == 0 |
201 | 291 | |
202 | 292 | session.add_all(vulns) |
203 | 293 | session.commit() |
204 | res = test_client.get(self.url(self.first_object) + querystring) | |
294 | res = test_client.get(urljoin(self.url(self.first_object), querystring)) | |
205 | 295 | assert res.status_code == 200 |
206 | 296 | assert res.json['stats']['code_vulns'] == 0 |
207 | 297 | assert res.json['stats']['web_vulns'] == 2 |
223 | 313 | confirmed=True) |
224 | 314 | session.add_all(vulns) |
225 | 315 | session.commit() |
226 | res = test_client.get(self.url(self.first_object) + querystring) | |
316 | res = test_client.get(urljoin(self.url(self.first_object), querystring)) | |
227 | 317 | assert res.status_code == 200 |
228 | 318 | assert res.json['stats']['total_vulns'] == 5 |
229 | 319 | |
354 | 444 | assert res.status_code == 201 |
355 | 445 | assert set(res.json['scope']) == set(desired_scope) |
356 | 446 | workspace = Workspace.query.get(res.json['id']) |
357 | assert set(s.name for s in workspace.scope) == set(desired_scope) | |
447 | assert {s.name for s in workspace.scope} == set(desired_scope) | |
358 | 448 | |
359 | 449 | def test_update_with_scope(self, session, test_client, workspace): |
360 | 450 | session.add(Scope(name='test.com', workspace=workspace)) |
368 | 458 | res = test_client.put(self.url(obj=workspace), data=raw_data) |
369 | 459 | assert res.status_code == 200 |
370 | 460 | assert set(res.json['scope']) == set(desired_scope) |
371 | assert set(s.name for s in workspace.scope) == set(desired_scope) | |
372 | ||
373 | @pytest.mark.skip # TODO fix fox sqlite | |
374 | def test_list_retrieves_all_items_from(self, test_client): | |
375 | super().test_list_retrieves_all_items_from(test_client) | |
461 | assert {s.name for s in workspace.scope} == set(desired_scope) | |
462 | ||
463 | @pytest.mark.skip_sql_dialect('sqlite') | |
464 | def test_list_retrieves_all_items_from(self, test_client, logged_user): | |
465 | super().test_list_retrieves_all_items_from(test_client, logged_user) | |
376 | 466 | |
377 | 467 | def test_workspace_activation(self, test_client, workspace, session): |
378 | 468 | workspace.active = False |
0 | # -*- coding: utf8 -*- | |
1 | 0 | ''' |
2 | 1 | Faraday Penetration Test IDE |
3 | 2 | Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/) |
4 | 3 | See the file 'doc/LICENSE' for the license information |
5 | 4 | |
6 | 5 | ''' |
7 | from builtins import str | |
8 | from posixpath import join as urljoin | |
6 | from posixpath import join | |
9 | 7 | |
10 | 8 | """Generic tests for APIs prefixed with a workspace_name""" |
11 | 9 | |
49 | 47 | |
50 | 48 | def url(self, obj=None, workspace=None): |
51 | 49 | workspace = workspace or self.workspace |
52 | url = API_PREFIX + workspace.name + '/' + self.api_endpoint | |
50 | url = join(API_PREFIX + workspace.name, self.api_endpoint) | |
53 | 51 | if obj is not None: |
54 | 52 | id_ = str(obj.id) if isinstance( |
55 | 53 | obj, self.model) else str(obj) |
56 | url += '/' + id_ | |
54 | url = join(url, id_) | |
57 | 55 | return url |
58 | 56 | |
59 | 57 | |
102 | 100 | res = test_client.get(self.url(self.first_object, second_workspace)) |
103 | 101 | assert res.status_code == 404 |
104 | 102 | |
105 | @pytest.mark.parametrize('object_id', [123456789, -1, 'xxx', u'áá']) | |
103 | @pytest.mark.parametrize('object_id', [123456789, -1, 'xxx', 'áá']) | |
106 | 104 | def test_404_when_retrieving_unexistent_object(self, test_client, |
107 | 105 | object_id): |
108 | 106 | url = self.url(object_id) |
203 | 201 | def test_update_an_object_readonly_fails(self, test_client, method): |
204 | 202 | self.workspace.readonly = True |
205 | 203 | db.session.commit() |
206 | for unique_field in self.unique_fields: | |
207 | data = self.factory.build_dict() | |
208 | old_field = getattr(self.objects[0], unique_field) | |
209 | old_id = getattr(self.objects[0], 'id') | |
210 | if method == "PUT": | |
211 | res = test_client.put(self.url(self.first_object), data=data) | |
212 | elif method == "PATCH": | |
213 | res = test_client.patch(self.url(self.first_object), data=data) | |
214 | db.session.commit() | |
215 | assert res.status_code == 403 | |
216 | assert self.model.query.count() == OBJECT_COUNT | |
217 | assert old_field == getattr(self.model.query.filter(self.model.id == old_id).one(), unique_field) | |
204 | ||
205 | data = self.factory.build_dict(workspace=self.workspace) | |
206 | count = self.model.query.count() | |
207 | if method == "PUT": | |
208 | res = test_client.put(self.url(self.first_object), | |
209 | data=data) | |
210 | elif method == "PATCH": | |
211 | res = test_client.patch(self.url(self.first_object), | |
212 | data=data) | |
213 | assert res.status_code == 403 | |
214 | assert self.model.query.count() == count | |
218 | 215 | |
219 | 216 | @pytest.mark.parametrize("method", ["PUT", "PATCH"]) |
220 | 217 | def test_update_inactive_fails(self, test_client, method): |
271 | 268 | assert object_id == expected_id |
272 | 269 | |
273 | 270 | |
271 | @pytest.mark.usefixtures('logged_user') | |
272 | class BulkUpdateTestsMixin: | |
273 | ||
274 | @staticmethod | |
275 | def control_data(test_suite, data: dict) -> dict: | |
276 | return { | |
277 | key: value for (key, value) in data.items() | |
278 | if key in UpdateTestsMixin.control_patcheable_data(test_suite, data) | |
279 | and key not in test_suite.unique_fields | |
280 | } | |
281 | ||
282 | def get_all_objs_and_ids(self): | |
283 | all_objs = self.model.query.all() | |
284 | all_objs_id = [obj.__getattribute__(self.view_class.lookup_field) for obj in all_objs] | |
285 | return all_objs, all_objs_id | |
286 | ||
287 | def test_bulk_update_an_object(self, test_client, logged_user): | |
288 | all_objs = self.model.query.all() | |
289 | all_objs_id = [obj.__getattribute__(self.view_class.lookup_field) for obj in self.model.query.all()] | |
290 | all_objs, all_objs_id = all_objs[:-1], all_objs_id[:-1] | |
291 | ||
292 | data = self.factory.build_dict(workspace=self.workspace) | |
293 | data = self.control_cant_change_data(data) | |
294 | count = self.model.query.count() | |
295 | data = BulkUpdateTestsMixin.control_data(self, data) | |
296 | ||
297 | res = test_client.patch(self.url(), data={}) | |
298 | assert res.status_code == 400 | |
299 | data["ids"] = all_objs_id | |
300 | res = test_client.patch(self.url(), data=data) | |
301 | ||
302 | assert res.status_code == 200, (res.status_code, res.json) | |
303 | assert self.model.query.count() == count | |
304 | assert res.json['updated'] == len(all_objs) | |
305 | for obj in self.model.query.all(): | |
306 | if getattr(obj, self.view_class.lookup_field) not in all_objs_id: | |
307 | assert any( | |
308 | [ | |
309 | data[updated_field] != getattr(obj, updated_field) | |
310 | for updated_field in data if updated_field != 'ids' | |
311 | ] | |
312 | ) | |
313 | else: | |
314 | assert all( | |
315 | [ | |
316 | data[updated_field] == getattr(obj, updated_field) | |
317 | for updated_field in data if updated_field != 'ids' | |
318 | ] | |
319 | ) | |
320 | ||
321 | def test_bulk_update_an_object_readonly_fails(self, test_client): | |
322 | self.workspace.readonly = True | |
323 | db.session.commit() | |
324 | all_objs, all_objs_id = self.get_all_objs_and_ids() | |
325 | data = self.factory.build_dict(workspace=self.workspace) | |
326 | data = self.control_cant_change_data(data) | |
327 | data = BulkUpdateTestsMixin.control_data(self, data) | |
328 | count = self.model.query.count() | |
329 | data["ids"] = all_objs_id | |
330 | res = test_client.patch(self.url(), data=data) | |
331 | assert res.status_code == 403 | |
332 | assert self.model.query.count() == count | |
333 | ||
334 | def test_bulk_update_inactive_fails(self, test_client): | |
335 | self.workspace.deactivate() | |
336 | db.session.commit() | |
337 | all_objs, all_objs_id = self.get_all_objs_and_ids() | |
338 | data = self.factory.build_dict(workspace=self.workspace) | |
339 | data = self.control_cant_change_data(data) | |
340 | data = BulkUpdateTestsMixin.control_data(self, data) | |
341 | count = self.model.query.count() | |
342 | data["ids"] = all_objs_id | |
343 | res = test_client.patch(self.url(), data=data) | |
344 | assert res.status_code == 403 | |
345 | assert self.model.query.count() == count | |
346 | ||
347 | @pytest.mark.parametrize('existing', (True, False)) | |
348 | def test_bulk_update_fails_with_repeated_unique(self, test_client, session, existing): | |
349 | for unique_field in self.unique_fields: | |
350 | data = self.factory.build_dict() | |
351 | if existing: | |
352 | data[unique_field] = getattr(self.objects[3], unique_field) | |
353 | data["ids"] = [getattr(self.objects[0], self.view_class.lookup_field)] | |
354 | else: | |
355 | data["ids"] = [getattr(self.objects[i], self.view_class.lookup_field) for i in range(0, 2)] | |
356 | res = test_client.patch(self.url(), data=data) | |
357 | assert res.status_code == 409 | |
358 | assert self.model.query.count() == OBJECT_COUNT | |
359 | ||
360 | def test_bulk_update_cant_change_id(self, test_client): | |
361 | raw_json = self.factory.build_dict(workspace=self.workspace) | |
362 | raw_json = self.control_cant_change_data(raw_json) | |
363 | raw_json['id'] = 100000 | |
364 | expected_id = self.first_object.id | |
365 | raw_json["ids"] = [expected_id] | |
366 | res = test_client.patch(self.url(), data=raw_json) | |
367 | assert res.status_code == 200, (res.status_code, res.data) | |
368 | assert self.model.query.filter(self.model.id == 100000).first() is None | |
369 | ||
370 | def test_patch_bulk_update_an_object_does_not_fail_with_partial_data(self, test_client, logged_user): | |
371 | """To do this the user should use a PATCH request""" | |
372 | all_objs_id = [obj.__getattribute__(self.view_class.lookup_field) for obj in self.model.query.all()] | |
373 | res = test_client.patch(self.url(), data={"ids": all_objs_id}) | |
374 | assert res.status_code == 200, (res.status_code, res.json) | |
375 | ||
376 | def test_bulk_update_invalid_ids(self, test_client): | |
377 | data = self.factory.build_dict(workspace=self.workspace) | |
378 | data = BulkUpdateTestsMixin.control_data(self, data) | |
379 | data['ids'] = [-1, 'test'] | |
380 | res = test_client.patch(self.url(), data=data) | |
381 | assert res.status_code == 200 | |
382 | assert res.json['updated'] == 0 | |
383 | data['ids'] = [-1, 'test', self.first_object.__getattribute__(self.view_class.lookup_field)] | |
384 | res = test_client.patch(self.url(), data=data) | |
385 | assert res.status_code == 200 | |
386 | assert res.json['updated'] == 1 | |
387 | ||
388 | def test_bulk_update_wrong_content_type(self, test_client): | |
389 | all_objs = self.model.query.all() | |
390 | all_objs_id = [obj.__getattribute__(self.view_class.lookup_field) for obj in all_objs] | |
391 | ||
392 | request_data = {'ids': all_objs_id} | |
393 | headers = [('content-type', 'text/xml')] | |
394 | ||
395 | res = test_client.patch(self.url(), data=request_data, headers=headers) | |
396 | assert res.status_code == 400 | |
397 | ||
398 | ||
274 | 399 | class CountTestsMixin: |
275 | 400 | def test_count(self, test_client, session, user_factory): |
276 | 401 | |
287 | 412 | |
288 | 413 | session.commit() |
289 | 414 | |
290 | res = test_client.get(urljoin(self.url(), "count?group_by=creator_id")) | |
415 | res = test_client.get(join(self.url(), "count?group_by=creator_id")) | |
291 | 416 | |
292 | 417 | assert res.status_code == 200, res.json |
293 | 418 | res = res.get_json() |
317 | 442 | |
318 | 443 | session.commit() |
319 | 444 | |
320 | res = test_client.get(urljoin(self.url(), "count?group_by=creator_id&order=desc")) | |
445 | res = test_client.get(join(self.url(), "count?group_by=creator_id&order=desc")) | |
321 | 446 | |
322 | 447 | assert res.status_code == 200, res.json |
323 | 448 | res = res.get_json() |
366 | 491 | assert self.model.query.count() == OBJECT_COUNT |
367 | 492 | |
368 | 493 | |
494 | @pytest.mark.usefixtures('logged_user') | |
495 | class BulkDeleteTestsMixin: | |
496 | ||
497 | def get_all_objs_and_ids(self): | |
498 | all_objs = self.model.query.all() | |
499 | all_objs_id = [obj.__getattribute__(self.view_class.lookup_field) for obj in all_objs] | |
500 | return all_objs, all_objs_id | |
501 | ||
502 | @pytest.mark.usefixtures('ignore_nplusone') | |
503 | def test_bulk_delete(self, test_client): | |
504 | all_objs, all_objs_id = self.get_all_objs_and_ids() | |
505 | ignored_obj = all_objs[-1] | |
506 | all_objs, all_objs_id = all_objs[:-1], all_objs_id[:-1] | |
507 | ||
508 | res = test_client.delete(self.url(), data={}) | |
509 | assert res.status_code == 400 | |
510 | data = {"ids": all_objs_id} | |
511 | res = test_client.delete(self.url(), data=data) | |
512 | assert res.status_code == 200 | |
513 | assert all([was_deleted(obj) for obj in all_objs]) | |
514 | assert res.json['deleted'] == len(all_objs) | |
515 | assert not was_deleted(ignored_obj) | |
516 | assert self.model.query.count() == 1 | |
517 | ||
518 | def test_bulk_delete_invalid_ids(self, test_client): | |
519 | request_data = {'ids': [-1, 'test']} | |
520 | count = self.model.query.count() | |
521 | res = test_client.delete(self.url(), data=request_data) | |
522 | assert res.status_code == 200 | |
523 | assert res.json['deleted'] == 0 | |
524 | assert self.model.query.count() == count | |
525 | ||
526 | def test_bulk_delete_wrong_content_type(self, test_client): | |
527 | all_objs = self.model.query.all() | |
528 | all_objs_id = [obj.__getattribute__(self.view_class.lookup_field) for obj in all_objs] | |
529 | count = self.model.query.count() | |
530 | ||
531 | request_data = {'ids': all_objs_id} | |
532 | headers = [('content-type', 'text/xml')] | |
533 | ||
534 | res = test_client.delete(self.url(), data=request_data, headers=headers) | |
535 | assert res.status_code == 400 | |
536 | assert self.model.query.count() == count | |
537 | assert all([not was_deleted(obj) for obj in all_objs]) | |
538 | ||
539 | def test_bulk_delete_readonly_fails(self, test_client, session): | |
540 | self.workspace.readonly = True | |
541 | session.commit() | |
542 | all_objs, all_objs_id = self.get_all_objs_and_ids() | |
543 | data = {"ids": all_objs_id} | |
544 | res = test_client.delete(self.url(), data=data) | |
545 | assert res.status_code == 403 # No content | |
546 | assert not any([was_deleted(obj) for obj in all_objs]) | |
547 | assert self.model.query.count() == OBJECT_COUNT | |
548 | ||
549 | def test_delete_inactive_fails(self, test_client): | |
550 | self.workspace.deactivate() | |
551 | db.session.commit() | |
552 | all_objs, all_objs_id = self.get_all_objs_and_ids() | |
553 | data = {"ids": all_objs_id} | |
554 | res = test_client.delete(self.url(), data=data) | |
555 | assert res.status_code == 403 # No content | |
556 | assert not any([was_deleted(obj) for obj in all_objs]) | |
557 | assert self.model.query.count() == OBJECT_COUNT | |
558 | ||
559 | def test_delete_from_other_workspace_fails(self, test_client): | |
560 | all_objs, all_objs_id = self.get_all_objs_and_ids() | |
561 | ||
562 | data = {"ids": all_objs_id + [10000000]} | |
563 | res = test_client.delete(self.url(), data=data) | |
564 | assert res.status_code == 204 | |
565 | assert all([was_deleted(obj) for obj in all_objs]) | |
566 | assert res.json['deleted'] == len(all_objs) | |
567 | assert self.model.query.count() == 0 | |
568 | ||
569 | ||
369 | 570 | class PaginationTestsMixin(OriginalPaginationTestsMixin): |
370 | 571 | def create_many_objects(self, session, n): |
371 | 572 | objects = self.factory.create_batch(n, workspace=self.workspace) |
0 | ||
1 | 0 | import os |
2 | 1 | import subprocess |
3 | 2 | |
42 | 41 | print(std) |
43 | 42 | print(err) |
44 | 43 | assert subproc.returncode == 0, ('manage migrate failed!', std, err) |
45 | ||
46 | ||
47 | # I'm Py3 |
17 | 17 | ) |
18 | 18 | |
19 | 19 | UNIQUE_FIELDS = { |
20 | License: [u'product', u'start_date', u'end_date'], | |
21 | Service: [u'port', u'protocol', u'host_id', u'workspace_id'], | |
22 | Host: [u'ip', u'workspace_id'], | |
20 | License: ['product', 'start_date', 'end_date'], | |
21 | Service: ['port', 'protocol', 'host_id', 'workspace_id'], | |
22 | Host: ['ip', 'workspace_id'], | |
23 | 23 | Vulnerability: [ |
24 | 24 | 'name', |
25 | 25 | 'description', |
68 | 68 | unique_constraints = get_unique_fields(session, object_) |
69 | 69 | for unique_constraint in unique_constraints: |
70 | 70 | assert unique_constraint == expected_unique_fields |
71 | ||
72 | # I'm Py3 |