Skip to content
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
28 changes: 28 additions & 0 deletions api_app/user_events_manager/migrations/0004_user_event_reason.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Generated by Django 4.2.25 on 2025-12-09 08:54

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("user_events_manager", "0003_user_analyzer_event_data_model_id_index"),
]

operations = [
migrations.AddField(
model_name="useranalyzableevent",
name="reason",
field=models.CharField(default="", max_length=256, null=True),
),
migrations.AddField(
model_name="userdomainwildcardevent",
name="reason",
field=models.CharField(default="", max_length=256, null=True),
),
migrations.AddField(
model_name="useripwildcardevent",
name="reason",
field=models.CharField(default="", max_length=256, null=True),
),
]
1 change: 1 addition & 0 deletions api_app/user_events_manager/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class UserEvent(models.Model):
on_delete=models.CASCADE,
)
date = models.DateTimeField(default=now, editable=False, db_index=True)
reason = models.CharField(max_length=256, default="", null=True)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
reason = models.CharField(max_length=256, default="", null=True)
reason = models.CharField(max_length=256, default="", blank=True)

null=True on a string field causes inconsistent data types because the value can be either str or None. This adds complexity and maybe bugs, but can be solved by replacing null=True with default="". Explained here.

data_model: ForeignKey

decay_progression = models.IntegerField(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ export const analyzablesHistoryTableColumns = [
} else if (value.type === AnalyzableHistoryTypes.JOB) {
text = "Custom Analysis";
} else {
text = value.data_model.related_threats.toString();
text = value.reason;
}
return text;
},
Expand Down
103 changes: 68 additions & 35 deletions frontend/src/components/userEvents/UserEventModal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ export function UserEventModal({ analyzables, toggle, isOpen }) {
tags: [],
malware_family: "",
// advanced fields
reason: "",
evaluation: DataModelEvaluations.MALICIOUS,
reliability: 10,
decay_progression: DecayProgressionTypes.LINEAR,
Expand All @@ -150,8 +151,8 @@ export function UserEventModal({ analyzables, toggle, isOpen }) {
errors[`analyzables-${index}`] = wildcardInputError[analyzable];
}
});
if (values.related_threats[0] === "") {
errors["related_threats-0"] = "Reason is required";
if (values.reason === "") {
errors.reason = "Reason is required";
}
if (!Number.isInteger(values.decay_timedelta_days)) {
errors.decay_timedelta_days = "The value must be a number.";
Expand All @@ -168,34 +169,29 @@ export function UserEventModal({ analyzables, toggle, isOpen }) {
},
validateOnMount: true,
onSubmit: async () => {
const editedFields = {};
delete formik.values.basic_evaluation; // not needed in the request
Object.entries(formik.values).forEach(([key, value]) => {
if (
/* order matters! kill chain also HTML and cannot be converted into JSON
check before the fields and then check if they are different from the default values
*/
!["analyzables", "kill_chain_phase", "tags"].includes(key) &&
JSON.stringify(value) !== JSON.stringify(formik.initialValues[key])
) {
editedFields[key] = value;
}
// special cases for kill chain: it has a key with html as value
if (key === "kill_chain_phase" && value !== "") {
editedFields.kill_chain_phase = value.value;
}
if (key === "tags" && value.length) {
editedFields.tags = value.map((tag) => tag.value);
}
});
console.debug("editedFields", editedFields);
const evaluation = {
reason: formik.values.reason,
decay_progression: formik.values.decay_progression,
decay_timedelta_days: formik.values.decay_timedelta_days,
data_model_content: {
...editedFields,
evaluation: formik.values.evaluation,
reliability: formik.values.reliability,
external_references:
formik.values.external_references[0] !== ""
? formik.values.external_references
: [],
related_threats:
formik.values.related_threats[0] !== ""
? formik.values.related_threats
: [],
malware_family: formik.values.malware_family,
kill_chain_phase:
formik.values.kill_chain_phase !== ""
? formik.values.kill_chain_phase.value
: "",
tags: formik.values.tags.length
? formik.values.tags.map((tag) => tag.value)
: [],
},
};
console.debug("evaluation", evaluation);
Expand Down Expand Up @@ -772,30 +768,67 @@ export function UserEventModal({ analyzables, toggle, isOpen }) {
<FormGroup>
<Row>
<Col md={2} className="d-flex align-items-center">
<Label
className="me-2 mb-0 required"
for="userEvent__related_threats"
>
<Label className="me-2 mb-0 required" for="userEvent__reason">
Reason:
</Label>
</Col>
<Col md={8}>
<Input
id="reason"
name="reason"
type="text"
className="input-dark"
values={formik.values.reason}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
invalid={formik.errors.reason && formik.touched.reason}
/>
{formik.errors.reason && formik.touched.reason && (
<span className="text-danger">{formik.errors.reason}</span>
)}
</Col>
</Row>
</FormGroup>
<hr />
<FormGroup>
<Row>
<Col md={2} className="d-flex align-items-center">
<Label className="me-2 mb-0" for="userEvent__malware_family">
Malware family:
</Label>
</Col>
<Col md={8}>
<Input
id="malware_family"
name="malware_family"
type="text"
className="input-dark"
values={formik.values.reason}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
/>
</Col>
</Row>
</FormGroup>
<hr />
<FormGroup>
<Row>
<Col md={2} className="d-flex align-items-center">
<Label className="me-2 mb-0" for="userEvent__related_threats">
Related Artifacts:
</Label>
</Col>
<Col md={10}>
<ListInput
id="related_threats"
values={formik.values.related_threats}
formikSetFieldValue={formik.setFieldValue}
formikHandlerBlur={formik.handleBlur}
/>
{formik.errors["related_threats-0"] &&
formik.touched["related_threats-0"] && (
<span className="text-danger">
{formik.errors["related_threats-0"]}
</span>
)}
</Col>
</Row>
<hr />
</FormGroup>
<hr />
<FormGroup>
<Row>
<Col md={2} className="d-flex align-items-center">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ export const userEventsTableEndColumns = [
{
Header: "Reasons",
id: "related_threats",
accessor: (userEvent) => userEvent.data_model.related_threats,
accessor: (userEvent) => userEvent.reason,
Cell: ({ value: comments, row }) =>
comments.length > 0 && (
<TableCell
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ describe("test AnalyzableOverview", () => {
rank: null,
resolutions: [],
},
reason: "my reason",
data_model_object_id: 15,
decay_progression: 0,
decay_timedelta_days: 3,
Expand Down Expand Up @@ -199,6 +200,8 @@ describe("test AnalyzableOverview", () => {
expect(
screen.getByRole("columnheader", { name: "Description" }),
).toBeInTheDocument();

expect(screen.getByText("my reason")).toBeInTheDocument();
// cell - job
expect(screen.getByRole("cell", { name: "#13" })).toBeInTheDocument();
expect(screen.getByText("#13").href).toContain("/jobs/13/visualizer");
Expand Down
Loading
Loading