PHALANX DIAGNOSTIC NAVIGATOR: SECURE FLEET TELEMETRY — DEEP ANALYSIS & DEBUGGING MANUAL

Date: June 25, 2026
Auditor: @zero_hunter
Classification: IRIS-STUDIO-INTERNAL
Target File: phalanx_diag_nav.rs
File Reference: Phalanx-Zero-Diag_Nav-20260625.md
Module Path: Standalone Binary Target (phalanx_diag_nav)
Platform: Cross-Platform (Interactive TUI for Terminal Environments)


1. Scope & Architectural Mandate

The Diagnostic Navigator (phalanx_diag_nav.rs) is a zero-footprint, high-performance TUI (Terminal User Interface) designed for administrators and operators to securely browse, search, and analyze massive diagnostic datasets collected from the Phalanx fleet.

1.1 Fleet Security & Decryption Pipeline

To prevent adversaries from intercepting sensitive host telemetry, all diagnostic logs generated by the Phalanx agents are encrypted in-transit and at-rest. The Navigator acts as the trusted viewer, decrypting these logs entirely in-memory using a Pre-Shared Key (PSK).

The decryption process follows a strict cryptographic pipeline:

  1. Key Derivation: The global PHALANX_PSK environment variable is combined with the specific target node_id using HMAC-SHA256 to derive a unique, node-specific 256-bit AES key.
  2. Authenticated Decryption: The derived key is used to decrypt the log payload using AES-256-GCM. The 12-byte initialization vector (nonce) is prefixed directly in the log file.
  3. Structured telemetry parsing: Once decrypted, the raw JSON payload is parsed into structured DiagnosticEvent objects and formatted for high-performance viewing and regex-based filtering.

1.2 Ghost Architecture Role

Within the "Ghost" operations system, the Navigator is the primary forensic visibility tool. It allows operators to inspect system snapshots, security alerts, and hardening logs without exposing the plaintext data to the disk of the monitoring workstation. It acts as a lightweight, dependencies-free diagnostic terminal that can be executed directly from external, encrypted media.


2. Dependency Analysis

The Navigator uses a curated set of terminal rendering and cryptographic crates:

Crate / API Version Purpose Defined In
ratatui 0.26 Terminal rendering layout, borders, lists, and widgets Cargo.toml:40
crossterm 0.27 Terminal raw mode management and keyboard/mouse event pump Cargo.toml:41
aes-gcm 0.10 Authenticated AES-256-GCM decryption Cargo.toml:27
serde / serde_json 1.0 Parsing structured diagnostic events from JSON Cargo.toml:17-18
chrono 0.4 Formatting epoch timestamps to human-readable dates Cargo.toml:23

3. Comprehensive Architecture & Walkthrough

3.1 TUI Architecture & Event Handling

[Diagram]

3.2 Diagnostic Decryption & Rendering Pipeline

[Diagram]

4. Security & Logic Auditing Findings

4.1 [CRITICAL] Arbitrary File Read via Symlink Follow (Information Disclosure)

Location: Lines 168-243

