Skip to content

Commit

Permalink
[MRG] Merge pull request #614 from dfir-iris/api_v2_get_cases
Browse files Browse the repository at this point in the history
More fine tuning
  • Loading branch information
whikernel authored Oct 14, 2024
2 parents 899f55f + cfdc0bb commit 8ca28ba
Show file tree
Hide file tree
Showing 92 changed files with 622 additions and 680 deletions.
2 changes: 1 addition & 1 deletion docker/webApp/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ RUN pip3 install -r requirements.txt
###############
# BUILD IMAGE #
###############
FROM python:3.9 as iriswebapp
FROM python:3.9 AS iriswebapp
ENV PYTHONUNBUFFERED=1 DOCKERIZED=1

COPY --from=compile-image /opt/venv /opt/venv
Expand Down
7 changes: 7 additions & 0 deletions e2e/tests/administrator/manage/customers.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,10 @@ test('should be able to open "Add customer" modal', async ({ page }) => {
await page.getByRole('button', { name: 'Add customer' }).click();
await expect(page.getByRole('heading', { name: 'Add customer' })).toBeVisible()
});

test('should present IrisInitialClient associated cases', async ({ page }) => {
await page.getByRole('link', { name: 'IrisInitialClient' }).click();

await page.getByRole('button', { name: ' Cases' }).click();
await expect(page.getByRole('gridcell', { name: '#1 - Initial Demo' })).toBeVisible();
});
1 change: 1 addition & 0 deletions source/app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ class AlertsNamespace(Namespace):

app = Flask(__name__, static_folder="../static")


