New upstream version 3.16.0
Sophie Brun
2 years ago
100 | 100 | stage: build |
101 | 101 | image: nixorg/nix |
102 | 102 | script: |
103 | - nix-env -if pynixify/nixpkgs.nix -A cachix | |
103 | - nix-env -if pynixify/nixpkgs.nix -A cachix gnugrep | |
104 | - nix-env -if pynixify/nixpkgs.nix -A vault | |
105 | - !reference [ .get_secrets, script ] | |
104 | 106 | - mkdir -p ~/.config/cachix |
105 | 107 | - export USER=$(whoami) |
106 | 108 | - echo "$CACHIX_CONFG" >~/.config/cachix/cachix.dhall |
34 | 34 | image: nixorg/nix |
35 | 35 | stage: pre_build |
36 | 36 | script: |
37 | - nix-env -if pynixify/nixpkgs.nix -A cachix | |
37 | - nix-env -if pynixify/nixpkgs.nix -A cachix gnugrep | |
38 | - nix-env -if pynixify/nixpkgs.nix -A vault | |
39 | - !reference [ .get_secrets, script ] | |
38 | 40 | - mkdir -p ~/.config/cachix |
39 | 41 | - export USER=$(whoami) |
40 | 42 | - echo "$CACHIX_CONFG" >~/.config/cachix/cachix.dhall |
19 | 19 | - /opt/faraday/bin/faraday-server & |
20 | 20 | - sleep 5 |
21 | 21 | - curl -v http://localhost:5985/_api/v2/info |
22 | - faraday-manage status-check | |
23 | 22 | - kill $(cat ~faraday/.faraday/faraday-server-port-5985.pid) |
24 | 23 | - jobs |
25 | 24 | rules: |
0 | .get_secrets: | |
1 | script: | |
2 | - export VAULT_TOKEN="$(vault write -field=token auth/jwt/login role=$VAULT_ROLE jwt=$CI_JOB_JWT)" | |
3 | - if [ -z "$CACHIX_CONFG" ]; then export CACHIX_CONFG="$(vault kv get -field=CACHIX_CONFG secrets/gitlab/faraday)"; fi; if [ -z "$CACHIX_CONFG" ]; then exit 1; fi #(WHITE) | |
4 | - if [ -z "$DOCKER_PASS" ]; then export DOCKER_PASS="$(vault kv get -field=DOCKER_PASS secrets/gitlab/faraday)"; fi; if [ -z "$DOCKER_PÁSS" ]; then exit 1; fi #(WHITE) | |
5 | - if [ -z "$DOCKER_USER" ]; then export DOCKER_USER="$(vault kv get -field=DOCKER_USER secrets/gitlab/faraday)"; fi; if [ -z "$DOCKER_USER" ]; then exit 1; fi #(WHITE) | |
6 | - if [ -z "$GCLOUD_STORAGE_KEY_FILE" ]; then export GCLOUD_STORAGE_KEY_FILE="$(vault kv get -field=GCLOUD_STORAGE_KEY_FILE secrets/gitlab/faraday)"; fi; if [ -z "$GCLOUD_STORAGE_KEY_FILE" ]; then exit 1; fi #(WHITE) |
28 | 28 | CI_REGISTRY: docker.io |
29 | 29 | CI_REGISTRY_IMAGE: index.docker.io/faradaysec/faraday |
30 | 30 | script: |
31 | - !reference [ .get_secrets, script ] | |
31 | 32 | - docker image tag registry.gitlab.com/faradaysec/faraday:latest $CI_REGISTRY_IMAGE:latest |
32 | 33 | - docker push $CI_REGISTRY_IMAGE:latest |
33 | 34 | - docker image tag $CI_REGISTRY_IMAGE:latest $CI_REGISTRY_IMAGE:$VERSION |
4 | 4 | stage: test |
5 | 5 | allow_failure: true |
6 | 6 | script: |
7 | - nix-env -if pynixify/nixpkgs.nix -A vault | |
8 | - !reference [ .get_secrets, script ] | |
7 | 9 | - nix-env -if pynixify/nixpkgs.nix -A cachix |
8 | 10 | - mkdir -p ~/.config/cachix |
9 | 11 | - export USER=$(whoami) |
0 | ||
1 | 0 | pylint: |
2 | 1 | tags: |
3 | 2 | - faradaytests |
4 | 3 | image: nixorg/nix |
5 | 4 | stage: test # This should be after build_and_push_to_cachix to improve performance |
6 | 5 | script: |
6 | - nix-env -if pynixify/nixpkgs.nix -A vault | |
7 | - !reference [ .get_secrets, script ] | |
7 | 8 | - nix-env -if pynixify/nixpkgs.nix -A cachix |
8 | 9 | - mkdir -p ~/.config/cachix |
9 | 10 | - export USER=$(whoami) |
32 | 33 | stage: test |
33 | 34 | coverage: '/TOTAL\s+\d+\s+\d+\s+(\d+%)/' |
34 | 35 | script: |
36 | - nix-env -if pynixify/nixpkgs.nix -A vault | |
37 | - !reference [ .get_secrets, script ] | |
35 | 38 | - nix-env -if pynixify/nixpkgs.nix -A cachix |
36 | 39 | - mkdir -p ~/.config/cachix |
37 | 40 | - export USER=$(whoami) |
67 | 70 | stage: test |
68 | 71 | coverage: '/TOTAL\s+\d+\s+\d+\s+(\d+%)/' |
69 | 72 | script: |
73 | - nix-env -if pynixify/nixpkgs.nix -A vault | |
74 | - !reference [ .get_secrets, script ] | |
70 | 75 | - nix-env -if pynixify/nixpkgs.nix -A cachix |
71 | 76 | - mkdir -p ~/.config/cachix |
72 | 77 | - export USER=$(whoami) |
72 | 72 | # Note: this size has to fit both our community, professional and corporate versions |
73 | 73 | MAX_CLOSURE_SIZE_IN_MB: 850 |
74 | 74 | script: |
75 | - nix-env -if pynixify/nixpkgs.nix -A vault | |
75 | 76 | - nix-env -if pynixify/nixpkgs.nix -A cachix |
76 | 77 | - nix-env -if pynixify/nixpkgs.nix -A gawk |
78 | - !reference [ .get_secrets, script ] | |
77 | 79 | - mkdir -p ~/.config/cachix |
78 | 80 | - export USER=$(whoami) |
79 | 81 | - echo "$CACHIX_CONFG" >~/.config/cachix/cachix.dhall |
24 | 24 | variables: |
25 | 25 | STORAGE_SPACE_BASE: gs://faraday-dev |
26 | 26 | script: |
27 | - cp $GCLOUD_STORAGE_KEY_FILE auth_file.json | |
27 | - apt-get update && apt-get install -y software-properties-common curl | |
28 | - curl -fsSL https://apt.releases.hashicorp.com/gpg | apt-key add - | |
29 | - apt-add-repository "deb [arch=amd64] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | |
30 | - apt update && apt install jq vault -y --fix-missing | |
31 | - setcap cap_ipc_lock= /usr/bin/vault | |
32 | - !reference [ .get_secrets, script ] | |
33 | - echo $GCLOUD_STORAGE_KEY_FILE > auth_file.json | |
28 | 34 | - *google_storage_deb_rpm_base |
29 | 35 | - "gsutil setmeta -h x-goog-meta-branch:${CI_COMMIT_BRANCH} ${GCLOUD_FILE_PATH}*.*" |
30 | 36 | rules: |
7 | 7 | APT_CACHE_DIR: "$CI_PROJECT_DIR/apt-cache" |
8 | 8 | DEBIAN_FRONTEND: noninteractive |
9 | 9 | IMAGE_TAG: 'egrep -o "[0-9]\.([0-9]|[0-9][0-9])(\.[0-9])?" faraday/__init__.py' |
10 | VAULT_ROLE: 'faraday-readonly' | |
11 | VAULT_ADDR: 'https://tluav-lb.faradaysec.com' | |
10 | 12 | |
11 | 13 | ## ENV_VARS LIST |
12 | 14 | # FULL_TEST = Test all jobs |
27 | 29 | - mkdir -pv $APT_CACHE_DIR |
28 | 30 | |
29 | 31 | include: |
32 | - local: .gitlab/ci/fetch-secrets.yaml | |
30 | 33 | - local: .gitlab/ci/testing/.pretesting-gitlab-ci.yml |
31 | 34 | - local: .gitlab/ci/testing/.nix-testing-gitlab-ci.yml |
32 | 35 | - local: .gitlab/ci/testing/.venv-testing-gitlab-ci.yml |
0 | * BREAKING CHANGE: API V2 discontinued | |
1 | * BREAKING CHANGE: Changed minimum version of python to 3.7 | |
2 | * ADD agent parameters has types (protocol with agent and its APIs) | |
3 | * ADD move settings from `server.in` to a db model | |
4 | * ADD (optional) query logs | |
5 | * MOD new threads management | |
6 | * MOD vulnerabilities' endpoint no longer loads evidence unless requested with `get_evidence=true` | |
7 | * FIX now it is not possible to create workspace of name "filter" | |
8 | * FIX bug with dates in the future | |
9 | * FIX bug with click 8 | |
10 | * FIX bug using --port command | |
11 | * FIX endpoints returning 500 as status code | |
12 | * REMOVE the need tom CSRF token from evidence upload api |
0 | Jun 29th, 2021 |
1 | 1 | ===================================== |
2 | 2 | |
3 | 3 | |
4 | 3.16.0 [Jun 29th, 2021]: | |
5 | --- | |
6 | * BREAKING CHANGE: API V2 discontinued | |
7 | * BREAKING CHANGE: Changed minimum version of python to 3.7 | |
8 | * ADD agent parameters has types (protocol with agent and its APIs) | |
9 | * ADD move settings from `server.in` to a db model | |
10 | * ADD (optional) query logs | |
11 | * MOD new threads management | |
12 | * MOD vulnerabilities' endpoint no longer loads evidence unless requested with `get_evidence=true` | |
13 | * FIX now it is not possible to create workspace of name "filter" | |
14 | * FIX bug with dates in the future | |
15 | * FIX bug with click 8 | |
16 | * FIX bug using --port command | |
17 | * FIX endpoints returning 500 as status code | |
18 | * REMOVE the need tom CSRF token from evidence upload api | |
19 | ||
4 | 20 | 3.15.0 [May 18th, 2021]: |
5 | 21 | --- |
6 | ||
7 | 22 | * ADD `Basic Auth` support |
8 | 23 | * ADD support for GET method in websocket_tokens, POST will be deprecated in the future |
9 | 24 | * ADD CVSS(String), CWE(String), CVE(relationship) columns to vulnerability model and API |
106 | 106 | ## Links |
107 | 107 | |
108 | 108 | * Homepage: [FaradaySEC](https://www.faradaysec.com) |
109 | * User forum: [Faraday Forum](https://forum.faradaysec.com) | |
109 | * User forum: [Faraday Forum](https://github.com/infobyte/faraday/issues) | |
110 | 110 | * User's manual: [Faraday Wiki](https://github.com/infobyte/faraday/wiki) or check our [support portal](https://support.faradaysec.com/portal/home) |
111 | 111 | * Download: [Download .deb/.rpm from releases page](https://github.com/infobyte/faraday/releases) |
112 | 112 | * Commits RSS feed: https://github.com/infobyte/faraday/commits/master.atom |
1 | 1 | ===================================== |
2 | 2 | |
3 | 3 | |
4 | 3.16.0 [Jun 29th, 2021]: | |
5 | --- | |
6 | * BREAKING CHANGE: API V2 discontinued | |
7 | * BREAKING CHANGE: Changed minimum version of python to 3.7 | |
8 | * ADD agent parameters has types (protocol with agent and its APIs) | |
9 | * ADD move settings from `server.in` to a db model | |
10 | * ADD (optional) query logs | |
11 | * MOD new threads management | |
12 | * MOD vulnerabilities' endpoint no longer loads evidence unless requested with `get_evidence=true` | |
13 | * FIX now it is not possible to create workspace of name "filter" | |
14 | * FIX bug with dates in the future | |
15 | * FIX bug with click 8 | |
16 | * FIX bug using --port command | |
17 | * FIX endpoints returning 500 as status code | |
18 | * REMOVE the need tom CSRF token from evidence upload api | |
19 | ||
4 | 20 | 3.15.0 [May 18th, 2021]: |
5 | 21 | --- |
6 | ||
7 | 22 | * ADD `Basic Auth` support |
8 | 23 | * ADD support for GET method in websocket_tokens, POST will be deprecated in the future |
9 | 24 | * ADD CVSS(String), CWE(String), CVE(relationship) columns to vulnerability model and API |
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.15.0' | |
4 | __version__ = '3.16.0' | |
5 | 5 | __license_version__ = __version__ |
41 | 41 | from faraday.server.commands.faraday_schema_display import DatabaseSchema |
42 | 42 | from faraday.server.commands.app_urls import show_all_urls |
43 | 43 | from faraday.server.commands.app_urls import openapi_format |
44 | from faraday.server.commands import status_check as status_check_functions | |
45 | 44 | from faraday.server.commands import change_password as change_pass |
46 | 45 | from faraday.server.commands.custom_fields import add_custom_field_main, delete_custom_field_main |
47 | from faraday.server.commands import support as support_zip | |
48 | 46 | from faraday.server.commands import change_username |
49 | 47 | from faraday.server.commands import nginx_config |
50 | 48 | from faraday.server.commands import import_vulnerability_template |
49 | from faraday.server.commands import manage_settings | |
51 | 50 | from faraday.server.models import db, User |
52 | 51 | from faraday.server.web import get_app |
53 | 52 | from faraday_plugins.plugins.manager import PluginsManager |
96 | 95 | 'ask for the desired one') |
97 | 96 | ) |
98 | 97 | @click.option( |
99 | '--password', type=str, default=False, | |
98 | '--password', type=str, | |
100 | 99 | help=('Instead of using a random password for the user "faraday", ' |
101 | 100 | 'use the one provided') |
102 | 101 | ) |
124 | 123 | pgcli.run_cli() |
125 | 124 | |
126 | 125 | |
127 | @click.command(help='Checks configuration and faraday status.') | |
128 | @click.option('--check_postgresql', default=False, is_flag=True) | |
129 | @click.option('--check_faraday', default=False, is_flag=True) | |
130 | @click.option('--check_dependencies', default=False, is_flag=True) | |
131 | @click.option('--check_config', default=False, is_flag=True) | |
132 | def status_check(check_postgresql, check_faraday, check_dependencies, check_config): | |
133 | selected = False | |
134 | exit_code = 0 | |
135 | if check_postgresql: | |
136 | # exit_code was created for Faraday automation-testing purposes | |
137 | exit_code = status_check_functions.print_postgresql_status() | |
138 | status_check_functions.print_postgresql_other_status() | |
139 | selected = True | |
140 | ||
141 | if check_faraday: | |
142 | status_check_functions.print_faraday_status() | |
143 | selected = True | |
144 | ||
145 | if check_dependencies: | |
146 | status_check_functions.print_depencencies_status() | |
147 | selected = True | |
148 | ||
149 | if check_config: | |
150 | status_check_functions.print_config_status() | |
151 | selected = True | |
152 | ||
153 | if not selected: | |
154 | status_check_functions.full_status_check() | |
155 | ||
156 | sys.exit(exit_code) | |
157 | ||
158 | ||
159 | 126 | @click.command(help="Changes the password of a user") |
160 | 127 | @click.option('--username', required=True, prompt=True) |
161 | 128 | @click.option('--password', required=True, prompt=True, confirmation_prompt=True, hide_input=True) |
213 | 180 | get_app().user_datastore.create_user(username=username, |
214 | 181 | email=email, |
215 | 182 | password=hash_password(password), |
216 | role='admin', | |
183 | roles=['admin'], | |
217 | 184 | is_ldap=False) |
218 | 185 | db.session.commit() |
219 | 186 | click.echo(click.style( |
239 | 206 | click.echo(click.style( |
240 | 207 | 'Tables created successfully!', |
241 | 208 | fg='green', bold=True)) |
242 | ||
243 | ||
244 | @click.command(help="Generates a .zip file with technical information") | |
245 | def support(): | |
246 | support_zip.all_for_support() | |
247 | 209 | |
248 | 210 | |
249 | 211 | @click.command( |
315 | 277 | nginx_config.generate_nginx_config(fqdn, port, ws_port, ssl_certificate, ssl_key, multitenant_url) |
316 | 278 | |
317 | 279 | |
280 | @click.command(help="Manage settings") | |
281 | @click.option('-a', '--action', type=click.Choice(['show', 'update', 'list'], case_sensitive=False), default='list', show_default=True) | |
282 | @click.argument('name', required=False) | |
283 | def settings(action, name): | |
284 | manage_settings.manage(action.lower(), name) | |
285 | ||
286 | ||
318 | 287 | cli.add_command(show_urls) |
319 | 288 | cli.add_command(initdb) |
320 | 289 | cli.add_command(database_schema) |
321 | 290 | cli.add_command(create_superuser) |
322 | 291 | cli.add_command(sql_shell) |
323 | cli.add_command(status_check) | |
324 | 292 | cli.add_command(create_tables) |
325 | 293 | cli.add_command(change_password) |
326 | 294 | cli.add_command(migrate) |
327 | 295 | cli.add_command(add_custom_field) |
328 | 296 | cli.add_command(delete_custom_field) |
329 | cli.add_command(support) | |
330 | 297 | cli.add_command(list_plugins) |
331 | 298 | cli.add_command(rename_user) |
332 | 299 | cli.add_command(openapi_yaml) |
333 | 300 | cli.add_command(generate_nginx_config) |
334 | 301 | cli.add_command(import_vulnerability_templates) |
302 | cli.add_command(settings) | |
335 | 303 | |
336 | 304 | if __name__ == '__main__': |
337 | 305 | cli() |
0 | """add fields to report | |
1 | ||
2 | Revision ID: 97a9348d0406 | |
3 | Revises: f0439bf6688a | |
4 | Create Date: 2021-06-03 18:36:28.695990+00:00 | |
5 | ||
6 | """ | |
7 | from alembic import op | |
8 | import sqlalchemy as sa | |
9 | from sqlalchemy.sql import expression | |
10 | ||
11 | ||
12 | # revision identifiers, used by Alembic. | |
13 | revision = '97a9348d0406' | |
14 | down_revision = 'f0439bf6688a' | |
15 | branch_labels = None | |
16 | depends_on = None | |
17 | ||
18 | ||
19 | def upgrade(): | |
20 | # ### commands auto generated by Alembic - please adjust! ### | |
21 | op.add_column('executive_report', sa.Column('duplicate_detection', sa.Boolean(), nullable=False, | |
22 | server_default=expression.false())) | |
23 | op.add_column('executive_report', sa.Column('border_size', sa.Integer(), nullable=False, server_default='3')) | |
24 | # ### end Alembic commands ### | |
25 | ||
26 | ||
27 | def downgrade(): | |
28 | # ### commands auto generated by Alembic - please adjust! ### | |
29 | op.drop_column('executive_report', 'border_size') | |
30 | op.drop_column('executive_report', 'duplicate_detection') | |
31 | # ### end Alembic commands ### |
+103
-0
0 | """Updating to a Role model to use faraday security RBAC | |
1 | ||
2 | Revision ID: f0439bf6688a | |
3 | Revises: 18891ca61db6 | |
4 | Create Date: 2021-05-26 18:38:23.267138+00:00 | |
5 | ||
6 | """ | |
7 | from alembic import op | |
8 | import sqlalchemy as sa | |
9 | from sqlalchemy.dialects import postgresql | |
10 | ||
11 | # revision identifiers, used by Alembic. | |
12 | revision = 'f0439bf6688a' | |
13 | down_revision = '18891ca61db6' | |
14 | branch_labels = None | |
15 | depends_on = None | |
16 | ||
17 | ROLES = ['admin', 'pentester', 'client', 'asset_owner'] | |
18 | ||
19 | ||
20 | def upgrade(): | |
21 | # ### commands auto generated by Alembic - please adjust! ### | |
22 | op.create_table('faraday_role', | |
23 | sa.Column('id', sa.Integer(), nullable=False), | |
24 | sa.Column('name', sa.String(length=80), nullable=True), | |
25 | sa.PrimaryKeyConstraint('id'), | |
26 | sa.UniqueConstraint('name') | |
27 | ) | |
28 | op.create_table('roles_users', | |
29 | sa.Column('user_id', sa.Integer(), nullable=True), | |
30 | sa.Column('role_id', sa.Integer(), nullable=True), | |
31 | sa.ForeignKeyConstraint(['role_id'], ['faraday_role.id'], ), | |
32 | sa.ForeignKeyConstraint(['user_id'], ['faraday_user.id'], ) | |
33 | ) | |
34 | ||
35 | op.execute("INSERT INTO faraday_role(name) VALUES ('admin'),('pentester'),('client'),('asset_owner');") | |
36 | ||
37 | roles_users = sa.table( | |
38 | 'roles_users', | |
39 | sa.column('user_id', sa.Integer), | |
40 | sa.column('role_id', sa.Integer) | |
41 | ) | |
42 | ||
43 | conn = op.get_bind() | |
44 | res = conn.execute('SELECT name, id FROM faraday_role').fetchall() | |
45 | roles = dict(res) | |
46 | ||
47 | res = conn.execute('SELECT id, role FROM faraday_user').fetchall() | |
48 | ||
49 | for _id, role in res: | |
50 | op.execute( | |
51 | roles_users.insert().values({'user_id': _id, 'role_id': roles[role]}) | |
52 | ) | |
53 | ||
54 | op.drop_column('faraday_user', 'role') | |
55 | op.alter_column('vulnerability', 'risk', | |
56 | existing_type=sa.REAL(), | |
57 | type_=sa.Float(precision=3, asdecimal=1), | |
58 | existing_nullable=True) | |
59 | op.alter_column('vulnerability_template', 'risk', | |
60 | existing_type=sa.REAL(), | |
61 | type_=sa.Float(precision=3, asdecimal=1), | |
62 | existing_nullable=True) | |
63 | # ### end Alembic commands ### | |
64 | ||
65 | ||
66 | def downgrade(): | |
67 | # ### commands auto generated by Alembic - please adjust! ### | |
68 | op.alter_column('vulnerability_template', 'risk', | |
69 | existing_type=sa.Float(precision=3, asdecimal=1), | |
70 | type_=sa.REAL(), | |
71 | existing_nullable=True) | |
72 | op.alter_column('vulnerability', 'risk', | |
73 | existing_type=sa.Float(precision=3, asdecimal=1), | |
74 | type_=sa.REAL(), | |
75 | existing_nullable=True) | |
76 | op.add_column('faraday_user', sa.Column('role', postgresql.ENUM('admin', 'pentester', 'client', 'asset_owner', | |
77 | name='user_roles'), | |
78 | autoincrement=False, | |
79 | nullable=True)) | |
80 | ||
81 | users = sa.table( | |
82 | 'faraday_user', | |
83 | sa.column('id', sa.Integer), | |
84 | sa.Column('role', sa.Enum(*ROLES, 'user_roles')), | |
85 | ) | |
86 | ||
87 | conn = op.get_bind() | |
88 | res = conn.execute('SELECT id, name FROM faraday_role').fetchall() | |
89 | roles = dict(res) | |
90 | ||
91 | res = conn.execute('SELECT user_id, role_id FROM roles_users').fetchall() | |
92 | ||
93 | for _id, role_id in res: | |
94 | op.execute( | |
95 | users.update().where(users.c.id == _id).values({'role': roles[role_id]}) | |
96 | ) | |
97 | ||
98 | op.alter_column('faraday_user', 'role', nullable=False) | |
99 | ||
100 | op.drop_table('roles_users') | |
101 | op.drop_table('faraday_role') | |
102 | # ### end Alembic commands ### |
56 | 56 | raise UserWarning('Invalid username or password') |
57 | 57 | |
58 | 58 | def _url(self, path, is_get=False): |
59 | url = self.base + 'v2/' + path | |
59 | url = self.base + 'v3/' + path | |
60 | 60 | if self.command_id and 'commands' not in url and not url.endswith('}') and not is_get: |
61 | 61 | if '?' in url: |
62 | 62 | url += f'&command_id={self.command_id}' |
63 | 63 | elif url.endswith('/'): |
64 | url = f'{url[:-1]}?command_id={self.command_id}' | |
65 | else: | |
64 | 66 | url += f'?command_id={self.command_id}' |
65 | else: | |
66 | url += f'/?command_id={self.command_id}' | |
67 | 67 | return url |
68 | 68 | |
69 | 69 | def _get(self, url, object_name): |
127 | 127 | else: |
128 | 128 | cookies = getattr(resp, 'cookies', None) |
129 | 129 | if cookies is not None: |
130 | token_response = self.requests.get(self.base + 'v2/token/', cookies=cookies) | |
130 | token_response = self.requests.get(self.base + 'v3/token', cookies=cookies) | |
131 | 131 | if token_response.status_code != 404: |
132 | 132 | token = token_response.json() |
133 | 133 | else: |
134 | token = self.requests.get(self.base + 'v2/token/').json | |
134 | token = self.requests.get(self.base + 'v3/token').json | |
135 | 135 | |
136 | 136 | header = {'Authorization': f'Token {token}'} |
137 | 137 | |
148 | 148 | self.params = params |
149 | 149 | self.tool_name = tool_name |
150 | 150 | data = self._command_info() |
151 | res = self._post(self._url(f'ws/{self.workspace}/commands/'), data, 'command') | |
151 | res = self._post(self._url(f'ws/{self.workspace}/commands'), data, 'command') | |
152 | 152 | return res["_id"] |
153 | 153 | |
154 | 154 | def _command_info(self, duration=None): |
170 | 170 | |
171 | 171 | def close_command(self, command_id, duration): |
172 | 172 | data = self._command_info(duration) |
173 | self._put(self._url(f'ws/{self.workspace}/commands/{command_id}/'), data, 'command') | |
173 | self._put(self._url(f'ws/{self.workspace}/commands/{command_id}'), data, 'command') | |
174 | 174 | |
175 | 175 | def fetch_vulnerabilities(self): |
176 | return [Structure(**item['value']) for item in self._get(self._url(f'ws/{self.workspace}/vulns/', True), | |
176 | return [Structure(**item['value']) for item in self._get(self._url(f'ws/{self.workspace}/vulns', True), | |
177 | 177 | 'vulnerabilities')['vulnerabilities']] |
178 | 178 | |
179 | 179 | def fetch_services(self): |
180 | return [Structure(**item['value']) for item in self._get(self._url(f'ws/{self.workspace}/services/', True), | |
180 | return [Structure(**item['value']) for item in self._get(self._url(f'ws/{self.workspace}/services', True), | |
181 | 181 | 'services')['services']] |
182 | 182 | |
183 | 183 | def fetch_hosts(self): |
184 | return [Structure(**item['value']) for item in self._get(self._url(f'ws/{self.workspace}/hosts/', True), | |
184 | return [Structure(**item['value']) for item in self._get(self._url(f'ws/{self.workspace}/hosts', True), | |
185 | 185 | 'hosts')['rows']] |
186 | 186 | |
187 | 187 | def fetch_templates(self): |
188 | 188 | return [Structure(**item['doc']) for item in |
189 | self._get(self._url('vulnerability_template/', True), 'templates')['rows']] | |
189 | self._get(self._url('vulnerability_template', True), 'templates')['rows']] | |
190 | 190 | |
191 | 191 | def filter_vulnerabilities(self, **kwargs): |
192 | 192 | if len(list(kwargs.keys())) > 1: |
193 | 193 | params = urlencode(kwargs) |
194 | url = self._url(f'ws/{self.workspace}/vulns/?{params}') | |
194 | url = self._url(f'ws/{self.workspace}/vulns?{params}') | |
195 | 195 | else: |
196 | 196 | params = self.parse_args(**kwargs) |
197 | 197 | url = self._url(f'ws/{self.workspace}/vulns/{params}', True) |
200 | 200 | |
201 | 201 | def filter_services(self, **kwargs): |
202 | 202 | params = urlencode(kwargs) |
203 | url = self._url(f'ws/{self.workspace}/services/?{params}', True) | |
203 | url = self._url(f'ws/{self.workspace}/services?{params}', True) | |
204 | 204 | return [Structure(**item['value']) for item in |
205 | 205 | self._get(url, 'services')['services']] |
206 | 206 | |
207 | 207 | def filter_hosts(self, **kwargs): |
208 | 208 | params = urlencode(kwargs) |
209 | url = self._url(f'ws/{self.workspace}/hosts/?{params}', True) | |
209 | url = self._url(f'ws/{self.workspace}/hosts?{params}', True) | |
210 | 210 | return [Structure(**item['value']) for item in |
211 | 211 | self._get(url, 'hosts')['rows']] |
212 | 212 | |
221 | 221 | return filtered_templates |
222 | 222 | |
223 | 223 | def update_vulnerability(self, vulnerability): |
224 | return Structure(**self._put(self._url(f'ws/{self.workspace}/vulns/{vulnerability.id}/'), | |
224 | return Structure(**self._put(self._url(f'ws/{self.workspace}/vulns/{vulnerability.id}'), | |
225 | 225 | vulnerability.__dict__, 'vulnerability')) |
226 | 226 | |
227 | 227 | def update_service(self, service): |
229 | 229 | service.ports = [service.ports] |
230 | 230 | else: |
231 | 231 | service.ports = [] |
232 | return Structure(**self._put(self._url(f'ws/{self.workspace}/services/{service.id}/'), | |
232 | return Structure(**self._put(self._url(f'ws/{self.workspace}/services/{service.id}'), | |
233 | 233 | service.__dict__, 'service')) |
234 | 234 | |
235 | 235 | def update_host(self, host): |
236 | return Structure(**self._put(self._url(f'ws/{self.workspace}/hosts/{host.id}/'), | |
236 | return Structure(**self._put(self._url(f'ws/{self.workspace}/hosts/{host.id}'), | |
237 | 237 | host.__dict__, 'hosts')) |
238 | 238 | |
239 | 239 | def delete_vulnerability(self, vulnerability_id): |
240 | return self._delete(self._url(f'ws/{self.workspace}/vulns/{vulnerability_id}/'), 'vulnerability') | |
240 | return self._delete(self._url(f'ws/{self.workspace}/vulns/{vulnerability_id}'), 'vulnerability') | |
241 | 241 | |
242 | 242 | def delete_service(self, service_id): |
243 | return self._delete(self._url(f'ws/{self.workspace}/services/{service_id}/'), 'service') | |
243 | return self._delete(self._url(f'ws/{self.workspace}/services/{service_id}'), 'service') | |
244 | 244 | |
245 | 245 | def delete_host(self, host_id): |
246 | return self._delete(self._url(f'ws/{self.workspace}/hosts/{host_id}/'), 'host') | |
246 | return self._delete(self._url(f'ws/{self.workspace}/hosts/{host_id}'), 'host') | |
247 | 247 | |
248 | 248 | @staticmethod |
249 | 249 | def parse_args(**kwargs): |
581 | 581 | _objs_value = None |
582 | 582 | if 'object' in rule: |
583 | 583 | _objs_value = rule['object'] |
584 | command_start = datetime.now() | |
584 | command_start = datetime.utcnow() | |
585 | 585 | command_id = self.api.create_command( |
586 | 586 | itime=time.mktime(command_start.timetuple()), |
587 | 587 | params=self.rules, |
629 | 629 | if self.mail_notification: |
630 | 630 | subject = 'Faraday searcher alert' |
631 | 631 | body = '%s %s have been modified by rule %s at %s' % ( |
632 | object_type, obj.name, rule['id'], str(datetime.now())) | |
632 | object_type, obj.name, rule['id'], str(datetime.utcnow())) | |
633 | 633 | self.mail_notification.send_mail(expression, subject, body) |
634 | 634 | logger.info(f"Sending mail to: '{expression}'") |
635 | 635 | else: |
636 | 636 | logger.warn("Searcher needs SMTP configuration to send mails") |
637 | 637 | |
638 | duration = (datetime.now() - command_start).seconds | |
638 | duration = (datetime.utcnow() - command_start).seconds | |
639 | 639 | self.api.close_command(self.api.command_id, duration) |
640 | 640 | return True |
641 | 641 |
31 | 31 | from faraday.server.schemas import NullToBlankString |
32 | 32 | from faraday.server.utils.database import ( |
33 | 33 | get_conflict_object, |
34 | is_unique_constraint_violation | |
35 | ) | |
34 | is_unique_constraint_violation, | |
35 | not_null_constraint_violation | |
36 | ) | |
36 | 37 | from faraday.server.utils.filters import FlaskRestlessSchema |
37 | 38 | from faraday.server.utils.search import search |
38 | 39 | |
96 | 97 | |
97 | 98 | #: The prefix where the endpoint should be registered. |
98 | 99 | #: This is useful for API versioning |
99 | route_prefix = '/v2/' | |
100 | route_prefix = '/v3/' | |
100 | 101 | |
101 | 102 | #: Arguments that are passed to the view but shouldn't change the route |
102 | 103 | #: rule. This should be used when route_prefix is parametrized |
155 | 156 | #: it, indicate it here to prevent doing an extra SQL query. |
156 | 157 | get_undefer = [] # List of columns to undefer |
157 | 158 | |
159 | trailing_slash = False | |
160 | ||
158 | 161 | def _get_schema_class(self): |
159 | 162 | """By default, it returns ``self.schema_class``. |
160 | 163 | |
361 | 364 | """ |
362 | 365 | |
363 | 366 | # Default attributes |
364 | route_prefix = '/v2/ws/<workspace_name>/' | |
367 | route_prefix = '/v3/ws/<workspace_name>/' | |
365 | 368 | base_args = ['workspace_name'] # Required to prevent double usage of <workspace_name> |
366 | 369 | |
367 | 370 | def _get_workspace(self, workspace_name): |
944 | 947 | db.session.commit() |
945 | 948 | except sqlalchemy.exc.IntegrityError as ex: |
946 | 949 | if not is_unique_constraint_violation(ex): |
947 | raise | |
950 | if not_null_constraint_violation(ex): | |
951 | flask.abort(flask.make_response({'message': 'Be sure to send all required parameters.'}, 400)) | |
952 | else: | |
953 | raise | |
948 | 954 | db.session.rollback() |
949 | 955 | conflict_obj = get_conflict_object(db.session, obj, data) |
950 | 956 | if conflict_obj: |
1152 | 1158 | else: |
1153 | 1159 | raise |
1154 | 1160 | return obj |
1155 | ||
1156 | ||
1157 | class PatchableMixin: | |
1158 | # TODO must be used with a UpdateMixin, when v2 be deprecated, add patch() to that Mixin | |
1159 | 1161 | |
1160 | 1162 | def patch(self, object_id, **kwargs): |
1161 | 1163 | """ |
1250 | 1252 | self._set_command_id(obj, False) |
1251 | 1253 | return super()._perform_update(object_id, obj, data, workspace_name) |
1252 | 1254 | |
1253 | ||
1254 | class PatchableWorkspacedMixin(PatchableMixin): | |
1255 | # TODO must be used with a UpdateWorkspacedMixin, when v2 be deprecated, add patch() to that Mixin | |
1256 | ||
1257 | 1255 | def patch(self, object_id, workspace_name=None): |
1258 | 1256 | """ |
1259 | 1257 | --- |
0 | 0 | # Faraday Penetration Test IDE |
1 | 1 | # Copyright (C) 2018 Infobyte LLC (http://www.infobytesec.com/) |
2 | 2 | # See the file 'doc/LICENSE' for the license information |
3 | import time | |
4 | 3 | from datetime import datetime |
5 | 4 | |
5 | import pytz | |
6 | 6 | from flask import Blueprint |
7 | 7 | from marshmallow import fields |
8 | 8 | |
9 | from faraday.server.api.base import AutoSchema, ReadWriteWorkspacedView, PaginatedMixin, PatchableWorkspacedMixin | |
9 | from faraday.server.api.base import AutoSchema, ReadWriteWorkspacedView, PaginatedMixin | |
10 | 10 | from faraday.server.models import Command |
11 | 11 | from faraday.server.schemas import PrimaryKeyRelatedField |
12 | 12 | |
29 | 29 | creator = PrimaryKeyRelatedField('username', dump_only=True) |
30 | 30 | |
31 | 31 | def load_itime(self, value): |
32 | return datetime.fromtimestamp(value) | |
32 | return datetime.utcfromtimestamp(value) | |
33 | 33 | |
34 | 34 | def get_itime(self, obj): |
35 | return time.mktime(obj.start_date.utctimetuple()) * 1000 | |
35 | return obj.start_date.replace(tzinfo=pytz.utc).timestamp() * 1000 | |
36 | 36 | |
37 | 37 | def get_sum_created_vulnerabilities(self, obj): |
38 | 38 | return obj.sum_created_vulnerabilities |
89 | 89 | } |
90 | 90 | |
91 | 91 | |
92 | class ActivityFeedV3View(ActivityFeedView, PatchableWorkspacedMixin): | |
93 | route_prefix = '/v3/ws/<workspace_name>/' | |
94 | trailing_slash = False | |
95 | ||
96 | ||
97 | 92 | ActivityFeedView.register(activityfeed_api) |
98 | ActivityFeedV3View.register(activityfeed_api) |
6 | 6 | import logging |
7 | 7 | |
8 | 8 | import pyotp |
9 | from faraday_agent_parameters_types.utils import type_validate, get_manifests | |
9 | 10 | from flask import Blueprint, abort, request, make_response, jsonify |
10 | 11 | from flask_classful import route |
11 | 12 | from marshmallow import fields, Schema, EXCLUDE |
19 | 20 | ReadOnlyView, |
20 | 21 | CreateMixin, |
21 | 22 | GenericView, |
22 | ReadOnlyMultiWorkspacedView, | |
23 | PatchableMixin | |
23 | ReadOnlyMultiWorkspacedView | |
24 | 24 | ) |
25 | 25 | from faraday.server.api.modules.workspaces import WorkspaceSchema |
26 | 26 | from faraday.server.models import Agent, Executor, AgentExecution, db, \ |
177 | 177 | return agent |
178 | 178 | |
179 | 179 | |
180 | class AgentCreationV3View(AgentCreationView): | |
181 | route_prefix = '/v3' | |
182 | trailing_slash = False | |
183 | ||
184 | ||
185 | 180 | class ExecutorDataSchema(Schema): |
186 | 181 | executor = fields.String(default=None) |
187 | 182 | args = fields.Dict(default=None) |
258 | 253 | return obj |
259 | 254 | |
260 | 255 | |
261 | class AgentWithWorkspacesV3View(AgentWithWorkspacesView, PatchableMixin): | |
262 | route_prefix = '/v3' | |
263 | trailing_slash = False | |
264 | ||
265 | ||
266 | 256 | class AgentView(ReadOnlyMultiWorkspacedView): |
267 | 257 | route_base = 'agents' |
268 | 258 | model_class = Agent |
269 | 259 | schema_class = AgentSchema |
270 | 260 | get_joinedloads = [Agent.creator, Agent.executors, Agent.workspaces] |
271 | 261 | |
272 | @route('/<int:agent_id>/', methods=['DELETE']) | |
262 | @route('/<int:agent_id>', methods=['DELETE']) | |
273 | 263 | def remove_workspace(self, workspace_name, agent_id): |
274 | 264 | """ |
275 | 265 | --- |
291 | 281 | db.session.commit() |
292 | 282 | return make_response({"description": "ok"}, 204) |
293 | 283 | |
294 | @route('/<int:agent_id>/run/', methods=['POST']) | |
284 | @route('/<int:agent_id>/run', methods=['POST']) | |
295 | 285 | def run_agent(self, workspace_name, agent_id): |
296 | 286 | """ |
297 | 287 | --- |
316 | 306 | try: |
317 | 307 | executor = Executor.query.filter(Executor.name == executor_data['executor'], |
318 | 308 | Executor.agent_id == agent_id).one() |
309 | ||
310 | # VALIDATE | |
311 | errors = {} | |
312 | for param_name, param_data in executor_data["args"].items(): | |
313 | val_error = type_validate(executor.parameters_metadata[param_name]['type'], param_data) | |
314 | if val_error: | |
315 | errors[param_name] = val_error | |
316 | if errors: | |
317 | response = jsonify(errors) | |
318 | response.status_code = 400 | |
319 | abort(response) | |
320 | ||
319 | 321 | params = ', '.join([f'{key}={value}' for (key, value) in executor_data["args"].items()]) |
320 | 322 | command = Command( |
321 | 323 | import_source="agent", |
324 | 326 | user='', |
325 | 327 | hostname='', |
326 | 328 | params=params, |
327 | start_date=datetime.now(), | |
329 | start_date=datetime.utcnow(), | |
328 | 330 | workspace=workspace |
329 | 331 | ) |
330 | 332 | |
357 | 359 | 'command_id': command.id, |
358 | 360 | }) |
359 | 361 | |
360 | ||
361 | class AgentV3View(AgentView): | |
362 | route_prefix = '/v3/ws/<workspace_name>/' | |
363 | trailing_slash = False | |
364 | ||
365 | @route('/<int:agent_id>', methods=['DELETE']) | |
366 | def remove_workspace(self, workspace_name, agent_id): | |
367 | # This endpoint is not an exception for V3, overrides logic of DELETE | |
368 | return super().remove_workspace(workspace_name, agent_id) | |
369 | ||
370 | @route('/<int:agent_id>/run', methods=['POST']) | |
371 | def run_agent(self, workspace_name, agent_id): | |
372 | return super().run_agent(workspace_name, agent_id) | |
373 | ||
374 | remove_workspace.__doc__ = AgentView.remove_workspace.__doc__ | |
375 | run_agent.__doc__ = AgentView.run_agent.__doc__ | |
362 | @route('/get_manifests', methods=['GET']) | |
363 | def manifests_get(self, workspace_name): | |
364 | """ | |
365 | --- | |
366 | get: | |
367 | tags: ["Agent"] | |
368 | summary: Get all manifests, Optionally choose latest version with parameter | |
369 | parameters: | |
370 | - in: version | |
371 | name: agent_version | |
372 | description: latest version to request | |
373 | ||
374 | responses: | |
375 | 200: | |
376 | description: Ok | |
377 | """ | |
378 | try: | |
379 | return flask.jsonify(get_manifests(request.args.get("agent_version"))) | |
380 | except ValueError as e: | |
381 | flask.abort(400, e) | |
376 | 382 | |
377 | 383 | |
378 | 384 | AgentWithWorkspacesView.register(agent_api) |
379 | AgentWithWorkspacesV3View.register(agent_api) | |
380 | 385 | AgentCreationView.register(agent_creation_api) |
381 | AgentCreationV3View.register(agent_creation_api) | |
382 | 386 | AgentView.register(agent_api) |
383 | AgentV3View.register(agent_api) |
44 | 44 | faraday_server.agent_token_expiration)) |
45 | 45 | return AgentAuthTokenSchema().dump( |
46 | 46 | {'token': totp.now(), |
47 | 'expires_in': totp.interval - datetime.datetime.now().timestamp() % totp.interval, | |
47 | 'expires_in': totp.interval - datetime.datetime.utcnow().timestamp() % totp.interval, | |
48 | 48 | 'total_duration': totp.interval}) |
49 | 49 | |
50 | 50 | |
51 | class AgentAuthTokenV3View(AgentAuthTokenView): | |
52 | route_prefix = '/v3' | |
53 | trailing_slash = False | |
54 | ||
55 | ||
56 | 51 | AgentAuthTokenView.register(agent_auth_token_api) |
57 | AgentAuthTokenV3View.register(agent_auth_token_api) |
242 | 242 | _create_host(ws, host, command) |
243 | 243 | |
244 | 244 | if 'command' in data and set_end_date: |
245 | command.end_date = datetime.now() if command.end_date is None else \ | |
245 | command.end_date = datetime.utcnow() if command.end_date is None else \ | |
246 | 246 | command.end_date |
247 | 247 | db.session.commit() |
248 | 248 | |
315 | 315 | updated = True |
316 | 316 | |
317 | 317 | if updated: |
318 | service.update_date = datetime.now() | |
318 | service.update_date = datetime.utcnow() | |
319 | 319 | |
320 | 320 | return service |
321 | 321 | |
378 | 378 | try: |
379 | 379 | run_timestamp = float(run_date_string) |
380 | 380 | run_date = datetime.utcfromtimestamp(run_timestamp) |
381 | if run_date < datetime.now() + timedelta(hours=24): | |
381 | if run_date < datetime.utcnow() + timedelta(hours=24): | |
382 | 382 | logger.debug("Valid run date") |
383 | 383 | else: |
384 | 384 | run_date = None |
549 | 549 | post.is_public = True |
550 | 550 | |
551 | 551 | |
552 | class BulkCreateV3View(BulkCreateView): | |
553 | route_prefix = '/v3/ws/<workspace_name>/' | |
554 | trailing_slash = False | |
555 | ||
556 | ||
557 | 552 | BulkCreateView.register(bulk_create_api) |
558 | BulkCreateV3View.register(bulk_create_api) |
3 | 3 | import time |
4 | 4 | import datetime |
5 | 5 | |
6 | import pytz | |
6 | 7 | import flask |
7 | 8 | from flask import Blueprint |
8 | 9 | from flask_classful import route |
9 | 10 | from marshmallow import fields, post_load, ValidationError |
10 | 11 | |
11 | from faraday.server.api.base import AutoSchema, ReadWriteWorkspacedView, PaginatedMixin, PatchableWorkspacedMixin | |
12 | from faraday.server.api.base import AutoSchema, ReadWriteWorkspacedView, PaginatedMixin | |
12 | 13 | from faraday.server.models import Command, Workspace |
13 | 14 | from faraday.server.schemas import MutableField, PrimaryKeyRelatedField, SelfNestedField, MetadataSchema |
14 | 15 | |
28 | 29 | |
29 | 30 | def load_itime(self, value): |
30 | 31 | try: |
31 | return datetime.datetime.fromtimestamp(value) | |
32 | return datetime.datetime.utcfromtimestamp(value) | |
32 | 33 | except ValueError: |
33 | 34 | raise ValidationError('Invalid Itime Value') |
34 | 35 | |
35 | 36 | def get_itime(self, obj): |
36 | return time.mktime(obj.start_date.utctimetuple()) * 1000 | |
37 | return obj.start_date.replace(tzinfo=pytz.utc).timestamp() * 1000 | |
37 | 38 | |
38 | 39 | def get_duration(self, obj): |
39 | 40 | # obj.start_date can't be None |
40 | 41 | if obj.end_date: |
41 | 42 | return (obj.end_date - obj.start_date).seconds + ((obj.end_date - obj.start_date).microseconds / 1000000.0) |
42 | 43 | else: |
43 | if (datetime.datetime.now() - obj.start_date).total_seconds() > 86400: # 86400 is 1d TODO BY CONFIG | |
44 | if (datetime.datetime.utcnow() - obj.start_date).total_seconds() > 86400: # 86400 is 1d TODO BY CONFIG | |
44 | 45 | return 'Timeout' |
45 | 46 | return 'In progress' |
46 | 47 | |
78 | 79 | 'commands': commands, |
79 | 80 | } |
80 | 81 | |
81 | @route('/activity_feed/') | |
82 | @route('/activity_feed') | |
82 | 83 | def activity_feed(self, workspace_name): |
83 | 84 | """ |
84 | 85 | --- |
109 | 110 | }) |
110 | 111 | return res |
111 | 112 | |
112 | @route('/last/', methods=['GET']) | |
113 | @route('/last', methods=['GET']) | |
113 | 114 | def last_command(self, workspace_name): |
114 | 115 | """ |
115 | 116 | --- |
139 | 140 | return flask.jsonify(command_obj) |
140 | 141 | |
141 | 142 | |
142 | class CommandV3View(CommandView, PatchableWorkspacedMixin): | |
143 | route_prefix = '/v3/ws/<workspace_name>/' | |
144 | trailing_slash = False | |
145 | ||
146 | @route('/activity_feed') | |
147 | def activity_feed(self, workspace_name): | |
148 | return super().activity_feed(workspace_name) | |
149 | ||
150 | @route('/last', methods=['GET']) | |
151 | def last_command(self, workspace_name): | |
152 | return super().last_command(workspace_name) | |
153 | ||
154 | activity_feed.__doc__ = CommandView.activity_feed.__doc__ | |
155 | last_command.__doc__ = CommandView.last_command.__doc__ | |
156 | ||
157 | ||
158 | 143 | CommandView.register(commandsrun_api) |
159 | CommandV3View.register(commandsrun_api) |
9 | 9 | from faraday.server.api.base import ( |
10 | 10 | AutoSchema, |
11 | 11 | ReadWriteWorkspacedView, |
12 | InvalidUsage, CreateWorkspacedMixin, GenericWorkspacedView, PatchableWorkspacedMixin) | |
12 | InvalidUsage, | |
13 | CreateWorkspacedMixin, | |
14 | GenericWorkspacedView | |
15 | ) | |
13 | 16 | from faraday.server.models import Comment |
14 | 17 | comment_api = Blueprint('comment_api', __name__) |
15 | 18 | |
83 | 86 | return res |
84 | 87 | |
85 | 88 | |
86 | class CommentV3View(CommentView, PatchableWorkspacedMixin): | |
87 | route_prefix = '/v3/ws/<workspace_name>/' | |
88 | trailing_slash = False | |
89 | ||
90 | ||
91 | class UniqueCommentV3View(UniqueCommentView, PatchableWorkspacedMixin): | |
92 | route_prefix = '/v3/ws/<workspace_name>/' | |
93 | trailing_slash = False | |
94 | ||
95 | ||
96 | 89 | CommentView.register(comment_api) |
97 | 90 | UniqueCommentView.register(comment_api) |
98 | CommentV3View.register(comment_api) | |
99 | UniqueCommentV3View.register(comment_api) |
10 | 10 | ReadWriteWorkspacedView, |
11 | 11 | FilterSetMeta, |
12 | 12 | FilterAlchemyMixin, |
13 | InvalidUsage, | |
14 | PatchableWorkspacedMixin | |
13 | InvalidUsage | |
15 | 14 | ) |
16 | 15 | from faraday.server.models import Credential, Host, Service, Workspace, db |
17 | 16 | from faraday.server.schemas import MutableField, SelfNestedField, MetadataSchema |
130 | 129 | } |
131 | 130 | |
132 | 131 | |
133 | class CredentialV3View(CredentialView, PatchableWorkspacedMixin): | |
134 | route_prefix = '/v3/ws/<workspace_name>/' | |
135 | trailing_slash = False | |
136 | ||
137 | ||
138 | 132 | CredentialView.register(credentials_api) |
139 | CredentialV3View.register(credentials_api) |
6 | 6 | from faraday.server.models import CustomFieldsSchema |
7 | 7 | from faraday.server.api.base import ( |
8 | 8 | AutoSchema, |
9 | ReadWriteView, | |
10 | PatchableMixin, | |
9 | ReadWriteView | |
11 | 10 | ) |
12 | 11 | |
13 | 12 | |
51 | 50 | return super()._update_object(obj, data) |
52 | 51 | |
53 | 52 | |
54 | class CustomFieldsSchemaV3View(CustomFieldsSchemaView, PatchableMixin): | |
55 | route_prefix = '/v3' | |
56 | trailing_slash = False | |
57 | ||
58 | ||
59 | 53 | CustomFieldsSchemaView.register(custom_fields_schema_api) |
60 | CustomFieldsSchemaV3View.register(custom_fields_schema_api) |
10 | 10 | logger = logging.getLogger(__name__) |
11 | 11 | |
12 | 12 | |
13 | @export_data_api.route('/v2/ws/<workspace_name>/export_data', methods=['GET']) | |
13 | @export_data_api.route('/v3/ws/<workspace_name>/export_data', methods=['GET']) | |
14 | 14 | def export_data(workspace_name): |
15 | 15 | """ |
16 | 16 | --- |
43 | 43 | else: |
44 | 44 | logger.error("Invalid format. Please, specify a valid format.") |
45 | 45 | abort(400, "Invalid format.") |
46 | ||
47 | ||
48 | @export_data_api.route('/v3/ws/<workspace_name>/export_data', methods=['GET']) | |
49 | def export_data_v3(workspace_name): | |
50 | return export_data(workspace_name) | |
51 | ||
52 | ||
53 | export_data_v3.__doc__ = export_data.__doc__ | |
54 | 46 | |
55 | 47 | |
56 | 48 | def xml_metasploit_format(workspace): |
13 | 13 | |
14 | 14 | |
15 | 15 | @gzipped |
16 | @exploits_api.route('/v2/vulners/exploits/<cveid>', methods=['GET']) | |
16 | @exploits_api.route('/v3/vulners/exploits/<cveid>', methods=['GET']) | |
17 | 17 | def get_exploits(cveid): |
18 | 18 | """ |
19 | 19 | --- |
73 | 73 | abort(make_response(jsonify(message=f'Could not find {str(ex)}'), 400)) |
74 | 74 | |
75 | 75 | return flask.jsonify(json_response) |
76 | ||
77 | ||
78 | @gzipped | |
79 | @exploits_api.route('/v3/vulners/exploits/<cveid>', methods=['GET']) | |
80 | def get_exploits_v3(cveid): | |
81 | """ | |
82 | --- | |
83 | get: | |
84 | tags: ["Vulnerability"] | |
85 | description: Use Vulns API to get all exploits available for a specific CVE-ID | |
86 | responses: | |
87 | 200: | |
88 | description: Ok | |
89 | """ | |
90 | get_exploits(cveid) |
23 | 23 | AutoSchema, |
24 | 24 | FilterAlchemyMixin, |
25 | 25 | FilterSetMeta, |
26 | FilterWorkspacedMixin, | |
27 | PatchableWorkspacedMixin | |
26 | FilterWorkspacedMixin | |
28 | 27 | ) |
29 | 28 | from faraday.server.schemas import ( |
30 | 29 | MetadataSchema, |
191 | 190 | pagination_metadata.total = count |
192 | 191 | return self._envelope_list(filtered_objs, pagination_metadata) |
193 | 192 | |
194 | @route('/bulk_create/', methods=['POST']) | |
193 | @route('/bulk_create', methods=['POST']) | |
195 | 194 | def bulk_create(self, workspace_name): |
196 | 195 | """ |
197 | 196 | --- |
258 | 257 | logger.error("Error parsing hosts CSV (%s)", e) |
259 | 258 | abort(400, f"Error parsing hosts CSV ({e})") |
260 | 259 | |
261 | @route('/<host_id>/services/') | |
260 | @route('/<host_id>/services') | |
262 | 261 | def service_list(self, workspace_name, host_id): |
263 | 262 | """ |
264 | 263 | --- |
279 | 278 | services = self._get_object(host_id, workspace_name).services |
280 | 279 | return ServiceSchema(many=True).dump(services) |
281 | 280 | |
282 | @route('/countVulns/') | |
281 | @route('/countVulns') | |
283 | 282 | def count_vulns(self, workspace_name): |
284 | 283 | """ |
285 | 284 | --- |
314 | 313 | |
315 | 314 | return res_dict |
316 | 315 | |
317 | @route('/<host_id>/tools_history/') | |
316 | @route('/<host_id>/tools_history') | |
318 | 317 | def tool_impacted_by_host(self, workspace_name, host_id): |
319 | 318 | """ |
320 | 319 | --- |
342 | 341 | res_dict = {'tools': []} |
343 | 342 | for row in result: |
344 | 343 | _, command = row |
345 | res_dict['tools'].append({'command': command.tool, 'user': command.user, 'params': command.params, 'command_id': command.id, 'create_date': command.create_date.replace(tzinfo=pytz.utc).strftime("%c")}) | |
344 | res_dict['tools'].append({'command': command.tool, 'user': command.user, 'params': command.params, 'command_id': command.id, 'create_date': command.create_date.replace(tzinfo=pytz.utc).isoformat()}) | |
346 | 345 | return res_dict |
347 | 346 | |
348 | 347 | def _perform_create(self, data, **kwargs): |
399 | 398 | or len(hosts)), |
400 | 399 | } |
401 | 400 | |
401 | # ### THIS WAS FROM V2 | |
402 | 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) | |
439 | ||
440 | ||
441 | class HostsV3View(HostsView, PatchableWorkspacedMixin): | |
442 | route_prefix = '/v3/ws/<workspace_name>/' | |
443 | trailing_slash = False | |
444 | ||
445 | @route('/<host_id>/services') | |
446 | def service_list(self, workspace_name, host_id): | |
447 | return super().service_list(workspace_name, host_id) | |
448 | ||
449 | @route('/<host_id>/tools_history') | |
450 | def tool_impacted_by_host(self, workspace_name, host_id): | |
451 | return super().tool_impacted_by_host(workspace_name, host_id) | |
452 | ||
453 | @route('/bulk_create', methods=['POST']) | |
454 | def bulk_create(self, workspace_name): | |
455 | return super().bulk_create(workspace_name) | |
456 | ||
457 | @route('/countVulns') | |
458 | def count_vulns(self, workspace_name): | |
459 | return super().count_vulns() | |
460 | ||
461 | service_list.__doc__ = HostsView.service_list.__doc__ | |
462 | tool_impacted_by_host.__doc__ = HostsView.tool_impacted_by_host.__doc__ | |
463 | bulk_create.__doc__ = HostsView.bulk_create.__doc__ | |
464 | count_vulns.__doc__ = HostsView.count_vulns.__doc__ | |
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) | |
465 | 439 | |
466 | 440 | |
467 | 441 | HostsView.register(host_api) |
468 | HostsV3View.register(host_api) |
5 | 5 | from flask import Blueprint |
6 | 6 | |
7 | 7 | from faraday import __version__ as f_version |
8 | from faraday.server.config import gen_web_config | |
9 | ||
8 | from faraday.server.config import faraday_server | |
9 | from faraday.settings.dashboard import DashboardSettings | |
10 | 10 | |
11 | 11 | info_api = Blueprint('info_api', __name__) |
12 | 12 | |
13 | 13 | |
14 | @info_api.route('/v2/info', methods=['GET']) | |
14 | @info_api.route('/v3/info', methods=['GET']) | |
15 | 15 | def show_info(): |
16 | 16 | """ |
17 | 17 | --- |
29 | 29 | return response |
30 | 30 | |
31 | 31 | |
32 | @info_api.route('/v3/info', methods=['GET']) | |
33 | def show_info_v3(): | |
34 | return show_info() | |
35 | ||
36 | ||
37 | show_info_v3.__doc__ = show_info.__doc__ | |
38 | ||
39 | ||
40 | 32 | @info_api.route('/config') |
41 | 33 | def get_config(): |
42 | 34 | """ |
48 | 40 | 200: |
49 | 41 | description: Ok |
50 | 42 | """ |
51 | return flask.jsonify(gen_web_config()) | |
43 | doc = { | |
44 | 'ver': f_version, | |
45 | 'websocket_port': faraday_server.websocket_port, | |
46 | 'show_vulns_by_price': DashboardSettings.settings.show_vulns_by_price, | |
47 | 'smtp_enabled': False | |
48 | } | |
49 | ||
50 | return flask.jsonify(doc) | |
52 | 51 | |
53 | 52 | |
54 | 53 | get_config.is_public = True |
55 | 54 | show_info.is_public = True |
56 | show_info_v3.is_public = True |
6 | 6 | from faraday.server.models import License |
7 | 7 | from faraday.server.api.base import ( |
8 | 8 | ReadWriteView, |
9 | AutoSchema, | |
10 | PatchableMixin | |
9 | AutoSchema | |
11 | 10 | ) |
12 | 11 | from faraday.server.schemas import ( |
13 | 12 | StrictDateTimeField, |
36 | 35 | schema_class = LicenseSchema |
37 | 36 | |
38 | 37 | |
39 | class LicenseV3View(LicenseView, PatchableMixin): | |
40 | route_prefix = 'v3/' | |
41 | trailing_slash = False | |
42 | ||
43 | ||
44 | 38 | LicenseView.register(license_api) |
45 | LicenseV3View.register(license_api) |
50 | 50 | return jsonify({'preferences': flask_login.current_user.preferences}), 200 |
51 | 51 | |
52 | 52 | |
53 | class PreferencesV3View(PreferencesView): | |
54 | route_prefix = '/v3' | |
55 | trailing_slash = False | |
56 | ||
57 | ||
58 | 53 | PreferencesView.register(preferences_api) |
59 | PreferencesV3View.register(preferences_api) |
7 | 7 | from faraday.server.models import SearchFilter |
8 | 8 | from faraday.server.api.base import ( |
9 | 9 | ReadWriteView, |
10 | AutoSchema, | |
11 | PatchableMixin, | |
10 | AutoSchema | |
12 | 11 | ) |
13 | 12 | |
14 | 13 | searchfilter_api = Blueprint('searchfilter_api', __name__) |
34 | 33 | return query.filter(SearchFilter.creator_id == flask_login.current_user.id) |
35 | 34 | |
36 | 35 | |
37 | class SearchFilterV3View(SearchFilterView, PatchableMixin): | |
38 | route_prefix = 'v3/' | |
39 | trailing_slash = False | |
40 | ||
41 | ||
42 | 36 | SearchFilterView.register(searchfilter_api) |
43 | SearchFilterV3View.register(searchfilter_api) |
6 | 6 | from marshmallow.validate import OneOf, Range |
7 | 7 | from sqlalchemy.orm.exc import NoResultFound |
8 | 8 | |
9 | from faraday.server.api.base import AutoSchema, ReadWriteWorkspacedView, FilterSetMeta, \ | |
10 | FilterAlchemyMixin, PatchableWorkspacedMixin | |
9 | from faraday.server.api.base import ( | |
10 | AutoSchema, | |
11 | ReadWriteWorkspacedView, | |
12 | FilterSetMeta, | |
13 | FilterAlchemyMixin | |
14 | ) | |
11 | 15 | from faraday.server.models import Host, Service, Workspace |
12 | 16 | from faraday.server.schemas import ( |
13 | 17 | MetadataSchema, |
134 | 138 | return super()._perform_create(data, **kwargs) |
135 | 139 | |
136 | 140 | |
137 | class ServiceV3View(ServiceView, PatchableWorkspacedMixin): | |
138 | route_prefix = '/v3/ws/<workspace_name>/' | |
139 | trailing_slash = False | |
140 | ||
141 | ||
142 | 141 | ServiceView.register(services_api) |
143 | ServiceV3View.register(services_api) |
0 | # Faraday Penetration Test IDE | |
1 | # Copyright (C) 2019 Infobyte LLC (http://www.infobytesec.com/) | |
2 | # See the file 'doc/LICENSE' for the license information | |
3 | import flask | |
4 | import logging | |
5 | from flask import abort, make_response | |
6 | from marshmallow import Schema, ValidationError | |
7 | ||
8 | from faraday.settings import get_settings | |
9 | from faraday.settings.exceptions import InvalidConfigurationError | |
10 | ||
11 | from faraday.server.api.base import ( | |
12 | GenericView | |
13 | ) | |
14 | ||
15 | logger = logging.getLogger(__name__) | |
16 | ||
17 | ||
18 | class EmptySchema(Schema): | |
19 | pass | |
20 | ||
21 | ||
22 | class SettingsAPIView(GenericView): | |
23 | route_prefix = '/v3/settings/' | |
24 | schema_class = EmptySchema | |
25 | ||
26 | def get(self, **kwargs): | |
27 | """ | |
28 | --- | |
29 | get: | |
30 | tags: ["settings"] | |
31 | summary: Retrieves settings of {route_base} | |
32 | responses: | |
33 | 200: | |
34 | description: Ok | |
35 | content: | |
36 | application/json: | |
37 | schema: {schema_class} | |
38 | 403: | |
39 | description: Admin user required | |
40 | """ | |
41 | settings = get_settings(self.route_base) | |
42 | return self._dump(settings.value, kwargs) | |
43 | ||
44 | def patch(self, **kwargs): | |
45 | """ | |
46 | --- | |
47 | patch: | |
48 | tags: ["settings"] | |
49 | summary: Creates/Updates settings of {route_base} | |
50 | requestBody: | |
51 | required: true | |
52 | content: | |
53 | application/json: | |
54 | schema: {schema_class} | |
55 | responses: | |
56 | 200: | |
57 | description: Created | |
58 | content: | |
59 | application/json: | |
60 | schema: {schema_class} | |
61 | 403: | |
62 | description: Admin user required | |
63 | """ | |
64 | context = {'updating': False} | |
65 | ||
66 | data = self._parse_data(self._get_schema_instance(kwargs, context=context), | |
67 | flask.request) | |
68 | settings = get_settings(self.route_base) | |
69 | try: | |
70 | valid_setting_config = settings.validate_configuration(data) | |
71 | settings.update(valid_setting_config) | |
72 | except (ValidationError, InvalidConfigurationError) as e: | |
73 | logger.error(f'Invalid setting for {data}: {e}.') | |
74 | abort(make_response({'messages': {'json': {'error': f'{e}.'}}}, 400)) | |
75 | return self._dump(settings.value, kwargs), 200 |
0 | # Faraday Penetration Test IDE | |
1 | # Copyright (C) 2021 Infobyte LLC (http://www.infobytesec.com/) | |
2 | # See the file 'doc/LICENSE' for the license information | |
3 | ||
4 | import logging | |
5 | from flask import Blueprint | |
6 | ||
7 | from faraday.settings.dashboard import DashboardSettingSchema, DashboardSettings | |
8 | from faraday.server.api.modules.settings import SettingsAPIView | |
9 | ||
10 | logger = logging.getLogger(__name__) | |
11 | dashboard_settings_api = Blueprint('dashboard_settings_api', __name__) | |
12 | ||
13 | ||
14 | class DashboardSettingsAPI(SettingsAPIView): | |
15 | route_base = DashboardSettings.settings_id | |
16 | schema_class = DashboardSettingSchema | |
17 | ||
18 | ||
19 | DashboardSettingsAPI.register(dashboard_settings_api) |
0 | # Faraday Penetration Test IDE | |
1 | # Copyright (C) 2021 Infobyte LLC (http://www.infobytesec.com/) | |
2 | # See the file 'doc/LICENSE' for the license information | |
3 | ||
4 | import logging | |
5 | from flask import Blueprint | |
6 | ||
7 | from faraday.settings.reports import ReportsSettingSchema, ReportsSettings | |
8 | from faraday.server.api.modules.settings import SettingsAPIView | |
9 | ||
10 | logger = logging.getLogger(__name__) | |
11 | reports_settings_api = Blueprint('reports_settings_api', __name__) | |
12 | ||
13 | ||
14 | class ReportsSettingsAPI(SettingsAPIView): | |
15 | route_base = ReportsSettings.settings_id | |
16 | schema_class = ReportsSettingSchema | |
17 | ||
18 | ||
19 | ReportsSettingsAPI.register(reports_settings_api) |
41 | 41 | ) |
42 | 42 | hashed_data = hash_data(flask_login.current_user.password) if flask_login.current_user.password else None |
43 | 43 | user_ip = request.headers.get('X-Forwarded-For', request.remote_addr) |
44 | requested_at = datetime.datetime.now() | |
44 | requested_at = datetime.datetime.utcnow() | |
45 | 45 | audit_logger.info(f"User [{flask_login.current_user.username}] requested token from IP [{user_ip}] at [{requested_at}]") |
46 | 46 | return serializer.dumps({'user_id': user_id, "validation_check": hashed_data}).decode('utf-8') |
47 | 47 | |
48 | 48 | |
49 | class TokenAuthV3View(TokenAuthView): | |
50 | route_prefix = '/v3' | |
51 | trailing_slash = False | |
52 | ||
53 | ||
54 | 49 | TokenAuthView.register(token_api) |
55 | TokenAuthV3View.register(token_api) |
35 | 35 | |
36 | 36 | |
37 | 37 | @gzipped |
38 | @upload_api.route('/v2/ws/<workspace>/upload_report', methods=['POST']) | |
38 | @upload_api.route('/v3/ws/<workspace>/upload_report', methods=['POST']) | |
39 | 39 | def file_upload(workspace=None): |
40 | 40 | """ |
41 | 41 | --- |
102 | 102 | name=workspace).one() |
103 | 103 | command = Command() |
104 | 104 | command.workspace = workspace_instance |
105 | command.start_date = datetime.now() | |
105 | command.start_date = datetime.utcnow() | |
106 | 106 | command.import_source = 'report' |
107 | 107 | # The data will be updated in the bulk_create function |
108 | 108 | command.tool = "In progress" |
126 | 126 | ) |
127 | 127 | else: |
128 | 128 | abort(make_response(jsonify(message="Missing report file"), 400)) |
129 | ||
130 | ||
131 | @gzipped | |
132 | @upload_api.route('/v3/ws/<workspace>/upload_report', methods=['POST']) | |
133 | def file_upload_v3(workspace=None): | |
134 | """ | |
135 | --- | |
136 | post: | |
137 | tags: ["Workspace", "File"] | |
138 | description: Upload a report file to create data within the given workspace | |
139 | responses: | |
140 | 201: | |
141 | description: Created | |
142 | 400: | |
143 | description: Bad request | |
144 | 403: | |
145 | description: Forbidden | |
146 | tags: ["Workspace", "File"] | |
147 | responses: | |
148 | 200: | |
149 | description: Ok | |
150 | """ | |
151 | return file_upload(workspace) |
26 | 26 | FilterSetMeta, |
27 | 27 | PaginatedMixin, |
28 | 28 | ReadWriteView, |
29 | FilterMixin, | |
30 | PatchableMixin | |
29 | FilterMixin | |
31 | 30 | ) |
32 | 31 | |
33 | 32 | from faraday.server.schemas import ( |
188 | 187 | |
189 | 188 | return schema |
190 | 189 | |
191 | @route('/bulk_create/', methods=['POST']) | |
190 | @route('/bulk_create', methods=['POST']) | |
192 | 191 | def bulk_create(self): |
193 | 192 | """ |
194 | 193 | --- |
319 | 318 | return vulns_list |
320 | 319 | |
321 | 320 | |
322 | class VulnerabilityTemplateV3View(VulnerabilityTemplateView, PatchableMixin): | |
323 | route_prefix = 'v3/' | |
324 | trailing_slash = False | |
325 | ||
326 | @route('/bulk_create', methods=['POST']) | |
327 | def bulk_create(self): | |
328 | return super().bulk_create() | |
329 | ||
330 | bulk_create.__doc__ = VulnerabilityTemplateView.bulk_create.__doc__ | |
331 | ||
332 | ||
333 | 321 | VulnerabilityTemplateView.register(vulnerability_template_api) |
334 | VulnerabilityTemplateV3View.register(vulnerability_template_api) |
9 | 9 | from pathlib import Path |
10 | 10 | |
11 | 11 | import flask |
12 | import wtforms | |
13 | 12 | from filteralchemy import Filter, FilterSet, operators |
14 | 13 | from flask import request, send_file |
15 | from flask import Blueprint | |
14 | from flask import Blueprint, make_response | |
16 | 15 | from flask_classful import route |
17 | from flask_wtf.csrf import validate_csrf | |
18 | 16 | from marshmallow import Schema, fields, post_load, ValidationError |
19 | 17 | from marshmallow.validate import OneOf |
20 | from sqlalchemy.orm import aliased, joinedload, selectin_polymorphic, undefer | |
18 | from sqlalchemy.orm import aliased, joinedload, selectin_polymorphic, undefer, noload | |
21 | 19 | from sqlalchemy.orm.exc import NoResultFound |
22 | 20 | from sqlalchemy import desc, or_, func |
23 | 21 | from werkzeug.datastructures import ImmutableMultiDict |
34 | 32 | PaginatedMixin, |
35 | 33 | ReadWriteWorkspacedView, |
36 | 34 | InvalidUsage, |
37 | CountMultiWorkspacedMixin, | |
38 | PatchableWorkspacedMixin | |
35 | CountMultiWorkspacedMixin | |
39 | 36 | ) |
40 | 37 | from faraday.server.fields import FaradayUploadedFile |
41 | 38 | from faraday.server.models import ( |
159 | 156 | dump_only=True) # This is only used for sorting |
160 | 157 | custom_fields = FaradayCustomField(table_name='vulnerability', attribute='custom_fields') |
161 | 158 | external_id = fields.String(allow_none=True) |
159 | attachments_count = fields.Integer(dump_only=True, attribute='attachments_count') | |
162 | 160 | |
163 | 161 | class Meta: |
164 | 162 | model = Vulnerability |
172 | 170 | 'service', 'obj_id', 'type', 'policyviolations', |
173 | 171 | '_attachments', |
174 | 172 | 'target', 'host_os', 'resolution', 'metadata', |
175 | 'custom_fields', 'external_id', 'tool', | |
173 | 'custom_fields', 'external_id', 'tool', 'attachments_count', | |
176 | 174 | 'cvss', 'cwe', 'cve', 'owasp', |
177 | 175 | ) |
178 | 176 | |
306 | 304 | 'service', 'obj_id', 'type', 'policyviolations', |
307 | 305 | 'request', '_attachments', 'params', |
308 | 306 | 'target', 'host_os', 'resolution', 'method', 'metadata', |
309 | 'status_code', 'custom_fields', 'external_id', 'tool', | |
307 | 'status_code', 'custom_fields', 'external_id', 'tool', 'attachments_count', | |
310 | 308 | 'cve', 'cwe', 'owasp', 'cvss', |
311 | 309 | ) |
312 | 310 | |
541 | 539 | db.session.delete(old_attachment) |
542 | 540 | for filename, attachment in attachments.items(): |
543 | 541 | faraday_file = FaradayUploadedFile(b64decode(attachment['data'])) |
542 | filename = filename.replace(" ", "_") | |
544 | 543 | get_or_create( |
545 | 544 | db.session, |
546 | 545 | File, |
573 | 572 | """ |
574 | 573 | query = super()._get_eagerloaded_query( |
575 | 574 | *args, **kwargs) |
576 | joinedloads = [ | |
575 | options = [ | |
577 | 576 | joinedload(Vulnerability.host) |
578 | 577 | .load_only(Host.id) # Only hostnames are needed |
579 | 578 | .joinedload(Host.hostnames), |
590 | 589 | undefer(VulnerabilityGeneric.creator_command_tool), |
591 | 590 | undefer(VulnerabilityGeneric.target_host_ip), |
592 | 591 | undefer(VulnerabilityGeneric.target_host_os), |
593 | joinedload(VulnerabilityGeneric.evidence), | |
594 | 592 | joinedload(VulnerabilityGeneric.tags), |
595 | 593 | ] |
594 | ||
595 | if flask.request.args.get('get_evidence'): | |
596 | options.append(joinedload(VulnerabilityGeneric.evidence)) | |
597 | else: | |
598 | options.append(noload(VulnerabilityGeneric.evidence)) | |
599 | ||
596 | 600 | return query.options(selectin_polymorphic( |
597 | 601 | VulnerabilityGeneric, |
598 | 602 | [Vulnerability, VulnerabilityWeb] |
599 | ), *joinedloads) | |
603 | ), *options) | |
600 | 604 | |
601 | 605 | def _filter_query(self, query): |
602 | 606 | query = super()._filter_query(query) |
618 | 622 | |
619 | 623 | def _get_schema_class(self): |
620 | 624 | assert self.schema_class_dict is not None, "You must define schema_class" |
621 | if request.method == 'POST': | |
625 | if request.method == 'POST' and request.json: | |
622 | 626 | requested_type = request.json.get('type', None) |
623 | 627 | if not requested_type: |
624 | 628 | raise InvalidUsage('Type is required.') |
680 | 684 | res['groups'] = [convert_group(group) for group in res['groups']] |
681 | 685 | return res |
682 | 686 | |
683 | @route('/<int:vuln_id>/attachment/', methods=['POST']) | |
687 | @route('/<int:vuln_id>/attachment', methods=['POST']) | |
684 | 688 | def post_attachment(self, workspace_name, vuln_id): |
685 | 689 | """ |
686 | 690 | --- |
696 | 700 | description: Ok |
697 | 701 | """ |
698 | 702 | |
699 | try: | |
700 | validate_csrf(request.form.get('csrf_token')) | |
701 | except wtforms.ValidationError: | |
702 | flask.abort(403) | |
703 | 703 | vuln_workspace_check = db.session.query(VulnerabilityGeneric, Workspace.id).join( |
704 | 704 | Workspace).filter(VulnerabilityGeneric.id == vuln_id, |
705 | 705 | Workspace.name == workspace_name).first() |
707 | 707 | if vuln_workspace_check: |
708 | 708 | if 'file' not in request.files: |
709 | 709 | flask.abort(400) |
710 | ||
711 | faraday_file = FaradayUploadedFile(request.files['file'].read()) | |
710 | vuln = VulnerabilitySchema().dump(vuln_workspace_check[0]) | |
712 | 711 | filename = request.files['file'].filename |
713 | ||
714 | get_or_create( | |
715 | db.session, | |
716 | File, | |
717 | object_id=vuln_id, | |
718 | object_type='vulnerability', | |
719 | name=filename, | |
720 | filename=filename, | |
721 | content=faraday_file | |
722 | ) | |
723 | db.session.commit() | |
724 | return flask.jsonify({'message': 'Evidence upload was successful'}) | |
712 | _attachments = vuln['_attachments'] | |
713 | if filename in _attachments: | |
714 | message = 'Evidence already exists in vuln' | |
715 | return make_response(flask.jsonify(message=message, success=False, code=400), 400) | |
716 | else: | |
717 | faraday_file = FaradayUploadedFile(request.files['file'].read()) | |
718 | instance, created = get_or_create( | |
719 | db.session, | |
720 | File, | |
721 | object_id=vuln_id, | |
722 | object_type='vulnerability', | |
723 | name=filename, | |
724 | filename=filename, | |
725 | content=faraday_file | |
726 | ) | |
727 | db.session.commit() | |
728 | message = 'Evidence upload was successful' | |
729 | return flask.jsonify({'message': message}) | |
725 | 730 | else: |
726 | 731 | flask.abort(404, "Vulnerability not found") |
727 | 732 | |
749 | 754 | 200: |
750 | 755 | description: Ok |
751 | 756 | """ |
752 | filters = request.args.get('q') | |
757 | filters = request.args.get('q', '{}') | |
753 | 758 | filtered_vulns, count = self._filter(filters, workspace_name) |
754 | 759 | |
755 | 760 | class PageMeta: |
883 | 888 | |
884 | 889 | return vulns_data, len(rows) |
885 | 890 | |
886 | @route('/<int:vuln_id>/attachment/<attachment_filename>/', methods=['GET']) | |
891 | @route('/<int:vuln_id>/attachment/<attachment_filename>', methods=['GET']) | |
887 | 892 | def get_attachment(self, workspace_name, vuln_id, attachment_filename): |
888 | 893 | """ |
889 | 894 | --- |
926 | 931 | else: |
927 | 932 | flask.abort(404, "Vulnerability not found") |
928 | 933 | |
929 | @route('/<int:vuln_id>/attachments/', methods=['GET']) | |
934 | @route('/<int:vuln_id>/attachment', methods=['GET']) | |
930 | 935 | def get_attachments_by_vuln(self, workspace_name, vuln_id): |
931 | 936 | """ |
932 | 937 | --- |
964 | 969 | else: |
965 | 970 | flask.abort(404, "Vulnerability not found") |
966 | 971 | |
967 | @route('/<int:vuln_id>/attachment/<attachment_filename>/', methods=['DELETE']) | |
972 | @route('/<int:vuln_id>/attachment/<attachment_filename>', methods=['DELETE']) | |
968 | 973 | def delete_attachment(self, workspace_name, vuln_id, attachment_filename): |
969 | 974 | """ |
970 | 975 | --- |
994 | 999 | else: |
995 | 1000 | flask.abort(404, "Vulnerability not found") |
996 | 1001 | |
997 | @route('export_csv/', methods=['GET']) | |
1002 | @route('export_csv', methods=['GET']) | |
998 | 1003 | def export_csv(self, workspace_name): |
999 | 1004 | """ |
1000 | 1005 | --- |
1073 | 1078 | response = {'deleted_vulns': deleted_vulns} |
1074 | 1079 | return flask.jsonify(response) |
1075 | 1080 | |
1076 | @route('top_users/', methods=['GET']) | |
1081 | @route('top_users', methods=['GET']) | |
1077 | 1082 | def top_users(self, workspace_name): |
1078 | 1083 | """ |
1079 | 1084 | --- |
1106 | 1111 | return flask.jsonify(response) |
1107 | 1112 | |
1108 | 1113 | |
1109 | class VulnerabilityV3View(VulnerabilityView, PatchableWorkspacedMixin): | |
1110 | route_prefix = '/v3/ws/<workspace_name>/' | |
1111 | trailing_slash = False | |
1112 | ||
1113 | @route('/<int:vuln_id>/attachment', methods=['POST']) | |
1114 | def post_attachment(self, workspace_name, vuln_id): | |
1115 | return super().post_attachment(workspace_name, vuln_id) | |
1116 | ||
1117 | @route('/<int:vuln_id>/attachment/<attachment_filename>', methods=['GET']) | |
1118 | def get_attachment(self, workspace_name, vuln_id, attachment_filename): | |
1119 | return super().get_attachment(workspace_name, vuln_id, attachment_filename) | |
1120 | ||
1121 | @route('/<int:vuln_id>/attachment', methods=['GET']) | |
1122 | def get_attachments_by_vuln(self, workspace_name, vuln_id): | |
1123 | return super().get_attachments_by_vuln(workspace_name, vuln_id) | |
1124 | ||
1125 | @route('/<int:vuln_id>/attachment/<attachment_filename>', methods=['DELETE']) | |
1126 | def delete_attachment(self, workspace_name, vuln_id, attachment_filename): | |
1127 | return super().delete_attachment(workspace_name, vuln_id, attachment_filename) | |
1128 | ||
1129 | @route('/export_csv', methods=['GET']) | |
1130 | def export_csv(self, workspace_name): | |
1131 | return super().export_csv(workspace_name) | |
1132 | ||
1133 | @route('/top_users', methods=['GET']) | |
1134 | def top_users(self, workspace_name): | |
1135 | return super().top_users(workspace_name) | |
1136 | ||
1137 | post_attachment.__doc__ = VulnerabilityView.post_attachment.__doc__ | |
1138 | get_attachment.__doc__ = VulnerabilityView.post_attachment.__doc__ | |
1139 | get_attachments_by_vuln.__doc__ = VulnerabilityView.post_attachment.__doc__ | |
1140 | delete_attachment.__doc__ = VulnerabilityView.post_attachment.__doc__ | |
1141 | export_csv.__doc__ = VulnerabilityView.post_attachment.__doc__ | |
1142 | top_users.__doc__ = VulnerabilityView.post_attachment.__doc__ | |
1143 | ||
1144 | ||
1145 | 1114 | VulnerabilityView.register(vulns_api) |
1146 | VulnerabilityV3View.register(vulns_api) |
25 | 25 | route_base = 'websocket_token' |
26 | 26 | schema_class = WebsocketWorkspaceAuthSchema |
27 | 27 | |
28 | @route('/', methods=['GET', 'POST']) | |
28 | @route('', methods=['GET', 'POST']) | |
29 | 29 | def get(self, workspace_name): |
30 | 30 | """ |
31 | 31 | --- |
41 | 41 | return {"token": token} |
42 | 42 | |
43 | 43 | |
44 | class WebsocketWorkspaceAuthV3View(WebsocketWorkspaceAuthView): | |
45 | route_prefix = "/v3/ws/<workspace_name>/" | |
46 | trailing_slash = False | |
47 | ||
48 | @route('', methods=['GET', 'POST']) | |
49 | def get(self, workspace_name): | |
50 | """ | |
51 | --- | |
52 | get: | |
53 | tags: ["Token"] | |
54 | responses: | |
55 | 200: | |
56 | description: Ok | |
57 | """ | |
58 | return super().get(workspace_name) | |
44 | WebsocketWorkspaceAuthView.register(websocket_auth_api) | |
59 | 45 | |
60 | 46 | |
61 | WebsocketWorkspaceAuthView.register(websocket_auth_api) | |
62 | WebsocketWorkspaceAuthV3View.register(websocket_auth_api) | |
63 | ||
64 | ||
65 | @websocket_auth_api.route('/v2/agent_websocket_token/', methods=['POST']) | |
47 | @websocket_auth_api.route('/v3/agent_websocket_token', methods=['POST']) | |
66 | 48 | def agent_websocket_token(): |
67 | 49 | """ |
68 | 50 | --- |
77 | 59 | return flask.jsonify({"token": generate_agent_websocket_token(agent)}) |
78 | 60 | |
79 | 61 | |
80 | @websocket_auth_api.route('/v3/agent_websocket_token', methods=['POST']) | |
81 | def agent_websocket_token_w3(): | |
82 | return agent_websocket_token() | |
83 | ||
84 | ||
85 | agent_websocket_token_w3.__doc__ = agent_websocket_token.__doc__ | |
86 | ||
87 | ||
88 | 62 | agent_websocket_token.is_public = True |
89 | agent_websocket_token_w3.is_public = True | |
90 | 63 | |
91 | 64 | |
92 | 65 | def generate_agent_websocket_token(agent): |
0 | 0 | # Faraday Penetration Test IDE |
1 | 1 | # Copyright (C) 2016 Infobyte LLC (http://www.infobytesec.com/) |
2 | 2 | # See the file 'doc/LICENSE' for the license information |
3 | import re | |
3 | 4 | from builtins import str |
4 | 5 | |
5 | 6 | import json |
8 | 9 | import flask |
9 | 10 | from flask import Blueprint, abort, make_response, jsonify |
10 | 11 | from flask_classful import route |
11 | from marshmallow import Schema, fields, post_load, validate, ValidationError | |
12 | from marshmallow import Schema, fields, post_load, ValidationError | |
12 | 13 | from sqlalchemy.orm import ( |
13 | 14 | with_expression |
14 | 15 | ) |
23 | 24 | PrimaryKeyRelatedField, |
24 | 25 | SelfNestedField, |
25 | 26 | ) |
26 | from faraday.server.api.base import ReadWriteView, AutoSchema, FilterMixin, PatchableMixin | |
27 | from faraday.server.api.base import ReadWriteView, AutoSchema, FilterMixin | |
27 | 28 | |
28 | 29 | logger = logging.getLogger(__name__) |
29 | 30 | |
62 | 63 | end_date = JSTimestampField(attribute='end_date') |
63 | 64 | |
64 | 65 | |
66 | def validate_workspace_name(name): | |
67 | blacklist = ["filter"] | |
68 | if name in blacklist: | |
69 | raise ValidationError(f"Not possible to create workspace of name: {name}") | |
70 | if not re.match(r"^[a-z0-9][a-z0-9_$()+-]*$", name): | |
71 | raise ValidationError("The workspace name must validate with the regex " | |
72 | "^[a-z0-9][a-z0-9_$()+-]*$") | |
73 | ||
74 | ||
65 | 75 | class WorkspaceSchema(AutoSchema): |
66 | 76 | |
67 | name = fields.String(required=True, | |
68 | validate=validate.Regexp(r"^[a-z0-9][a-z0-9\_\$\(\)\+\-]*$", 0, | |
69 | error="The workspace name must validate with the regex " | |
70 | "^[a-z0-9][a-z0-9\\_\\$\\(\\)\\+\\-\\/]*$")) | |
77 | name = fields.String(required=True, validate=validate_workspace_name) | |
71 | 78 | stats = SelfNestedField(WorkspaceSummarySchema()) |
72 | 79 | duration = SelfNestedField(WorkspaceDurationSchema()) |
73 | 80 | _id = fields.Integer(dump_only=True, attribute='id') |
336 | 343 | return self._get_object(workspace_id).readonly |
337 | 344 | |
338 | 345 | |
339 | class WorkspaceV3View(WorkspaceView, PatchableMixin): | |
340 | route_prefix = 'v3/' | |
341 | trailing_slash = False | |
342 | ||
343 | ||
344 | 346 | WorkspaceView.register(workspace_api) |
345 | WorkspaceV3View.register(workspace_api) |
1 | 1 | # Copyright (C) 2016 Infobyte LLC (http://www.infobytesec.com/) |
2 | 2 | # See the file 'doc/LICENSE' for the license information |
3 | 3 | import logging |
4 | import os | |
4 | 5 | import string |
5 | 6 | import datetime |
6 | 7 | |
12 | 13 | from itsdangerous import TimedJSONWebSignatureSerializer, SignatureExpired, BadSignature |
13 | 14 | from random import SystemRandom |
14 | 15 | |
16 | from faraday.settings import load_settings | |
15 | 17 | from faraday.server.config import LOCAL_CONFIG_FILE, copy_default_config_to_local |
16 | from faraday.server.models import User | |
18 | from faraday.server.models import User, Role | |
17 | 19 | from configparser import ConfigParser, NoSectionError, NoOptionError, DuplicateSectionError |
18 | 20 | |
19 | 21 | import flask |
98 | 100 | from faraday.server.api.modules.export_data import export_data_api # pylint:disable=import-outside-toplevel |
99 | 101 | # Custom reset password |
100 | 102 | from faraday.server.api.modules.auth import auth # pylint:disable=import-outside-toplevel |
103 | from faraday.server.api.modules.settings_reports import reports_settings_api # pylint:disable=import-outside-toplevel | |
104 | from faraday.server.api.modules.settings_dashboard import \ | |
105 | dashboard_settings_api # pylint:disable=import-outside-toplevel | |
101 | 106 | |
102 | 107 | app.register_blueprint(commandsrun_api) |
103 | 108 | app.register_blueprint(activityfeed_api) |
124 | 129 | app.register_blueprint(preferences_api) |
125 | 130 | app.register_blueprint(export_data_api) |
126 | 131 | app.register_blueprint(auth) |
132 | app.register_blueprint(reports_settings_api) | |
133 | app.register_blueprint(dashboard_settings_api) | |
127 | 134 | |
128 | 135 | |
129 | 136 | def check_testing_configuration(testing, app): |
256 | 263 | KVSessionExtension(app=app).cleanup_sessions(app) |
257 | 264 | |
258 | 265 | user_ip = request.headers.get('X-Forwarded-For', request.remote_addr) |
259 | user_logout_at = datetime.datetime.now() | |
266 | user_logout_at = datetime.datetime.utcnow() | |
260 | 267 | audit_logger.info(f"User [{user.username}] logged out from IP [{user_ip}] at [{user_logout_at}]") |
261 | 268 | |
262 | 269 | |
276 | 283 | KVSessionExtension(app=app).cleanup_sessions(app) |
277 | 284 | |
278 | 285 | user_ip = request.headers.get('X-Forwarded-For', request.remote_addr) |
279 | user_login_at = datetime.datetime.now() | |
286 | user_login_at = datetime.datetime.utcnow() | |
280 | 287 | audit_logger.info(f"User [{user.username}] logged in from IP [{user_ip}] at [{user_login_at}]") |
281 | 288 | |
282 | 289 | |
385 | 392 | DepotManager.configure('default', { |
386 | 393 | 'depot.storage_path': storage_path |
387 | 394 | }) |
388 | ||
395 | app.config['SQLALCHEMY_ECHO'] = 'FARADAY_LOG_QUERY' in os.environ | |
389 | 396 | check_testing_configuration(testing, app) |
390 | 397 | |
391 | 398 | try: |
407 | 414 | app.user_datastore = SQLAlchemyUserDatastore( |
408 | 415 | db, |
409 | 416 | user_model=User, |
410 | role_model=None) # We won't use flask security roles feature | |
417 | role_model=Role) | |
411 | 418 | |
412 | 419 | from faraday.server.api.modules.agent import agent_creation_api # pylint: disable=import-outside-toplevel |
413 | 420 | |
437 | 444 | register_handlers(app) |
438 | 445 | |
439 | 446 | app.view_functions['agent_creation_api.AgentCreationView:post'].is_public = True |
440 | app.view_functions['agent_creation_api.AgentCreationV3View:post'].is_public = True | |
441 | ||
447 | load_settings() | |
442 | 448 | return app |
443 | 449 | |
444 | 450 | |
462 | 468 | def validate(self): |
463 | 469 | |
464 | 470 | user_ip = request.headers.get('X-Forwarded-For', request.remote_addr) |
465 | time_now = datetime.datetime.now() | |
471 | time_now = datetime.datetime.utcnow() | |
466 | 472 | |
467 | 473 | # Use super of LoginForm, not super of CustomLoginForm, since I |
468 | 474 | # want to skip the LoginForm validate logic |
103 | 103 | print('User cancelled.') |
104 | 104 | sys.exit(1) |
105 | 105 | |
106 | def _create_roles(self, conn_string): | |
107 | engine = create_engine(conn_string) | |
108 | try: | |
109 | statement = text( | |
110 | "INSERT INTO faraday_role(name) VALUES ('admin'),('pentester'),('client'),('asset_owner');" | |
111 | ) | |
112 | connection = engine.connect() | |
113 | connection.execute(statement) | |
114 | except sqlalchemy.exc.IntegrityError as ex: | |
115 | if is_unique_constraint_violation(ex): | |
116 | # when re using database user could be created previously | |
117 | print( | |
118 | "{yellow}WARNING{white}: Faraday administrator user already exists.".format( | |
119 | yellow=Fore.YELLOW, white=Fore.WHITE)) | |
120 | else: | |
121 | print( | |
122 | "{yellow}WARNING{white}: Can't create administrator user.".format( | |
123 | yellow=Fore.YELLOW, white=Fore.WHITE)) | |
124 | raise | |
125 | ||
106 | 126 | def _create_admin_user(self, conn_string, choose_password, faraday_user_password): |
107 | 127 | engine = create_engine(conn_string) |
108 | 128 | # TODO change the random_password variable name, it is not always |
126 | 146 | INSERT INTO faraday_user ( |
127 | 147 | username, name, password, |
128 | 148 | is_ldap, active, last_login_ip, |
129 | current_login_ip, role, state_otp, fs_uniquifier | |
149 | current_login_ip, state_otp, fs_uniquifier | |
130 | 150 | ) VALUES ( |
131 | 151 | 'faraday', 'Administrator', :password, |
132 | 152 | false, true, '127.0.0.1', |
133 | '127.0.0.1', 'admin', 'disabled', :fs_uniquifier | |
153 | '127.0.0.1', 'disabled', :fs_uniquifier | |
134 | 154 | ) |
135 | 155 | """) |
136 | 156 | params = { |
139 | 159 | } |
140 | 160 | connection = engine.connect() |
141 | 161 | connection.execute(statement, **params) |
162 | result = connection.execute(text("""SELECT id, username FROM faraday_user""")) | |
163 | user_id = list(user_tuple[0] for user_tuple in result if user_tuple[1] == "faraday")[0] | |
164 | result = connection.execute(text("""SELECT id, name FROM faraday_role""")) | |
165 | role_id = list(role_tuple[0] for role_tuple in result if role_tuple[1] == "admin")[0] | |
166 | params = { | |
167 | "user_id": user_id, | |
168 | "role_id": role_id | |
169 | } | |
170 | connection.execute(text("INSERT INTO roles_users(user_id, role_id) VALUES (:user_id, :role_id)"), **params) | |
142 | 171 | except sqlalchemy.exc.IntegrityError as ex: |
143 | 172 | if is_unique_constraint_violation(ex): |
144 | 173 | # when re using database user could be created previously |
232 | 261 | connection.commit() |
233 | 262 | connection.close() |
234 | 263 | except psycopg2.Error as e: |
235 | if 'authentication failed' in e.message: | |
264 | if 'authentication failed' in str(e): | |
236 | 265 | print('{red}ERROR{white}: User {username} already ' |
237 | 266 | 'exists'.format(white=Fore.WHITE, |
238 | 267 | red=Fore.RED, |
325 | 354 | os.chdir(FARADAY_BASE) |
326 | 355 | command.stamp(alembic_cfg, "head") |
327 | 356 | # TODO ADD RETURN TO PREV DIR |
357 | self._create_roles(conn_string) |
0 | import sys | |
1 | import click | |
2 | ||
3 | from faraday.server.web import get_app | |
4 | from faraday.server.models import ( | |
5 | db, | |
6 | Configuration | |
7 | ) | |
8 | from faraday.server.utils.database import get_or_create | |
9 | from faraday.settings import get_settings, get_all_settings, load_settings | |
10 | from faraday.settings.exceptions import InvalidConfigurationError | |
11 | ||
12 | ||
13 | def manage(action, name): | |
14 | load_settings() | |
15 | if name: | |
16 | name = name.lower() | |
17 | available_settings = get_all_settings() | |
18 | if action in ('show', 'update'): | |
19 | if not name: | |
20 | click.secho(f"You must indicate a settings name to {action}", fg="red") | |
21 | sys.exit(1) | |
22 | if name not in available_settings: | |
23 | click.secho(f'Invalid settings: {name}', fg='red') | |
24 | else: | |
25 | settings = get_settings(name) | |
26 | if action == "show": | |
27 | click.secho(f"Settings for: {name}", fg="green") | |
28 | for key, value in settings.value.items(): | |
29 | click.secho(f"{key}: {value}") | |
30 | elif action == "update": | |
31 | new_settings = {} | |
32 | click.secho(f"Update settings for: {name}", fg="green") | |
33 | for key, value in settings.value.items(): | |
34 | new_value = click.prompt(f'{key}', default=value) | |
35 | new_settings[key] = new_value | |
36 | try: | |
37 | settings.validate_configuration(new_settings) | |
38 | except InvalidConfigurationError as e: | |
39 | click.secho(f"Invalid configuration for: {name}", fg="red") | |
40 | click.secho(e, fg="red") | |
41 | sys.exit(1) | |
42 | else: | |
43 | settings_message = "\n".join([f"{key}: {value}" for key, value in new_settings.items()]) | |
44 | if click.confirm(f"Do you confirm your changes on {name}?" | |
45 | f"\n----------------------" | |
46 | f"\n{settings_message}\n", default=True): | |
47 | with get_app().app_context(): | |
48 | saved_config, created = get_or_create(db.session, Configuration, key=settings.settings_key) | |
49 | if created: | |
50 | saved_config.value = settings.update_configuration(new_settings) | |
51 | else: | |
52 | # SQLAlchemy doesn't detect in-place mutations to the structure of a JSON type. | |
53 | # Thus, we make a deepcopy of the JSON so SQLAlchemy can detect the changes. | |
54 | saved_config.value = settings.update_configuration(new_settings, saved_config.value) | |
55 | db.session.commit() | |
56 | click.secho("Updated!!", fg='green') | |
57 | else: | |
58 | click.secho("No changes where made to the settings", fg="green") | |
59 | else: | |
60 | click.secho("Available settings:", fg="green") | |
61 | for i in available_settings: | |
62 | click.secho(i) |
0 | """ | |
1 | Faraday Penetration Test IDE | |
2 | Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/) | |
3 | See the file 'doc/LICENSE' for the license information | |
4 | ||
5 | """ | |
6 | import socket | |
7 | ||
8 | import sqlalchemy | |
9 | from colorama import init | |
10 | from colorama import Fore | |
11 | ||
12 | import faraday.server.config | |
13 | from faraday.server.web import get_app | |
14 | from faraday.server.models import db | |
15 | from faraday.server.config import CONST_FARADAY_HOME_PATH | |
16 | from faraday.server.utils.daemonize import is_server_running | |
17 | import faraday_plugins | |
18 | ||
19 | init() | |
20 | ||
21 | ||
22 | def check_server_running(): | |
23 | port = int(faraday.server.config.faraday_server.port) | |
24 | pid = is_server_running(port) | |
25 | return pid | |
26 | ||
27 | ||
28 | def check_open_ports(): | |
29 | address = faraday.server.config.faraday_server.bind_address | |
30 | port = int(faraday.server.config.faraday_server.port) | |
31 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | |
32 | result = sock.connect_ex((address, port)) | |
33 | if result == 0: | |
34 | return True | |
35 | else: | |
36 | return False | |
37 | ||
38 | ||
39 | def check_postgres(): | |
40 | with get_app().app_context(): | |
41 | try: | |
42 | result = ( | |
43 | db.session.query("version()").one(), db.session.query("current_setting('server_version_num')").one()) | |
44 | return result | |
45 | except sqlalchemy.exc.OperationalError: | |
46 | return False | |
47 | except sqlalchemy.exc.ArgumentError: | |
48 | return None | |
49 | ||
50 | ||
51 | def check_locks_postgresql(): | |
52 | with get_app().app_context(): | |
53 | psql_status = check_postgres() | |
54 | if psql_status: | |
55 | result = db.engine.execute("""SELECT blocked_locks.pid AS blocked_pid, | |
56 | blocked_activity.usename AS blocked_user, | |
57 | blocking_locks.pid AS blocking_pid, | |
58 | blocking_activity.usename AS blocking_user, | |
59 | blocked_activity.query AS blocked_statement, | |
60 | blocking_activity.query AS current_statement_in_blocking_process | |
61 | FROM pg_catalog.pg_locks blocked_locks | |
62 | JOIN pg_catalog.pg_stat_activity blocked_activity ON blocked_activity.pid = blocked_locks.pid | |
63 | JOIN pg_catalog.pg_locks blocking_locks | |
64 | ON blocking_locks.locktype = blocked_locks.locktype | |
65 | AND blocking_locks.DATABASE IS NOT DISTINCT FROM blocked_locks.DATABASE | |
66 | AND blocking_locks.relation IS NOT DISTINCT FROM blocked_locks.relation | |
67 | AND blocking_locks.page IS NOT DISTINCT FROM blocked_locks.page | |
68 | AND blocking_locks.tuple IS NOT DISTINCT FROM blocked_locks.tuple | |
69 | AND blocking_locks.virtualxid IS NOT DISTINCT FROM blocked_locks.virtualxid | |
70 | AND blocking_locks.transactionid IS NOT DISTINCT FROM blocked_locks.transactionid | |
71 | AND blocking_locks.classid IS NOT DISTINCT FROM blocked_locks.classid | |
72 | AND blocking_locks.objid IS NOT DISTINCT FROM blocked_locks.objid | |
73 | AND blocking_locks.objsubid IS NOT DISTINCT FROM blocked_locks.objsubid | |
74 | AND blocking_locks.pid != blocked_locks.pid | |
75 | JOIN pg_catalog.pg_stat_activity blocking_activity ON blocking_activity.pid = blocking_locks.pid | |
76 | WHERE NOT blocked_locks.GRANTED;""") | |
77 | fetch = result.fetchall() | |
78 | if fetch: | |
79 | return True | |
80 | else: | |
81 | return False | |
82 | ||
83 | else: | |
84 | return None | |
85 | ||
86 | ||
87 | def check_postgresql_encoding(): | |
88 | with get_app().app_context(): | |
89 | psql_status = check_postgres() | |
90 | if psql_status: | |
91 | encoding = db.engine.execute("SHOW SERVER_ENCODING").first()[0] | |
92 | return encoding | |
93 | else: | |
94 | return None | |
95 | ||
96 | ||
97 | def check_storage_permission(): | |
98 | path = CONST_FARADAY_HOME_PATH / 'storage' / 'test' | |
99 | ||
100 | try: | |
101 | path.mkdir() | |
102 | path.rmdir() | |
103 | return True | |
104 | except OSError: | |
105 | return None | |
106 | ||
107 | ||
108 | def print_config_info(): | |
109 | print(f'\n{Fore.WHITE}Showing faraday server configuration') | |
110 | print(f"{Fore.BLUE} version: {Fore.WHITE}{faraday.__version__}") | |
111 | ||
112 | data_keys = ['bind_address', 'port', 'websocket_port', 'debug'] | |
113 | for key in data_keys: | |
114 | print('{blue} {KEY}: {white}{VALUE}'. | |
115 | format(KEY=key, VALUE=getattr(faraday.server.config.faraday_server, key), white=Fore.WHITE, | |
116 | blue=Fore.BLUE)) | |
117 | ||
118 | print(f'\n{Fore.WHITE}Showing faraday plugins data') | |
119 | print(f"{Fore.BLUE} version: {Fore.WHITE}{faraday_plugins.__version__}") | |
120 | ||
121 | print(f'\n{Fore.WHITE}Showing dashboard configuration') | |
122 | data_keys = ['show_vulns_by_price'] | |
123 | for key in data_keys: | |
124 | print('{blue} {KEY}: {white}{VALUE}'. | |
125 | format(KEY=key, VALUE=getattr(faraday.server.config.dashboard, key), white=Fore.WHITE, blue=Fore.BLUE)) | |
126 | ||
127 | print(f'\n{Fore.WHITE}Showing storage configuration') | |
128 | data_keys = ['path'] | |
129 | for key in data_keys: | |
130 | print('{blue} {KEY}: {white}{VALUE}'. | |
131 | format(KEY=key, VALUE=getattr(faraday.server.config.storage, key), white=Fore.WHITE, blue=Fore.BLUE)) | |
132 | ||
133 | ||
134 | def print_postgresql_status(): | |
135 | """Prints the status of PostgreSQL using check_postgres()""" | |
136 | exit_code = 0 | |
137 | result = check_postgres() | |
138 | ||
139 | if not result: | |
140 | print('[{red}-{white}] Could not connect to PostgreSQL, please check if database is running' | |
141 | .format(red=Fore.RED, white=Fore.WHITE)) | |
142 | exit_code = 1 | |
143 | return exit_code | |
144 | elif result is None: | |
145 | print('[{red}-{white}] Database not initialized. Execute: faraday-manage initdb' | |
146 | .format(red=Fore.RED, white=Fore.WHITE)) | |
147 | exit_code = 1 | |
148 | return exit_code | |
149 | elif int(result[1][0]) < 90400: | |
150 | print('[{red}-{white}] PostgreSQL is running, but needs to be 9.4 or newer, please update PostgreSQL' | |
151 | .format(red=Fore.RED, white=Fore.WHITE)) | |
152 | elif result: | |
153 | print(f'[{Fore.GREEN}+{Fore.WHITE}] PostgreSQL is running and up to date') | |
154 | return exit_code | |
155 | ||
156 | ||
157 | def print_postgresql_other_status(): | |
158 | """Prints the status of locks in Postgresql using check_locks_postgresql() and | |
159 | prints Postgresql encoding using check_postgresql_encoding()""" | |
160 | ||
161 | lock_status = check_locks_postgresql() | |
162 | if lock_status: | |
163 | print(f'[{Fore.YELLOW}-{Fore.WHITE}] Warning: PostgreSQL lock detected.') | |
164 | elif not lock_status: | |
165 | print(f'[{Fore.GREEN}+{Fore.WHITE}] PostgreSQL lock not detected. ') | |
166 | elif lock_status is None: | |
167 | pass | |
168 | ||
169 | encoding = check_postgresql_encoding() | |
170 | if encoding: | |
171 | print(f'[{Fore.GREEN}+{Fore.WHITE}] PostgreSQL encoding: {encoding}') | |
172 | elif encoding is None: | |
173 | pass | |
174 | ||
175 | ||
176 | def print_faraday_status(): | |
177 | """Prints Status of farday using check_server_running() """ | |
178 | ||
179 | # Prints Status of the server using check_server_running() | |
180 | pid = check_server_running() | |
181 | if pid is not None: | |
182 | print('[{green}+{white}] Faraday Server is running. PID:{PID} \ | |
183 | '.format(green=Fore.GREEN, PID=pid, white=Fore.WHITE)) | |
184 | else: | |
185 | print('[{red}-{white}] Faraday Server is not running {white} \ | |
186 | '.format(red=Fore.RED, white=Fore.WHITE)) | |
187 | ||
188 | ||
189 | def print_config_status(): | |
190 | """Prints Status of the configuration using check_credentials(), check_storage_permission() and check_open_ports()""" | |
191 | ||
192 | check_server_running() | |
193 | check_postgres() | |
194 | ||
195 | if check_storage_permission(): | |
196 | print(f'[{Fore.GREEN}+{Fore.WHITE}] /.faraday/storage -> Permission accepted') | |
197 | else: | |
198 | print(f'[{Fore.RED}-{Fore.WHITE}] /.faraday/storage -> Permission denied') | |
199 | ||
200 | if check_open_ports(): | |
201 | print("[{green}+{white}] Port {PORT} in {ad} is open" | |
202 | .format(PORT=faraday.server.config.faraday_server.port, | |
203 | green=Fore.GREEN, white=Fore.WHITE, ad=faraday.server.config.faraday_server.bind_address)) | |
204 | else: | |
205 | print("[{red}-{white}] Port {PORT} in {ad} is not open" | |
206 | .format(PORT=faraday.server.config.faraday_server.port, | |
207 | red=Fore.RED, white=Fore.WHITE, ad=faraday.server.config.faraday_server.bind_address)) | |
208 | ||
209 | ||
210 | def full_status_check(): | |
211 | print_config_info() | |
212 | ||
213 | print(f'\n{Fore.WHITE}Checking if postgreSQL is running...') | |
214 | print_postgresql_status() | |
215 | print_postgresql_other_status() | |
216 | ||
217 | print(f'\n{Fore.WHITE}Checking if Faraday is running...') | |
218 | print_faraday_status() | |
219 | ||
220 | print('\n{white}Checking Faraday config...{white}'.format(white=Fore.WHITE)) | |
221 | print_config_status() |
0 | import sys | |
1 | import shutil | |
2 | import tempfile | |
3 | from pathlib import Path | |
4 | ||
5 | from tqdm import tqdm | |
6 | from colorama import init | |
7 | from colorama import Fore, Style | |
8 | ||
9 | import distro | |
10 | ||
11 | from faraday.server.config import CONST_FARADAY_HOME_PATH | |
12 | ||
13 | from faraday.server.commands import status_check | |
14 | ||
15 | init() | |
16 | ||
17 | ||
18 | def init_config(): | |
19 | # Creates the directory where all the info will go to | |
20 | return Path(tempfile.mkdtemp()) | |
21 | ||
22 | ||
23 | def get_status_check(path: Path): | |
24 | # Executes status check from with-in the code and uses stdout to save | |
25 | # info to file | |
26 | # stdout was the only way to get this information without doing a big | |
27 | # refactor | |
28 | original_stdout = sys.stdout | |
29 | ||
30 | sys.stdout = (path / 'status_check.txt').open('wt') | |
31 | status_check.full_status_check() | |
32 | ||
33 | sys.stdout.close() | |
34 | sys.stdout = original_stdout | |
35 | ||
36 | ||
37 | def get_logs(path: Path): | |
38 | # Copies the logs using the logs path saved on constants | |
39 | orig_path = CONST_FARADAY_HOME_PATH / 'logs' | |
40 | dst_path = path / 'logs' | |
41 | shutil.copytree(str(orig_path), str(dst_path), # I would do this in other | |
42 | ignore=shutil.ignore_patterns('access*.*')) # way. by Eric | |
43 | ||
44 | ||
45 | def make_zip(path: Path): | |
46 | # Makes a zip file of the new folder with all the information obtained | |
47 | # inside | |
48 | shutil.make_archive('faraday_support', 'zip', str(path)) | |
49 | ||
50 | ||
51 | def end_config(path: Path): | |
52 | # Deletes recursively the directory created on the init_config | |
53 | shutil.rmtree(path) | |
54 | ||
55 | ||
56 | def revise_os(path: Path): | |
57 | with (path / 'os_distro.txt').open('wt') as os_file: | |
58 | os_file.write(f"{distro.linux_distribution()}") | |
59 | ||
60 | ||
61 | def all_for_support(): | |
62 | with tqdm(total=6) as pbar: | |
63 | path = init_config() | |
64 | get_status_check(path) | |
65 | pbar.update(1) | |
66 | get_logs(path) | |
67 | pbar.update(1) | |
68 | pbar.update(1) | |
69 | revise_os(path) | |
70 | pbar.update(1) | |
71 | make_zip(path) | |
72 | pbar.update(1) | |
73 | end_config(path) | |
74 | pbar.update(1) | |
75 | ||
76 | print('[{green}+{white}] Process Completed. A {bright}faraday_support.zip{normal} was generated' | |
77 | .format(green=Fore.GREEN, white=Fore.WHITE, bright=Style.BRIGHT, normal=Style.NORMAL)) |
14 | 14 | ) |
15 | 15 | from pathlib import Path |
16 | 16 | |
17 | from faraday import __license_version__ as license_version | |
18 | 17 | |
19 | 18 | CONST_FARADAY_HOME_PATH = Path( |
20 | 19 | os.getenv('FARADAY_HOME', Path('~/').expanduser()) |
37 | 36 | LOCAL_REPORTS_FOLDER = CONST_FARADAY_HOME_PATH / 'uploaded_reports' |
38 | 37 | |
39 | 38 | CONFIG_FILES = [DEFAULT_CONFIG_FILE, LOCAL_CONFIG_FILE] |
40 | CONST_LICENSES_DB = 'faraday_licenses' | |
41 | CONST_VULN_MODEL_DB = 'cwe' | |
42 | 39 | |
43 | 40 | logger = logging.getLogger(__name__) |
44 | 41 | |
63 | 60 | |
64 | 61 | # Copy default config file into faraday local config |
65 | 62 | shutil.copyfile(DEFAULT_CONFIG_FILE, LOCAL_CONFIG_FILE) |
66 | ||
67 | 63 | logger.info(f"Local faraday-server configuration created at {LOCAL_CONFIG_FILE}") |
68 | 64 | |
69 | 65 | |
91 | 87 | self.__setattr__(att, True) |
92 | 88 | else: |
93 | 89 | self.__setattr__(att, False) |
90 | elif isinstance(self.__dict__[att], int): | |
91 | if value: | |
92 | self.__setattr__(att, int(value)) | |
94 | 93 | else: |
95 | 94 | if value: |
96 | 95 | self.__setattr__(att, value) |
96 | ||
97 | def set(self, option_name, value): | |
98 | return self.__setattr__(option_name, value) | |
97 | 99 | |
98 | 100 | @staticmethod |
99 | 101 | def parse_section(section_name, __parser): |
100 | 102 | section = None |
101 | 103 | if section_name == 'database': |
102 | 104 | section = database |
103 | elif section_name == 'dashboard': | |
104 | section = dashboard | |
105 | 105 | elif section_name == 'faraday_server': |
106 | 106 | section = faraday_server |
107 | elif section_name == 'ldap': | |
108 | section = ldap | |
109 | 107 | elif section_name == 'storage': |
110 | 108 | section = storage |
111 | 109 | elif section_name == 'logger': |
112 | 110 | section = logger_config |
113 | 111 | elif section_name == 'limiter': |
114 | 112 | section = limiter_config |
115 | elif section_name == 'smtp': | |
116 | section = smtp | |
117 | 113 | else: |
118 | 114 | return |
119 | 115 | section.parse(__parser) |
124 | 120 | self.connection_string = None |
125 | 121 | |
126 | 122 | |
127 | class DashboardConfigObject(ConfigSection): | |
128 | def __init__(self): | |
129 | self.show_vulns_by_price = False | |
130 | ||
131 | ||
132 | 123 | class LimiterConfigObject(ConfigSection): |
133 | 124 | def __init__(self): |
134 | 125 | self.enabled = False |
137 | 128 | |
138 | 129 | class FaradayServerConfigObject(ConfigSection): |
139 | 130 | def __init__(self): |
140 | self.bind_address = None | |
141 | self.port = None | |
131 | self.bind_address = "127.0.0.1" | |
132 | self.port = 5985 | |
142 | 133 | self.secret_key = None |
143 | self.websocket_port = None | |
134 | self.websocket_port = 9000 | |
144 | 135 | self.session_timeout = 12 |
145 | 136 | self.api_token_expiration = 43200 # Default as 12 hs |
146 | 137 | self.agent_registration_secret = None |
148 | 139 | self.debug = False |
149 | 140 | self.custom_plugins_folder = None |
150 | 141 | self.ignore_info_severity = False |
151 | ||
152 | ||
153 | class LDAPConfigObject(ConfigSection): | |
154 | def __init__(self): | |
155 | self.admin_group = None | |
156 | self.client_group = None | |
157 | self.disconnect_timeout = None | |
158 | self.domain_dn = None | |
159 | self.enabled = None | |
160 | self.pentester_group = None | |
161 | self.port = None | |
162 | self.server = None | |
163 | self.use_ldaps = None | |
164 | self.use_start_tls = None | |
165 | ||
166 | ||
167 | class SmtpConfigObject(ConfigSection): | |
168 | def __init__(self): | |
169 | self.username = None | |
170 | self.password = None | |
171 | self.host = None | |
172 | self.port = None | |
173 | self.sender = None | |
174 | self.ssl = False | |
175 | self.certfile = None | |
176 | self.keyfile = None | |
177 | self.enabled = False | |
178 | ||
179 | def is_enabled(self): | |
180 | return self.enabled is True | |
181 | 142 | |
182 | 143 | |
183 | 144 | class StorageConfigObject(ConfigSection): |
191 | 152 | |
192 | 153 | |
193 | 154 | database = DatabaseConfigObject() |
194 | dashboard = DashboardConfigObject() | |
195 | 155 | faraday_server = FaradayServerConfigObject() |
196 | ldap = LDAPConfigObject() | |
197 | 156 | storage = StorageConfigObject() |
198 | 157 | logger_config = LoggerConfig() |
199 | smtp = SmtpConfigObject() | |
200 | 158 | limiter_config = LimiterConfigObject() |
201 | ||
202 | 159 | parse_and_bind_configuration() |
203 | ||
204 | ||
205 | def gen_web_config(): | |
206 | # Warning: This is publicly accesible via the API, it doesn't even need an | |
207 | # authenticated user. Don't add sensitive information here. | |
208 | doc = { | |
209 | 'ver': license_version, | |
210 | 'lic_db': CONST_LICENSES_DB, | |
211 | 'vuln_model_db': CONST_VULN_MODEL_DB, | |
212 | 'show_vulns_by_price': dashboard.show_vulns_by_price, | |
213 | 'websocket_port': faraday_server.websocket_port, | |
214 | } | |
215 | return doc |
6 | 6 | api_token_expiration=43200 |
7 | 7 | ;custom_plugins_folder=/path/to/custom/plugins |
8 | 8 | |
9 | [smtp] | |
10 | ;username=username | |
11 | ;password=password | |
12 | ;host=localhost | |
13 | ;port=25 | |
14 | ;[email protected] | |
15 | ;ssl=false | |
16 | ;certfile=/path/to/custom/certfile | |
17 | ;keyfile=/path/to/custom/keyfile | |
18 | ||
19 | [dashboard] | |
20 | show_vulns_by_price = false | |
21 | ||
22 | 9 | [logger] |
23 | 10 | use_rfc5424_formatter = false |
47 | 47 | |
48 | 48 | from faraday.server.fields import JSONType |
49 | 49 | from flask_security import ( |
50 | UserMixin, | |
50 | UserMixin, RoleMixin, | |
51 | 51 | ) |
52 | 52 | |
53 | 53 | from faraday.server.fields import FaradayUploadedFile |
1212 | 1212 | object_type='vulnerability' |
1213 | 1213 | ) |
1214 | 1214 | |
1215 | @property | |
1216 | def attachments_count(self): | |
1217 | return db.session.query(func.count(File.id)).filter_by( | |
1218 | object_id=self.id, | |
1219 | object_type='vulnerability' | |
1220 | ).scalar() | |
1221 | ||
1215 | 1222 | @hybrid_property |
1216 | 1223 | def target(self): |
1217 | 1224 | return self.target_host_ip |
1754 | 1761 | return db.session.query(Workspace).filter_by(name=workspace_name).first() |
1755 | 1762 | |
1756 | 1763 | |
1764 | roles_users = db.Table('roles_users', | |
1765 | db.Column('user_id', db.Integer(), db.ForeignKey('faraday_user.id')), | |
1766 | db.Column('role_id', db.Integer(), db.ForeignKey('faraday_role.id'))) | |
1767 | ||
1768 | ||
1769 | class Role(db.Model, RoleMixin): | |
1770 | __tablename__ = 'faraday_role' | |
1771 | id = db.Column(db.Integer(), primary_key=True) | |
1772 | name = db.Column(db.String(80), unique=True) | |
1773 | ||
1774 | ||
1757 | 1775 | class User(db.Model, UserMixin): |
1758 | 1776 | __tablename__ = 'faraday_user' |
1759 | 1777 | ROLES = ['admin', 'pentester', 'client', 'asset_owner'] |
1772 | 1790 | login_count = Column(Integer) # flask-security |
1773 | 1791 | active = Column(Boolean(), default=True, nullable=False) # TBI flask-security |
1774 | 1792 | confirmed_at = Column(DateTime()) |
1775 | role = Column(Enum(*ROLES, name='user_roles'), | |
1776 | nullable=False, default='client') | |
1777 | 1793 | _otp_secret = Column( |
1778 | 1794 | String(32), |
1779 | 1795 | name="otp_secret", nullable=True |
1782 | 1798 | preferences = Column(JSONType, nullable=True, default={}) |
1783 | 1799 | fs_uniquifier = Column(String(64), unique=True, nullable=False) # flask-security |
1784 | 1800 | |
1801 | roles = db.relationship('Role', secondary=roles_users, | |
1802 | backref='users') | |
1785 | 1803 | # TODO: add many to many relationship to add permission to workspace |
1804 | ||
1805 | @property | |
1806 | def roles_list(self): | |
1807 | return [role.name for role in self.roles] | |
1786 | 1808 | |
1787 | 1809 | workspace_permission_instances = relationship( |
1788 | 1810 | "WorkspacePermission", |
1789 | 1811 | cascade="all, delete-orphan") |
1790 | ||
1791 | def __init__(self, *args, **kwargs): | |
1792 | # added for compatibility with flask security | |
1793 | try: | |
1794 | kwargs.pop('roles') | |
1795 | except KeyError: | |
1796 | pass | |
1797 | super().__init__(*args, **kwargs) | |
1798 | 1812 | |
1799 | 1813 | def __repr__(self): |
1800 | 1814 | return f"<{'LDAP ' if self.is_ldap else ''}User: {self.username}>" |
1804 | 1818 | "username": self.username, |
1805 | 1819 | "name": self.username, |
1806 | 1820 | "email": self.email, |
1807 | "role": self.role, | |
1808 | "roles": [self.role], | |
1821 | "roles": self.roles_list, | |
1809 | 1822 | } |
1810 | 1823 | |
1811 | 1824 | |
2062 | 2075 | confirmed = Column(Boolean, nullable=False, default=False) |
2063 | 2076 | vuln_count = Column(Integer, default=0) # saves the amount of vulns when the report was generated. |
2064 | 2077 | markdown = Column(Boolean, default=False, nullable=False) |
2078 | duplicate_detection = Column(Boolean, default=False, nullable=False) | |
2079 | border_size = Column(Integer, default=3, nullable=True) | |
2065 | 2080 | advanced_filter = Column(Boolean, default=False, nullable=False) |
2066 | 2081 | advanced_filter_parsed = Column(String, nullable=False, default="") |
2067 | 2082 |
14 | 14 | self.__event = threading.Event() |
15 | 15 | |
16 | 16 | def run(self): |
17 | logger.info("Ping Home Thread [Start]") | |
17 | 18 | while not self.__event.is_set(): |
18 | 19 | try: |
19 | 20 | res = requests.get(HOME_URL, params={'version': faraday.__version__, 'key': 'white'}, |
26 | 27 | logger.exception(ex) |
27 | 28 | logger.warning("Can't connect to portal...") |
28 | 29 | self.__event.wait(RUN_INTERVAL) |
30 | else: | |
31 | logger.info("Ping Home Thread [Stop]") | |
29 | 32 | |
30 | 33 | def stop(self): |
34 | logger.info("Ping Home Thread [Stopping...]") | |
31 | 35 | self.__event.set() |
6 | 6 | |
7 | 7 | from faraday_plugins.plugins.manager import PluginsManager |
8 | 8 | from faraday.server.api.modules.bulk_create import bulk_create, BulkCreateSchema |
9 | from faraday.server import config | |
10 | 9 | |
11 | 10 | from faraday.server.models import Workspace, Command, User |
12 | 11 | from faraday.server.utils.bulk_create import add_creator |
13 | ||
12 | from faraday.settings.reports import ReportsSettings | |
14 | 13 | logger = logging.getLogger(__name__) |
15 | 14 | |
16 | 15 | |
17 | 16 | REPORTS_QUEUE = Queue() |
18 | 17 | |
18 | INTERVAL = 0.5 | |
19 | ||
19 | 20 | |
20 | 21 | class ReportsManager(Thread): |
21 | 22 | |
22 | 23 | def __init__(self, upload_reports_queue, *args, **kwargs): |
23 | super().__init__(*args, **kwargs) | |
24 | super().__init__(name="ReportsManager-Thread", daemon=True, *args, **kwargs) | |
24 | 25 | self.upload_reports_queue = upload_reports_queue |
25 | self.plugins_manager = PluginsManager(config.faraday_server.custom_plugins_folder, | |
26 | ignore_info=config.faraday_server.ignore_info_severity) | |
26 | self.plugins_manager = PluginsManager(ReportsSettings.settings.custom_plugins_folder, | |
27 | ignore_info=ReportsSettings.settings.ignore_info_severity) | |
28 | logger.info(f"Reports Manager: [Custom plugins folder: [{ReportsSettings.settings.custom_plugins_folder}]" | |
29 | f"[Ignore info severity: {ReportsSettings.settings.ignore_info_severity}]") | |
27 | 30 | self.__event = threading.Event() |
28 | 31 | |
29 | 32 | def stop(self): |
30 | logger.debug("Stop Reports Manager") | |
33 | logger.info("Reports Manager Thread [Stopping...]") | |
31 | 34 | self.__event.set() |
32 | 35 | |
33 | 36 | def send_report_request(self, |
76 | 79 | logger.info(f"No plugin detected for report [{file_path}]") |
77 | 80 | |
78 | 81 | def run(self): |
79 | logger.debug("Start Reports Manager") | |
82 | logger.info("Reports Manager Thread [Start]") | |
83 | ||
80 | 84 | while not self.__event.is_set(): |
81 | 85 | try: |
82 | 86 | tpl: Tuple[str, int, Path, int, int] = \ |
97 | 101 | logger.warning(f"Report file [{file_path}] don't exists", |
98 | 102 | file_path) |
99 | 103 | except Empty: |
100 | self.__event.wait(0.1) | |
104 | self.__event.wait(INTERVAL) | |
101 | 105 | except KeyboardInterrupt: |
102 | 106 | logger.info("Keyboard interrupt, stopping report processing thread") |
103 | 107 | self.stop() |
104 | 108 | except Exception as ex: |
105 | 109 | logger.exception(ex) |
106 | 110 | continue |
111 | else: | |
112 | logger.info("Reports Manager Thread [Stop]") |
317 | 317 | return True |
318 | 318 | assert isinstance(exception.orig.pgcode, str) |
319 | 319 | return exception.orig.pgcode == UNIQUE_VIOLATION |
320 | ||
321 | ||
322 | NOT_NULL_VIOLATION = '23502' | |
323 | ||
324 | ||
325 | def not_null_constraint_violation(exception): | |
326 | from faraday.server.models import db # pylint:disable=import-outside-toplevel | |
327 | if db.engine.dialect.name != 'postgresql': | |
328 | # Not implemened for RDMS other than postgres, we can live without | |
329 | # this since it is just an extra check | |
330 | return True | |
331 | assert isinstance(exception.orig.pgcode, str) | |
332 | return exception.orig.pgcode == NOT_NULL_VIOLATION |
0 | 0 | # Faraday Penetration Test IDE |
1 | 1 | # Copyright (C) 2016 Infobyte LLC (http://www.infobytesec.com/) |
2 | 2 | # See the file 'doc/LICENSE' for the license information |
3 | import multiprocessing | |
3 | 4 | import sys |
4 | 5 | import logging |
5 | from signal import SIGABRT, SIGILL, SIGINT, SIGSEGV, SIGTERM, SIG_DFL, signal | |
6 | from signal import SIGABRT, SIGILL, SIGINT, SIGSEGV, SIGTERM, SIG_DFL, SIGUSR1, signal | |
6 | 7 | |
7 | 8 | import twisted.web |
8 | 9 | from twisted.web.resource import Resource, ForbiddenResource |
9 | 10 | |
10 | from twisted.internet import ssl, reactor, error | |
11 | from twisted.internet import reactor, error | |
11 | 12 | from twisted.web.static import File |
12 | 13 | from twisted.web.util import Redirect |
13 | 14 | from twisted.web.http import proxiedLogFormatter |
16 | 17 | listenWS |
17 | 18 | ) |
18 | 19 | |
19 | from flask_mail import Mail | |
20 | ||
21 | 20 | import faraday.server.config |
22 | 21 | |
23 | from faraday.server.config import CONST_FARADAY_HOME_PATH, smtp | |
22 | from faraday.server.config import CONST_FARADAY_HOME_PATH | |
24 | 23 | from faraday.server.threads.reports_processor import ReportsManager, REPORTS_QUEUE |
25 | 24 | from faraday.server.threads.ping_home import PingHomeThread |
26 | 25 | from faraday.server.app import create_app |
29 | 28 | BroadcastServerProtocol |
30 | 29 | ) |
31 | 30 | |
31 | from faraday.server.config import faraday_server as server_config | |
32 | 32 | FARADAY_APP = None |
33 | 33 | |
34 | 34 | logger = logging.getLogger(__name__) |
66 | 66 | |
67 | 67 | |
68 | 68 | class WebServer: |
69 | UI_URL_PATH = b'_ui' | |
70 | 69 | API_URL_PATH = b'_api' |
71 | 70 | WEB_UI_LOCAL_PATH = faraday.server.config.FARADAY_BASE / 'server/www' |
71 | # Threads | |
72 | raw_report_processor = None | |
73 | ping_home_thread = None | |
72 | 74 | |
73 | 75 | def __init__(self): |
74 | 76 | |
75 | 77 | logger.info('Starting web server at http://' |
76 | f'{faraday.server.config.faraday_server.bind_address}:' | |
77 | f'{faraday.server.config.faraday_server.port}/') | |
78 | self.__websocket_port = faraday.server.config.faraday_server.websocket_port or 9000 | |
79 | self.__config_server() | |
78 | f'{server_config.bind_address}:' | |
79 | f'{server_config.port}/') | |
80 | 80 | self.__build_server_tree() |
81 | 81 | |
82 | def __config_server(self): | |
83 | self.__bind_address = faraday.server.config.faraday_server.bind_address | |
84 | self.__listen_port = int(faraday.server.config.faraday_server.port) | |
85 | ||
86 | def __load_ssl_certs(self): | |
87 | certs = (faraday.server.config.ssl.keyfile, faraday.server.config.ssl.certificate) | |
88 | if not all(certs): | |
89 | logger.critical("HTTPS request but SSL certificates are not configured") | |
90 | sys.exit(1) # Abort web-server startup | |
91 | return ssl.DefaultOpenSSLContextFactory(*certs) | |
92 | ||
93 | 82 | def __build_server_tree(self): |
94 | self.__root_resource = self.__build_web_resource() | |
95 | self.__root_resource.putChild(WebServer.UI_URL_PATH, | |
96 | self.__build_web_redirect()) | |
97 | self.__root_resource.putChild( | |
83 | self.root_resource = self.__build_web_resource() | |
84 | self.root_resource.putChild( | |
98 | 85 | WebServer.API_URL_PATH, self.__build_api_resource()) |
99 | ||
100 | def __build_web_redirect(self): | |
101 | return FaradayRedirectResource(b'/') | |
102 | 86 | |
103 | 87 | def __build_web_resource(self): |
104 | 88 | return FileWithoutDirectoryListing(WebServer.WEB_UI_LOCAL_PATH) |
107 | 91 | return FaradayWSGIResource(reactor, reactor.getThreadPool(), get_app()) |
108 | 92 | |
109 | 93 | def __build_websockets_resource(self): |
110 | websocket_port = int(faraday.server.config.faraday_server.websocket_port) | |
111 | url = f'ws://{self.__bind_address}:{websocket_port}/websockets' | |
112 | # logger.info(u"Websocket listening at {url}".format(url=url)) | |
94 | url = f'ws://{server_config.bind_address}:{server_config.websocket_port}/websockets' | |
113 | 95 | logger.info(f'Starting websocket server at port ' |
114 | f'{self.__websocket_port} with bind address {self.__bind_address}.') | |
96 | f'{server_config.websocket_port} with bind address {server_config.bind_address}.') | |
115 | 97 | factory = WorkspaceServerFactory(url=url) |
116 | 98 | factory.protocol = BroadcastServerProtocol |
117 | 99 | return factory |
118 | ||
119 | def __stop_all_threads(self): | |
120 | if self.raw_report_processor.is_alive(): | |
121 | self.raw_report_processor.stop() | |
122 | self.ping_home_thread.stop() | |
123 | 100 | |
124 | 101 | def install_signal(self): |
125 | 102 | for sig in (SIGABRT, SIGILL, SIGINT, SIGSEGV, SIGTERM): |
126 | 103 | signal(sig, SIG_DFL) |
127 | 104 | |
105 | def stop_threads(self): | |
106 | logger.info("Stopping threads...") | |
107 | if self.raw_report_processor.is_alive(): | |
108 | self.raw_report_processor.stop() | |
109 | if self.ping_home_thread.is_alive(): | |
110 | self.ping_home_thread.stop() | |
111 | ||
112 | def restart_threads(self, *args): | |
113 | logger.info("Restart threads") | |
114 | if self.raw_report_processor.is_alive(): | |
115 | self.raw_report_processor.stop() | |
116 | self.raw_report_processor.join() | |
117 | self.raw_report_processor = ReportsManager(REPORTS_QUEUE) | |
118 | self.raw_report_processor.start() | |
119 | ||
120 | def start_threads(self): | |
121 | self.raw_report_processor = ReportsManager(REPORTS_QUEUE) | |
122 | self.raw_report_processor.start() | |
123 | self.ping_home_thread = PingHomeThread() | |
124 | self.ping_home_thread.start() | |
125 | ||
128 | 126 | def run(self): |
129 | 127 | def signal_handler(*args): |
130 | 128 | logger.info('Received SIGTERM, shutting down.') |
131 | 129 | logger.info("Stopping threads, please wait...") |
132 | self.__stop_all_threads() | |
130 | self.stop_threads() | |
133 | 131 | |
134 | 132 | log_path = CONST_FARADAY_HOME_PATH / 'logs' / 'access-logging.log' |
135 | site = twisted.web.server.Site(self.__root_resource, | |
133 | site = twisted.web.server.Site(self.root_resource, | |
136 | 134 | logPath=log_path, |
137 | 135 | logFormatter=proxiedLogFormatter) |
138 | 136 | site.displayTracebacks = False |
139 | self.__listen_func = reactor.listenTCP | |
140 | 137 | |
141 | 138 | try: |
142 | 139 | self.install_signal() |
143 | 140 | # start threads and processes |
144 | self.raw_report_processor = ReportsManager(REPORTS_QUEUE, name="ReportsManager-Thread", daemon=True) | |
145 | self.raw_report_processor.start() | |
146 | self.ping_home_thread = PingHomeThread() | |
147 | self.ping_home_thread.start() | |
141 | self.start_threads() | |
148 | 142 | # web and static content |
149 | self.__listen_func( | |
150 | self.__listen_port, site, | |
151 | interface=self.__bind_address) | |
143 | reactor.listenTCP( | |
144 | server_config.port, site, | |
145 | interface=server_config.bind_address) | |
146 | num_threads = multiprocessing.cpu_count() * 2 | |
147 | logger.info(f'Starting webserver with {num_threads} threads.') | |
148 | reactor.suggestThreadPoolSize(num_threads) | |
152 | 149 | # websockets |
153 | 150 | try: |
154 | listenWS(self.__build_websockets_resource(), interface=self.__bind_address) | |
151 | listenWS(self.__build_websockets_resource(), interface=server_config.bind_address) | |
155 | 152 | except error.CannotListenError: |
156 | 153 | logger.warn('Could not start websockets, address already open. This is ok is you wan to run multiple instances.') |
157 | 154 | except Exception as ex: |
158 | 155 | logger.warn(f'Could not start websocket, error: {ex}') |
159 | 156 | logger.info('Faraday Server is ready') |
160 | 157 | reactor.addSystemEventTrigger('before', 'shutdown', signal_handler) |
158 | signal(SIGUSR1, self.restart_threads) | |
161 | 159 | reactor.run() |
162 | 160 | |
163 | 161 | except error.CannotListenError as e: |
164 | 162 | logger.error(e) |
165 | self.__stop_all_threads() | |
163 | self.stop_threads() | |
166 | 164 | sys.exit(1) |
167 | 165 | |
168 | 166 | except Exception as e: |
169 | 167 | logger.exception('Something went wrong when trying to setup the Web UI') |
170 | 168 | logger.exception(e) |
171 | self.__stop_all_threads() | |
169 | self.stop_threads() | |
172 | 170 | sys.exit(1) |
173 | 171 | |
174 | 172 | |
177 | 175 | if not FARADAY_APP: |
178 | 176 | app = create_app() # creates a Flask(__name__) app |
179 | 177 | # After 'Create app' |
180 | app.config['MAIL_SERVER'] = smtp.host | |
181 | app.config['MAIL_PORT'] = smtp.port | |
182 | app.config['MAIL_USE_SSL'] = smtp.ssl | |
183 | app.config['MAIL_USERNAME'] = smtp.username | |
184 | app.config['MAIL_PASSWORD'] = smtp.password | |
185 | mail = Mail(app) | |
186 | 178 | FARADAY_APP = app |
187 | 179 | return FARADAY_APP |
0 | from typing import List | |
1 | ||
2 | from faraday.settings.base import LOADED_SETTINGS | |
3 | ||
4 | ||
5 | def get_settings(name: str): | |
6 | name_key = f'{name}_settings' | |
7 | return LOADED_SETTINGS.get(name_key, None) | |
8 | ||
9 | ||
10 | def get_all_settings() -> List: | |
11 | return [x.settings_id for x in LOADED_SETTINGS.values()] | |
12 | ||
13 | ||
14 | def load_settings(): | |
15 | import faraday.settings.smtp # pylint: disable=import-outside-toplevel | |
16 | import faraday.settings.dashboard # pylint: disable=import-outside-toplevel # noqa: F401 |
0 | import logging | |
1 | from functools import lru_cache | |
2 | from typing import Dict, Optional | |
3 | from copy import deepcopy | |
4 | import os | |
5 | import signal | |
6 | ||
7 | from faraday.server.utils.database import get_or_create | |
8 | from faraday.server.models import ( | |
9 | db, | |
10 | Configuration | |
11 | ) | |
12 | ||
13 | logger = logging.getLogger(__name__) | |
14 | ||
15 | LOADED_SETTINGS = {} | |
16 | ||
17 | ||
18 | class classproperty: | |
19 | ||
20 | def __init__(self, fget): | |
21 | self.fget = fget | |
22 | ||
23 | def __get__(self, owner_self, owner_cls): | |
24 | return self.fget(owner_cls) | |
25 | ||
26 | ||
27 | class Settings: | |
28 | settings_id = None | |
29 | settings_key = None | |
30 | must_restart_threads = False | |
31 | ||
32 | def __init__(self): | |
33 | if self.settings_key not in LOADED_SETTINGS: | |
34 | logger.debug(f"Loading settings [{self.settings_id}]") | |
35 | LOADED_SETTINGS[self.settings_key] = self | |
36 | ||
37 | def load_configuration(self) -> Dict: | |
38 | from faraday.server.web import get_app # pylint: disable=import-outside-toplevel | |
39 | with get_app().app_context(): | |
40 | query = db.session.query(Configuration).filter(Configuration.key == self.settings_key).first() | |
41 | settings_config = self.get_default_config() | |
42 | if query: | |
43 | settings_config.update(query.value) | |
44 | settings_config = self.clear_configuration(settings_config) | |
45 | return settings_config | |
46 | ||
47 | def get_default_config(self): | |
48 | return {} | |
49 | ||
50 | def clear_configuration(self, config: Dict): | |
51 | return config | |
52 | ||
53 | def custom_validation(self, validated_config): | |
54 | pass | |
55 | ||
56 | def after_update(self): | |
57 | pass | |
58 | ||
59 | def update_configuration(self, new_config: dict, old_config: Optional[Dict] = None) -> Dict: | |
60 | if old_config: | |
61 | config = deepcopy(old_config) | |
62 | config.update(new_config) | |
63 | else: | |
64 | config = new_config | |
65 | self.after_update() | |
66 | return config | |
67 | ||
68 | def validate_configuration(self, config: Dict): | |
69 | valid_config = self.schema.load(config) | |
70 | self.custom_validation(valid_config) | |
71 | return valid_config | |
72 | ||
73 | @property | |
74 | @lru_cache(maxsize=None) | |
75 | def value(self) -> Dict: | |
76 | return self.load_configuration() | |
77 | ||
78 | def __getattr__(self, item): | |
79 | return self.value.get(item, None) | |
80 | ||
81 | @classproperty | |
82 | def settings(cls): | |
83 | return LOADED_SETTINGS.get(cls.settings_key, cls()) | |
84 | ||
85 | def update(self, new_config=None): | |
86 | saved_config, created = get_or_create(db.session, Configuration, key=self.settings_key) | |
87 | if created: | |
88 | saved_config.value = self.update_configuration(new_config) | |
89 | else: | |
90 | # SQLAlchemy doesn't detect in-place mutations to the structure of a JSON type. | |
91 | # Thus, we make a deepcopy of the JSON so SQLAlchemy can detect the changes. | |
92 | saved_config.value = self.update_configuration(new_config, saved_config.value) | |
93 | db.session.commit() | |
94 | self.__class__.value.fget.cache_clear() | |
95 | if self.must_restart_threads: | |
96 | os.kill(os.getpid(), signal.SIGUSR1) |
0 | # Faraday Penetration Test IDE | |
1 | # Copyright (C) 2021 Infobyte LLC (http://www.infobytesec.com/) | |
2 | # See the file 'doc/LICENSE' for the license information | |
3 | from marshmallow import fields | |
4 | ||
5 | from faraday.settings.base import Settings | |
6 | from faraday.server.api.base import AutoSchema | |
7 | ||
8 | DEFAULT_SHOW_VULNS_BY_PRICE = False | |
9 | ||
10 | ||
11 | class DashboardSettingSchema(AutoSchema): | |
12 | show_vulns_by_price = fields.Boolean(default=DEFAULT_SHOW_VULNS_BY_PRICE, required=True) | |
13 | ||
14 | ||
15 | class DashboardSettings(Settings): | |
16 | settings_id = "dashboard" | |
17 | settings_key = f'{settings_id}_settings' | |
18 | schema = DashboardSettingSchema() | |
19 | ||
20 | def get_default_config(self): | |
21 | return {'show_vulns_by_price': DEFAULT_SHOW_VULNS_BY_PRICE} | |
22 | ||
23 | ||
24 | def init_setting(): | |
25 | DashboardSettings() | |
26 | ||
27 | ||
28 | init_setting() |
0 | ||
1 | class MissingConfigurationError(Exception): | |
2 | """Raised when setting configuration is missing""" | |
3 | pass | |
4 | ||
5 | ||
6 | class InvalidConfigurationError(Exception): | |
7 | """Raised when setting configuration is invalid""" | |
8 | pass |
0 | # Faraday Penetration Test IDE | |
1 | # Copyright (C) 2021 Infobyte LLC (http://www.infobytesec.com/) | |
2 | # See the file 'doc/LICENSE' for the license information | |
3 | from marshmallow import fields | |
4 | from pathlib import Path | |
5 | ||
6 | from faraday.settings.base import Settings | |
7 | from faraday.settings.exceptions import InvalidConfigurationError | |
8 | from faraday.server.api.base import AutoSchema | |
9 | ||
10 | DEFAULT_IGNORE_INFO_SEVERITY = False | |
11 | DEFAULT_CUSTOM_PLUGINS_FOLDER = "" | |
12 | ||
13 | ||
14 | class ReportsSettingSchema(AutoSchema): | |
15 | ignore_info_severity = fields.Boolean(required=True, default=DEFAULT_IGNORE_INFO_SEVERITY) | |
16 | custom_plugins_folder = fields.String(default=DEFAULT_CUSTOM_PLUGINS_FOLDER, required=True) | |
17 | ||
18 | ||
19 | class ReportsSettings(Settings): | |
20 | settings_id = "reports" | |
21 | settings_key = f'{settings_id}_settings' | |
22 | must_restart_threads = True | |
23 | schema = ReportsSettingSchema() | |
24 | ||
25 | def custom_validation(self, validated_config): | |
26 | if validated_config['custom_plugins_folder']: | |
27 | if validated_config['custom_plugins_folder'] and not Path(validated_config['custom_plugins_folder']).is_dir(): | |
28 | raise InvalidConfigurationError(f"{validated_config['custom_plugins_folder']} is not valid path") | |
29 | ||
30 | def get_default_config(self): | |
31 | return {'ignore_info_severity': DEFAULT_IGNORE_INFO_SEVERITY, | |
32 | 'custom_plugins_folder': DEFAULT_CUSTOM_PLUGINS_FOLDER} | |
33 | ||
34 | ||
35 | def init_setting(): | |
36 | ReportsSettings() | |
37 | ||
38 | ||
39 | init_setting() |
0 | # Faraday Penetration Test IDE | |
1 | # Copyright (C) 2021 Infobyte LLC (http://www.infobytesec.com/) | |
2 | # See the file 'doc/LICENSE' for the license information | |
3 | from marshmallow import fields | |
4 | ||
5 | from faraday.settings.base import Settings | |
6 | from faraday.settings.exceptions import InvalidConfigurationError | |
7 | from faraday.server.api.base import AutoSchema | |
8 | ||
9 | DEFAULT_ENABLED = False | |
10 | DEFAULT_USERNAME = "" | |
11 | DEFAULT_PASSWORD = "" # nosec | |
12 | DEFAULT_HOST = "" | |
13 | DEFAULT_PORT = 25 | |
14 | DEFAULT_SENDER = "" | |
15 | DEFAULT_SSL = False | |
16 | ||
17 | ||
18 | class SMTPSettingSchema(AutoSchema): | |
19 | enabled = fields.Boolean(default=DEFAULT_ENABLED, required=True) | |
20 | username = fields.String(default=DEFAULT_USERNAME, required=True) | |
21 | password = fields.String(default=DEFAULT_PASSWORD, required=True) | |
22 | host = fields.String(default=DEFAULT_HOST, required=True) | |
23 | port = fields.Integer(default=DEFAULT_PORT, required=True) | |
24 | sender = fields.Email(default="[email protected]", required=True) | |
25 | ssl = fields.Boolean(default=DEFAULT_SSL, required=True) | |
26 | ||
27 | ||
28 | class SMTPSettings(Settings): | |
29 | settings_id = "smtp" | |
30 | settings_key = f'{settings_id}_settings' | |
31 | schema = SMTPSettingSchema() | |
32 | ||
33 | def custom_validation(self, validated_config): | |
34 | if validated_config['enabled']: | |
35 | for field in ('username', 'password', 'host'): | |
36 | if not validated_config[field]: | |
37 | raise InvalidConfigurationError(f"{field} is requiered if smtp is enabled") | |
38 | ||
39 | def get_default_config(self): | |
40 | return {'enabled': DEFAULT_ENABLED, 'username': DEFAULT_USERNAME, 'password': DEFAULT_PASSWORD, | |
41 | 'host': DEFAULT_HOST, 'port': DEFAULT_PORT, 'sender': DEFAULT_SENDER, 'ssl': DEFAULT_SSL} | |
42 | ||
43 | ||
44 | def init_setting(): | |
45 | SMTPSettings() | |
46 | ||
47 | ||
48 | init_setting() |
27 | 27 | def setup_environment(check_deps=False): |
28 | 28 | # Configuration files generation |
29 | 29 | faraday.server.config.copy_default_config_to_local() |
30 | # Web configuration file generation | |
31 | faraday.server.config.gen_web_config() | |
32 | 30 | |
33 | 31 | |
34 | 32 | def is_server_running(port): |
112 | 110 | parser.add_argument('--debug', action='store_true', help='run Faraday Server in debug mode') |
113 | 111 | parser.add_argument('--nodeps', action='store_true', help='Skip dependency check') |
114 | 112 | parser.add_argument('--no-setup', action='store_true', help=argparse.SUPPRESS) |
115 | parser.add_argument('--port', help='Overides server.ini port configuration') | |
113 | parser.add_argument('--port', type=int, help='Overides server.ini port configuration') | |
116 | 114 | parser.add_argument('--websocket_port', help='Overides server.ini websocket port configuration') |
117 | 115 | parser.add_argument('--bind_address', help='Overides server.ini bind_address configuration') |
118 | 116 | f_version = faraday.__version__ |
121 | 119 | if args.debug or faraday.server.config.faraday_server.debug: |
122 | 120 | faraday.server.utils.logger.set_logging_level(faraday.server.config.DEBUG) |
123 | 121 | args.port = faraday.server.config.faraday_server.port = args.port or \ |
124 | faraday.server.config.faraday_server.port or '5985' | |
122 | faraday.server.config.faraday_server.port or 5985 | |
125 | 123 | if args.bind_address: |
126 | 124 | faraday.server.config.faraday_server.bind_address = args.bind_address |
127 | 125 |
130 | 130 | f'{view_name} / {class_model} / {rule.methods} / {view_name} / {view_instance._get_schema_class().__name__}') |
131 | 131 | operations[view_name] = yaml_utils.load_yaml_from_docstring( |
132 | 132 | view.__doc__.format(schema_class=view_instance._get_schema_class().__name__, |
133 | class_model=class_model, tag_name=class_model) | |
133 | class_model=class_model, | |
134 | tag_name=class_model, | |
135 | route_base=view_instance.route_base) | |
134 | 136 | ) |
135 | 137 | elif hasattr(view, "__doc__"): |
136 | 138 | if not view.__doc__: |
145 | 147 | if method not in ['HEAD', 'OPTIONS'] or os.environ.get("FULL_API_DOC", None): |
146 | 148 | operations[method.lower()] = yaml_utils.load_yaml_from_docstring( |
147 | 149 | view.__doc__.format(schema_class=view_instance._get_schema_class().__name__, |
148 | class_model=class_model, tag_name=class_model) | |
150 | class_model=class_model, | |
151 | tag_name=class_model, | |
152 | route_base=view_instance.route_base) | |
149 | 153 | ) |
150 | 154 | if hasattr(view, "view_class") and issubclass(view.view_class, MethodView): |
151 | 155 | for method in view.methods: |
3 | 3 | from email.mime.multipart import MIMEMultipart |
4 | 4 | from email.mime.text import MIMEText |
5 | 5 | |
6 | from faraday.server.config import smtp | |
6 | from faraday.settings.smtp import SMTPSettings | |
7 | 7 | |
8 | 8 | logger = logging.getLogger(__name__) |
9 | 9 | |
12 | 12 | def __init__(self, smtp_host: str, smtp_sender: str, |
13 | 13 | smtp_username: str = None, smtp_password: str = None, |
14 | 14 | smtp_port: int = 0, smtp_ssl: bool = False): |
15 | self.smtp_username = smtp_username or smtp.username | |
16 | self.smtp_sender = smtp_sender or smtp.sender | |
17 | self.smtp_password = smtp_password or smtp.password | |
18 | self.smtp_host = smtp_host or smtp.host | |
19 | self.smtp_port = smtp_port or smtp.port | |
20 | if smtp.keyfile is not None and smtp.certfile is not None: | |
21 | self.smtp_ssl = True | |
22 | self.smtp_keyfile = smtp.keyfile | |
23 | self.smtp_certfile = smtp.certfile | |
24 | else: | |
25 | self.smtp_ssl = smtp_ssl or smtp.ssl | |
26 | self.smtp_keyfile = None | |
27 | self.smtp_certfile = None | |
15 | self.smtp_username = smtp_username or SMTPSettings.settings.username | |
16 | self.smtp_sender = smtp_sender or SMTPSettings.settings.sender | |
17 | self.smtp_password = smtp_password or SMTPSettings.settings.password | |
18 | self.smtp_host = smtp_host or SMTPSettings.settings.host | |
19 | self.smtp_port = smtp_port or SMTPSettings.settings.port | |
20 | self.smtp_ssl = smtp_ssl or SMTPSettings.settings.ssl | |
28 | 21 | |
29 | 22 | def send_mail(self, to_addr: str, subject: str, body: str): |
30 | 23 | msg = MIMEMultipart() |
37 | 30 | try: |
38 | 31 | with SMTP(host=self.smtp_host, port=self.smtp_port) as server_mail: |
39 | 32 | if self.smtp_ssl: |
40 | server_mail.starttls(keyfile=smtp.keyfile, | |
41 | certfile=smtp.certfile) | |
33 | server_mail.starttls() | |
42 | 34 | if self.smtp_username and self.smtp_password: |
43 | 35 | server_mail.login(self.smtp_username, self.smtp_password) |
44 | 36 | text = msg.as_string() |
71 | 71 | ./packages/bleach |
72 | 72 | { }; |
73 | 73 | |
74 | faraday-agent-parameters-types = | |
75 | self.callPackage | |
76 | ./packages/faraday-agent-parameters-types | |
77 | { }; | |
78 | ||
74 | 79 | faraday-plugins = |
75 | 80 | self.callPackage |
76 | 81 | ./packages/faraday-plugins |
106 | 111 | ./packages/flask-security-too |
107 | 112 | { }; |
108 | 113 | |
114 | marshmallow = | |
115 | self.callPackage | |
116 | ./packages/marshmallow | |
117 | { }; | |
118 | ||
119 | marshmallow-sqlalchemy = | |
120 | self.callPackage | |
121 | ./packages/marshmallow-sqlalchemy | |
122 | { }; | |
123 | ||
109 | 124 | pyotp = |
110 | 125 | self.callPackage |
111 | 126 | ./packages/pyotp |
0 | # WARNING: This file was automatically generated. You should avoid editing it. | |
1 | # If you run pynixify again, the file will be either overwritten or | |
2 | # deleted, and you will lose the changes you made to it. | |
3 | ||
4 | { buildPythonPackage | |
5 | , fetchPypi | |
6 | , lib | |
7 | , marshmallow | |
8 | , packaging | |
9 | , pytestrunner | |
10 | }: | |
11 | ||
12 | buildPythonPackage rec { | |
13 | pname = | |
14 | "faraday-agent-parameters-types"; | |
15 | version = | |
16 | "1.0.0"; | |
17 | ||
18 | src = | |
19 | fetchPypi { | |
20 | inherit | |
21 | version; | |
22 | pname = | |
23 | "faraday_agent_parameters_types"; | |
24 | sha256 = | |
25 | "0qnm7q7561kwx54k23brkh5d5lkyqss6r31bvi4rmzs61pik5jvk"; | |
26 | }; | |
27 | ||
28 | buildInputs = | |
29 | [ | |
30 | pytestrunner | |
31 | ]; | |
32 | propagatedBuildInputs = | |
33 | [ | |
34 | marshmallow | |
35 | packaging | |
36 | ]; | |
37 | ||
38 | # TODO FIXME | |
39 | doCheck = | |
40 | false; | |
41 | ||
42 | meta = | |
43 | with lib; { | |
44 | description = | |
45 | '' | |
46 | The faraday agents run code remotely to ensure your domains. This info is triggered and published | |
47 | to a faraday server instance, which had set the parameters of the code. This repository sets the models to be used | |
48 | by both sides.''; | |
49 | homepage = | |
50 | "https://github.com/infobyte/faraday_agent_parameters_types"; | |
51 | }; | |
52 | } |
13 | 13 | , pytz |
14 | 14 | , requests |
15 | 15 | , simplejson |
16 | , tabulate | |
16 | 17 | }: |
17 | 18 | |
18 | 19 | buildPythonPackage rec { |
19 | 20 | pname = |
20 | 21 | "faraday-plugins"; |
21 | 22 | version = |
22 | "1.4.5"; | |
23 | "1.5.0"; | |
23 | 24 | |
24 | 25 | src = |
25 | 26 | fetchPypi { |
27 | 28 | pname |
28 | 29 | version; |
29 | 30 | sha256 = |
30 | "0k4m6pz5dzy8x03wycya2n86aag42nydl67a1vak4kd09ain9vd7"; | |
31 | "1wf313s2kricd44s4m0x62psk2xq69fp6n4qm0f7k1rrwilwdxyd"; | |
31 | 32 | }; |
32 | 33 | |
33 | 34 | propagatedBuildInputs = |
41 | 42 | pytz |
42 | 43 | dateutil |
43 | 44 | colorama |
45 | tabulate | |
44 | 46 | ]; |
45 | 47 | |
46 | 48 | # TODO FIXME |
14 | 14 | , distro |
15 | 15 | , email_validator |
16 | 16 | , factory_boy |
17 | , faraday-agent-parameters-types | |
17 | 18 | , faraday-plugins |
18 | 19 | , fetchPypi |
19 | 20 | , filedepot |
62 | 63 | pname = |
63 | 64 | "faradaysec"; |
64 | 65 | version = |
65 | "3.15.0"; | |
66 | "3.16.0"; | |
66 | 67 | |
67 | 68 | src = |
68 | 69 | lib.cleanSource |
116 | 117 | pyotp |
117 | 118 | flask-limiter |
118 | 119 | flask_mail |
120 | faraday-agent-parameters-types | |
119 | 121 | ]; |
120 | 122 | checkInputs = |
121 | 123 | [ |
0 | # WARNING: This file was automatically generated. You should avoid editing it. | |
1 | # If you run pynixify again, the file will be either overwritten or | |
2 | # deleted, and you will lose the changes you made to it. | |
3 | ||
4 | { buildPythonPackage | |
5 | , fetchPypi | |
6 | , lib | |
7 | }: | |
8 | ||
9 | buildPythonPackage rec { | |
10 | pname = | |
11 | "marshmallow"; | |
12 | version = | |
13 | "3.12.1"; | |
14 | ||
15 | src = | |
16 | fetchPypi { | |
17 | inherit | |
18 | pname | |
19 | version; | |
20 | sha256 = | |
21 | "0h70m4z1kbcwsd0sv8cwlcmpj4dnblvr5vj18j7wa327f1dlfl40"; | |
22 | }; | |
23 | ||
24 | # TODO FIXME | |
25 | doCheck = | |
26 | false; | |
27 | ||
28 | meta = | |
29 | with lib; { | |
30 | description = | |
31 | "A lightweight library for converting complex datatypes to and from native Python datatypes."; | |
32 | homepage = | |
33 | "https://github.com/marshmallow-code/marshmallow"; | |
34 | }; | |
35 | } |
0 | # WARNING: This file was automatically generated. You should avoid editing it. | |
1 | # If you run pynixify again, the file will be either overwritten or | |
2 | # deleted, and you will lose the changes you made to it. | |
3 | ||
4 | { buildPythonPackage | |
5 | , fetchPypi | |
6 | , lib | |
7 | , marshmallow | |
8 | , sqlalchemy | |
9 | }: | |
10 | ||
11 | buildPythonPackage rec { | |
12 | pname = | |
13 | "marshmallow-sqlalchemy"; | |
14 | version = | |
15 | "0.25.0"; | |
16 | ||
17 | src = | |
18 | fetchPypi { | |
19 | inherit | |
20 | pname | |
21 | version; | |
22 | sha256 = | |
23 | "0i39ckrixh1w9fmkm0wl868gvza72j5la0x6dd0cij9shf1iyjgi"; | |
24 | }; | |
25 | ||
26 | propagatedBuildInputs = | |
27 | [ | |
28 | marshmallow | |
29 | sqlalchemy | |
30 | ]; | |
31 | ||
32 | # TODO FIXME | |
33 | doCheck = | |
34 | false; | |
35 | ||
36 | meta = | |
37 | with lib; { | |
38 | description = | |
39 | "SQLAlchemy integration with the marshmallow (de)serialization library"; | |
40 | homepage = | |
41 | "https://github.com/marshmallow-code/marshmallow-sqlalchemy"; | |
42 | }; | |
43 | } |
11 | 11 | flask-login>=0.5.0 |
12 | 12 | Flask-Security-Too>=4.0.0 |
13 | 13 | bleach>=3.3.0 |
14 | marshmallow>=3.0.0,<3.11.0 | |
14 | marshmallow>=3.11.0 | |
15 | 15 | Pillow>=4.2.1 |
16 | 16 | psycopg2 |
17 | 17 | pgcli |
24 | 24 | tqdm>=4.15.0 |
25 | 25 | twisted>=18.9.0 |
26 | 26 | webargs>=7.0.0 |
27 | marshmallow-sqlalchemy | |
27 | marshmallow-sqlalchemy==0.25.0 | |
28 | 28 | filteralchemy-fork |
29 | 29 | filedepot>=0.5.0 |
30 | 30 | nplusone>=0.8.1 |
39 | 39 | pyotp>=2.6.0 |
40 | 40 | Flask-Limiter |
41 | 41 | Flask-Mail |
42 | faraday_agent_parameters_types>=1.0.0 |
14 | 14 | |
15 | 15 | |
16 | 16 | if __name__ == '__main__': |
17 | check("origin/white/dev", "origin/pink/dev") | |
18 | check("origin/pink/dev", "origin/black/dev") | |
17 | check("origin/white/dev", "origin/black/dev") |
176 | 176 | # and refuse to install the project if the version does not match. If you |
177 | 177 | # do not support Python 2, you can simplify this to '>=3.5' or similar, see |
178 | 178 | # https://packaging.python.org/guides/distributing-packages-using-setuptools/#python-requires |
179 | python_requires='>=3.6.*', | |
179 | python_requires='>=3.7.*', | |
180 | 180 | |
181 | 181 | # This field lists other packages that your project depends on to run. |
182 | 182 | # Any package you put here will be installed by pip when your project is |
136 | 136 | |
137 | 137 | db.app = app |
138 | 138 | db.create_all() |
139 | db.engine.execute("INSERT INTO faraday_role(name) VALUES ('admin'),('pentester'),('client'),('asset_owner');") | |
139 | 140 | |
140 | 141 | request.addfinalizer(teardown) |
141 | 142 | return db |
229 | 230 | |
230 | 231 | |
231 | 232 | def create_user(app, session, username, email, password, **kwargs): |
233 | single_role = kwargs.pop('role', None) | |
234 | if 'roles' not in kwargs: | |
235 | kwargs['roles'] = [single_role] if single_role is not None else ['client'] | |
232 | 236 | user = app.user_datastore.create_user(username=username, |
233 | 237 | email=email, |
234 | 238 | password=password, |
52 | 52 | Rule, |
53 | 53 | Action, |
54 | 54 | RuleAction, |
55 | Condition) | |
55 | Condition, | |
56 | Role | |
57 | ) | |
56 | 58 | |
57 | 59 | |
58 | 60 | # Make partials for start and end date. End date must be after start date |
59 | 61 | def FuzzyStartTime(): |
60 | 62 | return ( |
61 | 63 | FuzzyNaiveDateTime( |
62 | datetime.datetime.now() - datetime.timedelta(days=40), | |
63 | datetime.datetime.now() - datetime.timedelta(days=20), | |
64 | datetime.datetime.utcnow() - datetime.timedelta(days=40), | |
65 | datetime.datetime.utcnow() - datetime.timedelta(days=20), | |
64 | 66 | ) |
65 | 67 | ) |
66 | 68 | |
68 | 70 | def FuzzyEndTime(): |
69 | 71 | return ( |
70 | 72 | FuzzyNaiveDateTime( |
71 | datetime.datetime.now() - datetime.timedelta(days=19), | |
72 | datetime.datetime.now() | |
73 | datetime.datetime.utcnow() - datetime.timedelta(days=19), | |
74 | datetime.datetime.utcnow() | |
73 | 75 | ) |
74 | 76 | ) |
75 | 77 | |
97 | 99 | fs_uniquifier = factory.LazyAttribute( |
98 | 100 | lambda e: uuid.uuid4().hex |
99 | 101 | ) |
102 | ||
103 | @factory.post_generation | |
104 | def roles(self, create, extracted, **kwargs): | |
105 | ||
106 | def get_role(_role: str) -> Role: | |
107 | return Role.query.filter(Role.name == _role).one() | |
108 | ||
109 | if not create: | |
110 | # Simple build, do nothing. | |
111 | if extracted: | |
112 | # A list of roles were passed in, use them | |
113 | self['roles'] = self.get('roles', []) | |
114 | for role in extracted: | |
115 | role = role if not isinstance(role, str) else get_role(role) | |
116 | self['roles'].append(role.name) | |
117 | else: | |
118 | self.roles.append(get_role('client')) | |
119 | elif extracted: | |
120 | # A list of groups were passed in, use them | |
121 | for role in extracted: | |
122 | role = role if not isinstance(role, str) else get_role(role) | |
123 | self.roles.append(role) | |
124 | else: | |
125 | self.roles.append(get_role('client')) | |
100 | 126 | |
101 | 127 | class Meta: |
102 | 128 | model = User |
561 | 587 | name = FuzzyText() |
562 | 588 | agent = factory.SubFactory(AgentFactory) |
563 | 589 | parameters_metadata = factory.LazyAttribute( |
564 | lambda e: {"param_name": False} | |
590 | lambda e: {"param_name": {"mandatory": False, "type": "string", "base": "string"}} | |
565 | 591 | ) |
566 | 592 | |
567 | 593 | class Meta: |
6 | 6 | import random |
7 | 7 | import pytest |
8 | 8 | from functools import partial |
9 | from posixpath import join as urljoin | |
10 | ||
9 | 11 | from faraday.server.models import Hostname, Host |
10 | ||
11 | 12 | from faraday.server.api.modules.hosts import HostsView |
12 | 13 | |
13 | 14 | from tests.test_api_workspaced_base import ( |
142 | 143 | |
143 | 144 | # This test the api endpoint for some of the host in the ws, with existing other host with vulns in the same and |
144 | 145 | # other ws |
145 | @pytest.mark.parametrize('querystring', ['countVulns/?hosts={}', | |
146 | @pytest.mark.parametrize('querystring', ['countVulns?hosts={}', | |
146 | 147 | ]) |
147 | 148 | def test_vuln_count(self, |
148 | 149 | vulnerability_factory, |
182 | 183 | session.add_all(vulns) |
183 | 184 | session.commit() |
184 | 185 | |
185 | url = self.url(workspace=workspace1) + querystring.format(",".join(map(lambda x: str(x.id), hosts_to_query))) | |
186 | url = urljoin( | |
187 | self.url(workspace=workspace1), | |
188 | querystring.format(",".join(map(lambda x: str(x.id), hosts_to_query))) | |
189 | ) | |
186 | 190 | res = test_client.get(url) |
187 | 191 | |
188 | 192 | assert res.status_code == 200 |
193 | 197 | |
194 | 198 | # This test the api endpoint for some of the host in the ws, with existing other host in other ws and ask for the |
195 | 199 | # other hosts and test the api endpoint for all of the host in the ws, retrieving all host when none is required |
196 | @pytest.mark.parametrize('querystring', ['countVulns/?hosts={}', 'countVulns/', | |
200 | @pytest.mark.parametrize('querystring', ['countVulns?hosts={}', 'countVulns', | |
197 | 201 | ]) |
198 | 202 | def test_vuln_count_ignore_other_ws(self, |
199 | 203 | vulnerability_factory, |
231 | 235 | session.add_all(vulns) |
232 | 236 | session.commit() |
233 | 237 | |
234 | url = self.url(workspace=workspace1) + querystring.format(",".join(map(lambda x: str(x.id), hosts))) | |
238 | url = urljoin(self.url(workspace=workspace1), querystring.format(",".join(map(lambda x: str(x.id), hosts)))) | |
235 | 239 | res = test_client.get(url) |
236 | 240 | |
237 | 241 | assert res.status_code == 200 |
243 | 247 | |
244 | 248 | for host in hosts_not_to_query_w2: |
245 | 249 | assert str(host.id) not in res.json['hosts'] |
246 | # I'm Py3 |
3 | 3 | See the file 'doc/LICENSE' for the license information |
4 | 4 | |
5 | 5 | ''' |
6 | import datetime | |
6 | 7 | import pytest |
7 | 8 | |
8 | 9 | from tests.factories import (WorkspaceFactory, |
11 | 12 | EmptyCommandFactory, |
12 | 13 | HostFactory, |
13 | 14 | CommandObjectFactory) |
14 | from tests.utils.url import v2_to_v3 | |
15 | 15 | |
16 | 16 | |
17 | 17 | @pytest.mark.usefixtures('logged_user') |
18 | 18 | class TestActivityFeed: |
19 | ||
20 | def check_url(self, url): | |
21 | return url | |
22 | 19 | |
23 | 20 | @pytest.mark.usefixtures('ignore_nplusone') |
24 | 21 | def test_activity_feed(self, test_client, session): |
28 | 25 | session.add(command) |
29 | 26 | session.commit() |
30 | 27 | |
31 | res = test_client.get( | |
32 | self.check_url(f'/v2/ws/{ws.name}/activities/') | |
33 | ) | |
28 | res = test_client.get(f'/v3/ws/{ws.name}/activities') | |
34 | 29 | |
35 | 30 | assert res.status_code == 200 |
36 | 31 | activities = res.json['activities'][0] |
45 | 40 | session.add(command) |
46 | 41 | session.commit() |
47 | 42 | |
48 | # Timestamp of 14/12/2018 | |
49 | itime = 1544745600.0 | |
43 | new_start_date = command.end_date - datetime.timedelta(days=1) | |
50 | 44 | data = { |
51 | 45 | 'command': command.command, |
52 | 46 | 'tool': command.tool, |
53 | 'itime': itime | |
47 | 'itime': new_start_date.timestamp() | |
54 | 48 | |
55 | 49 | } |
56 | 50 | |
57 | res = test_client.put( | |
58 | self.check_url(f'/v2/ws/{ws.name}/activities/{command.id}/'), | |
51 | res = test_client.put(f'/v3/ws/{ws.name}/activities/{command.id}', | |
59 | 52 | data=data, |
60 | 53 | ) |
61 | 54 | assert res.status_code == 200 |
63 | 56 | # Changing res.json['itime'] to timestamp format of itime |
64 | 57 | res_itime = res.json['itime'] / 1000.0 |
65 | 58 | assert res.status_code == 200 |
66 | assert res_itime == itime | |
59 | assert datetime.datetime.fromtimestamp(res_itime) == new_start_date | |
67 | 60 | |
68 | 61 | @pytest.mark.usefixtures('ignore_nplusone') |
69 | 62 | def test_verify_correct_severities_sum_values(self, session, test_client): |
134 | 127 | workspace=workspace |
135 | 128 | ) |
136 | 129 | session.commit() |
137 | res = test_client.get(self.check_url(f'/v2/ws/{command.workspace.name}/activities/')) | |
130 | res = test_client.get(f'/v3/ws/{command.workspace.name}/activities') | |
138 | 131 | assert res.status_code == 200 |
139 | 132 | assert res.json['activities'][0]['vulnerabilities_count'] == 8 |
140 | 133 | assert res.json['activities'][0]['criticalIssue'] == 1 |
143 | 136 | assert res.json['activities'][0]['lowIssue'] == 1 |
144 | 137 | assert res.json['activities'][0]['infoIssue'] == 2 |
145 | 138 | assert res.json['activities'][0]['unclassifiedIssue'] == 1 |
146 | ||
147 | ||
148 | class TestActivityFeedV3(TestActivityFeed): | |
149 | def check_url(self, url): | |
150 | return v2_to_v3(url) |
12 | 12 | from faraday.server.api.modules.agent import AgentWithWorkspacesView, AgentView |
13 | 13 | from faraday.server.models import Agent, Command |
14 | 14 | from tests.factories import AgentFactory, WorkspaceFactory, ExecutorFactory |
15 | from tests.test_api_non_workspaced_base import ReadWriteAPITests, PatchableTestsMixin | |
15 | from tests.test_api_non_workspaced_base import ReadWriteAPITests | |
16 | 16 | from tests.test_api_workspaced_base import ReadOnlyMultiWorkspacedAPITests |
17 | 17 | from tests import factories |
18 | 18 | from tests.test_api_workspaced_base import API_PREFIX |
19 | from tests.utils.url import v2_to_v3 | |
20 | 19 | |
21 | 20 | |
22 | 21 | def http_req(method, client, endpoint, json_dict, expected_status_codes, follow_redirects=False): |
58 | 57 | @pytest.mark.usefixtures('logged_user') |
59 | 58 | class TestAgentAuthTokenAPIGeneric: |
60 | 59 | |
61 | def check_url(self, url): | |
62 | return url | |
63 | ||
64 | 60 | @mock.patch('faraday.server.api.modules.agent.faraday_server') |
65 | 61 | def test_get_agent_token(self, faraday_server_config, test_client, session): |
66 | 62 | faraday_server_config.agent_registration_secret = None |
67 | res = test_client.get(self.check_url('/v2/agent_token/')) | |
63 | res = test_client.get('/v3/agent_token') | |
68 | 64 | assert 'token' in res.json and 'expires_in' in res.json |
69 | 65 | assert len(res.json['token']) |
70 | 66 | |
71 | 67 | @mock.patch('faraday.server.api.modules.agent.faraday_server') |
72 | 68 | def test_create_agent_token_fails(self, faraday_server_config, test_client, session): |
73 | 69 | faraday_server_config.agent_registration_secret = None |
74 | res = test_client.post(self.check_url('/v2/agent_token/')) | |
70 | res = test_client.post('/v3/agent_token') | |
75 | 71 | assert res.status_code == 405 |
76 | 72 | |
77 | 73 | |
78 | @pytest.mark.usefixtures('logged_user') | |
79 | class TestAgentAuthTokenAPIGenericV3(TestAgentAuthTokenAPIGeneric): | |
80 | def check_url(self, url): | |
81 | return v2_to_v3(url) | |
82 | ||
83 | ||
84 | 74 | class TestAgentCreationAPI: |
85 | ||
86 | def check_url(self, url): | |
87 | return url | |
88 | 75 | |
89 | 76 | @mock.patch('faraday.server.api.modules.agent.faraday_server') |
90 | 77 | @pytest.mark.usefixtures('ignore_nplusone') |
105 | 92 | token=pyotp.TOTP(secret, interval=60).now(), |
106 | 93 | workspaces=[workspace, other_workspace] |
107 | 94 | ) |
108 | # /v2/agent_registration/ | |
109 | res = test_client.post(self.check_url('/v2/agent_registration/'), data=raw_data) | |
95 | res = test_client.post('/v3/agent_registration', data=raw_data) | |
110 | 96 | assert res.status_code == 201, (res.json, raw_data) |
111 | 97 | assert len(session.query(Agent).all()) == initial_agent_count + 1 |
112 | 98 | assert workspace.name in res.json['workspaces'] |
133 | 119 | token=pyotp.TOTP(secret, interval=60).now(), |
134 | 120 | workspaces=[workspace] |
135 | 121 | ) |
136 | # /v2/agent_registration/ | |
137 | res = test_client.post( | |
138 | self.check_url('/v2/agent_registration/'), | |
122 | res = test_client.post( | |
123 | '/v3/agent_registration', | |
139 | 124 | data=raw_data |
140 | 125 | ) |
141 | 126 | assert res.status_code == 400 |
155 | 140 | name="test agent", |
156 | 141 | workspaces=[workspace] |
157 | 142 | ) |
158 | # /v2/agent_registration/ | |
159 | res = test_client.post(self.check_url('/v2/agent_registration/'), data=raw_data) | |
143 | res = test_client.post('/v3/agent_registration', data=raw_data) | |
160 | 144 | assert res.status_code == 401 |
161 | 145 | |
162 | 146 | @mock.patch('faraday.server.api.modules.agent.faraday_server') |
171 | 155 | name="test agent", |
172 | 156 | workspaces=[workspace], |
173 | 157 | ) |
174 | # /v2/agent_registration/ | |
175 | res = test_client.post(self.check_url('/v2/agent_registration/'), data=raw_data) | |
158 | res = test_client.post('/v3/agent_registration', data=raw_data) | |
176 | 159 | assert res.status_code == 400 |
177 | 160 | |
178 | 161 | @mock.patch('faraday.server.api.modules.agent.faraday_server') |
181 | 164 | faraday_server_config.agent_registration_secret = None |
182 | 165 | logout(test_client, [302]) |
183 | 166 | raw_data = {"PEPE": 'INVALID'} |
184 | # /v2/agent_registration/ | |
185 | res = test_client.post(self.check_url('/v2/agent_registration/'), data=raw_data) | |
167 | res = test_client.post('/v3/agent_registration', data=raw_data) | |
186 | 168 | assert res.status_code == 400 |
187 | 169 | |
188 | 170 | @mock.patch('faraday.server.api.modules.agent.faraday_server') |
200 | 182 | name="test agent", |
201 | 183 | workspaces=[] |
202 | 184 | ) |
203 | # /v2/agent_registration/ | |
204 | res = test_client.post(self.check_url('/v2/agent_registration/'), data=raw_data) | |
185 | res = test_client.post('/v3/agent_registration', data=raw_data) | |
205 | 186 | assert res.status_code == 400 |
206 | 187 | |
207 | 188 | @mock.patch('faraday.server.api.modules.agent.faraday_server') |
220 | 201 | workspaces=[] |
221 | 202 | ) |
222 | 203 | raw_data["workspaces"] = ["donotexist"] |
223 | # /v2/agent_registration/ | |
224 | res = test_client.post(self.check_url('/v2/agent_registration/'), data=raw_data) | |
204 | res = test_client.post('/v3/agent_registration', data=raw_data) | |
225 | 205 | assert res.status_code == 404 |
226 | 206 | |
227 | 207 | @mock.patch('faraday.server.api.modules.agent.faraday_server') |
238 | 218 | name="test agent", |
239 | 219 | token=pyotp.TOTP(secret, interval=60).now() |
240 | 220 | ) |
241 | # /v2/agent_registration/ | |
242 | res = test_client.post(self.check_url('/v2/agent_registration/'), data=raw_data) | |
243 | assert res.status_code == 400 | |
244 | ||
245 | ||
246 | class TestAgentCreationAPIV3(TestAgentCreationAPI): | |
247 | def check_url(self, url): | |
248 | return v2_to_v3(url) | |
221 | res = test_client.post('/v3/agent_registration', data=raw_data) | |
222 | assert res.status_code == 400 | |
249 | 223 | |
250 | 224 | |
251 | 225 | class TestAgentWithWorkspacesAPIGeneric(ReadWriteAPITests): |
266 | 240 | assert '405' in exc_info.value.args[0] |
267 | 241 | |
268 | 242 | def workspaced_url(self, workspace, obj=None): |
269 | url = API_PREFIX + workspace.name + '/' + self.api_endpoint + '/' | |
243 | url = API_PREFIX + workspace.name + '/' + self.api_endpoint | |
270 | 244 | if obj is not None: |
271 | 245 | id_ = str(obj.id) if isinstance(obj, self.model) else str(obj) |
272 | url += id_ + u'/' | |
246 | url += u'/' + id_ | |
273 | 247 | return url |
274 | 248 | |
275 | 249 | def create_raw_agent(self, active=False, token="TOKEN", |
280 | 254 | def test_create_agent_invalid(self, test_client, session): |
281 | 255 | """ |
282 | 256 | To create new agent use the |
283 | <Rule '/v2/agent_registration/' (POST, OPTIONS) | |
257 | <Rule '/v3/agent_registration/' (POST, OPTIONS) | |
284 | 258 | """ |
285 | 259 | initial_agent_count = len(session.query(Agent).all()) |
286 | 260 | raw_agent = self.create_raw_agent() |
425 | 399 | assert res.status_code == 404 |
426 | 400 | |
427 | 401 | |
428 | class TestAgentWithWorkspacesAPIGenericV3(TestAgentWithWorkspacesAPIGeneric, PatchableTestsMixin): | |
429 | def url(self, obj=None): | |
430 | return v2_to_v3(super().url(obj)) | |
431 | ||
432 | ||
433 | 402 | class TestAgentAPI(ReadOnlyMultiWorkspacedAPITests): |
434 | 403 | model = Agent |
435 | 404 | factory = factories.AgentFactory |
436 | 405 | view_class = AgentView |
437 | 406 | api_endpoint = 'agents' |
438 | ||
439 | def check_url(self, url): | |
440 | return url | |
441 | 407 | |
442 | 408 | def test_get_workspaced(self, test_client, session): |
443 | 409 | workspace = WorkspaceFactory.create() |
485 | 451 | 'csrf_token': csrf_token |
486 | 452 | } |
487 | 453 | res = test_client.post( |
488 | self.check_url(urljoin(self.url(agent), 'run/')), | |
454 | urljoin(self.url(agent), 'run'), | |
489 | 455 | json=payload |
490 | 456 | ) |
491 | 457 | assert res.status_code == 400 |
495 | 461 | session.add(agent) |
496 | 462 | session.commit() |
497 | 463 | res = test_client.post( |
498 | self.check_url(urljoin(self.url(agent), 'run/')), | |
464 | urljoin(self.url(agent), 'run'), | |
499 | 465 | data='[" broken]"{' |
500 | 466 | ) |
501 | 467 | assert res.status_code == 400 |
517 | 483 | ('content-type', 'text/html'), |
518 | 484 | ] |
519 | 485 | res = test_client.post( |
520 | self.check_url(urljoin(self.url(agent), 'run/')), | |
486 | urljoin(self.url(agent), 'run'), | |
521 | 487 | data=payload, |
522 | 488 | headers=headers) |
523 | 489 | assert res.status_code == 400 |
536 | 502 | }, |
537 | 503 | } |
538 | 504 | res = test_client.post( |
539 | self.check_url(urljoin(self.url(agent), 'run/')), | |
505 | urljoin(self.url(agent), 'run'), | |
540 | 506 | json=payload |
541 | 507 | ) |
542 | 508 | assert res.status_code == 400 |
557 | 523 | 'csrf_token': csrf_token, |
558 | 524 | 'executorData': { |
559 | 525 | "args": { |
560 | "param1": True | |
526 | "param_name": "test" | |
561 | 527 | }, |
562 | 528 | "executor": executor.name |
563 | 529 | }, |
564 | 530 | } |
565 | 531 | res = test_client.post( |
566 | self.check_url(urljoin(self.url(agent), 'run/')), | |
532 | urljoin(self.url(agent), 'run'), | |
567 | 533 | json=payload |
568 | 534 | ) |
569 | 535 | assert res.status_code == 200 |
575 | 541 | assert executor2.last_run is None |
576 | 542 | assert agent.last_run == executor.last_run |
577 | 543 | |
544 | def test_invalid_parameter_type(self, test_client, session, csrf_token): | |
545 | agent = AgentFactory.create(workspaces=[self.workspace]) | |
546 | executor = ExecutorFactory.create(agent=agent) | |
547 | ||
548 | session.add(executor) | |
549 | session.commit() | |
550 | ||
551 | payload = { | |
552 | 'csrf_token': csrf_token, | |
553 | 'executorData': { | |
554 | "args": { | |
555 | "param_name": ["test"] | |
556 | }, | |
557 | "executor": executor.name | |
558 | }, | |
559 | } | |
560 | res = test_client.post( | |
561 | urljoin(self.url(agent), 'run'), | |
562 | json=payload | |
563 | ) | |
564 | assert res.status_code == 400 | |
565 | ||
578 | 566 | def test_invalid_json_on_executorData_breaks_the_api(self, csrf_token, |
579 | 567 | session, test_client): |
580 | 568 | agent = AgentFactory.create(workspaces=[self.workspace]) |
585 | 573 | 'executorData': '[][dassa', |
586 | 574 | } |
587 | 575 | res = test_client.post( |
588 | self.check_url(urljoin(self.url(agent), 'run/')), | |
576 | urljoin(self.url(agent), 'run'), | |
589 | 577 | json=payload |
590 | 578 | ) |
591 | 579 | assert res.status_code == 400 |
599 | 587 | 'executorData': '', |
600 | 588 | } |
601 | 589 | res = test_client.post( |
602 | self.check_url(urljoin(self.url(agent), 'run/')), | |
590 | urljoin(self.url(agent), 'run'), | |
603 | 591 | json=payload |
604 | 592 | ) |
605 | 593 | assert res.status_code == 400 |
606 | 594 | |
607 | ||
608 | class TestAgentAPIV3(TestAgentAPI): | |
609 | def url(self, obj=None, workspace=None): | |
610 | return v2_to_v3(super().url(obj, workspace)) | |
611 | ||
612 | def check_url(self, url): | |
613 | return v2_to_v3(url) | |
595 | def test_get_manifests(self, session, csrf_token, test_client): | |
596 | agent = AgentFactory.create(workspaces=[self.workspace]) | |
597 | session.add(agent) | |
598 | session.commit() | |
599 | res = test_client.get(urljoin(self.url(), 'get_manifests')) | |
600 | assert res.status_code == 200 |
10 | 10 | from tests import factories |
11 | 11 | from flask_security.utils import hash_password |
12 | 12 | from faraday.server.api.modules.websocket_auth import decode_agent_websocket_token |
13 | from tests.utils.url import v2_to_v3 | |
14 | 13 | |
15 | 14 | |
16 | 15 | class TestWebsocketAuthEndpoint: |
17 | def check_url(self, url): | |
18 | return url | |
19 | ||
20 | 16 | def test_not_logged_in_request_fail(self, test_client, workspace): |
21 | res = test_client.post(self.check_url(f'/v2/ws/{workspace.name}/websocket_token/')) | |
17 | res = test_client.post(f'/v3/ws/{workspace.name}/websocket_token') | |
22 | 18 | assert res.status_code == 401 |
23 | 19 | |
24 | 20 | @pytest.mark.usefixtures('logged_user') |
25 | 21 | def test_get_method_succeeds(self, test_client, workspace): |
26 | res = test_client.get(self.check_url(f'/v2/ws/{workspace.name}/websocket_token/')) | |
22 | res = test_client.get(f'/v3/ws/{workspace.name}/websocket_token') | |
27 | 23 | assert res.status_code == 200 |
28 | 24 | |
29 | 25 | # A token for that workspace should be generated, |
33 | 29 | |
34 | 30 | @pytest.mark.usefixtures('logged_user') |
35 | 31 | def test_post_method_succeeds(self, test_client, workspace): |
36 | res = test_client.post(self.check_url(f'/v2/ws/{workspace.name}/websocket_token/')) | |
32 | res = test_client.post(f'/v3/ws/{workspace.name}/websocket_token') | |
37 | 33 | assert res.status_code == 200 |
38 | 34 | |
39 | 35 | # A token for that workspace should be generated, |
42 | 38 | assert res.json['token'].startswith(str(workspace.id)) |
43 | 39 | |
44 | 40 | |
45 | class TestWebsocketAuthEndpointV3(TestWebsocketAuthEndpoint): | |
46 | def check_url(self, url): | |
47 | return v2_to_v3(url) | |
48 | ||
49 | ||
50 | 41 | class TestAgentWebsocketToken: |
51 | ||
52 | def check_url(self, url): | |
53 | return url | |
54 | 42 | |
55 | 43 | @pytest.mark.usefixtures('session') # I don't know why this is required |
56 | 44 | def test_fails_without_authorization_header(self, test_client): |
57 | res = test_client.post( | |
58 | self.check_url('/v2/agent_websocket_token/') | |
59 | ) | |
45 | res = test_client.post('/v3/agent_websocket_token') | |
46 | ||
60 | 47 | assert res.status_code == 401 |
61 | 48 | |
62 | 49 | @pytest.mark.usefixtures('logged_user') |
63 | 50 | def test_fails_with_logged_user(self, test_client): |
64 | 51 | res = test_client.post( |
65 | self.check_url('/v2/agent_websocket_token/') | |
52 | '/v3/agent_websocket_token' | |
66 | 53 | ) |
67 | 54 | assert res.status_code == 401 |
68 | 55 | |
69 | 56 | @pytest.mark.usefixtures('logged_user') |
70 | 57 | def test_fails_with_user_token(self, test_client, session): |
71 | res = test_client.get(self.check_url('/v2/token/')) | |
58 | res = test_client.get('/v3/token') | |
72 | 59 | |
73 | 60 | assert res.status_code == 200 |
74 | 61 | |
77 | 64 | # clean cookies make sure test_client has no session |
78 | 65 | test_client.cookie_jar.clear() |
79 | 66 | res = test_client.post( |
80 | self.check_url('/v2/agent_websocket_token/'), | |
67 | '/v3/agent_websocket_token', | |
81 | 68 | headers=headers, |
82 | 69 | ) |
83 | 70 | assert res.status_code == 401 |
86 | 73 | def test_fails_with_invalid_agent_token(self, test_client): |
87 | 74 | headers = [('Authorization', 'Agent 13123')] |
88 | 75 | res = test_client.post( |
89 | self.check_url('/v2/agent_websocket_token/'), | |
76 | '/v3/agent_websocket_token', | |
90 | 77 | headers=headers, |
91 | 78 | ) |
92 | 79 | assert res.status_code == 403 |
98 | 85 | assert agent.token |
99 | 86 | headers = [('Authorization', 'Agent ' + agent.token)] |
100 | 87 | res = test_client.post( |
101 | self.check_url('/v2/agent_websocket_token/'), | |
88 | '/v3/agent_websocket_token', | |
102 | 89 | headers=headers, |
103 | 90 | ) |
104 | 91 | assert res.status_code == 200 |
107 | 94 | |
108 | 95 | |
109 | 96 | class TestBasicAuth: |
110 | ||
111 | def check_url(self, url): | |
112 | return url | |
113 | 97 | |
114 | 98 | def test_basic_auth_invalid_credentials(self, test_client, session): |
115 | 99 | """ |
120 | 104 | active=True, |
121 | 105 | username='asdasd', |
122 | 106 | password=hash_password('asdasd'), |
123 | role='admin') | |
107 | roles=['admin']) | |
124 | 108 | session.add(alice) |
125 | 109 | session.commit() |
126 | 110 | |
130 | 114 | |
131 | 115 | valid_credentials = base64.b64encode(b"asdasd:wrong_password").decode("utf-8") |
132 | 116 | headers = [('Authorization', f'Basic {valid_credentials}')] |
133 | res = test_client.get(self.check_url('/v2/agents/'), headers=headers) | |
117 | res = test_client.get('/v3/agents', headers=headers) | |
134 | 118 | assert res.status_code == 401 |
135 | 119 | |
136 | 120 | def test_basic_auth_valid_credentials(self, test_client, session): |
142 | 126 | active=True, |
143 | 127 | username='asdasd', |
144 | 128 | password=hash_password('asdasd'), |
145 | role='admin') | |
129 | roles=['admin']) | |
146 | 130 | session.add(alice) |
147 | 131 | session.commit() |
148 | 132 | |
152 | 136 | |
153 | 137 | valid_credentials = base64.b64encode(b"asdasd:asdasd").decode("utf-8") |
154 | 138 | headers = [('Authorization', f'Basic {valid_credentials}')] |
155 | res = test_client.get(self.check_url('/v2/agents/'), headers=headers) | |
139 | res = test_client.get('/v3/agents', headers=headers) | |
156 | 140 | assert res.status_code == 200 |
157 | ||
158 | ||
159 | class TestAgentWebsocketTokenV3(TestAgentWebsocketToken): | |
160 | def check_url(self, url): | |
161 | return v2_to_v3(url) | |
162 | ||
163 | ||
164 | class TestBasicAuthV3(TestBasicAuth): | |
165 | def check_url(self, url): | |
166 | return v2_to_v3(url) |
17 | 17 | ) |
18 | 18 | from faraday.server.api.modules import bulk_create as bc |
19 | 19 | from tests.factories import CustomFieldsSchemaFactory |
20 | from tests.utils.url import v2_to_v3 | |
21 | 20 | |
22 | 21 | host_data = { |
23 | 22 | "ip": "127.0.0.1", |
623 | 622 | |
624 | 623 | class TestBulkCreateAPI: |
625 | 624 | |
626 | def check_url(self, url): | |
627 | return url | |
628 | ||
629 | 625 | @pytest.mark.usefixtures('logged_user') |
630 | 626 | def test_bulk_create_endpoint(self, session, workspace, test_client, logged_user): |
631 | 627 | assert count(Host, workspace) == 0 |
632 | 628 | assert count(VulnerabilityGeneric, workspace) == 0 |
633 | url = self.check_url(f'/v2/ws/{workspace.name}/bulk_create/') | |
629 | url = f'/v3/ws/{workspace.name}/bulk_create' | |
634 | 630 | host_data_ = host_data.copy() |
635 | 631 | service_data_ = service_data.copy() |
636 | 632 | service_data_['vulnerabilities'] = [vuln_data] |
665 | 661 | def test_bulk_create_endpoint_run_over_closed_vuln(self, session, workspace, test_client): |
666 | 662 | assert count(Host, workspace) == 0 |
667 | 663 | assert count(VulnerabilityGeneric, workspace) == 0 |
668 | url = self.check_url(f'/v2/ws/{workspace.name}/bulk_create/') | |
664 | url = f'/v3/ws/{workspace.name}/bulk_create' | |
669 | 665 | host_data_ = host_data.copy() |
670 | 666 | host_data_['vulnerabilities'] = [vuln_data] |
671 | 667 | res = test_client.post(url, data=dict(hosts=[host_data_])) |
677 | 673 | assert host.ip == "127.0.0.1" |
678 | 674 | assert set({hn.name for hn in host.hostnames}) == {"test.com", "test2.org"} |
679 | 675 | assert vuln.status == "open" |
680 | close_url = self.check_url(f"/v2/ws/{workspace.name}/vulns/{vuln.id}/") | |
676 | close_url = f'/v3/ws/{workspace.name}/vulns/{vuln.id}' | |
681 | 677 | res = test_client.get(close_url) |
682 | 678 | vuln_data_del = res.json |
683 | 679 | vuln_data_del["status"] = "closed" |
695 | 691 | |
696 | 692 | @pytest.mark.usefixtures('logged_user') |
697 | 693 | def test_bulk_create_endpoint_without_host_ip(self, session, workspace, test_client): |
698 | url = self.check_url(f'/v2/ws/{workspace.name}/bulk_create/') | |
694 | url = f'/v3/ws/{workspace.name}/bulk_create' | |
699 | 695 | host_data_ = host_data.copy() |
700 | 696 | host_data_.pop('ip') |
701 | 697 | res = test_client.post(url, data=dict(hosts=[host_data_])) |
702 | 698 | assert res.status_code == 400 |
703 | 699 | |
704 | 700 | def test_bulk_create_endpoints_fails_without_auth(self, session, workspace, test_client): |
705 | url = self.check_url(f'/v2/ws/{workspace.name}/bulk_create/') | |
701 | url = f'/v3/ws/{workspace.name}/bulk_create' | |
706 | 702 | res = test_client.post(url, data=dict(hosts=[host_data])) |
707 | 703 | assert res.status_code == 401 |
708 | 704 | assert count(Host, workspace) == 0 |
709 | 705 | |
710 | 706 | @pytest.mark.parametrize('token_type', ['agent', 'token']) |
711 | 707 | def test_bulk_create_endpoints_fails_with_invalid_token(self, token_type, workspace, test_client): |
712 | url = self.check_url(f'/v2/ws/{workspace.name}/bulk_create/') | |
708 | url = f'/v3/ws/{workspace.name}/bulk_create' | |
713 | 709 | res = test_client.post( |
714 | 710 | url, |
715 | 711 | data=dict(hosts=[host_data]), |
730 | 726 | session.add(agent) |
731 | 727 | session.commit() |
732 | 728 | assert agent.token |
733 | url = self.check_url(f'/v2/ws/{second_workspace.name}/bulk_create/') | |
729 | url = f'/v3/ws/{second_workspace.name}/bulk_create' | |
734 | 730 | res = test_client.post( |
735 | 731 | url, |
736 | 732 | data=dict(hosts=[host_data]), |
745 | 741 | session.add(agent) |
746 | 742 | session.commit() |
747 | 743 | assert agent.token |
748 | url = self.check_url("/v2/ws/im_a_incorrect_ws/bulk_create/") | |
744 | url = "/v3/ws/im_a_incorrect_ws/bulk_create" | |
749 | 745 | res = test_client.post( |
750 | 746 | url, |
751 | 747 | data=dict(hosts=[host_data]), |
761 | 757 | session.commit() |
762 | 758 | for workspace in agent.workspaces: |
763 | 759 | assert count(Host, workspace) == 0 |
764 | url = self.check_url(f'/v2/ws/{workspace.name}/bulk_create/') | |
760 | url = f'/v3/ws/{workspace.name}/bulk_create' | |
765 | 761 | res = test_client.post( |
766 | 762 | url, |
767 | 763 | data=dict(hosts=[host_data]), |
823 | 819 | |
824 | 820 | initial_host_count = Host.query.filter(Host.workspace == workspace and Host.creator_id is None).count() |
825 | 821 | assert count(Command, workspace) == 1 |
826 | url = self.check_url(f'/v2/ws/{workspace.name}/bulk_create/') | |
822 | url = f'/v3/ws/{workspace.name}/bulk_create' | |
827 | 823 | res = test_client.post( |
828 | 824 | url, |
829 | 825 | data=dict(**data_kwargs), |
883 | 879 | session.commit() |
884 | 880 | assert count(Host, workspace) == 0 |
885 | 881 | assert count(Command, workspace) == 1 |
886 | url = self.check_url(f'/v2/ws/{workspace.name}/bulk_create/') | |
882 | url = f'/v3/ws/{workspace.name}/bulk_create' | |
887 | 883 | res = test_client.post( |
888 | 884 | url, |
889 | 885 | data=dict(hosts=[host_data], execution_id=agent_execution.id), |
911 | 907 | session.add(workspace) |
912 | 908 | session.commit() |
913 | 909 | for workspace in agent.workspaces: |
914 | url = self.check_url(f'/v2/ws/{workspace.name}/bulk_create/') | |
910 | url = f'/v3/ws/{workspace.name}/bulk_create' | |
915 | 911 | res = test_client.post( |
916 | 912 | url, |
917 | 913 | data=dict(hosts=[host_data]), |
926 | 922 | session.add(workspace) |
927 | 923 | session.commit() |
928 | 924 | for workspace in agent.workspaces: |
929 | url = self.check_url(f'/v2/ws/{workspace.name}/bulk_create/') | |
925 | url = f'/v3/ws/{workspace.name}/bulk_create' | |
930 | 926 | res = test_client.post( |
931 | 927 | url, |
932 | 928 | data=dict(hosts=[host_data]), |
936 | 932 | |
937 | 933 | @pytest.mark.usefixtures('logged_user') |
938 | 934 | def test_bulk_create_endpoint_raises_400_with_no_data(self, session, test_client, workspace): |
939 | url = self.check_url(f'/v2/ws/{workspace.name}/bulk_create/') | |
935 | url = f'/v3/ws/{workspace.name}/bulk_create' | |
940 | 936 | res = test_client.post( |
941 | 937 | url, |
942 | 938 | data="", |
949 | 945 | def test_bulk_create_endpoint_with_vuln_run_date(self, session, workspace, test_client): |
950 | 946 | assert count(Host, workspace) == 0 |
951 | 947 | assert count(VulnerabilityGeneric, workspace) == 0 |
952 | url = self.check_url(f'/v2/ws/{workspace.name}/bulk_create/') | |
948 | url = f'/v3/ws/{workspace.name}/bulk_create' | |
953 | 949 | run_date = datetime.now(timezone.utc) - timedelta(days=30) |
954 | 950 | host_data_copy = host_data.copy() |
955 | 951 | vuln_data_copy = vuln_data.copy() |
966 | 962 | def test_bulk_create_endpoint_with_vuln_future_run_date(self, session, workspace, test_client): |
967 | 963 | assert count(Host, workspace) == 0 |
968 | 964 | assert count(VulnerabilityGeneric, workspace) == 0 |
969 | url = self.check_url(f'/v2/ws/{workspace.name}/bulk_create/') | |
965 | url = f'/v3/ws/{workspace.name}/bulk_create' | |
970 | 966 | run_date = datetime.now(timezone.utc) + timedelta(days=10) |
971 | 967 | host_data_copy = host_data.copy() |
972 | 968 | vuln_data_copy = vuln_data.copy() |
984 | 980 | def test_bulk_create_endpoint_with_invalid_vuln_run_date(self, session, workspace, test_client): |
985 | 981 | assert count(Host, workspace) == 0 |
986 | 982 | assert count(VulnerabilityGeneric, workspace) == 0 |
987 | url = self.check_url(f'/v2/ws/{workspace.name}/bulk_create/') | |
983 | url = f'/v3/ws/{workspace.name}/bulk_create' | |
988 | 984 | host_data_copy = host_data.copy() |
989 | 985 | vuln_data_copy = vuln_data.copy() |
990 | 986 | vuln_data_copy['run_date'] = "INVALID_VALUE" |
998 | 994 | logged_user): |
999 | 995 | assert count(Host, workspace) == 0 |
1000 | 996 | assert count(VulnerabilityGeneric, workspace) == 0 |
1001 | url = self.check_url(f'/v2/ws/{workspace.name}/bulk_create/') | |
997 | url = f'/v3/ws/{workspace.name}/bulk_create' | |
1002 | 998 | host_data_ = host_data.copy() |
1003 | 999 | host_data_['services'] = [service_data] |
1004 | 1000 | host_data_['credentials'] = [credential_data] |
1024 | 1020 | |
1025 | 1021 | assert count(Host, workspace) == 0 |
1026 | 1022 | assert count(VulnerabilityGeneric, workspace) == 0 |
1027 | url = self.check_url(f'/v2/ws/{workspace.name}/bulk_create/') | |
1023 | url = f'/v3/ws/{workspace.name}/bulk_create' | |
1028 | 1024 | host_data_ = host_data.copy() |
1029 | 1025 | service_data_ = service_data.copy() |
1030 | 1026 | vuln_data_ = vuln_data.copy() |
1061 | 1057 | |
1062 | 1058 | @pytest.mark.usefixtures('logged_user') |
1063 | 1059 | def test_vuln_web_cannot_have_host_parent(self, session, workspace, test_client, logged_user): |
1064 | url = self.check_url(f'/v2/ws/{workspace.name}/bulk_create/') | |
1060 | url = f'/v3/ws/{workspace.name}/bulk_create' | |
1065 | 1061 | host_data_ = host_data.copy() |
1066 | 1062 | vuln_web_data_ = vuln_web_data.copy() |
1067 | 1063 | vuln_web_data_['severity'] = "high" |
1072 | 1068 | data=dict(hosts=[host_data_], command=command_data) |
1073 | 1069 | ) |
1074 | 1070 | assert res.status_code == 400 |
1075 | ||
1076 | ||
1077 | class TestBulkCreateAPIV3(TestBulkCreateAPI): | |
1078 | def check_url(self, url): | |
1079 | return v2_to_v3(url) |
4 | 4 | See the file 'doc/LICENSE' for the license information |
5 | 5 | |
6 | 6 | ''' |
7 | from tests.utils.url import v2_to_v3 | |
8 | 7 | |
9 | 8 | """Tests for many API endpoints that do not depend on workspace_name""" |
10 | 9 | from posixpath import join as urljoin |
13 | 12 | import time |
14 | 13 | |
15 | 14 | from tests import factories |
16 | from tests.test_api_workspaced_base import ReadWriteAPITests, PatchableTestsMixin | |
15 | from tests.test_api_workspaced_base import ReadWriteAPITests | |
17 | 16 | from faraday.server.models import ( |
18 | 17 | Command, |
19 | 18 | Vulnerability) |
20 | from faraday.server.api.modules.commandsrun import CommandView, CommandV3View | |
19 | from faraday.server.api.modules.commandsrun import CommandView | |
21 | 20 | from tests.factories import VulnerabilityFactory, EmptyCommandFactory, CommandObjectFactory, HostFactory, \ |
22 | 21 | WorkspaceFactory, ServiceFactory |
23 | 22 | |
35 | 34 | api_endpoint = 'commands' |
36 | 35 | view_class = CommandView |
37 | 36 | patchable_fields = ["ip"] |
38 | ||
39 | def check_url(self, url): | |
40 | return url | |
41 | 37 | |
42 | 38 | @pytest.mark.usefixtures('ignore_nplusone') |
43 | 39 | @pytest.mark.usefixtures('mock_envelope_list') |
96 | 92 | ) |
97 | 93 | session.commit() |
98 | 94 | |
99 | res = test_client.get(self.check_url(urljoin(self.url(workspace=command.workspace), 'activity_feed/'))) | |
95 | res = test_client.get(urljoin(self.url(workspace=command.workspace), 'activity_feed')) | |
100 | 96 | assert res.status_code == 200 |
101 | 97 | |
102 | 98 | assert list(filter(lambda stats: stats['_id'] == command.id, res.json)) == [ |
153 | 149 | workspace=workspace |
154 | 150 | ) |
155 | 151 | session.commit() |
156 | res = test_client.get(self.check_url(urljoin(self.url(workspace=command.workspace), 'activity_feed/'))) | |
152 | res = test_client.get(urljoin(self.url(workspace=command.workspace), 'activity_feed')) | |
157 | 153 | assert res.status_code == 200 |
158 | 154 | assert res.json == [ |
159 | 155 | {u'_id': command.id, |
202 | 198 | workspace=workspace |
203 | 199 | ) |
204 | 200 | session.commit() |
205 | res = test_client.get(self.check_url(urljoin(self.url(workspace=command.workspace), 'activity_feed/'))) | |
201 | res = test_client.get(urljoin(self.url(workspace=command.workspace), 'activity_feed')) | |
206 | 202 | assert res.status_code == 200 |
207 | 203 | assert res.json == [{ |
208 | 204 | u'_id': command.id, |
268 | 264 | workspace=workspace |
269 | 265 | ) |
270 | 266 | session.commit() |
271 | res = test_client.get(self.check_url(urljoin(self.url(workspace=command.workspace), 'activity_feed/'))) | |
267 | res = test_client.get(urljoin(self.url(workspace=command.workspace), 'activity_feed')) | |
272 | 268 | assert res.status_code == 200 |
273 | 269 | raw_first_command = list(filter(lambda comm: comm['_id'] == commands[0].id, res.json)) |
274 | 270 | |
416 | 412 | ) |
417 | 413 | session.commit() |
418 | 414 | |
419 | res = test_client.get(self.check_url(f'/v2/ws/{host.workspace.name}/hosts/{host.id}/')) | |
420 | assert res.status_code == 200 | |
421 | ||
422 | res = test_client.delete(self.check_url(f'/v2/ws/{host.workspace.name}/hosts/{host.id}/')) | |
415 | res = test_client.get(f'/v3/ws/{host.workspace.name}/hosts/{host.id}') | |
416 | assert res.status_code == 200 | |
417 | ||
418 | res = test_client.delete(f'/v3/ws/{host.workspace.name}/hosts/{host.id}') | |
423 | 419 | assert res.status_code == 204 |
424 | 420 | |
425 | res = test_client.get(self.check_url(urljoin(self.url(workspace=command.workspace), 'activity_feed/'))) | |
421 | res = test_client.get(urljoin(self.url(workspace=command.workspace), 'activity_feed')) | |
426 | 422 | assert res.status_code == 200 |
427 | 423 | command_history = list(filter(lambda hist: hist['_id'] == command.id, res.json)) |
428 | 424 | assert len(command_history) |
445 | 441 | res = test_client.post(self.url(), data=raw_data) |
446 | 442 | |
447 | 443 | assert res.status_code == 400 |
448 | ||
449 | ||
450 | class TestListCommandViewV3(TestListCommandView, PatchableTestsMixin): | |
451 | view_class = CommandV3View | |
452 | ||
453 | def url(self, obj=None, workspace=None): | |
454 | return v2_to_v3(super().url(obj, workspace)) | |
455 | ||
456 | def check_url(self, url): | |
457 | return v2_to_v3(url) |
4 | 4 | |
5 | 5 | ''' |
6 | 6 | |
7 | from faraday.server.api.modules.comments import CommentView, CommentV3View | |
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, PatchableTestsMixin | |
10 | from tests.test_api_workspaced_base import ReadWriteAPITests | |
11 | 11 | from tests import factories |
12 | from tests.utils.url import v2_to_v3 | |
13 | 12 | |
14 | 13 | |
15 | 14 | class TestCommentAPIGeneric(ReadWriteAPITests): |
19 | 18 | api_endpoint = 'comment' |
20 | 19 | update_fields = ['text'] |
21 | 20 | patchable_fields = ['text'] |
22 | ||
23 | def check_url(self, url): | |
24 | return url | |
25 | 21 | |
26 | 22 | def _create_raw_comment(self, object_type, object_id): |
27 | 23 | return { |
90 | 86 | assert res.status_code == 201 |
91 | 87 | assert len(session.query(Comment).all()) == initial_comment_count + 1 |
92 | 88 | |
93 | url = self.check_url(self.url(workspace=self.workspace).strip('/') + '_unique/') | |
89 | url = self.url(workspace=self.workspace).strip('/') + '_unique' | |
94 | 90 | res = test_client.post(url, data=raw_comment) |
95 | 91 | assert res.status_code == 409 |
96 | 92 | assert 'object' in res.json |
105 | 101 | session.commit() |
106 | 102 | initial_comment_count = len(session.query(Comment).all()) |
107 | 103 | raw_comment = self._create_raw_comment('service', service.id) |
108 | url = self.check_url(self.url(workspace=self.workspace).strip('/') + '_unique/') | |
104 | url = self.url(workspace=self.workspace).strip('/') + '_unique' | |
109 | 105 | res = test_client.post(url, |
110 | 106 | data=raw_comment) |
111 | 107 | assert res.status_code == 201 |
125 | 121 | get_comments = test_client.get(self.url(workspace=workspace)) |
126 | 122 | expected = ['first', 'second', 'third', 'fourth'] |
127 | 123 | assert expected == [comment['text'] for comment in get_comments.json] |
128 | ||
129 | ||
130 | class TestCommentAPIGenericV3(TestCommentAPIGeneric, PatchableTestsMixin): | |
131 | view_class = CommentV3View | |
132 | ||
133 | def url(self, obj=None, workspace=None): | |
134 | return v2_to_v3(super().url(obj, workspace)) | |
135 | ||
136 | def check_url(self, url): | |
137 | return v2_to_v3(url) |
9 | 9 | from tests import factories |
10 | 10 | from tests.test_api_workspaced_base import ( |
11 | 11 | ReadWriteAPITests, |
12 | PatchableTestsMixin, | |
13 | 12 | ) |
14 | from faraday.server.api.modules.credentials import CredentialView, CredentialV3View | |
13 | from faraday.server.api.modules.credentials import CredentialView | |
15 | 14 | from faraday.server.models import Credential |
16 | 15 | from tests.factories import HostFactory, ServiceFactory |
17 | from tests.utils.url import v2_to_v3 | |
18 | 16 | |
19 | 17 | |
20 | 18 | class TestCredentialsAPIGeneric(ReadWriteAPITests): |
264 | 262 | response = test_client.get(self.url(workspace=second_workspace) + "?sort=target&sort_dir=asc") |
265 | 263 | assert response.status_code == 200 |
266 | 264 | assert sorted(credentials_target) == [v['value']['target'] for v in response.json['rows']] |
267 | ||
268 | ||
269 | class TestCredentialsAPIGenericV3(TestCredentialsAPIGeneric, PatchableTestsMixin): | |
270 | view_class = CredentialV3View | |
271 | ||
272 | def url(self, obj=None, workspace=None): | |
273 | return v2_to_v3(super().url(obj, workspace)) |
0 | 0 | import pytest |
1 | 1 | |
2 | 2 | from tests.factories import CustomFieldsSchemaFactory |
3 | from tests.test_api_non_workspaced_base import ReadWriteAPITests, PatchableTestsMixin | |
3 | from tests.test_api_non_workspaced_base import ReadWriteAPITests | |
4 | 4 | |
5 | 5 | from faraday.server.api.modules.custom_fields import CustomFieldsSchemaView |
6 | 6 | from faraday.server.models import ( |
7 | 7 | CustomFieldsSchema |
8 | 8 | ) |
9 | from tests.utils.url import v2_to_v3 | |
10 | 9 | |
11 | 10 | |
12 | 11 | @pytest.mark.usefixtures('logged_user') |
81 | 80 | assert {u'table_name': u'vulnerability', u'id': add_choice_field.id, u'field_type': u'choice', |
82 | 81 | u'field_name': u'gender', u'field_display_name': u'Gender', u'field_metadata': "['Male', 'Female']", |
83 | 82 | u'field_order': 1} in res.json |
84 | ||
85 | ||
86 | class TestVulnerabilityCustomFieldsV3(TestVulnerabilityCustomFields, PatchableTestsMixin): | |
87 | def url(self, obj=None): | |
88 | return v2_to_v3(super().url(obj)) |
15 | 15 | VulnerabilityFactory, |
16 | 16 | VulnerabilityWebFactory |
17 | 17 | ) |
18 | from tests.utils.url import v2_to_v3 | |
19 | 18 | |
20 | 19 | |
21 | 20 | @pytest.mark.usefixtures('logged_user') |
22 | 21 | class TestExportData: |
23 | 22 | |
24 | def check_url(self, url): | |
25 | return url | |
26 | ||
27 | 23 | def test_export_data_without_format(self, test_client): |
28 | 24 | workspace = WorkspaceFactory.create() |
29 | url = self.check_url(f'/v2/ws/{workspace.name}/export_data') | |
25 | url = f'/v3/ws/{workspace.name}/export_data' | |
30 | 26 | response = test_client.get(url) |
31 | 27 | assert response.status_code == 400 |
32 | 28 | |
89 | 85 | session.add(vuln_web) |
90 | 86 | session.commit() |
91 | 87 | |
92 | url = self.check_url(f'/v2/ws/{workspace.name}/export_data?format=xml_metasploit') | |
88 | url = f'/v3/ws/{workspace.name}/export_data?format=xml_metasploit' | |
93 | 89 | response = test_client.get(url) |
94 | 90 | assert response.status_code == 200 |
95 | 91 | response_xml = response.data |
142 | 138 | assert response_tree.xpath(full_xpath)[0].text == xml_file_hostnames |
143 | 139 | else: |
144 | 140 | assert response_tree.xpath(full_xpath)[0].text == xml_file_tree.xpath(full_xpath)[0].text |
145 | ||
146 | ||
147 | class TestExportDataV3(TestExportData): | |
148 | ||
149 | def check_url(self, url): | |
150 | return v2_to_v3(url) |
28 | 28 | assert len(rules) == 0, [rule.rule for rule in rules] |
29 | 29 | |
30 | 30 | |
31 | def test_v2_in_v3_endpoints(): | |
32 | exceptions = { | |
33 | '/v3/ws/<workspace_id>/activate', | |
34 | '/v3/ws/<workspace_id>/change_readonly', | |
35 | '/v3/ws/<workspace_id>/deactivate', | |
36 | '/v3/ws/<workspace_name>/hosts/bulk_delete', | |
37 | '/v3/ws/<workspace_name>/vulns/bulk_delete', | |
38 | '/v3/ws/<workspace_name>/vulns/<int:vuln_id>/attachments' | |
39 | } | |
31 | def test_v2_endpoints_removed_in_v3(): | |
32 | exceptions = set() | |
33 | actaul_rules_v2 = list(filter(lambda rule: rule.rule.startswith("/v2"), get_app().url_map.iter_rules())) | |
34 | assert len(actaul_rules_v2) == 0, actaul_rules_v2 | |
40 | 35 | rules_v2 = set( |
41 | 36 | map( |
42 | 37 | lambda rule: rule.rule.replace("v2", "v3").rstrip("/"), |
47 | 42 | map(lambda rule: rule.rule, filter(lambda rule: rule.rule.startswith("/v3"), get_app().url_map.iter_rules())) |
48 | 43 | ) |
49 | 44 | exceptions_present_v2 = rules_v2.intersection(exceptions) |
50 | assert len(exceptions_present_v2) == len(exceptions), sorted(exceptions_present_v2) | |
45 | assert len(exceptions_present_v2) == 0, sorted(exceptions_present_v2) | |
51 | 46 | exceptions_present = rules.intersection(exceptions) |
52 | 47 | assert len(exceptions_present) == 0, sorted(exceptions_present) |
53 | 48 | # We can have extra endpoints in v3 (like all the PATCHS) |
12 | 12 | class TestGetExploits(): |
13 | 13 | def test_get_exploit(self, test_client): |
14 | 14 | cve_id = "CVE-2018-1999045" |
15 | res = test_client.get(f'v2/vulners/exploits/{cve_id}') | |
15 | res = test_client.get(f'v3/vulners/exploits/{cve_id}') | |
16 | 16 | assert res.status_code == 200 |
17 | 17 | |
18 | 18 | @pytest.mark.skip() |
19 | 19 | def test_key_error(self, test_client): |
20 | 20 | cve_id = "CVE-2018-1999035ERROR" |
21 | res = test_client.get(f'v2/vulners/exploits/{cve_id}') | |
21 | res = test_client.get(f'v3/vulners/exploits/{cve_id}') | |
22 | 22 | assert res.status_code == 400 |
23 | 23 | |
24 | 24 | @pytest.mark.skip() |
25 | 25 | def test_get_exploit_with_modules(self, test_client): |
26 | 26 | cve_id = "CVE-2016-9299" |
27 | res = test_client.get(f'v2/vulners/exploits/{cve_id}') | |
27 | res = test_client.get(f'v3/vulners/exploits/{cve_id}') | |
28 | 28 | assert res.status_code == 200 |
29 | 29 | assert res.json.get('metasploit') != [] |
30 | 30 | assert res.json.get('exploitdb') != [] |
9 | 9 | |
10 | 10 | import pytz |
11 | 11 | |
12 | from tests.utils.url import v2_to_v3 | |
13 | ||
14 | 12 | from urllib.parse import urlencode |
15 | 13 | from random import choice |
16 | 14 | from sqlalchemy.orm.util import was_deleted |
22 | 20 | from tests.test_api_workspaced_base import ( |
23 | 21 | API_PREFIX, |
24 | 22 | ReadWriteAPITests, |
25 | PaginationTestsMixin, PatchableTestsMixin, | |
23 | PaginationTestsMixin, | |
26 | 24 | ) |
27 | 25 | from faraday.server.models import db, Host, Hostname |
28 | from faraday.server.api.modules.hosts import HostsView, HostsV3View | |
26 | from faraday.server.api.modules.hosts import HostsView | |
29 | 27 | from tests.factories import HostFactory, EmptyCommandFactory, WorkspaceFactory |
30 | 28 | |
31 | 29 | HOSTS_COUNT = 5 |
34 | 32 | |
35 | 33 | @pytest.mark.usefixtures('database', 'logged_user') |
36 | 34 | class TestHostAPI: |
37 | ||
38 | def check_url(self, url): | |
39 | return url | |
40 | 35 | |
41 | 36 | @pytest.fixture(autouse=True) |
42 | 37 | def load_workspace_with_hosts(self, database, session, workspace, host_factory): |
65 | 60 | |
66 | 61 | def url(self, host=None, workspace=None): |
67 | 62 | workspace = workspace or self.workspace |
68 | url = API_PREFIX + workspace.name + '/hosts/' | |
63 | url = API_PREFIX + workspace.name + '/hosts' | |
69 | 64 | if host is not None: |
70 | url += str(host.id) + '/' | |
65 | url += '/' + str(host.id) | |
71 | 66 | return url |
72 | 67 | |
73 | 68 | def services_url(self, host, workspace=None): |
74 | return self.url(host, workspace) + 'services/' | |
69 | return self.url(host, workspace) + '/services' | |
75 | 70 | |
76 | 71 | def compare_results(self, hosts, response): |
77 | 72 | """ |
617 | 612 | vulnerability_factory.create(service=service, host=None, workspace=workspace) |
618 | 613 | session.commit() |
619 | 614 | |
620 | res = test_client.get(self.check_url(urljoin(self.url(host), 'services/'))) | |
615 | res = test_client.get(urljoin(self.url(host), 'services')) | |
621 | 616 | assert res.status_code == 200 |
622 | 617 | assert res.json[0]['vulns'] == 1 |
623 | 618 | |
758 | 753 | 'csrf_token': csrf_token |
759 | 754 | } |
760 | 755 | headers = {'Content-type': 'multipart/form-data'} |
761 | res = test_client.post(self.check_url(f'/v2/ws/{ws.name}/hosts/bulk_create/'), | |
756 | res = test_client.post(f'/v3/ws/{ws.name}/hosts/bulk_create', | |
762 | 757 | data=data, headers=headers, use_json_data=False) |
763 | 758 | assert res.status_code == 200 |
764 | 759 | assert res.json['hosts_created'] == expected_created_hosts |
765 | 760 | assert res.json['hosts_with_errors'] == 0 |
766 | 761 | assert session.query(Host).filter_by(description="test_host").count() == expected_created_hosts |
767 | 762 | |
763 | @pytest.mark.skip("This was a v2 test, will be reimplemented") | |
768 | 764 | def test_bulk_delete_hosts(self, test_client, session): |
769 | 765 | ws = WorkspaceFactory.create(name="abc") |
770 | 766 | host_1 = HostFactory.create(workspace=ws) |
773 | 769 | hosts_ids = [host_1.id, host_2.id] |
774 | 770 | request_data = {'hosts_ids': hosts_ids} |
775 | 771 | |
776 | delete_response = test_client.delete(self.check_url(f'/v2/ws/{ws.name}/hosts/bulk_delete/'), data=request_data) | |
772 | delete_response = test_client.delete(f'/v3/ws/{ws.name}/hosts/bulk_delete', data=request_data) | |
777 | 773 | |
778 | 774 | deleted_hosts = delete_response.json['deleted_hosts'] |
779 | 775 | host_count_after_delete = db.session.query(Host).filter( |
784 | 780 | assert deleted_hosts == len(hosts_ids) |
785 | 781 | assert host_count_after_delete == 0 |
786 | 782 | |
783 | @pytest.mark.skip("This was a v2 test, will be reimplemented") | |
787 | 784 | def test_bulk_delete_hosts_without_hosts_ids(self, test_client): |
788 | 785 | ws = WorkspaceFactory.create(name="abc") |
789 | 786 | request_data = {'hosts_ids': []} |
790 | 787 | |
791 | delete_response = test_client.delete(self.check_url(f'/v2/ws/{ws.name}/hosts/bulk_delete/'), data=request_data) | |
788 | delete_response = test_client.delete(f'/v3/ws/{ws.name}/hosts/bulk_delete', data=request_data) | |
792 | 789 | |
793 | 790 | assert delete_response.status_code == 400 |
794 | 791 | |
792 | @pytest.mark.skip("This was a v2 test, will be reimplemented") | |
795 | 793 | def test_bulk_delete_hosts_from_another_workspace(self, test_client, session): |
796 | 794 | workspace_1 = WorkspaceFactory.create(name='workspace_1') |
797 | 795 | host_of_ws_1 = HostFactory.create(workspace=workspace_1) |
801 | 799 | |
802 | 800 | # Try to delete workspace_2's host from workspace_1 |
803 | 801 | request_data = {'hosts_ids': [host_of_ws_2.id]} |
804 | url = self.check_url(f'/v2/ws/{workspace_1.name}/hosts/bulk_delete/') | |
802 | url = f'/v3/ws/{workspace_1.name}/hosts/bulk_delete' | |
805 | 803 | delete_response = test_client.delete(url, data=request_data) |
806 | 804 | |
807 | 805 | assert delete_response.json['deleted_hosts'] == 0 |
808 | 806 | |
807 | @pytest.mark.skip("This was a v2 test, will be reimplemented") | |
809 | 808 | def test_bulk_delete_hosts_invalid_characters_in_request(self, test_client): |
810 | 809 | ws = WorkspaceFactory.create(name="abc") |
811 | 810 | request_data = {'hosts_ids': [-1, 'test']} |
812 | delete_response = test_client.delete(self.check_url(f'/v2/ws/{ws.name}/hosts/bulk_delete/'), data=request_data) | |
811 | delete_response = test_client.delete(f'/v3/ws/{ws.name}/hosts/bulk_delete', data=request_data) | |
813 | 812 | |
814 | 813 | assert delete_response.json['deleted_hosts'] == 0 |
815 | 814 | |
815 | @pytest.mark.skip("This was a v2 test, will be reimplemented") | |
816 | 816 | def test_bulk_delete_hosts_wrong_content_type(self, test_client, session): |
817 | 817 | ws = WorkspaceFactory.create(name="abc") |
818 | 818 | host_1 = HostFactory.create(workspace=ws) |
824 | 824 | headers = [('content-type', 'text/xml')] |
825 | 825 | |
826 | 826 | delete_response = test_client.delete( |
827 | self.check_url(f'/v2/ws/{ws.name}/hosts/bulk_delete/'), | |
827 | f'/v3/ws/{ws.name}/hosts/bulk_delete', | |
828 | 828 | data=request_data, |
829 | 829 | headers=headers) |
830 | 830 | |
831 | 831 | assert delete_response.status_code == 400 |
832 | ||
833 | ||
834 | class TestHostAPIV3(TestHostAPI): | |
835 | def url(self, host=None, workspace=None): | |
836 | return v2_to_v3(super().url(host, workspace)) | |
837 | ||
838 | def check_url(self, url): | |
839 | return v2_to_v3(url) | |
840 | ||
841 | def services_url(self, host, workspace=None): | |
842 | return self.url(host, workspace) + '/services' | |
843 | ||
844 | @pytest.mark.skip(reason="To be reimplemented") | |
845 | def test_bulk_delete_hosts(self, test_client, session): | |
846 | pass | |
847 | ||
848 | @pytest.mark.skip(reason="To be reimplemented") | |
849 | def test_bulk_delete_hosts_without_hosts_ids(self, test_client): | |
850 | pass | |
851 | ||
852 | @pytest.mark.skip(reason="To be reimplemented") | |
853 | def test_bulk_delete_hosts_from_another_workspace(self, test_client, session): | |
854 | pass | |
855 | ||
856 | @pytest.mark.skip(reason="To be reimplemented") | |
857 | def test_bulk_delete_hosts_invalid_characters_in_request(self, test_client): | |
858 | pass | |
859 | ||
860 | @pytest.mark.skip(reason="To be reimplemented") | |
861 | def test_bulk_delete_hosts_wrong_content_type(self, test_client, session): | |
862 | pass | |
863 | 832 | |
864 | 833 | |
865 | 834 | class TestHostAPIGeneric(ReadWriteAPITests, PaginationTestsMixin): |
1132 | 1101 | index_in_response_hosts = response_hosts.index(host) |
1133 | 1102 | |
1134 | 1103 | assert index_in_hosts_ids == index_in_response_hosts |
1135 | ||
1136 | ||
1137 | class TestHostAPIGenericV3(TestHostAPIGeneric, PatchableTestsMixin): | |
1138 | view_class = HostsV3View | |
1139 | ||
1140 | def url(self, obj=None, workspace=None): | |
1141 | return v2_to_v3(super().url(obj, workspace)) | |
1142 | 1104 | |
1143 | 1105 | |
1144 | 1106 | def host_json(): |
1181 | 1143 | @given(HostData) |
1182 | 1144 | def send_api_request(raw_data): |
1183 | 1145 | ws_name = host_with_hostnames.workspace.name |
1184 | res = test_client.post(f'/v2/ws/{ws_name}/vulns/', | |
1185 | data=raw_data) | |
1186 | assert res.status_code in [201, 400, 409] | |
1187 | ||
1188 | @given(HostData) | |
1189 | def send_api_request_v3(raw_data): | |
1190 | ws_name = host_with_hostnames.workspace.name | |
1191 | 1146 | res = test_client.post(f'/v3/ws/{ws_name}/vulns', |
1192 | 1147 | data=raw_data) |
1193 | 1148 | assert res.status_code in [201, 400, 409] |
1194 | 1149 | |
1195 | 1150 | send_api_request() |
1196 | send_api_request_v3() |
11 | 11 | class TestAPIInfoEndpoint: |
12 | 12 | |
13 | 13 | def test_api_info(self, test_client): |
14 | response = test_client.get('v2/info') | |
15 | assert response.status_code == 200 | |
16 | assert response.json['Faraday Server'] == 'Running' | |
17 | ||
18 | def test_api_info_v3(self, test_client): | |
19 | 14 | response = test_client.get('v3/info') |
20 | 15 | assert response.status_code == 200 |
21 | 16 | assert response.json['Faraday Server'] == 'Running' |
22 | 17 | |
23 | 18 | def test_get_config(self, test_client): |
19 | from faraday import __version__ | |
24 | 20 | res = test_client.get('/config') |
25 | 21 | assert res.status_code == 200 |
26 | assert res.json['lic_db'] == 'faraday_licenses' | |
22 | assert __version__ in res.json['ver'] |
4 | 4 | See the file 'doc/LICENSE' for the license information |
5 | 5 | |
6 | 6 | ''' |
7 | from tests.utils.url import v2_to_v3 | |
8 | 7 | |
9 | 8 | """Tests for many API endpoints that do not depend on workspace_name""" |
10 | 9 | |
13 | 12 | from hypothesis import given, strategies as st |
14 | 13 | |
15 | 14 | from tests import factories |
16 | from tests.test_api_non_workspaced_base import ReadWriteAPITests, API_PREFIX, PatchableTestsMixin | |
15 | from tests.test_api_non_workspaced_base import ReadWriteAPITests, API_PREFIX | |
17 | 16 | from faraday.server.models import ( |
18 | 17 | License, |
19 | 18 | ) |
35 | 34 | api_endpoint = 'licenses' |
36 | 35 | patchable_fields = ["products"] |
37 | 36 | |
37 | # @pytest.mark.skip(reason="Not a license actually test") | |
38 | 38 | def test_envelope_list(self, test_client, app): |
39 | 39 | LicenseEnvelopedView.register(app) |
40 | 40 | original_res = test_client.get(self.url()) |
41 | 41 | assert original_res.status_code == 200 |
42 | new_res = test_client.get(API_PREFIX + 'test_envelope_list/') | |
42 | new_res = test_client.get(API_PREFIX + 'test_envelope_list') | |
43 | 43 | assert new_res.status_code == 200 |
44 | 44 | |
45 | 45 | assert new_res.json == {"object_list": original_res.json} |
51 | 51 | res = test_client.get(self.url(obj=lic)) |
52 | 52 | assert res.status_code == 200 |
53 | 53 | assert res.json['notes'] == 'A great note. License' |
54 | ||
55 | ||
56 | class TestLicensesAPIV3(TestLicensesAPI, PatchableTestsMixin): | |
57 | def url(self, obj=None): | |
58 | return v2_to_v3(super().url(obj)) | |
59 | ||
60 | @pytest.mark.skip(reason="Not a license actually test") | |
61 | def test_envelope_list(self, test_client, app): | |
62 | pass | |
63 | 54 | |
64 | 55 | |
65 | 56 | def license_json(): |
92 | 83 | def send_api_request(raw_data): |
93 | 84 | raw_data['start'] = pytz.UTC.localize(raw_data['start']).isoformat() |
94 | 85 | raw_data['end'] = pytz.UTC.localize(raw_data['end']).isoformat() |
95 | res = test_client.post('v2/licenses/', data=raw_data) | |
96 | assert res.status_code in [201, 400, 409] | |
97 | ||
98 | @given(LicenseData) | |
99 | def send_api_request_v3(raw_data): | |
100 | raw_data['start'] = pytz.UTC.localize(raw_data['start']).isoformat() | |
101 | raw_data['end'] = pytz.UTC.localize(raw_data['end']).isoformat() | |
102 | 86 | res = test_client.post('v3/licenses/', data=raw_data) |
103 | 87 | assert res.status_code in [201, 400, 409] |
104 | 88 | |
105 | 89 | send_api_request() |
106 | send_api_request_v3() |
5 | 5 | from faraday.server.models import User |
6 | 6 | from faraday.server.web import get_app |
7 | 7 | from tests import factories |
8 | from tests.utils.url import v2_to_v3 | |
9 | 8 | |
10 | 9 | |
11 | 10 | class TestLogin: |
12 | ||
13 | def check_url(self, url): | |
14 | return url | |
15 | ||
16 | 11 | def test_case_bug_with_username(self, test_client, session): |
17 | 12 | """ |
18 | 13 | When the user case does not match the one in database, |
23 | 18 | active=True, |
24 | 19 | username='Susan', |
25 | 20 | password=hash_password('pepito'), |
26 | role='pentester') | |
21 | roles=['pentester']) | |
27 | 22 | session.add(susan) |
28 | 23 | session.commit() |
29 | 24 | # we use lower case username, but in db is Capitalized |
44 | 39 | active=True, |
45 | 40 | username='alice', |
46 | 41 | password=hash_password('passguord'), |
47 | role='pentester') | |
42 | roles=['pentester']) | |
48 | 43 | session.add(alice) |
49 | 44 | session.commit() |
50 | 45 | |
62 | 57 | |
63 | 58 | headers = {'Authentication-Token': res.json['response']['user']['authentication_token']} |
64 | 59 | |
65 | ws = test_client.get(self.check_url('/v2/ws/wonderland/'), headers=headers) | |
60 | ws = test_client.get('/v3/ws/wonderland', headers=headers) | |
66 | 61 | assert ws.status_code == 200 |
67 | 62 | |
68 | 63 | def test_case_ws_with_invalid_authentication_token(self, test_client, session): |
76 | 71 | active=True, |
77 | 72 | username='alice', |
78 | 73 | password=hash_password('passguord'), |
79 | role='pentester') | |
74 | roles=['pentester']) | |
80 | 75 | session.add(alice) |
81 | 76 | session.commit() |
82 | 77 | |
89 | 84 | |
90 | 85 | headers = {'Authorization': b'Token ' + token} |
91 | 86 | |
92 | ws = test_client.get(self.check_url('/v2/ws/wonderland/'), headers=headers) | |
87 | ws = test_client.get('/v3/ws/wonderland', headers=headers) | |
93 | 88 | assert ws.status_code == 401 |
94 | 89 | |
95 | 90 | @pytest.mark.usefixtures('logged_user') |
96 | 91 | def test_retrieve_token_from_api_and_use_it(self, test_client, session): |
97 | res = test_client.get(self.check_url('/v2/token/')) | |
92 | res = test_client.get('/v3/token') | |
98 | 93 | cookies = [cookie.name for cookie in test_client.cookie_jar] |
99 | 94 | assert "faraday_session_2" in cookies |
100 | 95 | assert res.status_code == 200 |
105 | 100 | session.commit() |
106 | 101 | # clean cookies make sure test_client has no session |
107 | 102 | test_client.cookie_jar.clear() |
108 | res = test_client.get(self.check_url('/v2/ws/wonderland/'), headers=headers) | |
103 | res = test_client.get('/v3/ws/wonderland', headers=headers) | |
109 | 104 | assert res.status_code == 200 |
110 | 105 | assert 'Set-Cookie' not in res.headers |
111 | 106 | cookies = [cookie.name for cookie in test_client.cookie_jar] |
114 | 109 | def test_cant_retrieve_token_unauthenticated(self, test_client): |
115 | 110 | # clean cookies make sure test_client has no session |
116 | 111 | test_client.cookie_jar.clear() |
117 | res = test_client.get(self.check_url('/v2/token/')) | |
112 | res = test_client.get('/v3/token') | |
118 | 113 | |
119 | 114 | assert res.status_code == 401 |
120 | 115 | |
121 | 116 | @pytest.mark.usefixtures('logged_user') |
122 | 117 | def test_token_expires_after_password_change(self, test_client, session): |
123 | 118 | user = User.query.filter_by(username="test").first() |
124 | res = test_client.get(self.check_url('/v2/token/')) | |
119 | res = test_client.get('/v3/token') | |
125 | 120 | |
126 | 121 | assert res.status_code == 200 |
127 | 122 | |
134 | 129 | |
135 | 130 | # clean cookies make sure test_client has no session |
136 | 131 | test_client.cookie_jar.clear() |
137 | res = test_client.get(self.check_url('/v2/ws/'), headers=headers) | |
132 | res = test_client.get('/v3/ws', headers=headers) | |
138 | 133 | assert res.status_code == 401 |
139 | 134 | |
140 | 135 | def test_null_caracters(self, test_client, session): |
146 | 141 | active=True, |
147 | 142 | username='asdasd', |
148 | 143 | password=hash_password('asdasd'), |
149 | role='pentester') | |
144 | roles=['pentester']) | |
150 | 145 | session.add(alice) |
151 | 146 | session.commit() |
152 | 147 | |
165 | 160 | |
166 | 161 | headers = {'Authentication-Token': res.json['response']['user']['authentication_token']} |
167 | 162 | |
168 | ws = test_client.get(self.check_url('/v2/ws/wonderland/'), headers=headers) | |
163 | ws = test_client.get('/v3/ws/wonderland', headers=headers) | |
169 | 164 | assert ws.status_code == 200 |
170 | 165 | |
171 | 166 | def test_login_remember_me(self, test_client, session): |
177 | 172 | active=True, |
178 | 173 | username='susan', |
179 | 174 | password=hash_password('pepito'), |
180 | role='pentester') | |
175 | roles=['pentester']) | |
181 | 176 | session.add(susan) |
182 | 177 | session.commit() |
183 | 178 | |
201 | 196 | active=True, |
202 | 197 | username='susan', |
203 | 198 | password=hash_password('pepito'), |
204 | role='pentester') | |
199 | roles=['pentester']) | |
205 | 200 | session.add(susan) |
206 | 201 | session.commit() |
207 | 202 | login_payload = { |
224 | 219 | active=True, |
225 | 220 | username='susan', |
226 | 221 | password=hash_password('pepito'), |
227 | role='pentester') | |
222 | roles=['pentester']) | |
228 | 223 | session.add(susan) |
229 | 224 | session.commit() |
230 | 225 | login_payload = { |
235 | 230 | assert res.status_code == 200 |
236 | 231 | cookies = [cookie.name for cookie in test_client.cookie_jar] |
237 | 232 | assert "remember_token" not in cookies |
238 | ||
239 | ||
240 | class TestLoginV3(TestLogin): | |
241 | def check_url(self, url): | |
242 | return v2_to_v3(url) |
11 | 11 | import pytest |
12 | 12 | from sqlalchemy.orm.util import was_deleted |
13 | 13 | |
14 | API_PREFIX = '/v2/' | |
14 | API_PREFIX = '/v3/' | |
15 | 15 | OBJECT_COUNT = 5 |
16 | 16 | |
17 | 17 | |
40 | 40 | return obj |
41 | 41 | |
42 | 42 | def url(self, obj=None): |
43 | url = API_PREFIX + self.api_endpoint + '/' | |
43 | url = API_PREFIX + self.api_endpoint | |
44 | 44 | if obj is not None: |
45 | 45 | id_ = str(getattr(obj, self.lookup_field)) if isinstance( |
46 | 46 | obj, self.model) else str(obj) |
47 | url += id_ + u'/' | |
47 | url += u'/' + id_ | |
48 | 48 | return url |
49 | 49 | |
50 | 50 | |
102 | 102 | @pytest.mark.usefixtures('logged_user') |
103 | 103 | class UpdateTestsMixin: |
104 | 104 | |
105 | @pytest.mark.parametrize("method", ["PUT"]) | |
105 | @staticmethod | |
106 | def control_data(test_suite, data: dict) -> dict: | |
107 | return {key: value for (key, value) in data.items() if key in test_suite.patchable_fields} | |
108 | ||
109 | @pytest.mark.parametrize("method", ["PUT", "PATCH"]) | |
106 | 110 | def test_update_an_object(self, test_client, logged_user, method): |
107 | 111 | data = self.factory.build_dict() |
108 | 112 | if method == "PUT": |
109 | 113 | res = test_client.put(self.url(self.first_object), data=data) |
110 | 114 | elif method == "PATCH": |
111 | data = PatchableTestsMixin.control_data(self, data) | |
115 | data = self.control_data(self, data) | |
112 | 116 | res = test_client.patch(self.url(self.first_object), data=data) |
113 | 117 | assert res.status_code == 200, (res.status_code, res.json) |
114 | 118 | assert self.model.query.count() == OBJECT_COUNT |
132 | 136 | """To do this the user should use a PATCH request""" |
133 | 137 | res = test_client.put(self.url(self.first_object), data={}) |
134 | 138 | assert res.status_code == 400, (res.status_code, res.json) |
135 | ||
136 | ||
137 | @pytest.mark.usefixtures('logged_user') | |
138 | class PatchableTestsMixin(UpdateTestsMixin): | |
139 | ||
140 | @staticmethod | |
141 | def control_data(test_suite, data: dict) -> dict: | |
142 | return {key: value for (key, value) in data.items() if key in test_suite.patchable_fields} | |
143 | ||
144 | @pytest.mark.parametrize("method", ["PUT", "PATCH"]) | |
145 | def test_update_an_object(self, test_client, logged_user, method): | |
146 | super().test_update_an_object(test_client, logged_user, method) | |
147 | ||
148 | @pytest.mark.parametrize("method", ["PUT", "PATCH"]) | |
149 | def test_update_fails_with_existing(self, test_client, session, method): | |
150 | super().test_update_fails_with_existing(test_client, session, method) | |
151 | 139 | |
152 | 140 | def test_patch_update_an_object_does_not_fail_with_partial_data(self, test_client, logged_user): |
153 | 141 | """To do this the user should use a PATCH request""" |
1 | 1 | from tests.factories import UserFactory |
2 | 2 | from faraday.server.models import User |
3 | 3 | from faraday.server.api.modules.preferences import PreferencesView |
4 | from tests.utils.url import v2_to_v3 | |
5 | 4 | |
6 | 5 | |
7 | 6 | # pytest.fixture('logged_user') |
41 | 40 | response = test_client.post(self.url(), data=data) |
42 | 41 | |
43 | 42 | assert response.status_code == 400 |
44 | ||
45 | ||
46 | class TestPreferencesV3(TestPreferences): | |
47 | def url(self, obj=None): | |
48 | return v2_to_v3(super().url(obj)) |
9 | 9 | import pytest |
10 | 10 | |
11 | 11 | from tests.factories import SearchFilterFactory, UserFactory |
12 | from tests.test_api_non_workspaced_base import ReadWriteAPITests, PatchableTestsMixin | |
12 | from tests.test_api_non_workspaced_base import ReadWriteAPITests | |
13 | 13 | from tests.test_api_agent import logout |
14 | 14 | from tests.conftest import login_as |
15 | 15 | from faraday.server.models import SearchFilter |
16 | 16 | |
17 | 17 | from faraday.server.api.modules.search_filter import SearchFilterView |
18 | from tests.utils.url import v2_to_v3 | |
19 | 18 | |
20 | 19 | |
21 | 20 | @pytest.mark.usefixtures('logged_user') |
103 | 102 | res = test_client.delete(self.url(user_filter)) |
104 | 103 | assert res.status_code == 404 |
105 | 104 | |
106 | @pytest.mark.parametrize("method", ["PUT"]) | |
105 | @pytest.mark.parametrize("method", ["PUT", "PATCH"]) | |
107 | 106 | def test_update_an_object(self, test_client, logged_user, method): |
108 | 107 | self.first_object.creator = logged_user |
109 | 108 | super().test_update_an_object(test_client, logged_user, method) |
116 | 115 | self.first_object.creator = logged_user |
117 | 116 | super().test_delete(test_client, logged_user) |
118 | 117 | |
119 | ||
120 | @pytest.mark.usefixtures('logged_user') | |
121 | class TestSearchFilterAPIV3(TestSearchFilterAPI, PatchableTestsMixin): | |
122 | def url(self, obj=None): | |
123 | return v2_to_v3(super().url(obj)) | |
124 | ||
125 | @pytest.mark.parametrize("method", ["PUT", "PATCH"]) | |
126 | def test_update_an_object(self, test_client, logged_user, method): | |
127 | super().test_update_an_object(test_client, logged_user, method) | |
128 | ||
129 | 118 | def test_patch_update_an_object_does_not_fail_with_partial_data(self, test_client, logged_user): |
130 | 119 | self.first_object.creator = logged_user |
131 | 120 | super().test_update_an_object_fails_with_empty_dict(test_client, logged_user) |
4 | 4 | See the file 'doc/LICENSE' for the license information |
5 | 5 | |
6 | 6 | ''' |
7 | from tests.utils.url import v2_to_v3 | |
8 | ||
9 | 7 | """Tests for many API endpoints that do not depend on workspace_name""" |
10 | 8 | try: |
11 | 9 | from urllib import urlencode |
15 | 13 | import pytest |
16 | 14 | import json |
17 | 15 | |
18 | from faraday.server.api.modules.services import ServiceView, ServiceV3View | |
16 | from faraday.server.api.modules.services import ServiceView | |
19 | 17 | from tests import factories |
20 | from tests.test_api_workspaced_base import ReadWriteAPITests, PatchableTestsMixin | |
18 | from tests.test_api_workspaced_base import ReadWriteAPITests | |
21 | 19 | from faraday.server.models import ( |
22 | 20 | Service |
23 | 21 | ) |
232 | 230 | updated_service = Service.query.filter_by(id=service.id).first() |
233 | 231 | assert updated_service.port == 221 |
234 | 232 | |
235 | @pytest.mark.parametrize("method", ["PUT"]) | |
233 | @pytest.mark.parametrize("method", ["PUT", "PATCH"]) | |
236 | 234 | def test_update_cant_change_id(self, test_client, session, method): |
237 | 235 | service = self.factory() |
238 | 236 | host = HostFactory.create() |
336 | 334 | res = test_client.post(self.url(), data=data) |
337 | 335 | print(res.data) |
338 | 336 | assert res.status_code == 400 |
339 | ||
340 | ||
341 | class TestListServiceViewV3(TestListServiceView, PatchableTestsMixin): | |
342 | view_class = ServiceV3View | |
343 | ||
344 | def url(self, obj=None, workspace=None): | |
345 | return v2_to_v3(super().url(obj, workspace)) | |
346 | ||
347 | @pytest.mark.parametrize("method", ["PUT", "PATCH"]) | |
348 | def test_update_cant_change_id(self, test_client, session, method): | |
349 | super().test_update_cant_change_id(test_client, session, method) |
5 | 5 | ''' |
6 | 6 | |
7 | 7 | import pytest |
8 | ||
9 | from faraday.server.models import Role | |
8 | 10 | from tests.conftest import login_as |
9 | 11 | |
10 | 12 | |
16 | 18 | |
17 | 19 | @pytest.mark.parametrize('role', ['admin', 'pentester', 'client', 'asset_owner']) |
18 | 20 | def test_session_when_user_is_logged_with_different_roles(self, test_client, session, user, role): |
19 | user.role = role | |
21 | user.roles = [Role.query.filter(Role.name == role).one()] | |
20 | 22 | session.commit() |
21 | 23 | login_as(test_client, user) |
22 | 24 | res = test_client.get('/session') |
23 | assert res.json['role'] == role | |
25 | assert role in res.json['roles'] | |
24 | 26 | |
25 | 27 | |
26 | 28 | class TestSessionNotLogged: |
0 | """ | |
1 | Faraday Penetration Test IDE | |
2 | Copyright (C) 2019 Infobyte LLC (http://www.infobytesec.com/) | |
3 | See the file 'doc/LICENSE' for the license information | |
4 | """ | |
5 | import pytest | |
6 | from unittest import mock | |
7 | ||
8 | from faraday.settings.reports import ReportsSettings | |
9 | ||
10 | ||
11 | @mock.patch("faraday.settings.reports.ReportsSettings.must_restart_threads", False) | |
12 | @pytest.mark.usefixtures('logged_user') | |
13 | class TestServerConfig: | |
14 | def test_update_server_config(self, test_client): | |
15 | new_config = {"ignore_info_severity": True, 'custom_plugins_folder': ''} | |
16 | response = test_client.patch("/v3/settings/reports", json=new_config) | |
17 | assert response.status_code == 200 | |
18 | assert ReportsSettings.settings.ignore_info_severity == new_config["ignore_info_severity"] | |
19 | ||
20 | def test_get_valid_settings(self, test_client): | |
21 | response = test_client.get("/v3/settings/reports") | |
22 | assert response.status_code == 200 | |
23 | assert "ignore_info_severity" in response.json | |
24 | assert "custom_plugins_folder" in response.json | |
25 | ||
26 | def test_get_invalid_settings(self, test_client): | |
27 | response = test_client.get("/v3/settings/invalid") | |
28 | assert response.status_code == 404 | |
29 | ||
30 | def test_update_settings_with_empty_json(self, test_client): | |
31 | response = test_client.patch("/v3/settings/reports", json={}) | |
32 | assert response.status_code == 400 | |
33 | assert "messages" in response.json | |
34 | ||
35 | def test_update_settings_with_invalid_value(self, test_client): | |
36 | data = { | |
37 | "INVALID_VALUE": "", | |
38 | "ignore_info_severity": True | |
39 | } | |
40 | response = test_client.patch("/v3/settings/reports", json=data) | |
41 | assert response.status_code == 400 | |
42 | assert "messages" in response.json | |
43 | ||
44 | def test_update_settings_with_valid_value(self, test_client): | |
45 | response = test_client.get("/v3/settings/reports") | |
46 | assert response.status_code == 200 | |
47 | actual_value = response.json['ignore_info_severity'] | |
48 | data = {'ignore_info_severity': not actual_value, 'custom_plugins_folder': response.json['custom_plugins_folder']} | |
49 | response = test_client.patch("/v3/settings/reports", json=data) | |
50 | assert response.status_code == 200 | |
51 | assert response.json["ignore_info_severity"] != actual_value |
13 | 13 | from faraday.server.threads.reports_processor import REPORTS_QUEUE |
14 | 14 | |
15 | 15 | from faraday.server.models import Host, Service, Command |
16 | from tests.utils.url import v2_to_v3 | |
17 | 16 | |
18 | 17 | |
19 | 18 | @pytest.mark.usefixtures('logged_user') |
20 | 19 | class TestFileUpload: |
21 | ||
22 | def check_url(self, url): | |
23 | return url | |
24 | 20 | |
25 | 21 | def test_file_upload(self, test_client, session, csrf_token, logged_user): |
26 | 22 | ws = WorkspaceFactory.create(name="abc") |
36 | 32 | } |
37 | 33 | |
38 | 34 | res = test_client.post( |
39 | self.check_url(f'/v2/ws/{ws.name}/upload_report'), | |
35 | f'/v3/ws/{ws.name}/upload_report', | |
40 | 36 | data=data, |
41 | 37 | use_json_data=False) |
42 | 38 | |
73 | 69 | session.add(ws) |
74 | 70 | session.commit() |
75 | 71 | |
76 | res = test_client.post(self.check_url(f'/v2/ws/{ws.name}/upload_report')) | |
72 | res = test_client.post(f'/v3/ws/{ws.name}/upload_report') | |
77 | 73 | |
78 | 74 | assert res.status_code == 400 |
79 | 75 | |
91 | 87 | } |
92 | 88 | |
93 | 89 | res = test_client.post( |
94 | self.check_url(f'/v2/ws/{ws.name}/upload_report'), | |
90 | f'/v3/ws/{ws.name}/upload_report', | |
95 | 91 | data=data, |
96 | 92 | use_json_data=False) |
97 | 93 | |
112 | 108 | 'csrf_token': csrf_token |
113 | 109 | } |
114 | 110 | res = test_client.post( |
115 | self.check_url(f'/v2/ws/{ws.name}/upload_report'), | |
111 | f'/v3/ws/{ws.name}/upload_report', | |
116 | 112 | data=data, |
117 | 113 | use_json_data=False |
118 | 114 | ) |
119 | 115 | |
120 | 116 | assert res.status_code == 404 |
121 | ||
122 | ||
123 | class TestFileUploadV3(TestFileUpload): | |
124 | def check_url(self, url): | |
125 | return v2_to_v3(url) |
15 | 15 | from io import BytesIO, StringIO |
16 | 16 | from posixpath import join as urljoin |
17 | 17 | |
18 | from tests.utils.url import v2_to_v3 | |
19 | ||
20 | 18 | try: |
21 | 19 | from urllib import urlencode |
22 | 20 | except ImportError: |
32 | 30 | from faraday.server.api.modules.vulns import ( |
33 | 31 | VulnerabilityFilterSet, |
34 | 32 | VulnerabilitySchema, |
35 | VulnerabilityView, | |
36 | VulnerabilityV3View | |
33 | VulnerabilityView | |
37 | 34 | ) |
38 | 35 | from faraday.server.fields import FaradayUploadedFile |
39 | 36 | from faraday.server.schemas import NullToBlankString |
40 | 37 | from tests import factories |
41 | 38 | from tests.conftest import TEST_DATA_PATH |
42 | 39 | from tests.test_api_workspaced_base import ( |
43 | ReadWriteAPITests, PatchableTestsMixin | |
40 | ReadWriteAPITests | |
44 | 41 | ) |
45 | 42 | from faraday.server.models import ( |
46 | 43 | VulnerabilityGeneric, |
162 | 159 | view_class = VulnerabilityView |
163 | 160 | patchable_fields = ['description'] |
164 | 161 | |
165 | def check_url(self, url): | |
166 | return url | |
167 | ||
168 | 162 | def test_backward_json_compatibility(self, test_client, second_workspace, session): |
169 | 163 | new_obj = self.factory.create(workspace=second_workspace) |
170 | 164 | session.add(new_obj) |
329 | 323 | assert res.status_code == 400 |
330 | 324 | assert b'Shorter than minimum length 1' in res.data |
331 | 325 | |
326 | def test_create_cannot_create_vuln_with_empty_fields( | |
327 | self, session, test_client): | |
328 | # I'm using this to test the NonBlankColumn which works for | |
329 | # all models. Think twice before removing this test | |
330 | session.commit() # flush host_with_hostnames | |
331 | raw_data = _create_post_data_vulnerability( | |
332 | name='', | |
333 | vuln_type='', | |
334 | parent_id='', | |
335 | parent_type='', | |
336 | refs=[], | |
337 | policyviolations=[], | |
338 | description='', | |
339 | severity='', | |
340 | ) | |
341 | res = test_client.post(self.url(), data=raw_data) | |
342 | assert res.status_code == 400 | |
343 | ||
332 | 344 | def test_create_create_vuln_with_empty_desc_success( |
333 | 345 | self, host, session, test_client): |
334 | 346 | # I'm using this to test the NonBlankColumn which works for |
372 | 384 | assert filename in res.json['_attachments'] |
373 | 385 | attachment.close() |
374 | 386 | # check the attachment can be downloaded |
375 | res = test_client.get(self.check_url(urljoin(self.url(), f'{vuln_id}/attachment/{filename}/'))) | |
387 | res = test_client.get(urljoin(self.url(), f'{vuln_id}/attachment/{filename}')) | |
376 | 388 | assert res.status_code == 200 |
377 | 389 | assert res.data == file_content |
378 | 390 | |
379 | res = test_client.get(self.check_url(urljoin( | |
391 | res = test_client.get(urljoin( | |
380 | 392 | self.url(), |
381 | f'{vuln_id}/attachment/notexistingattachment.png/' | |
382 | ))) | |
393 | f'{vuln_id}/attachment/notexistingattachment.png' | |
394 | )) | |
383 | 395 | assert res.status_code == 404 |
384 | 396 | |
385 | 397 | @pytest.mark.usefixtures('ignore_nplusone') |
403 | 415 | res = test_client.put(self.url(obj=vuln, workspace=self.workspace), data=raw_data) |
404 | 416 | assert res.status_code == 200 |
405 | 417 | filename = attachment.name.split('/')[-1] |
406 | res = test_client.get(self.check_url(urljoin( | |
407 | self.url(), f'{vuln.id}/attachment/{filename}/' | |
408 | ))) | |
418 | res = test_client.get(urljoin( | |
419 | self.url(), f'{vuln.id}/attachment/{filename}' | |
420 | )) | |
409 | 421 | assert res.status_code == 200 |
410 | 422 | assert res.data == file_content |
411 | 423 | |
427 | 439 | assert res.status_code == 200 |
428 | 440 | |
429 | 441 | # verify that the old file was deleted and the new one exists |
430 | res = test_client.get(self.check_url(urljoin( | |
431 | self.url(), f'{vuln.id}/attachment/{filename}/' | |
432 | ))) | |
442 | res = test_client.get(urljoin( | |
443 | self.url(), f'{vuln.id}/attachment/{filename}' | |
444 | )) | |
433 | 445 | assert res.status_code == 404 |
434 | res = test_client.get(self.check_url(urljoin( | |
435 | self.url(), f'{vuln.id}/attachment/{new_filename}/' | |
436 | ))) | |
446 | res = test_client.get(urljoin( | |
447 | self.url(), f'{vuln.id}/attachment/{new_filename}' | |
448 | )) | |
437 | 449 | assert res.status_code == 200 |
438 | 450 | assert res.data == file_content |
439 | 451 | |
451 | 463 | session.add(new_attach) |
452 | 464 | session.commit() |
453 | 465 | |
454 | if 'v2' in self.view_class.route_prefix: | |
455 | route_part = 'attachments' | |
456 | else: | |
457 | route_part = 'attachment' | |
458 | ||
459 | res = test_client.get(self.check_url(urljoin(self.url(workspace=workspace), f'{vuln.id}/{route_part}/'))) | |
466 | res = test_client.get(urljoin(self.url(workspace=workspace), f'{vuln.id}/attachment')) | |
460 | 467 | assert res.status_code == 200 |
461 | 468 | assert new_attach.filename in res.json |
462 | 469 | assert 'image/png' in res.json[new_attach.filename]['content_type'] |
848 | 855 | 10, workspace=self.workspace, host=host2, service=None) |
849 | 856 | |
850 | 857 | session.commit() |
851 | res = test_client.get(self.check_url(urljoin( | |
858 | res = test_client.get(urljoin( | |
852 | 859 | self.url(), 'filter?q={"filters":[{"name": "target", "op":"eq", "val":"192.168.0.1"}]}' |
853 | ))) | |
860 | )) | |
854 | 861 | |
855 | 862 | assert res.status_code == 200 |
856 | 863 | assert len(res.json['vulnerabilities']) == 10 |
869 | 876 | 10, workspace=self.workspace, host=host2, service=None) |
870 | 877 | |
871 | 878 | session.commit() |
872 | res = test_client.get(self.check_url(urljoin( | |
879 | res = test_client.get(urljoin( | |
873 | 880 | self.url(), |
874 | 881 | 'filter?q={"filters":[{"name": "target_host_ip", "op":"eq", "val":"192.168.0.2"}]}' |
875 | ))) | |
882 | )) | |
876 | 883 | assert res.status_code == 200 |
877 | 884 | assert len(res.json['vulnerabilities']) == 1 |
878 | 885 | assert res.json['vulnerabilities'][0]['value']['target'] == '192.168.0.2' |
892 | 899 | 10, workspace=self.workspace, host=None, service=service) |
893 | 900 | |
894 | 901 | session.commit() |
895 | res = test_client.get(self.check_url(urljoin( | |
902 | res = test_client.get(urljoin( | |
896 | 903 | self.url(), |
897 | 904 | 'filter?q={"filters":[{"name": "service", "op":"has", "val":{"name": "port", "op":"eq", "val":"8956"}}]}' |
898 | ))) | |
905 | )) | |
899 | 906 | assert res.status_code == 200 |
900 | 907 | assert len(res.json['vulnerabilities']) == 10 |
901 | 908 | assert res.json['count'] == 10 |
915 | 922 | 10, workspace=self.workspace, host=None, service=service) |
916 | 923 | |
917 | 924 | session.commit() |
918 | res = test_client.get(self.check_url(urljoin( | |
925 | res = test_client.get(urljoin( | |
919 | 926 | self.url(), |
920 | 927 | 'filter?q={"filters":[{"name": "service", "op":"has", "val":{"name": "name", "op":"eq", "val":"ssh"}}]}' |
921 | ))) | |
928 | )) | |
922 | 929 | assert res.status_code == 200 |
923 | 930 | assert len(res.json['vulnerabilities']) == 1 |
924 | 931 | assert res.json['count'] == 1 |
1188 | 1195 | |
1189 | 1196 | # Desc |
1190 | 1197 | res = test_client.get( |
1191 | self.check_url(urljoin(self.url(), "count/")) | |
1192 | + "?confirmed=1&group_by=severity&order=sc" | |
1193 | ) | |
1198 | urljoin(self.url(), "count?confirmed=1&group_by=severity&order=sc" | |
1199 | )) | |
1194 | 1200 | assert res.status_code == 400 |
1195 | 1201 | |
1196 | 1202 | # Asc |
1197 | res = test_client.get( | |
1198 | self.check_url(urljoin(self.url(), "count/")) | |
1199 | + "?confirmed=1&group_by=severity&order=name,asc" | |
1200 | ) | |
1203 | res = test_client.get(urljoin(self.url(), "count?confirmed=1&group_by=severity&order=name,asc")) | |
1201 | 1204 | assert res.status_code == 400 |
1202 | 1205 | |
1203 | 1206 | def test_count_order_by(self, test_client, session): |
1214 | 1217 | |
1215 | 1218 | # Desc |
1216 | 1219 | res = test_client.get( |
1217 | self.check_url(urljoin(self.url(), "count/")) + "?confirmed=1&group_by=severity&order=desc" | |
1218 | ) | |
1220 | urljoin(self.url(), "count?confirmed=1&group_by=severity&order=desc" | |
1221 | )) | |
1219 | 1222 | assert res.status_code == 200 |
1220 | 1223 | assert res.json['total_count'] == 3 |
1221 | 1224 | assert sorted(res.json['groups'], key=lambda i: (i['name'], i['count'], i['severity'])) == sorted([ |
1225 | 1228 | |
1226 | 1229 | # Asc |
1227 | 1230 | res = test_client.get( |
1228 | self.check_url(urljoin(self.url(), "count/")) + "?confirmed=1&group_by=severity&order=asc") | |
1231 | urljoin(self.url(), "count?confirmed=1&group_by=severity&order=asc")) | |
1229 | 1232 | assert res.status_code == 200 |
1230 | 1233 | assert res.json['total_count'] == 3 |
1231 | 1234 | assert sorted(res.json['groups'], key=lambda i: (i['name'], i['count'], i['severity']), reverse=True) == sorted( |
1246 | 1249 | session.add(vuln) |
1247 | 1250 | session.commit() |
1248 | 1251 | |
1249 | res = test_client.get(self.check_url(urljoin(self.url(), "count/")) + "?confirmed=1&group_by=username") | |
1252 | res = test_client.get(urljoin(self.url(), "count?confirmed=1&group_by=username")) | |
1250 | 1253 | assert res.status_code == 400 |
1251 | 1254 | |
1252 | res = test_client.get(self.check_url(urljoin(self.url(), "count/")) + "?confirmed=1&group_by=") | |
1255 | res = test_client.get(urljoin(self.url(), "count?confirmed=1&group_by=")) | |
1253 | 1256 | assert res.status_code == 400 |
1254 | 1257 | |
1255 | 1258 | def test_count_confirmed(self, test_client, session): |
1265 | 1268 | session.add(vuln) |
1266 | 1269 | session.commit() |
1267 | 1270 | |
1268 | res = test_client.get(self.check_url(urljoin(self.url(), 'count/')) + '?confirmed=1&group_by=severity') | |
1271 | res = test_client.get(urljoin(self.url(), 'count?confirmed=1&group_by=severity')) | |
1269 | 1272 | assert res.status_code == 200 |
1270 | 1273 | assert res.json['total_count'] == 3 |
1271 | 1274 | assert sorted(res.json['groups'], key=lambda i: (i['count'], i['name'], i['severity'])) == sorted([ |
1284 | 1287 | session.commit() |
1285 | 1288 | |
1286 | 1289 | res = test_client.get( |
1287 | self.check_url(urljoin(self.url(workspace=second_workspace), 'count/')) + '?group_by=severity' | |
1288 | ) | |
1290 | urljoin(self.url(workspace=second_workspace), 'count?group_by=severity' | |
1291 | )) | |
1289 | 1292 | assert res.status_code == 200 |
1290 | 1293 | assert res.json['total_count'] == 9 |
1291 | 1294 | assert sorted(res.json['groups'], key=lambda i: (i['count'], i['name'], i['severity'])) == sorted([ |
1307 | 1310 | session.commit() |
1308 | 1311 | |
1309 | 1312 | res = test_client.get( |
1310 | self.check_url(urljoin(self.url(), 'count_multi_workspace/')) | |
1311 | + f'?workspaces={self.workspace.name}&confirmed=1&group_by=severity&order=desc' | |
1313 | urljoin( | |
1314 | self.url(), | |
1315 | f'count_multi_workspace?workspaces={self.workspace.name}&confirmed=1&group_by=severity&order=desc' | |
1316 | ) | |
1312 | 1317 | ) |
1313 | 1318 | |
1314 | 1319 | assert res.status_code == 200 |
1337 | 1342 | session.commit() |
1338 | 1343 | |
1339 | 1344 | res = test_client.get( |
1340 | self.check_url(urljoin(self.url(), 'count_multi_workspace/')) | |
1341 | + f'?workspaces={self.workspace.name},{second_workspace.name}&confirmed=1&group_by=severity&order=desc' | |
1345 | urljoin( | |
1346 | self.url(), | |
1347 | f'count_multi_workspace?workspaces={self.workspace.name},' | |
1348 | f'{second_workspace.name}&confirmed=1&group_by=severity&order=desc' | |
1349 | ) | |
1342 | 1350 | ) |
1343 | 1351 | |
1344 | 1352 | assert res.status_code == 200 |
1347 | 1355 | |
1348 | 1356 | def test_count_multiworkspace_no_workspace_param(self, test_client): |
1349 | 1357 | res = test_client.get( |
1350 | self.check_url(urljoin(self.url(), 'count_multi_workspace/')) | |
1351 | + '?confirmed=1&group_by=severity&order=desc' | |
1352 | ) | |
1358 | urljoin(self.url(), 'count_multi_workspace?confirmed=1&group_by=severity&order=desc' | |
1359 | )) | |
1353 | 1360 | assert res.status_code == 400 |
1354 | 1361 | |
1355 | 1362 | def test_count_multiworkspace_no_groupby_param(self, test_client): |
1356 | 1363 | res = test_client.get( |
1357 | self.check_url(urljoin(self.url(), 'count_multi_workspace/')) | |
1358 | + f'?workspaces={self.workspace.name}&confirmed=1&order=desc' | |
1359 | ) | |
1364 | urljoin(self.url(), f'count_multi_workspace?workspaces={self.workspace.name}&confirmed=1&order=desc' | |
1365 | )) | |
1360 | 1366 | assert res.status_code == 400 |
1361 | 1367 | |
1362 | 1368 | def test_count_multiworkspace_nonexistent_ws(self, test_client): |
1363 | 1369 | res = test_client.get( |
1364 | self.check_url(urljoin(self.url(), 'count_multi_workspace/')) | |
1365 | + '?workspaces=asdf,{self.workspace.name}&confirmed=1&group_by=severity&order=desc' | |
1370 | urljoin( | |
1371 | self.url(), | |
1372 | f'count_multi_workspace?workspaces=asdf,{self.workspace.name}&confirmed=1&group_by=severity&order=desc' | |
1373 | ) | |
1366 | 1374 | ) |
1367 | 1375 | assert res.status_code == 404 |
1368 | 1376 | |
1703 | 1711 | ) |
1704 | 1712 | ws_name = host_with_hostnames.workspace.name |
1705 | 1713 | res = test_client.post( |
1706 | self.check_url(self.url(workspace=host_with_hostnames.workspace)) + f'?command_id={command.id}', | |
1714 | urljoin(self.url(workspace=host_with_hostnames.workspace) + f'?command_id={command.id}'), | |
1707 | 1715 | data=raw_data |
1708 | 1716 | ) |
1709 | 1717 | assert res.status_code == 201 |
1718 | 1726 | severity='high', |
1719 | 1727 | ) |
1720 | 1728 | res = test_client.put( |
1721 | self.check_url(urljoin(self.url(workspace=host_with_hostnames.workspace), f'{res.json["_id"]}/')) | |
1722 | + f'?command_id={command.id}', | |
1723 | data=raw_data) | |
1729 | urljoin( | |
1730 | self.url(workspace=host_with_hostnames.workspace), f'{res.json["_id"]}?command_id={command.id}' | |
1731 | ), | |
1732 | data=raw_data | |
1733 | ) | |
1724 | 1734 | assert res.status_code == 200 |
1725 | 1735 | |
1726 | 1736 | def test_create_vuln_from_command(self, test_client, session): |
1854 | 1864 | def test_search_by_hostnames_service_case(self, session, test_client): |
1855 | 1865 | workspace = WorkspaceFactory.create() |
1856 | 1866 | vuln2 = VulnerabilityFactory.create(workspace=workspace) |
1857 | hostname = HostnameFactory.create(workspace=workspace, name='test.com') | |
1858 | 1867 | host = HostFactory.create(workspace=workspace) |
1868 | hostname = HostnameFactory.create(workspace=workspace, name='test.com', host=host) | |
1859 | 1869 | host.hostnames.append(hostname) |
1860 | 1870 | service = ServiceFactory.create(workspace=workspace, host=host) |
1861 | 1871 | vuln = VulnerabilityFactory.create(service=service, host=None, workspace=workspace) |
1874 | 1884 | def test_search_by_hostnames_host_case(self, session, test_client): |
1875 | 1885 | workspace = WorkspaceFactory.create() |
1876 | 1886 | vuln2 = VulnerabilityFactory.create(workspace=workspace) |
1877 | hostname = HostnameFactory.create(workspace=workspace, name='test.com') | |
1878 | 1887 | host = HostFactory.create(workspace=workspace) |
1888 | hostname = HostnameFactory.create(workspace=workspace, name='test.com', host=host) | |
1879 | 1889 | host.hostnames.append(hostname) |
1880 | 1890 | vuln = VulnerabilityFactory.create(host=host, service=None, workspace=workspace) |
1881 | 1891 | session.add(vuln) |
1959 | 1969 | headers = {'Content-type': 'multipart/form-data'} |
1960 | 1970 | |
1961 | 1971 | res = test_client.post( |
1962 | self.check_url(f'/v2/ws/abc/vulns/{vuln.id}/attachment/'), | |
1972 | f'/v3/ws/abc/vulns/{vuln.id}/attachment', | |
1963 | 1973 | data=data, headers=headers, use_json_data=False) |
1964 | assert res.status_code == 403 # Missing CSRF protection | |
1965 | ||
1966 | data = { | |
1967 | 'file': (BytesIO(file_contents), 'borrar.txt'), | |
1968 | 'csrf_token': csrf_token | |
1969 | } | |
1970 | res = test_client.post( | |
1971 | self.check_url(f'/v2/ws/abc/vulns/{vuln.id}/attachment/'), | |
1972 | data=data, headers=headers, use_json_data=False) | |
1973 | assert res.status_code == 200 # Now it should work | |
1974 | ||
1975 | assert res.status_code == 200 | |
1974 | 1976 | |
1975 | 1977 | file_id = session.query(Vulnerability).filter_by(id=vuln.id).first().evidence[0].content['file_id'] |
1976 | 1978 | depot = DepotManager.get() |
1992 | 1994 | session.commit() |
1993 | 1995 | |
1994 | 1996 | res = test_client.post( |
1995 | self.check_url(f'/v2/ws/abc/vulns/{vuln.id}/attachment/'), | |
1997 | f'/v3/ws/abc/vulns/{vuln.id}/attachment', | |
1996 | 1998 | data=data, headers=headers, use_json_data=False) |
1997 | 1999 | assert res.status_code == 403 |
1998 | 2000 | query_test = session.query(Vulnerability).filter_by(id=vuln.id).first().evidence |
2014 | 2016 | policyviolations=[], |
2015 | 2017 | attachments=[attachment] |
2016 | 2018 | ) |
2017 | res = test_client.post(self.check_url(f'/v2/ws/{ws_name}/vulns/'), data=vuln) | |
2019 | res = test_client.post(f'/v3/ws/{ws_name}/vulns', data=vuln) | |
2018 | 2020 | assert res.status_code == 201 |
2019 | 2021 | |
2020 | 2022 | filename = attachment.name.split('/')[-1] |
2021 | 2023 | vuln_id = res.json['_id'] |
2022 | 2024 | res = test_client.delete( |
2023 | self.check_url(f'/v2/ws/{ws_name}/vulns/{vuln_id}/attachment/{filename}/') | |
2025 | f'/v3/ws/{ws_name}/vulns/{vuln_id}/attachment/{filename}' | |
2024 | 2026 | ) |
2025 | 2027 | assert res.status_code == 200 |
2026 | 2028 | |
2043 | 2045 | policyviolations=[], |
2044 | 2046 | attachments=[attachment] |
2045 | 2047 | ) |
2046 | res = test_client.post(self.check_url(f'/v2/ws/{ws_name}/vulns/'), data=vuln) | |
2048 | res = test_client.post(f'/v3/ws/{ws_name}/vulns', data=vuln) | |
2047 | 2049 | assert res.status_code == 201 |
2048 | 2050 | |
2049 | 2051 | self.workspace.readonly = True |
2052 | 2054 | filename = attachment.name.split('/')[-1] |
2053 | 2055 | vuln_id = res.json['_id'] |
2054 | 2056 | res = test_client.delete( |
2055 | self.check_url(f'/v2/ws/{ws_name}/vulns/{vuln_id}/attachment/{filename}/') | |
2057 | f'/v3/ws/{ws_name}/vulns/{vuln_id}/attachment/{filename}' | |
2056 | 2058 | ) |
2057 | 2059 | assert res.status_code == 403 |
2058 | 2060 | |
2074 | 2076 | description='helloworld', |
2075 | 2077 | severity='medium', |
2076 | 2078 | ) |
2077 | res = test_client.post(self.check_url(f'/v2/ws/{workspace.name}/vulns/'), data=raw_data) | |
2079 | res = test_client.post(f'/v3/ws/{workspace.name}/vulns', data=raw_data) | |
2078 | 2080 | |
2079 | 2081 | data = { |
2080 | 2082 | 'q': '{"filters":[{"name":"severity","op":"eq","val":"medium"}]}' |
2081 | 2083 | } |
2082 | res = test_client.get(self.check_url(f'/v2/ws/{workspace.name}/vulns/filter'), query_string=data) | |
2084 | res = test_client.get(f'/v3/ws/{workspace.name}/vulns/filter', query_string=data) | |
2083 | 2085 | |
2084 | 2086 | assert res.status_code == 200 |
2085 | 2087 | value = res.json['vulnerabilities'][0]['value'] |
2089 | 2091 | data = { |
2090 | 2092 | "q": {"filters": [{"name": "severity", "op": "eq", "val": "medium"}]} |
2091 | 2093 | } |
2092 | res = test_client.get(self.check_url(f'/v2/ws/{workspace.name}/vulns/filter'), query_string=data) | |
2094 | res = test_client.get(f'/v3/ws/{workspace.name}/vulns/filter', query_string=data) | |
2093 | 2095 | assert res.status_code == 400 |
2094 | 2096 | |
2095 | 2097 | def test_vuln_filter_exception(self, test_client, workspace, session): |
2099 | 2101 | data = { |
2100 | 2102 | 'q': '{"filters":[{"name":"severity","op":"eq","val":"medium"}]}' |
2101 | 2103 | } |
2102 | res = test_client.get(self.check_url(f'/v2/ws/{workspace.name}/vulns/filter'), query_string=data) | |
2104 | res = test_client.get(f'/v3/ws/{workspace.name}/vulns/filter', query_string=data) | |
2103 | 2105 | assert res.status_code == 200 |
2104 | 2106 | assert res.json['count'] == 1 |
2105 | 2107 | |
2122 | 2124 | data = { |
2123 | 2125 | 'q': '{"group_by":[{"field":"creator_id"}]}' |
2124 | 2126 | } |
2125 | res = test_client.get(self.check_url(f'/v2/ws/{workspace.name}/vulns/filter'), query_string=data) | |
2127 | res = test_client.get(f'/v3/ws/{workspace.name}/vulns/filter', query_string=data) | |
2126 | 2128 | assert res.status_code == 200 |
2127 | 2129 | assert res.json['count'] == 1 # all vulns created by the same creator |
2128 | 2130 | expected = [{'count': 2, 'creator_id': creator.id}] |
2147 | 2149 | data = { |
2148 | 2150 | 'q': '{"group_by":[{"field":"severity"}]}' |
2149 | 2151 | } |
2150 | res = test_client.get(self.check_url(f'/v2/ws/{workspace.name}/vulns/filter'), query_string=data) | |
2152 | res = test_client.get(f'/v3/ws/{workspace.name}/vulns/filter', query_string=data) | |
2151 | 2153 | assert res.status_code == 200, res.json |
2152 | 2154 | assert res.json['count'] == 1, res.json # all vulns created by the same creator |
2153 | 2155 | expected = { |
2179 | 2181 | data = { |
2180 | 2182 | 'q': '{"group_by":[{"field":"severity"}, {"field": "name"}]}' |
2181 | 2183 | } |
2182 | res = test_client.get(self.check_url(f'/v2/ws/{workspace.name}/vulns/filter'), query_string=data) | |
2184 | res = test_client.get(f'/v3/ws/{workspace.name}/vulns/filter', query_string=data) | |
2183 | 2185 | assert res.status_code == 200, res.json |
2184 | 2186 | assert res.json['count'] == 2, res.json # all vulns created by the same creator |
2185 | 2187 | expected = {'vulnerabilities': [ |
2213 | 2215 | data = { |
2214 | 2216 | 'q': json.dumps({"group_by": [{"field": col_name}]}) |
2215 | 2217 | } |
2216 | res = test_client.get(self.check_url(f'/v2/ws/{workspace.name}/vulns/filter'), query_string=data) | |
2218 | res = test_client.get(f'/v3/ws/{workspace.name}/vulns/filter', query_string=data) | |
2217 | 2219 | assert res.status_code == 200, res.json |
2218 | 2220 | |
2219 | 2221 | def test_vuln_restless_group_same_name_description(self, test_client, session): |
2247 | 2249 | data = { |
2248 | 2250 | 'q': '{"group_by":[{"field":"name"}, {"field":"description"}]}' |
2249 | 2251 | } |
2250 | res = test_client.get(self.check_url(f'/v2/ws/{workspace.name}/vulns/filter'), query_string=data) | |
2252 | res = test_client.get(f'/v3/ws/{workspace.name}/vulns/filter', query_string=data) | |
2251 | 2253 | assert res.status_code == 200 |
2252 | 2254 | assert res.json['count'] == 2 |
2253 | 2255 | expected = [{'count': 2, 'name': 'test', 'description': 'test'}, |
2313 | 2315 | data = { |
2314 | 2316 | 'q': json.dumps(query) |
2315 | 2317 | } |
2316 | res = test_client.get(self.check_url(f'/v2/ws/{workspace.name}/vulns/filter'), query_string=data) | |
2318 | res = test_client.get(f'/v3/ws/{workspace.name}/vulns/filter', query_string=data) | |
2317 | 2319 | assert res.status_code == 200 |
2318 | 2320 | assert res.json['count'] == 12 |
2319 | 2321 | expected_order = ['critical', 'critical', 'med', 'med', 'med', 'med', 'med', 'med', 'med', 'med', 'med', 'med'] |
2327 | 2329 | data = { |
2328 | 2330 | 'q': json.dumps({"filters": [{"name": "creator", "op": "eq", "val": vuln.creator.username}]}) |
2329 | 2331 | } |
2330 | res = test_client.get(self.check_url(f'/v2/ws/{workspace.name}/vulns/filter'), query_string=data) | |
2332 | res = test_client.get(f'/v3/ws/{workspace.name}/vulns/filter', query_string=data) | |
2331 | 2333 | assert res.status_code == 200 |
2332 | 2334 | |
2333 | 2335 | def test_vuln_web_filter_exception(self, test_client, workspace, session): |
2337 | 2339 | data = { |
2338 | 2340 | 'q': '{"filters":[{"name":"severity","op":"eq","val":"medium"}]}' |
2339 | 2341 | } |
2340 | res = test_client.get(self.check_url(f'/v2/ws/{workspace.name}/vulns/filter'), query_string=data) | |
2342 | res = test_client.get(f'/v3/ws/{workspace.name}/vulns/filter', query_string=data) | |
2341 | 2343 | assert res.status_code == 200 |
2342 | 2344 | assert res.json['count'] == 1 |
2343 | 2345 | |
2373 | 2375 | session.commit() |
2374 | 2376 | |
2375 | 2377 | res = test_client.post( |
2376 | self.check_url(f'/v2/ws/{workspace.name}/vulns/{vuln.id}/attachment/'), | |
2378 | f'/v3/ws/{workspace.name}/vulns/{vuln.id}/attachment', | |
2377 | 2379 | data={'csrf_token': csrf_token}, |
2378 | 2380 | headers={'Content-Type': 'multipart/form-data'}, |
2379 | 2381 | use_json_data=False) |
2381 | 2383 | |
2382 | 2384 | def test_get_attachment_with_invalid_workspace_and_vuln(self, test_client): |
2383 | 2385 | res = test_client.get( |
2384 | self.check_url("/v2/ws/invalid_ws/vulns/invalid_vuln/attachment/random_name/") | |
2386 | "/v3/ws/invalid_ws/vulns/invalid_vuln/attachment/random_name" | |
2385 | 2387 | ) |
2386 | 2388 | assert res.status_code == 404 |
2387 | 2389 | |
2388 | 2390 | def test_delete_attachment_with_invalid_workspace_and_vuln(self, test_client): |
2389 | 2391 | res = test_client.delete( |
2390 | self.check_url("/v2/ws/invalid_ws/vulns/invalid_vuln/attachment/random_name/") | |
2392 | "/v3/ws/invalid_ws/vulns/invalid_vuln/attachment/random_name" | |
2391 | 2393 | ) |
2392 | 2394 | assert res.status_code == 404 |
2393 | 2395 | |
2396 | 2398 | session.add(vuln) |
2397 | 2399 | session.commit() |
2398 | 2400 | res = test_client.delete( |
2399 | self.check_url(f"/v2/ws/{workspace.name}/vulns/{vuln.id}/attachment/random_name/") | |
2401 | f"/v3/ws/{workspace.name}/vulns/{vuln.id}/attachment/random_name" | |
2400 | 2402 | ) |
2401 | 2403 | assert res.status_code == 404 |
2402 | 2404 | |
2403 | 2405 | def test_export_vuln_csv_empty_workspace(self, test_client, session): |
2404 | 2406 | ws = WorkspaceFactory(name='abc') |
2405 | res = test_client.get(self.check_url(f'/v2/ws/{ws.name}/vulns/export_csv/')) | |
2407 | res = test_client.get(f'/v3/ws/{ws.name}/vulns/export_csv') | |
2406 | 2408 | expected_headers = [ |
2407 | 2409 | "confirmed", "id", "date", "name", "severity", "service", |
2408 | 2410 | "target", "desc", "status", "hostnames", "comments", "owner", |
2425 | 2427 | session.add(confirmed_vulns) |
2426 | 2428 | session.commit() |
2427 | 2429 | res = test_client.get( |
2428 | self.check_url(urljoin(self.url(workspace=workspace), 'export_csv/')) | |
2429 | + '?q={"filters":[{"name":"confirmed","op":"==","val":"true"}]}' | |
2430 | urljoin( | |
2431 | self.url(workspace=workspace), | |
2432 | 'export_csv?q={"filters":[{"name":"confirmed","op":"==","val":"true"}]}' | |
2433 | ) | |
2430 | 2434 | ) |
2431 | 2435 | assert res.status_code == 200 |
2432 | 2436 | assert self._verify_csv(res.data, confirmed=True) |
2441 | 2445 | workspace=workspace) |
2442 | 2446 | session.add(confirmed_vulns) |
2443 | 2447 | session.commit() |
2444 | res = test_client.get(self.check_url(urljoin(self.url(workspace=workspace), 'export_csv/'))) | |
2448 | res = test_client.get(urljoin(self.url(workspace=workspace), 'export_csv')) | |
2445 | 2449 | assert res.status_code == 200 |
2446 | 2450 | assert self._verify_csv(res.data, confirmed=True) |
2447 | 2451 | |
2452 | 2456 | session.add(confirmed_vulns) |
2453 | 2457 | session.commit() |
2454 | 2458 | res = test_client.get( |
2455 | self.check_url(urljoin(self.url(workspace=workspace), 'export_csv/')) | |
2456 | + '?q={"filters":[{"name":"severity","op":"==","val":"critical"}]}' | |
2459 | urljoin( | |
2460 | self.url(workspace=workspace), | |
2461 | 'export_csv?q={"filters":[{"name":"severity","op":"==","val":"critical"}]}' | |
2462 | ) | |
2457 | 2463 | ) |
2458 | 2464 | assert res.status_code == 200 |
2459 | 2465 | assert self._verify_csv(res.data, confirmed=True, severity='critical') |
2464 | 2470 | session.add(self.first_object) |
2465 | 2471 | session.commit() |
2466 | 2472 | res = test_client.get( |
2467 | self.check_url(urljoin(self.url(), 'export_csv/')) | |
2468 | + '?confirmed=true' | |
2473 | urljoin(self.url(), 'export_csv?confirmed=true') | |
2469 | 2474 | ) |
2470 | 2475 | assert res.status_code == 200 |
2471 | 2476 | self._verify_csv(res.data, confirmed=True) |
2490 | 2495 | session.add(vuln) |
2491 | 2496 | session.commit() |
2492 | 2497 | |
2493 | res = test_client.get(self.check_url(f'/v2/ws/{workspace.name}/vulns/export_csv/')) | |
2498 | res = test_client.get(f'/v3/ws/{workspace.name}/vulns/export_csv') | |
2494 | 2499 | assert res.status_code == 200 |
2495 | 2500 | |
2496 | 2501 | csv_data = csv.DictReader(StringIO(res.data.decode('utf-8')), delimiter=',') |
2530 | 2535 | session.add(vuln) |
2531 | 2536 | session.commit() |
2532 | 2537 | |
2533 | res = test_client.get(self.check_url(urljoin(self.url(), 'export_csv/'))) | |
2538 | res = test_client.get(urljoin(self.url(), 'export_csv')) | |
2534 | 2539 | assert self._verify_csv(res.data) |
2535 | 2540 | |
2536 | 2541 | def _verify_csv(self, raw_csv_data, confirmed=False, severity=None): |
2602 | 2607 | assert res.status_code == 200 |
2603 | 2608 | assert res.json['tool'] == tool |
2604 | 2609 | |
2605 | ||
2606 | class TestListVulnerabilityViewV3(TestListVulnerabilityView, PatchableTestsMixin): | |
2607 | view_class = VulnerabilityV3View | |
2608 | ||
2609 | def url(self, obj=None, workspace=None): | |
2610 | return v2_to_v3(super().url(obj, workspace)) | |
2611 | ||
2612 | def check_url(self, url): | |
2613 | return v2_to_v3(url) | |
2614 | ||
2615 | 2610 | def test_patch_with_attachments(self, test_client, session, workspace): |
2616 | 2611 | vuln = VulnerabilityFactory.create(workspace=workspace) |
2617 | 2612 | session.add(vuln) |
2641 | 2636 | api_endpoint = 'vulns' |
2642 | 2637 | view_class = VulnerabilityView |
2643 | 2638 | patchable_fields = ['description'] |
2644 | ||
2645 | def check_url(self, url): | |
2646 | return url | |
2647 | 2639 | |
2648 | 2640 | def test_create_vuln_with_custom_fields_shown(self, test_client, second_workspace, session): |
2649 | 2641 | host = HostFactory.create(workspace=self.workspace) |
2810 | 2802 | assert res.status_code == 400 |
2811 | 2803 | |
2812 | 2804 | @pytest.mark.usefixtures('ignore_nplusone') |
2805 | @pytest.mark.skip(reason="To be reimplemented") | |
2813 | 2806 | def test_bulk_delete_vuln_id(self, host_with_hostnames, test_client, session): |
2814 | 2807 | """ |
2815 | 2808 | This one should only check basic vuln properties |
2841 | 2834 | ) |
2842 | 2835 | ws_name = host_with_hostnames.workspace.name |
2843 | 2836 | vuln_count_previous = session.query(Vulnerability).count() |
2844 | res_1 = test_client.post(self.check_url(f'/v2/ws/{ws_name}/vulns/'), data=raw_data_vuln_1) | |
2845 | res_2 = test_client.post(self.check_url(f'/v2/ws/{ws_name}/vulns/'), data=raw_data_vuln_2) | |
2837 | res_1 = test_client.post(f'/v3/ws/{ws_name}/vulns', data=raw_data_vuln_1) | |
2838 | res_2 = test_client.post(f'/v3/ws/{ws_name}/vulns', data=raw_data_vuln_2) | |
2846 | 2839 | vuln_1_id = res_1.json['obj_id'] |
2847 | 2840 | vuln_2_id = res_2.json['obj_id'] |
2848 | 2841 | vulns_to_delete = [vuln_1_id, vuln_2_id] |
2849 | 2842 | request_data = {'vulnerability_ids': vulns_to_delete} |
2850 | delete_response = test_client.delete(self.check_url(f'/v2/ws/{ws_name}/vulns/bulk_delete/'), data=request_data) | |
2843 | delete_response = test_client.delete(f'/v3/ws/{ws_name}/vulns/bulk_delete', data=request_data) | |
2851 | 2844 | vuln_count_after = session.query(Vulnerability).count() |
2852 | 2845 | deleted_vulns = delete_response.json['deleted_vulns'] |
2853 | 2846 | assert delete_response.status_code == 200 |
2855 | 2848 | assert deleted_vulns == len(vulns_to_delete) |
2856 | 2849 | |
2857 | 2850 | @pytest.mark.usefixtures('ignore_nplusone') |
2851 | @pytest.mark.skip(reason="To be reimplemented") | |
2858 | 2852 | def test_bulk_delete_vuln_severity(self, host_with_hostnames, test_client, session): |
2859 | 2853 | """ |
2860 | 2854 | This one should only check basic vuln properties |
2886 | 2880 | ) |
2887 | 2881 | ws_name = host_with_hostnames.workspace.name |
2888 | 2882 | vuln_count_previous = session.query(Vulnerability).count() |
2889 | res_1 = test_client.post(self.check_url(f'/v2/ws/{ws_name}/vulns/'), data=raw_data_vuln_1) | |
2890 | res_2 = test_client.post(self.check_url(f'/v2/ws/{ws_name}/vulns/'), data=raw_data_vuln_2) | |
2883 | res_1 = test_client.post(f'/v3/ws/{ws_name}/vulns', data=raw_data_vuln_1) | |
2884 | res_2 = test_client.post(f'/v3/ws/{ws_name}/vulns', data=raw_data_vuln_2) | |
2891 | 2885 | vuln_1_id = res_1.json['obj_id'] |
2892 | 2886 | vuln_2_id = res_2.json['obj_id'] |
2893 | 2887 | vulns_to_delete = [vuln_1_id, vuln_2_id] |
2894 | 2888 | request_data = {'severities': ['low']} |
2895 | delete_response = test_client.delete(self.check_url(f'/v2/ws/{ws_name}/vulns/bulk_delete/'), data=request_data) | |
2889 | delete_response = test_client.delete(f'/v3/ws/{ws_name}/vulns/bulk_delete', data=request_data) | |
2896 | 2890 | vuln_count_after = session.query(Vulnerability).count() |
2897 | 2891 | deleted_vulns = delete_response.json['deleted_vulns'] |
2898 | 2892 | assert delete_response.status_code == 200 |
3030 | 3024 | assert ref_example == get_response.json[ref_name][0] |
3031 | 3025 | |
3032 | 3026 | |
3033 | class TestCustomFieldVulnerabilityV3(TestCustomFieldVulnerability, PatchableTestsMixin): | |
3034 | view_class = VulnerabilityV3View | |
3035 | ||
3036 | def url(self, obj=None, workspace=None): | |
3037 | return v2_to_v3(super().url(obj, workspace)) | |
3038 | ||
3039 | def check_url(self, url): | |
3040 | return v2_to_v3(url) | |
3041 | ||
3042 | @pytest.mark.skip(reason="To be reimplemented") | |
3043 | def test_bulk_delete_vuln_id(self, host_with_hostnames, test_client, session): | |
3044 | pass | |
3045 | ||
3046 | @pytest.mark.skip(reason="To be reimplemented") | |
3047 | def test_bulk_delete_vuln_severity(self, host_with_hostnames, test_client, session): | |
3048 | pass | |
3049 | ||
3050 | ||
3051 | 3027 | @pytest.mark.usefixtures('logged_user') |
3052 | 3028 | class TestVulnerabilityCustomFields(ReadWriteAPITests): |
3053 | 3029 | model = Vulnerability |
3067 | 3043 | session.commit() |
3068 | 3044 | |
3069 | 3045 | |
3070 | class TestVulnerabilityCustomFieldsV3(TestVulnerabilityCustomFields, PatchableTestsMixin): | |
3071 | view_class = VulnerabilityV3View | |
3072 | ||
3073 | def url(self, obj=None, workspace=None): | |
3074 | return v2_to_v3(super().url(obj, workspace)) | |
3075 | ||
3076 | ||
3077 | 3046 | @pytest.mark.usefixtures('logged_user') |
3078 | 3047 | class TestVulnerabilitySearch: |
3079 | ||
3080 | def check_url(self, url): | |
3081 | return url | |
3082 | 3048 | |
3083 | 3049 | @pytest.mark.skip_sql_dialect('sqlite') |
3084 | 3050 | def test_search_by_hostname_vulns(self, test_client, session): |
3094 | 3060 | [{"name": "hostnames", "op": "eq", "val": "pepe"}] |
3095 | 3061 | } |
3096 | 3062 | res = test_client.get( |
3097 | self.check_url(f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3063 | f'/v3/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}' | |
3098 | 3064 | ) |
3099 | 3065 | assert res.status_code == 200 |
3100 | 3066 | assert res.json['count'] == 1 |
3115 | 3081 | [{"name": "hostnames", "op": "eq", "val": "pepe"}] |
3116 | 3082 | } |
3117 | 3083 | res = test_client.get( |
3118 | self.check_url(f'/v2/ws/{workspace.name}/vulns/') + f'?q={json.dumps(query_filter)}' | |
3084 | f'/v3/ws/{workspace.name}/vulns?q={json.dumps(query_filter)}' | |
3119 | 3085 | ) |
3120 | 3086 | assert res.status_code == 200 |
3121 | 3087 | assert res.json['count'] == 1 |
3137 | 3103 | [{"name": "hostnames", "op": "eq", "val": "pepe"}] |
3138 | 3104 | } |
3139 | 3105 | res = test_client.get( |
3140 | self.check_url(f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3106 | f'/v3/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}' | |
3141 | 3107 | ) |
3142 | 3108 | assert res.status_code == 200 |
3143 | 3109 | assert res.json['count'] == 1 |
3148 | 3114 | [] |
3149 | 3115 | } |
3150 | 3116 | res = test_client.get( |
3151 | self.check_url(f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3117 | f'/v3/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}' | |
3152 | 3118 | ) |
3153 | 3119 | assert res.status_code == 200 |
3154 | 3120 | assert res.json['count'] == 0 |
3158 | 3124 | [{"name": "code", "op": "eq", "val": "test"}] |
3159 | 3125 | } |
3160 | 3126 | res = test_client.get( |
3161 | self.check_url(f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3127 | f'/v3/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}' | |
3162 | 3128 | ) |
3163 | 3129 | |
3164 | 3130 | assert res.status_code == 400, res.json |
3177 | 3143 | {"and": [{"name": "hostnames", "op": "eq", "val": "pepe"}]} |
3178 | 3144 | ]} |
3179 | 3145 | res = test_client.get( |
3180 | self.check_url(f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3146 | f'/v3/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}' | |
3181 | 3147 | ) |
3182 | 3148 | assert res.status_code == 200 |
3183 | 3149 | assert res.json['count'] == 1 |
3209 | 3175 | "offset": offset * 10, |
3210 | 3176 | } |
3211 | 3177 | res = test_client.get( |
3212 | self.check_url(f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3178 | f'/v3/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}' | |
3213 | 3179 | ) |
3214 | 3180 | assert res.status_code == 200 |
3215 | 3181 | assert res.json['count'] == 20, query_filter |
3239 | 3205 | "offset": 10 * offset, |
3240 | 3206 | } |
3241 | 3207 | res = test_client.get( |
3242 | self.check_url(f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3208 | f'/v3/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}' | |
3243 | 3209 | ) |
3244 | 3210 | assert res.status_code == 200 |
3245 | 3211 | assert res.json['count'] == 100 |
3278 | 3244 | "offset": offset, |
3279 | 3245 | } |
3280 | 3246 | res = test_client.get( |
3281 | self.check_url(f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3247 | f'/v3/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}' | |
3282 | 3248 | ) |
3283 | 3249 | assert res.status_code == 200 |
3284 | 3250 | assert res.json['count'] == 10 |
3317 | 3283 | {"name": "host__os", "op": "has", "val": "Linux"} |
3318 | 3284 | ]} |
3319 | 3285 | res = test_client.get( |
3320 | self.check_url(f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3286 | f'/v3/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}' | |
3321 | 3287 | ) |
3322 | 3288 | assert res.status_code == 200 |
3323 | 3289 | assert res.json['count'] == 1 |
3365 | 3331 | {"name": "create_date", "op": "eq", "val": vuln.create_date.strftime("%Y-%m-%d")} |
3366 | 3332 | ]} |
3367 | 3333 | res = test_client.get( |
3368 | self.check_url(f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3334 | f'/v3/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}' | |
3369 | 3335 | ) |
3370 | 3336 | assert res.status_code == 200 |
3371 | 3337 | assert res.json['count'] == 3 |
3380 | 3346 | {"name": "create_date", "op": "eq", "val": "30/01/2020"} |
3381 | 3347 | ]} |
3382 | 3348 | res = test_client.get( |
3383 | self.check_url(f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3349 | f'/v3/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}' | |
3384 | 3350 | ) |
3385 | 3351 | assert res.status_code == 200 |
3386 | 3352 | |
3389 | 3355 | query_filter = {'filters': [{'name': 'host_id', 'op': 'not_in', |
3390 | 3356 | 'val': '\U0010a1a7\U00093553\U000eb46a\x1e\x10\r\x18%\U0005ddfa0\x05\U000fdeba\x08\x04絮'}]} |
3391 | 3357 | res = test_client.get( |
3392 | self.check_url(f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3358 | f'/v3/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}' | |
3393 | 3359 | ) |
3394 | 3360 | assert res.status_code == 400 |
3395 | 3361 | |
3397 | 3363 | def test_search_hypothesis_test_found_case_2(self, test_client, session, workspace): |
3398 | 3364 | query_filter = {'filters': [{'name': 'host__os', 'op': 'ilike', 'val': -1915870387}]} |
3399 | 3365 | res = test_client.get( |
3400 | self.check_url(f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3366 | f'/v3/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}' | |
3401 | 3367 | ) |
3402 | 3368 | assert res.status_code == 400 |
3403 | 3369 | |
3409 | 3375 | ]) |
3410 | 3376 | def test_search_hypothesis_test_found_case_3(self, query_filter, test_client, session, workspace): |
3411 | 3377 | res = test_client.get( |
3412 | self.check_url(f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3378 | f'/v3/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}' | |
3413 | 3379 | ) |
3414 | 3380 | assert res.status_code == 400 |
3415 | 3381 | |
3421 | 3387 | ]) |
3422 | 3388 | def test_search_hypothesis_test_found_case_4(self, query_filter, test_client, session, workspace): |
3423 | 3389 | res = test_client.get( |
3424 | self.check_url(f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3390 | f'/v3/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}' | |
3425 | 3391 | ) |
3426 | 3392 | assert res.status_code == 400 |
3427 | 3393 | |
3433 | 3399 | ]) |
3434 | 3400 | def test_search_hypothesis_test_found_case_5(self, query_filter, test_client, session, workspace): |
3435 | 3401 | res = test_client.get( |
3436 | self.check_url(f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3402 | f'/v3/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}' | |
3437 | 3403 | ) |
3438 | 3404 | assert res.status_code == 400 |
3439 | 3405 | |
3441 | 3407 | def test_search_hypothesis_test_found_case_6(self, test_client, session, workspace): |
3442 | 3408 | query_filter = {'filters': [{'name': 'resolution', 'op': '==', 'val': ''}]} |
3443 | 3409 | res = test_client.get( |
3444 | self.check_url(f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3410 | f'/v3/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}' | |
3445 | 3411 | ) |
3446 | 3412 | assert res.status_code == 200 |
3447 | 3413 | |
3451 | 3417 | {'name': 'name', 'op': '>', 'val': '\U0004e755\U0007a789\U000e02d1\U000b3d32\x10\U000ad0e2,\x05\x1a'}, |
3452 | 3418 | {'name': 'creator', 'op': 'eq', 'val': 21883}]} |
3453 | 3419 | res = test_client.get( |
3454 | self.check_url(f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3420 | f'/v3/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}' | |
3455 | 3421 | ) |
3456 | 3422 | assert res.status_code == 400 |
3457 | 3423 | |
3462 | 3428 | ]) |
3463 | 3429 | def test_search_hypothesis_test_found_case_7_valid(self, query_filter, test_client, session, workspace): |
3464 | 3430 | res = test_client.get( |
3465 | self.check_url(f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3431 | f'/v3/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}' | |
3466 | 3432 | ) |
3467 | 3433 | assert res.status_code == 200 |
3468 | 3434 | |
3470 | 3436 | def test_search_hypothesis_test_found_case_8(self, test_client, session, workspace): |
3471 | 3437 | query_filter = {'filters': [{'name': 'hostnames', 'op': '==', 'val': ''}]} |
3472 | 3438 | res = test_client.get( |
3473 | self.check_url(f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3439 | f'/v3/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}' | |
3474 | 3440 | ) |
3475 | 3441 | assert res.status_code == 200 |
3476 | 3442 | |
3480 | 3446 | 'val': '0\x00\U00034383$\x13-\U000375fb\U0007add2\x01\x01\U0010c23a'}]} |
3481 | 3447 | |
3482 | 3448 | res = test_client.get( |
3483 | self.check_url(f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3449 | f'/v3/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}' | |
3484 | 3450 | ) |
3485 | 3451 | assert res.status_code == 400 |
3486 | 3452 | |
3489 | 3455 | query_filter = {'filters': [{'name': 'impact_integrity', 'op': 'neq', 'val': 0}]} |
3490 | 3456 | |
3491 | 3457 | res = test_client.get( |
3492 | self.check_url(f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3458 | f'/v3/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}' | |
3493 | 3459 | ) |
3494 | 3460 | assert res.status_code == 400 |
3495 | 3461 | |
3498 | 3464 | query_filter = {'filters': [{'name': 'host_id', 'op': 'like', 'val': '0'}]} |
3499 | 3465 | |
3500 | 3466 | res = test_client.get( |
3501 | self.check_url(f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3467 | f'/v3/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}' | |
3502 | 3468 | ) |
3503 | 3469 | assert res.status_code == 400 |
3504 | 3470 | |
3507 | 3473 | query_filter = {'filters': [{'name': 'custom_fields', 'op': 'like', 'val': ''}]} |
3508 | 3474 | |
3509 | 3475 | res = test_client.get( |
3510 | self.check_url(f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3476 | f'/v3/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}' | |
3511 | 3477 | ) |
3512 | 3478 | assert res.status_code == 400 |
3513 | 3479 | |
3516 | 3482 | query_filter = {'filters': [{'name': 'impact_accountability', 'op': 'ilike', 'val': '0'}]} |
3517 | 3483 | |
3518 | 3484 | res = test_client.get( |
3519 | self.check_url(f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3485 | f'/v3/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}' | |
3520 | 3486 | ) |
3521 | 3487 | assert res.status_code == 400 |
3522 | 3488 | |
3533 | 3499 | query_filter = {'filters': [{'name': 'severity', 'op': 'eq', 'val': 'high'}]} |
3534 | 3500 | |
3535 | 3501 | res = test_client.get( |
3536 | self.check_url(f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3502 | f'/v3/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}' | |
3537 | 3503 | ) |
3538 | 3504 | assert res.status_code == 200 |
3539 | 3505 | assert res.json['count'] == 20 |
3556 | 3522 | } |
3557 | 3523 | |
3558 | 3524 | res = test_client.get( |
3559 | self.check_url(f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3525 | f'/v3/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}' | |
3560 | 3526 | ) |
3561 | 3527 | assert res.status_code == 400 |
3562 | 3528 | |
3581 | 3547 | } |
3582 | 3548 | |
3583 | 3549 | res = test_client.get( |
3584 | self.check_url(f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3550 | f'/v3/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}' | |
3585 | 3551 | ) |
3586 | 3552 | assert res.status_code == 200 |
3587 | 3553 | expected_order = sort_order["expected"] |
3605 | 3571 | } |
3606 | 3572 | |
3607 | 3573 | res = test_client.get( |
3608 | self.check_url(f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3574 | f'/v3/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}' | |
3609 | 3575 | ) |
3610 | 3576 | assert res.status_code == 200 |
3611 | 3577 | expected_order = ['critical', 'high', 'med', 'low'] |
3646 | 3612 | } |
3647 | 3613 | |
3648 | 3614 | res = test_client.get( |
3649 | self.check_url(f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3615 | f'/v3/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}' | |
3650 | 3616 | ) |
3651 | 3617 | assert res.status_code == 200 |
3652 | 3618 | assert res.json['count'] == 100 |
3653 | ||
3654 | ||
3655 | class TestVulnerabilitySearchV3(TestVulnerabilitySearch): | |
3656 | def check_url(self, url): | |
3657 | return v2_to_v3(url) | |
3658 | 3619 | |
3659 | 3620 | |
3660 | 3621 | def test_type_filter(workspace, session, |
3828 | 3789 | @given(VulnerabilityData) |
3829 | 3790 | def send_api_create_request(raw_data): |
3830 | 3791 | ws_name = host_with_hostnames.workspace.name |
3831 | res = test_client.post(f'/v2/ws/{ws_name}/vulns/', | |
3792 | res = test_client.post(f'/v3/ws/{ws_name}/vulns/', | |
3832 | 3793 | data=raw_data) |
3833 | 3794 | assert res.status_code in [201, 400, 409] |
3834 | 3795 | |
3842 | 3803 | @given(VulnerabilityDataWithId) |
3843 | 3804 | def send_api_update_request(raw_data): |
3844 | 3805 | ws_name = host_with_hostnames.workspace.name |
3845 | res = test_client.put(f"/v2/ws/{ws_name}/vulns/{raw_data['_id']}", | |
3806 | res = test_client.put(f"/v3/ws/{ws_name}/vulns/{raw_data['_id']}", | |
3846 | 3807 | data=raw_data) |
3847 | 3808 | assert res.status_code in [200, 400, 409, 405] |
3848 | 3809 | |
3900 | 3861 | def send_api_filter_request(raw_filter): |
3901 | 3862 | ws_name = host_with_hostnames.workspace.name |
3902 | 3863 | encoded_filter = urllib.parse.quote(json.dumps(raw_filter)) |
3903 | res = test_client.get(f'/v2/ws/{ws_name}/vulns/filter?q={encoded_filter}') | |
3864 | res = test_client.get(f'/v3/ws/{ws_name}/vulns/filter?q={encoded_filter}') | |
3904 | 3865 | if res.status_code not in [200, 400]: |
3905 | 3866 | print(json.dumps(raw_filter)) |
3906 | 3867 |
11 | 11 | from faraday.server.api.modules.vulnerability_template import VulnerabilityTemplateView |
12 | 12 | from tests import factories |
13 | 13 | from tests.test_api_non_workspaced_base import ( |
14 | ReadWriteAPITests, PatchableTestsMixin | |
14 | ReadWriteAPITests | |
15 | 15 | ) |
16 | 16 | from faraday.server.models import ( |
17 | 17 | VulnerabilityTemplate, |
23 | 23 | UserFactory, |
24 | 24 | VulnerabilityFactory |
25 | 25 | ) |
26 | from tests.utils.url import v2_to_v3 | |
27 | 26 | |
28 | 27 | TEMPLATES_DATA = [ |
29 | 28 | {'name': 'XML Injection (aka Blind XPath Injection) (Type: Base)', |
51 | 50 | view_class = VulnerabilityTemplateView |
52 | 51 | patchable_fields = ['description'] |
53 | 52 | |
54 | def check_url(self, url): | |
55 | return url | |
56 | ||
57 | 53 | def test_backwards_json_compatibility(self, test_client, session): |
58 | 54 | self.factory.create() |
59 | 55 | session.commit() |
92 | 88 | def test_create_new_vulnerability_template(self, session, test_client): |
93 | 89 | vuln_count_previous = session.query(VulnerabilityTemplate).count() |
94 | 90 | raw_data = self._create_post_data_vulnerability_template(references='') |
95 | res = test_client.post(self.check_url('/v2/vulnerability_template/'), data=raw_data) | |
91 | res = test_client.post('/v3/vulnerability_template', data=raw_data) | |
96 | 92 | assert res.status_code == 201 |
97 | 93 | assert isinstance(res.json['_id'], int) |
98 | 94 | assert vuln_count_previous + 1 == session.query(VulnerabilityTemplate).count() |
165 | 161 | )) |
166 | 162 | session.commit() |
167 | 163 | |
168 | query = self.check_url(f'/v2/vulnerability_template/filter?q={{"filters": [' | |
169 | f'{{ "name": "{filters["field"]}",' | |
170 | f' "op": "{filters["op"]}", ' | |
171 | f' "val": "{filters["filtered_value"]}" }}]}}') | |
164 | query = f'/v3/vulnerability_template/filter?q={{"filters": [{{ "name": "{filters["field"]}",' \ | |
165 | f' "op": "{filters["op"]}", "val": "{filters["filtered_value"]}" }}]}}' | |
172 | 166 | |
173 | 167 | res = test_client.get(query) |
174 | 168 | assert res.status_code == 200 |
203 | 197 | )) |
204 | 198 | session.commit() |
205 | 199 | |
206 | query = self.check_url(f'/v2/vulnerability_template/filter?q={{"filters": [' | |
207 | f'{{ "name": "{filters["field"]}",' | |
208 | f' "op": "{filters["op"]}", ' | |
209 | f' "val": "{templates[0].creator.id}" }}]}}') | |
200 | query = f'/v3/vulnerability_template/filter?q={{"filters": [{{ "name": "{filters["field"]}",' \ | |
201 | f' "op": "{filters["op"]}", "val": "{templates[0].creator.id}" }}]}}' | |
210 | 202 | |
211 | 203 | res = test_client.get(query) |
212 | 204 | assert res.status_code == 200 |
241 | 233 | )) |
242 | 234 | session.commit() |
243 | 235 | |
244 | query = self.check_url(f'/v2/vulnerability_template/filter?q={{"filters": [' | |
245 | f'{{ "name": "{filters["field"]}",' | |
246 | f' "op": "{filters["op"]}", ' | |
247 | f' "val": "{filters["filtered_value"]}" }}]}}') | |
236 | query = f'/v3/vulnerability_template/filter?q={{"filters": [{{ "name": "{filters["field"]}",' \ | |
237 | f' "op": "{filters["op"]}", "val": "{filters["filtered_value"]}" }}]}}' | |
248 | 238 | |
249 | 239 | res = test_client.get(query) |
250 | 240 | assert res.status_code == 200 |
256 | 246 | template = self.factory.create() |
257 | 247 | session.commit() |
258 | 248 | raw_data = self._create_post_data_vulnerability_template(references='') |
259 | res = test_client.put(self.check_url(f'/v2/vulnerability_template/{template.id}/'), data=raw_data) | |
249 | res = test_client.put(f'/v3/vulnerability_template/{template.id}', data=raw_data) | |
260 | 250 | assert res.status_code == 200 |
261 | 251 | updated_template = session.query(VulnerabilityTemplate).filter_by(id=template.id).first() |
262 | 252 | assert updated_template.name == raw_data['name'] |
278 | 268 | session.commit() |
279 | 269 | raw_data = self._create_post_data_vulnerability_template( |
280 | 270 | references=references) |
281 | res = test_client.put(self.check_url(f'/v2/vulnerability_template/{template.id}/'), data=raw_data) | |
271 | res = test_client.put(f'/v3/vulnerability_template/{template.id}', data=raw_data) | |
282 | 272 | assert res.status_code == 400 |
283 | 273 | |
284 | 274 | def test_update_vulnerabiliy_template_change_refs(self, session, test_client): |
288 | 278 | self.first_object.reference_template_instances.add(ref) |
289 | 279 | session.commit() |
290 | 280 | raw_data = self._create_post_data_vulnerability_template(references='new_ref,another_ref') |
291 | res = test_client.put(self.check_url(f'/v2/vulnerability_template/{template.id}/'), data=raw_data) | |
281 | res = test_client.put(f'/v3/vulnerability_template/{template.id}', data=raw_data) | |
292 | 282 | assert res.status_code == 200 |
293 | 283 | updated_template = session.query(VulnerabilityTemplate).filter_by(id=template.id).first() |
294 | 284 | assert updated_template.name == raw_data['name'] |
300 | 290 | def test_create_new_vulnerability_template_with_references(self, session, test_client): |
301 | 291 | vuln_count_previous = session.query(VulnerabilityTemplate).count() |
302 | 292 | raw_data = self._create_post_data_vulnerability_template(references='ref1,ref2') |
303 | res = test_client.post(self.check_url('/v2/vulnerability_template/'), data=raw_data) | |
293 | res = test_client.post('/v3/vulnerability_template', data=raw_data) | |
304 | 294 | assert res.status_code == 201 |
305 | 295 | assert isinstance(res.json['_id'], int) |
306 | 296 | assert set(res.json['refs']) == set(['ref1', 'ref2']) |
311 | 301 | def test_delete_vuln_template(self, session, test_client): |
312 | 302 | template = self.factory.create() |
313 | 303 | vuln_count_previous = session.query(VulnerabilityTemplate).count() |
314 | res = test_client.delete(self.check_url(f'/v2/vulnerability_template/{template.id}/')) | |
304 | res = test_client.delete(f'/v3/vulnerability_template/{template.id}') | |
315 | 305 | |
316 | 306 | assert res.status_code == 204 |
317 | 307 | assert vuln_count_previous - 1 == session.query(VulnerabilityTemplate).count() |
439 | 429 | 'csrf_token': csrf_token |
440 | 430 | } |
441 | 431 | headers = {'Content-type': 'multipart/form-data'} |
442 | res = test_client.post(self.check_url('/v2/vulnerability_template/bulk_create/'), | |
432 | res = test_client.post('/v3/vulnerability_template/bulk_create', | |
443 | 433 | data=data, headers=headers, use_json_data=False) |
444 | 434 | assert res.status_code == 200 |
445 | 435 | assert len(res.json['vulns_created']) == expected_created_vuln_template |
457 | 447 | 'csrf_token': csrf_token |
458 | 448 | } |
459 | 449 | headers = {'Content-type': 'multipart/form-data'} |
460 | res = test_client.post(self.check_url('/v2/vulnerability_template/bulk_create/'), | |
450 | res = test_client.post('/v3/vulnerability_template/bulk_create', | |
461 | 451 | data=data, headers=headers, use_json_data=False) |
462 | 452 | assert res.status_code == 200 |
463 | 453 | assert len(res.json['vulns_created']) == expected_created_vuln_template |
475 | 465 | 'csrf_token': csrf_token |
476 | 466 | } |
477 | 467 | headers = {'Content-type': 'multipart/form-data'} |
478 | res = test_client.post(self.check_url('/v2/vulnerability_template/bulk_create/'), | |
468 | res = test_client.post('/v3/vulnerability_template/bulk_create', | |
479 | 469 | data=data, headers=headers, use_json_data=False) |
480 | 470 | assert res.status_code == 200 |
481 | 471 | assert len(res.json['vulns_created']) == expected_created_vuln_template |
492 | 482 | 'csrf_token': csrf_token |
493 | 483 | } |
494 | 484 | headers = {'Content-type': 'multipart/form-data'} |
495 | res = test_client.post(self.check_url('/v2/vulnerability_template/bulk_create/'), | |
485 | res = test_client.post('/v3/vulnerability_template/bulk_create', | |
496 | 486 | data=data, headers=headers, use_json_data=False) |
497 | 487 | assert res.status_code == 400 |
498 | 488 | assert 'name' not in res.data.decode('utf8') |
516 | 506 | 'csrf_token': csrf_token |
517 | 507 | } |
518 | 508 | headers = {'Content-type': 'multipart/form-data'} |
519 | res = test_client.post(self.check_url('/v2/vulnerability_template/bulk_create/'), | |
509 | res = test_client.post('/v3/vulnerability_template/bulk_create', | |
520 | 510 | data=data, headers=headers, use_json_data=False) |
521 | 511 | assert res.status_code == 200 |
522 | 512 | assert len(res.json['vulns_created']) == 1 |
538 | 528 | 'vulns': [vuln_1, vuln_2] |
539 | 529 | } |
540 | 530 | |
541 | res = test_client.post(self.check_url('/v2/vulnerability_template/bulk_create/'), json=data) | |
531 | res = test_client.post('/v3/vulnerability_template/bulk_create', json=data) | |
542 | 532 | assert res.status_code == 200 |
543 | 533 | |
544 | 534 | vulns_created = res.json['vulns_created'] |
556 | 546 | 'vulns': [vuln_1, vuln_2] |
557 | 547 | } |
558 | 548 | |
559 | res = test_client.post(self.check_url('/v2/vulnerability_template/bulk_create/'), json=data) | |
549 | res = test_client.post('/v3/vulnerability_template/bulk_create', json=data) | |
560 | 550 | assert res.status_code == 403 |
561 | 551 | assert res.json['message'] == 'Invalid CSRF token.' |
562 | 552 | |
563 | 553 | def test_bulk_create_without_data(self, test_client, csrf_token): |
564 | 554 | data = {'csrf_token': csrf_token} |
565 | res = test_client.post(self.check_url('/v2/vulnerability_template/bulk_create/'), json=data) | |
555 | res = test_client.post('/v3/vulnerability_template/bulk_create', json=data) | |
566 | 556 | |
567 | 557 | assert res.status_code == 400 |
568 | 558 | assert res.json['message'] == 'Missing data to create vulnerabilities templates.' |
583 | 573 | 'vulns': [vuln_1, vuln_2] |
584 | 574 | } |
585 | 575 | |
586 | res = test_client.post(self.check_url('/v2/vulnerability_template/bulk_create/'), json=data) | |
576 | res = test_client.post('/v3/vulnerability_template/bulk_create', json=data) | |
587 | 577 | assert res.status_code == 200 |
588 | 578 | |
589 | 579 | assert len(res.json['vulns_with_conflict']) == 1 |
611 | 601 | 'vulns': [vuln_1, vuln_2] |
612 | 602 | } |
613 | 603 | |
614 | res = test_client.post(self.check_url('/v2/vulnerability_template/bulk_create/'), json=data) | |
604 | res = test_client.post('/v3/vulnerability_template/bulk_create', json=data) | |
615 | 605 | assert res.status_code == 409 |
616 | 606 | |
617 | 607 | assert len(res.json['vulns_with_conflict']) == 2 |
619 | 609 | assert res.json['vulns_with_conflict'][1][1] == vuln_2['name'] |
620 | 610 | |
621 | 611 | assert len(res.json['vulns_created']) == 0 |
622 | ||
623 | ||
624 | class TestListVulnerabilityTemplateViewV3(TestListVulnerabilityTemplateView, PatchableTestsMixin): | |
625 | def url(self, obj=None): | |
626 | return v2_to_v3(super().url(obj)) | |
627 | ||
628 | def check_url(self, url): | |
629 | return v2_to_v3(url) |
10 | 10 | |
11 | 11 | from faraday.server.models import Workspace, Scope |
12 | 12 | from faraday.server.api.modules.workspaces import WorkspaceView |
13 | from tests.test_api_non_workspaced_base import ReadWriteAPITests, PatchableTestsMixin | |
13 | from tests.test_api_non_workspaced_base import ReadWriteAPITests | |
14 | 14 | from tests import factories |
15 | from tests.utils.url import v2_to_v3 | |
16 | 15 | |
17 | 16 | |
18 | 17 | class TestWorkspaceAPI(ReadWriteAPITests): |
22 | 21 | lookup_field = 'name' |
23 | 22 | view_class = WorkspaceView |
24 | 23 | patchable_fields = ['name'] |
25 | ||
26 | def check_url(self, url): | |
27 | return url | |
28 | 24 | |
29 | 25 | @pytest.mark.usefixtures('ignore_nplusone') |
30 | 26 | def test_filter_restless_by_name(self, test_client): |
370 | 366 | workspace.active = False |
371 | 367 | session.add(workspace) |
372 | 368 | session.commit() |
373 | res = test_client.put(f'{self.url()}{workspace.name}/activate/') | |
374 | assert res.status_code == 200 | |
375 | ||
376 | res = test_client.get(f'{self.url()}{workspace.name}/') | |
369 | res = test_client.patch(self.url(workspace), data={'active': True}) | |
370 | assert res.status_code == 200 | |
371 | ||
372 | res = test_client.get(self.url(workspace)) | |
377 | 373 | active = res.json.get('active') |
378 | 374 | assert active |
379 | 375 | |
384 | 380 | workspace.active = True |
385 | 381 | session.add(workspace) |
386 | 382 | session.commit() |
387 | res = test_client.put(f'{self.url()}{workspace.name}/deactivate/') | |
388 | assert res.status_code == 200 | |
389 | ||
390 | res = test_client.get(f'{self.url()}{workspace.name}/') | |
383 | res = test_client.patch(self.url(workspace), data={'active': False}) | |
384 | assert res.status_code == 200 | |
385 | ||
386 | res = test_client.get(self.url(workspace)) | |
391 | 387 | active = res.json.get('active') |
392 | 388 | assert not active |
393 | 389 | |
403 | 399 | res = test_client.post(self.url(), data=raw_data) |
404 | 400 | assert res.status_code == 400 |
405 | 401 | assert workspace_count_previous == session.query(Workspace).count() |
406 | ||
407 | ||
408 | class TestWorkspaceAPIV3(TestWorkspaceAPI, PatchableTestsMixin): | |
409 | ||
410 | def check_url(self, url): | |
411 | return v2_to_v3(url) | |
412 | ||
413 | def url(self, obj=None): | |
414 | return v2_to_v3(super().url(obj)) | |
415 | ||
416 | def test_workspace_activation(self, test_client, workspace, session): | |
417 | workspace.active = False | |
418 | session.add(workspace) | |
419 | session.commit() | |
420 | res = test_client.patch(self.url(workspace), data={'active': True}) | |
421 | assert res.status_code == 200 | |
422 | ||
423 | res = test_client.get(self.url(workspace)) | |
424 | active = res.json.get('active') | |
425 | assert active | |
426 | ||
427 | active_query = session.query(Workspace).filter_by(id=workspace.id).first().active | |
428 | assert active_query | |
429 | ||
430 | def test_workspace_deactivation(self, test_client, workspace, session): | |
431 | workspace.active = True | |
432 | session.add(workspace) | |
433 | session.commit() | |
434 | res = test_client.patch(self.url(workspace), data={'active': False}) | |
435 | assert res.status_code == 200 | |
436 | ||
437 | res = test_client.get(self.url(workspace)) | |
438 | active = res.json.get('active') | |
439 | assert not active | |
440 | ||
441 | active_query = session.query(Workspace).filter_by(id=workspace.id).first().active | |
442 | assert not active_query |
15 | 15 | from tests.test_api_pagination import PaginationTestsMixin as \ |
16 | 16 | OriginalPaginationTestsMixin |
17 | 17 | |
18 | API_PREFIX = '/v2/ws/' | |
18 | API_PREFIX = '/v3/ws/' | |
19 | 19 | OBJECT_COUNT = 5 |
20 | 20 | |
21 | 21 | |
49 | 49 | |
50 | 50 | def url(self, obj=None, workspace=None): |
51 | 51 | workspace = workspace or self.workspace |
52 | url = API_PREFIX + workspace.name + '/' + self.api_endpoint + '/' | |
52 | url = API_PREFIX + workspace.name + '/' + self.api_endpoint | |
53 | 53 | if obj is not None: |
54 | 54 | id_ = str(obj.id) if isinstance( |
55 | 55 | obj, self.model) else str(obj) |
56 | url += id_ + u'/' | |
56 | url += '/' + id_ | |
57 | 57 | return url |
58 | 58 | |
59 | 59 | |
178 | 178 | def control_cant_change_data(self, data: dict) -> dict: |
179 | 179 | return data |
180 | 180 | |
181 | @pytest.mark.parametrize("method", ["PUT"]) | |
181 | @staticmethod | |
182 | def control_patcheable_data(test_suite, data: dict) -> dict: | |
183 | return {key: value for (key, value) in data.items() if key in test_suite.patchable_fields} | |
184 | ||
185 | @pytest.mark.parametrize("method", ["PUT", "PATCH"]) | |
182 | 186 | def test_update_an_object(self, test_client, method): |
183 | 187 | data = self.factory.build_dict(workspace=self.workspace) |
184 | 188 | data = self.control_cant_change_data(data) |
187 | 191 | res = test_client.put(self.url(self.first_object), |
188 | 192 | data=data) |
189 | 193 | elif method == "PATCH": |
190 | data = PatchableTestsMixin.control_data(self, data) | |
194 | data = self.control_patcheable_data(self, data) | |
191 | 195 | res = test_client.patch(self.url(self.first_object), data=data) |
192 | 196 | assert res.status_code == 200 |
193 | 197 | assert self.model.query.count() == count |
195 | 199 | assert res.json[updated_field] == getattr(self.first_object, |
196 | 200 | updated_field) |
197 | 201 | |
198 | @pytest.mark.parametrize("method", ["PUT"]) | |
202 | @pytest.mark.parametrize("method", ["PUT", "PATCH"]) | |
199 | 203 | def test_update_an_object_readonly_fails(self, test_client, method): |
200 | 204 | self.workspace.readonly = True |
201 | 205 | db.session.commit() |
212 | 216 | assert self.model.query.count() == OBJECT_COUNT |
213 | 217 | assert old_field == getattr(self.model.query.filter(self.model.id == old_id).one(), unique_field) |
214 | 218 | |
215 | @pytest.mark.parametrize("method", ["PUT"]) | |
219 | @pytest.mark.parametrize("method", ["PUT", "PATCH"]) | |
216 | 220 | def test_update_inactive_fails(self, test_client, method): |
217 | 221 | self.workspace.deactivate() |
218 | 222 | db.session.commit() |
227 | 231 | assert res.status_code == 403 |
228 | 232 | assert self.model.query.count() == count |
229 | 233 | |
230 | @pytest.mark.parametrize("method", ["PUT"]) | |
234 | @pytest.mark.parametrize("method", ["PUT", "PATCH"]) | |
231 | 235 | def test_update_fails_with_existing(self, test_client, session, method): |
232 | 236 | for unique_field in self.unique_fields: |
233 | 237 | unique_field_value = getattr(self.objects[1], unique_field) |
243 | 247 | def test_update_an_object_fails_with_empty_dict(self, test_client): |
244 | 248 | """To do this the user should use a PATCH request""" |
245 | 249 | res = test_client.put(self.url(self.first_object), data={}) |
246 | assert res.status_code == 400 | |
247 | ||
248 | @pytest.mark.parametrize("method", ["PUT"]) | |
250 | assert res.status_code == 400, (res.status_code, res.json) | |
251 | ||
252 | def test_patch_update_an_object_does_not_fail_with_partial_data(self, test_client): | |
253 | """To do this the user should use a PATCH request""" | |
254 | res = test_client.patch(self.url(self.first_object), data={}) | |
255 | assert res.status_code == 200, (res.status_code, res.json) | |
256 | ||
257 | @pytest.mark.parametrize("method", ["PUT", "PATCH"]) | |
249 | 258 | def test_update_cant_change_id(self, test_client, method): |
250 | 259 | raw_json = self.factory.build_dict(workspace=self.workspace) |
251 | 260 | raw_json = self.control_cant_change_data(raw_json) |
262 | 271 | assert object_id == expected_id |
263 | 272 | |
264 | 273 | |
265 | class PatchableTestsMixin(UpdateTestsMixin): | |
266 | ||
267 | @staticmethod | |
268 | def control_data(test_suite, data: dict) -> dict: | |
269 | return {key: value for (key, value) in data.items() if key in test_suite.patchable_fields} | |
270 | ||
271 | @pytest.mark.parametrize("method", ["PUT", "PATCH"]) | |
272 | def test_update_an_object(self, test_client, method): | |
273 | super().test_update_an_object(test_client, method) | |
274 | ||
275 | @pytest.mark.parametrize("method", ["PUT", "PATCH"]) | |
276 | def test_update_an_object_readonly_fails(self, test_client, method): | |
277 | super().test_update_an_object_readonly_fails(test_client, method) | |
278 | ||
279 | @pytest.mark.parametrize("method", ["PUT", "PATCH"]) | |
280 | def test_update_inactive_fails(self, test_client, method): | |
281 | super().test_update_inactive_fails(test_client, method) | |
282 | ||
283 | @pytest.mark.parametrize("method", ["PUT", "PATCH"]) | |
284 | def test_update_fails_with_existing(self, test_client, session, method): | |
285 | super().test_update_fails_with_existing(test_client, session, method) | |
286 | ||
287 | def test_update_an_object_fails_with_empty_dict(self, test_client): | |
288 | """To do this the user should use a PATCH request""" | |
289 | res = test_client.patch(self.url(self.first_object), data={}) | |
290 | assert res.status_code == 200, (res.status_code, res.json) | |
291 | ||
292 | @pytest.mark.parametrize("method", ["PUT", "PATCH"]) | |
293 | def test_update_cant_change_id(self, test_client, method): | |
294 | super().test_update_cant_change_id(test_client, method) | |
295 | ||
296 | ||
297 | 274 | class CountTestsMixin: |
298 | 275 | def test_count(self, test_client, session, user_factory): |
299 | 276 | |
310 | 287 | |
311 | 288 | session.commit() |
312 | 289 | |
313 | if self.view_class.route_prefix.startswith("/v2"): | |
314 | res = test_client.get(urljoin(self.url(), "count/?group_by=creator_id")) | |
315 | else: | |
316 | res = test_client.get(urljoin(self.url(), "count?group_by=creator_id")) | |
290 | res = test_client.get(urljoin(self.url(), "count?group_by=creator_id")) | |
317 | 291 | |
318 | 292 | assert res.status_code == 200, res.json |
319 | 293 | res = res.get_json() |
343 | 317 | |
344 | 318 | session.commit() |
345 | 319 | |
346 | if self.view_class.route_prefix.startswith("/v2"): | |
347 | res = test_client.get(urljoin(self.url(), "count/?group_by=creator_id&order=desc")) | |
348 | else: | |
349 | res = test_client.get(urljoin(self.url(), "count?group_by=creator_id&order=desc")) | |
320 | res = test_client.get(urljoin(self.url(), "count?group_by=creator_id&order=desc")) | |
350 | 321 | |
351 | 322 | assert res.status_code == 200, res.json |
352 | 323 | res = res.get_json() |
14 | 14 | self.secret_key = faraday_server.secret_key |
15 | 15 | self.websocket_port = faraday_server.websocket_port |
16 | 16 | |
17 | def test_ldap(self): | |
18 | from faraday.server.config import ldap | |
19 | self.admin_group = ldap.admin_group | |
20 | self.client_group = ldap.client_group | |
21 | self.disconnect_timeout = ldap.disconnect_timeout | |
22 | self.domain_dn = ldap.domain_dn | |
23 | self.enabled = ldap.enabled | |
24 | self.pentester_group = ldap.pentester_group | |
25 | self.port = ldap.port | |
26 | self.server = ldap.server | |
27 | self.use_ldaps = ldap.use_ldaps | |
28 | self.use_start_tls = ldap.use_start_tls | |
29 | ||
30 | 17 | def test_storage(self): |
31 | 18 | from faraday.server.config import storage |
32 | 19 | self.path = storage.path |
312 | 312 | lambda workspace, test_client, session: SqlApi(workspace.name, test_client, session), |
313 | 313 | ]) |
314 | 314 | @pytest.mark.usefixtures('ignore_nplusone') |
315 | @pytest.mark.skip("No available in community") | |
315 | 316 | def test_mail_notification(self, api, session, test_client): |
316 | 317 | workspace = WorkspaceFactory.create() |
317 | 318 | vuln = VulnerabilityFactory.create(workspace=workspace, severity='low') |
856 | 857 | searcher = Searcher(api(workspace, test_client, session)) |
857 | 858 | rule_disabled: Rule = RuleFactory.create(disabled=True, workspace=workspace) |
858 | 859 | rule_enabled = RuleFactory.create(disabled=False, workspace=workspace) |
859 | rule_disabled.conditions = [ConditionFactory.create(field='severity', value="low")] | |
860 | rule_enabled.conditions = [ConditionFactory.create(field='severity', value="medium")] | |
860 | ||
861 | with session.no_autoflush: | |
862 | rule_disabled.conditions = [ConditionFactory.create(field='severity', value="low")] | |
863 | rule_enabled.conditions = [ConditionFactory.create(field='severity', value="medium")] | |
861 | 864 | |
862 | 865 | action = ActionFactory.create(command='DELETE') |
863 | 866 | session.add(action) |
4 | 4 | update_executors, BroadcastServerProtocol |
5 | 5 | |
6 | 6 | from tests.factories import AgentFactory, ExecutorFactory |
7 | from tests.utils.url import v2_to_v3 | |
8 | 7 | |
9 | 8 | |
10 | 9 | class TransportMock: |
27 | 26 | |
28 | 27 | class TestWebsocketBroadcastServerProtocol: |
29 | 28 | |
30 | def check_url(self, url): | |
31 | return url | |
32 | ||
33 | 29 | def _join_agent(self, test_client, session): |
34 | 30 | agent = AgentFactory.create(token='pepito') |
35 | 31 | session.add(agent) |
36 | 32 | session.commit() |
37 | 33 | |
38 | 34 | headers = {"Authorization": f"Agent {agent.token}"} |
39 | token = test_client.post(self.check_url('/v2/agent_websocket_token/'), headers=headers).json['token'] | |
35 | token = test_client.post('/v3/agent_websocket_token', headers=headers).json['token'] | |
40 | 36 | return token |
41 | 37 | |
42 | 38 | def test_join_agent_message_with_invalid_token_fails(self, session, proto, test_client): |
71 | 67 | message = '{"action": "LEAVE_AGENT"}' |
72 | 68 | assert proto.onMessage(message, False) |
73 | 69 | assert not agent.is_online |
74 | ||
75 | ||
76 | class TestWebsocketBroadcastServerProtocolV3(TestWebsocketBroadcastServerProtocol): | |
77 | def check_url(self, url): | |
78 | return v2_to_v3(url) | |
79 | 70 | |
80 | 71 | |
81 | 72 | class TestCheckExecutors: |