class TechMethodsGet(AuthenticatedService): decorators = [ rbac.allow(["View"], ["GET"]), ] def __init__(self): super(TechMethodsGet, self).__init__() def get(self, tech_ids): """ .. http:get:: /api/1/techmethods Get a list of technologies and associated auditor check methods **Example Request**: .. sourcecode:: http GET /api/1/techmethods HTTP/1.1 Host: example.com Accept: application/json, text/javascript **Example Response**: .. sourcecode:: http HTTP/1.1 200 OK Vary: Accept Content-Type: application/json { "technologies": [ "subnet" ] "tech_methods": { "subnet": [ "check_internet_access" ] } auth: { authenticated: true, user: "******" } } :statuscode 200: no error :statuscode 401: Authentication failure. Please login. """ tech_methods = {} for key in list(auditor_registry.keys()): methods = [] for auditor_class in auditor_registry[key]: auditor = auditor_class('') for method_name in dir(auditor): method_name = method_name + ' (' + auditor.__class__.__name__ + ')' if (method_name.find("check_")) == 0: methods.append(method_name) tech_methods[key] = methods marshaled_dict = {'tech_methods': tech_methods, 'auth': self.auth_dict} return marshaled_dict, 200
class ItemAuditGet(AuthenticatedService): decorators = [ rbac.allow(["View"], ["GET"]) ] def get(self, audit_id): """ .. http:get:: /api/1/issue/1234 Get a specific issue **Example Request**: .. sourcecode:: http GET /api/1/issue/1234 HTTP/1.1 Host: example.com Accept: application/json **Example Response**: .. sourcecode:: http HTTP/1.1 200 OK Vary: Accept Content-Type: application/json { justification: null, name: "example_name", issue: "Example Audit Issue", notes: "Example Notes on Audit Issue", auth: { authenticated: true, user: "******" }, score: 0, item_id: 704, region: AWS_DEFAULT_REGION, justified: false, justified_date: null, id: 704 } :statuscode 200: no error :statuscode 401: Authentication Error. Please login. """ query = ItemAudit.query.join("item").filter(ItemAudit.id == audit_id) result = query.first() issue_marshaled = marshal(result, AUDIT_FIELDS) item_marshaled = marshal(result.item, ITEM_FIELDS) issue_marshaled = dict( list(issue_marshaled.items()) + list(item_marshaled.items()) + list({'auth': self.auth_dict}.items()) ) return issue_marshaled, 200
class Providers(Resource): decorators = [rbac.allow(["anonymous"], ["GET"])] def __init__(self): super(Providers, self).__init__() def get(self): active_providers = [] for provider in current_app.config.get("ACTIVE_PROVIDERS"): provider = provider.lower() if provider == "ping": active_providers.append({ 'name': current_app.config.get("PING_NAME"), 'url': current_app.config.get('PING_REDIRECT_URI'), 'redirectUri': current_app.config.get("PING_REDIRECT_URI"), 'clientId': current_app.config.get("PING_CLIENT_ID"), 'responseType': 'code', 'scope': ['openid', 'profile', 'email'], 'scopeDelimiter': ' ', 'authorizationEndpoint': current_app.config.get("PING_AUTH_ENDPOINT"), 'requiredUrlParams': ['scope'], 'type': '2.0' }) elif provider == "google": google_provider = { 'name': 'google', 'clientId': current_app.config.get("GOOGLE_CLIENT_ID"), 'url': api.url_for(Google, _external=True, _scheme='https'), 'redirectUri': api.url_for(Google, _external=True, _scheme='https'), 'authorizationEndpoint': current_app.config.get("GOOGLE_AUTH_ENDPOINT"), 'scope': ['openid email'], 'responseType': 'code' } google_hosted_domain = current_app.config.get( "GOOGLE_HOSTED_DOMAIN") if google_hosted_domain is not None: google_provider['hd'] = google_hosted_domain active_providers.append(google_provider) else: raise Exception( "Unknown authentication provider: {0}".format(provider)) return active_providers
class WatcherConfigGetList(AuthenticatedService): decorators = [ rbac.allow(["Admin"], ["GET"]), ] def __init__(self): super(WatcherConfigGetList, self).__init__() self.reqparse = reqparse.RequestParser() def get(self): self.reqparse.add_argument('count', type=int, default=30, location='args') self.reqparse.add_argument('page', type=int, default=1, location='args') args = self.reqparse.parse_args() page = args.pop('page', None) count = args.pop('count', None) configs = [] all_keys = list(watcher_registry.keys()) all_keys.sort() start_index = (page - 1) * count keys = all_keys[start_index:start_index + count] for key in keys: watcher_class = watcher_registry[key] config = WatcherConfig.query.filter( WatcherConfig.index == watcher_class.index).first() if config is None: config = WatcherConfig(id=0, index=watcher_class.index, interval=watcher_class.interval, active=True) configs.append(config) return_dict = { "page": page, "total": len(all_keys), "count": len(configs), "items": [ marshal(item.__dict__, WATCHER_CONFIG_FIELDS) for item in configs ], "auth": self.auth_dict } return return_dict, 200
class RevisionCommentGet(AuthenticatedService): decorators = [rbac.allow(["Comment"], ["GET"])] def __init__(self): super(RevisionCommentGet, self).__init__() def get(self, revision_id, comment_id): """ .. http:get:: /api/1/revisions/<int:revision_id>/comments/<int:comment_id> Get a specific Revision Comment **Example Request**: .. sourcecode:: http GET /api/1/revisions/1141/comments/22 HTTP/1.1 Host: example.com Accept: application/json **Example Response**: .. sourcecode:: http HTTP/1.1 200 OK Vary: Accept Content-Type: application/json { 'id': 22, 'revision_id': 1141, "date_created": "2013-10-04 22:01:47", 'text': 'This is a Revision Comment.' } :statuscode 200: no error :statuscode 404: Revision Comment with given ID not found. :statuscode 401: Authentication Error. Please Login. """ auth, retval = __check_auth__(self.auth_dict) if auth: return retval query = ItemRevisionComment.query.filter( ItemRevisionComment.id == comment_id) query = query.filter(ItemRevisionComment.revision_id == revision_id) irc = query.first() if irc is None: return {"status": "Revision Comment Not Found"}, 404 revision_marshaled = marshal(irc.__dict__, REVISION_COMMENT_FIELDS) revision_marshaled = dict(revision_marshaled.items() + {'user': irc.user.email}.items()) return revision_marshaled, 200
class WatcherConfigPut(AuthenticatedService): decorators = [ rbac.allow(["Admin"], ["Put"]), ] def __init__(self): super(WatcherConfigPut, self).__init__() def put(self, id): self.reqparse.add_argument('index', required=True, type=text_type, location='json') self.reqparse.add_argument('interval', required=True, type=int, location='json') self.reqparse.add_argument('active', required=True, type=bool, location='json') self.reqparse.add_argument('remove_items', required=False, type=bool, location='json') args = self.reqparse.parse_args() index = args['index'] interval = args['interval'] active = args['active'] remove_items = args.get('remove_items', False) if id > 0: config = WatcherConfig.query.filter(WatcherConfig.id == id).first() config.interval = interval config.active = active else: config = WatcherConfig(index=index, interval=interval, active=active) db.session.add(config) db.session.commit() if active is False and remove_items is True: results = Item.query.join((Technology, Item.tech_id == Technology.id)) \ .filter(Technology.name == index).all() for item in results: db.session.delete(item) db.session.commit() marshaled_dict = {'auth': self.auth_dict} return marshaled_dict, 200
class Roles(AuthenticatedService): decorators = [rbac.allow(["Admin"], ["GET"])] def get(self): """ .. http:get:: /api/1/roles Get a list of roles, checking that the requester is an admin. **Example Request**: .. sourcecode:: http GET /api/1/roles HTTP/1.1 Host: example.com Accept: application/json **Example Response**: .. sourcecode:: http HTTP/1.1 200 OK Vary: Accept Content-Type: application/json { "auth": { "authenticated": true, "administrator": true, "user": "******" }, "roles": [ { "id": 1, "name": "admin", "description": "Administrators" } ] } :statuscode 200: no error :statuscode 401: Authentication Error. Please ensure you have administrator rights. """ return_dict = {"auth": self.auth_dict} roles = [] for name in RBACRole.roles: roles.append({"name": RBACRole.roles[name].name}) return_dict["roles"] = roles return return_dict, 200
class RevisionCommentDelete(AuthenticatedService): decorators = [rbac.allow(["Comment"], ["DELETE"])] def __init__(self): super(RevisionCommentDelete, self).__init__() def delete(self, revision_id, comment_id): """ .. http:delete:: /api/1/revisions/<int:revision_id>/comments/<int:comment_id> Delete a specific Revision Comment **Example Request**: .. sourcecode:: http DELETE /api/1/revisions/1141/comments/22 HTTP/1.1 Host: example.com Accept: application/json **Example Response**: .. sourcecode:: http HTTP/1.1 200 OK Vary: Accept Content-Type: application/json { 'status': "deleted" } :statuscode 202: Comment Deleted :statuscode 404: Revision Comment with given ID not found. :statuscode 401: Authentication Error. Please Login. """ auth, retval = __check_auth__(self.auth_dict) if auth: return retval query = ItemRevisionComment.query.filter( ItemRevisionComment.id == comment_id) query = query.filter(ItemRevisionComment.revision_id == revision_id) irc = query.first() if irc is None: return {"status": "Revision Comment Not Found"}, 404 query.delete() db.session.commit() return {"status": "deleted"}, 202
class AuditorSettingsPut(AuthenticatedService): decorators = [rbac.allow(["Justify"], ["PUT"])] def __init__(self): self.reqparse = reqparse.RequestParser() super(AuditorSettingsPut, self).__init__() def put(self, as_id): """ .. http:put:: /api/1/auditorsettings/<int ID> Update an AuditorSetting **Example Request**: .. sourcecode:: http PUT /api/1/auditorsettings/1 HTTP/1.1 Host: example.com Accept: application/json, text/javascript { account: "aws-account-name", disabled: false, id: 1, issue: "User with password login.", technology: "iamuser" } **Example Response**: .. sourcecode:: http HTTP/1.1 200 OK Content-Type: application/json :statuscode 200: no error :statuscode 401: Authentication failure. Please login. """ self.reqparse.add_argument('disabled', type=bool, required=True, location='json') args = self.reqparse.parse_args() disabled = args.pop('disabled', None) results = AuditorSettings.query.get(as_id) results.disabled = disabled db.session.add(results) db.session.commit() return 200
class ItemCommentGet(AuthenticatedService): decorators = [rbac.allow(['View'], ["GET"])] def get(self, item_id, comment_id): """ .. http:get:: /api/1/items/<int:item_id>/comment/<int:comment_id> Retrieves an item comment. **Example Request**: .. sourcecode:: http GET /api/1/items/1234/comment/7718 HTTP/1.1 Host: example.com Accept: application/json **Example Response**: .. sourcecode:: http HTTP/1.1 200 OK Vary: Accept Content-Type: application/json { 'id': 7719, 'date_created': "2013-10-04 22:01:47", 'text': 'This is an Item Comment.', 'item_id': 1111 } :statuscode 200: Success :statuscode 404: Comment with given ID not found. :statuscode 401: Authentication Error. Please Login. """ query = ItemComment.query.filter(ItemComment.id == comment_id) query = query.filter(ItemComment.item_id == item_id) ic = query.first() if ic is None: return {"status": "Item Comment Not Found"}, 404 comment_marshaled = marshal(ic.__dict__, ITEM_COMMENT_FIELDS) comment_marshaled = dict( list(comment_marshaled.items()) + list({'user': ic.user.email}.items()) ) return comment_marshaled, 200
class ItemCommentDelete(AuthenticatedService): decorators = [ rbac.allow(['Justify'], ["DELETE"]) ] def delete(self, item_id, comment_id): """ .. http:delete:: /api/1/items/<int:item_id>/comment/<int:comment_id> Deletes an item comment. **Example Request**: .. sourcecode:: http DELETE /api/1/items/1234/comment/7718 HTTP/1.1 Host: example.com Accept: application/json { } **Example Response**: .. sourcecode:: http HTTP/1.1 202 OK Vary: Accept Content-Type: application/json { 'status': 'deleted' } :statuscode 202: Deleted :statuscode 401: Authentication Error. Please Login. """ query = ItemComment.query.filter(ItemComment.id == comment_id) query.filter(ItemComment.user_id == current_user.id).delete() db.session.commit() return {'result': 'success'}, 202
class AccountListPut(AuthenticatedService): decorators = [rbac.allow(["Admin"], ["PUT"])] def __init__(self): super(AccountListPut, self).__init__() self.reqparse = reqparse.RequestParser() def put(self): values = json.loads(request.json) app.logger.debug("Account bulk update {}".format(values)) for account_name in values.keys(): account = Account.query.filter( Account.name == account_name).first() if account: account.active = values[account_name] db.session.add(account) db.session.commit() db.session.close() return {'status': 'updated'}, 200
class Distinct(AuthenticatedService): decorators = [rbac.allow(["View"], ["GET"])] def __init__(self): self.reqparse = reqparse.RequestParser() super(Distinct, self).__init__() def get(self, key_id): """ .. http:get:: /api/1/distinct Get a list of distinct regions, names, accounts, accounttypes or technologies **Example Request**: .. sourcecode:: http GET /api/1/distinct/name HTTP/1.1 Host: example.com Accept: application/json, text/javascript **Example Response**: .. sourcecode:: http HTTP/1.1 200 OK Vary: Accept Content-Type: application/json :statuscode 200: no error """ self.reqparse.add_argument('count', type=int, default=30, location='args') self.reqparse.add_argument('page', type=int, default=1, location='args') self.reqparse.add_argument('select2', type=str, default="", location='args') self.reqparse.add_argument('searchconfig', type=str, default="", location='args') self.reqparse.add_argument('regions', type=str, default=None, location='args') self.reqparse.add_argument('accounts', type=str, default=None, location='args') self.reqparse.add_argument('accounttypes', type=str, default=None, location='args') self.reqparse.add_argument('technologies', type=str, default=None, location='args') self.reqparse.add_argument('names', type=str, default=None, location='args') self.reqparse.add_argument('active', type=str, default=None, location='args') args = self.reqparse.parse_args() page = args.pop('page', None) count = args.pop('count', None) q = args.pop('searchconfig', "").lower() select2 = args.pop('select2', "") for k, v in args.items(): if not v: del args[k] if select2.lower() == 'true': select2 = True else: select2 = False query = Item.query if 'regions' in args and key_id != 'region': regions = args['regions'].split(',') query = query.filter(Item.region.in_(regions)) if 'accounts' in args and key_id != 'account': query = query.join((Account, Account.id == Item.account_id)) accounts = args['accounts'].split(',') query = query.filter(Account.name.in_(accounts)) if 'accounttypes' in args and key_id != 'accounttype': query = query.join((Account, Account.id == Item.account_id)).join( (AccountType, AccountType.id == Account.account_type_id)) accounttypes = args['accounttypes'].split(',') query = query.filter(AccountType.name.in_(accounttypes)) if 'technologies' in args and key_id != 'tech': query = query.join((Technology, Technology.id == Item.tech_id)) technologies = args['technologies'].split(',') query = query.filter(Technology.name.in_(technologies)) if 'names' in args and key_id != 'name': names = args['names'].split(',') query = query.filter(Item.name.in_(names)) if 'arns' in args and key_id != 'arn': names = args['arns'].split(',') query = query.filter(Item.arn.in_(names)) if 'active' in args: query = query.join( (ItemRevision, Item.latest_revision_id == ItemRevision.id)) active = args['active'].lower() == "true" query = query.filter(ItemRevision.active == active) if key_id == 'tech': query = query.join((Technology, Technology.id == Item.tech_id)) if select2: query = query.distinct(Technology.name).filter( func.lower(Technology.name).like('%' + q + '%')) else: query = query.distinct(Technology.name) elif key_id == 'accounttype': query = query.join((Account, Account.id == Item.account_id)).join( (AccountType, AccountType.id == Account.account_type_id)) if select2: query = query.distinct(AccountType.name).filter( func.lower(AccountType.name).like('%' + q + '%')) else: query = query.distinct(AccountType.name) elif key_id == 'account': query = query.join((Account, Account.id == Item.account_id)) if select2: query = query.filter(Account.third_party == False) query = query.distinct(Account.name).filter( func.lower(Account.name).like('%' + q + '%')) else: query = query.distinct(Account.name) else: filter_by = None if key_id == "region": filter_by = Item.region elif key_id == "name": filter_by = Item.name elif key_id == "arn": filter_by = Item.arn else: return json.loads( '{ "error": "Supply key in type,region,account,name,arn" }' ) if select2: query = query.distinct(filter_by).filter( func.lower(filter_by).like('%' + q + '%')) else: query = query.distinct(filter_by) items = query.paginate(page, count, error_out=False) marshaled_dict = {} list_distinct = [] for item in items.items: if key_id == "tech": text = item.technology.name item_id = item.id elif key_id == "account": text = item.account.name item_id = item.id elif key_id == "accounttype": text = item.account.account_type.name item_id = item.id elif key_id == "region": text = item.region item_id = item.id elif key_id == "name": text = item.name item_id = item.id elif key_id == "arn": text = item.arn item_id = item.id if (select2): list_distinct.append({"id": item_id, "text": text}) else: list_distinct.append(text) marshaled_dict['auth'] = self.auth_dict marshaled_dict['items'] = list_distinct marshaled_dict['page'] = items.page marshaled_dict['total'] = items.total marshaled_dict['key_id'] = key_id return marshaled_dict, 200
class RevisionList(AuthenticatedService): decorators = [rbac.allow(["View"], ["GET"])] def get(self): """ .. http:get:: /api/1/revisions Get a list of revisions **Example Request**: .. sourcecode:: http GET /api/1/revisions?count=1 HTTP/1.1 Host: example.com Accept: application/json **Example Response**: .. sourcecode:: http HTTP/1.1 200 OK Vary: Accept Content-Type: application/json { "items": [ { "account": "example_account", "name": "Example Name", "region": "us-east-1", "item_id": 144, "active": false, "date_created": "2014-06-19 20:54:12.962951", "technology": "sqs", "id": 223757 } ], "total": 1, "page": 1, "auth": { "authenticated": true, "user": "******" } } :statuscode 200: no error :statuscode 401: Authentication Error. Please Login. """ self.reqparse.add_argument('count', type=int, default=30, location='args') self.reqparse.add_argument('page', type=int, default=1, location='args') self.reqparse.add_argument('active', type=str, default=None, location='args') self.reqparse.add_argument('regions', type=str, default=None, location='args') self.reqparse.add_argument('accounts', type=str, default=None, location='args') self.reqparse.add_argument('names', type=str, default=None, location='args') self.reqparse.add_argument('arns', type=str, default=None, location='args') self.reqparse.add_argument('technologies', type=str, default=None, location='args') self.reqparse.add_argument('searchconfig', type=str, default=None, location='args') args = self.reqparse.parse_args() page = args.pop('page', None) count = args.pop('count', None) for k, v in args.items(): if not v: del args[k] query = ItemRevision.query.join("item") if 'regions' in args: regions = args['regions'].split(',') query = query.filter(Item.region.in_(regions)) if 'accounts' in args: accounts = args['accounts'].split(',') query = query.join((Account, Account.id == Item.account_id)) query = query.filter(Account.name.in_(accounts)) if 'technologies' in args: technologies = args['technologies'].split(',') query = query.join((Technology, Technology.id == Item.tech_id)) query = query.filter(Technology.name.in_(technologies)) if 'names' in args: names = args['names'].split(',') query = query.filter(Item.name.in_(names)) if 'arns' in args: arns = args['arns'].split(',') query = query.filter(Item.arn.in_(arns)) if 'active' in args: active = args['active'].lower() == "true" query = query.filter(ItemRevision.active == active) if 'searchconfig' in args: searchconfig = args['searchconfig'] query = query.filter( cast(ItemRevision.config, String).ilike('%{}%'.format(searchconfig))) query = query.order_by(ItemRevision.date_created.desc()) revisions = query.paginate(page, count) marshaled_dict = { 'page': revisions.page, 'total': revisions.total, 'auth': self.auth_dict } items_marshaled = [] for revision in revisions.items: item_marshaled = marshal(revision.item.__dict__, ITEM_FIELDS) revision_marshaled = marshal(revision.__dict__, REVISION_FIELDS) account_marshaled = {'account': revision.item.account.name} technology_marshaled = { 'technology': revision.item.technology.name } merged_marshaled = dict(item_marshaled.items() + revision_marshaled.items() + account_marshaled.items() + technology_marshaled.items()) items_marshaled.append(merged_marshaled) marshaled_dict['items'] = items_marshaled marshaled_dict['count'] = len(items_marshaled) return marshaled_dict, 200
class RevisionGet(AuthenticatedService): decorators = [rbac.allow(["View"], ["GET"])] def __init__(self): self.reqparse = reqparse.RequestParser() super(RevisionGet, self).__init__() def get(self, revision_id): """ .. http:get:: /api/1/revision/1234 Get a specific revision. **Example Request**: .. sourcecode:: http GET /api/1/revision/123 HTTP/1.1 Host: example.com Accept: application/json, text/javascript **Example Response**: .. sourcecode:: http HTTP/1.1 200 OK Vary: Accept Content-Type: application/json { "auth": { "authenticated": true, "user": "******" }, "item_id": 114, "comments": [], "active": false, "date_created": "2013-10-04 22:01:47", "config": {}, "id":123 } :statuscode 200: no error :statuscode 401: Authentication failure. Please login. """ query = ItemRevision.query.filter(ItemRevision.id == revision_id) result = query.first() comments = [] for comment in result.comments: comment_marshaled = marshal(comment, REVISION_COMMENT_FIELDS) comments.append( dict(comment_marshaled.items() + {'user': comment.user.email}.items())) cloudtrail_entries = [] for entry in result.cloudtrail_entries: cloudtrail_entries.append(entry.full_entry) revision_marshaled = marshal(result, REVISION_FIELDS) revision_marshaled = dict( revision_marshaled.items() + {'config': OrderedDict(sorted(sub_dict(result.config).items())) }.items() + {'auth': self.auth_dict}.items() + {'comments': comments}.items() + {'cloudtrail': cloudtrail_entries}.items()) self.reqparse.add_argument('compare', type=int, default=None, location='args') args = self.reqparse.parse_args() compare_id = args.pop('compare', None) if compare_id: query = ItemRevision.query.filter(ItemRevision.id == compare_id) compare_result = query.first() pdiff = PolicyDiff( OrderedDict(sorted(sub_dict(result.config).items())), OrderedDict(sorted(sub_dict(compare_result.config).items()))) revision_marshaled = dict( revision_marshaled.items() + {'diff_html': pdiff.produceDiffHTML()}.items()) return revision_marshaled, 200
class ItemCommentPost(AuthenticatedService): decorators = [rbac.allow(['Comment'], ["POST"])] def post(self, item_id): """ .. http:post:: /api/1/items/<int:item_id>/comments Adds an item comment. **Example Request**: .. sourcecode:: http POST /api/1/items/1234/comments HTTP/1.1 Host: example.com Accept: application/json { "text": "This item is my favorite." } **Example Response**: .. sourcecode:: http HTTP/1.1 201 OK Vary: Accept Content-Type: application/json { 'item_id': 1234, 'id': 7718, 'comment': 'This item is my favorite.', 'user': '******' } { "date_created": "2014-10-11 23:03:47.716698", "id": 1, "text": "This is an item comment." } :statuscode 201: Created :statuscode 401: Authentication Error. Please Login. """ self.reqparse.add_argument('text', required=False, type=unicode, help='Must provide comment', location='json') args = self.reqparse.parse_args() ic = ItemComment() ic.user_id = current_user.id ic.item_id = item_id ic.text = args['text'] ic.date_created = datetime.datetime.utcnow() db.session.add(ic) db.session.commit() db.session.refresh(ic) comment_marshaled = marshal(ic.__dict__, ITEM_COMMENT_FIELDS) comment_marshaled = dict(comment_marshaled.items() + {'user': ic.user.email}.items()) return comment_marshaled, 201
class Ping(Resource): """ This class serves as an example of how one might implement an SSO provider for use with Security Monkey. In this example we use a OpenIDConnect authentication flow, that is essentially OAuth2 underneath. """ decorators = [rbac.allow(["anonymous"], ["GET", "POST"])] def __init__(self): self.reqparse = reqparse.RequestParser() super(Ping, self).__init__() def get(self): return self.post() def post(self): if "ping" not in current_app.config.get("ACTIVE_PROVIDERS"): return "Ping is not enabled in the config. See the ACTIVE_PROVIDERS section.", 404 default_state = 'clientId,{client_id},redirectUri,{redirectUri},return_to,{return_to}'.format( client_id=current_app.config.get('PING_CLIENT_ID'), redirectUri=current_app.config.get('PING_REDIRECT_URI'), return_to=current_app.config.get('WEB_PATH')) self.reqparse.add_argument('code', type=str, required=True) self.reqparse.add_argument('state', type=str, required=False, default=default_state) args = self.reqparse.parse_args() client_id = args['state'].split(',')[1] redirect_uri = args['state'].split(',')[3] return_to = args['state'].split(',')[5] if not validate_redirect_url(return_to): return_to = current_app.config.get('WEB_PATH') # take the information we have received from the provider to create a new request params = { 'client_id': client_id, 'grant_type': 'authorization_code', 'scope': 'openid email profile address', 'redirect_uri': redirect_uri, 'code': args['code'] } # you can either discover these dynamically or simply configure them access_token_url = current_app.config.get('PING_ACCESS_TOKEN_URL') user_api_url = current_app.config.get('PING_USER_API_URL') # the secret and cliendId will be given to you when you signup for the provider basic = base64.b64encode( bytes('{0}:{1}'.format(client_id, current_app.config.get("PING_SECRET")))) headers = {'Authorization': 'Basic {0}'.format(basic.decode('utf-8'))} # exchange authorization code for access token. r = requests.post(access_token_url, headers=headers, params=params) id_token = r.json()['id_token'] access_token = r.json()['access_token'] # fetch token public key header_data = fetch_token_header_payload(id_token)[0] jwks_url = current_app.config.get('PING_JWKS_URL') # retrieve the key material as specified by the token header r = requests.get(jwks_url) for key in r.json()['keys']: if key['kid'] == header_data['kid']: secret = get_rsa_public_key(key['n'], key['e']) algo = header_data['alg'] break else: return dict(message='Key not found'), 403 # validate your token based on the key it was signed with try: current_app.logger.debug(id_token) current_app.logger.debug(secret) current_app.logger.debug(algo) jwt.decode(id_token, secret.decode('utf-8'), algorithms=[algo], audience=client_id) except jwt.DecodeError: return dict(message='Token is invalid'), 403 except jwt.ExpiredSignatureError: return dict(message='Token has expired'), 403 except jwt.InvalidTokenError: return dict(message='Token is invalid'), 403 user_params = dict(access_token=access_token, schema='profile') # retrieve information about the current user. r = requests.get(user_api_url, params=user_params) profile = r.json() user = User.query.filter(User.email == profile['email']).first() # if we get an sso user create them an account if not user: user = User(email=profile['email'], active=True, role='View' # profile_picture=profile.get('thumbnailPhotoUrl') ) db.session.add(user) db.session.commit() db.session.refresh(user) # Tell Flask-Principal the identity changed identity_changed.send(current_app._get_current_object(), identity=Identity(user.id)) login_user(user) return redirect(return_to, code=302)
class RevisionCommentPost(AuthenticatedService): decorators = [ rbac.allow(["Comment"], ["POST"]) ] def post(self, revision_id): """ .. http:post:: /api/1/revisions/<int:revision_id>/comments Create a new Revision Comment. **Example Request**: .. sourcecode:: http POST /api/1/revisions/1141/comments HTTP/1.1 Host: example.com Accept: application/json { "text": "This is a Revision Comment." } **Example Response**: .. sourcecode:: http HTTP/1.1 201 OK Vary: Accept Content-Type: application/json { 'id': 22, 'revision_id': 1141, "date_created": "2013-10-04 22:01:47", 'text': 'This is a Revision Comment.' } :statuscode 201: Revision Comment Created :statuscode 401: Authentication Error. Please Login. """ self.reqparse.add_argument('text', required=False, type=unicode, help='Must provide comment', location='json') args = self.reqparse.parse_args() irc = ItemRevisionComment() irc.user_id = current_user.id irc.revision_id = revision_id irc.text = args['text'] irc.date_created = datetime.datetime.utcnow() db.session.add(irc) db.session.commit() db.session.refresh(irc) revision_marshaled = marshal(irc.__dict__, REVISION_COMMENT_FIELDS) revision_marshaled = dict( revision_marshaled.items() + {'user': irc.user.email}.items() ) return revision_marshaled, 200
class Google(Resource): decorators = [rbac.allow(["anonymous"], ["GET", "POST"])] def __init__(self): self.reqparse = reqparse.RequestParser() super(Google, self).__init__() def get(self): return self.post() def post(self): if "google" not in current_app.config.get("ACTIVE_PROVIDERS"): return "Google is not enabled in the config. See the ACTIVE_PROVIDERS section.", 404 default_state = 'clientId,{client_id},redirectUri,{redirectUri},return_to,{return_to}'.format( client_id=current_app.config.get("GOOGLE_CLIENT_ID"), redirectUri=api.url_for(Google), return_to=current_app.config.get('WEB_PATH')) self.reqparse.add_argument('code', type=str, required=True) self.reqparse.add_argument('state', type=str, required=False, default=default_state) args = self.reqparse.parse_args() client_id = args['state'].split(',')[1] redirect_uri = args['state'].split(',')[3] return_to = args['state'].split(',')[5] if not validate_redirect_url(return_to): return_to = current_app.config.get('WEB_PATH') access_token_url = 'https://accounts.google.com/o/oauth2/token' people_api_url = 'https://www.googleapis.com/plus/v1/people/me/openIdConnect' args = self.reqparse.parse_args() # Step 1. Exchange authorization code for access token payload = { 'client_id': client_id, 'grant_type': 'authorization_code', 'redirect_uri': redirect_uri, 'code': args['code'], 'client_secret': current_app.config.get('GOOGLE_SECRET') } r = requests.post(access_token_url, data=payload) token = r.json() # Step 1bis. Validate (some information of) the id token (if necessary) google_hosted_domain = current_app.config.get("GOOGLE_HOSTED_DOMAIN") if google_hosted_domain is not None: current_app.logger.debug( 'We need to verify that the token was issued for this hosted domain: %s ' % (google_hosted_domain)) # Get the JSON Web Token id_token = r.json()['id_token'] current_app.logger.debug('The id_token is: %s' % (id_token)) # Extract the payload (header_data, payload_data) = fetch_token_header_payload(id_token) current_app.logger.debug('id_token.header_data: %s' % (header_data)) current_app.logger.debug('id_token.payload_data: %s' % (payload_data)) token_hd = payload_data.get('hd') if token_hd != google_hosted_domain: current_app.logger.debug('Verification failed: %s != %s' % (token_hd, google_hosted_domain)) return dict(message='Token is invalid %s' % token), 403 current_app.logger.debug('Verification passed') # Step 2. Retrieve information about the current user headers = {'Authorization': 'Bearer {0}'.format(token['access_token'])} r = requests.get(people_api_url, headers=headers) profile = r.json() user = User.query.filter(User.email == profile['email']).first() # if we get an sso user create them an account if not user: user = User(email=profile['email'], active=True, role='View' # profile_picture=profile.get('thumbnailPhotoUrl') ) db.session.add(user) db.session.commit() db.session.refresh(user) # Tell Flask-Principal the identity changed identity_changed.send(current_app._get_current_object(), identity=Identity(user.id)) login_user(user) return redirect(return_to, code=302)
class AccountConfigGet(AuthenticatedService): decorators = [ rbac.allow(["View"], ["GET"]), ] def __init__(self): self.reqparse = reqparse.RequestParser() super(AccountConfigGet, self).__init__() def get(self, account_fields): """ .. http:get:: /api/1/account_config/account_fields (all or custom) Get a list of Account types **Example Request**: .. sourcecode:: http GET /api/1/account_config/all HTTP/1.1 Host: example.com Accept: application/json, text/javascript **Example Response**: .. sourcecode:: http HTTP/1.1 200 OK Vary: Accept Content-Type: application/json { auth: { authenticated: true, user: "******" } } :statuscode 200: no error :statuscode 401: Authentication failure. Please login. """ load_all_account_types() marshaled = {} account_types = AccountType.query.all() configs_marshaled = {} for account_type in account_types: acc_manager = account_registry.get(account_type.name) if acc_manager is not None: values = {} values['identifier_label'] = acc_manager.identifier_label values['identifier_tool_tip'] = acc_manager.identifier_tool_tip fields = [] if account_fields == 'all': fields.append({ 'name': 'identifier', 'label': '', 'editable': True, 'tool_tip': '', 'password': False, 'allowed_values': None }) fields.append({ 'name': 'name', 'label': '', 'editable': True, 'tool_tip': '', 'password': False, 'allowed_values': None }) fields.append({ 'name': 'notes', 'label': '', 'editable': True, 'tool_tip': '', 'password': False, 'allowed_values': None }) for config in acc_manager.custom_field_configs: if account_fields == 'custom' or not config.password: field_marshaled = { 'name': config.name, 'label': config.label, 'editable': config.db_item, 'tool_tip': config.tool_tip, 'password': config.password, 'allowed_values': config.allowed_values } fields.append(field_marshaled) values['fields'] = fields configs_marshaled[account_type.name] = values marshaled['custom_configs'] = configs_marshaled marshaled['auth'] = self.auth_dict return marshaled, 200
class AccountPostList(AuthenticatedService): decorators = [ rbac.allow(["View"], ["GET"]), rbac.allow(["Admin"], ["POST"]) ] def __init__(self): super(AccountPostList, self).__init__() self.reqparse = reqparse.RequestParser() def post(self): """ .. http:post:: /api/1/account/ Create a new account. **Example Request**: .. sourcecode:: http POST /api/1/account/ HTTP/1.1 Host: example.com Accept: application/json { 'name': 'new_account' 'identifier': '0123456789', 'notes': 'this account is for ...', 'active': true, 'third_party': false 'account_type': 'AWS' } **Example Response**: .. sourcecode:: http HTTP/1.1 201 Created Vary: Accept Content-Type: application/json { 'name': 'new_account' 'identifier': '0123456789', 'notes': 'this account is for ...', 'active': true, 'third_party': false 'account_type': 'AWS' '' } :statuscode 201: created :statuscode 401: Authentication Error. Please Login. """ args = json.loads(request.json) account_type = args['account_type'] name = args['name'] identifier = args['identifier'] notes = args['notes'] active = args['active'] third_party = args['third_party'] custom_fields = args['custom_fields'] from security_monkey.account_manager import account_registry account_manager = account_registry.get(account_type)() account = account_manager.create(account_type, name, active, third_party, notes, identifier, custom_fields=custom_fields) marshaled_account = marshal(account.__dict__, ACCOUNT_FIELDS) marshaled_account['auth'] = self.auth_dict return marshaled_account, 200 def get(self): """ .. http:get:: /api/1/accounts Get a list of Accounts matching the given criteria **Example Request**: .. sourcecode:: http GET /api/1/accounts HTTP/1.1 Host: example.com Accept: application/json, text/javascript **Example Response**: .. sourcecode:: http HTTP/1.1 200 OK Vary: Accept Content-Type: application/json { count: 1, items: [ { third_party: false, name: "example_name", notes: null, role_name: null, identifier: "111111111111", active: true, id: 1, s3_name: "example_name" }, ], total: 1, page: 1, auth: { authenticated: true, user: "******" } } :statuscode 200: no error :statuscode 401: Authentication failure. Please login. """ self.reqparse.add_argument('count', type=int, default=30, location='args') self.reqparse.add_argument('page', type=int, default=1, location='args') self.reqparse.add_argument('order_by', type=str, default=None, location='args') self.reqparse.add_argument('order_dir', type=str, default='desc', location='args') self.reqparse.add_argument('active', type=str, default=None, location='args') self.reqparse.add_argument('third_party', type=str, default=None, location='args') args = self.reqparse.parse_args() page = args.pop('page', None) count = args.pop('count', None) order_by = args.pop('order_by', None) order_dir = args.pop('order_dir', None) for k, v in list(args.items()): if not v: del args[k] query = Account.query if 'active' in args: active = args['active'].lower() == "true" query = query.filter(Account.active == active) if 'third_party' in args: third_party = args['third_party'].lower() == "true" query = query.filter(Account.third_party == third_party) if order_by and hasattr(Account, order_by): if order_dir.lower() == 'asc': if order_by == 'account_type': query = query.join(Account.account_type).order_by( getattr(AccountType, 'name').asc()) else: query = query.order_by(getattr(Account, order_by).asc()) else: if order_by == 'account_type': query = query.join(Account.account_type).order_by( getattr(AccountType, 'name').desc()) else: query = query.order_by(getattr(Account, order_by).desc()) else: query = query.order_by(Account.id) result = query.paginate(page, count, error_out=False) items = [] for account in result.items: account_marshaled = marshal(account.__dict__, ACCOUNT_FIELDS) account_marshaled = dict( list(account_marshaled.items()) + list({'account_type': account.account_type.name}.items())) items.append(account_marshaled) marshaled_dict = { 'total': result.total, 'count': len(items), 'page': result.page, 'items': items, 'auth': self.auth_dict } return marshaled_dict, 200
class ItemList(AuthenticatedService): decorators = [rbac.allow(['View'], ["GET"])] def get(self): """ .. http:get:: /api/1/items Get a list of items matching the given criteria. **Example Request**: .. sourcecode:: http GET /api/1/items HTTP/1.1 Host: example.com Accept: application/json **Example Response**: .. sourcecode:: http HTTP/1.1 200 OK Vary: Accept Content-Type: application/json { "items": [ { "account": "example_account", "region": "us-east-1", "technology": "sqs", "id": 14414, "name": "example_name", "num_issues": 3, "issue_score": 9, "unjustified_issue_score": 3, "active" true, "first_seen": "2014-06-17 19:47:07.299760", "last_seen": "2014-06-18 11:53:16.467709" } ], "total": 144, "page": 1, "auth": { "authenticated": true, "user": "******" } } :statuscode 200: no error :statuscode 401: Authenciation Error. Please Login. """ self.reqparse.add_argument('count', type=int, default=30, location='args') self.reqparse.add_argument('page', type=int, default=1, location='args') self.reqparse.add_argument('regions', type=str, default=None, location='args') self.reqparse.add_argument('accounts', type=str, default=None, location='args') self.reqparse.add_argument('active', type=str, default=None, location='args') self.reqparse.add_argument('names', type=str, default=None, location='args') self.reqparse.add_argument('arns', type=str, default=None, location='args') self.reqparse.add_argument('technologies', type=str, default=None, location='args') self.reqparse.add_argument('searchconfig', type=str, default=None, location='args') self.reqparse.add_argument('ids', type=int, default=None, location='args') self.reqparse.add_argument('summary', type=bool, default=False, location='args') self.reqparse.add_argument('min_score', type=int, default=False, location='args') self.reqparse.add_argument('min_unjustified_score', type=int, default=False, location='args') args = self.reqparse.parse_args() page = args.pop('page', None) count = args.pop('count', None) for k, v in args.items(): if not v: del args[k] # Read more about filtering: # http://docs.sqlalchemy.org/en/rel_0_7/orm/query.html query = Item.query.join( (ItemRevision, Item.latest_revision_id == ItemRevision.id)) if 'regions' in args: regions = args['regions'].split(',') query = query.filter(Item.region.in_(regions)) if 'accounts' in args: accounts = args['accounts'].split(',') query = query.join((Account, Account.id == Item.account_id)) query = query.filter(Account.name.in_(accounts)) if 'technologies' in args: technologies = args['technologies'].split(',') query = query.join((Technology, Technology.id == Item.tech_id)) query = query.filter(Technology.name.in_(technologies)) if 'names' in args: names = args['names'].split(',') query = query.filter(Item.name.in_(names)) if 'arns' in args: arns = args['arns'].split(',') query = query.filter(Item.arn.in_(arns)) if 'ids' in args: ids = args['ids'].split(',') query = query.filter(Item.id.in_(ids)) if 'active' in args: active = args['active'].lower() == "true" query = query.filter(ItemRevision.active == active) if 'searchconfig' in args: searchconfig = args['searchconfig'] query = query.filter( cast(ItemRevision.config, String).ilike('%{}%'.format(searchconfig))) if 'min_score' in args: min_score = args['min_score'] query = query.filter(Item.score >= min_score) if 'min_unjustified_score' in args: min_unjustified_score = args['min_unjustified_score'] query = query.filter( Item.unjustified_score >= min_unjustified_score) # Eager load the joins except for the revisions because of the dynamic lazy relationship query = query.options(joinedload('issues')) query = query.options(joinedload('account')) query = query.options(joinedload('technology')) query = query.order_by(ItemRevision.date_created.desc()) items = query.paginate(page, count) marshaled_dict = { 'page': items.page, 'total': items.total, 'auth': self.auth_dict } marshaled_items = [] for item in items.items: item_marshaled = marshal(item.__dict__, ITEM_FIELDS) if 'summary' in args and args['summary']: item_marshaled = dict( item_marshaled.items() + { 'account': item.account.name, 'technology': item.technology.name, 'num_issues': item.issue_count, 'issue_score': item.score, 'unjustified_issue_score': item.unjustified_score, 'active': active, #'last_rev': item.revisions[0].config, }.items()) else: first_seen_query = ItemRevision.query.filter( ItemRevision.item_id == item.id).order_by( ItemRevision.date_created.asc()) first_seen = str(first_seen_query.first().date_created) last_seen = str(item.revisions.first().date_created) active = item.revisions.first().active item_marshaled = dict( item_marshaled.items() + { 'account': item.account.name, 'technology': item.technology.name, 'num_issues': item.issue_count, 'issue_score': item.score, 'unjustified_issue_score': item.unjustified_score, 'active': active, 'first_seen': first_seen, 'last_seen': last_seen # 'last_rev': item.revisions[0].config, }.items()) marshaled_items.append(item_marshaled) marshaled_dict['items'] = marshaled_items marshaled_dict['count'] = len(marshaled_items) return marshaled_dict, 200
class AccountGetPutDelete(AuthenticatedService): decorators = [ rbac.allow(["View"], ["GET"]), rbac.allow(["Admin"], ["PUT", "DELETE"]) ] def __init__(self): self.reqparse = reqparse.RequestParser() super(AccountGetPutDelete, self).__init__() def get(self, account_id): """ .. http:get:: /api/1/account/<int:id> Get a list of Accounts matching the given criteria **Example Request**: .. sourcecode:: http GET /api/1/account/1 HTTP/1.1 Host: example.com Accept: application/json, text/javascript **Example Response**: .. sourcecode:: http HTTP/1.1 200 OK Vary: Accept Content-Type: application/json { third_party: false, name: "example_name", notes: null, identifier: "111111111111", active: true, id: 1, account_type: "AWS", auth: { authenticated: true, user: "******" } } :statuscode 200: no error :statuscode 401: Authentication failure. Please login. """ result = get_account_by_id(account_id) account_marshaled = marshal(result.__dict__, ACCOUNT_FIELDS) account_marshaled = dict( list(account_marshaled.items()) + list({'account_type': result.account_type.name}.items())) custom_fields_marshaled = [] for field in result.custom_fields: field_marshaled = { 'name': field.name, 'value': field.value, } custom_fields_marshaled.append(field_marshaled) account_marshaled['custom_fields'] = custom_fields_marshaled account_marshaled['auth'] = self.auth_dict return account_marshaled, 200 def put(self, account_id): """ .. http:put:: /api/1/account/1 Edit an existing account. **Example Request**: .. sourcecode:: http PUT /api/1/account/1 HTTP/1.1 Host: example.com Accept: application/json { 'name': 'edited_account' 'identifier': '0123456789', 'notes': 'this account is for ...', 'active': true, 'third_party': false } **Example Response**: .. sourcecode:: http HTTP/1.1 200 OK Vary: Accept Content-Type: application/json { 'name': 'edited_account' 'identifier': '0123456789', 'notes': 'this account is for ...', 'active': true, 'third_party': false 'account_type': 'AWS' } :statuscode 200: no error :statuscode 401: Authentication Error. Please Login. """ args = json.loads(request.json) account_type = args['account_type'] name = args['name'] identifier = args['identifier'] notes = args['notes'] active = args['active'] third_party = args['third_party'] custom_fields = args['custom_fields'] from security_monkey.account_manager import account_registry account_manager = account_registry.get(account_type)() try: account = account_manager.update(account_id, account_type, name, active, third_party, notes, identifier, custom_fields=custom_fields) except AccountNameExists as _: return {'status': 'error. Account name exists.'}, 409 if not account: return {'status': 'error. Account ID not found.'}, 404 from security_monkey.common.audit_issue_cleanup import clean_account_issues clean_account_issues(account) marshaled_account = marshal(account.__dict__, ACCOUNT_FIELDS) marshaled_account['auth'] = self.auth_dict return marshaled_account, 200 def delete(self, account_id): """ .. http:delete:: /api/1/account/1 Delete an account. **Example Request**: .. sourcecode:: http DELETE /api/1/account/1 HTTP/1.1 Host: example.com Accept: application/json **Example Response**: .. sourcecode:: http HTTP/1.1 202 Accepted Vary: Accept Content-Type: application/json { 'status': 'deleted' } :statuscode 202: accepted :statuscode 401: Authentication Error. Please Login. """ delete_account_by_id(account_id) return {'status': 'deleted'}, 202
class WhitelistGetPutDelete(AuthenticatedService): decorators = [ rbac.allow(["Admin"], ["GET", "PUT", "DELETE"]) ] def __init__(self): self.reqparse = reqparse.RequestParser() super(WhitelistGetPutDelete, self).__init__() def get(self, item_id): """ .. http:get:: /api/1/whitelistcidrs/<int:id> Get the whitelist entry with the given ID. **Example Request**: .. sourcecode:: http GET /api/1/whitelistcidrs/123 HTTP/1.1 Host: example.com Accept: application/json, text/javascript **Example Response**: .. sourcecode:: http HTTP/1.1 200 OK Vary: Accept Content-Type: application/json { "id": 123, "name": "Corp", "notes": "Corporate Network", "cidr": "1.2.3.4/22", auth: { authenticated: true, user: "******" } } :statuscode 200: no error :statuscode 404: item with given ID not found :statuscode 401: Authentication failure. Please login. """ result = NetworkWhitelistEntry.query.filter(NetworkWhitelistEntry.id == item_id).first() if not result: return {"status": "Whitelist entry with the given ID not found."}, 404 whitelistentry_marshaled = marshal(result.__dict__, WHITELIST_FIELDS) whitelistentry_marshaled['auth'] = self.auth_dict return whitelistentry_marshaled, 200 def put(self, item_id): """ .. http:get:: /api/1/whitelistcidrs/<int:id> Update the whitelist entry with the given ID. **Example Request**: .. sourcecode:: http PUT /api/1/whitelistcidrs/123 HTTP/1.1 Host: example.com Accept: application/json, text/javascript { "id": 123, "name": "Corp", "notes": "Corporate Network - New", "cidr": "2.2.0.0/16" } **Example Response**: .. sourcecode:: http HTTP/1.1 200 OK Vary: Accept Content-Type: application/json { "id": 123, "name": "Corp", "notes": "Corporate Network - New", "cidr": "2.2.0.0/16", auth: { authenticated: true, user: "******" } } :statuscode 200: no error :statuscode 404: item with given ID not found :statuscode 401: Authentication failure. Please login. """ self.reqparse.add_argument('name', required=True, type=text_type, help='Must provide account name', location='json') self.reqparse.add_argument('cidr', required=True, type=text_type, help='Network CIDR required.', location='json') self.reqparse.add_argument('notes', required=False, type=text_type, help='Add context.', location='json') args = self.reqparse.parse_args() name = args['name'] cidr = args.get('cidr', True) notes = args.get('notes', None) result = NetworkWhitelistEntry.query.filter(NetworkWhitelistEntry.id == item_id).first() if not result: return {"status": "Whitelist entry with the given ID not found."}, 404 result.name = name result.cidr = cidr result.notes = notes db.session.add(result) db.session.commit() db.session.refresh(result) whitelistentry_marshaled = marshal(result.__dict__, WHITELIST_FIELDS) whitelistentry_marshaled['auth'] = self.auth_dict return whitelistentry_marshaled, 200 def delete(self, item_id): """ .. http:delete:: /api/1/whitelistcidrs/123 Delete a network whitelist entry. **Example Request**: .. sourcecode:: http DELETE /api/1/whitelistcidrs/123 HTTP/1.1 Host: example.com Accept: application/json **Example Response**: .. sourcecode:: http HTTP/1.1 202 Accepted Vary: Accept Content-Type: application/json { 'status': 'deleted' } :statuscode 202: accepted :statuscode 401: Authentication Error. Please Login. """ NetworkWhitelistEntry.query.filter(NetworkWhitelistEntry.id == item_id).delete() db.session.commit() return {'status': 'deleted'}, 202
class WhitelistListPost(AuthenticatedService): decorators = [ rbac.allow(["Admin"], ["GET", "POST"]), rbac.allow(["View"], ["GET"]) ] def get(self): """ .. http:get:: /api/1/whitelistcidrs Get a list of CIDR's whitelisted to be used in security groups. **Example Request**: .. sourcecode:: http GET /api/1/whitelistcidrs HTTP/1.1 Host: example.com Accept: application/json, text/javascript **Example Response**: .. sourcecode:: http HTTP/1.1 200 OK Vary: Accept Content-Type: application/json { count: 1, items: [ { "id": 123, "name": "Corp", "notes": "Corporate Network", "cidr": "1.2.3.4/22" }, ], total: 1, page: 1, auth: { authenticated: true, user: "******" } } :statuscode 200: no error :statuscode 401: Authentication failure. Please login. """ self.reqparse.add_argument('count', type=int, default=30, location='args') self.reqparse.add_argument('page', type=int, default=1, location='args') args = self.reqparse.parse_args() page = args.pop('page', None) count = args.pop('count', None) result = NetworkWhitelistEntry.query.order_by(NetworkWhitelistEntry.id).paginate(page, count, error_out=False) items = [] for entry in result.items: whitelistentry_marshaled = marshal(entry.__dict__, WHITELIST_FIELDS) items.append(whitelistentry_marshaled) marshaled_dict = { 'total': result.total, 'count': len(items), 'page': result.page, 'items': items, 'auth': self.auth_dict } return marshaled_dict, 200 def post(self): """ .. http:post:: /api/1/whitelistcidrs Create a new CIDR whitelist entry. **Example Request**: .. sourcecode:: http POST /api/1/whitelistcidrs HTTP/1.1 Host: example.com Accept: application/json { "name": "Corp", "notes": "Corporate Network", "cidr": "1.2.3.4/22" } **Example Response**: .. sourcecode:: http HTTP/1.1 201 Created Vary: Accept Content-Type: application/json { "id": 123, "name": "Corp", "notes": "Corporate Network", "cidr": "1.2.3.4/22" } :statuscode 201: created :statuscode 401: Authentication Error. Please Login. """ self.reqparse.add_argument('name', required=True, type=text_type, help='Must provide account name', location='json') self.reqparse.add_argument('cidr', required=True, type=text_type, help='Network CIDR required.', location='json') self.reqparse.add_argument('notes', required=False, type=text_type, help='Add context.', location='json') args = self.reqparse.parse_args() name = args['name'] cidr = args.get('cidr', True) notes = args.get('notes', None) whitelist_entry = NetworkWhitelistEntry() whitelist_entry.name = name whitelist_entry.cidr = cidr if notes: whitelist_entry.notes = notes db.session.add(whitelist_entry) db.session.commit() db.session.refresh(whitelist_entry) whitelistentry_marshaled = marshal(whitelist_entry.__dict__, WHITELIST_FIELDS) whitelistentry_marshaled['auth'] = self.auth_dict return whitelistentry_marshaled, 201
class ItemAuditList(AuthenticatedService): decorators = [rbac.allow(["View"], ["GET"])] def get(self): """ .. http:get:: /api/1/issues Get a list of Audit Issues matching the given criteria **Example Request**: .. sourcecode:: http GET /api/1/issues HTTP/1.1 Host: example.com Accept: application/json, text/javascript **Example Response**: .. sourcecode:: http HTTP/1.1 200 OK Vary: Accept Content-Type: application/json { items: [ { account: "example_account", justification: null, name: "example_name", technology: "s3", issue: "Example Issue", region: "us-east-1", score: 10, notes: "Example Notes", item_id: 11, justified: false, justified_date: null, id: 595 } ], total: 1, page: 1, auth: { authenticated: true, user: "******" } } :statuscode 200: no error :statuscode 401: Authentication failure. Please login. """ self.reqparse.add_argument('count', type=int, default=30, location='args') self.reqparse.add_argument('page', type=int, default=1, location='args') self.reqparse.add_argument('regions', type=str, default=None, location='args') self.reqparse.add_argument('accounts', type=str, default=None, location='args') self.reqparse.add_argument('technologies', type=str, default=None, location='args') self.reqparse.add_argument('names', type=str, default=None, location='args') self.reqparse.add_argument('arns', type=str, default=None, location='args') self.reqparse.add_argument('active', type=str, default=None, location='args') self.reqparse.add_argument('searchconfig', type=str, default=None, location='args') self.reqparse.add_argument('enabledonly', type=bool, default=None, location='args') args = self.reqparse.parse_args() page = args.pop('page', None) count = args.pop('count', None) for k, v in args.items(): if not v: del args[k] query = ItemAudit.query.join("item") if 'regions' in args: regions = args['regions'].split(',') query = query.filter(Item.region.in_(regions)) if 'accounts' in args: accounts = args['accounts'].split(',') query = query.join((Account, Account.id == Item.account_id)) query = query.filter(Account.name.in_(accounts)) if 'technologies' in args: technologies = args['technologies'].split(',') query = query.join((Technology, Technology.id == Item.tech_id)) query = query.filter(Technology.name.in_(technologies)) if 'names' in args: names = args['names'].split(',') query = query.filter(Item.name.in_(names)) if 'arns' in args: arns = args['arns'].split(',') query = query.filter(Item.arn.in_(arns)) if 'active' in args: active = args['active'].lower() == "true" query = query.join( (ItemRevision, Item.latest_revision_id == ItemRevision.id)) query = query.filter(ItemRevision.active == active) if 'searchconfig' in args: search = args['searchconfig'] query = query.filter( (ItemAudit.issue.ilike('%{}%'.format(search))) | (ItemAudit.notes.ilike('%{}%'.format(search))) | (ItemAudit.justification.ilike('%{}%'.format(search))) | (Item.name.ilike('%{}%'.format(search)))) if 'enabledonly' in args: query = query.join( (AuditorSettings, AuditorSettings.id == ItemAudit.auditor_setting_id)) query = query.filter(AuditorSettings.disabled == False) query = query.order_by(ItemAudit.justified, ItemAudit.score.desc()) issues = query.paginate(page, count) marshaled_dict = { 'page': issues.page, 'total': issues.total, 'auth': self.auth_dict } items_marshaled = [] for issue in issues.items: item_marshaled = marshal(issue.item.__dict__, ITEM_FIELDS) issue_marshaled = marshal(issue.__dict__, AUDIT_FIELDS) account_marshaled = {'account': issue.item.account.name} technology_marshaled = {'technology': issue.item.technology.name} if issue.justified: issue_marshaled = dict( issue_marshaled.items() + {'justified_user': issue.user.email}.items()) merged_marshaled = dict(item_marshaled.items() + issue_marshaled.items() + account_marshaled.items() + technology_marshaled.items()) items_marshaled.append(merged_marshaled) marshaled_dict['items'] = items_marshaled marshaled_dict['count'] = len(items_marshaled) return marshaled_dict, 200
class ItemAuditList(AuthenticatedService): decorators = [ rbac.allow(["View"], ["GET"]) ] def get(self): """ .. http:get:: /api/1/issues Get a list of Audit Issues matching the given criteria **Example Request**: .. sourcecode:: http GET /api/1/issues HTTP/1.1 Host: example.com Accept: application/json, text/javascript **Example Response**: .. sourcecode:: http HTTP/1.1 200 OK Vary: Accept Content-Type: application/json { items: [ { account: "example_account", account_type: "AWS", justification: null, name: "example_name", technology: "s3", issue: "Example Issue", region: AWS_DEFAULT_REGION, score: 10, notes: "Example Notes", item_id: 11, justified: false, justified_date: null, id: 595 } ], total: 1, page: 1, auth: { authenticated: true, user: "******" } } :statuscode 200: no error :statuscode 401: Authentication failure. Please login. """ self.reqparse.add_argument('count', type=int, default=30, location='args') self.reqparse.add_argument('page', type=int, default=1, location='args') self.reqparse.add_argument('regions', type=str, default=None, location='args') self.reqparse.add_argument('accounts', type=str, default=None, location='args') self.reqparse.add_argument('accounttypes', type=str, default=None, location='args') self.reqparse.add_argument('technologies', type=str, default=None, location='args') self.reqparse.add_argument('names', type=str, default=None, location='args') self.reqparse.add_argument('arns', type=str, default=None, location='args') self.reqparse.add_argument('active', type=str, default=None, location='args') self.reqparse.add_argument('searchconfig', type=str, default=None, location='args') self.reqparse.add_argument('enabledonly', type=bool, default=None, location='args') self.reqparse.add_argument('justified', type=str, default=None, location='args') self.reqparse.add_argument('summary', type=str, default=None, location='args') args = self.reqparse.parse_args() page = args.pop('page', None) count = args.pop('count', None) for k, v in list(args.items()): if not v: del args[k] query = ItemAudit.query.join("item") query = query.filter(ItemAudit.fixed == False) if 'regions' in args: regions = args['regions'].split(',') query = query.filter(Item.region.in_(regions)) if 'accounts' in args: accounts = args['accounts'].split(',') query = query.join((Account, Account.id == Item.account_id)) query = query.filter(Account.name.in_(accounts)) if 'accounttypes' in args: accounttypes = args['accounttypes'].split(',') query = query.join((Account, Account.id == Item.account_id)) query = query.join((AccountType, AccountType.id == Account.account_type_id)) query = query.filter(AccountType.name.in_(accounttypes)) if 'technologies' in args: technologies = args['technologies'].split(',') query = query.join((Technology, Technology.id == Item.tech_id)) query = query.filter(Technology.name.in_(technologies)) if 'names' in args: names = args['names'].split(',') query = query.filter(Item.name.in_(names)) if 'arns' in args: arns = args['arns'].split(',') query = query.filter(Item.arn.in_(arns)) if 'active' in args: active = args['active'].lower() == "true" query = query.join((ItemRevision, Item.latest_revision_id == ItemRevision.id)) query = query.filter(ItemRevision.active == active) if 'searchconfig' in args: search = args['searchconfig'].split(',') conditions = [] for searchterm in search: conditions.append(ItemAudit.issue.ilike('%{}%'.format(searchterm))) conditions.append(ItemAudit.notes.ilike('%{}%'.format(searchterm))) conditions.append(ItemAudit.justification.ilike('%{}%'.format(searchterm))) conditions.append(Item.name.ilike('%{}%'.format(searchterm))) query = query.filter(or_(*conditions)) if 'enabledonly' in args: query = query.join((AuditorSettings, AuditorSettings.id == ItemAudit.auditor_setting_id)) query = query.filter(AuditorSettings.disabled == False) if 'justified' in args: justified = args['justified'].lower() == "true" query = query.filter(ItemAudit.justified == justified) if 'summary' in args: # Summary wants to order by oldest issues # TODO: Add date_created column to ItemAudit, and have summary order by date_created # Order by justified_date until date_created exists query = query.order_by(ItemAudit.justified_date.asc()) else: query = query.order_by(ItemAudit.justified, ItemAudit.score.desc()) issues = query.paginate(page, count) marshaled_dict = { 'page': issues.page, 'total': issues.total, 'auth': self.auth_dict } items_marshaled = [] for issue in issues.items: # TODO: This MUST be modified when switching to new issue logic in future: # Currently there should be exactly 1 item in the list of sub_items: item_marshaled = marshal(issue.item.__dict__, ITEM_FIELDS) issue_marshaled = marshal(issue.__dict__, AUDIT_FIELDS) account_marshaled = {'account': issue.item.account.name} accounttype_marshaled = {'account_type': issue.item.account.account_type.name} technology_marshaled = {'technology': issue.item.technology.name} links = [] for link in issue.sub_items: item_link_marshaled = marshal(link.__dict__, ITEM_LINK_FIELDS) links.append(item_link_marshaled) issue_marshaled['item_links'] = links if issue.justified: if issue.user is not None: issue_marshaled = dict( list(issue_marshaled.items()) + list({'justified_user': issue.user.email}.items())) merged_marshaled = dict( list(item_marshaled.items()) + list(issue_marshaled.items()) + list(account_marshaled.items()) + list(accounttype_marshaled.items()) + list(technology_marshaled.items())) items_marshaled.append(merged_marshaled) marshaled_dict['items'] = items_marshaled marshaled_dict['count'] = len(items_marshaled) return marshaled_dict, 200
class AzureAD(Resource): """ This class serves as an example of how one might implement an SSO provider for use with Security Monkey. In this example we use a OpenIDConnect authentication flow, that is essentially OAuth2 underneath. """ decorators = [rbac.allow(["anonymous"], ["GET", "POST"])] def __init__(self): self.reqparse = reqparse.RequestParser() super(AzureAD, self).__init__() def get(self): return self.post() def get_idp_cert(self, id_token, jwks_url): header_data = fetch_token_header_payload(id_token)[0] # retrieve the key material as specified by the token header r = requests.get(jwks_url) for key in r.json()['keys']: if key['kid'] == header_data['kid']: secret = get_rsa_public_key(key['n'], key['e']) algo = header_data['alg'] return secret, algo else: return dict(message='Key not found'), 403 def validate_id_token(self, id_token, client_id, jwks_url): # validate your token based on the key it was signed with try: (secret, algo) = self.get_idp_cert(id_token, jwks_url) token = jwt.decode(id_token, secret.decode('utf-8'), algorithms=[algo], audience=client_id) if 'upn' in token: return token['upn'] elif 'email' in token: return token['email'] else: return dict( message="Unable to obtain user information from token") except jwt.DecodeError: return dict(message='Token is invalid'), 403 except jwt.ExpiredSignatureError: return dict(message='Token has expired'), 403 except jwt.InvalidTokenError: return dict(message='Token is invalid'), 403 def post(self): if "aad" not in current_app.config.get("ACTIVE_PROVIDERS"): return "AzureAD is not enabled in the config. See the ACTIVE_PROVIDERS section.", 404 default_state = 'clientId,{client_id},redirectUri,{redirectUri},return_to,{return_to}'.format( client_id=current_app.config.get('AAD_CLIENT_ID'), redirectUri=current_app.config.get('AAD_REDIRECT_URI'), return_to=current_app.config.get('WEB_PATH')) self.reqparse.add_argument('code', type=str, required=True) self.reqparse.add_argument('id_token', type=str, required=True) self.reqparse.add_argument('state', type=str, required=False, default=default_state) args = self.reqparse.parse_args() client_id = args['state'].split(',')[1] redirect_uri = args['state'].split(',')[3] return_to = args['state'].split(',')[5] id_token = args['id_token'] if not validate_redirect_url(return_to): return_to = current_app.config.get('WEB_PATH') # fetch token public key jwks_url = current_app.config.get('AAD_JWKS_URL') # Validate id_token and extract username (email) username = self.validate_id_token(id_token, client_id, jwks_url) user = setup_user(username, '', current_app.config.get('AAD_DEFAULT_ROLE', 'View')) # Tell Flask-Principal the identity changed identity_changed.send(current_app._get_current_object(), identity=Identity(user.id)) login_user(user) db.session.commit() db.session.refresh(user) return redirect(return_to, code=302)
class ItemGet(AuthenticatedService): decorators = [rbac.allow(['View'], ["GET"])] def get(self, item_id): """ .. http:get:: /api/1/item/1234 Get a specific item **Example Request**: .. sourcecode:: http GET /api/1/item/1234 HTTP/1.1 Host: example.com Accept: application/json **Example Response**: .. sourcecode:: http HTTP/1.1 200 OK Vary: Accept Content-Type: application/json { "item": { "account": "example_account", "region": "us-east-1", "technology": "elb", "id": 1234, "name": "example_name" }, "revisions": [ { "active": false, "date_created": "2014-04-11 17:05:06.701936", "config": {}, "item_id": 1234, "id": 213784 } ], "auth": { "authenticated": true, "user": "******" }, "issues": [], "comments": [] } :statuscode 200: no error :statuscode 401: Authenticaiton Error Please login. """ query = Item.query.filter(Item.id == item_id) result = query.first() # result should be an Item with a list of audit thingers and a list of # revisions retval = {} item_marshaled = marshal(result.__dict__, ITEM_FIELDS) item_marshaled = dict(item_marshaled.items() + {'account': result.account.name}.items() + {'technology': result.technology.name}.items()) retval['item'] = item_marshaled retval['issues'] = [] retval['auth'] = self.auth_dict comments_marshaled = [] for comment in result.comments: comment_marshaled = marshal(comment, ITEM_COMMENT_FIELDS) comment_marshaled = dict(comment_marshaled.items() + {'user': comment.user.email}.items()) comments_marshaled.append(comment_marshaled) retval['comments'] = comments_marshaled for issue in result.issues: if issue.auditor_setting.disabled: continue issue_marshaled = marshal(issue.__dict__, AUDIT_FIELDS) if issue.user is not None: issue_marshaled = dict( issue_marshaled.items() + {'justified_user': issue.user.email}.items()) links = [] for link in issue.sub_items: item_link_marshaled = marshal(link.__dict__, ITEM_LINK_FIELDS) links.append(item_link_marshaled) issue_marshaled['item_links'] = links retval['issues'].append(issue_marshaled) retval['revisions'] = [] for revision in result.revisions.all(): revision_marshaled = marshal(revision.__dict__, REVISION_FIELDS) retval['revisions'].append(revision_marshaled) return retval, 200
class OneLogin(Resource): decorators = [rbac.allow(["anonymous"], ["GET", "POST"])] def __init__(self): self.reqparse = reqparse.RequestParser() self.req = OneLogin.prepare_from_flask_request(request) super(OneLogin, self).__init__() @staticmethod def prepare_from_flask_request(req): url_data = urlparse(req.url) return { 'http_host': req.host, 'server_port': url_data.port, 'script_name': req.path, 'get_data': req.args.copy(), 'post_data': req.form.copy(), 'https': ("on" if current_app.config.get("ONELOGIN_HTTPS") else "off") } def get(self): return self.post() def _consumer(self, auth): auth.process_response() errors = auth.get_errors() if not errors: if auth.is_authenticated: return True else: return False else: current_app.logger.error('Error processing %s' % (', '.join(errors))) return False def post(self): if "onelogin" not in current_app.config.get("ACTIVE_PROVIDERS"): return "Onelogin is not enabled in the config. See the ACTIVE_PROVIDERS section.", 404 auth = OneLogin_Saml2_Auth(self.req, current_app.config.get("ONELOGIN_SETTINGS")) self.reqparse.add_argument('return_to', required=False, default=current_app.config.get('WEB_PATH')) self.reqparse.add_argument('acs', required=False) self.reqparse.add_argument('sls', required=False) args = self.reqparse.parse_args() return_to = args['return_to'] if args['acs'] != None: # valids the SAML response and checks if successfully authenticated if self._consumer(auth): email = auth.get_attribute( current_app.config.get("ONELOGIN_EMAIL_FIELD"))[0] user = User.query.filter(User.email == email).first() # if we get an sso user create them an account if not user: user = User( email=email, active=True, role=current_app.config.get('ONELOGIN_DEFAULT_ROLE') # profile_picture=profile.get('thumbnailPhotoUrl') ) db.session.add(user) db.session.commit() db.session.refresh(user) # Tell Flask-Principal the identity changed identity_changed.send(current_app._get_current_object(), identity=Identity(user.id)) login_user(user) db.session.commit() db.session.refresh(user) self_url = OneLogin_Saml2_Utils.get_self_url(self.req) if 'RelayState' in request.form and self_url != request.form[ 'RelayState']: return redirect(auth.redirect_to( request.form['RelayState']), code=302) else: return redirect(current_app.config.get('BASE_URL'), code=302) else: return dict(message='OneLogin authentication failed.'), 403 elif args['sls'] != None: return dict(message='OneLogin SLS not implemented yet.'), 405 else: return redirect(auth.login(return_to=return_to))