├── CONTRIBUTING.md ├── frida ├── controller │ ├── __init__.py │ ├── mongodb.py │ ├── base.py │ ├── factory.py │ ├── mariadb.py │ ├── postgres.py │ └── vertica.py ├── manifest │ ├── __init__.py │ ├── profile_mariadb.py │ ├── profile_mongodb.py │ ├── profile_postgres.py │ ├── profile_vertica.py │ └── base.py ├── adapter │ ├── __init__.py │ ├── factory.py │ ├── mariadb.py │ ├── postgres.py │ ├── vertica.py │ ├── mongodb.py │ └── base.py ├── __init__.py ├── utils.py └── database.py ├── flask_aaf ├── __init__.py ├── util.py ├── decorators.py └── aaf.py ├── Dockerfile ├── base_ms_blueprint.py ├── requirements.txt ├── LICENSE.md ├── README.md ├── .gitignore └── app.py /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 AT&T Intellectual Property. All rights reserved. -------------------------------------------------------------------------------- /frida/controller/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 AT&T Intellectual Property. All rights reserved. 2 | 3 | -------------------------------------------------------------------------------- /frida/manifest/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Manifests provide a structure to define a query and its return format 3 | for use with frida Adapters. 4 | """ -------------------------------------------------------------------------------- /flask_aaf/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 AT&T Intellectual Property. All rights reserved. 2 | 3 | """ 4 | Flask Extension for AT&T's AAF Framework 5 | 6 | author: sm663k@att.com 7 | 8 | """ -------------------------------------------------------------------------------- /frida/adapter/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 AT&T Intellectual Property. All rights reserved. 2 | 3 | """ 4 | Adapters extract content from target hosts and return it as iterables 5 | """ -------------------------------------------------------------------------------- /frida/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 AT&T Intellectual Property. All rights reserved. 2 | 3 | """ 4 | FRIendly Database Analyzer 5 | """ 6 | # Modes that Frida will operate in 7 | VALID_MODES = {"modes": ["local", "sync", "async"]} -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 AT&T Intellectual Property. All rights reserved. 2 | 3 | FROM python:3.6-slim 4 | LABEL author=sm663k@att.com name=frida version=0.0.1 5 | WORKDIR /usr/src/app 6 | COPY . . 7 | RUN pip --proxy="http://one.proxy.att.com:8080/" install -r requirements.txt 8 | EXPOSE 5000 9 | CMD [ "gunicorn", "-w 4", "-b 0.0.0.0:5000", "app:app" ] -------------------------------------------------------------------------------- /flask_aaf/util.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 AT&T Intellectual Property. All rights reserved. 2 | 3 | import http 4 | 5 | def auth_reject(realm=None): 6 | """ 7 | Returns a HTTP 401 UNAUTHORIZED response to a user that is being rejected from requesting a resource. 8 | 9 | Use the realm param to control which realm is provided in the rejection 10 | """ 11 | auth_header_value = 'Basic' 12 | if realm is not None: 13 | auth_header_value = auth_header_value + ' realm={realm}'.format(realm=realm) 14 | 15 | return ('', http.HTTPStatus.UNAUTHORIZED, {'WWW-Authenticate': auth_header_value}) -------------------------------------------------------------------------------- /base_ms_blueprint.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 AT&T Intellectual Property. All rights reserved. 2 | 3 | from flask import Blueprint, abort 4 | from http import HTTPStatus 5 | 6 | base_ms = Blueprint('base_ms', __name__, static_folder='static') 7 | 8 | @base_ms.route('/health/') 9 | def health(): 10 | try: 11 | # Add additional health checks here 12 | return ('', HTTPStatus.OK) 13 | except: 14 | abort(500) 15 | 16 | @base_ms.route('/ready/') 17 | def ready(): 18 | try: 19 | # Add additional ready checks here 20 | return ('', HTTPStatus.OK) 21 | except: 22 | abort(500) 23 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | asn1crypto==0.24.0 2 | astroid==2.0.4 3 | certifi==2018.10.15 4 | cffi==1.11.5 5 | chardet==3.0.4 6 | Click==7.0 7 | colorama==0.4.0 8 | cryptography==2.3.1 9 | cx-Oracle==7.0.0 10 | Flask==1.0.2 11 | future==0.17.1 12 | gunicorn==19.9.0 13 | idna==2.7 14 | isort==4.3.4 15 | itsdangerous==1.1.0 16 | Jinja2==2.10 17 | lazy-object-proxy==1.3.1 18 | MarkupSafe==1.1.0 19 | mccabe==0.6.1 20 | psycopg2==2.7.6.1 21 | pycparser==2.19 22 | pylint==2.1.1 23 | pymongo==3.7.2 24 | PyMySQL==0.9.2 25 | python-dateutil==2.7.5 26 | pytz==2018.7 27 | requests==2.20.1 28 | six==1.11.0 29 | typed-ast==1.1.0 30 | urllib3==1.24.1 31 | vertica-python==0.8.0 32 | Werkzeug==0.14.1 33 | wrapt==1.10.11 34 | -------------------------------------------------------------------------------- /flask_aaf/decorators.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 AT&T Intellectual Property. All rights reserved. 2 | 3 | from functools import wraps 4 | from flask_aaf.util import auth_reject 5 | import flask 6 | 7 | def basic_auth_req(f): 8 | @wraps(f) 9 | def decorated_function(*args, **kwargs): 10 | if flask.request.authorization is None: 11 | return auth_reject() 12 | return f(*args, **kwargs) 13 | return decorated_function 14 | 15 | # Use as an example for decorators with params 16 | def basic_auth_req_option(active=False): 17 | def decorator(f): 18 | @wraps(f) 19 | def decorated_function(*args, **kwargs): 20 | if active: 21 | if flask.request.authorization is None: 22 | return auth_reject() 23 | return f(*args, **kwargs) 24 | return decorated_function 25 | return decorator -------------------------------------------------------------------------------- /frida/controller/mongodb.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 AT&T Intellectual Property. All rights reserved. 2 | 3 | import json 4 | import frida.database as fdb 5 | import frida.manifest.profile_mongodb as modb 6 | from frida.controller.base import BaseController 7 | from frida.adapter.factory import AdapterFactory 8 | 9 | class MongoDbController(BaseController): 10 | def __init__(self): 11 | pass 12 | 13 | def profile(self, target_db: fdb.Database): 14 | """ 15 | Profiles a database and returns its technical metadata to a JSON file 16 | """ 17 | adapter = AdapterFactory.get_bound_adapter(database=target_db) 18 | metadata = {"FRIDA.DATABASE": []} 19 | 20 | for model in adapter.execute(modb._mongo_get_db_stats): 21 | metadata["FRIDA.DATABASE"].append(model) 22 | 23 | 24 | return json.dumps(metadata) 25 | -------------------------------------------------------------------------------- /frida/utils.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 AT&T Intellectual Property. All rights reserved. 2 | 3 | """ 4 | Helper functions for frida 5 | """ 6 | import datetime 7 | 8 | def get_unique_filename(*args, delim='_', ext='', datetimed=True, ms=False) -> str: 9 | """ 10 | Returns filenames unique-ified by the args, delimited by delim (default underscore 11 | to make splitting the name from the datetime easier), ending with ext, 12 | and default including the current ISO datetime, optionally with millisecond precision 13 | """ 14 | base = delim.join(map(str, args)) 15 | if datetimed: 16 | base = delim.join([base, '{iso_datetime}']) 17 | iso_now = datetime.datetime.now().isoformat() 18 | if not ms: 19 | # Drop milliseconds - only 1 period in ISODT-6 20 | iso_now, __ , __ = iso_now.partition('.') 21 | 22 | base = base.format(iso_datetime=iso_now) 23 | 24 | return base + ext -------------------------------------------------------------------------------- /frida/database.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 AT&T Intellectual Property. All rights reserved. 2 | 3 | DB_TECH_TYPES = [ 4 | 'mariadb', 5 | 'vertica', 6 | 'postgres', 7 | 'oracle', 8 | 'mongodb' 9 | ] 10 | 11 | class Database(object): 12 | def __init__(self, host, port, user, passwd, database, tech_type: "Database type from DB_TECH_TYPES"): 13 | self.host = host 14 | self.port = port 15 | self.user = user 16 | self.passwd = passwd 17 | self.database = database 18 | 19 | #To perform @property validation 20 | self.tech_type = tech_type 21 | 22 | @property 23 | def tech_type(self): 24 | return self._tech_type 25 | 26 | @tech_type.setter 27 | def tech_type(self, tech_type): 28 | if tech_type not in DB_TECH_TYPES: 29 | raise ValueError("tech_type must be enumerated in frida.database.DB_TECH_TYPES.") 30 | self._tech_type = tech_type 31 | 32 | @tech_type.deleter 33 | def tech_type(self): 34 | del self._tech_type 35 | -------------------------------------------------------------------------------- /frida/controller/base.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 AT&T Intellectual Property. All rights reserved. 2 | 3 | """ 4 | Controllers execute Manifests against Adapters to extract metadata from target hosts, 5 | then format and return it as JSON 6 | """ 7 | import json 8 | import frida.database as fdb 9 | from frida.adapter.factory import AdapterFactory 10 | from abc import ABC, abstractmethod 11 | 12 | class BaseController(ABC): 13 | def __init__(self): 14 | pass 15 | 16 | @abstractmethod 17 | def profile(self, target_db: fdb.Database): 18 | """ 19 | Uses an Adapter to profile a database and returns its technical metadata to a JSON file 20 | 21 | NOTE: Some SQL types that appear in DB metadata (datetime) are not JSON 22 | serializable, so you must first ensure that json.dumps(obj, default=str) is properly set. 23 | 24 | See: json.dumps `default` param, frida.controller.mariadb for reference 25 | """ 26 | 27 | adapter = AdapterFactory.get_bound_adapter(target_db) 28 | raise NotImplementedError("BaseController is abstract - you must instantiate a child class") 29 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright 2019 AT&T Intellectual Property. All other rights reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software 6 | and associated documentation files (the "Software"), to deal in the Software without 7 | restriction, including without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all copies or 12 | substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN 18 | AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 19 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 AT&T Intellectual Property. All rights reserved. 2 | 3 | # Introduction 4 | 5 | Meet Frida - the FRIendly Database Analyzer 6 | 7 | Developed for AT&T by Stuart Minshull, 2018 8 | 9 | Technical Advisor is Don Subert (ds390s@att.com) of AT&T 10 | 11 | # Configuration 12 | 13 | Requires a config.py file, in the project root directory with the following properties: 14 | 15 | * AAF_SERVICE_AUTH = ('< full@qualified.aaf.id >', '< password >') 16 | * AAF_SERVICE_REALM = '' 17 | * AAF_ROOT_URI = 'https://aaf.it.att.com:8095/proxy' # or whatever alternate AAF root URI 18 | 19 | # Client Requests 20 | 21 | Example API request to analyze a MariaDB instance: 22 | 23 | curl -X POST \ 24 | http://:/profile/ \ 25 | -H 'Authorization: Basic ' \ 26 | -H 'Content-Type: application/json' \ 27 | -d '{ 28 | "host": "", 29 | "port": , 30 | "user": "", 31 | "passwd": " BaseController: 19 | 20 | return cls._init_controller_subclass(database.tech_type) 21 | 22 | @classmethod 23 | def _init_controller_subclass(cls, tech_type: str): 24 | """ 25 | Instantiates the subclass of BaseController corresponding to the passed tech_type 26 | """ 27 | if tech_type not in fdb.DB_TECH_TYPES: 28 | raise ValueError("Error: tech_type must be enumerated in database.DB_TECH_TYPES") 29 | 30 | elif tech_type == 'mariadb': 31 | return MariaDbController() 32 | 33 | elif tech_type == 'vertica': 34 | return VerticaController() 35 | 36 | elif tech_type == 'postgres': 37 | return PostgresController() 38 | 39 | elif tech_type == 'mongodb': 40 | return MongoDbController() 41 | 42 | else: 43 | raise ValueError("No Controller subclass matching the passed tech_type") 44 | -------------------------------------------------------------------------------- /frida/controller/mariadb.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 AT&T Intellectual Property. All rights reserved. 2 | 3 | import json 4 | import frida.database as fdb 5 | import frida.manifest.profile_mariadb as pmdb 6 | from frida.controller.base import BaseController 7 | from frida.adapter.factory import AdapterFactory 8 | 9 | class MariaDbController(BaseController): 10 | def __init__(self): 11 | pass 12 | 13 | def profile(self, target_db: fdb.Database): 14 | """ 15 | Profiles a database and returns its technical metadata to a JSON file 16 | """ 17 | adapter = AdapterFactory.get_bound_adapter(database=target_db) 18 | metadata = {"FRIDA.SCHEMAS": []} 19 | 20 | pmdb._maria_get_models.add_bindings(db=target_db.database) 21 | for model in adapter.execute(pmdb._maria_get_models): 22 | schema_name = model['SCHEMA_NAME'] 23 | 24 | tables = [] 25 | pmdb._maria_get_entities.add_bindings(db=target_db.database, schema=schema_name) 26 | for entity in adapter.execute(pmdb._maria_get_entities): 27 | table_name = entity['TABLE_NAME'] 28 | 29 | columns = [] 30 | pmdb._maria_get_attributes.add_bindings(db=target_db.database, schema=schema_name, entity=table_name) 31 | for attr in adapter.execute(pmdb._maria_get_attributes): 32 | columns.append(attr) 33 | 34 | entity["FRIDA.COLUMNS"] = columns 35 | tables.append(entity) 36 | 37 | model["FRIDA.TABLES"] = tables 38 | metadata["FRIDA.SCHEMAS"].append(model) 39 | 40 | return json.dumps(metadata, default=str) -------------------------------------------------------------------------------- /frida/manifest/profile_postgres.py: -------------------------------------------------------------------------------- 1 | from frida.manifest.base import Manifest 2 | 3 | # Name of the metadata catalog for Postgres 4 | MD_CATALOG = 'information_schema' 5 | 6 | _postgres_get_models = Manifest( 7 | name='get_models', 8 | tech_type = 'postgres', 9 | query_template="SELECT SCHEMA_NAME FROM {catalog}.SCHEMATA;", 10 | query_bindings={'catalog': MD_CATALOG}, 11 | return_format={ 12 | 'SCHEMA_NAME': 0 13 | }) 14 | 15 | _postgres_get_entities = Manifest( 16 | name='get_entities', 17 | tech_type = 'postgres', 18 | query_template="SELECT TABLE_NAME FROM \ 19 | {catalog}.TABLES WHERE TABLE_SCHEMA='{schema}' \ 20 | AND TABLE_TYPE='BASE TABLE';", 21 | query_bindings={'catalog': MD_CATALOG}, 22 | return_format={ 23 | 'TABLE_NAME': 0 24 | }) 25 | 26 | _postgres_get_views = Manifest( 27 | name='get_views', 28 | tech_type = 'postgres', 29 | query_template="SELECT TABLE_NAME, TABLE_SCHEMA, IS_UPDATABLE \ 30 | FROM {catalog}.VIEWS WHERE TABLE_SCHEMA='{schema}';", 31 | query_bindings={'catalog': MD_CATALOG}, 32 | return_format={ 33 | 'VIEW_NAME': 0, 34 | 'TABLE_SCHEMA': 1, 35 | 'IS_UPDATABLE': 2, 36 | }) 37 | 38 | _postgres_get_attributes = Manifest( 39 | name='get_attributes', 40 | tech_type = 'postgres', 41 | query_template="SELECT COLUMN_NAME, DATA_TYPE, \ 42 | COLUMN_DEFAULT, IS_IDENTITY FROM \ 43 | {catalog}.COLUMNS WHERE TABLE_SCHEMA='{schema}' \ 44 | AND TABLE_NAME='{entity}';", 45 | query_bindings={'catalog': MD_CATALOG}, 46 | return_format={ 47 | 'COLUMN_NAME': 0, 48 | 'COLUMN_TYPE': 1, 49 | 'COLUMN_DEFAULT': 2, 50 | 'IS_IDENTITY': 3 51 | }) 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 AT&T Intellectual Property. All rights reserved. 2 | 3 | # Convention 4 | config.py 5 | config/ 6 | 7 | # Byte-compiled / optimized / DLL files 8 | __pycache__/ 9 | __pycache__ 10 | *.py[cod] 11 | *$py.class 12 | 13 | # C extensions 14 | *.so 15 | 16 | # Distribution / packaging 17 | .Python 18 | env/ 19 | build/ 20 | develop-eggs/ 21 | dist/ 22 | downloads/ 23 | eggs/ 24 | .eggs/ 25 | lib/ 26 | lib64/ 27 | parts/ 28 | sdist/ 29 | var/ 30 | wheels/ 31 | *.egg-info/ 32 | .installed.cfg 33 | *.egg 34 | 35 | # PyInstaller 36 | # Usually these files are written by a python script from a template 37 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 38 | *.manifest 39 | *.spec 40 | 41 | # Installer logs 42 | pip-log.txt 43 | pip-delete-this-directory.txt 44 | 45 | # Unit test / coverage reports 46 | htmlcov/ 47 | .tox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *.cover 54 | .hypothesis/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # pyenv 81 | .python-version 82 | 83 | # celery beat schedule file 84 | celerybeat-schedule 85 | 86 | # SageMath parsed files 87 | *.sage.py 88 | 89 | # dotenv 90 | .env 91 | 92 | # virtualenv 93 | .venv 94 | venv/ 95 | ENV/ 96 | 97 | # venv on windows 98 | Scripts/ 99 | Lib/ 100 | 101 | # venv on linux 102 | pyvenv.cfg 103 | bin/ 104 | lib/ 105 | pip-selfcheck.json 106 | 107 | 108 | # Spyder project settings 109 | .spyderproject 110 | .spyproject 111 | 112 | # Rope project settings 113 | .ropeproject 114 | 115 | # mkdocs documentation 116 | /site 117 | 118 | # mypy 119 | .mypy_cache/ 120 | 121 | # VS Code 122 | .vscode 123 | 124 | # Pycharm 125 | .idea 126 | -------------------------------------------------------------------------------- /frida/controller/postgres.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 AT&T Intellectual Property. All rights reserved. 2 | 3 | import json 4 | import frida.database as fdb 5 | import frida.manifest.profile_postgres as ppdb 6 | from frida.controller.base import BaseController 7 | from frida.adapter.factory import AdapterFactory 8 | 9 | class PostgresController(BaseController): 10 | def __init__(self): 11 | pass 12 | 13 | def profile(self, target_db: fdb.Database): 14 | """ 15 | Profiles a database and returns its technical metadata to a JSON file 16 | """ 17 | adapter = AdapterFactory.get_bound_adapter(database=target_db) 18 | metadata = {"FRIDA.SCHEMAS": []} 19 | 20 | for model in adapter.execute(ppdb._postgres_get_models): 21 | schema_name = model['SCHEMA_NAME'] 22 | 23 | tables = [] 24 | ppdb._postgres_get_entities.add_bindings(schema=schema_name) 25 | for entity in adapter.execute(ppdb._postgres_get_entities): 26 | entity_name = entity['TABLE_NAME'] 27 | 28 | t_columns = [] 29 | ppdb._postgres_get_attributes.add_bindings(schema=schema_name, entity=entity_name) 30 | for t_attr in adapter.execute(ppdb._postgres_get_attributes): 31 | t_columns.append(t_attr) 32 | 33 | entity["FRIDA.COLUMNS"] = t_columns 34 | tables.append(entity) 35 | 36 | views = [] 37 | ppdb._postgres_get_views.add_bindings(schema=schema_name) 38 | for view in adapter.execute(ppdb._postgres_get_views): 39 | view_name = view['VIEW_NAME'] 40 | 41 | v_columns = [] 42 | ppdb._postgres_get_attributes.add_bindings(schema=schema_name, entity=view_name) 43 | for v_attr in adapter.execute(ppdb._postgres_get_attributes): 44 | v_columns.append(v_attr) 45 | 46 | view["FRIDA.COLUMNS"] = v_columns 47 | views.append(view) 48 | 49 | model["FRIDA.TABLES"] = tables 50 | model["FRIDA.VIEWS"] = views 51 | metadata["FRIDA.SCHEMAS"].append(model) 52 | 53 | return json.dumps(metadata, default=str) -------------------------------------------------------------------------------- /frida/controller/vertica.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 AT&T Intellectual Property. All rights reserved. 2 | 3 | import json 4 | import frida.database as fdb 5 | import frida.manifest.profile_vertica as pvdb 6 | from frida.controller.base import BaseController 7 | from frida.adapter.factory import AdapterFactory 8 | 9 | class VerticaController(BaseController): 10 | def __init__(self): 11 | pass 12 | 13 | def profile(self, target_db: fdb.Database): 14 | """ 15 | Profiles a database and returns its technical metadata to a JSON file 16 | """ 17 | adapter = AdapterFactory.get_bound_adapter(database=target_db) 18 | metadata = {"FRIDA.SCHEMAS": []} 19 | 20 | for model in adapter.execute(pvdb._vertica_get_models): 21 | schema_name = model['SCHEMA_NAME'] 22 | 23 | tables = [] 24 | pvdb._vertica_get_entities.add_bindings(catalog='v_catalog', schema=schema_name) 25 | for entity in adapter.execute(pvdb._vertica_get_entities): 26 | table_name = entity['TABLE_NAME'] 27 | 28 | t_columns = [] 29 | pvdb._vertica_get_attributes.add_bindings(catalog='v_catalog', schema=schema_name, entity=table_name) 30 | for t_attr in adapter.execute(pvdb._vertica_get_attributes): 31 | t_columns.append(t_attr) 32 | 33 | entity["FRIDA.COLUMNS"] = t_columns 34 | tables.append(entity) 35 | 36 | views = [] 37 | pvdb._vertica_get_views.add_bindings(catalog='v_catalog', schema=schema_name) 38 | for view in adapter.execute(pvdb._vertica_get_views): 39 | view_name = view['VIEW_NAME'] 40 | 41 | v_columns = [] 42 | pvdb._vertica_get_view_attributes.add_bindings(catalog='v_catalog', schema=schema_name, view=view_name) 43 | for v_attr in adapter.execute(pvdb._vertica_get_view_attributes): 44 | v_columns.append(v_attr) 45 | 46 | view["FRIDA.COLUMNS"] = v_columns 47 | views.append(view) 48 | 49 | model["FRIDA.TABLES"] = tables 50 | model["FRIDA.VIEWS"] = views 51 | metadata["FRIDA.SCHEMAS"].append(model) 52 | 53 | return json.dumps(metadata, default=str) 54 | -------------------------------------------------------------------------------- /frida/adapter/factory.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 AT&T Intellectual Property. All rights reserved. 2 | 3 | import frida.database as fdb 4 | from frida.adapter.base import BaseAdapter 5 | from frida.adapter.mariadb import MariaDbAdapter 6 | from frida.adapter.vertica import VerticaAdapter 7 | from frida.adapter.postgres import PostgresAdapter 8 | from frida.adapter.mongodb import MongoDbAdapter 9 | 10 | 11 | class AdapterFactory: 12 | """ 13 | Receives Database objects and returns properly initialized Adapter that 14 | are bound to the passed Database 15 | """ 16 | @classmethod 17 | def get_unbound_adapter(cls, tech_type) -> BaseAdapter: 18 | """ 19 | Returns an unbound Adapter subclass matching the specified tech_type 20 | """ 21 | return cls._init_adapter_subclass(tech_type) 22 | 23 | @classmethod 24 | def get_bound_adapter(cls, database: fdb.Database) -> BaseAdapter: 25 | 26 | adapter = cls._init_adapter_subclass(database.tech_type) 27 | adapter.bind_host(database) 28 | 29 | return adapter 30 | 31 | @classmethod 32 | def _init_adapter_subclass(cls, tech_type: str) -> BaseAdapter: 33 | """ 34 | Instantiates the subclass of Adapter corresponding to the passed tech_type 35 | """ 36 | tech_type = tech_type.lower() 37 | 38 | if tech_type not in fdb.DB_TECH_TYPES: 39 | raise ValueError("Error: tech_type must be enumerated in database.DB_TECH_TYPES") 40 | 41 | elif tech_type == 'mariadb': 42 | return MariaDbAdapter() 43 | 44 | elif tech_type == 'vertica': 45 | return VerticaAdapter() 46 | 47 | elif tech_type == 'postgres': 48 | return PostgresAdapter() 49 | 50 | elif tech_type == 'mongodb': 51 | return MongoDbAdapter() 52 | 53 | else: 54 | raise ValueError("No Adapter subclass matching the passed tech_type") 55 | 56 | @classmethod 57 | def check_valid_type(cls, type_str) -> bool: 58 | """ 59 | Returns a Boolean indicating if the passed str is a valid DB_TECH_TYPE 60 | """ 61 | return type_str in fdb.DB_TECH_TYPES 62 | 63 | @classmethod 64 | def get_valid_types(cls) -> list: 65 | """ 66 | Returns the list of valid DB_TECH_TYPEs 67 | """ 68 | return fdb.DB_TECH_TYPES 69 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 AT&T Intellectual Property. All rights reserved. 2 | 3 | # Relative local & stdlib 4 | # config.py is gitignored by convention, so make sure you have your own copy locally. 5 | from config import AAF_SERVICE_REALM, AAF_SERVICE_AUTH, AAF_ROOT_URI 6 | import json 7 | import os 8 | 9 | # Frida 10 | from frida.controller.factory import ControllerFactory 11 | import frida.database as fdb 12 | from frida import VALID_MODES 13 | 14 | # flask_aaf 15 | from flask_aaf.aaf import Aaf 16 | from flask_aaf.decorators import basic_auth_req 17 | from flask_aaf.util import auth_reject 18 | 19 | # Flask 20 | # Blueprint adds /health/ and /ready/ routes 21 | from flask import Flask, request, abort 22 | from base_ms_blueprint import base_ms 23 | 24 | app = Flask(__name__) 25 | app.register_blueprint(base_ms) 26 | 27 | # AAF_SERVICE_AUTH is a tuple of (username, password) - unpack it 28 | aaf = Aaf(*AAF_SERVICE_AUTH, AAF_SERVICE_REALM, AAF_ROOT_URI) 29 | 30 | 31 | @app.route('/') 32 | def index(): 33 | return "

