Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

More fine tuning #614

Merged
merged 46 commits into from
Oct 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
7207df8
[IMP] For endpoint GET /api/v2/cases, changed field cases into data
c8y3 Oct 4, 2024
dd7ebe8
[IMP] For endpoint GET /api/v2/cases/{identifier}/iocs, changed field…
c8y3 Oct 4, 2024
88f5ed0
[IMP] Uniform syntax FROM and AS
c8y3 Oct 7, 2024
1aa8bbd
[IMP] Change user customer to initial so that the customer can be rem…
c8y3 Oct 7, 2024
6fe85fa
[IMP] Remove customers only in tests_rest_customers
c8y3 Oct 8, 2024
38183aa
[IMP] Remove users after each test
c8y3 Oct 8, 2024
2f7ef1f
[IMP] Removed spurious line
c8y3 Oct 8, 2024
2904d9b
[IMP] Return 404 when trying to delete inexistant IOC
c8y3 Oct 8, 2024
ccbc43b
[IMP] Simple quotes instead of double quotes
c8y3 Oct 8, 2024
afb054d
[IMP] Return 404 when trying to delete inexistant task
c8y3 Oct 8, 2024
8541fd4
[IMP] Missing line break
c8y3 Oct 8, 2024
f9487c2
[IMP] Renamed method to follow coding conventions
c8y3 Oct 8, 2024
d747c8d
[IMP] Introduced method in business layer
c8y3 Oct 8, 2024
0c40079
[IMP] Return 404 when deleting inexistant asset of creating an asset …
c8y3 Oct 8, 2024
91e6088
[IMP] Moved ac_api_return_access_denied into namespace blueprints
c8y3 Oct 8, 2024
e756d25
[IMP] Moved ac_api_requires_client_access into namespace blueprints
c8y3 Oct 8, 2024
c53f071
[IMP] Moved is_user_authenticated into namespace blueprints
c8y3 Oct 8, 2024
44f275a
[IMP] Removed seemingly dead method is_authentication_local
c8y3 Oct 8, 2024
70a5afd
[IMP] Moved code out of util.py into blueprints namespace
c8y3 Oct 8, 2024
fbe2f1f
[IMP] Moved update_current_case out of util.py into blueprints namespace
c8y3 Oct 8, 2024
1f302b0
[IMP] Made update_current_case private
c8y3 Oct 8, 2024
7415700
[IMP] Moved log_exception_and_error out of util.py into blueprints na…
c8y3 Oct 8, 2024
b3f561b
[IMP] Moved page_not_found out of util.py into blueprints namespace
c8y3 Oct 8, 2024
73dc62c
[IMP] Moved endpoint_removed out of util.py into blueprints namespace
c8y3 Oct 8, 2024
6c3ac34
[IMP] Moved response_error out of util.py into blueprints namespace
c8y3 Oct 8, 2024
6d1eccd
[IMP] Moved response_success out of util.py into blueprints namespace
c8y3 Oct 8, 2024
3e9627b
[FIX] Path of moved imports
c8y3 Oct 9, 2024
12204dc
[IMP] Moved response into namespace blueprints
c8y3 Oct 9, 2024
68e0e85
[IMP] Removed seemingly dead code
c8y3 Oct 9, 2024
38734b6
[IMP] Moved method is_authentication_oidc into namespace blueprints
c8y3 Oct 9, 2024
678799c
[IMP] Moved method is_authentication_ldap into namespace blueprints
c8y3 Oct 9, 2024
68d192b
[IMP] Moved method not_authenticated_redirection_url into namespace b…
c8y3 Oct 9, 2024
348e800
[IMP] Removed seemingly unused methods
c8y3 Oct 9, 2024
d5297d3
[IMP] Removed seemingly unused class
c8y3 Oct 9, 2024
a1c24f6
[IMP] Moved class AlchemyEncoder into blueprints namespace
c8y3 Oct 9, 2024
397731c
[IMP] Removed seemingly unused methods
c8y3 Oct 9, 2024
e2180a2
[IMP] Made method private
c8y3 Oct 9, 2024
4ee993a
[IMP] Removed seemingly unused methods
c8y3 Oct 9, 2024
6952ef4
[IMP] Removed seemingly unused method
c8y3 Oct 9, 2024
2d17510
[IMP] Simple quotes
c8y3 Oct 11, 2024
939c97b
[IMP] Removed method which was duplicated
c8y3 Oct 11, 2024
f2ca43b
[IMP] Explicit GET method
c8y3 Oct 11, 2024
99bec08
[IMP] Alignment
c8y3 Oct 11, 2024
8b4e66f
[IMP] Using full url
c8y3 Oct 11, 2024
c3aa44d
[IMP] Explicit method
c8y3 Oct 11, 2024
cfdc0bb
[IMP] Moved endpoints in namespace app.blueprints.rest.v2
c8y3 Oct 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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