From d0d6ae95b0e9ad5908e1ee8c4d19e0e87c776056 Mon Sep 17 00:00:00 2001 From: Johan Berggren Date: Wed, 25 Oct 2023 15:12:46 +0200 Subject: [PATCH] Add DFIQ context to SearchHistory (#2957) * Append DFIQ context to SearchHistory schema and save context when searching * Handle setting DFIQ context * Update DFIQ context card * styling * remove prints * context card styling * tests * format * Add link to dfiq.org for questions --- timesketch/api/v1/resources/__init__.py | 2 + timesketch/api/v1/resources/explore.py | 44 ++++++++ timesketch/api/v1/resources_test.py | 1 + .../src/components/Explore/EventList.vue | 10 +- .../src/components/Scenarios/ContextCard.vue | 102 +++++++++++++++++- .../Scenarios/ContextCardApproach.vue | 76 +++++++++++++ .../src/components/Scenarios/Facet.vue | 40 ++----- .../src/components/Scenarios/Question.vue | 59 ++++++++-- .../src/components/Scenarios/SearchChip.vue | 18 +++- timesketch/frontend-ng/src/store.js | 12 +-- timesketch/frontend-ng/src/views/Explore.vue | 43 +++++--- ...732e1_add_dfiq_context_to_searchhistory.py | 50 +++++++++ timesketch/models/sketch.py | 13 +++ 13 files changed, 397 insertions(+), 73 deletions(-) create mode 100644 timesketch/frontend-ng/src/components/Scenarios/ContextCardApproach.vue create mode 100644 timesketch/migrations/versions/710c224732e1_add_dfiq_context_to_searchhistory.py diff --git a/timesketch/api/v1/resources/__init__.py b/timesketch/api/v1/resources/__init__.py index ef0b2d2a7e..a01161d12d 100644 --- a/timesketch/api/v1/resources/__init__.py +++ b/timesketch/api/v1/resources/__init__.py @@ -275,6 +275,7 @@ class ResourceMixin(object): "name": fields.String, "display_name": fields.String, "description": fields.String, + "dfiq_identifier": fields.String, "spec_json": fields.String, "user": fields.Nested(user_fields), "approaches": fields.List(fields.Nested(approach_fields)), @@ -288,6 +289,7 @@ class ResourceMixin(object): "name": fields.String, "display_name": fields.String, "description": fields.String, + "dfiq_identifier": fields.String, "spec_json": fields.String, "user": fields.Nested(user_fields), "questions": fields.List(fields.Nested(question_fields)), diff --git a/timesketch/api/v1/resources/explore.py b/timesketch/api/v1/resources/explore.py index 389e66fdb9..166cb557ce 100644 --- a/timesketch/api/v1/resources/explore.py +++ b/timesketch/api/v1/resources/explore.py @@ -44,6 +44,9 @@ from timesketch.models.sketch import Sketch from timesketch.models.sketch import View from timesketch.models.sketch import SearchHistory +from timesketch.models.sketch import Scenario +from timesketch.models.sketch import Facet +from timesketch.models.sketch import InvestigativeQuestion # Metrics definitions METRICS = { @@ -93,6 +96,42 @@ def post(self, sketch_id): "Unable to explore data, unable to validate form data", ) + # DFIQ context + scenario = None + facet = None + question = None + + scenario_id = request.json.get("scenario", None) + facet_id = request.json.get("facet", None) + question_id = request.json.get("question", None) + + if scenario_id: + scenario = Scenario.query.get(scenario_id) + if scenario: + if scenario.sketch_id != sketch.id: + abort( + HTTP_STATUS_CODE_BAD_REQUEST, + "Scenario is not part of this sketch.", + ) + + if facet_id: + facet = Facet.query.get(facet_id) + if facet: + if facet.scenario.sketch_id != sketch.id: + abort( + HTTP_STATUS_CODE_BAD_REQUEST, + "Facet is not part of this sketch.", + ) + + if question_id: + question = InvestigativeQuestion.query.get(question_id) + if question: + if question.facet.scenario.sketch_id != sketch.id: + abort( + HTTP_STATUS_CODE_BAD_REQUEST, + "Question is not part of this sketch.", + ) + # TODO: Remove form and use json instead. query_dsl = form.dsl.data enable_scroll = form.enable_scroll.data @@ -340,6 +379,11 @@ def post(self, sketch_id): new_search.query_result_count = count_total_complete new_search.query_time = result["took"] + # Add DFIQ context + new_search.scenario = scenario + new_search.facet = facet + new_search.question = question + if previous_search: new_search.parent = previous_search diff --git a/timesketch/api/v1/resources_test.py b/timesketch/api/v1/resources_test.py index 973a4b6fb1..56478b4925 100644 --- a/timesketch/api/v1/resources_test.py +++ b/timesketch/api/v1/resources_test.py @@ -209,6 +209,7 @@ class ExploreResourceTest(BaseTest): "query_filter": "{}", "query_result_count": 0, "query_string": "test", + "scenario_id": None, }, }, "objects": [ diff --git a/timesketch/frontend-ng/src/components/Explore/EventList.vue b/timesketch/frontend-ng/src/components/Explore/EventList.vue index 9d670ba071..6357514bd1 100644 --- a/timesketch/frontend-ng/src/components/Explore/EventList.vue +++ b/timesketch/frontend-ng/src/components/Explore/EventList.vue @@ -155,7 +155,7 @@ limitations under the License. Download current view as csv @@ -646,6 +646,9 @@ export default { } return baseHeaders }, + activeContext() { + return this.$store.state.activeContext + }, }, methods: { sortEvents(sortAsc) { @@ -821,6 +824,11 @@ export default { formData['parent'] = this.branchParent } + // Get DFIQ context + formData['scenario'] = this.activeContext.scenario.id + formData['facet'] = this.activeContext.facet.id + formData['question'] = this.activeContext.question.id + ApiClient.search(this.sketch.id, formData) .then((response) => { this.eventList.objects = response.data.objects diff --git a/timesketch/frontend-ng/src/components/Scenarios/ContextCard.vue b/timesketch/frontend-ng/src/components/Scenarios/ContextCard.vue index 6ff18297a5..ca771c1663 100644 --- a/timesketch/frontend-ng/src/components/Scenarios/ContextCard.vue +++ b/timesketch/frontend-ng/src/components/Scenarios/ContextCard.vue @@ -14,33 +14,125 @@ See the License for the specific language governing permissions and limitations under the License. --> diff --git a/timesketch/frontend-ng/src/components/Scenarios/ContextCardApproach.vue b/timesketch/frontend-ng/src/components/Scenarios/ContextCardApproach.vue new file mode 100644 index 0000000000..3c16f553bf --- /dev/null +++ b/timesketch/frontend-ng/src/components/Scenarios/ContextCardApproach.vue @@ -0,0 +1,76 @@ + + + + + diff --git a/timesketch/frontend-ng/src/components/Scenarios/Facet.vue b/timesketch/frontend-ng/src/components/Scenarios/Facet.vue index 97cb9f5610..832e556f53 100644 --- a/timesketch/frontend-ng/src/components/Scenarios/Facet.vue +++ b/timesketch/frontend-ng/src/components/Scenarios/Facet.vue @@ -36,9 +36,7 @@ limitations under the License. - - {{ facet.display_name }} - + {{ facet.display_name }} @@ -50,13 +48,8 @@ limitations under the License.
- - + +
@@ -81,14 +74,12 @@ export default { sketch() { return this.$store.state.sketch }, + activeContext() { + return this.$store.state.activeContext + }, questionsWithConclusion() { return this.facet.questions.filter((question) => question.conclusions.length) }, - isActive() { - return ( - this.questionsWithConclusion.length > 0 && this.questionsWithConclusion.length < this.facet.questions.length - ) - }, isResolved() { return this.questionsWithConclusion.length === this.facet.questions.length }, @@ -98,26 +89,13 @@ export default { }, methods: { toggleFacet: function () { - if (!this.expanded) { - this.setActiveContext() - } else { - if (this.$store.state.activeContext.facet != null) { - if (this.facet.id === this.$store.state.activeContext.facet.id) { - this.$store.dispatch('clearActiveContext') - } + if (this.activeContext.facet != null) { + if (this.facet.id === this.activeContext.facet.id) { + this.$store.dispatch('clearActiveContext') } } this.expanded = !this.expanded }, - setActiveContext: function (question) { - let payload = { - scenario: this.scenario, - facet: this.facet, - question: question, - } - this.$store.dispatch('setActiveContext', payload) - }, }, - created() {}, } diff --git a/timesketch/frontend-ng/src/components/Scenarios/Question.vue b/timesketch/frontend-ng/src/components/Scenarios/Question.vue index 7de8a0f50c..01de93ac8e 100644 --- a/timesketch/frontend-ng/src/components/Scenarios/Question.vue +++ b/timesketch/frontend-ng/src/components/Scenarios/Question.vue @@ -19,7 +19,7 @@ limitations under the License. no-gutters class="pa-2 pl-5" style="cursor: pointer; font-size: 0.9em" - @click="expanded = !expanded" + @click="toggleQuestion()" :class=" $vuetify.theme.dark ? expanded @@ -30,7 +30,7 @@ limitations under the License. : 'light-hover' " > - {{ question.display_name }} {{ question.display_name }} @@ -49,6 +49,7 @@ limitations under the License. : 'light-hover' " class="pb-1" + @click="setActiveContext()" >
@@ -59,11 +60,13 @@ limitations under the License. v-for="searchtemplate in searchTemplates" :key="searchtemplate.id" :searchchip="searchtemplate" + type="chip" >
@@ -128,7 +131,7 @@ import TsSearchChip from './SearchChip' import TsQuestionConclusion from './QuestionConclusion' export default { - props: ['question'], + props: ['scenario', 'facet', 'question'], components: { TsSearchChip, TsQuestionConclusion, @@ -155,6 +158,12 @@ export default { currentUserConclusion() { return this.question.conclusions.filter((conclusion) => conclusion.user.username === this.currentUser).length }, + activeContext() { + return this.$store.state.activeContext + }, + isActive() { + return this.activeContext.question.id === this.question.id + }, }, methods: { createConclusion() { @@ -166,20 +175,50 @@ export default { .catch((e) => {}) }, getSuggestedQueries() { - let analyses = this.question.approaches - .map((approach) => JSON.parse(approach.spec_json)) - .map((approach) => approach._view.processors) - .map((processor) => processor[0].analysis.timesketch) - .flat() - this.opensearchQueries = analyses.filter((analysis) => analysis.type === 'opensearch-query') + let approaches = this.question.approaches.map((approach) => JSON.parse(approach.spec_json)) + approaches.forEach((approach) => { + approach._view.processors.forEach((processor) => { + processor.analysis.forEach((analysis) => { + if (analysis.name === 'OpenSearch') { + analysis.steps.forEach((step) => { + this.opensearchQueries.push(step) + }) + } + }) + }) + }) + }, + toggleQuestion() { + if (this.expanded && !this.isActive) { + this.setActiveContext() + return + } else { + this.$store.dispatch('clearActiveContext') + } + this.expanded = !this.expanded + }, + setActiveContext: function (question) { + let payload = { + scenario: this.scenario, + facet: this.facet, + question: this.question, + } + this.$store.dispatch('setActiveContext', payload) }, }, watch: { expanded: function (isExpanded) { - if (!isExpanded) return + if (isExpanded) { + this.setActiveContext() + } if (this.opensearchQueries.length) return this.getSuggestedQueries() }, + isActive: function (newVal) { + if (!newVal) { + this.expanded = false + } + }, }, } diff --git a/timesketch/frontend-ng/src/components/Scenarios/SearchChip.vue b/timesketch/frontend-ng/src/components/Scenarios/SearchChip.vue index b98fab2bd7..dce80579b1 100644 --- a/timesketch/frontend-ng/src/components/Scenarios/SearchChip.vue +++ b/timesketch/frontend-ng/src/components/Scenarios/SearchChip.vue @@ -14,9 +14,19 @@ See the License for the specific language governing permissions and limitations under the License. -->