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

Run analyzers only once per timeline #2883

Merged
merged 6 commits into from
Sep 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 7 additions & 1 deletion timesketch/api/v1/resources/analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,10 @@ def post(self, sketch_id):
"Kwargs needs to be a dictionary of parameters.",
)

analyzer_force_run = False
if form.get("analyzer_force_run"):
analyzer_force_run = True

analyzers = []
all_analyzers = [x for x, _ in analyzer_manager.AnalysisManager.get_analyzers()]
for analyzer in analyzer_names:
Expand Down Expand Up @@ -296,6 +300,7 @@ def post(self, sketch_id):
analyzer_names=analyzers,
analyzer_kwargs=analyzer_kwargs,
timeline_id=timeline_id,
analyzer_force_run=analyzer_force_run,
)
except KeyError as e:
logger.warning(
Expand All @@ -308,6 +313,7 @@ def post(self, sketch_id):
pipeline = tasks.run_sketch_init.s([searchindex_name]) | analyzer_group
pipeline.apply_async()

sessions.append(session)
if session:
sessions.append(session)

return self.to_json(sessions)
50 changes: 44 additions & 6 deletions timesketch/frontend-ng/src/components/Analyzer/AnalyzerList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ limitations under the License.
icon
color="primary"
:disabled="(timelineSelection.length > 0) ? false : true"
@click="runAnalyzer(analyzer.analyzerName)"
@click="!showRerunIcon(analyzer.analyzerName) ? runAnalyzer(analyzer.analyzerName) : handleReRunDialog(analyzer.analyzerName, analyzer.info.display_name)"
>
<v-icon v-if="!showRerunIcon(analyzer.analyzerName)">
mdi-play-circle-outline
Expand All @@ -64,14 +64,44 @@ limitations under the License.
<td>{{ analyzer.info.description }}</td>
</tr>
</tbody>
<v-dialog v-model="reRunDialog" max-width="515" :retain-focus="false">
<v-card>
<v-card-title>
<v-icon large>mdi-replay</v-icon>
<span class="text-h6 ml-2">Run "{{ reRunDialogAnalyzerDisplayName }}" again?</span>
</v-card-title>
<v-card-text>
<div class="mb-2">
The "{{ reRunDialogAnalyzerDisplayName }}" analyzer was already run on the selected timelines. Do you really want to run it again?
</div>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
color="primary"
text
@click="reRunDialog = false"
>
cancel
</v-btn>
<v-btn
color="primary"
text
@click="runAnalyzer(reRunDialogAnalyzerName, true); reRunDialog = false"
>
continue
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
</v-simple-table>
</template>

<script>
import ApiClient from '../../utils/RestApiClient'
const LOADING_INDICATOR_DURATION_MS = 3000;
const LOADING_INDICATOR_DURATION_MS = 3000
export default {
props: ['timelineSelection'],
Expand All @@ -83,7 +113,10 @@ export default {
* Analyzers that should show loading indicators. Those are triggered
* analyzers for a duration of LOADING_INDICATOR_DURATION_MS.
*/
loadingAnalyzers: []
loadingAnalyzers: [],
reRunDialog: false,
reRunDialogAnalyzerName: '',
reRunDialogAnalyzerDisplayName: '',
}
},
computed: {
Expand Down Expand Up @@ -117,7 +150,7 @@ export default {
this.triggered.forEach(analyzer => analyzerSet.has(analyzer) ? null : analyzerSet.add(analyzer))
this.resetTriggeredAnalyzers()
return analyzerSet
},
Expand Down Expand Up @@ -145,6 +178,11 @@ export default {
}
},
methods: {
handleReRunDialog(analyzerName, analyzerDisplayName) {
this.reRunDialogAnalyzerName = analyzerName
this.reRunDialogAnalyzerDisplayName = analyzerDisplayName
this.reRunDialog = true
},
isLoading(analyzerName) {
return this.loading.includes(analyzerName)
},
Expand All @@ -155,7 +193,7 @@ export default {
const timelinesSet = this.activeAnalyzerTimelinesMap.get(analyzerName)
return timelinesSet ? timelinesSet.size : 0
},
runAnalyzer(analyzerName) {
runAnalyzer(analyzerName, force = false) {
this.triggeredAnalyzers = [...this.triggeredAnalyzers, analyzerName]
this.loadingAnalyzers = [...this.loadingAnalyzers, analyzerName]
Expand All @@ -167,7 +205,7 @@ export default {
// The loading indicator should stay at least LOADING_INDICATOR_DURATION_MS.
const analyzerTriggeredTime = new Date().getTime()
ApiClient.runAnalyzers(this.sketch.id, this.timelineSelection, [analyzerName])
ApiClient.runAnalyzers(this.sketch.id, this.timelineSelection, [analyzerName], force)
.then((response) => {
let analyses = []
let sessionIds = []
Expand Down
3 changes: 2 additions & 1 deletion timesketch/frontend-ng/src/utils/RestApiClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -307,10 +307,11 @@ export default {
getAnalyzers(sketchId) {
return RestApiClient.get('/sketches/' + sketchId + '/analyzer/')
},
runAnalyzers(sketchId, timelineIds, analyzers) {
runAnalyzers(sketchId, timelineIds, analyzers, forceRun = false) {
let formData = {
timeline_ids: timelineIds,
analyzer_names: analyzers,
analyzer_force_run: forceRun,
}
return RestApiClient.post('/sketches/' + sketchId + /analyzer/, formData)
},
Expand Down
33 changes: 31 additions & 2 deletions timesketch/lib/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import codecs
import io
import json
from hashlib import sha1
import six
import yaml

Expand Down Expand Up @@ -329,6 +330,7 @@ def build_sketch_analysis_pipeline(
user_id,
analyzer_names=None,
analyzer_kwargs=None,
analyzer_force_run=False,
timeline_id=None,
):
"""Build a pipeline for sketch analysis.
Expand All @@ -345,6 +347,7 @@ def build_sketch_analysis_pipeline(
user_id (int): The ID of the user who started the analyzer.
analyzer_names (list): List of analyzers to run.
analyzer_kwargs (dict): Arguments to the analyzers.
analyzer_force_run (bool): If true then force the analyzer to run.
timeline_id (int): Optional int of the timeline to run the analyzer on.

Returns:
Expand Down Expand Up @@ -399,6 +402,30 @@ def build_sketch_analysis_pipeline(
if not kwargs_list:
kwargs_list = [base_kwargs]

# Create a hash of the analyzer arguments to compare with later analyzer
# executions if the analyzer arguments / config changed.
kwargs_list_hash = sha1(
jkppr marked this conversation as resolved.
Show resolved Hide resolved
json.dumps(kwargs_list, sort_keys=True).encode("utf-8")
).hexdigest()

if not analyzer_force_run:
skip_analysis = False
for past_analysis in timeline.analysis:
if (
(past_analysis.analyzer_name == analyzer_name)
and (past_analysis.get_status.status == "DONE")
and (past_analysis.created_at > timeline.updated_at)
):
for attribute in past_analysis.get_attributes:
if attribute.value == kwargs_list_hash:
skip_analysis = True
break
if skip_analysis:
break

if skip_analysis:
continue

for kwargs in kwargs_list:
analysis = Analysis(
name=analyzer_name,
Expand All @@ -409,6 +436,7 @@ def build_sketch_analysis_pipeline(
sketch=sketch,
timeline=timeline,
)
analysis.add_attribute(name="kwargs_hash", value=kwargs_list_hash)
analysis.set_status("PENDING")
analysis_session.analyses.append(analysis)
db_session.add(analysis)
Expand All @@ -425,8 +453,9 @@ def build_sketch_analysis_pipeline(
)

# Commit the analysis session to the database.
db_session.add(analysis_session)
db_session.commit()
if len(analysis_session.analyses) > 0:
db_session.add(analysis_session)
db_session.commit()

if current_app.config.get("ENABLE_EMAIL_NOTIFICATIONS"):
tasks.append(run_email_result_task.s(sketch_id))
Expand Down
2 changes: 1 addition & 1 deletion timesketch/models/sketch.py
Original file line number Diff line number Diff line change
Expand Up @@ -580,7 +580,7 @@ def __init__(
self.view = view


class Analysis(LabelMixin, StatusMixin, CommentMixin, BaseModel):
class Analysis(GenericAttributeMixin, LabelMixin, StatusMixin, CommentMixin, BaseModel):
"""Implements the analysis model."""

name = Column(Unicode(255))
Expand Down
Loading