mirror of
https://github.com/infinition/Bjorn.git
synced 2026-03-16 17:12:00 +00:00
Add RLUtils class for managing RL/AI dashboard endpoints
- Implemented methods for fetching AI stats, training history, and recent experiences. - Added functionality to set operation mode (MANUAL, AUTO, AI) with appropriate handling. - Included helper methods for querying the database and sending JSON responses. - Integrated model metadata extraction for visualization purposes.
This commit is contained in:
556
web/js/pages/loot.js
Normal file
556
web/js/pages/loot.js
Normal file
@@ -0,0 +1,556 @@
|
||||
import { ResourceTracker } from '../core/resource-tracker.js';
|
||||
import { api } from '../core/api.js';
|
||||
import { el, $, $$, empty } from '../core/dom.js';
|
||||
import { t } from '../core/i18n.js';
|
||||
|
||||
const PAGE = 'loot';
|
||||
const MAC_IP_RE = /^[0-9a-f:]{17}_\d+\.\d+\.\d+\.\d+$/i;
|
||||
|
||||
let tracker = null;
|
||||
let root = null;
|
||||
let fileData = [];
|
||||
let allFiles = [];
|
||||
let currentView = 'tree';
|
||||
let currentCategory = 'all';
|
||||
let currentSort = 'name';
|
||||
let sortDirection = 'asc';
|
||||
let searchTerm = '';
|
||||
let searchTimer = null;
|
||||
|
||||
const FILE_ICONS = {
|
||||
ssh: '🔐',
|
||||
sql: '🗄️',
|
||||
smb: '🌐',
|
||||
other: '📄',
|
||||
};
|
||||
|
||||
export async function mount(container) {
|
||||
tracker = new ResourceTracker(PAGE);
|
||||
root = buildShell();
|
||||
container.appendChild(root);
|
||||
bindEvents();
|
||||
await loadFiles();
|
||||
}
|
||||
|
||||
export function unmount() {
|
||||
if (searchTimer) {
|
||||
clearTimeout(searchTimer);
|
||||
searchTimer = null;
|
||||
}
|
||||
if (tracker) {
|
||||
tracker.cleanupAll();
|
||||
tracker = null;
|
||||
}
|
||||
root = null;
|
||||
fileData = [];
|
||||
allFiles = [];
|
||||
currentView = 'tree';
|
||||
currentCategory = 'all';
|
||||
currentSort = 'name';
|
||||
sortDirection = 'asc';
|
||||
searchTerm = '';
|
||||
}
|
||||
|
||||
function buildShell() {
|
||||
return el('div', { class: 'loot-container' }, [
|
||||
el('div', { class: 'stats-bar' }, [
|
||||
statItem('👥', 'stat-victims', t('common.host')),
|
||||
statItem('📄', 'stat-files', t('loot.totalFiles')),
|
||||
statItem('📁', 'stat-folders', t('loot.directories')),
|
||||
]),
|
||||
|
||||
el('div', { class: 'controls-bar' }, [
|
||||
el('div', { class: 'search-container' }, [
|
||||
el('span', { class: 'search-icon' }, ['🔍']),
|
||||
el('input', {
|
||||
type: 'text',
|
||||
class: 'search-input',
|
||||
id: 'searchInput',
|
||||
placeholder: `${t('common.search')}...`,
|
||||
}),
|
||||
el('span', { class: 'clear-search', id: 'clearSearch' }, ['✖']),
|
||||
]),
|
||||
el('div', { class: 'view-controls' }, [
|
||||
el('button', { class: 'view-btn active', id: 'treeViewBtn', title: 'Tree View', type: 'button' }, ['🌳']),
|
||||
el('button', { class: 'view-btn', id: 'listViewBtn', title: t('common.list'), type: 'button' }, ['📋']),
|
||||
el('div', { class: 'sort-dropdown', id: 'sortDropdown' }, [
|
||||
el('button', { class: 'sort-btn', id: 'sortBtn', type: 'button', title: t('common.sortBy') }, ['⬇️']),
|
||||
el('div', { class: 'sort-menu' }, [
|
||||
sortOption('name', t('common.name'), true),
|
||||
sortOption('type', t('common.type')),
|
||||
sortOption('date', t('common.date')),
|
||||
sortOption('asc', t('common.ascending')),
|
||||
sortOption('desc', t('common.descending')),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
|
||||
el('div', { class: 'tabs-container', id: 'tabsContainer' }),
|
||||
|
||||
el('div', { class: 'explorer' }, [
|
||||
el('div', { class: 'explorer-content', id: 'explorerContent' }, [
|
||||
el('div', { class: 'loading' }, [
|
||||
el('div', { class: 'loading-spinner' }),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
function statItem(icon, id, label) {
|
||||
return el('div', { class: 'stat-item' }, [
|
||||
el('span', { class: 'stat-icon' }, [icon]),
|
||||
el('span', { class: 'stat-value', id }, ['0']),
|
||||
el('span', { class: 'stat-label' }, [label]),
|
||||
]);
|
||||
}
|
||||
|
||||
function sortOption(value, label, active = false) {
|
||||
return el('div', {
|
||||
class: `sort-option${active ? ' active' : ''}`,
|
||||
'data-sort': value,
|
||||
role: 'button',
|
||||
tabindex: '0',
|
||||
}, [label]);
|
||||
}
|
||||
|
||||
function bindEvents() {
|
||||
const searchInput = $('#searchInput', root);
|
||||
const clearBtn = $('#clearSearch', root);
|
||||
const treeBtn = $('#treeViewBtn', root);
|
||||
const listBtn = $('#listViewBtn', root);
|
||||
const sortDropdown = $('#sortDropdown', root);
|
||||
const sortBtn = $('#sortBtn', root);
|
||||
|
||||
if (searchInput) {
|
||||
tracker.trackEventListener(searchInput, 'input', (e) => {
|
||||
if (searchTimer) clearTimeout(searchTimer);
|
||||
searchTimer = setTimeout(() => {
|
||||
searchTerm = String(e.target.value || '').toLowerCase().trim();
|
||||
renderContent(true);
|
||||
}, 300);
|
||||
});
|
||||
}
|
||||
|
||||
if (clearBtn) {
|
||||
tracker.trackEventListener(clearBtn, 'click', () => {
|
||||
if (searchInput) searchInput.value = '';
|
||||
searchTerm = '';
|
||||
renderContent();
|
||||
});
|
||||
}
|
||||
|
||||
if (treeBtn) tracker.trackEventListener(treeBtn, 'click', () => setView('tree'));
|
||||
if (listBtn) tracker.trackEventListener(listBtn, 'click', () => setView('list'));
|
||||
|
||||
if (sortBtn && sortDropdown) {
|
||||
tracker.trackEventListener(sortBtn, 'click', () => {
|
||||
sortDropdown.classList.toggle('active');
|
||||
});
|
||||
}
|
||||
|
||||
$$('.sort-option', root).forEach((option) => {
|
||||
tracker.trackEventListener(option, 'click', () => onSortOption(option));
|
||||
tracker.trackEventListener(option, 'keydown', (e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
onSortOption(option);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
tracker.trackEventListener(document, 'click', (e) => {
|
||||
const dropdown = $('#sortDropdown', root);
|
||||
if (dropdown && !dropdown.contains(e.target)) {
|
||||
dropdown.classList.remove('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function onSortOption(option) {
|
||||
$$('.sort-option', root).forEach((opt) => opt.classList.remove('active'));
|
||||
option.classList.add('active');
|
||||
|
||||
const value = option.dataset.sort;
|
||||
if (value === 'asc' || value === 'desc') {
|
||||
sortDirection = value;
|
||||
} else {
|
||||
currentSort = value;
|
||||
}
|
||||
|
||||
$('#sortDropdown', root)?.classList.remove('active');
|
||||
renderContent();
|
||||
}
|
||||
|
||||
function setView(view) {
|
||||
currentView = view;
|
||||
$$('.view-btn', root).forEach((btn) => btn.classList.remove('active'));
|
||||
$(`#${view}ViewBtn`, root)?.classList.add('active');
|
||||
renderContent();
|
||||
}
|
||||
|
||||
async function loadFiles() {
|
||||
try {
|
||||
const data = await api.get('/loot_directories', { timeout: 15000 });
|
||||
if (!data || data.status !== 'success' || !Array.isArray(data.data)) {
|
||||
throw new Error('Invalid response');
|
||||
}
|
||||
|
||||
fileData = data.data;
|
||||
processFiles();
|
||||
updateStats();
|
||||
renderContent();
|
||||
} catch (err) {
|
||||
const explorer = $('#explorerContent', root);
|
||||
if (!explorer) return;
|
||||
empty(explorer);
|
||||
explorer.appendChild(noResults('⚠️', `${t('common.error')}: ${t('common.noData')}`));
|
||||
}
|
||||
}
|
||||
|
||||
function processFiles() {
|
||||
allFiles = [];
|
||||
const stats = {};
|
||||
|
||||
function extractFiles(items, path = '') {
|
||||
for (const item of items || []) {
|
||||
if (item.type === 'directory' && Array.isArray(item.children)) {
|
||||
extractFiles(item.children, `${path}${item.name}/`);
|
||||
} else if (item.type === 'file') {
|
||||
const category = getFileCategory(item.name, path);
|
||||
const fullPath = `${path}${item.name}`;
|
||||
allFiles.push({
|
||||
...item,
|
||||
category,
|
||||
fullPath,
|
||||
path: item.path || fullPath,
|
||||
});
|
||||
stats[category] = (stats[category] || 0) + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extractFiles(fileData);
|
||||
renderTabs(Object.keys(stats));
|
||||
|
||||
const allBadge = $('#badge-all', root);
|
||||
if (allBadge) allBadge.textContent = String(allFiles.length);
|
||||
|
||||
for (const cat of Object.keys(stats)) {
|
||||
const badge = $(`#badge-${cat}`, root);
|
||||
if (badge) badge.textContent = String(stats[cat]);
|
||||
}
|
||||
}
|
||||
|
||||
function getFileCategory(filename, path) {
|
||||
const lowerName = String(filename || '').toLowerCase();
|
||||
const lowerPath = String(path || '').toLowerCase();
|
||||
|
||||
if (lowerPath.includes('ssh') || lowerName.includes('ssh') || lowerName.includes('key')) return 'ssh';
|
||||
if (lowerPath.includes('sql') || lowerName.includes('sql') || lowerName.includes('database')) return 'sql';
|
||||
if (lowerPath.includes('smb') || lowerName.includes('smb') || lowerName.includes('share')) return 'smb';
|
||||
return 'other';
|
||||
}
|
||||
|
||||
function getDirCategory(path) {
|
||||
const lowerPath = String(path || '').toLowerCase();
|
||||
if (lowerPath.includes('ssh')) return 'ssh';
|
||||
if (lowerPath.includes('sql')) return 'sql';
|
||||
if (lowerPath.includes('smb')) return 'smb';
|
||||
return 'other';
|
||||
}
|
||||
|
||||
function updateStats() {
|
||||
const victims = new Set();
|
||||
let totalFiles = 0;
|
||||
let totalFolders = 0;
|
||||
|
||||
function scan(items) {
|
||||
for (const item of items || []) {
|
||||
if (item.type === 'directory') {
|
||||
totalFolders += 1;
|
||||
if (MAC_IP_RE.test(String(item.name || ''))) victims.add(item.name);
|
||||
if (Array.isArray(item.children)) scan(item.children);
|
||||
} else if (item.type === 'file') {
|
||||
totalFiles += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scan(fileData);
|
||||
|
||||
setText('stat-victims', victims.size);
|
||||
setText('stat-files', totalFiles);
|
||||
setText('stat-folders', totalFolders);
|
||||
}
|
||||
|
||||
function setText(id, value) {
|
||||
const node = $(`#${id}`, root);
|
||||
if (node) node.textContent = String(value ?? '');
|
||||
}
|
||||
|
||||
function fileMatchesSearch(file) {
|
||||
if (!searchTerm) return true;
|
||||
const n = String(file?.name || '').toLowerCase();
|
||||
const p = String(file?.fullPath || '').toLowerCase();
|
||||
return n.includes(searchTerm) || p.includes(searchTerm);
|
||||
}
|
||||
|
||||
function computeSearchFilteredFiles() {
|
||||
return allFiles.filter(fileMatchesSearch);
|
||||
}
|
||||
|
||||
function updateBadgesFromFiltered() {
|
||||
const filtered = computeSearchFilteredFiles();
|
||||
setText('badge-all', filtered.length);
|
||||
|
||||
const byCat = filtered.reduce((acc, f) => {
|
||||
acc[f.category] = (acc[f.category] || 0) + 1;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
$$('.tab', root).forEach((tab) => {
|
||||
const cat = tab.dataset.category;
|
||||
if (cat === 'all') return;
|
||||
setText(`badge-${cat}`, byCat[cat] || 0);
|
||||
});
|
||||
}
|
||||
|
||||
function renderTabs(categories) {
|
||||
const tabs = $('#tabsContainer', root);
|
||||
if (!tabs) return;
|
||||
empty(tabs);
|
||||
|
||||
tabs.appendChild(tabNode('all', 'All', true));
|
||||
for (const cat of categories) {
|
||||
tabs.appendChild(tabNode(cat, cat.toUpperCase(), false));
|
||||
}
|
||||
|
||||
$$('.tab', tabs).forEach((tab) => {
|
||||
tracker.trackEventListener(tab, 'click', () => {
|
||||
$$('.tab', tabs).forEach((tEl) => tEl.classList.remove('active'));
|
||||
tab.classList.add('active');
|
||||
currentCategory = tab.dataset.category;
|
||||
renderContent();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function tabNode(category, label, active) {
|
||||
return el('div', {
|
||||
class: `tab${active ? ' active' : ''}`,
|
||||
'data-category': category,
|
||||
}, [
|
||||
label,
|
||||
el('span', { class: 'tab-badge', id: `badge-${category}` }, ['0']),
|
||||
]);
|
||||
}
|
||||
|
||||
function renderContent(autoExpand = false) {
|
||||
const container = $('#explorerContent', root);
|
||||
if (!container) return;
|
||||
|
||||
if (currentView === 'tree') {
|
||||
renderTreeView(container, autoExpand);
|
||||
} else {
|
||||
renderListView(container);
|
||||
}
|
||||
}
|
||||
|
||||
function renderTreeView(container, autoExpand = false) {
|
||||
updateBadgesFromFiltered();
|
||||
const filteredData = filterDataForTree();
|
||||
|
||||
empty(container);
|
||||
|
||||
if (!filteredData.length) {
|
||||
container.appendChild(noResults('🔍', t('common.noData')));
|
||||
return;
|
||||
}
|
||||
|
||||
const tree = el('div', { class: 'tree-view active' });
|
||||
tree.appendChild(renderTreeItems(filteredData, 0, '', autoExpand || !!searchTerm));
|
||||
container.appendChild(tree);
|
||||
}
|
||||
|
||||
function filterDataForTree() {
|
||||
function filterItems(items, path = '', isRoot = false) {
|
||||
return (items || [])
|
||||
.map((item) => {
|
||||
if (item.type === 'directory') {
|
||||
const dirPath = `${path}${item.name}/`;
|
||||
const dirCategory = getDirCategory(dirPath);
|
||||
const filteredChildren = Array.isArray(item.children)
|
||||
? filterItems(item.children, dirPath, false)
|
||||
: [];
|
||||
const nameMatch = String(item.name || '').toLowerCase().includes(searchTerm);
|
||||
|
||||
if (isRoot) {
|
||||
if (currentCategory !== 'all' && dirCategory !== currentCategory) return null;
|
||||
if (!searchTerm) return { ...item, children: filteredChildren };
|
||||
if (filteredChildren.length > 0 || nameMatch) return { ...item, children: filteredChildren };
|
||||
return null;
|
||||
}
|
||||
|
||||
if (nameMatch || filteredChildren.length > 0) {
|
||||
return { ...item, children: filteredChildren };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (item.type === 'file') {
|
||||
const category = getFileCategory(item.name, path);
|
||||
const temp = {
|
||||
...item,
|
||||
category,
|
||||
fullPath: `${path}${item.name}`,
|
||||
path: item.path || `${path}${item.name}`,
|
||||
};
|
||||
const matchesSearch = fileMatchesSearch(temp);
|
||||
const matchesCategory = currentCategory === 'all' || category === currentCategory;
|
||||
return matchesSearch && matchesCategory ? temp : null;
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
return filterItems(fileData, '', true);
|
||||
}
|
||||
|
||||
function renderTreeItems(items, level, path = '', expanded = false) {
|
||||
const frag = document.createDocumentFragment();
|
||||
|
||||
items.forEach((item, index) => {
|
||||
if (item.type === 'directory') {
|
||||
const hasChildren = Array.isArray(item.children) && item.children.length > 0;
|
||||
const treeItem = el('div', { class: `tree-item${expanded ? ' expanded' : ''}` });
|
||||
treeItem.style.animationDelay = `${index * 0.05}s`;
|
||||
|
||||
const header = el('div', { class: 'tree-header' }, [
|
||||
el('div', { class: 'tree-icon folder-icon' }, ['📁']),
|
||||
el('div', { class: 'tree-name' }, [item.name]),
|
||||
]);
|
||||
|
||||
if (hasChildren) {
|
||||
header.appendChild(el('div', { class: 'tree-chevron' }, ['▶']));
|
||||
}
|
||||
|
||||
tracker.trackEventListener(header, 'click', (e) => {
|
||||
e.stopPropagation();
|
||||
treeItem.classList.toggle('expanded');
|
||||
});
|
||||
|
||||
treeItem.appendChild(header);
|
||||
|
||||
if (hasChildren) {
|
||||
const children = el('div', { class: 'tree-children' });
|
||||
children.appendChild(renderTreeItems(item.children, level + 1, `${path}${item.name}/`, expanded));
|
||||
treeItem.appendChild(children);
|
||||
}
|
||||
|
||||
frag.appendChild(treeItem);
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.type === 'file') {
|
||||
const category = getFileCategory(item.name, path);
|
||||
frag.appendChild(renderFileItem({
|
||||
...item,
|
||||
category,
|
||||
fullPath: `${path}${item.name}`,
|
||||
path: item.path || `${path}${item.name}`,
|
||||
}, category, index, false));
|
||||
}
|
||||
});
|
||||
|
||||
return frag;
|
||||
}
|
||||
|
||||
function renderListView(container) {
|
||||
updateBadgesFromFiltered();
|
||||
|
||||
let filtered = allFiles.filter((f) => fileMatchesSearch(f) && (currentCategory === 'all' || f.category === currentCategory));
|
||||
|
||||
filtered.sort((a, b) => {
|
||||
let res = 0;
|
||||
switch (currentSort) {
|
||||
case 'type':
|
||||
res = a.category.localeCompare(b.category) || a.name.localeCompare(b.name);
|
||||
break;
|
||||
case 'date':
|
||||
res = fileTimestamp(a) - fileTimestamp(b);
|
||||
break;
|
||||
case 'name':
|
||||
default:
|
||||
res = String(a.name || '').localeCompare(String(b.name || ''));
|
||||
break;
|
||||
}
|
||||
return sortDirection === 'desc' ? -res : res;
|
||||
});
|
||||
|
||||
empty(container);
|
||||
|
||||
if (!filtered.length) {
|
||||
container.appendChild(noResults('🔍', t('common.noData')));
|
||||
return;
|
||||
}
|
||||
|
||||
const list = el('div', { class: 'list-view active' });
|
||||
filtered.forEach((file, index) => {
|
||||
list.appendChild(renderFileItem(file, file.category, index, true));
|
||||
});
|
||||
container.appendChild(list);
|
||||
}
|
||||
|
||||
function fileTimestamp(file) {
|
||||
const candidates = [
|
||||
file?.modified,
|
||||
file?.modified_at,
|
||||
file?.date,
|
||||
file?.mtime,
|
||||
file?.created_at,
|
||||
];
|
||||
for (const v of candidates) {
|
||||
if (v == null || v === '') continue;
|
||||
if (typeof v === 'number' && Number.isFinite(v)) return v;
|
||||
const ts = Date.parse(String(v));
|
||||
if (Number.isFinite(ts)) return ts;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function renderFileItem(file, category, index = 0, showPath = false) {
|
||||
const path = file.path || file.fullPath || file.name;
|
||||
const item = el('div', { class: 'file-item', 'data-path': path });
|
||||
item.style.animationDelay = `${index * 0.02}s`;
|
||||
|
||||
tracker.trackEventListener(item, 'click', () => {
|
||||
downloadFile(path);
|
||||
});
|
||||
|
||||
const icon = el('div', { class: `file-icon ${category}` }, [FILE_ICONS[category] || FILE_ICONS.other]);
|
||||
const name = el('div', { class: 'file-name' }, [String(file.name || '')]);
|
||||
|
||||
if (showPath) {
|
||||
name.appendChild(el('span', { style: 'color:var(--_muted);font-size:0.75rem' }, [` — ${file.fullPath || path}`]));
|
||||
}
|
||||
|
||||
const type = el('span', { class: `file-type ${category}` }, [String(category || 'other')]);
|
||||
item.append(icon, name, type);
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
function downloadFile(path) {
|
||||
window.location.href = `/loot_download?path=${encodeURIComponent(path)}`;
|
||||
}
|
||||
|
||||
function noResults(icon, message) {
|
||||
return el('div', { class: 'no-results' }, [
|
||||
el('div', { class: 'no-results-icon' }, [icon]),
|
||||
String(message || t('common.noData')),
|
||||
]);
|
||||
}
|
||||
Reference in New Issue
Block a user