| import crypto from "node:crypto"; |
| import { execSync } from "node:child_process"; |
| import { createHash } from "node:crypto"; |
| import { readFileSync, statSync, readdirSync, existsSync } from "node:fs"; |
| import { join, relative, extname, basename } from "node:path"; |
| import { hostname, platform, arch, cpus, totalmem, freemem } from "node:os"; |
|
|
| export interface MachineGraphNode { |
| id: string; |
| type: "file" | "process" | "port" | "project" | "artifact" | "service"; |
| properties: Record<string, unknown>; |
| } |
|
|
| export interface MachineGraph { |
| hostId: string; |
| timestamp: string; |
| nodes: MachineGraphNode[]; |
| } |
|
|
| export interface CorpusEntry { |
| resource_id: string; |
| path: string; |
| name: string; |
| extension: string; |
| hash: string; |
| size: number; |
| modified: string; |
| project?: string; |
| semanticTags: string[]; |
| sensitivity: number; |
| } |
|
|
| export interface OperatingState { |
| hostname: string; |
| platform: string; |
| arch: string; |
| cpuCount: number; |
| totalMemoryMB: number; |
| freeMemoryMB: number; |
| processes: ProcessInfo[]; |
| listeningPorts: PortInfo[]; |
| uptime: number; |
| } |
|
|
| export interface ProcessInfo { |
| pid: number; |
| name: string; |
| command: string; |
| listeningPort?: number; |
| } |
|
|
| export interface PortInfo { |
| port: number; |
| protocol: string; |
| address: string; |
| state: string; |
| } |
|
|
| export interface PolicyEntry { |
| principal: string; |
| action: string; |
| resource: string; |
| conditions: Record<string, unknown>; |
| decision: "permit" | "forbid"; |
| } |
|
|
| export interface Receipt { |
| id: string; |
| timestamp: string; |
| principal: string; |
| action: string; |
| resource: string; |
| resourceId: string; |
| decision: "permit" | "deny"; |
| result: "success" | "failure" | "denied"; |
| contentHash?: string; |
| disclosureTier?: number; |
| details: string; |
| signature: string; |
| } |
|
|
| export interface DisclosureLayer { |
| tier: number; |
| name: string; |
| description: string; |
| keyId: string; |
| encryptedContent: string; |
| iv: string; |
| keyLifetimeMs: number; |
| expiresAt: string; |
| revoked: boolean; |
| } |
|
|
| export interface Mailbox { |
| id: string; |
| hostId: string; |
| recipientPublicKey: string; |
| resourceId: string; |
| contentHash: string; |
| operations: string[]; |
| issuedAt: string; |
| expiresAt: string; |
| decayPolicy: string; |
| layers: DisclosureLayer[]; |
| claimed: boolean; |
| claimChallenge?: string; |
| attendanceLog: AttendanceRecord[]; |
| } |
|
|
| export interface AttendanceRecord { |
| timestamp: string; |
| operation: string; |
| tier: number; |
| result: "success" | "denied" | "expired"; |
| receiptId: string; |
| } |
|
|
| export interface HostModel { |
| hostId: string; |
| hostname: string; |
| graph: MachineGraph; |
| corpus: CorpusEntry[]; |
| state: OperatingState; |
| policies: PolicyEntry[]; |
| receipts: Receipt[]; |
| mailboxes: Map<string, Mailbox>; |
| keyPair: { publicKey: string; privateKey: string }; |
| } |
|
|
| const SENSITIVITY_BY_EXT: Record<string, number> = { |
| ".pem": 5, ".key": 5, ".env": 4, ".secret": 5, |
| ".p12": 5, ".pfx": 5, ".keystore": 5, |
| ".ts": 1, ".js": 1, ".py": 1, ".rs": 1, |
| ".md": 1, ".json": 2, ".yaml": 2, ".yml": 2, |
| ".sql": 3, ".db": 3, ".sqlite": 3, |
| }; |
|
|
| function hashContent(content: Buffer | string): string { |
| return createHash("sha256").update(content).digest("hex"); |
| } |
|
|
| function generateHostId(): string { |
| const raw = `${hostname()}-${platform()}-${arch()}-${cpus().length}`; |
| return `host_${hashContent(raw).substring(0, 12)}`; |
| } |
|
|
| function generateKeyPair(): { publicKey: string; privateKey: string } { |
| const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519"); |
| return { |
| publicKey: publicKey.export({ type: "spki", format: "pem" }).toString(), |
| privateKey: privateKey.export({ type: "pkcs8", format: "pem" }).toString(), |
| }; |
| } |
|
|
| export function signData(privateKey: string, data: string): string { |
| const key = crypto.createPrivateKey(privateKey); |
| return crypto.sign(null, Buffer.from(data), key).toString("base64url"); |
| } |
|
|
| export function verifySignature(publicKey: string, data: string, signature: string): boolean { |
| try { |
| const key = crypto.createPublicKey(publicKey); |
| return crypto.verify(null, Buffer.from(data), key, Buffer.from(signature, "base64url")); |
| } catch { |
| return false; |
| } |
| } |
|
|
| function encryptLayer(plaintext: string, tierKey: Buffer): { content: string; iv: string } { |
| const iv = crypto.randomBytes(12); |
| const cipher = crypto.createCipheriv("aes-256-gcm", tierKey, iv); |
| const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]); |
| const tag = cipher.getAuthTag(); |
| return { |
| content: Buffer.concat([encrypted, tag]).toString("base64"), |
| iv: iv.toString("base64"), |
| }; |
| } |
|
|
| export function decryptLayer(encryptedContent: string, iv: string, tierKey: Buffer): string { |
| const data = Buffer.from(encryptedContent, "base64"); |
| const tag = data.subarray(data.length - 16); |
| const ciphertext = data.subarray(0, data.length - 16); |
| const decipher = crypto.createDecipheriv("aes-256-gcm", tierKey, Buffer.from(iv, "base64")); |
| decipher.setAuthTag(tag); |
| return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8"); |
| } |
|
|
| function deriveTierKey(masterKey: Buffer, tier: number): Buffer { |
| return crypto.createHash("sha256").update(masterKey).update(Buffer.from([tier])).digest(); |
| } |
|
|
| function scanDirectory(dir: string, maxDepth: number, currentDepth: number = 0): CorpusEntry[] { |
| if (currentDepth >= maxDepth || !existsSync(dir)) return []; |
|
|
| const entries: CorpusEntry[] = []; |
|
|
| try { |
| const items = readdirSync(dir, { withFileTypes: true }); |
|
|
| for (const item of items) { |
| if (item.name.startsWith(".") || item.name === "node_modules" || item.name === ".git") continue; |
|
|
| const fullPath = join(dir, item.name); |
|
|
| if (item.isDirectory()) { |
| entries.push(...scanDirectory(fullPath, maxDepth, currentDepth + 1)); |
| } else if (item.isFile()) { |
| try { |
| const stat = statSync(fullPath); |
| const content = readFileSync(fullPath); |
| const hash = hashContent(content); |
| const ext = extname(item.name); |
| const sensitivity = SENSITIVITY_BY_EXT[ext] ?? 1; |
|
|
| entries.push({ |
| resource_id: `filecap://${generateHostId()}/object_${hash.substring(0, 16)}/version_${hash.substring(0, 8)}`, |
| path: fullPath, |
| name: item.name, |
| extension: ext, |
| hash, |
| size: stat.size, |
| modified: stat.mtime.toISOString(), |
| semanticTags: deriveSemanticTags(item.name, ext, fullPath), |
| sensitivity, |
| }); |
| } catch { |
| |
| } |
| } |
| } |
| } catch { |
| |
| } |
|
|
| return entries; |
| } |
|
|
| function deriveSemanticTags(name: string, ext: string, path: string): string[] { |
| const tags: string[] = []; |
| const lower = name.toLowerCase(); |
|
|
| if (ext === ".ts" || ext === ".js") tags.push("code"); |
| if (ext === ".md") tags.push("documentation"); |
| if (ext === ".json") tags.push("config"); |
| if (ext === ".py") tags.push("code", "python"); |
| if (ext === ".rs") tags.push("code", "rust"); |
| if (lower.includes("test")) tags.push("test"); |
| if (lower.includes("spec")) tags.push("spec"); |
| if (lower.includes("proof")) tags.push("proof"); |
| if (lower.includes("key") || lower.includes("secret")) tags.push("sensitive"); |
| if (path.includes("src/")) tags.push("source"); |
| if (path.includes("docs/")) tags.push("docs"); |
| if (path.includes("test")) tags.push("test"); |
|
|
| return tags; |
| } |
|
|
| function getRunningProcesses(): ProcessInfo[] { |
| const processes: ProcessInfo[] = []; |
|
|
| try { |
| if (platform() === "darwin") { |
| const output = execSync("ps aux | head -50", { encoding: "utf8", timeout: 3000 }); |
| const lines = output.split("\n").slice(1); |
|
|
| for (const line of lines) { |
| const parts = line.trim().split(/\s+/); |
| if (parts.length >= 11) { |
| const pid = parseInt(parts[1], 10); |
| const command = parts.slice(10).join(" "); |
| const name = parts[10] ?? "unknown"; |
|
|
| if (!isNaN(pid) && pid > 0) { |
| processes.push({ pid, name, command }); |
| } |
| } |
| } |
| } else { |
| const output = execSync("ps aux | head -50", { encoding: "utf8", timeout: 3000 }); |
| const lines = output.split("\n").slice(1); |
|
|
| for (const line of lines) { |
| const parts = line.trim().split(/\s+/); |
| if (parts.length >= 11) { |
| const pid = parseInt(parts[1], 10); |
| const command = parts.slice(10).join(" "); |
| const name = parts[10] ?? "unknown"; |
|
|
| if (!isNaN(pid) && pid > 0) { |
| processes.push({ pid, name, command }); |
| } |
| } |
| } |
| } |
| } catch { |
| |
| } |
|
|
| return processes.slice(0, 30); |
| } |
|
|
| function getListeningPorts(): PortInfo[] { |
| const ports: PortInfo[] = []; |
|
|
| try { |
| const output = execSync("lsof -i -P -n 2>/dev/null | grep LISTEN | head -20", { |
| encoding: "utf8", |
| timeout: 3000, |
| }); |
|
|
| for (const line of output.split("\n")) { |
| const parts = line.trim().split(/\s+/); |
| if (parts.length >= 9) { |
| const addr = parts[8] ?? ""; |
| const portMatch = addr.match(/:(\d+)$/); |
| if (portMatch) { |
| ports.push({ |
| port: parseInt(portMatch[1], 10), |
| protocol: parts[7] ?? "tcp", |
| address: addr.split(":")[0] ?? "*", |
| state: "LISTEN", |
| }); |
| } |
| } |
| } |
| } catch { |
| |
| } |
|
|
| return ports; |
| } |
|
|
| export function buildHostModel(scanRoots: string[]): HostModel { |
| const hostId = generateHostId(); |
| const keyPair = generateKeyPair(); |
|
|
| const corpus: CorpusEntry[] = []; |
| for (const root of scanRoots) { |
| corpus.push(...scanDirectory(root, 4)); |
| } |
|
|
| const state: OperatingState = { |
| hostname: hostname(), |
| platform: platform(), |
| arch: arch(), |
| cpuCount: cpus().length, |
| totalMemoryMB: Math.round(totalmem() / 1024 / 1024), |
| freeMemoryMB: Math.round(freemem() / 1024 / 1024), |
| processes: getRunningProcesses(), |
| listeningPorts: getListeningPorts(), |
| uptime: process.uptime(), |
| }; |
|
|
| const graphNodes: MachineGraphNode[] = []; |
|
|
| for (const entry of corpus) { |
| graphNodes.push({ |
| id: entry.resource_id, |
| type: "file", |
| properties: { |
| name: entry.name, |
| hash: entry.hash, |
| extension: entry.extension, |
| size: entry.size, |
| modified: entry.modified, |
| semanticTags: entry.semanticTags, |
| sensitivity: entry.sensitivity, |
| project: entry.project, |
| }, |
| }); |
| } |
|
|
| for (const proc of state.processes) { |
| graphNodes.push({ |
| id: `proc:${proc.pid}`, |
| type: "process", |
| properties: { |
| pid: proc.pid, |
| name: proc.name, |
| command: proc.command, |
| listeningPort: proc.listeningPort, |
| }, |
| }); |
| } |
|
|
| for (const port of state.listeningPorts) { |
| graphNodes.push({ |
| id: `port:${port.port}`, |
| type: "port", |
| properties: { |
| port: port.port, |
| protocol: port.protocol, |
| address: port.address, |
| state: port.state, |
| }, |
| }); |
| } |
|
|
| const defaultPolicies: PolicyEntry[] = [ |
| { |
| principal: "*", |
| action: "read", |
| resource: "file[sensitivity<=2]", |
| conditions: {}, |
| decision: "permit", |
| }, |
| { |
| principal: "*", |
| action: "read", |
| resource: "file[sensitivity>=4]", |
| conditions: {}, |
| decision: "forbid", |
| }, |
| { |
| principal: "*", |
| action: "search", |
| resource: "*", |
| conditions: {}, |
| decision: "permit", |
| }, |
| { |
| principal: "*", |
| action: "describe", |
| resource: "*", |
| conditions: {}, |
| decision: "permit", |
| }, |
| { |
| principal: "*", |
| action: "verify", |
| resource: "*", |
| conditions: {}, |
| decision: "permit", |
| }, |
| { |
| principal: "*", |
| action: "write", |
| resource: "*", |
| conditions: {}, |
| decision: "forbid", |
| }, |
| ]; |
|
|
| return { |
| hostId, |
| hostname: hostname(), |
| graph: { |
| hostId, |
| timestamp: new Date().toISOString(), |
| nodes: graphNodes, |
| }, |
| corpus, |
| state, |
| policies: defaultPolicies, |
| receipts: [], |
| mailboxes: new Map(), |
| keyPair, |
| }; |
| } |
|
|
| export function createMailbox( |
| model: HostModel, |
| recipientPublicKey: string, |
| resourcePath: string, |
| operations: string[], |
| decayPolicy: string = "full→extract→summary→receipt" |
| ): Mailbox { |
| const hostId = model.hostId; |
| const mailboxId = `mbx_${crypto.randomUUID().substring(0, 12)}`; |
|
|
| let content: string; |
| let contentHash: string; |
| let resourceId: string; |
|
|
| try { |
| const raw = readFileSync(resourcePath); |
| content = raw.toString("utf8"); |
| contentHash = hashContent(raw); |
| resourceId = `filecap://${hostId}/object_${contentHash.substring(0, 16)}/version_${contentHash.substring(0, 8)}`; |
| } catch { |
| content = ""; |
| contentHash = hashContent(resourcePath); |
| resourceId = `filecap://${hostId}/object_${contentHash.substring(0, 16)}`; |
| } |
|
|
| const masterKey = crypto.randomBytes(32); |
| const now = Date.now(); |
|
|
| const tiers = [ |
| { tier: 0, name: "full", desc: "Full file content", lifetimeMs: 15 * 60 * 1000 }, |
| { tier: 1, name: "extract", desc: "Selected exact passages", lifetimeMs: 60 * 60 * 1000 }, |
| { tier: 2, name: "structured", desc: "Structured extraction", lifetimeMs: 6 * 60 * 60 * 1000 }, |
| { tier: 3, name: "summary", desc: "Semantic summary and embeddings", lifetimeMs: 7 * 24 * 60 * 60 * 1000 }, |
| { tier: 4, name: "receipt", desc: "Hash, provenance and access receipt", lifetimeMs: Infinity }, |
| ]; |
|
|
| const layers: DisclosureLayer[] = tiers.map((t) => { |
| const tierKey = deriveTierKey(masterKey, t.tier); |
| let plaintext: string; |
|
|
| switch (t.tier) { |
| case 0: |
| plaintext = content; |
| break; |
| case 1: |
| plaintext = content.split("\n").slice(0, 50).join("\n"); |
| break; |
| case 2: |
| plaintext = JSON.stringify({ |
| name: basename(resourcePath), |
| size: content.length, |
| hash: contentHash, |
| firstLines: content.split("\n").slice(0, 10), |
| lineCount: content.split("\n").length, |
| }, null, 2); |
| break; |
| case 3: |
| plaintext = JSON.stringify({ |
| name: basename(resourcePath), |
| hash: contentHash, |
| size: content.length, |
| summary: `File contains ${content.split("\n").length} lines, ${content.length} bytes`, |
| tags: deriveSemanticTags(basename(resourcePath), extname(resourcePath), resourcePath), |
| }); |
| break; |
| case 4: |
| plaintext = JSON.stringify({ |
| resourceId, |
| hash: contentHash, |
| hostId, |
| mailboxId, |
| provenance: "HostModel receipt layer", |
| }); |
| break; |
| default: |
| plaintext = ""; |
| } |
|
|
| const encrypted = encryptLayer(plaintext, tierKey); |
| const expiresAt = t.lifetimeMs === Infinity ? "permanent" : new Date(now + t.lifetimeMs).toISOString(); |
|
|
| return { |
| tier: t.tier, |
| name: t.name, |
| description: t.desc, |
| keyId: `key_${mailboxId}_t${t.tier}`, |
| encryptedContent: encrypted.content, |
| iv: encrypted.iv, |
| keyLifetimeMs: t.lifetimeMs, |
| expiresAt, |
| revoked: false, |
| }; |
| }); |
|
|
| const claimChallenge = crypto.randomBytes(16).toString("base64url"); |
|
|
| const mailbox: Mailbox = { |
| id: mailboxId, |
| hostId, |
| recipientPublicKey, |
| resourceId, |
| contentHash, |
| operations, |
| issuedAt: new Date(now).toISOString(), |
| expiresAt: new Date(now + 24 * 60 * 60 * 1000).toISOString(), |
| decayPolicy, |
| layers, |
| claimed: false, |
| claimChallenge, |
| attendanceLog: [], |
| }; |
|
|
| model.mailboxes.set(mailboxId, mailbox); |
| return mailbox; |
| } |
|
|
| export function getCurrentDecayTier(mailbox: Mailbox): number { |
| const now = Date.now(); |
| const issued = new Date(mailbox.issuedAt).getTime(); |
|
|
| const tierLifetimes = [ |
| 15 * 60 * 1000, |
| 60 * 60 * 1000, |
| 6 * 60 * 60 * 1000, |
| 7 * 24 * 60 * 60 * 1000, |
| Infinity, |
| ]; |
|
|
| const elapsed = now - issued; |
|
|
| for (let i = 0; i < tierLifetimes.length; i++) { |
| if (elapsed < tierLifetimes[i]) { |
| if (mailbox.layers[i] && !mailbox.layers[i].revoked) { |
| return i; |
| } |
| } |
| } |
|
|
| return 4; |
| } |
|
|
| export function revokeExpiredLayers(mailbox: Mailbox): void { |
| const now = Date.now(); |
| const issued = new Date(mailbox.issuedAt).getTime(); |
|
|
| const tierLifetimes = [ |
| 15 * 60 * 1000, |
| 60 * 60 * 1000, |
| 6 * 60 * 60 * 1000, |
| 7 * 24 * 60 * 60 * 1000, |
| Infinity, |
| ]; |
|
|
| for (let i = 0; i < mailbox.layers.length; i++) { |
| const elapsed = now - issued; |
| if (elapsed >= tierLifetimes[i] && !mailbox.layers[i].revoked) { |
| mailbox.layers[i].revoked = true; |
| } |
| } |
| } |
|
|
| export function evaluatePolicy( |
| model: HostModel, |
| principal: string, |
| action: string, |
| resource: string, |
| context: Record<string, unknown> = {} |
| ): { decision: "permit" | "deny"; matchedPolicy?: PolicyEntry } { |
| let permitMatch: PolicyEntry | undefined; |
| let forbidMatch: PolicyEntry | undefined; |
|
|
| for (const policy of model.policies) { |
| if (policy.principal !== "*" && policy.principal !== principal) continue; |
| if (policy.action !== "*" && policy.action !== action) continue; |
|
|
| if (policy.resource === "*" || matchesResource(policy.resource, resource)) { |
| if (policy.decision === "permit" && !permitMatch) { |
| permitMatch = policy; |
| } |
| if (policy.decision === "forbid" && !forbidMatch) { |
| forbidMatch = policy; |
| } |
| } |
| } |
|
|
| if (forbidMatch) { |
| return { decision: "deny", matchedPolicy: forbidMatch }; |
| } |
| if (permitMatch) { |
| return { decision: "permit", matchedPolicy: permitMatch }; |
| } |
| return { decision: "deny" }; |
| } |
|
|
| function matchesResource(pattern: string, resource: string): boolean { |
| if (pattern === resource) return true; |
| if (pattern.includes("[") && pattern.includes("]")) { |
| const baseMatch = pattern.split("[")[0]; |
| if (resource.startsWith(baseMatch)) { |
| const condStr = pattern.substring(pattern.indexOf("[") + 1, pattern.lastIndexOf("]")); |
| const conditions = condStr.split("][").map((c) => c.replace(/[\[\]]/g, "")); |
|
|
| for (const cond of conditions) { |
| if (cond.includes("<=")) { |
| const [key, val] = cond.split("<="); |
| const resourceVal = extractResourceProperty(resource, key); |
| if (resourceVal !== undefined && resourceVal > parseInt(val, 10)) return false; |
| } else if (cond.includes(">=")) { |
| const [key, val] = cond.split(">="); |
| const resourceVal = extractResourceProperty(resource, key); |
| if (resourceVal !== undefined && resourceVal < parseInt(val, 10)) return false; |
| } else if (cond.includes("=")) { |
| const [key, val] = cond.split("="); |
| const resourceVal = extractResourceProperty(resource, key); |
| if (resourceVal !== undefined && String(resourceVal) !== val) return false; |
| } |
| } |
| return true; |
| } |
| } |
| return false; |
| } |
|
|
| function extractResourceProperty(resource: string, key: string): number | undefined { |
| if (key === "sensitivity") { |
| const match = resource.match(/sensitivity=(\d+)/); |
| return match ? parseInt(match[1], 10) : undefined; |
| } |
| return undefined; |
| } |
|
|
| export function createReceipt( |
| model: HostModel, |
| principal: string, |
| action: string, |
| resource: string, |
| resourceId: string, |
| decision: "permit" | "deny", |
| result: "success" | "failure" | "denied", |
| details: string, |
| contentHash?: string, |
| disclosureTier?: number |
| ): Receipt { |
| const id = `receipt_${crypto.randomUUID().substring(0, 12)}`; |
| const timestamp = new Date().toISOString(); |
|
|
| const receiptData = JSON.stringify({ |
| id, timestamp, principal, action, resource, resourceId, decision, result, details, contentHash, disclosureTier, |
| }); |
|
|
| const signature = signData(model.keyPair.privateKey, receiptData); |
|
|
| const receipt: Receipt = { |
| id, timestamp, principal, action, resource, resourceId, decision, result, |
| details, contentHash, disclosureTier, signature, |
| }; |
|
|
| model.receipts.push(receipt); |
| return receipt; |
| } |
|
|
| export function resolveMachineXPath(model: HostModel, selector: string): MachineGraphNode[] { |
| const results: MachineGraphNode[] = []; |
|
|
| const parsed = parseMXPath(selector); |
| if (!parsed) return results; |
|
|
| for (const node of model.graph.nodes) { |
| if (parsed.type && node.type !== parsed.type) continue; |
|
|
| let matches = true; |
| for (const [key, condition] of Object.entries(parsed.conditions)) { |
| const nodeValue = node.properties[key]; |
| if (nodeValue === undefined) { |
| matches = false; |
| break; |
| } |
|
|
| if (condition.op === "=" && String(nodeValue) !== condition.value) { |
| matches = false; |
| break; |
| } |
| if (condition.op === "<=" && Number(nodeValue) > Number(condition.value)) { |
| matches = false; |
| break; |
| } |
| if (condition.op === ">=" && Number(nodeValue) < Number(condition.value)) { |
| matches = false; |
| break; |
| } |
| if (condition.op === "<" && Number(nodeValue) >= Number(condition.value)) { |
| matches = false; |
| break; |
| } |
| if (condition.op === ">" && Number(nodeValue) <= Number(condition.value)) { |
| matches = false; |
| break; |
| } |
| if (condition.op === "contains" && !String(nodeValue).includes(condition.value)) { |
| matches = false; |
| break; |
| } |
| } |
|
|
| if (matches) { |
| results.push(node); |
| } |
| } |
|
|
| return results; |
| } |
|
|
| interface ParsedMXPath { |
| type?: string; |
| conditions: Record<string, { op: string; value: string }>; |
| } |
|
|
| function parseMXPath(selector: string): ParsedMXPath | null { |
| const cleaned = selector.replace(/^mx:\/\//, "").replace(/^\/+/, ""); |
|
|
| const parts = cleaned.split("/"); |
| const lastSegment = parts[parts.length - 1] || parts[0] || ""; |
|
|
| const typeMatch = lastSegment.match(/^(\w+)\[/); |
| const type = typeMatch ? typeMatch[1] : (lastSegment.match(/^(\w+)$/) ? lastSegment : undefined); |
|
|
| const conditions: Record<string, { op: string; value: string }> = {}; |
|
|
| const condMatches = lastSegment.matchAll(/\[([^\]]+)\]/g); |
| for (const match of condMatches) { |
| const cond = match[1]; |
| const operators = ["<=", ">=", "!=", "=", "<", ">"]; |
| let matched = false; |
|
|
| for (const op of operators) { |
| const idx = cond.indexOf(op); |
| if (idx > 0) { |
| const key = cond.substring(0, idx).trim(); |
| const value = cond.substring(idx + op.length).trim().replace(/^["']|["']$/g, ""); |
| conditions[key] = { op, value }; |
| matched = true; |
| break; |
| } |
| } |
|
|
| if (!matched) { |
| conditions[cond] = { op: "contains", value: cond }; |
| } |
| } |
|
|
| return { type, conditions }; |
| } |
|
|
| export function getHostSummary(model: HostModel): Record<string, unknown> { |
| return { |
| hostId: model.hostId, |
| hostname: model.hostname, |
| platform: model.state.platform, |
| arch: model.state.arch, |
| cpuCount: model.state.cpuCount, |
| totalMemoryMB: model.state.totalMemoryMB, |
| freeMemoryMB: model.state.freeMemoryMB, |
| corpusSize: model.corpus.length, |
| processCount: model.state.processes.length, |
| listeningPortCount: model.state.listeningPorts.length, |
| graphNodeCount: model.graph.nodes.length, |
| mailboxCount: model.mailboxes.size, |
| receiptCount: model.receipts.length, |
| publicKey: model.keyPair.publicKey, |
| }; |
| } |
|
|