Files
Bjorn/web/js/pages/loot.js
Fabien POLLY eb20b168a6 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.
2026-02-18 22:36:10 +01:00

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')),
]);
}