423e9e8208
- Added comprehensive documentation: - System Design Requirements (SDR) - System Design Specification (SDS) - API Reference - User Stories & Use Cases - Fixed minor UI issues related to double encoding
1300 lines
38 KiB
Markdown
1300 lines
38 KiB
Markdown
# 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=<Your Organization>',
|
|
"--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://<gitea-instance>/api/packages/<user>/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*
|