Skip to content
79 changes: 65 additions & 14 deletions cortex/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
validate_installation_id,
ValidationError
)
# Import the new Notification Manager
# Import Notification Manager
from cortex.notification_manager import NotificationManager


Expand Down Expand Up @@ -112,10 +112,9 @@
sys.stdout.write('\r\033[K')
sys.stdout.flush()

# --- New Notification Method ---
# --- Notification Method ---
def notify(self, args):
"""Handle notification commands"""
# Addressing CodeRabbit feedback: Handle missing subcommand gracefully
if not args.notify_action:
self._print_error("Please specify a subcommand (config/enable/disable/dnd/send)")
return 1
Expand All @@ -132,24 +131,21 @@

elif args.notify_action == 'enable':
mgr.config["enabled"] = True
# Addressing CodeRabbit feedback: Ideally should use a public method instead of private _save_config,
# but keeping as is for a simple fix (or adding a save method to NotificationManager would be best).
mgr._save_config()
mgr.save_config()
self._print_success("Notifications enabled")
return 0

elif args.notify_action == 'disable':
mgr.config["enabled"] = False
mgr._save_config()
cx_print("Notifications disabled (Critical alerts will still show)", "warning")
mgr.save_config()
self._print_success("Notifications disabled (Critical alerts will still show)")
return 0

elif args.notify_action == 'dnd':
if not args.start or not args.end:
self._print_error("Please provide start and end times (HH:MM)")
return 1

# Addressing CodeRabbit feedback: Add time format validation
try:
datetime.strptime(args.start, "%H:%M")
datetime.strptime(args.end, "%H:%M")
Expand All @@ -159,7 +155,7 @@

mgr.config["dnd_start"] = args.start
mgr.config["dnd_end"] = args.end
mgr._save_config()
mgr.save_config()
self._print_success(f"DND Window updated: {args.start} - {args.end}")
return 0

Expand All @@ -174,7 +170,56 @@
else:
self._print_error("Unknown notify command")
return 1
# -------------------------------

# --- New Health Command ---
def health(self, args):

Check warning on line 175 in cortex/cli.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove the unused function parameter "args".

See more on https://sonarcloud.io/project/issues?id=cortexlinux_cortex&issues=AZsN52d-RWcWs7230WwW&open=AZsN52d-RWcWs7230WwW&pullRequest=292
"""Run system health checks and show recommendations"""
from cortex.health.monitor import HealthMonitor

self._print_status("🔍", "Running system health checks...")
monitor = HealthMonitor()
report = monitor.run_all()

# --- Display Results ---
score = report['total_score']

# Color code the score
score_color = "green"
if score < 60: score_color = "red"
elif score < 80: score_color = "yellow"

console.print()
console.print(f"📊 [bold]System Health Score:[/bold] [{score_color}]{score}/100[/{score_color}]")
console.print()

console.print("[bold]Factors:[/bold]")
recommendations = []

for res in report['results']:
status_icon = "✅"
if res['status'] == 'WARNING': status_icon = "⚠️ "
elif res['status'] == 'CRITICAL': status_icon = "❌"

console.print(f" {status_icon} {res['name']:<15}: {res['score']}/100 ({res['details']})")

if res['recommendation']:
recommendations.append(res['recommendation'])

console.print()

if recommendations:
console.print("[bold]Recommendations:[/bold]")
for i, rec in enumerate(recommendations, 1):
console.print(f" {i}. {rec}")

console.print()
# Note: Auto-fix logic would go here, prompting user to apply specific commands.
# For this iteration, we display actionable advice.
console.print("[dim]Run suggested commands manually to improve your score.[/dim]")
else:
self._print_success("System is in excellent health! No actions needed.")

return 0

def install(self, software: str, execute: bool = False, dry_run: bool = False):
# Validate input first
Expand Down Expand Up @@ -543,7 +588,8 @@
table.add_row("install <pkg>", "Install software")
table.add_row("history", "View history")
table.add_row("rollback <id>", "Undo installation")
table.add_row("notify", "Manage desktop notifications") # Added this line
table.add_row("notify", "Manage desktop notifications")
table.add_row("health", "Check system health score") # Added this line

