mirror of
https://github.com/infinition/Bjorn.git
synced 2026-03-11 23:21:59 +00:00
- 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.
557 lines
16 KiB
JavaScript
557 lines
16 KiB
JavaScript
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')),
|
|
]);
|
|
}
|