Codebase list python-faraday / e0b110f
New upstream version 3.16.0 Sophie Brun 2 years ago
111 changed file(s) with 1699 addition(s) and 1799 deletion(s). Raw diff Collapse all Expand all
100100 stage: build
101101 image: nixorg/nix
102102 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 ]
104106 - mkdir -p ~/.config/cachix
105107 - export USER=$(whoami)
106108 - echo "$CACHIX_CONFG" >~/.config/cachix/cachix.dhall
3434 image: nixorg/nix
3535 stage: pre_build
3636 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 ]
3840 - mkdir -p ~/.config/cachix
3941 - export USER=$(whoami)
4042 - echo "$CACHIX_CONFG" >~/.config/cachix/cachix.dhall
1919 - /opt/faraday/bin/faraday-server &
2020 - sleep 5
2121 - curl -v http://localhost:5985/_api/v2/info
22 - faraday-manage status-check
2322 - kill $(cat ~faraday/.faraday/faraday-server-port-5985.pid)
2423 - jobs
2524 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)
2828 CI_REGISTRY: docker.io
2929 CI_REGISTRY_IMAGE: index.docker.io/faradaysec/faraday
3030 script:
31 - !reference [ .get_secrets, script ]
3132 - docker image tag registry.gitlab.com/faradaysec/faraday:latest $CI_REGISTRY_IMAGE:latest
3233 - docker push $CI_REGISTRY_IMAGE:latest
3334 - docker image tag $CI_REGISTRY_IMAGE:latest $CI_REGISTRY_IMAGE:$VERSION
44 stage: test
55 allow_failure: true
66 script:
7 - nix-env -if pynixify/nixpkgs.nix -A vault
8 - !reference [ .get_secrets, script ]
79 - nix-env -if pynixify/nixpkgs.nix -A cachix
810 - mkdir -p ~/.config/cachix
911 - export USER=$(whoami)
0
10 pylint:
21 tags:
32 - faradaytests
43 image: nixorg/nix
54 stage: test # This should be after build_and_push_to_cachix to improve performance
65 script:
6 - nix-env -if pynixify/nixpkgs.nix -A vault
7 - !reference [ .get_secrets, script ]
78 - nix-env -if pynixify/nixpkgs.nix -A cachix
89 - mkdir -p ~/.config/cachix
910 - export USER=$(whoami)
3233 stage: test
3334 coverage: '/TOTAL\s+\d+\s+\d+\s+(\d+%)/'
3435 script:
36 - nix-env -if pynixify/nixpkgs.nix -A vault
37 - !reference [ .get_secrets, script ]
3538 - nix-env -if pynixify/nixpkgs.nix -A cachix
3639 - mkdir -p ~/.config/cachix
3740 - export USER=$(whoami)
6770 stage: test
6871 coverage: '/TOTAL\s+\d+\s+\d+\s+(\d+%)/'
6972 script:
73 - nix-env -if pynixify/nixpkgs.nix -A vault
74 - !reference [ .get_secrets, script ]
7075 - nix-env -if pynixify/nixpkgs.nix -A cachix
7176 - mkdir -p ~/.config/cachix
7277 - export USER=$(whoami)
7272 # Note: this size has to fit both our community, professional and corporate versions
7373 MAX_CLOSURE_SIZE_IN_MB: 850
7474 script:
75 - nix-env -if pynixify/nixpkgs.nix -A vault
7576 - nix-env -if pynixify/nixpkgs.nix -A cachix
7677 - nix-env -if pynixify/nixpkgs.nix -A gawk
78 - !reference [ .get_secrets, script ]
7779 - mkdir -p ~/.config/cachix
7880 - export USER=$(whoami)
7981 - echo "$CACHIX_CONFG" >~/.config/cachix/cachix.dhall
2424 variables:
2525 STORAGE_SPACE_BASE: gs://faraday-dev
2626 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
2834 - *google_storage_deb_rpm_base
2935 - "gsutil setmeta -h x-goog-meta-branch:${CI_COMMIT_BRANCH} ${GCLOUD_FILE_PATH}*.*"
3036 rules:
77 APT_CACHE_DIR: "$CI_PROJECT_DIR/apt-cache"
88 DEBIAN_FRONTEND: noninteractive
99 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'
1012
1113 ## ENV_VARS LIST
1214 # FULL_TEST = Test all jobs
2729 - mkdir -pv $APT_CACHE_DIR
2830
2931 include:
32 - local: .gitlab/ci/fetch-secrets.yaml
3033 - local: .gitlab/ci/testing/.pretesting-gitlab-ci.yml
3134 - local: .gitlab/ci/testing/.nix-testing-gitlab-ci.yml
3235 - 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
11 =====================================
22
33
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
420 3.15.0 [May 18th, 2021]:
521 ---
6
722 * ADD `Basic Auth` support
823 * ADD support for GET method in websocket_tokens, POST will be deprecated in the future
924 * ADD CVSS(String), CWE(String), CVE(relationship) columns to vulnerability model and API
106106 ## Links
107107
108108 * 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)
110110 * User's manual: [Faraday Wiki](https://github.com/infobyte/faraday/wiki) or check our [support portal](https://support.faradaysec.com/portal/home)
111111 * Download: [Download .deb/.rpm from releases page](https://github.com/infobyte/faraday/releases)
112112 * Commits RSS feed: https://github.com/infobyte/faraday/commits/master.atom
11 =====================================
22
33
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
420 3.15.0 [May 18th, 2021]:
521 ---
6
722 * ADD `Basic Auth` support
823 * ADD support for GET method in websocket_tokens, POST will be deprecated in the future
924 * ADD CVSS(String), CWE(String), CVE(relationship) columns to vulnerability model and API
11 # Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/)
22 # See the file 'doc/LICENSE' for the license information
33
4 __version__ = '3.15.0'
4 __version__ = '3.16.0'
55 __license_version__ = __version__
4141 from faraday.server.commands.faraday_schema_display import DatabaseSchema
4242 from faraday.server.commands.app_urls import show_all_urls
4343 from faraday.server.commands.app_urls import openapi_format
44 from faraday.server.commands import status_check as status_check_functions
4544 from faraday.server.commands import change_password as change_pass
4645 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
4846 from faraday.server.commands import change_username
4947 from faraday.server.commands import nginx_config
5048 from faraday.server.commands import import_vulnerability_template
49 from faraday.server.commands import manage_settings
5150 from faraday.server.models import db, User
5251 from faraday.server.web import get_app
5352 from faraday_plugins.plugins.manager import PluginsManager
9695 'ask for the desired one')
9796 )
9897 @click.option(
99 '--password', type=str, default=False,
98 '--password', type=str,
10099 help=('Instead of using a random password for the user "faraday", '
101100 'use the one provided')
102101 )
124123 pgcli.run_cli()
125124
126125
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
159126 @click.command(help="Changes the password of a user")
160127 @click.option('--username', required=True, prompt=True)
161128 @click.option('--password', required=True, prompt=True, confirmation_prompt=True, hide_input=True)
213180 get_app().user_datastore.create_user(username=username,
214181 email=email,
215182 password=hash_password(password),
216 role='admin',
183 roles=['admin'],
217184 is_ldap=False)
218185 db.session.commit()
219186 click.echo(click.style(
239206 click.echo(click.style(
240207 'Tables created successfully!',
241208 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()
247209
248210
249211 @click.command(
315277 nginx_config.generate_nginx_config(fqdn, port, ws_port, ssl_certificate, ssl_key, multitenant_url)
316278
317279
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
318287 cli.add_command(show_urls)
319288 cli.add_command(initdb)
320289 cli.add_command(database_schema)
321290 cli.add_command(create_superuser)
322291 cli.add_command(sql_shell)
323 cli.add_command(status_check)
324292 cli.add_command(create_tables)
325293 cli.add_command(change_password)
326294 cli.add_command(migrate)
327295 cli.add_command(add_custom_field)
328296 cli.add_command(delete_custom_field)
329 cli.add_command(support)
330297 cli.add_command(list_plugins)
331298 cli.add_command(rename_user)
332299 cli.add_command(openapi_yaml)
333300 cli.add_command(generate_nginx_config)
334301 cli.add_command(import_vulnerability_templates)
302 cli.add_command(settings)
335303
336304 if __name__ == '__main__':
337305 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 ###
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 ###
5656 raise UserWarning('Invalid username or password')
5757
5858 def _url(self, path, is_get=False):
59 url = self.base + 'v2/' + path
59 url = self.base + 'v3/' + path
6060 if self.command_id and 'commands' not in url and not url.endswith('}') and not is_get:
6161 if '?' in url:
6262 url += f'&command_id={self.command_id}'
6363 elif url.endswith('/'):
64 url = f'{url[:-1]}?command_id={self.command_id}'
65 else:
6466 url += f'?command_id={self.command_id}'
65 else:
66 url += f'/?command_id={self.command_id}'
6767 return url
6868
6969 def _get(self, url, object_name):
127127 else:
128128 cookies = getattr(resp, 'cookies', None)
129129 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)
131131 if token_response.status_code != 404:
132132 token = token_response.json()
133133 else:
134 token = self.requests.get(self.base + 'v2/token/').json
134 token = self.requests.get(self.base + 'v3/token').json
135135
136136 header = {'Authorization': f'Token {token}'}
137137
148148 self.params = params
149149 self.tool_name = tool_name
150150 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')
152152 return res["_id"]
153153
154154 def _command_info(self, duration=None):
170170
171171 def close_command(self, command_id, duration):
172172 data = self._command_info(duration)
173 self._put(self._url(f'ws/{self.workspace}/commands/{command_id}/'), data, 'command')
173 self._put(self._url(f'ws/{self.workspace}/commands/{command_id}'), data, 'command')
174174
175175 def fetch_vulnerabilities(self):
176 return [Structure(**item['value']) for item in self._get(self._url(f'ws/{self.workspace}/vulns/', True),
176 return [Structure(**item['value']) for item in self._get(self._url(f'ws/{self.workspace}/vulns', True),
177177 'vulnerabilities')['vulnerabilities']]
178178
179179 def fetch_services(self):
180 return [Structure(**item['value']) for item in self._get(self._url(f'ws/{self.workspace}/services/', True),
180 return [Structure(**item['value']) for item in self._get(self._url(f'ws/{self.workspace}/services', True),
181181 'services')['services']]
182182
183183 def fetch_hosts(self):
184 return [Structure(**item['value']) for item in self._get(self._url(f'ws/{self.workspace}/hosts/', True),
184 return [Structure(**item['value']) for item in self._get(self._url(f'ws/{self.workspace}/hosts', True),
185185 'hosts')['rows']]
186186
187187 def fetch_templates(self):
188188 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']]
190190
191191 def filter_vulnerabilities(self, **kwargs):
192192 if len(list(kwargs.keys())) > 1:
193193 params = urlencode(kwargs)
194 url = self._url(f'ws/{self.workspace}/vulns/?{params}')
194 url = self._url(f'ws/{self.workspace}/vulns?{params}')
195195 else:
196196 params = self.parse_args(**kwargs)
197197 url = self._url(f'ws/{self.workspace}/vulns/{params}', True)
200200
201201 def filter_services(self, **kwargs):
202202 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)
204204 return [Structure(**item['value']) for item in
205205 self._get(url, 'services')['services']]
206206
207207 def filter_hosts(self, **kwargs):
208208 params = urlencode(kwargs)
209 url = self._url(f'ws/{self.workspace}/hosts/?{params}', True)
209 url = self._url(f'ws/{self.workspace}/hosts?{params}', True)
210210 return [Structure(**item['value']) for item in
211211 self._get(url, 'hosts')['rows']]
212212
221221 return filtered_templates
222222
223223 def update_vulnerability(self, vulnerability):
224 return Structure(**self._put(self._url(f'ws/{self.workspace}/vulns/{vulnerability.id}/'),
224 return Structure(**self._put(self._url(f'ws/{self.workspace}/vulns/{vulnerability.id}'),
225225 vulnerability.__dict__, 'vulnerability'))
226226
227227 def update_service(self, service):
229229 service.ports = [service.ports]
230230 else:
231231 service.ports = []
232 return Structure(**self._put(self._url(f'ws/{self.workspace}/services/{service.id}/'),
232 return Structure(**self._put(self._url(f'ws/{self.workspace}/services/{service.id}'),
233233 service.__dict__, 'service'))
234234
235235 def update_host(self, host):
236 return Structure(**self._put(self._url(f'ws/{self.workspace}/hosts/{host.id}/'),
236 return Structure(**self._put(self._url(f'ws/{self.workspace}/hosts/{host.id}'),
237237 host.__dict__, 'hosts'))
238238
239239 def delete_vulnerability(self, vulnerability_id):
240 return self._delete(self._url(f'ws/{self.workspace}/vulns/{vulnerability_id}/'), 'vulnerability')
240 return self._delete(self._url(f'ws/{self.workspace}/vulns/{vulnerability_id}'), 'vulnerability')
241241
242242 def delete_service(self, service_id):
243 return self._delete(self._url(f'ws/{self.workspace}/services/{service_id}/'), 'service')
243 return self._delete(self._url(f'ws/{self.workspace}/services/{service_id}'), 'service')
244244
245245 def delete_host(self, host_id):
246 return self._delete(self._url(f'ws/{self.workspace}/hosts/{host_id}/'), 'host')
246 return self._delete(self._url(f'ws/{self.workspace}/hosts/{host_id}'), 'host')
247247
248248 @staticmethod
249249 def parse_args(**kwargs):
581581 _objs_value = None
582582 if 'object' in rule:
583583 _objs_value = rule['object']
584 command_start = datetime.now()
584 command_start = datetime.utcnow()
585585 command_id = self.api.create_command(
586586 itime=time.mktime(command_start.timetuple()),
587587 params=self.rules,
629629 if self.mail_notification:
630630 subject = 'Faraday searcher alert'
631631 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()))
633633 self.mail_notification.send_mail(expression, subject, body)
634634 logger.info(f"Sending mail to: '{expression}'")
635635 else:
636636 logger.warn("Searcher needs SMTP configuration to send mails")
637637
638 duration = (datetime.now() - command_start).seconds
638 duration = (datetime.utcnow() - command_start).seconds
639639 self.api.close_command(self.api.command_id, duration)
640640 return True
641641
3131 from faraday.server.schemas import NullToBlankString
3232 from faraday.server.utils.database import (
3333 get_conflict_object,
34 is_unique_constraint_violation
35 )
34 is_unique_constraint_violation,
35 not_null_constraint_violation
36 )
3637 from faraday.server.utils.filters import FlaskRestlessSchema
3738 from faraday.server.utils.search import search
3839
9697
9798 #: The prefix where the endpoint should be registered.
9899 #: This is useful for API versioning
99 route_prefix = '/v2/'
100 route_prefix = '/v3/'
100101
101102 #: Arguments that are passed to the view but shouldn't change the route
102103 #: rule. This should be used when route_prefix is parametrized
155156 #: it, indicate it here to prevent doing an extra SQL query.
156157 get_undefer = [] # List of columns to undefer
157158
159 trailing_slash = False
160
158161 def _get_schema_class(self):
159162 """By default, it returns ``self.schema_class``.
160163
361364 """
362365
363366 # Default attributes
364 route_prefix = '/v2/ws/<workspace_name>/'
367 route_prefix = '/v3/ws/<workspace_name>/'
365368 base_args = ['workspace_name'] # Required to prevent double usage of <workspace_name>
366369
367370 def _get_workspace(self, workspace_name):
944947 db.session.commit()
945948 except sqlalchemy.exc.IntegrityError as ex:
946949 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
948954 db.session.rollback()
949955 conflict_obj = get_conflict_object(db.session, obj, data)
950956 if conflict_obj:
11521158 else:
11531159 raise
11541160 return obj
1155
1156
1157 class PatchableMixin:
1158 # TODO must be used with a UpdateMixin, when v2 be deprecated, add patch() to that Mixin
11591161
11601162 def patch(self, object_id, **kwargs):
11611163 """
12501252 self._set_command_id(obj, False)
12511253 return super()._perform_update(object_id, obj, data, workspace_name)
12521254
1253
1254 class PatchableWorkspacedMixin(PatchableMixin):
1255 # TODO must be used with a UpdateWorkspacedMixin, when v2 be deprecated, add patch() to that Mixin
1256
12571255 def patch(self, object_id, workspace_name=None):
12581256 """
12591257 ---
00 # Faraday Penetration Test IDE
11 # Copyright (C) 2018 Infobyte LLC (http://www.infobytesec.com/)
22 # See the file 'doc/LICENSE' for the license information
3 import time
43 from datetime import datetime
54
5 import pytz
66 from flask import Blueprint
77 from marshmallow import fields
88
9 from faraday.server.api.base import AutoSchema, ReadWriteWorkspacedView, PaginatedMixin, PatchableWorkspacedMixin
9 from faraday.server.api.base import AutoSchema, ReadWriteWorkspacedView, PaginatedMixin
1010 from faraday.server.models import Command
1111 from faraday.server.schemas import PrimaryKeyRelatedField
1212
2929 creator = PrimaryKeyRelatedField('username', dump_only=True)
3030
3131 def load_itime(self, value):
32 return datetime.fromtimestamp(value)
32 return datetime.utcfromtimestamp(value)
3333
3434 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
3636
3737 def get_sum_created_vulnerabilities(self, obj):
3838 return obj.sum_created_vulnerabilities
8989 }
9090
9191
92 class ActivityFeedV3View(ActivityFeedView, PatchableWorkspacedMixin):
93 route_prefix = '/v3/ws/<workspace_name>/'
94 trailing_slash = False
95
96
9792 ActivityFeedView.register(activityfeed_api)
98 ActivityFeedV3View.register(activityfeed_api)
66 import logging
77
88 import pyotp
9 from faraday_agent_parameters_types.utils import type_validate, get_manifests
910 from flask import Blueprint, abort, request, make_response, jsonify
1011 from flask_classful import route
1112 from marshmallow import fields, Schema, EXCLUDE
1920 ReadOnlyView,
2021 CreateMixin,
2122 GenericView,
22 ReadOnlyMultiWorkspacedView,
23 PatchableMixin
23 ReadOnlyMultiWorkspacedView
2424 )
2525 from faraday.server.api.modules.workspaces import WorkspaceSchema
2626 from faraday.server.models import Agent, Executor, AgentExecution, db, \
177177 return agent
178178
179179
180 class AgentCreationV3View(AgentCreationView):
181 route_prefix = '/v3'
182 trailing_slash = False
183
184
185180 class ExecutorDataSchema(Schema):
186181 executor = fields.String(default=None)
187182 args = fields.Dict(default=None)
258253 return obj
259254
260255
261 class AgentWithWorkspacesV3View(AgentWithWorkspacesView, PatchableMixin):
262 route_prefix = '/v3'
263 trailing_slash = False
264
265
266256 class AgentView(ReadOnlyMultiWorkspacedView):
267257 route_base = 'agents'
268258 model_class = Agent
269259 schema_class = AgentSchema
270260 get_joinedloads = [Agent.creator, Agent.executors, Agent.workspaces]
271261
272 @route('/<int:agent_id>/', methods=['DELETE'])
262 @route('/<int:agent_id>', methods=['DELETE'])
273263 def remove_workspace(self, workspace_name, agent_id):
274264 """
275265 ---
291281 db.session.commit()
292282 return make_response({"description": "ok"}, 204)
293283
294 @route('/<int:agent_id>/run/', methods=['POST'])
284 @route('/<int:agent_id>/run', methods=['POST'])
295285 def run_agent(self, workspace_name, agent_id):
296286 """
297287 ---
316306 try:
317307 executor = Executor.query.filter(Executor.name == executor_data['executor'],
318308 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
319321 params = ', '.join([f'{key}={value}' for (key, value) in executor_data["args"].items()])
320322 command = Command(
321323 import_source="agent",
324326 user='',
325327 hostname='',
326328 params=params,
327 start_date=datetime.now(),
329 start_date=datetime.utcnow(),
328330 workspace=workspace
329331 )
330332
357359 'command_id': command.id,
358360 })
359361
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)
376382
377383
378384 AgentWithWorkspacesView.register(agent_api)
379 AgentWithWorkspacesV3View.register(agent_api)
380385 AgentCreationView.register(agent_creation_api)
381 AgentCreationV3View.register(agent_creation_api)
382386 AgentView.register(agent_api)
383 AgentV3View.register(agent_api)
4444 faraday_server.agent_token_expiration))
4545 return AgentAuthTokenSchema().dump(
4646 {'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,
4848 'total_duration': totp.interval})
4949
5050
51 class AgentAuthTokenV3View(AgentAuthTokenView):
52 route_prefix = '/v3'
53 trailing_slash = False
54
55
5651 AgentAuthTokenView.register(agent_auth_token_api)
57 AgentAuthTokenV3View.register(agent_auth_token_api)
242242 _create_host(ws, host, command)
243243
244244 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 \
246246 command.end_date
247247 db.session.commit()
248248
315315 updated = True
316316
317317 if updated:
318 service.update_date = datetime.now()
318 service.update_date = datetime.utcnow()
319319
320320 return service
321321
378378 try:
379379 run_timestamp = float(run_date_string)
380380 run_date = datetime.utcfromtimestamp(run_timestamp)
381 if run_date < datetime.now() + timedelta(hours=24):
381 if run_date < datetime.utcnow() + timedelta(hours=24):
382382 logger.debug("Valid run date")
383383 else:
384384 run_date = None
549549 post.is_public = True
550550
551551
552 class BulkCreateV3View(BulkCreateView):
553 route_prefix = '/v3/ws/<workspace_name>/'
554 trailing_slash = False
555
556
557552 BulkCreateView.register(bulk_create_api)
558 BulkCreateV3View.register(bulk_create_api)
33 import time
44 import datetime
55
6 import pytz
67 import flask
78 from flask import Blueprint
89 from flask_classful import route
910 from marshmallow import fields, post_load, ValidationError
1011
11 from faraday.server.api.base import AutoSchema, ReadWriteWorkspacedView, PaginatedMixin, PatchableWorkspacedMixin
12 from faraday.server.api.base import AutoSchema, ReadWriteWorkspacedView, PaginatedMixin
1213 from faraday.server.models import Command, Workspace
1314 from faraday.server.schemas import MutableField, PrimaryKeyRelatedField, SelfNestedField, MetadataSchema
1415
2829
2930 def load_itime(self, value):
3031 try:
31 return datetime.datetime.fromtimestamp(value)
32 return datetime.datetime.utcfromtimestamp(value)
3233 except ValueError:
3334 raise ValidationError('Invalid Itime Value')
3435
3536 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
3738
3839 def get_duration(self, obj):
3940 # obj.start_date can't be None
4041 if obj.end_date:
4142 return (obj.end_date - obj.start_date).seconds + ((obj.end_date - obj.start_date).microseconds / 1000000.0)
4243 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
4445 return 'Timeout'
4546 return 'In progress'
4647
7879 'commands': commands,
7980 }
8081
81 @route('/activity_feed/')
82 @route('/activity_feed')
8283 def activity_feed(self, workspace_name):
8384 """
8485 ---
109110 })
110111 return res
111112
112 @route('/last/', methods=['GET'])
113 @route('/last', methods=['GET'])
113114 def last_command(self, workspace_name):
114115 """
115116 ---
139140 return flask.jsonify(command_obj)
140141
141142
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
158143 CommandView.register(commandsrun_api)
159 CommandV3View.register(commandsrun_api)
99 from faraday.server.api.base import (
1010 AutoSchema,
1111 ReadWriteWorkspacedView,
12 InvalidUsage, CreateWorkspacedMixin, GenericWorkspacedView, PatchableWorkspacedMixin)
12 InvalidUsage,
13 CreateWorkspacedMixin,
14 GenericWorkspacedView
15 )
1316 from faraday.server.models import Comment
1417 comment_api = Blueprint('comment_api', __name__)
1518
8386 return res
8487
8588
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
9689 CommentView.register(comment_api)
9790 UniqueCommentView.register(comment_api)
98 CommentV3View.register(comment_api)
99 UniqueCommentV3View.register(comment_api)
1010 ReadWriteWorkspacedView,
1111 FilterSetMeta,
1212 FilterAlchemyMixin,
13 InvalidUsage,
14 PatchableWorkspacedMixin
13 InvalidUsage
1514 )
1615 from faraday.server.models import Credential, Host, Service, Workspace, db
1716 from faraday.server.schemas import MutableField, SelfNestedField, MetadataSchema
130129 }
131130
132131
133 class CredentialV3View(CredentialView, PatchableWorkspacedMixin):
134 route_prefix = '/v3/ws/<workspace_name>/'
135 trailing_slash = False
136
137
138132 CredentialView.register(credentials_api)
139 CredentialV3View.register(credentials_api)
66 from faraday.server.models import CustomFieldsSchema
77 from faraday.server.api.base import (
88 AutoSchema,
9 ReadWriteView,
10 PatchableMixin,
9 ReadWriteView
1110 )
1211
1312
5150 return super()._update_object(obj, data)
5251
5352
54 class CustomFieldsSchemaV3View(CustomFieldsSchemaView, PatchableMixin):
55 route_prefix = '/v3'
56 trailing_slash = False
57
58
5953 CustomFieldsSchemaView.register(custom_fields_schema_api)
60 CustomFieldsSchemaV3View.register(custom_fields_schema_api)
1010 logger = logging.getLogger(__name__)
1111
1212
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'])
1414 def export_data(workspace_name):
1515 """
1616 ---
4343 else:
4444 logger.error("Invalid format. Please, specify a valid format.")
4545 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__
5446
5547
5648 def xml_metasploit_format(workspace):
1313
1414
1515 @gzipped
16 @exploits_api.route('/v2/vulners/exploits/<cveid>', methods=['GET'])
16 @exploits_api.route('/v3/vulners/exploits/<cveid>', methods=['GET'])
1717 def get_exploits(cveid):
1818 """
1919 ---
7373 abort(make_response(jsonify(message=f'Could not find {str(ex)}'), 400))
7474
7575 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)
2323 AutoSchema,
2424 FilterAlchemyMixin,
2525 FilterSetMeta,
26 FilterWorkspacedMixin,
27 PatchableWorkspacedMixin
26 FilterWorkspacedMixin
2827 )
2928 from faraday.server.schemas import (
3029 MetadataSchema,
191190 pagination_metadata.total = count
192191 return self._envelope_list(filtered_objs, pagination_metadata)
193192
194 @route('/bulk_create/', methods=['POST'])
193 @route('/bulk_create', methods=['POST'])
195194 def bulk_create(self, workspace_name):
196195 """
197196 ---
258257 logger.error("Error parsing hosts CSV (%s)", e)
259258 abort(400, f"Error parsing hosts CSV ({e})")
260259
261 @route('/<host_id>/services/')
260 @route('/<host_id>/services')
262261 def service_list(self, workspace_name, host_id):
263262 """
264263 ---
279278 services = self._get_object(host_id, workspace_name).services
280279 return ServiceSchema(many=True).dump(services)
281280
282 @route('/countVulns/')
281 @route('/countVulns')
283282 def count_vulns(self, workspace_name):
284283 """
285284 ---
314313
315314 return res_dict
316315
317 @route('/<host_id>/tools_history/')
316 @route('/<host_id>/tools_history')
318317 def tool_impacted_by_host(self, workspace_name, host_id):
319318 """
320319 ---
342341 res_dict = {'tools': []}
343342 for row in result:
344343 _, 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()})
346345 return res_dict
347346
348347 def _perform_create(self, data, **kwargs):
399398 or len(hosts)),
400399 }
401400
401 # ### THIS WAS FROM V2
402402 # 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)
465439
466440
467441 HostsView.register(host_api)
468 HostsV3View.register(host_api)
55 from flask import Blueprint
66
77 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
1010
1111 info_api = Blueprint('info_api', __name__)
1212
1313
14 @info_api.route('/v2/info', methods=['GET'])
14 @info_api.route('/v3/info', methods=['GET'])
1515 def show_info():
1616 """
1717 ---
2929 return response
3030
3131
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
4032 @info_api.route('/config')
4133 def get_config():
4234 """
4840 200:
4941 description: Ok
5042 """
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)
5251
5352
5453 get_config.is_public = True
5554 show_info.is_public = True
56 show_info_v3.is_public = True
66 from faraday.server.models import License
77 from faraday.server.api.base import (
88 ReadWriteView,
9 AutoSchema,
10 PatchableMixin
9 AutoSchema
1110 )
1211 from faraday.server.schemas import (
1312 StrictDateTimeField,
3635 schema_class = LicenseSchema
3736
3837
39 class LicenseV3View(LicenseView, PatchableMixin):
40 route_prefix = 'v3/'
41 trailing_slash = False
42
43
4438 LicenseView.register(license_api)
45 LicenseV3View.register(license_api)
5050 return jsonify({'preferences': flask_login.current_user.preferences}), 200
5151
5252
53 class PreferencesV3View(PreferencesView):
54 route_prefix = '/v3'
55 trailing_slash = False
56
57
5853 PreferencesView.register(preferences_api)
59 PreferencesV3View.register(preferences_api)
77 from faraday.server.models import SearchFilter
88 from faraday.server.api.base import (
99 ReadWriteView,
10 AutoSchema,
11 PatchableMixin,
10 AutoSchema
1211 )
1312
1413 searchfilter_api = Blueprint('searchfilter_api', __name__)
3433 return query.filter(SearchFilter.creator_id == flask_login.current_user.id)
3534
3635
37 class SearchFilterV3View(SearchFilterView, PatchableMixin):
38 route_prefix = 'v3/'
39 trailing_slash = False
40
41
4236 SearchFilterView.register(searchfilter_api)
43 SearchFilterV3View.register(searchfilter_api)
66 from marshmallow.validate import OneOf, Range
77 from sqlalchemy.orm.exc import NoResultFound
88
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 )
1115 from faraday.server.models import Host, Service, Workspace
1216 from faraday.server.schemas import (
1317 MetadataSchema,
134138 return super()._perform_create(data, **kwargs)
135139
136140
137 class ServiceV3View(ServiceView, PatchableWorkspacedMixin):
138 route_prefix = '/v3/ws/<workspace_name>/'
139 trailing_slash = False
140
141
142141 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)
4141 )
4242 hashed_data = hash_data(flask_login.current_user.password) if flask_login.current_user.password else None
4343 user_ip = request.headers.get('X-Forwarded-For', request.remote_addr)
44 requested_at = datetime.datetime.now()
44 requested_at = datetime.datetime.utcnow()
4545 audit_logger.info(f"User [{flask_login.current_user.username}] requested token from IP [{user_ip}] at [{requested_at}]")
4646 return serializer.dumps({'user_id': user_id, "validation_check": hashed_data}).decode('utf-8')
4747
4848
49 class TokenAuthV3View(TokenAuthView):
50 route_prefix = '/v3'
51 trailing_slash = False
52
53
5449 TokenAuthView.register(token_api)
55 TokenAuthV3View.register(token_api)
3535
3636
3737 @gzipped
38 @upload_api.route('/v2/ws/<workspace>/upload_report', methods=['POST'])
38 @upload_api.route('/v3/ws/<workspace>/upload_report', methods=['POST'])
3939 def file_upload(workspace=None):
4040 """
4141 ---
102102 name=workspace).one()
103103 command = Command()
104104 command.workspace = workspace_instance
105 command.start_date = datetime.now()
105 command.start_date = datetime.utcnow()
106106 command.import_source = 'report'
107107 # The data will be updated in the bulk_create function
108108 command.tool = "In progress"
126126 )
127127 else:
128128 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)
2626 FilterSetMeta,
2727 PaginatedMixin,
2828 ReadWriteView,
29 FilterMixin,
30 PatchableMixin
29 FilterMixin
3130 )
3231
3332 from faraday.server.schemas import (
188187
189188 return schema
190189
191 @route('/bulk_create/', methods=['POST'])
190 @route('/bulk_create', methods=['POST'])
192191 def bulk_create(self):
193192 """
194193 ---
319318 return vulns_list
320319
321320
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
333321 VulnerabilityTemplateView.register(vulnerability_template_api)
334 VulnerabilityTemplateV3View.register(vulnerability_template_api)
99 from pathlib import Path
1010
1111 import flask
12 import wtforms
1312 from filteralchemy import Filter, FilterSet, operators
1413 from flask import request, send_file
15 from flask import Blueprint
14 from flask import Blueprint, make_response
1615 from flask_classful import route
17 from flask_wtf.csrf import validate_csrf
1816 from marshmallow import Schema, fields, post_load, ValidationError
1917 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
2119 from sqlalchemy.orm.exc import NoResultFound
2220 from sqlalchemy import desc, or_, func
2321 from werkzeug.datastructures import ImmutableMultiDict
3432 PaginatedMixin,
3533 ReadWriteWorkspacedView,
3634 InvalidUsage,
37 CountMultiWorkspacedMixin,
38 PatchableWorkspacedMixin
35 CountMultiWorkspacedMixin
3936 )
4037 from faraday.server.fields import FaradayUploadedFile
4138 from faraday.server.models import (
159156 dump_only=True) # This is only used for sorting
160157 custom_fields = FaradayCustomField(table_name='vulnerability', attribute='custom_fields')
161158 external_id = fields.String(allow_none=True)
159 attachments_count = fields.Integer(dump_only=True, attribute='attachments_count')
162160
163161 class Meta:
164162 model = Vulnerability
172170 'service', 'obj_id', 'type', 'policyviolations',
173171 '_attachments',
174172 'target', 'host_os', 'resolution', 'metadata',
175 'custom_fields', 'external_id', 'tool',
173 'custom_fields', 'external_id', 'tool', 'attachments_count',
176174 'cvss', 'cwe', 'cve', 'owasp',
177175 )
178176
306304 'service', 'obj_id', 'type', 'policyviolations',
307305 'request', '_attachments', 'params',
308306 'target', 'host_os', 'resolution', 'method', 'metadata',
309 'status_code', 'custom_fields', 'external_id', 'tool',
307 'status_code', 'custom_fields', 'external_id', 'tool', 'attachments_count',
310308 'cve', 'cwe', 'owasp', 'cvss',
311309 )
312310
541539 db.session.delete(old_attachment)
542540 for filename, attachment in attachments.items():
543541 faraday_file = FaradayUploadedFile(b64decode(attachment['data']))
542 filename = filename.replace(" ", "_")
544543 get_or_create(
545544 db.session,
546545 File,
573572 """
574573 query = super()._get_eagerloaded_query(
575574 *args, **kwargs)
576 joinedloads = [
575 options = [
577576 joinedload(Vulnerability.host)
578577 .load_only(Host.id) # Only hostnames are needed
579578 .joinedload(Host.hostnames),
590589 undefer(VulnerabilityGeneric.creator_command_tool),
591590 undefer(VulnerabilityGeneric.target_host_ip),
592591 undefer(VulnerabilityGeneric.target_host_os),
593 joinedload(VulnerabilityGeneric.evidence),
594592 joinedload(VulnerabilityGeneric.tags),
595593 ]
594
595 if flask.request.args.get('get_evidence'):
596 options.append(joinedload(VulnerabilityGeneric.evidence))
597 else:
598 options.append(noload(VulnerabilityGeneric.evidence))
599
596600 return query.options(selectin_polymorphic(
597601 VulnerabilityGeneric,
598602 [Vulnerability, VulnerabilityWeb]
599 ), *joinedloads)
603 ), *options)
600604
601605 def _filter_query(self, query):
602606 query = super()._filter_query(query)
618622
619623 def _get_schema_class(self):
620624 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:
622626 requested_type = request.json.get('type', None)
623627 if not requested_type:
624628 raise InvalidUsage('Type is required.')
680684 res['groups'] = [convert_group(group) for group in res['groups']]
681685 return res
682686
683 @route('/<int:vuln_id>/attachment/', methods=['POST'])
687 @route('/<int:vuln_id>/attachment', methods=['POST'])
684688 def post_attachment(self, workspace_name, vuln_id):
685689 """
686690 ---
696700 description: Ok
697701 """
698702
699 try:
700 validate_csrf(request.form.get('csrf_token'))
701 except wtforms.ValidationError:
702 flask.abort(403)
703703 vuln_workspace_check = db.session.query(VulnerabilityGeneric, Workspace.id).join(
704704 Workspace).filter(VulnerabilityGeneric.id == vuln_id,
705705 Workspace.name == workspace_name).first()
707707 if vuln_workspace_check:
708708 if 'file' not in request.files:
709709 flask.abort(400)
710
711 faraday_file = FaradayUploadedFile(request.files['file'].read())
710 vuln = VulnerabilitySchema().dump(vuln_workspace_check[0])
712711 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})
725730 else:
726731 flask.abort(404, "Vulnerability not found")
727732
749754 200:
750755 description: Ok
751756 """
752 filters = request.args.get('q')
757 filters = request.args.get('q', '{}')
753758 filtered_vulns, count = self._filter(filters, workspace_name)
754759
755760 class PageMeta:
883888
884889 return vulns_data, len(rows)
885890
886 @route('/<int:vuln_id>/attachment/<attachment_filename>/', methods=['GET'])
891 @route('/<int:vuln_id>/attachment/<attachment_filename>', methods=['GET'])
887892 def get_attachment(self, workspace_name, vuln_id, attachment_filename):
888893 """
889894 ---
926931 else:
927932 flask.abort(404, "Vulnerability not found")
928933
929 @route('/<int:vuln_id>/attachments/', methods=['GET'])
934 @route('/<int:vuln_id>/attachment', methods=['GET'])
930935 def get_attachments_by_vuln(self, workspace_name, vuln_id):
931936 """
932937 ---
964969 else:
965970 flask.abort(404, "Vulnerability not found")
966971
967 @route('/<int:vuln_id>/attachment/<attachment_filename>/', methods=['DELETE'])
972 @route('/<int:vuln_id>/attachment/<attachment_filename>', methods=['DELETE'])
968973 def delete_attachment(self, workspace_name, vuln_id, attachment_filename):
969974 """
970975 ---
994999 else:
9951000 flask.abort(404, "Vulnerability not found")
9961001
997 @route('export_csv/', methods=['GET'])
1002 @route('export_csv', methods=['GET'])
9981003 def export_csv(self, workspace_name):
9991004 """
10001005 ---
10731078 response = {'deleted_vulns': deleted_vulns}
10741079 return flask.jsonify(response)
10751080
1076 @route('top_users/', methods=['GET'])
1081 @route('top_users', methods=['GET'])
10771082 def top_users(self, workspace_name):
10781083 """
10791084 ---
11061111 return flask.jsonify(response)
11071112
11081113
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
11451114 VulnerabilityView.register(vulns_api)
1146 VulnerabilityV3View.register(vulns_api)
2525 route_base = 'websocket_token'
2626 schema_class = WebsocketWorkspaceAuthSchema
2727
28 @route('/', methods=['GET', 'POST'])
28 @route('', methods=['GET', 'POST'])
2929 def get(self, workspace_name):
3030 """
3131 ---
4141 return {"token": token}
4242
4343
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)
5945
6046
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'])
6648 def agent_websocket_token():
6749 """
6850 ---
7759 return flask.jsonify({"token": generate_agent_websocket_token(agent)})
7860
7961
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
8862 agent_websocket_token.is_public = True
89 agent_websocket_token_w3.is_public = True
9063
9164
9265 def generate_agent_websocket_token(agent):
00 # Faraday Penetration Test IDE
11 # Copyright (C) 2016 Infobyte LLC (http://www.infobytesec.com/)
22 # See the file 'doc/LICENSE' for the license information
3 import re
34 from builtins import str
45
56 import json
89 import flask
910 from flask import Blueprint, abort, make_response, jsonify
1011 from flask_classful import route
11 from marshmallow import Schema, fields, post_load, validate, ValidationError
12 from marshmallow import Schema, fields, post_load, ValidationError
1213 from sqlalchemy.orm import (
1314 with_expression
1415 )
2324 PrimaryKeyRelatedField,
2425 SelfNestedField,
2526 )
26 from faraday.server.api.base import ReadWriteView, AutoSchema, FilterMixin, PatchableMixin
27 from faraday.server.api.base import ReadWriteView, AutoSchema, FilterMixin
2728
2829 logger = logging.getLogger(__name__)
2930
6263 end_date = JSTimestampField(attribute='end_date')
6364
6465
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
6575 class WorkspaceSchema(AutoSchema):
6676
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)
7178 stats = SelfNestedField(WorkspaceSummarySchema())
7279 duration = SelfNestedField(WorkspaceDurationSchema())
7380 _id = fields.Integer(dump_only=True, attribute='id')
336343 return self._get_object(workspace_id).readonly
337344
338345
339 class WorkspaceV3View(WorkspaceView, PatchableMixin):
340 route_prefix = 'v3/'
341 trailing_slash = False
342
343
344346 WorkspaceView.register(workspace_api)
345 WorkspaceV3View.register(workspace_api)
11 # Copyright (C) 2016 Infobyte LLC (http://www.infobytesec.com/)
22 # See the file 'doc/LICENSE' for the license information
33 import logging
4 import os
45 import string
56 import datetime
67
1213 from itsdangerous import TimedJSONWebSignatureSerializer, SignatureExpired, BadSignature
1314 from random import SystemRandom
1415
16 from faraday.settings import load_settings
1517 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
1719 from configparser import ConfigParser, NoSectionError, NoOptionError, DuplicateSectionError
1820
1921 import flask
98100 from faraday.server.api.modules.export_data import export_data_api # pylint:disable=import-outside-toplevel
99101 # Custom reset password
100102 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
101106
102107 app.register_blueprint(commandsrun_api)
103108 app.register_blueprint(activityfeed_api)
124129 app.register_blueprint(preferences_api)
125130 app.register_blueprint(export_data_api)
126131 app.register_blueprint(auth)
132 app.register_blueprint(reports_settings_api)
133 app.register_blueprint(dashboard_settings_api)
127134
128135
129136 def check_testing_configuration(testing, app):
256263 KVSessionExtension(app=app).cleanup_sessions(app)
257264
258265 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()
260267 audit_logger.info(f"User [{user.username}] logged out from IP [{user_ip}] at [{user_logout_at}]")
261268
262269
276283 KVSessionExtension(app=app).cleanup_sessions(app)
277284
278285 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()
280287 audit_logger.info(f"User [{user.username}] logged in from IP [{user_ip}] at [{user_login_at}]")
281288
282289
385392 DepotManager.configure('default', {
386393 'depot.storage_path': storage_path
387394 })
388
395 app.config['SQLALCHEMY_ECHO'] = 'FARADAY_LOG_QUERY' in os.environ
389396 check_testing_configuration(testing, app)
390397
391398 try:
407414 app.user_datastore = SQLAlchemyUserDatastore(
408415 db,
409416 user_model=User,
410 role_model=None) # We won't use flask security roles feature
417 role_model=Role)
411418
412419 from faraday.server.api.modules.agent import agent_creation_api # pylint: disable=import-outside-toplevel
413420
437444 register_handlers(app)
438445
439446 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()
442448 return app
443449
444450
462468 def validate(self):
463469
464470 user_ip = request.headers.get('X-Forwarded-For', request.remote_addr)
465 time_now = datetime.datetime.now()
471 time_now = datetime.datetime.utcnow()
466472
467473 # Use super of LoginForm, not super of CustomLoginForm, since I
468474 # want to skip the LoginForm validate logic
103103 print('User cancelled.')
104104 sys.exit(1)
105105
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
106126 def _create_admin_user(self, conn_string, choose_password, faraday_user_password):
107127 engine = create_engine(conn_string)
108128 # TODO change the random_password variable name, it is not always
126146 INSERT INTO faraday_user (
127147 username, name, password,
128148 is_ldap, active, last_login_ip,
129 current_login_ip, role, state_otp, fs_uniquifier
149 current_login_ip, state_otp, fs_uniquifier
130150 ) VALUES (
131151 'faraday', 'Administrator', :password,
132152 false, true, '127.0.0.1',
133 '127.0.0.1', 'admin', 'disabled', :fs_uniquifier
153 '127.0.0.1', 'disabled', :fs_uniquifier
134154 )
135155 """)
136156 params = {
139159 }
140160 connection = engine.connect()
141161 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)
142171 except sqlalchemy.exc.IntegrityError as ex:
143172 if is_unique_constraint_violation(ex):
144173 # when re using database user could be created previously
232261 connection.commit()
233262 connection.close()
234263 except psycopg2.Error as e:
235 if 'authentication failed' in e.message:
264 if 'authentication failed' in str(e):
236265 print('{red}ERROR{white}: User {username} already '
237266 'exists'.format(white=Fore.WHITE,
238267 red=Fore.RED,
325354 os.chdir(FARADAY_BASE)
326355 command.stamp(alembic_cfg, "head")
327356 # 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
-222
faraday/server/commands/status_check.py less more
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
-78
faraday/server/commands/support.py less more
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))
1414 )
1515 from pathlib import Path
1616
17 from faraday import __license_version__ as license_version
1817
1918 CONST_FARADAY_HOME_PATH = Path(
2019 os.getenv('FARADAY_HOME', Path('~/').expanduser())
3736 LOCAL_REPORTS_FOLDER = CONST_FARADAY_HOME_PATH / 'uploaded_reports'
3837
3938 CONFIG_FILES = [DEFAULT_CONFIG_FILE, LOCAL_CONFIG_FILE]
40 CONST_LICENSES_DB = 'faraday_licenses'
41 CONST_VULN_MODEL_DB = 'cwe'
4239
4340 logger = logging.getLogger(__name__)
4441
6360
6461 # Copy default config file into faraday local config
6562 shutil.copyfile(DEFAULT_CONFIG_FILE, LOCAL_CONFIG_FILE)
66
6763 logger.info(f"Local faraday-server configuration created at {LOCAL_CONFIG_FILE}")
6864
6965
9187 self.__setattr__(att, True)
9288 else:
9389 self.__setattr__(att, False)
90 elif isinstance(self.__dict__[att], int):
91 if value:
92 self.__setattr__(att, int(value))
9493 else:
9594 if value:
9695 self.__setattr__(att, value)
96
97 def set(self, option_name, value):
98 return self.__setattr__(option_name, value)
9799
98100 @staticmethod
99101 def parse_section(section_name, __parser):
100102 section = None
101103 if section_name == 'database':
102104 section = database
103 elif section_name == 'dashboard':
104 section = dashboard
105105 elif section_name == 'faraday_server':
106106 section = faraday_server
107 elif section_name == 'ldap':
108 section = ldap
109107 elif section_name == 'storage':
110108 section = storage
111109 elif section_name == 'logger':
112110 section = logger_config
113111 elif section_name == 'limiter':
114112 section = limiter_config
115 elif section_name == 'smtp':
116 section = smtp
117113 else:
118114 return
119115 section.parse(__parser)
124120 self.connection_string = None
125121
126122
127 class DashboardConfigObject(ConfigSection):
128 def __init__(self):
129 self.show_vulns_by_price = False
130
131
132123 class LimiterConfigObject(ConfigSection):
133124 def __init__(self):
134125 self.enabled = False
137128
138129 class FaradayServerConfigObject(ConfigSection):
139130 def __init__(self):
140 self.bind_address = None
141 self.port = None
131 self.bind_address = "127.0.0.1"
132 self.port = 5985
142133 self.secret_key = None
143 self.websocket_port = None
134 self.websocket_port = 9000
144135 self.session_timeout = 12
145136 self.api_token_expiration = 43200 # Default as 12 hs
146137 self.agent_registration_secret = None
148139 self.debug = False
149140 self.custom_plugins_folder = None
150141 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
181142
182143
183144 class StorageConfigObject(ConfigSection):
191152
192153
193154 database = DatabaseConfigObject()
194 dashboard = DashboardConfigObject()
195155 faraday_server = FaradayServerConfigObject()
196 ldap = LDAPConfigObject()
197156 storage = StorageConfigObject()
198157 logger_config = LoggerConfig()
199 smtp = SmtpConfigObject()
200158 limiter_config = LimiterConfigObject()
201
202159 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
66 api_token_expiration=43200
77 ;custom_plugins_folder=/path/to/custom/plugins
88
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
229 [logger]
2310 use_rfc5424_formatter = false
4747
4848 from faraday.server.fields import JSONType
4949 from flask_security import (
50 UserMixin,
50 UserMixin, RoleMixin,
5151 )
5252
5353 from faraday.server.fields import FaradayUploadedFile
12121212 object_type='vulnerability'
12131213 )
12141214
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
12151222 @hybrid_property
12161223 def target(self):
12171224 return self.target_host_ip
17541761 return db.session.query(Workspace).filter_by(name=workspace_name).first()
17551762
17561763
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
17571775 class User(db.Model, UserMixin):
17581776 __tablename__ = 'faraday_user'
17591777 ROLES = ['admin', 'pentester', 'client', 'asset_owner']
17721790 login_count = Column(Integer) # flask-security
17731791 active = Column(Boolean(), default=True, nullable=False) # TBI flask-security
17741792 confirmed_at = Column(DateTime())
1775 role = Column(Enum(*ROLES, name='user_roles'),
1776 nullable=False, default='client')
17771793 _otp_secret = Column(
17781794 String(32),
17791795 name="otp_secret", nullable=True
17821798 preferences = Column(JSONType, nullable=True, default={})
17831799 fs_uniquifier = Column(String(64), unique=True, nullable=False) # flask-security
17841800
1801 roles = db.relationship('Role', secondary=roles_users,
1802 backref='users')
17851803 # 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]
17861808
17871809 workspace_permission_instances = relationship(
17881810 "WorkspacePermission",
17891811 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)
17981812
17991813 def __repr__(self):
18001814 return f"<{'LDAP ' if self.is_ldap else ''}User: {self.username}>"
18041818 "username": self.username,
18051819 "name": self.username,
18061820 "email": self.email,
1807 "role": self.role,
1808 "roles": [self.role],
1821 "roles": self.roles_list,
18091822 }
18101823
18111824
20622075 confirmed = Column(Boolean, nullable=False, default=False)
20632076 vuln_count = Column(Integer, default=0) # saves the amount of vulns when the report was generated.
20642077 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)
20652080 advanced_filter = Column(Boolean, default=False, nullable=False)
20662081 advanced_filter_parsed = Column(String, nullable=False, default="")
20672082
1414 self.__event = threading.Event()
1515
1616 def run(self):
17 logger.info("Ping Home Thread [Start]")
1718 while not self.__event.is_set():
1819 try:
1920 res = requests.get(HOME_URL, params={'version': faraday.__version__, 'key': 'white'},
2627 logger.exception(ex)
2728 logger.warning("Can't connect to portal...")
2829 self.__event.wait(RUN_INTERVAL)
30 else:
31 logger.info("Ping Home Thread [Stop]")
2932
3033 def stop(self):
34 logger.info("Ping Home Thread [Stopping...]")
3135 self.__event.set()
66
77 from faraday_plugins.plugins.manager import PluginsManager
88 from faraday.server.api.modules.bulk_create import bulk_create, BulkCreateSchema
9 from faraday.server import config
109
1110 from faraday.server.models import Workspace, Command, User
1211 from faraday.server.utils.bulk_create import add_creator
13
12 from faraday.settings.reports import ReportsSettings
1413 logger = logging.getLogger(__name__)
1514
1615
1716 REPORTS_QUEUE = Queue()
1817
18 INTERVAL = 0.5
19
1920
2021 class ReportsManager(Thread):
2122
2223 def __init__(self, upload_reports_queue, *args, **kwargs):
23 super().__init__(*args, **kwargs)
24 super().__init__(name="ReportsManager-Thread", daemon=True, *args, **kwargs)
2425 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}]")
2730 self.__event = threading.Event()
2831
2932 def stop(self):
30 logger.debug("Stop Reports Manager")
33 logger.info("Reports Manager Thread [Stopping...]")
3134 self.__event.set()
3235
3336 def send_report_request(self,
7679 logger.info(f"No plugin detected for report [{file_path}]")
7780
7881 def run(self):
79 logger.debug("Start Reports Manager")
82 logger.info("Reports Manager Thread [Start]")
83
8084 while not self.__event.is_set():
8185 try:
8286 tpl: Tuple[str, int, Path, int, int] = \
97101 logger.warning(f"Report file [{file_path}] don't exists",
98102 file_path)
99103 except Empty:
100 self.__event.wait(0.1)
104 self.__event.wait(INTERVAL)
101105 except KeyboardInterrupt:
102106 logger.info("Keyboard interrupt, stopping report processing thread")
103107 self.stop()
104108 except Exception as ex:
105109 logger.exception(ex)
106110 continue
111 else:
112 logger.info("Reports Manager Thread [Stop]")
317317 return True
318318 assert isinstance(exception.orig.pgcode, str)
319319 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
00 # Faraday Penetration Test IDE
11 # Copyright (C) 2016 Infobyte LLC (http://www.infobytesec.com/)
22 # See the file 'doc/LICENSE' for the license information
3 import multiprocessing
34 import sys
45 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
67
78 import twisted.web
89 from twisted.web.resource import Resource, ForbiddenResource
910
10 from twisted.internet import ssl, reactor, error
11 from twisted.internet import reactor, error
1112 from twisted.web.static import File
1213 from twisted.web.util import Redirect
1314 from twisted.web.http import proxiedLogFormatter
1617 listenWS
1718 )
1819
19 from flask_mail import Mail
20
2120 import faraday.server.config
2221
23 from faraday.server.config import CONST_FARADAY_HOME_PATH, smtp
22 from faraday.server.config import CONST_FARADAY_HOME_PATH
2423 from faraday.server.threads.reports_processor import ReportsManager, REPORTS_QUEUE
2524 from faraday.server.threads.ping_home import PingHomeThread
2625 from faraday.server.app import create_app
2928 BroadcastServerProtocol
3029 )
3130
31 from faraday.server.config import faraday_server as server_config
3232 FARADAY_APP = None
3333
3434 logger = logging.getLogger(__name__)
6666
6767
6868 class WebServer:
69 UI_URL_PATH = b'_ui'
7069 API_URL_PATH = b'_api'
7170 WEB_UI_LOCAL_PATH = faraday.server.config.FARADAY_BASE / 'server/www'
71 # Threads
72 raw_report_processor = None
73 ping_home_thread = None
7274
7375 def __init__(self):
7476
7577 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}/')
8080 self.__build_server_tree()
8181
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
9382 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(
9885 WebServer.API_URL_PATH, self.__build_api_resource())
99
100 def __build_web_redirect(self):
101 return FaradayRedirectResource(b'/')
10286
10387 def __build_web_resource(self):
10488 return FileWithoutDirectoryListing(WebServer.WEB_UI_LOCAL_PATH)
10791 return FaradayWSGIResource(reactor, reactor.getThreadPool(), get_app())
10892
10993 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'
11395 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}.')
11597 factory = WorkspaceServerFactory(url=url)
11698 factory.protocol = BroadcastServerProtocol
11799 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()
123100
124101 def install_signal(self):
125102 for sig in (SIGABRT, SIGILL, SIGINT, SIGSEGV, SIGTERM):
126103 signal(sig, SIG_DFL)
127104
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
128126 def run(self):
129127 def signal_handler(*args):
130128 logger.info('Received SIGTERM, shutting down.')
131129 logger.info("Stopping threads, please wait...")
132 self.__stop_all_threads()
130 self.stop_threads()
133131
134132 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,
136134 logPath=log_path,
137135 logFormatter=proxiedLogFormatter)
138136 site.displayTracebacks = False
139 self.__listen_func = reactor.listenTCP
140137
141138 try:
142139 self.install_signal()
143140 # 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()
148142 # 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)
152149 # websockets
153150 try:
154 listenWS(self.__build_websockets_resource(), interface=self.__bind_address)
151 listenWS(self.__build_websockets_resource(), interface=server_config.bind_address)
155152 except error.CannotListenError:
156153 logger.warn('Could not start websockets, address already open. This is ok is you wan to run multiple instances.')
157154 except Exception as ex:
158155 logger.warn(f'Could not start websocket, error: {ex}')
159156 logger.info('Faraday Server is ready')
160157 reactor.addSystemEventTrigger('before', 'shutdown', signal_handler)
158 signal(SIGUSR1, self.restart_threads)
161159 reactor.run()
162160
163161 except error.CannotListenError as e:
164162 logger.error(e)
165 self.__stop_all_threads()
163 self.stop_threads()
166164 sys.exit(1)
167165
168166 except Exception as e:
169167 logger.exception('Something went wrong when trying to setup the Web UI')
170168 logger.exception(e)
171 self.__stop_all_threads()
169 self.stop_threads()
172170 sys.exit(1)
173171
174172
177175 if not FARADAY_APP:
178176 app = create_app() # creates a Flask(__name__) app
179177 # 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)
186178 FARADAY_APP = app
187179 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()
2727 def setup_environment(check_deps=False):
2828 # Configuration files generation
2929 faraday.server.config.copy_default_config_to_local()
30 # Web configuration file generation
31 faraday.server.config.gen_web_config()
3230
3331
3432 def is_server_running(port):
112110 parser.add_argument('--debug', action='store_true', help='run Faraday Server in debug mode')
113111 parser.add_argument('--nodeps', action='store_true', help='Skip dependency check')
114112 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')
116114 parser.add_argument('--websocket_port', help='Overides server.ini websocket port configuration')
117115 parser.add_argument('--bind_address', help='Overides server.ini bind_address configuration')
118116 f_version = faraday.__version__
121119 if args.debug or faraday.server.config.faraday_server.debug:
122120 faraday.server.utils.logger.set_logging_level(faraday.server.config.DEBUG)
123121 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
125123 if args.bind_address:
126124 faraday.server.config.faraday_server.bind_address = args.bind_address
127125
130130 f'{view_name} / {class_model} / {rule.methods} / {view_name} / {view_instance._get_schema_class().__name__}')
131131 operations[view_name] = yaml_utils.load_yaml_from_docstring(
132132 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)
134136 )
135137 elif hasattr(view, "__doc__"):
136138 if not view.__doc__:
145147 if method not in ['HEAD', 'OPTIONS'] or os.environ.get("FULL_API_DOC", None):
146148 operations[method.lower()] = yaml_utils.load_yaml_from_docstring(
147149 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)
149153 )
150154 if hasattr(view, "view_class") and issubclass(view.view_class, MethodView):
151155 for method in view.methods:
33 from email.mime.multipart import MIMEMultipart
44 from email.mime.text import MIMEText
55
6 from faraday.server.config import smtp
6 from faraday.settings.smtp import SMTPSettings
77
88 logger = logging.getLogger(__name__)
99
1212 def __init__(self, smtp_host: str, smtp_sender: str,
1313 smtp_username: str = None, smtp_password: str = None,
1414 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
2821
2922 def send_mail(self, to_addr: str, subject: str, body: str):
3023 msg = MIMEMultipart()
3730 try:
3831 with SMTP(host=self.smtp_host, port=self.smtp_port) as server_mail:
3932 if self.smtp_ssl:
40 server_mail.starttls(keyfile=smtp.keyfile,
41 certfile=smtp.certfile)
33 server_mail.starttls()
4234 if self.smtp_username and self.smtp_password:
4335 server_mail.login(self.smtp_username, self.smtp_password)
4436 text = msg.as_string()
7171 ./packages/bleach
7272 { };
7373
74 faraday-agent-parameters-types =
75 self.callPackage
76 ./packages/faraday-agent-parameters-types
77 { };
78
7479 faraday-plugins =
7580 self.callPackage
7681 ./packages/faraday-plugins
106111 ./packages/flask-security-too
107112 { };
108113
114 marshmallow =
115 self.callPackage
116 ./packages/marshmallow
117 { };
118
119 marshmallow-sqlalchemy =
120 self.callPackage
121 ./packages/marshmallow-sqlalchemy
122 { };
123
109124 pyotp =
110125 self.callPackage
111126 ./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 }
1313 , pytz
1414 , requests
1515 , simplejson
16 , tabulate
1617 }:
1718
1819 buildPythonPackage rec {
1920 pname =
2021 "faraday-plugins";
2122 version =
22 "1.4.5";
23 "1.5.0";
2324
2425 src =
2526 fetchPypi {
2728 pname
2829 version;
2930 sha256 =
30 "0k4m6pz5dzy8x03wycya2n86aag42nydl67a1vak4kd09ain9vd7";
31 "1wf313s2kricd44s4m0x62psk2xq69fp6n4qm0f7k1rrwilwdxyd";
3132 };
3233
3334 propagatedBuildInputs =
4142 pytz
4243 dateutil
4344 colorama
45 tabulate
4446 ];
4547
4648 # TODO FIXME
1414 , distro
1515 , email_validator
1616 , factory_boy
17 , faraday-agent-parameters-types
1718 , faraday-plugins
1819 , fetchPypi
1920 , filedepot
6263 pname =
6364 "faradaysec";
6465 version =
65 "3.15.0";
66 "3.16.0";
6667
6768 src =
6869 lib.cleanSource
116117 pyotp
117118 flask-limiter
118119 flask_mail
120 faraday-agent-parameters-types
119121 ];
120122 checkInputs =
121123 [
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 }
1111 flask-login>=0.5.0
1212 Flask-Security-Too>=4.0.0
1313 bleach>=3.3.0
14 marshmallow>=3.0.0,<3.11.0
14 marshmallow>=3.11.0
1515 Pillow>=4.2.1
1616 psycopg2
1717 pgcli
2424 tqdm>=4.15.0
2525 twisted>=18.9.0
2626 webargs>=7.0.0
27 marshmallow-sqlalchemy
27 marshmallow-sqlalchemy==0.25.0
2828 filteralchemy-fork
2929 filedepot>=0.5.0
3030 nplusone>=0.8.1
3939 pyotp>=2.6.0
4040 Flask-Limiter
4141 Flask-Mail
42 faraday_agent_parameters_types>=1.0.0
1414
1515
1616 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")
176176 # and refuse to install the project if the version does not match. If you
177177 # do not support Python 2, you can simplify this to '>=3.5' or similar, see
178178 # https://packaging.python.org/guides/distributing-packages-using-setuptools/#python-requires
179 python_requires='>=3.6.*',
179 python_requires='>=3.7.*',
180180
181181 # This field lists other packages that your project depends on to run.
182182 # Any package you put here will be installed by pip when your project is
136136
137137 db.app = app
138138 db.create_all()
139 db.engine.execute("INSERT INTO faraday_role(name) VALUES ('admin'),('pentester'),('client'),('asset_owner');")
139140
140141 request.addfinalizer(teardown)
141142 return db
229230
230231
231232 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']
232236 user = app.user_datastore.create_user(username=username,
233237 email=email,
234238 password=password,
5252 Rule,
5353 Action,
5454 RuleAction,
55 Condition)
55 Condition,
56 Role
57 )
5658
5759
5860 # Make partials for start and end date. End date must be after start date
5961 def FuzzyStartTime():
6062 return (
6163 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),
6466 )
6567 )
6668
6870 def FuzzyEndTime():
6971 return (
7072 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()
7375 )
7476 )
7577
9799 fs_uniquifier = factory.LazyAttribute(
98100 lambda e: uuid.uuid4().hex
99101 )
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'))
100126
101127 class Meta:
102128 model = User
561587 name = FuzzyText()
562588 agent = factory.SubFactory(AgentFactory)
563589 parameters_metadata = factory.LazyAttribute(
564 lambda e: {"param_name": False}
590 lambda e: {"param_name": {"mandatory": False, "type": "string", "base": "string"}}
565591 )
566592
567593 class Meta:
66 import random
77 import pytest
88 from functools import partial
9 from posixpath import join as urljoin
10
911 from faraday.server.models import Hostname, Host
10
1112 from faraday.server.api.modules.hosts import HostsView
1213
1314 from tests.test_api_workspaced_base import (
142143
143144 # This test the api endpoint for some of the host in the ws, with existing other host with vulns in the same and
144145 # other ws
145 @pytest.mark.parametrize('querystring', ['countVulns/?hosts={}',
146 @pytest.mark.parametrize('querystring', ['countVulns?hosts={}',
146147 ])
147148 def test_vuln_count(self,
148149 vulnerability_factory,
182183 session.add_all(vulns)
183184 session.commit()
184185
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 )
186190 res = test_client.get(url)
187191
188192 assert res.status_code == 200
193197
194198 # This test the api endpoint for some of the host in the ws, with existing other host in other ws and ask for the
195199 # 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',
197201 ])
198202 def test_vuln_count_ignore_other_ws(self,
199203 vulnerability_factory,
231235 session.add_all(vulns)
232236 session.commit()
233237
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))))
235239 res = test_client.get(url)
236240
237241 assert res.status_code == 200
243247
244248 for host in hosts_not_to_query_w2:
245249 assert str(host.id) not in res.json['hosts']
246 # I'm Py3
33 See the file 'doc/LICENSE' for the license information
44
55 '''
6 import datetime
67 import pytest
78
89 from tests.factories import (WorkspaceFactory,
1112 EmptyCommandFactory,
1213 HostFactory,
1314 CommandObjectFactory)
14 from tests.utils.url import v2_to_v3
1515
1616
1717 @pytest.mark.usefixtures('logged_user')
1818 class TestActivityFeed:
19
20 def check_url(self, url):
21 return url
2219
2320 @pytest.mark.usefixtures('ignore_nplusone')
2421 def test_activity_feed(self, test_client, session):
2825 session.add(command)
2926 session.commit()
3027
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')
3429
3530 assert res.status_code == 200
3631 activities = res.json['activities'][0]
4540 session.add(command)
4641 session.commit()
4742
48 # Timestamp of 14/12/2018
49 itime = 1544745600.0
43 new_start_date = command.end_date - datetime.timedelta(days=1)
5044 data = {
5145 'command': command.command,
5246 'tool': command.tool,
53 'itime': itime
47 'itime': new_start_date.timestamp()
5448
5549 }
5650
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}',
5952 data=data,
6053 )
6154 assert res.status_code == 200
6356 # Changing res.json['itime'] to timestamp format of itime
6457 res_itime = res.json['itime'] / 1000.0
6558 assert res.status_code == 200
66 assert res_itime == itime
59 assert datetime.datetime.fromtimestamp(res_itime) == new_start_date
6760
6861 @pytest.mark.usefixtures('ignore_nplusone')
6962 def test_verify_correct_severities_sum_values(self, session, test_client):
134127 workspace=workspace
135128 )
136129 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')
138131 assert res.status_code == 200
139132 assert res.json['activities'][0]['vulnerabilities_count'] == 8
140133 assert res.json['activities'][0]['criticalIssue'] == 1
143136 assert res.json['activities'][0]['lowIssue'] == 1
144137 assert res.json['activities'][0]['infoIssue'] == 2
145138 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)
1212 from faraday.server.api.modules.agent import AgentWithWorkspacesView, AgentView
1313 from faraday.server.models import Agent, Command
1414 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
1616 from tests.test_api_workspaced_base import ReadOnlyMultiWorkspacedAPITests
1717 from tests import factories
1818 from tests.test_api_workspaced_base import API_PREFIX
19 from tests.utils.url import v2_to_v3
2019
2120
2221 def http_req(method, client, endpoint, json_dict, expected_status_codes, follow_redirects=False):
5857 @pytest.mark.usefixtures('logged_user')
5958 class TestAgentAuthTokenAPIGeneric:
6059
61 def check_url(self, url):
62 return url
63
6460 @mock.patch('faraday.server.api.modules.agent.faraday_server')
6561 def test_get_agent_token(self, faraday_server_config, test_client, session):
6662 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')
6864 assert 'token' in res.json and 'expires_in' in res.json
6965 assert len(res.json['token'])
7066
7167 @mock.patch('faraday.server.api.modules.agent.faraday_server')
7268 def test_create_agent_token_fails(self, faraday_server_config, test_client, session):
7369 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')
7571 assert res.status_code == 405
7672
7773
78 @pytest.mark.usefixtures('logged_user')
79 class TestAgentAuthTokenAPIGenericV3(TestAgentAuthTokenAPIGeneric):
80 def check_url(self, url):
81 return v2_to_v3(url)
82
83
8474 class TestAgentCreationAPI:
85
86 def check_url(self, url):
87 return url
8875
8976 @mock.patch('faraday.server.api.modules.agent.faraday_server')
9077 @pytest.mark.usefixtures('ignore_nplusone')
10592 token=pyotp.TOTP(secret, interval=60).now(),
10693 workspaces=[workspace, other_workspace]
10794 )
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)
11096 assert res.status_code == 201, (res.json, raw_data)
11197 assert len(session.query(Agent).all()) == initial_agent_count + 1
11298 assert workspace.name in res.json['workspaces']
133119 token=pyotp.TOTP(secret, interval=60).now(),
134120 workspaces=[workspace]
135121 )
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',
139124 data=raw_data
140125 )
141126 assert res.status_code == 400
155140 name="test agent",
156141 workspaces=[workspace]
157142 )
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)
160144 assert res.status_code == 401
161145
162146 @mock.patch('faraday.server.api.modules.agent.faraday_server')
171155 name="test agent",
172156 workspaces=[workspace],
173157 )
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)
176159 assert res.status_code == 400
177160
178161 @mock.patch('faraday.server.api.modules.agent.faraday_server')
181164 faraday_server_config.agent_registration_secret = None
182165 logout(test_client, [302])
183166 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)
186168 assert res.status_code == 400
187169
188170 @mock.patch('faraday.server.api.modules.agent.faraday_server')
200182 name="test agent",
201183 workspaces=[]
202184 )
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)
205186 assert res.status_code == 400
206187
207188 @mock.patch('faraday.server.api.modules.agent.faraday_server')
220201 workspaces=[]
221202 )
222203 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)
225205 assert res.status_code == 404
226206
227207 @mock.patch('faraday.server.api.modules.agent.faraday_server')
238218 name="test agent",
239219 token=pyotp.TOTP(secret, interval=60).now()
240220 )
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
249223
250224
251225 class TestAgentWithWorkspacesAPIGeneric(ReadWriteAPITests):
266240 assert '405' in exc_info.value.args[0]
267241
268242 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
270244 if obj is not None:
271245 id_ = str(obj.id) if isinstance(obj, self.model) else str(obj)
272 url += id_ + u'/'
246 url += u'/' + id_
273247 return url
274248
275249 def create_raw_agent(self, active=False, token="TOKEN",
280254 def test_create_agent_invalid(self, test_client, session):
281255 """
282256 To create new agent use the
283 <Rule '/v2/agent_registration/' (POST, OPTIONS)
257 <Rule '/v3/agent_registration/' (POST, OPTIONS)
284258 """
285259 initial_agent_count = len(session.query(Agent).all())
286260 raw_agent = self.create_raw_agent()
425399 assert res.status_code == 404
426400
427401
428 class TestAgentWithWorkspacesAPIGenericV3(TestAgentWithWorkspacesAPIGeneric, PatchableTestsMixin):
429 def url(self, obj=None):
430 return v2_to_v3(super().url(obj))
431
432
433402 class TestAgentAPI(ReadOnlyMultiWorkspacedAPITests):
434403 model = Agent
435404 factory = factories.AgentFactory
436405 view_class = AgentView
437406 api_endpoint = 'agents'
438
439 def check_url(self, url):
440 return url
441407
442408 def test_get_workspaced(self, test_client, session):
443409 workspace = WorkspaceFactory.create()
485451 'csrf_token': csrf_token
486452 }
487453 res = test_client.post(
488 self.check_url(urljoin(self.url(agent), 'run/')),
454 urljoin(self.url(agent), 'run'),
489455 json=payload
490456 )
491457 assert res.status_code == 400
495461 session.add(agent)
496462 session.commit()
497463 res = test_client.post(
498 self.check_url(urljoin(self.url(agent), 'run/')),
464 urljoin(self.url(agent), 'run'),
499465 data='[" broken]"{'
500466 )
501467 assert res.status_code == 400
517483 ('content-type', 'text/html'),
518484 ]
519485 res = test_client.post(
520 self.check_url(urljoin(self.url(agent), 'run/')),
486 urljoin(self.url(agent), 'run'),
521487 data=payload,
522488 headers=headers)
523489 assert res.status_code == 400
536502 },
537503 }
538504 res = test_client.post(
539 self.check_url(urljoin(self.url(agent), 'run/')),
505 urljoin(self.url(agent), 'run'),
540506 json=payload
541507 )
542508 assert res.status_code == 400
557523 'csrf_token': csrf_token,
558524 'executorData': {
559525 "args": {
560 "param1": True
526 "param_name": "test"
561527 },
562528 "executor": executor.name
563529 },
564530 }
565531 res = test_client.post(
566 self.check_url(urljoin(self.url(agent), 'run/')),
532 urljoin(self.url(agent), 'run'),
567533 json=payload
568534 )
569535 assert res.status_code == 200
575541 assert executor2.last_run is None
576542 assert agent.last_run == executor.last_run
577543
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
578566 def test_invalid_json_on_executorData_breaks_the_api(self, csrf_token,
579567 session, test_client):
580568 agent = AgentFactory.create(workspaces=[self.workspace])
585573 'executorData': '[][dassa',
586574 }
587575 res = test_client.post(
588 self.check_url(urljoin(self.url(agent), 'run/')),
576 urljoin(self.url(agent), 'run'),
589577 json=payload
590578 )
591579 assert res.status_code == 400
599587 'executorData': '',
600588 }
601589 res = test_client.post(
602 self.check_url(urljoin(self.url(agent), 'run/')),
590 urljoin(self.url(agent), 'run'),
603591 json=payload
604592 )
605593 assert res.status_code == 400
606594
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
1010 from tests import factories
1111 from flask_security.utils import hash_password
1212 from faraday.server.api.modules.websocket_auth import decode_agent_websocket_token
13 from tests.utils.url import v2_to_v3
1413
1514
1615 class TestWebsocketAuthEndpoint:
17 def check_url(self, url):
18 return url
19
2016 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')
2218 assert res.status_code == 401
2319
2420 @pytest.mark.usefixtures('logged_user')
2521 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')
2723 assert res.status_code == 200
2824
2925 # A token for that workspace should be generated,
3329
3430 @pytest.mark.usefixtures('logged_user')
3531 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')
3733 assert res.status_code == 200
3834
3935 # A token for that workspace should be generated,
4238 assert res.json['token'].startswith(str(workspace.id))
4339
4440
45 class TestWebsocketAuthEndpointV3(TestWebsocketAuthEndpoint):
46 def check_url(self, url):
47 return v2_to_v3(url)
48
49
5041 class TestAgentWebsocketToken:
51
52 def check_url(self, url):
53 return url
5442
5543 @pytest.mark.usefixtures('session') # I don't know why this is required
5644 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
6047 assert res.status_code == 401
6148
6249 @pytest.mark.usefixtures('logged_user')
6350 def test_fails_with_logged_user(self, test_client):
6451 res = test_client.post(
65 self.check_url('/v2/agent_websocket_token/')
52 '/v3/agent_websocket_token'
6653 )
6754 assert res.status_code == 401
6855
6956 @pytest.mark.usefixtures('logged_user')
7057 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')
7259
7360 assert res.status_code == 200
7461
7764 # clean cookies make sure test_client has no session
7865 test_client.cookie_jar.clear()
7966 res = test_client.post(
80 self.check_url('/v2/agent_websocket_token/'),
67 '/v3/agent_websocket_token',
8168 headers=headers,
8269 )
8370 assert res.status_code == 401
8673 def test_fails_with_invalid_agent_token(self, test_client):
8774 headers = [('Authorization', 'Agent 13123')]
8875 res = test_client.post(
89 self.check_url('/v2/agent_websocket_token/'),
76 '/v3/agent_websocket_token',
9077 headers=headers,
9178 )
9279 assert res.status_code == 403
9885 assert agent.token
9986 headers = [('Authorization', 'Agent ' + agent.token)]
10087 res = test_client.post(
101 self.check_url('/v2/agent_websocket_token/'),
88 '/v3/agent_websocket_token',
10289 headers=headers,
10390 )
10491 assert res.status_code == 200
10794
10895
10996 class TestBasicAuth:
110
111 def check_url(self, url):
112 return url
11397
11498 def test_basic_auth_invalid_credentials(self, test_client, session):
11599 """
120104 active=True,
121105 username='asdasd',
122106 password=hash_password('asdasd'),
123 role='admin')
107 roles=['admin'])
124108 session.add(alice)
125109 session.commit()
126110
130114
131115 valid_credentials = base64.b64encode(b"asdasd:wrong_password").decode("utf-8")
132116 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)
134118 assert res.status_code == 401
135119
136120 def test_basic_auth_valid_credentials(self, test_client, session):
142126 active=True,
143127 username='asdasd',
144128 password=hash_password('asdasd'),
145 role='admin')
129 roles=['admin'])
146130 session.add(alice)
147131 session.commit()
148132
152136
153137 valid_credentials = base64.b64encode(b"asdasd:asdasd").decode("utf-8")
154138 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)
156140 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)
1717 )
1818 from faraday.server.api.modules import bulk_create as bc
1919 from tests.factories import CustomFieldsSchemaFactory
20 from tests.utils.url import v2_to_v3
2120
2221 host_data = {
2322 "ip": "127.0.0.1",
623622
624623 class TestBulkCreateAPI:
625624
626 def check_url(self, url):
627 return url
628
629625 @pytest.mark.usefixtures('logged_user')
630626 def test_bulk_create_endpoint(self, session, workspace, test_client, logged_user):
631627 assert count(Host, workspace) == 0
632628 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'
634630 host_data_ = host_data.copy()
635631 service_data_ = service_data.copy()
636632 service_data_['vulnerabilities'] = [vuln_data]
665661 def test_bulk_create_endpoint_run_over_closed_vuln(self, session, workspace, test_client):
666662 assert count(Host, workspace) == 0
667663 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'
669665 host_data_ = host_data.copy()
670666 host_data_['vulnerabilities'] = [vuln_data]
671667 res = test_client.post(url, data=dict(hosts=[host_data_]))
677673 assert host.ip == "127.0.0.1"
678674 assert set({hn.name for hn in host.hostnames}) == {"test.com", "test2.org"}
679675 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}'
681677 res = test_client.get(close_url)
682678 vuln_data_del = res.json
683679 vuln_data_del["status"] = "closed"
695691
696692 @pytest.mark.usefixtures('logged_user')
697693 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'
699695 host_data_ = host_data.copy()
700696 host_data_.pop('ip')
701697 res = test_client.post(url, data=dict(hosts=[host_data_]))
702698 assert res.status_code == 400
703699
704700 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'
706702 res = test_client.post(url, data=dict(hosts=[host_data]))
707703 assert res.status_code == 401
708704 assert count(Host, workspace) == 0
709705
710706 @pytest.mark.parametrize('token_type', ['agent', 'token'])
711707 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'
713709 res = test_client.post(
714710 url,
715711 data=dict(hosts=[host_data]),
730726 session.add(agent)
731727 session.commit()
732728 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'
734730 res = test_client.post(
735731 url,
736732 data=dict(hosts=[host_data]),
745741 session.add(agent)
746742 session.commit()
747743 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"
749745 res = test_client.post(
750746 url,
751747 data=dict(hosts=[host_data]),
761757 session.commit()
762758 for workspace in agent.workspaces:
763759 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'
765761 res = test_client.post(
766762 url,
767763 data=dict(hosts=[host_data]),
823819
824820 initial_host_count = Host.query.filter(Host.workspace == workspace and Host.creator_id is None).count()
825821 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'
827823 res = test_client.post(
828824 url,
829825 data=dict(**data_kwargs),
883879 session.commit()
884880 assert count(Host, workspace) == 0
885881 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'
887883 res = test_client.post(
888884 url,
889885 data=dict(hosts=[host_data], execution_id=agent_execution.id),
911907 session.add(workspace)
912908 session.commit()
913909 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'
915911 res = test_client.post(
916912 url,
917913 data=dict(hosts=[host_data]),
926922 session.add(workspace)
927923 session.commit()
928924 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'
930926 res = test_client.post(
931927 url,
932928 data=dict(hosts=[host_data]),
936932
937933 @pytest.mark.usefixtures('logged_user')
938934 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'
940936 res = test_client.post(
941937 url,
942938 data="",
949945 def test_bulk_create_endpoint_with_vuln_run_date(self, session, workspace, test_client):
950946 assert count(Host, workspace) == 0
951947 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'
953949 run_date = datetime.now(timezone.utc) - timedelta(days=30)
954950 host_data_copy = host_data.copy()
955951 vuln_data_copy = vuln_data.copy()
966962 def test_bulk_create_endpoint_with_vuln_future_run_date(self, session, workspace, test_client):
967963 assert count(Host, workspace) == 0
968964 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'
970966 run_date = datetime.now(timezone.utc) + timedelta(days=10)
971967 host_data_copy = host_data.copy()
972968 vuln_data_copy = vuln_data.copy()
984980 def test_bulk_create_endpoint_with_invalid_vuln_run_date(self, session, workspace, test_client):
985981 assert count(Host, workspace) == 0
986982 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'
988984 host_data_copy = host_data.copy()
989985 vuln_data_copy = vuln_data.copy()
990986 vuln_data_copy['run_date'] = "INVALID_VALUE"
998994 logged_user):
999995 assert count(Host, workspace) == 0
1000996 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'
1002998 host_data_ = host_data.copy()
1003999 host_data_['services'] = [service_data]
10041000 host_data_['credentials'] = [credential_data]
10241020
10251021 assert count(Host, workspace) == 0
10261022 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'
10281024 host_data_ = host_data.copy()
10291025 service_data_ = service_data.copy()
10301026 vuln_data_ = vuln_data.copy()
10611057
10621058 @pytest.mark.usefixtures('logged_user')
10631059 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'
10651061 host_data_ = host_data.copy()
10661062 vuln_web_data_ = vuln_web_data.copy()
10671063 vuln_web_data_['severity'] = "high"
10721068 data=dict(hosts=[host_data_], command=command_data)
10731069 )
10741070 assert res.status_code == 400
1075
1076
1077 class TestBulkCreateAPIV3(TestBulkCreateAPI):
1078 def check_url(self, url):
1079 return v2_to_v3(url)
44 See the file 'doc/LICENSE' for the license information
55
66 '''
7 from tests.utils.url import v2_to_v3
87
98 """Tests for many API endpoints that do not depend on workspace_name"""
109 from posixpath import join as urljoin
1312 import time
1413
1514 from tests import factories
16 from tests.test_api_workspaced_base import ReadWriteAPITests, PatchableTestsMixin
15 from tests.test_api_workspaced_base import ReadWriteAPITests
1716 from faraday.server.models import (
1817 Command,
1918 Vulnerability)
20 from faraday.server.api.modules.commandsrun import CommandView, CommandV3View
19 from faraday.server.api.modules.commandsrun import CommandView
2120 from tests.factories import VulnerabilityFactory, EmptyCommandFactory, CommandObjectFactory, HostFactory, \
2221 WorkspaceFactory, ServiceFactory
2322
3534 api_endpoint = 'commands'
3635 view_class = CommandView
3736 patchable_fields = ["ip"]
38
39 def check_url(self, url):
40 return url
4137
4238 @pytest.mark.usefixtures('ignore_nplusone')
4339 @pytest.mark.usefixtures('mock_envelope_list')
9692 )
9793 session.commit()
9894
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'))
10096 assert res.status_code == 200
10197
10298 assert list(filter(lambda stats: stats['_id'] == command.id, res.json)) == [
153149 workspace=workspace
154150 )
155151 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'))
157153 assert res.status_code == 200
158154 assert res.json == [
159155 {u'_id': command.id,
202198 workspace=workspace
203199 )
204200 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'))
206202 assert res.status_code == 200
207203 assert res.json == [{
208204 u'_id': command.id,
268264 workspace=workspace
269265 )
270266 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'))
272268 assert res.status_code == 200
273269 raw_first_command = list(filter(lambda comm: comm['_id'] == commands[0].id, res.json))
274270
416412 )
417413 session.commit()
418414
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}')
423419 assert res.status_code == 204
424420
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'))
426422 assert res.status_code == 200
427423 command_history = list(filter(lambda hist: hist['_id'] == command.id, res.json))
428424 assert len(command_history)
445441 res = test_client.post(self.url(), data=raw_data)
446442
447443 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)
44
55 '''
66
7 from faraday.server.api.modules.comments import CommentView, CommentV3View
7 from faraday.server.api.modules.comments import CommentView
88 from faraday.server.models import Comment
99 from tests.factories import ServiceFactory
10 from tests.test_api_workspaced_base import ReadWriteAPITests, PatchableTestsMixin
10 from tests.test_api_workspaced_base import ReadWriteAPITests
1111 from tests import factories
12 from tests.utils.url import v2_to_v3
1312
1413
1514 class TestCommentAPIGeneric(ReadWriteAPITests):
1918 api_endpoint = 'comment'
2019 update_fields = ['text']
2120 patchable_fields = ['text']
22
23 def check_url(self, url):
24 return url
2521
2622 def _create_raw_comment(self, object_type, object_id):
2723 return {
9086 assert res.status_code == 201
9187 assert len(session.query(Comment).all()) == initial_comment_count + 1
9288
93 url = self.check_url(self.url(workspace=self.workspace).strip('/') + '_unique/')
89 url = self.url(workspace=self.workspace).strip('/') + '_unique'
9490 res = test_client.post(url, data=raw_comment)
9591 assert res.status_code == 409
9692 assert 'object' in res.json
105101 session.commit()
106102 initial_comment_count = len(session.query(Comment).all())
107103 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'
109105 res = test_client.post(url,
110106 data=raw_comment)
111107 assert res.status_code == 201
125121 get_comments = test_client.get(self.url(workspace=workspace))
126122 expected = ['first', 'second', 'third', 'fourth']
127123 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)
99 from tests import factories
1010 from tests.test_api_workspaced_base import (
1111 ReadWriteAPITests,
12 PatchableTestsMixin,
1312 )
14 from faraday.server.api.modules.credentials import CredentialView, CredentialV3View
13 from faraday.server.api.modules.credentials import CredentialView
1514 from faraday.server.models import Credential
1615 from tests.factories import HostFactory, ServiceFactory
17 from tests.utils.url import v2_to_v3
1816
1917
2018 class TestCredentialsAPIGeneric(ReadWriteAPITests):
264262 response = test_client.get(self.url(workspace=second_workspace) + "?sort=target&sort_dir=asc")
265263 assert response.status_code == 200
266264 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))
00 import pytest
11
22 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
44
55 from faraday.server.api.modules.custom_fields import CustomFieldsSchemaView
66 from faraday.server.models import (
77 CustomFieldsSchema
88 )
9 from tests.utils.url import v2_to_v3
109
1110
1211 @pytest.mark.usefixtures('logged_user')
8180 assert {u'table_name': u'vulnerability', u'id': add_choice_field.id, u'field_type': u'choice',
8281 u'field_name': u'gender', u'field_display_name': u'Gender', u'field_metadata': "['Male', 'Female']",
8382 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))
1515 VulnerabilityFactory,
1616 VulnerabilityWebFactory
1717 )
18 from tests.utils.url import v2_to_v3
1918
2019
2120 @pytest.mark.usefixtures('logged_user')
2221 class TestExportData:
2322
24 def check_url(self, url):
25 return url
26
2723 def test_export_data_without_format(self, test_client):
2824 workspace = WorkspaceFactory.create()
29 url = self.check_url(f'/v2/ws/{workspace.name}/export_data')
25 url = f'/v3/ws/{workspace.name}/export_data'
3026 response = test_client.get(url)
3127 assert response.status_code == 400
3228
8985 session.add(vuln_web)
9086 session.commit()
9187
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'
9389 response = test_client.get(url)
9490 assert response.status_code == 200
9591 response_xml = response.data
142138 assert response_tree.xpath(full_xpath)[0].text == xml_file_hostnames
143139 else:
144140 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)
2828 assert len(rules) == 0, [rule.rule for rule in rules]
2929
3030
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
4035 rules_v2 = set(
4136 map(
4237 lambda rule: rule.rule.replace("v2", "v3").rstrip("/"),
4742 map(lambda rule: rule.rule, filter(lambda rule: rule.rule.startswith("/v3"), get_app().url_map.iter_rules()))
4843 )
4944 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)
5146 exceptions_present = rules.intersection(exceptions)
5247 assert len(exceptions_present) == 0, sorted(exceptions_present)
5348 # We can have extra endpoints in v3 (like all the PATCHS)
1212 class TestGetExploits():
1313 def test_get_exploit(self, test_client):
1414 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}')
1616 assert res.status_code == 200
1717
1818 @pytest.mark.skip()
1919 def test_key_error(self, test_client):
2020 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}')
2222 assert res.status_code == 400
2323
2424 @pytest.mark.skip()
2525 def test_get_exploit_with_modules(self, test_client):
2626 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}')
2828 assert res.status_code == 200
2929 assert res.json.get('metasploit') != []
3030 assert res.json.get('exploitdb') != []
99
1010 import pytz
1111
12 from tests.utils.url import v2_to_v3
13
1412 from urllib.parse import urlencode
1513 from random import choice
1614 from sqlalchemy.orm.util import was_deleted
2220 from tests.test_api_workspaced_base import (
2321 API_PREFIX,
2422 ReadWriteAPITests,
25 PaginationTestsMixin, PatchableTestsMixin,
23 PaginationTestsMixin,
2624 )
2725 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
2927 from tests.factories import HostFactory, EmptyCommandFactory, WorkspaceFactory
3028
3129 HOSTS_COUNT = 5
3432
3533 @pytest.mark.usefixtures('database', 'logged_user')
3634 class TestHostAPI:
37
38 def check_url(self, url):
39 return url
4035
4136 @pytest.fixture(autouse=True)
4237 def load_workspace_with_hosts(self, database, session, workspace, host_factory):
6560
6661 def url(self, host=None, workspace=None):
6762 workspace = workspace or self.workspace
68 url = API_PREFIX + workspace.name + '/hosts/'
63 url = API_PREFIX + workspace.name + '/hosts'
6964 if host is not None:
70 url += str(host.id) + '/'
65 url += '/' + str(host.id)
7166 return url
7267
7368 def services_url(self, host, workspace=None):
74 return self.url(host, workspace) + 'services/'
69 return self.url(host, workspace) + '/services'
7570
7671 def compare_results(self, hosts, response):
7772 """
617612 vulnerability_factory.create(service=service, host=None, workspace=workspace)
618613 session.commit()
619614
620 res = test_client.get(self.check_url(urljoin(self.url(host), 'services/')))
615 res = test_client.get(urljoin(self.url(host), 'services'))
621616 assert res.status_code == 200
622617 assert res.json[0]['vulns'] == 1
623618
758753 'csrf_token': csrf_token
759754 }
760755 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',
762757 data=data, headers=headers, use_json_data=False)
763758 assert res.status_code == 200
764759 assert res.json['hosts_created'] == expected_created_hosts
765760 assert res.json['hosts_with_errors'] == 0
766761 assert session.query(Host).filter_by(description="test_host").count() == expected_created_hosts
767762
763 @pytest.mark.skip("This was a v2 test, will be reimplemented")
768764 def test_bulk_delete_hosts(self, test_client, session):
769765 ws = WorkspaceFactory.create(name="abc")
770766 host_1 = HostFactory.create(workspace=ws)
773769 hosts_ids = [host_1.id, host_2.id]
774770 request_data = {'hosts_ids': hosts_ids}
775771
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)
777773
778774 deleted_hosts = delete_response.json['deleted_hosts']
779775 host_count_after_delete = db.session.query(Host).filter(
784780 assert deleted_hosts == len(hosts_ids)
785781 assert host_count_after_delete == 0
786782
783 @pytest.mark.skip("This was a v2 test, will be reimplemented")
787784 def test_bulk_delete_hosts_without_hosts_ids(self, test_client):
788785 ws = WorkspaceFactory.create(name="abc")
789786 request_data = {'hosts_ids': []}
790787
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)
792789
793790 assert delete_response.status_code == 400
794791
792 @pytest.mark.skip("This was a v2 test, will be reimplemented")
795793 def test_bulk_delete_hosts_from_another_workspace(self, test_client, session):
796794 workspace_1 = WorkspaceFactory.create(name='workspace_1')
797795 host_of_ws_1 = HostFactory.create(workspace=workspace_1)
801799
802800 # Try to delete workspace_2's host from workspace_1
803801 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'
805803 delete_response = test_client.delete(url, data=request_data)
806804
807805 assert delete_response.json['deleted_hosts'] == 0
808806
807 @pytest.mark.skip("This was a v2 test, will be reimplemented")
809808 def test_bulk_delete_hosts_invalid_characters_in_request(self, test_client):
810809 ws = WorkspaceFactory.create(name="abc")
811810 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)
813812
814813 assert delete_response.json['deleted_hosts'] == 0
815814
815 @pytest.mark.skip("This was a v2 test, will be reimplemented")
816816 def test_bulk_delete_hosts_wrong_content_type(self, test_client, session):
817817 ws = WorkspaceFactory.create(name="abc")
818818 host_1 = HostFactory.create(workspace=ws)
824824 headers = [('content-type', 'text/xml')]
825825
826826 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',
828828 data=request_data,
829829 headers=headers)
830830
831831 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
863832
864833
865834 class TestHostAPIGeneric(ReadWriteAPITests, PaginationTestsMixin):
11321101 index_in_response_hosts = response_hosts.index(host)
11331102
11341103 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))
11421104
11431105
11441106 def host_json():
11811143 @given(HostData)
11821144 def send_api_request(raw_data):
11831145 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
11911146 res = test_client.post(f'/v3/ws/{ws_name}/vulns',
11921147 data=raw_data)
11931148 assert res.status_code in [201, 400, 409]
11941149
11951150 send_api_request()
1196 send_api_request_v3()
1111 class TestAPIInfoEndpoint:
1212
1313 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):
1914 response = test_client.get('v3/info')
2015 assert response.status_code == 200
2116 assert response.json['Faraday Server'] == 'Running'
2217
2318 def test_get_config(self, test_client):
19 from faraday import __version__
2420 res = test_client.get('/config')
2521 assert res.status_code == 200
26 assert res.json['lic_db'] == 'faraday_licenses'
22 assert __version__ in res.json['ver']
44 See the file 'doc/LICENSE' for the license information
55
66 '''
7 from tests.utils.url import v2_to_v3
87
98 """Tests for many API endpoints that do not depend on workspace_name"""
109
1312 from hypothesis import given, strategies as st
1413
1514 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
1716 from faraday.server.models import (
1817 License,
1918 )
3534 api_endpoint = 'licenses'
3635 patchable_fields = ["products"]
3736
37 # @pytest.mark.skip(reason="Not a license actually test")
3838 def test_envelope_list(self, test_client, app):
3939 LicenseEnvelopedView.register(app)
4040 original_res = test_client.get(self.url())
4141 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')
4343 assert new_res.status_code == 200
4444
4545 assert new_res.json == {"object_list": original_res.json}
5151 res = test_client.get(self.url(obj=lic))
5252 assert res.status_code == 200
5353 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
6354
6455
6556 def license_json():
9283 def send_api_request(raw_data):
9384 raw_data['start'] = pytz.UTC.localize(raw_data['start']).isoformat()
9485 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()
10286 res = test_client.post('v3/licenses/', data=raw_data)
10387 assert res.status_code in [201, 400, 409]
10488
10589 send_api_request()
106 send_api_request_v3()
55 from faraday.server.models import User
66 from faraday.server.web import get_app
77 from tests import factories
8 from tests.utils.url import v2_to_v3
98
109
1110 class TestLogin:
12
13 def check_url(self, url):
14 return url
15
1611 def test_case_bug_with_username(self, test_client, session):
1712 """
1813 When the user case does not match the one in database,
2318 active=True,
2419 username='Susan',
2520 password=hash_password('pepito'),
26 role='pentester')
21 roles=['pentester'])
2722 session.add(susan)
2823 session.commit()
2924 # we use lower case username, but in db is Capitalized
4439 active=True,
4540 username='alice',
4641 password=hash_password('passguord'),
47 role='pentester')
42 roles=['pentester'])
4843 session.add(alice)
4944 session.commit()
5045
6257
6358 headers = {'Authentication-Token': res.json['response']['user']['authentication_token']}
6459
65 ws = test_client.get(self.check_url('/v2/ws/wonderland/'), headers=headers)
60 ws = test_client.get('/v3/ws/wonderland', headers=headers)
6661 assert ws.status_code == 200
6762
6863 def test_case_ws_with_invalid_authentication_token(self, test_client, session):
7671 active=True,
7772 username='alice',
7873 password=hash_password('passguord'),
79 role='pentester')
74 roles=['pentester'])
8075 session.add(alice)
8176 session.commit()
8277
8984
9085 headers = {'Authorization': b'Token ' + token}
9186
92 ws = test_client.get(self.check_url('/v2/ws/wonderland/'), headers=headers)
87 ws = test_client.get('/v3/ws/wonderland', headers=headers)
9388 assert ws.status_code == 401
9489
9590 @pytest.mark.usefixtures('logged_user')
9691 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')
9893 cookies = [cookie.name for cookie in test_client.cookie_jar]
9994 assert "faraday_session_2" in cookies
10095 assert res.status_code == 200
105100 session.commit()
106101 # clean cookies make sure test_client has no session
107102 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)
109104 assert res.status_code == 200
110105 assert 'Set-Cookie' not in res.headers
111106 cookies = [cookie.name for cookie in test_client.cookie_jar]
114109 def test_cant_retrieve_token_unauthenticated(self, test_client):
115110 # clean cookies make sure test_client has no session
116111 test_client.cookie_jar.clear()
117 res = test_client.get(self.check_url('/v2/token/'))
112 res = test_client.get('/v3/token')
118113
119114 assert res.status_code == 401
120115
121116 @pytest.mark.usefixtures('logged_user')
122117 def test_token_expires_after_password_change(self, test_client, session):
123118 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')
125120
126121 assert res.status_code == 200
127122
134129
135130 # clean cookies make sure test_client has no session
136131 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)
138133 assert res.status_code == 401
139134
140135 def test_null_caracters(self, test_client, session):
146141 active=True,
147142 username='asdasd',
148143 password=hash_password('asdasd'),
149 role='pentester')
144 roles=['pentester'])
150145 session.add(alice)
151146 session.commit()
152147
165160
166161 headers = {'Authentication-Token': res.json['response']['user']['authentication_token']}
167162
168 ws = test_client.get(self.check_url('/v2/ws/wonderland/'), headers=headers)
163 ws = test_client.get('/v3/ws/wonderland', headers=headers)
169164 assert ws.status_code == 200
170165
171166 def test_login_remember_me(self, test_client, session):
177172 active=True,
178173 username='susan',
179174 password=hash_password('pepito'),
180 role='pentester')
175 roles=['pentester'])
181176 session.add(susan)
182177 session.commit()
183178
201196 active=True,
202197 username='susan',
203198 password=hash_password('pepito'),
204 role='pentester')
199 roles=['pentester'])
205200 session.add(susan)
206201 session.commit()
207202 login_payload = {
224219 active=True,
225220 username='susan',
226221 password=hash_password('pepito'),
227 role='pentester')
222 roles=['pentester'])
228223 session.add(susan)
229224 session.commit()
230225 login_payload = {
235230 assert res.status_code == 200
236231 cookies = [cookie.name for cookie in test_client.cookie_jar]
237232 assert "remember_token" not in cookies
238
239
240 class TestLoginV3(TestLogin):
241 def check_url(self, url):
242 return v2_to_v3(url)
1111 import pytest
1212 from sqlalchemy.orm.util import was_deleted
1313
14 API_PREFIX = '/v2/'
14 API_PREFIX = '/v3/'
1515 OBJECT_COUNT = 5
1616
1717
4040 return obj
4141
4242 def url(self, obj=None):
43 url = API_PREFIX + self.api_endpoint + '/'
43 url = API_PREFIX + self.api_endpoint
4444 if obj is not None:
4545 id_ = str(getattr(obj, self.lookup_field)) if isinstance(
4646 obj, self.model) else str(obj)
47 url += id_ + u'/'
47 url += u'/' + id_
4848 return url
4949
5050
102102 @pytest.mark.usefixtures('logged_user')
103103 class UpdateTestsMixin:
104104
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"])
106110 def test_update_an_object(self, test_client, logged_user, method):
107111 data = self.factory.build_dict()
108112 if method == "PUT":
109113 res = test_client.put(self.url(self.first_object), data=data)
110114 elif method == "PATCH":
111 data = PatchableTestsMixin.control_data(self, data)
115 data = self.control_data(self, data)
112116 res = test_client.patch(self.url(self.first_object), data=data)
113117 assert res.status_code == 200, (res.status_code, res.json)
114118 assert self.model.query.count() == OBJECT_COUNT
132136 """To do this the user should use a PATCH request"""
133137 res = test_client.put(self.url(self.first_object), data={})
134138 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)
151139
152140 def test_patch_update_an_object_does_not_fail_with_partial_data(self, test_client, logged_user):
153141 """To do this the user should use a PATCH request"""
11 from tests.factories import UserFactory
22 from faraday.server.models import User
33 from faraday.server.api.modules.preferences import PreferencesView
4 from tests.utils.url import v2_to_v3
54
65
76 # pytest.fixture('logged_user')
4140 response = test_client.post(self.url(), data=data)
4241
4342 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))
99 import pytest
1010
1111 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
1313 from tests.test_api_agent import logout
1414 from tests.conftest import login_as
1515 from faraday.server.models import SearchFilter
1616
1717 from faraday.server.api.modules.search_filter import SearchFilterView
18 from tests.utils.url import v2_to_v3
1918
2019
2120 @pytest.mark.usefixtures('logged_user')
103102 res = test_client.delete(self.url(user_filter))
104103 assert res.status_code == 404
105104
106 @pytest.mark.parametrize("method", ["PUT"])
105 @pytest.mark.parametrize("method", ["PUT", "PATCH"])
107106 def test_update_an_object(self, test_client, logged_user, method):
108107 self.first_object.creator = logged_user
109108 super().test_update_an_object(test_client, logged_user, method)
116115 self.first_object.creator = logged_user
117116 super().test_delete(test_client, logged_user)
118117
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
129118 def test_patch_update_an_object_does_not_fail_with_partial_data(self, test_client, logged_user):
130119 self.first_object.creator = logged_user
131120 super().test_update_an_object_fails_with_empty_dict(test_client, logged_user)
44 See the file 'doc/LICENSE' for the license information
55
66 '''
7 from tests.utils.url import v2_to_v3
8
97 """Tests for many API endpoints that do not depend on workspace_name"""
108 try:
119 from urllib import urlencode
1513 import pytest
1614 import json
1715
18 from faraday.server.api.modules.services import ServiceView, ServiceV3View
16 from faraday.server.api.modules.services import ServiceView
1917 from tests import factories
20 from tests.test_api_workspaced_base import ReadWriteAPITests, PatchableTestsMixin
18 from tests.test_api_workspaced_base import ReadWriteAPITests
2119 from faraday.server.models import (
2220 Service
2321 )
232230 updated_service = Service.query.filter_by(id=service.id).first()
233231 assert updated_service.port == 221
234232
235 @pytest.mark.parametrize("method", ["PUT"])
233 @pytest.mark.parametrize("method", ["PUT", "PATCH"])
236234 def test_update_cant_change_id(self, test_client, session, method):
237235 service = self.factory()
238236 host = HostFactory.create()
336334 res = test_client.post(self.url(), data=data)
337335 print(res.data)
338336 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)
55 '''
66
77 import pytest
8
9 from faraday.server.models import Role
810 from tests.conftest import login_as
911
1012
1618
1719 @pytest.mark.parametrize('role', ['admin', 'pentester', 'client', 'asset_owner'])
1820 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()]
2022 session.commit()
2123 login_as(test_client, user)
2224 res = test_client.get('/session')
23 assert res.json['role'] == role
25 assert role in res.json['roles']
2426
2527
2628 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
1313 from faraday.server.threads.reports_processor import REPORTS_QUEUE
1414
1515 from faraday.server.models import Host, Service, Command
16 from tests.utils.url import v2_to_v3
1716
1817
1918 @pytest.mark.usefixtures('logged_user')
2019 class TestFileUpload:
21
22 def check_url(self, url):
23 return url
2420
2521 def test_file_upload(self, test_client, session, csrf_token, logged_user):
2622 ws = WorkspaceFactory.create(name="abc")
3632 }
3733
3834 res = test_client.post(
39 self.check_url(f'/v2/ws/{ws.name}/upload_report'),
35 f'/v3/ws/{ws.name}/upload_report',
4036 data=data,
4137 use_json_data=False)
4238
7369 session.add(ws)
7470 session.commit()
7571
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')
7773
7874 assert res.status_code == 400
7975
9187 }
9288
9389 res = test_client.post(
94 self.check_url(f'/v2/ws/{ws.name}/upload_report'),
90 f'/v3/ws/{ws.name}/upload_report',
9591 data=data,
9692 use_json_data=False)
9793
112108 'csrf_token': csrf_token
113109 }
114110 res = test_client.post(
115 self.check_url(f'/v2/ws/{ws.name}/upload_report'),
111 f'/v3/ws/{ws.name}/upload_report',
116112 data=data,
117113 use_json_data=False
118114 )
119115
120116 assert res.status_code == 404
121
122
123 class TestFileUploadV3(TestFileUpload):
124 def check_url(self, url):
125 return v2_to_v3(url)
1515 from io import BytesIO, StringIO
1616 from posixpath import join as urljoin
1717
18 from tests.utils.url import v2_to_v3
19
2018 try:
2119 from urllib import urlencode
2220 except ImportError:
3230 from faraday.server.api.modules.vulns import (
3331 VulnerabilityFilterSet,
3432 VulnerabilitySchema,
35 VulnerabilityView,
36 VulnerabilityV3View
33 VulnerabilityView
3734 )
3835 from faraday.server.fields import FaradayUploadedFile
3936 from faraday.server.schemas import NullToBlankString
4037 from tests import factories
4138 from tests.conftest import TEST_DATA_PATH
4239 from tests.test_api_workspaced_base import (
43 ReadWriteAPITests, PatchableTestsMixin
40 ReadWriteAPITests
4441 )
4542 from faraday.server.models import (
4643 VulnerabilityGeneric,
162159 view_class = VulnerabilityView
163160 patchable_fields = ['description']
164161
165 def check_url(self, url):
166 return url
167
168162 def test_backward_json_compatibility(self, test_client, second_workspace, session):
169163 new_obj = self.factory.create(workspace=second_workspace)
170164 session.add(new_obj)
329323 assert res.status_code == 400
330324 assert b'Shorter than minimum length 1' in res.data
331325
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
332344 def test_create_create_vuln_with_empty_desc_success(
333345 self, host, session, test_client):
334346 # I'm using this to test the NonBlankColumn which works for
372384 assert filename in res.json['_attachments']
373385 attachment.close()
374386 # 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}'))
376388 assert res.status_code == 200
377389 assert res.data == file_content
378390
379 res = test_client.get(self.check_url(urljoin(
391 res = test_client.get(urljoin(
380392 self.url(),
381 f'{vuln_id}/attachment/notexistingattachment.png/'
382 )))
393 f'{vuln_id}/attachment/notexistingattachment.png'
394 ))
383395 assert res.status_code == 404
384396
385397 @pytest.mark.usefixtures('ignore_nplusone')
403415 res = test_client.put(self.url(obj=vuln, workspace=self.workspace), data=raw_data)
404416 assert res.status_code == 200
405417 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 ))
409421 assert res.status_code == 200
410422 assert res.data == file_content
411423
427439 assert res.status_code == 200
428440
429441 # 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 ))
433445 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 ))
437449 assert res.status_code == 200
438450 assert res.data == file_content
439451
451463 session.add(new_attach)
452464 session.commit()
453465
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'))
460467 assert res.status_code == 200
461468 assert new_attach.filename in res.json
462469 assert 'image/png' in res.json[new_attach.filename]['content_type']
848855 10, workspace=self.workspace, host=host2, service=None)
849856
850857 session.commit()
851 res = test_client.get(self.check_url(urljoin(
858 res = test_client.get(urljoin(
852859 self.url(), 'filter?q={"filters":[{"name": "target", "op":"eq", "val":"192.168.0.1"}]}'
853 )))
860 ))
854861
855862 assert res.status_code == 200
856863 assert len(res.json['vulnerabilities']) == 10
869876 10, workspace=self.workspace, host=host2, service=None)
870877
871878 session.commit()
872 res = test_client.get(self.check_url(urljoin(
879 res = test_client.get(urljoin(
873880 self.url(),
874881 'filter?q={"filters":[{"name": "target_host_ip", "op":"eq", "val":"192.168.0.2"}]}'
875 )))
882 ))
876883 assert res.status_code == 200
877884 assert len(res.json['vulnerabilities']) == 1
878885 assert res.json['vulnerabilities'][0]['value']['target'] == '192.168.0.2'
892899 10, workspace=self.workspace, host=None, service=service)
893900
894901 session.commit()
895 res = test_client.get(self.check_url(urljoin(
902 res = test_client.get(urljoin(
896903 self.url(),
897904 'filter?q={"filters":[{"name": "service", "op":"has", "val":{"name": "port", "op":"eq", "val":"8956"}}]}'
898 )))
905 ))
899906 assert res.status_code == 200
900907 assert len(res.json['vulnerabilities']) == 10
901908 assert res.json['count'] == 10
915922 10, workspace=self.workspace, host=None, service=service)
916923
917924 session.commit()
918 res = test_client.get(self.check_url(urljoin(
925 res = test_client.get(urljoin(
919926 self.url(),
920927 'filter?q={"filters":[{"name": "service", "op":"has", "val":{"name": "name", "op":"eq", "val":"ssh"}}]}'
921 )))
928 ))
922929 assert res.status_code == 200
923930 assert len(res.json['vulnerabilities']) == 1
924931 assert res.json['count'] == 1
11881195
11891196 # Desc
11901197 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 ))
11941200 assert res.status_code == 400
11951201
11961202 # 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"))
12011204 assert res.status_code == 400
12021205
12031206 def test_count_order_by(self, test_client, session):
12141217
12151218 # Desc
12161219 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 ))
12191222 assert res.status_code == 200
12201223 assert res.json['total_count'] == 3
12211224 assert sorted(res.json['groups'], key=lambda i: (i['name'], i['count'], i['severity'])) == sorted([
12251228
12261229 # Asc
12271230 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"))
12291232 assert res.status_code == 200
12301233 assert res.json['total_count'] == 3
12311234 assert sorted(res.json['groups'], key=lambda i: (i['name'], i['count'], i['severity']), reverse=True) == sorted(
12461249 session.add(vuln)
12471250 session.commit()
12481251
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"))
12501253 assert res.status_code == 400
12511254
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="))
12531256 assert res.status_code == 400
12541257
12551258 def test_count_confirmed(self, test_client, session):
12651268 session.add(vuln)
12661269 session.commit()
12671270
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'))
12691272 assert res.status_code == 200
12701273 assert res.json['total_count'] == 3
12711274 assert sorted(res.json['groups'], key=lambda i: (i['count'], i['name'], i['severity'])) == sorted([
12841287 session.commit()
12851288
12861289 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 ))
12891292 assert res.status_code == 200
12901293 assert res.json['total_count'] == 9
12911294 assert sorted(res.json['groups'], key=lambda i: (i['count'], i['name'], i['severity'])) == sorted([
13071310 session.commit()
13081311
13091312 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 )
13121317 )
13131318
13141319 assert res.status_code == 200
13371342 session.commit()
13381343
13391344 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 )
13421350 )
13431351
13441352 assert res.status_code == 200
13471355
13481356 def test_count_multiworkspace_no_workspace_param(self, test_client):
13491357 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 ))
13531360 assert res.status_code == 400
13541361
13551362 def test_count_multiworkspace_no_groupby_param(self, test_client):
13561363 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 ))
13601366 assert res.status_code == 400
13611367
13621368 def test_count_multiworkspace_nonexistent_ws(self, test_client):
13631369 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 )
13661374 )
13671375 assert res.status_code == 404
13681376
17031711 )
17041712 ws_name = host_with_hostnames.workspace.name
17051713 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}'),
17071715 data=raw_data
17081716 )
17091717 assert res.status_code == 201
17181726 severity='high',
17191727 )
17201728 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 )
17241734 assert res.status_code == 200
17251735
17261736 def test_create_vuln_from_command(self, test_client, session):
18541864 def test_search_by_hostnames_service_case(self, session, test_client):
18551865 workspace = WorkspaceFactory.create()
18561866 vuln2 = VulnerabilityFactory.create(workspace=workspace)
1857 hostname = HostnameFactory.create(workspace=workspace, name='test.com')
18581867 host = HostFactory.create(workspace=workspace)
1868 hostname = HostnameFactory.create(workspace=workspace, name='test.com', host=host)
18591869 host.hostnames.append(hostname)
18601870 service = ServiceFactory.create(workspace=workspace, host=host)
18611871 vuln = VulnerabilityFactory.create(service=service, host=None, workspace=workspace)
18741884 def test_search_by_hostnames_host_case(self, session, test_client):
18751885 workspace = WorkspaceFactory.create()
18761886 vuln2 = VulnerabilityFactory.create(workspace=workspace)
1877 hostname = HostnameFactory.create(workspace=workspace, name='test.com')
18781887 host = HostFactory.create(workspace=workspace)
1888 hostname = HostnameFactory.create(workspace=workspace, name='test.com', host=host)
18791889 host.hostnames.append(hostname)
18801890 vuln = VulnerabilityFactory.create(host=host, service=None, workspace=workspace)
18811891 session.add(vuln)
19591969 headers = {'Content-type': 'multipart/form-data'}
19601970
19611971 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',
19631973 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
19741976
19751977 file_id = session.query(Vulnerability).filter_by(id=vuln.id).first().evidence[0].content['file_id']
19761978 depot = DepotManager.get()
19921994 session.commit()
19931995
19941996 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',
19961998 data=data, headers=headers, use_json_data=False)
19971999 assert res.status_code == 403
19982000 query_test = session.query(Vulnerability).filter_by(id=vuln.id).first().evidence
20142016 policyviolations=[],
20152017 attachments=[attachment]
20162018 )
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)
20182020 assert res.status_code == 201
20192021
20202022 filename = attachment.name.split('/')[-1]
20212023 vuln_id = res.json['_id']
20222024 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}'
20242026 )
20252027 assert res.status_code == 200
20262028
20432045 policyviolations=[],
20442046 attachments=[attachment]
20452047 )
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)
20472049 assert res.status_code == 201
20482050
20492051 self.workspace.readonly = True
20522054 filename = attachment.name.split('/')[-1]
20532055 vuln_id = res.json['_id']
20542056 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}'
20562058 )
20572059 assert res.status_code == 403
20582060
20742076 description='helloworld',
20752077 severity='medium',
20762078 )
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)
20782080
20792081 data = {
20802082 'q': '{"filters":[{"name":"severity","op":"eq","val":"medium"}]}'
20812083 }
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)
20832085
20842086 assert res.status_code == 200
20852087 value = res.json['vulnerabilities'][0]['value']
20892091 data = {
20902092 "q": {"filters": [{"name": "severity", "op": "eq", "val": "medium"}]}
20912093 }
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)
20932095 assert res.status_code == 400
20942096
20952097 def test_vuln_filter_exception(self, test_client, workspace, session):
20992101 data = {
21002102 'q': '{"filters":[{"name":"severity","op":"eq","val":"medium"}]}'
21012103 }
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)
21032105 assert res.status_code == 200
21042106 assert res.json['count'] == 1
21052107
21222124 data = {
21232125 'q': '{"group_by":[{"field":"creator_id"}]}'
21242126 }
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)
21262128 assert res.status_code == 200
21272129 assert res.json['count'] == 1 # all vulns created by the same creator
21282130 expected = [{'count': 2, 'creator_id': creator.id}]
21472149 data = {
21482150 'q': '{"group_by":[{"field":"severity"}]}'
21492151 }
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)
21512153 assert res.status_code == 200, res.json
21522154 assert res.json['count'] == 1, res.json # all vulns created by the same creator
21532155 expected = {
21792181 data = {
21802182 'q': '{"group_by":[{"field":"severity"}, {"field": "name"}]}'
21812183 }
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)
21832185 assert res.status_code == 200, res.json
21842186 assert res.json['count'] == 2, res.json # all vulns created by the same creator
21852187 expected = {'vulnerabilities': [
22132215 data = {
22142216 'q': json.dumps({"group_by": [{"field": col_name}]})
22152217 }
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)
22172219 assert res.status_code == 200, res.json
22182220
22192221 def test_vuln_restless_group_same_name_description(self, test_client, session):
22472249 data = {
22482250 'q': '{"group_by":[{"field":"name"}, {"field":"description"}]}'
22492251 }
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)
22512253 assert res.status_code == 200
22522254 assert res.json['count'] == 2
22532255 expected = [{'count': 2, 'name': 'test', 'description': 'test'},
23132315 data = {
23142316 'q': json.dumps(query)
23152317 }
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)
23172319 assert res.status_code == 200
23182320 assert res.json['count'] == 12
23192321 expected_order = ['critical', 'critical', 'med', 'med', 'med', 'med', 'med', 'med', 'med', 'med', 'med', 'med']
23272329 data = {
23282330 'q': json.dumps({"filters": [{"name": "creator", "op": "eq", "val": vuln.creator.username}]})
23292331 }
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)
23312333 assert res.status_code == 200
23322334
23332335 def test_vuln_web_filter_exception(self, test_client, workspace, session):
23372339 data = {
23382340 'q': '{"filters":[{"name":"severity","op":"eq","val":"medium"}]}'
23392341 }
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)
23412343 assert res.status_code == 200
23422344 assert res.json['count'] == 1
23432345
23732375 session.commit()
23742376
23752377 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',
23772379 data={'csrf_token': csrf_token},
23782380 headers={'Content-Type': 'multipart/form-data'},
23792381 use_json_data=False)
23812383
23822384 def test_get_attachment_with_invalid_workspace_and_vuln(self, test_client):
23832385 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"
23852387 )
23862388 assert res.status_code == 404
23872389
23882390 def test_delete_attachment_with_invalid_workspace_and_vuln(self, test_client):
23892391 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"
23912393 )
23922394 assert res.status_code == 404
23932395
23962398 session.add(vuln)
23972399 session.commit()
23982400 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"
24002402 )
24012403 assert res.status_code == 404
24022404
24032405 def test_export_vuln_csv_empty_workspace(self, test_client, session):
24042406 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')
24062408 expected_headers = [
24072409 "confirmed", "id", "date", "name", "severity", "service",
24082410 "target", "desc", "status", "hostnames", "comments", "owner",
24252427 session.add(confirmed_vulns)
24262428 session.commit()
24272429 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 )
24302434 )
24312435 assert res.status_code == 200
24322436 assert self._verify_csv(res.data, confirmed=True)
24412445 workspace=workspace)
24422446 session.add(confirmed_vulns)
24432447 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'))
24452449 assert res.status_code == 200
24462450 assert self._verify_csv(res.data, confirmed=True)
24472451
24522456 session.add(confirmed_vulns)
24532457 session.commit()
24542458 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 )
24572463 )
24582464 assert res.status_code == 200
24592465 assert self._verify_csv(res.data, confirmed=True, severity='critical')
24642470 session.add(self.first_object)
24652471 session.commit()
24662472 res = test_client.get(
2467 self.check_url(urljoin(self.url(), 'export_csv/'))
2468 + '?confirmed=true'
2473 urljoin(self.url(), 'export_csv?confirmed=true')
24692474 )
24702475 assert res.status_code == 200
24712476 self._verify_csv(res.data, confirmed=True)
24902495 session.add(vuln)
24912496 session.commit()
24922497
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')
24942499 assert res.status_code == 200
24952500
24962501 csv_data = csv.DictReader(StringIO(res.data.decode('utf-8')), delimiter=',')
25302535 session.add(vuln)
25312536 session.commit()
25322537
2533 res = test_client.get(self.check_url(urljoin(self.url(), 'export_csv/')))
2538 res = test_client.get(urljoin(self.url(), 'export_csv'))
25342539 assert self._verify_csv(res.data)
25352540
25362541 def _verify_csv(self, raw_csv_data, confirmed=False, severity=None):
26022607 assert res.status_code == 200
26032608 assert res.json['tool'] == tool
26042609
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
26152610 def test_patch_with_attachments(self, test_client, session, workspace):
26162611 vuln = VulnerabilityFactory.create(workspace=workspace)
26172612 session.add(vuln)
26412636 api_endpoint = 'vulns'
26422637 view_class = VulnerabilityView
26432638 patchable_fields = ['description']
2644
2645 def check_url(self, url):
2646 return url
26472639
26482640 def test_create_vuln_with_custom_fields_shown(self, test_client, second_workspace, session):
26492641 host = HostFactory.create(workspace=self.workspace)
28102802 assert res.status_code == 400
28112803
28122804 @pytest.mark.usefixtures('ignore_nplusone')
2805 @pytest.mark.skip(reason="To be reimplemented")
28132806 def test_bulk_delete_vuln_id(self, host_with_hostnames, test_client, session):
28142807 """
28152808 This one should only check basic vuln properties
28412834 )
28422835 ws_name = host_with_hostnames.workspace.name
28432836 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)
28462839 vuln_1_id = res_1.json['obj_id']
28472840 vuln_2_id = res_2.json['obj_id']
28482841 vulns_to_delete = [vuln_1_id, vuln_2_id]
28492842 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)
28512844 vuln_count_after = session.query(Vulnerability).count()
28522845 deleted_vulns = delete_response.json['deleted_vulns']
28532846 assert delete_response.status_code == 200
28552848 assert deleted_vulns == len(vulns_to_delete)
28562849
28572850 @pytest.mark.usefixtures('ignore_nplusone')
2851 @pytest.mark.skip(reason="To be reimplemented")
28582852 def test_bulk_delete_vuln_severity(self, host_with_hostnames, test_client, session):
28592853 """
28602854 This one should only check basic vuln properties
28862880 )
28872881 ws_name = host_with_hostnames.workspace.name
28882882 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)
28912885 vuln_1_id = res_1.json['obj_id']
28922886 vuln_2_id = res_2.json['obj_id']
28932887 vulns_to_delete = [vuln_1_id, vuln_2_id]
28942888 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)
28962890 vuln_count_after = session.query(Vulnerability).count()
28972891 deleted_vulns = delete_response.json['deleted_vulns']
28982892 assert delete_response.status_code == 200
30303024 assert ref_example == get_response.json[ref_name][0]
30313025
30323026
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
30513027 @pytest.mark.usefixtures('logged_user')
30523028 class TestVulnerabilityCustomFields(ReadWriteAPITests):
30533029 model = Vulnerability
30673043 session.commit()
30683044
30693045
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
30773046 @pytest.mark.usefixtures('logged_user')
30783047 class TestVulnerabilitySearch:
3079
3080 def check_url(self, url):
3081 return url
30823048
30833049 @pytest.mark.skip_sql_dialect('sqlite')
30843050 def test_search_by_hostname_vulns(self, test_client, session):
30943060 [{"name": "hostnames", "op": "eq", "val": "pepe"}]
30953061 }
30963062 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)}'
30983064 )
30993065 assert res.status_code == 200
31003066 assert res.json['count'] == 1
31153081 [{"name": "hostnames", "op": "eq", "val": "pepe"}]
31163082 }
31173083 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)}'
31193085 )
31203086 assert res.status_code == 200
31213087 assert res.json['count'] == 1
31373103 [{"name": "hostnames", "op": "eq", "val": "pepe"}]
31383104 }
31393105 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)}'
31413107 )
31423108 assert res.status_code == 200
31433109 assert res.json['count'] == 1
31483114 []
31493115 }
31503116 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)}'
31523118 )
31533119 assert res.status_code == 200
31543120 assert res.json['count'] == 0
31583124 [{"name": "code", "op": "eq", "val": "test"}]
31593125 }
31603126 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)}'
31623128 )
31633129
31643130 assert res.status_code == 400, res.json
31773143 {"and": [{"name": "hostnames", "op": "eq", "val": "pepe"}]}
31783144 ]}
31793145 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)}'
31813147 )
31823148 assert res.status_code == 200
31833149 assert res.json['count'] == 1
32093175 "offset": offset * 10,
32103176 }
32113177 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)}'
32133179 )
32143180 assert res.status_code == 200
32153181 assert res.json['count'] == 20, query_filter
32393205 "offset": 10 * offset,
32403206 }
32413207 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)}'
32433209 )
32443210 assert res.status_code == 200
32453211 assert res.json['count'] == 100
32783244 "offset": offset,
32793245 }
32803246 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)}'
32823248 )
32833249 assert res.status_code == 200
32843250 assert res.json['count'] == 10
33173283 {"name": "host__os", "op": "has", "val": "Linux"}
33183284 ]}
33193285 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)}'
33213287 )
33223288 assert res.status_code == 200
33233289 assert res.json['count'] == 1
33653331 {"name": "create_date", "op": "eq", "val": vuln.create_date.strftime("%Y-%m-%d")}
33663332 ]}
33673333 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)}'
33693335 )
33703336 assert res.status_code == 200
33713337 assert res.json['count'] == 3
33803346 {"name": "create_date", "op": "eq", "val": "30/01/2020"}
33813347 ]}
33823348 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)}'
33843350 )
33853351 assert res.status_code == 200
33863352
33893355 query_filter = {'filters': [{'name': 'host_id', 'op': 'not_in',
33903356 'val': '\U0010a1a7\U00093553\U000eb46a\x1e\x10\r\x18%\U0005ddfa0\x05\U000fdeba\x08\x04絮'}]}
33913357 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)}'
33933359 )
33943360 assert res.status_code == 400
33953361
33973363 def test_search_hypothesis_test_found_case_2(self, test_client, session, workspace):
33983364 query_filter = {'filters': [{'name': 'host__os', 'op': 'ilike', 'val': -1915870387}]}
33993365 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)}'
34013367 )
34023368 assert res.status_code == 400
34033369
34093375 ])
34103376 def test_search_hypothesis_test_found_case_3(self, query_filter, test_client, session, workspace):
34113377 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)}'
34133379 )
34143380 assert res.status_code == 400
34153381
34213387 ])
34223388 def test_search_hypothesis_test_found_case_4(self, query_filter, test_client, session, workspace):
34233389 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)}'
34253391 )
34263392 assert res.status_code == 400
34273393
34333399 ])
34343400 def test_search_hypothesis_test_found_case_5(self, query_filter, test_client, session, workspace):
34353401 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)}'
34373403 )
34383404 assert res.status_code == 400
34393405
34413407 def test_search_hypothesis_test_found_case_6(self, test_client, session, workspace):
34423408 query_filter = {'filters': [{'name': 'resolution', 'op': '==', 'val': ''}]}
34433409 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)}'
34453411 )
34463412 assert res.status_code == 200
34473413
34513417 {'name': 'name', 'op': '>', 'val': '\U0004e755\U0007a789\U000e02d1\U000b3d32\x10\U000ad0e2,\x05\x1a'},
34523418 {'name': 'creator', 'op': 'eq', 'val': 21883}]}
34533419 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)}'
34553421 )
34563422 assert res.status_code == 400
34573423
34623428 ])
34633429 def test_search_hypothesis_test_found_case_7_valid(self, query_filter, test_client, session, workspace):
34643430 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)}'
34663432 )
34673433 assert res.status_code == 200
34683434
34703436 def test_search_hypothesis_test_found_case_8(self, test_client, session, workspace):
34713437 query_filter = {'filters': [{'name': 'hostnames', 'op': '==', 'val': ''}]}
34723438 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)}'
34743440 )
34753441 assert res.status_code == 200
34763442
34803446 'val': '0\x00\U00034383$\x13-\U000375fb\U0007add2\x01\x01\U0010c23a'}]}
34813447
34823448 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)}'
34843450 )
34853451 assert res.status_code == 400
34863452
34893455 query_filter = {'filters': [{'name': 'impact_integrity', 'op': 'neq', 'val': 0}]}
34903456
34913457 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)}'
34933459 )
34943460 assert res.status_code == 400
34953461
34983464 query_filter = {'filters': [{'name': 'host_id', 'op': 'like', 'val': '0'}]}
34993465
35003466 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)}'
35023468 )
35033469 assert res.status_code == 400
35043470
35073473 query_filter = {'filters': [{'name': 'custom_fields', 'op': 'like', 'val': ''}]}
35083474
35093475 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)}'
35113477 )
35123478 assert res.status_code == 400
35133479
35163482 query_filter = {'filters': [{'name': 'impact_accountability', 'op': 'ilike', 'val': '0'}]}
35173483
35183484 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)}'
35203486 )
35213487 assert res.status_code == 400
35223488
35333499 query_filter = {'filters': [{'name': 'severity', 'op': 'eq', 'val': 'high'}]}
35343500
35353501 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)}'
35373503 )
35383504 assert res.status_code == 200
35393505 assert res.json['count'] == 20
35563522 }
35573523
35583524 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)}'
35603526 )
35613527 assert res.status_code == 400
35623528
35813547 }
35823548
35833549 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)}'
35853551 )
35863552 assert res.status_code == 200
35873553 expected_order = sort_order["expected"]
36053571 }
36063572
36073573 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)}'
36093575 )
36103576 assert res.status_code == 200
36113577 expected_order = ['critical', 'high', 'med', 'low']
36463612 }
36473613
36483614 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)}'
36503616 )
36513617 assert res.status_code == 200
36523618 assert res.json['count'] == 100
3653
3654
3655 class TestVulnerabilitySearchV3(TestVulnerabilitySearch):
3656 def check_url(self, url):
3657 return v2_to_v3(url)
36583619
36593620
36603621 def test_type_filter(workspace, session,
38283789 @given(VulnerabilityData)
38293790 def send_api_create_request(raw_data):
38303791 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/',
38323793 data=raw_data)
38333794 assert res.status_code in [201, 400, 409]
38343795
38423803 @given(VulnerabilityDataWithId)
38433804 def send_api_update_request(raw_data):
38443805 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']}",
38463807 data=raw_data)
38473808 assert res.status_code in [200, 400, 409, 405]
38483809
39003861 def send_api_filter_request(raw_filter):
39013862 ws_name = host_with_hostnames.workspace.name
39023863 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}')
39043865 if res.status_code not in [200, 400]:
39053866 print(json.dumps(raw_filter))
39063867
1111 from faraday.server.api.modules.vulnerability_template import VulnerabilityTemplateView
1212 from tests import factories
1313 from tests.test_api_non_workspaced_base import (
14 ReadWriteAPITests, PatchableTestsMixin
14 ReadWriteAPITests
1515 )
1616 from faraday.server.models import (
1717 VulnerabilityTemplate,
2323 UserFactory,
2424 VulnerabilityFactory
2525 )
26 from tests.utils.url import v2_to_v3
2726
2827 TEMPLATES_DATA = [
2928 {'name': 'XML Injection (aka Blind XPath Injection) (Type: Base)',
5150 view_class = VulnerabilityTemplateView
5251 patchable_fields = ['description']
5352
54 def check_url(self, url):
55 return url
56
5753 def test_backwards_json_compatibility(self, test_client, session):
5854 self.factory.create()
5955 session.commit()
9288 def test_create_new_vulnerability_template(self, session, test_client):
9389 vuln_count_previous = session.query(VulnerabilityTemplate).count()
9490 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)
9692 assert res.status_code == 201
9793 assert isinstance(res.json['_id'], int)
9894 assert vuln_count_previous + 1 == session.query(VulnerabilityTemplate).count()
165161 ))
166162 session.commit()
167163
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"]}" }}]}}'
172166
173167 res = test_client.get(query)
174168 assert res.status_code == 200
203197 ))
204198 session.commit()
205199
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}" }}]}}'
210202
211203 res = test_client.get(query)
212204 assert res.status_code == 200
241233 ))
242234 session.commit()
243235
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"]}" }}]}}'
248238
249239 res = test_client.get(query)
250240 assert res.status_code == 200
256246 template = self.factory.create()
257247 session.commit()
258248 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)
260250 assert res.status_code == 200
261251 updated_template = session.query(VulnerabilityTemplate).filter_by(id=template.id).first()
262252 assert updated_template.name == raw_data['name']
278268 session.commit()
279269 raw_data = self._create_post_data_vulnerability_template(
280270 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)
282272 assert res.status_code == 400
283273
284274 def test_update_vulnerabiliy_template_change_refs(self, session, test_client):
288278 self.first_object.reference_template_instances.add(ref)
289279 session.commit()
290280 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)
292282 assert res.status_code == 200
293283 updated_template = session.query(VulnerabilityTemplate).filter_by(id=template.id).first()
294284 assert updated_template.name == raw_data['name']
300290 def test_create_new_vulnerability_template_with_references(self, session, test_client):
301291 vuln_count_previous = session.query(VulnerabilityTemplate).count()
302292 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)
304294 assert res.status_code == 201
305295 assert isinstance(res.json['_id'], int)
306296 assert set(res.json['refs']) == set(['ref1', 'ref2'])
311301 def test_delete_vuln_template(self, session, test_client):
312302 template = self.factory.create()
313303 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}')
315305
316306 assert res.status_code == 204
317307 assert vuln_count_previous - 1 == session.query(VulnerabilityTemplate).count()
439429 'csrf_token': csrf_token
440430 }
441431 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',
443433 data=data, headers=headers, use_json_data=False)
444434 assert res.status_code == 200
445435 assert len(res.json['vulns_created']) == expected_created_vuln_template
457447 'csrf_token': csrf_token
458448 }
459449 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',
461451 data=data, headers=headers, use_json_data=False)
462452 assert res.status_code == 200
463453 assert len(res.json['vulns_created']) == expected_created_vuln_template
475465 'csrf_token': csrf_token
476466 }
477467 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',
479469 data=data, headers=headers, use_json_data=False)
480470 assert res.status_code == 200
481471 assert len(res.json['vulns_created']) == expected_created_vuln_template
492482 'csrf_token': csrf_token
493483 }
494484 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',
496486 data=data, headers=headers, use_json_data=False)
497487 assert res.status_code == 400
498488 assert 'name' not in res.data.decode('utf8')
516506 'csrf_token': csrf_token
517507 }
518508 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',
520510 data=data, headers=headers, use_json_data=False)
521511 assert res.status_code == 200
522512 assert len(res.json['vulns_created']) == 1
538528 'vulns': [vuln_1, vuln_2]
539529 }
540530
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)
542532 assert res.status_code == 200
543533
544534 vulns_created = res.json['vulns_created']
556546 'vulns': [vuln_1, vuln_2]
557547 }
558548
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)
560550 assert res.status_code == 403
561551 assert res.json['message'] == 'Invalid CSRF token.'
562552
563553 def test_bulk_create_without_data(self, test_client, csrf_token):
564554 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)
566556
567557 assert res.status_code == 400
568558 assert res.json['message'] == 'Missing data to create vulnerabilities templates.'
583573 'vulns': [vuln_1, vuln_2]
584574 }
585575
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)
587577 assert res.status_code == 200
588578
589579 assert len(res.json['vulns_with_conflict']) == 1
611601 'vulns': [vuln_1, vuln_2]
612602 }
613603
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)
615605 assert res.status_code == 409
616606
617607 assert len(res.json['vulns_with_conflict']) == 2
619609 assert res.json['vulns_with_conflict'][1][1] == vuln_2['name']
620610
621611 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)
1010
1111 from faraday.server.models import Workspace, Scope
1212 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
1414 from tests import factories
15 from tests.utils.url import v2_to_v3
1615
1716
1817 class TestWorkspaceAPI(ReadWriteAPITests):
2221 lookup_field = 'name'
2322 view_class = WorkspaceView
2423 patchable_fields = ['name']
25
26 def check_url(self, url):
27 return url
2824
2925 @pytest.mark.usefixtures('ignore_nplusone')
3026 def test_filter_restless_by_name(self, test_client):
370366 workspace.active = False
371367 session.add(workspace)
372368 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))
377373 active = res.json.get('active')
378374 assert active
379375
384380 workspace.active = True
385381 session.add(workspace)
386382 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))
391387 active = res.json.get('active')
392388 assert not active
393389
403399 res = test_client.post(self.url(), data=raw_data)
404400 assert res.status_code == 400
405401 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
1515 from tests.test_api_pagination import PaginationTestsMixin as \
1616 OriginalPaginationTestsMixin
1717
18 API_PREFIX = '/v2/ws/'
18 API_PREFIX = '/v3/ws/'
1919 OBJECT_COUNT = 5
2020
2121
4949
5050 def url(self, obj=None, workspace=None):
5151 workspace = workspace or self.workspace
52 url = API_PREFIX + workspace.name + '/' + self.api_endpoint + '/'
52 url = API_PREFIX + workspace.name + '/' + self.api_endpoint
5353 if obj is not None:
5454 id_ = str(obj.id) if isinstance(
5555 obj, self.model) else str(obj)
56 url += id_ + u'/'
56 url += '/' + id_
5757 return url
5858
5959
178178 def control_cant_change_data(self, data: dict) -> dict:
179179 return data
180180
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"])
182186 def test_update_an_object(self, test_client, method):
183187 data = self.factory.build_dict(workspace=self.workspace)
184188 data = self.control_cant_change_data(data)
187191 res = test_client.put(self.url(self.first_object),
188192 data=data)
189193 elif method == "PATCH":
190 data = PatchableTestsMixin.control_data(self, data)
194 data = self.control_patcheable_data(self, data)
191195 res = test_client.patch(self.url(self.first_object), data=data)
192196 assert res.status_code == 200
193197 assert self.model.query.count() == count
195199 assert res.json[updated_field] == getattr(self.first_object,
196200 updated_field)
197201
198 @pytest.mark.parametrize("method", ["PUT"])
202 @pytest.mark.parametrize("method", ["PUT", "PATCH"])
199203 def test_update_an_object_readonly_fails(self, test_client, method):
200204 self.workspace.readonly = True
201205 db.session.commit()
212216 assert self.model.query.count() == OBJECT_COUNT
213217 assert old_field == getattr(self.model.query.filter(self.model.id == old_id).one(), unique_field)
214218
215 @pytest.mark.parametrize("method", ["PUT"])
219 @pytest.mark.parametrize("method", ["PUT", "PATCH"])
216220 def test_update_inactive_fails(self, test_client, method):
217221 self.workspace.deactivate()
218222 db.session.commit()
227231 assert res.status_code == 403
228232 assert self.model.query.count() == count
229233
230 @pytest.mark.parametrize("method", ["PUT"])
234 @pytest.mark.parametrize("method", ["PUT", "PATCH"])
231235 def test_update_fails_with_existing(self, test_client, session, method):
232236 for unique_field in self.unique_fields:
233237 unique_field_value = getattr(self.objects[1], unique_field)
243247 def test_update_an_object_fails_with_empty_dict(self, test_client):
244248 """To do this the user should use a PATCH request"""
245249 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"])
249258 def test_update_cant_change_id(self, test_client, method):
250259 raw_json = self.factory.build_dict(workspace=self.workspace)
251260 raw_json = self.control_cant_change_data(raw_json)
262271 assert object_id == expected_id
263272
264273
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
297274 class CountTestsMixin:
298275 def test_count(self, test_client, session, user_factory):
299276
310287
311288 session.commit()
312289
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"))
317291
318292 assert res.status_code == 200, res.json
319293 res = res.get_json()
343317
344318 session.commit()
345319
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"))
350321
351322 assert res.status_code == 200, res.json
352323 res = res.get_json()
1414 self.secret_key = faraday_server.secret_key
1515 self.websocket_port = faraday_server.websocket_port
1616
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
3017 def test_storage(self):
3118 from faraday.server.config import storage
3219 self.path = storage.path
312312 lambda workspace, test_client, session: SqlApi(workspace.name, test_client, session),
313313 ])
314314 @pytest.mark.usefixtures('ignore_nplusone')
315 @pytest.mark.skip("No available in community")
315316 def test_mail_notification(self, api, session, test_client):
316317 workspace = WorkspaceFactory.create()
317318 vuln = VulnerabilityFactory.create(workspace=workspace, severity='low')
856857 searcher = Searcher(api(workspace, test_client, session))
857858 rule_disabled: Rule = RuleFactory.create(disabled=True, workspace=workspace)
858859 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")]
861864
862865 action = ActionFactory.create(command='DELETE')
863866 session.add(action)
44 update_executors, BroadcastServerProtocol
55
66 from tests.factories import AgentFactory, ExecutorFactory
7 from tests.utils.url import v2_to_v3
87
98
109 class TransportMock:
2726
2827 class TestWebsocketBroadcastServerProtocol:
2928
30 def check_url(self, url):
31 return url
32
3329 def _join_agent(self, test_client, session):
3430 agent = AgentFactory.create(token='pepito')
3531 session.add(agent)
3632 session.commit()
3733
3834 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']
4036 return token
4137
4238 def test_join_agent_message_with_invalid_token_fails(self, session, proto, test_client):
7167 message = '{"action": "LEAVE_AGENT"}'
7268 assert proto.onMessage(message, False)
7369 assert not agent.is_online
74
75
76 class TestWebsocketBroadcastServerProtocolV3(TestWebsocketBroadcastServerProtocol):
77 def check_url(self, url):
78 return v2_to_v3(url)
7970
8071
8172 class TestCheckExecutors:
+0
-0
tests/utils/__init__.py less more
(Empty file)
+0
-4
tests/utils/url.py less more
0 def v2_to_v3(url):
1 if url.endswith("/"):
2 url = url[:-1]
3 return url.replace("/v2/", "/v3/", 1)