console.print(table)
console.print()
Expand Down Expand Up @@ -598,7 +644,7 @@
edit_pref_parser.add_argument('key', nargs='?')
edit_pref_parser.add_argument('value', nargs='?')

# --- New Notify Command ---
# --- Notify Command ---
notify_parser = subparsers.add_parser('notify', help='Manage desktop notifications')
notify_subs = notify_parser.add_subparsers(dest='notify_action', help='Notify actions')

Expand All @@ -615,6 +661,9 @@
send_parser.add_argument('--title', default='Cortex Notification')
send_parser.add_argument('--level', choices=['low', 'normal', 'critical'], default='normal')
send_parser.add_argument('--actions', nargs='*', help='Action buttons')

# --- New Health Command ---
health_parser = subparsers.add_parser('health', help='Check system health score')

Check warning on line 666 in cortex/cli.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove the unused local variable "health_parser".

See more on https://sonarcloud.io/project/issues?id=cortexlinux_cortex&issues=AZsN52d-RWcWs7230WwX&open=AZsN52d-RWcWs7230WwX&pullRequest=292
# --------------------------

args = parser.parse_args()
Expand Down Expand Up @@ -642,9 +691,11 @@
return cli.check_pref(key=args.key)
elif args.command == 'edit-pref':
return cli.edit_pref(action=args.action, key=args.key, value=args.value)
# Handle the new notify command
elif args.command == 'notify':
return cli.notify(args)
# Handle new command
elif args.command == 'health':
return cli.health(args)
else:
parser.print_help()
return 1
Expand Down
Empty file added cortex/health/__init__.py
Empty file.
60 changes: 60 additions & 0 deletions cortex/health/checks/disk.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import shutil
from ..monitor import HealthCheck, CheckResult

class DiskCheck(HealthCheck):
"""Check root filesystem disk usage."""

def run(self) -> CheckResult:
"""
Calculate disk usage percentage.

Returns:
CheckResult based on usage thresholds.
"""
try:
# Use _ for unused variable (free space)
total, used, _ = shutil.disk_usage("/")
usage_percent = (used / total) * 100
except Exception as e:
return CheckResult(
name="Disk Usage",

Check failure on line 20 in cortex/health/checks/disk.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "Disk Usage" 4 times.

See more on https://sonarcloud.io/project/issues?id=cortexlinux_cortex&issues=AZsN9_Tlij_ZJK_uEhq3&open=AZsN9_Tlij_ZJK_uEhq3&pullRequest=292
category="disk",
score=0,
status="CRITICAL",
details=f"Check failed: {e}",
recommendation="Check disk mounts and permissions",
weight=0.20
)

# Explicit early returns to avoid static analysis confusion
if usage_percent > 90:
return CheckResult(
name="Disk Usage",
category="disk",
score=0,
status="CRITICAL",
details=f"{usage_percent:.1f}% used",
recommendation="Clean up disk space immediately",
weight=0.20
)

if usage_percent > 80:
return CheckResult(
name="Disk Usage",
category="disk",
score=50,
status="WARNING",
details=f"{usage_percent:.1f}% used",
recommendation="Consider cleaning up disk space",
weight=0.20
)

return CheckResult(
name="Disk Usage",
category="disk",
score=100,
status="OK",
details=f"{usage_percent:.1f}% used",
recommendation=None,
weight=0.20
)
63 changes: 63 additions & 0 deletions cortex/health/checks/performance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import os
import multiprocessing
from ..monitor import HealthCheck, CheckResult

class PerformanceCheck(HealthCheck):
def run(self) -> CheckResult:
score = 100
issues = []
rec = None

# 1. Load Average (1min)
try:
load1, _, _ = os.getloadavg()
cores = multiprocessing.cpu_count()
# Load ratio against core count
load_ratio = load1 / cores

if load_ratio > 1.0:
score -= 50
issues.append(f"High Load ({load1:.2f})")
rec = "Check top processes"
except Exception:
pass # Skip on Windows etc.

# 2. Memory Usage (Linux /proc/meminfo)
try:
with open('/proc/meminfo', 'r') as f:
meminfo = {}
for line in f:
parts = line.split(':')
if len(parts) == 2:
meminfo[parts[0].strip()] = int(parts[1].strip().split()[0])

