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
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ private static ConditionOutput toConditionOutput(Condition c) {
.keySubtype(c.getKeySubtype())
.type(c.getType() != null ? c.getType().name() : null)
.value(c.getValue())
.caseSensitive(c.isCaseSensitive())
.conditionParentId(parentId)
.mappingType(c.getMappingType())
.build();
Expand Down Expand Up @@ -120,6 +121,7 @@ public static Condition toCondition(ConditionCreateInput input, Condition condit
.keySubtype(input.getKeySubtype())
.type(input.getType())
.value(input.getValue())
.caseSensitive(input.isCaseSensitive())
.conditionParent(conditionParent)
.mappingType(resolveMappingType(input))
.build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ public class ConditionCreateInput {
@JsonProperty("condition_value")
private String value;

/** Whether the comparison is case-sensitive (default: true) */
@Schema(description = "Whether the comparison is case-sensitive")
@JsonProperty("condition_case_sensitive")
@Builder.Default
private boolean caseSensitive = true;

/** Condition key: Property to be mapped */
@Schema(description = "Property to be mapped")
@JsonProperty("condition_key")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ public class ConditionOutput {
@JsonProperty("condition_value")
private String value;

@JsonProperty("condition_case_sensitive")
private boolean caseSensitive;

@JsonProperty("condition_parent_id")
private String conditionParentId;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.openaev.migration;

import java.sql.Statement;
import org.flywaydb.core.api.migration.BaseJavaMigration;
import org.flywaydb.core.api.migration.Context;
import org.springframework.stereotype.Component;

@Component
public class V5_34__Add_condition_case_sensitive extends BaseJavaMigration {

@Override
public void migrate(Context context) throws Exception {
try (Statement statement = context.getConnection().createStatement()) {
statement.execute(
"ALTER TABLE conditions ADD COLUMN IF NOT EXISTS condition_case_sensitive BOOLEAN NOT NULL DEFAULT TRUE");
}
}
}
14 changes: 11 additions & 3 deletions openaev-api/src/main/java/io/openaev/utils/ConditionUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -82,22 +82,30 @@ public boolean evaluateLeafCondition(String actualValue, Condition filter) {
return true;
}
String target = filter.getValue();
boolean caseSensitive = filter.isCaseSensitive();

switch (type) {
case IS_NULL:
return actualValue == null;
case IS_NOT_NULL:
return actualValue != null;
case EQ:
return actualValue != null && actualValue.equalsIgnoreCase(target);
return actualValue != null
&& (caseSensitive ? actualValue.equals(target) : actualValue.equalsIgnoreCase(target));
case NEQ:
return actualValue != null && !actualValue.equalsIgnoreCase(target);
return actualValue != null
&& (caseSensitive
? !actualValue.equals(target)
: !actualValue.equalsIgnoreCase(target));
case IN, NIN:
if (actualValue == null || target == null) {
return false;
}
List<String> targetList = Arrays.asList(target.split("\\s*,\\s*"));
boolean contains = targetList.stream().anyMatch(actualValue::equalsIgnoreCase);
boolean contains =
caseSensitive
? targetList.stream().anyMatch(actualValue::equals)
: targetList.stream().anyMatch(actualValue::equalsIgnoreCase);
return (type == ConditionType.IN) == contains;
case GT, GTE, LT, LTE:
return handleNumericComparison(actualValue, target, type);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1089,9 +1089,14 @@ void shouldLeaveMappingTypeNull_whenNonMapperCondition() {
class IsFilterConditionValid {

private Condition leaf(ConditionType type, String value) {
return leaf(type, value, true);
}

private Condition leaf(ConditionType type, String value, boolean caseSensitive) {
Condition c = new Condition();
c.setType(type);
c.setValue(value);
c.setCaseSensitive(caseSensitive);
return c;
}

Expand Down Expand Up @@ -1215,7 +1220,8 @@ void isNotNull_shouldReturnFalse_whenValueIsNull() {

@Test
void eq_shouldReturnTrue_whenValuesMatch_caseInsensitive() {
assertTrue(conditionUtils.isFilterConditionValid("Admin", leaf(ConditionType.EQ, "admin")));
assertTrue(
conditionUtils.isFilterConditionValid("Admin", leaf(ConditionType.EQ, "admin", false)));
}

@Test
Expand Down Expand Up @@ -1265,7 +1271,8 @@ void in_shouldReturnFalse_whenValueIsNotInTargetList() {
@Test
void in_shouldBeCaseInsensitive() {
assertTrue(
conditionUtils.isFilterConditionValid("ADMIN", leaf(ConditionType.IN, "admin, root")));
conditionUtils.isFilterConditionValid(
"ADMIN", leaf(ConditionType.IN, "admin, root", false)));
}

@Test
Expand Down
30 changes: 29 additions & 1 deletion openaev-front/src/admin/components/chaining/logic/Logic.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import type {
import AddComponentButton, { type LogicContext } from './AddComponentButton';
import ChainingFlowConfiguration, { type DrawerView } from './chaining_flow/ChainingFlowConfiguration';
import LogicFlow from './chaining_flow/LogicFlow';
import type { ActionMeta } from './types';
import type { ActionMeta, EventMeta } from './types';

interface LogicProps {
workflowId: string | undefined;
Expand All @@ -22,6 +22,8 @@ const Logic = ({ workflowId, context }: LogicProps) => {
const [validAssets, setValidAssets] = useState<ScopeAssetOutput[]>([]);
// Track whether existing steps/events exist
const [hasExistingData, setHasExistingData] = useState<boolean | null>(null);
// Count of existing events (used to generate default names)
const [eventCount, setEventCount] = useState(0);
// Key to force LogicFlow re-mount after adding a step
const [refreshKey, setRefreshKey] = useState(0);
// Drawer navigation state (shared with ChainingFlowConfiguration)
Expand All @@ -32,6 +34,12 @@ const Logic = ({ workflowId, context }: LogicProps) => {
meta: ActionMeta;
} | null>(null);

// Event currently being edited
const [editingEvent, setEditingEvent] = useState<{
eventId: string;
meta: EventMeta;
} | null>(null);

useEffect(() => {
if (workflowId) {
fetchValidAssets(workflowId).then((assets: ScopeAssetOutput[]) => {
Expand All @@ -50,6 +58,7 @@ const Logic = ({ workflowId, context }: LogicProps) => {
const steps: StepOutput[] = stepsRes.data ?? [];
const events: EventOutput[] = conditionsRes.data ?? [];
setHasExistingData(steps.length > 0 || events.length > 0);
setEventCount(events.length);
});
}, [workflowId]);

Expand All @@ -58,6 +67,12 @@ const Logic = ({ workflowId, context }: LogicProps) => {
setRefreshKey(k => k + 1);
}, []);

const handleEventCreated = useCallback(() => {
setHasExistingData(true);
setEventCount(c => c + 1);
setRefreshKey(k => k + 1);
}, []);

const handleOpenDrawer = useCallback(() => {
setDrawerView('choose');
}, []);
Expand All @@ -70,6 +85,14 @@ const Logic = ({ workflowId, context }: LogicProps) => {
setDrawerView('actionDetail');
}, []);

const handleEditEvent = useCallback((eventId: string, meta: EventMeta) => {
setEditingEvent({
eventId,
meta,
});
setDrawerView('event');
}, []);

// Loading state
if (hasExistingData === null) {
return null;
Expand All @@ -89,6 +112,7 @@ const Logic = ({ workflowId, context }: LogicProps) => {
workflowId={workflowId}
onAddComponent={handleOpenDrawer}
onEditStep={handleEditStep}
onEditEvent={handleEditEvent}
/>
)
: (
Expand All @@ -101,7 +125,11 @@ const Logic = ({ workflowId, context }: LogicProps) => {
onDrawerViewChange={setDrawerView}
editingStep={editingStep}
onEditingStepChange={setEditingStep}
editingEvent={editingEvent}
onEditingEventChange={setEditingEvent}
onStepCreated={handleStepCreated}
onEventCreated={handleEventCreated}
eventCount={eventCount}
/>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { useCallback, useMemo, useState } from 'react';

import { createStep, updateStep } from '../../../../../actions/chaining/chaining-actions';
import {
createCondition,
createStep,
updateCondition,
updateStep,
} from '../../../../../actions/chaining/chaining-actions';
import { useFormatter } from '../../../../../components/i18n';
import type {
ConditionCreateInput,
Expand All @@ -12,9 +17,14 @@ import { MESSAGING$ } from '../../../../../utils/Environment';
import AddActionList from '../drawer/AddActionList';
import AddComponentDrawer from '../drawer/AddComponentDrawer';
import ConfigureActionDetail from '../drawer/ConfigureActionDetail';
import type { ActionDetailData, ActionMeta } from '../types';
import ConfigureEventDetail from '../events/ConfigureEventDetail';
import {
conditionGroupsToApi,
type EventFormData,
} from '../events/event-types';
import type { ActionDetailData, ActionMeta, EventMeta } from '../types';

export type DrawerView = 'closed' | 'choose' | 'action' | 'actionDetail';
export type DrawerView = 'closed' | 'choose' | 'action' | 'actionDetail' | 'event';

interface ChainingFlowConfigurationProps {
workflowId: string | undefined;
Expand All @@ -29,7 +39,17 @@ interface ChainingFlowConfigurationProps {
stepId: string;
meta: ActionMeta;
} | null) => void;
editingEvent: {
eventId: string;
meta: EventMeta;
} | null;
onEditingEventChange: (event: {
eventId: string;
meta: EventMeta;
} | null) => void;
onStepCreated: () => void;
onEventCreated: () => void;
eventCount: number;
}

const ChainingFlowConfiguration = ({
Expand All @@ -39,7 +59,11 @@ const ChainingFlowConfiguration = ({
onDrawerViewChange,
editingStep,
onEditingStepChange,
editingEvent,
onEditingEventChange,
onStepCreated,
onEventCreated,
eventCount,
}: ChainingFlowConfigurationProps) => {
const { t } = useFormatter();

Expand Down Expand Up @@ -100,14 +124,14 @@ const ChainingFlowConfiguration = ({
onDrawerViewChange('closed');
setSelectedAction(null);
onEditingStepChange(null);
}, [onDrawerViewChange, onEditingStepChange]);
onEditingEventChange(null);
}, [onDrawerViewChange, onEditingStepChange, onEditingEventChange]);

const handleSelectComponent = (type: 'action' | 'event') => {
if (type === 'action') {
onDrawerViewChange('action');
} else {
// TODO: handle event creation drawers
onDrawerViewChange('closed');
onDrawerViewChange('event');
}
};

Expand Down Expand Up @@ -214,7 +238,34 @@ const ChainingFlowConfiguration = ({
});
};

// -- Events (future drawers will go here) --
// -- Events --
const handleSaveEvent = async (data: EventFormData) => {
if (!workflowId) return;

const apiConditions = conditionGroupsToApi(data.conditionGroups, data.groupOperators);
const event = {
event_name: data.name,
event_description: data.description || undefined,
event_workflow_id: workflowId,
event_conditions: apiConditions,
};

try {
if (editingEvent) {
await updateCondition(editingEvent.eventId, event);
MESSAGING$.notifySuccess(t('Event updated successfully.'));
} else {
await createCondition(event);
MESSAGING$.notifySuccess(t('Event added successfully.'));
onEventCreated();
}
handleCloseAll();
onStepCreated();
} catch {
if (editingEvent) MESSAGING$.notifyError(t('Failed to update event.'));
else MESSAGING$.notifyError(t('Failed to create event.'));
}
Comment on lines +262 to +267
};

// Resolve the action to show in ConfigureActionDetail
const activeAction = editingAction ?? selectedAction;
Expand Down Expand Up @@ -243,6 +294,15 @@ const ChainingFlowConfiguration = ({
onBackToRoot={handleCloseAll}
onSave={handleSaveActionDetail}
/>
<ConfigureEventDetail
open={drawerView === 'event'}
onClose={handleCloseAll}
onBack={editingEvent ? handleCloseAll : handleBackToChoose}
onSave={handleSaveEvent}
initialData={editingEvent?.meta.formData}
isEditing={!!editingEvent}
defaultEventName={!editingEvent ? `Event ${eventCount + 1}` : undefined}
/>
</>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ interface LogicFlowProps {
reloadTrigger?: number;
onAddComponent: () => void;
onEditStep?: (stepId: string, meta: ActionMeta) => void;
onEditEvent?: (eventId: string, meta: EventMeta) => void;
}

const proOptions = {
Expand All @@ -57,7 +58,7 @@ const proOptions = {
* grouped into MITRE tactic columns. Supports connecting events to actions, editing,
* deleting nodes, and adding new components.
*/
const LogicFlow = ({ workflowId, reloadTrigger, onAddComponent, onEditStep }: LogicFlowProps) => {
const LogicFlow = ({ workflowId, reloadTrigger, onAddComponent, onEditStep, onEditEvent }: LogicFlowProps) => {
const { t } = useFormatter();
const theme = useTheme();

Expand All @@ -82,7 +83,7 @@ const LogicFlow = ({ workflowId, reloadTrigger, onAddComponent, onEditStep }: Lo

// Extra metadata per node (keyed by node id)
const [actionMetas, setActionMetas] = useState<Record<string, ActionMeta>>({});
const [_eventMetas, setEventMetas] = useState<Record<string, EventMeta>>({});
const [eventMetas, setEventMetas] = useState<Record<string, EventMeta>>({});

// Graph loading state — true until first data load completes
const [loading, setLoading] = useState(true);
Expand Down Expand Up @@ -303,12 +304,19 @@ const LogicFlow = ({ workflowId, reloadTrigger, onAddComponent, onEditStep }: Lo
* @param nodeId the step ID to edit
* @param _type the node type (unused)
*/
const editNode = useCallback((nodeId: string, _type: string) => {
const meta = actionMetas[nodeId];
if (meta && onEditStep) {
onEditStep(nodeId, meta);
const editNode = useCallback((nodeId: string, type: string) => {
if (type === 'action') {
const meta = actionMetas[nodeId];
if (meta && onEditStep) {
onEditStep(nodeId, meta);
}
} else if (type === 'event') {
const meta = eventMetas[nodeId];
if (meta && onEditEvent) {
onEditEvent(nodeId, meta);
}
}
}, [actionMetas, onEditStep]);
}, [actionMetas, eventMetas, onEditStep, onEditEvent]);

/**
* Enrich all nodes with edit/delete callbacks so custom node components can trigger actions.
Expand Down
Loading
Loading