feat: Add login page with dynamic RGB effects and password toggle functionality

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
This commit is contained in:
infinition
2026-03-19 00:40:04 +01:00
parent 3fa4d5742a
commit b0584a1a8e
176 changed files with 7795 additions and 1781 deletions

View File

@@ -0,0 +1,280 @@
/**
* 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;
}

View File

@@ -7,7 +7,7 @@
* @module core/console-sse
*/
import { $, el, toast } from './dom.js';
import { $, el, toast, escapeHtml } from './dom.js';
import { api } from './api.js';
import { t } from './i18n.js';
@@ -47,6 +47,7 @@ const HEALTHY_THRESHOLD = 5; // messages needed before resetting reconnect coun
let isUserScrolling = false;
let autoScroll = true;
let lineBuffer = []; // lines held while user is scrolled up
const MAX_BUFFER_LINES = 500; // cap buffer to prevent unbounded memory growth
let isDocked = false;
let consoleMode = 'log'; // 'log' | 'bubble'
const CONSOLE_SESSION_ID = 'console';
@@ -435,6 +436,10 @@ function connectSSE() {
const html = processLogLine(raw);
if (isUserScrolling && !autoScroll) {
lineBuffer.push(html);
// Evict oldest lines if buffer exceeds max
if (lineBuffer.length > MAX_BUFFER_LINES) {
lineBuffer = lineBuffer.slice(-MAX_BUFFER_LINES);
}
updateBufferBadge();
} else {
appendLogHtml(html);
@@ -660,8 +665,10 @@ async function loadManualTargets() {
if (currentIp && ips.includes(currentIp)) elSelIp.value = currentIp;
}
const customActions = Array.isArray(data?.custom_actions) ? data.custom_actions : [];
elSelAction.innerHTML = '';
if (!actions.length) {
if (!actions.length && !customActions.length) {
const op = document.createElement('option');
op.value = '';
op.textContent = t('console.noAction');
@@ -673,7 +680,20 @@ async function loadManualTargets() {
op.textContent = String(action);
elSelAction.appendChild(op);
}
if (currentAction && actions.includes(currentAction)) elSelAction.value = currentAction;
if (customActions.length) {
const grp = document.createElement('optgroup');
grp.label = 'Custom Scripts';
for (const action of customActions) {
const op = document.createElement('option');
op.value = String(action);
op.textContent = String(action);
grp.appendChild(op);
}
elSelAction.appendChild(grp);
}
if (currentAction && (actions.includes(currentAction) || customActions.includes(currentAction))) {
elSelAction.value = currentAction;
}
}
updatePortsForSelectedIp(portsByIp);
@@ -1090,26 +1110,29 @@ async function sendConsoleChat(inputEl) {
if (!msg) return;
inputEl.value = '';
// Show user message in console
// Show user message in console (escape to prevent XSS)
const safeMsg = escapeHtml(msg);
if (consoleMode === 'bubble') {
appendLogHtml(`<div class="console-bubble-user">${msg}</div>`);
appendLogHtml(`<div class="console-bubble-user">${safeMsg}</div>`);
} else {
appendLogHtml(`<span class="comment-line"><span class="comment-status">YOU</span> ${msg}</span>`);
appendLogHtml(`<span class="comment-line"><span class="comment-status">YOU</span> ${safeMsg}</span>`);
}
// Call LLM
try {
const data = await api.post('/api/llm/chat', { message: msg, session_id: CONSOLE_SESSION_ID });
if (data?.status === 'ok' && data.response) {
// Escape LLM response to prevent stored XSS via prompt injection
const safeResp = escapeHtml(data.response);
if (consoleMode === 'bubble') {
appendLogHtml(`<div class="console-bubble-bjorn llm"><img src="/web/images/icon-60x60.png" class="comment-icon" alt="" style="width:14px;height:14px;vertical-align:middle;margin-right:4px">${data.response}</div>`);
appendLogHtml(`<div class="console-bubble-bjorn llm"><img src="/web/images/icon-60x60.png" class="comment-icon" alt="" style="width:14px;height:14px;vertical-align:middle;margin-right:4px">${safeResp}</div>`);
} else {
appendLogHtml(`<span class="comment-line"><span class="comment-llm-badge">LLM</span><span class="comment-status">BJORN</span> <img src="/web/images/icon-60x60.png" class="comment-icon" alt=""> ${data.response}</span>`);
appendLogHtml(`<span class="comment-line"><span class="comment-llm-badge">LLM</span><span class="comment-status">BJORN</span> <img src="/web/images/icon-60x60.png" class="comment-icon" alt=""> ${safeResp}</span>`);
}
} else {
appendLogHtml(`<span class="loglvl error">Chat error: ${data?.message || 'unknown'}</span>`);
appendLogHtml(`<span class="loglvl error">Chat error: ${escapeHtml(data?.message || 'unknown')}</span>`);
}
} catch (e) {
appendLogHtml(`<span class="loglvl error">Chat error: ${e.message}</span>`);
appendLogHtml(`<span class="loglvl error">Chat error: ${escapeHtml(e.message)}</span>`);
}
}