From cc82ed9e527c07b8e83cadc2de4b951c2f1d48fc Mon Sep 17 00:00:00 2001 From: Zarithas Date: Mon, 22 Dec 2025 12:25:39 -0500 Subject: [PATCH] Add Software Design Document --- Software-Design-Document.md | 1299 +++++++++++++++++++++++++++++++++++ 1 file changed, 1299 insertions(+) create mode 100644 Software-Design-Document.md diff --git a/Software-Design-Document.md b/Software-Design-Document.md new file mode 100644 index 0000000..d54c862 --- /dev/null +++ b/Software-Design-Document.md @@ -0,0 +1,1299 @@ +# Loxide +## Software Design Document + +**Document Version:** 1.0 +**Date:** December 2025 +**Authors:** Brandon Wickline (Lead Python Developer), James Brotosky (Lead Rust Developer) +**Status:** Approved + +--- + +## Document Control + +### Revision History + +| Version | Date | Author | Description | +|---------|------|--------|-------------| +| 0.1 | Jul 2025 | B. Wickline | Initial architecture draft | +| 0.2 | Oct 2025 | J. Brotosky | Added Rust integration design | +| 0.3 | Oct 2025 | B. Wickline | Added security and configuration design | +| 1.0 | Dec 2025 | B. Wickline | First release candidate | + +### Document Approval + +| Role | Name | Signature | Date | +|------|------|-----------|------| +| Lead Developer | Brandon Wickline | | | +| Lead Developer | James Brotosky | | | +| Technical Reviewer | | | | + +--- + +## Table of Contents + +1. Introduction +2. System Overview +3. Architecture Design +4. Component Design +5. Data Design +6. Security Design +7. User Interface Design +8. External Interface Design +9. Distribution and Deployment +10. Error Handling +11. Phase 2: LEMON Integration +12. Appendices + +--- + +## 1. Introduction + +### 1.1 Purpose + +This Software Design Document (SDD) provides a comprehensive description of the design and architecture of Loxide, a terminal-based user interface application for Airlock endpoint security management. This document translates the requirements specified in the Software Requirements Specification (SRS) into a detailed design that can be implemented by the development team. + +### 1.2 Scope + +This document covers: + +- System architecture and component design +- Data models and storage design +- Security implementation details +- User interface design patterns +- External interface specifications +- Distribution and deployment design +- Error handling strategies + +### 1.3 Intended Audience + +This document is intended for: + +- Software developers implementing system components +- System architects reviewing design decisions +- Quality assurance engineers developing test plans +- Operations personnel preparing deployment procedures +- Technical stakeholders requiring design visibility + +### 1.4 Design Goals + +The design prioritizes the following goals in order of importance: + +1. **Security** - Protect credentials and sensitive operations +2. **Reliability** - Handle errors gracefully without data loss +3. **Usability** - Provide intuitive keyboard-driven interface +4. **Performance** - Respond quickly to user actions +5. **Maintainability** - Enable easy modification and extension + +### 1.5 References + +| Reference | Description | +|-----------|-------------| +| Loxide SRS | Software Requirements Specification | +| Textual Documentation | TUI framework reference | +| Airlock API Specification | External API documentation | +| Python Style Guide (PEP 8) | Code style reference | + +--- + +## 2. System Overview + +### 2.1 System Context + +Loxide operates as a client application connecting to an Airlock server for endpoint security management. The system provides a terminal-based interface enabling bulk operations that are cumbersome in the standard web console. + +### 2.2 Design Constraints + +The following constraints influenced the design: + +| Constraint | Impact on Design | +|------------|------------------| +| Terminal-based UI | All interaction via keyboard; no mouse-dependent features | +| Cross-platform (Windows/Linux) | Platform-specific code isolated; keyring abstraction | +| Standalone executable | Nuitka compilation; bundled dependencies | +| Private dependency | airlock_libs requires Gitea access | +| Secure credential storage | Platform keyring with encryption layer | + +### 2.3 System Decomposition + +The system is decomposed into the following major subsystems: + +``` ++------------------------------------------------------------------+ +| LOXIDE | +| | +| +------------------------+ +---------------------------+ | +| | User Interface | | Business Logic | | +| | | | | | +| | +------------------+ | | +---------------------+ | | +| | | Main App | | | | Policy Operations | | | +| | | (Loxide.py) | | | +---------------------+ | | +| | +------------------+ | | +---------------------+ | | +| | +------------------+ | | | Agent Operations | | | +| | | Screens | | | +---------------------+ | | +| | +------------------+ | | +---------------------+ | | +| | +------------------+ | | | OTP Operations | | | +| | | Widgets | | | +---------------------+ | | +| | +------------------+ | | +---------------------+ | | +| +------------------------+ | | Analysis Engine | | | +| | +---------------------+ | | +| +------------------------+ +---------------------------+ | +| | Data Access | | +| | | +---------------------------+ | +| | +------------------+ | | Infrastructure | | +| | | API Wrapper | | | | | +| | | (API.py) | | | +---------------------+ | | +| | +------------------+ | | | Configuration | | | +| | +------------------+ | | +---------------------+ | | +| | | Data Models | | | +---------------------+ | | +| | +------------------+ | | | Security | | | +| +------------------------+ | +---------------------+ | | +| | +---------------------+ | | +| +------------------------+ | | Logging | | | +| | External Libraries | | +---------------------+ | | +| | | +---------------------------+ | +| | +------------------+ | | +| | | airlock_libs | | | +| | | (Rust) | | | +| | +------------------+ | | +| +------------------------+ | ++------------------------------------------------------------------+ +``` + +--- + +## 3. Architecture Design + +### 3.1 Architectural Style + +Loxide employs a layered architecture with clear separation between: + +1. **Presentation Layer** - TUI screens and widgets +2. **Business Logic Layer** - Operations and workflows +3. **Data Access Layer** - API communication and data models +4. **Infrastructure Layer** - Configuration, security, logging + +### 3.2 Module Organization + +``` +loxide/ +├── Loxide.py # Main application entry point +├── models/ +│ ├── agent.py # Agent data model +│ ├── policy.py # Policy data model +│ └── execution.py # Execution event model +├── services/ +│ ├── API.py # Airlock API wrapper +│ └── security.py # Credential management +├── TUI/ +│ ├── Screens/ +│ │ ├── policyprepworkflowscreen.py +│ │ ├── quietagentworkflowscreen.py +│ │ ├── executionhistoryscreen.py +│ │ ├── otpactivityscreen.py +│ │ ├── otprevokescreen.py +│ │ ├── otpworkflowscreen.py +│ │ └── moveagentworkflowscreen.py +│ ├── Widgets/ +│ │ ├── multiagentselector.py +│ │ ├── policyselector.py +│ │ ├── policytreewidget.py +│ │ ├── agentmoveoperations.py +│ │ ├── serverlogwidget.py +│ │ ├── resultsdisplay.py +│ │ └── themeselector.py +│ └── Themes/ +│ ├── theme_amber_terminal.py +│ └── theme_retro_terminal.py +├── utils/ +│ ├── configmanager.py # Configuration management +│ ├── setup.py # Initialization and logging +│ └── utils.py # Utility functions +└── system_config.json # System configuration (bundled) +``` + +### 3.3 Component Interactions + +#### 3.3.1 Startup Sequence + +``` +1. main() called + │ + ├─> setup() initializes logging and configuration + │ ├─> load_system_config() reads system_config.json + │ ├─> load_user_config() reads/creates user_config.json + │ └─> configure logging based on LOG_LEVEL + │ + ├─> getAPI() retrieves credentials + │ ├─> check keyring for stored credential + │ ├─> if found: prompt for password, decrypt + │ └─> if not found: prompt for API key, encrypt, store + │ + ├─> AirlockAPIWrapper() created with base_url and api_key + │ + └─> LoxideApp().run() starts the TUI + ├─> compose() builds initial UI + └─> on_mount() loads initial data +``` + +#### 3.3.2 Screen Navigation Flow + +``` +MainScreen (Tabs) +│ +├── Dashboard Tab +│ └── Summary statistics and charts +│ +├── Agent Operations Tab +│ ├── MultiAgentSelector (widget) +│ └── AgentMoveOperations (widget) +│ ├── Move to Policy → PolicySelector (screen) +│ ├── Toggle Enforcement +│ ├── Generate OTP → OTPWorkflowScreen +│ └── View History → ExecutionHistoryScreen +│ +├── Policy Prep Tab +│ └── PolicyPrepWorkflowScreen +│ ├── Step 1: Select Policies +│ ├── Step 2: Configure +│ ├── Step 3: Fetch Data +│ ├── Step 4: Review Results +│ └── Step 5: Approve Hashes +│ +├── Quiet Agents Tab +│ └── QuietAgentWorkflowScreen +│ ├── Configure thresholds +│ ├── Run analysis (Rust) +│ └── Review results +│ +├── OTP Management Tab +│ ├── Active OTPs list +│ ├── OTPActivitiesScreen +│ └── OTPRevokeScreen +│ +└── Server Logs Tab + └── ServerLogWidget +``` + +### 3.4 Data Flow + +#### 3.4.1 Agent Selection and Operation + +``` +User Input (hostnames) + │ + ▼ +MultiAgentSelector + │ + ├── Parse input (split by newline/comma) + │ + ├── For each hostname: + │ └── API.agent_find_by_hostname() + │ + ├── Compile matches/unmatches + │ + ▼ +User Selection (checkboxes) + │ + ▼ +AgentMoveOperations + │ + ├── Move: API.agent_move() + ├── Toggle: API.policy_set_auditmode() + ├── OTP: API.otp_generate() + └── History: API.history_execution() + │ + ▼ +ResultsDisplay (success/failure counts) +``` + +#### 3.4.2 Policy Preparation Workflow + +``` +Policy Selection + │ + ▼ +Configuration (days, allowlists) + │ + ▼ +Data Fetch + │ + ├── For each policy: + │ └── API.policy_list_agents() + │ + ├── For each agent: + │ └── API.history_execution() + │ + ▼ +Categorization + │ + ├── Check against allowlists + ├── Check against baselines + ├── Apply VT_THREAT_TOLERANCE (using Airlock VT data) + ├── Apply BAD_PUBLISHERS filter + │ + ▼ +Results Display + │ + ├── Approved hashes + ├── Unapproved hashes (by category) + └── Blocked hashes + │ + ▼ +Hash Approval + │ + └── API.hash_add_to_allowlist() +``` + +--- + +## 4. Component Design + +### 4.1 Main Application (Loxide.py) + +#### 4.1.1 Class: LoxideApp + +```python +class LoxideApp(App): + """Main Textual application class.""" + + # Reactive properties + api: reactive[AirlockAPIWrapper] + current_tab: reactive[str] + + # Key bindings + BINDINGS = [ + ("q", "quit", "Quit"), + ("t", "toggle_theme", "Theme"), + ("r", "refresh", "Refresh"), + ("?", "help", "Help"), + ] + + def compose(self) -> ComposeResult: + """Build the main UI layout.""" + yield Header() + yield Tabs(...) + yield ContentSwitcher(...) + yield Footer() + + def on_mount(self) -> None: + """Initialize on application start.""" + self.load_initial_data() + + async def action_quit(self) -> None: + """Handle quit action with confirmation.""" + ... +``` + +#### 4.1.2 Responsibilities + +- Application lifecycle management +- Global keyboard shortcut handling +- Tab navigation coordination +- Theme management +- Error handling and notification display + +### 4.2 API Wrapper (API.py) + +#### 4.2.1 Class: AirlockAPIWrapper + +```python +class AirlockAPIWrapper: + """Wrapper for Airlock REST API communication.""" + + def __init__(self, base_url: str, api_key: str): + self.base_url = base_url.rstrip("/") + self.api_key = api_key + self.headers = {"X-APIKey": self.api_key} + + def _post(self, endpoint: str, payload: dict = None) -> dict: + """Send POST request and return JSON response.""" + ... + + # Agent operations + def agent_find_all(self) -> pd.DataFrame: ... + def agent_find_by_hostname(self, hostname: str) -> pd.DataFrame: ... + def agent_move(self, agentid: str, groupid: str) -> dict: ... + + # Policy operations + def policy_find_all(self) -> pd.DataFrame: ... + def policy_set_auditmode(self, groupid: str, auditmode: str) -> dict: ... + + # OTP operations + def otp_generate(self, agentid: str, duration: int, purpose: str) -> str: ... + def otp_find_active(self) -> pd.DataFrame: ... + def otp_revoke(self, otpid: str) -> dict: ... + + # Hash operations (includes VT data from Airlock) + def hash_query(self, hashes: List[str]) -> pd.DataFrame: ... + def hash_add_to_allowlist(self, applicationid: str, hashes: List[str]) -> dict: ... + + # History operations + def history_execution(self, today: str, date_selected: str, agent_name: str) -> List[Dict]: ... +``` + +#### 4.2.2 Error Handling + +All API methods implement consistent error handling: + +```python +def _post(self, endpoint: str, payload: dict = None) -> dict: + url = f"{self.base_url}{endpoint}" + try: + response = requests.post( + url, + headers=self.headers, + data=json.dumps(payload or {}), + verify=False, # Configurable for self-signed certs + timeout=30 + ) + response.raise_for_status() + return response.json() + except requests.exceptions.Timeout: + logger.error(f"Request timeout: {endpoint}") + raise APITimeoutError(f"Request to {endpoint} timed out") + except requests.exceptions.ConnectionError: + logger.error(f"Connection error: {endpoint}") + raise APIConnectionError(f"Cannot connect to {self.base_url}") + except requests.exceptions.HTTPError as e: + logger.error(f"HTTP error {e.response.status_code}: {endpoint}") + raise APIError(f"API error: {e.response.status_code}") +``` + +### 4.3 Multi-Agent Selector (multiagentselector.py) + +#### 4.3.1 Class: MultiAgentSelector + +```python +class MultiAgentSelector(Widget): + """Widget for selecting multiple agents via various input methods.""" + + # Signals + class AgentsSelected(Message): + def __init__(self, agents: List[Agent]): ... + + def compose(self) -> ComposeResult: + yield Input(placeholder="Paste hostnames or wildcards...") + yield Button("Load from File") + yield DataTable(id="agent-table") + yield Static(id="status") + + async def on_input_submitted(self, event: Input.Submitted) -> None: + """Process pasted input.""" + lines = event.value.strip().split("\n") + await self.process_hostnames(lines) + + async def process_hostnames(self, hostnames: List[str]) -> None: + """Query API for each hostname and populate table.""" + matched = [] + unmatched = [] + for hostname in hostnames: + if "*" in hostname or "?" in hostname: + # Wildcard handling + agents = await self.expand_wildcard(hostname) + else: + agents = self.api.agent_find_by_hostname(hostname) + ... + + def get_selected_agents(self) -> List[Agent]: + """Return list of currently selected agents.""" + ... +``` + +#### 4.3.2 Wildcard Expansion + +```python +async def expand_wildcard(self, pattern: str) -> List[Agent]: + """Expand wildcard pattern to matching agents.""" + import fnmatch + all_agents = self.api.agent_find_all() + matches = [] + for _, agent in all_agents.iterrows(): + if fnmatch.fnmatch(agent["hostname"].lower(), pattern.lower()): + matches.append(agent) + return matches +``` + +### 4.4 Policy Selector (policyselector.py) + +#### 4.4.1 Class: PolicySelector + +```python +class PolicySelector(Widget): + """Reusable widget for policy/group selection.""" + + # Configuration + multi_select: bool = True + show_agent_count: bool = True + + def compose(self) -> ComposeResult: + yield Input(placeholder="Filter policies...") + yield PolicyTreeWidget(id="policy-tree") + + def on_mount(self) -> None: + self.load_policies() + + def load_policies(self) -> None: + """Load policy tree from API.""" + policies = self.api.policy_find_all() + self.query_one(PolicyTreeWidget).build_tree(policies) + + def get_selected_policies(self) -> List[Policy]: + """Return selected policy objects.""" + ... +``` + +### 4.5 Policy Tree Widget (policytreewidget.py) + +#### 4.5.1 Class: PolicyTreeWidget + +```python +class PolicyTreeWidget(Tree): + """Tree view of policy hierarchy with checkboxes.""" + + def build_tree(self, policies: pd.DataFrame) -> None: + """Build tree structure from flat policy list.""" + # Build parent-child relationships + root_policies = policies[policies["parentid"].isna()] + for _, policy in root_policies.iterrows(): + node = self.root.add(policy["groupname"], data=policy) + self._add_children(node, policy["groupid"], policies) + + def _add_children(self, parent_node, parent_id: str, policies: pd.DataFrame) -> None: + """Recursively add child policies.""" + children = policies[policies["parentid"] == parent_id] + for _, child in children.iterrows(): + node = parent_node.add(child["groupname"], data=child) + self._add_children(node, child["groupid"], policies) +``` + +### 4.6 Configuration Manager (configmanager.py) + +#### 4.6.1 Architecture + +```python +# Two-tier configuration architecture + +# System config (immutable) - bundled in executable +SYSTEM_CONFIG_KEYS = [ + "URL", # Airlock server URL + "APPNAME", # Application name + "LOG_LEVEL", # Logging level + "BAD_PATH_PARTS", # Paths to flag + "BAD_PUBLISHERS", # Publishers to flag + "PUPS", # Potentially unwanted programs + "PATH_EXCLUSION_CONST", # Path exclusion threshold + "MIN_FILES_FOR_PATH", # Minimum files for path suggestion + "VT_THREAT_TOLERANCE", # VT score threshold (Airlock VT data) + "POLICY_MAP_ENF_AUD", # Policy mappings +] + +# User config (mutable) - in user's config directory +USER_CONFIG_KEYS = [ + "TELEMETRY", # Telemetry opt-in + "TELEM_URL", # Telemetry endpoint + "TEXTUAL_THEME", # UI theme + "EXTRAS", # Feature flags +] +``` + +#### 4.6.2 Configuration Loading + +```python +def load_system_config() -> dict: + """Load system configuration from bundled file.""" + # Check for bundled location (Nuitka) + bundled_path = Path(getattr(sys, "_MEIPASS", "")) / "system_config.json" + if bundled_path.exists(): + return json.load(open(bundled_path)) + # Fallback to development location + return json.load(open(Path(__file__).parent.parent / "system_config.json")) + +def load_user_config(config_dir: Path) -> dict: + """Load or create user configuration.""" + user_config_path = config_dir / "user_config.json" + if not user_config_path.exists(): + # Create with defaults + default_config = { + "TELEMETRY": False, + "TELEM_URL": "", + "TEXTUAL_THEME": "gruvbox", + "EXTRAS": "NOTTODAY", + } + user_config_path.parent.mkdir(parents=True, exist_ok=True) + json.dump(default_config, open(user_config_path, "w"), indent=4) + return default_config + return json.load(open(user_config_path)) +``` + +--- + +## 5. Data Design + +### 5.1 Data Models + +#### 5.1.1 Agent Model + +```python +@dataclass +class Agent: + """Represents an Airlock agent/endpoint.""" + agentid: str + hostname: str + groupid: str + groupname: str + status: int # 0=Offline, 1=Online, 3=Safemode + username: str + osversion: str + agentversion: str + lastcheckin: datetime + + @property + def is_online(self) -> bool: + return self.status == 1 + + @property + def status_display(self) -> str: + return {0: "Offline", 1: "Online", 3: "Safemode"}.get(self.status, "Unknown") +``` + +#### 5.1.2 Policy Model + +```python +@dataclass +class Policy: + """Represents an Airlock policy/group.""" + groupid: str + groupname: str + parentid: Optional[str] + auditmode: int # 0=Enforcement, 1=Audit + agentcount: int + + @property + def is_audit(self) -> bool: + return self.auditmode == 1 + + @property + def mode_display(self) -> str: + return "Audit" if self.is_audit else "Enforcement" +``` + +#### 5.1.3 Execution Model + +```python +@dataclass +class Execution: + """Represents an application execution event.""" + id: str + hostname: str + filename: str + filepath: str + sha256: str + publisher: str + category: int + timestamp: datetime + username: str + commandline: str + policyname: str + vtscore: Optional[int] = None # From Airlock VT integration + + @property + def category_display(self) -> str: + categories = { + 0: "Approved", + 1: "Unapproved", + 2: "Blocked", + 3: "OTP Bypass", + } + return categories.get(self.category, f"Unknown ({self.category})") +``` + +### 5.2 Data Storage + +#### 5.2.1 Configuration Files + +**System Configuration (system_config.json)** + +```json +{ + "URL": "https://airlock.example.com/api", + "APPNAME": "Loxide", + "LOG_LEVEL": "INFO", + "BAD_PATH_PARTS": ["temp", "tmp", "cache"], + "BAD_PUBLISHERS": ["Unknown Publisher", "Self-signed"], + "PUPS": ["toolbars", "adware"], + "PATH_EXCLUSION_CONST": 4, + "MIN_FILES_FOR_PATH": 4, + "VT_THREAT_TOLERANCE": 4, + "POLICY_MAP_ENF_AUD": {} +} +``` + +**User Configuration (user_config.json)** + +```json +{ + "TELEMETRY": false, + "TELEM_URL": "", + "TEXTUAL_THEME": "gruvbox", + "EXTRAS": "NOTTODAY" +} +``` + +#### 5.2.2 File Locations + +| Platform | Base Directory | Subdirectories | +|----------|----------------|----------------| +| Windows | %APPDATA%\Loxide | config/, logs/, cache/, data/ | +| Linux | ~/.local/share/Loxide | config/, logs/, cache/, data/ | + +--- + +## 6. Security Design + +### 6.1 Credential Storage Architecture + +``` ++-------------------+ +-------------------+ +-------------------+ +| User Password | --> | Key Derivation | --> | Encryption Key | ++-------------------+ | (PBKDF2) | | (256-bit) | + +-------------------+ +-------------------+ + | + v ++-------------------+ +-------------------+ +-------------------+ +| API Key | --> | AES-256-GCM | --> | Encrypted Blob | +| (plaintext) | | Encryption | | (salt+nonce+ct) | ++-------------------+ +-------------------+ +-------------------+ + | + v + +-------------------+ + | Platform Keyring | + | (Base64 encoded) | + +-------------------+ +``` + +### 6.2 Key Derivation Implementation + +```python +# security.py + +KDF_ITERATIONS = 200_000 +SALT_SIZE = 16 # 128-bit +NONCE_SIZE = 12 # AES-GCM standard +KEY_SIZE = 32 # AES-256 + +def _derive_key(password: bytes, salt: bytes) -> bytes: + """Derive encryption key from password using PBKDF2.""" + kdf = PBKDF2HMAC( + algorithm=hashes.SHA256(), + length=KEY_SIZE, + salt=salt, + iterations=KDF_ITERATIONS, + ) + return kdf.derive(password) +``` + +### 6.3 Credential Encryption + +```python +def store_api_key(service: str, username: str, api_key: str, password: str): + """Encrypt and store API key in platform keyring.""" + configure_keyring_backend() + + # Generate random salt + salt = os.urandom(SALT_SIZE) + + # Derive encryption key + key = _derive_key(password.encode(), salt) + + # Encrypt API key + aesgcm = AESGCM(key) + nonce = os.urandom(NONCE_SIZE) + ciphertext = aesgcm.encrypt(nonce, api_key.encode(), associated_data=None) + + # Combine: salt || nonce || ciphertext + blob = salt + nonce + ciphertext + + # Store base64-encoded in keyring + keyring.set_password(service, username, base64.b64encode(blob).decode()) +``` + +### 6.4 Credential Retrieval + +```python +def retrieve_api_key(service: str, username: str, password: str) -> str: + """Retrieve and decrypt API key from platform keyring.""" + configure_keyring_backend() + + # Get from keyring + b64_blob = keyring.get_password(service, username) + if b64_blob is None: + raise ValueError("No stored credential") + + # Decode + blob = base64.b64decode(b64_blob) + + # Extract components + salt = blob[:SALT_SIZE] + nonce = blob[SALT_SIZE:SALT_SIZE + NONCE_SIZE] + ciphertext = blob[SALT_SIZE + NONCE_SIZE:] + + # Derive key and decrypt + key = _derive_key(password.encode(), salt) + aesgcm = AESGCM(key) + plaintext = aesgcm.decrypt(nonce, ciphertext, associated_data=None) + + return plaintext.decode() +``` + +### 6.5 Platform Keyring Configuration + +```python +def configure_keyring_backend(): + """Configure appropriate keyring backend for current platform.""" + system = platform.system() + if system == "Windows": + import keyring.backends.Windows + keyring.set_keyring(keyring.backends.Windows.WinVaultKeyring()) + elif system == "Linux": + import keyring.backends.kwallet + keyring.set_keyring(keyring.backends.kwallet.DBusKeyring()) + else: + raise EnvironmentError(f"Unsupported platform: {system}") +``` + +### 6.6 Password Complexity Validation + +```python +def check_password_complexity(password: str) -> bool: + """Validate password meets complexity requirements.""" + if len(password) < 12: + return False + if not re.search(r"[A-Z]", password): # Uppercase + return False + if not re.search(r"[a-z]", password): # Lowercase + return False + if not re.search(r"[0-9]", password): # Digit + return False + if not re.search(r"[^A-Za-z0-9]", password): # Special + return False + return True +``` + +--- + +## 7. User Interface Design + +### 7.1 Screen Layout Standards + +#### 7.1.1 Screen Structure + +All screens follow a consistent layout: + +``` ++------------------------------------------------------------------+ +| Header: Application Name | Current Screen | Connection Status | ++------------------------------------------------------------------+ +| | +| Content Area | +| | +| (Varies by screen - tables, forms, wizards, etc.) | +| | ++------------------------------------------------------------------+ +| Footer: [Q]uit [R]efresh [Esc]Back [?]Help [Tab]Navigate | ++------------------------------------------------------------------+ +``` + +#### 7.1.2 Widget vs Screen Pattern + +| Type | Has Header/Footer | Navigation | Usage | +|------|-------------------|------------|-------| +| Screen | Yes | push_screen() | Full-page workflows | +| Widget | No | Embedded | Reusable components | + +### 7.2 Keyboard Navigation + +#### 7.2.1 Global Bindings + +| Key | Action | Context | +|-----|--------|---------| +| q | Quit application | Global | +| Esc | Back/Cancel | Screens | +| Tab | Next widget | Navigation | +| Shift+Tab | Previous widget | Navigation | +| r | Refresh data | Data views | +| e | Export to CSV | Data tables | +| ? | Show help | Global | + +#### 7.2.2 Data Table Navigation + +| Key | Action | +|-----|--------| +| Up/Down | Move row cursor | +| Page Up/Down | Scroll page | +| Home/End | First/last row | +| Space | Toggle selection | +| Enter | Select/activate | +| a | Select all | +| n | Deselect all | + +### 7.3 Theme System + +#### 7.3.1 Theme Architecture + +```python +# Themes are Python functions returning CSS strings +def get_amber_terminal_theme() -> str: + return """ + Screen { + background: #1a1a1a; + } + Header { + background: #ff8c00; + color: #000000; + } + DataTable > .datatable--cursor { + background: #ff8c00 30%; + } + Button { + background: #ff8c00; + color: #000000; + } + ... + """ +``` + +#### 7.3.2 Available Themes + +| Theme | Description | +|-------|-------------| +| gruvbox | Default warm retro theme | +| amber_terminal | Classic amber CRT look | +| retro_terminal | Green phosphor terminal | + +--- + +## 8. External Interface Design + +### 8.1 Airlock API Integration + +#### 8.1.1 Request Format + +All API requests use POST with JSON payload: + +```python +def _post(self, endpoint: str, payload: dict = None) -> dict: + url = f"{self.base_url}{endpoint}" + headers = { + "X-APIKey": self.api_key, + "Content-Type": "application/json" + } + response = requests.post( + url, + headers=headers, + data=json.dumps(payload or {}), + verify=False, + timeout=30 + ) + return response.json() +``` + +#### 8.1.2 Response Handling + +```python +# Standard response structure +{ + "status": "success" | "error", + "response": { + # Endpoint-specific data + }, + "message": "Optional error message" +} + +# Extraction pattern +def agent_find_all(self) -> pd.DataFrame: + result = self._post("/v1/agent/find", {}) + return pd.DataFrame(result["response"]["agents"]) +``` + +### 8.2 Data Export + +#### 8.2.1 CSV Export Implementation + +```python +def export_to_csv(data: pd.DataFrame, filepath: str) -> None: + """Export DataFrame to CSV with Excel-compatible encoding.""" + data.to_csv( + filepath, + index=False, + encoding="utf-8-sig", # BOM for Excel + quoting=csv.QUOTE_NONNUMERIC + ) +``` + +#### 8.2.2 Timestamp Formatting + +```python +def format_timestamp(dt: datetime) -> str: + """Format timestamp for display and export.""" + return dt.strftime("%Y-%m-%d %H:%M:%S") +``` + +--- + +## 9. Distribution and Deployment + +### 9.1 Windows Executable (Nuitka) + +#### 9.1.1 Build Configuration + +```powershell +# Set inputs +$icon = '.\Loxide_Icon.ico' +$config = '.\system_config.json' + +# Clean prior outputs +Remove-Item -Recurse -Force ` + '.\Loxide.build', + '.\Loxide.dist', + '.\Loxide.onefile-build' ` + -ErrorAction SilentlyContinue + +# Build arguments +$args = @( + '--onefile', + '--follow-imports', + '--msvc=latest', + + # Keyring + pywin32 modules (ensure lazy imports are included) + '--include-module=keyring.backends.Windows', + '--include-module=win32cred', + '--include-module=pywintypes', + '--include-module=pythoncom', + '--include-module=win32api', + '--include-module=win32security', + '--include-module=win32con', + + # Product metadata and resources + '--windows-product-name=Loxide', + '--windows-file-version=0.9.0.0', + '--windows-product-version=0.9.0', + '--windows-company-name=', + "--windows-icon-from-ico=$icon", + "--include-data-file=$config=system_config.json", + + # Deployment mode (disable helper checks for production) + '--deployment', + + '.\Loxide.py' +) + +& nuitka @args +``` + +#### 9.1.2 Build Options Explained + +| Option | Purpose | +|--------|---------| +| --onefile | Single executable output | +| --follow-imports | Follow all import statements | +| --msvc=latest | Use latest MSVC compiler | +| --include-module=keyring.backends.Windows | Windows Credential Manager support | +| --include-module=win32cred | Windows credential API | +| --include-module=pywintypes | PyWin32 types | +| --include-module=pythoncom | COM support | +| --include-module=win32api | Windows API bindings | +| --include-module=win32security | Security API bindings | +| --include-module=win32con | Windows constants | +| --windows-product-name | Executable metadata | +| --windows-file-version | Version in file properties | +| --windows-company-name | Company in file properties | +| --windows-icon-from-ico | Application icon | +| --include-data-file | Bundle system_config.json | +| --deployment | Disable debug checks for production | + +#### 9.1.3 Build Requirements + +- Windows 10/11 build machine +- Python 3.10+ +- Nuitka package (`pip install nuitka`) +- MSVC compiler (Visual Studio Build Tools) +- PyWin32 package (`pip install pywin32`) +- All runtime dependencies installed +- Access to Gitea for airlock_libs + +#### 9.1.4 Build Artifacts + +| Directory | Contents | +|-----------|----------| +| Loxide.build | Intermediate build files | +| Loxide.dist | Distribution files (standalone mode) | +| Loxide.onefile-build | Onefile build cache | +| Loxide.exe | Final executable (onefile mode) | + +### 9.2 Linux Distribution + +#### 9.2.1 pip Installation + +```bash +# From Gitea PyPI registry +pip install loxide \ + --extra-index-url https:///api/packages//pypi/simple/ +``` + +#### 9.2.2 Standalone Binary + +```bash +# Nuitka on Linux +python -m nuitka \ + --standalone \ + --onefile \ + --include-package-data=textual \ + Loxide.py +``` + +### 9.3 Platform Dependencies + +| Platform | Keyring Backend | Additional Requirements | +|----------|-----------------|------------------------| +| Windows | Windows Credential Manager | None | +| Linux | Secret Service | libsecret, KWallet or GNOME Keyring | + +--- + +## 10. Error Handling + +### 10.1 Error Categories + +| Category | Examples | Handling | +|----------|----------|----------| +| API Errors | Timeout, 4xx, 5xx | Toast notification, retry option | +| Input Errors | Invalid hostname, bad date | Inline validation message | +| UI Errors | Widget crash | Graceful degradation, error screen | +| Configuration Errors | Missing config, bad JSON | Startup warning, defaults | +| Credential Errors | Wrong password, no credential | Re-prompt with retry limit | + +### 10.2 Logging Strategy + +```python +# setup.py + +class TextualNotificationHandler(logging.Handler): + """Log handler that displays toasts for important messages.""" + + def emit(self, record): + if record.levelno >= logging.WARNING: + # Show as toast notification + app = App.get_running_app() + if app: + app.notify(record.getMessage(), severity="warning") + +def setup_logging(log_level: str, log_dir: Path): + """Configure logging with file and notification handlers.""" + logger = logging.getLogger() + logger.setLevel(getattr(logging, log_level)) + + # File handler for all messages + file_handler = logging.FileHandler(log_dir / "Loxide.log") + file_handler.setFormatter(logging.Formatter( + "%(asctime)s - %(name)s - %(levelname)s - %(message)s" + )) + logger.addHandler(file_handler) + + # Notification handler for warnings and above + notification_handler = TextualNotificationHandler() + notification_handler.setLevel(logging.WARNING) + logger.addHandler(notification_handler) +``` + +### 10.3 Common Error Patterns + +#### 10.3.1 DataFrame Boolean Ambiguity + +```python +# Problem: "The truth value of a Series is ambiguous" +# Wrong: +agents[not agents["enforce_ready"]] +# Correct: +agents[~agents["enforce_ready"]] +``` + +#### 10.3.2 Empty DataFrame Access + +```python +# Problem: KeyError on empty DataFrame +# Wrong: +value = df["column"].iloc[0] +# Correct: +if not df.empty and "column" in df.columns: + value = df["column"].iloc[0] +``` + +#### 10.3.3 Duplicate Widget IDs + +```python +# Problem: "DuplicateIds" error on widget mount +# Solution: Remove existing widgets first +async def update_content(self): + await self.query("#my-widget").remove() + await self.mount(MyWidget(id="my-widget")) +``` + +--- + +## 11. Phase 2: LEMON Integration + +### 11.1 Overview + +LEMON (Loxide Execution MONitoring) is a planned backend service for automated local approval sessions. Full specifications are in separate LEMON documentation. + +### 11.2 Loxide Integration Points + +| Component | Integration | +|-----------|-------------| +| LEMONClient | New service class for LEMON API | +| LEMONSessionsScreen | View/create sessions | +| LEMONHashReviewScreen | Review pending hashes | +| Certificate Configuration | mTLS setup | + +### 11.3 Configuration Additions + +```json +{ + "LEMON_ENABLED": true, + "LEMON_URL": "https://lemon.example.com/api/v1", + "LEMON_USER_CERT": "/path/to/user.crt", + "LEMON_CA_CERT": "/path/to/internal_ca.crt" +} +``` + +--- + +## 12. Appendices + +### Appendix A: Textual Framework Patterns + +Key patterns used throughout the application. + +### Appendix B: API Response Schemas + +Complete API response schema documentation. + +### Appendix C: Configuration Schema + +JSON schema for configuration files. + +--- + +## Document Approval + +This Software Design Document has been reviewed and approved for implementation. + +| Role | Name | Date | +|------|------|------| +| Author | Brandon Wickline | | +| Technical Reviewer | James Brotosky | | +| Approver | | | + +--- + +*End of Document*