APK wrapper registers platform=android via AETHERFORGE_PLATFORM; fleet UI shows robot icons, Android Access Depth probes, and a shortened mining onion timeline.
150 lines
4.7 KiB
TypeScript
150 lines
4.7 KiB
TypeScript
import { DEFAULT_LOTL_ONION_TIERS, LOTL_ONION_TIER_DOCS } from './lotlOnionTiers';
|
|
import type { AtlasSkipView } from './accessDepth';
|
|
import { ANDROID_DESKTOP_SKIPPED_TIER, ANDROID_MINING_TIER_ORDER } from './accessDepth';
|
|
import type { Agent } from '../types';
|
|
import { isAndroidPlatform } from './platform';
|
|
import { formatLotlTierLabel, type TierAttempt } from '../types/lotl';
|
|
|
|
/** Per-tier state for the live onion timeline UI. */
|
|
export type LotlTimelineTierState = 'pending' | 'trying' | 'success' | 'failed' | 'skipped' | 'skipped_by_atlas';
|
|
|
|
export interface LotlTimelineTierRow {
|
|
index: number;
|
|
tier: string;
|
|
label: string;
|
|
hint: string;
|
|
state: LotlTimelineTierState;
|
|
attempt?: TierAttempt;
|
|
}
|
|
|
|
export interface LotlTimelineModel {
|
|
tiers: LotlTimelineTierRow[];
|
|
total: number;
|
|
succeeded: number;
|
|
activeTier?: string;
|
|
tryingTier?: string;
|
|
}
|
|
|
|
/** Map attempt tier ids to spread onion tier ids (case-insensitive). */
|
|
const TIER_ALIASES: Record<string, string> = {
|
|
container: 'docker',
|
|
docker_load: 'docker',
|
|
vuln_probe: 'vuln_recon',
|
|
kev_scan: 'vuln_recon',
|
|
service_probe: 'vuln_recon',
|
|
ps_memory: 'powershell',
|
|
ps_inmemory: 'powershell',
|
|
bits: 'bits_curl',
|
|
curl: 'bits_curl',
|
|
wsus: 'wsus_cache_peer',
|
|
wsus_cache: 'wsus_cache_peer',
|
|
webrtc: 'webrtc_mesh',
|
|
mesh: 'webrtc_mesh',
|
|
exe_subprocess: 'powershell',
|
|
cpu_inprocess: 'dotnet',
|
|
gpu_subprocess: 'dotnet',
|
|
stratum_direct: 'dotnet',
|
|
};
|
|
|
|
function normalizeTierKey(tier: string): string {
|
|
return tier.trim().toLowerCase().replace(/[\s-]+/g, '_');
|
|
}
|
|
|
|
function canonicalSpreadTier(tier: string): string {
|
|
const key = normalizeTierKey(tier);
|
|
return TIER_ALIASES[key] ?? key;
|
|
}
|
|
|
|
function tierDocHint(tier: string): string {
|
|
const key = canonicalSpreadTier(tier) as (typeof DEFAULT_LOTL_ONION_TIERS)[number];
|
|
return LOTL_ONION_TIER_DOCS.find((d) => d.id === key)?.hint ?? '';
|
|
}
|
|
|
|
function tierLabel(tier: string): string {
|
|
if (tier === ANDROID_DESKTOP_SKIPPED_TIER) return 'Desktop tiers skipped';
|
|
const key = canonicalSpreadTier(tier) as (typeof DEFAULT_LOTL_ONION_TIERS)[number];
|
|
return LOTL_ONION_TIER_DOCS.find((d) => d.id === key)?.label ?? formatLotlTierLabel(tier);
|
|
}
|
|
|
|
function lastAttemptForTier(attempts: TierAttempt[], spreadTier: string): TierAttempt | undefined {
|
|
const target = canonicalSpreadTier(spreadTier);
|
|
for (let i = attempts.length - 1; i >= 0; i--) {
|
|
if (canonicalSpreadTier(attempts[i].tier) === target) return attempts[i];
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
export function resolveLotlTierOrder(policyTiers?: string[], agent?: Agent): string[] {
|
|
if (agent && isAndroidPlatform(agent.platform)) {
|
|
return [...ANDROID_MINING_TIER_ORDER, ANDROID_DESKTOP_SKIPPED_TIER];
|
|
}
|
|
if (policyTiers?.length) return [...policyTiers];
|
|
return [...DEFAULT_LOTL_ONION_TIERS];
|
|
}
|
|
|
|
export function buildLotlTimelineModel(
|
|
agent: Agent,
|
|
order: string[],
|
|
attempts: TierAttempt[],
|
|
skipped: string[] = [],
|
|
atlasSkips: AtlasSkipView[] = [],
|
|
): LotlTimelineModel {
|
|
const effectiveOrder = isAndroidPlatform(agent.platform)
|
|
? [...ANDROID_MINING_TIER_ORDER, ANDROID_DESKTOP_SKIPPED_TIER]
|
|
: order;
|
|
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';
|
|
|
|
let tryingTier: string | undefined;
|
|
if (online && activeCanon) {
|
|
const last = lastAttemptForTier(attempts, activeCanon);
|
|
if (!last?.ok) tryingTier = activeCanon;
|
|
}
|
|
|
|
const tiers: LotlTimelineTierRow[] = effectiveOrder.map((tier, i) => {
|
|
const key = canonicalSpreadTier(tier);
|
|
const attempt = lastAttemptForTier(attempts, tier);
|
|
let state: LotlTimelineTierState = 'pending';
|
|
|
|
if (tier === ANDROID_DESKTOP_SKIPPED_TIER) {
|
|
state = 'skipped';
|
|
} else 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';
|
|
} else if (attempt?.ok) {
|
|
state = 'success';
|
|
} else if (attempt && !attempt.ok) {
|
|
state = 'failed';
|
|
}
|
|
|
|
return {
|
|
index: i + 1,
|
|
tier,
|
|
label: tierLabel(tier),
|
|
hint: tierDocHint(tier),
|
|
state,
|
|
attempt,
|
|
};
|
|
});
|
|
|
|
const succeeded = tiers.filter((t) => t.state === 'success').length;
|
|
|
|
return {
|
|
tiers,
|
|
total: effectiveOrder.length,
|
|
succeeded,
|
|
activeTier,
|
|
tryingTier,
|
|
};
|
|
}
|
|
|
|
export function lotlTimelineProgressLabel(model: LotlTimelineModel): string {
|
|
return `${model.succeeded}/${model.total}`;
|
|
}
|