mirror of
https://github.com/infinition/Bjorn.git
synced 2026-03-19 10:10:24 +00:00
feat: Implement package management utilities with JSON endpoints for listing and uninstalling packages feat: Create plugin management utilities with endpoints for listing, configuring, and installing plugins feat: Develop schedule and trigger management utilities with CRUD operations for schedules and triggers
281 lines
8.7 KiB
JavaScript
281 lines
8.7 KiB
JavaScript
/**
|
|
* condition-builder.js - Visual block-based condition editor for triggers.
|
|
* Produces/consumes JSON condition trees with AND/OR groups + leaf conditions.
|
|
*/
|
|
import { el, empty } from './dom.js';
|
|
|
|
// Condition source definitions (drives the parameter UI)
|
|
const SOURCES = {
|
|
action_result: {
|
|
label: 'Action Result',
|
|
params: [
|
|
{ key: 'action', type: 'text', placeholder: 'e.g. scanning', label: 'Action' },
|
|
{ key: 'check', type: 'select', choices: ['eq', 'neq'], label: 'Check' },
|
|
{ key: 'value', type: 'select', choices: ['success', 'failed'], label: 'Value' },
|
|
],
|
|
},
|
|
hosts_with_port: {
|
|
label: 'Hosts with Port',
|
|
params: [
|
|
{ key: 'port', type: 'number', placeholder: '22', label: 'Port' },
|
|
{ key: 'check', type: 'select', choices: ['gt', 'lt', 'eq', 'gte', 'lte'], label: 'Op' },
|
|
{ key: 'value', type: 'number', placeholder: '0', label: 'Count' },
|
|
],
|
|
},
|
|
hosts_alive: {
|
|
label: 'Hosts Alive',
|
|
params: [
|
|
{ key: 'check', type: 'select', choices: ['gt', 'lt', 'eq', 'gte', 'lte'], label: 'Op' },
|
|
{ key: 'value', type: 'number', placeholder: '0', label: 'Count' },
|
|
],
|
|
},
|
|
cred_found: {
|
|
label: 'Credentials Found',
|
|
params: [
|
|
{ key: 'service', type: 'text', placeholder: 'e.g. ssh, ftp', label: 'Service' },
|
|
],
|
|
},
|
|
has_vuln: {
|
|
label: 'Has Vulnerabilities',
|
|
params: [],
|
|
},
|
|
db_count: {
|
|
label: 'DB Row Count',
|
|
params: [
|
|
{ key: 'table', type: 'select', choices: ['hosts', 'creds', 'vulnerabilities', 'services'], label: 'Table' },
|
|
{ key: 'check', type: 'select', choices: ['gt', 'lt', 'eq', 'gte', 'lte'], label: 'Op' },
|
|
{ key: 'value', type: 'number', placeholder: '0', label: 'Count' },
|
|
],
|
|
},
|
|
time_after: {
|
|
label: 'Time After',
|
|
params: [
|
|
{ key: 'hour', type: 'number', placeholder: '9', label: 'Hour (0-23)', min: 0, max: 23 },
|
|
{ key: 'minute', type: 'number', placeholder: '0', label: 'Minute (0-59)', min: 0, max: 59 },
|
|
],
|
|
},
|
|
time_before: {
|
|
label: 'Time Before',
|
|
params: [
|
|
{ key: 'hour', type: 'number', placeholder: '18', label: 'Hour (0-23)', min: 0, max: 23 },
|
|
{ key: 'minute', type: 'number', placeholder: '0', label: 'Minute (0-59)', min: 0, max: 59 },
|
|
],
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Build a condition editor inside a container element.
|
|
* @param {HTMLElement} container - DOM element to render into
|
|
* @param {object|null} initial - Initial condition JSON tree (null = empty AND group)
|
|
*/
|
|
export function buildConditionEditor(container, initial = null) {
|
|
empty(container);
|
|
container.classList.add('cond-editor');
|
|
const root = initial || { type: 'group', op: 'AND', children: [] };
|
|
container.appendChild(_renderNode(root));
|
|
}
|
|
|
|
/**
|
|
* Read the current condition tree from the DOM.
|
|
* @param {HTMLElement} container - The editor container
|
|
* @returns {object} JSON condition tree
|
|
*/
|
|
export function getConditions(container) {
|
|
const rootEl = container.querySelector('.cond-group, .cond-block');
|
|
if (!rootEl) return null;
|
|
return _readNode(rootEl);
|
|
}
|
|
|
|
// --- Internal rendering ---
|
|
|
|
function _renderNode(node) {
|
|
if (node.type === 'group') return _renderGroup(node);
|
|
return _renderCondition(node);
|
|
}
|
|
|
|
function _renderGroup(node) {
|
|
const op = (node.op || 'AND').toUpperCase();
|
|
const childContainer = el('div', { class: 'cond-children' });
|
|
|
|
// Render existing children
|
|
(node.children || []).forEach(child => {
|
|
childContainer.appendChild(_wrapDeletable(_renderNode(child)));
|
|
});
|
|
|
|
const opToggle = el('select', { class: 'cond-op-toggle', 'data-op': op }, [
|
|
el('option', { value: 'AND', selected: op === 'AND' ? '' : null }, ['AND']),
|
|
el('option', { value: 'OR', selected: op === 'OR' ? '' : null }, ['OR']),
|
|
]);
|
|
opToggle.value = op;
|
|
opToggle.addEventListener('change', () => {
|
|
group.dataset.op = opToggle.value;
|
|
group.classList.toggle('cond-group-or', opToggle.value === 'OR');
|
|
group.classList.toggle('cond-group-and', opToggle.value === 'AND');
|
|
});
|
|
|
|
const addCondBtn = el('button', {
|
|
class: 'cond-add-btn',
|
|
type: 'button',
|
|
onClick: () => {
|
|
const newCond = { type: 'condition', source: 'action_result', action: '', check: 'eq', value: 'success' };
|
|
childContainer.appendChild(_wrapDeletable(_renderCondition(newCond)));
|
|
},
|
|
}, ['+ Condition']);
|
|
|
|
const addGroupBtn = el('button', {
|
|
class: 'cond-add-btn cond-add-group-btn',
|
|
type: 'button',
|
|
onClick: () => {
|
|
const newGroup = { type: 'group', op: 'AND', children: [] };
|
|
childContainer.appendChild(_wrapDeletable(_renderGroup(newGroup)));
|
|
},
|
|
}, ['+ Group']);
|
|
|
|
const group = el('div', {
|
|
class: `cond-group cond-group-${op.toLowerCase()}`,
|
|
'data-type': 'group',
|
|
'data-op': op,
|
|
}, [
|
|
el('div', { class: 'cond-group-header' }, [opToggle]),
|
|
childContainer,
|
|
el('div', { class: 'cond-group-actions' }, [addCondBtn, addGroupBtn]),
|
|
]);
|
|
|
|
return group;
|
|
}
|
|
|
|
function _renderCondition(node) {
|
|
const source = node.source || 'action_result';
|
|
const paramsContainer = el('div', { class: 'cond-params' });
|
|
|
|
const sourceSelect = el('select', { class: 'cond-source-select' });
|
|
Object.entries(SOURCES).forEach(([key, def]) => {
|
|
const opt = el('option', { value: key, selected: key === source ? '' : null }, [def.label]);
|
|
sourceSelect.appendChild(opt);
|
|
});
|
|
sourceSelect.value = source;
|
|
|
|
// Build params for current source
|
|
_buildParams(paramsContainer, source, node);
|
|
|
|
sourceSelect.addEventListener('change', () => {
|
|
const newSource = sourceSelect.value;
|
|
block.dataset.source = newSource;
|
|
_buildParams(paramsContainer, newSource, {});
|
|
});
|
|
|
|
const block = el('div', {
|
|
class: 'cond-block',
|
|
'data-type': 'condition',
|
|
'data-source': source,
|
|
}, [sourceSelect, paramsContainer]);
|
|
|
|
return block;
|
|
}
|
|
|
|
function _buildParams(container, source, data) {
|
|
empty(container);
|
|
const def = SOURCES[source];
|
|
if (!def) return;
|
|
|
|
def.params.forEach(p => {
|
|
const val = data[p.key] !== undefined ? data[p.key] : (p.placeholder || '');
|
|
let input;
|
|
|
|
if (p.type === 'select') {
|
|
input = el('select', { class: 'cond-param-input', 'data-key': p.key });
|
|
(p.choices || []).forEach(c => {
|
|
const opt = el('option', { value: c, selected: String(c) === String(data[p.key] || '') ? '' : null }, [c]);
|
|
input.appendChild(opt);
|
|
});
|
|
if (data[p.key] !== undefined) input.value = String(data[p.key]);
|
|
} else if (p.type === 'number') {
|
|
input = el('input', {
|
|
type: 'number',
|
|
class: 'cond-param-input',
|
|
'data-key': p.key,
|
|
value: data[p.key] !== undefined ? String(data[p.key]) : '',
|
|
placeholder: p.placeholder || '',
|
|
min: p.min !== undefined ? String(p.min) : undefined,
|
|
max: p.max !== undefined ? String(p.max) : undefined,
|
|
});
|
|
} else {
|
|
input = el('input', {
|
|
type: 'text',
|
|
class: 'cond-param-input',
|
|
'data-key': p.key,
|
|
value: data[p.key] !== undefined ? String(data[p.key]) : '',
|
|
placeholder: p.placeholder || '',
|
|
});
|
|
}
|
|
|
|
container.appendChild(
|
|
el('label', { class: 'cond-param-label' }, [
|
|
el('span', { class: 'cond-param-name' }, [p.label]),
|
|
input,
|
|
])
|
|
);
|
|
});
|
|
}
|
|
|
|
function _wrapDeletable(nodeEl) {
|
|
const wrapper = el('div', { class: 'cond-item-wrapper' }, [
|
|
nodeEl,
|
|
el('button', {
|
|
class: 'cond-delete-btn',
|
|
type: 'button',
|
|
title: 'Remove',
|
|
onClick: () => wrapper.remove(),
|
|
}, ['\u00d7']),
|
|
]);
|
|
return wrapper;
|
|
}
|
|
|
|
// --- Read DOM -> JSON ---
|
|
|
|
function _readNode(nodeEl) {
|
|
const type = nodeEl.dataset.type;
|
|
if (type === 'group') return _readGroup(nodeEl);
|
|
if (type === 'condition') return _readCondition(nodeEl);
|
|
|
|
// Check if it's a wrapper
|
|
const inner = nodeEl.querySelector('.cond-group, .cond-block');
|
|
if (inner) return _readNode(inner);
|
|
return null;
|
|
}
|
|
|
|
function _readGroup(groupEl) {
|
|
const op = groupEl.dataset.op || 'AND';
|
|
const children = [];
|
|
const childrenContainer = groupEl.querySelector('.cond-children');
|
|
if (childrenContainer) {
|
|
for (const wrapper of childrenContainer.children) {
|
|
const inner = wrapper.querySelector('.cond-group, .cond-block');
|
|
if (inner) {
|
|
const child = _readNode(inner);
|
|
if (child) children.push(child);
|
|
}
|
|
}
|
|
}
|
|
return { type: 'group', op: op.toUpperCase(), children };
|
|
}
|
|
|
|
function _readCondition(blockEl) {
|
|
const source = blockEl.dataset.source || 'action_result';
|
|
const result = { type: 'condition', source };
|
|
|
|
const inputs = blockEl.querySelectorAll('.cond-param-input');
|
|
inputs.forEach(input => {
|
|
const key = input.dataset.key;
|
|
if (!key) return;
|
|
let val = input.value;
|
|
// Auto-cast numbers
|
|
if (input.type === 'number' && val !== '') {
|
|
val = Number(val);
|
|
}
|
|
result[key] = val;
|
|
});
|
|
|
|
return result;
|
|
}
|