if let Some(node_index) = self.nodes_state.selected() 
    && let Some(file_index) = self.files_state.selected() {
        let base_node_id = &self.nodes[node_index];
        let file_name = &self.files[file_index];
        let file_path = self.base_dir.join(base_node_id).join(file_name);
        
        // ... metadata check ...
        match fs::read(file_path.clone()) { ... }

The Bug: The Navigator reads and previews selected files directly using fs::read(file_path) without checking if the file is a symbolic link.

If an adversary has write access to the diagnostics directory (e.g., on a shared log server or a compromised agent workstation), they can create a symbolic link pointing to a sensitive system file:

ln -s /etc/shadow diagnostics/NODE-01/shadow_leak.bin

When the operator runs the TUI (typically with administrative or root privileges to read system logs) and selects shadow_leak.bin, the program follows the symlink, reads the contents of /etc/shadow, fails to decrypt it, and falls back to displaying the raw plaintext contents directly in the TUI log preview.

This allows low-privileged users to read any file on the system that the TUI operator has access to.

Severity: CRITICAL

Mitigation: Verify that the target file is not a symbolic link before reading. Use fs::symlink_metadata to inspect the file type, and reject any file that is a symlink:

let metadata = fs::symlink_metadata(&file_path)?;
if metadata.file_type().is_symlink() {
    self.content = format!("Error: Symlink detected and rejected: {}", file_path.display());
    return;
}

4.2 [HIGH] Silent Decryption Bypass & False Sense of Security in Fleet Summary

Location: Lines 358-383

fn generate_fleet_summary(&mut self) {
    // ...
    if reader.read_to_string(&mut buffer).is_ok() 
        && (buffer.contains("ALERT") || buffer.contains("CRITICAL") || buffer.contains("ERROR")) {
            self.fleet_anomalies.push(...);
    }
    // ...
}

The Bug: The generate_fleet_summary function scans all diagnostic files in the base directory to identify anomalies. However, it reads the files as raw, un-decrypted text files.

Since all production diagnostic logs are encrypted with AES-256-GCM, their raw contents consist of binary ciphertext. The plaintext strings "ALERT", "CRITICAL", and "ERROR" will never appear in the ciphertext. Furthermore, trying to read binary ciphertext as a UTF-8 string using read_to_string will fail on invalid UTF-8 sequences, causing the file to be silently skipped.

As a result, the fleet summary will always fail to find anomalies in encrypted logs, and will constantly report Fleet status: NOMINAL. No critical anomalies detected. even if every node in the fleet is reporting critical breaches or failing. This creates a dangerous false sense of security for the operator.

Severity: HIGH

Mitigation: For each file scanned during the fleet summary, extract the node ID, derive the decryption key, and decrypt the first block of data in-memory before searching for anomaly patterns.


4.3 [HIGH] Memory Exhaustion DoS via Large Decrypted JSON Payloads

Location: Lines 245-287

fn format_structured_content(&mut self, decrypted_data: &[u8]) {
    match serde_json::from_slice::<Vec<DiagnosticEvent>>(decrypted_data) {
        Ok(events) => {
            // ...
            for event in events.iter().take(10000) { ... }
        }
    }
}

The Bug: Although the TUI limits file sizes to 100MB and restricts rendering to the first 10,000 events for UI performance, the JSON parser serde_json::from_slice still parses the entire decrypted array into a Vec<DiagnosticEvent> in memory.

A 99MB file containing millions of small diagnostic events will be successfully read and decrypted. However, parsing it into a massive vector of Rust structs with allocated String fields will cause a 10x to 20x memory expansion. The process will attempt to allocate 1 to 2 GB of RAM, causing an Out-Of-Memory (OOM) panic and crashing the TUI.

Severity: HIGH

Mitigation: Use a streaming JSON parser (serde_json::Deserializer::from_slice(...).into_iter()) to parse events sequentially, stopping immediately once the 10,000-event preview limit is reached. This bounds memory consumption to a small, constant size.


4.4 [MEDIUM] Severe Blocking I/O & UI Freeze in Fleet Summary Scan

Location: Lines 358-383

The Bug: When the operator presses f to enter Fleet View, the generate_fleet_summary function is called. This function runs synchronously on the main thread, performing nested filesystem traversals and opening and reading up to 1MB of data from every single file in the fleet directory.

On a large deployment with hundreds of nodes and thousands of historical log files, this synchronous file I/O will block the TUI event loop completely. The terminal will freeze, key inputs will be ignored, and the interface will appear to have crashed for several seconds or minutes while the disk is thrashed.

Severity: MEDIUM

Mitigation: Offload the fleet summary scanning to a background worker thread using std::thread::spawn, and update the TUI asynchronously once the results are compiled, showing a "Scanning fleet..." loading indicator in the meantime.


4.5 [MEDIUM] Corrupted Terminal State on Panic (Missing Panic Hook)

Location: Lines 443-459

The Bug: The TUI enables terminal raw mode and enters the alternate screen buffer during initialization. If a panic occurs anywhere in the application (e.g., OOM in the JSON parser, or index out of bounds in rendering), the program aborts immediately.

Because the program exits abruptly without running the cleanup code in main(), the terminal is left in a corrupted state: raw mode remains active (disabling keyboard input echo and carriage returns) and the alternate screen is not exited. The operator's terminal session becomes unusable, requiring them to blindly type the reset command to restore their shell.

Severity: MEDIUM

Mitigation: Register a custom panic hook at the very start of main() that catches panics, restores raw mode, exits the alternate screen, and then prints the panic backtrace:

let default_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
    let _ = crossterm::terminal::disable_raw_mode();
    let _ = crossterm::execute!(std::io::stdout(), crossterm::terminal::LeaveAlternateScreen, crossterm::event::DisableMouseCapture);
    default_hook(info);
}));

