Codebase list python-faraday / upstream/3.14.1
New upstream version 3.14.1 Sophie Brun 3 years ago
94 changed file(s) with 3467 addition(s) and 1127 deletion(s). Raw diff Collapse all Expand all
55 - git config remote.github.url >/dev/null || git remote add github https://${GH_TOKEN}@github.com/infobyte/faraday.git
66 - export FARADAY_VERSION=$(eval $IMAGE_TAG)
77 - CHANGELOG/check_pre_tag.py
8 - git push github white/dev:master
8 - git push github $CI_COMMIT_TAG:master
99 - git tag v$FARADAY_VERSION -m "$(cat CHANGELOG/$FARADAY_VERSION/white.md)"
1010 - 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
1212 rules:
1313 - if: '$CI_COMMIT_TAG =~ /^white-v[0-9.]+$/'
1414 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`
00 New features in the latest update
11 =====================================
22
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`
333
434 3.14.0 [Dec 23th, 2020]:
535 ---
00 New features in the latest update
11 =====================================
22
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`
333
434 3.14.0 [Dec 23th, 2020]:
535 ---
11 # Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/)
22 # See the file 'doc/LICENSE' for the license information
33
4 __version__ = '3.14.0'
4 __version__ = '3.14.1'
55 __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')
120120 def login(self, username, password):
121121 auth = {"email": username, "password": password}
122122 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)
124124 if resp.status_code not in [200, 302]:
125125 logger.info("Invalid credentials")
126126 return None
672672 workspace,
673673 severity_count=severity_count
674674 )
675
675 count = filter_query.count()
676676 if limit:
677677 filter_query = filter_query.limit(limit)
678678 if offset:
679679 filter_query = filter_query.offset(offset)
680 count = filter_query.count()
681680 objs = self.schema_class(**marshmallow_params).dumps(filter_query.all())
682681 return json.loads(objs), count
683682 else:
10941093 flask.request)
10951094 # just in case an schema allows id as writable.
10961095 data.pop('id', None)
1097 self._update_object(obj, data)
1096 self._update_object(obj, data, partial=False)
10981097 self._perform_update(object_id, obj, data, **kwargs)
10991098
11001099 return self._dump(obj, kwargs), 200
11011100
1102 def _update_object(self, obj, data):
1101 def _update_object(self, obj, data, **kwargs):
11031102 """Perform changes in the selected object
11041103
11051104 It modifies the attributes of the SQLAlchemy model to match
11121111 for (key, value) in data.items():
11131112 setattr(obj, key, value)
11141113
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):
11161115 """Commit the SQLAlchemy session, check for updating conflicts"""
11171116 try:
11181117 db.session.add(obj)
11371136 raise
11381137 return obj
11391138
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
11401181
11411182 class UpdateWorkspacedMixin(UpdateMixin, CommandMixin):
11421183 """Add PUT /<workspace_name>/<route_base>/<id>/ route
11811222 """
11821223 return super(UpdateWorkspacedMixin, self).put(object_id, workspace_name=workspace_name)
11831224
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):
11851226 # # Make sure that if I created new objects, I had properly commited them
11861227 # assert not db.session.new
11871228
11911232 self._set_command_id(obj, False)
11921233 return super(UpdateWorkspacedMixin, self)._perform_update(
11931234 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)
11941274
11951275
11961276 class DeleteMixin:
66 from flask import Blueprint
77 from marshmallow import fields
88
9 from faraday.server.api.base import AutoSchema, ReadWriteWorkspacedView, PaginatedMixin
9 from faraday.server.api.base import AutoSchema, ReadWriteWorkspacedView, PaginatedMixin, PatchableWorkspacedMixin
1010 from faraday.server.models import Command
1111 from faraday.server.schemas import PrimaryKeyRelatedField
1212
8989 }
9090
9191
92 class ActivityFeedV3View(ActivityFeedView, PatchableWorkspacedMixin):
93 route_prefix = '/v3/ws/<workspace_name>/'
94 trailing_slash = False
95
96
9297 ActivityFeedView.register(activityfeed_api)
98 ActivityFeedV3View.register(activityfeed_api)
1818 ReadOnlyView,
1919 CreateMixin,
2020 GenericView,
21 ReadOnlyMultiWorkspacedView
21 ReadOnlyMultiWorkspacedView,
22 PatchableMixin
2223 )
2324 from faraday.server.api.modules.workspaces import WorkspaceSchema
2425 from faraday.server.models import Agent, Executor, AgentExecution, db, \
9596 'token',
9697 'workspaces',
9798 )
99
98100
99101 class AgentCreationView(CreateMixin, GenericView):
100102 """
167169 return agent
168170
169171
172 class AgentCreationV3View(AgentCreationView):
173 route_prefix = '/v3'
174 trailing_slash = False
175
176
170177 class ExecutorDataSchema(Schema):
171178 executor = fields.String(default=None)
172179 args = fields.Dict(default=None)
200207 except NoResultFound:
201208 flask.abort(404, f"No such workspace: {workspace_name}")
202209
203 def _update_object(self, obj, data):
210 def _update_object(self, obj, data, **kwargs):
204211 """Perform changes in the selected object
205212
206213 It modifies the attributes of the SQLAlchemy model to match
210217 with some specific field. Typically the new method should call
211218 this one to handle the update of the rest of the fields.
212219 """
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:
216224 abort(
217225 make_response(
218226 jsonify(
240248 obj.workspaces = workspaces
241249
242250 return obj
251
252
253 class AgentWithWorkspacesV3View(AgentWithWorkspacesView, PatchableMixin):
254 route_prefix = '/v3'
255 trailing_slash = False
243256
244257
245258 class AgentView(ReadOnlyMultiWorkspacedView):
336349 })
337350
338351
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
339369 AgentWithWorkspacesView.register(agent_api)
370 AgentWithWorkspacesV3View.register(agent_api)
340371 AgentCreationView.register(agent_api)
372 AgentCreationV3View.register(agent_api)
341373 AgentView.register(agent_api)
374 AgentV3View.register(agent_api)
6565 {'token': faraday_server.agent_token})
6666
6767
68 class AgentAuthTokenV3View(AgentAuthTokenView):
69 route_prefix = '/v3'
70 trailing_slash = False
71
6872 AgentAuthTokenView.register(agent_auth_token_api)
73 AgentAuthTokenV3View.register(agent_auth_token_api)
6974
7075
7176 # 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
00 import logging
11 from datetime import datetime, timedelta
22 from typing import Type, Optional
3
34
45 import flask
56 import sqlalchemy
4445
4546 logger = logging.getLogger(__name__)
4647
48
4749 class VulnerabilitySchema(vulns.VulnerabilitySchema):
4850 class Meta(vulns.VulnerabilitySchema.Meta):
4951 extra_fields = ('run_date',)
275277 db.session.commit()
276278
277279
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
278299 def _create_service(ws, host, service_data, command=None):
279300 service_data = service_data.copy()
280301 vulns = service_data.pop('vulnerabilities')
281302 creds = service_data.pop('credentials')
282303 service_data['host'] = host
283304 (created, service) = get_or_create(ws, Service, service_data)
305
306 if not created:
307 service = _update_service(service, service_data)
284308 db.session.commit()
285309
286310 if command is not None:
338362 if created and run_date:
339363 logger.debug("Apply run date to vuln")
340364 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', {})
341369 db.session.commit()
342370
343371 if command is not None:
491519
492520 post.is_public = True
493521
522
523 class BulkCreateV3View(BulkCreateView):
524 route_prefix = '/v3/ws/<workspace_name>/'
525 trailing_slash = False
526
527
494528 BulkCreateView.register(bulk_create_api)
529 BulkCreateV3View.register(bulk_create_api)
88 from flask_classful import route
99 from marshmallow import fields, post_load, ValidationError
1010
11 from faraday.server.api.base import AutoSchema, ReadWriteWorkspacedView, PaginatedMixin
11 from faraday.server.api.base import AutoSchema, ReadWriteWorkspacedView, PaginatedMixin, PatchableWorkspacedMixin
1212 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
1414
1515 commandsrun_api = Blueprint('commandsrun_api', __name__)
1616
2424 allow_none=True)
2525 workspace = PrimaryKeyRelatedField('name', dump_only=True)
2626 creator = PrimaryKeyRelatedField('username', dump_only=True)
27 metadata = SelfNestedField(MetadataSchema())
2728
2829 def load_itime(self, value):
2930 try:
5556 class Meta:
5657 model = Command
5758 fields = ('_id', 'command', 'duration', 'itime', 'ip', 'hostname',
58 'params', 'user', 'creator', 'workspace', 'tool', 'import_source')
59 'params', 'user', 'creator', 'workspace', 'tool', 'import_source', 'metadata')
5960
6061
6162 class CommandView(PaginatedMixin, ReadWriteWorkspacedView):
137138 return flask.jsonify(command_obj)
138139
139140
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
140157 CommandView.register(commandsrun_api)
158 CommandV3View.register(commandsrun_api)
55 from marshmallow.validate import OneOf
66
77
8 from faraday.server.models import db, Host, Service
8 from faraday.server.models import db, Host, Service, VulnerabilityGeneric
99 from faraday.server.api.base import (
1010 AutoSchema,
1111 ReadWriteWorkspacedView,
12 InvalidUsage, CreateWorkspacedMixin, GenericWorkspacedView)
12 InvalidUsage, CreateWorkspacedMixin, GenericWorkspacedView, PatchableWorkspacedMixin)
1313 from faraday.server.models import Comment
1414 comment_api = Blueprint('comment_api', __name__)
1515
1616
1717 class CommentSchema(AutoSchema):
1818 _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)
2124
2225 class Meta:
2326 model = Comment
3235 model = {
3336 'host': Host,
3437 'service': Service,
38 'vulnerability': VulnerabilityGeneric,
3539 'comment': Comment
3640 }
3741 obj = db.session.query(model[data['object_type']]).get(
4953 model_class = Comment
5054 schema_class = CommentSchema
5155 order_field = 'create_date'
56
5257
5358 class UniqueCommentView(GenericWorkspacedView, CommentCreateMixing):
5459 """
7782 res = super(UniqueCommentView, self)._perform_create(data, workspace_name)
7883 return res
7984
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
8096 CommentView.register(comment_api)
8197 UniqueCommentView.register(comment_api)
82 # I'm Py3
98 CommentV3View.register(comment_api)
99 UniqueCommentV3View.register(comment_api)
99 AutoSchema,
1010 ReadWriteWorkspacedView,
1111 FilterSetMeta,
12 FilterAlchemyMixin, InvalidUsage)
12 FilterAlchemyMixin,
13 InvalidUsage,
14 PatchableWorkspacedMixin
15 )
1316 from faraday.server.models import Credential, Host, Service, Workspace, db
1417 from faraday.server.schemas import MutableField, SelfNestedField, MetadataSchema
1518
7780 parent_class = Service
7881 parent_field = 'service_id'
7982 not_parent_field = 'host_id'
83 elif 'partial' in kwargs and kwargs['partial']:
84 return data
8085 else:
8186 raise ValidationError(
8287 f'Unknown parent type: {parent_type}')
125130 }
126131
127132
133 class CredentialV3View(CredentialView, PatchableWorkspacedMixin):
134 route_prefix = '/v3/ws/<workspace_name>/'
135 trailing_slash = False
136
137
128138 CredentialView.register(credentials_api)
129 # I'm Py3
139 CredentialV3View.register(credentials_api)
11 # Copyright (C) 2018 Infobyte LLC (http://www.infobytesec.com/)
22 # See the file 'doc/LICENSE' for the license information
33 from flask import Blueprint
4 from marshmallow import fields
45
56 from faraday.server.models import CustomFieldsSchema
67 from faraday.server.api.base import (
78 AutoSchema,
89 ReadWriteView,
10 PatchableMixin,
911 )
1012
1113
1315
1416
1517 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)
1626
1727 class Meta:
1828 model = CustomFieldsSchema
3141 model_class = CustomFieldsSchema
3242 schema_class = CustomFieldsSchemaSchema
3343
34 def _update_object(self, obj, data):
44 def _update_object(self, obj, data, **kwargs):
3545 """
3646 Field name must be read only
3747 """
4050 data.pop(read_only_key)
4151 return super(CustomFieldsSchemaView, self)._update_object(obj, data)
4252
53
54 class CustomFieldsSchemaV3View(CustomFieldsSchemaView, PatchableMixin):
55 route_prefix = '/v3'
56 trailing_slash = False
57
58
4359 CustomFieldsSchemaView.register(custom_fields_schema_api)
44 # I'm Py3
60 CustomFieldsSchemaV3View.register(custom_fields_schema_api)
1313
1414 @export_data_api.route('/v2/ws/<workspace_name>/export_data', methods=['GET'])
1515 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
1626 workspace = Workspace.query.filter_by(name=workspace_name).first()
1727 if not workspace:
1828 logger.error("No such workspace. Please, specify a valid workspace.")
3444 else:
3545 logger.error("Invalid format. Please, specify a valid format.")
3646 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__
3755
3856
3957 def xml_metasploit_format(workspace):
1515 @exploits_api.route('/v2/vulners/exploits/<cveid>', methods=['GET'])
1616 def get_exploits(cveid):
1717 """
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
1925 """
2026
2127 logger.debug(
6672 abort(make_response(jsonify(message=f'Could not find {str(ex)}'), 400))
6773
6874 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)
2323 AutoSchema,
2424 FilterAlchemyMixin,
2525 FilterSetMeta,
26 FilterWorkspacedMixin)
26 FilterWorkspacedMixin,
27 PatchableWorkspacedMixin
28 )
2729 from faraday.server.schemas import (
2830 MetadataSchema,
2931 MutableField,
3739 host_api = Blueprint('host_api', __name__)
3840
3941 logger = logging.getLogger(__name__)
40
4142
4243
4344 class HostCountSchema(Schema):
258259 logger.error("Error parsing hosts CSV (%s)", e)
259260 abort(400, f"Error parsing hosts CSV ({e})")
260261
261
262262 @route('/<host_id>/services/')
263263 def service_list(self, workspace_name, host_id):
264264 """
355355 db.session.commit()
356356 return host
357357
358 def _update_object(self, obj, data):
358 def _update_object(self, obj, data, **kwargs):
359359 try:
360360 hostnames = data.pop('hostnames')
361361 except KeyError:
396396 })
397397 return {
398398 'rows': hosts,
399 'total_rows': (pagination_metadata and pagination_metadata.total
399 'count': (pagination_metadata and pagination_metadata.total
400400 or len(hosts)),
401401 }
402402
439439 return flask.jsonify(response)
440440
441441
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
442467 HostsView.register(host_api)
443 # I'm Py3
468 HostsV3View.register(host_api)
1212
1313 @info_api.route('/v2/info', methods=['GET'])
1414 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 """
1524
1625 response = flask.jsonify({'Faraday Server': 'Running', 'Version': f_version})
1726 response.status_code = 200
1928 return response
2029
2130
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
2239 @info_api.route('/config')
2340 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 """
2450 return flask.jsonify(gen_web_config())
51
2552
2653 get_config.is_public = True
2754 show_info.is_public = True
28 # I'm Py3
55 show_info_v3.is_public = True
77 from faraday.server.api.base import (
88 ReadWriteView,
99 AutoSchema,
10 PatchableMixin
1011 )
1112 from faraday.server.schemas import (
1213 StrictDateTimeField,
3536 schema_class = LicenseSchema
3637
3738
39 class LicenseV3View(LicenseView, PatchableMixin):
40 route_prefix = 'v3/'
41 trailing_slash = False
42
43
3844 LicenseView.register(license_api)
39 # I'm Py3
45 LicenseV3View.register(license_api)
00 from faraday.server.api.base import GenericView
11 from faraday.server.models import User, db
22 from flask import Blueprint, request, jsonify, g, abort
3 from marshmallow import Schema, fields
34
45 preferences_api = Blueprint('preferences_api', __name__)
6
7
8 class PreferenceSchema(Schema):
9 preferences = fields.Dict()
510
611
712 class PreferencesView(GenericView):
813 model_class = User
914 route_base = 'preferences'
15 schema_class = PreferenceSchema
1016
1117 def post(self):
18 """
19 ---
20 set:
21 tags: ["User"]
22 description: Set the user preferences
23 responses:
24 200:
25 description: Ok
26 """
1227 user = g.user
1328
1429 if request.json and 'preferences' not in request.json:
2237 return jsonify(''), 200
2338
2439 def get(self):
40 """
41 ---
42 get:
43 tags: ["User"]
44 description: Show the user preferences
45 responses:
46 200:
47 description: Ok
48 """
2549 return jsonify({'preferences': g.user.preferences}), 200
2650
51
52 class PreferencesV3View(PreferencesView):
53 route_prefix = '/v3'
54 trailing_slash = False
55
56
2757 PreferencesView.register(preferences_api)
58 PreferencesV3View.register(preferences_api)
77 from faraday.server.api.base import (
88 ReadWriteView,
99 AutoSchema,
10 PatchableMixin,
1011 )
1112
1213 searchfilter_api = Blueprint('searchfilter_api', __name__)
3233 return query.filter(SearchFilter.creator_id == g.user.id)
3334
3435
36 class SearchFilterV3View(SearchFilterView, PatchableMixin):
37 route_prefix = 'v3/'
38 trailing_slash = False
39
40
3541 SearchFilterView.register(searchfilter_api)
36 # I'm Py3
42 SearchFilterV3View.register(searchfilter_api)
77 from sqlalchemy.orm.exc import NoResultFound
88
99 from faraday.server.api.base import AutoSchema, ReadWriteWorkspacedView, FilterSetMeta, \
10 FilterAlchemyMixin
10 FilterAlchemyMixin, PatchableWorkspacedMixin
1111 from faraday.server.models import Host, Service, Workspace
1212 from faraday.server.schemas import (
1313 MetadataSchema,
133133 abort(make_response(jsonify(message="Invalid Port number"), 400))
134134 return super(ServiceView, self)._perform_create(data, **kwargs)
135135
136
137 class ServiceV3View(ServiceView, PatchableWorkspacedMixin):
138 route_prefix = '/v3/ws/<workspace_name>/'
139 trailing_slash = False
140
136141 ServiceView.register(services_api)
137 # I'm Py3
142 ServiceV3View.register(services_api)
1212
1313 @session_api.route('/session')
1414 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 """
1524 user = flask.g.user
1625 data = user.get_security_payload()
1726 data['csrf_token'] = generate_csrf()
0 import datetime
1 import logging
2
03 from itsdangerous import TimedJSONWebSignatureSerializer
1 from flask import Blueprint, g
4 from flask import Blueprint, g, request
25 from flask_security.utils import hash_data
36 from flask import current_app as app
4
7 from marshmallow import Schema
58
69 from faraday.server.config import faraday_server
710 from faraday.server.api.base import GenericView
811
912 token_api = Blueprint('token_api', __name__)
1013
14 audit_logger = logging.getLogger('audit')
15
16
17 class EmptySchema(Schema):
18 pass
19
1120
1221 class TokenAuthView(GenericView):
1322 route_base = 'token'
23 schema_class = EmptySchema
1424
1525 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 """
1635 user_id = g.user.id
1736 serializer = TimedJSONWebSignatureSerializer(
1837 app.config['SECRET_KEY'],
2039 expires_in=int(faraday_server.api_token_expiration)
2140 )
2241 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}]")
2345 return serializer.dumps({'user_id': user_id, "validation_check": hashed_data}).decode('utf-8')
2446
2547
48 class TokenAuthV3View(TokenAuthView):
49 route_prefix = '/v3'
50 trailing_slash = False
51
52
2653 TokenAuthView.register(token_api)
54 TokenAuthV3View.register(token_api)
3333 plugins_manager = PluginsManager(config.faraday_server.custom_plugins_folder)
3434 report_analyzer = ReportAnalyzer(plugins_manager)
3535
36
3637 @gzipped
3738 @upload_api.route('/v2/ws/<workspace>/upload_report', methods=['POST'])
3839 def file_upload(workspace=None):
3940 """
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
4156 """
4257 logger.info("Importing new plugin report in server...")
4358 # Authorization code copy-pasted from server/api/base.py
109124 )
110125 else:
111126 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)
2727 FilterSetMeta,
2828 PaginatedMixin,
2929 ReadWriteView,
30 FilterMixin)
30 FilterMixin,
31 PatchableMixin
32 )
3133
3234 from faraday.server.schemas import (
3335 PrimaryKeyRelatedField,
156158 }
157159
158160 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 """
159183 with lock:
160184 return super(VulnerabilityTemplateView, self).post(**kwargs)
161185
297321 return vulns_list
298322
299323
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
300335 VulnerabilityTemplateView.register(vulnerability_template_api)
336 VulnerabilityTemplateV3View.register(vulnerability_template_api)
3434 PaginatedMixin,
3535 ReadWriteWorkspacedView,
3636 InvalidUsage,
37 CountMultiWorkspacedMixin)
37 CountMultiWorkspacedMixin,
38 PatchableWorkspacedMixin
39 )
3840 from faraday.server.fields import FaradayUploadedFile
3941 from faraday.server.models import (
4042 db,
4648 Vulnerability,
4749 VulnerabilityWeb,
4850 CustomFieldsSchema,
49 VulnerabilityGeneric, User,
51 VulnerabilityGeneric,
52 User
5053 )
5154 from faraday.server.utils.database import get_or_create
5255 from faraday.server.utils.export import export_vulns_to_csv
454457 sort_model_class = VulnerabilityWeb # It has all the fields
455458 sort_pass_silently = True # For compatibility with the Web UI
456459 order_field = desc(VulnerabilityGeneric.confirmed), VulnerabilityGeneric.severity, VulnerabilityGeneric.create_date
460 get_joinedloads = [Vulnerability.evidence, Vulnerability.creator]
457461
458462 unique_fields_by_class = {
459463 'Vulnerability': [('name', 'description', 'host_id', 'service_id')],
510514 return obj
511515
512516 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(
514521 object_id=obj.id,
515522 object_type='vulnerability',
516523 )
528535 content=faraday_file,
529536 )
530537
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!
533540 data.pop('tool', '')
534541 return super(VulnerabilityView, self)._update_object(obj, data)
535542
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 {})
538545 obj = super(VulnerabilityView, self)._perform_update(object_id, obj, data, workspace_name)
539546 db.session.flush()
540 self._process_attachments(obj, attachments)
547 if attachments is not None:
548 self._process_attachments(obj, attachments)
541549 db.session.commit()
542550 return obj
543551
836844 if offset:
837845 vulns = vulns.offset(offset)
838846
839 vulns = self.schema_class_dict['VulnerabilityWeb'](**marshmallow_params).dumps(
847 vulns = self.schema_class_dict['VulnerabilityWeb'](**marshmallow_params).dump(
840848 vulns.all())
841 return json.loads(vulns), total_vulns.count()
849 return vulns, total_vulns.count()
842850 else:
843851 vulns = self._generate_filter_query(
844852 VulnerabilityGeneric,
10801088 return flask.jsonify(response)
10811089
10821090
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
10831127 VulnerabilityView.register(vulns_api)
1084
1085 # I'm Py3
1128 VulnerabilityV3View.register(vulns_api)
55 from flask import Blueprint
66 from flask import current_app as app
77 from itsdangerous import BadData, TimestampSigner
8 from marshmallow import Schema
89 from sqlalchemy.orm.exc import NoResultFound
910 from faraday.server.models import Agent
1011 from faraday.server.api.base import GenericWorkspacedView
1516 websocket_auth_api = Blueprint('websocket_auth_api', __name__)
1617
1718
19 class WebsocketWorkspaceAuthSchema(Schema):
20 pass
21
22
1823 class WebsocketWorkspaceAuthView(GenericWorkspacedView):
1924 route_base = 'websocket_token'
25 schema_class = WebsocketWorkspaceAuthSchema
2026
2127 def post(self, workspace_name):
28 """
29 ---
30 post:
31 tags: ["Token"]
32 responses:
33 200:
34 description: Ok
35 """
2236 workspace = self._get_workspace(workspace_name)
2337 signer = TimestampSigner(app.config['SECRET_KEY'], salt="websocket")
2438 token = signer.sign(str(workspace.id)).decode('utf-8')
2539 return {"token": token}
2640
2741
42 class WebsocketWorkspaceAuthV3View(WebsocketWorkspaceAuthView):
43 route_prefix = "/v3/ws/<workspace_name>/"
44 trailing_slash = False
45
46
2847 WebsocketWorkspaceAuthView.register(websocket_auth_api)
48 WebsocketWorkspaceAuthV3View.register(websocket_auth_api)
2949
3050
3151 @websocket_auth_api.route('/v2/agent_websocket_token/', methods=['POST'])
3252 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 """
3362 agent = require_agent_token()
3463 return flask.jsonify({"token": generate_agent_websocket_token(agent)})
3564
3665
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
3774 agent_websocket_token.is_public = True
75 agent_websocket_token_w3.is_public = True
3876
3977
4078 def generate_agent_websocket_token(agent):
77115 except NoResultFound:
78116 flask.abort(403)
79117 return agent
80
81 # I'm Py3
2323 PrimaryKeyRelatedField,
2424 SelfNestedField,
2525 )
26 from faraday.server.api.base import ReadWriteView, AutoSchema, FilterMixin
26 from faraday.server.api.base import ReadWriteView, AutoSchema, FilterMixin, PatchableMixin
2727
2828 logger = logging.getLogger(__name__)
2929
7676 PrimaryKeyRelatedField('name', many=True, dump_only=True),
7777 fields.List(fields.String)
7878 )
79 active = fields.Boolean(dump_only=True)
79 active = fields.Boolean()
8080
8181 create_date = fields.DateTime(attribute='create_date',
8282 dump_only=True)
268268 db.session.commit()
269269 return workspace
270270
271 def _update_object(self, obj, data):
271 def _update_object(self, obj, data, **kwargs):
272272 scope = data.pop('scope', [])
273273 obj.set_scope(scope)
274274 return super(WorkspaceView, self)._update_object(obj, data)
338338 return self._get_object(workspace_id).readonly
339339
340340
341 class WorkspaceV3View(WorkspaceView, PatchableMixin):
342 route_prefix = 'v3/'
343 trailing_slash = False
344
345
341346 WorkspaceView.register(workspace_api)
342 # I'm Py3
347 WorkspaceV3View.register(workspace_api)
2626 get_message,
2727 verify_and_update_password,
2828 verify_hash)
29
2930 from flask_kvsession import KVSessionExtension
3031 from simplekv.fs import FilesystemStore
3132 from simplekv.decorator import PrefixDecorator
4243
4344
4445 logger = logging.getLogger(__name__)
46 audit_logger = logging.getLogger('audit')
4547
4648
4749 def setup_storage_path():
8890 from faraday.server.api.modules.search_filter import searchfilter_api # pylint:disable=import-outside-toplevel
8991 from faraday.server.api.modules.preferences import preferences_api # pylint:disable=import-outside-toplevel
9092 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
9195
9296 app.register_blueprint(commandsrun_api)
9397 app.register_blueprint(activityfeed_api)
113117 app.register_blueprint(searchfilter_api)
114118 app.register_blueprint(preferences_api)
115119 app.register_blueprint(export_data_api)
120 app.register_blueprint(auth)
116121
117122
118123 def check_testing_configuration(testing, app):
252257 session.destroy()
253258 KVSessionExtension(app=app).cleanup_sessions(app)
254259
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
255264
256265 def user_logged_in_succesfull(app, user):
257266 user_agent = request.headers.get('User-Agent')
268277 logger.debug("Cleanup sessions")
269278 KVSessionExtension(app=app).cleanup_sessions(app)
270279
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
271285 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)
273305
274306 try:
275307 secret_key = faraday.server.config.faraday_server.secret_key
291323 login_failed_message = ("Invalid username or password", 'error')
292324
293325 app.config.update({
326 'SECURITY_BACKWARDS_COMPAT_AUTH_TOKEN': True,
294327 'SECURITY_PASSWORD_SINGLE_HASH': True,
295328 'WTF_CSRF_ENABLED': False,
296329 'SECURITY_USER_IDENTITY_ATTRIBUTES': ['username'],
297330 'SECURITY_POST_LOGIN_VIEW': '/_api/session',
298 'SECURITY_POST_LOGOUT_VIEW': '/_api/login',
331 'SECURITY_POST_LOGOUT_VIEW': '/_api/logout',
299332 '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]",
300338 'SECURITY_CHANGEABLE': True,
301339 'SECURITY_SEND_PASSWORD_CHANGE_EMAIL': False,
302340 'SECURITY_MSG_USER_DOES_NOT_EXIST': login_failed_message,
303341 'SECURITY_TOKEN_AUTHENTICATION_HEADER': 'Authorization',
342
304343
305344 # The line bellow should not be necessary because of the
306345 # CustomLoginForm, but i'll include it anyway.
370409
371410 app.view_functions['security.login'].is_public = True
372411 app.view_functions['security.logout'].is_public = True
373
374412 app.debug = faraday.server.config.is_debug_mode()
375413 minify_json_output(app)
376414
381419 register_handlers(app)
382420
383421 app.view_functions['agent_api.AgentCreationView:post'].is_public = True
422 app.view_functions['agent_api.AgentCreationV3View:post'].is_public = True
384423
385424 return app
386425
33 See the file 'doc/LICENSE' for the license information
44
55 """
6 import yaml
67 from apispec import APISpec
78 from apispec.ext.marshmallow import MarshmallowPlugin
89 from apispec_webframeworks.flask import FlaskPlugin
910 from faraday.server.web import app
10 from faraday import __version__ as f_version
1111 import json
1212
1313 from faraday.utils.faraday_openapi_plugin import FaradayAPIPlugin
1414
1515
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):
1717 extra_specs = {'info': {
1818 'description': 'The Faraday REST API enables you to interact with '
1919 '[our server](https://github.com/infobyte/faraday).\n'
4646 }
4747 spec.components.response("UnauthorizedError", response_401_unauthorized)
4848
49 tags = set()
50
4951 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
5268 if format.lower() == "yaml":
5369 print(spec.to_yaml())
5470 else:
183183 self.ssl = False
184184 self.certfile = None
185185 self.keyfile = None
186 self.enabled = False
187
188 def is_enabled(self):
189 return self.enabled is True
190
186191
187192 class StorageConfigObject(ConfigSection):
188193 def __init__(self):
7373 'executive_report',
7474 'workspace',
7575 'task',
76 ]
77
78 COMMENT_TYPES = [
79 'system',
80 'user'
7681 ]
7782
7883
12381243 class VulnerabilityWeb(VulnerabilityGeneric):
12391244 __tablename__ = None
12401245
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
12411255 @declared_attr
12421256 def service_id(cls):
12431257 return VulnerabilityGeneric.__table__.c.get(
19952009 class Comment(Metadata):
19962010 __tablename__ = 'comment'
19972011 id = Column(Integer, primary_key=True)
2012 comment_type = Column(Enum(*COMMENT_TYPES, name='comment_types'), nullable=False, default='user')
19982013
19992014 text = BlankColumn(Text)
20002015
66 import json
77 import time
88 import datetime
9 import logging
910 from flask import g
1011 from marshmallow import fields, Schema, post_dump, EXCLUDE
1112 from marshmallow.utils import missing as missing_
1718 VulnerabilityABC,
1819 CustomFieldsSchema,
1920 )
21
22 logger = logging.getLogger(__name__)
2023
2124
2225 class JSTimestampField(fields.Integer):
7275 field_name=key,
7376 ).first()
7477 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
7682 if field_schema.field_type == 'str':
7783 serialized[key] = str(raw_data)
7884 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 %}
4242 writer = csv.DictWriter(buffer, fieldnames=headers)
4343 writer.writeheader()
4444
45 hosts_data = {}
46 services_data = {}
45 comments_dict = dict()
46 hosts_ids = set()
47 services_ids = set()
48 vulns_ids = set()
49
4750 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)
4974 if vuln['parent_type'] == 'Host':
5075 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]
5677 row = {**vuln_data, **host_data}
5778 elif vuln['parent_type'] == 'Service':
5879 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]
6481 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]
7083 row = {**vuln_data, **host_data, **service_data}
7184
7285 writer.writerow(row)
7790 return memory_file
7891
7992
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 []
127145 vuln_date = vuln['metadata']['create_time']
128146 if vuln['service']:
129147 service_fields = ["status", "protocol", "name", "summary", "version", "ports"]
145163 "severity": vuln.get('severity', None),
146164 "service": vuln_service,
147165 "target": vuln.get('target', None),
148 "desc": vuln_description,
166 "desc": vuln.get('description', None),
149167 "status": vuln.get('status', None),
150168 "hostnames": vuln_hostnames,
151169 "comments": comments_list,
44 import logging.handlers
55 import faraday.server.config
66 import errno
7 import os
78
89 from syslog_rfc5424_formatter import RFC5424Formatter
910 from faraday.server.config import CONST_FARADAY_HOME_PATH
1011
1112 LOG_FILE = CONST_FARADAY_HOME_PATH / 'logs' / 'faraday-server.log'
13 AUDIT_LOG_FILE = CONST_FARADAY_HOME_PATH / 'logs' / 'audit.log'
1214
1315 MAX_LOG_FILE_SIZE = 5 * 1024 * 1024 # 5 MB
1416 MAX_LOG_FILE_BACKUP_COUNT = 5
2527 if faraday.server.config.logger_config.use_rfc5424_formatter:
2628 formatter = RFC5424Formatter()
2729 else:
28
2930 formatter = logging.Formatter(LOG_FORMAT, LOG_DATE_FORMAT)
3031 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')
3236
3337
3438 def setup_console_logging(formatter):
3943 LVL_SETTABLE_HANDLERS.append(console_handler)
4044
4145
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)
4448 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)
4650 file_handler.setFormatter(formatter)
4751 file_handler.setLevel(faraday.server.config.LOGGING_LEVEL)
48 add_handler(file_handler)
52 add_handler(file_handler, log_name)
4953 LVL_SETTABLE_HANDLERS.append(file_handler)
5054
5155
52 def add_handler(handler):
53 logger = logging.getLogger()
56 def add_handler(handler, log_name=None):
57 logger = logging.getLogger(log_name)
5458 logger.addHandler(handler)
59 logger.propagate = False
5560 LOGGING_HANDLERS.append(handler)
5661
5762
6166 handler.setLevel(level)
6267
6368
64 def create_logging_path():
69 def create_logging_path(path_file):
6570 try:
66 LOG_FILE.parent.mkdir(parents=True)
71 path_file.parent.mkdir(parents=True)
6772 except OSError as e:
6873 if e.errno != errno.EEXIST:
6974 raise
75
7076
7177 setup_logging()
7278
1717 listenWS
1818 )
1919
20 from flask_mail import Mail
21
2022 from OpenSSL.SSL import Error as SSLError
2123
2224 import faraday.server.config
2325
24 from faraday.server.config import CONST_FARADAY_HOME_PATH
26 from faraday.server.config import CONST_FARADAY_HOME_PATH, smtp
2527 from faraday.server.utils import logger
2628 from faraday.server.threads.reports_processor import ReportsManager, REPORTS_QUEUE
2729 from faraday.server.threads.ping_home import PingHomeThread
3335
3436
3537 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)
3645 logger = logging.getLogger(__name__)
3746
3847
132141 factory.protocol = BroadcastServerProtocol
133142 return factory
134143
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
135149 def install_signal(self):
136150 for sig in (SIGABRT, SIGILL, SIGINT, SIGSEGV, SIGTERM):
137151 signal(sig, SIG_DFL)
140154 def signal_handler(*args):
141155 logger.info('Received SIGTERM, shutting down.')
142156 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()
147158
148159 log_path = CONST_FARADAY_HOME_PATH / 'logs' / 'access-logging.log'
149160 site = twisted.web.server.Site(self.__root_resource,
199210
200211 except error.CannotListenError as e:
201212 logger.error(e)
213 self.__stop_all_threads()
202214 sys.exit(1)
203
204215
205216 except Exception as e:
206217 logger.exception('Something went wrong when trying to setup the Web UI')
218 logger.exception(e)
219 self.__stop_all_threads()
207220 sys.exit(1)
208221 # I'm Py3
14101410 white-space: nowrap;
14111411 text-overflow: ellipsis;
14121412 overflow: hidden;
1413 }
1413 }
1414
1415 .forgot-password{
1416 margin-top: 10px;
1417 text-align: center;
1418 padding-bottom: 10px;
1419 }
173173 <script type="text/javascript" src="scripts/workspaces/providers/workspaces.js"></script>
174174 <script type="text/javascript" src="scripts/auth/controllers/login.js"></script>
175175 <script type="text/javascript" src="scripts/auth/controllers/resetPassword.js"></script>
176 <script type="text/javascript" src="scripts/auth/controllers/forgotPassword.js"></script>
176177 <script type="text/javascript" src="scripts/auth/directives/compareTo.js"></script>
177178 <script type="text/javascript" src="scripts/auth/services/account.js"></script>
178179 <script type="text/javascript" src="scripts/auth/services/login.js"></script>
102102 if ($scope.selected_cf.field_order === null)
103103 $scope.selected_cf.field_order = getMaxOrder() + 1;
104104
105 if($scope.selected_cf.field_metadata.length === 0){
105 if(!$scope.selected_cf.field_metadata || $scope.selected_cf.field_metadata.length === 0){
106106 $scope.selected_cf.field_metadata = null;
107107 }
108108
118118
119119
120120 $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){
122122 $scope.selected_cf.field_metadata = null;
123123 }
124124
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 }]);
22 // See the file 'doc/LICENSE' for the license information
33
44 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) {
77
88 $scope.data = {
99 "user": null,
4141 if(auth) $location.path('/');
4242 });
4343 });
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
4457
4558 }]);
4659
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>
2929 <input id="remember" type="checkbox" ng-model="data.remember" ng-change="checkResetError()">
3030 </label>
3131 </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>
3333 </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>
3636 </div>
3737 <div id="footer-login">
3838 <p class="normal-size fg-white">Faraday Community - <a class="fg-white" href="https://www.faradaysec.com">faradaysec.com</a></p>
9191 error: callback
9292 });
9393 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;
94114 }
95115 }
96116
6767 host = new Host(host_data.value);
6868 result.hosts.push(host);
6969 });
70 result.total = response.data.total_rows;
70 result.total = response.data.count;
7171 deferred.resolve(result);
7272 }, function(response) {
7373 deferred.reject();
124124 class_model = view_instance.model_class.__name__
125125 else:
126126 class_model = 'No name'
127 #print(f'{view_name} / {class_model}')
127128 logger.debug(f'{view_name} / {class_model} / {rule.methods} / {view_name} / {view_instance._get_schema_class().__name__}')
128129 operations[view_name] = yaml_utils.load_yaml_from_docstring(
129130 view.__doc__.format(schema_class=view_instance._get_schema_class().__name__, class_model=class_model, tag_name=class_model)
9696 ./packages/flask-kvsession-fork
9797 { };
9898
99 flask-security =
99 flask-security-too =
100100 self.callPackage
101 ./packages/flask-security
101 ./packages/flask-security-too
102102 { };
103103
104104 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 }
2020 , flask
2121 , flask-classful
2222 , flask-kvsession-fork
23 , flask-security
23 , flask-security-too
2424 , flask_login
2525 , flask_sqlalchemy
2626 , hypothesis
5858 pname =
5959 "faradaysec";
6060 version =
61 "3.14.0";
61 "3.14.1";
6262
6363 src =
6464 lib.cleanSource
8282 email_validator
8383 wtforms
8484 flask_login
85 flask-security
85 flask-security-too
8686 marshmallow
8787 pillow
8888 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 }
99 email_validator
1010 WTForms>=2.1
1111 flask-login>=0.5.0
12 Flask-Security>=3.0.0
12 Flask-Security-Too>=3.4.4,<4.0.0
1313 marshmallow>=3.0.0
1414 Pillow>=4.2.1
1515 psycopg2
00 from pathlib import Path
11 import os
22 import requests
3 import click
34
45
56 VERSION = os.environ.get('FARADAY_VERSION')
7 TOKEN = os.environ.get('GH_TOKEN')
68
79
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):
913 release_data = dict()
1014 release_data["tag_name"] = f"v{VERSION}"
1115 release_data["name"] = f"v{VERSION}"
1418 ) as body_file:
1519 release_data["body"] = body_file.read()
1620
17 headers = {'Accept': 'application/vnd.github.v3+json'}
21 headers = {
22 'Accept': 'application/vnd.github.v3+json',
23 'Authorization': 'token ' + TOKEN,
24 }
1825 res = requests.post(
1926 "https://api.github.com/repos/infobyte/faraday/releases",
2027 json=release_data,
2229 )
2330 res.raise_for_status()
2431 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()
4147
4248
4349 if __name__ == '__main__':
1111 import datetime
1212 import itertools
1313 import unicodedata
14 import time
1415
1516 import pytz
1617 from factory import SubFactory
111112
112113 @classmethod
113114 def build_dict(cls, **kwargs):
114 ret = super(WorkspaceObjectFactory, cls).build_dict(**kwargs)
115 ret = super().build_dict(**kwargs)
115116 del ret['workspace'] # It is passed in the URL, not in POST data
116117 return ret
117118
118119
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
119132 class HostFactory(WorkspaceObjectFactory):
133 id = FuzzyIncrementalInteger(1, 65535)
120134 ip = FuzzyText()
121135 description = FuzzyText()
122136 os = FuzzyChoice(['Linux', 'Windows', 'OSX', 'Android', 'iOS'])
158172 class Meta:
159173 model = ReferenceTemplate
160174 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)
173175
174176
175177 class ServiceFactory(WorkspaceObjectFactory):
185187 model = Service
186188 sqlalchemy_session = db.session
187189
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
188199
189200 class SourceCodeFactory(WorkspaceObjectFactory):
190201 filename = FuzzyText()
194205 sqlalchemy_session = db.session
195206
196207
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()
198215
199216 class Meta:
200217 model = CustomFieldsSchema
208225 severity = FuzzyChoice(['critical', 'high'])
209226
210227
211 class HasParentHostOrService:
228 class HasParentHostOrService(WorkspaceObjectFactory):
212229 """
213230 Mixins for objects that must have either a host or a service,
214231 but ont both, as a parent.
226243 raise ValueError('You should pass both service and host and '
227244 'set one of them to None to prevent random '
228245 'stuff to happen')
229 return super(HasParentHostOrService, cls).attributes(create, extra)
246 return super().attributes(create, extra)
230247
231248 @classmethod
232249 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)
235251 if isinstance(obj, dict):
236252 # This happens when built with build_dict
237253 if obj['host'] and obj['service']:
247263 obj.host = None
248264 else:
249265 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)
254273 service = ret.pop('service')
255274 host = ret.pop('host')
256275 if host is not None:
281300 return ret
282301
283302
284 class VulnerabilityFactory(HasParentHostOrService,
285 VulnerabilityGenericFactory):
303 class VulnerabilityFactory(VulnerabilityGenericFactory,
304 HasParentHostOrService):
286305
287306 host = factory.SubFactory(HostFactory, workspace=factory.SelfAttribute('..workspace'))
288307 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
289317
290318 class Meta:
291319 model = Vulnerability
296324 method = FuzzyChoice(['GET', 'POST', 'PUT', 'PATCH', 'DELETE'])
297325 parameter_name = FuzzyText()
298326 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
299336
300337 class Meta:
301338 model = VulnerabilityWeb
322359 class Meta:
323360 model = VulnerabilityTemplate
324361 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
325369
326370
327371 class CredentialFactory(HasParentHostOrService, WorkspaceObjectFactory):
381425 workspace=self.workspace
382426 )
383427
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
384438
385439 class EmptyCommandFactory(WorkspaceObjectFactory):
386440 """
405459 A command without command objects.
406460 """
407461 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
411479
412480 class Meta:
413481 model = Comment
454522 class AgentFactory(FaradayFactory):
455523 name = FuzzyText()
456524 active = True
525 id = FuzzyIncrementalInteger(1, 10000)
457526
458527 @factory.post_generation
459528 def workspaces(self, create, extracted, **kwargs):
460529 if not create:
461530 # 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:
465540 # A list of groups were passed in, use them
466541 for workspace in extracted:
467542 self.workspaces.append(workspace)
469544 self.workspaces.append(WorkspaceFactory())
470545 self.workspaces.append(WorkspaceFactory())
471546
547 @classmethod
548 def build_dict(cls, **kwargs):
549 return super(AgentFactory, cls).build_dict(**kwargs)
472550
473551 class Meta:
474552 model = Agent
479557 name = FuzzyText()
480558 agent = factory.SubFactory(AgentFactory)
481559 parameters_metadata = factory.LazyAttribute(
482 lambda e: str({"param_name": False})
560 lambda e: {"param_name": False}
483561 )
484562 class Meta:
485563 model = Executor
497575 lambda agent_execution: agent_execution.executor.agent.workspaces[0]
498576 )
499577 command = factory.SubFactory(
500 CommandFactory,
578 EmptyCommandFactory,
501579 workspace=factory.SelfAttribute("..workspace"),
502580 end_date=None
503581 )
167167
168168 def test_create_workspace_and_vuln_with_childs(
169169 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
172172 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'})
175174
176175 def test_create_vuln_with_childs(self, session, vulnerability_factory):
177176 vuln = vulnerability_factory.build()
251250 session.commit()
252251 assert vulnerability.creator_command_id == command.id
253252 assert vulnerability.creator_command_tool == command.tool
254 # I'm Py3
253 # I'm Py3
1111 EmptyCommandFactory,
1212 HostFactory,
1313 CommandObjectFactory)
14 from tests.utils.url import v2_to_v3
15
1416
1517 @pytest.mark.usefixtures('logged_user')
16 class TestActivityFeed():
18 class TestActivityFeed:
19
20 def check_url(self, url):
21 return url
1722
1823 @pytest.mark.usefixtures('ignore_nplusone')
1924 def test_activity_feed(self, test_client, session):
2429 session.commit()
2530
2631 res = test_client.get(
27 f'/v2/ws/{ws.name}/activities/'
32 self.check_url(f'/v2/ws/{ws.name}/activities/')
2833 )
2934
3035 assert res.status_code == 200
5156 }
5257
5358 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}/'),
5560 data=data,
5661 )
5762 assert res.status_code == 200
130135 workspace=workspace
131136 )
132137 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/'))
134139 assert res.status_code == 200
135140 assert res.json['activities'][0]['vulnerabilities_count'] == 8
136141 assert res.json['activities'][0]['criticalIssue'] == 1
139144 assert res.json['activities'][0]['lowIssue'] == 1
140145 assert res.json['activities'][0]['infoIssue'] == 2
141146 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)
44 """
55
66 from unittest import mock
7 from posixpath import join as urljoin
78 import pytest
89
910 from faraday.server.api.modules.agent import AgentWithWorkspacesView, AgentView
1011 from faraday.server.models import Agent, Command
1112 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
1415 from tests import factories
1516 from tests.test_api_workspaced_base import API_PREFIX
17 from tests.utils.url import v2_to_v3
1618
1719
1820 def http_req(method, client, endpoint, json_dict, expected_status_codes, follow_redirects=False):
5254
5355
5456 @pytest.mark.usefixtures('logged_user')
55 class TestAgentAuthTokenAPIGeneric():
57 class TestAgentAuthTokenAPIGeneric:
58
59 def check_url(self, url):
60 return url
5661
5762 @mock.patch('faraday.server.api.modules.agent.faraday_server')
5863 def test_create_agent_token(self, faraday_server_config, test_client, session):
5964 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/'))
6166 assert 'token' in res.json
6267 assert len(res.json['token'])
6368
6469 @mock.patch('faraday.server.api.modules.agent.faraday_server')
6570 def test_create_agent_token_without_csrf_fails(self, faraday_server_config, test_client, session):
6671 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/'))
6873 assert res.status_code == 403
6974
7075 @mock.patch('faraday.server.api.modules.agent.faraday_server')
7176 def test_create_new_agent_token(self, faraday_server_config, test_client, session, csrf_token):
7277 faraday_server_config.agent_token = None
7378 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/'),
7580 data={"csrf_token": csrf_token},
7681 headers=headers,
7782 use_json_data=False)
7984 assert len(res.json['token'])
8085
8186
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
8397
8498 @mock.patch('faraday.server.api.modules.agent.faraday_server')
8599 @pytest.mark.usefixtures('ignore_nplusone')
99113 workspaces=[workspace, other_workspace]
100114 )
101115 # /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)
103117 assert res.status_code == 201, (res.json, raw_data)
104118 assert len(session.query(Agent).all()) == initial_agent_count + 1
105119 assert workspace.name in res.json['workspaces']
126140 )
127141 # /v2/agent_registration/
128142 res = test_client.post(
129 '/v2/agent_registration/',
143 self.check_url('/v2/agent_registration/'),
130144 data=raw_data
131145 )
132146 assert res.status_code == 400
146160 workspaces=[workspace]
147161 )
148162 # /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)
150164 assert res.status_code == 401
151165
152166 @mock.patch('faraday.server.api.modules.agent.faraday_server')
162176 workspaces=[workspace],
163177 )
164178 # /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)
166180 assert res.status_code == 400
167181
168182 @mock.patch('faraday.server.api.modules.agent.faraday_server')
172186 logout(test_client, [302])
173187 raw_data = {"PEPE": 'INVALID'}
174188 # /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)
176190 assert res.status_code == 400
177191
178192 @mock.patch('faraday.server.api.modules.agent.faraday_server')
189203 workspaces=[]
190204 )
191205 # /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)
193207 assert res.status_code == 400
194208
195209 @mock.patch('faraday.server.api.modules.agent.faraday_server')
207221 )
208222 raw_data["workspaces"] = ["donotexist"]
209223 # /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)
211225 assert res.status_code == 404
212226
213227 @mock.patch('faraday.server.api.modules.agent.faraday_server')
223237 token='sarasa'
224238 )
225239 # /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):
231250 model = Agent
232251 factory = factories.AgentFactory
233252 view_class = AgentWithWorkspacesView
234253 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]
235265
236266 def workspaced_url(self, workspace, obj= None):
237267 url = API_PREFIX + workspace.name + '/' + self.api_endpoint + '/'
392422 )
393423 assert res.status_code == 404
394424
425
426 class TestAgentWithWorkspacesAPIGenericV3(TestAgentWithWorkspacesAPIGeneric, PatchableTestsMixin):
427 def url(self, obj=None):
428 return v2_to_v3(super(TestAgentWithWorkspacesAPIGenericV3, self).url(obj))
429
430
395431 class TestAgentAPI(ReadOnlyMultiWorkspacedAPITests):
396432 model = Agent
397433 factory = factories.AgentFactory
398434 view_class = AgentView
399435 api_endpoint = 'agents'
436
437 def check_url(self, url):
438 return url
400439
401440 def test_get_workspaced(self, test_client, session):
402441 workspace = WorkspaceFactory.create()
444483 'csrf_token': csrf_token
445484 }
446485 res = test_client.post(
447 self.url(agent) + 'run/',
486 self.check_url(urljoin(self.url(agent), 'run/')),
448487 json=payload
449488 )
450489 assert res.status_code == 400
454493 session.add(agent)
455494 session.commit()
456495 res = test_client.post(
457 self.url(agent) + 'run/',
496 self.check_url(urljoin(self.url(agent), 'run/')),
458497 data='[" broken]"{'
459498 )
460499 assert res.status_code == 400
476515 ('content-type', 'text/html'),
477516 ]
478517 res = test_client.post(
479 self.url(agent) + 'run/',
518 self.check_url(urljoin(self.url(agent), 'run/')),
480519 data=payload,
481520 headers=headers)
482521 assert res.status_code == 400
495534 },
496535 }
497536 res = test_client.post(
498 self.url(agent) + 'run/',
537 self.check_url(urljoin(self.url(agent), 'run/')),
499538 json=payload
500539 )
501540 assert res.status_code == 400
516555 },
517556 }
518557 res = test_client.post(
519 self.url(agent.id) + 'run/',
558 self.check_url(urljoin(self.url(agent), 'run/')),
520559 json=payload
521560 )
522561 assert res.status_code == 200
534573 'executorData': '[][dassa',
535574 }
536575 res = test_client.post(
537 self.url(agent) + 'run/',
576 self.check_url(urljoin(self.url(agent), 'run/')),
538577 json=payload
539578 )
540579 assert res.status_code == 400
548587 'executorData': '',
549588 }
550589 res = test_client.post(
551 self.url(agent) + 'run/',
590 self.check_url(urljoin(self.url(agent), 'run/')),
552591 json=payload
553592 )
554593 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)
00 from datetime import datetime, timedelta, timezone
1 import string
12
23 import pytest
34 from marshmallow import ValidationError
1516 )
1617 from faraday.server.api.modules import bulk_create as bc
1718 from tests.factories import CustomFieldsSchemaFactory
19 from tests.utils.url import v2_to_v3
1820
1921 host_data = {
2022 "ip": "127.0.0.1",
4042 'refs': ['CVE-1234'],
4143 'tool': 'some_tool',
4244 'data': 'test data',
45 'custom_fields': {}
4346 }
4447
4548 vuln_web_data = {
567570 ).one()
568571
569572
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>'
575602 host_data_ = host_data.copy()
576603 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_]
578610 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_])
584616 )
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]))
666705 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:
706706 assert count(Host, workspace) == 0
707707
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
713721 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/')
715732 res = test_client.post(
716733 url,
717734 data=dict(hosts=[host_data]),
718735 headers=[("authorization", f"agent {agent.token}")]
719736 )
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)
743744 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/")
868747 res = test_client.post(
869748 url,
870749 data=dict(hosts=[host_data]),
871750 headers=[("authorization", f"agent {agent.token}")]
872751 )
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):
880758 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/')
885937 res = test_client.post(
886938 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")]
889942 )
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)
44 See the file 'doc/LICENSE' for the license information
55
66 '''
7 from tests.utils.url import v2_to_v3
78
89 """Tests for many API endpoints that do not depend on workspace_name"""
10 from posixpath import join as urljoin
911 import datetime
1012 import pytest
1113 import time
1214
1315 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
1517 from faraday.server.models import (
1618 Command,
1719 Workspace,
1820 Vulnerability)
19 from faraday.server.api.modules.commandsrun import CommandView
21 from faraday.server.api.modules.commandsrun import CommandView, CommandV3View
2022 from faraday.server.api.modules.workspaces import WorkspaceView
2123 from tests.factories import VulnerabilityFactory, EmptyCommandFactory, CommandObjectFactory, HostFactory, \
2224 WorkspaceFactory, ServiceFactory
2931 # and https://github.com/pytest-dev/pytest/issues/568 for more information
3032
3133 @pytest.mark.usefixtures('logged_user')
32 class TestListCommandView(ReadOnlyAPITests):
34 class TestListCommandView(ReadWriteAPITests):
3335 model = Command
3436 factory = factories.CommandFactory
3537 api_endpoint = 'commands'
3638 view_class = CommandView
39 patchable_fields = ["ip"]
40
41 def check_url(self, url):
42 return url
3743
3844 @pytest.mark.usefixtures('ignore_nplusone')
3945 @pytest.mark.usefixtures('mock_envelope_list')
6470 u'tool',
6571 u'import_source',
6672 u'creator',
73 u'metadata'
6774 ]
6875 assert command['value']['workspace'] == self.workspace.name
6976 assert set(object_properties) == set(command['value'].keys())
9097 workspace=command.workspace
9198 )
9299 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/')))
94102 assert res.status_code == 200
95103
96104 assert list(filter(lambda stats: stats['_id'] == command.id, res.json)) == [
147155 workspace=workspace
148156 )
149157 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/')))
151159 assert res.status_code == 200
152160 assert res.json == [
153161 {u'_id': command.id,
196204 workspace=workspace
197205 )
198206 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/')))
200208 assert res.status_code == 200
201209 assert res.json == [{
202210 u'_id': command.id,
262270 workspace=workspace
263271 )
264272 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/')))
266274 assert res.status_code == 200
267275 raw_first_command = list(filter(lambda comm: comm['_id'] == commands[0].id, res.json))
268276
308316 u'vulnerabilities_count': 1,
309317 u'criticalIssue': 0}
310318
319 @pytest.mark.usefixtures('ignore_nplusone')
311320 def test_sub_second_command_returns_correct_duration_value(self, test_client):
312321 command = self.factory(
313322 start_date=datetime.datetime(2017, 11, 14, 12, 29, 21, 248433),
317326 assert res.status_code == 200
318327 assert res.json['commands'][0]['value']['duration'] == 0.442406
319328
329 @pytest.mark.usefixtures('ignore_nplusone')
320330 def test_more_than_one_second_command_returns_correct_duration_value(self, test_client):
321331 command = self.factory(
322332 start_date=datetime.datetime(2017, 11, 14, 12, 29, 20, 248433),
326336 assert res.status_code == 200
327337 assert res.json['commands'][0]['value']['duration'] == 1.442406
328338
339 @pytest.mark.usefixtures('ignore_nplusone')
329340 def test_more_than_one_minute_command_returns_correct_duration_value(self, test_client):
330341 command = self.factory(
331342 start_date=datetime.datetime(2017, 11, 14, 12, 28, 20, 248433),
335346 assert res.status_code == 200
336347 assert res.json['commands'][0]['value']['duration'] == 61.442406
337348
349 @pytest.mark.usefixtures('ignore_nplusone')
338350 def test_more_than_one_day_none_end_date_command_returns_msg(self, test_client):
339351 command = self.factory(
340352 start_date=datetime.datetime(2017, 11, 14, 12, 28, 20, 0),
344356 assert res.status_code == 200
345357 assert res.json['commands'][0]['value']['duration'].lower() == "timeout"
346358
359 @pytest.mark.usefixtures('ignore_nplusone')
347360 def test_less_than_one_day_none_end_date_command_returns_msg(self, test_client):
348361 command = self.factory(
349362 start_date=datetime.datetime.now(),
404417 )
405418 session.commit()
406419
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}/'))
411424 assert res.status_code == 204
412425
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/')))
414427 assert res.status_code == 200
415428 command_history = list(filter(lambda hist: hist['_id'] == command.id, res.json))
416429 assert len(command_history)
434447
435448 assert res.status_code == 400
436449
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)
44
55 '''
66
7 from faraday.server.api.modules.comments import CommentView
7 from faraday.server.api.modules.comments import CommentView, CommentV3View
88 from faraday.server.models import Comment
99 from tests.factories import ServiceFactory
10 from tests.test_api_workspaced_base import ReadOnlyAPITests
10 from tests.test_api_workspaced_base import ReadWriteAPITests, PatchableTestsMixin
1111 from tests import factories
12 from tests.utils.url import v2_to_v3
1213
1314
14 class TestCredentialsAPIGeneric(ReadOnlyAPITests):
15 class TestCommentAPIGeneric(ReadWriteAPITests):
1516 model = Comment
1617 factory = factories.CommentFactory
1718 view_class = CommentView
1819 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
2025
2126 def _create_raw_comment(self, object_type, object_id):
2227 return {
8691 assert res.status_code == 201
8792 assert len(session.query(Comment).all()) == initial_comment_count + 1
8893
89 url = self.url(workspace=self.workspace).strip('/') + '_unique/'
94 url = self.check_url(self.url(workspace=self.workspace).strip('/') + '_unique/')
9095 res = test_client.post(url, data=raw_comment)
9196 assert res.status_code == 409
9297 assert 'object' in res.json
101106 session.commit()
102107 initial_comment_count = len(session.query(Comment).all())
103108 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/')
105110 res = test_client.post(url,
106111 data=raw_comment)
107112 assert res.status_code == 201
121126 get_comments = test_client.get(self.url(workspace=workspace))
122127 expected = ['first', 'second', 'third','fourth']
123128 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)
99 from tests import factories
1010 from tests.test_api_workspaced_base import (
1111 ReadWriteAPITests,
12 PatchableTestsMixin,
1213 )
13 from faraday.server.api.modules.credentials import CredentialView
14 from faraday.server.api.modules.credentials import CredentialView, CredentialV3View
1415 from faraday.server.models import Credential
1516 from tests.factories import HostFactory, ServiceFactory
17 from tests.utils.url import v2_to_v3
1618
1719
1820 class TestCredentialsAPIGeneric(ReadWriteAPITests):
2123 view_class = CredentialView
2224 api_endpoint = 'credential'
2325 update_fields = ['username', 'password']
26 patchable_fields = update_fields
2427
2528 def test_get_list_backwards_compatibility(self, test_client, session, second_workspace):
2629 cred = self.factory.create(workspace=second_workspace)
141144
142145 raw_data = self._generate_raw_update_data('Name1', 'Username2', 'Password3', parent_id=43)
143146
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)
145148 assert res.status_code == 400
146149
147150 def test_create_with_invalid_parent_type(
176179 raw_data = self._generate_raw_update_data(
177180 'Name1', 'Username2', 'Password3', parent_id=credential.host.id)
178181
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)
180183 assert res.status_code == 200
181184 assert res.json['username'] == u'Username2'
182185 assert res.json['password'] == u'Password3'
263266 response = test_client.get(self.url(workspace=second_workspace) + "?sort=target&sort_dir=asc")
264267 assert response.status_code == 200
265268 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))
11 import pytest
22
33 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
55
66 from faraday.server.api.modules.custom_fields import CustomFieldsSchemaView
77 from faraday.server.models import (
88 CustomFieldsSchema
99 )
10 from tests.utils.url import v2_to_v3
11
1012
1113 @pytest.mark.usefixtures('logged_user')
12 class TestVulnerabilityCustomFields(ReadOnlyAPITests):
14 class TestVulnerabilityCustomFields(ReadWriteAPITests):
1315 model = CustomFieldsSchema
1416 factory = CustomFieldsSchemaFactory
1517 api_endpoint = 'custom_fields_schema'
1618 #unique_fields = ['ip']
1719 #update_fields = ['ip', 'description', 'os']
1820 view_class = CustomFieldsSchemaView
21 patchable_fields = ['field_name']
1922
2023 def test_custom_fields_data(self, session, test_client):
2124 add_text_field = CustomFieldsSchemaFactory.create(
2831 session.add(add_text_field)
2932 session.commit()
3033
31 res = test_client.get(self.url()) # '/v2/custom_fields_schema/')
34 res = test_client.get(self.url())
3235 assert res.status_code == 200
3336 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
3437
4649 data = {
4750 u'field_name': u'cvss 2',
4851 u'field_type': 'int',
49 u'talbe_name': 'sarasa',
52 u'table_name': 'sarasa',
5053 u'field_display_name': u'CVSS new',
5154 u'field_order': 1
5255 }
7275 session.add(add_choice_field)
7376 session.commit()
7477
75 res = test_client.get(self.url()) # '/v2/custom_fields_schema/')
78 res = test_client.get(self.url())
7679 assert res.status_code == 200
7780 assert {u'table_name': u'vulnerability', u'id': add_choice_field.id, u'field_type': u'choice',
7881 u'field_name': u'gender', u'field_display_name': u'Gender', u'field_metadata': "['Male', 'Female']",
7982 u'field_order': 1} in res.json
8083
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)
1515 VulnerabilityFactory,
1616 VulnerabilityWebFactory
1717 )
18 from tests.utils.url import v2_to_v3
1819
1920
2021 @pytest.mark.usefixtures('logged_user')
21 class TestExportData():
22 class TestExportData:
23
24 def check_url(self, url):
25 return url
26
2227 def test_export_data_without_format(self, test_client):
2328 workspace = WorkspaceFactory.create()
24 url = f'/v2/ws/{workspace.name}/export_data'
29 url = self.check_url(f'/v2/ws/{workspace.name}/export_data')
2530 response = test_client.get(url)
2631 assert response.status_code == 400
2732
8489 session.add(vuln_web)
8590 session.commit()
8691
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')
8893 response = test_client.get(url)
8994 assert response.status_code == 200
9095 response_xml = response.data
137142 assert response_tree.xpath(full_xpath)[0].text == xml_file_hostnames
138143 else:
139144 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)
1919 if 'OPTIONS' in rule.methods:
2020 res = test_client.options(replace_placeholders(rule.rule))
2121 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)
2727 assert res.status_code == 200
2828 assert res.json.get('metasploit') != []
2929 assert res.json.get('exploitdb') != []
30
31
32 # I'm Py3
33 See the file 'doc/LICENSE' for the license information
44
55 '''
6 import time
76 import operator
87 from io import BytesIO
8 from posixpath import join as urljoin
99
1010 import pytz
1111
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
1815 from random import choice
1916 from sqlalchemy.orm.util import was_deleted
2017 from hypothesis import given, assume, settings, strategies as st
2522 from tests.test_api_workspaced_base import (
2623 API_PREFIX,
2724 ReadWriteAPITests,
28 PaginationTestsMixin,
25 PaginationTestsMixin, PatchableTestsMixin,
2926 )
3027 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
3229 from tests.factories import HostFactory, CommandFactory, \
3330 EmptyCommandFactory, WorkspaceFactory
3431
3734
3835 @pytest.mark.usefixtures('database', 'logged_user')
3936 class TestHostAPI:
37
38 def check_url(self, url):
39 return url
4040
4141 @pytest.fixture(autouse=True)
4242 def load_workspace_with_hosts(self, database, session, workspace, host_factory):
301301 host_factory.create_batch(5, workspace=second_workspace, os='Unix')
302302
303303 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"}]}'))
305305 assert res.status_code == 200
306306 self.compare_results(hosts, res)
307307
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
308322
309323 @pytest.mark.usefixtures('ignore_nplusone')
310324 def test_filter_restless_filter_and_group_by_os(self, test_client, session, workspace, host_factory):
311325 host_factory.create_batch(10, workspace=workspace, os='Unix')
312326 host_factory.create_batch(1, workspace=workspace, os='unix')
313327 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"}]}'))
317330 assert res.status_code == 200
318331 assert len(res.json['rows']) == 2
319 assert res.json['total_rows'] == 2
332 assert res.json['count'] == 2
320333 assert 'unix' in [row['value']['os'] for row in res.json['rows']]
321334 assert 'Unix' in [row['value']['os'] for row in res.json['rows']]
322
323335
324336 def test_filter_by_os_like_ilike(self, test_client, session, workspace,
325337 second_workspace, host_factory):
364376 host_factory.create_batch(5, workspace=second_workspace, os='Unix')
365377
366378 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 )
368384 assert res.status_code == 200
369385 self.compare_results(hosts, res)
370386
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 )
372392 assert res.status_code == 200
373393 self.compare_results(hosts + [case_insensitive_host], res)
374394
400420
401421 session.commit()
402422
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 )
405429 assert res.status_code == 200
406430 shown_hosts_ids = set(obj['id'] for obj in res.json['rows'])
407431 expected_host_ids = set(host.id for host in hosts)
434458 host_factory.create_batch(5, workspace=workspace)
435459
436460 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 )
439467 assert res.status_code == 200
440468 shown_hosts_ids = set(obj['id'] for obj in res.json['rows'])
441469 expected_host_ids = set(host.id for host in hosts)
456484
457485 session.commit()
458486
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 )
461493
462494 assert res.status_code == 200
463495
481513 session.commit()
482514 res = test_client.get(self.url() + '?port=invalid_port')
483515 assert res.status_code == 200
484 assert res.json['total_rows'] == 0
516 assert res.json['count'] == 0
485517
486518 def test_filter_restless_by_invalid_service_port(self, test_client, session, workspace,
487519 service_factory, host_factory):
492524 host_factory.create_batch(5, workspace=workspace)
493525
494526 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 )
497533 assert res.status_code == 400
498534
499535 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 )
502542 assert res.status_code == 400
503543
504544 @pytest.mark.usefixtures('ignore_nplusone')
505545 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'))
507547 assert res.status_code == 200
508548 assert len(res.json['rows']) == HOSTS_COUNT
509549
510550 @pytest.mark.usefixtures('ignore_nplusone')
511551 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'))
513553 assert res.status_code == 400
514554
515555 def test_search_ip(self, test_client, session, workspace, host_factory):
579619 vulnerability_factory.create(service=service, host=None, workspace=workspace)
580620 session.commit()
581621
582 res = test_client.get(self.url() + str(host.id) + "/" + 'services/')
622 res = test_client.get(self.check_url(urljoin(self.url(host),'services/')))
583623 assert res.status_code == 200
584624 assert res.json[0]['vulns'] == 1
585625
720760 'csrf_token': csrf_token
721761 }
722762 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/'),
724764 data=data, headers=headers, use_json_data=False)
725765 assert res.status_code == 200
726766 assert res.json['hosts_created'] == expected_created_hosts
735775 hosts_ids = [host_1.id, host_2.id]
736776 request_data = {'hosts_ids': hosts_ids}
737777
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)
739779
740780 deleted_hosts = delete_response.json['deleted_hosts']
741781 host_count_after_delete = db.session.query(Host).filter(
750790 ws = WorkspaceFactory.create(name="abc")
751791 request_data = {'hosts_ids': []}
752792
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)
754794
755795 assert delete_response.status_code == 400
756796
763803
764804 # Try to delete workspace_2's host from workspace_1
765805 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/')
767807 delete_response = test_client.delete(url, data=request_data)
768808
769809 assert delete_response.json['deleted_hosts'] == 0
771811 def test_bulk_delete_hosts_invalid_characters_in_request(self, test_client):
772812 ws = WorkspaceFactory.create(name="abc")
773813 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)
775815
776816 assert delete_response.json['deleted_hosts'] == 0
777817
786826 headers = [('content-type', 'text/xml')]
787827
788828 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/'),
790830 data=request_data,
791831 headers=headers)
792832
793833 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
794865
795866
796867 class TestHostAPIGeneric(ReadWriteAPITests, PaginationTestsMixin):
799870 api_endpoint = 'hosts'
800871 unique_fields = ['ip']
801872 update_fields = ['ip', 'description', 'os']
873 patchable_fields = update_fields
802874 view_class = HostsView
803875
804876 @pytest.mark.usefixtures("mock_envelope_list")
9741046 "os":"Unknown",
9751047 }
9761048
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)
9781050 assert res.status_code == 200
9791051
9801052 assert session.query(Hostname).filter_by(host=host).count() == 1
10621134 index_in_response_hosts = response_hosts.index(host)
10631135
10641136 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))
10651144
10661145
10671146 def host_json():
11091188 data=raw_data)
11101189 assert res.status_code in [201, 400, 409]
11111190
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
11121199 send_api_request()
1200 send_api_request_v3()
1515 assert response.status_code == 200
1616 assert response.json['Faraday Server'] == 'Running'
1717
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
1823 def test_get_config(self, test_client):
1924 res = test_client.get('/config')
2025 assert res.status_code == 200
44 See the file 'doc/LICENSE' for the license information
55
66 '''
7 from tests.utils.url import v2_to_v3
8
79 """Tests for many API endpoints that do not depend on workspace_name"""
810
911 import pytest
1113 from hypothesis import given, strategies as st
1214
1315 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
1517 from faraday.server.models import (
1618 License,
1719 )
3133 model = License
3234 factory = factories.LicenseFactory
3335 api_endpoint = 'licenses'
34 # unique_fields = ['ip']
35 # update_fields = ['ip', 'description', 'os']
36 patchable_fields = ["products"]
3637
3738 def test_envelope_list(self, test_client, app):
3839 LicenseEnvelopedView.register(app)
5051 res = test_client.get(self.url(obj=lic))
5152 assert res.status_code == 200
5253 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
5363
5464
5565 def license_json():
8595 res = test_client.post('v2/licenses/', data=raw_data)
8696 assert res.status_code in [201, 400, 409]
8797
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
88105 send_api_request()
89
90
91 # I'm Py3
106 send_api_request_v3()
66 from faraday.server.web import app
77 from tests import factories
88 from tests.conftest import logged_user, login_as
9 from tests.utils.url import v2_to_v3
910
1011
1112 class TestLogin:
13
14 def check_url(self, url):
15 return url
16
1217 def test_case_bug_with_username(self, test_client, session):
1318 """
1419 When the user case does not match the one in database,
5863
5964 headers = {'Authentication-Token': res.json['response']['user']['authentication_token']}
6065
61 ws = test_client.get('/v2/ws/wonderland/', headers=headers)
66 ws = test_client.get(self.check_url('/v2/ws/wonderland/'), headers=headers)
6267 assert ws.status_code == 200
6368
6469 def test_case_ws_with_invalid_authentication_token(self, test_client, session):
8590
8691 headers = {'Authorization': b'Token ' + token}
8792
88 ws = test_client.get('/v2/ws/wonderland/', headers=headers)
93 ws = test_client.get(self.check_url('/v2/ws/wonderland/'), headers=headers)
8994 assert ws.status_code == 401
9095
9196 @pytest.mark.usefixtures('logged_user')
9297 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/'))
9499 cookies = [cookie.name for cookie in test_client.cookie_jar]
95100 assert "faraday_session_2" in cookies
96101 assert res.status_code == 200
101106 session.commit()
102107 # clean cookies make sure test_client has no session
103108 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)
105110 assert res.status_code == 200
106111 assert 'Set-Cookie' not in res.headers
107112 cookies = [cookie.name for cookie in test_client.cookie_jar]
111116 def test_cant_retrieve_token_unauthenticated(self, test_client):
112117 # clean cookies make sure test_client has no session
113118 test_client.cookie_jar.clear()
114 res = test_client.get('/v2/token/')
119 res = test_client.get(self.check_url('/v2/token/'))
115120
116121 assert res.status_code == 401
117122
118123 @pytest.mark.usefixtures('logged_user')
119124 def test_token_expires_after_password_change(self, test_client, session):
120125 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/'))
122127
123128 assert res.status_code == 200
124129
131136
132137 # clean cookies make sure test_client has no session
133138 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)
135140 assert res.status_code == 401
136141
137142 def test_null_caracters(self, test_client, session):
162167
163168 headers = {'Authentication-Token': res.json['response']['user']['authentication_token']}
164169
165 ws = test_client.get('/v2/ws/wonderland/', headers=headers)
170 ws = test_client.get(self.check_url('/v2/ws/wonderland/'), headers=headers)
166171 assert ws.status_code == 200
167172
168173 def test_login_remember_me(self, test_client, session):
232237 assert res.status_code == 200
233238 cookies = [cookie.name for cookie in test_client.cookie_jar]
234239 assert "remember_token" not in cookies
240
241
242 class TestLoginV3(TestLogin):
243 def check_url(self, url):
244 return v2_to_v3(url)
66 '''
77 from builtins import str
88
9 from tests.utils.url import v2_to_v3
10
911 """Generic tests for APIs NOT prefixed with a workspace_name"""
1012
1113 import pytest
1315
1416 API_PREFIX = '/v2/'
1517 OBJECT_COUNT = 5
16
1718
1819 @pytest.mark.usefixtures('logged_user')
1920 class GenericAPITest:
4950 return url
5051
5152
53 @pytest.mark.usefixtures('logged_user')
5254 class ListTestsMixin:
5355
54 def test_list_retrieves_all_items_from(self, test_client):
56 def test_list_retrieves_all_items_from(self, test_client, logged_user):
5557 res = test_client.get(self.url())
5658 assert res.status_code == 200
5759 if 'rows' in res.json:
6062 assert len(res.json) == OBJECT_COUNT
6163
6264
63
65 @pytest.mark.usefixtures('logged_user')
6466 class RetrieveTestsMixin:
6567
66 def test_retrieve_one_object(self, test_client):
68 def test_retrieve_one_object(self, test_client, logged_user):
6769 res = test_client.get(self.url(self.first_object))
6870 assert res.status_code == 200
6971 assert isinstance(res.json, dict)
7072
71 @pytest.mark.parametrize('object_id', [123, -1, 'xxx', u'áá'])
73 @pytest.mark.parametrize('object_id', [123456789, -1, 'xxx', u'áá'])
7274 def test_404_when_retrieving_unexistent_object(self, test_client,
7375 object_id):
7476 url = self.url(object_id)
8183 def test_create_succeeds(self, test_client):
8284 res = test_client.post(self.url(),
8385 data=self.factory.build_dict())
84 assert res.status_code == 201
86 assert res.status_code == 201, (res.status_code, res.json)
8587 assert self.model.query.count() == OBJECT_COUNT + 1
8688 object_id = res.json['id']
8789 self.model.query.get(object_id)
8890
8991 def test_create_fails_with_empty_dict(self, test_client):
9092 res = test_client.post(self.url(), data={})
91 assert res.status_code == 400
93 assert res.status_code == 400, (res.status_code, res.json)
9294
9395 def test_create_fails_with_existing(self, session, test_client):
9496 for unique_field in self.unique_fields:
99101 assert self.model.query.count() == OBJECT_COUNT
100102
101103
104 @pytest.mark.usefixtures('logged_user')
102105 class UpdateTestsMixin:
103106
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)
108116 assert self.model.query.count() == OBJECT_COUNT
109117 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)
112119
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):
114122 for unique_field in self.unique_fields:
115123 data = self.factory.build_dict()
116124 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)
118130 assert res.status_code == 400
119131 assert self.model.query.count() == OBJECT_COUNT
120132
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):
122134 """To do this the user should use a PATCH request"""
123135 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)
125137
126138
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')
127161 class DeleteTestsMixin:
128162
129 def test_delete(self, test_client):
163 def test_delete(self, test_client, logged_user):
130164 res = test_client.delete(self.url(self.first_object))
131165 assert res.status_code == 204 # No content
132166 assert was_deleted(self.first_object)
133167 assert self.model.query.count() == OBJECT_COUNT - 1
134168
135 @pytest.mark.parametrize('object_id', [123, -1, 'xxx', u'áá'])
169 @pytest.mark.parametrize('object_id', [12300, -1, 'xxx', u'áá'])
136170 def test_delete_non_existent_raises_404(self, test_client,
137171 object_id):
138172 res = test_client.delete(self.url(object_id))
107107 res = test_client.get(self.page_url(1, 5))
108108 assert res.status_code == 200
109109 assert len(res.json['data']) == 0
110
111
112 # I'm Py3
33 from tests.factories import UserFactory
44 from faraday.server.models import User
55 from faraday.server.api.modules.preferences import PreferencesView
6 from tests.utils.url import v2_to_v3
7
68
79 pytest.fixture('logged_user')
810 class TestPreferences(GenericAPITest):
4244 response = test_client.post(self.url(), data=data)
4345
4446 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))
99 import pytest
1010
1111 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
1313 from tests.test_api_agent import logout, http_req
1414 from tests.conftest import login_as
1515 from faraday.server.models import SearchFilter
1616
1717
1818 from faraday.server.api.modules.search_filter import SearchFilterView
19 from tests.utils.url import v2_to_v3
1920
2021
2122 @pytest.mark.usefixtures('logged_user')
22 class TestSearchFilterAPI(ReadOnlyAPITests):
23 class TestSearchFilterAPI(ReadWriteAPITests):
2324 model = SearchFilter
2425 factory = SearchFilterFactory
2526 api_endpoint = 'searchfilter'
2627 view_class = SearchFilterView
28 patchable_fields = ['name']
2729
2830 pytest.fixture(autouse=True)
2931
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)
3236
3337 def test_list_retrieves_all_items_from_logger_user(self, test_client, session, logged_user):
3438 user_filter = SearchFilterFactory.create(creator=logged_user)
4145 else:
4246 assert len(res.json) == 1
4347
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)
4651
4752 def test_retrieve_one_object_from_logged_user(self, test_client, session, logged_user):
4853
5358 filters.append(user_filter)
5459
5560 session.commit()
56
61
5762 print(self.url(filters[randrange(5)]))
5863 res = test_client.get(self.url(filters[randrange(5)]))
5964 assert res.status_code == 200
6166
6267 def test_retrieve_filter_from_another_user(self, test_client, session, logged_user):
6368 user_filter = SearchFilterFactory.create(creator=logged_user)
64 another_user = UserFactory.create()
69 another_user = UserFactory.create()
6570 session.add(user_filter)
6671 session.add(another_user)
6772 session.commit()
7479
7580 def test_retrieve_filter_list_is_empty_from_another_user(self, test_client, session, logged_user):
7681 user_filter = SearchFilterFactory.create(creator=logged_user)
77 another_user = UserFactory.create()
82 another_user = UserFactory.create()
7883 session.add(user_filter)
7984 session.add(another_user)
8085 session.commit()
8893
8994 def test_delete_filter_from_another_user(self, test_client, session, logged_user):
9095 user_filter = SearchFilterFactory.create(creator=logged_user)
91 another_user = UserFactory.create()
96 another_user = UserFactory.create()
9297 session.add(user_filter)
9398 session.add(another_user)
9499 session.commit()
97102 login_as(test_client, another_user)
98103
99104 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)
44 See the file 'doc/LICENSE' for the license information
55
66 '''
7 from tests.utils.url import v2_to_v3
8
79 """Tests for many API endpoints that do not depend on workspace_name"""
810 try:
911 from urllib import urlencode
1315 import pytest
1416 import json
1517
16 from faraday.server.api.modules.services import ServiceView
18 from faraday.server.api.modules.services import ServiceView, ServiceV3View
1719 from tests import factories
18 from tests.test_api_workspaced_base import ReadOnlyAPITests
20 from tests.test_api_workspaced_base import ReadWriteAPITests, PatchableTestsMixin
1921 from faraday.server.models import (
2022 Service
2123 )
2325
2426
2527 @pytest.mark.usefixtures('logged_user')
26 class TestListServiceView(ReadOnlyAPITests):
28 class TestListServiceView(ReadWriteAPITests):
2729 model = Service
2830 factory = factories.ServiceFactory
2931 api_endpoint = 'services'
3032 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
3139
3240 def test_service_list_backwards_compatibility(self, test_client,
3341 second_workspace, session):
7381 assert service.port == 21
7482 assert service.host is host
7583
84 @pytest.mark.skip # more detailed test above
85 def test_create_succeeds(self, test_client):
86 pass
87
7688 def test_create_fails_with_invalid_status(self, test_client,
7789 host, session):
7890 session.commit()
220232 updated_service = Service.query.filter_by(id=service.id).first()
221233 assert updated_service.port == 221
222234
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):
224237 service = self.factory()
225238 host = HostFactory.create()
226239 session.commit()
227240 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
229246 assert res.status_code == 200, res.json
230247 assert res.json['id'] == service.id
231248
322339 assert res.status_code == 400
323340
324341
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)
77 import pytest
88 from tests.conftest import login_as
99
10
1011 @pytest.mark.usefixtures('logged_user')
11 class TestSessionLogged():
12 class TestSessionLogged:
1213 def test_session_when_user_is_logged(self, test_client):
1314 res = test_client.get('/session')
1415 assert res.status_code == 200
2223 assert res.json['role'] == role
2324
2425
25 class TestSessionNotLogged():
26 class TestSessionNotLogged:
2627 def test_session_when_user_is_not_logged(self, test_client):
2728 res = test_client.get('/session')
2829 assert res.status_code == 401
29
30
31 # I'm Py3
1313 from faraday.server.threads.reports_processor import REPORTS_QUEUE
1414
1515 from faraday.server.models import Host, Service, Command
16 from tests.utils.url import v2_to_v3
1617
1718
1819 @pytest.mark.usefixtures('logged_user')
19 class TestFileUpload():
20 class TestFileUpload:
21
22 def check_url(self, url):
23 return url
2024
2125 def test_file_upload(self, test_client, session, csrf_token, logged_user):
2226 ws = WorkspaceFactory.create(name="abc")
3236 }
3337
3438 res = test_client.post(
35 f'/v2/ws/{ws.name}/upload_report',
39 self.check_url(f'/v2/ws/{ws.name}/upload_report'),
3640 data=data,
3741 use_json_data=False)
3842
3943 assert res.status_code == 200
4044 assert len(REPORTS_QUEUE.queue) == 1
41 queue_elem = REPORTS_QUEUE.queue[0]
45 queue_elem = REPORTS_QUEUE.get_nowait()
4246 assert queue_elem[0] == ws.name
4347 assert queue_elem[3].lower() == "nmap"
4448 assert queue_elem[4] == logged_user.id
6468 assert service
6569 assert service.creator_id == logged_user_id
6670
67
6871 def test_no_file_in_request(self, test_client, session):
6972 ws = WorkspaceFactory.create(name="abc")
7073 session.add(ws)
7174 session.commit()
7275
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'))
7577
7678 assert res.status_code == 400
77
7879
7980 def test_request_without_csrf_token(self, test_client, session):
8081 ws = WorkspaceFactory.create(name="abc")
9091 }
9192
9293 res = test_client.post(
93 f'/v2/ws/{ws.name}/upload_report',
94 self.check_url(f'/v2/ws/{ws.name}/upload_report'),
9495 data=data,
9596 use_json_data=False)
9697
111112 'csrf_token': csrf_token
112113 }
113114 res = test_client.post(
114 f'/v2/ws/{ws.name}/upload_report',
115 self.check_url(f'/v2/ws/{ws.name}/upload_report'),
115116 data=data,
116 use_json_data=False)
117 use_json_data=False
118 )
117119
118120 assert res.status_code == 404
121
122
123 class TestFileUploadV3(TestFileUpload):
124 def check_url(self, url):
125 return v2_to_v3(url)
1313 from tempfile import NamedTemporaryFile
1414 from base64 import b64encode
1515 from io import BytesIO, StringIO
16 from posixpath import join as urljoin
17
18 from tests.utils.url import v2_to_v3
19
1620 try:
1721 from urllib import urlencode
1822 except ImportError:
3034 from faraday.server.api.modules.vulns import (
3135 VulnerabilityFilterSet,
3236 VulnerabilitySchema,
33 VulnerabilityView
37 VulnerabilityView,
38 VulnerabilityV3View
3439 )
3540 from faraday.server.fields import FaradayUploadedFile
3641 from faraday.server.schemas import NullToBlankString
3742 from tests import factories
3843 from tests.conftest import TEST_DATA_PATH
3944 from tests.test_api_workspaced_base import (
40 ReadOnlyAPITests
45 ReadWriteAPITests, PatchableTestsMixin
4146 )
4247 from faraday.server.models import (
4348 VulnerabilityGeneric,
150155
151156
152157 @pytest.mark.usefixtures('logged_user')
153 class TestListVulnerabilityView(ReadOnlyAPITests): # TODO migration: use read write api tests
158 class TestListVulnerabilityView(ReadWriteAPITests):
154159 model = Vulnerability
155160 factory = factories.VulnerabilityFactory
156161 api_endpoint = 'vulns'
157162 #unique_fields = ['ip']
158163 #update_fields = ['ip', 'description', 'os']
159164 view_class = VulnerabilityView
165 patchable_fields = ['description']
166
167 def check_url(self, url):
168 return url
160169
161170 def test_backward_json_compatibility(self, test_client, second_workspace, session):
162171 new_obj = self.factory.create(workspace=second_workspace)
290299 description='helloworld',
291300 severity='low',
292301 )
293 ws_name = host_with_hostnames.workspace.name
302 ws = host_with_hostnames.workspace
294303 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)
296305 assert res.status_code == 201
297306 assert vuln_count_previous + 1 == session.query(Vulnerability).count()
298307 assert res.json['name'] == 'New vulns'
365374 assert filename in res.json['_attachments']
366375 attachment.close()
367376 # 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}/')))
369378 assert res.status_code == 200
370379 assert res.data == file_content
371380
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 )))
375385 assert res.status_code == 404
376386
377387 @pytest.mark.usefixtures('ignore_nplusone')
395405 res = test_client.put(self.url(obj=vuln, workspace=self.workspace), data=raw_data)
396406 assert res.status_code == 200
397407 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 )))
400411 assert res.status_code == 200
401412 assert res.data == file_content
402413
418429 assert res.status_code == 200
419430
420431 # 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 )))
423435 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 )))
426439 assert res.status_code == 200
427440 assert res.data == file_content
428441
440453 session.add(new_attach)
441454 session.commit()
442455
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}/')))
444462 assert res.status_code == 200
445463 assert new_attach.filename in res.json
446464 assert 'image/png' in res.json[new_attach.filename]['content_type']
447
448465
449466 def test_create_vuln_props(self, host_with_hostnames, test_client, session):
450467 """
475492 severity='low',
476493 **vuln_props
477494 )
478 ws_name = host_with_hostnames.workspace.name
495 ws = host_with_hostnames.workspace
479496 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)
481498 assert res.status_code == 201
482499 for prop, value in vuln_props.items():
483500 if prop not in vuln_props_excluded:
502519 description='Description goes here',
503520 severity='critical',
504521 )
505 ws_name = host_with_hostnames.workspace.name
522 ws = host_with_hostnames.workspace
506523 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)
508525 assert res.status_code == 201
509526 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)
511528 assert res.status_code == 409
512529 assert vuln_count_previous + 1 == session.query(Vulnerability).count()
513530
522539 refs=[],
523540 policyviolations=[]
524541 )
525 ws_name = host_with_hostnames.workspace.name
542 ws = host_with_hostnames.workspace
526543 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)
528545 assert res.status_code == 201
529546 assert vuln_count_previous + 1 == session.query(Vulnerability).count()
530547 assert res.json['status'] == 'closed'
638655 refs=[],
639656 policyviolations=[]
640657 )
641 ws_name = host_with_hostnames.workspace.name
658 ws = host_with_hostnames.workspace
642659 vuln_count_previous = session.query(Vulnerability).count()
643660 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)
645662 assert res.status_code == 201
646663 assert vuln_web_count_previous + 1 == session.query(VulnerabilityWeb).count()
647664 assert vuln_count_previous == session.query(Vulnerability).count()
828845 host_factory.create(workspace=workspace, ip="192.168.0.2")
829846
830847 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 )))
833851 assert res.status_code == 200
834852
835853 @pytest.mark.usefixtures('ignore_nplusone')
846864 10, workspace=self.workspace, host=host2, service=None)
847865
848866 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 )))
851871 assert res.status_code == 200
852872 assert len(res.json['vulnerabilities']) == 1
853873 assert res.json['vulnerabilities'][0]['value']['target'] == '192.168.0.2'
869889 10, workspace=self.workspace, host=None, service=service)
870890
871891 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 )))
874896 assert res.status_code == 200
875897 assert len(res.json['vulnerabilities']) == 10
876898 assert res.json['count'] == 10
877
878899
879900 @pytest.mark.usefixtures('ignore_nplusone')
880901 def test_filter_restless_by_service_name(self, test_client, session, workspace,
892913 10, workspace=self.workspace, host=None, service=service)
893914
894915 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 )))
897920 assert res.status_code == 200
898921 assert len(res.json['vulnerabilities']) == 1
899922 assert res.json['count'] == 1
944967 policyviolations=[],
945968 attachments=attachments,
946969 )
947 ws_name = host_with_hostnames.workspace.name
970 ws = host_with_hostnames.workspace
948971 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)
950973
951974 assert res.status_code == 201
952975 assert len(res.json['_attachments']) == 2
963986 refs=['CVE-2017-0002', 'CVE-2017-0012', 'CVE-2017-0012'],
964987 policyviolations=[]
965988 )
966 ws_name = host_with_hostnames.workspace.name
989 ws = host_with_hostnames.workspace
967990 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)
969992 assert res.status_code == 201
970993 assert session.query(Reference).count() == 2
971994 assert vuln_count_previous + 1 == session.query(Vulnerability).count()
9811004 policyviolations=['PCI DSS Credir card not encrypted',
9821005 'PCI DSS Credir card not encrypted'],
9831006 )
984 ws_name = host_with_hostnames.workspace.name
1007 ws = host_with_hostnames.workspace
9851008 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)
9871010 assert res.status_code == 201
9881011 assert session.query(PolicyViolation).count() == 1
9891012 assert vuln_count_previous + 1 == session.query(Vulnerability).count()
10041027 'integrity': True
10051028 }
10061029 )
1007 ws_name = host_with_hostnames.workspace.name
1030 ws = host_with_hostnames.workspace
10081031 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)
10101033 assert res.status_code == 201
10111034 assert vuln_count_previous + 1 == session.query(Vulnerability).count()
10121035 assert res.json['name'] == 'New vulns'
10311054 'invalid': None,
10321055 }
10331056 )
1034 ws_name = host_with_hostnames.workspace.name
1057 ws = host_with_hostnames.workspace
10351058 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)
10371060 assert res.status_code == 400
10381061
10391062 def test_create_vuln_with_invalid_type(self,
10491072 refs=[],
10501073 policyviolations=[]
10511074 )
1052 ws_name = host_with_hostnames.workspace.name
1075 ws = host_with_hostnames.workspace
10531076 vuln_count_previous = session.query(Vulnerability).count()
10541077 res = test_client.post(
1055 f'/v2/ws/{ws_name}/vulns/',
1078 self.url(workspace=ws),
10561079 data=raw_data,
10571080 )
10581081 assert res.status_code == 400
10791102 severity='low',
10801103 )
10811104 raw_data.pop("type")
1082 ws_name = host_with_hostnames.workspace.name
1105 ws = host_with_hostnames.workspace
10831106 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)
10851108 assert res.status_code == 400
10861109 assert vuln_count_previous == session.query(Vulnerability).count()
10871110 assert res.json['message'] == 'Type is required.'
10991122 policyviolations=[],
11001123 severity="invalid",
11011124 )
1102 ws_name = host_with_hostnames.workspace.name
1125 ws = host_with_hostnames.workspace
11031126 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)
11051128 assert res.status_code == 400
11061129 assert vuln_count_previous == session.query(Vulnerability).count()
11071130 assert b'Invalid severity type.' in res.data
11201143 policyviolations=[],
11211144 easeofresolution='frutafrutafruta'
11221145 )
1123 ws_name = host_with_hostnames.workspace.name
1146 ws = host_with_hostnames.workspace
11241147 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)
11261149 assert res.status_code == 400
11271150 assert vuln_count_previous == session.query(Vulnerability).count()
11281151 assert list(res.json['messages']['json'].keys()) == ['easeofresolution']
11421165 policyviolations=[],
11431166 easeofresolution=None,
11441167 )
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),
11471170 data=raw_data)
11481171 assert res.status_code == 201, (res.status_code, res.data)
11491172 created_vuln = Vulnerability.query.get(res.json['_id'])
11501173 assert created_vuln.ease_of_resolution is None
1151
11521174
11531175 def test_count_order_by_incorrect_keyword(self, test_client, session):
11541176 for i, vuln in enumerate(self.objects[:3]):
11631185 session.commit()
11641186
11651187 #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 )
11681192 assert res.status_code == 400
11691193
11701194 #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 )
11731199 assert res.status_code == 400
11741200
11751201
11861212 session.commit()
11871213
11881214 #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 )
11911218 assert res.status_code == 200
11921219 assert res.json['total_count'] == 3
11931220 assert sorted(res.json['groups'], key=lambda i: (i['name'],i['count'],i['severity'])) == sorted([
11961223 ], key=lambda i: (i['name'],i['count'],i['severity']))
11971224
11981225 #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")
12011227 assert res.status_code == 200
12021228 assert res.json['total_count'] == 3
12031229 assert sorted(res.json['groups'], key=lambda i: (i['name'],i['count'],i['severity']), reverse=True) == sorted([
12041230 {"name": "critical", "severity": "critical", "count": 1},
12051231 {"name": "high", "severity": "high", "count": 2},
12061232 ], key=lambda i: (i['name'],i['count'],i['severity']), reverse=True)
1207
12081233
12091234 def test_count_group_by_incorrect_vuln_column(self, test_client, session):
12101235 for i, vuln in enumerate(self.objects[:3]):
12181243 session.add(vuln)
12191244 session.commit()
12201245
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")
12231247 assert res.status_code == 400
12241248
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=")
12271250 assert res.status_code == 400
1228
1229
12301251
12311252 def test_count_confirmed(self, test_client, session):
12321253 for i, vuln in enumerate(self.objects[:3]):
12411262 session.add(vuln)
12421263 session.commit()
12431264
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')
12461266 assert res.status_code == 200
12471267 assert res.json['total_count'] == 3
12481268 assert sorted(res.json['groups'], key=lambda i: (i['count'],i['name'],i['severity'])) == sorted([
12601280 session.add_all(vulns)
12611281 session.commit()
12621282
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 )
12651286 assert res.status_code == 200
12661287 assert res.json['total_count'] == 9
12671288 assert sorted(res.json['groups'], key=lambda i: (i['count'],i['name'],i['severity'])) == sorted([
12821303 session.add(vuln)
12831304 session.commit()
12841305
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 )
12891310
12901311 assert res.status_code == 200
12911312 assert len(res.json['groups']) == 1
12921313 assert res.json['total_count'] == 5
1293
12941314
12951315 def test_count_multiworkspace_two_public_workspaces(self, test_client, session, second_workspace):
12961316 vulns = self.factory.create_batch(1, severity='informational',
13131333 session.add(vuln)
13141334 session.commit()
13151335
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 )
13221340
13231341 assert res.status_code == 200
13241342 assert len(res.json['groups']) == 2
13251343 assert res.json['total_count'] == 10
13261344
13271345 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 )
13291350 assert res.status_code == 400
13301351
13311352 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 )
13331357 assert res.status_code == 400
13341358
13351359 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 )
13371364 assert res.status_code == 404
1338
13391365
13401366 @pytest.mark.usefixtures('mock_envelope_list')
13411367 def test_target(self, test_client, session, second_workspace,
15801606 description='helloworld',
15811607 severity='low',
15821608 )
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)
15851611 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)
15881613 assert res.status_code == 409
15891614
15901615 def test_create_webvuln_multiple_times_returns_conflict(self, host_with_hostnames, test_client, session):
16071632 description='helloworld',
16081633 severity='low',
16091634 )
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)
16121637 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)
16151639 assert res.status_code == 409
16161640
16171641 def test_create_similar_vuln_service_and_vuln_web_conflict_succeed(
16771701 severity='low',
16781702 )
16791703 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 )
16811708 assert res.status_code == 201
16821709 raw_data = _create_post_data_vulnerability(
16831710 name='Update vulnsweb',
16891716 description='Update helloworld',
16901717 severity='high',
16911718 )
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
16961724
16971725 def test_create_vuln_from_command(self, test_client, session):
16981726 command = EmptyCommandFactory.create(workspace=self.workspace)
19301958 headers = {'Content-type': 'multipart/form-data'}
19311959
19321960 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/'),
19341962 data=data, headers=headers, use_json_data=False)
19351963 assert res.status_code == 403 # Missing CSRF protection
19361964
19391967 'csrf_token': csrf_token
19401968 }
19411969 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/'),
19431971 data=data, headers=headers, use_json_data=False)
19441972 assert res.status_code == 200 # Now it should work
19451973
19631991 session.commit()
19641992
19651993 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/'),
19671995 data=data, headers=headers, use_json_data=False)
19681996 assert res.status_code == 403
19691997 query_test = session.query(Vulnerability).filter_by(id=vuln.id).first().evidence
19852013 policyviolations=[],
19862014 attachments=[attachment]
19872015 )
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)
19892017 assert res.status_code == 201
19902018
19912019 filename = attachment.name.split('/')[-1]
19922020 vuln_id = res.json['_id']
19932021 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}/')
19952023 )
19962024 assert res.status_code == 200
19972025
20142042 policyviolations=[],
20152043 attachments=[attachment]
20162044 )
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)
20182046 assert res.status_code == 201
20192047
20202048 self.workspace.readonly = True
20232051 filename = attachment.name.split('/')[-1]
20242052 vuln_id = res.json['_id']
20252053 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}/')
20272055 )
20282056 assert res.status_code == 403
20292057
20452073 description='helloworld',
20462074 severity='medium',
20472075 )
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)
20492077
20502078 data = {
20512079 'q': '{"filters":[{"name":"severity","op":"eq","val":"medium"}]}'
20522080 }
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)
20552082
20562083 assert res.status_code == 200
20572084 value = res.json['vulnerabilities'][0]['value']
20612088 data = {
20622089 "q": {"filters":[{"name":"severity","op":"eq","val":"medium"}]}
20632090 }
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)
20652092 assert res.status_code == 400
20662093
20672094 def test_vuln_filter_exception(self, test_client, workspace, session):
20712098 data = {
20722099 'q': '{"filters":[{"name":"severity","op":"eq","val":"medium"}]}'
20732100 }
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)
20752102 assert res.status_code == 200
20762103 assert res.json['count'] == 1
20772104
20942121 data = {
20952122 'q': '{"group_by":[{"field":"creator_id"}]}'
20962123 }
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)
20982125 assert res.status_code == 200
20992126 assert res.json['count'] == 1 # all vulns created by the same creator
21002127 expected = [{'count': 2, 'creator_id': creator.id}]
21192146 data = {
21202147 'q': '{"group_by":[{"field":"severity"}]}'
21212148 }
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)
21232150 assert res.status_code == 200, res.json
21242151 assert res.json['count'] == 1, res.json # all vulns created by the same creator
21252152 expected = {
21512178 data = {
21522179 'q': '{"group_by":[{"field":"severity"}, {"field": "name"}]}'
21532180 }
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)
21552182 assert res.status_code == 200, res.json
21562183 assert res.json['count'] == 2, res.json # all vulns created by the same creator
21572184 expected ={'vulnerabilities': [
21842211 data = {
21852212 'q': json.dumps({"group_by":[{"field":col_name}]})
21862213 }
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)
21882215 assert res.status_code == 200, res.json
21892216
21902217 def test_vuln_restless_group_same_name_description(self, test_client, session):
22182245 data = {
22192246 'q': '{"group_by":[{"field":"name"}, {"field":"description"}]}'
22202247 }
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)
22222249 assert res.status_code == 200
22232250 assert res.json['count'] == 2
22242251 expected = [{'count': 2, 'name': 'test', 'description': 'test'}, {'count': 1, 'name': 'test2', 'description': 'test'}]
22832310 data = {
22842311 'q': json.dumps(query)
22852312 }
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)
22872314 assert res.status_code == 200
22882315 assert res.json['count'] == 12
22892316 expected_order = ['critical', 'critical', 'med', 'med', 'med', 'med', 'med', 'med', 'med', 'med', 'med', 'med']
22972324 data = {
22982325 'q': json.dumps({"filters":[{"name":"creator","op":"eq","val": vuln.creator.username}]})
22992326 }
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)
23012328 assert res.status_code == 200
23022329
23032330 def test_vuln_web_filter_exception(self, test_client, workspace, session):
23072334 data = {
23082335 'q': '{"filters":[{"name":"severity","op":"eq","val":"medium"}]}'
23092336 }
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)
23112338 assert res.status_code == 200
23122339 assert res.json['count'] == 1
23132340
23432370 session.commit()
23442371
23452372 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/'),
23472374 data={'csrf_token': csrf_token},
23482375 headers={'Content-Type': 'multipart/form-data'},
23492376 use_json_data=False)
23512378
23522379 def test_get_attachment_with_invalid_workspace_and_vuln(self, test_client):
23532380 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 )
23552383 assert res.status_code == 404
23562384
23572385 def test_delete_attachment_with_invalid_workspace_and_vuln(self, test_client):
23582386 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 )
23602389 assert res.status_code == 404
23612390
23622391 def test_delete_invalid_attachment(self, test_client, workspace, session):
23642393 session.add(vuln)
23652394 session.commit()
23662395 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 )
23682398 assert res.status_code == 404
23692399
23702400 def test_export_vuln_csv_empty_workspace(self, test_client, session):
23712401 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/'))
23732403 expected_headers = [
23742404 "confirmed", "id", "date", "name", "severity", "service",
23752405 "target", "desc", "status", "hostnames", "comments", "owner",
23912421 confirmed_vulns = VulnerabilityFactory.create(confirmed=True, workspace=workspace)
23922422 session.add(confirmed_vulns)
23932423 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 )
23952428 assert res.status_code == 200
23962429 assert self._verify_csv(res.data, confirmed=True)
23972430
24052438 workspace=workspace)
24062439 session.add(confirmed_vulns)
24072440 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/')))
24092442 assert res.status_code == 200
24102443 assert self._verify_csv(res.data, confirmed=True)
24112444
24152448 confirmed_vulns = VulnerabilityFactory.create(confirmed=True, severity='critical', workspace=workspace)
24162449 session.add(confirmed_vulns)
24172450 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 )
24192455 assert res.status_code == 200
24202456 assert self._verify_csv(res.data, confirmed=True, severity='critical')
24212457
24242460 self.first_object.confirmed = True
24252461 session.add(self.first_object)
24262462 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 )
24282467 assert res.status_code == 200
24292468 self._verify_csv(res.data, confirmed=True)
24302469
24482487 session.add(vuln)
24492488 session.commit()
24502489
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/'))
24522491 assert res.status_code == 200
24532492
24542493 csv_data = csv.DictReader(StringIO(res.data.decode('utf-8')), delimiter=',')
24882527 session.add(vuln)
24892528 session.commit()
24902529
2491 res = test_client.get(self.url() + 'export_csv/')
2530 res = test_client.get(self.check_url(urljoin(self.url(), 'export_csv/')))
24922531 assert self._verify_csv(res.data)
24932532
24942533 def _verify_csv(self, raw_csv_data, confirmed=False, severity=None):
25612600 assert res.json['tool'] == tool
25622601
25632602
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']
25642632
25652633
25662634 @pytest.mark.usefixtures('logged_user')
2567 class TestCustomFieldVulnerability(ReadOnlyAPITests): # TODO migration: use read write api tests
2635 class TestCustomFieldVulnerability(ReadWriteAPITests):
25682636 model = Vulnerability
25692637 factory = factories.VulnerabilityFactory
25702638 api_endpoint = 'vulns'
25712639 view_class = VulnerabilityView
2640 patchable_fields = ['description']
2641
2642 def check_url(self, url):
2643 return url
25722644
25732645 def test_create_vuln_with_custom_fields_shown(self, test_client, second_workspace, session):
25742646 host = HostFactory.create(workspace=self.workspace)
25972669 assert res.status_code == 201
25982670 assert res.json['custom_fields']['cvss'] == '321321'
25992671
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):
26012673 host = HostFactory.create(workspace=self.workspace)
26022674 custom_field_schema = CustomFieldsSchemaFactory(
26032675 field_name='cvss',
26212693 }
26222694 res = test_client.post(self.url(), data=data)
26232695
2624 assert res.status_code == 400
2696 assert res.status_code == 201
2697 assert "Invalid custom field" in caplog.text
26252698
26262699 def test_create_vuln_with_custom_fields_list(self, test_client, second_workspace, session):
26272700 host = HostFactory.create(workspace=self.workspace)
26692742 'parent': host.id,
26702743 'type': 'Vulnerability',
26712744 'custom_fields': {
2672 'CVSS': 'pepe',
2745 'cvss': 'pepe',
26732746 }
26742747 }
26752748 res = test_client.post(self.url(), data=data)
26762749
26772750 assert res.status_code == 400
26782751
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):
26802753 host = HostFactory.create(workspace=self.workspace)
26812754 session.add(host)
26822755 session.commit()
26932766 }
26942767 res = test_client.post(self.url(), data=data)
26952768
2696 assert res.status_code == 400
2769 assert res.status_code == 201
2770 assert "Invalid custom field" in caplog.text
26972771
26982772 def test_create_create_vuln_web_with_host_as_parent_fails(
26992773 self, host, session, test_client):
27612835 )
27622836 ws_name = host_with_hostnames.workspace.name
27632837 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)
27662840 vuln_1_id = res_1.json['obj_id']
27672841 vuln_2_id = res_2.json['obj_id']
27682842 vulns_to_delete = [vuln_1_id, vuln_2_id]
27692843 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)
27712845 vuln_count_after = session.query(Vulnerability).count()
27722846 deleted_vulns = delete_response.json['deleted_vulns']
27732847 assert delete_response.status_code == 200
28062880 )
28072881 ws_name = host_with_hostnames.workspace.name
28082882 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)
28112885 vuln_1_id = res_1.json['obj_id']
28122886 vuln_2_id = res_2.json['obj_id']
28132887 vulns_to_delete = [vuln_1_id, vuln_2_id]
28142888 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)
28162890 vuln_count_after = session.query(Vulnerability).count()
28172891 deleted_vulns = delete_response.json['deleted_vulns']
28182892 assert delete_response.status_code == 200
28402914 severity='low',
28412915 tool=tool_name
28422916 )
2843 ws_name = host_with_hostnames.workspace.name
2917 ws = host_with_hostnames.workspace
28442918 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)
28462920 assert res.status_code == 201
28472921 assert vuln_count_previous + 1 == session.query(Vulnerability).count()
28482922 assert res.json['tool'] == tool_name
28662940 description='helloworld',
28672941 severity='low',
28682942 )
2869 ws_name = host_with_hostnames.workspace.name
2943 ws = host_with_hostnames.workspace
28702944 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)
28722946 assert res.status_code == 201
28732947 assert vuln_count_previous + 1 == session.query(Vulnerability).count()
28742948 assert res.json['tool'] == "Web UI"
29222996 assert res.json['tool'] == command.tool
29232997
29242998
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
29253017
29263018 @pytest.mark.usefixtures('logged_user')
2927 class TestVulnerabilityCustomFields(ReadOnlyAPITests):
3019 class TestVulnerabilityCustomFields(ReadWriteAPITests):
29283020 model = Vulnerability
29293021 factory = factories.VulnerabilityFactory
29303022 api_endpoint = 'vulns'
29313023 view_class = VulnerabilityView
3024 patchable_fields = ['description']
29323025
29333026 def test_custom_field_cvss(self, session, test_client):
29343027 add_text_field = CustomFieldsSchemaFactory.create(
29413034 session.commit()
29423035
29433036
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
29443044 @pytest.mark.usefixtures('logged_user')
2945 class TestVulnerabilitySearch():
3045 class TestVulnerabilitySearch:
3046
3047 def check_url(self, url):
3048 return url
29463049
29473050 @pytest.mark.skip_sql_dialect('sqlite')
29483051 def test_search_by_hostname_vulns(self, test_client, session):
29583061 [{"name":"hostnames","op":"eq","val":"pepe"}]
29593062 }
29603063 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 )
29623066 assert res.status_code == 200
29633067 assert res.json['count'] == 1
29643068 assert res.json['vulnerabilities'][0]['id'] == vuln.id
29783082 [{"name":"hostnames","op":"eq","val":"pepe"}]
29793083 }
29803084 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 )
29823087 assert res.status_code == 200
29833088 assert res.json['count'] == 1
29843089 assert res.json['vulnerabilities'][0]['id'] == vuln.id
29993104 [{"name": "hostnames", "op": "eq", "val": "pepe"}]
30003105 }
30013106 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 )
30033109 assert res.status_code == 200
30043110 assert res.json['count'] == 1
30053111 assert res.json['vulnerabilities'][0]['id'] == vuln.id
30093115 []
30103116 }
30113117 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 )
30133120 assert res.status_code == 200
30143121 assert res.json['count'] == 0
30153122
30183125 [{"name":"code", "op": "eq", "val": "test"}]
30193126 }
30203127 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 )
30223130
30233131 assert res.status_code == 400, res.json
30243132
30363144 {"and": [{"name": "hostnames","op": "eq", "val": "pepe"}]}
30373145 ]}
30383146 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 )
30403149 assert res.status_code == 200
30413150 assert res.json['count'] == 1
30423151 assert res.json['vulnerabilities'][0]['id'] == vuln.id
30673176 "offset": offset * 10,
30683177 }
30693178 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 )
30713181 assert res.status_code == 200
30723182 assert res.json['count'] == 20, query_filter
30733183 assert len(res.json['vulnerabilities']) == 10
30963206 "offset": 10 * offset,
30973207 }
30983208 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 )
31003211 assert res.status_code == 200
31013212 assert res.json['count'] == 100
31023213 for vuln in res.json['vulnerabilities']:
31343245 "offset": offset,
31353246 }
31363247 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 )
31383250 assert res.status_code == 200
31393251 assert res.json['count'] == 10
31403252 paginated_vulns.add(res.json['vulnerabilities'][0]['id'])
31723284 {"name": "host__os", "op": "has", "val": "Linux"}
31733285 ]}
31743286 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 )
31763289 assert res.status_code == 200
31773290 assert res.json['count'] == 1
31783291 assert res.json['vulnerabilities'][0]['id'] == vuln.id
32193332 {"name": "create_date", "op": "eq", "val": vuln.create_date.strftime("%Y-%m-%d")}
32203333 ]}
32213334 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 )
32233337 assert res.status_code == 200
32243338 assert res.json['count'] == 3
32253339
32333347 {"name": "create_date", "op": "eq", "val": "30/01/2020"}
32343348 ]}
32353349 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 )
32373352 assert res.status_code == 200
32383353
32393354 @pytest.mark.skip_sql_dialect('sqlite')
32403355 def test_search_hypothesis_test_found_case(self, test_client, session, workspace):
32413356 query_filter = {'filters': [{'name': 'host_id', 'op': 'not_in', 'val': '\U0010a1a7\U00093553\U000eb46a\x1e\x10\r\x18%\U0005ddfa0\x05\U000fdeba\x08\x04絮'}]}
32423357 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 )
32443360 assert res.status_code == 400
32453361
32463362 @pytest.mark.skip_sql_dialect('sqlite')
32473363 def test_search_hypothesis_test_found_case_2(self, test_client, session, workspace):
32483364 query_filter = {'filters': [{'name': 'host__os', 'op': 'ilike', 'val': -1915870387}]}
32493365 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 )
32513368 assert res.status_code == 400
32523369
32533370 @pytest.mark.skip_sql_dialect('sqlite')
32583375 ])
32593376 def test_search_hypothesis_test_found_case_3(self, query_filter, test_client, session, workspace):
32603377 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 )
32623380 assert res.status_code == 400
32633381
32643382 @pytest.mark.skip_sql_dialect('sqlite')
32693387 ])
32703388 def test_search_hypothesis_test_found_case_4(self, query_filter, test_client, session, workspace):
32713389 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 )
32733392 assert res.status_code == 400
32743393
32753394 @pytest.mark.skip_sql_dialect('sqlite')
32803399 ])
32813400 def test_search_hypothesis_test_found_case_5(self, query_filter, test_client, session, workspace):
32823401 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 )
32843404 assert res.status_code == 400
32853405
32863406 @pytest.mark.skip_sql_dialect('sqlite')
32873407 def test_search_hypothesis_test_found_case_6(self, test_client, session, workspace):
32883408 query_filter = {'filters': [{'name': 'resolution', 'op': 'any', 'val': ''}]}
32893409 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 )
32913412 assert res.status_code == 200
32923413
32933414 @pytest.mark.skip_sql_dialect('sqlite')
32943415 def test_search_hypothesis_test_found_case_7(self, test_client, session, workspace):
32953416 query_filter = {'filters': [{'name': 'name', 'op': '>', 'val': '\U0004e755\U0007a789\U000e02d1\U000b3d32\x10\U000ad0e2,\x05\x1a'}, {'name': 'creator', 'op': 'eq', 'val': 21883}]}
32963417 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 )
32983420 assert res.status_code == 400
32993421
33003422 @pytest.mark.skip_sql_dialect('sqlite')
33043426 ])
33053427 def test_search_hypothesis_test_found_case_7_valid(self, query_filter, test_client, session, workspace):
33063428 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 )
33083431 assert res.status_code == 200
33093432
33103433 @pytest.mark.skip_sql_dialect('sqlite')
33113434 def test_search_hypothesis_test_found_case_8(self, test_client, session, workspace):
33123435 query_filter = {'filters': [{'name': 'hostnames', 'op': '==', 'val': ''}]}
33133436 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 )
33153439 assert res.status_code == 200
33163440
33173441 @pytest.mark.skip_sql_dialect('sqlite')
33193443 query_filter = {'filters': [{'name': 'issuetracker', 'op': 'not_equal_to', 'val': '0\x00\U00034383$\x13-\U000375fb\U0007add2\x01\x01\U0010c23a'}]}
33203444
33213445 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 )
33233448 assert res.status_code == 400
33243449
33253450 @pytest.mark.skip_sql_dialect('sqlite')
33273452 query_filter = {'filters': [{'name': 'impact_integrity', 'op': 'neq', 'val': 0}]}
33283453
33293454 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 )
33313457 assert res.status_code == 400
33323458
33333459 @pytest.mark.skip_sql_dialect('sqlite')
33353461 query_filter = {'filters': [{'name': 'host_id', 'op': 'like', 'val': '0'}]}
33363462
33373463 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 )
33393466 assert res.status_code == 400
33403467
33413468 @pytest.mark.skip_sql_dialect('sqlite')
33433470 query_filter = {'filters': [{'name': 'custom_fields', 'op': 'like', 'val': ''}]}
33443471
33453472 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 )
33473475 assert res.status_code == 400
33483476
33493477 @pytest.mark.skip_sql_dialect('sqlite')
33513479 query_filter = {'filters': [{'name': 'impact_accountability', 'op': 'ilike', 'val': '0'}]}
33523480
33533481 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 )
33553484 assert res.status_code == 400
33563485
33573486 @pytest.mark.usefixtures('ignore_nplusone')
33673496 query_filter = {'filters': [{'name': 'severity', 'op': 'eq', 'val': 'high'}]}
33683497
33693498 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 )
33713501 assert res.status_code == 200
33723502 assert res.json['count'] == 20
33733503
33893519 }
33903520
33913521 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 )
33933524 assert res.status_code == 400
33943525
33953526 @pytest.mark.skip_sql_dialect('sqlite')
34133544 }
34143545
34153546 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 )
34173549 assert res.status_code == 200
34183550 expected_order = sort_order["expected"]
34193551
34363568 }
34373569
34383570 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 )
34403573 assert res.status_code == 200
34413574 expected_order = ['critical', 'high', 'med', 'low']
34423575
34763609 }
34773610
34783611 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 )
34803614 assert res.status_code == 200
34813615 assert res.json['count'] == 100
3616
3617
3618 class TestVulnerabilitySearchV3(TestVulnerabilitySearch):
3619 def check_url(self, url):
3620 return v2_to_v3(url)
34823621
34833622 def test_type_filter(workspace, session,
34843623 vulnerability_factory,
36563795 data=raw_data)
36573796 assert res.status_code in [201, 400, 409]
36583797
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
36593806 @given(VulnerabilityDataWithId)
36603807 def send_api_update_request(raw_data):
36613808
36623809 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']}",
36643811 data=raw_data)
36653812 assert res.status_code in [200, 400, 409, 405]
36663813
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
36673822 send_api_create_request()
36683823 send_api_update_request()
3824 send_api_create_request_v3()
3825 send_api_update_request_v3()
36693826
36703827
36713828 def filter_json():
37143871
37153872 assert res.status_code in [200, 400]
37163873
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
37173885 send_api_filter_request()
3886 send_api_filter_request_v3()
37183887
37193888
37203889 def test_model_converter():
37243893 field = VulnerabilitySchema().fields['data']
37253894 assert isinstance(field, NullToBlankString)
37263895 assert field.allow_none
3727
3728
3729 # I'm Py3
1111 from faraday.server.api.modules.vulnerability_template import VulnerabilityTemplateView
1212 from tests import factories
1313 from tests.test_api_non_workspaced_base import (
14 ReadOnlyAPITests
14 ReadWriteAPITests, PatchableTestsMixin
1515 )
1616 from faraday.server.models import (
1717 VulnerabilityTemplate,
2323 UserFactory,
2424 VulnerabilityFactory
2525 )
26 from tests.utils.url import v2_to_v3
2627
2728 TEMPLATES_DATA = [
2829 {'name': 'XML Injection (aka Blind XPath Injection) (Type: Base)',
4142 }
4243 ]
4344
45
4446 @pytest.mark.usefixtures('logged_user')
45 class TestListVulnerabilityTemplateView(ReadOnlyAPITests):
47 class TestListVulnerabilityTemplateView(ReadWriteAPITests):
4648 model = VulnerabilityTemplate
4749 factory = factories.VulnerabilityTemplateFactory
4850 api_endpoint = 'vulnerability_template'
4951 view_class = VulnerabilityTemplateView
52 patchable_fields = ['description']
53
54 def check_url(self, url):
55 return url
5056
5157 def test_backwards_json_compatibility(self, test_client, session):
5258 self.factory.create()
8692 def test_create_new_vulnerability_template(self, session, test_client):
8793 vuln_count_previous = session.query(VulnerabilityTemplate).count()
8894 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)
9096 assert res.status_code == 201
9197 assert isinstance(res.json['_id'], int)
9298 assert vuln_count_previous + 1 == session.query(VulnerabilityTemplate).count()
159165 ))
160166 session.commit()
161167
162 query = f'/v2/vulnerability_template/filter?q={{"filters": [' \
168 query = self.check_url(f'/v2/vulnerability_template/filter?q={{"filters": [' \
163169 f'{{ "name": "{filters["field"]}",' \
164170 f' "op": "{filters["op"]}", ' \
165 f' "val": "{filters["filtered_value"]}" }}]}}'
171 f' "val": "{filters["filtered_value"]}" }}]}}')
166172
167173 res = test_client.get(query)
168174 assert res.status_code == 200
197203 ))
198204 session.commit()
199205
200 query = f'/v2/vulnerability_template/filter?q={{"filters": [' \
206 query = self.check_url(f'/v2/vulnerability_template/filter?q={{"filters": [' \
201207 f'{{ "name": "{filters["field"]}",' \
202208 f' "op": "{filters["op"]}", ' \
203 f' "val": "{templates[0].creator.id}" }}]}}'
209 f' "val": "{templates[0].creator.id}" }}]}}')
204210
205211 res = test_client.get(query)
206212 assert res.status_code == 200
236242 ))
237243 session.commit()
238244
239 query = f'/v2/vulnerability_template/filter?q={{"filters": [' \
245 query = self.check_url(f'/v2/vulnerability_template/filter?q={{"filters": [' \
240246 f'{{ "name": "{filters["field"]}",' \
241247 f' "op": "{filters["op"]}", ' \
242 f' "val": "{filters["filtered_value"]}" }}]}}'
248 f' "val": "{filters["filtered_value"]}" }}]}}')
243249
244250 res = test_client.get(query)
245251 assert res.status_code == 200
251257 template = self.factory.create()
252258 session.commit()
253259 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)
255261 assert res.status_code == 200
256262 updated_template = session.query(VulnerabilityTemplate).filter_by(id=template.id).first()
257263 assert updated_template.name == raw_data['name']
273279 session.commit()
274280 raw_data = self._create_post_data_vulnerability_template(
275281 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)
278283 assert res.status_code == 400
279284
280285 def test_update_vulnerabiliy_template_change_refs(self, session, test_client):
284289 self.first_object.reference_template_instances.add(ref)
285290 session.commit()
286291 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)
288293 assert res.status_code == 200
289294 updated_template = session.query(VulnerabilityTemplate).filter_by(id=template.id).first()
290295 assert updated_template.name == raw_data['name']
296301 def test_create_new_vulnerability_template_with_references(self, session, test_client):
297302 vuln_count_previous = session.query(VulnerabilityTemplate).count()
298303 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)
300305 assert res.status_code == 201
301306 assert isinstance(res.json['_id'], int)
302307 assert set(res.json['refs']) == set(['ref1', 'ref2'])
307312 def test_delete_vuln_template(self, session, test_client):
308313 template = self.factory.create()
309314 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}/'))
311316
312317 assert res.status_code == 204
313318 assert vuln_count_previous - 1 == session.query(VulnerabilityTemplate).count()
435440 'csrf_token': csrf_token
436441 }
437442 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/'),
439444 data=data, headers=headers, use_json_data=False)
440445 assert res.status_code == 200
441446 assert len(res.json['vulns_created']) == expected_created_vuln_template
453458 'csrf_token': csrf_token
454459 }
455460 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/'),
457462 data=data, headers=headers, use_json_data=False)
458463 assert res.status_code == 200
459464 assert len(res.json['vulns_created']) == expected_created_vuln_template
471476 'csrf_token': csrf_token
472477 }
473478 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/'),
475480 data=data, headers=headers, use_json_data=False)
476481 assert res.status_code == 200
477482 assert len(res.json['vulns_created']) == expected_created_vuln_template
489494 'csrf_token': csrf_token
490495 }
491496 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/'),
493498 data=data, headers=headers, use_json_data=False)
494499 assert res.status_code == 400
495500 assert 'name' not in res.data.decode('utf8')
513518 'csrf_token': csrf_token
514519 }
515520 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/'),
517522 data=data, headers=headers, use_json_data=False)
518523 assert res.status_code == 200
519524 assert len(res.json['vulns_created']) == 1
535540 'vulns': [vuln_1, vuln_2]
536541 }
537542
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)
539544 assert res.status_code == 200
540545
541546 vulns_created = res.json['vulns_created']
553558 'vulns': [vuln_1, vuln_2]
554559 }
555560
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)
557562 assert res.status_code == 403
558563 assert res.json['message'] == 'Invalid CSRF token.'
559564
560565 def test_bulk_create_without_data(self, test_client, csrf_token):
561566 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)
563568
564569 assert res.status_code == 400
565570 assert res.json['message'] == 'Missing data to create vulnerabilities templates.'
580585 'vulns': [vuln_1, vuln_2]
581586 }
582587
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)
584589 assert res.status_code == 200
585590
586591 assert len(res.json['vulns_with_conflict']) == 1
608613 'vulns': [vuln_1, vuln_2]
609614 }
610615
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)
612617 assert res.status_code == 409
613618
614619 assert len(res.json['vulns_with_conflict']) == 2
616621 assert res.json['vulns_with_conflict'][1][1] == vuln_2['name']
617622
618623 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)
77
88 import pytest
99 from faraday.server.api.modules.websocket_auth import decode_agent_websocket_token
10 from tests.utils.url import v2_to_v3
1011
1112
1213 class TestWebsocketAuthEndpoint:
14 def check_url(self, url):
15 return url
1316
1417 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/'))
1619 assert res.status_code == 401
1720
1821 @pytest.mark.usefixtures('logged_user')
1922 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/'))
2124 assert res.status_code == 405
2225
2326 @pytest.mark.usefixtures('logged_user')
2427 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/'))
2629 assert res.status_code == 200
2730
2831 # A token for that workspace should be generated,
3134 assert res.json['token'].startswith(str(workspace.id))
3235
3336
37 class TestWebsocketAuthEndpointV3(TestWebsocketAuthEndpoint):
38 def check_url(self, url):
39 return v2_to_v3(url)
40
41
3442 class TestAgentWebsocketToken:
43
44 def check_url(self, url):
45 return url
46
3547 @pytest.mark.usefixtures('session') # I don't know why this is required
3648 def test_fails_without_authorization_header(self, test_client):
3749 res = test_client.post(
38 '/v2/agent_websocket_token/',
50 self.check_url('/v2/agent_websocket_token/')
3951 )
4052 assert res.status_code == 401
4153
4254 @pytest.mark.usefixtures('logged_user')
4355 def test_fails_with_logged_user(self, test_client):
4456 res = test_client.post(
45 '/v2/agent_websocket_token/',
57 self.check_url('/v2/agent_websocket_token/')
4658 )
4759 assert res.status_code == 401
4860
4961 @pytest.mark.usefixtures('logged_user')
5062 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/'))
5264
5365 assert res.status_code == 200
5466
5769 # clean cookies make sure test_client has no session
5870 test_client.cookie_jar.clear()
5971 res = test_client.post(
60 '/v2/agent_websocket_token/',
72 self.check_url('/v2/agent_websocket_token/'),
6173 headers=headers,
6274 )
6375 assert res.status_code == 401
6678 def test_fails_with_invalid_agent_token(self, test_client):
6779 headers = [('Authorization', 'Agent 13123')]
6880 res = test_client.post(
69 '/v2/agent_websocket_token/',
81 self.check_url('/v2/agent_websocket_token/'),
7082 headers=headers,
7183 )
7284 assert res.status_code == 403
7890 assert agent.token
7991 headers = [('Authorization', 'Agent ' + agent.token)]
8092 res = test_client.post(
81 '/v2/agent_websocket_token/',
93 self.check_url('/v2/agent_websocket_token/'),
8294 headers=headers,
8395 )
8496 assert res.status_code == 200
8698 assert decoded_agent == agent
8799
88100
89 # I'm Py3
101 class TestAgentWebsocketTokenV3(TestAgentWebsocketToken):
102 def check_url(self, url):
103 return v2_to_v3(url)
66
77 import time
88 import pytest
9 from posixpath import join as urljoin
910
1011 from faraday.server.models import Workspace, Scope
1112 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
1314 from tests import factories
15 from tests.utils.url import v2_to_v3
16
1417
1518 class TestWorkspaceAPI(ReadWriteAPITests):
1619 model = Workspace
1821 api_endpoint = 'ws'
1922 lookup_field = 'name'
2023 view_class = WorkspaceView
24 patchable_fields = ['name']
25
26 def check_url(self, url):
27 return url
2128
2229 @pytest.mark.usefixtures('ignore_nplusone')
2330 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 )
2637 assert res.status_code == 200
2738 assert len(res.json) == 1
2839 assert res.json[0]['name'] == self.first_object.name
2940
3041 @pytest.mark.usefixtures('ignore_nplusone')
3142 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 )
3449 assert res.status_code == 200
3550 assert len(res.json) == 0
3651
3752 def test_filter_restless_by_description(self, test_client):
3853 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 )
4161 assert res.status_code == 200
4262 assert len(res.json) == 1
4363 assert res.json[0]['description'] == self.first_object.description
6787 session.commit()
6888
6989 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 )
7297 assert res.status_code == 200
7398 assert len(res.json) == 1
7499 assert res.json[0]['description'] == self.first_object.description
226251 end_date = start_date+86400000
227252 duration = {'start_date': start_date, 'end_date': end_date}
228253 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)
230255 assert res.status_code == 201
231256 assert workspace_count_previous + 1 == session.query(Workspace).count()
232257 assert res.json['duration']['start_date'] == start_date
235260 def test_create_fails_with_mayus(self, session, test_client):
236261 workspace_count_previous = session.query(Workspace).count()
237262 raw_data = {'name': 'sWtr'}
238 res = test_client.post('/v2/ws/', data=raw_data)
263 res = test_client.post(self.url(), data=raw_data)
239264 assert res.status_code == 400
240265 assert workspace_count_previous == session.query(Workspace).count()
241266
242267 def test_create_fails_with_special_character(self, session, test_client):
243268 workspace_count_previous = session.query(Workspace).count()
244269 raw_data = {'name': '$wtr'}
245 res = test_client.post('/v2/ws/', data=raw_data)
270 res = test_client.post(self.url(), data=raw_data)
246271 assert res.status_code == 400
247272 assert workspace_count_previous == session.query(Workspace).count()
248273
249274 def test_create_with_initial_number(self, session, test_client):
250275 workspace_count_previous = session.query(Workspace).count()
251276 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)
253278 assert res.status_code == 201
254279 assert workspace_count_previous + 1 == session.query(Workspace).count()
255280
260285 start_date = 'this should clearly fail'
261286 duration = {'start_date': start_date, 'end_date': 86400000}
262287 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)
264289 assert res.status_code == 400
265290 assert workspace_count_previous == session.query(Workspace).count()
266291
272297 start_date = int(time.time())*1000
273298 duration = {'start_date': start_date, 'end_date': start_date-86400000}
274299 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)
276301 assert res.status_code == 400
277302 assert workspace_count_previous == session.query(Workspace).count()
278303
280305 description = 'darkside'
281306 raw_data = {'name': 'something', 'description': description}
282307 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)
284309 assert res.status_code == 201
285310 assert workspace_count_previous + 1 == session.query(Workspace).count()
286311 assert res.json['description'] == description
291316 ])
292317 def test_create_stat_is_zero(self, test_client, stat_name):
293318 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)
295320 assert res.status_code == 201
296321 assert res.json['stats'][stat_name] == 0
297322
303328 session.add_all(vulns)
304329 session.commit()
305330 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)
308332 assert res.status_code == 200
309333 assert res.json['stats']['web_vulns'] == 5
310334 assert res.json['stats']['std_vulns'] == 10
317341 ]
318342 raw_data = {'name': 'something', 'description': 'test',
319343 'scope': desired_scope}
320 res = test_client.post('/v2/ws/', data=raw_data)
344 res = test_client.post(self.url(), data=raw_data)
321345 assert res.status_code == 201
322346 assert set(res.json['scope']) == set(desired_scope)
323347 workspace = Workspace.query.get(res.json['id'])
332356 ]
333357 raw_data = {'name': 'something', 'description': 'test',
334358 '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)
336360 assert res.status_code == 200
337361 assert set(res.json['scope']) == set(desired_scope)
338362 assert set(s.name for s in workspace.scope) == set(desired_scope)
375399 workspace_count_previous = session.query(Workspace).count()
376400 duration = {'start_date': 1563638577, 'end_date': 1563538577}
377401 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)
379403 assert res.status_code == 400
380404 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
55
66 '''
77 from builtins import str
8 from posixpath import join as urljoin
9
10 from tests.utils.url import v2_to_v3
811
912 """Generic tests for APIs prefixed with a workspace_name"""
1013
100103 res = test_client.get(self.url(self.first_object, second_workspace))
101104 assert res.status_code == 404
102105
103 @pytest.mark.parametrize('object_id', [12345, -1, 'xxx', u'áá'])
106 @pytest.mark.parametrize('object_id', [123456789, -1, 'xxx', u'áá'])
104107 def test_404_when_retrieving_unexistent_object(self, test_client,
105108 object_id):
106109 url = self.url(object_id)
112115
113116 def test_create_succeeds(self, test_client):
114117 data = self.factory.build_dict(workspace=self.workspace)
118 count = self.model.query.count()
115119 res = test_client.post(self.url(),
116120 data=data)
117121 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']
120124 obj = self.model.query.get(object_id)
121125 assert obj.workspace == self.workspace
122126
124128 self.workspace.readonly = True
125129 db.session.commit()
126130 data = self.factory.build_dict(workspace=self.workspace)
131 count = self.model.query.count()
127132 res = test_client.post(self.url(),
128133 data=data)
129134 db.session.commit()
130135 assert res.status_code == 403
131 assert self.model.query.count() == OBJECT_COUNT
136 assert self.model.query.count() == count
132137
133138
134139 def test_create_inactive_fails(self, test_client):
135140 self.workspace.deactivate()
136141 db.session.commit()
137142 data = self.factory.build_dict(workspace=self.workspace)
143 count = self.model.query.count()
138144 res = test_client.post(self.url(),
139145 data=data)
140146 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
142148
143149 def test_create_fails_with_empty_dict(self, test_client):
144150 res = test_client.post(self.url(), data={})
171177
172178 class UpdateTestsMixin:
173179
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):
175185 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)
178194 assert res.status_code == 200
179 assert self.model.query.count() == OBJECT_COUNT
195 assert self.model.query.count() == count
180196 for updated_field in self.update_fields:
181197 assert res.json[updated_field] == getattr(self.first_object,
182198 updated_field)
183199
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):
185202 self.workspace.readonly = True
186203 db.session.commit()
187204 for unique_field in self.unique_fields:
188205 data = self.factory.build_dict()
189206 old_field = getattr(self.objects[0], unique_field)
190207 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)
192212 db.session.commit()
193213 assert res.status_code == 403
194214 assert self.model.query.count() == OBJECT_COUNT
195215 assert old_field == getattr(self.model.query.filter(self.model.id == old_id).one(), unique_field)
196216
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):
198219 self.workspace.deactivate()
199220 db.session.commit()
200221 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)
203229 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):
207234 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})
211242 assert res.status_code == 409
212243 assert self.model.query.count() == OBJECT_COUNT
213244
216247 res = test_client.put(self.url(self.first_object), data={})
217248 assert res.status_code == 400
218249
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):
220252 raw_json = self.factory.build_dict(workspace=self.workspace)
253 raw_json = self.control_cant_change_data(raw_json)
221254 expected_id = self.first_object.id
222255 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)
228297
229298 class CountTestsMixin:
230299 def test_count(self, test_client, session, user_factory):
231300
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
232308 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
237319 assert res.status_code == 200, res.json
238320 res = res.get_json()
239321
244326 grouped += 1
245327 creators.append(obj['creator_id'])
246328
247 assert grouped == 1
329 assert grouped == 1, (res)
248330 assert creators == sorted(creators)
249331
250332 def test_count_descending(self, test_client, session, user_factory):
251333
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
252341 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
257352 assert res.status_code == 200, res.json
258353 res = res.get_json()
259354
264359 grouped += 1
265360 creators.append(obj['creator_id'])
266361
267 assert grouped == 1
362 assert grouped == 1, res
268363 assert creators == sorted(creators, reverse=True)
269364
270365
354449 res = test_client.get(self.url())
355450 assert res.status_code == 200
356451 assert len(res.json['data']) == OBJECT_COUNT
452
453 class ReadWriteMultiWorkspacedAPITests(ReadOnlyMultiWorkspacedAPITests,
454 ReadWriteTestsMixin):
455 pass
44 update_executors, BroadcastServerProtocol
55
66 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
178
189
1910 class TransportMock:
3627
3728 class TestWebsocketBroadcastServerProtocol:
3829
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
3942 def test_join_agent_message_with_invalid_token_fails(self, session, proto, test_client):
4043 message = '{"action": "JOIN_AGENT", "token": "pepito" }'
4144 assert not proto.onMessage(message, False)
4548 assert not proto.onMessage(message, False)
4649
4750 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)
4952 message = f'{{"action": "JOIN_AGENT", "workspace": "{workspace.name}", "token": "{token}", "executors": [] }}'
5053 assert proto.onMessage(message, False)
5154
5255 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)
5457 message = f'{{"action": "JOIN_AGENT", "workspace": "{workspace.name}", "token": "{token}", "executors": [] }}'
5558 assert proto.onMessage(message, False)
5659
5861 assert proto.onMessage(message, False)
5962
6063 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)
6265 agent = Agent.query.one()
6366 assert not agent.is_online
6467 message = f'{{"action": "JOIN_AGENT", "workspace": "{workspace.name}", "token": "{token}", "executors": [] }}'
6871 message = '{"action": "LEAVE_AGENT"}'
6972 assert proto.onMessage(message, False)
7073 assert not agent.is_online
74
75
76 class TestWebsocketBroadcastServerProtocolV3(TestWebsocketBroadcastServerProtocol):
77 def check_url(self, url):
78 return v2_to_v3(url)
7179
7280
7381 class TestCheckExecutors:
(New empty file)
0 def v2_to_v3(url):
1 if url.endswith("/"):
2 url = url[:-1]
3 return url.replace("/v2/", "/v3/", 1)