New upstream version 3.14.1
Sophie Brun
3 years ago
5 | 5 | - git config remote.github.url >/dev/null || git remote add github https://${GH_TOKEN}@github.com/infobyte/faraday.git |
6 | 6 | - export FARADAY_VERSION=$(eval $IMAGE_TAG) |
7 | 7 | - CHANGELOG/check_pre_tag.py |
8 | - git push github white/dev:master | |
8 | - git push github $CI_COMMIT_TAG:master | |
9 | 9 | - git tag v$FARADAY_VERSION -m "$(cat CHANGELOG/$FARADAY_VERSION/white.md)" |
10 | 10 | - git push github v$FARADAY_VERSION |
11 | - scripts/github_release.py | |
11 | - scripts/github_release.py --deb-file ./faraday-server_amd64.deb --rpm-file ./faraday-server_amd64.rpm | |
12 | 12 | rules: |
13 | 13 | - if: '$CI_COMMIT_TAG =~ /^white-v[0-9.]+$/' |
14 | 14 | when: on_success |
0 | Feb 17th, 2021 |
0 | * ADD forgot password | |
1 | * ADD update services by bulk_create | |
2 | * ADD FARADAY_DISABLE_LOGS varibale to disable logs to filesystem | |
3 | * ADD security logs in `audit.log` file | |
4 | * UPD security dependency Flask-Security-Too v3.4.4 | |
5 | * MOD rename total_rows field in filter host response | |
6 | * MOD improved Export cvs performance by reducing the number of queries | |
7 | * MOD sanitize the content of vulns' request and response | |
8 | * MOD dont strip new line in description when exporting csv | |
9 | * MOD improved threads management on exception | |
10 | * MOD improved performance on vulnerability filter | |
11 | * MOD improved [API documentation](www.api.faradaysec.com) | |
12 | * FIX upload a report with invalid custom fields | |
13 | * ADD v3 API, which includes: | |
14 | * All endpoints ends without `/` | |
15 | * `PATCH {model}/id` endpoints | |
16 | * Bulk update via PATCH `{model}` endpoints | |
17 | * Bulk delete via DELETE `{model}` endpoints | |
18 | * Endpoints removed: | |
19 | * `/v2/ws/<workspace_id>/activate/` | |
20 | * `/v2/ws/<workspace_id>/change_readonly/` | |
21 | * `/v2/ws/<workspace_id>/deactivate/` | |
22 | * `/v2/ws/<workspace_name>/hosts/bulk_delete/` | |
23 | * `/v2/ws/<workspace_name>/vulns/bulk_delete/` | |
24 | * Endpoints updated: | |
25 | * `/v2/ws/<workspace_name>/vulns/<int:vuln_id>/attachments/` => \ | |
26 | `/v3/ws/<workspace_name>/vulns/<int:vuln_id>/attachment` |
0 | 0 | New features in the latest update |
1 | 1 | ===================================== |
2 | 2 | |
3 | ||
4 | 3.14.1 [Feb 17th, 2021]: | |
5 | --- | |
6 | * ADD forgot password | |
7 | * ADD update services by bulk_create | |
8 | * ADD FARADAY_DISABLE_LOGS varibale to disable logs to filesystem | |
9 | * ADD security logs in `audit.log` file | |
10 | * UPD security dependency Flask-Security-Too v3.4.4 | |
11 | * MOD rename total_rows field in filter host response | |
12 | * MOD improved Export cvs performance by reducing the number of queries | |
13 | * MOD sanitize the content of vulns' request and response | |
14 | * MOD dont strip new line in description when exporting csv | |
15 | * MOD improved threads management on exception | |
16 | * MOD improved performance on vulnerability filter | |
17 | * MOD improved [API documentation](www.api.faradaysec.com) | |
18 | * FIX upload a report with invalid custom fields | |
19 | * ADD v3 API, which includes: | |
20 | * All endpoints ends without `/` | |
21 | * `PATCH {model}/id` endpoints | |
22 | * Bulk update via PATCH `{model}` endpoints | |
23 | * Bulk delete via DELETE `{model}` endpoints | |
24 | * Endpoints removed: | |
25 | * `/v2/ws/<workspace_id>/activate/` | |
26 | * `/v2/ws/<workspace_id>/change_readonly/` | |
27 | * `/v2/ws/<workspace_id>/deactivate/` | |
28 | * `/v2/ws/<workspace_name>/hosts/bulk_delete/` | |
29 | * `/v2/ws/<workspace_name>/vulns/bulk_delete/` | |
30 | * Endpoints updated: | |
31 | * `/v2/ws/<workspace_name>/vulns/<int:vuln_id>/attachments/` => \ | |
32 | `/v3/ws/<workspace_name>/vulns/<int:vuln_id>/attachment` | |
3 | 33 | |
4 | 34 | 3.14.0 [Dec 23th, 2020]: |
5 | 35 | --- |
0 | 0 | New features in the latest update |
1 | 1 | ===================================== |
2 | 2 | |
3 | ||
4 | 3.14.1 [Feb 17th, 2021]: | |
5 | --- | |
6 | * ADD forgot password | |
7 | * ADD update services by bulk_create | |
8 | * ADD FARADAY_DISABLE_LOGS varibale to disable logs to filesystem | |
9 | * ADD security logs in `audit.log` file | |
10 | * UPD security dependency Flask-Security-Too v3.4.4 | |
11 | * MOD rename total_rows field in filter host response | |
12 | * MOD improved Export cvs performance by reducing the number of queries | |
13 | * MOD sanitize the content of vulns' request and response | |
14 | * MOD dont strip new line in description when exporting csv | |
15 | * MOD improved threads management on exception | |
16 | * MOD improved performance on vulnerability filter | |
17 | * MOD improved [API documentation](www.api.faradaysec.com) | |
18 | * FIX upload a report with invalid custom fields | |
19 | * ADD v3 API, which includes: | |
20 | * All endpoints ends without `/` | |
21 | * `PATCH {model}/id` endpoints | |
22 | * Bulk update via PATCH `{model}` endpoints | |
23 | * Bulk delete via DELETE `{model}` endpoints | |
24 | * Endpoints removed: | |
25 | * `/v2/ws/<workspace_id>/activate/` | |
26 | * `/v2/ws/<workspace_id>/change_readonly/` | |
27 | * `/v2/ws/<workspace_id>/deactivate/` | |
28 | * `/v2/ws/<workspace_name>/hosts/bulk_delete/` | |
29 | * `/v2/ws/<workspace_name>/vulns/bulk_delete/` | |
30 | * Endpoints updated: | |
31 | * `/v2/ws/<workspace_name>/vulns/<int:vuln_id>/attachments/` => \ | |
32 | `/v3/ws/<workspace_name>/vulns/<int:vuln_id>/attachment` | |
3 | 33 | |
4 | 34 | 3.14.0 [Dec 23th, 2020]: |
5 | 35 | --- |
1 | 1 | # Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/) |
2 | 2 | # See the file 'doc/LICENSE' for the license information |
3 | 3 | |
4 | __version__ = '3.14.0' | |
4 | __version__ = '3.14.1' | |
5 | 5 | __license_version__ = __version__ |
0 | """Added delete on cascade to schedule | |
1 | ||
2 | Revision ID: 6471033046cb | |
3 | Revises: d3afdb4e0b11 | |
4 | Create Date: 2021-02-09 13:19:45.393841+00:00 | |
5 | ||
6 | """ | |
7 | from alembic import op | |
8 | import sqlalchemy as sa | |
9 | ||
10 | ||
11 | # revision identifiers, used by Alembic. | |
12 | revision = '6471033046cb' | |
13 | down_revision = 'd3afdb4e0b11' | |
14 | branch_labels = None | |
15 | depends_on = None | |
16 | ||
17 | ||
18 | def upgrade(): | |
19 | op.drop_constraint('executor_agent_id_fkey', 'executor') | |
20 | op.drop_constraint('agent_schedule_executor_id_fkey', 'agent_schedule') | |
21 | ||
22 | op.create_foreign_key( | |
23 | 'executor_agent_id_fkey', | |
24 | 'executor', | |
25 | 'agent', ['agent_id'], ['id'], | |
26 | ondelete='CASCADE' | |
27 | ) | |
28 | ||
29 | op.create_foreign_key( | |
30 | 'agent_schedule_executor_id_fkey', | |
31 | 'agent_schedule', | |
32 | 'executor', ['executor_id'], ['id'], | |
33 | ondelete='CASCADE' | |
34 | ) | |
35 | ||
36 | ||
37 | def downgrade(): | |
38 | op.drop_constraint('executor_agent_id_fkey', 'executor') | |
39 | op.drop_constraint('agent_schedule_executor_id_fkey', 'agent_schedule') | |
40 | ||
41 | op.create_foreign_key( | |
42 | 'executor_agent_id_fkey', | |
43 | 'executor', | |
44 | 'agent', ['agent_id'], ['id'] | |
45 | ) | |
46 | ||
47 | op.create_foreign_key( | |
48 | 'agent_schedule_executor_id_fkey', | |
49 | 'agent_schedule', | |
50 | 'executor', ['executor_id'], ['id'] | |
51 | ) |
0 | """add enum to comment model | |
1 | ||
2 | Revision ID: d3afdb4e0b11 | |
3 | Revises: 5658775e113f | |
4 | Create Date: 2021-02-01 14:43:49.849647+00:00 | |
5 | ||
6 | """ | |
7 | from alembic import op | |
8 | import sqlalchemy as sa | |
9 | ||
10 | ||
11 | # revision identifiers, used by Alembic. | |
12 | revision = 'd3afdb4e0b11' | |
13 | down_revision = '5658775e113f' | |
14 | branch_labels = None | |
15 | depends_on = None | |
16 | ||
17 | ||
18 | def upgrade(): | |
19 | op.execute("CREATE TYPE comment_types AS ENUM('system', 'user')") | |
20 | op.add_column('comment', sa.Column( | |
21 | 'comment_type', | |
22 | sa.Enum(('system', 'user'), name='comment_types'), | |
23 | nullable=False, | |
24 | server_default='user', | |
25 | default='user')) | |
26 | ||
27 | ||
28 | def downgrade(): | |
29 | op.drop_column('comment', 'comment_type') | |
30 | op.execute('DROP TYPE comment_types') |
120 | 120 | def login(self, username, password): |
121 | 121 | auth = {"email": username, "password": password} |
122 | 122 | try: |
123 | resp = self.requests.post(self.base + 'login', json=auth) | |
123 | resp = self.requests.post(self.base + 'login', data=auth, use_json_data=False) | |
124 | 124 | if resp.status_code not in [200, 302]: |
125 | 125 | logger.info("Invalid credentials") |
126 | 126 | return None |
672 | 672 | workspace, |
673 | 673 | severity_count=severity_count |
674 | 674 | ) |
675 | ||
675 | count = filter_query.count() | |
676 | 676 | if limit: |
677 | 677 | filter_query = filter_query.limit(limit) |
678 | 678 | if offset: |
679 | 679 | filter_query = filter_query.offset(offset) |
680 | count = filter_query.count() | |
681 | 680 | objs = self.schema_class(**marshmallow_params).dumps(filter_query.all()) |
682 | 681 | return json.loads(objs), count |
683 | 682 | else: |
1094 | 1093 | flask.request) |
1095 | 1094 | # just in case an schema allows id as writable. |
1096 | 1095 | data.pop('id', None) |
1097 | self._update_object(obj, data) | |
1096 | self._update_object(obj, data, partial=False) | |
1098 | 1097 | self._perform_update(object_id, obj, data, **kwargs) |
1099 | 1098 | |
1100 | 1099 | return self._dump(obj, kwargs), 200 |
1101 | 1100 | |
1102 | def _update_object(self, obj, data): | |
1101 | def _update_object(self, obj, data, **kwargs): | |
1103 | 1102 | """Perform changes in the selected object |
1104 | 1103 | |
1105 | 1104 | It modifies the attributes of the SQLAlchemy model to match |
1112 | 1111 | for (key, value) in data.items(): |
1113 | 1112 | setattr(obj, key, value) |
1114 | 1113 | |
1115 | def _perform_update(self, object_id, obj, data, workspace_name=None): | |
1114 | def _perform_update(self, object_id, obj, data, workspace_name=None, partial=False): | |
1116 | 1115 | """Commit the SQLAlchemy session, check for updating conflicts""" |
1117 | 1116 | try: |
1118 | 1117 | db.session.add(obj) |
1137 | 1136 | raise |
1138 | 1137 | return obj |
1139 | 1138 | |
1139 | ||
1140 | class PatchableMixin: | |
1141 | # TODO must be used with a UpdateMixin, when v2 be deprecated, add patch() to that Mixin | |
1142 | ||
1143 | def patch(self, object_id, **kwargs): | |
1144 | """ | |
1145 | --- | |
1146 | tags: ["{tag_name}"] | |
1147 | summary: Updates {class_model} | |
1148 | parameters: | |
1149 | - in: path | |
1150 | name: object_id | |
1151 | required: true | |
1152 | schema: | |
1153 | type: integer | |
1154 | requestBody: | |
1155 | required: true | |
1156 | content: | |
1157 | application/json: | |
1158 | schema: {schema_class} | |
1159 | responses: | |
1160 | 200: | |
1161 | description: Ok | |
1162 | content: | |
1163 | application/json: | |
1164 | schema: {schema_class} | |
1165 | 409: | |
1166 | description: Duplicated key found | |
1167 | content: | |
1168 | application/json: | |
1169 | schema: {schema_class} | |
1170 | """ | |
1171 | obj = self._get_object(object_id, **kwargs) | |
1172 | context = {'updating': True, 'object': obj} | |
1173 | data = self._parse_data(self._get_schema_instance(kwargs, context=context, partial=True), | |
1174 | flask.request) | |
1175 | # just in case an schema allows id as writable. | |
1176 | data.pop('id', None) | |
1177 | self._update_object(obj, data, partial=True) | |
1178 | self._perform_update(object_id, obj, data, partial=True, **kwargs) | |
1179 | ||
1180 | return self._dump(obj, kwargs), 200 | |
1140 | 1181 | |
1141 | 1182 | class UpdateWorkspacedMixin(UpdateMixin, CommandMixin): |
1142 | 1183 | """Add PUT /<workspace_name>/<route_base>/<id>/ route |
1181 | 1222 | """ |
1182 | 1223 | return super(UpdateWorkspacedMixin, self).put(object_id, workspace_name=workspace_name) |
1183 | 1224 | |
1184 | def _perform_update(self, object_id, obj, data, workspace_name=None): | |
1225 | def _perform_update(self, object_id, obj, data, workspace_name=None, partial=False): | |
1185 | 1226 | # # Make sure that if I created new objects, I had properly commited them |
1186 | 1227 | # assert not db.session.new |
1187 | 1228 | |
1191 | 1232 | self._set_command_id(obj, False) |
1192 | 1233 | return super(UpdateWorkspacedMixin, self)._perform_update( |
1193 | 1234 | object_id, obj, data, workspace_name) |
1235 | ||
1236 | ||
1237 | class PatchableWorkspacedMixin(PatchableMixin): | |
1238 | # TODO must be used with a UpdateWorkspacedMixin, when v2 be deprecated, add patch() to that Mixin | |
1239 | ||
1240 | def patch(self, object_id, workspace_name=None): | |
1241 | """ | |
1242 | --- | |
1243 | tags: ["{tag_name}"] | |
1244 | summary: Updates {class_model} | |
1245 | parameters: | |
1246 | - in: path | |
1247 | name: object_id | |
1248 | required: true | |
1249 | schema: | |
1250 | type: integer | |
1251 | - in: path | |
1252 | name: workspace_name | |
1253 | required: true | |
1254 | schema: | |
1255 | type: string | |
1256 | requestBody: | |
1257 | required: true | |
1258 | content: | |
1259 | application/json: | |
1260 | schema: {schema_class} | |
1261 | responses: | |
1262 | 200: | |
1263 | description: Ok | |
1264 | content: | |
1265 | application/json: | |
1266 | schema: {schema_class} | |
1267 | 409: | |
1268 | description: Duplicated key found | |
1269 | content: | |
1270 | application/json: | |
1271 | schema: {schema_class} | |
1272 | """ | |
1273 | return super(PatchableWorkspacedMixin, self).patch(object_id, workspace_name=workspace_name) | |
1194 | 1274 | |
1195 | 1275 | |
1196 | 1276 | class DeleteMixin: |
6 | 6 | from flask import Blueprint |
7 | 7 | from marshmallow import fields |
8 | 8 | |
9 | from faraday.server.api.base import AutoSchema, ReadWriteWorkspacedView, PaginatedMixin | |
9 | from faraday.server.api.base import AutoSchema, ReadWriteWorkspacedView, PaginatedMixin, PatchableWorkspacedMixin | |
10 | 10 | from faraday.server.models import Command |
11 | 11 | from faraday.server.schemas import PrimaryKeyRelatedField |
12 | 12 | |
89 | 89 | } |
90 | 90 | |
91 | 91 | |
92 | class ActivityFeedV3View(ActivityFeedView, PatchableWorkspacedMixin): | |
93 | route_prefix = '/v3/ws/<workspace_name>/' | |
94 | trailing_slash = False | |
95 | ||
96 | ||
92 | 97 | ActivityFeedView.register(activityfeed_api) |
98 | ActivityFeedV3View.register(activityfeed_api) |
18 | 18 | ReadOnlyView, |
19 | 19 | CreateMixin, |
20 | 20 | GenericView, |
21 | ReadOnlyMultiWorkspacedView | |
21 | ReadOnlyMultiWorkspacedView, | |
22 | PatchableMixin | |
22 | 23 | ) |
23 | 24 | from faraday.server.api.modules.workspaces import WorkspaceSchema |
24 | 25 | from faraday.server.models import Agent, Executor, AgentExecution, db, \ |
95 | 96 | 'token', |
96 | 97 | 'workspaces', |
97 | 98 | ) |
99 | ||
98 | 100 | |
99 | 101 | class AgentCreationView(CreateMixin, GenericView): |
100 | 102 | """ |
167 | 169 | return agent |
168 | 170 | |
169 | 171 | |
172 | class AgentCreationV3View(AgentCreationView): | |
173 | route_prefix = '/v3' | |
174 | trailing_slash = False | |
175 | ||
176 | ||
170 | 177 | class ExecutorDataSchema(Schema): |
171 | 178 | executor = fields.String(default=None) |
172 | 179 | args = fields.Dict(default=None) |
200 | 207 | except NoResultFound: |
201 | 208 | flask.abort(404, f"No such workspace: {workspace_name}") |
202 | 209 | |
203 | def _update_object(self, obj, data): | |
210 | def _update_object(self, obj, data, **kwargs): | |
204 | 211 | """Perform changes in the selected object |
205 | 212 | |
206 | 213 | It modifies the attributes of the SQLAlchemy model to match |
210 | 217 | with some specific field. Typically the new method should call |
211 | 218 | this one to handle the update of the rest of the fields. |
212 | 219 | """ |
213 | workspace_names = data.pop('workspaces') | |
214 | ||
215 | if len(workspace_names) == 0: | |
220 | workspace_names = data.pop('workspaces', '') | |
221 | partial = False if 'partial' not in kwargs else kwargs['partial'] | |
222 | ||
223 | if len(workspace_names) == 0 and not partial: | |
216 | 224 | abort( |
217 | 225 | make_response( |
218 | 226 | jsonify( |
240 | 248 | obj.workspaces = workspaces |
241 | 249 | |
242 | 250 | return obj |
251 | ||
252 | ||
253 | class AgentWithWorkspacesV3View(AgentWithWorkspacesView, PatchableMixin): | |
254 | route_prefix = '/v3' | |
255 | trailing_slash = False | |
243 | 256 | |
244 | 257 | |
245 | 258 | class AgentView(ReadOnlyMultiWorkspacedView): |
336 | 349 | }) |
337 | 350 | |
338 | 351 | |
352 | class AgentV3View(AgentView): | |
353 | route_prefix = '/v3/ws/<workspace_name>/' | |
354 | trailing_slash = False | |
355 | ||
356 | @route('/<int:agent_id>', methods=['DELETE']) | |
357 | def remove_workspace(self, workspace_name, agent_id): | |
358 | # This endpoint is not an exception for V3, overrides logic of DELETE | |
359 | return super(AgentV3View, self).remove_workspace(workspace_name, agent_id) | |
360 | ||
361 | @route('/<int:agent_id>/run', methods=['POST']) | |
362 | def run_agent(self, workspace_name, agent_id): | |
363 | return super(AgentV3View, self).run_agent(workspace_name, agent_id) | |
364 | ||
365 | remove_workspace.__doc__ = AgentView.remove_workspace.__doc__ | |
366 | run_agent.__doc__ = AgentView.run_agent.__doc__ | |
367 | ||
368 | ||
339 | 369 | AgentWithWorkspacesView.register(agent_api) |
370 | AgentWithWorkspacesV3View.register(agent_api) | |
340 | 371 | AgentCreationView.register(agent_api) |
372 | AgentCreationV3View.register(agent_api) | |
341 | 373 | AgentView.register(agent_api) |
374 | AgentV3View.register(agent_api) |
65 | 65 | {'token': faraday_server.agent_token}) |
66 | 66 | |
67 | 67 | |
68 | class AgentAuthTokenV3View(AgentAuthTokenView): | |
69 | route_prefix = '/v3' | |
70 | trailing_slash = False | |
71 | ||
68 | 72 | AgentAuthTokenView.register(agent_auth_token_api) |
73 | AgentAuthTokenV3View.register(agent_auth_token_api) | |
69 | 74 | |
70 | 75 | |
71 | 76 | # I'm Py3 |
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 | from __future__ import print_function | |
6 | from __future__ import absolute_import | |
7 | from builtins import range | |
8 | ||
9 | import flask | |
10 | from flask_login import current_user | |
11 | from marshmallow import Schema, fields | |
12 | ||
13 | from werkzeug.local import LocalProxy | |
14 | from werkzeug.datastructures import MultiDict | |
15 | from urllib.parse import urlparse | |
16 | import re | |
17 | import logging | |
18 | ||
19 | from flask import current_app as app | |
20 | from flask import abort, Blueprint, jsonify, g, request, make_response | |
21 | from flask_security.confirmable import requires_confirmation | |
22 | from flask_security.forms import LoginForm, ChangePasswordForm | |
23 | from flask_security.datastore import SQLAlchemyUserDatastore | |
24 | from flask_security.utils import ( | |
25 | get_message, | |
26 | get_identity_attributes, | |
27 | ) | |
28 | from flask_security.signals import password_reset, reset_password_instructions_sent | |
29 | from faraday.server import config | |
30 | ||
31 | from flask_security.recoverable import generate_reset_password_token, update_password | |
32 | from flask_security.views import anonymous_user_required | |
33 | from werkzeug.middleware.proxy_fix import ProxyFix | |
34 | #from flask_security.recoverable import _security | |
35 | from flask_security.utils import do_flash, send_mail, \ | |
36 | config_value, get_token_status, verify_hash | |
37 | from flask_security.forms import ResetPasswordForm | |
38 | ||
39 | from faraday.server.models import User | |
40 | _security = LocalProxy(lambda: app.extensions['security']) | |
41 | _datastore = LocalProxy(lambda: _security.datastore) | |
42 | ||
43 | auth = Blueprint('auth', __name__) | |
44 | logger = logging.getLogger(__name__) | |
45 | ||
46 | ||
47 | @auth.route('/auth/forgot_password', methods= ['POST']) | |
48 | @anonymous_user_required | |
49 | def forgot_password(): | |
50 | """ | |
51 | --- | |
52 | post: | |
53 | tags: ["User"] | |
54 | description: Send a token within an email to the user for password recovery | |
55 | responses: | |
56 | 200: | |
57 | description: Ok | |
58 | """ | |
59 | ||
60 | if not config.smtp.is_enabled(): | |
61 | logger.warning('Missing SMTP Config.') | |
62 | return make_response(flask.jsonify(response=dict(message="Operation not implemented"), success=False, code=501), 501) | |
63 | ||
64 | if 'email' not in request.json: | |
65 | return make_response(flask.jsonify(response=dict(message="Operation not allowed"), success=False, code=406),406) | |
66 | ||
67 | ||
68 | try: | |
69 | email = request.json.get('email') | |
70 | user = User.query.filter_by(email=email).first() | |
71 | if not user: | |
72 | return make_response(flask.jsonify(response=dict(email=email, message="Invalid Email"), success=False, code=400), 400) | |
73 | ||
74 | send_reset_password_instructions(user) | |
75 | return flask.jsonify(response=dict(email=email), success=True, code=200) | |
76 | except Exception as e: | |
77 | logger.exception(e) | |
78 | return make_response(flask.jsonify(response=dict(email=email, message="Server Error"), success=False, code=500), 500) | |
79 | ||
80 | ||
81 | @auth.route('/auth/reset_password/<token>', methods= ['POST']) | |
82 | @anonymous_user_required | |
83 | def reset_password(token): | |
84 | """ | |
85 | --- | |
86 | post: | |
87 | tags: ["User"] | |
88 | description: Reset the user's password based on the given token | |
89 | responses: | |
90 | 200: | |
91 | description: Ok | |
92 | """ | |
93 | if not config.smtp.is_enabled(): | |
94 | logger.warning('Missing SMTP Config.') | |
95 | return make_response(flask.jsonify(response=dict(message="Operation not implemented"), success=False, code=501), 501) | |
96 | ||
97 | try: | |
98 | if 'password' not in request.json or 'password_confirm' not in request.json: | |
99 | return make_response(flask.jsonify(response=dict(message="Invalid data provided"), success=False, code=406),406) | |
100 | ||
101 | expired, invalid, user = reset_password_token_status(token) | |
102 | ||
103 | if not user or invalid: | |
104 | invalid = True | |
105 | ||
106 | if invalid or expired: | |
107 | return make_response(flask.jsonify(response=dict(message="Invalid Token"), success=False, code=406),406) | |
108 | if request.is_json: | |
109 | form = ResetPasswordForm(MultiDict(request.get_json())) | |
110 | if form.validate_on_submit() and validate_strong_password(form.password.data, form.password_confirm.data): | |
111 | update_password(user, form.password.data) | |
112 | _datastore.commit() | |
113 | return flask.jsonify(response=dict(message="Password changed successfully"), success=True, code=200) | |
114 | ||
115 | return make_response(flask.jsonify(response=dict(message="Bad request"), success=False, code=400),400) | |
116 | ||
117 | except Exception as e: | |
118 | logger.exception(e) | |
119 | return make_response(flask.jsonify(response=dict(token=token, message="Server Error"), success=False, code=500), 500) | |
120 | ||
121 | ||
122 | def send_reset_password_instructions(user): | |
123 | """Sends the reset password instructions email for the specified user. | |
124 | :param user: The user to send the instructions to | |
125 | """ | |
126 | token = generate_reset_password_token(user) | |
127 | ||
128 | url_data = urlparse(request.base_url) | |
129 | reset_link = f"{url_data.scheme}://{url_data.netloc}/#resetpass/{token}/" | |
130 | ||
131 | if config_value('SEND_PASSWORD_RESET_EMAIL'): | |
132 | send_mail(config_value('EMAIL_SUBJECT_PASSWORD_RESET'), | |
133 | user.email, 'reset_instructions', | |
134 | user=user, reset_link=reset_link) | |
135 | ||
136 | reset_password_instructions_sent.send( | |
137 | app._get_current_object(), user=user, token=token | |
138 | ) | |
139 | ||
140 | ||
141 | def send_password_reset_notice(user): | |
142 | """Sends the password reset notice email for the specified user. | |
143 | :param user: The user to send the notice to | |
144 | """ | |
145 | if config_value('SEND_PASSWORD_RESET_NOTICE_EMAIL'): | |
146 | send_mail(config_value('EMAIL_SUBJECT_PASSWORD_NOTICE'), | |
147 | user.email, 'reset_notice', user=user) | |
148 | ||
149 | ||
150 | def reset_password_token_status(token): | |
151 | """Returns the expired status, invalid status, and user of a password reset | |
152 | token. For example:: | |
153 | expired, invalid, user, data = reset_password_token_status('...') | |
154 | :param token: The password reset token | |
155 | """ | |
156 | expired, invalid, user, data = get_token_status( | |
157 | token, 'reset', 'RESET_PASSWORD', return_data=True | |
158 | ) | |
159 | if not invalid and user: | |
160 | if user.password: | |
161 | if not verify_hash(data[1], user.password): | |
162 | invalid = True | |
163 | ||
164 | return expired, invalid, user | |
165 | ||
166 | ||
167 | def validate_strong_password(password: str, password_confirm: str): | |
168 | # Regex from faraday change password feature | |
169 | r = r'^.*(?=.{8,})(?=.*[a-z])(?=.*[A-Z])(?=.*[\d\W]).*$' | |
170 | is_valid = (password_confirm == password and re.match(r, password)) | |
171 | return is_valid | |
172 | ||
173 | ||
174 | forgot_password.is_public = True | |
175 | reset_password.is_public = True |
0 | 0 | import logging |
1 | 1 | from datetime import datetime, timedelta |
2 | 2 | from typing import Type, Optional |
3 | ||
3 | 4 | |
4 | 5 | import flask |
5 | 6 | import sqlalchemy |
44 | 45 | |
45 | 46 | logger = logging.getLogger(__name__) |
46 | 47 | |
48 | ||
47 | 49 | class VulnerabilitySchema(vulns.VulnerabilitySchema): |
48 | 50 | class Meta(vulns.VulnerabilitySchema.Meta): |
49 | 51 | extra_fields = ('run_date',) |
275 | 277 | db.session.commit() |
276 | 278 | |
277 | 279 | |
280 | def _update_service(service: Service, service_data: dict) -> Service: | |
281 | keys = ['version', 'description', 'name', 'status', 'owned'] | |
282 | updated = False | |
283 | ||
284 | for key in keys: | |
285 | if key == 'owned': | |
286 | value = service_data.get(key, False) | |
287 | else: | |
288 | value = service_data.get(key, '') | |
289 | if value != getattr(service, key): | |
290 | setattr(service, key, value) | |
291 | updated = True | |
292 | ||
293 | if updated: | |
294 | service.update_date = datetime.now() | |
295 | ||
296 | return service | |
297 | ||
298 | ||
278 | 299 | def _create_service(ws, host, service_data, command=None): |
279 | 300 | service_data = service_data.copy() |
280 | 301 | vulns = service_data.pop('vulnerabilities') |
281 | 302 | creds = service_data.pop('credentials') |
282 | 303 | service_data['host'] = host |
283 | 304 | (created, service) = get_or_create(ws, Service, service_data) |
305 | ||
306 | if not created: | |
307 | service = _update_service(service, service_data) | |
284 | 308 | db.session.commit() |
285 | 309 | |
286 | 310 | if command is not None: |
338 | 362 | if created and run_date: |
339 | 363 | logger.debug("Apply run date to vuln") |
340 | 364 | vuln.create_date = run_date |
365 | db.session.commit() | |
366 | elif not created and ("custom_fields" in vuln_data and any(vuln_data["custom_fields"])): | |
367 | # Updates Custom Fields | |
368 | vuln.custom_fields = vuln_data.pop('custom_fields', {}) | |
341 | 369 | db.session.commit() |
342 | 370 | |
343 | 371 | if command is not None: |
491 | 519 | |
492 | 520 | post.is_public = True |
493 | 521 | |
522 | ||
523 | class BulkCreateV3View(BulkCreateView): | |
524 | route_prefix = '/v3/ws/<workspace_name>/' | |
525 | trailing_slash = False | |
526 | ||
527 | ||
494 | 528 | BulkCreateView.register(bulk_create_api) |
529 | BulkCreateV3View.register(bulk_create_api) |
8 | 8 | from flask_classful import route |
9 | 9 | from marshmallow import fields, post_load, ValidationError |
10 | 10 | |
11 | from faraday.server.api.base import AutoSchema, ReadWriteWorkspacedView, PaginatedMixin | |
11 | from faraday.server.api.base import AutoSchema, ReadWriteWorkspacedView, PaginatedMixin, PatchableWorkspacedMixin | |
12 | 12 | from faraday.server.models import Command, Workspace |
13 | from faraday.server.schemas import MutableField, PrimaryKeyRelatedField | |
13 | from faraday.server.schemas import MutableField, PrimaryKeyRelatedField, SelfNestedField, MetadataSchema | |
14 | 14 | |
15 | 15 | commandsrun_api = Blueprint('commandsrun_api', __name__) |
16 | 16 | |
24 | 24 | allow_none=True) |
25 | 25 | workspace = PrimaryKeyRelatedField('name', dump_only=True) |
26 | 26 | creator = PrimaryKeyRelatedField('username', dump_only=True) |
27 | metadata = SelfNestedField(MetadataSchema()) | |
27 | 28 | |
28 | 29 | def load_itime(self, value): |
29 | 30 | try: |
55 | 56 | class Meta: |
56 | 57 | model = Command |
57 | 58 | fields = ('_id', 'command', 'duration', 'itime', 'ip', 'hostname', |
58 | 'params', 'user', 'creator', 'workspace', 'tool', 'import_source') | |
59 | 'params', 'user', 'creator', 'workspace', 'tool', 'import_source', 'metadata') | |
59 | 60 | |
60 | 61 | |
61 | 62 | class CommandView(PaginatedMixin, ReadWriteWorkspacedView): |
137 | 138 | return flask.jsonify(command_obj) |
138 | 139 | |
139 | 140 | |
141 | class CommandV3View(CommandView, PatchableWorkspacedMixin): | |
142 | route_prefix = '/v3/ws/<workspace_name>/' | |
143 | trailing_slash = False | |
144 | ||
145 | @route('/activity_feed') | |
146 | def activity_feed(self, workspace_name): | |
147 | return super(CommandV3View, self).activity_feed(workspace_name) | |
148 | ||
149 | @route('/last', methods=['GET']) | |
150 | def last_command(self, workspace_name): | |
151 | return super(CommandV3View, self).last_command(workspace_name) | |
152 | ||
153 | activity_feed.__doc__ = CommandView.activity_feed.__doc__ | |
154 | last_command.__doc__ = CommandView.last_command.__doc__ | |
155 | ||
156 | ||
140 | 157 | CommandView.register(commandsrun_api) |
158 | CommandV3View.register(commandsrun_api) |
5 | 5 | from marshmallow.validate import OneOf |
6 | 6 | |
7 | 7 | |
8 | from faraday.server.models import db, Host, Service | |
8 | from faraday.server.models import db, Host, Service, VulnerabilityGeneric | |
9 | 9 | from faraday.server.api.base import ( |
10 | 10 | AutoSchema, |
11 | 11 | ReadWriteWorkspacedView, |
12 | InvalidUsage, CreateWorkspacedMixin, GenericWorkspacedView) | |
12 | InvalidUsage, CreateWorkspacedMixin, GenericWorkspacedView, PatchableWorkspacedMixin) | |
13 | 13 | from faraday.server.models import Comment |
14 | 14 | comment_api = Blueprint('comment_api', __name__) |
15 | 15 | |
16 | 16 | |
17 | 17 | class CommentSchema(AutoSchema): |
18 | 18 | _id = fields.Integer(dump_only=True, attribute='id') |
19 | object_id = fields.Integer(attribute='object_id') | |
20 | object_type = fields.String(attribute='object_type', validate=OneOf(['host', 'service', 'comment'])) | |
19 | object_id = fields.Integer(attribute='object_id', required=True) | |
20 | object_type = fields.String(attribute='object_type', | |
21 | validate=OneOf(['host', 'service', 'comment', 'vulnerability']), | |
22 | required=True) | |
23 | text = fields.String(attribute='text', required=True) | |
21 | 24 | |
22 | 25 | class Meta: |
23 | 26 | model = Comment |
32 | 35 | model = { |
33 | 36 | 'host': Host, |
34 | 37 | 'service': Service, |
38 | 'vulnerability': VulnerabilityGeneric, | |
35 | 39 | 'comment': Comment |
36 | 40 | } |
37 | 41 | obj = db.session.query(model[data['object_type']]).get( |
49 | 53 | model_class = Comment |
50 | 54 | schema_class = CommentSchema |
51 | 55 | order_field = 'create_date' |
56 | ||
52 | 57 | |
53 | 58 | class UniqueCommentView(GenericWorkspacedView, CommentCreateMixing): |
54 | 59 | """ |
77 | 82 | res = super(UniqueCommentView, self)._perform_create(data, workspace_name) |
78 | 83 | return res |
79 | 84 | |
85 | ||
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 | ||
80 | 96 | CommentView.register(comment_api) |
81 | 97 | UniqueCommentView.register(comment_api) |
82 | # I'm Py3 | |
98 | CommentV3View.register(comment_api) | |
99 | UniqueCommentV3View.register(comment_api) |
9 | 9 | AutoSchema, |
10 | 10 | ReadWriteWorkspacedView, |
11 | 11 | FilterSetMeta, |
12 | FilterAlchemyMixin, InvalidUsage) | |
12 | FilterAlchemyMixin, | |
13 | InvalidUsage, | |
14 | PatchableWorkspacedMixin | |
15 | ) | |
13 | 16 | from faraday.server.models import Credential, Host, Service, Workspace, db |
14 | 17 | from faraday.server.schemas import MutableField, SelfNestedField, MetadataSchema |
15 | 18 | |
77 | 80 | parent_class = Service |
78 | 81 | parent_field = 'service_id' |
79 | 82 | not_parent_field = 'host_id' |
83 | elif 'partial' in kwargs and kwargs['partial']: | |
84 | return data | |
80 | 85 | else: |
81 | 86 | raise ValidationError( |
82 | 87 | f'Unknown parent type: {parent_type}') |
125 | 130 | } |
126 | 131 | |
127 | 132 | |
133 | class CredentialV3View(CredentialView, PatchableWorkspacedMixin): | |
134 | route_prefix = '/v3/ws/<workspace_name>/' | |
135 | trailing_slash = False | |
136 | ||
137 | ||
128 | 138 | CredentialView.register(credentials_api) |
129 | # I'm Py3 | |
139 | CredentialV3View.register(credentials_api) |
1 | 1 | # Copyright (C) 2018 Infobyte LLC (http://www.infobytesec.com/) |
2 | 2 | # See the file 'doc/LICENSE' for the license information |
3 | 3 | from flask import Blueprint |
4 | from marshmallow import fields | |
4 | 5 | |
5 | 6 | from faraday.server.models import CustomFieldsSchema |
6 | 7 | from faraday.server.api.base import ( |
7 | 8 | AutoSchema, |
8 | 9 | ReadWriteView, |
10 | PatchableMixin, | |
9 | 11 | ) |
10 | 12 | |
11 | 13 | |
13 | 15 | |
14 | 16 | |
15 | 17 | class CustomFieldsSchemaSchema(AutoSchema): |
18 | ||
19 | id = fields.Integer(dump_only=True, attribute='id') | |
20 | field_name = fields.String(attribute='field_name', required=True) | |
21 | field_type = fields.String(attribute='field_type', required=True) | |
22 | field_metadata = fields.String(attribute='field_metadata', allow_none=True) | |
23 | field_display_name = fields.String(attribute='field_display_name', required=True) | |
24 | field_order = fields.Integer(attribute='field_order', required=True) | |
25 | table_name = fields.String(attribute='table_name', required=True) | |
16 | 26 | |
17 | 27 | class Meta: |
18 | 28 | model = CustomFieldsSchema |
31 | 41 | model_class = CustomFieldsSchema |
32 | 42 | schema_class = CustomFieldsSchemaSchema |
33 | 43 | |
34 | def _update_object(self, obj, data): | |
44 | def _update_object(self, obj, data, **kwargs): | |
35 | 45 | """ |
36 | 46 | Field name must be read only |
37 | 47 | """ |
40 | 50 | data.pop(read_only_key) |
41 | 51 | return super(CustomFieldsSchemaView, self)._update_object(obj, data) |
42 | 52 | |
53 | ||
54 | class CustomFieldsSchemaV3View(CustomFieldsSchemaView, PatchableMixin): | |
55 | route_prefix = '/v3' | |
56 | trailing_slash = False | |
57 | ||
58 | ||
43 | 59 | CustomFieldsSchemaView.register(custom_fields_schema_api) |
44 | # I'm Py3⏎ | |
60 | CustomFieldsSchemaV3View.register(custom_fields_schema_api) |
13 | 13 | |
14 | 14 | @export_data_api.route('/v2/ws/<workspace_name>/export_data', methods=['GET']) |
15 | 15 | def export_data(workspace_name): |
16 | """ | |
17 | --- | |
18 | get: | |
19 | tags: ["File","Workspace"] | |
20 | description: Exports all the workspace data in a XML file | |
21 | responses: | |
22 | 200: | |
23 | description: Ok | |
24 | """ | |
25 | ||
16 | 26 | workspace = Workspace.query.filter_by(name=workspace_name).first() |
17 | 27 | if not workspace: |
18 | 28 | logger.error("No such workspace. Please, specify a valid workspace.") |
34 | 44 | else: |
35 | 45 | logger.error("Invalid format. Please, specify a valid format.") |
36 | 46 | abort(400, "Invalid format.") |
47 | ||
48 | ||
49 | @export_data_api.route('/v3/ws/<workspace_name>/export_data', methods=['GET']) | |
50 | def export_data_v3(workspace_name): | |
51 | return export_data(workspace_name) | |
52 | ||
53 | ||
54 | export_data_v3.__doc__ = export_data.__doc__ | |
37 | 55 | |
38 | 56 | |
39 | 57 | def xml_metasploit_format(workspace): |
15 | 15 | @exploits_api.route('/v2/vulners/exploits/<cveid>', methods=['GET']) |
16 | 16 | def get_exploits(cveid): |
17 | 17 | """ |
18 | Use Vulns API to get all exploits available for a specific CVE-ID | |
18 | --- | |
19 | get: | |
20 | tags: ["Vulnerability"] | |
21 | description: Use Vulns API to get all exploits available for a specific CVE-ID | |
22 | responses: | |
23 | 200: | |
24 | description: Ok | |
19 | 25 | """ |
20 | 26 | |
21 | 27 | logger.debug( |
66 | 72 | abort(make_response(jsonify(message=f'Could not find {str(ex)}'), 400)) |
67 | 73 | |
68 | 74 | return flask.jsonify(json_response) |
69 | # I'm Py3 | |
75 | ||
76 | ||
77 | @gzipped | |
78 | @exploits_api.route('/v3/vulners/exploits/<cveid>', methods=['GET']) | |
79 | def get_exploits_v3(cveid): | |
80 | """ | |
81 | --- | |
82 | get: | |
83 | tags: ["Vulnerability"] | |
84 | description: Use Vulns API to get all exploits available for a specific CVE-ID | |
85 | responses: | |
86 | 200: | |
87 | description: Ok | |
88 | """ | |
89 | get_exploits(cveid) |
23 | 23 | AutoSchema, |
24 | 24 | FilterAlchemyMixin, |
25 | 25 | FilterSetMeta, |
26 | FilterWorkspacedMixin) | |
26 | FilterWorkspacedMixin, | |
27 | PatchableWorkspacedMixin | |
28 | ) | |
27 | 29 | from faraday.server.schemas import ( |
28 | 30 | MetadataSchema, |
29 | 31 | MutableField, |
37 | 39 | host_api = Blueprint('host_api', __name__) |
38 | 40 | |
39 | 41 | logger = logging.getLogger(__name__) |
40 | ||
41 | 42 | |
42 | 43 | |
43 | 44 | class HostCountSchema(Schema): |
258 | 259 | logger.error("Error parsing hosts CSV (%s)", e) |
259 | 260 | abort(400, f"Error parsing hosts CSV ({e})") |
260 | 261 | |
261 | ||
262 | 262 | @route('/<host_id>/services/') |
263 | 263 | def service_list(self, workspace_name, host_id): |
264 | 264 | """ |
355 | 355 | db.session.commit() |
356 | 356 | return host |
357 | 357 | |
358 | def _update_object(self, obj, data): | |
358 | def _update_object(self, obj, data, **kwargs): | |
359 | 359 | try: |
360 | 360 | hostnames = data.pop('hostnames') |
361 | 361 | except KeyError: |
396 | 396 | }) |
397 | 397 | return { |
398 | 398 | 'rows': hosts, |
399 | 'total_rows': (pagination_metadata and pagination_metadata.total | |
399 | 'count': (pagination_metadata and pagination_metadata.total | |
400 | 400 | or len(hosts)), |
401 | 401 | } |
402 | 402 | |
439 | 439 | return flask.jsonify(response) |
440 | 440 | |
441 | 441 | |
442 | class HostsV3View(HostsView, PatchableWorkspacedMixin): | |
443 | route_prefix = '/v3/ws/<workspace_name>/' | |
444 | trailing_slash = False | |
445 | ||
446 | @route('/<host_id>/services') | |
447 | def service_list(self, workspace_name, host_id): | |
448 | return super(HostsV3View, self).service_list(workspace_name, host_id) | |
449 | ||
450 | @route('/<host_id>/tools_history') | |
451 | def tool_impacted_by_host(self, workspace_name, host_id): | |
452 | return super(HostsV3View, self).tool_impacted_by_host(workspace_name, host_id) | |
453 | ||
454 | @route('/bulk_create', methods=['POST']) | |
455 | def bulk_create(self, workspace_name): | |
456 | return super(HostsV3View, self).bulk_create(workspace_name) | |
457 | ||
458 | @route('/countVulns') | |
459 | def count_vulns(self, workspace_name): | |
460 | return super(HostsV3View, self).count_vulns() | |
461 | ||
462 | service_list.__doc__ = HostsView.service_list.__doc__ | |
463 | tool_impacted_by_host.__doc__ = HostsView.tool_impacted_by_host.__doc__ | |
464 | bulk_create.__doc__ = HostsView.bulk_create.__doc__ | |
465 | count_vulns.__doc__ = HostsView.count_vulns.__doc__ | |
466 | ||
442 | 467 | HostsView.register(host_api) |
443 | # I'm Py3 | |
468 | HostsV3View.register(host_api) |
12 | 12 | |
13 | 13 | @info_api.route('/v2/info', methods=['GET']) |
14 | 14 | def show_info(): |
15 | """ | |
16 | --- | |
17 | get: | |
18 | tags: ["Informational"] | |
19 | description: Gives basic info about the faraday service | |
20 | responses: | |
21 | 200: | |
22 | description: Ok | |
23 | """ | |
15 | 24 | |
16 | 25 | response = flask.jsonify({'Faraday Server': 'Running', 'Version': f_version}) |
17 | 26 | response.status_code = 200 |
19 | 28 | return response |
20 | 29 | |
21 | 30 | |
31 | @info_api.route('/v3/info', methods=['GET']) | |
32 | def show_info_v3(): | |
33 | return show_info() | |
34 | ||
35 | ||
36 | show_info_v3.__doc__ = show_info.__doc__ | |
37 | ||
38 | ||
22 | 39 | @info_api.route('/config') |
23 | 40 | def get_config(): |
41 | """ | |
42 | --- | |
43 | get: | |
44 | tags: ["Informational"] | |
45 | description: Gives basic info about the faraday configuration | |
46 | responses: | |
47 | 200: | |
48 | description: Ok | |
49 | """ | |
24 | 50 | return flask.jsonify(gen_web_config()) |
51 | ||
25 | 52 | |
26 | 53 | get_config.is_public = True |
27 | 54 | show_info.is_public = True |
28 | # I'm Py3⏎ | |
55 | show_info_v3.is_public = True |
7 | 7 | from faraday.server.api.base import ( |
8 | 8 | ReadWriteView, |
9 | 9 | AutoSchema, |
10 | PatchableMixin | |
10 | 11 | ) |
11 | 12 | from faraday.server.schemas import ( |
12 | 13 | StrictDateTimeField, |
35 | 36 | schema_class = LicenseSchema |
36 | 37 | |
37 | 38 | |
39 | class LicenseV3View(LicenseView, PatchableMixin): | |
40 | route_prefix = 'v3/' | |
41 | trailing_slash = False | |
42 | ||
43 | ||
38 | 44 | LicenseView.register(license_api) |
39 | # I'm Py3⏎ | |
45 | LicenseV3View.register(license_api) |
0 | 0 | from faraday.server.api.base import GenericView |
1 | 1 | from faraday.server.models import User, db |
2 | 2 | from flask import Blueprint, request, jsonify, g, abort |
3 | from marshmallow import Schema, fields | |
3 | 4 | |
4 | 5 | preferences_api = Blueprint('preferences_api', __name__) |
6 | ||
7 | ||
8 | class PreferenceSchema(Schema): | |
9 | preferences = fields.Dict() | |
5 | 10 | |
6 | 11 | |
7 | 12 | class PreferencesView(GenericView): |
8 | 13 | model_class = User |
9 | 14 | route_base = 'preferences' |
15 | schema_class = PreferenceSchema | |
10 | 16 | |
11 | 17 | def post(self): |
18 | """ | |
19 | --- | |
20 | set: | |
21 | tags: ["User"] | |
22 | description: Set the user preferences | |
23 | responses: | |
24 | 200: | |
25 | description: Ok | |
26 | """ | |
12 | 27 | user = g.user |
13 | 28 | |
14 | 29 | if request.json and 'preferences' not in request.json: |
22 | 37 | return jsonify(''), 200 |
23 | 38 | |
24 | 39 | def get(self): |
40 | """ | |
41 | --- | |
42 | get: | |
43 | tags: ["User"] | |
44 | description: Show the user preferences | |
45 | responses: | |
46 | 200: | |
47 | description: Ok | |
48 | """ | |
25 | 49 | return jsonify({'preferences': g.user.preferences}), 200 |
26 | 50 | |
51 | ||
52 | class PreferencesV3View(PreferencesView): | |
53 | route_prefix = '/v3' | |
54 | trailing_slash = False | |
55 | ||
56 | ||
27 | 57 | PreferencesView.register(preferences_api) |
58 | PreferencesV3View.register(preferences_api) |
7 | 7 | from faraday.server.api.base import ( |
8 | 8 | ReadWriteView, |
9 | 9 | AutoSchema, |
10 | PatchableMixin, | |
10 | 11 | ) |
11 | 12 | |
12 | 13 | searchfilter_api = Blueprint('searchfilter_api', __name__) |
32 | 33 | return query.filter(SearchFilter.creator_id == g.user.id) |
33 | 34 | |
34 | 35 | |
36 | class SearchFilterV3View(SearchFilterView, PatchableMixin): | |
37 | route_prefix = 'v3/' | |
38 | trailing_slash = False | |
39 | ||
40 | ||
35 | 41 | SearchFilterView.register(searchfilter_api) |
36 | # I'm Py3⏎ | |
42 | SearchFilterV3View.register(searchfilter_api) |
7 | 7 | from sqlalchemy.orm.exc import NoResultFound |
8 | 8 | |
9 | 9 | from faraday.server.api.base import AutoSchema, ReadWriteWorkspacedView, FilterSetMeta, \ |
10 | FilterAlchemyMixin | |
10 | FilterAlchemyMixin, PatchableWorkspacedMixin | |
11 | 11 | from faraday.server.models import Host, Service, Workspace |
12 | 12 | from faraday.server.schemas import ( |
13 | 13 | MetadataSchema, |
133 | 133 | abort(make_response(jsonify(message="Invalid Port number"), 400)) |
134 | 134 | return super(ServiceView, self)._perform_create(data, **kwargs) |
135 | 135 | |
136 | ||
137 | class ServiceV3View(ServiceView, PatchableWorkspacedMixin): | |
138 | route_prefix = '/v3/ws/<workspace_name>/' | |
139 | trailing_slash = False | |
140 | ||
136 | 141 | ServiceView.register(services_api) |
137 | # I'm Py3 | |
142 | ServiceV3View.register(services_api) |
12 | 12 | |
13 | 13 | @session_api.route('/session') |
14 | 14 | def session_info(): |
15 | """ | |
16 | --- | |
17 | get: | |
18 | tags: ["Informational"] | |
19 | description: Gives info about the current session | |
20 | responses: | |
21 | 200: | |
22 | description: Ok | |
23 | """ | |
15 | 24 | user = flask.g.user |
16 | 25 | data = user.get_security_payload() |
17 | 26 | data['csrf_token'] = generate_csrf() |
0 | import datetime | |
1 | import logging | |
2 | ||
0 | 3 | from itsdangerous import TimedJSONWebSignatureSerializer |
1 | from flask import Blueprint, g | |
4 | from flask import Blueprint, g, request | |
2 | 5 | from flask_security.utils import hash_data |
3 | 6 | from flask import current_app as app |
4 | ||
7 | from marshmallow import Schema | |
5 | 8 | |
6 | 9 | from faraday.server.config import faraday_server |
7 | 10 | from faraday.server.api.base import GenericView |
8 | 11 | |
9 | 12 | token_api = Blueprint('token_api', __name__) |
10 | 13 | |
14 | audit_logger = logging.getLogger('audit') | |
15 | ||
16 | ||
17 | class EmptySchema(Schema): | |
18 | pass | |
19 | ||
11 | 20 | |
12 | 21 | class TokenAuthView(GenericView): |
13 | 22 | route_base = 'token' |
23 | schema_class = EmptySchema | |
14 | 24 | |
15 | 25 | def get(self): |
26 | """ | |
27 | --- | |
28 | get: | |
29 | tags: ["Token"] | |
30 | description: Gets a new user token | |
31 | responses: | |
32 | 200: | |
33 | description: Ok | |
34 | """ | |
16 | 35 | user_id = g.user.id |
17 | 36 | serializer = TimedJSONWebSignatureSerializer( |
18 | 37 | app.config['SECRET_KEY'], |
20 | 39 | expires_in=int(faraday_server.api_token_expiration) |
21 | 40 | ) |
22 | 41 | hashed_data = hash_data(g.user.password) if g.user.password else None |
42 | user_ip = request.headers.get('X-Forwarded-For', request.remote_addr) | |
43 | requested_at = datetime.datetime.now() | |
44 | audit_logger.info(f"User [{g.user.username}] requested token from IP [{user_ip}] at [{requested_at}]") | |
23 | 45 | return serializer.dumps({'user_id': user_id, "validation_check": hashed_data}).decode('utf-8') |
24 | 46 | |
25 | 47 | |
48 | class TokenAuthV3View(TokenAuthView): | |
49 | route_prefix = '/v3' | |
50 | trailing_slash = False | |
51 | ||
52 | ||
26 | 53 | TokenAuthView.register(token_api) |
54 | TokenAuthV3View.register(token_api) |
33 | 33 | plugins_manager = PluginsManager(config.faraday_server.custom_plugins_folder) |
34 | 34 | report_analyzer = ReportAnalyzer(plugins_manager) |
35 | 35 | |
36 | ||
36 | 37 | @gzipped |
37 | 38 | @upload_api.route('/v2/ws/<workspace>/upload_report', methods=['POST']) |
38 | 39 | def file_upload(workspace=None): |
39 | 40 | """ |
40 | Upload a report file to Server and process that report with Faraday client plugins. | |
41 | --- | |
42 | post: | |
43 | tags: ["Workspace", "File"] | |
44 | description: Upload a report file to create data within the given workspace | |
45 | responses: | |
46 | 201: | |
47 | description: Created | |
48 | 400: | |
49 | description: Bad request | |
50 | 403: | |
51 | description: Forbidden | |
52 | tags: ["Workspace", "File"] | |
53 | responses: | |
54 | 200: | |
55 | description: Ok | |
41 | 56 | """ |
42 | 57 | logger.info("Importing new plugin report in server...") |
43 | 58 | # Authorization code copy-pasted from server/api/base.py |
109 | 124 | ) |
110 | 125 | else: |
111 | 126 | abort(make_response(jsonify(message="Missing report file"), 400)) |
127 | ||
128 | ||
129 | @gzipped | |
130 | @upload_api.route('/v3/ws/<workspace>/upload_report', methods=['POST']) | |
131 | def file_upload_v3(workspace=None): | |
132 | """ | |
133 | --- | |
134 | post: | |
135 | tags: ["Workspace", "File"] | |
136 | description: Upload a report file to create data within the given workspace | |
137 | responses: | |
138 | 201: | |
139 | description: Created | |
140 | 400: | |
141 | description: Bad request | |
142 | 403: | |
143 | description: Forbidden | |
144 | tags: ["Workspace", "File"] | |
145 | responses: | |
146 | 200: | |
147 | description: Ok | |
148 | """ | |
149 | return file_upload(workspace) |
27 | 27 | FilterSetMeta, |
28 | 28 | PaginatedMixin, |
29 | 29 | ReadWriteView, |
30 | FilterMixin) | |
30 | FilterMixin, | |
31 | PatchableMixin | |
32 | ) | |
31 | 33 | |
32 | 34 | from faraday.server.schemas import ( |
33 | 35 | PrimaryKeyRelatedField, |
156 | 158 | } |
157 | 159 | |
158 | 160 | def post(self, **kwargs): |
161 | """ | |
162 | --- | |
163 | post: | |
164 | tags: ["VulnerabilityTemplate"] | |
165 | summary: Creates VulnerabilityTemplate | |
166 | requestBody: | |
167 | required: true | |
168 | content: | |
169 | application/json: | |
170 | schema: VulnerabilityTemplateSchema | |
171 | responses: | |
172 | 201: | |
173 | description: Created | |
174 | content: | |
175 | application/json: | |
176 | schema: VulnerabilityTemplateSchema | |
177 | 409: | |
178 | description: Duplicated key found | |
179 | content: | |
180 | application/json: | |
181 | schema: VulnerabilityTemplateSchema | |
182 | """ | |
159 | 183 | with lock: |
160 | 184 | return super(VulnerabilityTemplateView, self).post(**kwargs) |
161 | 185 | |
297 | 321 | return vulns_list |
298 | 322 | |
299 | 323 | |
324 | class VulnerabilityTemplateV3View(VulnerabilityTemplateView, PatchableMixin): | |
325 | route_prefix = 'v3/' | |
326 | trailing_slash = False | |
327 | ||
328 | @route('/bulk_create', methods=['POST']) | |
329 | def bulk_create(self): | |
330 | return super(VulnerabilityTemplateV3View, self).bulk_create() | |
331 | ||
332 | bulk_create.__doc__ = VulnerabilityTemplateView.bulk_create.__doc__ | |
333 | ||
334 | ||
300 | 335 | VulnerabilityTemplateView.register(vulnerability_template_api) |
336 | VulnerabilityTemplateV3View.register(vulnerability_template_api) |
34 | 34 | PaginatedMixin, |
35 | 35 | ReadWriteWorkspacedView, |
36 | 36 | InvalidUsage, |
37 | CountMultiWorkspacedMixin) | |
37 | CountMultiWorkspacedMixin, | |
38 | PatchableWorkspacedMixin | |
39 | ) | |
38 | 40 | from faraday.server.fields import FaradayUploadedFile |
39 | 41 | from faraday.server.models import ( |
40 | 42 | db, |
46 | 48 | Vulnerability, |
47 | 49 | VulnerabilityWeb, |
48 | 50 | CustomFieldsSchema, |
49 | VulnerabilityGeneric, User, | |
51 | VulnerabilityGeneric, | |
52 | User | |
50 | 53 | ) |
51 | 54 | from faraday.server.utils.database import get_or_create |
52 | 55 | from faraday.server.utils.export import export_vulns_to_csv |
454 | 457 | sort_model_class = VulnerabilityWeb # It has all the fields |
455 | 458 | sort_pass_silently = True # For compatibility with the Web UI |
456 | 459 | order_field = desc(VulnerabilityGeneric.confirmed), VulnerabilityGeneric.severity, VulnerabilityGeneric.create_date |
460 | get_joinedloads = [Vulnerability.evidence, Vulnerability.creator] | |
457 | 461 | |
458 | 462 | unique_fields_by_class = { |
459 | 463 | 'Vulnerability': [('name', 'description', 'host_id', 'service_id')], |
510 | 514 | return obj |
511 | 515 | |
512 | 516 | def _process_attachments(self, obj, attachments): |
513 | old_attachments = db.session.query(File).filter_by( | |
517 | old_attachments = db.session.query(File).options( | |
518 | joinedload(File.creator), | |
519 | joinedload(File.update_user) | |
520 | ).filter_by( | |
514 | 521 | object_id=obj.id, |
515 | 522 | object_type='vulnerability', |
516 | 523 | ) |
528 | 535 | content=faraday_file, |
529 | 536 | ) |
530 | 537 | |
531 | def _update_object(self, obj, data): | |
532 | data.pop('type') # It's forbidden to change vuln type! | |
538 | def _update_object(self, obj, data, **kwargs): | |
539 | data.pop('type', '') # It's forbidden to change vuln type! | |
533 | 540 | data.pop('tool', '') |
534 | 541 | return super(VulnerabilityView, self)._update_object(obj, data) |
535 | 542 | |
536 | def _perform_update(self, object_id, obj, data, workspace_name): | |
537 | attachments = data.pop('_attachments', {}) | |
543 | def _perform_update(self, object_id, obj, data, workspace_name=None, partial=False): | |
544 | attachments = data.pop('_attachments', None if partial else {}) | |
538 | 545 | obj = super(VulnerabilityView, self)._perform_update(object_id, obj, data, workspace_name) |
539 | 546 | db.session.flush() |
540 | self._process_attachments(obj, attachments) | |
547 | if attachments is not None: | |
548 | self._process_attachments(obj, attachments) | |
541 | 549 | db.session.commit() |
542 | 550 | return obj |
543 | 551 | |
836 | 844 | if offset: |
837 | 845 | vulns = vulns.offset(offset) |
838 | 846 | |
839 | vulns = self.schema_class_dict['VulnerabilityWeb'](**marshmallow_params).dumps( | |
847 | vulns = self.schema_class_dict['VulnerabilityWeb'](**marshmallow_params).dump( | |
840 | 848 | vulns.all()) |
841 | return json.loads(vulns), total_vulns.count() | |
849 | return vulns, total_vulns.count() | |
842 | 850 | else: |
843 | 851 | vulns = self._generate_filter_query( |
844 | 852 | VulnerabilityGeneric, |
1080 | 1088 | return flask.jsonify(response) |
1081 | 1089 | |
1082 | 1090 | |
1091 | class VulnerabilityV3View(VulnerabilityView, PatchableWorkspacedMixin): | |
1092 | route_prefix = '/v3/ws/<workspace_name>/' | |
1093 | trailing_slash = False | |
1094 | ||
1095 | @route('/<int:vuln_id>/attachment', methods=['POST']) | |
1096 | def post_attachment(self, workspace_name, vuln_id): | |
1097 | return super(VulnerabilityV3View, self).post_attachment(workspace_name, vuln_id) | |
1098 | ||
1099 | @route('/<int:vuln_id>/attachment/<attachment_filename>', methods=['GET']) | |
1100 | def get_attachment(self, workspace_name, vuln_id, attachment_filename): | |
1101 | return super(VulnerabilityV3View, self).get_attachment(workspace_name, vuln_id, attachment_filename) | |
1102 | ||
1103 | @route('/<int:vuln_id>/attachment', methods=['GET']) | |
1104 | def get_attachments_by_vuln(self, workspace_name, vuln_id): | |
1105 | return super(VulnerabilityV3View, self).get_attachments_by_vuln(workspace_name, vuln_id) | |
1106 | ||
1107 | @route('/<int:vuln_id>/attachment/<attachment_filename>', methods=['DELETE']) | |
1108 | def delete_attachment(self, workspace_name, vuln_id, attachment_filename): | |
1109 | return super(VulnerabilityV3View, self).delete_attachment(workspace_name, vuln_id, attachment_filename) | |
1110 | ||
1111 | @route('/export_csv', methods=['GET']) | |
1112 | def export_csv(self, workspace_name): | |
1113 | return super(VulnerabilityV3View, self).export_csv(workspace_name) | |
1114 | ||
1115 | @route('/top_users', methods=['GET']) | |
1116 | def top_users(self, workspace_name): | |
1117 | return super(VulnerabilityV3View, self).top_users(workspace_name) | |
1118 | ||
1119 | post_attachment.__doc__ = VulnerabilityView.post_attachment.__doc__ | |
1120 | get_attachment.__doc__ = VulnerabilityView.post_attachment.__doc__ | |
1121 | get_attachments_by_vuln.__doc__ = VulnerabilityView.post_attachment.__doc__ | |
1122 | delete_attachment.__doc__ = VulnerabilityView.post_attachment.__doc__ | |
1123 | export_csv.__doc__ = VulnerabilityView.post_attachment.__doc__ | |
1124 | top_users.__doc__ = VulnerabilityView.post_attachment.__doc__ | |
1125 | ||
1126 | ||
1083 | 1127 | VulnerabilityView.register(vulns_api) |
1084 | ||
1085 | # I'm Py3 | |
1128 | VulnerabilityV3View.register(vulns_api) |
5 | 5 | from flask import Blueprint |
6 | 6 | from flask import current_app as app |
7 | 7 | from itsdangerous import BadData, TimestampSigner |
8 | from marshmallow import Schema | |
8 | 9 | from sqlalchemy.orm.exc import NoResultFound |
9 | 10 | from faraday.server.models import Agent |
10 | 11 | from faraday.server.api.base import GenericWorkspacedView |
15 | 16 | websocket_auth_api = Blueprint('websocket_auth_api', __name__) |
16 | 17 | |
17 | 18 | |
19 | class WebsocketWorkspaceAuthSchema(Schema): | |
20 | pass | |
21 | ||
22 | ||
18 | 23 | class WebsocketWorkspaceAuthView(GenericWorkspacedView): |
19 | 24 | route_base = 'websocket_token' |
25 | schema_class = WebsocketWorkspaceAuthSchema | |
20 | 26 | |
21 | 27 | def post(self, workspace_name): |
28 | """ | |
29 | --- | |
30 | post: | |
31 | tags: ["Token"] | |
32 | responses: | |
33 | 200: | |
34 | description: Ok | |
35 | """ | |
22 | 36 | workspace = self._get_workspace(workspace_name) |
23 | 37 | signer = TimestampSigner(app.config['SECRET_KEY'], salt="websocket") |
24 | 38 | token = signer.sign(str(workspace.id)).decode('utf-8') |
25 | 39 | return {"token": token} |
26 | 40 | |
27 | 41 | |
42 | class WebsocketWorkspaceAuthV3View(WebsocketWorkspaceAuthView): | |
43 | route_prefix = "/v3/ws/<workspace_name>/" | |
44 | trailing_slash = False | |
45 | ||
46 | ||
28 | 47 | WebsocketWorkspaceAuthView.register(websocket_auth_api) |
48 | WebsocketWorkspaceAuthV3View.register(websocket_auth_api) | |
29 | 49 | |
30 | 50 | |
31 | 51 | @websocket_auth_api.route('/v2/agent_websocket_token/', methods=['POST']) |
32 | 52 | def agent_websocket_token(): |
53 | """ | |
54 | --- | |
55 | post: | |
56 | tags: ["Token", "Agent"] | |
57 | description: Gives a token to establish a websocket connection. For agents logic only | |
58 | responses: | |
59 | 200: | |
60 | description: Ok | |
61 | """ | |
33 | 62 | agent = require_agent_token() |
34 | 63 | return flask.jsonify({"token": generate_agent_websocket_token(agent)}) |
35 | 64 | |
36 | 65 | |
66 | @websocket_auth_api.route('/v3/agent_websocket_token', methods=['POST']) | |
67 | def agent_websocket_token_w3(): | |
68 | return agent_websocket_token() | |
69 | ||
70 | ||
71 | agent_websocket_token_w3.__doc__ = agent_websocket_token.__doc__ | |
72 | ||
73 | ||
37 | 74 | agent_websocket_token.is_public = True |
75 | agent_websocket_token_w3.is_public = True | |
38 | 76 | |
39 | 77 | |
40 | 78 | def generate_agent_websocket_token(agent): |
77 | 115 | except NoResultFound: |
78 | 116 | flask.abort(403) |
79 | 117 | return agent |
80 | ||
81 | # I'm Py3 |
23 | 23 | PrimaryKeyRelatedField, |
24 | 24 | SelfNestedField, |
25 | 25 | ) |
26 | from faraday.server.api.base import ReadWriteView, AutoSchema, FilterMixin | |
26 | from faraday.server.api.base import ReadWriteView, AutoSchema, FilterMixin, PatchableMixin | |
27 | 27 | |
28 | 28 | logger = logging.getLogger(__name__) |
29 | 29 | |
76 | 76 | PrimaryKeyRelatedField('name', many=True, dump_only=True), |
77 | 77 | fields.List(fields.String) |
78 | 78 | ) |
79 | active = fields.Boolean(dump_only=True) | |
79 | active = fields.Boolean() | |
80 | 80 | |
81 | 81 | create_date = fields.DateTime(attribute='create_date', |
82 | 82 | dump_only=True) |
268 | 268 | db.session.commit() |
269 | 269 | return workspace |
270 | 270 | |
271 | def _update_object(self, obj, data): | |
271 | def _update_object(self, obj, data, **kwargs): | |
272 | 272 | scope = data.pop('scope', []) |
273 | 273 | obj.set_scope(scope) |
274 | 274 | return super(WorkspaceView, self)._update_object(obj, data) |
338 | 338 | return self._get_object(workspace_id).readonly |
339 | 339 | |
340 | 340 | |
341 | class WorkspaceV3View(WorkspaceView, PatchableMixin): | |
342 | route_prefix = 'v3/' | |
343 | trailing_slash = False | |
344 | ||
345 | ||
341 | 346 | WorkspaceView.register(workspace_api) |
342 | # I'm Py3 | |
347 | WorkspaceV3View.register(workspace_api) |
26 | 26 | get_message, |
27 | 27 | verify_and_update_password, |
28 | 28 | verify_hash) |
29 | ||
29 | 30 | from flask_kvsession import KVSessionExtension |
30 | 31 | from simplekv.fs import FilesystemStore |
31 | 32 | from simplekv.decorator import PrefixDecorator |
42 | 43 | |
43 | 44 | |
44 | 45 | logger = logging.getLogger(__name__) |
46 | audit_logger = logging.getLogger('audit') | |
45 | 47 | |
46 | 48 | |
47 | 49 | def setup_storage_path(): |
88 | 90 | from faraday.server.api.modules.search_filter import searchfilter_api # pylint:disable=import-outside-toplevel |
89 | 91 | from faraday.server.api.modules.preferences import preferences_api # pylint:disable=import-outside-toplevel |
90 | 92 | from faraday.server.api.modules.export_data import export_data_api # pylint:disable=import-outside-toplevel |
93 | #Custom reset password | |
94 | from faraday.server.api.modules.auth import auth # pylint:disable=import-outside-toplevel | |
91 | 95 | |
92 | 96 | app.register_blueprint(commandsrun_api) |
93 | 97 | app.register_blueprint(activityfeed_api) |
113 | 117 | app.register_blueprint(searchfilter_api) |
114 | 118 | app.register_blueprint(preferences_api) |
115 | 119 | app.register_blueprint(export_data_api) |
120 | app.register_blueprint(auth) | |
116 | 121 | |
117 | 122 | |
118 | 123 | def check_testing_configuration(testing, app): |
252 | 257 | session.destroy() |
253 | 258 | KVSessionExtension(app=app).cleanup_sessions(app) |
254 | 259 | |
260 | user_ip = request.headers.get('X-Forwarded-For', request.remote_addr) | |
261 | user_logout_at = datetime.datetime.now() | |
262 | audit_logger.info(f"User [{user.username}] logged out from IP [{user_ip}] at [{user_logout_at}]") | |
263 | ||
255 | 264 | |
256 | 265 | def user_logged_in_succesfull(app, user): |
257 | 266 | user_agent = request.headers.get('User-Agent') |
268 | 277 | logger.debug("Cleanup sessions") |
269 | 278 | KVSessionExtension(app=app).cleanup_sessions(app) |
270 | 279 | |
280 | user_ip = request.headers.get('X-Forwarded-For', request.remote_addr) | |
281 | user_login_at = datetime.datetime.now() | |
282 | audit_logger.info(f"User [{user.username}] logged in from IP [{user_ip}] at [{user_login_at}]") | |
283 | ||
284 | ||
271 | 285 | def create_app(db_connection_string=None, testing=None): |
272 | app = Flask(__name__, static_folder=None) | |
286 | ||
287 | class CustomFlask(Flask): | |
288 | SKIP_RULES = [ # These endpoints will be removed for v3 | |
289 | '/v3/ws/<workspace_name>/hosts/bulk_delete/', | |
290 | '/v3/ws/<workspace_name>/vulns/bulk_delete/', | |
291 | '/v3/ws/<workspace_id>/change_readonly/', | |
292 | '/v3/ws/<workspace_id>/deactivate/', | |
293 | '/v3/ws/<workspace_id>/activate/', | |
294 | ] | |
295 | ||
296 | def add_url_rule(self, rule, endpoint=None, view_func=None, **options): | |
297 | # Flask registers views when an application starts | |
298 | # do not add view from SKIP_VIEWS | |
299 | for rule_ in CustomFlask.SKIP_RULES: | |
300 | if rule_ == rule: | |
301 | return | |
302 | return super(CustomFlask, self).add_url_rule(rule, endpoint, view_func, **options) | |
303 | ||
304 | app = CustomFlask(__name__, static_folder=None) | |
273 | 305 | |
274 | 306 | try: |
275 | 307 | secret_key = faraday.server.config.faraday_server.secret_key |
291 | 323 | login_failed_message = ("Invalid username or password", 'error') |
292 | 324 | |
293 | 325 | app.config.update({ |
326 | 'SECURITY_BACKWARDS_COMPAT_AUTH_TOKEN': True, | |
294 | 327 | 'SECURITY_PASSWORD_SINGLE_HASH': True, |
295 | 328 | 'WTF_CSRF_ENABLED': False, |
296 | 329 | 'SECURITY_USER_IDENTITY_ATTRIBUTES': ['username'], |
297 | 330 | 'SECURITY_POST_LOGIN_VIEW': '/_api/session', |
298 | 'SECURITY_POST_LOGOUT_VIEW': '/_api/login', | |
331 | 'SECURITY_POST_LOGOUT_VIEW': '/_api/logout', | |
299 | 332 | 'SECURITY_POST_CHANGE_VIEW': '/_api/change', |
333 | 'SECURITY_RESET_PASSWORD_TEMPLATE': '/security/reset.html', | |
334 | 'SECURITY_POST_RESET_VIEW': '/', | |
335 | 'SECURITY_SEND_PASSWORD_RESET_EMAIL':True, | |
336 | #For testing porpouse | |
337 | 'SECURITY_EMAIL_SENDER': "[email protected]", | |
300 | 338 | 'SECURITY_CHANGEABLE': True, |
301 | 339 | 'SECURITY_SEND_PASSWORD_CHANGE_EMAIL': False, |
302 | 340 | 'SECURITY_MSG_USER_DOES_NOT_EXIST': login_failed_message, |
303 | 341 | 'SECURITY_TOKEN_AUTHENTICATION_HEADER': 'Authorization', |
342 | ||
304 | 343 | |
305 | 344 | # The line bellow should not be necessary because of the |
306 | 345 | # CustomLoginForm, but i'll include it anyway. |
370 | 409 | |
371 | 410 | app.view_functions['security.login'].is_public = True |
372 | 411 | app.view_functions['security.logout'].is_public = True |
373 | ||
374 | 412 | app.debug = faraday.server.config.is_debug_mode() |
375 | 413 | minify_json_output(app) |
376 | 414 | |
381 | 419 | register_handlers(app) |
382 | 420 | |
383 | 421 | app.view_functions['agent_api.AgentCreationView:post'].is_public = True |
422 | app.view_functions['agent_api.AgentCreationV3View:post'].is_public = True | |
384 | 423 | |
385 | 424 | return app |
386 | 425 |
3 | 3 | See the file 'doc/LICENSE' for the license information |
4 | 4 | |
5 | 5 | """ |
6 | import yaml | |
6 | 7 | from apispec import APISpec |
7 | 8 | from apispec.ext.marshmallow import MarshmallowPlugin |
8 | 9 | from apispec_webframeworks.flask import FlaskPlugin |
9 | 10 | from faraday.server.web import app |
10 | from faraday import __version__ as f_version | |
11 | 11 | import json |
12 | 12 | |
13 | 13 | from faraday.utils.faraday_openapi_plugin import FaradayAPIPlugin |
14 | 14 | |
15 | 15 | |
16 | def openapi_format(format="yaml", server="localhost", no_servers=False): | |
16 | def openapi_format(format="yaml", server="localhost", no_servers=False, return_tags=False): | |
17 | 17 | extra_specs = {'info': { |
18 | 18 | 'description': 'The Faraday REST API enables you to interact with ' |
19 | 19 | '[our server](https://github.com/infobyte/faraday).\n' |
46 | 46 | } |
47 | 47 | spec.components.response("UnauthorizedError", response_401_unauthorized) |
48 | 48 | |
49 | tags = set() | |
50 | ||
49 | 51 | with app.test_request_context(): |
50 | for endpoint in app.view_functions: | |
51 | spec.path(view=app.view_functions[endpoint], app=app) | |
52 | for endpoint in app.view_functions.values(): | |
53 | spec.path(view=endpoint, app=app) | |
54 | ||
55 | # Set up global tags | |
56 | spec_yaml = yaml.load(spec.to_yaml(), Loader=yaml.SafeLoader) | |
57 | for path_value in spec_yaml["paths"].values(): | |
58 | for data_value in path_value.values(): | |
59 | if 'tags' in data_value and any(data_value['tags']): | |
60 | for tag in data_value['tags']: | |
61 | tags.add(tag) | |
62 | for tag in sorted(tags): | |
63 | spec.tag({'name': tag}) | |
64 | ||
65 | if return_tags: | |
66 | return sorted(tags) | |
67 | ||
52 | 68 | if format.lower() == "yaml": |
53 | 69 | print(spec.to_yaml()) |
54 | 70 | else: |
183 | 183 | self.ssl = False |
184 | 184 | self.certfile = None |
185 | 185 | self.keyfile = None |
186 | self.enabled = False | |
187 | ||
188 | def is_enabled(self): | |
189 | return self.enabled is True | |
190 | ||
186 | 191 | |
187 | 192 | class StorageConfigObject(ConfigSection): |
188 | 193 | def __init__(self): |
73 | 73 | 'executive_report', |
74 | 74 | 'workspace', |
75 | 75 | 'task', |
76 | ] | |
77 | ||
78 | COMMENT_TYPES = [ | |
79 | 'system', | |
80 | 'user' | |
76 | 81 | ] |
77 | 82 | |
78 | 83 | |
1238 | 1243 | class VulnerabilityWeb(VulnerabilityGeneric): |
1239 | 1244 | __tablename__ = None |
1240 | 1245 | |
1246 | def __init__(self, *args, **kwargs): | |
1247 | # Sanitize some fields on creation | |
1248 | if 'request' in kwargs: | |
1249 | kwargs['request'] = ''.join([x for x in kwargs['request'] if x in string.printable]) | |
1250 | if 'response' in kwargs: | |
1251 | kwargs['response'] = ''.join([x for x in kwargs['response'] if x in string.printable]) | |
1252 | super().__init__(*args, **kwargs) | |
1253 | ||
1254 | ||
1241 | 1255 | @declared_attr |
1242 | 1256 | def service_id(cls): |
1243 | 1257 | return VulnerabilityGeneric.__table__.c.get( |
1995 | 2009 | class Comment(Metadata): |
1996 | 2010 | __tablename__ = 'comment' |
1997 | 2011 | id = Column(Integer, primary_key=True) |
2012 | comment_type = Column(Enum(*COMMENT_TYPES, name='comment_types'), nullable=False, default='user') | |
1998 | 2013 | |
1999 | 2014 | text = BlankColumn(Text) |
2000 | 2015 |
6 | 6 | import json |
7 | 7 | import time |
8 | 8 | import datetime |
9 | import logging | |
9 | 10 | from flask import g |
10 | 11 | from marshmallow import fields, Schema, post_dump, EXCLUDE |
11 | 12 | from marshmallow.utils import missing as missing_ |
17 | 18 | VulnerabilityABC, |
18 | 19 | CustomFieldsSchema, |
19 | 20 | ) |
21 | ||
22 | logger = logging.getLogger(__name__) | |
20 | 23 | |
21 | 24 | |
22 | 25 | class JSTimestampField(fields.Integer): |
72 | 75 | field_name=key, |
73 | 76 | ).first() |
74 | 77 | if not field_schema: |
75 | raise ValidationError("Invalid custom field, not found in schema. Did you add it first?") | |
78 | logger.warning( | |
79 | f"Invalid custom field {key}. Did you forget to add it?" | |
80 | ) | |
81 | continue | |
76 | 82 | if field_schema.field_type == 'str': |
77 | 83 | serialized[key] = str(raw_data) |
78 | 84 | elif field_schema.field_type == 'int': |
0 | {% macro render_field_with_errors(field) %} | |
1 | <p> | |
2 | {{ field.label }} {{ field(**kwargs)|safe }} | |
3 | {% if field.errors %} | |
4 | <ul> | |
5 | {% for error in field.errors %} | |
6 | <li>{{ error }}</li> | |
7 | {% endfor %} | |
8 | </ul> | |
9 | {% endif %} | |
10 | </p> | |
11 | {% endmacro %} | |
12 | ||
13 | {% macro render_field(field) %} | |
14 | <p>{{ field(**kwargs)|safe }}</p> | |
15 | {% endmacro %} |
0 | {%- with messages = get_flashed_messages(with_categories=true) -%} | |
1 | {% if messages %} | |
2 | <ul class="flashes"> | |
3 | {% for category, message in messages %} | |
4 | <li class="{{ category }}">{{ message }}</li> | |
5 | {% endfor %} | |
6 | </ul> | |
7 | {% endif %} | |
8 | {%- endwith %} |
0 | <!-- Faraday Penetration Test IDE --> | |
1 | <!-- Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/) --> | |
2 | <!-- See the file 'doc/LICENSE' for the license information --> | |
3 | <!DOCTYPE html> | |
4 | <!--[if lt IE 7 ]> <html lang="en" class="no-js ie6"> <![endif]--> | |
5 | <!--[if IE 7 ]> <html lang="en" class="no-js ie7"> <![endif]--> | |
6 | <!--[if IE 8 ]> <html lang="en" class="no-js ie8"> <![endif]--> | |
7 | <!--[if IE 9 ]> <html lang="en" class="no-js ie9"> <![endif]--> | |
8 | <!--[if (gt IE 9)|!(IE)]><!--> <html lang="en" class="no-js" ng-app="faradayApp"> <!--<![endif]--> | |
9 | <head> | |
10 | <meta charset="utf-8"/> | |
11 | <!--[if IE]><![endif]--> | |
12 | <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/> | |
13 | <title ng-bind="title + 'Faraday'"></title> | |
14 | <meta name="description" content=""/> | |
15 | <meta name="keywords" content=""/> | |
16 | <meta name="author" content=""/> | |
17 | ||
18 | <link rel="apple-touch-icon" sizes="57x57" href="apple-icon-57x57.png"> | |
19 | <link rel="apple-touch-icon" sizes="60x60" href="apple-icon-60x60.png"> | |
20 | <link rel="apple-touch-icon" sizes="72x72" href="apple-icon-72x72.png"> | |
21 | <link rel="apple-touch-icon" sizes="76x76" href="apple-icon-76x76.png"> | |
22 | <link rel="apple-touch-icon" sizes="114x114" href="apple-icon-114x114.png"> | |
23 | <link rel="apple-touch-icon" sizes="120x120" href="apple-icon-120x120.png"> | |
24 | <link rel="apple-touch-icon" sizes="144x144" href="apple-icon-144x144.png"> | |
25 | <link rel="apple-touch-icon" sizes="152x152" href="apple-icon-152x152.png"> | |
26 | <link rel="apple-touch-icon" sizes="180x180" href="apple-icon-180x180.png"> | |
27 | <link rel="icon" type="image/png" sizes="192x192" href="android-icon-192x192.png"> | |
28 | <link rel="icon" type="image/png" sizes="32x32" href="favicon-32x32.png"> | |
29 | <link rel="icon" type="image/png" sizes="96x96" href="favicon-96x96.png"> | |
30 | <link rel="icon" type="image/png" sizes="16x16" href="favicon-16x16.png"> | |
31 | <link rel="manifest" href="/manifest.json"> | |
32 | <meta name="msapplication-TileColor" content="#ffffff"> | |
33 | <meta name="msapplication-TileImage" content="/ms-icon-144x144.png"> | |
34 | <meta name="theme-color" content="#ffffff"> | |
35 | ||
36 | <!-- CSS --> | |
37 | <link rel="stylesheet" type="text/css" href="/script/anguilar-ui-notification.min.css" /> | |
38 | <link rel="stylesheet" type="text/css" href="/script/font.opensans.css" /> | |
39 | <link rel="stylesheet" type="text/css" href="/normalize.css" /> | |
40 | <link rel="stylesheet" type="text/css" href="/estilos.css" /> | |
41 | <link rel="stylesheet" type="text/css" href="/styles/material-input.css" /> | |
42 | <link rel="stylesheet" type="text/css" href="/script/animate.css" /> | |
43 | <link rel="stylesheet" href="/script/bootstrap.min.css"> | |
44 | <link rel="stylesheet" type="text/css" href="/styles/font-awesome.css" /> | |
45 | <link rel="stylesheet" type="text/css" href="/styles/angular-hotkeys.css" /> | |
46 | <link rel="stylesheet" type="text/css" href="/script/angular-chart.css" /> | |
47 | <link rel="stylesheet" type="text/css" href="/script/ui-grid.css" /> | |
48 | <!-- Custom css will have priority --> | |
49 | <link rel="stylesheet" type="text/css" href="/estilos-v3.css" /> | |
50 | <link rel="stylesheet" type="text/css" href="/dashboard-v3.css" /> | |
51 | <link rel="stylesheet" type="text/css" href="/header.css" /> | |
52 | <link rel="stylesheet" type="text/css" href="/scripts/statusReport/styles/status.css" /> | |
53 | <link rel="stylesheet" type="text/css" href="/table-v3.css" /> | |
54 | ||
55 | <!-- Icons --> | |
56 | <link href="/favicon.ico" rel="shortcut icon"> | |
57 | <link href="/favicon.ico" type="image/vnd.microsoft.icon" rel="icon" /> | |
58 | <link href="/images/site_preview.jpg" rel="image_src" /> | |
59 | ||
60 | </head> | |
61 | <body> | |
62 | <div id="cont"> | |
63 | <div class="wrapper"> | |
64 | {% block content -%} | |
65 | {%- endblock content %} | |
66 | </div><!--!/#wrapper --> | |
67 | </div><!--!/#container --> | |
68 | <!-- Scripts --> | |
69 | <script type="text/javascript" src="/script/mousetrap.min.js"></script> | |
70 | <script type="text/javascript" src="/script/jquery-1.9.1.min.js"></script> | |
71 | <script type="text/javascript" src="/script/bootstrap.min.js"></script> | |
72 | <script type="text/javascript" src="/script/angular.min.js"></script> | |
73 | <script type="text/javascript" src="/script/angular-cookies.min.js"></script> | |
74 | <script type="text/javascript" src="/script/angular-hotkeys.js"></script> | |
75 | <script type="text/javascript" src="/script/angular-route.min.js"></script> | |
76 | <script type="text/javascript" src="/script/angular-selection-model.min.js"></script> | |
77 | <script type="text/javascript" src="/script/angular-file-upload-shim.min.js"></script><!-- compatibility with older browsers --> | |
78 | <script type="text/javascript" src="/script/angular-file-upload.min.js"></script> | |
79 | <script type="text/javascript" src="/script/angular-file-upload-lib.min.js"></script> | |
80 | <script type="text/javascript" src="/script/ui-bootstrap-tpls-0.14.1.min.js"></script> | |
81 | <script type="text/javascript" src="/script/cryptojs-sha1.js"></script> | |
82 | <script type="text/javascript" src="/script/sanitize.js"></script> | |
83 | <script type="text/javascript" src="/script/showdown.min.js"></script> | |
84 | <script type="text/javascript" src="/script/showdown-table.min.js"></script> | |
85 | <script type="text/javascript" src="/script/angular-ui-notification.min.js"></script> | |
86 | <script type="text/javascript" src="/script/angular-ui.min.js"></script> | |
87 | <script type="text/javascript" src="/script/angular-clipboard.min.js"></script> | |
88 | ||
89 | </body> | |
90 | </html> |
0 | {% extends "security/base.html" %} | |
1 | {% from "security/_macros.html" import render_field_with_errors, render_field %} | |
2 | {% block content %} | |
3 | {% include "security/_messages.html" %} | |
4 | <div id="login-main"> | |
5 | <div id="login-container" class=""> | |
6 | <section id="main" class=""> | |
7 | <form action="{{ url_for_security('reset_password', token=reset_password_token) }}" method="POST" name="reset_password_form"> | |
8 | {{ reset_password_form.hidden_tag() }} | |
9 | <div id="form-signin"> | |
10 | <img src="/images/logo-faraday.svg" class="height-39px"> | |
11 | <h3 class="clear-margin-bottom margin-top-22px">{{ _('Reset password') }}</h3> | |
12 | <div class="form-input margin-top-30px" style="position:relative;"> | |
13 | {{ render_field_with_errors(reset_password_form.password) }} | |
14 | </div> | |
15 | <div class="form-input margin-top-30px" style="position:relative;"> | |
16 | {{ render_field_with_errors(reset_password_form.password_confirm) }} | |
17 | </div> | |
18 | </div><!-- .form-signin --> | |
19 | <button class="btn-frd btn-xl bg-blue btn-block" style="background-color: #00a8e1" type="submit">Reset Password</button> | |
20 | </form> | |
21 | </section> | |
22 | </div> | |
23 | </div> | |
24 | {% endblock %} |
42 | 42 | writer = csv.DictWriter(buffer, fieldnames=headers) |
43 | 43 | writer.writeheader() |
44 | 44 | |
45 | hosts_data = {} | |
46 | services_data = {} | |
45 | comments_dict = dict() | |
46 | hosts_ids = set() | |
47 | services_ids = set() | |
48 | vulns_ids = set() | |
49 | ||
47 | 50 | for vuln in vulns: |
48 | vuln_data = _build_vuln_data(vuln, custom_fields_columns) | |
51 | if vuln['parent_type'] == 'Host': | |
52 | hosts_ids.add(vuln['parent']) | |
53 | elif vuln['parent_type'] == 'Service': | |
54 | services_ids.add(vuln['parent']) | |
55 | vulns_ids.add(vuln['_id']) | |
56 | ||
57 | comments = db.session.query(Comment)\ | |
58 | .filter(Comment.object_type == 'vulnerability')\ | |
59 | .filter(Comment.object_id.in_(vulns_ids)).all() | |
60 | for comment in comments: | |
61 | if comment.object_id in comments_dict: | |
62 | comments_dict[comment.object_id].append(comment.text) | |
63 | else: | |
64 | comments_dict[comment.object_id] = [comment.text] | |
65 | ||
66 | services_data = _build_services_data(services_ids) | |
67 | ||
68 | hosts_ids.update({elem['service_parent_id'] for elem in services_data.values()}) | |
69 | ||
70 | hosts_data = _build_hosts_data(hosts_ids) | |
71 | ||
72 | for vuln in vulns: | |
73 | vuln_data = _build_vuln_data(vuln, custom_fields_columns, comments_dict) | |
49 | 74 | if vuln['parent_type'] == 'Host': |
50 | 75 | host_id = vuln['parent'] |
51 | if host_id in hosts_data: | |
52 | host_data = hosts_data[host_id] | |
53 | else: | |
54 | host_data = _build_host_data(host_id) | |
55 | hosts_data[host_id] = host_data | |
76 | host_data = hosts_data[host_id] | |
56 | 77 | row = {**vuln_data, **host_data} |
57 | 78 | elif vuln['parent_type'] == 'Service': |
58 | 79 | service_id = vuln['parent'] |
59 | if service_id in services_data: | |
60 | service_data = services_data[service_id] | |
61 | else: | |
62 | service_data = _build_service_data(service_id) | |
63 | services_data[service_id] = service_data | |
80 | service_data = services_data[service_id] | |
64 | 81 | host_id = service_data['service_parent_id'] |
65 | if host_id in hosts_data: | |
66 | host_data = hosts_data[host_id] | |
67 | else: | |
68 | host_data = _build_host_data(host_id) | |
69 | hosts_data[host_id] = host_data | |
82 | host_data = hosts_data[host_id] | |
70 | 83 | row = {**vuln_data, **host_data, **service_data} |
71 | 84 | |
72 | 85 | writer.writerow(row) |
77 | 90 | return memory_file |
78 | 91 | |
79 | 92 | |
80 | def _build_host_data(host_id): | |
81 | host = db.session.query(Host)\ | |
82 | .filter(Host.id == host_id).one() | |
83 | ||
84 | host_data = { | |
85 | "host_id": host.id, | |
86 | "host_description": host.description, | |
87 | "mac": host.mac, | |
88 | "host_owned": host.owned, | |
89 | "host_creator_id": host.creator_id, | |
90 | "host_date": host.create_date, | |
91 | "host_update_date": host.update_date, | |
92 | } | |
93 | ||
94 | return host_data | |
95 | ||
96 | ||
97 | def _build_service_data(service_id): | |
98 | service = db.session.query(Service)\ | |
99 | .filter(Service.id == service_id).one() | |
100 | service_data = { | |
101 | "service_id": service.id, | |
102 | "service_name": service.name, | |
103 | "service_description": service.description, | |
104 | "service_owned": service.owned, | |
105 | "port": service.port, | |
106 | "protocol": service.protocol, | |
107 | "summary": service.summary, | |
108 | "version": service.version, | |
109 | "service_status": service.status, | |
110 | "service_creator_id": service.creator_id, | |
111 | "service_date": service.create_date, | |
112 | "service_update_date": service.update_date, | |
113 | "service_parent_id": service.host_id, | |
114 | } | |
115 | ||
116 | return service_data | |
117 | ||
118 | ||
119 | def _build_vuln_data(vuln, custom_fields_columns): | |
120 | comments_list = [] | |
121 | comments = db.session.query(Comment).filter_by( | |
122 | object_type='vulnerability', | |
123 | object_id=vuln['_id']).all() | |
124 | for comment in comments: | |
125 | comments_list.append(comment.text) | |
126 | vuln_description = re.sub(' +', ' ', vuln['description'].strip().replace("\n", "")) | |
93 | def _build_hosts_data(hosts_id): | |
94 | hosts = db.session.query(Host)\ | |
95 | .filter(Host.id.in_(hosts_id)).all() | |
96 | ||
97 | hosts_dict = {} | |
98 | ||
99 | for host in hosts: | |
100 | host_data = { | |
101 | "host_id": host.id, | |
102 | "host_description": host.description, | |
103 | "mac": host.mac, | |
104 | "host_owned": host.owned, | |
105 | "host_creator_id": host.creator_id, | |
106 | "host_date": host.create_date, | |
107 | "host_update_date": host.update_date, | |
108 | } | |
109 | ||
110 | hosts_dict[host.id] = host_data | |
111 | ||
112 | return hosts_dict | |
113 | ||
114 | ||
115 | def _build_services_data(services_ids): | |
116 | services = db.session.query(Service)\ | |
117 | .filter(Service.id.in_(services_ids)).all() | |
118 | services_dict = {} | |
119 | ||
120 | for service in services: | |
121 | ||
122 | service_data = { | |
123 | "service_id": service.id, | |
124 | "service_name": service.name, | |
125 | "service_description": service.description, | |
126 | "service_owned": service.owned, | |
127 | "port": service.port, | |
128 | "protocol": service.protocol, | |
129 | "summary": service.summary, | |
130 | "version": service.version, | |
131 | "service_status": service.status, | |
132 | "service_creator_id": service.creator_id, | |
133 | "service_date": service.create_date, | |
134 | "service_update_date": service.update_date, | |
135 | "service_parent_id": service.host_id, | |
136 | } | |
137 | ||
138 | services_dict[service.id] = service_data | |
139 | ||
140 | return services_dict | |
141 | ||
142 | ||
143 | def _build_vuln_data(vuln, custom_fields_columns, comments_dict): | |
144 | comments_list = comments_dict[vuln['_id']] if vuln['_id'] in comments_dict else [] | |
127 | 145 | vuln_date = vuln['metadata']['create_time'] |
128 | 146 | if vuln['service']: |
129 | 147 | service_fields = ["status", "protocol", "name", "summary", "version", "ports"] |
145 | 163 | "severity": vuln.get('severity', None), |
146 | 164 | "service": vuln_service, |
147 | 165 | "target": vuln.get('target', None), |
148 | "desc": vuln_description, | |
166 | "desc": vuln.get('description', None), | |
149 | 167 | "status": vuln.get('status', None), |
150 | 168 | "hostnames": vuln_hostnames, |
151 | 169 | "comments": comments_list, |
4 | 4 | import logging.handlers |
5 | 5 | import faraday.server.config |
6 | 6 | import errno |
7 | import os | |
7 | 8 | |
8 | 9 | from syslog_rfc5424_formatter import RFC5424Formatter |
9 | 10 | from faraday.server.config import CONST_FARADAY_HOME_PATH |
10 | 11 | |
11 | 12 | LOG_FILE = CONST_FARADAY_HOME_PATH / 'logs' / 'faraday-server.log' |
13 | AUDIT_LOG_FILE = CONST_FARADAY_HOME_PATH / 'logs' / 'audit.log' | |
12 | 14 | |
13 | 15 | MAX_LOG_FILE_SIZE = 5 * 1024 * 1024 # 5 MB |
14 | 16 | MAX_LOG_FILE_BACKUP_COUNT = 5 |
25 | 27 | if faraday.server.config.logger_config.use_rfc5424_formatter: |
26 | 28 | formatter = RFC5424Formatter() |
27 | 29 | else: |
28 | ||
29 | 30 | formatter = logging.Formatter(LOG_FORMAT, LOG_DATE_FORMAT) |
30 | 31 | setup_console_logging(formatter) |
31 | setup_file_logging(formatter) | |
32 | ||
33 | if not os.environ.get("FARADAY_DISABLE_LOGS"): | |
34 | setup_file_logging(formatter, LOG_FILE) | |
35 | setup_file_logging(formatter, AUDIT_LOG_FILE, 'audit') | |
32 | 36 | |
33 | 37 | |
34 | 38 | def setup_console_logging(formatter): |
39 | 43 | LVL_SETTABLE_HANDLERS.append(console_handler) |
40 | 44 | |
41 | 45 | |
42 | def setup_file_logging(formatter): | |
43 | create_logging_path() | |
46 | def setup_file_logging(formatter, log_file, log_name=None): | |
47 | create_logging_path(log_file) | |
44 | 48 | file_handler = logging.handlers.RotatingFileHandler( |
45 | LOG_FILE, maxBytes=MAX_LOG_FILE_SIZE, backupCount=MAX_LOG_FILE_BACKUP_COUNT) | |
49 | log_file, maxBytes=MAX_LOG_FILE_SIZE, backupCount=MAX_LOG_FILE_BACKUP_COUNT) | |
46 | 50 | file_handler.setFormatter(formatter) |
47 | 51 | file_handler.setLevel(faraday.server.config.LOGGING_LEVEL) |
48 | add_handler(file_handler) | |
52 | add_handler(file_handler, log_name) | |
49 | 53 | LVL_SETTABLE_HANDLERS.append(file_handler) |
50 | 54 | |
51 | 55 | |
52 | def add_handler(handler): | |
53 | logger = logging.getLogger() | |
56 | def add_handler(handler, log_name=None): | |
57 | logger = logging.getLogger(log_name) | |
54 | 58 | logger.addHandler(handler) |
59 | logger.propagate = False | |
55 | 60 | LOGGING_HANDLERS.append(handler) |
56 | 61 | |
57 | 62 | |
61 | 66 | handler.setLevel(level) |
62 | 67 | |
63 | 68 | |
64 | def create_logging_path(): | |
69 | def create_logging_path(path_file): | |
65 | 70 | try: |
66 | LOG_FILE.parent.mkdir(parents=True) | |
71 | path_file.parent.mkdir(parents=True) | |
67 | 72 | except OSError as e: |
68 | 73 | if e.errno != errno.EEXIST: |
69 | 74 | raise |
75 | ||
70 | 76 | |
71 | 77 | setup_logging() |
72 | 78 |
17 | 17 | listenWS |
18 | 18 | ) |
19 | 19 | |
20 | from flask_mail import Mail | |
21 | ||
20 | 22 | from OpenSSL.SSL import Error as SSLError |
21 | 23 | |
22 | 24 | import faraday.server.config |
23 | 25 | |
24 | from faraday.server.config import CONST_FARADAY_HOME_PATH | |
26 | from faraday.server.config import CONST_FARADAY_HOME_PATH, smtp | |
25 | 27 | from faraday.server.utils import logger |
26 | 28 | from faraday.server.threads.reports_processor import ReportsManager, REPORTS_QUEUE |
27 | 29 | from faraday.server.threads.ping_home import PingHomeThread |
33 | 35 | |
34 | 36 | |
35 | 37 | app = create_app() # creates a Flask(__name__) app |
38 | # After 'Create app' | |
39 | app.config['MAIL_SERVER'] = smtp.host | |
40 | app.config['MAIL_PORT'] = smtp.port | |
41 | app.config['MAIL_USE_SSL'] = smtp.ssl | |
42 | app.config['MAIL_USERNAME'] = smtp.username | |
43 | app.config['MAIL_PASSWORD'] = smtp.password | |
44 | mail = Mail(app) | |
36 | 45 | logger = logging.getLogger(__name__) |
37 | 46 | |
38 | 47 | |
132 | 141 | factory.protocol = BroadcastServerProtocol |
133 | 142 | return factory |
134 | 143 | |
144 | def __stop_all_threads(self): | |
145 | if self.raw_report_processor.is_alive(): | |
146 | self.raw_report_processor.stop() | |
147 | self.ping_home_thread.stop() | |
148 | ||
135 | 149 | def install_signal(self): |
136 | 150 | for sig in (SIGABRT, SIGILL, SIGINT, SIGSEGV, SIGTERM): |
137 | 151 | signal(sig, SIG_DFL) |
140 | 154 | def signal_handler(*args): |
141 | 155 | logger.info('Received SIGTERM, shutting down.') |
142 | 156 | logger.info("Stopping threads, please wait...") |
143 | # teardown() | |
144 | if self.raw_report_processor.isAlive(): | |
145 | self.raw_report_processor.stop() | |
146 | self.ping_home_thread.stop() | |
157 | self.__stop_all_threads() | |
147 | 158 | |
148 | 159 | log_path = CONST_FARADAY_HOME_PATH / 'logs' / 'access-logging.log' |
149 | 160 | site = twisted.web.server.Site(self.__root_resource, |
199 | 210 | |
200 | 211 | except error.CannotListenError as e: |
201 | 212 | logger.error(e) |
213 | self.__stop_all_threads() | |
202 | 214 | sys.exit(1) |
203 | ||
204 | 215 | |
205 | 216 | except Exception as e: |
206 | 217 | logger.exception('Something went wrong when trying to setup the Web UI') |
218 | logger.exception(e) | |
219 | self.__stop_all_threads() | |
207 | 220 | sys.exit(1) |
208 | 221 | # I'm Py3 |
1410 | 1410 | white-space: nowrap; |
1411 | 1411 | text-overflow: ellipsis; |
1412 | 1412 | overflow: hidden; |
1413 | }⏎ | |
1413 | } | |
1414 | ||
1415 | .forgot-password{ | |
1416 | margin-top: 10px; | |
1417 | text-align: center; | |
1418 | padding-bottom: 10px; | |
1419 | } |
173 | 173 | <script type="text/javascript" src="scripts/workspaces/providers/workspaces.js"></script> |
174 | 174 | <script type="text/javascript" src="scripts/auth/controllers/login.js"></script> |
175 | 175 | <script type="text/javascript" src="scripts/auth/controllers/resetPassword.js"></script> |
176 | <script type="text/javascript" src="scripts/auth/controllers/forgotPassword.js"></script> | |
176 | 177 | <script type="text/javascript" src="scripts/auth/directives/compareTo.js"></script> |
177 | 178 | <script type="text/javascript" src="scripts/auth/services/account.js"></script> |
178 | 179 | <script type="text/javascript" src="scripts/auth/services/login.js"></script> |
102 | 102 | if ($scope.selected_cf.field_order === null) |
103 | 103 | $scope.selected_cf.field_order = getMaxOrder() + 1; |
104 | 104 | |
105 | if($scope.selected_cf.field_metadata.length === 0){ | |
105 | if(!$scope.selected_cf.field_metadata || $scope.selected_cf.field_metadata.length === 0){ | |
106 | 106 | $scope.selected_cf.field_metadata = null; |
107 | 107 | } |
108 | 108 | |
118 | 118 | |
119 | 119 | |
120 | 120 | $scope.updateCustomCustomField = function () { |
121 | if($scope.selected_cf.field_metadata.length === 0){ | |
121 | if(!$scope.selected_cf.field_metadata || $scope.selected_cf.field_metadata.length === 0){ | |
122 | 122 | $scope.selected_cf.field_metadata = null; |
123 | 123 | } |
124 | 124 |
0 | angular.module('faradayApp').controller('forgotPasswordCtrl', ['$modalInstance', '$scope', 'AccountSrv', | |
1 | function($modalInstance, $scope, AccountSrv) { | |
2 | ||
3 | $scope.cancel = function() { | |
4 | $modalInstance.dismiss('cancel'); | |
5 | }; | |
6 | ||
7 | $scope.data; | |
8 | ||
9 | init = function () { | |
10 | $scope.data = { | |
11 | "email": "", | |
12 | "recover":{ | |
13 | "valid" : false, | |
14 | "not_found": false | |
15 | } | |
16 | }; | |
17 | }; | |
18 | ||
19 | $scope.recover = function(){ | |
20 | if ($scope.data.email){ | |
21 | loginSrv.recover($scope.data.email).then(function(result){ | |
22 | $scope.data.recover.valid = true; | |
23 | $scope.data.recover.not_found = false; | |
24 | //$modalInstance.close(); | |
25 | }, function(){ | |
26 | $scope.errorMessage = "Invalid email"; | |
27 | $scope.data.recover.valid = false; | |
28 | $scope.data.recover.not_found = true; | |
29 | }); | |
30 | } else { | |
31 | $scope.errorMessage = "Email user is required"; | |
32 | } | |
33 | }; | |
34 | ||
35 | init(); | |
36 | }]); |
2 | 2 | // See the file 'doc/LICENSE' for the license information |
3 | 3 | |
4 | 4 | angular.module('faradayApp') |
5 | .controller('loginCtrl', ['$scope', '$location', '$cookies', 'loginSrv', 'BASEURL', | |
6 | function($scope, $location, $cookies, loginSrv, BASEURL) { | |
5 | .controller('loginCtrl', ['$scope', '$location', '$cookies', 'loginSrv', 'BASEURL' ,'$uibModal', | |
6 | function($scope, $location, $cookies, loginSrv, BASEURL, $uibModal) { | |
7 | 7 | |
8 | 8 | $scope.data = { |
9 | 9 | "user": null, |
41 | 41 | if(auth) $location.path('/'); |
42 | 42 | }); |
43 | 43 | }); |
44 | ||
45 | $scope._forgotPassword = function () { | |
46 | var modal = $uibModal.open({ | |
47 | templateUrl: 'scripts/auth/partials/forgotPassword.html', | |
48 | controller: 'forgotPasswordCtrl', | |
49 | size: '' | |
50 | }); | |
51 | ||
52 | modal.result.then(function () { | |
53 | debugger; | |
54 | }); | |
55 | }; | |
56 | ||
44 | 57 | |
45 | 58 | }]); |
46 | 59 |
0 | <!-- Faraday Penetration Test IDE --> | |
1 | <!-- Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/) --> | |
2 | <!-- See the file 'doc/LICENSE' for the license information --> | |
3 | <form name="form" novalidate > | |
4 | <div class="modal-header"> | |
5 | <h3 class="modal-title">Recover password</h3> | |
6 | </div> | |
7 | <div class="modal-body"> | |
8 | <div class="row"> | |
9 | <div class="col-md-10 col-md-offset-1"> | |
10 | <div ng-if="data.recover.valid"> | |
11 | <div class="alert alert-success target_not_selected" role="alert" ng-hide=""> | |
12 | <span class="sr-only">Success!</span> | |
13 | A recovery email has been sent to {{data.email}}. | |
14 | </div> | |
15 | </div> | |
16 | <div ng-if="data.recover.not_found"> | |
17 | <div class="alert alert-danger target_not_selected" role="alert" ng-hide=""> | |
18 | <span class="sr-only">Error!</span> | |
19 | The email {{data.email}} is invalid. | |
20 | </div> | |
21 | </div> | |
22 | ||
23 | <div class="form-input margin-top-10px"> | |
24 | <label> | |
25 | <input type="email" required id="email" ng-model="data.email" ng-keyup="$event.keyCode == 13 && recover()" ng-class="{'has-error': errorMessage !== undefined }"> | |
26 | <span class="placeholder">Please enter your email for recovery instructions</span> | |
27 | </label> | |
28 | </div> | |
29 | <p class="font-xs font-bold fg-red pull-left margin-top-18px">{{errorMessage}}</p> | |
30 | </div> | |
31 | </div> | |
32 | ||
33 | </div><!-- .modal-body --> | |
34 | <div class="modal-footer"> | |
35 | <div class="modal-button"> | |
36 | <button class="btn btn-success" ng-disabled="form.$invalid" ng-click="recover()">Recover</button> | |
37 | <button class="btn btn-danger" ng-click="cancel()">Cancel</button> | |
38 | </div> | |
39 | </div> | |
40 | </form> |
29 | 29 | <input id="remember" type="checkbox" ng-model="data.remember" ng-change="checkResetError()"> |
30 | 30 | </label> |
31 | 31 | </div> |
32 | <p class="font-xs font-bold fg-red pull-left margin-top-18px">{{errorMessage}}</p> | |
32 | <p class="font-xs font-bold fg-red pull-left margin-top-18px">{{errorMessage}}</p> | |
33 | 33 | </div><!-- .form-signin --> |
34 | <button class="btn-frd btn-xl bg-blue btn-block" style="background-color: #00a8e1" type="submit" ng-click="login()">Login</button> | |
35 | </section> | |
34 | <button class="btn-frd btn-xl bg-blue btn-block" style="background-color: #00a8e1" type="submit" ng-click="login()">Login</button> | |
35 | </section> | |
36 | 36 | </div> |
37 | 37 | <div id="footer-login"> |
38 | 38 | <p class="normal-size fg-white">Faraday Community - <a class="fg-white" href="https://www.faradaysec.com">faradaysec.com</a></p> |
91 | 91 | error: callback |
92 | 92 | }); |
93 | 93 | return deferred.promise; |
94 | }, | |
95 | ||
96 | recover: function(email){ | |
97 | var deferred = $q.defer(); | |
98 | ||
99 | $.ajax({ | |
100 | type: 'POST', | |
101 | url: BASEURL + '_api/auth/forgot_password', | |
102 | data: JSON.stringify({"email": email}), | |
103 | dataType: 'json', | |
104 | contentType: 'application/json' | |
105 | }) | |
106 | .done(function(data){ | |
107 | deferred.resolve(data); | |
108 | }) | |
109 | .fail(function(){ | |
110 | deferred.reject(); | |
111 | }); | |
112 | ||
113 | return deferred.promise; | |
94 | 114 | } |
95 | 115 | } |
96 | 116 |
67 | 67 | host = new Host(host_data.value); |
68 | 68 | result.hosts.push(host); |
69 | 69 | }); |
70 | result.total = response.data.total_rows; | |
70 | result.total = response.data.count; | |
71 | 71 | deferred.resolve(result); |
72 | 72 | }, function(response) { |
73 | 73 | deferred.reject(); |
124 | 124 | class_model = view_instance.model_class.__name__ |
125 | 125 | else: |
126 | 126 | class_model = 'No name' |
127 | #print(f'{view_name} / {class_model}') | |
127 | 128 | logger.debug(f'{view_name} / {class_model} / {rule.methods} / {view_name} / {view_instance._get_schema_class().__name__}') |
128 | 129 | operations[view_name] = yaml_utils.load_yaml_from_docstring( |
129 | 130 | view.__doc__.format(schema_class=view_instance._get_schema_class().__name__, class_model=class_model, tag_name=class_model) |
96 | 96 | ./packages/flask-kvsession-fork |
97 | 97 | { }; |
98 | 98 | |
99 | flask-security = | |
99 | flask-security-too = | |
100 | 100 | self.callPackage |
101 | ./packages/flask-security | |
101 | ./packages/flask-security-too | |
102 | 102 | { }; |
103 | 103 | |
104 | 104 | simplekv = |
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 | "apispec"; | |
12 | version = | |
13 | "4.0.0"; | |
14 | ||
15 | src = | |
16 | fetchPypi { | |
17 | inherit | |
18 | pname | |
19 | version; | |
20 | sha256 = | |
21 | "12n4w5zkn4drcn8izq68vmixmqvz6abviqkdn4ip0kaax3jjh3in"; | |
22 | }; | |
23 | ||
24 | # TODO FIXME | |
25 | doCheck = | |
26 | false; | |
27 | ||
28 | meta = | |
29 | with lib; { | |
30 | description = | |
31 | "A pluggable API specification generator. Currently supports the OpenAPI Specification (f.k.a. the Swagger specification)."; | |
32 | homepage = | |
33 | "https://github.com/marshmallow-code/apispec"; | |
34 | }; | |
35 | } |
20 | 20 | , flask |
21 | 21 | , flask-classful |
22 | 22 | , flask-kvsession-fork |
23 | , flask-security | |
23 | , flask-security-too | |
24 | 24 | , flask_login |
25 | 25 | , flask_sqlalchemy |
26 | 26 | , hypothesis |
58 | 58 | pname = |
59 | 59 | "faradaysec"; |
60 | 60 | version = |
61 | "3.14.0"; | |
61 | "3.14.1"; | |
62 | 62 | |
63 | 63 | src = |
64 | 64 | lib.cleanSource |
82 | 82 | email_validator |
83 | 83 | wtforms |
84 | 84 | flask_login |
85 | flask-security | |
85 | flask-security-too | |
86 | 86 | marshmallow |
87 | 87 | pillow |
88 | 88 | psycopg2 |
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 | { Babel | |
5 | , buildPythonPackage | |
6 | , email_validator | |
7 | , fetchPypi | |
8 | , flask | |
9 | , flask-babelex | |
10 | , flask_login | |
11 | , flask_mail | |
12 | , flask_principal | |
13 | , flask_wtf | |
14 | , itsdangerous | |
15 | , lib | |
16 | , passlib | |
17 | , pytestrunner | |
18 | , twine | |
19 | , wheel | |
20 | }: | |
21 | ||
22 | buildPythonPackage rec { | |
23 | pname = | |
24 | "flask-security-too"; | |
25 | version = | |
26 | "3.4.5"; | |
27 | ||
28 | src = | |
29 | fetchPypi { | |
30 | inherit | |
31 | version; | |
32 | pname = | |
33 | "Flask-Security-Too"; | |
34 | sha256 = | |
35 | "19cdad65bxs23zz5hmr41s12359ija3p2kk0mbf9jsk1swg0b7d0"; | |
36 | }; | |
37 | ||
38 | buildInputs = | |
39 | [ | |
40 | Babel | |
41 | pytestrunner | |
42 | twine | |
43 | wheel | |
44 | ]; | |
45 | propagatedBuildInputs = | |
46 | [ | |
47 | flask | |
48 | flask_login | |
49 | flask_mail | |
50 | flask_principal | |
51 | flask_wtf | |
52 | flask-babelex | |
53 | email_validator | |
54 | itsdangerous | |
55 | passlib | |
56 | ]; | |
57 | ||
58 | # TODO FIXME | |
59 | doCheck = | |
60 | false; | |
61 | ||
62 | meta = | |
63 | with lib; { | |
64 | description = | |
65 | "Simple security for Flask apps."; | |
66 | homepage = | |
67 | "https://github.com/Flask-Middleware/flask-security"; | |
68 | }; | |
69 | } |
9 | 9 | email_validator |
10 | 10 | WTForms>=2.1 |
11 | 11 | flask-login>=0.5.0 |
12 | Flask-Security>=3.0.0 | |
12 | Flask-Security-Too>=3.4.4,<4.0.0 | |
13 | 13 | marshmallow>=3.0.0 |
14 | 14 | Pillow>=4.2.1 |
15 | 15 | psycopg2 |
0 | 0 | from pathlib import Path |
1 | 1 | import os |
2 | 2 | import requests |
3 | import click | |
3 | 4 | |
4 | 5 | |
5 | 6 | VERSION = os.environ.get('FARADAY_VERSION') |
7 | TOKEN = os.environ.get('GH_TOKEN') | |
6 | 8 | |
7 | 9 | |
8 | def main(): | |
10 | @click.option("--deb-file", required=True, type=click.Path(exists=True,dir_okay=False,resolve_path=True)) | |
11 | @click.option("--rpm-file", required=True, type=click.Path(exists=True,dir_okay=False,resolve_path=True)) | |
12 | def main(deb_file,rpm_file): | |
9 | 13 | release_data = dict() |
10 | 14 | release_data["tag_name"] = f"v{VERSION}" |
11 | 15 | release_data["name"] = f"v{VERSION}" |
14 | 18 | ) as body_file: |
15 | 19 | release_data["body"] = body_file.read() |
16 | 20 | |
17 | headers = {'Accept': 'application/vnd.github.v3+json'} | |
21 | headers = { | |
22 | 'Accept': 'application/vnd.github.v3+json', | |
23 | 'Authorization': 'token ' + TOKEN, | |
24 | } | |
18 | 25 | res = requests.post( |
19 | 26 | "https://api.github.com/repos/infobyte/faraday/releases", |
20 | 27 | json=release_data, |
22 | 29 | ) |
23 | 30 | res.raise_for_status() |
24 | 31 | release_id = res.json()['id'] |
25 | # TODO ADD THIS | |
26 | # for asset_file in ["rpm", "deb"]: | |
27 | # | |
28 | # res = requests.post( | |
29 | # "https://api.github.com/repos/infobyte/faraday/releases/" | |
30 | # f"{release_id}/assets", | |
31 | # headers=headers, | |
32 | # files={ | |
33 | # 'file': ( | |
34 | # asset_file, # TODO FIX NAME | |
35 | # open(asset_file, mode="rb"), # TODO FIX NAME | |
36 | # asset_file # TODO FIX TYPE | |
37 | # ) | |
38 | # } | |
39 | # ) | |
40 | # res.raise_for_status() | |
32 | for asset_file_data in [{"file": Path(deb_file), "mimetype": "application/vnd.debian.binary-package"}, | |
33 | {"file": Path(rpm_file), "mimetype": "application/x-redhat-package-manager"}]: | |
34 | asset_file = asset_file_data["file"] | |
35 | res = requests.post( | |
36 | f"https://api.github.com/repos/infobyte/faraday/releases/{release_id}/assets", | |
37 | headers=headers, | |
38 | files={ | |
39 | 'file': ( | |
40 | asset_file.name, | |
41 | open(asset_file, mode="rb"), | |
42 | asset_file_data["mimetype"] | |
43 | ) | |
44 | } | |
45 | ) | |
46 | res.raise_for_status() | |
41 | 47 | |
42 | 48 | |
43 | 49 | if __name__ == '__main__': |
11 | 11 | import datetime |
12 | 12 | import itertools |
13 | 13 | import unicodedata |
14 | import time | |
14 | 15 | |
15 | 16 | import pytz |
16 | 17 | from factory import SubFactory |
111 | 112 | |
112 | 113 | @classmethod |
113 | 114 | def build_dict(cls, **kwargs): |
114 | ret = super(WorkspaceObjectFactory, cls).build_dict(**kwargs) | |
115 | ret = super().build_dict(**kwargs) | |
115 | 116 | del ret['workspace'] # It is passed in the URL, not in POST data |
116 | 117 | return ret |
117 | 118 | |
118 | 119 | |
120 | class FuzzyIncrementalInteger(BaseFuzzyAttribute): | |
121 | """Like a FuzzyInteger, but tries to prevent generating duplicated | |
122 | values""" | |
123 | ||
124 | def __init__(self, low, high, **kwargs): | |
125 | self.iterator = itertools.cycle(range(low, high - 1)) | |
126 | super(FuzzyIncrementalInteger, self).__init__(**kwargs) | |
127 | ||
128 | def fuzz(self): | |
129 | return next(self.iterator) | |
130 | ||
131 | ||
119 | 132 | class HostFactory(WorkspaceObjectFactory): |
133 | id = FuzzyIncrementalInteger(1, 65535) | |
120 | 134 | ip = FuzzyText() |
121 | 135 | description = FuzzyText() |
122 | 136 | os = FuzzyChoice(['Linux', 'Windows', 'OSX', 'Android', 'iOS']) |
158 | 172 | class Meta: |
159 | 173 | model = ReferenceTemplate |
160 | 174 | sqlalchemy_session = db.session |
161 | ||
162 | ||
163 | class FuzzyIncrementalInteger(BaseFuzzyAttribute): | |
164 | """Like a FuzzyInteger, but tries to prevent generating duplicated | |
165 | values""" | |
166 | ||
167 | def __init__(self, low, high, **kwargs): | |
168 | self.iterator = itertools.cycle(range(low, high - 1)) | |
169 | super(FuzzyIncrementalInteger, self).__init__(**kwargs) | |
170 | ||
171 | def fuzz(self): | |
172 | return next(self.iterator) | |
173 | 175 | |
174 | 176 | |
175 | 177 | class ServiceFactory(WorkspaceObjectFactory): |
185 | 187 | model = Service |
186 | 188 | sqlalchemy_session = db.session |
187 | 189 | |
190 | @classmethod | |
191 | def build_dict(cls, **kwargs): | |
192 | ret = super(ServiceFactory, cls).build_dict(**kwargs) | |
193 | ret['host'].workspace = kwargs['workspace'] | |
194 | ret['parent'] = ret['host'].id | |
195 | ret['ports'] = [ret['port']] | |
196 | ret.pop('host') | |
197 | return ret | |
198 | ||
188 | 199 | |
189 | 200 | class SourceCodeFactory(WorkspaceObjectFactory): |
190 | 201 | filename = FuzzyText() |
194 | 205 | sqlalchemy_session = db.session |
195 | 206 | |
196 | 207 | |
197 | class CustomFieldsSchemaFactory(factory.alchemy.SQLAlchemyModelFactory): | |
208 | class CustomFieldsSchemaFactory(FaradayFactory): | |
209 | ||
210 | field_name = FuzzyText() | |
211 | field_type = FuzzyText() | |
212 | field_display_name = FuzzyText() | |
213 | field_order = FuzzyInteger(1, 10) | |
214 | table_name = FuzzyText() | |
198 | 215 | |
199 | 216 | class Meta: |
200 | 217 | model = CustomFieldsSchema |
208 | 225 | severity = FuzzyChoice(['critical', 'high']) |
209 | 226 | |
210 | 227 | |
211 | class HasParentHostOrService: | |
228 | class HasParentHostOrService(WorkspaceObjectFactory): | |
212 | 229 | """ |
213 | 230 | Mixins for objects that must have either a host or a service, |
214 | 231 | but ont both, as a parent. |
226 | 243 | raise ValueError('You should pass both service and host and ' |
227 | 244 | 'set one of them to None to prevent random ' |
228 | 245 | 'stuff to happen') |
229 | return super(HasParentHostOrService, cls).attributes(create, extra) | |
246 | return super().attributes(create, extra) | |
230 | 247 | |
231 | 248 | @classmethod |
232 | 249 | def _after_postgeneration(cls, obj, create, results=None): |
233 | super(HasParentHostOrService, cls)._after_postgeneration( | |
234 | obj, create, results) | |
250 | super()._after_postgeneration(obj, create, results) | |
235 | 251 | if isinstance(obj, dict): |
236 | 252 | # This happens when built with build_dict |
237 | 253 | if obj['host'] and obj['service']: |
247 | 263 | obj.host = None |
248 | 264 | else: |
249 | 265 | obj.service = None |
250 | ||
251 | @classmethod | |
252 | def build_dict(cls, **kwargs): | |
253 | ret = super(HasParentHostOrService, cls).build_dict(**kwargs) | |
266 | session = cls._meta.sqlalchemy_session | |
267 | session.add(obj) | |
268 | session.commit() | |
269 | ||
270 | @classmethod | |
271 | def build_dict(cls, **kwargs): | |
272 | ret = super().build_dict(**kwargs) | |
254 | 273 | service = ret.pop('service') |
255 | 274 | host = ret.pop('host') |
256 | 275 | if host is not None: |
281 | 300 | return ret |
282 | 301 | |
283 | 302 | |
284 | class VulnerabilityFactory(HasParentHostOrService, | |
285 | VulnerabilityGenericFactory): | |
303 | class VulnerabilityFactory(VulnerabilityGenericFactory, | |
304 | HasParentHostOrService): | |
286 | 305 | |
287 | 306 | host = factory.SubFactory(HostFactory, workspace=factory.SelfAttribute('..workspace')) |
288 | 307 | service = factory.SubFactory(ServiceFactory, workspace=factory.SelfAttribute('..workspace')) |
308 | description = FuzzyText() | |
309 | type = "vulnerability" | |
310 | ||
311 | @classmethod | |
312 | def build_dict(cls, **kwargs): | |
313 | ret = super().build_dict(**kwargs) | |
314 | assert ret['type'] == 'vulnerability' | |
315 | ret['type'] = 'Vulnerability' | |
316 | return ret | |
289 | 317 | |
290 | 318 | class Meta: |
291 | 319 | model = Vulnerability |
296 | 324 | method = FuzzyChoice(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']) |
297 | 325 | parameter_name = FuzzyText() |
298 | 326 | service = factory.SubFactory(ServiceFactory, workspace=factory.SelfAttribute('..workspace')) |
327 | type = "vulnerability_web" | |
328 | ||
329 | ||
330 | @classmethod | |
331 | def build_dict(cls, **kwargs): | |
332 | ret = super(VulnerabilityWebFactory, cls).build_dict(**kwargs) | |
333 | assert ret['type'] == 'vulnerability_web' | |
334 | ret['type'] = 'VulnerabilityWeb' | |
335 | return ret | |
299 | 336 | |
300 | 337 | class Meta: |
301 | 338 | model = VulnerabilityWeb |
322 | 359 | class Meta: |
323 | 360 | model = VulnerabilityTemplate |
324 | 361 | sqlalchemy_session = db.session |
362 | ||
363 | ||
364 | @classmethod | |
365 | def build_dict(cls, **kwargs): | |
366 | ret = super(VulnerabilityTemplateFactory, cls).build_dict(**kwargs) | |
367 | ret['exploitation'] = ret['severity'] | |
368 | return ret | |
325 | 369 | |
326 | 370 | |
327 | 371 | class CredentialFactory(HasParentHostOrService, WorkspaceObjectFactory): |
381 | 425 | workspace=self.workspace |
382 | 426 | ) |
383 | 427 | |
428 | @classmethod | |
429 | def build_dict(cls, **kwargs): | |
430 | # Ugly hack to JSON-serialize datetimes | |
431 | ret = super(CommandFactory, cls).build_dict(**kwargs) | |
432 | ret['itime'] = time.mktime(ret['start_date'].utctimetuple()) | |
433 | ret['duration'] = (ret['end_date'] - ret['start_date']).seconds + ((ret['end_date'] - ret['start_date']).microseconds / 1000000.0) | |
434 | ret.pop('start_date') | |
435 | ret.pop('end_date') | |
436 | return ret | |
437 | ||
384 | 438 | |
385 | 439 | class EmptyCommandFactory(WorkspaceObjectFactory): |
386 | 440 | """ |
405 | 459 | A command without command objects. |
406 | 460 | """ |
407 | 461 | text = FuzzyText() |
408 | object_id = FuzzyInteger(1) | |
409 | object_type = FuzzyChoice(['host', 'service', 'comment']) | |
410 | ||
462 | object_id = FuzzyInteger(1, 10000) | |
463 | object_type = FuzzyChoice(['host', 'service', 'comment', 'vulnerability']) | |
464 | ||
465 | @classmethod | |
466 | def build_dict(cls, **kwargs): | |
467 | # The host, service or comment must be created | |
468 | ret = super(CommentFactory, cls).build_dict(**kwargs) | |
469 | workspace = kwargs['workspace'] | |
470 | if ret['object_type'] == 'host': | |
471 | HostFactory.create(workspace=workspace, id=ret['object_id']) | |
472 | elif ret['object_type'] == 'service': | |
473 | ServiceFactory.create(workspace=workspace, id=ret['object_id']) | |
474 | elif ret['object_type'] == 'vulnerability': | |
475 | VulnerabilityFactory.create(workspace=workspace, id=ret['object_id']) | |
476 | elif ret['object_type'] == 'comment': | |
477 | cls.create(workspace=workspace, id=ret['object_id']) | |
478 | return ret | |
411 | 479 | |
412 | 480 | class Meta: |
413 | 481 | model = Comment |
454 | 522 | class AgentFactory(FaradayFactory): |
455 | 523 | name = FuzzyText() |
456 | 524 | active = True |
525 | id = FuzzyIncrementalInteger(1, 10000) | |
457 | 526 | |
458 | 527 | @factory.post_generation |
459 | 528 | def workspaces(self, create, extracted, **kwargs): |
460 | 529 | if not create: |
461 | 530 | # Simple build, do nothing. |
462 | return | |
463 | ||
464 | if extracted: | |
531 | if extracted: | |
532 | # A list of groups were passed in, use them | |
533 | self['workspaces'] = [] | |
534 | for workspace in extracted: | |
535 | self['workspaces'].append(workspace.name) | |
536 | else: | |
537 | self['workspaces'] = [WorkspaceFactory().name, WorkspaceFactory().name] | |
538 | ||
539 | elif extracted: | |
465 | 540 | # A list of groups were passed in, use them |
466 | 541 | for workspace in extracted: |
467 | 542 | self.workspaces.append(workspace) |
469 | 544 | self.workspaces.append(WorkspaceFactory()) |
470 | 545 | self.workspaces.append(WorkspaceFactory()) |
471 | 546 | |
547 | @classmethod | |
548 | def build_dict(cls, **kwargs): | |
549 | return super(AgentFactory, cls).build_dict(**kwargs) | |
472 | 550 | |
473 | 551 | class Meta: |
474 | 552 | model = Agent |
479 | 557 | name = FuzzyText() |
480 | 558 | agent = factory.SubFactory(AgentFactory) |
481 | 559 | parameters_metadata = factory.LazyAttribute( |
482 | lambda e: str({"param_name": False}) | |
560 | lambda e: {"param_name": False} | |
483 | 561 | ) |
484 | 562 | class Meta: |
485 | 563 | model = Executor |
497 | 575 | lambda agent_execution: agent_execution.executor.agent.workspaces[0] |
498 | 576 | ) |
499 | 577 | command = factory.SubFactory( |
500 | CommandFactory, | |
578 | EmptyCommandFactory, | |
501 | 579 | workspace=factory.SelfAttribute("..workspace"), |
502 | 580 | end_date=None |
503 | 581 | ) |
167 | 167 | |
168 | 168 | def test_create_workspace_and_vuln_with_childs( |
169 | 169 | self, session, vulnerability_factory, child): |
170 | # This sould raise an error since the workspace_id won't be propagated | |
171 | # to the childs | |
170 | # This should not raise an error since the workspace will be propagated | |
171 | # to the childs as created vuln has a workspace and is persisted | |
172 | 172 | vuln = vulnerability_factory.create() |
173 | with pytest.raises(AssertionError): | |
174 | setattr(vuln, self.field_name, {'CVE-2017-1234'}) | |
173 | setattr(vuln, self.field_name, {'CVE-2017-1234'}) | |
175 | 174 | |
176 | 175 | def test_create_vuln_with_childs(self, session, vulnerability_factory): |
177 | 176 | vuln = vulnerability_factory.build() |
251 | 250 | session.commit() |
252 | 251 | assert vulnerability.creator_command_id == command.id |
253 | 252 | assert vulnerability.creator_command_tool == command.tool |
254 | # I'm Py3⏎ | |
253 | # I'm Py3 |
11 | 11 | EmptyCommandFactory, |
12 | 12 | HostFactory, |
13 | 13 | CommandObjectFactory) |
14 | from tests.utils.url import v2_to_v3 | |
15 | ||
14 | 16 | |
15 | 17 | @pytest.mark.usefixtures('logged_user') |
16 | class TestActivityFeed(): | |
18 | class TestActivityFeed: | |
19 | ||
20 | def check_url(self, url): | |
21 | return url | |
17 | 22 | |
18 | 23 | @pytest.mark.usefixtures('ignore_nplusone') |
19 | 24 | def test_activity_feed(self, test_client, session): |
24 | 29 | session.commit() |
25 | 30 | |
26 | 31 | res = test_client.get( |
27 | f'/v2/ws/{ws.name}/activities/' | |
32 | self.check_url(f'/v2/ws/{ws.name}/activities/') | |
28 | 33 | ) |
29 | 34 | |
30 | 35 | assert res.status_code == 200 |
51 | 56 | } |
52 | 57 | |
53 | 58 | res = test_client.put( |
54 | f'/v2/ws/{ws.name}/activities/{command.id}/', | |
59 | self.check_url(f'/v2/ws/{ws.name}/activities/{command.id}/'), | |
55 | 60 | data=data, |
56 | 61 | ) |
57 | 62 | assert res.status_code == 200 |
130 | 135 | workspace=workspace |
131 | 136 | ) |
132 | 137 | session.commit() |
133 | res = test_client.get(f'/v2/ws/{command.workspace.name}/activities/') | |
138 | res = test_client.get(self.check_url(f'/v2/ws/{command.workspace.name}/activities/')) | |
134 | 139 | assert res.status_code == 200 |
135 | 140 | assert res.json['activities'][0]['vulnerabilities_count'] == 8 |
136 | 141 | assert res.json['activities'][0]['criticalIssue'] == 1 |
139 | 144 | assert res.json['activities'][0]['lowIssue'] == 1 |
140 | 145 | assert res.json['activities'][0]['infoIssue'] == 2 |
141 | 146 | assert res.json['activities'][0]['unclassifiedIssue'] == 1 |
147 | ||
148 | ||
149 | class TestActivityFeedV3(TestActivityFeed): | |
150 | def check_url(self, url): | |
151 | return v2_to_v3(url) |
4 | 4 | """ |
5 | 5 | |
6 | 6 | from unittest import mock |
7 | from posixpath import join as urljoin | |
7 | 8 | import pytest |
8 | 9 | |
9 | 10 | from faraday.server.api.modules.agent import AgentWithWorkspacesView, AgentView |
10 | 11 | from faraday.server.models import Agent, Command |
11 | 12 | from tests.factories import AgentFactory, WorkspaceFactory, ExecutorFactory |
12 | from tests.test_api_non_workspaced_base import ReadOnlyAPITests | |
13 | from tests.test_api_workspaced_base import ReadOnlyMultiWorkspacedAPITests | |
13 | from tests.test_api_non_workspaced_base import ReadWriteAPITests, OBJECT_COUNT, PatchableTestsMixin | |
14 | from tests.test_api_workspaced_base import ReadWriteMultiWorkspacedAPITests, ReadOnlyMultiWorkspacedAPITests | |
14 | 15 | from tests import factories |
15 | 16 | from tests.test_api_workspaced_base import API_PREFIX |
17 | from tests.utils.url import v2_to_v3 | |
16 | 18 | |
17 | 19 | |
18 | 20 | def http_req(method, client, endpoint, json_dict, expected_status_codes, follow_redirects=False): |
52 | 54 | |
53 | 55 | |
54 | 56 | @pytest.mark.usefixtures('logged_user') |
55 | class TestAgentAuthTokenAPIGeneric(): | |
57 | class TestAgentAuthTokenAPIGeneric: | |
58 | ||
59 | def check_url(self, url): | |
60 | return url | |
56 | 61 | |
57 | 62 | @mock.patch('faraday.server.api.modules.agent.faraday_server') |
58 | 63 | def test_create_agent_token(self, faraday_server_config, test_client, session): |
59 | 64 | faraday_server_config.agent_token = None |
60 | res = test_client.get('/v2/agent_token/') | |
65 | res = test_client.get(self.check_url('/v2/agent_token/')) | |
61 | 66 | assert 'token' in res.json |
62 | 67 | assert len(res.json['token']) |
63 | 68 | |
64 | 69 | @mock.patch('faraday.server.api.modules.agent.faraday_server') |
65 | 70 | def test_create_agent_token_without_csrf_fails(self, faraday_server_config, test_client, session): |
66 | 71 | faraday_server_config.agent_token = None |
67 | res = test_client.post('/v2/agent_token/') | |
72 | res = test_client.post(self.check_url('/v2/agent_token/')) | |
68 | 73 | assert res.status_code == 403 |
69 | 74 | |
70 | 75 | @mock.patch('faraday.server.api.modules.agent.faraday_server') |
71 | 76 | def test_create_new_agent_token(self, faraday_server_config, test_client, session, csrf_token): |
72 | 77 | faraday_server_config.agent_token = None |
73 | 78 | headers = {'Content-type': 'multipart/form-data'} |
74 | res = test_client.post('/v2/agent_token/', | |
79 | res = test_client.post(self.check_url('/v2/agent_token/'), | |
75 | 80 | data={"csrf_token": csrf_token}, |
76 | 81 | headers=headers, |
77 | 82 | use_json_data=False) |
79 | 84 | assert len(res.json['token']) |
80 | 85 | |
81 | 86 | |
82 | class TestAgentCreationAPI(): | |
87 | @pytest.mark.usefixtures('logged_user') | |
88 | class TestAgentAuthTokenAPIGenericV3(TestAgentAuthTokenAPIGeneric): | |
89 | def check_url(self, url): | |
90 | return v2_to_v3(url) | |
91 | ||
92 | ||
93 | class TestAgentCreationAPI: | |
94 | ||
95 | def check_url(self, url): | |
96 | return url | |
83 | 97 | |
84 | 98 | @mock.patch('faraday.server.api.modules.agent.faraday_server') |
85 | 99 | @pytest.mark.usefixtures('ignore_nplusone') |
99 | 113 | workspaces=[workspace, other_workspace] |
100 | 114 | ) |
101 | 115 | # /v2/agent_registration/ |
102 | res = test_client.post('/v2/agent_registration/', data=raw_data) | |
116 | res = test_client.post(self.check_url('/v2/agent_registration/'), data=raw_data) | |
103 | 117 | assert res.status_code == 201, (res.json, raw_data) |
104 | 118 | assert len(session.query(Agent).all()) == initial_agent_count + 1 |
105 | 119 | assert workspace.name in res.json['workspaces'] |
126 | 140 | ) |
127 | 141 | # /v2/agent_registration/ |
128 | 142 | res = test_client.post( |
129 | '/v2/agent_registration/', | |
143 | self.check_url('/v2/agent_registration/'), | |
130 | 144 | data=raw_data |
131 | 145 | ) |
132 | 146 | assert res.status_code == 400 |
146 | 160 | workspaces=[workspace] |
147 | 161 | ) |
148 | 162 | # /v2/agent_registration/ |
149 | res = test_client.post('/v2/agent_registration/', data=raw_data) | |
163 | res = test_client.post(self.check_url('/v2/agent_registration/'), data=raw_data) | |
150 | 164 | assert res.status_code == 401 |
151 | 165 | |
152 | 166 | @mock.patch('faraday.server.api.modules.agent.faraday_server') |
162 | 176 | workspaces=[workspace], |
163 | 177 | ) |
164 | 178 | # /v2/agent_registration/ |
165 | res = test_client.post('/v2/agent_registration/', data=raw_data) | |
179 | res = test_client.post(self.check_url('/v2/agent_registration/'), data=raw_data) | |
166 | 180 | assert res.status_code == 400 |
167 | 181 | |
168 | 182 | @mock.patch('faraday.server.api.modules.agent.faraday_server') |
172 | 186 | logout(test_client, [302]) |
173 | 187 | raw_data = {"PEPE": 'INVALID'} |
174 | 188 | # /v2/agent_registration/ |
175 | res = test_client.post('/v2/agent_registration/', data=raw_data) | |
189 | res = test_client.post(self.check_url('/v2/agent_registration/'), data=raw_data) | |
176 | 190 | assert res.status_code == 400 |
177 | 191 | |
178 | 192 | @mock.patch('faraday.server.api.modules.agent.faraday_server') |
189 | 203 | workspaces=[] |
190 | 204 | ) |
191 | 205 | # /v2/agent_registration/ |
192 | res = test_client.post('/v2/agent_registration/', data=raw_data) | |
206 | res = test_client.post(self.check_url('/v2/agent_registration/'), data=raw_data) | |
193 | 207 | assert res.status_code == 400 |
194 | 208 | |
195 | 209 | @mock.patch('faraday.server.api.modules.agent.faraday_server') |
207 | 221 | ) |
208 | 222 | raw_data["workspaces"] = ["donotexist"] |
209 | 223 | # /v2/agent_registration/ |
210 | res = test_client.post('/v2/agent_registration/', data=raw_data) | |
224 | res = test_client.post(self.check_url('/v2/agent_registration/'), data=raw_data) | |
211 | 225 | assert res.status_code == 404 |
212 | 226 | |
213 | 227 | @mock.patch('faraday.server.api.modules.agent.faraday_server') |
223 | 237 | token='sarasa' |
224 | 238 | ) |
225 | 239 | # /v2/agent_registration/ |
226 | res = test_client.post('/v2/agent_registration/', data=raw_data) | |
227 | assert res.status_code == 400 | |
228 | ||
229 | ||
230 | class TestAgentWithWorkspacesAPIGeneric(ReadOnlyAPITests): | |
240 | res = test_client.post(self.check_url('/v2/agent_registration/'), data=raw_data) | |
241 | assert res.status_code == 400 | |
242 | ||
243 | ||
244 | class TestAgentCreationAPIV3(TestAgentCreationAPI): | |
245 | def check_url(self, url): | |
246 | return v2_to_v3(url) | |
247 | ||
248 | ||
249 | class TestAgentWithWorkspacesAPIGeneric(ReadWriteAPITests): | |
231 | 250 | model = Agent |
232 | 251 | factory = factories.AgentFactory |
233 | 252 | view_class = AgentWithWorkspacesView |
234 | 253 | api_endpoint = 'agents' |
254 | patchable_fields = ['name'] | |
255 | ||
256 | def test_create_succeeds(self, test_client): | |
257 | with pytest.raises(AssertionError) as exc_info: | |
258 | super(TestAgentWithWorkspacesAPIGeneric, self).test_create_succeeds(test_client) | |
259 | assert '405' in exc_info.value.args[0] | |
260 | ||
261 | def test_create_fails_with_empty_dict(self, test_client): | |
262 | with pytest.raises(AssertionError) as exc_info: | |
263 | super(TestAgentWithWorkspacesAPIGeneric, self).test_create_fails_with_empty_dict(test_client) | |
264 | assert '405' in exc_info.value.args[0] | |
235 | 265 | |
236 | 266 | def workspaced_url(self, workspace, obj= None): |
237 | 267 | url = API_PREFIX + workspace.name + '/' + self.api_endpoint + '/' |
392 | 422 | ) |
393 | 423 | assert res.status_code == 404 |
394 | 424 | |
425 | ||
426 | class TestAgentWithWorkspacesAPIGenericV3(TestAgentWithWorkspacesAPIGeneric, PatchableTestsMixin): | |
427 | def url(self, obj=None): | |
428 | return v2_to_v3(super(TestAgentWithWorkspacesAPIGenericV3, self).url(obj)) | |
429 | ||
430 | ||
395 | 431 | class TestAgentAPI(ReadOnlyMultiWorkspacedAPITests): |
396 | 432 | model = Agent |
397 | 433 | factory = factories.AgentFactory |
398 | 434 | view_class = AgentView |
399 | 435 | api_endpoint = 'agents' |
436 | ||
437 | def check_url(self, url): | |
438 | return url | |
400 | 439 | |
401 | 440 | def test_get_workspaced(self, test_client, session): |
402 | 441 | workspace = WorkspaceFactory.create() |
444 | 483 | 'csrf_token': csrf_token |
445 | 484 | } |
446 | 485 | res = test_client.post( |
447 | self.url(agent) + 'run/', | |
486 | self.check_url(urljoin(self.url(agent), 'run/')), | |
448 | 487 | json=payload |
449 | 488 | ) |
450 | 489 | assert res.status_code == 400 |
454 | 493 | session.add(agent) |
455 | 494 | session.commit() |
456 | 495 | res = test_client.post( |
457 | self.url(agent) + 'run/', | |
496 | self.check_url(urljoin(self.url(agent), 'run/')), | |
458 | 497 | data='[" broken]"{' |
459 | 498 | ) |
460 | 499 | assert res.status_code == 400 |
476 | 515 | ('content-type', 'text/html'), |
477 | 516 | ] |
478 | 517 | res = test_client.post( |
479 | self.url(agent) + 'run/', | |
518 | self.check_url(urljoin(self.url(agent), 'run/')), | |
480 | 519 | data=payload, |
481 | 520 | headers=headers) |
482 | 521 | assert res.status_code == 400 |
495 | 534 | }, |
496 | 535 | } |
497 | 536 | res = test_client.post( |
498 | self.url(agent) + 'run/', | |
537 | self.check_url(urljoin(self.url(agent), 'run/')), | |
499 | 538 | json=payload |
500 | 539 | ) |
501 | 540 | assert res.status_code == 400 |
516 | 555 | }, |
517 | 556 | } |
518 | 557 | res = test_client.post( |
519 | self.url(agent.id) + 'run/', | |
558 | self.check_url(urljoin(self.url(agent), 'run/')), | |
520 | 559 | json=payload |
521 | 560 | ) |
522 | 561 | assert res.status_code == 200 |
534 | 573 | 'executorData': '[][dassa', |
535 | 574 | } |
536 | 575 | res = test_client.post( |
537 | self.url(agent) + 'run/', | |
576 | self.check_url(urljoin(self.url(agent), 'run/')), | |
538 | 577 | json=payload |
539 | 578 | ) |
540 | 579 | assert res.status_code == 400 |
548 | 587 | 'executorData': '', |
549 | 588 | } |
550 | 589 | res = test_client.post( |
551 | self.url(agent) + 'run/', | |
590 | self.check_url(urljoin(self.url(agent), 'run/')), | |
552 | 591 | json=payload |
553 | 592 | ) |
554 | 593 | assert res.status_code == 400 |
594 | ||
595 | ||
596 | class TestAgentAPIV3(TestAgentAPI): | |
597 | def url(self, obj=None, workspace=None): | |
598 | return v2_to_v3(super(TestAgentAPIV3, self).url(obj, workspace)) | |
599 | ||
600 | def check_url(self, url): | |
601 | return v2_to_v3(url) |
0 | 0 | from datetime import datetime, timedelta, timezone |
1 | import string | |
1 | 2 | |
2 | 3 | import pytest |
3 | 4 | from marshmallow import ValidationError |
15 | 16 | ) |
16 | 17 | from faraday.server.api.modules import bulk_create as bc |
17 | 18 | from tests.factories import CustomFieldsSchemaFactory |
19 | from tests.utils.url import v2_to_v3 | |
18 | 20 | |
19 | 21 | host_data = { |
20 | 22 | "ip": "127.0.0.1", |
40 | 42 | 'refs': ['CVE-1234'], |
41 | 43 | 'tool': 'some_tool', |
42 | 44 | 'data': 'test data', |
45 | 'custom_fields': {} | |
43 | 46 | } |
44 | 47 | |
45 | 48 | vuln_web_data = { |
567 | 570 | ).one() |
568 | 571 | |
569 | 572 | |
570 | @pytest.mark.usefixtures('logged_user') | |
571 | def test_bulk_create_endpoint(session, workspace, test_client, logged_user): | |
572 | assert count(Host, workspace) == 0 | |
573 | assert count(VulnerabilityGeneric, workspace) == 0 | |
574 | url = f'v2/ws/{workspace.name}/bulk_create/' | |
573 | def test_bulk_create_update_service(session, service): | |
574 | session.add(service) | |
575 | session.commit() | |
576 | new_service_version = f"{service.name}_changed" | |
577 | new_service_name = f"{service.name}_changed" | |
578 | new_service_description = f"{service.description}_changed" | |
579 | new_service_owned = not service.owned | |
580 | data = { | |
581 | "version": new_service_version, | |
582 | "name": new_service_name, | |
583 | "description": new_service_description, | |
584 | "port": service.port, | |
585 | "protocol": service.protocol, | |
586 | "owned": new_service_owned, | |
587 | } | |
588 | data = bc.BulkServiceSchema().load(data) | |
589 | bc._create_service(service.workspace, service.host, data) | |
590 | assert count(Service, service.host.workspace) == 1 | |
591 | assert service.version == new_service_version | |
592 | assert service.name == new_service_name | |
593 | assert service.description == new_service_description | |
594 | assert service.owned == new_service_owned | |
595 | ||
596 | ||
597 | def test_sanitize_request_and_response(session, workspace, host): | |
598 | invalid_request_text = 'GET /exampla.do HTTP/1.0\n \x89\n\x1a SOME_TEXT' | |
599 | invalid_response_text = '<html> \x89\n\x1a SOME_TEXT</html>' | |
600 | sanitized_request_text = 'GET /exampla.do HTTP/1.0\n \n SOME_TEXT' | |
601 | sanitized_response_text = '<html> \n SOME_TEXT</html>' | |
575 | 602 | host_data_ = host_data.copy() |
576 | 603 | service_data_ = service_data.copy() |
577 | service_data_['vulnerabilities'] = [vuln_data] | |
604 | vuln_web_data_ = vuln_web_data.copy() | |
605 | vuln_web_data_['name'] = 'test' | |
606 | vuln_web_data_['severity'] = 'low' | |
607 | vuln_web_data_['request'] = invalid_request_text | |
608 | vuln_web_data_['response'] = invalid_response_text | |
609 | service_data_['vulnerabilities'] = [vuln_web_data_] | |
578 | 610 | host_data_['services'] = [service_data_] |
579 | host_data_['credentials'] = [credential_data] | |
580 | host_data_['vulnerabilities'] = [vuln_data] | |
581 | res = test_client.post( | |
582 | url, | |
583 | data=dict(hosts=[host_data_], command=command_data) | |
611 | command = new_empty_command(workspace) | |
612 | bc.bulk_create( | |
613 | workspace, | |
614 | command, | |
615 | dict(command=command_data, hosts=[host_data_]) | |
584 | 616 | ) |
585 | assert res.status_code == 201, res.json | |
586 | assert count(Host, workspace) == 1 | |
587 | assert count(Service, workspace) == 1 | |
588 | assert count(Vulnerability, workspace) == 2 | |
589 | assert count(Command, workspace) == 1 | |
590 | host = Host.query.filter(Host.workspace == workspace).one() | |
591 | assert host.ip == "127.0.0.1" | |
592 | assert host.creator_id == logged_user.id | |
593 | assert set({hn.name for hn in host.hostnames}) == {"test.com", "test2.org"} | |
594 | assert len(host.services) == 1 | |
595 | assert len(host.vulnerabilities) == 1 | |
596 | assert len(host.services[0].vulnerabilities) == 1 | |
597 | service = Service.query.filter(Service.workspace == workspace).one() | |
598 | assert service.creator_id == logged_user.id | |
599 | credential = Credential.query.filter(Credential.workspace == workspace).one() | |
600 | assert credential.creator_id == logged_user.id | |
601 | command = Command.query.filter(Credential.workspace == workspace).one() | |
602 | assert command.creator_id == logged_user.id | |
603 | assert res.json["command_id"] == command.id | |
604 | ||
605 | ||
606 | @pytest.mark.usefixtures('logged_user') | |
607 | def test_bulk_create_endpoint_run_over_closed_vuln(session, workspace, test_client): | |
608 | assert count(Host, workspace) == 0 | |
609 | assert count(VulnerabilityGeneric, workspace) == 0 | |
610 | url = f'v2/ws/{workspace.name}/bulk_create/' | |
611 | host_data_ = host_data.copy() | |
612 | host_data_['vulnerabilities'] = [vuln_data] | |
613 | res = test_client.post(url, data=dict(hosts=[host_data_])) | |
614 | assert res.status_code == 201, res.json | |
615 | assert count(Host, workspace) == 1 | |
616 | assert count(Vulnerability, workspace) == 1 | |
617 | host = Host.query.filter(Host.workspace == workspace).one() | |
618 | vuln = Vulnerability.query.filter(Vulnerability.workspace == workspace).one() | |
619 | assert host.ip == "127.0.0.1" | |
620 | assert set({hn.name for hn in host.hostnames}) == {"test.com", "test2.org"} | |
621 | assert vuln.status == "open" | |
622 | close_url = f"v2/ws/{workspace.name}/vulns/{vuln.id}/" | |
623 | res = test_client.get(close_url) | |
624 | vuln_data_del = res.json | |
625 | vuln_data_del["status"] = "closed" | |
626 | res = test_client.put(close_url, data=dict(vuln_data_del)) | |
627 | assert res.status_code == 200, res.json | |
628 | assert count(Host, workspace) == 1 | |
629 | assert count(Vulnerability, workspace) == 1 | |
630 | assert vuln.status == "closed" | |
631 | res = test_client.post(url, data=dict(hosts=[host_data_])) | |
632 | assert res.status_code == 201, res.json | |
633 | assert count(Host, workspace) == 1 | |
634 | assert count(Vulnerability, workspace) == 1 | |
635 | vuln = Vulnerability.query.filter(Vulnerability.workspace == workspace).one() | |
636 | assert vuln.status == "re-opened" | |
637 | ||
638 | ||
639 | @pytest.mark.usefixtures('logged_user') | |
640 | def test_bulk_create_endpoint_without_host_ip(session, workspace, test_client): | |
641 | url = f'v2/ws/{workspace.name}/bulk_create/' | |
642 | host_data_ = host_data.copy() | |
643 | host_data_.pop('ip') | |
644 | res = test_client.post(url, data=dict(hosts=[host_data_])) | |
645 | assert res.status_code == 400 | |
646 | ||
647 | ||
648 | def test_bulk_create_endpoints_fails_without_auth(session, workspace, test_client): | |
649 | url = f'v2/ws/{workspace.name}/bulk_create/' | |
650 | res = test_client.post(url, data=dict(hosts=[host_data])) | |
651 | assert res.status_code == 401 | |
652 | assert count(Host, workspace) == 0 | |
653 | ||
654 | ||
655 | @pytest.mark.parametrize('token_type', ['agent', 'token']) | |
656 | def test_bulk_create_endpoints_fails_with_invalid_token( | |
657 | session, token_type, workspace, test_client): | |
658 | url = f'v2/ws/{workspace.name}/bulk_create/' | |
659 | res = test_client.post( | |
660 | url, | |
661 | data=dict(hosts=[host_data]), | |
662 | headers=[("authorization", f"{token_type} 1234")] | |
663 | ) | |
664 | if token_type == 'token': | |
665 | # TODO change expected status code to 403 | |
617 | vuln = VulnerabilityWeb.query.filter(VulnerabilityWeb.workspace == workspace).one() | |
618 | assert vuln.request == sanitized_request_text | |
619 | assert vuln.response == sanitized_response_text | |
620 | ||
621 | ||
622 | class TestBulkCreateAPI: | |
623 | ||
624 | def check_url(self, url): | |
625 | return url | |
626 | ||
627 | @pytest.mark.usefixtures('logged_user') | |
628 | def test_bulk_create_endpoint(self, session, workspace, test_client, logged_user): | |
629 | assert count(Host, workspace) == 0 | |
630 | assert count(VulnerabilityGeneric, workspace) == 0 | |
631 | url = self.check_url(f'/v2/ws/{workspace.name}/bulk_create/') | |
632 | host_data_ = host_data.copy() | |
633 | service_data_ = service_data.copy() | |
634 | service_data_['vulnerabilities'] = [vuln_data] | |
635 | host_data_['services'] = [service_data_] | |
636 | host_data_['credentials'] = [credential_data] | |
637 | host_data_['vulnerabilities'] = [vuln_data] | |
638 | res = test_client.post( | |
639 | url, | |
640 | data=dict(hosts=[host_data_], command=command_data) | |
641 | ) | |
642 | assert res.status_code == 201, res.json | |
643 | assert count(Host, workspace) == 1 | |
644 | assert count(Service, workspace) == 1 | |
645 | assert count(Vulnerability, workspace) == 2 | |
646 | assert count(Command, workspace) == 1 | |
647 | host = Host.query.filter(Host.workspace == workspace).one() | |
648 | assert host.ip == "127.0.0.1" | |
649 | assert host.creator_id == logged_user.id | |
650 | assert set({hn.name for hn in host.hostnames}) == {"test.com", "test2.org"} | |
651 | assert len(host.services) == 1 | |
652 | assert len(host.vulnerabilities) == 1 | |
653 | assert len(host.services[0].vulnerabilities) == 1 | |
654 | service = Service.query.filter(Service.workspace == workspace).one() | |
655 | assert service.creator_id == logged_user.id | |
656 | credential = Credential.query.filter(Credential.workspace == workspace).one() | |
657 | assert credential.creator_id == logged_user.id | |
658 | command = Command.query.filter(Credential.workspace == workspace).one() | |
659 | assert command.creator_id == logged_user.id | |
660 | assert res.json["command_id"] == command.id | |
661 | ||
662 | @pytest.mark.usefixtures('logged_user') | |
663 | def test_bulk_create_endpoint_run_over_closed_vuln(self, session, workspace, test_client): | |
664 | assert count(Host, workspace) == 0 | |
665 | assert count(VulnerabilityGeneric, workspace) == 0 | |
666 | url = self.check_url(f'/v2/ws/{workspace.name}/bulk_create/') | |
667 | host_data_ = host_data.copy() | |
668 | host_data_['vulnerabilities'] = [vuln_data] | |
669 | res = test_client.post(url, data=dict(hosts=[host_data_])) | |
670 | assert res.status_code == 201, res.json | |
671 | assert count(Host, workspace) == 1 | |
672 | assert count(Vulnerability, workspace) == 1 | |
673 | host = Host.query.filter(Host.workspace == workspace).one() | |
674 | vuln = Vulnerability.query.filter(Vulnerability.workspace == workspace).one() | |
675 | assert host.ip == "127.0.0.1" | |
676 | assert set({hn.name for hn in host.hostnames}) == {"test.com", "test2.org"} | |
677 | assert vuln.status == "open" | |
678 | close_url = self.check_url(f"/v2/ws/{workspace.name}/vulns/{vuln.id}/") | |
679 | res = test_client.get(close_url) | |
680 | vuln_data_del = res.json | |
681 | vuln_data_del["status"] = "closed" | |
682 | res = test_client.put(close_url, data=dict(vuln_data_del)) | |
683 | assert res.status_code == 200, res.json | |
684 | assert count(Host, workspace) == 1 | |
685 | assert count(Vulnerability, workspace) == 1 | |
686 | assert vuln.status == "closed" | |
687 | res = test_client.post(url, data=dict(hosts=[host_data_])) | |
688 | assert res.status_code == 201, res.json | |
689 | assert count(Host, workspace) == 1 | |
690 | assert count(Vulnerability, workspace) == 1 | |
691 | vuln = Vulnerability.query.filter(Vulnerability.workspace == workspace).one() | |
692 | assert vuln.status == "re-opened" | |
693 | ||
694 | @pytest.mark.usefixtures('logged_user') | |
695 | def test_bulk_create_endpoint_without_host_ip(self, session, workspace, test_client): | |
696 | url = self.check_url(f'/v2/ws/{workspace.name}/bulk_create/') | |
697 | host_data_ = host_data.copy() | |
698 | host_data_.pop('ip') | |
699 | res = test_client.post(url, data=dict(hosts=[host_data_])) | |
700 | assert res.status_code == 400 | |
701 | ||
702 | def test_bulk_create_endpoints_fails_without_auth(self, session, workspace, test_client): | |
703 | url = self.check_url(f'/v2/ws/{workspace.name}/bulk_create/') | |
704 | res = test_client.post(url, data=dict(hosts=[host_data])) | |
666 | 705 | assert res.status_code == 401 |
667 | else: | |
668 | assert res.status_code == 403 | |
669 | assert count(Host, workspace) == 0 | |
670 | ||
671 | ||
672 | def test_bulk_create_with_agent_token_in_different_workspace_fails( | |
673 | session, agent, second_workspace, test_client): | |
674 | assert agent.workspaces | |
675 | assert second_workspace not in agent.workspaces | |
676 | session.add(second_workspace) | |
677 | session.add(agent) | |
678 | session.commit() | |
679 | assert agent.token | |
680 | url = f'v2/ws/{second_workspace.name}/bulk_create/' | |
681 | res = test_client.post( | |
682 | url, | |
683 | data=dict(hosts=[host_data]), | |
684 | headers=[("authorization", f"agent {agent.token}")] | |
685 | ) | |
686 | assert res.status_code == 404 | |
687 | assert b'No such workspace' in res.data | |
688 | assert count(Host, second_workspace) == 0 | |
689 | ||
690 | ||
691 | def test_bulk_create_with_not_existent_workspace_fails( | |
692 | session, agent, test_client): | |
693 | assert agent.workspaces | |
694 | session.add(agent) | |
695 | session.commit() | |
696 | assert agent.token | |
697 | url = "v2/ws/im_a_incorrect_ws/bulk_create/" | |
698 | res = test_client.post( | |
699 | url, | |
700 | data=dict(hosts=[host_data]), | |
701 | headers=[("authorization", f"agent {agent.token}")] | |
702 | ) | |
703 | assert res.status_code == 404 | |
704 | assert b'No such workspace' in res.data | |
705 | for workspace in agent.workspaces: | |
706 | 706 | assert count(Host, workspace) == 0 |
707 | 707 | |
708 | ||
709 | def test_bulk_create_endpoint_with_agent_token_without_execution_id(session, agent, test_client): | |
710 | session.add(agent) | |
711 | session.commit() | |
712 | for workspace in agent.workspaces: | |
708 | @pytest.mark.parametrize('token_type', ['agent', 'token']) | |
709 | def test_bulk_create_endpoints_fails_with_invalid_token(self, token_type, workspace, test_client): | |
710 | url = self.check_url(f'/v2/ws/{workspace.name}/bulk_create/') | |
711 | res = test_client.post( | |
712 | url, | |
713 | data=dict(hosts=[host_data]), | |
714 | headers=[("authorization", f"{token_type} 1234")] | |
715 | ) | |
716 | if token_type == 'token': | |
717 | # TODO change expected status code to 403 | |
718 | assert res.status_code == 401 | |
719 | else: | |
720 | assert res.status_code == 403 | |
713 | 721 | assert count(Host, workspace) == 0 |
714 | url = f'v2/ws/{workspace.name}/bulk_create/' | |
722 | ||
723 | def test_bulk_create_with_agent_token_in_different_workspace_fails( | |
724 | self, session, agent, second_workspace, test_client): | |
725 | assert agent.workspaces | |
726 | assert second_workspace not in agent.workspaces | |
727 | session.add(second_workspace) | |
728 | session.add(agent) | |
729 | session.commit() | |
730 | assert agent.token | |
731 | url = self.check_url(f'/v2/ws/{second_workspace.name}/bulk_create/') | |
715 | 732 | res = test_client.post( |
716 | 733 | url, |
717 | 734 | data=dict(hosts=[host_data]), |
718 | 735 | headers=[("authorization", f"agent {agent.token}")] |
719 | 736 | ) |
720 | assert res.status_code == 400 | |
721 | assert b"\'execution_id\' argument expected" in res.data | |
722 | assert count(Host, workspace) == 0 | |
723 | assert count(Command, workspace) == 0 | |
724 | ||
725 | ||
726 | @pytest.mark.parametrize('start_date', [None, datetime.now()]) | |
727 | @pytest.mark.parametrize('duration', [None, 1200]) | |
728 | def test_bulk_create_endpoint_with_agent_token(session, | |
729 | test_client, | |
730 | agent_execution_factory, | |
731 | start_date, duration): | |
732 | agent_execution = agent_execution_factory.create() | |
733 | agent = agent_execution.executor.agent | |
734 | extra_agent_execution = agent_execution_factory.create() | |
735 | ||
736 | for workspace in agent.workspaces: | |
737 | agent_execution.executor.parameters_metadata = {} | |
738 | agent_execution.parameters_data = {} | |
739 | agent_execution.workspace = workspace | |
740 | agent_execution.command.workspace = workspace | |
741 | session.add(agent_execution) | |
742 | session.add(extra_agent_execution) | |
737 | assert res.status_code == 404 | |
738 | assert b'No such workspace' in res.data | |
739 | assert count(Host, second_workspace) == 0 | |
740 | ||
741 | def test_bulk_create_with_not_existent_workspace_fails(self, session, agent, test_client): | |
742 | assert agent.workspaces | |
743 | session.add(agent) | |
743 | 744 | session.commit() |
744 | ||
745 | command_data = {} | |
746 | if start_date: | |
747 | command_data.update({ | |
748 | 'tool': agent.name, # Agent name | |
749 | 'command': agent_execution.executor.name, | |
750 | 'user': '', | |
751 | 'hostname': '', | |
752 | 'params': '', | |
753 | 'import_source': 'agent', | |
754 | 'start_date': str(start_date) | |
755 | }) | |
756 | if duration: | |
757 | command_data.update({ | |
758 | 'tool': agent.name, # Agent name | |
759 | 'command': agent_execution.executor.name, | |
760 | 'user': '', | |
761 | 'hostname': '', | |
762 | 'params': '', | |
763 | 'import_source': 'agent', | |
764 | 'duration': str(duration) | |
765 | }) | |
766 | ||
767 | data_kwargs = { | |
768 | "hosts": [host_data], | |
769 | "execution_id": -1 | |
770 | } | |
771 | if command_data: | |
772 | data_kwargs["command"] = command_data | |
773 | ||
774 | initial_host_count = Host.query.filter(Host.workspace == workspace and Host.creator_id is None).count() | |
775 | assert count(Command, workspace) == 1 | |
776 | url = f'v2/ws/{workspace.name}/bulk_create/' | |
777 | res = test_client.post( | |
778 | url, | |
779 | data=dict(**data_kwargs), | |
780 | headers=[("authorization", f"agent {agent.token}")] | |
781 | ) | |
782 | assert res.status_code == 400 | |
783 | ||
784 | assert Host.query.filter(Host.workspace == workspace and Host.creator_id is None).count() == initial_host_count | |
785 | assert count(Command, workspace) == 1 | |
786 | data_kwargs["execution_id"] = extra_agent_execution.id | |
787 | res = test_client.post( | |
788 | url, | |
789 | data=dict(**data_kwargs), | |
790 | headers=[("authorization", f"agent {agent.token}")] | |
791 | ) | |
792 | assert res.status_code == 400 | |
793 | assert Host.query.filter(Host.workspace == workspace and Host.creator_id is None).count() == initial_host_count | |
794 | assert count(Command, workspace) == 1 | |
795 | data_kwargs["execution_id"] = agent_execution.id | |
796 | res = test_client.post( | |
797 | url, | |
798 | data=dict(**data_kwargs), | |
799 | headers=[("authorization", f"agent {agent.token}")] | |
800 | ) | |
801 | ||
802 | if start_date or duration is None: | |
803 | assert res.status_code == 201, res.json | |
804 | assert Host.query.filter(Host.workspace == workspace and Host.creator_id is None).count() == \ | |
805 | initial_host_count + 1 | |
806 | assert count(Command, workspace) == 1 | |
807 | command = Command.query.filter(Command.workspace == workspace).one() | |
808 | assert command.tool == agent.name | |
809 | assert command.command == agent_execution.executor.name | |
810 | assert command.params == "" | |
811 | assert command.import_source == 'agent' | |
812 | command_id = res.json["command_id"] | |
813 | assert command.id == command_id | |
814 | assert command.id == agent_execution.command.id | |
815 | assert command.start_date is not None | |
816 | if duration is None: | |
817 | assert command.end_date is None | |
818 | else: | |
819 | assert command.end_date == command.start_date + timedelta(microseconds=duration) | |
820 | else: | |
821 | assert res.status_code == 400, res.json | |
822 | ||
823 | ||
824 | ||
825 | def test_bulk_create_endpoint_with_agent_token_with_param(session, agent_execution, test_client): | |
826 | agent = agent_execution.executor.agent | |
827 | session.add(agent_execution) | |
828 | session.commit() | |
829 | for workspace in agent.workspaces: | |
830 | agent_execution.workspace = workspace | |
831 | agent_execution.command.workspace = workspace | |
832 | session.add(agent_execution) | |
833 | session.commit() | |
834 | assert count(Host, workspace) == 0 | |
835 | assert count(Command, workspace) == 1 | |
836 | url = f'v2/ws/{workspace.name}/bulk_create/' | |
837 | res = test_client.post( | |
838 | url, | |
839 | data=dict(hosts=[host_data], execution_id=agent_execution.id), | |
840 | headers=[("authorization", f"agent {agent.token}")] | |
841 | ) | |
842 | assert res.status_code == 201 | |
843 | assert count(Host, workspace) == 1 | |
844 | host = Host.query.filter(Host.workspace == workspace).one() | |
845 | assert host.creator_id is None | |
846 | assert count(Command, workspace) == 1 | |
847 | command = Command.query.filter(Command.workspace == workspace).one() | |
848 | assert command.tool == agent.name | |
849 | assert command.command == agent_execution.executor.name | |
850 | params = ', '.join([f'{key}={value}' for (key, value) in agent_execution.parameters_data.items()]) | |
851 | assert command.params == str(params) | |
852 | assert command.import_source == 'agent' | |
853 | command_id = res.json["command_id"] | |
854 | assert command.id == command_id | |
855 | assert command.id == agent_execution.command.id | |
856 | ||
857 | ||
858 | def test_bulk_create_endpoint_with_agent_token_readonly_workspace( | |
859 | session, agent, test_client): | |
860 | for workspace in agent.workspaces: | |
861 | workspace.readonly = True | |
862 | session.add(agent) | |
863 | session.add(workspace) | |
864 | session.commit() | |
865 | for workspace in agent.workspaces: | |
866 | ||
867 | url = f'v2/ws/{workspace.name}/bulk_create/' | |
745 | assert agent.token | |
746 | url = self.check_url("/v2/ws/im_a_incorrect_ws/bulk_create/") | |
868 | 747 | res = test_client.post( |
869 | 748 | url, |
870 | 749 | data=dict(hosts=[host_data]), |
871 | 750 | headers=[("authorization", f"agent {agent.token}")] |
872 | 751 | ) |
873 | assert res.status_code == 403 | |
874 | ||
875 | ||
876 | def test_bulk_create_endpoint_with_agent_token_disabled_workspace( | |
877 | session, agent, test_client): | |
878 | for workspace in agent.workspaces: | |
879 | workspace.active = False | |
752 | assert res.status_code == 404 | |
753 | assert b'No such workspace' in res.data | |
754 | for workspace in agent.workspaces: | |
755 | assert count(Host, workspace) == 0 | |
756 | ||
757 | def test_bulk_create_endpoint_with_agent_token_without_execution_id(self, session, agent, test_client): | |
880 | 758 | session.add(agent) |
881 | session.add(workspace) | |
882 | session.commit() | |
883 | for workspace in agent.workspaces: | |
884 | url = f'v2/ws/{workspace.name}/bulk_create/' | |
759 | session.commit() | |
760 | for workspace in agent.workspaces: | |
761 | assert count(Host, workspace) == 0 | |
762 | url = self.check_url(f'/v2/ws/{workspace.name}/bulk_create/') | |
763 | res = test_client.post( | |
764 | url, | |
765 | data=dict(hosts=[host_data]), | |
766 | headers=[("authorization", f"agent {agent.token}")] | |
767 | ) | |
768 | assert res.status_code == 400 | |
769 | assert b"\'execution_id\' argument expected" in res.data | |
770 | assert count(Host, workspace) == 0 | |
771 | assert count(Command, workspace) == 0 | |
772 | ||
773 | @pytest.mark.parametrize('start_date', [None, datetime.now()]) | |
774 | @pytest.mark.parametrize('duration', [None, 1200]) | |
775 | def test_bulk_create_endpoint_with_agent_token(self, | |
776 | session, | |
777 | test_client, | |
778 | agent_execution_factory, | |
779 | start_date, duration): | |
780 | agent_execution = agent_execution_factory.create() | |
781 | agent = agent_execution.executor.agent | |
782 | extra_agent_execution = agent_execution_factory.create() | |
783 | ||
784 | for workspace in agent.workspaces: | |
785 | agent_execution.executor.parameters_metadata = {} | |
786 | agent_execution.parameters_data = {} | |
787 | agent_execution.workspace = workspace | |
788 | agent_execution.command.workspace = workspace | |
789 | session.add(agent_execution) | |
790 | session.add(extra_agent_execution) | |
791 | session.commit() | |
792 | ||
793 | command_data = {} | |
794 | if start_date: | |
795 | command_data.update({ | |
796 | 'tool': agent.name, # Agent name | |
797 | 'command': agent_execution.executor.name, | |
798 | 'user': '', | |
799 | 'hostname': '', | |
800 | 'params': '', | |
801 | 'import_source': 'agent', | |
802 | 'start_date': str(start_date) | |
803 | }) | |
804 | if duration: | |
805 | command_data.update({ | |
806 | 'tool': agent.name, # Agent name | |
807 | 'command': agent_execution.executor.name, | |
808 | 'user': '', | |
809 | 'hostname': '', | |
810 | 'params': '', | |
811 | 'import_source': 'agent', | |
812 | 'duration': str(duration) | |
813 | }) | |
814 | ||
815 | data_kwargs = { | |
816 | "hosts": [host_data], | |
817 | "execution_id": -1 | |
818 | } | |
819 | if command_data: | |
820 | data_kwargs["command"] = command_data | |
821 | ||
822 | initial_host_count = Host.query.filter(Host.workspace == workspace and Host.creator_id is None).count() | |
823 | assert count(Command, workspace) == 1 | |
824 | url = self.check_url(f'/v2/ws/{workspace.name}/bulk_create/') | |
825 | res = test_client.post( | |
826 | url, | |
827 | data=dict(**data_kwargs), | |
828 | headers=[("authorization", f"agent {agent.token}")] | |
829 | ) | |
830 | assert res.status_code == 400 | |
831 | ||
832 | assert Host.query.filter(Host.workspace == workspace and Host.creator_id is None).count() == initial_host_count | |
833 | assert count(Command, workspace) == 1 | |
834 | data_kwargs["execution_id"] = extra_agent_execution.id | |
835 | res = test_client.post( | |
836 | url, | |
837 | data=dict(**data_kwargs), | |
838 | headers=[("authorization", f"agent {agent.token}")] | |
839 | ) | |
840 | assert res.status_code == 400 | |
841 | assert Host.query.filter(Host.workspace == workspace and Host.creator_id is None).count() == initial_host_count | |
842 | assert count(Command, workspace) == 1 | |
843 | data_kwargs["execution_id"] = agent_execution.id | |
844 | res = test_client.post( | |
845 | url, | |
846 | data=dict(**data_kwargs), | |
847 | headers=[("authorization", f"agent {agent.token}")] | |
848 | ) | |
849 | ||
850 | if start_date or duration is None: | |
851 | assert res.status_code == 201, res.json | |
852 | assert Host.query.filter(Host.workspace == workspace and Host.creator_id is None).count() == \ | |
853 | initial_host_count + 1 | |
854 | assert count(Command, workspace) == 1 | |
855 | command = Command.query.filter(Command.workspace == workspace).one() | |
856 | assert command.tool == agent.name | |
857 | assert command.command == agent_execution.executor.name | |
858 | assert command.params == "" | |
859 | assert command.import_source == 'agent' | |
860 | command_id = res.json["command_id"] | |
861 | assert command.id == command_id | |
862 | assert command.id == agent_execution.command.id | |
863 | assert command.start_date is not None | |
864 | if duration is None: | |
865 | assert command.end_date is None | |
866 | else: | |
867 | assert command.end_date == command.start_date + timedelta(microseconds=duration) | |
868 | else: | |
869 | assert res.status_code == 400, res.json | |
870 | ||
871 | def test_bulk_create_endpoint_with_agent_token_with_param(self, session, agent_execution, test_client): | |
872 | agent = agent_execution.executor.agent | |
873 | session.add(agent_execution) | |
874 | session.commit() | |
875 | for workspace in agent.workspaces: | |
876 | agent_execution.workspace = workspace | |
877 | agent_execution.command.workspace = workspace | |
878 | session.add(agent_execution) | |
879 | session.commit() | |
880 | assert count(Host, workspace) == 0 | |
881 | assert count(Command, workspace) == 1 | |
882 | url = self.check_url(f'/v2/ws/{workspace.name}/bulk_create/') | |
883 | res = test_client.post( | |
884 | url, | |
885 | data=dict(hosts=[host_data], execution_id=agent_execution.id), | |
886 | headers=[("authorization", f"agent {agent.token}")] | |
887 | ) | |
888 | assert res.status_code == 201 | |
889 | assert count(Host, workspace) == 1 | |
890 | host = Host.query.filter(Host.workspace == workspace).one() | |
891 | assert host.creator_id is None | |
892 | assert count(Command, workspace) == 1 | |
893 | command = Command.query.filter(Command.workspace == workspace).one() | |
894 | assert command.tool == agent.name | |
895 | assert command.command == agent_execution.executor.name | |
896 | params = ', '.join([f'{key}={value}' for (key, value) in agent_execution.parameters_data.items()]) | |
897 | assert command.params == str(params) | |
898 | assert command.import_source == 'agent' | |
899 | command_id = res.json["command_id"] | |
900 | assert command.id == command_id | |
901 | assert command.id == agent_execution.command.id | |
902 | ||
903 | def test_bulk_create_endpoint_with_agent_token_readonly_workspace(self, session, agent, test_client): | |
904 | for workspace in agent.workspaces: | |
905 | workspace.readonly = True | |
906 | session.add(agent) | |
907 | session.add(workspace) | |
908 | session.commit() | |
909 | for workspace in agent.workspaces: | |
910 | ||
911 | url = self.check_url(f'/v2/ws/{workspace.name}/bulk_create/') | |
912 | res = test_client.post( | |
913 | url, | |
914 | data=dict(hosts=[host_data]), | |
915 | headers=[("authorization", f"agent {agent.token}")] | |
916 | ) | |
917 | assert res.status_code == 403 | |
918 | ||
919 | def test_bulk_create_endpoint_with_agent_token_disabled_workspace(self, session, agent, test_client): | |
920 | for workspace in agent.workspaces: | |
921 | workspace.active = False | |
922 | session.add(agent) | |
923 | session.add(workspace) | |
924 | session.commit() | |
925 | for workspace in agent.workspaces: | |
926 | url = self.check_url(f'/v2/ws/{workspace.name}/bulk_create/') | |
927 | res = test_client.post( | |
928 | url, | |
929 | data=dict(hosts=[host_data]), | |
930 | headers=[("authorization", f"agent {agent.token}")] | |
931 | ) | |
932 | assert res.status_code == 403 | |
933 | ||
934 | @pytest.mark.usefixtures('logged_user') | |
935 | def test_bulk_create_endpoint_raises_400_with_no_data(self, session, test_client, workspace): | |
936 | url = self.check_url(f'/v2/ws/{workspace.name}/bulk_create/') | |
885 | 937 | res = test_client.post( |
886 | 938 | url, |
887 | data=dict(hosts=[host_data]), | |
888 | headers=[("authorization", f"agent {agent.token}")] | |
939 | data="", | |
940 | use_json_data=False, | |
941 | headers=[("Content-Type", "application/json")] | |
889 | 942 | ) |
890 | assert res.status_code == 403 | |
891 | ||
892 | @pytest.mark.usefixtures('logged_user') | |
893 | def test_bulk_create_endpoint_raises_400_with_no_data( | |
894 | session, test_client, workspace): | |
895 | url = f'v2/ws/{workspace.name}/bulk_create/' | |
896 | res = test_client.post( | |
897 | url, | |
898 | data="", | |
899 | use_json_data=False, | |
900 | headers=[("Content-Type", "application/json")] | |
901 | ) | |
902 | assert res.status_code == 400 | |
903 | ||
904 | @pytest.mark.usefixtures('logged_user') | |
905 | def test_bulk_create_endpoint_with_vuln_run_date(session, workspace, test_client): | |
906 | assert count(Host, workspace) == 0 | |
907 | assert count(VulnerabilityGeneric, workspace) == 0 | |
908 | url = f'v2/ws/{workspace.name}/bulk_create/' | |
909 | run_date = datetime.now(timezone.utc) - timedelta(days=30) | |
910 | host_data_copy = host_data.copy() | |
911 | vuln_data_copy = vuln_data.copy() | |
912 | vuln_data_copy['run_date'] = run_date.timestamp() | |
913 | host_data_copy['vulnerabilities'] = [vuln_data_copy] | |
914 | res = test_client.post(url, data=dict(hosts=[host_data_copy])) | |
915 | assert res.status_code == 201, res.json | |
916 | assert count(Host, workspace) == 1 | |
917 | assert count(VulnerabilityGeneric, workspace) == 1 | |
918 | vuln = Vulnerability.query.filter(Vulnerability.workspace == workspace).one() | |
919 | assert vuln.create_date.date() == run_date.date() | |
920 | ||
921 | @pytest.mark.usefixtures('logged_user') | |
922 | def test_bulk_create_endpoint_with_vuln_future_run_date(session, workspace, test_client): | |
923 | assert count(Host, workspace) == 0 | |
924 | assert count(VulnerabilityGeneric, workspace) == 0 | |
925 | url = f'v2/ws/{workspace.name}/bulk_create/' | |
926 | run_date = datetime.now(timezone.utc) + timedelta(days=10) | |
927 | host_data_copy = host_data.copy() | |
928 | vuln_data_copy = vuln_data.copy() | |
929 | vuln_data_copy['run_date'] = run_date.timestamp() | |
930 | host_data_copy['vulnerabilities'] = [vuln_data_copy] | |
931 | res = test_client.post(url, data=dict(hosts=[host_data_copy])) | |
932 | assert res.status_code == 201, res.json | |
933 | assert count(Host, workspace) == 1 | |
934 | assert count(VulnerabilityGeneric, workspace) == 1 | |
935 | vuln = Vulnerability.query.filter(Vulnerability.workspace == workspace).one() | |
936 | print(vuln.create_date) | |
937 | assert vuln.create_date.date() < run_date.date() | |
938 | ||
939 | @pytest.mark.usefixtures('logged_user') | |
940 | def test_bulk_create_endpoint_with_invalid_vuln_run_date(session, workspace, test_client): | |
941 | assert count(Host, workspace) == 0 | |
942 | assert count(VulnerabilityGeneric, workspace) == 0 | |
943 | url = f'v2/ws/{workspace.name}/bulk_create/' | |
944 | host_data_copy = host_data.copy() | |
945 | vuln_data_copy = vuln_data.copy() | |
946 | vuln_data_copy['run_date'] = "INVALID_VALUE" | |
947 | host_data_copy['vulnerabilities'] = [vuln_data_copy] | |
948 | res = test_client.post(url, data=dict(hosts=[host_data_copy])) | |
949 | assert res.status_code == 400, res.json | |
950 | assert count(VulnerabilityGeneric, workspace) == 0 | |
951 | ||
952 | ||
953 | @pytest.mark.usefixtures('logged_user') | |
954 | def test_bulk_create_endpoint_fails_with_list_in_NullToBlankString(session, workspace, test_client, logged_user): | |
955 | assert count(Host, workspace) == 0 | |
956 | assert count(VulnerabilityGeneric, workspace) == 0 | |
957 | url = f'v2/ws/{workspace.name}/bulk_create/' | |
958 | host_data_ = host_data.copy() | |
959 | host_data_['services'] = [service_data] | |
960 | host_data_['credentials'] = [credential_data] | |
961 | host_data_['vulnerabilities'] = [vuln_data] | |
962 | host_data_['default_gateway'] = ["localhost"] # Can not be a list | |
963 | res = test_client.post(url, data=dict(hosts=[host_data_])) | |
964 | assert res.status_code == 400, res.json | |
965 | assert count(Host, workspace) == 0 | |
966 | assert count(Service, workspace) == 0 | |
967 | assert count(Credential, workspace) == 0 | |
968 | assert count(Vulnerability, workspace) == 0 | |
969 | ||
970 | ||
971 | @pytest.mark.usefixtures('logged_user') | |
972 | def test_bulk_create_with_custom_fields_list(test_client, workspace, session, logged_user): | |
973 | custom_field_schema = CustomFieldsSchemaFactory( | |
974 | field_name='changes', | |
975 | field_type='list', | |
976 | field_display_name='Changes', | |
977 | table_name='vulnerability' | |
978 | ) | |
979 | session.add(custom_field_schema) | |
980 | session.commit() | |
981 | ||
982 | assert count(Host, workspace) == 0 | |
983 | assert count(VulnerabilityGeneric, workspace) == 0 | |
984 | url = f'v2/ws/{workspace.name}/bulk_create/' | |
985 | host_data_ = host_data.copy() | |
986 | service_data_ = service_data.copy() | |
987 | vuln_data_ = vuln_data.copy() | |
988 | vuln_data_['custom_fields'] = {'changes': ['1', '2', '3']} | |
989 | service_data_['vulnerabilities'] = [vuln_data_] | |
990 | host_data_['services'] = [service_data_] | |
991 | host_data_['credentials'] = [credential_data] | |
992 | host_data_['vulnerabilities'] = [vuln_data_] | |
993 | res = test_client.post( | |
994 | url, | |
995 | data=dict(hosts=[host_data_], command=command_data) | |
996 | ) | |
997 | assert res.status_code == 201, res.json | |
998 | assert count(Host, workspace) == 1 | |
999 | assert count(Service, workspace) == 1 | |
1000 | assert count(Vulnerability, workspace) == 2 | |
1001 | assert count(Command, workspace) == 1 | |
1002 | host = Host.query.filter(Host.workspace == workspace).one() | |
1003 | assert host.ip == "127.0.0.1" | |
1004 | assert host.creator_id == logged_user.id | |
1005 | assert set({hn.name for hn in host.hostnames}) == {"test.com", "test2.org"} | |
1006 | assert len(host.services) == 1 | |
1007 | assert len(host.vulnerabilities) == 1 | |
1008 | assert len(host.services[0].vulnerabilities) == 1 | |
1009 | service = Service.query.filter(Service.workspace == workspace).one() | |
1010 | assert service.creator_id == logged_user.id | |
1011 | credential = Credential.query.filter(Credential.workspace == workspace).one() | |
1012 | assert credential.creator_id == logged_user.id | |
1013 | command = Command.query.filter(Credential.workspace == workspace).one() | |
1014 | assert command.creator_id == logged_user.id | |
1015 | assert res.json["command_id"] == command.id | |
1016 | for vuln in Vulnerability.query.filter(Vulnerability.workspace == workspace): | |
1017 | assert vuln.custom_fields['changes'] == ['1', '2', '3'] | |
1018 | ||
1019 | ||
1020 | @pytest.mark.usefixtures('logged_user') | |
1021 | def test_vuln_web_cannot_have_host_parent(session, workspace, test_client, logged_user): | |
1022 | url = f'v2/ws/{workspace.name}/bulk_create/' | |
1023 | host_data_ = host_data.copy() | |
1024 | vuln_web_data_ = vuln_web_data.copy() | |
1025 | vuln_web_data_['severity'] = "high" | |
1026 | vuln_web_data_['name'] = "test" | |
1027 | host_data_['vulnerabilities'] = [vuln_web_data_] | |
1028 | res = test_client.post( | |
1029 | url, | |
1030 | data=dict(hosts=[host_data_], command=command_data) | |
1031 | ) | |
1032 | assert res.status_code == 400 | |
943 | assert res.status_code == 400 | |
944 | ||
945 | @pytest.mark.usefixtures('logged_user') | |
946 | def test_bulk_create_endpoint_with_vuln_run_date(self, session, workspace, test_client): | |
947 | assert count(Host, workspace) == 0 | |
948 | assert count(VulnerabilityGeneric, workspace) == 0 | |
949 | url = self.check_url(f'/v2/ws/{workspace.name}/bulk_create/') | |
950 | run_date = datetime.now(timezone.utc) - timedelta(days=30) | |
951 | host_data_copy = host_data.copy() | |
952 | vuln_data_copy = vuln_data.copy() | |
953 | vuln_data_copy['run_date'] = run_date.timestamp() | |
954 | host_data_copy['vulnerabilities'] = [vuln_data_copy] | |
955 | res = test_client.post(url, data=dict(hosts=[host_data_copy])) | |
956 | assert res.status_code == 201, res.json | |
957 | assert count(Host, workspace) == 1 | |
958 | assert count(VulnerabilityGeneric, workspace) == 1 | |
959 | vuln = Vulnerability.query.filter(Vulnerability.workspace == workspace).one() | |
960 | assert vuln.create_date.date() == run_date.date() | |
961 | ||
962 | @pytest.mark.usefixtures('logged_user') | |
963 | def test_bulk_create_endpoint_with_vuln_future_run_date(self, session, workspace, test_client): | |
964 | assert count(Host, workspace) == 0 | |
965 | assert count(VulnerabilityGeneric, workspace) == 0 | |
966 | url = self.check_url(f'/v2/ws/{workspace.name}/bulk_create/') | |
967 | run_date = datetime.now(timezone.utc) + timedelta(days=10) | |
968 | host_data_copy = host_data.copy() | |
969 | vuln_data_copy = vuln_data.copy() | |
970 | vuln_data_copy['run_date'] = run_date.timestamp() | |
971 | host_data_copy['vulnerabilities'] = [vuln_data_copy] | |
972 | res = test_client.post(url, data=dict(hosts=[host_data_copy])) | |
973 | assert res.status_code == 201, res.json | |
974 | assert count(Host, workspace) == 1 | |
975 | assert count(VulnerabilityGeneric, workspace) == 1 | |
976 | vuln = Vulnerability.query.filter(Vulnerability.workspace == workspace).one() | |
977 | print(vuln.create_date) | |
978 | assert vuln.create_date.date() < run_date.date() | |
979 | ||
980 | @pytest.mark.usefixtures('logged_user') | |
981 | def test_bulk_create_endpoint_with_invalid_vuln_run_date(self, session, workspace, test_client): | |
982 | assert count(Host, workspace) == 0 | |
983 | assert count(VulnerabilityGeneric, workspace) == 0 | |
984 | url = self.check_url(f'/v2/ws/{workspace.name}/bulk_create/') | |
985 | host_data_copy = host_data.copy() | |
986 | vuln_data_copy = vuln_data.copy() | |
987 | vuln_data_copy['run_date'] = "INVALID_VALUE" | |
988 | host_data_copy['vulnerabilities'] = [vuln_data_copy] | |
989 | res = test_client.post(url, data=dict(hosts=[host_data_copy])) | |
990 | assert res.status_code == 400, res.json | |
991 | assert count(VulnerabilityGeneric, workspace) == 0 | |
992 | ||
993 | @pytest.mark.usefixtures('logged_user') | |
994 | def test_bulk_create_endpoint_fails_with_list_in_NullToBlankString(self, session, workspace, test_client, | |
995 | logged_user): | |
996 | assert count(Host, workspace) == 0 | |
997 | assert count(VulnerabilityGeneric, workspace) == 0 | |
998 | url = self.check_url(f'/v2/ws/{workspace.name}/bulk_create/') | |
999 | host_data_ = host_data.copy() | |
1000 | host_data_['services'] = [service_data] | |
1001 | host_data_['credentials'] = [credential_data] | |
1002 | host_data_['vulnerabilities'] = [vuln_data] | |
1003 | host_data_['default_gateway'] = ["localhost"] # Can not be a list | |
1004 | res = test_client.post(url, data=dict(hosts=[host_data_])) | |
1005 | assert res.status_code == 400, res.json | |
1006 | assert count(Host, workspace) == 0 | |
1007 | assert count(Service, workspace) == 0 | |
1008 | assert count(Credential, workspace) == 0 | |
1009 | assert count(Vulnerability, workspace) == 0 | |
1010 | ||
1011 | @pytest.mark.usefixtures('logged_user') | |
1012 | def test_bulk_create_with_custom_fields_list(self, test_client, workspace, session, logged_user): | |
1013 | custom_field_schema = CustomFieldsSchemaFactory( | |
1014 | field_name='changes', | |
1015 | field_type='list', | |
1016 | field_display_name='Changes', | |
1017 | table_name='vulnerability' | |
1018 | ) | |
1019 | session.add(custom_field_schema) | |
1020 | session.commit() | |
1021 | ||
1022 | assert count(Host, workspace) == 0 | |
1023 | assert count(VulnerabilityGeneric, workspace) == 0 | |
1024 | url = self.check_url(f'/v2/ws/{workspace.name}/bulk_create/') | |
1025 | host_data_ = host_data.copy() | |
1026 | service_data_ = service_data.copy() | |
1027 | vuln_data_ = vuln_data.copy() | |
1028 | vuln_data_['custom_fields'] = {'changes': ['1', '2', '3']} | |
1029 | service_data_['vulnerabilities'] = [vuln_data_] | |
1030 | host_data_['services'] = [service_data_] | |
1031 | host_data_['credentials'] = [credential_data] | |
1032 | host_data_['vulnerabilities'] = [vuln_data_] | |
1033 | res = test_client.post( | |
1034 | url, | |
1035 | data=dict(hosts=[host_data_], command=command_data) | |
1036 | ) | |
1037 | assert res.status_code == 201, res.json | |
1038 | assert count(Host, workspace) == 1 | |
1039 | assert count(Service, workspace) == 1 | |
1040 | assert count(Vulnerability, workspace) == 2 | |
1041 | assert count(Command, workspace) == 1 | |
1042 | host = Host.query.filter(Host.workspace == workspace).one() | |
1043 | assert host.ip == "127.0.0.1" | |
1044 | assert host.creator_id == logged_user.id | |
1045 | assert set({hn.name for hn in host.hostnames}) == {"test.com", "test2.org"} | |
1046 | assert len(host.services) == 1 | |
1047 | assert len(host.vulnerabilities) == 1 | |
1048 | assert len(host.services[0].vulnerabilities) == 1 | |
1049 | service = Service.query.filter(Service.workspace == workspace).one() | |
1050 | assert service.creator_id == logged_user.id | |
1051 | credential = Credential.query.filter(Credential.workspace == workspace).one() | |
1052 | assert credential.creator_id == logged_user.id | |
1053 | command = Command.query.filter(Credential.workspace == workspace).one() | |
1054 | assert command.creator_id == logged_user.id | |
1055 | assert res.json["command_id"] == command.id | |
1056 | for vuln in Vulnerability.query.filter(Vulnerability.workspace == workspace): | |
1057 | assert vuln.custom_fields['changes'] == ['1', '2', '3'] | |
1058 | ||
1059 | @pytest.mark.usefixtures('logged_user') | |
1060 | def test_vuln_web_cannot_have_host_parent(self, session, workspace, test_client, logged_user): | |
1061 | url = self.check_url(f'/v2/ws/{workspace.name}/bulk_create/') | |
1062 | host_data_ = host_data.copy() | |
1063 | vuln_web_data_ = vuln_web_data.copy() | |
1064 | vuln_web_data_['severity'] = "high" | |
1065 | vuln_web_data_['name'] = "test" | |
1066 | host_data_['vulnerabilities'] = [vuln_web_data_] | |
1067 | res = test_client.post( | |
1068 | url, | |
1069 | data=dict(hosts=[host_data_], command=command_data) | |
1070 | ) | |
1071 | assert res.status_code == 400 | |
1072 | ||
1073 | ||
1074 | class TestBulkCreateAPIV3(TestBulkCreateAPI): | |
1075 | def check_url(self, url): | |
1076 | return v2_to_v3(url) |
4 | 4 | See the file 'doc/LICENSE' for the license information |
5 | 5 | |
6 | 6 | ''' |
7 | from tests.utils.url import v2_to_v3 | |
7 | 8 | |
8 | 9 | """Tests for many API endpoints that do not depend on workspace_name""" |
10 | from posixpath import join as urljoin | |
9 | 11 | import datetime |
10 | 12 | import pytest |
11 | 13 | import time |
12 | 14 | |
13 | 15 | from tests import factories |
14 | from tests.test_api_workspaced_base import API_PREFIX, ReadOnlyAPITests | |
16 | from tests.test_api_workspaced_base import API_PREFIX, ReadWriteAPITests, PatchableTestsMixin | |
15 | 17 | from faraday.server.models import ( |
16 | 18 | Command, |
17 | 19 | Workspace, |
18 | 20 | Vulnerability) |
19 | from faraday.server.api.modules.commandsrun import CommandView | |
21 | from faraday.server.api.modules.commandsrun import CommandView, CommandV3View | |
20 | 22 | from faraday.server.api.modules.workspaces import WorkspaceView |
21 | 23 | from tests.factories import VulnerabilityFactory, EmptyCommandFactory, CommandObjectFactory, HostFactory, \ |
22 | 24 | WorkspaceFactory, ServiceFactory |
29 | 31 | # and https://github.com/pytest-dev/pytest/issues/568 for more information |
30 | 32 | |
31 | 33 | @pytest.mark.usefixtures('logged_user') |
32 | class TestListCommandView(ReadOnlyAPITests): | |
34 | class TestListCommandView(ReadWriteAPITests): | |
33 | 35 | model = Command |
34 | 36 | factory = factories.CommandFactory |
35 | 37 | api_endpoint = 'commands' |
36 | 38 | view_class = CommandView |
39 | patchable_fields = ["ip"] | |
40 | ||
41 | def check_url(self, url): | |
42 | return url | |
37 | 43 | |
38 | 44 | @pytest.mark.usefixtures('ignore_nplusone') |
39 | 45 | @pytest.mark.usefixtures('mock_envelope_list') |
64 | 70 | u'tool', |
65 | 71 | u'import_source', |
66 | 72 | u'creator', |
73 | u'metadata' | |
67 | 74 | ] |
68 | 75 | assert command['value']['workspace'] == self.workspace.name |
69 | 76 | assert set(object_properties) == set(command['value'].keys()) |
90 | 97 | workspace=command.workspace |
91 | 98 | ) |
92 | 99 | session.commit() |
93 | res = test_client.get(self.url(workspace=command.workspace) + 'activity_feed/') | |
100 | ||
101 | res = test_client.get(self.check_url(urljoin(self.url(workspace=command.workspace), 'activity_feed/'))) | |
94 | 102 | assert res.status_code == 200 |
95 | 103 | |
96 | 104 | assert list(filter(lambda stats: stats['_id'] == command.id, res.json)) == [ |
147 | 155 | workspace=workspace |
148 | 156 | ) |
149 | 157 | session.commit() |
150 | res = test_client.get(self.url(workspace=command.workspace) + 'activity_feed/') | |
158 | res = test_client.get(self.check_url(urljoin(self.url(workspace=command.workspace), 'activity_feed/'))) | |
151 | 159 | assert res.status_code == 200 |
152 | 160 | assert res.json == [ |
153 | 161 | {u'_id': command.id, |
196 | 204 | workspace=workspace |
197 | 205 | ) |
198 | 206 | session.commit() |
199 | res = test_client.get(self.url(workspace=command.workspace) + 'activity_feed/') | |
207 | res = test_client.get(self.check_url(urljoin(self.url(workspace=command.workspace), 'activity_feed/'))) | |
200 | 208 | assert res.status_code == 200 |
201 | 209 | assert res.json == [{ |
202 | 210 | u'_id': command.id, |
262 | 270 | workspace=workspace |
263 | 271 | ) |
264 | 272 | session.commit() |
265 | res = test_client.get(self.url(workspace=command.workspace) + 'activity_feed/') | |
273 | res = test_client.get(self.check_url(urljoin(self.url(workspace=command.workspace), 'activity_feed/'))) | |
266 | 274 | assert res.status_code == 200 |
267 | 275 | raw_first_command = list(filter(lambda comm: comm['_id'] == commands[0].id, res.json)) |
268 | 276 | |
308 | 316 | u'vulnerabilities_count': 1, |
309 | 317 | u'criticalIssue': 0} |
310 | 318 | |
319 | @pytest.mark.usefixtures('ignore_nplusone') | |
311 | 320 | def test_sub_second_command_returns_correct_duration_value(self, test_client): |
312 | 321 | command = self.factory( |
313 | 322 | start_date=datetime.datetime(2017, 11, 14, 12, 29, 21, 248433), |
317 | 326 | assert res.status_code == 200 |
318 | 327 | assert res.json['commands'][0]['value']['duration'] == 0.442406 |
319 | 328 | |
329 | @pytest.mark.usefixtures('ignore_nplusone') | |
320 | 330 | def test_more_than_one_second_command_returns_correct_duration_value(self, test_client): |
321 | 331 | command = self.factory( |
322 | 332 | start_date=datetime.datetime(2017, 11, 14, 12, 29, 20, 248433), |
326 | 336 | assert res.status_code == 200 |
327 | 337 | assert res.json['commands'][0]['value']['duration'] == 1.442406 |
328 | 338 | |
339 | @pytest.mark.usefixtures('ignore_nplusone') | |
329 | 340 | def test_more_than_one_minute_command_returns_correct_duration_value(self, test_client): |
330 | 341 | command = self.factory( |
331 | 342 | start_date=datetime.datetime(2017, 11, 14, 12, 28, 20, 248433), |
335 | 346 | assert res.status_code == 200 |
336 | 347 | assert res.json['commands'][0]['value']['duration'] == 61.442406 |
337 | 348 | |
349 | @pytest.mark.usefixtures('ignore_nplusone') | |
338 | 350 | def test_more_than_one_day_none_end_date_command_returns_msg(self, test_client): |
339 | 351 | command = self.factory( |
340 | 352 | start_date=datetime.datetime(2017, 11, 14, 12, 28, 20, 0), |
344 | 356 | assert res.status_code == 200 |
345 | 357 | assert res.json['commands'][0]['value']['duration'].lower() == "timeout" |
346 | 358 | |
359 | @pytest.mark.usefixtures('ignore_nplusone') | |
347 | 360 | def test_less_than_one_day_none_end_date_command_returns_msg(self, test_client): |
348 | 361 | command = self.factory( |
349 | 362 | start_date=datetime.datetime.now(), |
404 | 417 | ) |
405 | 418 | session.commit() |
406 | 419 | |
407 | res = test_client.get(f'/v2/ws/{host.workspace.name}/hosts/{host.id}/') | |
408 | assert res.status_code == 200 | |
409 | ||
410 | res = test_client.delete(f'/v2/ws/{host.workspace.name}/hosts/{host.id}/') | |
420 | res = test_client.get(self.check_url(f'/v2/ws/{host.workspace.name}/hosts/{host.id}/')) | |
421 | assert res.status_code == 200 | |
422 | ||
423 | res = test_client.delete(self.check_url(f'/v2/ws/{host.workspace.name}/hosts/{host.id}/')) | |
411 | 424 | assert res.status_code == 204 |
412 | 425 | |
413 | res = test_client.get(self.url(workspace=command.workspace) + 'activity_feed/') | |
426 | res = test_client.get(self.check_url(urljoin(self.url(workspace=command.workspace), 'activity_feed/'))) | |
414 | 427 | assert res.status_code == 200 |
415 | 428 | command_history = list(filter(lambda hist: hist['_id'] == command.id, res.json)) |
416 | 429 | assert len(command_history) |
434 | 447 | |
435 | 448 | assert res.status_code == 400 |
436 | 449 | |
437 | # I'm Py3 | |
450 | ||
451 | class TestListCommandViewV3(TestListCommandView, PatchableTestsMixin): | |
452 | view_class = CommandV3View | |
453 | ||
454 | def url(self, obj=None, workspace=None): | |
455 | return v2_to_v3(super(TestListCommandViewV3, self).url(obj, workspace)) | |
456 | ||
457 | def check_url(self, url): | |
458 | return v2_to_v3(url) |
4 | 4 | |
5 | 5 | ''' |
6 | 6 | |
7 | from faraday.server.api.modules.comments import CommentView | |
7 | from faraday.server.api.modules.comments import CommentView, CommentV3View | |
8 | 8 | from faraday.server.models import Comment |
9 | 9 | from tests.factories import ServiceFactory |
10 | from tests.test_api_workspaced_base import ReadOnlyAPITests | |
10 | from tests.test_api_workspaced_base import ReadWriteAPITests, PatchableTestsMixin | |
11 | 11 | from tests import factories |
12 | from tests.utils.url import v2_to_v3 | |
12 | 13 | |
13 | 14 | |
14 | class TestCredentialsAPIGeneric(ReadOnlyAPITests): | |
15 | class TestCommentAPIGeneric(ReadWriteAPITests): | |
15 | 16 | model = Comment |
16 | 17 | factory = factories.CommentFactory |
17 | 18 | view_class = CommentView |
18 | 19 | api_endpoint = 'comment' |
19 | update_fields = ['username', 'password'] | |
20 | update_fields = ['text'] | |
21 | patchable_fields = ['text'] | |
22 | ||
23 | def check_url(self, url): | |
24 | return url | |
20 | 25 | |
21 | 26 | def _create_raw_comment(self, object_type, object_id): |
22 | 27 | return { |
86 | 91 | assert res.status_code == 201 |
87 | 92 | assert len(session.query(Comment).all()) == initial_comment_count + 1 |
88 | 93 | |
89 | url = self.url(workspace=self.workspace).strip('/') + '_unique/' | |
94 | url = self.check_url(self.url(workspace=self.workspace).strip('/') + '_unique/') | |
90 | 95 | res = test_client.post(url, data=raw_comment) |
91 | 96 | assert res.status_code == 409 |
92 | 97 | assert 'object' in res.json |
101 | 106 | session.commit() |
102 | 107 | initial_comment_count = len(session.query(Comment).all()) |
103 | 108 | raw_comment = self._create_raw_comment('service', service.id) |
104 | url = self.url(workspace=self.workspace).strip('/') + '_unique/' | |
109 | url = self.check_url(self.url(workspace=self.workspace).strip('/') + '_unique/') | |
105 | 110 | res = test_client.post(url, |
106 | 111 | data=raw_comment) |
107 | 112 | assert res.status_code == 201 |
121 | 126 | get_comments = test_client.get(self.url(workspace=workspace)) |
122 | 127 | expected = ['first', 'second', 'third','fourth'] |
123 | 128 | assert expected == [comment['text'] for comment in get_comments.json] |
129 | ||
130 | ||
131 | class TestCommentAPIGenericV3(TestCommentAPIGeneric, PatchableTestsMixin): | |
132 | view_class = CommentV3View | |
133 | ||
134 | def url(self, obj=None, workspace=None): | |
135 | return v2_to_v3(super(TestCommentAPIGenericV3, self).url(obj, workspace)) | |
136 | ||
137 | def check_url(self, url): | |
138 | return v2_to_v3(url) |
9 | 9 | from tests import factories |
10 | 10 | from tests.test_api_workspaced_base import ( |
11 | 11 | ReadWriteAPITests, |
12 | PatchableTestsMixin, | |
12 | 13 | ) |
13 | from faraday.server.api.modules.credentials import CredentialView | |
14 | from faraday.server.api.modules.credentials import CredentialView, CredentialV3View | |
14 | 15 | from faraday.server.models import Credential |
15 | 16 | from tests.factories import HostFactory, ServiceFactory |
17 | from tests.utils.url import v2_to_v3 | |
16 | 18 | |
17 | 19 | |
18 | 20 | class TestCredentialsAPIGeneric(ReadWriteAPITests): |
21 | 23 | view_class = CredentialView |
22 | 24 | api_endpoint = 'credential' |
23 | 25 | update_fields = ['username', 'password'] |
26 | patchable_fields = update_fields | |
24 | 27 | |
25 | 28 | def test_get_list_backwards_compatibility(self, test_client, session, second_workspace): |
26 | 29 | cred = self.factory.create(workspace=second_workspace) |
141 | 144 | |
142 | 145 | raw_data = self._generate_raw_update_data('Name1', 'Username2', 'Password3', parent_id=43) |
143 | 146 | |
144 | res = test_client.put(self.url(workspace=credential.workspace) + str(credential.id) + '/', data=raw_data) | |
147 | res = test_client.put(self.url(credential, workspace=credential.workspace), data=raw_data) | |
145 | 148 | assert res.status_code == 400 |
146 | 149 | |
147 | 150 | def test_create_with_invalid_parent_type( |
176 | 179 | raw_data = self._generate_raw_update_data( |
177 | 180 | 'Name1', 'Username2', 'Password3', parent_id=credential.host.id) |
178 | 181 | |
179 | res = test_client.put(self.url(workspace=credential.workspace) + str(credential.id) + '/', data=raw_data) | |
182 | res = test_client.put(self.url(credential, workspace=credential.workspace), data=raw_data) | |
180 | 183 | assert res.status_code == 200 |
181 | 184 | assert res.json['username'] == u'Username2' |
182 | 185 | assert res.json['password'] == u'Password3' |
263 | 266 | response = test_client.get(self.url(workspace=second_workspace) + "?sort=target&sort_dir=asc") |
264 | 267 | assert response.status_code == 200 |
265 | 268 | assert sorted(credentials_target) == [v['value']['target'] for v in response.json['rows']] |
266 | # I'm Py3 | |
269 | ||
270 | ||
271 | class TestCredentialsAPIGenericV3(TestCredentialsAPIGeneric, PatchableTestsMixin): | |
272 | view_class = CredentialV3View | |
273 | ||
274 | def url(self, obj=None, workspace=None): | |
275 | return v2_to_v3(super(TestCredentialsAPIGenericV3, self).url(obj, workspace)) |
1 | 1 | import pytest |
2 | 2 | |
3 | 3 | from tests.factories import CustomFieldsSchemaFactory |
4 | from tests.test_api_non_workspaced_base import ReadOnlyAPITests | |
4 | from tests.test_api_non_workspaced_base import ReadWriteAPITests, PatchableTestsMixin | |
5 | 5 | |
6 | 6 | from faraday.server.api.modules.custom_fields import CustomFieldsSchemaView |
7 | 7 | from faraday.server.models import ( |
8 | 8 | CustomFieldsSchema |
9 | 9 | ) |
10 | from tests.utils.url import v2_to_v3 | |
11 | ||
10 | 12 | |
11 | 13 | @pytest.mark.usefixtures('logged_user') |
12 | class TestVulnerabilityCustomFields(ReadOnlyAPITests): | |
14 | class TestVulnerabilityCustomFields(ReadWriteAPITests): | |
13 | 15 | model = CustomFieldsSchema |
14 | 16 | factory = CustomFieldsSchemaFactory |
15 | 17 | api_endpoint = 'custom_fields_schema' |
16 | 18 | #unique_fields = ['ip'] |
17 | 19 | #update_fields = ['ip', 'description', 'os'] |
18 | 20 | view_class = CustomFieldsSchemaView |
21 | patchable_fields = ['field_name'] | |
19 | 22 | |
20 | 23 | def test_custom_fields_data(self, session, test_client): |
21 | 24 | add_text_field = CustomFieldsSchemaFactory.create( |
28 | 31 | session.add(add_text_field) |
29 | 32 | session.commit() |
30 | 33 | |
31 | res = test_client.get(self.url()) # '/v2/custom_fields_schema/') | |
34 | res = test_client.get(self.url()) | |
32 | 35 | assert res.status_code == 200 |
33 | 36 | assert {u'table_name': u'vulnerability', u'id': add_text_field.id, u'field_type': u'text', u'field_name': u'cvss', u'field_display_name': u'CVSS', u'field_metadata': None, u'field_order': 1} in res.json |
34 | 37 | |
46 | 49 | data = { |
47 | 50 | u'field_name': u'cvss 2', |
48 | 51 | u'field_type': 'int', |
49 | u'talbe_name': 'sarasa', | |
52 | u'table_name': 'sarasa', | |
50 | 53 | u'field_display_name': u'CVSS new', |
51 | 54 | u'field_order': 1 |
52 | 55 | } |
72 | 75 | session.add(add_choice_field) |
73 | 76 | session.commit() |
74 | 77 | |
75 | res = test_client.get(self.url()) # '/v2/custom_fields_schema/') | |
78 | res = test_client.get(self.url()) | |
76 | 79 | assert res.status_code == 200 |
77 | 80 | assert {u'table_name': u'vulnerability', u'id': add_choice_field.id, u'field_type': u'choice', |
78 | 81 | u'field_name': u'gender', u'field_display_name': u'Gender', u'field_metadata': "['Male', 'Female']", |
79 | 82 | u'field_order': 1} in res.json |
80 | 83 | |
81 | # I'm Py3 | |
84 | ||
85 | class TestVulnerabilityCustomFieldsV3(TestVulnerabilityCustomFields, PatchableTestsMixin): | |
86 | def url(self, obj=None): | |
87 | return v2_to_v3(super(TestVulnerabilityCustomFieldsV3, self).url(obj)) |
0 | import json | |
1 | ||
2 | import yaml | |
3 | from apispec import APISpec | |
4 | from faraday.server.web import app | |
5 | from apispec.ext.marshmallow import MarshmallowPlugin | |
6 | from apispec_webframeworks.flask import FlaskPlugin | |
7 | from faraday.utils.faraday_openapi_plugin import FaradayAPIPlugin | |
8 | from faraday.server.commands.app_urls import openapi_format | |
9 | ||
10 | extra_specs = { | |
11 | 'info': {'description': 'TEST'}, | |
12 | 'security': {"ApiKeyAuth": []}, | |
13 | 'servers': [{'url': 'https://localhost/_api'}] | |
14 | } | |
15 | ||
16 | spec = APISpec( | |
17 | title="Faraday API", | |
18 | version="2", | |
19 | openapi_version="3.0.2", | |
20 | plugins=[FaradayAPIPlugin(), FlaskPlugin(), MarshmallowPlugin()], | |
21 | **extra_specs | |
22 | ) | |
23 | ||
24 | ||
25 | class TestDocs: | |
26 | ||
27 | def test_yaml_docs_with_no_doc(self): | |
28 | ||
29 | exc = {'/login', '/logout', '/change', '/reset', '/reset/{token}', '/verify'} | |
30 | failing = [] | |
31 | ||
32 | with app.test_request_context(): | |
33 | for endpoint in app.view_functions: | |
34 | spec.path(view=app.view_functions[endpoint], app=app) | |
35 | ||
36 | spec_yaml = yaml.load(spec.to_yaml(), Loader=yaml.BaseLoader) | |
37 | ||
38 | for path_key, path_value in spec_yaml["paths"].items(): | |
39 | ||
40 | if path_key in exc: | |
41 | continue | |
42 | ||
43 | path_temp = {path_key: {}} | |
44 | ||
45 | if not any(path_value): | |
46 | failing.append(path_temp) | |
47 | ||
48 | if any(failing): | |
49 | print("Endpoints with no docs\n") | |
50 | print(json.dumps(failing, indent=1)) | |
51 | assert not any(failing) | |
52 | ||
53 | def test_yaml_docs_with_defaults(self): | |
54 | ||
55 | failing = [] | |
56 | ||
57 | with app.test_request_context(): | |
58 | for endpoint in app.view_functions: | |
59 | spec.path(view=app.view_functions[endpoint], app=app) | |
60 | ||
61 | spec_yaml = yaml.load(spec.to_yaml(), Loader=yaml.BaseLoader) | |
62 | ||
63 | for path_key, path_value in spec_yaml["paths"].items(): | |
64 | ||
65 | path_temp = {path_key: {}} | |
66 | ||
67 | for data_key, data_value in path_value.items(): | |
68 | if not any(data_value): | |
69 | path_temp[path_key][data_key] = data_value | |
70 | ||
71 | if any(path_temp[path_key]): | |
72 | failing.append(path_temp) | |
73 | ||
74 | if any(failing): | |
75 | print("Endpoints with default docs:\n") | |
76 | print(json.dumps(failing, indent=1)) | |
77 | assert not any(failing) | |
78 | ||
79 | def test_tags_sorted_correctly(self): | |
80 | ||
81 | tags = set() | |
82 | ||
83 | with app.test_request_context(): | |
84 | for endpoint in app.view_functions: | |
85 | spec.path(view=app.view_functions[endpoint], app=app) | |
86 | ||
87 | spec_yaml = yaml.load(spec.to_yaml(), Loader=yaml.BaseLoader) | |
88 | ||
89 | for path_value in spec_yaml["paths"].values(): | |
90 | for data_value in path_value.values(): | |
91 | if 'tags' in data_value and any(data_value['tags']): | |
92 | for tag in data_value['tags']: | |
93 | tags.add(tag) | |
94 | ||
95 | assert sorted(tags) == openapi_format(return_tags=True) |
15 | 15 | VulnerabilityFactory, |
16 | 16 | VulnerabilityWebFactory |
17 | 17 | ) |
18 | from tests.utils.url import v2_to_v3 | |
18 | 19 | |
19 | 20 | |
20 | 21 | @pytest.mark.usefixtures('logged_user') |
21 | class TestExportData(): | |
22 | class TestExportData: | |
23 | ||
24 | def check_url(self, url): | |
25 | return url | |
26 | ||
22 | 27 | def test_export_data_without_format(self, test_client): |
23 | 28 | workspace = WorkspaceFactory.create() |
24 | url = f'/v2/ws/{workspace.name}/export_data' | |
29 | url = self.check_url(f'/v2/ws/{workspace.name}/export_data') | |
25 | 30 | response = test_client.get(url) |
26 | 31 | assert response.status_code == 400 |
27 | 32 | |
84 | 89 | session.add(vuln_web) |
85 | 90 | session.commit() |
86 | 91 | |
87 | url = f'/v2/ws/{workspace.name}/export_data?format=xml_metasploit' | |
92 | url = self.check_url(f'/v2/ws/{workspace.name}/export_data?format=xml_metasploit') | |
88 | 93 | response = test_client.get(url) |
89 | 94 | assert response.status_code == 200 |
90 | 95 | response_xml = response.data |
137 | 142 | assert response_tree.xpath(full_xpath)[0].text == xml_file_hostnames |
138 | 143 | else: |
139 | 144 | 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) |
19 | 19 | if 'OPTIONS' in rule.methods: |
20 | 20 | res = test_client.options(replace_placeholders(rule.rule)) |
21 | 21 | assert res.status_code == 200, rule.rule |
22 | ||
23 | ||
24 | def test_v3_endpoints(): | |
25 | rules = list( | |
26 | filter(lambda rule: rule.rule.startswith("/v3") and rule.rule.endswith("/"), app.url_map.iter_rules()) | |
27 | ) | |
28 | assert len(rules) == 0, [rule.rule for rule in rules] | |
29 | ||
30 | ||
31 | def test_v2_in_v3_endpoints(): | |
32 | exceptions = { | |
33 | '/v3/ws/<workspace_id>/activate', | |
34 | '/v3/ws/<workspace_id>/change_readonly', | |
35 | '/v3/ws/<workspace_id>/deactivate', | |
36 | '/v3/ws/<workspace_name>/hosts/bulk_delete', | |
37 | '/v3/ws/<workspace_name>/vulns/bulk_delete', | |
38 | '/v3/ws/<workspace_name>/vulns/<int:vuln_id>/attachments' | |
39 | } | |
40 | rules_v2 = set( | |
41 | map( | |
42 | lambda rule: rule.rule.replace("v2", "v3").rstrip("/"), | |
43 | filter(lambda rule: rule.rule.startswith("/v2"), app.url_map.iter_rules()) | |
44 | ) | |
45 | ) | |
46 | rules = set( | |
47 | map(lambda rule: rule.rule, filter(lambda rule: rule.rule.startswith("/v3"), app.url_map.iter_rules())) | |
48 | ) | |
49 | exceptions_present_v2 = rules_v2.intersection(exceptions) | |
50 | assert len(exceptions_present_v2) == len(exceptions), sorted(exceptions_present_v2) | |
51 | exceptions_present = rules.intersection(exceptions) | |
52 | assert len(exceptions_present) == 0, sorted(exceptions_present) | |
53 | # We can have extra endpoints in v3 (like all the PATCHS) | |
54 | difference = rules_v2.difference(rules).difference(exceptions) | |
55 | assert len(difference) == 0, sorted(difference) |
27 | 27 | assert res.status_code == 200 |
28 | 28 | assert res.json.get('metasploit') != [] |
29 | 29 | assert res.json.get('exploitdb') != [] |
30 | ||
31 | ||
32 | # I'm Py3 |
3 | 3 | See the file 'doc/LICENSE' for the license information |
4 | 4 | |
5 | 5 | ''' |
6 | import time | |
7 | 6 | import operator |
8 | 7 | from io import BytesIO |
8 | from posixpath import join as urljoin | |
9 | 9 | |
10 | 10 | import pytz |
11 | 11 | |
12 | try: | |
13 | import urlparse | |
14 | from urllib import urlencode | |
15 | except ImportError: # For Python 3 | |
16 | import urllib.parse as urlparse | |
17 | from urllib.parse import urlencode | |
12 | from tests.utils.url import v2_to_v3 | |
13 | ||
14 | from urllib.parse import urlencode | |
18 | 15 | from random import choice |
19 | 16 | from sqlalchemy.orm.util import was_deleted |
20 | 17 | from hypothesis import given, assume, settings, strategies as st |
25 | 22 | from tests.test_api_workspaced_base import ( |
26 | 23 | API_PREFIX, |
27 | 24 | ReadWriteAPITests, |
28 | PaginationTestsMixin, | |
25 | PaginationTestsMixin, PatchableTestsMixin, | |
29 | 26 | ) |
30 | 27 | from faraday.server.models import db, Host, Hostname |
31 | from faraday.server.api.modules.hosts import HostsView | |
28 | from faraday.server.api.modules.hosts import HostsView, HostsV3View | |
32 | 29 | from tests.factories import HostFactory, CommandFactory, \ |
33 | 30 | EmptyCommandFactory, WorkspaceFactory |
34 | 31 | |
37 | 34 | |
38 | 35 | @pytest.mark.usefixtures('database', 'logged_user') |
39 | 36 | class TestHostAPI: |
37 | ||
38 | def check_url(self, url): | |
39 | return url | |
40 | 40 | |
41 | 41 | @pytest.fixture(autouse=True) |
42 | 42 | def load_workspace_with_hosts(self, database, session, workspace, host_factory): |
301 | 301 | host_factory.create_batch(5, workspace=second_workspace, os='Unix') |
302 | 302 | |
303 | 303 | session.commit() |
304 | res = test_client.get(f'{self.url()}filter?q={{"filters":[{{"name": "os", "op":"eq", "val":"Unix"}}]}}') | |
304 | res = test_client.get(urljoin(self.url(), 'filter?q={"filters":[{"name": "os", "op":"eq", "val":"Unix"}]}')) | |
305 | 305 | assert res.status_code == 200 |
306 | 306 | self.compare_results(hosts, res) |
307 | 307 | |
308 | @pytest.mark.usefixtures('ignore_nplusone') | |
309 | def test_filter_restless_count(self, test_client, session, workspace, | |
310 | second_workspace, host_factory): | |
311 | # The hosts that should be shown | |
312 | hosts = host_factory.create_batch(30, workspace=workspace, os='Unix') | |
313 | ||
314 | # This shouldn't be shown, they are from other workspace | |
315 | host_factory.create_batch(5, workspace=second_workspace, os='Unix') | |
316 | ||
317 | session.commit() | |
318 | res = test_client.get(urljoin(self.url(), 'filter?q={"filters":[{"name": "os", "op":"eq", "val":"Unix"}],' | |
319 | '"offset":0, "limit":20}')) | |
320 | assert res.status_code == 200 | |
321 | assert res.json['count'] == 30 | |
308 | 322 | |
309 | 323 | @pytest.mark.usefixtures('ignore_nplusone') |
310 | 324 | def test_filter_restless_filter_and_group_by_os(self, test_client, session, workspace, host_factory): |
311 | 325 | host_factory.create_batch(10, workspace=workspace, os='Unix') |
312 | 326 | host_factory.create_batch(1, workspace=workspace, os='unix') |
313 | 327 | session.commit() |
314 | res = test_client.get(f'{self.url()}filter?q={{"filters":[{{"name": "os", "op": "like", "val": "%nix"}}], ' | |
315 | f'"group_by":[{{"field": "os"}}], ' | |
316 | f'"order_by":[{{"field": "os", "direction": "desc"}}]}}') | |
328 | res = test_client.get(urljoin(self.url(), 'filter?q={"filters":[{"name": "os", "op": "like", "val": "%nix"}], ' | |
329 | '"group_by":[{"field": "os"}], "order_by":[{"field": "os", "direction": "desc"}]}')) | |
317 | 330 | assert res.status_code == 200 |
318 | 331 | assert len(res.json['rows']) == 2 |
319 | assert res.json['total_rows'] == 2 | |
332 | assert res.json['count'] == 2 | |
320 | 333 | assert 'unix' in [row['value']['os'] for row in res.json['rows']] |
321 | 334 | assert 'Unix' in [row['value']['os'] for row in res.json['rows']] |
322 | ||
323 | 335 | |
324 | 336 | def test_filter_by_os_like_ilike(self, test_client, session, workspace, |
325 | 337 | second_workspace, host_factory): |
364 | 376 | host_factory.create_batch(5, workspace=second_workspace, os='Unix') |
365 | 377 | |
366 | 378 | session.commit() |
367 | res = test_client.get(f'{self.url()}filter?q={{"filters":[{{"name": "os", "op":"like", "val":"Unix %"}}]}}') | |
379 | res = test_client.get(urljoin( | |
380 | self.url(), | |
381 | 'filter?q={"filters":[{"name": "os", "op":"like", "val":"Unix %"}]}' | |
382 | ) | |
383 | ) | |
368 | 384 | assert res.status_code == 200 |
369 | 385 | self.compare_results(hosts, res) |
370 | 386 | |
371 | res = test_client.get(f'{self.url()}filter?q={{"filters":[{{"name": "os", "op":"ilike", "val":"Unix %"}}]}}') | |
387 | res = test_client.get(urljoin( | |
388 | self.url(), | |
389 | 'filter?q={"filters":[{"name": "os", "op":"ilike", "val":"Unix %"}]}' | |
390 | ) | |
391 | ) | |
372 | 392 | assert res.status_code == 200 |
373 | 393 | self.compare_results(hosts + [case_insensitive_host], res) |
374 | 394 | |
400 | 420 | |
401 | 421 | session.commit() |
402 | 422 | |
403 | res = test_client.get(f'{self.url()}' | |
404 | f'filter?q={{"filters":[{{"name": "services__name", "op":"any", "val":"IRC"}}]}}') | |
423 | res = test_client.get( | |
424 | urljoin( | |
425 | self.url(), | |
426 | 'filter?q={"filters":[{"name": "services__name", "op":"any", "val":"IRC"}]}' | |
427 | ) | |
428 | ) | |
405 | 429 | assert res.status_code == 200 |
406 | 430 | shown_hosts_ids = set(obj['id'] for obj in res.json['rows']) |
407 | 431 | expected_host_ids = set(host.id for host in hosts) |
434 | 458 | host_factory.create_batch(5, workspace=workspace) |
435 | 459 | |
436 | 460 | session.commit() |
437 | res = test_client.get(f'{self.url()}' | |
438 | f'filter?q={{"filters":[{{"name": "services__port", "op":"any", "val":"25"}}]}}') | |
461 | res = test_client.get( | |
462 | urljoin( | |
463 | self.url(), | |
464 | 'filter?q={"filters":[{"name": "services__port", "op":"any", "val":"25"}]}' | |
465 | ) | |
466 | ) | |
439 | 467 | assert res.status_code == 200 |
440 | 468 | shown_hosts_ids = set(obj['id'] for obj in res.json['rows']) |
441 | 469 | expected_host_ids = set(host.id for host in hosts) |
456 | 484 | |
457 | 485 | session.commit() |
458 | 486 | |
459 | res = test_client.get(f'{self.url()}' | |
460 | f'filter?q={{"filters":[{{"name": "ip", "op":"eq", "val":"{host.ip}"}}]}}') | |
487 | res = test_client.get( | |
488 | urljoin( | |
489 | self.url(), | |
490 | f'filter?q={{"filters":[{{"name": "ip", "op":"eq", "val":"{host.ip}"}}]}}' | |
491 | ) | |
492 | ) | |
461 | 493 | |
462 | 494 | assert res.status_code == 200 |
463 | 495 | |
481 | 513 | session.commit() |
482 | 514 | res = test_client.get(self.url() + '?port=invalid_port') |
483 | 515 | assert res.status_code == 200 |
484 | assert res.json['total_rows'] == 0 | |
516 | assert res.json['count'] == 0 | |
485 | 517 | |
486 | 518 | def test_filter_restless_by_invalid_service_port(self, test_client, session, workspace, |
487 | 519 | service_factory, host_factory): |
492 | 524 | host_factory.create_batch(5, workspace=workspace) |
493 | 525 | |
494 | 526 | session.commit() |
495 | res = test_client.get(f'{self.url()}' | |
496 | f'filter?q={{"filters":[{{"name": "services__port", "op":"any", "val":"sarasa"}}]}}') | |
527 | res = test_client.get( | |
528 | urljoin( | |
529 | self.url(), | |
530 | 'filter?q={"filters":[{"name": "services__port", "op":"any", "val":"sarasa"}]}' | |
531 | ) | |
532 | ) | |
497 | 533 | assert res.status_code == 400 |
498 | 534 | |
499 | 535 | def test_filter_restless_by_invalid_field(self, test_client): |
500 | res = test_client.get(f'{self.url()}' | |
501 | f'filter?q={{"filters":[{{"name": "severity", "op":"any", "val":"sarasa"}}]}}') | |
536 | res = test_client.get( | |
537 | urljoin( | |
538 | self.url(), | |
539 | 'filter?q={"filters":[{"name": "severity", "op":"any", "val":"sarasa"}]}' | |
540 | ) | |
541 | ) | |
502 | 542 | assert res.status_code == 400 |
503 | 543 | |
504 | 544 | @pytest.mark.usefixtures('ignore_nplusone') |
505 | 545 | def test_filter_restless_with_no_q_param(self, test_client, session, workspace, host_factory): |
506 | res = test_client.get(f'{self.url()}filter') | |
546 | res = test_client.get(urljoin(self.url(),'filter')) | |
507 | 547 | assert res.status_code == 200 |
508 | 548 | assert len(res.json['rows']) == HOSTS_COUNT |
509 | 549 | |
510 | 550 | @pytest.mark.usefixtures('ignore_nplusone') |
511 | 551 | def test_filter_restless_with_empty_q_param(self, test_client, session, workspace, host_factory): |
512 | res = test_client.get(f'{self.url()}filter?q') | |
552 | res = test_client.get(urljoin(self.url(), 'filter?q')) | |
513 | 553 | assert res.status_code == 400 |
514 | 554 | |
515 | 555 | def test_search_ip(self, test_client, session, workspace, host_factory): |
579 | 619 | vulnerability_factory.create(service=service, host=None, workspace=workspace) |
580 | 620 | session.commit() |
581 | 621 | |
582 | res = test_client.get(self.url() + str(host.id) + "/" + 'services/') | |
622 | res = test_client.get(self.check_url(urljoin(self.url(host),'services/'))) | |
583 | 623 | assert res.status_code == 200 |
584 | 624 | assert res.json[0]['vulns'] == 1 |
585 | 625 | |
720 | 760 | 'csrf_token': csrf_token |
721 | 761 | } |
722 | 762 | headers = {'Content-type': 'multipart/form-data'} |
723 | res = test_client.post(f'/v2/ws/{ws.name}/hosts/bulk_create/', | |
763 | res = test_client.post(self.check_url(f'/v2/ws/{ws.name}/hosts/bulk_create/'), | |
724 | 764 | data=data, headers=headers, use_json_data=False) |
725 | 765 | assert res.status_code == 200 |
726 | 766 | assert res.json['hosts_created'] == expected_created_hosts |
735 | 775 | hosts_ids = [host_1.id, host_2.id] |
736 | 776 | request_data = {'hosts_ids': hosts_ids} |
737 | 777 | |
738 | delete_response = test_client.delete(f'/v2/ws/{ws.name}/hosts/bulk_delete/', data=request_data) | |
778 | delete_response = test_client.delete(self.check_url(f'/v2/ws/{ws.name}/hosts/bulk_delete/'), data=request_data) | |
739 | 779 | |
740 | 780 | deleted_hosts = delete_response.json['deleted_hosts'] |
741 | 781 | host_count_after_delete = db.session.query(Host).filter( |
750 | 790 | ws = WorkspaceFactory.create(name="abc") |
751 | 791 | request_data = {'hosts_ids': []} |
752 | 792 | |
753 | delete_response = test_client.delete(f'/v2/ws/{ws.name}/hosts/bulk_delete/', data=request_data) | |
793 | delete_response = test_client.delete(self.check_url(f'/v2/ws/{ws.name}/hosts/bulk_delete/'), data=request_data) | |
754 | 794 | |
755 | 795 | assert delete_response.status_code == 400 |
756 | 796 | |
763 | 803 | |
764 | 804 | # Try to delete workspace_2's host from workspace_1 |
765 | 805 | request_data = {'hosts_ids': [host_of_ws_2.id]} |
766 | url = f'/v2/ws/{workspace_1.name}/hosts/bulk_delete/' | |
806 | url = self.check_url(f'/v2/ws/{workspace_1.name}/hosts/bulk_delete/') | |
767 | 807 | delete_response = test_client.delete(url, data=request_data) |
768 | 808 | |
769 | 809 | assert delete_response.json['deleted_hosts'] == 0 |
771 | 811 | def test_bulk_delete_hosts_invalid_characters_in_request(self, test_client): |
772 | 812 | ws = WorkspaceFactory.create(name="abc") |
773 | 813 | request_data = {'hosts_ids': [-1, 'test']} |
774 | delete_response = test_client.delete(f'/v2/ws/{ws.name}/hosts/bulk_delete/', data=request_data) | |
814 | delete_response = test_client.delete(self.check_url(f'/v2/ws/{ws.name}/hosts/bulk_delete/'), data=request_data) | |
775 | 815 | |
776 | 816 | assert delete_response.json['deleted_hosts'] == 0 |
777 | 817 | |
786 | 826 | headers = [('content-type', 'text/xml')] |
787 | 827 | |
788 | 828 | delete_response = test_client.delete( |
789 | f'/v2/ws/{ws.name}/hosts/bulk_delete/', | |
829 | self.check_url(f'/v2/ws/{ws.name}/hosts/bulk_delete/'), | |
790 | 830 | data=request_data, |
791 | 831 | headers=headers) |
792 | 832 | |
793 | 833 | assert delete_response.status_code == 400 |
834 | ||
835 | ||
836 | class TestHostAPIV3(TestHostAPI): | |
837 | def url(self, host=None, workspace=None): | |
838 | return v2_to_v3(super(TestHostAPIV3, self).url(host, workspace)) | |
839 | ||
840 | def check_url(self, url): | |
841 | return v2_to_v3(url) | |
842 | ||
843 | def services_url(self, host, workspace=None): | |
844 | return self.url(host, workspace) + '/services' | |
845 | ||
846 | @pytest.mark.skip(reason="To be reimplemented") | |
847 | def test_bulk_delete_hosts(self, test_client, session): | |
848 | pass | |
849 | ||
850 | @pytest.mark.skip(reason="To be reimplemented") | |
851 | def test_bulk_delete_hosts_without_hosts_ids(self, test_client): | |
852 | pass | |
853 | ||
854 | @pytest.mark.skip(reason="To be reimplemented") | |
855 | def test_bulk_delete_hosts_from_another_workspace(self, test_client, session): | |
856 | pass | |
857 | ||
858 | @pytest.mark.skip(reason="To be reimplemented") | |
859 | def test_bulk_delete_hosts_invalid_characters_in_request(self, test_client): | |
860 | pass | |
861 | ||
862 | @pytest.mark.skip(reason="To be reimplemented") | |
863 | def test_bulk_delete_hosts_wrong_content_type(self, test_client, session): | |
864 | pass | |
794 | 865 | |
795 | 866 | |
796 | 867 | class TestHostAPIGeneric(ReadWriteAPITests, PaginationTestsMixin): |
799 | 870 | api_endpoint = 'hosts' |
800 | 871 | unique_fields = ['ip'] |
801 | 872 | update_fields = ['ip', 'description', 'os'] |
873 | patchable_fields = update_fields | |
802 | 874 | view_class = HostsView |
803 | 875 | |
804 | 876 | @pytest.mark.usefixtures("mock_envelope_list") |
974 | 1046 | "os":"Unknown", |
975 | 1047 | } |
976 | 1048 | |
977 | res = test_client.put(f'v2/ws/{host.workspace.name}/hosts/{host.id}/', data=data) | |
1049 | res = test_client.put(self.url(host, workspace=host.workspace), data=data) | |
978 | 1050 | assert res.status_code == 200 |
979 | 1051 | |
980 | 1052 | assert session.query(Hostname).filter_by(host=host).count() == 1 |
1062 | 1134 | index_in_response_hosts = response_hosts.index(host) |
1063 | 1135 | |
1064 | 1136 | assert index_in_hosts_ids == index_in_response_hosts |
1137 | ||
1138 | ||
1139 | class TestHostAPIGenericV3(TestHostAPIGeneric, PatchableTestsMixin): | |
1140 | view_class = HostsV3View | |
1141 | ||
1142 | def url(self, obj=None, workspace=None): | |
1143 | return v2_to_v3(super(TestHostAPIGenericV3, self).url(obj, workspace)) | |
1065 | 1144 | |
1066 | 1145 | |
1067 | 1146 | def host_json(): |
1109 | 1188 | data=raw_data) |
1110 | 1189 | assert res.status_code in [201, 400, 409] |
1111 | 1190 | |
1191 | @given(HostData) | |
1192 | def send_api_request_v3(raw_data): | |
1193 | ||
1194 | ws_name = host_with_hostnames.workspace.name | |
1195 | res = test_client.post(f'/v3/ws/{ws_name}/vulns', | |
1196 | data=raw_data) | |
1197 | assert res.status_code in [201, 400, 409] | |
1198 | ||
1112 | 1199 | send_api_request() |
1200 | send_api_request_v3() |
15 | 15 | assert response.status_code == 200 |
16 | 16 | assert response.json['Faraday Server'] == 'Running' |
17 | 17 | |
18 | def test_api_info_v3(self, test_client): | |
19 | response = test_client.get('v3/info') | |
20 | assert response.status_code == 200 | |
21 | assert response.json['Faraday Server'] == 'Running' | |
22 | ||
18 | 23 | def test_get_config(self, test_client): |
19 | 24 | res = test_client.get('/config') |
20 | 25 | assert res.status_code == 200 |
4 | 4 | See the file 'doc/LICENSE' for the license information |
5 | 5 | |
6 | 6 | ''' |
7 | from tests.utils.url import v2_to_v3 | |
8 | ||
7 | 9 | """Tests for many API endpoints that do not depend on workspace_name""" |
8 | 10 | |
9 | 11 | import pytest |
11 | 13 | from hypothesis import given, strategies as st |
12 | 14 | |
13 | 15 | from tests import factories |
14 | from tests.test_api_non_workspaced_base import ReadWriteAPITests, API_PREFIX | |
16 | from tests.test_api_non_workspaced_base import ReadWriteAPITests, API_PREFIX, PatchableTestsMixin | |
15 | 17 | from faraday.server.models import ( |
16 | 18 | License, |
17 | 19 | ) |
31 | 33 | model = License |
32 | 34 | factory = factories.LicenseFactory |
33 | 35 | api_endpoint = 'licenses' |
34 | # unique_fields = ['ip'] | |
35 | # update_fields = ['ip', 'description', 'os'] | |
36 | patchable_fields = ["products"] | |
36 | 37 | |
37 | 38 | def test_envelope_list(self, test_client, app): |
38 | 39 | LicenseEnvelopedView.register(app) |
50 | 51 | res = test_client.get(self.url(obj=lic)) |
51 | 52 | assert res.status_code == 200 |
52 | 53 | assert res.json['notes'] == 'A great note. License' |
54 | ||
55 | ||
56 | class TestLicensesAPIV3(TestLicensesAPI, PatchableTestsMixin): | |
57 | def url(self, obj=None): | |
58 | return v2_to_v3(super(TestLicensesAPIV3, self).url(obj)) | |
59 | ||
60 | @pytest.mark.skip(reason="Not a license actually test") | |
61 | def test_envelope_list(self, test_client, app): | |
62 | pass | |
53 | 63 | |
54 | 64 | |
55 | 65 | def license_json(): |
85 | 95 | res = test_client.post('v2/licenses/', data=raw_data) |
86 | 96 | assert res.status_code in [201, 400, 409] |
87 | 97 | |
98 | @given(LicenseData) | |
99 | def send_api_request_v3(raw_data): | |
100 | raw_data['start'] = pytz.UTC.localize(raw_data['start']).isoformat() | |
101 | raw_data['end'] = pytz.UTC.localize(raw_data['end']).isoformat() | |
102 | res = test_client.post('v3/licenses/', data=raw_data) | |
103 | assert res.status_code in [201, 400, 409] | |
104 | ||
88 | 105 | send_api_request() |
89 | ||
90 | ||
91 | # I'm Py3 | |
106 | send_api_request_v3() |
6 | 6 | from faraday.server.web import app |
7 | 7 | from tests import factories |
8 | 8 | from tests.conftest import logged_user, login_as |
9 | from tests.utils.url import v2_to_v3 | |
9 | 10 | |
10 | 11 | |
11 | 12 | class TestLogin: |
13 | ||
14 | def check_url(self, url): | |
15 | return url | |
16 | ||
12 | 17 | def test_case_bug_with_username(self, test_client, session): |
13 | 18 | """ |
14 | 19 | When the user case does not match the one in database, |
58 | 63 | |
59 | 64 | headers = {'Authentication-Token': res.json['response']['user']['authentication_token']} |
60 | 65 | |
61 | ws = test_client.get('/v2/ws/wonderland/', headers=headers) | |
66 | ws = test_client.get(self.check_url('/v2/ws/wonderland/'), headers=headers) | |
62 | 67 | assert ws.status_code == 200 |
63 | 68 | |
64 | 69 | def test_case_ws_with_invalid_authentication_token(self, test_client, session): |
85 | 90 | |
86 | 91 | headers = {'Authorization': b'Token ' + token} |
87 | 92 | |
88 | ws = test_client.get('/v2/ws/wonderland/', headers=headers) | |
93 | ws = test_client.get(self.check_url('/v2/ws/wonderland/'), headers=headers) | |
89 | 94 | assert ws.status_code == 401 |
90 | 95 | |
91 | 96 | @pytest.mark.usefixtures('logged_user') |
92 | 97 | def test_retrieve_token_from_api_and_use_it(self, test_client, session): |
93 | res = test_client.get('/v2/token/') | |
98 | res = test_client.get(self.check_url('/v2/token/')) | |
94 | 99 | cookies = [cookie.name for cookie in test_client.cookie_jar] |
95 | 100 | assert "faraday_session_2" in cookies |
96 | 101 | assert res.status_code == 200 |
101 | 106 | session.commit() |
102 | 107 | # clean cookies make sure test_client has no session |
103 | 108 | test_client.cookie_jar.clear() |
104 | res = test_client.get('/v2/ws/wonderland/', headers=headers) | |
109 | res = test_client.get(self.check_url('/v2/ws/wonderland/'), headers=headers) | |
105 | 110 | assert res.status_code == 200 |
106 | 111 | assert 'Set-Cookie' not in res.headers |
107 | 112 | cookies = [cookie.name for cookie in test_client.cookie_jar] |
111 | 116 | def test_cant_retrieve_token_unauthenticated(self, test_client): |
112 | 117 | # clean cookies make sure test_client has no session |
113 | 118 | test_client.cookie_jar.clear() |
114 | res = test_client.get('/v2/token/') | |
119 | res = test_client.get(self.check_url('/v2/token/')) | |
115 | 120 | |
116 | 121 | assert res.status_code == 401 |
117 | 122 | |
118 | 123 | @pytest.mark.usefixtures('logged_user') |
119 | 124 | def test_token_expires_after_password_change(self, test_client, session): |
120 | 125 | user = User.query.filter_by(username="test").first() |
121 | res = test_client.get('/v2/token/') | |
126 | res = test_client.get(self.check_url('/v2/token/')) | |
122 | 127 | |
123 | 128 | assert res.status_code == 200 |
124 | 129 | |
131 | 136 | |
132 | 137 | # clean cookies make sure test_client has no session |
133 | 138 | test_client.cookie_jar.clear() |
134 | res = test_client.get('/v2/ws/', headers=headers) | |
139 | res = test_client.get(self.check_url('/v2/ws/'), headers=headers) | |
135 | 140 | assert res.status_code == 401 |
136 | 141 | |
137 | 142 | def test_null_caracters(self, test_client, session): |
162 | 167 | |
163 | 168 | headers = {'Authentication-Token': res.json['response']['user']['authentication_token']} |
164 | 169 | |
165 | ws = test_client.get('/v2/ws/wonderland/', headers=headers) | |
170 | ws = test_client.get(self.check_url('/v2/ws/wonderland/'), headers=headers) | |
166 | 171 | assert ws.status_code == 200 |
167 | 172 | |
168 | 173 | def test_login_remember_me(self, test_client, session): |
232 | 237 | assert res.status_code == 200 |
233 | 238 | cookies = [cookie.name for cookie in test_client.cookie_jar] |
234 | 239 | assert "remember_token" not in cookies |
240 | ||
241 | ||
242 | class TestLoginV3(TestLogin): | |
243 | def check_url(self, url): | |
244 | return v2_to_v3(url) |
6 | 6 | ''' |
7 | 7 | from builtins import str |
8 | 8 | |
9 | from tests.utils.url import v2_to_v3 | |
10 | ||
9 | 11 | """Generic tests for APIs NOT prefixed with a workspace_name""" |
10 | 12 | |
11 | 13 | import pytest |
13 | 15 | |
14 | 16 | API_PREFIX = '/v2/' |
15 | 17 | OBJECT_COUNT = 5 |
16 | ||
17 | 18 | |
18 | 19 | @pytest.mark.usefixtures('logged_user') |
19 | 20 | class GenericAPITest: |
49 | 50 | return url |
50 | 51 | |
51 | 52 | |
53 | @pytest.mark.usefixtures('logged_user') | |
52 | 54 | class ListTestsMixin: |
53 | 55 | |
54 | def test_list_retrieves_all_items_from(self, test_client): | |
56 | def test_list_retrieves_all_items_from(self, test_client, logged_user): | |
55 | 57 | res = test_client.get(self.url()) |
56 | 58 | assert res.status_code == 200 |
57 | 59 | if 'rows' in res.json: |
60 | 62 | assert len(res.json) == OBJECT_COUNT |
61 | 63 | |
62 | 64 | |
63 | ||
65 | @pytest.mark.usefixtures('logged_user') | |
64 | 66 | class RetrieveTestsMixin: |
65 | 67 | |
66 | def test_retrieve_one_object(self, test_client): | |
68 | def test_retrieve_one_object(self, test_client, logged_user): | |
67 | 69 | res = test_client.get(self.url(self.first_object)) |
68 | 70 | assert res.status_code == 200 |
69 | 71 | assert isinstance(res.json, dict) |
70 | 72 | |
71 | @pytest.mark.parametrize('object_id', [123, -1, 'xxx', u'áá']) | |
73 | @pytest.mark.parametrize('object_id', [123456789, -1, 'xxx', u'áá']) | |
72 | 74 | def test_404_when_retrieving_unexistent_object(self, test_client, |
73 | 75 | object_id): |
74 | 76 | url = self.url(object_id) |
81 | 83 | def test_create_succeeds(self, test_client): |
82 | 84 | res = test_client.post(self.url(), |
83 | 85 | data=self.factory.build_dict()) |
84 | assert res.status_code == 201 | |
86 | assert res.status_code == 201, (res.status_code, res.json) | |
85 | 87 | assert self.model.query.count() == OBJECT_COUNT + 1 |
86 | 88 | object_id = res.json['id'] |
87 | 89 | self.model.query.get(object_id) |
88 | 90 | |
89 | 91 | def test_create_fails_with_empty_dict(self, test_client): |
90 | 92 | res = test_client.post(self.url(), data={}) |
91 | assert res.status_code == 400 | |
93 | assert res.status_code == 400, (res.status_code, res.json) | |
92 | 94 | |
93 | 95 | def test_create_fails_with_existing(self, session, test_client): |
94 | 96 | for unique_field in self.unique_fields: |
99 | 101 | assert self.model.query.count() == OBJECT_COUNT |
100 | 102 | |
101 | 103 | |
104 | @pytest.mark.usefixtures('logged_user') | |
102 | 105 | class UpdateTestsMixin: |
103 | 106 | |
104 | def test_update_an_object(self, test_client): | |
105 | res = test_client.put(self.url(self.first_object), | |
106 | data=self.factory.build_dict()) | |
107 | assert res.status_code == 200 | |
107 | @pytest.mark.parametrize("method", ["PUT"]) | |
108 | def test_update_an_object(self, test_client, logged_user, method): | |
109 | data = self.factory.build_dict() | |
110 | if method == "PUT": | |
111 | res = test_client.put(self.url(self.first_object), data=data) | |
112 | elif method == "PATCH": | |
113 | data = PatchableTestsMixin.control_data(self, data) | |
114 | res = test_client.patch(self.url(self.first_object), data=data) | |
115 | assert res.status_code == 200, (res.status_code, res.json) | |
108 | 116 | assert self.model.query.count() == OBJECT_COUNT |
109 | 117 | for updated_field in self.update_fields: |
110 | assert res.json[updated_field] == getattr(self.first_object, | |
111 | updated_field) | |
118 | assert res.json[updated_field] == getattr(self.first_object, updated_field) | |
112 | 119 | |
113 | def test_update_fails_with_existing(self, test_client, session): | |
120 | @pytest.mark.parametrize("method", ["PUT", "PATCH"]) | |
121 | def test_update_fails_with_existing(self, test_client, session, method): | |
114 | 122 | for unique_field in self.unique_fields: |
115 | 123 | data = self.factory.build_dict() |
116 | 124 | data[unique_field] = getattr(self.objects[1], unique_field) |
117 | res = test_client.put(self.url(self.first_object), data=data) | |
125 | if method == "PUT": | |
126 | res = test_client.put(self.url(self.first_object), | |
127 | data=data) | |
128 | elif method == "PATCH": | |
129 | res = test_client.patch(self.url(self.first_object), data=data) | |
118 | 130 | assert res.status_code == 400 |
119 | 131 | assert self.model.query.count() == OBJECT_COUNT |
120 | 132 | |
121 | def test_update_an_object_fails_with_empty_dict(self, test_client): | |
133 | def test_update_an_object_fails_with_empty_dict(self, test_client, logged_user): | |
122 | 134 | """To do this the user should use a PATCH request""" |
123 | 135 | res = test_client.put(self.url(self.first_object), data={}) |
124 | assert res.status_code == 400 | |
136 | assert res.status_code == 400, (res.status_code, res.json) | |
125 | 137 | |
126 | 138 | |
139 | @pytest.mark.usefixtures('logged_user') | |
140 | class PatchableTestsMixin(UpdateTestsMixin): | |
141 | ||
142 | @staticmethod | |
143 | def control_data(test_suite, data: dict) -> dict: | |
144 | return {key: value for (key, value) in data.items() if key in test_suite.patchable_fields} | |
145 | ||
146 | @pytest.mark.parametrize("method", ["PUT", "PATCH"]) | |
147 | def test_update_an_object(self, test_client, logged_user, method): | |
148 | super(PatchableTestsMixin, self).test_update_an_object(test_client, logged_user, method) | |
149 | ||
150 | @pytest.mark.parametrize("method", ["PUT", "PATCH"]) | |
151 | def test_update_fails_with_existing(self, test_client, session, method): | |
152 | super(PatchableTestsMixin, self).test_update_fails_with_existing(test_client, session, method) | |
153 | ||
154 | def test_patch_update_an_object_does_not_fail_with_partial_data(self, test_client, logged_user): | |
155 | """To do this the user should use a PATCH request""" | |
156 | res = test_client.patch(self.url(self.first_object), data={}) | |
157 | assert res.status_code == 200, (res.status_code, res.json) | |
158 | ||
159 | ||
160 | @pytest.mark.usefixtures('logged_user') | |
127 | 161 | class DeleteTestsMixin: |
128 | 162 | |
129 | def test_delete(self, test_client): | |
163 | def test_delete(self, test_client, logged_user): | |
130 | 164 | res = test_client.delete(self.url(self.first_object)) |
131 | 165 | assert res.status_code == 204 # No content |
132 | 166 | assert was_deleted(self.first_object) |
133 | 167 | assert self.model.query.count() == OBJECT_COUNT - 1 |
134 | 168 | |
135 | @pytest.mark.parametrize('object_id', [123, -1, 'xxx', u'áá']) | |
169 | @pytest.mark.parametrize('object_id', [12300, -1, 'xxx', u'áá']) | |
136 | 170 | def test_delete_non_existent_raises_404(self, test_client, |
137 | 171 | object_id): |
138 | 172 | res = test_client.delete(self.url(object_id)) |
107 | 107 | res = test_client.get(self.page_url(1, 5)) |
108 | 108 | assert res.status_code == 200 |
109 | 109 | assert len(res.json['data']) == 0 |
110 | ||
111 | ||
112 | # I'm Py3 |
3 | 3 | from tests.factories import UserFactory |
4 | 4 | from faraday.server.models import User |
5 | 5 | from faraday.server.api.modules.preferences import PreferencesView |
6 | from tests.utils.url import v2_to_v3 | |
7 | ||
6 | 8 | |
7 | 9 | pytest.fixture('logged_user') |
8 | 10 | class TestPreferences(GenericAPITest): |
42 | 44 | response = test_client.post(self.url(), data=data) |
43 | 45 | |
44 | 46 | assert response.status_code == 400 |
47 | ||
48 | ||
49 | class TestPreferencesV3(TestPreferences): | |
50 | def url(self, obj=None): | |
51 | return v2_to_v3(super(TestPreferencesV3, self).url(obj)) |
9 | 9 | import pytest |
10 | 10 | |
11 | 11 | from tests.factories import SearchFilterFactory, UserFactory, SubFactory |
12 | from tests.test_api_non_workspaced_base import ReadOnlyAPITests, OBJECT_COUNT | |
12 | from tests.test_api_non_workspaced_base import ReadWriteAPITests, OBJECT_COUNT, PatchableTestsMixin | |
13 | 13 | from tests.test_api_agent import logout, http_req |
14 | 14 | from tests.conftest import login_as |
15 | 15 | from faraday.server.models import SearchFilter |
16 | 16 | |
17 | 17 | |
18 | 18 | from faraday.server.api.modules.search_filter import SearchFilterView |
19 | from tests.utils.url import v2_to_v3 | |
19 | 20 | |
20 | 21 | |
21 | 22 | @pytest.mark.usefixtures('logged_user') |
22 | class TestSearchFilterAPI(ReadOnlyAPITests): | |
23 | class TestSearchFilterAPI(ReadWriteAPITests): | |
23 | 24 | model = SearchFilter |
24 | 25 | factory = SearchFilterFactory |
25 | 26 | api_endpoint = 'searchfilter' |
26 | 27 | view_class = SearchFilterView |
28 | patchable_fields = ['name'] | |
27 | 29 | |
28 | 30 | pytest.fixture(autouse=True) |
29 | 31 | |
30 | def test_list_retrieves_all_items_from(self, test_client): | |
31 | return | |
32 | def test_list_retrieves_all_items_from(self, test_client, logged_user): | |
33 | for searchfilter in SearchFilter.query.all(): | |
34 | searchfilter.creator = logged_user | |
35 | super(TestSearchFilterAPI, self).test_list_retrieves_all_items_from(test_client, logged_user) | |
32 | 36 | |
33 | 37 | def test_list_retrieves_all_items_from_logger_user(self, test_client, session, logged_user): |
34 | 38 | user_filter = SearchFilterFactory.create(creator=logged_user) |
41 | 45 | else: |
42 | 46 | assert len(res.json) == 1 |
43 | 47 | |
44 | def test_retrieve_one_object(self): | |
45 | return | |
48 | def test_retrieve_one_object(self, test_client, logged_user): | |
49 | self.first_object.creator = logged_user | |
50 | super(TestSearchFilterAPI, self).test_retrieve_one_object(test_client, logged_user) | |
46 | 51 | |
47 | 52 | def test_retrieve_one_object_from_logged_user(self, test_client, session, logged_user): |
48 | 53 | |
53 | 58 | filters.append(user_filter) |
54 | 59 | |
55 | 60 | session.commit() |
56 | ||
61 | ||
57 | 62 | print(self.url(filters[randrange(5)])) |
58 | 63 | res = test_client.get(self.url(filters[randrange(5)])) |
59 | 64 | assert res.status_code == 200 |
61 | 66 | |
62 | 67 | def test_retrieve_filter_from_another_user(self, test_client, session, logged_user): |
63 | 68 | user_filter = SearchFilterFactory.create(creator=logged_user) |
64 | another_user = UserFactory.create() | |
69 | another_user = UserFactory.create() | |
65 | 70 | session.add(user_filter) |
66 | 71 | session.add(another_user) |
67 | 72 | session.commit() |
74 | 79 | |
75 | 80 | def test_retrieve_filter_list_is_empty_from_another_user(self, test_client, session, logged_user): |
76 | 81 | user_filter = SearchFilterFactory.create(creator=logged_user) |
77 | another_user = UserFactory.create() | |
82 | another_user = UserFactory.create() | |
78 | 83 | session.add(user_filter) |
79 | 84 | session.add(another_user) |
80 | 85 | session.commit() |
88 | 93 | |
89 | 94 | def test_delete_filter_from_another_user(self, test_client, session, logged_user): |
90 | 95 | user_filter = SearchFilterFactory.create(creator=logged_user) |
91 | another_user = UserFactory.create() | |
96 | another_user = UserFactory.create() | |
92 | 97 | session.add(user_filter) |
93 | 98 | session.add(another_user) |
94 | 99 | session.commit() |
97 | 102 | login_as(test_client, another_user) |
98 | 103 | |
99 | 104 | res = test_client.delete(self.url(user_filter)) |
100 | assert res.status_code == 404⏎ | |
105 | assert res.status_code == 404 | |
106 | ||
107 | @pytest.mark.parametrize("method", ["PUT"]) | |
108 | def test_update_an_object(self, test_client, logged_user, method): | |
109 | self.first_object.creator = logged_user | |
110 | super(TestSearchFilterAPI, self).test_update_an_object(test_client, logged_user, method) | |
111 | ||
112 | def test_update_an_object_fails_with_empty_dict(self, test_client, logged_user): | |
113 | self.first_object.creator = logged_user | |
114 | super(TestSearchFilterAPI, self).test_update_an_object_fails_with_empty_dict(test_client, logged_user) | |
115 | ||
116 | def test_delete(self, test_client, logged_user): | |
117 | self.first_object.creator = logged_user | |
118 | super(TestSearchFilterAPI, self).test_delete(test_client, logged_user) | |
119 | ||
120 | ||
121 | @pytest.mark.usefixtures('logged_user') | |
122 | class TestSearchFilterAPIV3(TestSearchFilterAPI, PatchableTestsMixin): | |
123 | def url(self, obj=None): | |
124 | return v2_to_v3(super(TestSearchFilterAPIV3, self).url(obj)) | |
125 | ||
126 | @pytest.mark.parametrize("method", ["PUT", "PATCH"]) | |
127 | def test_update_an_object(self, test_client, logged_user, method): | |
128 | super(TestSearchFilterAPIV3, self).test_update_an_object(test_client, logged_user, method) | |
129 | ||
130 | def test_patch_update_an_object_does_not_fail_with_partial_data(self, test_client, logged_user): | |
131 | self.first_object.creator = logged_user | |
132 | super(TestSearchFilterAPIV3, self).test_update_an_object_fails_with_empty_dict(test_client, logged_user) |
4 | 4 | See the file 'doc/LICENSE' for the license information |
5 | 5 | |
6 | 6 | ''' |
7 | from tests.utils.url import v2_to_v3 | |
8 | ||
7 | 9 | """Tests for many API endpoints that do not depend on workspace_name""" |
8 | 10 | try: |
9 | 11 | from urllib import urlencode |
13 | 15 | import pytest |
14 | 16 | import json |
15 | 17 | |
16 | from faraday.server.api.modules.services import ServiceView | |
18 | from faraday.server.api.modules.services import ServiceView, ServiceV3View | |
17 | 19 | from tests import factories |
18 | from tests.test_api_workspaced_base import ReadOnlyAPITests | |
20 | from tests.test_api_workspaced_base import ReadWriteAPITests, PatchableTestsMixin | |
19 | 21 | from faraday.server.models import ( |
20 | 22 | Service |
21 | 23 | ) |
23 | 25 | |
24 | 26 | |
25 | 27 | @pytest.mark.usefixtures('logged_user') |
26 | class TestListServiceView(ReadOnlyAPITests): | |
28 | class TestListServiceView(ReadWriteAPITests): | |
27 | 29 | model = Service |
28 | 30 | factory = factories.ServiceFactory |
29 | 31 | api_endpoint = 'services' |
30 | 32 | view_class = ServiceView |
33 | patchable_fields = ['name'] | |
34 | ||
35 | def control_cant_change_data(self, data: dict): | |
36 | if 'parent' in data: | |
37 | data['parent'] = self.first_object.host_id | |
38 | return data | |
31 | 39 | |
32 | 40 | def test_service_list_backwards_compatibility(self, test_client, |
33 | 41 | second_workspace, session): |
73 | 81 | assert service.port == 21 |
74 | 82 | assert service.host is host |
75 | 83 | |
84 | @pytest.mark.skip # more detailed test above | |
85 | def test_create_succeeds(self, test_client): | |
86 | pass | |
87 | ||
76 | 88 | def test_create_fails_with_invalid_status(self, test_client, |
77 | 89 | host, session): |
78 | 90 | session.commit() |
220 | 232 | updated_service = Service.query.filter_by(id=service.id).first() |
221 | 233 | assert updated_service.port == 221 |
222 | 234 | |
223 | def test_update_cant_change_id(self, test_client, session): | |
235 | @pytest.mark.parametrize("method", ["PUT"]) | |
236 | def test_update_cant_change_id(self, test_client, session, method): | |
224 | 237 | service = self.factory() |
225 | 238 | host = HostFactory.create() |
226 | 239 | session.commit() |
227 | 240 | raw_data = self._raw_put_data(service.id) |
228 | res = test_client.put(self.url(service, workspace=service.workspace), data=raw_data) | |
241 | if method == "PUT": | |
242 | res = test_client.put(self.url(service, workspace=service.workspace), data=raw_data) | |
243 | if method == "PATCH": | |
244 | res = test_client.patch(self.url(service, workspace=service.workspace), data=raw_data) | |
245 | ||
229 | 246 | assert res.status_code == 200, res.json |
230 | 247 | assert res.json['id'] == service.id |
231 | 248 | |
322 | 339 | assert res.status_code == 400 |
323 | 340 | |
324 | 341 | |
325 | # I'm Py3 | |
342 | class TestListServiceViewV3(TestListServiceView, PatchableTestsMixin): | |
343 | view_class = ServiceV3View | |
344 | ||
345 | def url(self, obj=None, workspace=None): | |
346 | return v2_to_v3(super(TestListServiceViewV3, self).url(obj, workspace)) | |
347 | ||
348 | @pytest.mark.parametrize("method", ["PUT", "PATCH"]) | |
349 | def test_update_cant_change_id(self, test_client, session, method): | |
350 | super(TestListServiceViewV3, self).test_update_cant_change_id(test_client, session, method) |
7 | 7 | import pytest |
8 | 8 | from tests.conftest import login_as |
9 | 9 | |
10 | ||
10 | 11 | @pytest.mark.usefixtures('logged_user') |
11 | class TestSessionLogged(): | |
12 | class TestSessionLogged: | |
12 | 13 | def test_session_when_user_is_logged(self, test_client): |
13 | 14 | res = test_client.get('/session') |
14 | 15 | assert res.status_code == 200 |
22 | 23 | assert res.json['role'] == role |
23 | 24 | |
24 | 25 | |
25 | class TestSessionNotLogged(): | |
26 | class TestSessionNotLogged: | |
26 | 27 | def test_session_when_user_is_not_logged(self, test_client): |
27 | 28 | res = test_client.get('/session') |
28 | 29 | assert res.status_code == 401 |
29 | ||
30 | ||
31 | # I'm Py3 |
13 | 13 | from faraday.server.threads.reports_processor import REPORTS_QUEUE |
14 | 14 | |
15 | 15 | from faraday.server.models import Host, Service, Command |
16 | from tests.utils.url import v2_to_v3 | |
16 | 17 | |
17 | 18 | |
18 | 19 | @pytest.mark.usefixtures('logged_user') |
19 | class TestFileUpload(): | |
20 | class TestFileUpload: | |
21 | ||
22 | def check_url(self, url): | |
23 | return url | |
20 | 24 | |
21 | 25 | def test_file_upload(self, test_client, session, csrf_token, logged_user): |
22 | 26 | ws = WorkspaceFactory.create(name="abc") |
32 | 36 | } |
33 | 37 | |
34 | 38 | res = test_client.post( |
35 | f'/v2/ws/{ws.name}/upload_report', | |
39 | self.check_url(f'/v2/ws/{ws.name}/upload_report'), | |
36 | 40 | data=data, |
37 | 41 | use_json_data=False) |
38 | 42 | |
39 | 43 | assert res.status_code == 200 |
40 | 44 | assert len(REPORTS_QUEUE.queue) == 1 |
41 | queue_elem = REPORTS_QUEUE.queue[0] | |
45 | queue_elem = REPORTS_QUEUE.get_nowait() | |
42 | 46 | assert queue_elem[0] == ws.name |
43 | 47 | assert queue_elem[3].lower() == "nmap" |
44 | 48 | assert queue_elem[4] == logged_user.id |
64 | 68 | assert service |
65 | 69 | assert service.creator_id == logged_user_id |
66 | 70 | |
67 | ||
68 | 71 | def test_no_file_in_request(self, test_client, session): |
69 | 72 | ws = WorkspaceFactory.create(name="abc") |
70 | 73 | session.add(ws) |
71 | 74 | session.commit() |
72 | 75 | |
73 | res = test_client.post( | |
74 | f'/v2/ws/{ws.name}/upload_report') | |
76 | res = test_client.post(self.check_url(f'/v2/ws/{ws.name}/upload_report')) | |
75 | 77 | |
76 | 78 | assert res.status_code == 400 |
77 | ||
78 | 79 | |
79 | 80 | def test_request_without_csrf_token(self, test_client, session): |
80 | 81 | ws = WorkspaceFactory.create(name="abc") |
90 | 91 | } |
91 | 92 | |
92 | 93 | res = test_client.post( |
93 | f'/v2/ws/{ws.name}/upload_report', | |
94 | self.check_url(f'/v2/ws/{ws.name}/upload_report'), | |
94 | 95 | data=data, |
95 | 96 | use_json_data=False) |
96 | 97 | |
111 | 112 | 'csrf_token': csrf_token |
112 | 113 | } |
113 | 114 | res = test_client.post( |
114 | f'/v2/ws/{ws.name}/upload_report', | |
115 | self.check_url(f'/v2/ws/{ws.name}/upload_report'), | |
115 | 116 | data=data, |
116 | use_json_data=False) | |
117 | use_json_data=False | |
118 | ) | |
117 | 119 | |
118 | 120 | assert res.status_code == 404 |
121 | ||
122 | ||
123 | class TestFileUploadV3(TestFileUpload): | |
124 | def check_url(self, url): | |
125 | return v2_to_v3(url) |
13 | 13 | from tempfile import NamedTemporaryFile |
14 | 14 | from base64 import b64encode |
15 | 15 | from io import BytesIO, StringIO |
16 | from posixpath import join as urljoin | |
17 | ||
18 | from tests.utils.url import v2_to_v3 | |
19 | ||
16 | 20 | try: |
17 | 21 | from urllib import urlencode |
18 | 22 | except ImportError: |
30 | 34 | from faraday.server.api.modules.vulns import ( |
31 | 35 | VulnerabilityFilterSet, |
32 | 36 | VulnerabilitySchema, |
33 | VulnerabilityView | |
37 | VulnerabilityView, | |
38 | VulnerabilityV3View | |
34 | 39 | ) |
35 | 40 | from faraday.server.fields import FaradayUploadedFile |
36 | 41 | from faraday.server.schemas import NullToBlankString |
37 | 42 | from tests import factories |
38 | 43 | from tests.conftest import TEST_DATA_PATH |
39 | 44 | from tests.test_api_workspaced_base import ( |
40 | ReadOnlyAPITests | |
45 | ReadWriteAPITests, PatchableTestsMixin | |
41 | 46 | ) |
42 | 47 | from faraday.server.models import ( |
43 | 48 | VulnerabilityGeneric, |
150 | 155 | |
151 | 156 | |
152 | 157 | @pytest.mark.usefixtures('logged_user') |
153 | class TestListVulnerabilityView(ReadOnlyAPITests): # TODO migration: use read write api tests | |
158 | class TestListVulnerabilityView(ReadWriteAPITests): | |
154 | 159 | model = Vulnerability |
155 | 160 | factory = factories.VulnerabilityFactory |
156 | 161 | api_endpoint = 'vulns' |
157 | 162 | #unique_fields = ['ip'] |
158 | 163 | #update_fields = ['ip', 'description', 'os'] |
159 | 164 | view_class = VulnerabilityView |
165 | patchable_fields = ['description'] | |
166 | ||
167 | def check_url(self, url): | |
168 | return url | |
160 | 169 | |
161 | 170 | def test_backward_json_compatibility(self, test_client, second_workspace, session): |
162 | 171 | new_obj = self.factory.create(workspace=second_workspace) |
290 | 299 | description='helloworld', |
291 | 300 | severity='low', |
292 | 301 | ) |
293 | ws_name = host_with_hostnames.workspace.name | |
302 | ws = host_with_hostnames.workspace | |
294 | 303 | vuln_count_previous = session.query(Vulnerability).count() |
295 | res = test_client.post(f'/v2/ws/{ws_name}/vulns/', data=raw_data) | |
304 | res = test_client.post(self.url(workspace=ws), data=raw_data) | |
296 | 305 | assert res.status_code == 201 |
297 | 306 | assert vuln_count_previous + 1 == session.query(Vulnerability).count() |
298 | 307 | assert res.json['name'] == 'New vulns' |
365 | 374 | assert filename in res.json['_attachments'] |
366 | 375 | attachment.close() |
367 | 376 | # check the attachment can be downloaded |
368 | res = test_client.get(self.url() + f'{vuln_id}/attachment/{filename}/') | |
377 | res = test_client.get(self.check_url(urljoin(self.url(), f'{vuln_id}/attachment/{filename}/'))) | |
369 | 378 | assert res.status_code == 200 |
370 | 379 | assert res.data == file_content |
371 | 380 | |
372 | res = test_client.get( | |
373 | self.url() + | |
374 | f'{vuln_id}/attachment/notexistingattachment.png/') | |
381 | res = test_client.get(self.check_url(urljoin( | |
382 | self.url(), | |
383 | f'{vuln_id}/attachment/notexistingattachment.png/' | |
384 | ))) | |
375 | 385 | assert res.status_code == 404 |
376 | 386 | |
377 | 387 | @pytest.mark.usefixtures('ignore_nplusone') |
395 | 405 | res = test_client.put(self.url(obj=vuln, workspace=self.workspace), data=raw_data) |
396 | 406 | assert res.status_code == 200 |
397 | 407 | filename = attachment.name.split('/')[-1] |
398 | res = test_client.get( | |
399 | self.url() + f'{vuln.id}/attachment/{filename}/') | |
408 | res = test_client.get(self.check_url(urljoin( | |
409 | self.url(), f'{vuln.id}/attachment/{filename}/' | |
410 | ))) | |
400 | 411 | assert res.status_code == 200 |
401 | 412 | assert res.data == file_content |
402 | 413 | |
418 | 429 | assert res.status_code == 200 |
419 | 430 | |
420 | 431 | # verify that the old file was deleted and the new one exists |
421 | res = test_client.get( | |
422 | self.url() + f'{vuln.id}/attachment/{filename}/') | |
432 | res = test_client.get(self.check_url(urljoin( | |
433 | self.url(), f'{vuln.id}/attachment/{filename}/' | |
434 | ))) | |
423 | 435 | assert res.status_code == 404 |
424 | res = test_client.get( | |
425 | self.url() + f'{vuln.id}/attachment/{new_filename}/') | |
436 | res = test_client.get(self.check_url(urljoin( | |
437 | self.url(), f'{vuln.id}/attachment/{new_filename}/' | |
438 | ))) | |
426 | 439 | assert res.status_code == 200 |
427 | 440 | assert res.data == file_content |
428 | 441 | |
440 | 453 | session.add(new_attach) |
441 | 454 | session.commit() |
442 | 455 | |
443 | res = test_client.get(self.url(workspace=workspace) + f'{vuln.id}/attachments/') | |
456 | if 'v2' in self.view_class.route_prefix: | |
457 | route_part = 'attachments' | |
458 | else: | |
459 | route_part = 'attachment' | |
460 | ||
461 | res = test_client.get(self.check_url(urljoin(self.url(workspace=workspace), f'{vuln.id}/{route_part}/'))) | |
444 | 462 | assert res.status_code == 200 |
445 | 463 | assert new_attach.filename in res.json |
446 | 464 | assert 'image/png' in res.json[new_attach.filename]['content_type'] |
447 | ||
448 | 465 | |
449 | 466 | def test_create_vuln_props(self, host_with_hostnames, test_client, session): |
450 | 467 | """ |
475 | 492 | severity='low', |
476 | 493 | **vuln_props |
477 | 494 | ) |
478 | ws_name = host_with_hostnames.workspace.name | |
495 | ws = host_with_hostnames.workspace | |
479 | 496 | vuln_count_previous = session.query(Vulnerability).count() |
480 | res = test_client.post(f'/v2/ws/{ws_name}/vulns/', data=raw_data) | |
497 | res = test_client.post(self.url(workspace=ws), data=raw_data) | |
481 | 498 | assert res.status_code == 201 |
482 | 499 | for prop, value in vuln_props.items(): |
483 | 500 | if prop not in vuln_props_excluded: |
502 | 519 | description='Description goes here', |
503 | 520 | severity='critical', |
504 | 521 | ) |
505 | ws_name = host_with_hostnames.workspace.name | |
522 | ws = host_with_hostnames.workspace | |
506 | 523 | vuln_count_previous = session.query(Vulnerability).count() |
507 | res = test_client.post(f'/v2/ws/{ws_name}/vulns/', data=raw_data) | |
524 | res = test_client.post(self.url(workspace=ws), data=raw_data) | |
508 | 525 | assert res.status_code == 201 |
509 | 526 | assert vuln_count_previous + 1 == session.query(Vulnerability).count() |
510 | res = test_client.post(f'/v2/ws/{ws_name}/vulns/', data=raw_data) | |
527 | res = test_client.post(self.url(workspace=ws), data=raw_data) | |
511 | 528 | assert res.status_code == 409 |
512 | 529 | assert vuln_count_previous + 1 == session.query(Vulnerability).count() |
513 | 530 | |
522 | 539 | refs=[], |
523 | 540 | policyviolations=[] |
524 | 541 | ) |
525 | ws_name = host_with_hostnames.workspace.name | |
542 | ws = host_with_hostnames.workspace | |
526 | 543 | vuln_count_previous = session.query(Vulnerability).count() |
527 | res = test_client.post(f'/v2/ws/{ws_name}/vulns/', data=raw_data) | |
544 | res = test_client.post(self.url(workspace=ws), data=raw_data) | |
528 | 545 | assert res.status_code == 201 |
529 | 546 | assert vuln_count_previous + 1 == session.query(Vulnerability).count() |
530 | 547 | assert res.json['status'] == 'closed' |
638 | 655 | refs=[], |
639 | 656 | policyviolations=[] |
640 | 657 | ) |
641 | ws_name = host_with_hostnames.workspace.name | |
658 | ws = host_with_hostnames.workspace | |
642 | 659 | vuln_count_previous = session.query(Vulnerability).count() |
643 | 660 | vuln_web_count_previous = session.query(VulnerabilityWeb).count() |
644 | res = test_client.post(f'/v2/ws/{ws_name}/vulns/', data=raw_data) | |
661 | res = test_client.post(self.url(workspace=ws), data=raw_data) | |
645 | 662 | assert res.status_code == 201 |
646 | 663 | assert vuln_web_count_previous + 1 == session.query(VulnerabilityWeb).count() |
647 | 664 | assert vuln_count_previous == session.query(Vulnerability).count() |
828 | 845 | host_factory.create(workspace=workspace, ip="192.168.0.2") |
829 | 846 | |
830 | 847 | session.commit() |
831 | res = test_client.get(f'{self.url()}' | |
832 | f'filter?q={{"filters":[{{"name": "target", "op":"eq", "val":"192.168.0.2"}}]}}') | |
848 | res = test_client.get(self.check_url(urljoin( | |
849 | self.url(), 'filter?q={"filters":[{"name": "target", "op":"eq", "val":"192.168.0.2"}]}' | |
850 | ))) | |
833 | 851 | assert res.status_code == 200 |
834 | 852 | |
835 | 853 | @pytest.mark.usefixtures('ignore_nplusone') |
846 | 864 | 10, workspace=self.workspace, host=host2, service=None) |
847 | 865 | |
848 | 866 | session.commit() |
849 | res = test_client.get(f'{self.url()}' | |
850 | f'filter?q={{"filters":[{{"name": "target_host_ip", "op":"eq", "val":"192.168.0.2"}}]}}') | |
867 | res = test_client.get(self.check_url(urljoin( | |
868 | self.url(), | |
869 | 'filter?q={"filters":[{"name": "target_host_ip", "op":"eq", "val":"192.168.0.2"}]}' | |
870 | ))) | |
851 | 871 | assert res.status_code == 200 |
852 | 872 | assert len(res.json['vulnerabilities']) == 1 |
853 | 873 | assert res.json['vulnerabilities'][0]['value']['target'] == '192.168.0.2' |
869 | 889 | 10, workspace=self.workspace, host=None, service=service) |
870 | 890 | |
871 | 891 | session.commit() |
872 | res = test_client.get(f'{self.url()}' | |
873 | f'filter?q={{"filters":[{{"name": "service", "op":"has", "val":{{"name": "port", "op":"eq", "val":"8956"}} }}]}}') | |
892 | res = test_client.get(self.check_url(urljoin( | |
893 | self.url(), | |
894 | 'filter?q={"filters":[{"name": "service", "op":"has", "val":{"name": "port", "op":"eq", "val":"8956"}}]}' | |
895 | ))) | |
874 | 896 | assert res.status_code == 200 |
875 | 897 | assert len(res.json['vulnerabilities']) == 10 |
876 | 898 | assert res.json['count'] == 10 |
877 | ||
878 | 899 | |
879 | 900 | @pytest.mark.usefixtures('ignore_nplusone') |
880 | 901 | def test_filter_restless_by_service_name(self, test_client, session, workspace, |
892 | 913 | 10, workspace=self.workspace, host=None, service=service) |
893 | 914 | |
894 | 915 | session.commit() |
895 | res = test_client.get(f'{self.url()}' | |
896 | f'filter?q={{"filters":[{{"name": "service", "op":"has", "val":{{"name": "name", "op":"eq", "val":"ssh"}} }}]}}') | |
916 | res = test_client.get(self.check_url(urljoin( | |
917 | self.url(), | |
918 | 'filter?q={"filters":[{"name": "service", "op":"has", "val":{"name": "name", "op":"eq", "val":"ssh"}}]}' | |
919 | ))) | |
897 | 920 | assert res.status_code == 200 |
898 | 921 | assert len(res.json['vulnerabilities']) == 1 |
899 | 922 | assert res.json['count'] == 1 |
944 | 967 | policyviolations=[], |
945 | 968 | attachments=attachments, |
946 | 969 | ) |
947 | ws_name = host_with_hostnames.workspace.name | |
970 | ws = host_with_hostnames.workspace | |
948 | 971 | vuln_count_previous = session.query(Vulnerability).count() |
949 | res = test_client.post(f'/v2/ws/{ws_name}/vulns/', data=raw_data) | |
972 | res = test_client.post(self.url(workspace=ws), data=raw_data) | |
950 | 973 | |
951 | 974 | assert res.status_code == 201 |
952 | 975 | assert len(res.json['_attachments']) == 2 |
963 | 986 | refs=['CVE-2017-0002', 'CVE-2017-0012', 'CVE-2017-0012'], |
964 | 987 | policyviolations=[] |
965 | 988 | ) |
966 | ws_name = host_with_hostnames.workspace.name | |
989 | ws = host_with_hostnames.workspace | |
967 | 990 | vuln_count_previous = session.query(Vulnerability).count() |
968 | res = test_client.post(f'/v2/ws/{ws_name}/vulns/', data=raw_data) | |
991 | res = test_client.post(self.url(workspace=ws), data=raw_data) | |
969 | 992 | assert res.status_code == 201 |
970 | 993 | assert session.query(Reference).count() == 2 |
971 | 994 | assert vuln_count_previous + 1 == session.query(Vulnerability).count() |
981 | 1004 | policyviolations=['PCI DSS Credir card not encrypted', |
982 | 1005 | 'PCI DSS Credir card not encrypted'], |
983 | 1006 | ) |
984 | ws_name = host_with_hostnames.workspace.name | |
1007 | ws = host_with_hostnames.workspace | |
985 | 1008 | vuln_count_previous = session.query(Vulnerability).count() |
986 | res = test_client.post(f'/v2/ws/{ws_name}/vulns/', data=raw_data) | |
1009 | res = test_client.post(self.url(workspace=ws), data=raw_data) | |
987 | 1010 | assert res.status_code == 201 |
988 | 1011 | assert session.query(PolicyViolation).count() == 1 |
989 | 1012 | assert vuln_count_previous + 1 == session.query(Vulnerability).count() |
1004 | 1027 | 'integrity': True |
1005 | 1028 | } |
1006 | 1029 | ) |
1007 | ws_name = host_with_hostnames.workspace.name | |
1030 | ws = host_with_hostnames.workspace | |
1008 | 1031 | vuln_count_previous = session.query(Vulnerability).count() |
1009 | res = test_client.post(f'/v2/ws/{ws_name}/vulns/', data=raw_data) | |
1032 | res = test_client.post(self.url(workspace=ws), data=raw_data) | |
1010 | 1033 | assert res.status_code == 201 |
1011 | 1034 | assert vuln_count_previous + 1 == session.query(Vulnerability).count() |
1012 | 1035 | assert res.json['name'] == 'New vulns' |
1031 | 1054 | 'invalid': None, |
1032 | 1055 | } |
1033 | 1056 | ) |
1034 | ws_name = host_with_hostnames.workspace.name | |
1057 | ws = host_with_hostnames.workspace | |
1035 | 1058 | vuln_count_previous = session.query(Vulnerability).count() |
1036 | res = test_client.post(f'/v2/ws/{ws_name}/vulns/', data=raw_data) | |
1059 | res = test_client.post(self.url(workspace=ws), data=raw_data) | |
1037 | 1060 | assert res.status_code == 400 |
1038 | 1061 | |
1039 | 1062 | def test_create_vuln_with_invalid_type(self, |
1049 | 1072 | refs=[], |
1050 | 1073 | policyviolations=[] |
1051 | 1074 | ) |
1052 | ws_name = host_with_hostnames.workspace.name | |
1075 | ws = host_with_hostnames.workspace | |
1053 | 1076 | vuln_count_previous = session.query(Vulnerability).count() |
1054 | 1077 | res = test_client.post( |
1055 | f'/v2/ws/{ws_name}/vulns/', | |
1078 | self.url(workspace=ws), | |
1056 | 1079 | data=raw_data, |
1057 | 1080 | ) |
1058 | 1081 | assert res.status_code == 400 |
1079 | 1102 | severity='low', |
1080 | 1103 | ) |
1081 | 1104 | raw_data.pop("type") |
1082 | ws_name = host_with_hostnames.workspace.name | |
1105 | ws = host_with_hostnames.workspace | |
1083 | 1106 | vuln_count_previous = session.query(Vulnerability).count() |
1084 | res = test_client.post(f'/v2/ws/{ws_name}/vulns/', data=raw_data) | |
1107 | res = test_client.post(self.url(workspace=ws), data=raw_data) | |
1085 | 1108 | assert res.status_code == 400 |
1086 | 1109 | assert vuln_count_previous == session.query(Vulnerability).count() |
1087 | 1110 | assert res.json['message'] == 'Type is required.' |
1099 | 1122 | policyviolations=[], |
1100 | 1123 | severity="invalid", |
1101 | 1124 | ) |
1102 | ws_name = host_with_hostnames.workspace.name | |
1125 | ws = host_with_hostnames.workspace | |
1103 | 1126 | vuln_count_previous = session.query(Vulnerability).count() |
1104 | res = test_client.post(f'/v2/ws/{ws_name}/vulns/', data=raw_data) | |
1127 | res = test_client.post(self.url(workspace=ws), data=raw_data) | |
1105 | 1128 | assert res.status_code == 400 |
1106 | 1129 | assert vuln_count_previous == session.query(Vulnerability).count() |
1107 | 1130 | assert b'Invalid severity type.' in res.data |
1120 | 1143 | policyviolations=[], |
1121 | 1144 | easeofresolution='frutafrutafruta' |
1122 | 1145 | ) |
1123 | ws_name = host_with_hostnames.workspace.name | |
1146 | ws = host_with_hostnames.workspace | |
1124 | 1147 | vuln_count_previous = session.query(Vulnerability).count() |
1125 | res = test_client.post(f'/v2/ws/{ws_name}/vulns/', data=raw_data) | |
1148 | res = test_client.post(self.url(workspace=ws), data=raw_data) | |
1126 | 1149 | assert res.status_code == 400 |
1127 | 1150 | assert vuln_count_previous == session.query(Vulnerability).count() |
1128 | 1151 | assert list(res.json['messages']['json'].keys()) == ['easeofresolution'] |
1142 | 1165 | policyviolations=[], |
1143 | 1166 | easeofresolution=None, |
1144 | 1167 | ) |
1145 | ws_name = host_with_hostnames.workspace.name | |
1146 | res = test_client.post(f'/v2/ws/{ws_name}/vulns/', | |
1168 | ws = host_with_hostnames.workspace | |
1169 | res = test_client.post(self.url(workspace=ws), | |
1147 | 1170 | data=raw_data) |
1148 | 1171 | assert res.status_code == 201, (res.status_code, res.data) |
1149 | 1172 | created_vuln = Vulnerability.query.get(res.json['_id']) |
1150 | 1173 | assert created_vuln.ease_of_resolution is None |
1151 | ||
1152 | 1174 | |
1153 | 1175 | def test_count_order_by_incorrect_keyword(self, test_client, session): |
1154 | 1176 | for i, vuln in enumerate(self.objects[:3]): |
1163 | 1185 | session.commit() |
1164 | 1186 | |
1165 | 1187 | #Desc |
1166 | res = test_client.get(self.url() + | |
1167 | "count/?confirmed=1&group_by=severity&order=sc") | |
1188 | res = test_client.get( | |
1189 | self.check_url(urljoin(self.url(), "count/")) + | |
1190 | "?confirmed=1&group_by=severity&order=sc" | |
1191 | ) | |
1168 | 1192 | assert res.status_code == 400 |
1169 | 1193 | |
1170 | 1194 | #Asc |
1171 | res = test_client.get(self.url() + | |
1172 | "count/?confirmed=1&group_by=severity&order=name,asc") | |
1195 | res = test_client.get( | |
1196 | self.check_url(urljoin(self.url(), "count/")) + | |
1197 | "?confirmed=1&group_by=severity&order=name,asc" | |
1198 | ) | |
1173 | 1199 | assert res.status_code == 400 |
1174 | 1200 | |
1175 | 1201 | |
1186 | 1212 | session.commit() |
1187 | 1213 | |
1188 | 1214 | #Desc |
1189 | res = test_client.get(self.url() + | |
1190 | "count/?confirmed=1&group_by=severity&order=desc") | |
1215 | res = test_client.get( | |
1216 | self.check_url(urljoin(self.url(),"count/")) + "?confirmed=1&group_by=severity&order=desc" | |
1217 | ) | |
1191 | 1218 | assert res.status_code == 200 |
1192 | 1219 | assert res.json['total_count'] == 3 |
1193 | 1220 | assert sorted(res.json['groups'], key=lambda i: (i['name'],i['count'],i['severity'])) == sorted([ |
1196 | 1223 | ], key=lambda i: (i['name'],i['count'],i['severity'])) |
1197 | 1224 | |
1198 | 1225 | #Asc |
1199 | res = test_client.get(self.url() + | |
1200 | "count/?confirmed=1&group_by=severity&order=asc") | |
1226 | res = test_client.get(self.check_url(urljoin(self.url(),"count/"))+"?confirmed=1&group_by=severity&order=asc") | |
1201 | 1227 | assert res.status_code == 200 |
1202 | 1228 | assert res.json['total_count'] == 3 |
1203 | 1229 | assert sorted(res.json['groups'], key=lambda i: (i['name'],i['count'],i['severity']), reverse=True) == sorted([ |
1204 | 1230 | {"name": "critical", "severity": "critical", "count": 1}, |
1205 | 1231 | {"name": "high", "severity": "high", "count": 2}, |
1206 | 1232 | ], key=lambda i: (i['name'],i['count'],i['severity']), reverse=True) |
1207 | ||
1208 | 1233 | |
1209 | 1234 | def test_count_group_by_incorrect_vuln_column(self, test_client, session): |
1210 | 1235 | for i, vuln in enumerate(self.objects[:3]): |
1218 | 1243 | session.add(vuln) |
1219 | 1244 | session.commit() |
1220 | 1245 | |
1221 | res = test_client.get(self.url() + | |
1222 | "count/?confirmed=1&group_by=username") | |
1246 | res = test_client.get(self.check_url(urljoin(self.url(),"count/")) + "?confirmed=1&group_by=username") | |
1223 | 1247 | assert res.status_code == 400 |
1224 | 1248 | |
1225 | res = test_client.get(self.url() + | |
1226 | "count/?confirmed=1&group_by=") | |
1249 | res = test_client.get(self.check_url(urljoin(self.url(),"count/")) + "?confirmed=1&group_by=") | |
1227 | 1250 | assert res.status_code == 400 |
1228 | ||
1229 | ||
1230 | 1251 | |
1231 | 1252 | def test_count_confirmed(self, test_client, session): |
1232 | 1253 | for i, vuln in enumerate(self.objects[:3]): |
1241 | 1262 | session.add(vuln) |
1242 | 1263 | session.commit() |
1243 | 1264 | |
1244 | res = test_client.get(self.url() + | |
1245 | 'count/?confirmed=1&group_by=severity') | |
1265 | res = test_client.get(self.check_url(urljoin(self.url(),'count/')) + '?confirmed=1&group_by=severity') | |
1246 | 1266 | assert res.status_code == 200 |
1247 | 1267 | assert res.json['total_count'] == 3 |
1248 | 1268 | assert sorted(res.json['groups'], key=lambda i: (i['count'],i['name'],i['severity'])) == sorted([ |
1260 | 1280 | session.add_all(vulns) |
1261 | 1281 | session.commit() |
1262 | 1282 | |
1263 | res = test_client.get(self.url(workspace=second_workspace) + | |
1264 | 'count/?group_by=severity') | |
1283 | res = test_client.get( | |
1284 | self.check_url(urljoin(self.url(workspace=second_workspace),'count/')) + '?group_by=severity' | |
1285 | ) | |
1265 | 1286 | assert res.status_code == 200 |
1266 | 1287 | assert res.json['total_count'] == 9 |
1267 | 1288 | assert sorted(res.json['groups'], key=lambda i: (i['count'],i['name'],i['severity'])) == sorted([ |
1282 | 1303 | session.add(vuln) |
1283 | 1304 | session.commit() |
1284 | 1305 | |
1285 | res = test_client.get(f'{self.url()}' | |
1286 | f'count_multi_workspace/?workspaces=' | |
1287 | f'{self.workspace.name}' | |
1288 | f'&confirmed=1&group_by=severity&order=desc') | |
1306 | res = test_client.get( | |
1307 | self.check_url(urljoin(self.url(), 'count_multi_workspace/')) + | |
1308 | f'?workspaces={self.workspace.name}&confirmed=1&group_by=severity&order=desc' | |
1309 | ) | |
1289 | 1310 | |
1290 | 1311 | assert res.status_code == 200 |
1291 | 1312 | assert len(res.json['groups']) == 1 |
1292 | 1313 | assert res.json['total_count'] == 5 |
1293 | ||
1294 | 1314 | |
1295 | 1315 | def test_count_multiworkspace_two_public_workspaces(self, test_client, session, second_workspace): |
1296 | 1316 | vulns = self.factory.create_batch(1, severity='informational', |
1313 | 1333 | session.add(vuln) |
1314 | 1334 | session.commit() |
1315 | 1335 | |
1316 | res = test_client.get(f'{self.url()}' | |
1317 | f'count_multi_workspace/?workspaces=' | |
1318 | f'{self.workspace.name}' | |
1319 | f',' | |
1320 | f'{second_workspace.name}' | |
1321 | f'&confirmed=1&group_by=severity&order=desc') | |
1336 | res = test_client.get( | |
1337 | self.check_url(urljoin(self.url(), 'count_multi_workspace/')) + | |
1338 | f'?workspaces={self.workspace.name},{second_workspace.name}&confirmed=1&group_by=severity&order=desc' | |
1339 | ) | |
1322 | 1340 | |
1323 | 1341 | assert res.status_code == 200 |
1324 | 1342 | assert len(res.json['groups']) == 2 |
1325 | 1343 | assert res.json['total_count'] == 10 |
1326 | 1344 | |
1327 | 1345 | def test_count_multiworkspace_no_workspace_param(self, test_client): |
1328 | res = test_client.get(f'{self.url()}count_multi_workspace/?confirmed=1&group_by=severity&order=desc') | |
1346 | res = test_client.get( | |
1347 | self.check_url(urljoin(self.url(), 'count_multi_workspace/')) + | |
1348 | '?confirmed=1&group_by=severity&order=desc' | |
1349 | ) | |
1329 | 1350 | assert res.status_code == 400 |
1330 | 1351 | |
1331 | 1352 | def test_count_multiworkspace_no_groupby_param(self, test_client): |
1332 | res = test_client.get(f'{self.url()}count_multi_workspace/?workspaces={self.workspace.name}&confirmed=1&order=desc') | |
1353 | res = test_client.get( | |
1354 | self.check_url(urljoin(self.url(), 'count_multi_workspace/')) + | |
1355 | f'?workspaces={self.workspace.name}&confirmed=1&order=desc' | |
1356 | ) | |
1333 | 1357 | assert res.status_code == 400 |
1334 | 1358 | |
1335 | 1359 | def test_count_multiworkspace_nonexistent_ws(self, test_client): |
1336 | res = test_client.get(f'{self.url()}count_multi_workspace/?workspaces=asdf,{self.workspace.name}&confirmed=1&group_by=severity&order=desc') | |
1360 | res = test_client.get( | |
1361 | self.check_url(urljoin(self.url(), 'count_multi_workspace/')) + | |
1362 | '?workspaces=asdf,{self.workspace.name}&confirmed=1&group_by=severity&order=desc' | |
1363 | ) | |
1337 | 1364 | assert res.status_code == 404 |
1338 | ||
1339 | 1365 | |
1340 | 1366 | @pytest.mark.usefixtures('mock_envelope_list') |
1341 | 1367 | def test_target(self, test_client, session, second_workspace, |
1580 | 1606 | description='helloworld', |
1581 | 1607 | severity='low', |
1582 | 1608 | ) |
1583 | ws_name = host_with_hostnames.workspace.name | |
1584 | res = test_client.post(f'/v2/ws/{ws_name}/vulns/', data=raw_data) | |
1609 | ws = host_with_hostnames.workspace | |
1610 | res = test_client.post(self.url(workspace=ws), data=raw_data) | |
1585 | 1611 | assert res.status_code == 201 |
1586 | res = test_client.post(f'/v2/ws/{ws_name}/vulns/', | |
1587 | data=raw_data) | |
1612 | res = test_client.post(self.url(workspace=ws), data=raw_data) | |
1588 | 1613 | assert res.status_code == 409 |
1589 | 1614 | |
1590 | 1615 | def test_create_webvuln_multiple_times_returns_conflict(self, host_with_hostnames, test_client, session): |
1607 | 1632 | description='helloworld', |
1608 | 1633 | severity='low', |
1609 | 1634 | ) |
1610 | ws_name = host_with_hostnames.workspace.name | |
1611 | res = test_client.post(f'/v2/ws/{ws_name}/vulns/', data=raw_data) | |
1635 | ws = host_with_hostnames.workspace | |
1636 | res = test_client.post(self.url(workspace=ws), data=raw_data) | |
1612 | 1637 | assert res.status_code == 201 |
1613 | res = test_client.post(f'/v2/ws/{ws_name}/vulns/', | |
1614 | data=raw_data) | |
1638 | res = test_client.post(self.url(workspace=ws), data=raw_data) | |
1615 | 1639 | assert res.status_code == 409 |
1616 | 1640 | |
1617 | 1641 | def test_create_similar_vuln_service_and_vuln_web_conflict_succeed( |
1677 | 1701 | severity='low', |
1678 | 1702 | ) |
1679 | 1703 | ws_name = host_with_hostnames.workspace.name |
1680 | res = test_client.post(f'/v2/ws/{ws_name}/vulns/?command_id={command.id}', data=raw_data) | |
1704 | res = test_client.post( | |
1705 | self.check_url(self.url(workspace=host_with_hostnames.workspace)) + f'?command_id={command.id}', | |
1706 | data=raw_data | |
1707 | ) | |
1681 | 1708 | assert res.status_code == 201 |
1682 | 1709 | raw_data = _create_post_data_vulnerability( |
1683 | 1710 | name='Update vulnsweb', |
1689 | 1716 | description='Update helloworld', |
1690 | 1717 | severity='high', |
1691 | 1718 | ) |
1692 | res = test_client.put(f"/v2/ws/{ws_name}/vulns/{res.json['_id']}/?command_id={command.id}", | |
1693 | data=raw_data) | |
1694 | assert res.status_code == 200 | |
1695 | ||
1719 | res = test_client.put( | |
1720 | self.check_url(urljoin(self.url(workspace=host_with_hostnames.workspace), f'{res.json["_id"]}/')) + | |
1721 | f'?command_id={command.id}', | |
1722 | data=raw_data) | |
1723 | assert res.status_code == 200 | |
1696 | 1724 | |
1697 | 1725 | def test_create_vuln_from_command(self, test_client, session): |
1698 | 1726 | command = EmptyCommandFactory.create(workspace=self.workspace) |
1930 | 1958 | headers = {'Content-type': 'multipart/form-data'} |
1931 | 1959 | |
1932 | 1960 | res = test_client.post( |
1933 | f'/v2/ws/abc/vulns/{vuln.id}/attachment/', | |
1961 | self.check_url(f'/v2/ws/abc/vulns/{vuln.id}/attachment/'), | |
1934 | 1962 | data=data, headers=headers, use_json_data=False) |
1935 | 1963 | assert res.status_code == 403 # Missing CSRF protection |
1936 | 1964 | |
1939 | 1967 | 'csrf_token': csrf_token |
1940 | 1968 | } |
1941 | 1969 | res = test_client.post( |
1942 | f'/v2/ws/abc/vulns/{vuln.id}/attachment/', | |
1970 | self.check_url(f'/v2/ws/abc/vulns/{vuln.id}/attachment/'), | |
1943 | 1971 | data=data, headers=headers, use_json_data=False) |
1944 | 1972 | assert res.status_code == 200 # Now it should work |
1945 | 1973 | |
1963 | 1991 | session.commit() |
1964 | 1992 | |
1965 | 1993 | res = test_client.post( |
1966 | f'/v2/ws/abc/vulns/{vuln.id}/attachment/', | |
1994 | self.check_url(f'/v2/ws/abc/vulns/{vuln.id}/attachment/'), | |
1967 | 1995 | data=data, headers=headers, use_json_data=False) |
1968 | 1996 | assert res.status_code == 403 |
1969 | 1997 | query_test = session.query(Vulnerability).filter_by(id=vuln.id).first().evidence |
1985 | 2013 | policyviolations=[], |
1986 | 2014 | attachments=[attachment] |
1987 | 2015 | ) |
1988 | res = test_client.post(f'/v2/ws/{ws_name}/vulns/', data=vuln) | |
2016 | res = test_client.post(self.check_url(f'/v2/ws/{ws_name}/vulns/'), data=vuln) | |
1989 | 2017 | assert res.status_code == 201 |
1990 | 2018 | |
1991 | 2019 | filename = attachment.name.split('/')[-1] |
1992 | 2020 | vuln_id = res.json['_id'] |
1993 | 2021 | res = test_client.delete( |
1994 | f'/v2/ws/{ws_name}/vulns/{vuln_id}/attachment/{filename}/' | |
2022 | self.check_url(f'/v2/ws/{ws_name}/vulns/{vuln_id}/attachment/{filename}/') | |
1995 | 2023 | ) |
1996 | 2024 | assert res.status_code == 200 |
1997 | 2025 | |
2014 | 2042 | policyviolations=[], |
2015 | 2043 | attachments=[attachment] |
2016 | 2044 | ) |
2017 | res = test_client.post(f'/v2/ws/{ws_name}/vulns/', data=vuln) | |
2045 | res = test_client.post(self.check_url(f'/v2/ws/{ws_name}/vulns/'), data=vuln) | |
2018 | 2046 | assert res.status_code == 201 |
2019 | 2047 | |
2020 | 2048 | self.workspace.readonly = True |
2023 | 2051 | filename = attachment.name.split('/')[-1] |
2024 | 2052 | vuln_id = res.json['_id'] |
2025 | 2053 | res = test_client.delete( |
2026 | f'/v2/ws/{ws_name}/vulns/{vuln_id}/attachment/{filename}/' | |
2054 | self.check_url(f'/v2/ws/{ws_name}/vulns/{vuln_id}/attachment/{filename}/') | |
2027 | 2055 | ) |
2028 | 2056 | assert res.status_code == 403 |
2029 | 2057 | |
2045 | 2073 | description='helloworld', |
2046 | 2074 | severity='medium', |
2047 | 2075 | ) |
2048 | res = test_client.post(f'/v2/ws/{workspace.name}/vulns/', data=raw_data) | |
2076 | res = test_client.post(self.check_url(f'/v2/ws/{workspace.name}/vulns/'), data=raw_data) | |
2049 | 2077 | |
2050 | 2078 | data = { |
2051 | 2079 | 'q': '{"filters":[{"name":"severity","op":"eq","val":"medium"}]}' |
2052 | 2080 | } |
2053 | res = test_client.get('/v2/ws/{name}/vulns/filter' | |
2054 | .format(name=workspace.name), query_string=data) | |
2081 | res = test_client.get(self.check_url(f'/v2/ws/{workspace.name}/vulns/filter'), query_string=data) | |
2055 | 2082 | |
2056 | 2083 | assert res.status_code == 200 |
2057 | 2084 | value = res.json['vulnerabilities'][0]['value'] |
2061 | 2088 | data = { |
2062 | 2089 | "q": {"filters":[{"name":"severity","op":"eq","val":"medium"}]} |
2063 | 2090 | } |
2064 | res = test_client.get(f'/v2/ws/{workspace.name}/vulns/filter', query_string=data) | |
2091 | res = test_client.get(self.check_url(f'/v2/ws/{workspace.name}/vulns/filter'), query_string=data) | |
2065 | 2092 | assert res.status_code == 400 |
2066 | 2093 | |
2067 | 2094 | def test_vuln_filter_exception(self, test_client, workspace, session): |
2071 | 2098 | data = { |
2072 | 2099 | 'q': '{"filters":[{"name":"severity","op":"eq","val":"medium"}]}' |
2073 | 2100 | } |
2074 | res = test_client.get(f'/v2/ws/{workspace.name}/vulns/filter', query_string=data) | |
2101 | res = test_client.get(self.check_url(f'/v2/ws/{workspace.name}/vulns/filter'), query_string=data) | |
2075 | 2102 | assert res.status_code == 200 |
2076 | 2103 | assert res.json['count'] == 1 |
2077 | 2104 | |
2094 | 2121 | data = { |
2095 | 2122 | 'q': '{"group_by":[{"field":"creator_id"}]}' |
2096 | 2123 | } |
2097 | res = test_client.get(f'/v2/ws/{workspace.name}/vulns/filter', query_string=data) | |
2124 | res = test_client.get(self.check_url(f'/v2/ws/{workspace.name}/vulns/filter'), query_string=data) | |
2098 | 2125 | assert res.status_code == 200 |
2099 | 2126 | assert res.json['count'] == 1 # all vulns created by the same creator |
2100 | 2127 | expected = [{'count': 2, 'creator_id': creator.id}] |
2119 | 2146 | data = { |
2120 | 2147 | 'q': '{"group_by":[{"field":"severity"}]}' |
2121 | 2148 | } |
2122 | res = test_client.get(f'/v2/ws/{workspace.name}/vulns/filter', query_string=data) | |
2149 | res = test_client.get(self.check_url(f'/v2/ws/{workspace.name}/vulns/filter'), query_string=data) | |
2123 | 2150 | assert res.status_code == 200, res.json |
2124 | 2151 | assert res.json['count'] == 1, res.json # all vulns created by the same creator |
2125 | 2152 | expected = { |
2151 | 2178 | data = { |
2152 | 2179 | 'q': '{"group_by":[{"field":"severity"}, {"field": "name"}]}' |
2153 | 2180 | } |
2154 | res = test_client.get(f'/v2/ws/{workspace.name}/vulns/filter', query_string=data) | |
2181 | res = test_client.get(self.check_url(f'/v2/ws/{workspace.name}/vulns/filter'), query_string=data) | |
2155 | 2182 | assert res.status_code == 200, res.json |
2156 | 2183 | assert res.json['count'] == 2, res.json # all vulns created by the same creator |
2157 | 2184 | expected ={'vulnerabilities': [ |
2184 | 2211 | data = { |
2185 | 2212 | 'q': json.dumps({"group_by":[{"field":col_name}]}) |
2186 | 2213 | } |
2187 | res = test_client.get(f'/v2/ws/{workspace.name}/vulns/filter', query_string=data) | |
2214 | res = test_client.get(self.check_url(f'/v2/ws/{workspace.name}/vulns/filter'), query_string=data) | |
2188 | 2215 | assert res.status_code == 200, res.json |
2189 | 2216 | |
2190 | 2217 | def test_vuln_restless_group_same_name_description(self, test_client, session): |
2218 | 2245 | data = { |
2219 | 2246 | 'q': '{"group_by":[{"field":"name"}, {"field":"description"}]}' |
2220 | 2247 | } |
2221 | res = test_client.get(f'/v2/ws/{workspace.name}/vulns/filter', query_string=data) | |
2248 | res = test_client.get(self.check_url(f'/v2/ws/{workspace.name}/vulns/filter'), query_string=data) | |
2222 | 2249 | assert res.status_code == 200 |
2223 | 2250 | assert res.json['count'] == 2 |
2224 | 2251 | expected = [{'count': 2, 'name': 'test', 'description': 'test'}, {'count': 1, 'name': 'test2', 'description': 'test'}] |
2283 | 2310 | data = { |
2284 | 2311 | 'q': json.dumps(query) |
2285 | 2312 | } |
2286 | res = test_client.get(f'/v2/ws/{workspace.name}/vulns/filter', query_string=data) | |
2313 | res = test_client.get(self.check_url(f'/v2/ws/{workspace.name}/vulns/filter'), query_string=data) | |
2287 | 2314 | assert res.status_code == 200 |
2288 | 2315 | assert res.json['count'] == 12 |
2289 | 2316 | expected_order = ['critical', 'critical', 'med', 'med', 'med', 'med', 'med', 'med', 'med', 'med', 'med', 'med'] |
2297 | 2324 | data = { |
2298 | 2325 | 'q': json.dumps({"filters":[{"name":"creator","op":"eq","val": vuln.creator.username}]}) |
2299 | 2326 | } |
2300 | res = test_client.get(f'/v2/ws/{workspace.name}/vulns/filter', query_string=data) | |
2327 | res = test_client.get(self.check_url(f'/v2/ws/{workspace.name}/vulns/filter'), query_string=data) | |
2301 | 2328 | assert res.status_code == 200 |
2302 | 2329 | |
2303 | 2330 | def test_vuln_web_filter_exception(self, test_client, workspace, session): |
2307 | 2334 | data = { |
2308 | 2335 | 'q': '{"filters":[{"name":"severity","op":"eq","val":"medium"}]}' |
2309 | 2336 | } |
2310 | res = test_client.get(f'/v2/ws/{workspace.name}/vulns/filter', query_string=data) | |
2337 | res = test_client.get(self.check_url(f'/v2/ws/{workspace.name}/vulns/filter'), query_string=data) | |
2311 | 2338 | assert res.status_code == 200 |
2312 | 2339 | assert res.json['count'] == 1 |
2313 | 2340 | |
2343 | 2370 | session.commit() |
2344 | 2371 | |
2345 | 2372 | res = test_client.post( |
2346 | f'/v2/ws/{workspace.name}/vulns/{vuln.id}/attachment/', | |
2373 | self.check_url(f'/v2/ws/{workspace.name}/vulns/{vuln.id}/attachment/'), | |
2347 | 2374 | data={'csrf_token': csrf_token}, |
2348 | 2375 | headers={'Content-Type': 'multipart/form-data'}, |
2349 | 2376 | use_json_data=False) |
2351 | 2378 | |
2352 | 2379 | def test_get_attachment_with_invalid_workspace_and_vuln(self, test_client): |
2353 | 2380 | res = test_client.get( |
2354 | "/v2/ws/invalid_ws/vulns/invalid_vuln/attachment/random_name/") | |
2381 | self.check_url("/v2/ws/invalid_ws/vulns/invalid_vuln/attachment/random_name/") | |
2382 | ) | |
2355 | 2383 | assert res.status_code == 404 |
2356 | 2384 | |
2357 | 2385 | def test_delete_attachment_with_invalid_workspace_and_vuln(self, test_client): |
2358 | 2386 | res = test_client.delete( |
2359 | "/v2/ws/invalid_ws/vulns/invalid_vuln/attachment/random_name/") | |
2387 | self.check_url("/v2/ws/invalid_ws/vulns/invalid_vuln/attachment/random_name/") | |
2388 | ) | |
2360 | 2389 | assert res.status_code == 404 |
2361 | 2390 | |
2362 | 2391 | def test_delete_invalid_attachment(self, test_client, workspace, session): |
2364 | 2393 | session.add(vuln) |
2365 | 2394 | session.commit() |
2366 | 2395 | res = test_client.delete( |
2367 | f"/v2/ws/{workspace.name}/vulns/{vuln.id}/attachment/random_name/") | |
2396 | self.check_url(f"/v2/ws/{workspace.name}/vulns/{vuln.id}/attachment/random_name/") | |
2397 | ) | |
2368 | 2398 | assert res.status_code == 404 |
2369 | 2399 | |
2370 | 2400 | def test_export_vuln_csv_empty_workspace(self, test_client, session): |
2371 | 2401 | ws = WorkspaceFactory(name='abc') |
2372 | res = test_client.get(f'/v2/ws/{ws.name}/vulns/export_csv/') | |
2402 | res = test_client.get(self.check_url(f'/v2/ws/{ws.name}/vulns/export_csv/')) | |
2373 | 2403 | expected_headers = [ |
2374 | 2404 | "confirmed", "id", "date", "name", "severity", "service", |
2375 | 2405 | "target", "desc", "status", "hostnames", "comments", "owner", |
2391 | 2421 | confirmed_vulns = VulnerabilityFactory.create(confirmed=True, workspace=workspace) |
2392 | 2422 | session.add(confirmed_vulns) |
2393 | 2423 | session.commit() |
2394 | res = test_client.get(self.url(workspace=workspace) + 'export_csv/?q={"filters":[{"name":"confirmed","op":"==","val":"true"}]}') | |
2424 | res = test_client.get( | |
2425 | self.check_url(urljoin(self.url(workspace=workspace), 'export_csv/')) + | |
2426 | '?q={"filters":[{"name":"confirmed","op":"==","val":"true"}]}' | |
2427 | ) | |
2395 | 2428 | assert res.status_code == 200 |
2396 | 2429 | assert self._verify_csv(res.data, confirmed=True) |
2397 | 2430 | |
2405 | 2438 | workspace=workspace) |
2406 | 2439 | session.add(confirmed_vulns) |
2407 | 2440 | session.commit() |
2408 | res = test_client.get(self.url(workspace=workspace) + 'export_csv/') | |
2441 | res = test_client.get(self.check_url(urljoin(self.url(workspace=workspace), 'export_csv/'))) | |
2409 | 2442 | assert res.status_code == 200 |
2410 | 2443 | assert self._verify_csv(res.data, confirmed=True) |
2411 | 2444 | |
2415 | 2448 | confirmed_vulns = VulnerabilityFactory.create(confirmed=True, severity='critical', workspace=workspace) |
2416 | 2449 | session.add(confirmed_vulns) |
2417 | 2450 | session.commit() |
2418 | res = test_client.get(self.url(workspace=workspace) + 'export_csv/?q={"filters":[{"name":"severity","op":"==","val":"critical"}]}') | |
2451 | res = test_client.get( | |
2452 | self.check_url(urljoin(self.url(workspace=workspace), 'export_csv/')) + | |
2453 | '?q={"filters":[{"name":"severity","op":"==","val":"critical"}]}' | |
2454 | ) | |
2419 | 2455 | assert res.status_code == 200 |
2420 | 2456 | assert self._verify_csv(res.data, confirmed=True, severity='critical') |
2421 | 2457 | |
2424 | 2460 | self.first_object.confirmed = True |
2425 | 2461 | session.add(self.first_object) |
2426 | 2462 | session.commit() |
2427 | res = test_client.get(self.url() + 'export_csv/?confirmed=true') | |
2463 | res = test_client.get( | |
2464 | self.check_url(urljoin(self.url(), 'export_csv/')) + | |
2465 | '?confirmed=true' | |
2466 | ) | |
2428 | 2467 | assert res.status_code == 200 |
2429 | 2468 | self._verify_csv(res.data, confirmed=True) |
2430 | 2469 | |
2448 | 2487 | session.add(vuln) |
2449 | 2488 | session.commit() |
2450 | 2489 | |
2451 | res = test_client.get(f'v2/ws/{workspace.name}/vulns/export_csv/') | |
2490 | res = test_client.get(self.check_url(f'/v2/ws/{workspace.name}/vulns/export_csv/')) | |
2452 | 2491 | assert res.status_code == 200 |
2453 | 2492 | |
2454 | 2493 | csv_data = csv.DictReader(StringIO(res.data.decode('utf-8')), delimiter=',') |
2488 | 2527 | session.add(vuln) |
2489 | 2528 | session.commit() |
2490 | 2529 | |
2491 | res = test_client.get(self.url() + 'export_csv/') | |
2530 | res = test_client.get(self.check_url(urljoin(self.url(), 'export_csv/'))) | |
2492 | 2531 | assert self._verify_csv(res.data) |
2493 | 2532 | |
2494 | 2533 | def _verify_csv(self, raw_csv_data, confirmed=False, severity=None): |
2561 | 2600 | assert res.json['tool'] == tool |
2562 | 2601 | |
2563 | 2602 | |
2603 | class TestListVulnerabilityViewV3(TestListVulnerabilityView, PatchableTestsMixin): | |
2604 | view_class = VulnerabilityV3View | |
2605 | ||
2606 | def url(self, obj=None, workspace=None): | |
2607 | return v2_to_v3(super(TestListVulnerabilityViewV3, self).url(obj, workspace)) | |
2608 | ||
2609 | def check_url(self, url): | |
2610 | return v2_to_v3(url) | |
2611 | ||
2612 | def test_patch_with_attachments(self, test_client, session, workspace): | |
2613 | vuln = VulnerabilityFactory.create(workspace=workspace) | |
2614 | session.add(vuln) | |
2615 | session.commit() | |
2616 | png_file = Path(__file__).parent / 'data' / 'faraday.png' | |
2617 | ||
2618 | with open(png_file, 'rb') as file_obj: | |
2619 | new_file = FaradayUploadedFile(file_obj.read()) | |
2620 | ||
2621 | new_attach = File(object_type='vulnerability', object_id=vuln.id, name='Faraday', filename='faraday.png', | |
2622 | content=new_file) | |
2623 | session.add(new_attach) | |
2624 | session.commit() | |
2625 | ||
2626 | res = test_client.patch(f'{self.url(vuln, workspace=workspace)}', data={}) | |
2627 | assert res.status_code == 200 | |
2628 | res = test_client.get(f'{self.url(vuln, workspace=workspace)}/attachment') | |
2629 | assert res.status_code == 200 | |
2630 | assert new_attach.filename in res.json | |
2631 | assert 'image/png' in res.json[new_attach.filename]['content_type'] | |
2564 | 2632 | |
2565 | 2633 | |
2566 | 2634 | @pytest.mark.usefixtures('logged_user') |
2567 | class TestCustomFieldVulnerability(ReadOnlyAPITests): # TODO migration: use read write api tests | |
2635 | class TestCustomFieldVulnerability(ReadWriteAPITests): | |
2568 | 2636 | model = Vulnerability |
2569 | 2637 | factory = factories.VulnerabilityFactory |
2570 | 2638 | api_endpoint = 'vulns' |
2571 | 2639 | view_class = VulnerabilityView |
2640 | patchable_fields = ['description'] | |
2641 | ||
2642 | def check_url(self, url): | |
2643 | return url | |
2572 | 2644 | |
2573 | 2645 | def test_create_vuln_with_custom_fields_shown(self, test_client, second_workspace, session): |
2574 | 2646 | host = HostFactory.create(workspace=self.workspace) |
2597 | 2669 | assert res.status_code == 201 |
2598 | 2670 | assert res.json['custom_fields']['cvss'] == '321321' |
2599 | 2671 | |
2600 | def test_create_vuln_with_custom_fields_using_field_display_name_fails(self, test_client, second_workspace, session): | |
2672 | def test_create_vuln_with_custom_fields_using_field_display_name_continues_with_warning(self, test_client, second_workspace, session, caplog): | |
2601 | 2673 | host = HostFactory.create(workspace=self.workspace) |
2602 | 2674 | custom_field_schema = CustomFieldsSchemaFactory( |
2603 | 2675 | field_name='cvss', |
2621 | 2693 | } |
2622 | 2694 | res = test_client.post(self.url(), data=data) |
2623 | 2695 | |
2624 | assert res.status_code == 400 | |
2696 | assert res.status_code == 201 | |
2697 | assert "Invalid custom field" in caplog.text | |
2625 | 2698 | |
2626 | 2699 | def test_create_vuln_with_custom_fields_list(self, test_client, second_workspace, session): |
2627 | 2700 | host = HostFactory.create(workspace=self.workspace) |
2669 | 2742 | 'parent': host.id, |
2670 | 2743 | 'type': 'Vulnerability', |
2671 | 2744 | 'custom_fields': { |
2672 | 'CVSS': 'pepe', | |
2745 | 'cvss': 'pepe', | |
2673 | 2746 | } |
2674 | 2747 | } |
2675 | 2748 | res = test_client.post(self.url(), data=data) |
2676 | 2749 | |
2677 | 2750 | assert res.status_code == 400 |
2678 | 2751 | |
2679 | def test_create_vuln_with_invalid_custom_fields_fails(self, test_client, second_workspace, session): | |
2752 | def test_create_vuln_with_invalid_custom_fields_continues_with_warning(self, test_client, second_workspace, session, caplog): | |
2680 | 2753 | host = HostFactory.create(workspace=self.workspace) |
2681 | 2754 | session.add(host) |
2682 | 2755 | session.commit() |
2693 | 2766 | } |
2694 | 2767 | res = test_client.post(self.url(), data=data) |
2695 | 2768 | |
2696 | assert res.status_code == 400 | |
2769 | assert res.status_code == 201 | |
2770 | assert "Invalid custom field" in caplog.text | |
2697 | 2771 | |
2698 | 2772 | def test_create_create_vuln_web_with_host_as_parent_fails( |
2699 | 2773 | self, host, session, test_client): |
2761 | 2835 | ) |
2762 | 2836 | ws_name = host_with_hostnames.workspace.name |
2763 | 2837 | vuln_count_previous = session.query(Vulnerability).count() |
2764 | res_1 = test_client.post(f'/v2/ws/{ws_name}/vulns/', data=raw_data_vuln_1) | |
2765 | res_2 = test_client.post(f'/v2/ws/{ws_name}/vulns/', data=raw_data_vuln_2) | |
2838 | res_1 = test_client.post(self.check_url(f'/v2/ws/{ws_name}/vulns/'), data=raw_data_vuln_1) | |
2839 | res_2 = test_client.post(self.check_url(f'/v2/ws/{ws_name}/vulns/'), data=raw_data_vuln_2) | |
2766 | 2840 | vuln_1_id = res_1.json['obj_id'] |
2767 | 2841 | vuln_2_id = res_2.json['obj_id'] |
2768 | 2842 | vulns_to_delete = [vuln_1_id, vuln_2_id] |
2769 | 2843 | request_data = {'vulnerability_ids': vulns_to_delete} |
2770 | delete_response = test_client.delete(f'/v2/ws/{ws_name}/vulns/bulk_delete/', data=request_data) | |
2844 | delete_response = test_client.delete(self.check_url(f'/v2/ws/{ws_name}/vulns/bulk_delete/'), data=request_data) | |
2771 | 2845 | vuln_count_after = session.query(Vulnerability).count() |
2772 | 2846 | deleted_vulns = delete_response.json['deleted_vulns'] |
2773 | 2847 | assert delete_response.status_code == 200 |
2806 | 2880 | ) |
2807 | 2881 | ws_name = host_with_hostnames.workspace.name |
2808 | 2882 | vuln_count_previous = session.query(Vulnerability).count() |
2809 | res_1 = test_client.post(f'/v2/ws/{ws_name}/vulns/', data=raw_data_vuln_1) | |
2810 | res_2 = test_client.post(f'/v2/ws/{ws_name}/vulns/', data=raw_data_vuln_2) | |
2883 | res_1 = test_client.post(self.check_url(f'/v2/ws/{ws_name}/vulns/'), data=raw_data_vuln_1) | |
2884 | res_2 = test_client.post(self.check_url(f'/v2/ws/{ws_name}/vulns/'), data=raw_data_vuln_2) | |
2811 | 2885 | vuln_1_id = res_1.json['obj_id'] |
2812 | 2886 | vuln_2_id = res_2.json['obj_id'] |
2813 | 2887 | vulns_to_delete = [vuln_1_id, vuln_2_id] |
2814 | 2888 | request_data = {'severities': ['low']} |
2815 | delete_response = test_client.delete(f'/v2/ws/{ws_name}/vulns/bulk_delete/', data=request_data) | |
2889 | delete_response = test_client.delete(self.check_url(f'/v2/ws/{ws_name}/vulns/bulk_delete/'), data=request_data) | |
2816 | 2890 | vuln_count_after = session.query(Vulnerability).count() |
2817 | 2891 | deleted_vulns = delete_response.json['deleted_vulns'] |
2818 | 2892 | assert delete_response.status_code == 200 |
2840 | 2914 | severity='low', |
2841 | 2915 | tool=tool_name |
2842 | 2916 | ) |
2843 | ws_name = host_with_hostnames.workspace.name | |
2917 | ws = host_with_hostnames.workspace | |
2844 | 2918 | vuln_count_previous = session.query(Vulnerability).count() |
2845 | res = test_client.post(f'/v2/ws/{ws_name}/vulns/', data=raw_data) | |
2919 | res = test_client.post(self.url(workspace=ws), data=raw_data) | |
2846 | 2920 | assert res.status_code == 201 |
2847 | 2921 | assert vuln_count_previous + 1 == session.query(Vulnerability).count() |
2848 | 2922 | assert res.json['tool'] == tool_name |
2866 | 2940 | description='helloworld', |
2867 | 2941 | severity='low', |
2868 | 2942 | ) |
2869 | ws_name = host_with_hostnames.workspace.name | |
2943 | ws = host_with_hostnames.workspace | |
2870 | 2944 | vuln_count_previous = session.query(Vulnerability).count() |
2871 | res = test_client.post(f'/v2/ws/{ws_name}/vulns/', data=raw_data) | |
2945 | res = test_client.post(self.url(workspace=ws), data=raw_data) | |
2872 | 2946 | assert res.status_code == 201 |
2873 | 2947 | assert vuln_count_previous + 1 == session.query(Vulnerability).count() |
2874 | 2948 | assert res.json['tool'] == "Web UI" |
2922 | 2996 | assert res.json['tool'] == command.tool |
2923 | 2997 | |
2924 | 2998 | |
2999 | class TestCustomFieldVulnerabilityV3(TestCustomFieldVulnerability, PatchableTestsMixin): | |
3000 | view_class = VulnerabilityV3View | |
3001 | ||
3002 | def url(self, obj=None, workspace=None): | |
3003 | return v2_to_v3(super(TestCustomFieldVulnerabilityV3, self).url(obj, workspace)) | |
3004 | ||
3005 | def check_url(self, url): | |
3006 | return v2_to_v3(url) | |
3007 | ||
3008 | @pytest.mark.skip(reason="To be reimplemented") | |
3009 | def test_bulk_delete_vuln_id(self, host_with_hostnames, test_client, session): | |
3010 | pass | |
3011 | ||
3012 | @pytest.mark.skip(reason="To be reimplemented") | |
3013 | def test_bulk_delete_vuln_severity(self, host_with_hostnames, test_client, session): | |
3014 | pass | |
3015 | ||
3016 | ||
2925 | 3017 | |
2926 | 3018 | @pytest.mark.usefixtures('logged_user') |
2927 | class TestVulnerabilityCustomFields(ReadOnlyAPITests): | |
3019 | class TestVulnerabilityCustomFields(ReadWriteAPITests): | |
2928 | 3020 | model = Vulnerability |
2929 | 3021 | factory = factories.VulnerabilityFactory |
2930 | 3022 | api_endpoint = 'vulns' |
2931 | 3023 | view_class = VulnerabilityView |
3024 | patchable_fields = ['description'] | |
2932 | 3025 | |
2933 | 3026 | def test_custom_field_cvss(self, session, test_client): |
2934 | 3027 | add_text_field = CustomFieldsSchemaFactory.create( |
2941 | 3034 | session.commit() |
2942 | 3035 | |
2943 | 3036 | |
3037 | class TestVulnerabilityCustomFieldsV3(TestVulnerabilityCustomFields, PatchableTestsMixin): | |
3038 | view_class = VulnerabilityV3View | |
3039 | ||
3040 | def url(self, obj=None, workspace=None): | |
3041 | return v2_to_v3(super(TestVulnerabilityCustomFieldsV3, self).url(obj, workspace)) | |
3042 | ||
3043 | ||
2944 | 3044 | @pytest.mark.usefixtures('logged_user') |
2945 | class TestVulnerabilitySearch(): | |
3045 | class TestVulnerabilitySearch: | |
3046 | ||
3047 | def check_url(self, url): | |
3048 | return url | |
2946 | 3049 | |
2947 | 3050 | @pytest.mark.skip_sql_dialect('sqlite') |
2948 | 3051 | def test_search_by_hostname_vulns(self, test_client, session): |
2958 | 3061 | [{"name":"hostnames","op":"eq","val":"pepe"}] |
2959 | 3062 | } |
2960 | 3063 | res = test_client.get( |
2961 | f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3064 | self.check_url(f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3065 | ) | |
2962 | 3066 | assert res.status_code == 200 |
2963 | 3067 | assert res.json['count'] == 1 |
2964 | 3068 | assert res.json['vulnerabilities'][0]['id'] == vuln.id |
2978 | 3082 | [{"name":"hostnames","op":"eq","val":"pepe"}] |
2979 | 3083 | } |
2980 | 3084 | res = test_client.get( |
2981 | f'/v2/ws/{workspace.name}/vulns/?q={json.dumps(query_filter)}') | |
3085 | self.check_url(f'/v2/ws/{workspace.name}/vulns/') + f'?q={json.dumps(query_filter)}' | |
3086 | ) | |
2982 | 3087 | assert res.status_code == 200 |
2983 | 3088 | assert res.json['count'] == 1 |
2984 | 3089 | assert res.json['vulnerabilities'][0]['id'] == vuln.id |
2999 | 3104 | [{"name": "hostnames", "op": "eq", "val": "pepe"}] |
3000 | 3105 | } |
3001 | 3106 | res = test_client.get( |
3002 | f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3107 | self.check_url(f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3108 | ) | |
3003 | 3109 | assert res.status_code == 200 |
3004 | 3110 | assert res.json['count'] == 1 |
3005 | 3111 | assert res.json['vulnerabilities'][0]['id'] == vuln.id |
3009 | 3115 | [] |
3010 | 3116 | } |
3011 | 3117 | res = test_client.get( |
3012 | f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3118 | self.check_url(f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3119 | ) | |
3013 | 3120 | assert res.status_code == 200 |
3014 | 3121 | assert res.json['count'] == 0 |
3015 | 3122 | |
3018 | 3125 | [{"name":"code", "op": "eq", "val": "test"}] |
3019 | 3126 | } |
3020 | 3127 | res = test_client.get( |
3021 | f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3128 | self.check_url(f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3129 | ) | |
3022 | 3130 | |
3023 | 3131 | assert res.status_code == 400, res.json |
3024 | 3132 | |
3036 | 3144 | {"and": [{"name": "hostnames","op": "eq", "val": "pepe"}]} |
3037 | 3145 | ]} |
3038 | 3146 | res = test_client.get( |
3039 | f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3147 | self.check_url(f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3148 | ) | |
3040 | 3149 | assert res.status_code == 200 |
3041 | 3150 | assert res.json['count'] == 1 |
3042 | 3151 | assert res.json['vulnerabilities'][0]['id'] == vuln.id |
3067 | 3176 | "offset": offset * 10, |
3068 | 3177 | } |
3069 | 3178 | res = test_client.get( |
3070 | f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3179 | self.check_url(f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3180 | ) | |
3071 | 3181 | assert res.status_code == 200 |
3072 | 3182 | assert res.json['count'] == 20, query_filter |
3073 | 3183 | assert len(res.json['vulnerabilities']) == 10 |
3096 | 3206 | "offset": 10 * offset, |
3097 | 3207 | } |
3098 | 3208 | res = test_client.get( |
3099 | f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3209 | self.check_url(f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3210 | ) | |
3100 | 3211 | assert res.status_code == 200 |
3101 | 3212 | assert res.json['count'] == 100 |
3102 | 3213 | for vuln in res.json['vulnerabilities']: |
3134 | 3245 | "offset": offset, |
3135 | 3246 | } |
3136 | 3247 | res = test_client.get( |
3137 | f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3248 | self.check_url(f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3249 | ) | |
3138 | 3250 | assert res.status_code == 200 |
3139 | 3251 | assert res.json['count'] == 10 |
3140 | 3252 | paginated_vulns.add(res.json['vulnerabilities'][0]['id']) |
3172 | 3284 | {"name": "host__os", "op": "has", "val": "Linux"} |
3173 | 3285 | ]} |
3174 | 3286 | res = test_client.get( |
3175 | f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3287 | self.check_url(f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3288 | ) | |
3176 | 3289 | assert res.status_code == 200 |
3177 | 3290 | assert res.json['count'] == 1 |
3178 | 3291 | assert res.json['vulnerabilities'][0]['id'] == vuln.id |
3219 | 3332 | {"name": "create_date", "op": "eq", "val": vuln.create_date.strftime("%Y-%m-%d")} |
3220 | 3333 | ]} |
3221 | 3334 | res = test_client.get( |
3222 | f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3335 | self.check_url(f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3336 | ) | |
3223 | 3337 | assert res.status_code == 200 |
3224 | 3338 | assert res.json['count'] == 3 |
3225 | 3339 | |
3233 | 3347 | {"name": "create_date", "op": "eq", "val": "30/01/2020"} |
3234 | 3348 | ]} |
3235 | 3349 | res = test_client.get( |
3236 | f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3350 | self.check_url(f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3351 | ) | |
3237 | 3352 | assert res.status_code == 200 |
3238 | 3353 | |
3239 | 3354 | @pytest.mark.skip_sql_dialect('sqlite') |
3240 | 3355 | def test_search_hypothesis_test_found_case(self, test_client, session, workspace): |
3241 | 3356 | query_filter = {'filters': [{'name': 'host_id', 'op': 'not_in', 'val': '\U0010a1a7\U00093553\U000eb46a\x1e\x10\r\x18%\U0005ddfa0\x05\U000fdeba\x08\x04絮'}]} |
3242 | 3357 | res = test_client.get( |
3243 | f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3358 | self.check_url(f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3359 | ) | |
3244 | 3360 | assert res.status_code == 400 |
3245 | 3361 | |
3246 | 3362 | @pytest.mark.skip_sql_dialect('sqlite') |
3247 | 3363 | def test_search_hypothesis_test_found_case_2(self, test_client, session, workspace): |
3248 | 3364 | query_filter = {'filters': [{'name': 'host__os', 'op': 'ilike', 'val': -1915870387}]} |
3249 | 3365 | res = test_client.get( |
3250 | f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3366 | self.check_url(f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3367 | ) | |
3251 | 3368 | assert res.status_code == 400 |
3252 | 3369 | |
3253 | 3370 | @pytest.mark.skip_sql_dialect('sqlite') |
3258 | 3375 | ]) |
3259 | 3376 | def test_search_hypothesis_test_found_case_3(self, query_filter, test_client, session, workspace): |
3260 | 3377 | res = test_client.get( |
3261 | f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3378 | self.check_url(f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3379 | ) | |
3262 | 3380 | assert res.status_code == 400 |
3263 | 3381 | |
3264 | 3382 | @pytest.mark.skip_sql_dialect('sqlite') |
3269 | 3387 | ]) |
3270 | 3388 | def test_search_hypothesis_test_found_case_4(self, query_filter, test_client, session, workspace): |
3271 | 3389 | res = test_client.get( |
3272 | f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3390 | self.check_url(f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3391 | ) | |
3273 | 3392 | assert res.status_code == 400 |
3274 | 3393 | |
3275 | 3394 | @pytest.mark.skip_sql_dialect('sqlite') |
3280 | 3399 | ]) |
3281 | 3400 | def test_search_hypothesis_test_found_case_5(self, query_filter, test_client, session, workspace): |
3282 | 3401 | res = test_client.get( |
3283 | f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3402 | self.check_url(f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3403 | ) | |
3284 | 3404 | assert res.status_code == 400 |
3285 | 3405 | |
3286 | 3406 | @pytest.mark.skip_sql_dialect('sqlite') |
3287 | 3407 | def test_search_hypothesis_test_found_case_6(self, test_client, session, workspace): |
3288 | 3408 | query_filter = {'filters': [{'name': 'resolution', 'op': 'any', 'val': ''}]} |
3289 | 3409 | res = test_client.get( |
3290 | f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3410 | self.check_url(f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3411 | ) | |
3291 | 3412 | assert res.status_code == 200 |
3292 | 3413 | |
3293 | 3414 | @pytest.mark.skip_sql_dialect('sqlite') |
3294 | 3415 | def test_search_hypothesis_test_found_case_7(self, test_client, session, workspace): |
3295 | 3416 | query_filter = {'filters': [{'name': 'name', 'op': '>', 'val': '\U0004e755\U0007a789\U000e02d1\U000b3d32\x10\U000ad0e2,\x05\x1a'}, {'name': 'creator', 'op': 'eq', 'val': 21883}]} |
3296 | 3417 | res = test_client.get( |
3297 | f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3418 | self.check_url(f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3419 | ) | |
3298 | 3420 | assert res.status_code == 400 |
3299 | 3421 | |
3300 | 3422 | @pytest.mark.skip_sql_dialect('sqlite') |
3304 | 3426 | ]) |
3305 | 3427 | def test_search_hypothesis_test_found_case_7_valid(self, query_filter, test_client, session, workspace): |
3306 | 3428 | res = test_client.get( |
3307 | f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3429 | self.check_url(f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3430 | ) | |
3308 | 3431 | assert res.status_code == 200 |
3309 | 3432 | |
3310 | 3433 | @pytest.mark.skip_sql_dialect('sqlite') |
3311 | 3434 | def test_search_hypothesis_test_found_case_8(self, test_client, session, workspace): |
3312 | 3435 | query_filter = {'filters': [{'name': 'hostnames', 'op': '==', 'val': ''}]} |
3313 | 3436 | res = test_client.get( |
3314 | f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3437 | self.check_url(f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3438 | ) | |
3315 | 3439 | assert res.status_code == 200 |
3316 | 3440 | |
3317 | 3441 | @pytest.mark.skip_sql_dialect('sqlite') |
3319 | 3443 | query_filter = {'filters': [{'name': 'issuetracker', 'op': 'not_equal_to', 'val': '0\x00\U00034383$\x13-\U000375fb\U0007add2\x01\x01\U0010c23a'}]} |
3320 | 3444 | |
3321 | 3445 | res = test_client.get( |
3322 | f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3446 | self.check_url(f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3447 | ) | |
3323 | 3448 | assert res.status_code == 400 |
3324 | 3449 | |
3325 | 3450 | @pytest.mark.skip_sql_dialect('sqlite') |
3327 | 3452 | query_filter = {'filters': [{'name': 'impact_integrity', 'op': 'neq', 'val': 0}]} |
3328 | 3453 | |
3329 | 3454 | res = test_client.get( |
3330 | f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3455 | self.check_url(f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3456 | ) | |
3331 | 3457 | assert res.status_code == 400 |
3332 | 3458 | |
3333 | 3459 | @pytest.mark.skip_sql_dialect('sqlite') |
3335 | 3461 | query_filter = {'filters': [{'name': 'host_id', 'op': 'like', 'val': '0'}]} |
3336 | 3462 | |
3337 | 3463 | res = test_client.get( |
3338 | f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3464 | self.check_url(f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3465 | ) | |
3339 | 3466 | assert res.status_code == 400 |
3340 | 3467 | |
3341 | 3468 | @pytest.mark.skip_sql_dialect('sqlite') |
3343 | 3470 | query_filter = {'filters': [{'name': 'custom_fields', 'op': 'like', 'val': ''}]} |
3344 | 3471 | |
3345 | 3472 | res = test_client.get( |
3346 | f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3473 | self.check_url(f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3474 | ) | |
3347 | 3475 | assert res.status_code == 400 |
3348 | 3476 | |
3349 | 3477 | @pytest.mark.skip_sql_dialect('sqlite') |
3351 | 3479 | query_filter = {'filters': [{'name': 'impact_accountability', 'op': 'ilike', 'val': '0'}]} |
3352 | 3480 | |
3353 | 3481 | res = test_client.get( |
3354 | f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3482 | self.check_url(f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3483 | ) | |
3355 | 3484 | assert res.status_code == 400 |
3356 | 3485 | |
3357 | 3486 | @pytest.mark.usefixtures('ignore_nplusone') |
3367 | 3496 | query_filter = {'filters': [{'name': 'severity', 'op': 'eq', 'val': 'high'}]} |
3368 | 3497 | |
3369 | 3498 | res = test_client.get( |
3370 | f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3499 | self.check_url(f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3500 | ) | |
3371 | 3501 | assert res.status_code == 200 |
3372 | 3502 | assert res.json['count'] == 20 |
3373 | 3503 | |
3389 | 3519 | } |
3390 | 3520 | |
3391 | 3521 | res = test_client.get( |
3392 | f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3522 | self.check_url(f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3523 | ) | |
3393 | 3524 | assert res.status_code == 400 |
3394 | 3525 | |
3395 | 3526 | @pytest.mark.skip_sql_dialect('sqlite') |
3413 | 3544 | } |
3414 | 3545 | |
3415 | 3546 | res = test_client.get( |
3416 | f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3547 | self.check_url(f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3548 | ) | |
3417 | 3549 | assert res.status_code == 200 |
3418 | 3550 | expected_order = sort_order["expected"] |
3419 | 3551 | |
3436 | 3568 | } |
3437 | 3569 | |
3438 | 3570 | res = test_client.get( |
3439 | f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3571 | self.check_url(f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3572 | ) | |
3440 | 3573 | assert res.status_code == 200 |
3441 | 3574 | expected_order = ['critical', 'high', 'med', 'low'] |
3442 | 3575 | |
3476 | 3609 | } |
3477 | 3610 | |
3478 | 3611 | res = test_client.get( |
3479 | f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3612 | self.check_url(f'/v2/ws/{workspace.name}/vulns/filter?q={json.dumps(query_filter)}') | |
3613 | ) | |
3480 | 3614 | assert res.status_code == 200 |
3481 | 3615 | assert res.json['count'] == 100 |
3616 | ||
3617 | ||
3618 | class TestVulnerabilitySearchV3(TestVulnerabilitySearch): | |
3619 | def check_url(self, url): | |
3620 | return v2_to_v3(url) | |
3482 | 3621 | |
3483 | 3622 | def test_type_filter(workspace, session, |
3484 | 3623 | vulnerability_factory, |
3656 | 3795 | data=raw_data) |
3657 | 3796 | assert res.status_code in [201, 400, 409] |
3658 | 3797 | |
3798 | @given(VulnerabilityData) | |
3799 | def send_api_create_request_v3(raw_data): | |
3800 | ||
3801 | ws_name = host_with_hostnames.workspace.name | |
3802 | res = test_client.post(f'/v3/ws/{ws_name}/vulns/', | |
3803 | data=raw_data) | |
3804 | assert res.status_code in [201, 400, 409] | |
3805 | ||
3659 | 3806 | @given(VulnerabilityDataWithId) |
3660 | 3807 | def send_api_update_request(raw_data): |
3661 | 3808 | |
3662 | 3809 | ws_name = host_with_hostnames.workspace.name |
3663 | res = test_client.put(f"/v2/ws/{ws_name}/vulns/{raw_data['_id']}/", | |
3810 | res = test_client.put(f"/v2/ws/{ws_name}/vulns/{raw_data['_id']}", | |
3664 | 3811 | data=raw_data) |
3665 | 3812 | assert res.status_code in [200, 400, 409, 405] |
3666 | 3813 | |
3814 | @given(VulnerabilityDataWithId) | |
3815 | def send_api_update_request_v3(raw_data): | |
3816 | ||
3817 | ws_name = host_with_hostnames.workspace.name | |
3818 | res = test_client.put(f"/v3/ws/{ws_name}/vulns/{raw_data['_id']}", | |
3819 | data=raw_data) | |
3820 | assert res.status_code in [200, 400, 409, 405] | |
3821 | ||
3667 | 3822 | send_api_create_request() |
3668 | 3823 | send_api_update_request() |
3824 | send_api_create_request_v3() | |
3825 | send_api_update_request_v3() | |
3669 | 3826 | |
3670 | 3827 | |
3671 | 3828 | def filter_json(): |
3714 | 3871 | |
3715 | 3872 | assert res.status_code in [200, 400] |
3716 | 3873 | |
3874 | @given(FilterData) | |
3875 | @settings(deadline=None) | |
3876 | def send_api_filter_request_v3(raw_filter): | |
3877 | ws_name = host_with_hostnames.workspace.name | |
3878 | encoded_filter = urllib.parse.quote(json.dumps(raw_filter)) | |
3879 | res = test_client.get(f'/v3/ws/{ws_name}/vulns/filter?q={encoded_filter}') | |
3880 | if res.status_code not in [200, 400]: | |
3881 | print(json.dumps(raw_filter)) | |
3882 | ||
3883 | assert res.status_code in [200, 400] | |
3884 | ||
3717 | 3885 | send_api_filter_request() |
3886 | send_api_filter_request_v3() | |
3718 | 3887 | |
3719 | 3888 | |
3720 | 3889 | def test_model_converter(): |
3724 | 3893 | field = VulnerabilitySchema().fields['data'] |
3725 | 3894 | assert isinstance(field, NullToBlankString) |
3726 | 3895 | assert field.allow_none |
3727 | ||
3728 | ||
3729 | # I'm Py3 |
11 | 11 | from faraday.server.api.modules.vulnerability_template import VulnerabilityTemplateView |
12 | 12 | from tests import factories |
13 | 13 | from tests.test_api_non_workspaced_base import ( |
14 | ReadOnlyAPITests | |
14 | ReadWriteAPITests, PatchableTestsMixin | |
15 | 15 | ) |
16 | 16 | from faraday.server.models import ( |
17 | 17 | VulnerabilityTemplate, |
23 | 23 | UserFactory, |
24 | 24 | VulnerabilityFactory |
25 | 25 | ) |
26 | from tests.utils.url import v2_to_v3 | |
26 | 27 | |
27 | 28 | TEMPLATES_DATA = [ |
28 | 29 | {'name': 'XML Injection (aka Blind XPath Injection) (Type: Base)', |
41 | 42 | } |
42 | 43 | ] |
43 | 44 | |
45 | ||
44 | 46 | @pytest.mark.usefixtures('logged_user') |
45 | class TestListVulnerabilityTemplateView(ReadOnlyAPITests): | |
47 | class TestListVulnerabilityTemplateView(ReadWriteAPITests): | |
46 | 48 | model = VulnerabilityTemplate |
47 | 49 | factory = factories.VulnerabilityTemplateFactory |
48 | 50 | api_endpoint = 'vulnerability_template' |
49 | 51 | view_class = VulnerabilityTemplateView |
52 | patchable_fields = ['description'] | |
53 | ||
54 | def check_url(self, url): | |
55 | return url | |
50 | 56 | |
51 | 57 | def test_backwards_json_compatibility(self, test_client, session): |
52 | 58 | self.factory.create() |
86 | 92 | def test_create_new_vulnerability_template(self, session, test_client): |
87 | 93 | vuln_count_previous = session.query(VulnerabilityTemplate).count() |
88 | 94 | raw_data = self._create_post_data_vulnerability_template(references='') |
89 | res = test_client.post('/v2/vulnerability_template/', data=raw_data) | |
95 | res = test_client.post(self.check_url('/v2/vulnerability_template/'), data=raw_data) | |
90 | 96 | assert res.status_code == 201 |
91 | 97 | assert isinstance(res.json['_id'], int) |
92 | 98 | assert vuln_count_previous + 1 == session.query(VulnerabilityTemplate).count() |
159 | 165 | )) |
160 | 166 | session.commit() |
161 | 167 | |
162 | query = f'/v2/vulnerability_template/filter?q={{"filters": [' \ | |
168 | query = self.check_url(f'/v2/vulnerability_template/filter?q={{"filters": [' \ | |
163 | 169 | f'{{ "name": "{filters["field"]}",' \ |
164 | 170 | f' "op": "{filters["op"]}", ' \ |
165 | f' "val": "{filters["filtered_value"]}" }}]}}' | |
171 | f' "val": "{filters["filtered_value"]}" }}]}}') | |
166 | 172 | |
167 | 173 | res = test_client.get(query) |
168 | 174 | assert res.status_code == 200 |
197 | 203 | )) |
198 | 204 | session.commit() |
199 | 205 | |
200 | query = f'/v2/vulnerability_template/filter?q={{"filters": [' \ | |
206 | query = self.check_url(f'/v2/vulnerability_template/filter?q={{"filters": [' \ | |
201 | 207 | f'{{ "name": "{filters["field"]}",' \ |
202 | 208 | f' "op": "{filters["op"]}", ' \ |
203 | f' "val": "{templates[0].creator.id}" }}]}}' | |
209 | f' "val": "{templates[0].creator.id}" }}]}}') | |
204 | 210 | |
205 | 211 | res = test_client.get(query) |
206 | 212 | assert res.status_code == 200 |
236 | 242 | )) |
237 | 243 | session.commit() |
238 | 244 | |
239 | query = f'/v2/vulnerability_template/filter?q={{"filters": [' \ | |
245 | query = self.check_url(f'/v2/vulnerability_template/filter?q={{"filters": [' \ | |
240 | 246 | f'{{ "name": "{filters["field"]}",' \ |
241 | 247 | f' "op": "{filters["op"]}", ' \ |
242 | f' "val": "{filters["filtered_value"]}" }}]}}' | |
248 | f' "val": "{filters["filtered_value"]}" }}]}}') | |
243 | 249 | |
244 | 250 | res = test_client.get(query) |
245 | 251 | assert res.status_code == 200 |
251 | 257 | template = self.factory.create() |
252 | 258 | session.commit() |
253 | 259 | raw_data = self._create_post_data_vulnerability_template(references='') |
254 | res = test_client.put(f'/v2/vulnerability_template/{template.id}/', data=raw_data) | |
260 | res = test_client.put(self.check_url(f'/v2/vulnerability_template/{template.id}/'), data=raw_data) | |
255 | 261 | assert res.status_code == 200 |
256 | 262 | updated_template = session.query(VulnerabilityTemplate).filter_by(id=template.id).first() |
257 | 263 | assert updated_template.name == raw_data['name'] |
273 | 279 | session.commit() |
274 | 280 | raw_data = self._create_post_data_vulnerability_template( |
275 | 281 | references=references) |
276 | res = test_client.put('/v2/vulnerability_template/{0}/'.format( | |
277 | template.id), data=raw_data) | |
282 | res = test_client.put(self.check_url(f'/v2/vulnerability_template/{template.id}/'), data=raw_data) | |
278 | 283 | assert res.status_code == 400 |
279 | 284 | |
280 | 285 | def test_update_vulnerabiliy_template_change_refs(self, session, test_client): |
284 | 289 | self.first_object.reference_template_instances.add(ref) |
285 | 290 | session.commit() |
286 | 291 | raw_data = self._create_post_data_vulnerability_template(references='new_ref,another_ref') |
287 | res = test_client.put(f'/v2/vulnerability_template/{template.id}/', data=raw_data) | |
292 | res = test_client.put(self.check_url(f'/v2/vulnerability_template/{template.id}/'), data=raw_data) | |
288 | 293 | assert res.status_code == 200 |
289 | 294 | updated_template = session.query(VulnerabilityTemplate).filter_by(id=template.id).first() |
290 | 295 | assert updated_template.name == raw_data['name'] |
296 | 301 | def test_create_new_vulnerability_template_with_references(self, session, test_client): |
297 | 302 | vuln_count_previous = session.query(VulnerabilityTemplate).count() |
298 | 303 | raw_data = self._create_post_data_vulnerability_template(references='ref1,ref2') |
299 | res = test_client.post('/v2/vulnerability_template/', data=raw_data) | |
304 | res = test_client.post(self.check_url('/v2/vulnerability_template/'), data=raw_data) | |
300 | 305 | assert res.status_code == 201 |
301 | 306 | assert isinstance(res.json['_id'], int) |
302 | 307 | assert set(res.json['refs']) == set(['ref1', 'ref2']) |
307 | 312 | def test_delete_vuln_template(self, session, test_client): |
308 | 313 | template = self.factory.create() |
309 | 314 | vuln_count_previous = session.query(VulnerabilityTemplate).count() |
310 | res = test_client.delete(f'/v2/vulnerability_template/{template.id}/') | |
315 | res = test_client.delete(self.check_url(f'/v2/vulnerability_template/{template.id}/')) | |
311 | 316 | |
312 | 317 | assert res.status_code == 204 |
313 | 318 | assert vuln_count_previous - 1 == session.query(VulnerabilityTemplate).count() |
435 | 440 | 'csrf_token': csrf_token |
436 | 441 | } |
437 | 442 | headers = {'Content-type': 'multipart/form-data'} |
438 | res = test_client.post('/v2/vulnerability_template/bulk_create/', | |
443 | res = test_client.post(self.check_url('/v2/vulnerability_template/bulk_create/'), | |
439 | 444 | data=data, headers=headers, use_json_data=False) |
440 | 445 | assert res.status_code == 200 |
441 | 446 | assert len(res.json['vulns_created']) == expected_created_vuln_template |
453 | 458 | 'csrf_token': csrf_token |
454 | 459 | } |
455 | 460 | headers = {'Content-type': 'multipart/form-data'} |
456 | res = test_client.post('/v2/vulnerability_template/bulk_create/', | |
461 | res = test_client.post(self.check_url('/v2/vulnerability_template/bulk_create/'), | |
457 | 462 | data=data, headers=headers, use_json_data=False) |
458 | 463 | assert res.status_code == 200 |
459 | 464 | assert len(res.json['vulns_created']) == expected_created_vuln_template |
471 | 476 | 'csrf_token': csrf_token |
472 | 477 | } |
473 | 478 | headers = {'Content-type': 'multipart/form-data'} |
474 | res = test_client.post('/v2/vulnerability_template/bulk_create/', | |
479 | res = test_client.post(self.check_url('/v2/vulnerability_template/bulk_create/'), | |
475 | 480 | data=data, headers=headers, use_json_data=False) |
476 | 481 | assert res.status_code == 200 |
477 | 482 | assert len(res.json['vulns_created']) == expected_created_vuln_template |
489 | 494 | 'csrf_token': csrf_token |
490 | 495 | } |
491 | 496 | headers = {'Content-type': 'multipart/form-data'} |
492 | res = test_client.post('/v2/vulnerability_template/bulk_create/', | |
497 | res = test_client.post(self.check_url('/v2/vulnerability_template/bulk_create/'), | |
493 | 498 | data=data, headers=headers, use_json_data=False) |
494 | 499 | assert res.status_code == 400 |
495 | 500 | assert 'name' not in res.data.decode('utf8') |
513 | 518 | 'csrf_token': csrf_token |
514 | 519 | } |
515 | 520 | headers = {'Content-type': 'multipart/form-data'} |
516 | res = test_client.post('/v2/vulnerability_template/bulk_create/', | |
521 | res = test_client.post(self.check_url('/v2/vulnerability_template/bulk_create/'), | |
517 | 522 | data=data, headers=headers, use_json_data=False) |
518 | 523 | assert res.status_code == 200 |
519 | 524 | assert len(res.json['vulns_created']) == 1 |
535 | 540 | 'vulns': [vuln_1, vuln_2] |
536 | 541 | } |
537 | 542 | |
538 | res = test_client.post('/v2/vulnerability_template/bulk_create/', json=data) | |
543 | res = test_client.post(self.check_url('/v2/vulnerability_template/bulk_create/'), json=data) | |
539 | 544 | assert res.status_code == 200 |
540 | 545 | |
541 | 546 | vulns_created = res.json['vulns_created'] |
553 | 558 | 'vulns': [vuln_1, vuln_2] |
554 | 559 | } |
555 | 560 | |
556 | res = test_client.post('/v2/vulnerability_template/bulk_create/', json=data) | |
561 | res = test_client.post(self.check_url('/v2/vulnerability_template/bulk_create/'), json=data) | |
557 | 562 | assert res.status_code == 403 |
558 | 563 | assert res.json['message'] == 'Invalid CSRF token.' |
559 | 564 | |
560 | 565 | def test_bulk_create_without_data(self, test_client, csrf_token): |
561 | 566 | data = {'csrf_token': csrf_token} |
562 | res = test_client.post('/v2/vulnerability_template/bulk_create/', json=data) | |
567 | res = test_client.post(self.check_url('/v2/vulnerability_template/bulk_create/'), json=data) | |
563 | 568 | |
564 | 569 | assert res.status_code == 400 |
565 | 570 | assert res.json['message'] == 'Missing data to create vulnerabilities templates.' |
580 | 585 | 'vulns': [vuln_1, vuln_2] |
581 | 586 | } |
582 | 587 | |
583 | res = test_client.post('/v2/vulnerability_template/bulk_create/', json=data) | |
588 | res = test_client.post(self.check_url('/v2/vulnerability_template/bulk_create/'), json=data) | |
584 | 589 | assert res.status_code == 200 |
585 | 590 | |
586 | 591 | assert len(res.json['vulns_with_conflict']) == 1 |
608 | 613 | 'vulns': [vuln_1, vuln_2] |
609 | 614 | } |
610 | 615 | |
611 | res = test_client.post('/v2/vulnerability_template/bulk_create/', json=data) | |
616 | res = test_client.post(self.check_url('/v2/vulnerability_template/bulk_create/'), json=data) | |
612 | 617 | assert res.status_code == 409 |
613 | 618 | |
614 | 619 | assert len(res.json['vulns_with_conflict']) == 2 |
616 | 621 | assert res.json['vulns_with_conflict'][1][1] == vuln_2['name'] |
617 | 622 | |
618 | 623 | assert len(res.json['vulns_created']) == 0 |
624 | ||
625 | ||
626 | class TestListVulnerabilityTemplateViewV3(TestListVulnerabilityTemplateView, PatchableTestsMixin): | |
627 | def url(self, obj=None): | |
628 | return v2_to_v3(super(TestListVulnerabilityTemplateViewV3, self).url(obj)) | |
629 | ||
630 | def check_url(self, url): | |
631 | return v2_to_v3(url) |
7 | 7 | |
8 | 8 | import pytest |
9 | 9 | from faraday.server.api.modules.websocket_auth import decode_agent_websocket_token |
10 | from tests.utils.url import v2_to_v3 | |
10 | 11 | |
11 | 12 | |
12 | 13 | class TestWebsocketAuthEndpoint: |
14 | def check_url(self, url): | |
15 | return url | |
13 | 16 | |
14 | 17 | def test_not_logged_in_request_fail(self, test_client, workspace): |
15 | res = test_client.post(f'/v2/ws/{workspace.name}/websocket_token/') | |
18 | res = test_client.post(self.check_url(f'/v2/ws/{workspace.name}/websocket_token/')) | |
16 | 19 | assert res.status_code == 401 |
17 | 20 | |
18 | 21 | @pytest.mark.usefixtures('logged_user') |
19 | 22 | def test_get_method_not_allowed(self, test_client, workspace): |
20 | res = test_client.get(f'/v2/ws/{workspace.name}/websocket_token/') | |
23 | res = test_client.get(self.check_url(f'/v2/ws/{workspace.name}/websocket_token/')) | |
21 | 24 | assert res.status_code == 405 |
22 | 25 | |
23 | 26 | @pytest.mark.usefixtures('logged_user') |
24 | 27 | def test_succeeds(self, test_client, workspace): |
25 | res = test_client.post(f'/v2/ws/{workspace.name}/websocket_token/') | |
28 | res = test_client.post(self.check_url(f'/v2/ws/{workspace.name}/websocket_token/')) | |
26 | 29 | assert res.status_code == 200 |
27 | 30 | |
28 | 31 | # A token for that workspace should be generated, |
31 | 34 | assert res.json['token'].startswith(str(workspace.id)) |
32 | 35 | |
33 | 36 | |
37 | class TestWebsocketAuthEndpointV3(TestWebsocketAuthEndpoint): | |
38 | def check_url(self, url): | |
39 | return v2_to_v3(url) | |
40 | ||
41 | ||
34 | 42 | class TestAgentWebsocketToken: |
43 | ||
44 | def check_url(self, url): | |
45 | return url | |
46 | ||
35 | 47 | @pytest.mark.usefixtures('session') # I don't know why this is required |
36 | 48 | def test_fails_without_authorization_header(self, test_client): |
37 | 49 | res = test_client.post( |
38 | '/v2/agent_websocket_token/', | |
50 | self.check_url('/v2/agent_websocket_token/') | |
39 | 51 | ) |
40 | 52 | assert res.status_code == 401 |
41 | 53 | |
42 | 54 | @pytest.mark.usefixtures('logged_user') |
43 | 55 | def test_fails_with_logged_user(self, test_client): |
44 | 56 | res = test_client.post( |
45 | '/v2/agent_websocket_token/', | |
57 | self.check_url('/v2/agent_websocket_token/') | |
46 | 58 | ) |
47 | 59 | assert res.status_code == 401 |
48 | 60 | |
49 | 61 | @pytest.mark.usefixtures('logged_user') |
50 | 62 | def test_fails_with_user_token(self, test_client, session): |
51 | res = test_client.get('/v2/token/') | |
63 | res = test_client.get(self.check_url('/v2/token/')) | |
52 | 64 | |
53 | 65 | assert res.status_code == 200 |
54 | 66 | |
57 | 69 | # clean cookies make sure test_client has no session |
58 | 70 | test_client.cookie_jar.clear() |
59 | 71 | res = test_client.post( |
60 | '/v2/agent_websocket_token/', | |
72 | self.check_url('/v2/agent_websocket_token/'), | |
61 | 73 | headers=headers, |
62 | 74 | ) |
63 | 75 | assert res.status_code == 401 |
66 | 78 | def test_fails_with_invalid_agent_token(self, test_client): |
67 | 79 | headers = [('Authorization', 'Agent 13123')] |
68 | 80 | res = test_client.post( |
69 | '/v2/agent_websocket_token/', | |
81 | self.check_url('/v2/agent_websocket_token/'), | |
70 | 82 | headers=headers, |
71 | 83 | ) |
72 | 84 | assert res.status_code == 403 |
78 | 90 | assert agent.token |
79 | 91 | headers = [('Authorization', 'Agent ' + agent.token)] |
80 | 92 | res = test_client.post( |
81 | '/v2/agent_websocket_token/', | |
93 | self.check_url('/v2/agent_websocket_token/'), | |
82 | 94 | headers=headers, |
83 | 95 | ) |
84 | 96 | assert res.status_code == 200 |
86 | 98 | assert decoded_agent == agent |
87 | 99 | |
88 | 100 | |
89 | # I'm Py3 | |
101 | class TestAgentWebsocketTokenV3(TestAgentWebsocketToken): | |
102 | def check_url(self, url): | |
103 | return v2_to_v3(url) |
6 | 6 | |
7 | 7 | import time |
8 | 8 | import pytest |
9 | from posixpath import join as urljoin | |
9 | 10 | |
10 | 11 | from faraday.server.models import Workspace, Scope |
11 | 12 | from faraday.server.api.modules.workspaces import WorkspaceView |
12 | from tests.test_api_non_workspaced_base import ReadWriteAPITests | |
13 | from tests.test_api_non_workspaced_base import ReadWriteAPITests, PatchableTestsMixin | |
13 | 14 | from tests import factories |
15 | from tests.utils.url import v2_to_v3 | |
16 | ||
14 | 17 | |
15 | 18 | class TestWorkspaceAPI(ReadWriteAPITests): |
16 | 19 | model = Workspace |
18 | 21 | api_endpoint = 'ws' |
19 | 22 | lookup_field = 'name' |
20 | 23 | view_class = WorkspaceView |
24 | patchable_fields = ['name'] | |
25 | ||
26 | def check_url(self, url): | |
27 | return url | |
21 | 28 | |
22 | 29 | @pytest.mark.usefixtures('ignore_nplusone') |
23 | 30 | def test_filter_restless_by_name(self, test_client): |
24 | res = test_client.get(f'{self.url()}filter?q=' | |
25 | f'{{"filters":[{{"name": "name", "op":"eq", "val": "{self.first_object.name}"}}]}}') | |
31 | res = test_client.get( | |
32 | urljoin( | |
33 | self.url(), | |
34 | f'filter?q={{"filters":[{{"name": "name", "op":"eq", "val": "{self.first_object.name}"}}]}}' | |
35 | ) | |
36 | ) | |
26 | 37 | assert res.status_code == 200 |
27 | 38 | assert len(res.json) == 1 |
28 | 39 | assert res.json[0]['name'] == self.first_object.name |
29 | 40 | |
30 | 41 | @pytest.mark.usefixtures('ignore_nplusone') |
31 | 42 | def test_filter_restless_by_name_zero_results_found(self, test_client): |
32 | res = test_client.get(f'{self.url()}filter?q=' | |
33 | f'{{"filters":[{{"name": "name", "op":"eq", "val": "thiswsdoesnotexist"}}]}}') | |
43 | res = test_client.get( | |
44 | urljoin( | |
45 | self.url(), | |
46 | 'filter?q={"filters":[{"name": "name", "op":"eq", "val": "thiswsdoesnotexist"}]}' | |
47 | ) | |
48 | ) | |
34 | 49 | assert res.status_code == 200 |
35 | 50 | assert len(res.json) == 0 |
36 | 51 | |
37 | 52 | def test_filter_restless_by_description(self, test_client): |
38 | 53 | self.first_object.description = "this is a new description" |
39 | res = test_client.get(f'{self.url()}filter?q=' | |
40 | f'{{"filters":[{{"name": "description", "op":"eq", "val": "{self.first_object.description}"}}]}}') | |
54 | res = test_client.get( | |
55 | urljoin( | |
56 | self.url(), | |
57 | f'filter?q={{"filters":[{{"name": "description", "op":"eq", "val": "{self.first_object.description}"}}' | |
58 | ']}' | |
59 | ) | |
60 | ) | |
41 | 61 | assert res.status_code == 200 |
42 | 62 | assert len(res.json) == 1 |
43 | 63 | assert res.json[0]['description'] == self.first_object.description |
67 | 87 | session.commit() |
68 | 88 | |
69 | 89 | self.first_object.description = "this is a new description" |
70 | res = test_client.get(f'{self.url()}filter?q=' | |
71 | f'{{"filters":[{{"name": "description", "op":"eq", "val": "{self.first_object.description}"}}]}}') | |
90 | res = test_client.get( | |
91 | urljoin( | |
92 | self.url(), | |
93 | f'filter?q={{"filters":[{{"name": "description", "op":"eq", "val": "{self.first_object.description}"}}' | |
94 | ']}' | |
95 | ) | |
96 | ) | |
72 | 97 | assert res.status_code == 200 |
73 | 98 | assert len(res.json) == 1 |
74 | 99 | assert res.json[0]['description'] == self.first_object.description |
226 | 251 | end_date = start_date+86400000 |
227 | 252 | duration = {'start_date': start_date, 'end_date': end_date} |
228 | 253 | raw_data = {'name': 'somethingdarkside', 'duration': duration} |
229 | res = test_client.post('/v2/ws/', data=raw_data) | |
254 | res = test_client.post(self.url(), data=raw_data) | |
230 | 255 | assert res.status_code == 201 |
231 | 256 | assert workspace_count_previous + 1 == session.query(Workspace).count() |
232 | 257 | assert res.json['duration']['start_date'] == start_date |
235 | 260 | def test_create_fails_with_mayus(self, session, test_client): |
236 | 261 | workspace_count_previous = session.query(Workspace).count() |
237 | 262 | raw_data = {'name': 'sWtr'} |
238 | res = test_client.post('/v2/ws/', data=raw_data) | |
263 | res = test_client.post(self.url(), data=raw_data) | |
239 | 264 | assert res.status_code == 400 |
240 | 265 | assert workspace_count_previous == session.query(Workspace).count() |
241 | 266 | |
242 | 267 | def test_create_fails_with_special_character(self, session, test_client): |
243 | 268 | workspace_count_previous = session.query(Workspace).count() |
244 | 269 | raw_data = {'name': '$wtr'} |
245 | res = test_client.post('/v2/ws/', data=raw_data) | |
270 | res = test_client.post(self.url(), data=raw_data) | |
246 | 271 | assert res.status_code == 400 |
247 | 272 | assert workspace_count_previous == session.query(Workspace).count() |
248 | 273 | |
249 | 274 | def test_create_with_initial_number(self, session, test_client): |
250 | 275 | workspace_count_previous = session.query(Workspace).count() |
251 | 276 | raw_data = {'name': '2$wtr'} |
252 | res = test_client.post('/v2/ws/', data=raw_data) | |
277 | res = test_client.post(self.url(), data=raw_data) | |
253 | 278 | assert res.status_code == 201 |
254 | 279 | assert workspace_count_previous + 1 == session.query(Workspace).count() |
255 | 280 | |
260 | 285 | start_date = 'this should clearly fail' |
261 | 286 | duration = {'start_date': start_date, 'end_date': 86400000} |
262 | 287 | raw_data = {'name': 'somethingdarkside', 'duration': duration} |
263 | res = test_client.post('/v2/ws/', data=raw_data) | |
288 | res = test_client.post(self.url(), data=raw_data) | |
264 | 289 | assert res.status_code == 400 |
265 | 290 | assert workspace_count_previous == session.query(Workspace).count() |
266 | 291 | |
272 | 297 | start_date = int(time.time())*1000 |
273 | 298 | duration = {'start_date': start_date, 'end_date': start_date-86400000} |
274 | 299 | raw_data = {'name': 'somethingdarkside', 'duration': duration} |
275 | res = test_client.post('/v2/ws/', data=raw_data) | |
300 | res = test_client.post(self.url(), data=raw_data) | |
276 | 301 | assert res.status_code == 400 |
277 | 302 | assert workspace_count_previous == session.query(Workspace).count() |
278 | 303 | |
280 | 305 | description = 'darkside' |
281 | 306 | raw_data = {'name': 'something', 'description': description} |
282 | 307 | workspace_count_previous = session.query(Workspace).count() |
283 | res = test_client.post('/v2/ws/', data=raw_data) | |
308 | res = test_client.post(self.url(), data=raw_data) | |
284 | 309 | assert res.status_code == 201 |
285 | 310 | assert workspace_count_previous + 1 == session.query(Workspace).count() |
286 | 311 | assert res.json['description'] == description |
291 | 316 | ]) |
292 | 317 | def test_create_stat_is_zero(self, test_client, stat_name): |
293 | 318 | raw_data = {'name': 'something', 'description': ''} |
294 | res = test_client.post('/v2/ws/', data=raw_data) | |
319 | res = test_client.post(self.url(), data=raw_data) | |
295 | 320 | assert res.status_code == 201 |
296 | 321 | assert res.json['stats'][stat_name] == 0 |
297 | 322 | |
303 | 328 | session.add_all(vulns) |
304 | 329 | session.commit() |
305 | 330 | raw_data = {'name': 'something', 'description': ''} |
306 | res = test_client.put(f'/v2/ws/{workspace.name}/', | |
307 | data=raw_data) | |
331 | res = test_client.put(self.url(obj=workspace), data=raw_data) | |
308 | 332 | assert res.status_code == 200 |
309 | 333 | assert res.json['stats']['web_vulns'] == 5 |
310 | 334 | assert res.json['stats']['std_vulns'] == 10 |
317 | 341 | ] |
318 | 342 | raw_data = {'name': 'something', 'description': 'test', |
319 | 343 | 'scope': desired_scope} |
320 | res = test_client.post('/v2/ws/', data=raw_data) | |
344 | res = test_client.post(self.url(), data=raw_data) | |
321 | 345 | assert res.status_code == 201 |
322 | 346 | assert set(res.json['scope']) == set(desired_scope) |
323 | 347 | workspace = Workspace.query.get(res.json['id']) |
332 | 356 | ] |
333 | 357 | raw_data = {'name': 'something', 'description': 'test', |
334 | 358 | 'scope': desired_scope} |
335 | res = test_client.put(f'/v2/ws/{workspace.name}/', data=raw_data) | |
359 | res = test_client.put(self.url(obj=workspace), data=raw_data) | |
336 | 360 | assert res.status_code == 200 |
337 | 361 | assert set(res.json['scope']) == set(desired_scope) |
338 | 362 | assert set(s.name for s in workspace.scope) == set(desired_scope) |
375 | 399 | workspace_count_previous = session.query(Workspace).count() |
376 | 400 | duration = {'start_date': 1563638577, 'end_date': 1563538577} |
377 | 401 | raw_data = {'name': 'somethingdarkside', 'duration': duration} |
378 | res = test_client.post('/v2/ws/', data=raw_data) | |
402 | res = test_client.post(self.url(), data=raw_data) | |
379 | 403 | assert res.status_code == 400 |
380 | 404 | assert workspace_count_previous == session.query(Workspace).count() |
405 | ||
406 | ||
407 | class TestWorkspaceAPIV3(TestWorkspaceAPI, PatchableTestsMixin): | |
408 | ||
409 | def check_url(self, url): | |
410 | return v2_to_v3(url) | |
411 | ||
412 | def url(self, obj=None): | |
413 | return v2_to_v3(super(TestWorkspaceAPIV3, self).url(obj)) | |
414 | ||
415 | def test_workspace_activation(self, test_client, workspace, session): | |
416 | workspace.active = False | |
417 | session.add(workspace) | |
418 | session.commit() | |
419 | res = test_client.patch(self.url(workspace), data={'active': True}) | |
420 | assert res.status_code == 200 | |
421 | ||
422 | res = test_client.get(self.url(workspace)) | |
423 | active = res.json.get('active') | |
424 | assert active == True | |
425 | ||
426 | active_query = session.query(Workspace).filter_by(id=workspace.id).first().active | |
427 | assert active_query == True | |
428 | ||
429 | def test_workspace_deactivation(self, test_client, workspace, session): | |
430 | workspace.active = True | |
431 | session.add(workspace) | |
432 | session.commit() | |
433 | res = test_client.patch(self.url(workspace), data={'active': False}) | |
434 | assert res.status_code == 200 | |
435 | ||
436 | res = test_client.get(self.url(workspace)) | |
437 | active = res.json.get('active') | |
438 | assert active == False | |
439 | ||
440 | active_query = session.query(Workspace).filter_by(id=workspace.id).first().active | |
441 | assert active_query == False |
5 | 5 | |
6 | 6 | ''' |
7 | 7 | from builtins import str |
8 | from posixpath import join as urljoin | |
9 | ||
10 | from tests.utils.url import v2_to_v3 | |
8 | 11 | |
9 | 12 | """Generic tests for APIs prefixed with a workspace_name""" |
10 | 13 | |
100 | 103 | res = test_client.get(self.url(self.first_object, second_workspace)) |
101 | 104 | assert res.status_code == 404 |
102 | 105 | |
103 | @pytest.mark.parametrize('object_id', [12345, -1, 'xxx', u'áá']) | |
106 | @pytest.mark.parametrize('object_id', [123456789, -1, 'xxx', u'áá']) | |
104 | 107 | def test_404_when_retrieving_unexistent_object(self, test_client, |
105 | 108 | object_id): |
106 | 109 | url = self.url(object_id) |
112 | 115 | |
113 | 116 | def test_create_succeeds(self, test_client): |
114 | 117 | data = self.factory.build_dict(workspace=self.workspace) |
118 | count = self.model.query.count() | |
115 | 119 | res = test_client.post(self.url(), |
116 | 120 | data=data) |
117 | 121 | assert res.status_code == 201, (res.status_code, res.data) |
118 | assert self.model.query.count() == OBJECT_COUNT + 1 | |
119 | object_id = res.json['id'] | |
122 | assert self.model.query.count() == count + 1 | |
123 | object_id = res.json.get('id') or res.json['_id'] | |
120 | 124 | obj = self.model.query.get(object_id) |
121 | 125 | assert obj.workspace == self.workspace |
122 | 126 | |
124 | 128 | self.workspace.readonly = True |
125 | 129 | db.session.commit() |
126 | 130 | data = self.factory.build_dict(workspace=self.workspace) |
131 | count = self.model.query.count() | |
127 | 132 | res = test_client.post(self.url(), |
128 | 133 | data=data) |
129 | 134 | db.session.commit() |
130 | 135 | assert res.status_code == 403 |
131 | assert self.model.query.count() == OBJECT_COUNT | |
136 | assert self.model.query.count() == count | |
132 | 137 | |
133 | 138 | |
134 | 139 | def test_create_inactive_fails(self, test_client): |
135 | 140 | self.workspace.deactivate() |
136 | 141 | db.session.commit() |
137 | 142 | data = self.factory.build_dict(workspace=self.workspace) |
143 | count = self.model.query.count() | |
138 | 144 | res = test_client.post(self.url(), |
139 | 145 | data=data) |
140 | 146 | assert res.status_code == 403, (res.status_code, res.data) |
141 | assert self.model.query.count() == OBJECT_COUNT | |
147 | assert self.model.query.count() == count | |
142 | 148 | |
143 | 149 | def test_create_fails_with_empty_dict(self, test_client): |
144 | 150 | res = test_client.post(self.url(), data={}) |
171 | 177 | |
172 | 178 | class UpdateTestsMixin: |
173 | 179 | |
174 | def test_update_an_object(self, test_client): | |
180 | def control_cant_change_data(self, data: dict) -> dict: | |
181 | return data | |
182 | ||
183 | @pytest.mark.parametrize("method", ["PUT"]) | |
184 | def test_update_an_object(self, test_client, method): | |
175 | 185 | data = self.factory.build_dict(workspace=self.workspace) |
176 | res = test_client.put(self.url(self.first_object), | |
177 | data=data) | |
186 | data = self.control_cant_change_data(data) | |
187 | count = self.model.query.count() | |
188 | if method == "PUT": | |
189 | res = test_client.put(self.url(self.first_object), | |
190 | data=data) | |
191 | elif method == "PATCH": | |
192 | data = PatchableTestsMixin.control_data(self, data) | |
193 | res = test_client.patch(self.url(self.first_object), data=data) | |
178 | 194 | assert res.status_code == 200 |
179 | assert self.model.query.count() == OBJECT_COUNT | |
195 | assert self.model.query.count() == count | |
180 | 196 | for updated_field in self.update_fields: |
181 | 197 | assert res.json[updated_field] == getattr(self.first_object, |
182 | 198 | updated_field) |
183 | 199 | |
184 | def test_update_an_object_readonly_fails(self, test_client): | |
200 | @pytest.mark.parametrize("method", ["PUT"]) | |
201 | def test_update_an_object_readonly_fails(self, test_client, method): | |
185 | 202 | self.workspace.readonly = True |
186 | 203 | db.session.commit() |
187 | 204 | for unique_field in self.unique_fields: |
188 | 205 | data = self.factory.build_dict() |
189 | 206 | old_field = getattr(self.objects[0], unique_field) |
190 | 207 | old_id = getattr(self.objects[0], 'id') |
191 | res = test_client.put(self.url(self.first_object), data=data) | |
208 | if method == "PUT": | |
209 | res = test_client.put(self.url(self.first_object), data=data) | |
210 | elif method == "PATCH": | |
211 | res = test_client.patch(self.url(self.first_object), data=data) | |
192 | 212 | db.session.commit() |
193 | 213 | assert res.status_code == 403 |
194 | 214 | assert self.model.query.count() == OBJECT_COUNT |
195 | 215 | assert old_field == getattr(self.model.query.filter(self.model.id == old_id).one(), unique_field) |
196 | 216 | |
197 | def test_update_inactive_fails(self, test_client): | |
217 | @pytest.mark.parametrize("method", ["PUT"]) | |
218 | def test_update_inactive_fails(self, test_client, method): | |
198 | 219 | self.workspace.deactivate() |
199 | 220 | db.session.commit() |
200 | 221 | data = self.factory.build_dict(workspace=self.workspace) |
201 | res = test_client.put(self.url(self.first_object), | |
202 | data=data) | |
222 | count = self.model.query.count() | |
223 | if method == "PUT": | |
224 | res = test_client.put(self.url(self.first_object), | |
225 | data=data) | |
226 | elif method == "PATCH": | |
227 | res = test_client.patch(self.url(self.first_object), | |
228 | data=data) | |
203 | 229 | assert res.status_code == 403 |
204 | assert self.model.query.count() == OBJECT_COUNT | |
205 | ||
206 | def test_update_fails_with_existing(self, test_client, session): | |
230 | assert self.model.query.count() == count | |
231 | ||
232 | @pytest.mark.parametrize("method", ["PUT"]) | |
233 | def test_update_fails_with_existing(self, test_client, session, method): | |
207 | 234 | for unique_field in self.unique_fields: |
208 | data = self.factory.build_dict() | |
209 | data[unique_field] = getattr(self.objects[1], unique_field) | |
210 | res = test_client.put(self.url(self.first_object), data=data) | |
235 | unique_field_value = getattr(self.objects[1], unique_field) | |
236 | if method == "PUT": | |
237 | data = self.factory.build_dict() | |
238 | data[unique_field] = unique_field_value | |
239 | res = test_client.put(self.url(self.first_object), data=data) | |
240 | elif method == "PATCH": | |
241 | res = test_client.patch(self.url(self.first_object), data={unique_field: unique_field_value}) | |
211 | 242 | assert res.status_code == 409 |
212 | 243 | assert self.model.query.count() == OBJECT_COUNT |
213 | 244 | |
216 | 247 | res = test_client.put(self.url(self.first_object), data={}) |
217 | 248 | assert res.status_code == 400 |
218 | 249 | |
219 | def test_update_cant_change_id(self, test_client): | |
250 | @pytest.mark.parametrize("method", ["PUT"]) | |
251 | def test_update_cant_change_id(self, test_client, method): | |
220 | 252 | raw_json = self.factory.build_dict(workspace=self.workspace) |
253 | raw_json = self.control_cant_change_data(raw_json) | |
221 | 254 | expected_id = self.first_object.id |
222 | 255 | raw_json['id'] = 100000 |
223 | res = test_client.put(self.url(self.first_object), | |
224 | data=raw_json) | |
225 | assert res.status_code == 200 | |
226 | assert res.json['id'] == expected_id | |
227 | ||
256 | if method == "PUT": | |
257 | res = test_client.put(self.url(self.first_object), | |
258 | data=raw_json) | |
259 | if method == "PATCH": | |
260 | res = test_client.patch(self.url(self.first_object), | |
261 | data=raw_json) | |
262 | assert res.status_code == 200, (res.status_code, res.data) | |
263 | object_id = res.json.get('id') or res.json['_id'] | |
264 | assert object_id == expected_id | |
265 | ||
266 | ||
267 | class PatchableTestsMixin(UpdateTestsMixin): | |
268 | ||
269 | @staticmethod | |
270 | def control_data(test_suite, data: dict) -> dict: | |
271 | return {key: value for (key, value) in data.items() if key in test_suite.patchable_fields} | |
272 | ||
273 | @pytest.mark.parametrize("method", ["PUT", "PATCH"]) | |
274 | def test_update_an_object(self, test_client, method): | |
275 | super(PatchableTestsMixin, self).test_update_an_object(test_client, method) | |
276 | ||
277 | @pytest.mark.parametrize("method", ["PUT", "PATCH"]) | |
278 | def test_update_an_object_readonly_fails(self, test_client, method): | |
279 | super(PatchableTestsMixin, self).test_update_an_object_readonly_fails(test_client, method) | |
280 | ||
281 | @pytest.mark.parametrize("method", ["PUT", "PATCH"]) | |
282 | def test_update_inactive_fails(self, test_client, method): | |
283 | super(PatchableTestsMixin, self).test_update_inactive_fails(test_client, method) | |
284 | ||
285 | @pytest.mark.parametrize("method", ["PUT", "PATCH"]) | |
286 | def test_update_fails_with_existing(self, test_client, session, method): | |
287 | super(PatchableTestsMixin, self).test_update_fails_with_existing(test_client, session, method) | |
288 | ||
289 | def test_update_an_object_fails_with_empty_dict(self, test_client): | |
290 | """To do this the user should use a PATCH request""" | |
291 | res = test_client.patch(self.url(self.first_object), data={}) | |
292 | assert res.status_code == 200, (res.status_code, res.json) | |
293 | ||
294 | @pytest.mark.parametrize("method", ["PUT", "PATCH"]) | |
295 | def test_update_cant_change_id(self, test_client, method): | |
296 | super(PatchableTestsMixin, self).test_update_cant_change_id(test_client, method) | |
228 | 297 | |
229 | 298 | class CountTestsMixin: |
230 | 299 | def test_count(self, test_client, session, user_factory): |
231 | 300 | |
301 | factory_kwargs = {} | |
302 | for extra_filter in self.view_class.count_extra_filters: | |
303 | field = extra_filter.left.name | |
304 | value = extra_filter.right.effective_value | |
305 | setattr(self.first_object, field, value) | |
306 | factory_kwargs[field] = value | |
307 | ||
232 | 308 | session.add(self.factory.create(creator=self.first_object.creator, |
233 | workspace=self.first_object.workspace)) | |
234 | ||
235 | session.commit() | |
236 | res = test_client.get(self.url() + "count/?group_by=creator_id") | |
309 | workspace=self.first_object.workspace, | |
310 | **factory_kwargs)) | |
311 | ||
312 | session.commit() | |
313 | ||
314 | if self.view_class.route_prefix.startswith("/v2"): | |
315 | res = test_client.get(urljoin(self.url(), "count/?group_by=creator_id")) | |
316 | else: | |
317 | res = test_client.get(urljoin(self.url(), "count?group_by=creator_id")) | |
318 | ||
237 | 319 | assert res.status_code == 200, res.json |
238 | 320 | res = res.get_json() |
239 | 321 | |
244 | 326 | grouped += 1 |
245 | 327 | creators.append(obj['creator_id']) |
246 | 328 | |
247 | assert grouped == 1 | |
329 | assert grouped == 1, (res) | |
248 | 330 | assert creators == sorted(creators) |
249 | 331 | |
250 | 332 | def test_count_descending(self, test_client, session, user_factory): |
251 | 333 | |
334 | factory_kwargs = {} | |
335 | for extra_filter in self.view_class.count_extra_filters: | |
336 | field = extra_filter.left.name | |
337 | value = extra_filter.right.effective_value | |
338 | setattr(self.first_object, field, value) | |
339 | factory_kwargs[field] = value | |
340 | ||
252 | 341 | session.add(self.factory.create(creator=self.first_object.creator, |
253 | workspace=self.first_object.workspace)) | |
254 | ||
255 | session.commit() | |
256 | res = test_client.get(self.url() + "count/?group_by=creator_id&order=desc") | |
342 | workspace=self.first_object.workspace, | |
343 | **factory_kwargs)) | |
344 | ||
345 | session.commit() | |
346 | ||
347 | if self.view_class.route_prefix.startswith("/v2"): | |
348 | res = test_client.get(urljoin(self.url(), "count/?group_by=creator_id&order=desc")) | |
349 | else: | |
350 | res = test_client.get(urljoin(self.url(), "count?group_by=creator_id&order=desc")) | |
351 | ||
257 | 352 | assert res.status_code == 200, res.json |
258 | 353 | res = res.get_json() |
259 | 354 | |
264 | 359 | grouped += 1 |
265 | 360 | creators.append(obj['creator_id']) |
266 | 361 | |
267 | assert grouped == 1 | |
362 | assert grouped == 1, res | |
268 | 363 | assert creators == sorted(creators, reverse=True) |
269 | 364 | |
270 | 365 | |
354 | 449 | res = test_client.get(self.url()) |
355 | 450 | assert res.status_code == 200 |
356 | 451 | assert len(res.json['data']) == OBJECT_COUNT |
452 | ||
453 | class ReadWriteMultiWorkspacedAPITests(ReadOnlyMultiWorkspacedAPITests, | |
454 | ReadWriteTestsMixin): | |
455 | pass |
4 | 4 | update_executors, BroadcastServerProtocol |
5 | 5 | |
6 | 6 | from tests.factories import AgentFactory, ExecutorFactory |
7 | ||
8 | ||
9 | def _join_agent(test_client, session): | |
10 | agent = AgentFactory.create(token='pepito') | |
11 | session.add(agent) | |
12 | session.commit() | |
13 | ||
14 | headers = {"Authorization": f"Agent {agent.token}"} | |
15 | token = test_client.post('v2/agent_websocket_token/', headers=headers).json['token'] | |
16 | return token | |
7 | from tests.utils.url import v2_to_v3 | |
17 | 8 | |
18 | 9 | |
19 | 10 | class TransportMock: |
36 | 27 | |
37 | 28 | class TestWebsocketBroadcastServerProtocol: |
38 | 29 | |
30 | def check_url(self, url): | |
31 | return url | |
32 | ||
33 | def _join_agent(self, test_client, session): | |
34 | agent = AgentFactory.create(token='pepito') | |
35 | session.add(agent) | |
36 | session.commit() | |
37 | ||
38 | headers = {"Authorization": f"Agent {agent.token}"} | |
39 | token = test_client.post(self.check_url('/v2/agent_websocket_token/'), headers=headers).json['token'] | |
40 | return token | |
41 | ||
39 | 42 | def test_join_agent_message_with_invalid_token_fails(self, session, proto, test_client): |
40 | 43 | message = '{"action": "JOIN_AGENT", "token": "pepito" }' |
41 | 44 | assert not proto.onMessage(message, False) |
45 | 48 | assert not proto.onMessage(message, False) |
46 | 49 | |
47 | 50 | def test_join_agent_message_with_valid_token(self, session, proto, workspace, test_client): |
48 | token = _join_agent(test_client, session) | |
51 | token = self._join_agent(test_client, session) | |
49 | 52 | message = f'{{"action": "JOIN_AGENT", "workspace": "{workspace.name}", "token": "{token}", "executors": [] }}' |
50 | 53 | assert proto.onMessage(message, False) |
51 | 54 | |
52 | 55 | def test_leave_agent_happy_path(self, session, proto, workspace, test_client): |
53 | token = _join_agent(test_client, session) | |
56 | token = self._join_agent(test_client, session) | |
54 | 57 | message = f'{{"action": "JOIN_AGENT", "workspace": "{workspace.name}", "token": "{token}", "executors": [] }}' |
55 | 58 | assert proto.onMessage(message, False) |
56 | 59 | |
58 | 61 | assert proto.onMessage(message, False) |
59 | 62 | |
60 | 63 | def test_agent_status(self, session, proto, workspace, test_client): |
61 | token = _join_agent(test_client, session) | |
64 | token = self._join_agent(test_client, session) | |
62 | 65 | agent = Agent.query.one() |
63 | 66 | assert not agent.is_online |
64 | 67 | message = f'{{"action": "JOIN_AGENT", "workspace": "{workspace.name}", "token": "{token}", "executors": [] }}' |
68 | 71 | message = '{"action": "LEAVE_AGENT"}' |
69 | 72 | assert proto.onMessage(message, False) |
70 | 73 | assert not agent.is_online |
74 | ||
75 | ||
76 | class TestWebsocketBroadcastServerProtocolV3(TestWebsocketBroadcastServerProtocol): | |
77 | def check_url(self, url): | |
78 | return v2_to_v3(url) | |
71 | 79 | |
72 | 80 | |
73 | 81 | class TestCheckExecutors: |