Skip to content
This repository has been archived by the owner on Nov 2, 2024. It is now read-only.

Commit

Permalink
Files rescan and pivot (intelowlproject#2490)
Browse files Browse the repository at this point in the history
* changes

* removed log

* added test for pivoting with files in investigation

* fixed test

* updated test for rescan

* added backend test for rescan API

* recent scan fix

* support pivoting for files

* prettier

* added prettier ignore file for coverage

* added permission for rescan operation

* prettier

* added test

* improvement
  • Loading branch information
drosetti authored and Michalsus committed Oct 11, 2024
1 parent 44bf52b commit d50c673
Show file tree
Hide file tree
Showing 13 changed files with 397 additions and 165 deletions.
5 changes: 4 additions & 1 deletion api_app/playbooks_manager/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ def get_queryset(self):
)
@action(methods=["POST"], url_name="analyze_multiple_observables", detail=False)
def analyze_multiple_observables(self, request):
logger.debug(f"{request.data=}")
oas = ObservableAnalysisSerializer(
data=request.data, many=True, context={"request": request}
)
Expand All @@ -68,11 +69,13 @@ def analyze_multiple_observables(self, request):
)
@action(methods=["POST"], url_name="analyze_multiple_files", detail=False)
def analyze_multiple_files(self, request):
logger.debug(f"{request.data=}")
oas = FileJobSerializer(
data=request.data, many=True, context={"request": request}
)
oas.is_valid(raise_exception=True)
jobs = oas.save(send_task=True)
parent_job = oas.validated_data[0].get("parent_job", None)
jobs = oas.save(send_task=True, parent=parent_job)
return Response(
JobResponseSerializer(jobs, many=True).data,
status=status.HTTP_200_OK,
Expand Down
1 change: 1 addition & 0 deletions api_app/serializers/job.py
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,7 @@ class Meta:
"playbook",
"status",
"received_request_time",
"is_sample",
]

