def set_password(): """Set initial customer password. The template for this route contains bootstrap.css, bootstrap-theme.css and main.css. This is similar to the password reset option with two exceptions: it has a longer expiration time and does not require old password. :param token: Token generated by :meth:`app.models.User.generate_reset_token` :return: """ token = request.json['token'] password = request.json['password'] s = TimedJSONWebSignatureSerializer(current_app.config['SECRET_KEY']) try: s.loads(token) except BadSignature: raise ApiException('Invalid Token', 401) try: User.set_password(token, password) except AttributeError as e: raise ApiException(str(e), 401) return ApiResponse({'auth': 'authenticated'})
def query(): """ :status 501: NotImplemented :return: """ raise ApiException('Not Implemented', status=501)
def subscribe_list(list_id): """Subscribe email(s) to distribution list **Example request**: .. sourcecode:: http PUT /api/1.0/lists/certs.lists.cert.europa.eu/subscribe HTTP/1.1 Host: do.cert.europa.eu Accept: application/json Content-Type: application/json { "emails": [ "*****@*****.**", "*****@*****.**" ] } **Example response**: .. sourcecode:: http HTTP/1.0 200 OK Content-Type: application/json { "message": "List saved" } :param list_id: Distribution list ID :reqheader Accept: Content type(s) accepted by the client :resheader Content-Type: this depends on `Accept` header or request :<json array emails: E-mails for mass-subscription :>json string message: Status message :status 200: Group was deleted :status 400: Bad request """ l = MailmanList.get(fqdn_listname=list_id) try: data = [request.json['email']] except (KeyError, TypeError) as ke: current_app.log.info(ke) data = request.json['emails'] for email in data: try: l.subscribe(address=email.lower(), pre_verified=True, pre_confirmed=True) fetch_gpg_key(email.lower(), current_app.config['GPG_KEYSERVERS'][0]) except HTTPError as he: raise ApiException(email.lower() + ': ' + he.msg.decode(), he.code) return ApiResponse({'message': 'List saved'})
def submit_gpg_key(): """Submit GPG key to CERT-EU keyserver Keys are send to the first server from the GPG_KEYSERVERS configuration option **Example request**: .. sourcecode:: http POST /submit-key HTTP/1.1 Host: do.cert.europa.eu Accept: application/json Content-Type: application/json { "ascii_key": "-----BEGIN PGP PUBLIC KEY BLOCK-----nmQENBFHn ...-----END PGP PUBLIC KEY BLOCK-----" } **Example response**: .. sourcecode:: http HTTP/1.0 201 CREATED Content-Type: application/json { "fingerprints": [ "2D39D3A9ACCD18B1D7774A00A485C88DDA2AA2BF" ], "message": "Key saved" } :reqheader Accept: Content type(s) accepted by the client :resheader Content-Type: this depends on `Accept` header or request :<json string ascii_key: ASCII armored GPG public key :>json array fingerprints: List of fingerprints :>json string message: Status message :statuscode 201: GPG key successfully saved :statuscode 400: Bad request """ result = gpg.gnupg.import_keys(request.json['ascii_key']) if result.fingerprints: send_to_ks.delay( current_app.config['GPG_KEYSERVERS'][0], result.fingerprints ) return ApiResponse({ 'message': 'Key saved', 'fingerprints': [f for f in result.fingerprints]}, 201) else: raise ApiException('The PGP Key could not be imported')
def toggle_2fa(): """Toggle Two-Factor authentication **Example request**: .. sourcecode:: http POST /auth/toggle-2fa HTTP/1.1 Host: do.cert.europa.eu Content-Type: application/json { "otp_toggle": true, "totp": 453007 } **Example success response**: .. sourcecode:: http HTTP/1.0 200 OK Content-Type: application/json Content-Length: 31 { "message": "Your options have been saved" } :reqheader Accept: Content type(s) accepted by the client :resheader Content-Type: this depends on `Accept` header or request :<json boolean otp_toggle: Enable or disable second authentication factor :<json integer totp: 6 digit TOTP :>json string message: Action status :statuscode 200: Password successfully changed """ otp_toggle = request.json.get('otp_toggle', False) totp = request.json.pop('totp', False) user = User.query.filter_by(id=current_user.id).first_or_404() if not otp_toggle: user.otp_enabled = False db.session.add(user) db.session.commit() return ApiResponse({'message': 'Your options have been saved'}) if otp_toggle and user.verify_totp(totp): user.otp_enabled = True else: raise ApiException('Authentication code verification failed') db.session.add(user) db.session.commit() return ApiResponse({'message': 'Your options have been saved'})
def get_fireeye_report(sha256, envid, type): raise ApiException({}, 501) # XML, HTML, BIN and PCAP are GZipped Sample.query.filter_by(sha256=sha256).first_or_404() headers = { 'Accept': 'text/html', 'User-Agent': 'FireEye Sandbox API Client' } params = {'type': type, 'environmentId': envid} vx = fireeye.api.get('result/{}'.format(sha256), params=params, headers=headers) if type in ['xml', 'html', 'bin', 'pcap']: return gzip.decompress(vx) return vx
def get_fireeye_download(sha256, eid, ftype): raise ApiException({}, 501) Sample.query.filter_by(sha256=sha256).first_or_404() headers = { 'Accept': 'text/html', 'User-Agent': 'FireEye Sandbox API Client' } params = {'type': ftype, 'environmentId': eid} vx = fireeye.api.get('result/{}'.format(sha256), params=params, headers=headers) if ftype in ['xml', 'html', 'bin', 'pcap']: ftype += '.gz' return send_file(BytesIO(vx), attachment_filename='{}.{}'.format(sha256, ftype), as_attachment=True)
def do_ldap_authentication(username, password): """Authenticate users with CERT-EU LDAP server :param username: CERT-EU email or username :param password: Account password """ if '@' in username: ldap_user = username.split('@')[0] else: ldap_user = username ldap_info, ldap_authenticated = _ldap_authenticate(ldap_user, password) if ldap_authenticated: u = User.query.filter_by( email=ldap_info['userPrincipalName'][0]).first() if not u: _save_ldap_user(ldap_info) u = User.query.filter_by( email=ldap_info['userPrincipalName'][0]).first() if login_user(u, remember=True): return ApiResponse({'auth': 'authenticated'}, 200) raise ApiException('Invalid username or password', 401)
def get_fireeye_download(sha256, eid, ftype): raise ApiException({}, 501)
def get_fireeye_report(sha256, envid, type): raise ApiException({}, 501)
def get_fireeye_analyses(): raise ApiException({}, 501)
def add_fireeye_url_analysis(): """Submit URLs to the FireEye Sandbox. Also accepts :http:method:`put`. .. warning:: Not Implemented **Example request**: .. sourcecode:: http POST /api/1.0/analysis/fireeye-url HTTP/1.1 Host: do.cert.europa.eu Accept: application/json Content-Type: application/json { "urls": ["http://cert.europa.eu"], "dyn_analysis": { "fireeye": [5, 6] } } **Example response**: .. sourcecode:: http HTTP/1.0 200 OK Content-Type: application/json { "message": "Your URLs have been submitted for dynamic analysis", "statuses": [ { "sha256": "33a53e3b28ee41c29afe79f49ecc53b34ac125e5e15f9e7c..." } ] } :reqheader Accept: Content type(s) accepted by the client :resheader Content-Type: this depends on `Accept` header or request :<json array files: List of URLs to scan :<jsonarr string sha256: SHA256 of the shortcut file created :<json object dyn_analysis: Sandbox configuration :<jsonarr array fireeye: List of FireEye environments to submit to Get the list from :http:get:`/api/1.0/analysis/fireeye/environments` :>json array statuses: List of statuses returned by upstream APIs :>jsonarr string error: Error message from upstream API :>jsonarr string sha256: SHA256 calculated by upstream API :>json string message: Status message :status 202: The URLs have been accepted for scanning :status 400: Bad request """ raise ApiException({}, 501) statuses = [] samples = {} for env in request.json['dyn_analysis']['fireeye']: for url in request.json['urls']: sdata = {'environmentId': env, 'analyzeurl': url} headers = { 'User-Agent': 'FireEye Sandbox API Client', 'Accept': 'application/json' } resp = fireeye.submiturl(sdata, headers=headers) samples[resp['response']['sha256']] = url statuses.append(resp['response']) if resp['response_code'] != 0: current_app.log.debug(resp) for sha256, url in samples.items(): surl = Sample(filename=url, sha256=sha256, user_id=g.user.id, md5='N/A', sha1='N/A', sha512='N/A', ctph='N/A') db.session.add(surl) db.session.commit() return { 'statuses': statuses, 'message': 'Your URLs have been submitted for dynamic analysis' }, 202
def get_fireeye_analyses(): """Return a paginated list of FireEye Sandbox JSON reports. .. warning:: Not Implemented **Example request**: .. sourcecode:: http GET /api/1.0/analysis/fireeye?page=1 HTTP/1.1 Host: do.cert.europa.eu Accept: application/json **Example response**: .. sourcecode:: http HTTP/1.0 200 OK Content-Type: application/json DO-Page-Next: null DO-Page-Prev: null DO-Page-Current: 1 DO-Page-Item-Count: 1 { "count": 3, "items": [ { "created": "2016-03-21T16:52:52", "id": 4, "report": "...", "type": "Dynamic analysis" }, { "created": "2016-03-21T16:51:49", "id": 3, "report": "...", "type": "Dynamic analysis" }, { "created": "2016-03-20T17:09:03", "id": 2, "report": "...", "type": "Dynamic analysis" } ], "next": null, "page": 1, "prev": null } :reqheader Accept: Content type(s) accepted by the client :resheader Content-Type: this depends on `Accept` header or request :resheader DO-Page-Next: Next page URL :resheader DO-Page-Prev: Previous page URL :resheader DO-Page-Curent: Current page number :resheader DO-Page-Item-Count: Total number of items :>json array items: FireEye reports :>jsonarr integer id: AV scan unique ID :>jsonarr string name: File name :>jsonarr string sha256: SHA256 message-digest of file :>json integer page: Current page number :>json integer prev: Previous page number :>json integer next: Next page number :>json integer count: Total number of items :status 200: Reports found :status 404: Resource not found """ raise ApiException({}, 501)
def login(): """Authenticate users To authenticate make a JSON POST request with credentials. On success the API will return a ``rm`` cookie valid for 48 hours. Use the value of the ``rm`` cookie for all subsequent requests. The ``CP-TOTP-Required`` header notifies clients that they need to submit their 2FA token. Token will be check at :http:post:`/auth/verify-totp` .. note:: 2FA is used for CP requests only **Example request**: .. sourcecode:: http POST /auth/login HTTP/1.1 Host: do.cert.europa.eu Accept: application/json Content-Type: application/json { "email": "*****@*****.**", "password":"******" } **Example response**: .. sourcecode:: http HTTP/1.0 200 OK Content-Type: application/json Set-Cookie: rm=.eGwfP...; Expires=Sun, 29-Nov-2015 09:11:45 GMT; Path=/ Set-Cookie: session=.eJwlzrs...; Secure; HttpOnly; Path=/ { "auth": "authenticated" } :reqheader Accept: Content type(s) accepted by the client :resheader Content-Type: this depends on `Accept` header or request :resheader Set-Cookie: Pass the `rm` cookie to future requests :resheader CP-TOTP-Required: When true client must submit TOTP at :http:post:`/auth/verify-totp` :<json string email: Email or username :<json string password: Password :>json string auth: Authentication status :statuscode 200: Login successful :statuscode 401: Invalid credentials """ if current_user.is_authenticated: return ApiResponse({'auth': 'authenticated'}) email = request.json.get('email') or request.json.get('username') password = request.json.get('password') if email and password: cp_host = request.environ.get('HTTP_HOST', None) if cp_host.startswith('cp.'): user, authenticated = User.authenticate(email, password) if user and authenticated: if user.otp_enabled: session['cu'] = email session['cpasswd'] = password return ApiResponse({'auth': 'pre-authenticated'}, 200, {'CP-TOTP-Required': user.otp_enabled}) else: if login_user(user, remember=True): return ApiResponse({'auth': 'authenticated'}) raise ApiException('Invalid username or password', 401) if current_app.config['LDAP_AUTH_ENABLED']: return do_ldap_authentication(email, password) user, authenticated = User.authenticate(email, password) if user and authenticated: if login_user(user, remember=True): return ApiResponse({'auth': 'authenticated'}) raise ApiException('Invalid username or password', 401)
def post_message(): """Send email to mailing list. If ``encrypted`` is set to ``True`` the message body and all attachments will be encrypted with the public keys of all list members .. todo:: Add icons: https://twitter.com/JZdziarski/status/753223642297892864 Files in response.json.files are assumed uploaded via :http:post:`/api/1.0/upload` **Example request**: .. sourcecode:: http POST /api/1.0/lists/post HTTP/1.1 Host: do.cert.europa.eu Accept: application/json { "files": [ "targets.txt" ], "list_id": "certs.lists.cert.europa.eu", "subject": "zZz", "encrypt": false, "content": "ž" } **Example response**: .. sourcecode:: http HTTP/1.0 200 OK Content-Type: application/json { "message": "Email has been sent." } .. warning:: There is no error checking :reqheader Accept: Content type(s) accepted by the client :resheader Content-Type: this depends on `Accept` header or request :<json string list_id: Distribution list ID :<json string subject: E-mail subject :<json string content: E-mail content :<json array files: Files to attach :>json string message: Status message :status 200: Email successfully sent :status 400: Bad request :status 500: Error processing the request """ msg = request.json files = msg.get('files', None) encrypted = msg.get('encrypted', None) list_ = MailmanList.get(fqdn_listname=msg['list_id']) if encrypted: enc = _encrypt(BytesIO(msg['content'].encode('utf-8')), list_=list_, always_trust=True) if not enc.ok: raise ApiException('Could not encrypt message content. ' 'Please make sure you have all the keys.') content = enc.data.decode() attachedfiles = [] if files: for file in files: file_path = os.path.join(current_app.config['APP_UPLOADS'], file) with open(file_path, 'rb') as fb: outfile = file_path + '.asc' cipherfile = _encrypt(fb, list_, output=outfile, always_trust=True) if cipherfile.ok: attachedfiles.append(file + '.asc') else: content = msg['content'] attachedfiles = files send_email('*****@*****.**', [list_.fqdn_listname], msg['subject'], content, attach=attachedfiles) return ApiResponse({'message': 'Email has been sent.'})
def search_public_ks(email=None): """Search GPG keys on public keyserver pool. The keysever pool is the second server from the GPG_KEYSERVERS configuration option. **Example request**: .. sourcecode:: http POST /api/1.0/search-keys HTTP/1.1 Host: do.cert.europa.eu Accept: application/json Content-Type: application/json { "email": "*****@*****.**" } **Example response**: .. sourcecode:: http HTTP/1.0 200 OK Content-Type: application/json { "keys": [ { "algo": "1", "date": "1379079830", "expires": "1479375689", "keyid": "8CC4185CF057F6F8690309DD28432835514AA0F6", "length": "4096", "type": "pub", "uids": [ "Alexandru Ciobanu <*****@*****.**>" ] } ] } :reqheader Accept: Content type(s) accepted by the client :resheader Content-Type: this depends on `Accept` header or request :<json string email: E-mail address :>json array keys: List of found keys :>jsonarr string algo: `Key algorithm <https://tools.ietf.org/html/rfc4880#section-9.1>`_ :>jsonarr string date: Creation date :>jsonarr string expires: Expiration date :>jsonarr string keyid: KeyID date :>jsonarr string length: Key size :>jsonarr string type: Key type. Only public keys are returned :>jsonarr array uids: Key type. Only public keys are returned :statuscode 200: GPG keys found :statuscode 404: No keys found for given email address :param email: """ if email is None: email = request.json['email'] keys = gpg.gnupg.search_keys( email, current_app.config['GPG_KEYSERVERS'][1]) if not keys: raise ApiException('No keys found', 404) return ApiResponse({'keys': keys})
def check_gpg(list_id): """Try to encrypt a message to all members of `list_id`. .. note:: The GPG client only returns errors about the last recipient :param list_id: List FQDN name. E.g. ``certs.lists.cert.europa.eu`` **Example request**: .. sourcecode:: http GET /api/1.0/lists/certs.lists.cert.euroap.eu/check_gpg HTTP/1.1 Host: do.cert.europa.eu Accept: application/json **Example response**: .. sourcecode:: http HTTP/1.0 200 OK Content-Type: application/json { "message": "Sample data successfuly encrypted for this list" } **Example errpr response**: .. sourcecode:: http HTTP/1.0 400 BAD REQUEST Content-Type: application/json { "message": "Your message can not be encrypted for all the members of this list", "stderr": "gpg: [email protected]: skipped: No public key [GNUPG:] INV_RECP 1 [email protected] gpg: [stdin]: encryption failed: No public key" } .. note:: stderr contains information only about the last recipient :reqheader Accept: Content type(s) accepted by the client :resheader Content-Type: this depends on `Accept` header or request :>json string message: Status message :>json string stderr: STDERR :status 200: Email successfully sent :status 400: An error has occured while trying to encrypt """ l = MailmanList.get(fqdn_listname=list_id) emails = [m.email for m in l.members] if not emails: raise ApiException('This list has no members.') enc = gpg.gnupg.encrypt('test', emails, always_trust=True) if enc.ok: return ApiResponse({'message': 'Test successful.'}) raise ApiException('Your message can not be encrypted for all ' 'the members of this list ' + enc.stderr)
def change_password(): """Change password **Example request**: .. sourcecode:: http POST /auth/change-password HTTP/1.1 Host: do.cert.europa.eu Content-Type: application/json { "current_password": "******", "new_password": "******", "confirm_password": "******" } **Example success response**: .. sourcecode:: http HTTP/1.0 200 OK Content-Type: application/json Content-Length: 31 { "message": "Password updated" } **Example error response**: .. sourcecode:: http HTTP/1.0 422 UNPROCESSABLE ENTITY Content-Type: application/json Content-Length: 87 { "message": "'current_password' is a required property", "validator": "required" } :reqheader Accept: Content type(s) accepted by the client :resheader Content-Type: this depends on `Accept` header or request :<json string current_password: Current password :<json string new_password: New password :<json string confirm_password: Password confirmation :>json string message: Action status :statuscode 200: Password successfully changed :statuscode 422: Unprocessable Entity. """ if not current_user.check_password(request.json.get('current_password')): raise ApiException('Invalid current password') # it makes no sense to check the current password # if not current_user.check_password(request.json.get('current_password')): # return {'message': 'Invalid current password'}, 400 new_pass = request.json.get('new_password', None) confirm_pass = request.json.get('confirm_password', None) if new_pass != confirm_pass: raise ApiException('Confirmation password does not match') try: current_user.password = request.json.get('new_password') db.session.add(current_user) db.session.commit() return ApiResponse({'message': 'Your password has been updated'}) except AssertionError as ae: raise ApiException(ae)
def get_fireeye_analysis(sha256, envid): raise ApiException({}, 501)
def get_fireeye_analysis(sha256, envid): """Return FireEye Sandbox dynamic analysis for sample identified by :attr:`~app.models.Sample.sha256`, running in :param: envid. .. note:: FireEye Sandbox REST API returns mixed responses. Most errors will return :http:statuscode:`200`. **Example request**: .. sourcecode:: http GET api/1.0/analysis/fireeye/54abd4674f61029d9ae3f8f805b9b7/1 HTTP/1.1 Host: do.cert.europa.eu Accept: application/json **Example success response**: .. sourcecode:: http HTTP/1.0 200 OK Content-Type: application/json { "response": { "analysis_start_time": "2016-04-28 17:03:52", "domains": [], "environmentDescription": "Windows 7 64 bit (KERNELMODE)", "environmentId": "6", "hosts": [ "40.118.103.7" ], "isinteresting": false, "isurlanalysis": false, "md5": "864cc77a27d4618149ec0bba060bbca0", "multiscan_detectrate_pcnt": 0, "sha1": "31fced6d00e58147bff56902b986fd0cc6295aeb", "sha256": "54abd4674f61029d9ae3f8f8f5a484d396d10b87c9dc77765d87c2", "size": 336384, "submitname": "54abd4674f61029d9ae3f8f8f5a484d396d10b87c9dc777657", "targeturl": "", "threatlevel": 1, "threatscore": 41, "type": "PE32 executable (GUI) Intel 80386 Mono/.Net assembly" }, "response_code": 0 } **Example error response**: .. sourcecode:: http HTTP/1.0 200 OK Content-Type: application/json { "response": { "error": "Failed to save report to webservice", "state": "ERROR" }, "response_code": 0 } :param sha256: SHA256 of file :param envid: Environment ID. For the list of available environments see: :http:get:`/api/1.0/analysis/fireeye/environments`. :reqheader Accept: Content type(s) accepted by the client :resheader Content-Type: this depends on `Accept` header or request :>json integer response_code: Response code. ``0`` for success, ``-1`` for errors :>json object response: Analysis summary or error details :>jsonobj string analysis_start_time: Analysis start time :>jsonobj array domains: Domains contacted during analysis :>jsonobj string environmentDescription: Environment details :>jsonobj string environmentId: Environment unique ID :>jsonobj array hosts: Hosts contacted during analysis :>jsonobj boolean isinteresting: :>jsonobj boolean isurlanalysis: Marker for URL analyzer :>jsonobj integer multiscan_detectrate_pcnt: :>jsonobj string md5: MD5 digest calculated upstream :>jsonobj string sha1: SHA1 digest calculated upstream :>jsonobj string sha256: SHA256 digest calculated upstream :>jsonobj integer size: Size of sample in bytes :>jsonobj string submitname: Submission file name :>jsonobj string targeturl: :>jsonobj integer threatlevel: Threatlevel :>jsonobj integer threatscore: Threat score :>jsonobj string type: Type of sample :>jsonobj string state: State of analysis :>jsonobj string error: Error message reported by upstream :status 200: Request successful. ``response_code`` check required to determine if action was successful. :status 404: Resource not found """ raise ApiException({}, 501) sample = Sample.query.filter_by(sha256=sha256).first_or_404() state = fireeye.api.get('state/{}'.format(sha256), params={'environmentId': envid}) status = state['response_code'] == 0 if status and state['response']['state'] == 0: #: FIXME: return the fireeye.api.get('result/sha256') #: and create a summary from that #: offer HTML & JSON downloads params = {'type': 'json', 'environmentId': envid} if sample.filename.startswith('http'): vx = { 'response': { 'analysis_start_time': '1', 'environmentId': envid }, 'response_code': 0 } else: vx = fireeye.api.get('summary/{}'.format(sha256), params=params) return ApiResponse(vx) else: state['response']['environmentId'] = envid return ApiResponse(state)
def add_fireeye_url_analysis(): raise ApiException({}, 501)
def verify_totp(): """Check the `TOTP <https://en.wikipedia.org/wiki/Time-based_One-time_Password_Algorithm>`_ submitted by the client. **Example request**: .. sourcecode:: http POST /auth/verify-totp HTTP/1.1 Host: do.cert.europa.eu Accept: application/json Content-Type: application/json { "totp": "123456" } **Example response**: .. sourcecode:: http HTTP/1.0 200 OK Content-Type: application/json Set-Cookie: session=40e418c37cb4cc68_5776d7a0; Secure; HttpOnly; Path=/ Set-Cookie: rm=.eJwNzrEKAjEMANB_OW4USZqkTW_SwcnFQUQQhzZtUPQ44f4fvPVN7z Gsy9wPc3l_97bMw2741U_zMK2vIhgmBMAx8f10vh5vchmLdLS; Expires=Sun, 03-Jul-2016 20:52:58 GMT; Secure; HttpOnly; Path=/ { "auth": "authenticated" } :reqheader Accept: Content type(s) accepted by the client :resheader Content-Type: this depends on `Accept` header or request :resheader Set-Cookie: Pass the `rm` cookie to future requests :<json integer totp: 6 digit authentication code generated by one of these TOTP applications: `Google Authenticator <https://support.google.com/accounts/answer/1066447?hl=en>`_, `Duo Mobile <https://guide.duo.com/third-party-accounts>`_, `Authenticator <https://www.microsoft.com/en-US/store/apps/ Authenticator/9WZDNCRFJ3RJ>`_ :>json string auth: Authentication status :status 200: Login successful :status 404: User doesn't have 2FA enabled :status 401: User did not provide the first authentication factor :status 400: Invalid TOTP """ email = session.pop('cu', None) password = session.pop('cpasswd', None) user, authenticated = User.authenticate(email, password) if not authenticated: raise ApiException('Please login first', 401) token = request.json['totp'] user = User.query.filter_by(id=user.id).first_or_404() if not user.otp_enabled: raise ApiException('Verification failed', 400) if user.verify_totp(token): if login_user(user, remember=True): return ApiResponse({'auth': 'authenticated'}) raise ApiException('Authentication code verification failed', 400)