Hello, I'm Frida!

" 34 | 35 | 36 | @app.route('/data360/') 37 | @basic_auth_req 38 | def secret(): 39 | if aaf.has_role('com.att.dplr.nextgen.member'): 40 | return "

Hi - you're a Data360 member!

" 41 | else: 42 | return auth_reject(realm='com.att.dplr.nextgen.member') 43 | 44 | 45 | @app.route('/modes/') 46 | def get_modes(): 47 | """ 48 | Returns the list of valid modes to operate a Profiler in 49 | """ 50 | return json.dumps(VALID_MODES) 51 | 52 | 53 | # TODO: POST only, needs 'callback route' to return post, and/or local mode 54 | @app.route('/profile/', methods=['POST']) 55 | def profile(): 56 | 57 | # TODO Add mode and out_loc params, alter RETURN behavior depending on mode, check outloc etc. 58 | # TODO handle posted files 59 | 60 | try: 61 | # Requires client to set "Content-Type: application/json" 62 | if request.get_json() is None: 63 | abort(400) 64 | database = fdb.Database(**request.get_json()) 65 | 66 | except KeyError: 67 | # Bad Request 68 | abort(400) 69 | 70 | controller = ControllerFactory.get_controller(database) 71 | print("Starting inspection: ") 72 | print("{0}\t{1}".format(database.host, database.tech_type)) 73 | 74 | metadata = controller.profile(database) 75 | 76 | print("Inspection complete!") 77 | return metadata 78 | 79 | 80 | if __name__ == "__main__": 81 | _host = os.environ.get('HOST') if os.environ.get('HOST') else '127.0.0.1' 82 | app.run(host=_host) 83 | -------------------------------------------------------------------------------- /frida/adapter/mariadb.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 AT&T Intellectual Property. All rights reserved. 2 | 3 | import pymysql as pysql 4 | import frida.database as fdb 5 | from frida.adapter.base import Manifest, BaseAdapter 6 | 7 | class MariaDbAdapter(BaseAdapter): 8 | 9 | def __init__(self): 10 | # To gain self._bound_host & _bound_cnx 11 | super().__init__() 12 | self.tech_type = 'mariadb' 13 | 14 | @property 15 | def bound_cnx(self): 16 | """ 17 | The open connection to the bound host. 18 | """ 19 | return self._bound_cnx 20 | 21 | @bound_cnx.setter 22 | def bound_cnx(self, cnx): 23 | """ 24 | The open connection to the bound host. 25 | """ 26 | if self._bound_cnx: 27 | print('Closing existing connection...') 28 | self._bound_cnx.close() 29 | 30 | self._bound_cnx = cnx 31 | 32 | # Tech-specific deleter for the parent @property bound_cnx 33 | @bound_cnx.deleter 34 | def bound_cnx(self): 35 | """ 36 | Close open connections before un-binding the connection; tech-specific 37 | """ 38 | self._bound_cnx.close() 39 | del self._bound_cnx 40 | 41 | def bind_host(self, database: fdb.Database): 42 | """ 43 | Bind this Adapter to a target Database; opens a connection to the passed DB and assigns 44 | the connection object to self._bound_cnx 45 | """ 46 | try: 47 | self.bound_cnx = self._connect(database) 48 | self.bound_host = database 49 | 50 | except Exception as err: 51 | print("An error occured while binding to the database: {err}".format(err=err)) 52 | return None 53 | 54 | def _connect(self, target_db: fdb.Database): 55 | """ 56 | Open a connection to the passed database 57 | """ 58 | try: 59 | cnx = pysql.connect( 60 | host = target_db.host, 61 | port = target_db.port, 62 | user = target_db.user, 63 | passwd = target_db.passwd, 64 | db = target_db.database 65 | ) 66 | 67 | return cnx 68 | 69 | except Exception as err: 70 | print("An error occured while connecting to the database: {err}".format(err=err)) 71 | return None 72 | 73 | def _query(self, prepared_q): 74 | """ 75 | Execute a prepared query against the bound database & 76 | return a cursor to the result set 77 | """ 78 | cursor = self.bound_cnx.cursor() 79 | cursor.execute(prepared_q) 80 | 81 | return cursor 82 | -------------------------------------------------------------------------------- /frida/adapter/postgres.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 AT&T Intellectual Property. All rights reserved. 2 | 3 | import psycopg2 4 | import frida.database as fdb 5 | from frida.adapter.base import Manifest, BaseAdapter 6 | 7 | class PostgresAdapter(BaseAdapter): 8 | 9 | def __init__(self): 10 | # To gain self._bound_host & _bound_cnx 11 | super().__init__() 12 | self.tech_type = 'postgres' 13 | 14 | @property 15 | def bound_cnx(self): 16 | """ 17 | The open connection to the bound host. 18 | """ 19 | return self._bound_cnx 20 | 21 | @bound_cnx.setter 22 | def bound_cnx(self, cnx): 23 | """ 24 | The open connection to the bound host. 25 | """ 26 | if self._bound_cnx: 27 | print('Closing existing connection...') 28 | self._bound_cnx.close() 29 | 30 | self._bound_cnx = cnx 31 | 32 | # Tech-specific deleter for the parent @property bound_cnx 33 | @bound_cnx.deleter 34 | def bound_cnx(self): 35 | """ 36 | Close open connections before un-binding the connection; tech-specific 37 | """ 38 | self._bound_cnx.close() 39 | del self._bound_cnx 40 | 41 | def bind_host(self, database: fdb.Database): 42 | """ 43 | Bind this Adapter to a target Database; opens a connection to the passed DB and assigns 44 | the connection object to self._bound_cnx 45 | """ 46 | try: 47 | self.bound_cnx = self._connect(database) 48 | self.bound_host = database 49 | 50 | except Exception as err: 51 | print("An error occured while binding to the database: {err}".format(err=err)) 52 | return None 53 | 54 | def _connect(self, target_db: fdb.Database): 55 | """ 56 | Open a connection to the passed database 57 | """ 58 | try: 59 | cnx = psycopg2.connect( 60 | host = target_db.host, 61 | port = target_db.port, 62 | user = target_db.user, 63 | password = target_db.passwd, 64 | dbname = target_db.database 65 | ) 66 | 67 | return cnx 68 | 69 | except Exception as err: 70 | print("An error occured while connecting to the database: {err}".format(err=err)) 71 | return None 72 | 73 | def _query(self, prepared_q): 74 | """ 75 | Execute a prepared query against the bound database & 76 | return a cursor to the result set 77 | """ 78 | cursor = self.bound_cnx.cursor() 79 | cursor.execute(prepared_q) 80 | 81 | return cursor 82 | -------------------------------------------------------------------------------- /frida/manifest/profile_vertica.py: -------------------------------------------------------------------------------- 1 | from frida.manifest.base import Manifest 2 | 3 | # name of the metadata schema for Vertica 4 | MD_CATALOG = 'v_catalog' 5 | 6 | _vertica_get_models = Manifest( 7 | name='get_models', 8 | tech_type = 'vertica', 9 | query_template="SELECT SCHEMA_NAME FROM {catalog}.SCHEMATA;", 10 | query_bindings={'catalog': MD_CATALOG}, 11 | return_format={ 12 | 'SCHEMA_NAME': 0 13 | }) 14 | 15 | _vertica_get_entities = Manifest( 16 | name='get_entities', 17 | tech_type = 'vertica', 18 | query_template="SELECT TABLE_NAME, TABLE_SCHEMA, OWNER_NAME, \ 19 | IS_SYSTEM_TABLE, IS_FLEXTABLE, CREATE_TIME FROM \ 20 | {catalog}.TABLES WHERE TABLE_SCHEMA='{schema}';", 21 | query_bindings={'catalog': MD_CATALOG}, 22 | return_format={ 23 | 'TABLE_NAME': 0, 24 | 'TABLE_SCHEMA': 1, 25 | 'OWNER_NAME': 2, 26 | 'IS_SYSTEM_TABLE': 3, 27 | 'IS_FLEXTABLE': 4, 28 | 'TABLE_CREATE_TIME': 5 29 | }) 30 | 31 | _vertica_get_attributes = Manifest( 32 | name='get_attributes', 33 | tech_type = 'vertica', 34 | query_template="SELECT COLUMN_NAME, TABLE_NAME, \ 35 | DATA_TYPE, DATA_TYPE_LENGTH, NUMERIC_PRECISION, \ 36 | ORDINAL_POSITION, IS_NULLABLE, IS_IDENTITY FROM \ 37 | {catalog}.COLUMNS WHERE TABLE_SCHEMA='{schema}' \ 38 | AND TABLE_NAME='{entity}';", 39 | query_bindings={'catalog': MD_CATALOG}, 40 | return_format={ 41 | 'COLUMN_NAME': 0, 42 | 'TABLE_NAME': 1, 43 | 'DATA_TYPE': 2, 44 | 'TYPE_MAX_LEN': 3, 45 | 'NUMERIC_PRECISION': 4, 46 | 'ORDINAL_POSITION': 5, 47 | 'IS_NULLABLE': 6, 48 | 'IS_IDENTITY': 7 49 | }) 50 | 51 | _vertica_get_views = Manifest( 52 | name='get_views', 53 | tech_type = 'vertica', 54 | query_template="SELECT TABLE_NAME, TABLE_SCHEMA, OWNER_NAME, \ 55 | CREATE_TIME FROM \ 56 | {catalog}.VIEWS WHERE TABLE_SCHEMA='{schema}';", 57 | query_bindings={'catalog': MD_CATALOG}, 58 | return_format={ 59 | 'VIEW_NAME': 0, 60 | 'TABLE_SCHEMA': 1, 61 | 'OWNER_NAME': 2, 62 | 'VIEW_CREATE_TIME': 3 63 | }) 64 | 65 | _vertica_get_view_attributes = Manifest( 66 | name='get_attributes', 67 | tech_type = 'vertica', 68 | query_template="SELECT COLUMN_NAME, TABLE_NAME, \ 69 | DATA_TYPE, DATA_TYPE_LENGTH, NUMERIC_PRECISION, \ 70 | ORDINAL_POSITION FROM \ 71 | {catalog}.VIEW_COLUMNS WHERE TABLE_SCHEMA='{schema}' \ 72 | AND TABLE_NAME='{view}';", 73 | query_bindings={'catalog': MD_CATALOG}, 74 | return_format={ 75 | 'COLUMN_NAME': 0, 76 | 'VIEW_NAME': 1, #aliasing table_name to view_name 77 | 'DATA_TYPE': 2, 78 | 'DATA_TYPE_LENGTH': 3, 79 | 'NUMERIC_PRECISION': 4, 80 | 'ORDINAL_POSITION': 5 81 | }) 82 | -------------------------------------------------------------------------------- /frida/adapter/vertica.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 AT&T Intellectual Property. All rights reserved. 2 | 3 | import vertica_python 4 | import frida.database as fdb 5 | from frida.adapter.base import Manifest, BaseAdapter 6 | 7 | class VerticaAdapter(BaseAdapter): 8 | 9 | def __init__(self): 10 | # To gain self._bound_host & _bound_cnx 11 | super().__init__() 12 | self.tech_type = 'vertica' 13 | 14 | @property 15 | def bound_cnx(self): 16 | """ 17 | The open connection to the bound host. 18 | """ 19 | return self._bound_cnx 20 | 21 | @bound_cnx.setter 22 | def bound_cnx(self, cnx): 23 | """ 24 | The open connection to the bound host. 25 | """ 26 | if self._bound_cnx: 27 | print('Closing existing connection...') 28 | self._bound_cnx.close() 29 | 30 | self._bound_cnx = cnx 31 | 32 | # Tech-specific deleter for the parent @property bound_cnx 33 | @bound_cnx.deleter 34 | def bound_cnx(self): 35 | """ 36 | Close open connections before un-binding the connection; tech-specific 37 | """ 38 | self._bound_cnx.close() 39 | del self._bound_cnx 40 | 41 | def bind_host(self, database: fdb.Database): 42 | """ 43 | Bind this Adapter to a target Database; opens a connection to the passed DB and assigns 44 | the connection object to self._bound_cnx 45 | """ 46 | try: 47 | self.bound_cnx = self._connect(database) 48 | self.bound_host = database 49 | 50 | except Exception as err: 51 | print("An error occured while binding to the database: {err}".format(err=err)) 52 | return None 53 | 54 | def _connect(self, target_db: fdb.Database): 55 | """ 56 | Open a connection to the passed database 57 | """ 58 | try: 59 | cnx = vertica_python.connect( 60 | host = target_db.host, 61 | port = target_db.port, 62 | user = target_db.user, 63 | password = target_db.passwd, 64 | database = target_db.database 65 | ) 66 | 67 | return cnx 68 | 69 | except Exception as err: 70 | print("An error occured while connecting to the database: {err}".format(err=err)) 71 | return None 72 | 73 | def _query(self, prepared_q): 74 | """ 75 | Execute a prepared query against the bound database & 76 | return a cursor to the result set 77 | """ 78 | # Vertica SDK only supports 1 cursor per connection 79 | # https://github.com/uber/vertica-python/blob/master/vertica_python/vertica/connection.py#L61 80 | 81 | cursor = self._connect(self.bound_host).cursor() 82 | cursor.execute(prepared_q) 83 | 84 | # SDK cursors do not impl __iter__ - hide the call to .iterate() in a closure 85 | def yield_row(prep_cursor): 86 | for row in prep_cursor.iterate(): 87 | yield row 88 | 89 | raise StopIteration 90 | 91 | return yield_row(cursor) 92 | -------------------------------------------------------------------------------- /frida/adapter/mongodb.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 AT&T Intellectual Property. All rights reserved. 2 | 3 | import ast 4 | import pymongo as pym 5 | import frida.database as fdb 6 | from frida.adapter.base import Manifest, BaseAdapter 7 | 8 | class MongoDbAdapter(BaseAdapter): 9 | 10 | def __init__(self): 11 | # To gain self._bound_host & _bound_cnx 12 | super().__init__() 13 | self.tech_type = 'mongodb' 14 | 15 | @property 16 | def bound_cnx(self): 17 | """ 18 | The open connection to the bound host. 19 | """ 20 | return self._bound_cnx 21 | 22 | @bound_cnx.setter 23 | def bound_cnx(self, cnx): 24 | """ 25 | The open connection to the bound host. 26 | """ 27 | if self._bound_cnx: 28 | print('Closing existing connection...') 29 | self._bound_cnx.close() 30 | 31 | self._bound_cnx = cnx 32 | 33 | # Tech-specific deleter for the parent @property bound_cnx 34 | @bound_cnx.deleter 35 | def bound_cnx(self): 36 | """ 37 | Close open connections before un-binding the connection; tech-specific 38 | """ 39 | self._bound_cnx.close() 40 | del self._bound_cnx 41 | 42 | def bind_host(self, database: fdb.Database): 43 | """ 44 | Bind this Adapter to a target Database; opens a connection to the passed DB and assigns 45 | the connection object to self._bound_cnx 46 | """ 47 | try: 48 | self.bound_cnx = self._connect(database) 49 | self.bound_host = database 50 | 51 | except Exception as err: 52 | print("An error occured while binding to the database: {err}".format(err=err)) 53 | return None 54 | 55 | def _connect(self, target_db: fdb.Database) -> pym.database: 56 | """ 57 | Open a connection to the passed database, return a pymongo Database 58 | """ 59 | try: 60 | cnx = pym.MongoClient( 61 | host = target_db.host, 62 | port = target_db.port, 63 | username = target_db.user, 64 | password = target_db.passwd, 65 | ) 66 | 67 | # MongoClient constructor does not throw on conn failure 68 | # Check w/ cheap command 69 | # https://api.mongodb.com/python/current/api/pymongo/mongo_client.html#pymongo.mongo_client.MongoClient 70 | #__ = cnx.admin.command('ismaster') 71 | 72 | # Return the database object that allows commands, not the client 73 | # Must bind different adapters to each db on host 74 | return cnx[target_db.database] 75 | 76 | except Exception as err: 77 | print("An error occured while connecting to the database: {err}".format(err=err)) 78 | return None 79 | 80 | def _format_row(self, row, return_format: dict): 81 | """ 82 | Overloaded pass-through method - pymongo already returns JSON, 83 | so just return each row. 84 | 85 | """ 86 | return row 87 | 88 | def _prepare_query(self, manifest: Manifest): 89 | """ 90 | Overloaded method to support direct passing of frida.base.Manifests 91 | 92 | MongoDbAdapter._query expects prepared_q to be of type dict, use 93 | ast.literal_eval to convert strings to dicts safely 94 | """ 95 | return ast.literal_eval( 96 | manifest.query_template.format(**manifest.query_bindings) 97 | ) 98 | 99 | def _query(self, prepared_q: dict): 100 | """ 101 | Execute a prepared query (DICTIONARY!) against the bound database & 102 | return a cursor to the result set 103 | """ 104 | return self.bound_cnx.command(prepared_q) 105 | -------------------------------------------------------------------------------- /frida/adapter/base.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 AT&T Intellectual Property. All rights reserved. 2 | 3 | import re 4 | import frida.database as fdb 5 | from frida.manifest.base import Manifest 6 | from abc import ABC, abstractmethod 7 | 8 | class BaseAdapter(ABC): 9 | """ 10 | Abstract parent class for tech-specific database adapters 11 | """ 12 | 13 | def __init__(self): 14 | self._bound_host = None 15 | self._bound_cnx = None 16 | self.tech_type = None 17 | 18 | def __enter__(self): 19 | ''' 20 | Required to use this class as a context manager 21 | ''' 22 | return self 23 | 24 | def __exit__(self, exception_type, exception_value, traceback): 25 | ''' 26 | Leaving context - close the bound_host's open cursors & connections 27 | ''' 28 | del self.bound_host 29 | 30 | @property 31 | def bound_host(self) -> fdb.Database: 32 | """ 33 | The database host that this Adapter been bound to. 34 | """ 35 | return self._bound_host 36 | 37 | @bound_host.setter 38 | def bound_host(self, host: fdb.Database): 39 | """ 40 | The database host that this Adapter been bound to. 41 | """ 42 | if not isinstance(host, fdb.Database): 43 | raise TypeError("Error: host must be of type frida.Database") 44 | self._bound_host = host 45 | 46 | @bound_host.deleter 47 | def bound_host(self): 48 | """ 49 | The database host that this Adapter been bound to. 50 | """ 51 | del self.bound_cnx 52 | del self._bound_host 53 | 54 | @property 55 | @abstractmethod 56 | def bound_cnx(self): 57 | """ 58 | The open connection to the bound host. 59 | """ 60 | raise NotImplementedError("BaseAdapter is abstract; you must instantiate a subclass.") 61 | 62 | @abstractmethod 63 | def bind_host(self, database: fdb.Database): 64 | """ 65 | Binds this Adapter to a target Database; opens a connecton to the DB and assigns 66 | self.bound_host to an object that can execute queries. 67 | (cursor, connection, etc. depending on tech) 68 | """ 69 | raise NotImplementedError("BaseAdapter is abstract; you must instantiate a subclass.") 70 | 71 | @abstractmethod 72 | def _connect(self): 73 | """ 74 | Used by Adapter.bind_host() to open a connection to the host during binding. 75 | Tech specific 76 | """ 77 | raise NotImplementedError("BaseAdapter is abstract; you must instantiate a subclass.") 78 | 79 | @abstractmethod 80 | def _query(self, prepared_q): 81 | """ 82 | Execute a prepared statement against the bound database & return an iterable 83 | """ 84 | raise NotImplementedError("BaseAdapter is abstract; you must instantiate a subclass.") 85 | 86 | def _prepare_query(self, manifest: Manifest): 87 | """ 88 | Overloaded method to support direct passing of frida.base.Manifests 89 | """ 90 | return manifest.query_template.format(**manifest.query_bindings) 91 | 92 | def _format_row(self, row, return_format: dict): 93 | """ 94 | Formats a row returned by a query according to the 95 | name-index map provided in return_format 96 | """ 97 | formatted_row = {} 98 | for key, idx in return_format.items(): 99 | formatted_row[key] = row[idx] 100 | 101 | return formatted_row 102 | 103 | def _format_row(self, row, manifest: Manifest): 104 | """ 105 | Formats a row returned by a query according to the 106 | return_format specified in the passed Manifest 107 | """ 108 | formatted_row = {} 109 | for key, idx in manifest.return_format.items(): 110 | formatted_row[key] = row[idx] 111 | 112 | return formatted_row 113 | 114 | def execute(self, manifest: Manifest): 115 | """ 116 | Uses a passed Manifest to query the bound host and return 117 | the result set as a dict according to Manifest's return format 118 | """ 119 | if not manifest.seal(): 120 | raise RuntimeError("Manifests must be sealed before querying! \ 121 | See: Manifest.seal()") 122 | 123 | if manifest.tech_type != self.tech_type: 124 | raise TypeError("Adapters can only execute Manifests that share their tech_type: \ 125 | Adapter: {selftype} vs Manifest: {manifest_type}! \ 126 | See: database.DB_TECH_TYPES".format( 127 | selftype=self.tech_type, manifest_type=manifest.tech_type) 128 | ) 129 | 130 | # Generator for easy iteration, depends on the cursor staying alive 131 | def yield_formatted(result_cursor): 132 | for row in result_cursor: 133 | formatted = self._format_row(row, manifest) 134 | yield formatted 135 | 136 | # raise StopIteration 137 | 138 | # pytest: disable 139 | return yield_formatted(self._query(self._prepare_query(manifest))) 140 | -------------------------------------------------------------------------------- /frida/manifest/base.py: -------------------------------------------------------------------------------- 1 | """ Provides queries to Adapters as well as an interface to format the returned rows 2 | 3 | Uses a "sealing" metaphor to prevent Manifests from being executed until every stated 4 | binding has a value. 5 | """ 6 | import frida.database as fdb 7 | import re 8 | 9 | class Manifest: 10 | """ 11 | Provide a query template and corresponding bindings that 12 | an Adapter can prepare and execute. Also provides a name-position map 13 | to format & return the result set as a dictionary (for use with JSON) 14 | """ 15 | def __init__(self, name: str, tech_type: str, query_template: str, query_bindings: dict, return_format: dict): 16 | self.name = name 17 | self.tech_type = tech_type 18 | self.query_template = query_template 19 | self.query_bindings = query_bindings 20 | self.return_format = return_format 21 | 22 | self._sealed = False 23 | 24 | def seal(self) -> bool: 25 | """ 26 | Seals a manifest, ensuring that every placeholder in the query_template 27 | has a corresponding binding. Adapters will only execute sealed manifests. 28 | Returns True or False, indicating the state of the seal. 29 | """ 30 | # Empty lists are falsy, check first for minor optimization 31 | if self._sealed is False: 32 | if not self._get_unbound_bindings(): 33 | self._sealed = True 34 | 35 | return self._sealed 36 | 37 | def _get_all_bindings(self) -> list: 38 | """ 39 | Return a list of the binding placeholders in self.query_template 40 | """ 41 | bindings = [] 42 | matches = re.findall(r'{([a-zA-Z0-9]+)}', self.query_template) 43 | for match in matches: 44 | bindings.append(match) 45 | 46 | return bindings 47 | 48 | def _get_unbound_bindings(self) -> list: 49 | """ 50 | Returns a list of the un-bound bindings in the query template. 51 | """ 52 | bindings = self._get_all_bindings() 53 | if not bindings: 54 | unbound = [] 55 | else: 56 | unbound = [key for key in bindings if key not in self.query_bindings] 57 | 58 | return unbound 59 | 60 | def add_bindings(self, **kwargs) -> None: 61 | """ 62 | Adds query bindings to the Manifest - the bindings must be listed in 63 | the query template. 64 | """ 65 | bindings = self._get_all_bindings() 66 | if not bindings: 67 | raise KeyError("No bindings in this Manifest") 68 | else: 69 | for key, value in kwargs.items(): 70 | if key in bindings: 71 | self.query_bindings[key] = value 72 | else: 73 | raise KeyError("Binding {key} not found in Manifest query template".format(key=key)) 74 | 75 | @property 76 | def tech_type(self): 77 | return self._tech_type 78 | 79 | @tech_type.setter 80 | def tech_type(self, tech_type): 81 | if tech_type not in fdb.DB_TECH_TYPES: 82 | raise ValueError("tech_type must be enumerated in frida.database.DB_TECH_TYPES.") 83 | self._tech_type = tech_type 84 | 85 | @tech_type.deleter 86 | def tech_type(self): 87 | del self._tech_type 88 | 89 | @property 90 | def name(self) -> str: 91 | return self._name 92 | 93 | @name.setter 94 | def name(self, value): 95 | if not isinstance(value, str): 96 | raise TypeError("name must be a string") 97 | self._name = value 98 | # Unseal manifest on changes 99 | self._sealed = False 100 | 101 | @name.deleter 102 | def name(self): 103 | del self._name 104 | 105 | @property 106 | def query_template(self) -> str: 107 | return self._query_template 108 | 109 | @query_template.setter 110 | def query_template(self, value): 111 | if not isinstance(value, str): 112 | raise TypeError("query_template must be a string") 113 | self._query_template = value 114 | # Unseal manifest on changes 115 | self._sealed = False 116 | 117 | @query_template.deleter 118 | def query(self): 119 | del self._query_template 120 | 121 | @property 122 | def query_bindings(self) -> str: 123 | return self._query_bindings 124 | 125 | @query_bindings.setter 126 | def query_bindings(self, value): 127 | if not isinstance(value, dict): 128 | raise TypeError("query_bindings must be a dict") 129 | self._query_bindings = value 130 | # Unseal manifest on changes 131 | self._sealed = False 132 | 133 | @query_bindings.deleter 134 | def query_bindings(self): 135 | del self._query_bindings 136 | 137 | @property 138 | def return_format(self) -> dict: 139 | return self._return_format 140 | 141 | @return_format.setter 142 | def return_format(self, value): 143 | if not isinstance(value, dict): 144 | raise TypeError("return_format must be a dict") 145 | self._return_format = value 146 | # Unseal manifest on changes 147 | self._sealed = False 148 | 149 | @return_format.deleter 150 | def return_format(self): 151 | del self._return_format 152 | -------------------------------------------------------------------------------- /flask_aaf/aaf.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 AT&T Intellectual Property. All rights reserved. 2 | 3 | from functools import wraps 4 | from http import HTTPStatus 5 | import flask 6 | import requests 7 | import json 8 | 9 | class Aaf(object): 10 | """ 11 | Represents the AAF home server 12 | """ 13 | def __init__(self, service_user, service_password, service_realm, root_uri='https://aaf.it.att.com:8095/proxy'): 14 | self.root_uri = root_uri 15 | self.service_user = service_user 16 | # !!! 17 | self.service_password = service_password 18 | self.service_realm = service_realm 19 | 20 | # Validate service credentials 21 | try: 22 | if not self._validate_creds(service_user, service_password): 23 | raise ValueError("Invalid service account credentials") 24 | 25 | except Exception as e: 26 | print('ERROR: Unable to validate service account credentials.') 27 | raise e 28 | 29 | def _validate_creds(self, username, password) -> bool: 30 | """ 31 | This function validates usernames & passwords in namespaces for which the service account has READ permissions. 32 | Use this decorator to protect routes on namespaces you control. 33 | 34 | If the service account does not have permission in the provided namespace, a 403 is returned regardless of 35 | the validity of the provided credentials. Ensure that the service account has read perms on the namespace 36 | that you are trying to protect. 37 | 38 | AAF determines the namespace from the username. Ex: 39 | sm663k@csp.att.com -> user sm663k in AAF namespace 'com.att.csp' 40 | example@your.namespace.att.com -> user example in AAF namespace 'com.att.namespace.your' 41 | 42 | More info: https://wiki.web.att.com/pages/viewpage.action?pageId=738754793 43 | 44 | WARN: This method cannot validate human user credentials (user@csp.att.com) unless a service account 45 | with READ permissions on com.att.csp is used. 46 | """ 47 | route_uri = '/authn/validate' 48 | route_headers = { 49 | 'Content-Type': 'application/json' 50 | } 51 | route_data = json.dumps({"id":username,"password":password}) 52 | 53 | resp = requests.request( 54 | method='POST', 55 | url='{root}{route}'.format(root=self.root_uri, route=route_uri), 56 | headers=route_headers, 57 | auth=(self.service_user, self.service_password), 58 | data=route_data 59 | ) 60 | 61 | 62 | if resp.status_code == requests.codes.ok: # pylint: disable=no-member 63 | return True 64 | 65 | return False 66 | 67 | def has_perm(self, perm) -> bool: 68 | """ 69 | Checks if the requesting user has the specified AAF permission 70 | """ 71 | auth = flask.request.authorization 72 | route_uri = '/authz/perms/user/{user}'.format(user=auth.username) 73 | route_headers = { 74 | 'Content-Type': 'application/json' 75 | } 76 | 77 | resp = requests.request( 78 | method='GET', 79 | url='{root}{route}'.format(root=self.root_uri, route=route_uri), 80 | headers=route_headers, 81 | auth=(self.service_user, self.service_password) 82 | ) 83 | 84 | # pylint: disable=no-member 85 | if resp.status_code == requests.codes.ok: 86 | return True 87 | 88 | return False 89 | 90 | def has_role(self, role) -> bool: 91 | """ 92 | Checks if the requesting user has the specified AAF role 93 | """ 94 | auth = flask.request.authorization 95 | route_uri = '/authz/users/{user}/{role}'.format(user=auth.username, role=role) 96 | route_headers = { 97 | 'Accept': 'application/json' 98 | } 99 | 100 | resp = requests.request( 101 | method='GET', 102 | url='{root}{route}'.format(root=self.root_uri, route=route_uri), 103 | headers=route_headers, 104 | auth=(self.service_user, self.service_password) 105 | ) 106 | 107 | if resp.status_code == requests.codes.ok: # pylint: disable=no-member 108 | # Dictionary will be empty if the user does not have that role - empty dicts are falsy 109 | if resp.json(): 110 | return True 111 | 112 | return False 113 | 114 | def is_member_or_admin(self, namespace): 115 | """ 116 | Checks if the requesting user is a member or admin of the specified namespace 117 | """ 118 | auth = flask.request.authorization 119 | route_uri = '/authz/nss/either/{user}'.format(user=auth.username) 120 | route_headers = { 121 | 'Accept': 'application/json' 122 | } 123 | 124 | resp = requests.request( 125 | method='GET', 126 | url='{root}{route}'.format(root=self.root_uri, route=route_uri), 127 | headers=route_headers, 128 | auth=(self.service_user, self.service_password) 129 | ) 130 | 131 | if resp.status_code == requests.codes.ok: # pylint: disable=no-member 132 | # Returns a dict of the user's namespaces - check that the passed namespace 133 | # is in the dict 134 | print(resp.json()) 135 | if namespace in resp.json(): 136 | return True 137 | 138 | return False 139 | 140 | def ns_admin(self, user): 141 | """ 142 | Returns the namespaces where the specified user is an admin. 143 | """ 144 | raise NotImplementedError 145 | # GET /authz/nss/admin/ --------------------------------------------------------------------------------