if 'MemTotal' in meminfo and 'MemAvailable' in meminfo:
total = meminfo['MemTotal']
avail = meminfo['MemAvailable']
used_percent = ((total - avail) / total) * 100

if used_percent > 80:
penalty = int(used_percent - 80)
score -= penalty
issues.append(f"High Memory ({used_percent:.0f}%)")
except FileNotFoundError:
pass # Non-Linux systems

# Summary of results
status = "OK"
if score < 50:
status = "CRITICAL"
elif score < 90:
status = "WARNING"

details = ", ".join(issues) if issues else "Optimal"

return CheckResult(
name="System Load",
category="performance",
score=max(0, score),
status=status,
details=details,
recommendation=rec,
weight=0.20 # 20%
)
Comment on lines +5 to +63
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Add docstrings for PerformanceCheck and run to meet public-API guideline

PerformanceCheck is part of the public health-check surface but currently lacks docstrings. Adding brief class and method docstrings will align with the “docstrings required for all public APIs” guideline and clarify what this check measures.

-class PerformanceCheck(HealthCheck):
-    def run(self) -> CheckResult:
+class PerformanceCheck(HealthCheck):
+    """Health check for basic system performance (CPU load and memory usage)."""
+
+    def run(self) -> CheckResult:
+        """Compute a 0–100 performance score from load average and memory pressure."""
         score = 100
         issues = []
         rec = None
🤖 Prompt for AI Agents
In cortex/health/checks/performance.py around lines 5 to 63, the public class
PerformanceCheck and its run method lack docstrings; add a one- to two-line
class docstring describing that PerformanceCheck evaluates system performance
(load and memory) and its intended use in health checks, and add a short method
docstring for run describing what it measures, the return type (CheckResult),
and any important behavior (e.g., skipped checks on non-Linux/Windows handling);
keep both concise, present-tense, and follow the repository’s docstring style
(brief summary + returns note).

66 changes: 66 additions & 0 deletions cortex/health/checks/security.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import subprocess
import os
from ..monitor import HealthCheck, CheckResult

class SecurityCheck(HealthCheck):
def run(self) -> CheckResult:

Check failure on line 6 in cortex/health/checks/security.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 19 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=cortexlinux_cortex&issues=AZsOAmywVxORX-pF-mvb&open=AZsOAmywVxORX-pF-mvb&pullRequest=292
score = 100
issues = []
recommendations = []

# 1. Firewall (UFW) Check
ufw_active = False
try:
# Add timeout to prevent hanging (Fixes Reliability Issue)
res = subprocess.run(
["systemctl", "is-active", "ufw"],
capture_output=True,
text=True,
timeout=5
)
# Fix: Use exact match to avoid matching "inactive" which contains "active"
if res.returncode == 0 and res.stdout.strip() == "active":
ufw_active = True
except subprocess.TimeoutExpired:
pass # Command timed out, treat as inactive or unavailable
except FileNotFoundError:
pass # Environment without systemctl (e.g., Docker or non-systemd)
except Exception:
pass # Generic error protection

if not ufw_active:
score = 0 # Spec: 0 points if Firewall is inactive
issues.append("Firewall Inactive")
recommendations.append("Enable UFW Firewall")

# 2. SSH Root Login Check
try:
ssh_config = "/etc/ssh/sshd_config"
if os.path.exists(ssh_config):
with open(ssh_config, 'r') as f:
for line in f:
line = line.strip()
# Check for uncommented PermitRootLogin yes
if line.startswith("PermitRootLogin") and "yes" in line.split():
score -= 50
issues.append("Root SSH Allowed")
recommendations.append("Disable SSH Root Login in sshd_config")
break
except PermissionError:
pass # Cannot read config, skip check
except Exception:
pass # Generic error protection

status = "OK"
if score < 50: status = "CRITICAL"
elif score < 100: status = "WARNING"

return CheckResult(
name="Security Posture",
category="security",
score=max(0, score),
status=status,
details=", ".join(issues) if issues else "Secure",
recommendation=", ".join(recommendations) if recommendations else None,
weight=0.35
)
Loading
Loading