Skip to content

Commit d5bf88d

Browse files
authored
[MRG] Merge pull request #483 from dfir-iris/graphql_filters_query_ioc
Graphql filters query ioc
2 parents 768905b + 1ff786e commit d5bf88d

File tree

16 files changed

+777
-139
lines changed

16 files changed

+777
-139
lines changed

Diff for: source/app/blueprints/graphql/cases.py

+10-23
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@
1616
# along with this program; if not, write to the Free Software Foundation,
1717
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
1818

19-
from base64 import b64decode
20-
2119
from graphene_sqlalchemy import SQLAlchemyObjectType
2220
from graphene_sqlalchemy import SQLAlchemyConnectionField
2321
from graphene.relay import Node
@@ -29,42 +27,31 @@
2927
from graphene import Float
3028
from graphene import String
3129

30+
from app.business.iocs import build_filter_case_ioc_query
3231
from app.models.cases import Cases
33-
from app.blueprints.graphql.iocs import IOCObject
3432
from app.business.cases import create
3533
from app.business.cases import delete
3634
from app.business.cases import update
3735

3836
from app.blueprints.graphql.iocs import IOCConnection
39-
from app.blueprints.graphql.sliced_result import SlicedResult
4037

4138

4239
class CaseObject(SQLAlchemyObjectType):
4340
class Meta:
4441
model = Cases
4542
interfaces = [Node]
4643

47-
# TODO add filters
48-
iocs = SQLAlchemyConnectionField(IOCConnection)
44+
iocs = SQLAlchemyConnectionField(IOCConnection, ioc_id=Int(), ioc_uuid=String(), ioc_value=String(), ioc_type_id=Int(),
45+
ioc_description=String(), ioc_tlp_id=Int(), ioc_tags=String(), ioc_misp=String(),
46+
user_id=Float(), Linked_cases=Float())
4947

5048
@staticmethod
51-
def resolve_iocs(root, info, **kwargs):
52-
query = IOCObject.get_query(info)
53-
total = query.count()
54-
55-
first = kwargs.get('first')
56-
if not first:
57-
first = total
58-
59-
if kwargs.get('after'):
60-
after = kwargs.get('after')
61-
decode_after = b64decode(after)
62-
start = int(decode_after[16:].decode())
63-
start += 1
64-
else:
65-
start = 0
66-
query_slice = query.slice(start, start + first).all()
67-
return SlicedResult(query_slice, start, total)
49+
def resolve_iocs(root, info, ioc_id=None, ioc_uuid=None, ioc_value=None, ioc_type_id=None, ioc_description=None, ioc_tlp_id=None, ioc_tags=None,
50+
ioc_misp=None, user_id=None, Linked_cases=None, **kwargs):
51+
return build_filter_case_ioc_query(ioc_id=ioc_id, ioc_uuid=ioc_uuid, ioc_value=ioc_value,
52+
ioc_type_id=ioc_type_id, ioc_description=ioc_description,
53+
ioc_tlp_id=ioc_tlp_id, ioc_tags=ioc_tags, ioc_misp=ioc_misp,
54+
user_id=user_id, linked_cases=Linked_cases)
6855

6956

7057
class CaseConnection(Connection):

Diff for: source/app/blueprints/graphql/graphql_route.py

+10-18
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from flask import request
2222
from flask_wtf import FlaskForm
2323
from flask import Blueprint
24+
from flask_login import current_user
2425

2526
from graphql_server.flask import GraphQLView
2627
from graphene import ObjectType
@@ -52,28 +53,19 @@
5253
class Query(ObjectType):
5354
"""This is the IRIS GraphQL queries documentation!"""
5455

55-
cases = SQLAlchemyConnectionField(CaseConnection, classificationId=Float(), clientId=Float(), stateId=Int(),
56-
ownerId=Float(), openDate=String(), name=String(), socId=String(),
57-
severityId=Int())
56+
cases = SQLAlchemyConnectionField(CaseConnection, classification_id=Float(), client_id=Float(), state_id=Int(),
57+
owner_id=Float(), open_date=String(), name=String(), soc_id=String(),
58+
severity_id=Int(), tags=String(), open_since=Int())
5859
case = Field(CaseObject, case_id=Float(), description='Retrieve a case by its identifier')
5960
ioc = Field(IOCObject, ioc_id=Float(), description='Retrieve an ioc by its identifier')
6061