def ac_current_user_has_permission(*permissions):
"""
Return True if current user has permission
Expand Down
202 changes: 193 additions & 9 deletions source/app/blueprints/access_controls.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,27 +18,39 @@

import json
import logging as log
import traceback
import uuid
from functools import wraps

from flask import request, session, render_template
import jwt
import requests

from flask import Request
from flask import url_for
from flask import request
from flask import render_template
from flask import session
from flask_login import current_user
from flask_login import login_user
from flask_wtf import FlaskForm
from jwt import PyJWKClient
from requests.auth import HTTPBasicAuth
from werkzeug.utils import redirect

from app import TEMPLATE_PATH

from app import app
from app import db
from app.blueprints.responses import response_error
from app.datamgmt.case.case_db import get_case
from app.datamgmt.manage.manage_access_control_db import user_has_client_access
from app.datamgmt.manage.manage_users_db import get_user
from app.iris_engine.access_control.utils import ac_fast_check_user_has_case_access
from app.iris_engine.access_control.utils import ac_get_effective_permissions_of_user
from app.iris_engine.utils.tracker import track_activity
from app.models import Cases
from app.models.authorization import Permissions
from app.models.authorization import CaseAccessLevel
from app.util import update_current_case
from app.util import log_exception_and_error
from app.util import response_error
from app.util import is_user_authenticated
from app.util import not_authenticated_redirection_url
from app.util import ac_api_return_access_denied


def _user_has_at_least_a_required_permission(permissions: list[Permissions]):
Expand All @@ -65,6 +77,11 @@ def _set_caseid_from_current_user():
return redir, caseid


def _log_exception_and_error(e):
log.exception(e)
log.error(traceback.print_exc())


def _get_caseid_from_request_data(request_data, no_cid_required):
caseid = request_data.args.get('cid', default=None, type=int)
if caseid:
Expand Down Expand Up @@ -99,7 +116,7 @@ def _get_caseid_from_request_data(request_data, no_cid_required):
redir, caseid = _set_caseid_from_current_user()
return redir, caseid, True

log_exception_and_error(e)
_log_exception_and_error(e)
return True, 0, False


Expand Down Expand Up @@ -133,6 +150,18 @@ def _update_denied_case(caseid):
}


def _update_current_case(caseid, restricted_access):
if session['current_case']['case_id'] != caseid:
case = get_case(caseid)
if case:
session['current_case'] = {
'case_name': "{}".format(case.name),
'case_info': "(#{} - {})".format(caseid, case.client.name),
'case_id': caseid,
'access': restricted_access
}


def _update_session(caseid, eaccess_level):
restricted_access = ''
if not eaccess_level:
Expand All @@ -141,7 +170,7 @@ def _update_session(caseid, eaccess_level):
if CaseAccessLevel.read_only.value == eaccess_level:
restricted_access = '<i class="ml-2 text-warning mt-1 fa-solid fa-lock" title="Read only access"></i>'

update_current_case(caseid, restricted_access)
_update_current_case(caseid, restricted_access)


# TODO would be nice to remove parameter no_cid_required
Expand Down Expand Up @@ -233,6 +262,17 @@ def get_case_access_from_api(request_data, access_level):
return redir, caseid, True


def not_authenticated_redirection_url(request_url: str):
redirection_mapper = {
"oidc_proxy": lambda: app.config.get("AUTHENTICATION_PROXY_LOGOUT_URL"),
"local": lambda: url_for('login.login', next=request_url),
"ldap": lambda: url_for('login.login', next=request_url),
"oidc": lambda: url_for('login.login', next=request_url,)
}

return redirection_mapper.get(app.config.get("AUTHENTICATION_TYPE"))()


def ac_case_requires(*access_level):
def inner_wrap(f):
@wraps(f)
Expand Down Expand Up @@ -328,3 +368,147 @@ def wrap(*args, **kwargs):

return wrap
return inner_wrap


def ac_api_return_access_denied(caseid: int = None):
error_uuid = uuid.uuid4()
log.warning(f"EID {error_uuid} - Access denied with case #{caseid} for user ID {current_user.id} "
f"accessing URI {request.full_path}")
data = {
'user_id': current_user.id,
'case_id': caseid,
'error_uuid': error_uuid
}
return response_error('Permission denied', data=data, status=403)


def ac_api_requires_client_access():
def inner_wrap(f):
@wraps(f)
def wrap(*args, **kwargs):
client_id = kwargs.get('client_id')
if not user_has_client_access(current_user.id, client_id):
return response_error("Permission denied", status=403)

return f(*args, **kwargs)
return wrap
return inner_wrap


def _authenticate_with_email(user_email):
user = get_user(user_email, id_key="email")
if not user:
log.error(f'User with email {user_email} is not registered in the IRIS')
return False

login_user(user)
track_activity(f"User '{user.id}' successfully logged-in", ctx_less=True)

caseid = user.ctx_case
session['permissions'] = ac_get_effective_permissions_of_user(user)

if caseid is None:
case = Cases.query.order_by(Cases.case_id).first()
user.ctx_case = case.case_id
user.ctx_human_case = case.name
db.session.commit()

session['current_case'] = {
'case_name': user.ctx_human_case,
'case_info': "",
'case_id': user.ctx_case
}

return True


def _oidc_proxy_authentication_process(incoming_request: Request):
# Get the OIDC JWT authentication token from the request header
authentication_token = incoming_request.headers.get('X-Forwarded-Access-Token', '')

if app.config.get("AUTHENTICATION_TOKEN_VERIFY_MODE") == 'lazy':
user_email = incoming_request.headers.get('X-Email')

if user_email:
return _authenticate_with_email(user_email.split(',')[0])

elif app.config.get("AUTHENTICATION_TOKEN_VERIFY_MODE") == 'introspection':
# Use the authentication server's token introspection endpoint in order to determine if the request is valid /
# authenticated. The TLS_ROOT_CA is used to validate the authentication server's certificate.
# The other solution was to skip the certificate verification, BUT as the authentication server might be
# located on another server, this check is necessary.

introspection_body = {"token": authentication_token}
introspection = requests.post(
app.config.get("AUTHENTICATION_TOKEN_INTROSPECTION_URL"),
auth=HTTPBasicAuth(app.config.get('AUTHENTICATION_CLIENT_ID'), app.config.get('AUTHENTICATION_CLIENT_SECRET')),
data=introspection_body,
verify=app.config.get("TLS_ROOT_CA")
)
if introspection.status_code == 200:
response_json = introspection.json()

if response_json.get("active", False) is True:
user_email = response_json.get("sub")
return _authenticate_with_email(user_email=user_email)

else:
log.info("USER IS NOT AUTHENTICATED")
return False

elif app.config.get("AUTHENTICATION_TOKEN_VERIFY_MODE") == 'signature':
# Use the JWKS urls provided by the OIDC discovery to fetch the signing keys
# and check the signature of the token
try:
jwks_client = PyJWKClient(app.config.get("AUTHENTICATION_JWKS_URL"))
signing_key = jwks_client.get_signing_key_from_jwt(authentication_token)

try:

data = jwt.decode(
authentication_token,
signing_key.key,
algorithms=["RS256"],
audience=app.config.get("AUTHENTICATION_AUDIENCE"),
options={"verify_exp": app.config.get("AUTHENTICATION_VERIFY_TOKEN_EXP")},
)

except jwt.ExpiredSignatureError:
log.error("Provided token has expired")
return False

except Exception as e:
log.error(f"Error decoding JWT. {e.__str__()}")
return False

# Extract the user email
user_email = data.get("sub")

return _authenticate_with_email(user_email)

else:
log.error("ERROR DURING TOKEN INTROSPECTION PROCESS")
return False


def _local_authentication_process(incoming_request: Request):
return current_user.is_authenticated


def is_user_authenticated(incoming_request: Request):
authentication_mapper = {
"oidc_proxy": _oidc_proxy_authentication_process,
"local": _local_authentication_process,
"ldap": _local_authentication_process,
"oidc": _local_authentication_process,
}

return authentication_mapper.get(app.config.get("AUTHENTICATION_TYPE"))(incoming_request)


def is_authentication_oidc():
return app.config.get('AUTHENTICATION_TYPE') == "oidc"


def is_authentication_ldap():
return app.config.get('AUTHENTICATION_TYPE') == "ldap"
4 changes: 2 additions & 2 deletions source/app/blueprints/graphql/graphql_route.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@
from graphene_sqlalchemy import SQLAlchemyConnectionField

from app.datamgmt.manage.manage_cases_db import build_filter_case_query
from app.util import is_user_authenticated
from app.util import response_error
from app.blueprints.access_controls import is_user_authenticated
from app.blueprints.responses import response_error

from app.models.authorization import CaseAccessLevel

Expand Down
2 changes: 1 addition & 1 deletion source/app/blueprints/pages/alerts/alerts_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
from app.datamgmt.alerts.alerts_db import get_alert_by_id
from app.datamgmt.manage.manage_access_control_db import user_has_client_access
from app.models.authorization import Permissions
from app.util import response_error
from app.blueprints.responses import response_error
from app.blueprints.access_controls import ac_requires

alerts_blueprint = Blueprint(
Expand Down
2 changes: 1 addition & 1 deletion source/app/blueprints/pages/case/case_assets_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
from app.forms import ModalAddCaseAssetForm
from app.models.authorization import CaseAccessLevel
from app.blueprints.access_controls import ac_case_requires
from app.util import response_error
from app.blueprints.responses import response_error

case_assets_blueprint = Blueprint('case_assets',
__name__,
Expand Down
2 changes: 1 addition & 1 deletion source/app/blueprints/pages/case/case_ioc_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
from app.models.authorization import CaseAccessLevel
from app.models.models import Ioc
from app.blueprints.access_controls import ac_case_requires
from app.util import response_error
from app.blueprints.responses import response_error

case_ioc_blueprint = Blueprint(
'case_ioc',
Expand Down
3 changes: 1 addition & 2 deletions source/app/blueprints/pages/case/case_notes_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,7 @@
from app.datamgmt.case.case_notes_db import get_note
from app.models.authorization import CaseAccessLevel
from app.blueprints.access_controls import ac_case_requires
from app.util import response_error

from app.blueprints.responses import response_error

case_notes_blueprint = Blueprint('case_notes',
__name__,
Expand Down
2 changes: 1 addition & 1 deletion source/app/blueprints/pages/case/case_rfiles_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
from app.datamgmt.manage.manage_attribute_db import get_default_custom_attributes
from app.models.authorization import CaseAccessLevel
from app.blueprints.access_controls import ac_case_requires
from app.util import response_error
from app.blueprints.responses import response_error

case_rfiles_blueprint = Blueprint(
'case_rfiles',
Expand Down
2 changes: 1 addition & 1 deletion source/app/blueprints/pages/case/case_tasks_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
from app.models.authorization import User
from app.models.models import CaseTasks
from app.blueprints.access_controls import ac_case_requires
from app.util import response_error
from app.blueprints.responses import response_error

case_tasks_blueprint = Blueprint('case_tasks',
__name__,
Expand Down
2 changes: 1 addition & 1 deletion source/app/blueprints/pages/case/case_timeline_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
from app.models.cases import Cases
from app.models.cases import CasesEvent
from app.blueprints.access_controls import ac_case_requires
from app.util import response_error
from app.blueprints.responses import response_error

_EVENT_TAGS = ['Network', 'Server', 'ActiveDirectory', 'Computer', 'Malware', 'User Interaction']

Expand Down
4 changes: 1 addition & 3 deletions source/app/blueprints/pages/dashboard/dashboard_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,7 @@
from app.iris_engine.utils.tracker import track_activity
from app.models.authorization import User
from app.models.models import GlobalTasks
from app.blueprints.access_controls import ac_requires
from app.util import not_authenticated_redirection_url
from app.util import is_authentication_oidc
from app.blueprints.access_controls import ac_requires, is_authentication_oidc, not_authenticated_redirection_url

from oic.oauth2.exception import GrantError

Expand Down
2 changes: 1 addition & 1 deletion source/app/blueprints/pages/datastore/datastore_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from app.forms import ModalDSFileForm
from app.models.authorization import CaseAccessLevel
from app.blueprints.access_controls import ac_case_requires
from app.util import response_error
from app.blueprints.responses import response_error

datastore_blueprint = Blueprint(
'datastore',
Expand Down
2 changes: 1 addition & 1 deletion source/app/blueprints/pages/dim_tasks/dim_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
from app.models.authorization import CaseAccessLevel
from app.models.authorization import Permissions
from app.blueprints.access_controls import ac_case_requires, ac_requires
from app.util import response_error
from app.blueprints.responses import response_error
from iris_interface.IrisInterfaceStatus import IIStatus

dim_tasks_blueprint = Blueprint(
Expand Down
Loading

0 comments on commit 8ca28ba

Please sign in to comment.