playbook = rfs.SlugRelatedField(
Expand Down
33 changes: 32 additions & 1 deletion api_app/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet

from api_app.choices import ScanMode
from api_app.websocket import JobConsumer
from certego_saas.apps.organization.permissions import (
IsObjectOwnerOrSameOrgPermission as IsObjectUserOrSameOrgPermission,
Expand Down Expand Up @@ -452,7 +453,7 @@ def get_permissions(self):
- List of applicable permissions.
"""
permissions = super().get_permissions()
if self.action in ["destroy", "kill"]:
if self.action in ["destroy", "kill", "rescan"]:
permissions.append(IsObjectUserOrSameOrgPermission())
return permissions

Expand Down Expand Up @@ -541,6 +542,36 @@ def retry(self, request, pk=None):
job.retry()
return Response(status=status.HTTP_204_NO_CONTENT)

@action(detail=True, methods=["post"])
def rescan(self, request, pk=None):
logger.info(f"rescan request for job: {pk}")
existing_job: Job = self.get_object()
# create a new job
data = {
"tlp": existing_job.tlp,
"runtime_configuration": existing_job.runtime_configuration,
"scan_mode": ScanMode.FORCE_NEW_ANALYSIS,
}
if existing_job.playbook_requested:
data["playbook_requested"] = existing_job.playbook_requested
else:
data["analyzers_requested"] = existing_job.analyzers_requested.all()
data["connectors_requested"] = existing_job.connectors_requested.all()
if existing_job.is_sample:
data["file"] = existing_job.file
data["file_name"] = existing_job.file_name
job_serializer = FileJobSerializer(data=data, context={"request": request})
else:
data["observable_classification"] = existing_job.observable_classification
data["observable_name"] = existing_job.observable_name
job_serializer = ObservableAnalysisSerializer(
data=data, context={"request": request}
)
job_serializer.is_valid(raise_exception=True)
new_job = job_serializer.save(send_task=True)
logger.info(f"rescan request for job: {pk} generated job: {new_job.pk}")
return Response(data={"id": new_job.pk}, status=status.HTTP_202_ACCEPTED)

@add_docs(
description="Kill running job by closing celery tasks and marking as killed",
request=None,
Expand Down
2 changes: 2 additions & 0 deletions frontend/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Ignore artifacts:
.coverage
7 changes: 5 additions & 2 deletions frontend/src/components/investigations/flow/CustomJobNode.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@ function CustomJobNode({ data }) {
id="investigation-pivotbtn"
className="mx-1 p-2"
size="sm"
href={`/scan?parent=${data.id}&observable=${data.name}`}
href={`/scan?parent=${data.id}&${
data.is_sample ? "isSample=true" : `observable=${data.name}`
}`}
target="_blank"
rel="noreferrer"
>
Expand All @@ -67,7 +69,8 @@ function CustomJobNode({ data }) {
placement="top"
fade={false}
>
Analyze the same observable again
Analyze the same observable again. CAUTION! Samples require to
select again the file.
</UncontrolledTooltip>
{data.isFirstLevel && <RemoveJob data={data} />}
</div>
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/investigations/flow/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ function addJobNode(
investigation: investigationId,
children: job.children || [],
status: job.status,
is_sample: job.is_sample,
refetchTree,
refetchInvestigation,
isFirstLevel: isFirstLevel || false,
Expand Down
34 changes: 5 additions & 29 deletions frontend/src/components/jobs/result/bar/JobActionBar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@ import { ContentSection, IconButton, addToast } from "@certego/certego-ui";

import { SaveAsPlaybookButton } from "./SaveAsPlaybooksForm";

import { downloadJobSample, deleteJob } from "../jobApi";
import { createJob } from "../../../scan/scanApi";
import { ScanModesNumeric } from "../../../../constants/advancedSettingsConst";
import { downloadJobSample, deleteJob, rescanJob } from "../jobApi";
import { JobResultSections } from "../../../../constants/miscConst";
import {
DeleteIcon,
Expand Down Expand Up @@ -53,33 +51,11 @@ export function JobActionsBar({ job }) {
};

const handleRetry = async () => {
if (job.is_sample) {
addToast(
"Rescan File!",
"It's not possible to repeat a sample analysis",
"warning",
false,
2000,
);
} else {
addToast("Retrying the same job...", null, "spinner", false, 2000);
const response = await createJob(
[job.observable_name],
job.observable_classification,
job.playbook_requested,
job.analyzers_requested,
job.connectors_requested,
job.runtime_configuration,
job.tags.map((optTag) => optTag.label),
job.tlp,
ScanModesNumeric.FORCE_NEW_ANALYSIS,
0,
);
addToast("Retrying the same job...", null, "spinner", false, 2000);
const newJobId = await rescanJob(job.id);
if (newJobId) {
setTimeout(
() =>
navigate(
`/jobs/${response.jobIds[0]}/${JobResultSections.VISUALIZER}/`,
),
() => navigate(`/jobs/${newJobId}/${JobResultSections.VISUALIZER}/`),
1000,
);
}
Expand Down
27 changes: 27 additions & 0 deletions frontend/src/components/jobs/result/jobApi.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,33 @@ export async function deleteJob(jobId) {
return success;
}

export async function rescanJob(jobId) {
try {
const response = await axios.post(`${JOB_BASE_URI}/${jobId}/rescan`);
const newJobId = response.data.id;
if (response.status === 202) {
addToast(
<span>
Sent rescan request for job #{jobId}. Created job #{newJobId}.
</span>,
null,
"success",
2000,
);
}
return newJobId;
} catch (error) {
addToast(
<span>
Failed. Operation: <em>rescan job #{jobId}</em>
</span>,
error.parsedMsg,
"warning",
);
return null;
}
}

export async function killPlugin(jobId, plugin) {
const sure = await areYouSureConfirmDialog(
`kill ${plugin.type} '${plugin.name}'`,
Expand Down
49 changes: 24 additions & 25 deletions frontend/src/components/scan/ScanForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ function DangerErrorMessage(fieldName) {
export default function ScanForm() {
const [searchParams, _] = useSearchParams();
const observableParam = searchParams.get(JobTypes.OBSERVABLE);
const isSampleParam = searchParams.get("isSample") === "true";
const investigationIdParam = searchParams.get("investigation") || null;
const parentIdParam = searchParams.get("parent");
const { guideState, setGuideState } = useGuideContext();
Expand Down Expand Up @@ -416,6 +417,23 @@ export default function ScanForm() {
}))
.filter((item) => !item.isDisabled && item.starting);

const selectObservableType = (value) => {
formik.setFieldValue("observableType", value, false);
formik.setFieldValue(
"classification",
value === JobTypes.OBSERVABLE
? ObservableClassifications.GENERIC
: JobTypes.FILE,
);
formik.setFieldValue("observable_names", [""], false);
formik.setFieldValue("files", [""], false);
formik.setFieldValue("analysisOptionValues", ScanTypes.playbooks, false);
setScanType(ScanTypes.playbooks);
formik.setFieldValue("playbook", "", false); // reset
formik.setFieldValue("analyzers", [], false); // reset
formik.setFieldValue("connectors", [], false); // reset
};

const updateAdvancedConfig = (
tags,
tlp,
Expand Down Expand Up @@ -535,9 +553,11 @@ export default function ScanForm() {
if (observableParam) {
updateSelectedObservable(observableParam, 0);
if (formik.playbook) updateSelectedPlaybook(formik.playbook);
} else if (isSampleParam) {
selectObservableType(JobTypes.FILE);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [observableParam, playbooksLoading]);
}, [observableParam, playbooksLoading, isSampleParam]);

/* With the setFieldValue the validation and rerender don't work properly: the last update seems to not trigger the validation
and leaves the UI with values not valid, for this reason the scan button is disabled, but if the user set focus on the UI the last
Expand Down Expand Up @@ -614,30 +634,9 @@ export default function ScanForm() {
type="radio"
name="observableType"
value={jobType}
onClick={(event) => {
formik.setFieldValue(
"observableType",
event.target.value,
false,
);
formik.setFieldValue(
"classification",
event.target.value === JobTypes.OBSERVABLE
? ObservableClassifications.GENERIC
: JobTypes.FILE,
);
formik.setFieldValue("observable_names", [""], false);
formik.setFieldValue("files", [""], false);
formik.setFieldValue(
"analysisOptionValues",
ScanTypes.playbooks,
false,
);
setScanType(ScanTypes.playbooks);
formik.setFieldValue("playbook", "", false); // reset
formik.setFieldValue("analyzers", [], false); // reset
formik.setFieldValue("connectors", [], false); // reset
}}
onClick={(event) =>
selectObservableType(event.target.value)
}
/>
<Label check>
{jobType === JobTypes.OBSERVABLE
Expand Down
Loading

0 comments on commit d50c673

Please # to comment.