6162
@staticmethod
62-
def resolve_cases(root, info, **kwargs):
63-
case_classification_id = kwargs.get('classificationId')
64-
case_client_id = kwargs.get('clientId')
65-
case_state_id = kwargs.get('stateId')
66-
case_owner_id = kwargs.get('ownerId')
67-
start_open_date = kwargs.get('openDate')
68-
case_name = kwargs.get('name')
69-
case_soc_id = kwargs.get('socId')
70-
case_severity_id = kwargs.get('severityId')
71-
filtered_cases = build_filter_case_query(current_user_id=1, case_classification_id=case_classification_id,
72-
case_customer_id=case_client_id, case_state_id=case_state_id,
73-
case_owner_id=case_owner_id, start_open_date=start_open_date,
74-
case_name=case_name, case_soc_id=case_soc_id,
75-
case_severity_id=case_severity_id)
76-
return filtered_cases
63+
def resolve_cases(root, info, classification_id=None, client_id=None, state_id=None, owner_id=None, open_date=None, name=None, soc_id=None,
64+
severity_id=None, tags=None, open_since=None, **kwargs):
65+
return build_filter_case_query(current_user.id, start_open_date=open_date, end_open_date=None, case_customer_id=client_id, case_ids=None,
66+
case_name=name, case_description=None, case_classification_id=classification_id, case_owner_id=owner_id,
67+
case_opening_user_id=None, case_severity_id=severity_id, case_state_id=state_id, case_soc_id=soc_id,
68+
case_tags=tags, case_open_since=open_since)
7769

7870
@staticmethod
7971
def resolve_case(root, info, case_id):

Diff for: source/app/blueprints/graphql/iocs.py

