diff --git a/api_app/user_events_manager/migrations/0004_user_event_reason.py b/api_app/user_events_manager/migrations/0004_user_event_reason.py
new file mode 100644
index 0000000000..70d0c6732e
--- /dev/null
+++ b/api_app/user_events_manager/migrations/0004_user_event_reason.py
@@ -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),
+ ),
+ ]
diff --git a/api_app/user_events_manager/models.py b/api_app/user_events_manager/models.py
index 2fa6a710bc..2b8464cee4 100644
--- a/api_app/user_events_manager/models.py
+++ b/api_app/user_events_manager/models.py
@@ -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)
data_model: ForeignKey
decay_progression = models.IntegerField(
diff --git a/frontend/src/components/analyzables/result/analyzablesHistoryTableColumns.jsx b/frontend/src/components/analyzables/result/analyzablesHistoryTableColumns.jsx
index 817233b2b2..e5a1b82044 100644
--- a/frontend/src/components/analyzables/result/analyzablesHistoryTableColumns.jsx
+++ b/frontend/src/components/analyzables/result/analyzablesHistoryTableColumns.jsx
@@ -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;
},
diff --git a/frontend/src/components/userEvents/UserEventModal.jsx b/frontend/src/components/userEvents/UserEventModal.jsx
index 51200f8d6f..08dd6ba7ad 100644
--- a/frontend/src/components/userEvents/UserEventModal.jsx
+++ b/frontend/src/components/userEvents/UserEventModal.jsx
@@ -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,
@@ -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.";
@@ -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);
@@ -772,13 +768,56 @@ export function UserEventModal({ analyzables, toggle, isOpen }) {
-
+
+
+
+
+
+
+ Malware family:
+
+
+
+
+
+
+
+
+
+
+
+
+ Related Artifacts:
+
+
- {formik.errors["related_threats-0"] &&
- formik.touched["related_threats-0"] && (
-
- {formik.errors["related_threats-0"]}
-
- )}
-
+
diff --git a/frontend/src/components/userEvents/userEventsTableColumns.jsx b/frontend/src/components/userEvents/userEventsTableColumns.jsx
index dd8c4a1b84..02020d3709 100644
--- a/frontend/src/components/userEvents/userEventsTableColumns.jsx
+++ b/frontend/src/components/userEvents/userEventsTableColumns.jsx
@@ -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 && (
{
rank: null,
resolutions: [],
},
+ reason: "my reason",
data_model_object_id: 15,
decay_progression: 0,
decay_timedelta_days: 3,
@@ -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");
diff --git a/frontend/tests/components/userEvents/UserEventModal.test.jsx b/frontend/tests/components/userEvents/UserEventModal.test.jsx
index d1ed393596..2a5075ed4a 100644
--- a/frontend/tests/components/userEvents/UserEventModal.test.jsx
+++ b/frontend/tests/components/userEvents/UserEventModal.test.jsx
@@ -117,8 +117,17 @@ describe("test UserEventModal component", () => {
expect(trusted10).not.toBeChecked();
const reasonInput = screen.getAllByRole("textbox")[1];
expect(reasonInput).toBeInTheDocument();
- expect(reasonInput.id).toBe("related_threats-0");
- const externalReferencesInput = screen.getAllByRole("textbox")[2];
+ expect(reasonInput.id).toBe("reason");
+ expect(reasonInput.value).toBe("");
+ const malwareFamilyInput = screen.getAllByRole("textbox")[2];
+ expect(malwareFamilyInput).toBeInTheDocument();
+ expect(malwareFamilyInput.id).toBe("malware_family");
+ expect(malwareFamilyInput.value).toBe("");
+ const relatedThreatsInput = screen.getAllByRole("textbox")[3];
+ expect(relatedThreatsInput).toBeInTheDocument();
+ expect(relatedThreatsInput.id).toBe("related_threats-0");
+ expect(relatedThreatsInput.value).toBe("");
+ const externalReferencesInput = screen.getAllByRole("textbox")[4];
expect(externalReferencesInput).toBeInTheDocument();
expect(externalReferencesInput.id).toBe("external_references-0");
expect(screen.getByText("Kill chain phase:")).toBeInTheDocument();
@@ -162,9 +171,14 @@ describe("test UserEventModal component", () => {
analyzable: { name: "google.com" },
data_model_content: {
evaluation: "malicious",
- related_threats: ["my reason"],
reliability: 10,
+ malware_family: "",
+ kill_chain_phase: "",
+ related_threats: [],
+ external_references: [],
+ tags: [],
},
+ reason: "my reason",
decay_progression: "0",
decay_timedelta_days: 120,
},
@@ -177,9 +191,14 @@ describe("test UserEventModal component", () => {
network: "1.2.3.0/24",
data_model_content: {
evaluation: "malicious",
- related_threats: ["my reason"],
reliability: 10,
+ malware_family: "",
+ kill_chain_phase: "",
+ related_threats: [],
+ external_references: [],
+ tags: [],
},
+ reason: "my reason",
decay_progression: "0",
decay_timedelta_days: 120,
},
@@ -192,9 +211,14 @@ describe("test UserEventModal component", () => {
query: ".*\\.test.com",
data_model_content: {
evaluation: "malicious",
- related_threats: ["my reason"],
reliability: 10,
+ malware_family: "",
+ kill_chain_phase: "",
+ related_threats: [],
+ external_references: [],
+ tags: [],
},
+ reason: "my reason",
decay_progression: "0",
decay_timedelta_days: 120,
},
@@ -296,9 +320,17 @@ describe("test UserEventModal component", () => {
expect(trusted10).not.toBeChecked();
const reasonInput = screen.getAllByRole("textbox")[1];
expect(reasonInput).toBeInTheDocument();
- expect(reasonInput.id).toBe("related_threats-0");
+ expect(reasonInput.id).toBe("reason");
expect(reasonInput.value).toBe("");
- const externalReferencesInput = screen.getAllByRole("textbox")[2];
+ const malwareFamilyInput = screen.getAllByRole("textbox")[2];
+ expect(malwareFamilyInput).toBeInTheDocument();
+ expect(malwareFamilyInput.id).toBe("malware_family");
+ expect(malwareFamilyInput.value).toBe("");
+ const relatedThreatsInput = screen.getAllByRole("textbox")[3];
+ expect(relatedThreatsInput).toBeInTheDocument();
+ expect(relatedThreatsInput.id).toBe("related_threats-0");
+ expect(relatedThreatsInput.value).toBe("");
+ const externalReferencesInput = screen.getAllByRole("textbox")[4];
expect(externalReferencesInput).toBeInTheDocument();
expect(externalReferencesInput.id).toBe("external_references-0");
expect(externalReferencesInput.value).toBe("");
@@ -331,7 +363,7 @@ describe("test UserEventModal component", () => {
},
);
- test("UserEventModal - set killchain, tags and advanced evaluation", async () => {
+ test("UserEventModal - advanced fields (killchain, malware family, related threat, external ref, tags and advanced evaluation)", async () => {
const user = userEvent.setup();
axios.put.mockImplementation(() =>
Promise.resolve({ status: 200, data: [""] }),
@@ -382,9 +414,17 @@ describe("test UserEventModal component", () => {
expect(trusted10).not.toBeChecked();
const reasonInput = screen.getAllByRole("textbox")[1];
expect(reasonInput).toBeInTheDocument();
- expect(reasonInput.id).toBe("related_threats-0");
+ expect(reasonInput.id).toBe("reason");
expect(reasonInput.value).toBe("");
- const externalReferencesInput = screen.getAllByRole("textbox")[2];
+ const malwareFamilyInput = screen.getAllByRole("textbox")[2];
+ expect(malwareFamilyInput).toBeInTheDocument();
+ expect(malwareFamilyInput.id).toBe("malware_family");
+ expect(malwareFamilyInput.value).toBe("");
+ const relatedThreatsInput = screen.getAllByRole("textbox")[3];
+ expect(relatedThreatsInput).toBeInTheDocument();
+ expect(relatedThreatsInput.id).toBe("related_threats-0");
+ expect(relatedThreatsInput.value).toBe("");
+ const externalReferencesInput = screen.getAllByRole("textbox")[4];
expect(externalReferencesInput).toBeInTheDocument();
expect(externalReferencesInput.id).toBe("external_references-0");
const killChainPhaseInput = screen.getAllByRole("combobox")[0];
@@ -423,6 +463,24 @@ describe("test UserEventModal component", () => {
// add reason
fireEvent.change(reasonInput, { target: { value: "my reason" } });
expect(reasonInput.value).toBe("my reason");
+ // add malware family
+ fireEvent.change(malwareFamilyInput, { target: { value: "ursnif" } });
+ expect(malwareFamilyInput.value).toBe("ursnif");
+ // add related artifacts
+ fireEvent.change(relatedThreatsInput, {
+ target: { value: "anotherArtifact.com" },
+ });
+ expect(relatedThreatsInput.value).toBe("anotherArtifact.com");
+ // add external references
+ fireEvent.change(externalReferencesInput, {
+ target: { value: "http://test.com" },
+ });
+ expect(externalReferencesInput.value).toBe("http://test.com");
+ // add killchain phase
+ await userEvent.click(killChainPhaseInput);
+ await userEvent.click(screen.getByText("action"));
+ expect(screen.getByText("action")).toBeInTheDocument();
+ expect(screen.queryByText("c2")).not.toBeInTheDocument(); // check other option are not visible
// add tags (2 of them)
await userEvent.click(tagsInput);
await userEvent.click(screen.getByText("phishing"));
@@ -432,11 +490,6 @@ describe("test UserEventModal component", () => {
await userEvent.click(screen.getByText("malware"));
expect(screen.getByText("malware")).toBeInTheDocument();
expect(screen.queryByText("scanner")).not.toBeInTheDocument(); // check other option are not visible
- // add kill chain phase
- await userEvent.click(killChainPhaseInput);
- await userEvent.click(screen.getByText("action"));
- expect(screen.getByText("action")).toBeInTheDocument();
- expect(screen.queryByText("c2")).not.toBeInTheDocument(); // check other option are not visible
// IMPORTANT - wait for the state change
await screen.findByText("artifact");
@@ -452,11 +505,14 @@ describe("test UserEventModal component", () => {
analyzable: { name: "test.com" },
data_model_content: {
evaluation: "trusted",
- related_threats: ["my reason"],
reliability: "9",
+ malware_family: "ursnif",
+ related_threats: ["anotherArtifact.com"],
+ external_references: ["http://test.com"],
kill_chain_phase: "action",
tags: ["phishing", "malware"],
},
+ reason: "my reason",
decay_progression: "0",
decay_timedelta_days: 120,
});
@@ -514,9 +570,17 @@ describe("test UserEventModal component", () => {
expect(trusted10).not.toBeChecked();
const reasonInput = screen.getAllByRole("textbox")[1];
expect(reasonInput).toBeInTheDocument();
- expect(reasonInput.id).toBe("related_threats-0");
+ expect(reasonInput.id).toBe("reason");
expect(reasonInput.value).toBe("");
- const externalReferencesInput = screen.getAllByRole("textbox")[2];
+ const malwareFamilyInput = screen.getAllByRole("textbox")[2];
+ expect(malwareFamilyInput).toBeInTheDocument();
+ expect(malwareFamilyInput.id).toBe("malware_family");
+ expect(malwareFamilyInput.value).toBe("");
+ const relatedThreatsInput = screen.getAllByRole("textbox")[3];
+ expect(relatedThreatsInput).toBeInTheDocument();
+ expect(relatedThreatsInput.id).toBe("related_threats-0");
+ expect(relatedThreatsInput.value).toBe("");
+ const externalReferencesInput = screen.getAllByRole("textbox")[4];
expect(externalReferencesInput).toBeInTheDocument();
expect(externalReferencesInput.id).toBe("external_references-0");
const killChainPhaseInput = screen.getAllByRole("combobox")[0];
@@ -579,9 +643,14 @@ describe("test UserEventModal component", () => {
analyzable: { name: "test.com" },
data_model_content: {
evaluation: "malicious",
- related_threats: ["my reason"],
reliability: 7,
+ kill_chain_phase: "",
+ malware_family: "",
+ related_threats: [],
+ external_references: [],
+ tags: [],
},
+ reason: "my reason",
decay_progression: "0",
decay_timedelta_days: 120,
});