Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,54 @@
name: CI

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.11, 3.12]

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}

- name: Cache pip
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/pyproject.toml') }}
restore-keys: |
${{ runner.os }}-pip-

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e .[test]
pip install ruff mypy

- name: Run linter (ruff)
run: |
ruff check .

- name: Run mypy
run: |
mypy azazel_pi --ignore-missing-imports

- name: Run tests
run: |
pytest -q
name: CI

on:
push:
branches: [ main, feature/** ]
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

English | [日本語](README_ja.md)

![Azazel-Pi_image](images/azazel-pi-prototype.jpg)
![Azazel-Pi_image](images/azazel-pi-prototype.jpg)
![version](https://img.shields.io/github/v/tag/01rabbit/Azazel-Pi?label=Version)
![License](https://img.shields.io/github/license/01rabbit/Azazel-Pi)
![release-date](https://img.shields.io/github/release-date/01rabbit/Azazel-Pi)
Expand Down
2 changes: 1 addition & 1 deletion README_ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

[English](README.md) | 日本語

![Azazel-Pi_image](images/azazel-pi-prototype.jpg)
![Azazel-Pi_image](images/azazel-pi-prototype.jpg)
![version](https://img.shields.io/github/v/tag/01rabbit/Azazel-Pi?label=Version)
![License](https://img.shields.io/github/license/01rabbit/Azazel-Pi)
![release-date](https://img.shields.io/github/release-date/01rabbit/Azazel-Pi)
Expand Down
16 changes: 16 additions & 0 deletions azazel_pi/core/display/status_collector.py
Original file line number Diff line number Diff line change
Expand Up @@ -493,12 +493,28 @@ def _count_alerts(self, recent_window_seconds: int = 300) -> tuple[int, int]:

def _is_service_active(self, service_name: str) -> bool:
"""Check if a systemd service is active."""
if service_name == "opencanary":
return self._is_container_running("azazel_opencanary")
try:
result = run_cmd(["systemctl", "is-active", f"{service_name}.service"], capture_output=True, text=True, timeout=2, check=False)
return (result.stdout or "").strip() == "active"
except Exception:
return False

def _is_container_running(self, container_name: str) -> bool:
"""Check if a Docker container is running."""
try:
result = run_cmd(
["docker", "inspect", "-f", "{{.State.Running}}", container_name],
capture_output=True,
text=True,
timeout=2,
check=False,
)
return result.returncode == 0 and (result.stdout or "").strip().lower() == "true"
except Exception:
return False

def _get_uptime(self) -> int:
"""Get system uptime in seconds."""
try:
Expand Down
26 changes: 22 additions & 4 deletions azctl/menu/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -449,9 +449,9 @@ def _get_current_status(self) -> Dict[str, Any]:
mode_display = mode.upper() if mode else "UNKNOWN"

# Count active services (simplified)
services = ["suricata", "opencanary", "vector", "azctl"]
systemd_services = ["suricata", "vector", "azctl"]
services_active = 0
for service in services:
for service in systemd_services:
try:
result = run_cmd(
["systemctl", "is-active", service],
Expand All @@ -462,11 +462,15 @@ def _get_current_status(self) -> Dict[str, Any]:
except Exception:
pass

services_total = len(systemd_services) + 1 # include OpenCanary container
if self._is_container_running("azazel_opencanary"):
services_active += 1

return {
"mode": mode,
"mode_display": mode_display if 'mode_display' in locals() else (mode.upper() if mode else "UNKNOWN"),
"services_active": services_active,
"services_total": len(services),
"services_total": services_total,
}

def _get_enhanced_status(self) -> Dict[str, Any]:
Expand All @@ -487,4 +491,18 @@ def _get_enhanced_status(self) -> Dict[str, Any]:
"profile": profile,
"wlan0_info": wlan0_info,
"wlan1_info": wlan1_info,
}
}

def _is_container_running(self, container_name: str) -> bool:
"""Check whether a Docker container is running."""
try:
result = run_cmd(
["docker", "inspect", "-f", "{{.State.Running}}", container_name],
capture_output=True,
text=True,
timeout=5,
check=False,
)
return result.returncode == 0 and (result.stdout or "").strip().lower() == "true"
except Exception:
return False
23 changes: 18 additions & 5 deletions azctl/menu/defense.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,12 +214,11 @@ def _view_status(self) -> None:

# Add services status if available
try:
import subprocess
suricata_status = run_cmd(['systemctl', 'is-active', 'suricata'], capture_output=True, text=True).stdout.strip()
canary_status = run_cmd(['systemctl', 'is-active', 'opencanary'], capture_output=True, text=True).stdout.strip()
canary_running = self._is_container_running("azazel_opencanary")

services_info = f"Suricata: {'✅' if suricata_status == 'active' else '❌'} | Canary: {'✅' if canary_status == 'active' else '❌'}"
except:
services_info = f"Suricata: {'✅' if suricata_status == 'active' else '❌'} | Canary: {'✅' if canary_running else '❌'}"
except Exception:
services_info = "Status unknown"

info_table.add_row(
Expand Down Expand Up @@ -552,6 +551,20 @@ def _get_memory_usage(self) -> str:
except Exception:
return "N/A"

def _is_container_running(self, container_name: str) -> bool:
"""Check whether a Docker container is running."""
try:
result = run_cmd(
["docker", "inspect", "-f", "{{.State.Running}}", container_name],
capture_output=True,
text=True,
timeout=5,
check=False,
)
return result.returncode == 0 and (result.stdout or "").strip().lower() == "true"
except Exception:
return False

def _pause(self) -> None:
"""Pause for user input."""
Prompt.ask("\n[dim]Press Enter to continue[/dim]", default="", show_default=False)
Prompt.ask("\n[dim]Press Enter to continue[/dim]", default="", show_default=False)
54 changes: 37 additions & 17 deletions azctl/menu/emergency.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,13 +121,18 @@ def _emergency_lockdown(self) -> None:

# Step 4: Stop services
self.console.print("[blue]4. Stopping non-essential services...[/blue]")
services_to_stop = ["vector", "opencanary"]
services_to_stop = ["vector"]
for service in services_to_stop:
try:
run_cmd(["sudo", "systemctl", "stop", f"{service}.service"], timeout=15)
self.console.print(f"[green]✓ {service} stopped[/green]")
except Exception:
self.console.print(f"[yellow]! {service} stop failed[/yellow]")
try:
run_cmd(["sudo", "docker", "stop", "azazel_opencanary"], timeout=30)
self.console.print("[green]✓ azazel_opencanary stopped[/green]")
except Exception:
self.console.print("[yellow]! azazel_opencanary stop failed[/yellow]")

self.console.print("\n[bold red]EMERGENCY LOCKDOWN COMPLETED[/bold red]")
self.console.print("[yellow]System is now in maximum security lockdown mode.[/yellow]")
Expand Down Expand Up @@ -163,10 +168,10 @@ def _reset_network(self) -> None:
run_cmd(["sudo", "systemctl", "stop", "wpa_supplicant"], timeout=10)

# Backup and reset wpa_supplicant.conf
run_cmd([
"sudo", "cp", "/etc/wpa_supplicant/wpa_supplicant.conf",
f"/etc/wpa_supplicant/wpa_supplicant.conf.backup.{datetime.now().strftime('%Y%m%d_%H%M%S')}"
], timeout=5)
run_cmd([
"sudo", "cp", "/etc/wpa_supplicant/wpa_supplicant.conf",
f"/etc/wpa_supplicant/wpa_supplicant.conf.backup.{datetime.now().strftime('%Y%m%d_%H%M%S')}"
], timeout=5)

# Create minimal wpa_supplicant.conf
minimal_config = """ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
Expand All @@ -190,23 +195,23 @@ def _reset_network(self) -> None:
# Reset network interfaces
self.console.print("[blue]2. Resetting network interfaces...[/blue]")
try:
run_cmd(["sudo", "ip", "link", "set", self.wan_if, "down"], timeout=5)
run_cmd(["sudo", "ip", "link", "set", self.wan_if, "up"], timeout=5)
run_cmd(["sudo", "ip", "link", "set", self.lan_if, "down"], timeout=5)
run_cmd(["sudo", "ip", "link", "set", self.lan_if, "up"], timeout=5)
run_cmd(["sudo", "ip", "link", "set", self.wan_if, "down"], timeout=5)
run_cmd(["sudo", "ip", "link", "set", self.wan_if, "up"], timeout=5)
run_cmd(["sudo", "ip", "link", "set", self.lan_if, "down"], timeout=5)
run_cmd(["sudo", "ip", "link", "set", self.lan_if, "up"], timeout=5)
self.console.print("[green]✓ Network interfaces reset[/green]")
except Exception as e:
self.console.print(f"[red]✗ Interface reset failed: {e}[/red]")

# Restart network services
self.console.print("[blue]3. Restarting network services...[/blue]")
services = ["dhcpcd", "hostapd"]
for service in services:
try:
run_cmd(["sudo", "systemctl", "restart", service], timeout=15)
self.console.print(f"[green]✓ {service} restarted[/green]")
except Exception:
self.console.print(f"[yellow]! {service} restart failed[/yellow]")
for service in services:
try:
run_cmd(["sudo", "systemctl", "restart", service], timeout=15)
self.console.print(f"[green]✓ {service} restarted[/green]")
except Exception:
self.console.print(f"[yellow]! {service} restart failed[/yellow]")

self.console.print("\n[bold green]Network configuration reset completed[/bold green]")

Expand Down Expand Up @@ -292,7 +297,7 @@ def _system_report(self) -> None:
# Service status
report.write("SERVICE STATUS\n")
report.write("-" * 15 + "\n")
services = ["azctl", "azctl-serve", "suricata", "opencanary", "vector"]
services = ["azctl", "azctl-serve", "suricata", "vector"]
for service in services:
try:
result = run_cmd(
Expand All @@ -303,6 +308,7 @@ def _system_report(self) -> None:
report.write(f"{service}: {status}\n")
except Exception:
report.write(f"{service}: UNKNOWN\n")
report.write(f"azazel_opencanary (Docker): {'ACTIVE' if self._is_container_running('azazel_opencanary') else 'INACTIVE'}\n")

report.write("\n")

Expand Down Expand Up @@ -402,4 +408,18 @@ def _factory_reset(self) -> None:

def _pause(self) -> None:
"""Pause for user input."""
Prompt.ask("\n[dim]Press Enter to continue[/dim]", default="", show_default=False)
Prompt.ask("\n[dim]Press Enter to continue[/dim]", default="", show_default=False)

def _is_container_running(self, container_name: str) -> bool:
"""Check whether a Docker container is running."""
try:
result = run_cmd(
["docker", "inspect", "-f", "{{.State.Running}}", container_name],
capture_output=True,
text=True,
timeout=5,
check=False,
)
return result.returncode == 0 and (result.stdout or "").strip().lower() == "true"
except Exception:
return False
Loading