4.6 [LOW] Un-zeroized Plaintext PSK Retention in Memory

Location: Line 70 and Line 73

let psk = std::env::var("PHALANX_PSK").unwrap_or_default();

The Bug: The Pre-Shared Key (PSK) is loaded from the environment into self.psk as a standard, heap-allocated String. Unlike the core agent modules, the Navigator does not use the zeroize crate to clear this string from memory when the application exits or when the key is no longer needed. The plaintext key remains resident in the process heap, exposing it to memory-dump extraction.

Severity: LOW

Mitigation: Import the zeroize crate and implement ZeroizeOnDrop for the App structure, or zeroize the psk string buffer upon exit.


5. Debugging & Diagnostic Playbook

This section provides actionable workflows and diagnostic scripts to debug the Diagnostic Navigator under failure scenarios.

5.1 Diagnostic Workflow: Decryption Failures (Auth Mismatch)

If the TUI displays Decrypted: NO (Auth Failure - Node ID Mismatch) in the status bar, follow this diagnostic process:

[Diagram]

Step 1: Verify the Environment Variable

Ensure the PHALANX_PSK environment variable is set and matches the key used by the target node to encrypt the logs:

# On Linux/macOS
echo $PHALANX_PSK

Step 2: Audit Node ID Extraction

The Navigator derives the decryption key using the formula HMAC-SHA256(PSK, node_id). The node_id must match the ID used during encryption.


5.2 Diagnostic Script: Manual Log Decryption & Validation

Use this Python script to manually decrypt and validate a Phalanx diagnostic log file outside of the TUI, helping isolate key derivation or corruption issues:

import sys
import hmac
import hashlib
from cryptography.hazmat.primitives.ciphers.aead import AESGCM

def decrypt_log(psk_str, node_id, file_path):
    print(f"[*] Node ID: {node_id}")
    print(f"[*] File Path: {file_path}")
    
    # 1. Derive the key: HMAC-SHA256(PSK, node_id)
    psk = psk_str.encode('utf-8')
    derived_key = hmac.new(psk, node_id.encode('utf-8'), hashlib.sha256).digest()
    print(f"[+] Derived Key (hex): {derived_key.hex()}")
    
    # 2. Read the encrypted file
    with open(file_path, 'rb') as f:
        data = f.read()
        
    if len(data) < 28:
        print("[-] Error: File is too small to contain valid ciphertext!")
        return
        
    nonce = data[:12]
    ciphertext = data[12:]
    print(f"[+] Nonce (hex): {nonce.hex()}")
    print(f"[*] Ciphertext Length: {len(ciphertext)} bytes")
    
    # 3. Decrypt using AES-GCM
    try:
        aesgcm = AESGCM(derived_key)
        decrypted = aesgcm.decrypt(nonce, ciphertext, None)
        print("[+] Decryption Successful!")
        print("\n=== Decrypted Log Content ===")
        print(decrypted.decode('utf-8', errors='replace'))
    except Exception as e:
        print(f"[-] Decryption Failed: {e}")
        print("[-] Verification failed. Ensure the PSK and Node ID are correct.")

if __name__ == "__main__":
    if len(sys.argv) < 4:
        print("Usage: python decrypt_log.py <psk> <node_id> <path_to_bin>")
    else:
        decrypt_log(sys.argv[1], sys.argv[2], sys.argv[3])