Deploy Recon UI: consume new recon APIs with streaming results and action matrix
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled

This commit is contained in:
AetherForge
2026-06-07 12:20:27 -07:00
parent 6859d79552
commit 1fd3ec8618
5 changed files with 674 additions and 0 deletions

View File

@@ -0,0 +1,43 @@
/**
* @vitest-environment happy-dom
*/
import { afterEach, describe, expect, it, vi } from 'vitest';
import {
diffReconReports,
exportReconJson,
openPortsSignature,
saveReconHistoryEntry,
} from './reconHistory';
import type { ReconScanReport } from '../types/recon';
const base: ReconScanReport = {
host: '10.0.0.5',
scan_id: 's1',
scanned_at: '2026-06-07T10:00:00Z',
ports: [{ port: 80, open: true }],
};
describe('reconHistory', () => {
afterEach(() => localStorage.clear());
it('saves and signs open ports', () => {
const entry = saveReconHistoryEntry(base);
expect(entry.scan_id).toBe('s1');
expect(openPortsSignature(base)).toBe('80');
});
it('diffs port changes', () => {
const next = { ...base, ports: [{ port: 80, open: true }, { port: 443, open: true }] };
const diffs = diffReconReports(base, next);
expect(diffs.some((d) => d.kind === 'port_opened' && d.label.includes('443'))).toBe(true);
});
it('exports JSON without throwing', () => {
const click = vi.fn();
const create = vi.spyOn(document, 'createElement').mockReturnValue({ click } as unknown as HTMLAnchorElement);
exportReconJson(base, 'out.json');
expect(click).toHaveBeenCalled();
create.mockRestore();
});
});

View File

@@ -0,0 +1,131 @@
import type { ReconHistoryEntry, ReconScanReport } from '../types/recon';
const LOCAL_KEY = 'aetherforge.recon.history.v1';
const LOCAL_LIMIT = 64;
export type ReconReportDiffKind =
| 'port_opened'
| 'port_closed'
| 'ssrf_up'
| 'ssrf_down'
| 'cms_added';
export interface ReconReportDiff {
kind: ReconReportDiffKind;
label: string;
detail?: string;
}
function normalizeEntry(raw: ReconHistoryEntry & { id?: string }): ReconHistoryEntry {
const scan_id = raw.scan_id || raw.id || `${raw.host}-${raw.scanned_at}`;
return { ...raw, scan_id, report: raw.report };
}
function readLocalEntries(): ReconHistoryEntry[] {
try {
const raw = localStorage.getItem(LOCAL_KEY);
if (!raw) return [];
const parsed = JSON.parse(raw) as { entries?: (ReconHistoryEntry & { id?: string })[] };
if (!Array.isArray(parsed.entries)) return [];
return parsed.entries.map(normalizeEntry);
} catch {
return [];
}
}
function writeLocalEntries(entries: ReconHistoryEntry[]): void {
try {
localStorage.setItem(LOCAL_KEY, JSON.stringify({ entries: entries.slice(0, LOCAL_LIMIT) }));
} catch {
/* quota / private mode */
}
}
export function openPortsSignature(report: ReconScanReport): string {
return report.ports
.filter((p) => p.open)
.map((p) => p.port)
.sort((a, b) => a - b)
.join(',');
}
export async function loadReconHistory(host?: string): Promise<ReconHistoryEntry[]> {
const local = readLocalEntries();
if (!host?.trim()) {
return local;
}
const trimmed = host.trim();
try {
const res = await fetch(`/api/v1/recon/history?host=${encodeURIComponent(trimmed)}`, {
headers: { Accept: 'application/json' },
credentials: 'include',
});
if (res.ok) {
const body = (await res.json()) as { entries?: ReconHistoryEntry[] } | ReconHistoryEntry[];
const server = Array.isArray(body) ? body : body.entries ?? [];
if (server.length > 0) {
const byId = new Map<string, ReconHistoryEntry>();
for (const e of [...local, ...server.map(normalizeEntry)]) {
if (e.host.trim() === trimmed) byId.set(e.scan_id, e);
}
return [...byId.values()].sort((a, b) => b.scanned_at.localeCompare(a.scanned_at));
}
}
} catch {
/* offline */
}
return local.filter((e) => e.host.trim() === trimmed);
}
export function saveReconHistoryEntry(report: ReconScanReport): ReconHistoryEntry {
const scan_id = report.scan_id ?? `${report.host}-${report.scanned_at}`;
const entry: ReconHistoryEntry = {
scan_id,
host: report.host,
scanned_at: report.scanned_at,
profile: report.profile,
status: report.status,
report,
};
const prev = readLocalEntries().filter((e) => e.scan_id !== entry.scan_id);
writeLocalEntries([entry, ...prev]);
return entry;
}
export function exportReconJson(report: ReconScanReport, filename?: string): void {
const blob = new Blob([JSON.stringify(report, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename ?? `recon-${report.host.replace(/[^a-zA-Z0-9._-]+/g, '-')}.json`;
a.click();
URL.revokeObjectURL(url);
}
export function diffReconReports(prev: ReconScanReport, next: ReconScanReport): ReconReportDiff[] {
const diffs: ReconReportDiff[] = [];
const prevPorts = new Map(prev.ports.map((p) => [p.port, p.open]));
for (const p of next.ports) {
const was = prevPorts.get(p.port);
if (was !== true && p.open) {
diffs.push({ kind: 'port_opened', label: `TCP ${p.port} opened`, detail: 'Was closed on prior scan.' });
} else if (was === true && !p.open) {
diffs.push({ kind: 'port_closed', label: `TCP ${p.port} closed`, detail: 'Was open on prior scan.' });
}
}
const prevScore = prev.crawl?.ssrf_score ?? 0;
const nextScore = next.crawl?.ssrf_score ?? 0;
if (nextScore > prevScore) {
diffs.push({ kind: 'ssrf_up', label: 'SSRF score increased', detail: `${prevScore}${nextScore}` });
} else if (nextScore < prevScore) {
diffs.push({ kind: 'ssrf_down', label: 'SSRF score decreased', detail: `${prevScore}${nextScore}` });
}
const prevCms = new Set(prev.crawl?.cms_fingerprints ?? []);
for (const cms of next.crawl?.cms_fingerprints ?? []) {
if (!prevCms.has(cms)) {
diffs.push({ kind: 'cms_added', label: `CMS hint: ${cms}`, detail: 'New fingerprint vs prior scan.' });
}
}
return diffs;
}