+14-11
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,11 @@ def mutate(root, info, case_id, type_id, tlp_id, value, description=None, tags=N
7979
class IOCUpdate(Mutation):
8080

8181
class Arguments:
82-
ioc_id = Float()
83-
case_id = Float()
84-
type_id = NonNull(Int)
85-
tlp_id = NonNull(Int)
86-
value = NonNull(String)
82+
ioc_id = NonNull(Float)
83+
case_id = NonNull(Float)
84+
type_id = Int()
85+
tlp_id = Int()
86+
value = String()
8787
description = String()
8888
tags = String()
8989
ioc_misp = String()
@@ -95,12 +95,15 @@ class Arguments:
9595
ioc = Field(IOCObject)
9696

9797
@staticmethod
98-
def mutate(root, info, type_id, tlp_id, value, ioc_id=None, case_id=None, description=None, tags=None, ioc_misp=None, user_id=None, ioc_enrichment=None, modification_history=None):
99-
request = {
100-
'ioc_type_id': type_id,
101-
'ioc_tlp_id': tlp_id,
102-
'ioc_value': value
103-
}
98+
def mutate(root, info, ioc_id, case_id, type_id=None, tlp_id=None, value=None, description=None, tags=None,
99+
ioc_misp=None, user_id=None, ioc_enrichment=None, modification_history=None):
100+
request = {}
101+
if type_id:
102+
request['ioc_type_id'] = type_id
103+
if tlp_id:
104+
request['ioc_tlp_id'] = tlp_id
105+
if value:
106+
request['ioc_value'] = value
104107
if description:
105108
request['ioc_description'] = description
106109
if tags:

Diff for: source/app/blueprints/#/#_routes.py

+15-16
Original file line numberDiff line numberDiff line change
@@ -139,22 +139,21 @@ def login():
139139

140140
def wrap_login_user(user):
141141

142+
session['username'] = user.user
143+
142144
if 'SERVER_SETTINGS' not in app.config:
143145
app.config['SERVER_SETTINGS'] = get_server_settings_as_dict()
144146

145147
if app.config['SERVER_SETTINGS']['enforce_mfa']:
146148
if "mfa_verified" not in session or session["mfa_verified"] is False:
147149
return redirect(url_for('mfa_verify'))
148150

149-
print(session)
150151
login_user(user)
151152

152153

153154
#regenerate_session()
154155
#print(session)
155156

156-
session['username'] = user.user
157-
158157
caseid = user.ctx_case
159158
session['permissions'] = ac_get_effective_permissions_of_user(user)
160159

@@ -185,31 +184,26 @@ def wrap_login_user(user):
185184

186185

187186
@app.route('/auth/mfa-setup', methods=['GET', 'POST'])
188-
@login_required
189187
def mfa_setup():
190-
user = current_user
188+
user = _retrieve_user_by_username(username=session['username'])
191189
form = MFASetupForm()
192190

193-
if user.mfa_setup_complete:
194-
return redirect(url_for('dashboard'))
195-
196191
if form.submit() and form.validate():
197-
# if not user.mfa_secrets:
198-
# user.mfa_secrets = pyotp.random_base32()
199-
# db.session.commit()
200192

201193
token = form.token.data
202194
mfa_secret = form.mfa_secret.data
195+
user_password = form.user_password.data
203196
totp = pyotp.TOTP(mfa_secret)
204-
if totp.verify(token):
197+
if totp.verify(token) and bc.check_password_hash(user.password, user_password):
205198
user.mfa_secrets = mfa_secret
206199
user.mfa_setup_complete = True
207200
db.session.commit()
208201
session["mfa_verified"] = False
209-
track_activity(f'MFA setup successful for user {current_user.user}', ctx_less=True, display_in_ui=False)
202+
track_activity(f'MFA setup successful for user {user.user}', ctx_less=True, display_in_ui=False)
210203
return wrap_login_user(user)
211204
else:
212-
flash('Invalid token. Please try again.', 'danger')
205+
track_activity(f'Failed MFA setup for user {user.user}. Invalid token.', ctx_less=True, display_in_ui=False)
206+
flash('Invalid token or password. Please try again.', 'danger')
213207

214208
temp_otp_secret = pyotp.random_base32()
215209
otp_uri = pyotp.TOTP(temp_otp_secret).provisioning_uri(user.email, issuer_name="IRIS")
@@ -219,22 +213,25 @@ def mfa_setup():
219213
img.save(buf, format='PNG')
220214
img_str = base64.b64encode(buf.getvalue()).decode()
221215

222-
return render_template('mfa_setup.html', form=form, img_data=img_str, otp_setup_key=user.mfa_secrets)
216+
return render_template('mfa_setup.html', form=form, img_data=img_str, otp_setup_key=temp_otp_secret)
223217

224218

225219
@app.route('/auth/mfa-verify', methods=['GET', 'POST'])
226220
def mfa_verify():
227221
if 'username' not in session:
222+
228223
return redirect(url_for('login.login'))
229224

230225
user = _retrieve_user_by_username(username=session['username'])
231226

232227
# Redirect user to MFA setup if MFA is not fully set up
233228
if not user.mfa_secrets or not user.mfa_setup_complete:
234-
flash('MFA setup is required before verification.', 'warning')
229+
track_activity(f'MFA setup required for user {user.user}', ctx_less=True, display_in_ui=False)
235230
return redirect(url_for('mfa_setup'))
236231

237232
form = MFASetupForm()
233+
form.user_password.data = 'not required for verification'
234+
238235
if form.submit() and form.validate():
239236
token = form.token.data
240237
if not token:
@@ -245,8 +242,10 @@ def mfa_verify():
245242
if totp.verify(token):
246243
session.pop('username', None)
247244
session['mfa_verified'] = True
245+
track_activity(f'MFA verification successful for user {user.user}', ctx_less=True, display_in_ui=False)
248246
return wrap_login_user(user)
249247
else:
248+
track_activity(f'Failed MFA verification for user {user.user}. Invalid token.', ctx_less=True, display_in_ui=False)
250249
flash('Invalid token. Please try again.', 'danger')
251250

252251
return render_template('mfa_verify.html', form=form)

Diff for: source/app/blueprints/#/templates/mfa_setup.html

+4-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ <h1 class="text-white mb-3">Setup MFA</h1>
2626
<div class=" col-xs-12 col-md-4">
2727
<div class="d-flex flex-column justify-content-center align-items-center" style="height: 100%;">
2828
<h3 class="login__form_title">Your organisation requires to setup MFA</h3>
29-
<p>Scan the QR code with your authenticator app and enter the token.</p>
29+
<p>Scan the QR code with your authenticator app and enter the token and your password.</p>
3030
<form method="POST" action="">
3131
<div class="col-md-12 col-lg-12 col-sm-12">
3232
<div class="form-row ml-2">
@@ -35,6 +35,9 @@ <h3 class="login__form_title">Your organisation requires to setup MFA</h3>
3535
{{ form.mfa_secret(size=32, class="hidden", style="display:None;") }}
3636
<label for="token">Token</label>
3737
{{ form.token(size=32, class="form-control") }}
38+
39+
<label for="token">Password</label>
40+
{{ form.user_password(class="form-control") }}
3841
</div>
3942
</div>
4043
<div class="form-row ml-2">

Diff for: source/app/blueprints/manage/manage_srv_settings_routes.py

-1
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,6 @@ def manage_update_settings():
122122
try:
123123

124124
original_settings = srv_settings_schema.dump(server_settings)
125-
126125
new_settings = request.get_json()
127126

128127
differences = list(diff(original_settings, new_settings))

Diff for: source/app/business/iocs.py

+46-3
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from marshmallow.exceptions import ValidationError
2121

2222
from app import db
23+
from app.models import Ioc, IocLink
2324
from app.models.authorization import CaseAccessLevel
2425
from app.datamgmt.case.case_iocs_db import add_ioc
2526
from app.datamgmt.case.case_iocs_db import add_ioc_link
@@ -85,7 +86,7 @@ def update(identifier, request_json, case_identifier):
8586
check_current_user_has_some_case_access_stricter([CaseAccessLevel.full_access])
8687

8788
try:
88-
ioc = get_ioc(identifier, case_identifier)
89+
ioc = get_ioc(identifier, caseid=case_identifier)
8990
if not ioc:
9091
raise BusinessProcessingError('Invalid IOC ID for this case')
9192

@@ -96,13 +97,13 @@ def update(identifier, request_json, case_identifier):
9697
# validate before saving
9798
ioc_schema = IocSchema()
9899
request_data['ioc_id'] = identifier
99-
ioc_sc = ioc_schema.load(request_data, instance=ioc)
100+
ioc_sc = ioc_schema.load(request_data, instance=ioc, partial=True)
100101
ioc_sc.user_id = current_user.id
101102

102103
if not check_ioc_type_id(type_id=ioc_sc.ioc_type_id):
103104
raise BusinessProcessingError('Not a valid IOC type')
104105

105-
update_ioc_state(caseid=case_identifier)
106+
update_ioc_state(case_identifier)
106107
db.session.commit()
107108

108109
ioc_sc = call_modules_hook('on_postload_ioc_update', data=ioc_sc, caseid=case_identifier)
@@ -141,3 +142,45 @@ def get_iocs(case_identifier):
141142
check_current_user_has_some_case_access_stricter([CaseAccessLevel.read_only, CaseAccessLevel.full_access])
142143

143144
return get_iocs_by_case(case_identifier)
145+
146+
147+
def build_filter_case_ioc_query(ioc_id: int = None,
148+
ioc_uuid: str = None,
149+
ioc_value: str = None,
150+
ioc_type_id: int = None,
151+
ioc_description: str = None,
152+
ioc_tlp_id: int = None,
153+
ioc_tags: str = None,
154+
ioc_misp: str = None,
155+
user_id: float = None,
156+
linked_cases: float = None
157+
):
158+
"""
159+
Get a list of iocs from the database, filtered by the given parameters
160+
"""
161+
conditions = []
162+
if ioc_id is not None:
163+
conditions.append(Ioc.ioc_id == ioc_id)
164+
if ioc_uuid is not None:
165+
conditions.append(Ioc.ioc_uuid == ioc_uuid)
166+
if ioc_value is not None:
167+
conditions.append(Ioc.ioc_value == ioc_value)
168+
if ioc_type_id is not None:
169+
conditions.append(Ioc.ioc_type_id == ioc_type_id)
170+
if ioc_description is not None:
171+
conditions.append(Ioc.ioc_description == ioc_description)
172+
if ioc_tlp_id is not None:
173+
conditions.append(Ioc.ioc_tlp_id == ioc_tlp_id)
174+
if ioc_tags is not None:
175+
conditions.append(Ioc.ioc_tags == ioc_tags)
176+
if ioc_misp is not None:
177+
conditions.append(Ioc.ioc_misp == ioc_misp)
178+
if user_id is not None:
179+
conditions.append(Ioc.user_id == user_id)
180+
181+
query = Ioc.query.filter(*conditions)
182+
183+
if linked_cases is not None:
184+
return query.join(IocLink, Ioc.ioc_id == IocLink.ioc_id).filter(IocLink.case_id == linked_cases)
185+
186+
return query

Diff for: source/app/configuration.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ class Config:
291291
PERMANENT_SESSION_LIFETIME = timedelta(minutes=config.load('IRIS', 'SESSION_TIMEOUT', fallback=1440))
292292
SESSION_COOKIE_SAMESITE = 'Lax'
293293
SESSION_COOKIE_SECURE = True
294-
MFA_ENABLED = config.load('IRIS', 'MFA_ENABLED', fallback=False)
294+
MFA_ENABLED = config.load('IRIS', 'MFA_ENABLED', fallback=False) == 'True'
295295

296296
PG_ACCOUNT = PG_ACCOUNT_
297297
PG_PASSWD = PG_PASSWD_

0 commit comments

Comments
 (0)