diff --git a/Community Events Games/Pocs/Games.razor b/Community Events Games/Pocs/Games.razor new file mode 100644 index 00000000..08550184 --- /dev/null +++ b/Community Events Games/Pocs/Games.razor @@ -0,0 +1,925 @@ +@page "/community/events/retro" + +@using MySqlConnector +@using System.Data +@using System.Data.Common +@using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage +@using Microsoft.AspNetCore.DataProtection +@using System.Text.Json +@using System.Text.Json.Nodes +@using System.Text +@using System.Security.Claims + +@inject NavigationManager NavigationManager +@inject ILocalStorageService localStorage +@inject IDialogService DialogService +@inject ISnackbar Snackbar +@inject IJSRuntime JSRuntime +@inject IDataProtectionProvider DataProtectionProvider +@inject AuthenticationStateProvider AuthenticationStateProvider + + + + + + + + + + + +
+ + + Please wait... 🐧 + +
+
+ + + + + Hello @firstName! + + + + + + Tetris + + + + + + + + + Statistics + + + + Score: 0 + + + Level: 1 + + + Lines: 0 + + + + + + + + Controls + + + Move Left/Right + Soft Drop + Rotate + Space Hard Drop + P Pause/Resume + + + + + +
+ + + + + + + Start Game + + + + + Pause + + + + + Stop + + + + + +
+ + Statistics + Score: 0 + Level: 1 + Lines: 0 + + + + Controls: + ← β†’ : Move Left/Right + ↓ : Soft Drop + ↑ : Rotate + Space : Hard Drop + P : Pause/Resume + +
+
+
+
+
+
+ + + + Space Invaders + + + + + + + + + Statistics + + + + Score: 0 + + + Level: 1 + + + Lives: 3 + + + Kills: 0 + + + Max Combo: 0 + + + + + + + + Controls + + + Move Left/Right + Space Shoot + P Pause/Resume + + + + + + + Features + + + πŸ›‘οΈ Destructible shields + ⚑ Combo system + πŸ’« Invulnerability frames + 🎯 Shoot cooldown + + + + + +
+ + + + + + + Start Game + + + + + Pause + + + + + Stop + + + + + +
+ + Statistics + Score: 0 + Level: 1 + Lives: 3 + Kills: 0 + Max Combo: 0 + + + + Controls: + ← β†’ : Move Left/Right + Space : Shoot + P : Pause/Resume + +
+
+
+
+
+
+ + + + Pac-Man + + + + + + + + + Statistics + + + + Score: 0 + + + Level: 1 + + + Lives: 3 + + + Pellets: 0 / 0 + + + + + + + + Controls + + + Move Up + Move Down + Move Left + Move Right + P Pause/Resume + + + + + + + How to Play + + + 🟑 Eat all pellets + βšͺ Power pellets: 50pts + πŸ‘» Avoid ghosts + πŸ’™ Eat scared ghosts! + + + + + +
+ + + + + + + Start Game + + + + + Pause + + + + + Stop + + + + + +
+ + Statistics + Score: 0 + Level: 1 + Lives: 3 + Pellets: 0 / 0 + + + + Controls: + Arrow Keys : Move + P : Pause/Resume + +
+
+
+
+
+
+ + + + Snake + + + + + + + + + Statistics + + + + Score: 0 + + + High Score: 0 + + + Length: 3 + + + Speed: 10 + + + + + + + + Controls + + + Move Up + Move Down + Move Left + Move Right + P Pause/Resume + + + + + + + How to Play + + + 🐍 Eat red food: 10pts + ⭐ Golden food: 50pts + ⚠️ Don't hit walls + ⚠️ Don't hit yourself + + + + + +
+ + + + + + + Start Game + + + + + Pause + + + + + Stop + + + + + +
+ + Statistics + Score: 0 + High Score: 0 + Length: 3 + Speed: 10 + + + + Controls: + Arrow Keys : Move + P : Pause/Resume + +
+
+
+
+
+
+ + + + DOOM πŸ”₯ + + + + + + + + + Statistics + + + + Health: 100 + + + Ammo: 50 + + + Kills: 0 + + + Score: 0 + + + + + + + + Controls + + + W/↑ Move Forward + S/↓ Move Backward + A Strafe Left + D Strafe Right + ←/β†’ Turn Left/Right + Space Shoot + P Pause/Resume + + + + + + + Features + + + 🎯 3D First-Person Shooter + πŸ‘Ή Kill demons and monsters + πŸ’Š Collect health packs + πŸ”« Find ammo pickups + πŸ† Survive and get high score! + + + + + +
+ + + + + + + Start Game + + + + + Pause + + + + + Stop + + + + + +
+ + Statistics + Health: 100 + Ammo: 50 + Kills: 0 + Score: 0 + + + + Controls: + W/↑ : Move Forward + S/↓ : Move Backward + A/D : Strafe + ←/β†’ : Turn + Space : Shoot + P : Pause/Resume + +
+
+
+
+
+
+ + + + πŸ›‘οΈ Virus Defense + + + + + + + + + Statistics + + + + Resources: 200 + + + System Health: 20 + + + Attack Wave: 0 + + + Score: 0 + + + Viruses Removed: 0 + + + + + + + + Deploy Security Tools + + + + + πŸ›‘οΈ Firewall (50G) + + + πŸ” IDS (75G) + + + 🦠 Antivirus (100G) + + + πŸ”’ Quarantine (150G) + + + + Uninstall Tool + + + + + + + + + How to Play + + + πŸ›‘οΈ Select a security tool + πŸ–±οΈ Click on grid to deploy + ⬆️ Click tool to patch/upgrade (Max Lvl 5) + πŸ’Ž Hover tool to see upgrade cost + πŸ—‘οΈ Use uninstall mode to remove tools + πŸ’° Earn resources by removing viruses + ⚠️ Don't let viruses reach the end! + + + + + + + Security Tools + + + πŸ›‘οΈ Firewall: Blocks threats + πŸ” IDS: Fast detection + 🦠 Antivirus: High damage scan + πŸ”’ Quarantine: Isolates multiple + + + πŸ’‘ Upgrade Cost = Base Cost Γ— 50% Γ— Level + + + πŸ’° Refund Value = 70% of total invested + + + + + + +
+ + + + + + + New Game + + + + + Pause + + + + + +
+ + Statistics + Resources: 200 + System Health: 20 + Attack Wave: 0 + Score: 0 + Viruses Removed: 0 + + + + Deploy Security Tools + + + πŸ›‘οΈ Firewall (50G) + + + πŸ” IDS (75G) + + + 🦠 Antivirus (100G) + + + πŸ”’ Quarantine (150G) + + + + Uninstall + + + + + + How to Play: + Select tool β†’ Click grid to deploy + Click tool to upgrade (Max Lvl 5) + Hover tool to see upgrade cost + Earn resources by removing viruses + + + + Security Tools: + πŸ›‘οΈ Firewall: Blocks threats + πŸ” IDS: Fast detection + 🦠 Antivirus: High damage + πŸ”’ Quarantine: Isolates multiple + +
+
+
+
+
+
+ +
+ + "@funFact" + +
+
+ +@code { + + private bool loading_overlay = false; + + private bool _isDarkMode; + + private string netlock_username = String.Empty; + public static List permissions_tenants_list = new List { }; + + private async Task Permissions() + { + try + { + bool logout = false; + + // Get the current user from the authentication state + var user = (await AuthenticationStateProvider.GetAuthenticationStateAsync()).User; + + // Check if user is authenticated + if (user?.Identity is not { IsAuthenticated: true }) + logout = true; + + netlock_username = user.FindFirst(ClaimTypes.Email)?.Value; + + permissions_tenants_list = await Classes.Authentication.Permissions.Get_Tenants(netlock_username, false); + + if (logout) // Redirect to the login page + { + NavigationManager.NavigateTo("/logout", true); + return false; + } + + // All fine? Nice. + return true; + } + catch (Exception ex) + { + Logging.Handler.Error("/dashboard -> Permissions", "Error", ex.ToString()); + return false; + } + } + + protected override async Task OnInitializedAsync() + { + await base.OnInitializedAsync(); + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + await AfterInitializedAsync(); + } + } + + private async Task AfterInitializedAsync() + { + loading_overlay = true; + StateHasChanged(); + + if (!await Permissions()) + return; + + _isDarkMode = await JSRuntime.InvokeAsync("isDarkMode"); + + await LoadProfile(); + + funFact = await Classes.Miscellaneous.FunFacts.GetRandomFact(); + + loading_overlay = false; + StateHasChanged(); + } + + private string firstName = String.Empty; + private string lastName = String.Empty; + private string email = String.Empty; + private string phone = String.Empty; + private string role = String.Empty; + + private string funFact = String.Empty; + + private async Task LoadProfile() + { + MySqlConnection conn = new MySqlConnection(Configuration.MySQL.Connection_String); + + try + { + await conn.OpenAsync(); + + string query = "SELECT * FROM accounts WHERE username = @netlock_username;"; + + MySqlCommand cmd = new MySqlCommand(query, conn); + cmd.Parameters.AddWithValue("@netlock_username", netlock_username); + + Logging.Handler.Debug("Example", "MySQL_Prepared_Query", query); + + using (DbDataReader reader = await cmd.ExecuteReaderAsync()) + { + if (reader.HasRows) + { + while (await reader.ReadAsync()) + { + email = reader["mail"].ToString() ?? String.Empty; + firstName = reader["first_name"].ToString() ?? String.Empty; + lastName = reader["last_name"].ToString() ?? String.Empty; + phone = reader["phone"].ToString() ?? String.Empty; + role = reader["role"].ToString() ?? String.Empty; + } + } + } + } + catch (Exception ex) + { + Logging.Handler.Error("Example", "MySQL_Query", ex.ToString()); + } + finally + { + await conn.CloseAsync(); + } + } + + private async Task UpdateProfile() + { + Snackbar.Configuration.ShowCloseIcon = true; + Snackbar.Configuration.PositionClass = Defaults.Classes.Position.BottomRight; + + MySqlConnection conn = new MySqlConnection(Configuration.MySQL.Connection_String); + + string query = "UPDATE accounts SET first_name = @first_name, last_name = @last_name, phone = @phone, mail = @mail WHERE username = @netlock_username;"; + + try + { + await conn.OpenAsync(); + + MySqlCommand cmd = new MySqlCommand(query, conn); + cmd.Parameters.AddWithValue("@first_name", firstName); + cmd.Parameters.AddWithValue("@last_name", lastName); + cmd.Parameters.AddWithValue("@phone", phone); + cmd.Parameters.AddWithValue("@mail", email); + cmd.Parameters.AddWithValue("@netlock_username", netlock_username); + + Logging.Handler.Debug("UpdateProfile", "MySQL_Prepared_Query", query); + + await cmd.ExecuteNonQueryAsync(); + + Snackbar.Add("Saved.", Severity.Success); + } + catch (Exception ex) + { + Logging.Handler.Error("UpdateProfile", "MySQL_Query", ex.ToString()); + } + finally + { + await conn.CloseAsync(); + } + } +} \ No newline at end of file diff --git a/Community Events Games/doom.js b/Community Events Games/doom.js new file mode 100644 index 00000000..c5c4dc5a --- /dev/null +++ b/Community Events Games/doom.js @@ -0,0 +1,1165 @@ +// DOOM-style 3D FPS Game +window.doom = (function() { + let canvas, ctx; + let gameRunning = false; + let gamePaused = false; + let animationId; + + // Player + let player = { + x: 5, + y: 5, + angle: 0, + health: 100, + ammo: 50, + score: 0, + kills: 0 + }; + + // Game state + let enemies = []; + let bullets = []; + let pickups = []; + let wallHits = []; + let lastTime = 0; + let lastSpawnTime = 0; + + // Map (1 = wall, 0 = empty) + const mapWidth = 16; + const mapHeight = 16; + let map = [ + [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], + [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1], + [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1], + [1,0,0,0,1,1,1,0,0,1,1,1,0,0,0,1], + [1,0,0,0,0,0,1,0,0,1,0,0,0,0,0,1], + [1,0,0,0,0,0,1,0,0,1,0,0,0,0,0,1], + [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1], + [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1], + [1,0,0,1,1,0,0,0,0,0,0,1,1,0,0,1], + [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1], + [1,0,0,0,0,0,1,0,0,1,0,0,0,0,0,1], + [1,0,0,0,0,0,1,0,0,1,0,0,0,0,0,1], + [1,0,0,0,1,1,1,0,0,1,1,1,0,0,0,1], + [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1], + [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1], + [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1] + ]; + + // Controls + const keys = {}; + + function init() { + canvas = document.getElementById('doomCanvas'); + if (!canvas) { + console.error('Canvas element not found'); + return; + } + ctx = canvas.getContext('2d'); + + // Setup keyboard controls + document.addEventListener('keydown', (e) => { + keys[e.key.toLowerCase()] = true; + if (e.key === ' ' && gameRunning && !gamePaused) { + shoot(); + e.preventDefault(); + } + if (e.key.toLowerCase() === 'p') { + togglePause(); + e.preventDefault(); + } + }); + + document.addEventListener('keyup', (e) => { + keys[e.key.toLowerCase()] = false; + }); + } + + function startGame() { + if (gameRunning) return; + + gameRunning = true; + gamePaused = false; + + // Reset player + player = { + x: 5, + y: 5, + angle: 0, + health: 150, + ammo: 50, + score: 0, + kills: 0, + maxHealth: 150 + }; + + // Reset game state + enemies = []; + bullets = []; + pickups = []; + wallHits = []; + enemyProjectiles = []; + lastAttacker = null; + lastSpawnTime = performance.now(); + + // Spawn enemies + spawnEnemies(); + + // Spawn pickups + spawnPickups(); + + updateStats(); + lastTime = performance.now(); + animationId = requestAnimationFrame(gameLoop); + } + + function stopGame() { + gameRunning = false; + gamePaused = false; + if (animationId) { + cancelAnimationFrame(animationId); + } + + // Clear canvas + ctx.fillStyle = '#000'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + // Show game over + ctx.fillStyle = '#fff'; + ctx.font = '48px Arial'; + ctx.textAlign = 'center'; + ctx.fillText('GAME OVER', canvas.width / 2, canvas.height / 2); + ctx.font = '24px Arial'; + ctx.fillText(`Final Score: ${player.score}`, canvas.width / 2, canvas.height / 2 + 40); + ctx.fillText(`Kills: ${player.kills}`, canvas.width / 2, canvas.height / 2 + 70); + } + + function togglePause() { + if (!gameRunning) return; + gamePaused = !gamePaused; + if (!gamePaused) { + lastTime = performance.now(); + animationId = requestAnimationFrame(gameLoop); + } + } + + function spawnEnemies() { + const positions = [ + {x: 12, y: 3}, + {x: 3, y: 12} + ]; + + positions.forEach(pos => { + enemies.push({ + x: pos.x + 0.5, + y: pos.y + 0.5, + health: 100, + speed: 0.005, + lastShot: 0, + shootCooldown: 5000, + damage: 3, + attacking: false, + attackTime: 0, + state: 'idle', // idle, alert, chase, attack + alertTime: 0, + wanderAngle: Math.random() * Math.PI * 2, + wanderChangeTime: 0, + lastSeenPlayerX: null, + lastSeenPlayerY: null, + detectionRange: 7, + attackRange: 3.5, + loseInterestRange: 12 + }); + }); + } + + function spawnNewEnemy() { + let attempts = 0; + let spawned = false; + + // Difficulty scaling based on kills + const difficultyMultiplier = 1 + (player.kills * 0.05); + const enemySpeed = Math.min(0.008, 0.005 + (player.kills * 0.0002)); + const enemyHealth = Math.min(150, 100 + (player.kills * 2)); + const enemyDamage = Math.min(5, 3 + Math.floor(player.kills / 5)); + + while (!spawned && attempts < 30) { + const newX = 2 + Math.random() * 12; + const newY = 2 + Math.random() * 12; + const testMapX = Math.floor(newX); + const testMapY = Math.floor(newY); + + // Make sure spawn location is far from player + const distToPlayer = Math.sqrt((newX - player.x) ** 2 + (newY - player.y) ** 2); + + if (testMapX >= 0 && testMapX < mapWidth && testMapY >= 0 && testMapY < mapHeight && + map[testMapY][testMapX] === 0 && distToPlayer > 6) { + enemies.push({ + x: newX, + y: newY, + health: enemyHealth, + speed: enemySpeed + Math.random() * 0.002, + lastShot: performance.now() + 3000, + shootCooldown: Math.max(3500, 5000 - (player.kills * 50)), + damage: enemyDamage, + attacking: false, + attackTime: 0, + state: 'idle', + alertTime: 0, + wanderAngle: Math.random() * Math.PI * 2, + wanderChangeTime: performance.now(), + lastSeenPlayerX: null, + lastSeenPlayerY: null, + detectionRange: Math.min(9, 7 + Math.floor(player.kills / 5)), + attackRange: 3.5, + loseInterestRange: 12 + }); + spawned = true; + console.log(`New demon spawned! Total: ${enemies.length}, Difficulty: ${difficultyMultiplier.toFixed(2)}x`); + } + attempts++; + } + + return spawned; + } + + function spawnPickups() { + // Health pickups + pickups.push({x: 2, y: 2, type: 'health'}); + pickups.push({x: 13, y: 13, type: 'health'}); + + // Ammo pickups + pickups.push({x: 13, y: 2, type: 'ammo'}); + pickups.push({x: 2, y: 13, type: 'ammo'}); + pickups.push({x: 8, y: 8, type: 'ammo'}); + } + + let lastShotTime = 0; + const shootCooldown = 250; // milliseconds between shots + + function shoot() { + const currentTime = performance.now(); + if (!gameRunning || player.ammo <= 0 || currentTime - lastShotTime < shootCooldown) return; + + // Validate player position + if (isNaN(player.x) || isNaN(player.y) || isNaN(player.angle)) { + console.error('Invalid player position or angle'); + return; + } + + player.ammo--; + lastShotTime = currentTime; + updateStats(); + + console.log('Shooting! Angle:', player.angle, 'Position:', player.x, player.y); + + // Create bullet starting slightly in front of player + const spawnDist = 0.3; + const bulletX = player.x + Math.cos(player.angle) * spawnDist; + const bulletY = player.y + Math.sin(player.angle) * spawnDist; + + // Validate bullet position + if (!isNaN(bulletX) && !isNaN(bulletY)) { + bullets.push({ + x: bulletX, + y: bulletY, + angle: player.angle, + speed: 0.01, + damage: 50, + lifetime: 0, + maxLifetime: 3000 + }); + } + + // Muzzle flash effect + muzzleFlashTime = currentTime; + } + + let muzzleFlashTime = 0; + + function updatePlayer(deltaTime) { + const moveSpeed = 0.002 * deltaTime; + const rotSpeed = 0.003 * deltaTime; + + // Rotation + if (keys['arrowleft']) { + player.angle -= rotSpeed; + } + if (keys['arrowright']) { + player.angle += rotSpeed; + } + + // Movement + let newX = player.x; + let newY = player.y; + + if (keys['arrowup'] || keys['w']) { + newX += Math.cos(player.angle) * moveSpeed; + newY += Math.sin(player.angle) * moveSpeed; + } + if (keys['arrowdown'] || keys['s']) { + newX -= Math.cos(player.angle) * moveSpeed; + newY -= Math.sin(player.angle) * moveSpeed; + } + + // Strafing + if (keys['a']) { + newX += Math.cos(player.angle - Math.PI / 2) * moveSpeed; + newY += Math.sin(player.angle - Math.PI / 2) * moveSpeed; + } + if (keys['d']) { + newX += Math.cos(player.angle + Math.PI / 2) * moveSpeed; + newY += Math.sin(player.angle + Math.PI / 2) * moveSpeed; + } + + // Collision detection with buffer + const buffer = 0.2; + const mapX = Math.floor(newX); + const mapY = Math.floor(newY); + + // Check the cell and surrounding cells for walls + let canMoveX = true; + let canMoveY = true; + + if (mapX >= 0 && mapX < mapWidth && mapY >= 0 && mapY < mapHeight) { + // Check X movement + const testMapX = Math.floor(newX); + const testMapYCurrent = Math.floor(player.y); + if (map[testMapYCurrent][testMapX] !== 0) { + canMoveX = false; + } + + // Check Y movement + const testMapXCurrent = Math.floor(player.x); + const testMapY = Math.floor(newY); + if (map[testMapY][testMapXCurrent] !== 0) { + canMoveY = false; + } + + // Check diagonal + if (map[mapY][mapX] !== 0) { + canMoveX = false; + canMoveY = false; + } + + if (canMoveX) { + player.x = newX; + } + if (canMoveY) { + player.y = newY; + } + } + + // Check pickup collision + for (let i = pickups.length - 1; i >= 0; i--) { + const pickup = pickups[i]; + const dx = player.x - pickup.x; + const dy = player.y - pickup.y; + const dist = Math.sqrt(dx * dx + dy * dy); + + if (dist < 0.5) { + if (pickup.type === 'health') { + player.health = Math.min(player.maxHealth, player.health + 30); + } else if (pickup.type === 'ammo') { + player.ammo += 20; + } + pickups.splice(i, 1); + updateStats(); + } + } + } + + function updateEnemies(deltaTime, currentTime) { + for (let i = enemies.length - 1; i >= 0; i--) { + const enemy = enemies[i]; + + // Calculate distance to player + const dx = player.x - enemy.x; + const dy = player.y - enemy.y; + const dist = Math.sqrt(dx * dx + dy * dy); + + // Check line of sight to player + const hasLineOfSight = checkLineOfSight(enemy.x, enemy.y, player.x, player.y); + + // State machine + switch(enemy.state) { + case 'idle': + // Wander around randomly + if (currentTime - enemy.wanderChangeTime > 2000) { + enemy.wanderAngle = Math.random() * Math.PI * 2; + enemy.wanderChangeTime = currentTime; + } + + // Move in wander direction + const wanderSpeed = enemy.speed * 0.5 * deltaTime; + const wanderX = enemy.x + Math.cos(enemy.wanderAngle) * wanderSpeed; + const wanderY = enemy.y + Math.sin(enemy.wanderAngle) * wanderSpeed; + + if (canMoveTo(enemy, wanderX, wanderY)) { + enemy.x = wanderX; + enemy.y = wanderY; + } else { + // Hit a wall, change direction + enemy.wanderAngle = Math.random() * Math.PI * 2; + } + + // Check if player is in detection range + if (dist < enemy.detectionRange && hasLineOfSight) { + enemy.state = 'alert'; + enemy.alertTime = currentTime; + } + break; + + case 'alert': + // Stop and look around for a moment + if (currentTime - enemy.alertTime > 500) { + if (dist < enemy.detectionRange && hasLineOfSight) { + enemy.state = 'chase'; + enemy.lastSeenPlayerX = player.x; + enemy.lastSeenPlayerY = player.y; + } else { + enemy.state = 'idle'; + } + } + break; + + case 'chase': + // Update last seen position if we can see player + if (hasLineOfSight && dist < enemy.loseInterestRange) { + enemy.lastSeenPlayerX = player.x; + enemy.lastSeenPlayerY = player.y; + } + + // Chase the player (or last seen position) + let targetX = enemy.lastSeenPlayerX || player.x; + let targetY = enemy.lastSeenPlayerY || player.y; + + const chaseDx = targetX - enemy.x; + const chaseDy = targetY - enemy.y; + const chaseDist = Math.sqrt(chaseDx * chaseDx + chaseDy * chaseDy); + + // Keep some distance, don't get too close + if (chaseDist > 2.5) { + const chaseSpeed = enemy.speed * deltaTime; + const moveX = (chaseDx / chaseDist) * chaseSpeed; + const moveY = (chaseDy / chaseDist) * chaseSpeed; + + const newX = enemy.x + moveX; + const newY = enemy.y + moveY; + + if (canMoveTo(enemy, newX, enemy.y)) { + enemy.x = newX; + } + if (canMoveTo(enemy, enemy.x, newY)) { + enemy.y = newY; + } + } + + // Enter attack range + if (dist < enemy.attackRange && hasLineOfSight) { + enemy.state = 'attack'; + } + + // Lose interest if player is too far + if (dist > enemy.loseInterestRange || (chaseDist < 0.5 && !hasLineOfSight)) { + enemy.state = 'idle'; + enemy.lastSeenPlayerX = null; + enemy.lastSeenPlayerY = null; + } + break; + + case 'attack': + // Stop moving and attack + enemy.attacking = false; + + // Show warning when about to attack (1.5 seconds before) + if (currentTime - enemy.lastShot > enemy.shootCooldown - 1500) { + enemy.attacking = true; + } + + // Fire! + if (currentTime - enemy.lastShot > enemy.shootCooldown) { + player.health -= enemy.damage; + enemy.lastShot = currentTime; + enemy.attackTime = currentTime; + enemy.attacking = false; + updateStats(); + + // Flash red on hit with attacker info + flashDamage(enemy); + + // Create attack projectile visual + createEnemyProjectile(enemy.x, enemy.y, player.x, player.y); + + if (player.health <= 0) { + stopGame(); + return; + } + + // After attacking, go back to chase + enemy.state = 'chase'; + } + + // If player moves out of range, chase again + if (dist > enemy.attackRange) { + enemy.state = 'chase'; + enemy.attacking = false; + } + + // Lose line of sight + if (!hasLineOfSight) { + enemy.state = 'chase'; + enemy.lastSeenPlayerX = player.x; + enemy.lastSeenPlayerY = player.y; + enemy.attacking = false; + } + break; + } + } + } + + // Helper function to check if enemy can move to a position + function canMoveTo(enemy, newX, newY) { + const buffer = 0.3; + const corners = [ + {x: newX - buffer, y: newY - buffer}, + {x: newX + buffer, y: newY - buffer}, + {x: newX - buffer, y: newY + buffer}, + {x: newX + buffer, y: newY + buffer} + ]; + + for (const corner of corners) { + const cx = Math.floor(corner.x); + const cy = Math.floor(corner.y); + if (cx < 0 || cx >= mapWidth || cy < 0 || cy >= mapHeight || map[cy][cx] !== 0) { + return false; + } + } + return true; + } + + // Helper function to check line of sight between two points + function checkLineOfSight(x1, y1, x2, y2) { + // Validate input + if (typeof x1 !== 'number' || typeof y1 !== 'number' || + typeof x2 !== 'number' || typeof y2 !== 'number' || + isNaN(x1) || isNaN(y1) || isNaN(x2) || isNaN(y2)) { + return false; + } + + const dx = x2 - x1; + const dy = y2 - y1; + const dist = Math.sqrt(dx * dx + dy * dy); + + // Handle zero distance + if (dist < 0.01) { + return true; + } + + const steps = Math.max(1, Math.floor(dist * 2)); + + for (let i = 0; i <= steps; i++) { + const t = i / steps; + const checkX = x1 + dx * t; + const checkY = y1 + dy * t; + const mapX = Math.floor(checkX); + const mapY = Math.floor(checkY); + + // Validate array bounds + if (mapX < 0 || mapX >= mapWidth || mapY < 0 || mapY >= mapHeight) { + return false; + } + + // Check if position is valid in map + if (!map[mapY] || map[mapY][mapX] === undefined || map[mapY][mapX] !== 0) { + return false; + } + } + return true; + } + + // Enemy projectiles for visual effect + let enemyProjectiles = []; + + function createEnemyProjectile(fromX, fromY, toX, toY) { + const angle = Math.atan2(toY - fromY, toX - fromX); + enemyProjectiles.push({ + x: fromX, + y: fromY, + angle: angle, + speed: 0.015, + life: 0, + maxLife: 500 + }); + } + + function updateEnemyProjectiles(deltaTime) { + for (let i = enemyProjectiles.length - 1; i >= 0; i--) { + const proj = enemyProjectiles[i]; + + proj.life += deltaTime; + if (proj.life > proj.maxLife) { + enemyProjectiles.splice(i, 1); + continue; + } + + proj.x += Math.cos(proj.angle) * proj.speed * deltaTime; + proj.y += Math.sin(proj.angle) * proj.speed * deltaTime; + } + } + + let damageFlashTime = 0; + let lastAttacker = null; + + function flashDamage(attacker) { + damageFlashTime = performance.now(); + lastAttacker = attacker ? {x: attacker.x, y: attacker.y} : null; + } + + function updateBullets(deltaTime) { + for (let i = bullets.length - 1; i >= 0; i--) { + const bullet = bullets[i]; + + // Update lifetime + bullet.lifetime += deltaTime; + if (bullet.lifetime > bullet.maxLifetime) { + bullets.splice(i, 1); + continue; + } + + // Move bullet + bullet.x += Math.cos(bullet.angle) * bullet.speed * deltaTime; + bullet.y += Math.sin(bullet.angle) * bullet.speed * deltaTime; + + let bulletRemoved = false; + + // Check enemy collision FIRST (before wall check) + for (let j = enemies.length - 1; j >= 0; j--) { + const enemy = enemies[j]; + const dx = bullet.x - enemy.x; + const dy = bullet.y - enemy.y; + const dist = Math.sqrt(dx * dx + dy * dy); + + // Larger hitbox for better hit detection + if (dist < 0.5) { + enemy.health -= bullet.damage; + console.log(`Hit enemy! Health: ${enemy.health}`); + bullets.splice(i, 1); + bulletRemoved = true; + + if (enemy.health <= 0) { + console.log('Enemy killed!'); + enemies.splice(j, 1); + player.kills++; + player.score += 100; + updateStats(); + + // Dynamic max enemies based on kills (increases difficulty) + const maxEnemies = Math.min(8, 3 + Math.floor(player.kills / 3)); + + // Spawn new enemy after a delay + if (enemies.length < maxEnemies) { + const spawnDelay = Math.max(2000, 4000 - (player.kills * 100)); // Faster spawning as game progresses + + setTimeout(() => { + if (gameRunning && enemies.length < maxEnemies) { + spawnNewEnemy(); + } + }, spawnDelay); + } + } + break; + } + } + + if (bulletRemoved) continue; + + // Check wall collision + const mapX = Math.floor(bullet.x); + const mapY = Math.floor(bullet.y); + + if (mapX < 0 || mapX >= mapWidth || mapY < 0 || mapY >= mapHeight || map[mapY][mapX] === 1) { + wallHits.push({x: bullet.x, y: bullet.y, time: performance.now()}); + bullets.splice(i, 1); + } + } + + // Clean old wall hits + wallHits = wallHits.filter(hit => performance.now() - hit.time < 100); + } + + function render() { + const screenWidth = canvas.width; + const screenHeight = canvas.height; + const currentTime = performance.now(); + + // Draw floor and ceiling + ctx.fillStyle = '#333'; + ctx.fillRect(0, 0, screenWidth, screenHeight / 2); + ctx.fillStyle = '#222'; + ctx.fillRect(0, screenHeight / 2, screenWidth, screenHeight / 2); + + // Raycasting + const fov = Math.PI / 3; + const numRays = screenWidth; + const maxDepth = 20; + + const sprites = []; + + for (let ray = 0; ray < numRays; ray++) { + const rayAngle = player.angle - fov / 2 + (ray / numRays) * fov; + + let distanceToWall = 0; + let hitWall = false; + let wallType = 0; + + const eyeX = Math.cos(rayAngle); + const eyeY = Math.sin(rayAngle); + + while (!hitWall && distanceToWall < maxDepth) { + distanceToWall += 0.1; + + const testX = player.x + eyeX * distanceToWall; + const testY = player.y + eyeY * distanceToWall; + + const mapX = Math.floor(testX); + const mapY = Math.floor(testY); + + if (mapX < 0 || mapX >= mapWidth || mapY < 0 || mapY >= mapHeight) { + hitWall = true; + distanceToWall = maxDepth; + } else if (map[mapY][mapX] === 1) { + hitWall = true; + } + } + + // Calculate wall height + const correctedDistance = distanceToWall * Math.cos(rayAngle - player.angle); + const wallHeight = (screenHeight / correctedDistance) * 0.5; + + // Draw wall slice + const ceiling = (screenHeight / 2) - wallHeight; + const floor = (screenHeight / 2) + wallHeight; + + // Wall shading based on distance + const brightness = Math.max(0, 255 - (correctedDistance * 20)); + ctx.fillStyle = `rgb(${brightness}, ${brightness * 0.5}, ${brightness * 0.5})`; + ctx.fillRect(ray, ceiling, 1, floor - ceiling); + + // Add wall edge darkening + if (distanceToWall < maxDepth) { + const wallEdgeDarkness = Math.abs(Math.sin(distanceToWall * 5)) * 20; + ctx.fillStyle = `rgba(0, 0, 0, ${wallEdgeDarkness / 255})`; + ctx.fillRect(ray, ceiling, 1, floor - ceiling); + } + } + + // Collect all sprites with their properties + const allSprites = [ + ...enemies.map(e => ({...e, type: 'enemy'})), + ...pickups, + ...bullets.map(b => ({x: b.x, y: b.y, type: 'bullet'})), + ...enemyProjectiles.map(p => ({x: p.x, y: p.y, type: 'enemy_projectile'})) + ]; + + // Draw enemies, pickups, and bullets as sprites + allSprites.forEach(obj => { + // Validate object has coordinates + if (typeof obj.x !== 'number' || typeof obj.y !== 'number' || + isNaN(obj.x) || isNaN(obj.y)) { + return; + } + + const dx = obj.x - player.x; + const dy = obj.y - player.y; + const dist = Math.sqrt(dx * dx + dy * dy); + const angle = Math.atan2(dy, dx) - player.angle; + + // Check if object is in front of player + if (Math.cos(angle) > 0) { + // Check line of sight - only render if visible (not behind walls) + const hasLineOfSight = checkLineOfSight(player.x, player.y, obj.x, obj.y); + + if (hasLineOfSight) { + sprites.push({ + dist: dist, + angle: angle, + type: obj.type || 'enemy', + health: obj.health, + x: obj.x, + y: obj.y + }); + } + } + }); + + // Sort sprites by distance (far to near) + sprites.sort((a, b) => b.dist - a.dist); + + // Draw sprites + sprites.forEach(sprite => { + const correctedDist = sprite.dist * Math.cos(sprite.angle); + const spriteHeight = (screenHeight / correctedDist) * 0.5; + const spriteWidth = spriteHeight; + + const spriteX = (screenWidth / 2) + (Math.tan(sprite.angle) * screenWidth); + + if (spriteX > -spriteWidth && spriteX < screenWidth + spriteWidth) { + const brightness = Math.max(0, 255 - (correctedDist * 20)); + + if (sprite.type === 'enemy') { + // Find the actual enemy object to check attack state + const enemyObj = enemies.find(e => + sprite.x !== undefined && sprite.y !== undefined && + Math.abs(e.x - sprite.x) < 0.01 && Math.abs(e.y - sprite.y) < 0.01 + ); + const isAttacking = enemyObj && enemyObj.attacking; + const justAttacked = enemyObj && (currentTime - enemyObj.attackTime < 200); + const enemyState = enemyObj ? enemyObj.state : 'idle'; + + // Draw glow when attacking + if (isAttacking) { + ctx.fillStyle = `rgba(255, 100, 0, ${0.3 * Math.sin(currentTime / 100)})`; + ctx.fillRect(spriteX - spriteWidth * 0.6, screenHeight / 2 - spriteHeight * 0.6, spriteWidth * 1.2, spriteHeight * 1.2); + } + + // Draw alert glow + if (enemyState === 'alert') { + ctx.fillStyle = `rgba(255, 255, 0, ${0.2 * Math.sin(currentTime / 150)})`; + ctx.fillRect(spriteX - spriteWidth * 0.6, screenHeight / 2 - spriteHeight * 0.6, spriteWidth * 1.2, spriteHeight * 1.2); + } + + // Draw enemy body with color based on state + let bodyColor; + switch(enemyState) { + case 'idle': + bodyColor = `rgb(${brightness * 0.3}, ${brightness * 0.6}, ${brightness * 0.3})`; // Green + break; + case 'alert': + bodyColor = `rgb(${brightness * 0.6}, ${brightness * 0.6}, ${brightness * 0.3})`; // Yellow-green + break; + case 'chase': + bodyColor = `rgb(${brightness * 0.6}, ${brightness * 0.5}, ${brightness * 0.3})`; // Orange-green + break; + case 'attack': + bodyColor = `rgb(${brightness * 0.8}, ${brightness * 0.3}, ${brightness * 0.3})`; // Red + break; + default: + bodyColor = `rgb(${brightness * 0.3}, ${brightness * 0.8}, ${brightness * 0.3})`; + } + ctx.fillStyle = bodyColor; + ctx.fillRect(spriteX - spriteWidth / 2, screenHeight / 2 - spriteHeight / 2, spriteWidth, spriteHeight); + + // Draw muzzle flash when just attacked + if (justAttacked) { + const flashSize = spriteWidth * 0.5; + const flashAlpha = 1 - ((currentTime - enemyObj.attackTime) / 200); + ctx.fillStyle = `rgba(255, 200, 0, ${flashAlpha})`; + ctx.beginPath(); + ctx.arc(spriteX, screenHeight / 2, flashSize, 0, Math.PI * 2); + ctx.fill(); + } + + // Draw eyes (glow red when attacking) + if (isAttacking) { + ctx.fillStyle = `rgb(255, ${100 * Math.sin(currentTime / 100)}, 0)`; + ctx.shadowColor = 'red'; + ctx.shadowBlur = 10; + } else { + ctx.fillStyle = 'red'; + ctx.shadowBlur = 0; + } + const eyeSize = spriteWidth / 6; + ctx.fillRect(spriteX - spriteWidth / 4, screenHeight / 2 - spriteHeight / 4, eyeSize, eyeSize); + ctx.fillRect(spriteX + spriteWidth / 12, screenHeight / 2 - spriteHeight / 4, eyeSize, eyeSize); + ctx.shadowBlur = 0; + + // Draw health bar above enemy + if (sprite.health !== undefined && sprite.health < 100) { + const barWidth = spriteWidth * 0.8; + const barHeight = 5; + const barY = screenHeight / 2 - spriteHeight / 2 - 10; + + // Background + ctx.fillStyle = 'rgba(100, 0, 0, 0.8)'; + ctx.fillRect(spriteX - barWidth / 2, barY, barWidth, barHeight); + + // Health + ctx.fillStyle = 'rgba(0, 255, 0, 0.8)'; + ctx.fillRect(spriteX - barWidth / 2, barY, barWidth * (sprite.health / 100), barHeight); + } + + // Draw warning indicator when attacking + if (isAttacking) { + ctx.fillStyle = 'rgba(255, 0, 0, 0.8)'; + ctx.font = 'bold 20px Arial'; + ctx.textAlign = 'center'; + ctx.fillText('!', spriteX, screenHeight / 2 - spriteHeight / 2 - 20); + } + } else if (sprite.type === 'health') { + // Draw health pickup + ctx.fillStyle = `rgb(${brightness}, ${brightness * 0.3}, ${brightness * 0.3})`; + ctx.fillRect(spriteX - spriteWidth / 4, screenHeight / 2 - spriteHeight / 4, spriteWidth / 2, spriteHeight / 2); + ctx.fillRect(spriteX - spriteWidth / 8, screenHeight / 2 - spriteHeight / 2, spriteWidth / 4, spriteHeight); + } else if (sprite.type === 'ammo') { + // Draw ammo pickup + ctx.fillStyle = `rgb(${brightness * 0.8}, ${brightness * 0.8}, ${brightness * 0.3})`; + ctx.fillRect(spriteX - spriteWidth / 4, screenHeight / 2 - spriteHeight / 4, spriteWidth / 2, spriteHeight / 2); + } else if (sprite.type === 'bullet') { + // Draw bullet as yellow projectile + ctx.fillStyle = `rgb(${brightness}, ${brightness}, 0)`; + const bulletSize = Math.max(3, spriteWidth / 4); + ctx.fillRect(spriteX - bulletSize / 2, screenHeight / 2 - bulletSize / 2, bulletSize, bulletSize); + } else if (sprite.type === 'enemy_projectile') { + // Draw enemy projectile as red plasma ball + const projSize = Math.max(4, spriteWidth / 3); + + // Outer glow + ctx.fillStyle = `rgba(255, 0, 0, ${0.3 * brightness / 255})`; + ctx.beginPath(); + ctx.arc(spriteX, screenHeight / 2, projSize * 1.5, 0, Math.PI * 2); + ctx.fill(); + + // Inner core + ctx.fillStyle = `rgb(${brightness}, ${brightness * 0.2}, 0)`; + ctx.beginPath(); + ctx.arc(spriteX, screenHeight / 2, projSize, 0, Math.PI * 2); + ctx.fill(); + } + } + }); + + // Draw HUD + drawHUD(); + + // Draw damage flash with directional indicator + if (currentTime - damageFlashTime < 200) { + const alpha = 0.3 * (1 - (currentTime - damageFlashTime) / 200); + ctx.fillStyle = `rgba(255, 0, 0, ${alpha})`; + ctx.fillRect(0, 0, screenWidth, screenHeight); + + // Draw attack direction indicators on screen edges + if (lastAttacker) { + const dx = lastAttacker.x - player.x; + const dy = lastAttacker.y - player.y; + const angle = Math.atan2(dy, dx) - player.angle; + + // Normalize angle + let normalizedAngle = angle; + while (normalizedAngle < -Math.PI) normalizedAngle += Math.PI * 2; + while (normalizedAngle > Math.PI) normalizedAngle -= Math.PI * 2; + + // Draw arrow on screen edge + const arrowSize = 30; + const edgeMargin = 50; + + if (normalizedAngle > -Math.PI/4 && normalizedAngle < Math.PI/4) { + // Right + ctx.fillStyle = `rgba(255, 0, 0, ${alpha * 2})`; + ctx.beginPath(); + ctx.moveTo(screenWidth - edgeMargin, screenHeight / 2); + ctx.lineTo(screenWidth - edgeMargin - arrowSize, screenHeight / 2 - arrowSize / 2); + ctx.lineTo(screenWidth - edgeMargin - arrowSize, screenHeight / 2 + arrowSize / 2); + ctx.fill(); + } else if (normalizedAngle > Math.PI/4 && normalizedAngle < 3*Math.PI/4) { + // Bottom + ctx.fillStyle = `rgba(255, 0, 0, ${alpha * 2})`; + ctx.beginPath(); + ctx.moveTo(screenWidth / 2, screenHeight - edgeMargin); + ctx.lineTo(screenWidth / 2 - arrowSize / 2, screenHeight - edgeMargin - arrowSize); + ctx.lineTo(screenWidth / 2 + arrowSize / 2, screenHeight - edgeMargin - arrowSize); + ctx.fill(); + } else if (normalizedAngle < -Math.PI/4 && normalizedAngle > -3*Math.PI/4) { + // Top + ctx.fillStyle = `rgba(255, 0, 0, ${alpha * 2})`; + ctx.beginPath(); + ctx.moveTo(screenWidth / 2, edgeMargin); + ctx.lineTo(screenWidth / 2 - arrowSize / 2, edgeMargin + arrowSize); + ctx.lineTo(screenWidth / 2 + arrowSize / 2, edgeMargin + arrowSize); + ctx.fill(); + } else { + // Left + ctx.fillStyle = `rgba(255, 0, 0, ${alpha * 2})`; + ctx.beginPath(); + ctx.moveTo(edgeMargin, screenHeight / 2); + ctx.lineTo(edgeMargin + arrowSize, screenHeight / 2 - arrowSize / 2); + ctx.lineTo(edgeMargin + arrowSize, screenHeight / 2 + arrowSize / 2); + ctx.fill(); + } + } + } + + // Draw muzzle flash + if (currentTime - muzzleFlashTime < 100) { + const flashAlpha = 0.5 * (1 - (currentTime - muzzleFlashTime) / 100); + ctx.fillStyle = `rgba(255, 255, 0, ${flashAlpha})`; + ctx.beginPath(); + ctx.arc(screenWidth / 2, screenHeight / 2, 30, 0, Math.PI * 2); + ctx.fill(); + } + + // Draw crosshair + const crosshairColor = player.ammo > 0 ? '#0f0' : '#f00'; + ctx.strokeStyle = crosshairColor; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.moveTo(screenWidth / 2 - 10, screenHeight / 2); + ctx.lineTo(screenWidth / 2 + 10, screenHeight / 2); + ctx.moveTo(screenWidth / 2, screenHeight / 2 - 10); + ctx.lineTo(screenWidth / 2, screenHeight / 2 + 10); + ctx.stroke(); + + // Draw pause overlay + if (gamePaused) { + ctx.fillStyle = 'rgba(0, 0, 0, 0.7)'; + ctx.fillRect(0, 0, screenWidth, screenHeight); + ctx.fillStyle = '#fff'; + ctx.font = '48px Arial'; + ctx.textAlign = 'center'; + ctx.fillText('PAUSED', screenWidth / 2, screenHeight / 2); + ctx.font = '24px Arial'; + ctx.fillText('Press P to resume', screenWidth / 2, screenHeight / 2 + 40); + } + } + + function drawHUD() { + const padding = 10; + + // Health bar background + ctx.fillStyle = '#400'; + ctx.fillRect(padding, canvas.height - 45, 220, 35); + + // Health bar fill + let healthColor = '#0f0'; + const healthPercent = (player.health / player.maxHealth) * 100; + if (healthPercent < 25) { + healthColor = '#f00'; + } else if (healthPercent < 50) { + healthColor = '#ff0'; + } + ctx.fillStyle = healthColor; + ctx.fillRect(padding + 3, canvas.height - 42, (player.health / player.maxHealth) * 214, 29); + + // Health bar border + ctx.strokeStyle = '#fff'; + ctx.lineWidth = 2; + ctx.strokeRect(padding, canvas.height - 45, 220, 35); + + // Health text + ctx.fillStyle = '#fff'; + ctx.font = 'bold 18px Arial'; + ctx.textAlign = 'left'; + ctx.shadowColor = '#000'; + ctx.shadowBlur = 4; + ctx.fillText(`HEALTH: ${Math.max(0, player.health)}`, padding + 8, canvas.height - 20); + ctx.shadowBlur = 0; + + // Low health warning + if ((player.health / player.maxHealth) < 0.25) { + const flash = Math.sin(performance.now() / 200) > 0; + if (flash) { + ctx.fillStyle = '#f00'; + ctx.font = 'bold 24px Arial'; + ctx.textAlign = 'center'; + ctx.shadowBlur = 6; + ctx.fillText('LOW HEALTH!', canvas.width / 2, 40); + ctx.shadowBlur = 0; + } + } + + // Ammo + ctx.fillStyle = player.ammo > 10 ? '#fff' : '#f00'; + ctx.font = 'bold 24px Arial'; + ctx.textAlign = 'right'; + ctx.shadowColor = '#000'; + ctx.shadowBlur = 4; + ctx.fillText(`AMMO: ${player.ammo}`, canvas.width - padding, canvas.height - 20); + ctx.shadowBlur = 0; + + // Score + ctx.fillStyle = '#fff'; + ctx.textAlign = 'right'; + ctx.shadowBlur = 4; + ctx.fillText(`SCORE: ${player.score}`, canvas.width - padding, padding + 25); + ctx.fillText(`KILLS: ${player.kills}`, canvas.width - padding, padding + 55); + ctx.shadowBlur = 0; + + // Enemy count and wave info + ctx.fillStyle = '#ff0'; + ctx.font = 'bold 20px Arial'; + ctx.textAlign = 'left'; + ctx.shadowBlur = 4; + ctx.fillText(`DEMONS: ${enemies.length}`, padding, padding + 25); + + // Wave/Difficulty indicator + const wave = 1 + Math.floor(player.kills / 3); + ctx.fillStyle = '#f80'; + ctx.font = 'bold 18px Arial'; + ctx.fillText(`WAVE: ${wave}`, padding, padding + 50); + ctx.shadowBlur = 0; + } + + function updateStats() { + // Update desktop stats + updateElement('doom-health', player.health); + updateElement('doom-ammo', player.ammo); + updateElement('doom-kills', player.kills); + updateElement('doom-score', player.score); + + // Update mobile stats + updateElement('doom-health-mobile', player.health); + updateElement('doom-ammo-mobile', player.ammo); + updateElement('doom-kills-mobile', player.kills); + updateElement('doom-score-mobile', player.score); + } + + function updateElement(id, value) { + const element = document.getElementById(id); + if (element) { + element.textContent = value; + } + } + + function gameLoop(currentTime) { + if (!gameRunning || gamePaused) return; + + // Calculate delta time (capped to avoid huge jumps) + const deltaTime = Math.min(currentTime - lastTime, 100); + lastTime = currentTime; + + // Update game state - these should run every frame + updatePlayer(deltaTime); + updateEnemies(deltaTime, currentTime); + updateBullets(deltaTime); + updateEnemyProjectiles(deltaTime); + + // Periodic enemy spawning (every 15-30 seconds, faster as game progresses) + const maxEnemies = Math.min(8, 3 + Math.floor(player.kills / 3)); + const spawnInterval = Math.max(15000, 30000 - (player.kills * 500)); // Faster spawning over time + + if (currentTime - lastSpawnTime > spawnInterval && enemies.length < maxEnemies) { + if (spawnNewEnemy()) { + lastSpawnTime = currentTime; + } + } + // Render + render(); + + // Continue loop + animationId = requestAnimationFrame(gameLoop); + } + + // Initialize on load + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + init(); + } + + return { + startGame, + stopGame, + togglePause + }; +})(); + diff --git a/Community Events Games/pacman.js b/Community Events Games/pacman.js new file mode 100644 index 00000000..42469bcd --- /dev/null +++ b/Community Events Games/pacman.js @@ -0,0 +1,540 @@ +// Pac-Man Game Implementation +window.pacman = (function() { + let canvas, ctx; + let gameLoop = null; + let lastTime = 0; + let isPaused = false; + let isGameOver = false; + + // Game state + let score = 0; + let lives = 3; + let level = 1; + let pelletsEaten = 0; + let totalPellets = 0; + + // Grid settings + const TILE_SIZE = 20; + const COLS = 28; + const ROWS = 31; + const CANVAS_WIDTH = COLS * TILE_SIZE; + const CANVAS_HEIGHT = ROWS * TILE_SIZE; + + // Pac-Man + let pacman = { + x: 14, + y: 23, + direction: 0, // 0: right, 1: down, 2: left, 3: up + nextDirection: 0, + speed: 0.15, + mouthOpen: 0, + mouthSpeed: 0.3 + }; + + // Ghosts + let ghosts = []; + const ghostColors = ['#FF0000', '#FFB8FF', '#00FFFF', '#FFB852']; + const ghostNames = ['Blinky', 'Pinky', 'Inky', 'Clyde']; + + // Power mode + let powerMode = false; + let powerModeTimer = 0; + const powerModeDuration = 200; + + // Controls + let keys = {}; + + // Maze (1 = wall, 0 = empty, 2 = pellet, 3 = power pellet) + let maze = []; + + // Direction vectors + const directions = [ + { x: 1, y: 0 }, // right + { x: 0, y: 1 }, // down + { x: -1, y: 0 }, // left + { x: 0, y: -1 } // up + ]; + + function init() { + canvas = document.getElementById('pacmanCanvas'); + if (!canvas) { + console.error('Canvas element not found'); + return false; + } + ctx = canvas.getContext('2d'); + + // Setup keyboard controls + document.addEventListener('keydown', (e) => { + keys[e.key] = true; + + if (e.key === 'p' || e.key === 'P') { + togglePause(); + e.preventDefault(); + } + + if (!isPaused && !isGameOver) { + switch(e.key) { + case 'ArrowRight': + pacman.nextDirection = 0; + e.preventDefault(); + break; + case 'ArrowDown': + pacman.nextDirection = 1; + e.preventDefault(); + break; + case 'ArrowLeft': + pacman.nextDirection = 2; + e.preventDefault(); + break; + case 'ArrowUp': + pacman.nextDirection = 3; + e.preventDefault(); + break; + } + } + }); + + document.addEventListener('keyup', (e) => { + keys[e.key] = false; + }); + + return true; + } + + function createMaze() { + // Classic Pac-Man inspired maze + maze = [ + [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], + [1,2,2,2,2,2,2,2,2,2,2,2,2,1,1,2,2,2,2,2,2,2,2,2,2,2,2,1], + [1,2,1,1,1,1,2,1,1,1,1,1,2,1,1,2,1,1,1,1,1,2,1,1,1,1,2,1], + [1,3,1,1,1,1,2,1,1,1,1,1,2,1,1,2,1,1,1,1,1,2,1,1,1,1,3,1], + [1,2,1,1,1,1,2,1,1,1,1,1,2,1,1,2,1,1,1,1,1,2,1,1,1,1,2,1], + [1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1], + [1,2,1,1,1,1,2,1,1,2,1,1,1,1,1,1,1,1,2,1,1,2,1,1,1,1,2,1], + [1,2,1,1,1,1,2,1,1,2,1,1,1,1,1,1,1,1,2,1,1,2,1,1,1,1,2,1], + [1,2,2,2,2,2,2,1,1,2,2,2,2,1,1,2,2,2,2,1,1,2,2,2,2,2,2,1], + [1,1,1,1,1,1,2,1,1,1,1,1,0,1,1,0,1,1,1,1,1,2,1,1,1,1,1,1], + [1,1,1,1,1,1,2,1,1,1,1,1,0,1,1,0,1,1,1,1,1,2,1,1,1,1,1,1], + [1,1,1,1,1,1,2,1,1,0,0,0,0,0,0,0,0,0,0,1,1,2,1,1,1,1,1,1], + [1,1,1,1,1,1,2,1,1,0,1,1,1,0,0,1,1,1,0,1,1,2,1,1,1,1,1,1], + [1,1,1,1,1,1,2,1,1,0,1,0,0,0,0,0,0,1,0,1,1,2,1,1,1,1,1,1], + [0,0,0,0,0,0,2,0,0,0,1,0,0,0,0,0,0,1,0,0,0,2,0,0,0,0,0,0], + [1,1,1,1,1,1,2,1,1,0,1,0,0,0,0,0,0,1,0,1,1,2,1,1,1,1,1,1], + [1,1,1,1,1,1,2,1,1,0,1,1,1,1,1,1,1,1,0,1,1,2,1,1,1,1,1,1], + [1,1,1,1,1,1,2,1,1,0,0,0,0,0,0,0,0,0,0,1,1,2,1,1,1,1,1,1], + [1,1,1,1,1,1,2,1,1,0,1,1,1,1,1,1,1,1,0,1,1,2,1,1,1,1,1,1], + [1,1,1,1,1,1,2,1,1,0,1,1,1,1,1,1,1,1,0,1,1,2,1,1,1,1,1,1], + [1,2,2,2,2,2,2,2,2,2,2,2,2,1,1,2,2,2,2,2,2,2,2,2,2,2,2,1], + [1,2,1,1,1,1,2,1,1,1,1,1,2,1,1,2,1,1,1,1,1,2,1,1,1,1,2,1], + [1,2,1,1,1,1,2,1,1,1,1,1,2,1,1,2,1,1,1,1,1,2,1,1,1,1,2,1], + [1,3,2,2,1,1,2,2,2,2,2,2,2,0,0,2,2,2,2,2,2,2,1,1,2,2,3,1], + [1,1,1,2,1,1,2,1,1,2,1,1,1,1,1,1,1,1,2,1,1,2,1,1,2,1,1,1], + [1,1,1,2,1,1,2,1,1,2,1,1,1,1,1,1,1,1,2,1,1,2,1,1,2,1,1,1], + [1,2,2,2,2,2,2,1,1,2,2,2,2,1,1,2,2,2,2,1,1,2,2,2,2,2,2,1], + [1,2,1,1,1,1,1,1,1,1,1,1,2,1,1,2,1,1,1,1,1,1,1,1,1,1,2,1], + [1,2,1,1,1,1,1,1,1,1,1,1,2,1,1,2,1,1,1,1,1,1,1,1,1,1,2,1], + [1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1], + [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1] + ]; + + // Count pellets + totalPellets = 0; + for (let y = 0; y < ROWS; y++) { + for (let x = 0; x < COLS; x++) { + if (maze[y][x] === 2 || maze[y][x] === 3) { + totalPellets++; + } + } + } + } + + function startGame() { + if (!init()) { + setTimeout(startGame, 100); + return; + } + + // Reset game state + score = 0; + lives = 3; + level = 1; + pelletsEaten = 0; + isPaused = false; + isGameOver = false; + powerMode = false; + powerModeTimer = 0; + + createMaze(); + + // Initialize Pac-Man + pacman.x = 14; + pacman.y = 23; + pacman.direction = 0; + pacman.nextDirection = 0; + pacman.mouthOpen = 0; + + // Initialize ghosts + ghosts = []; + const startPositions = [ + { x: 13, y: 11 }, + { x: 14, y: 11 }, + { x: 13, y: 14 }, + { x: 14, y: 14 } + ]; + + for (let i = 0; i < 4; i++) { + ghosts.push({ + x: startPositions[i].x, + y: startPositions[i].y, + direction: Math.floor(Math.random() * 4), + color: ghostColors[i], + name: ghostNames[i], + speed: 0.1 - i * 0.01, + scared: false + }); + } + + updateScore(); + + if (gameLoop) { + cancelAnimationFrame(gameLoop); + } + + lastTime = 0; + gameLoop = requestAnimationFrame(update); + } + + function update(time = 0) { + if (isGameOver) { + drawGameOver(); + return; + } + + const deltaTime = time - lastTime; + lastTime = time; + + if (!isPaused) { + // Update power mode + if (powerMode) { + powerModeTimer--; + if (powerModeTimer <= 0) { + powerMode = false; + ghosts.forEach(ghost => ghost.scared = false); + } + } + + // Try to change direction + const nextDir = directions[pacman.nextDirection]; + const nextX = pacman.x + nextDir.x * pacman.speed; + const nextY = pacman.y + nextDir.y * pacman.speed; + + if (canMove(nextX, nextY)) { + pacman.direction = pacman.nextDirection; + } + + // Move Pac-Man + const dir = directions[pacman.direction]; + let newX = pacman.x + dir.x * pacman.speed; + let newY = pacman.y + dir.y * pacman.speed; + + // Tunnel wrap + if (newX < 0) newX = COLS - 1; + if (newX >= COLS) newX = 0; + + if (canMove(newX, newY)) { + pacman.x = newX; + pacman.y = newY; + } + + // Animate mouth + pacman.mouthOpen += pacman.mouthSpeed; + if (pacman.mouthOpen > 1) { + pacman.mouthOpen = 0; + } + + // Check pellet collision + const tileX = Math.round(pacman.x); + const tileY = Math.round(pacman.y); + + if (maze[tileY] && maze[tileY][tileX] === 2) { + maze[tileY][tileX] = 0; + score += 10; + pelletsEaten++; + } else if (maze[tileY] && maze[tileY][tileX] === 3) { + maze[tileY][tileX] = 0; + score += 50; + pelletsEaten++; + powerMode = true; + powerModeTimer = powerModeDuration; + ghosts.forEach(ghost => ghost.scared = true); + } + + // Check win condition + if (pelletsEaten >= totalPellets) { + level++; + pelletsEaten = 0; + createMaze(); + pacman.x = 14; + pacman.y = 23; + pacman.speed = Math.min(0.25, 0.15 + level * 0.02); + ghosts.forEach(ghost => { + ghost.speed = Math.min(0.18, 0.1 + level * 0.015); + }); + } + + // Move ghosts + ghosts.forEach(ghost => { + if (Math.random() < 0.02) { + ghost.direction = Math.floor(Math.random() * 4); + } + + const ghostDir = directions[ghost.direction]; + let ghostNewX = ghost.x + ghostDir.x * ghost.speed; + let ghostNewY = ghost.y + ghostDir.y * ghost.speed; + + // Tunnel wrap + if (ghostNewX < 0) ghostNewX = COLS - 1; + if (ghostNewX >= COLS) ghostNewX = 0; + + if (canMove(ghostNewX, ghostNewY)) { + ghost.x = ghostNewX; + ghost.y = ghostNewY; + } else { + ghost.direction = Math.floor(Math.random() * 4); + } + }); + + // Check ghost collision + ghosts.forEach((ghost, index) => { + const dist = Math.sqrt( + Math.pow(pacman.x - ghost.x, 2) + + Math.pow(pacman.y - ghost.y, 2) + ); + + if (dist < 0.6) { + if (powerMode && ghost.scared) { + // Eat ghost + score += 200; + ghost.x = 14; + ghost.y = 14; + ghost.scared = false; + } else if (!ghost.scared) { + // Lose life + lives--; + if (lives <= 0) { + isGameOver = true; + } else { + // Reset positions + pacman.x = 14; + pacman.y = 23; + ghosts.forEach((g, i) => { + g.x = 13 + (i % 2); + g.y = 11 + Math.floor(i / 2) * 3; + }); + } + } + } + }); + } + + draw(); + updateScore(); + gameLoop = requestAnimationFrame(update); + } + + function canMove(x, y) { + const tileX = Math.round(x); + const tileY = Math.round(y); + + if (tileY < 0 || tileY >= ROWS || tileX < 0 || tileX >= COLS) { + return false; + } + + return maze[tileY][tileX] !== 1; + } + + function draw() { + // Clear canvas + ctx.fillStyle = '#000'; + ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); + + // Draw maze + for (let y = 0; y < ROWS; y++) { + for (let x = 0; x < COLS; x++) { + const tile = maze[y][x]; + + if (tile === 1) { + // Wall + ctx.fillStyle = '#2121DE'; + ctx.fillRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE); + ctx.strokeStyle = '#1919B4'; + ctx.strokeRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE); + } else if (tile === 2) { + // Pellet + ctx.fillStyle = '#FFB8AE'; + ctx.beginPath(); + ctx.arc( + x * TILE_SIZE + TILE_SIZE / 2, + y * TILE_SIZE + TILE_SIZE / 2, + 2, + 0, + Math.PI * 2 + ); + ctx.fill(); + } else if (tile === 3) { + // Power pellet + ctx.fillStyle = '#FFB8AE'; + const pulse = Math.sin(Date.now() / 200) * 2 + 4; + ctx.beginPath(); + ctx.arc( + x * TILE_SIZE + TILE_SIZE / 2, + y * TILE_SIZE + TILE_SIZE / 2, + pulse, + 0, + Math.PI * 2 + ); + ctx.fill(); + } + } + } + + // Draw ghosts + ghosts.forEach(ghost => { + const x = ghost.x * TILE_SIZE; + const y = ghost.y * TILE_SIZE; + + if (ghost.scared) { + ctx.fillStyle = '#2121DE'; + } else { + ctx.fillStyle = ghost.color; + } + + // Ghost body + ctx.beginPath(); + ctx.arc(x + TILE_SIZE / 2, y + TILE_SIZE / 2, TILE_SIZE / 2 - 2, Math.PI, 0); + ctx.lineTo(x + TILE_SIZE - 2, y + TILE_SIZE); + ctx.lineTo(x + TILE_SIZE - 5, y + TILE_SIZE - 4); + ctx.lineTo(x + TILE_SIZE / 2 + 2, y + TILE_SIZE); + ctx.lineTo(x + TILE_SIZE / 2 - 2, y + TILE_SIZE - 4); + ctx.lineTo(x + 5, y + TILE_SIZE); + ctx.lineTo(x + 2, y + TILE_SIZE); + ctx.closePath(); + ctx.fill(); + + // Eyes + ctx.fillStyle = '#FFF'; + ctx.fillRect(x + 5, y + 7, 4, 6); + ctx.fillRect(x + TILE_SIZE - 9, y + 7, 4, 6); + + if (!ghost.scared) { + ctx.fillStyle = '#000'; + ctx.fillRect(x + 6, y + 9, 2, 3); + ctx.fillRect(x + TILE_SIZE - 8, y + 9, 2, 3); + } + }); + + // Draw Pac-Man + const pacX = pacman.x * TILE_SIZE; + const pacY = pacman.y * TILE_SIZE; + + ctx.fillStyle = '#FFFF00'; + ctx.beginPath(); + + const mouthAngle = Math.abs(Math.sin(pacman.mouthOpen * Math.PI)) * 0.4; + const rotation = pacman.direction * Math.PI / 2; + + ctx.arc( + pacX + TILE_SIZE / 2, + pacY + TILE_SIZE / 2, + TILE_SIZE / 2 - 2, + rotation + mouthAngle, + rotation + Math.PI * 2 - mouthAngle + ); + ctx.lineTo(pacX + TILE_SIZE / 2, pacY + TILE_SIZE / 2); + ctx.fill(); + + // Draw pause overlay + if (isPaused) { + ctx.fillStyle = 'rgba(0, 0, 0, 0.7)'; + ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); + ctx.fillStyle = '#fff'; + ctx.font = '40px Arial'; + ctx.textAlign = 'center'; + ctx.fillText('PAUSED', CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2); + } + } + + function drawGameOver() { + ctx.fillStyle = 'rgba(0, 0, 0, 0.8)'; + ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); + + ctx.fillStyle = '#f00'; + ctx.font = 'bold 50px Arial'; + ctx.textAlign = 'center'; + ctx.fillText('GAME OVER', CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2 - 40); + + ctx.fillStyle = '#fff'; + ctx.font = '24px Arial'; + ctx.fillText('Final Score: ' + score, CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2 + 10); + ctx.font = '20px Arial'; + ctx.fillText('Press Start to play again', CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2 + 50); + } + + function updateScore() { + document.getElementById('pm-score').textContent = score; + document.getElementById('pm-level').textContent = level; + document.getElementById('pm-lives').textContent = lives; + document.getElementById('pm-pellets').textContent = pelletsEaten + ' / ' + totalPellets; + + // Update mobile + const scoreMobile = document.getElementById('pm-score-mobile'); + const levelMobile = document.getElementById('pm-level-mobile'); + const livesMobile = document.getElementById('pm-lives-mobile'); + const pelletsMobile = document.getElementById('pm-pellets-mobile'); + + if (scoreMobile) scoreMobile.textContent = score; + if (levelMobile) levelMobile.textContent = level; + if (livesMobile) livesMobile.textContent = lives; + if (pelletsMobile) pelletsMobile.textContent = pelletsEaten + ' / ' + totalPellets; + } + + function togglePause() { + if (isGameOver || !gameLoop) { + return; + } + isPaused = !isPaused; + } + + function stopGame() { + if (gameLoop) { + cancelAnimationFrame(gameLoop); + gameLoop = null; + } + + isPaused = false; + isGameOver = true; + + // Draw stopped screen + ctx.fillStyle = '#000'; + ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); + + ctx.fillStyle = 'rgba(0, 0, 0, 0.8)'; + ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); + + ctx.fillStyle = '#ff9800'; + ctx.font = 'bold 50px Arial'; + ctx.textAlign = 'center'; + ctx.fillText('STOPPED', CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2 - 20); + + ctx.fillStyle = '#fff'; + ctx.font = '20px Arial'; + ctx.fillText('Press Start to play again', CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2 + 30); + } + + return { + startGame: startGame, + togglePause: togglePause, + stopGame: stopGame + }; +})(); + diff --git a/Community Events Games/snake.js b/Community Events Games/snake.js new file mode 100644 index 00000000..68f955d7 --- /dev/null +++ b/Community Events Games/snake.js @@ -0,0 +1,456 @@ +// Snake Game Implementation +window.snake = (function() { + let canvas, ctx; + let gameLoop = null; + let lastTime = 0; + let isPaused = false; + let isGameOver = false; + + // Game state + let score = 0; + let highScore = 0; + let speed = 10; + let foodEaten = 0; + + // Grid settings + const TILE_SIZE = 20; + const COLS = 30; + const ROWS = 25; + const CANVAS_WIDTH = COLS * TILE_SIZE; + const CANVAS_HEIGHT = ROWS * TILE_SIZE; + + // Snake + let snake = []; + let direction = { x: 1, y: 0 }; + let nextDirection = { x: 1, y: 0 }; + + // Food + let food = { x: 0, y: 0 }; + + // Special food (appears randomly, worth more points) + let specialFood = null; + let specialFoodTimer = 0; + const specialFoodDuration = 100; + + // Game speed + let moveCounter = 0; + let moveInterval = 10; + + // Controls + let keys = {}; + + function init() { + canvas = document.getElementById('snakeCanvas'); + if (!canvas) { + console.error('Canvas element not found'); + return false; + } + ctx = canvas.getContext('2d'); + + // Setup keyboard controls + document.addEventListener('keydown', (e) => { + keys[e.key] = true; + + if (e.key === 'p' || e.key === 'P') { + togglePause(); + e.preventDefault(); + } + + if (!isPaused && !isGameOver) { + switch(e.key) { + case 'ArrowRight': + if (direction.x === 0) { + nextDirection = { x: 1, y: 0 }; + } + e.preventDefault(); + break; + case 'ArrowLeft': + if (direction.x === 0) { + nextDirection = { x: -1, y: 0 }; + } + e.preventDefault(); + break; + case 'ArrowDown': + if (direction.y === 0) { + nextDirection = { x: 0, y: 1 }; + } + e.preventDefault(); + break; + case 'ArrowUp': + if (direction.y === 0) { + nextDirection = { x: 0, y: -1 }; + } + e.preventDefault(); + break; + } + } + }); + + document.addEventListener('keyup', (e) => { + keys[e.key] = false; + }); + + return true; + } + + function startGame() { + if (!init()) { + setTimeout(startGame, 100); + return; + } + + // Reset game state + score = 0; + speed = 10; + foodEaten = 0; + isPaused = false; + isGameOver = false; + moveInterval = 10; + specialFood = null; + specialFoodTimer = 0; + + // Initialize snake + snake = [ + { x: 15, y: 12 }, + { x: 14, y: 12 }, + { x: 13, y: 12 } + ]; + + direction = { x: 1, y: 0 }; + nextDirection = { x: 1, y: 0 }; + + // Place first food + placeFood(); + + updateScore(); + + if (gameLoop) { + cancelAnimationFrame(gameLoop); + } + + lastTime = 0; + gameLoop = requestAnimationFrame(update); + } + + function placeFood() { + let validPosition = false; + + while (!validPosition) { + food.x = Math.floor(Math.random() * COLS); + food.y = Math.floor(Math.random() * ROWS); + + // Check if food is on snake + validPosition = !snake.some(segment => segment.x === food.x && segment.y === food.y); + } + } + + function placeSpecialFood() { + let validPosition = false; + + while (!validPosition) { + specialFood = { + x: Math.floor(Math.random() * COLS), + y: Math.floor(Math.random() * ROWS) + }; + + // Check if special food is on snake or regular food + validPosition = !snake.some(segment => + segment.x === specialFood.x && segment.y === specialFood.y + ) && !(specialFood.x === food.x && specialFood.y === food.y); + } + + specialFoodTimer = specialFoodDuration; + } + + function update(time = 0) { + if (isGameOver) { + drawGameOver(); + return; + } + + const deltaTime = time - lastTime; + lastTime = time; + + if (!isPaused) { + moveCounter++; + + if (moveCounter >= moveInterval) { + moveCounter = 0; + + // Update direction + direction = { ...nextDirection }; + + // Calculate new head position + const head = { ...snake[0] }; + head.x += direction.x; + head.y += direction.y; + + // Check wall collision + if (head.x < 0 || head.x >= COLS || head.y < 0 || head.y >= ROWS) { + isGameOver = true; + if (score > highScore) { + highScore = score; + } + return; + } + + // Check self collision + if (snake.some(segment => segment.x === head.x && segment.y === head.y)) { + isGameOver = true; + if (score > highScore) { + highScore = score; + } + return; + } + + // Add new head + snake.unshift(head); + + // Check food collision + if (head.x === food.x && head.y === food.y) { + score += 10; + foodEaten++; + speed = 10 + Math.floor(foodEaten / 5); + moveInterval = Math.max(3, 10 - Math.floor(foodEaten / 5)); + placeFood(); + + // Spawn special food randomly + if (Math.random() < 0.2 && !specialFood) { + placeSpecialFood(); + } + } else if (specialFood && head.x === specialFood.x && head.y === specialFood.y) { + // Eat special food + score += 50; + specialFood = null; + specialFoodTimer = 0; + } else { + // Remove tail if no food eaten + snake.pop(); + } + } + + // Update special food timer + if (specialFood) { + specialFoodTimer--; + if (specialFoodTimer <= 0) { + specialFood = null; + } + } + } + + draw(); + updateScore(); + gameLoop = requestAnimationFrame(update); + } + + function draw() { + // Clear canvas with grid pattern + ctx.fillStyle = '#1a1a1a'; + ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); + + // Draw grid + ctx.strokeStyle = '#2a2a2a'; + ctx.lineWidth = 1; + for (let x = 0; x <= COLS; x++) { + ctx.beginPath(); + ctx.moveTo(x * TILE_SIZE, 0); + ctx.lineTo(x * TILE_SIZE, CANVAS_HEIGHT); + ctx.stroke(); + } + for (let y = 0; y <= ROWS; y++) { + ctx.beginPath(); + ctx.moveTo(0, y * TILE_SIZE); + ctx.lineTo(CANVAS_WIDTH, y * TILE_SIZE); + ctx.stroke(); + } + + // Draw food + ctx.fillStyle = '#ff0000'; + ctx.beginPath(); + ctx.arc( + food.x * TILE_SIZE + TILE_SIZE / 2, + food.y * TILE_SIZE + TILE_SIZE / 2, + TILE_SIZE / 2 - 2, + 0, + Math.PI * 2 + ); + ctx.fill(); + + // Add shine to food + ctx.fillStyle = '#ff6666'; + ctx.beginPath(); + ctx.arc( + food.x * TILE_SIZE + TILE_SIZE / 3, + food.y * TILE_SIZE + TILE_SIZE / 3, + TILE_SIZE / 6, + 0, + Math.PI * 2 + ); + ctx.fill(); + + // Draw special food + if (specialFood) { + const pulse = Math.sin(Date.now() / 100) * 2 + (TILE_SIZE / 2 - 2); + ctx.fillStyle = '#FFD700'; + ctx.beginPath(); + ctx.arc( + specialFood.x * TILE_SIZE + TILE_SIZE / 2, + specialFood.y * TILE_SIZE + TILE_SIZE / 2, + pulse, + 0, + Math.PI * 2 + ); + ctx.fill(); + + // Star effect + ctx.fillStyle = '#FFF'; + ctx.font = 'bold 16px Arial'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText('β˜…', specialFood.x * TILE_SIZE + TILE_SIZE / 2, specialFood.y * TILE_SIZE + TILE_SIZE / 2); + } + + // Draw snake + snake.forEach((segment, index) => { + if (index === 0) { + // Head + ctx.fillStyle = '#00ff00'; + } else { + // Body - gradient effect + const alpha = 1 - (index / snake.length) * 0.5; + ctx.fillStyle = `rgba(0, 200, 0, ${alpha})`; + } + + ctx.fillRect( + segment.x * TILE_SIZE + 1, + segment.y * TILE_SIZE + 1, + TILE_SIZE - 2, + TILE_SIZE - 2 + ); + + // Add shine to head + if (index === 0) { + ctx.fillStyle = '#66ff66'; + ctx.fillRect( + segment.x * TILE_SIZE + 3, + segment.y * TILE_SIZE + 3, + TILE_SIZE / 3, + TILE_SIZE / 3 + ); + + // Draw eyes based on direction + ctx.fillStyle = '#000'; + if (direction.x === 1) { + // Right + ctx.fillRect(segment.x * TILE_SIZE + 12, segment.y * TILE_SIZE + 5, 3, 3); + ctx.fillRect(segment.x * TILE_SIZE + 12, segment.y * TILE_SIZE + 12, 3, 3); + } else if (direction.x === -1) { + // Left + ctx.fillRect(segment.x * TILE_SIZE + 5, segment.y * TILE_SIZE + 5, 3, 3); + ctx.fillRect(segment.x * TILE_SIZE + 5, segment.y * TILE_SIZE + 12, 3, 3); + } else if (direction.y === 1) { + // Down + ctx.fillRect(segment.x * TILE_SIZE + 5, segment.y * TILE_SIZE + 12, 3, 3); + ctx.fillRect(segment.x * TILE_SIZE + 12, segment.y * TILE_SIZE + 12, 3, 3); + } else { + // Up + ctx.fillRect(segment.x * TILE_SIZE + 5, segment.y * TILE_SIZE + 5, 3, 3); + ctx.fillRect(segment.x * TILE_SIZE + 12, segment.y * TILE_SIZE + 5, 3, 3); + } + } + }); + + // Draw pause overlay + if (isPaused) { + ctx.fillStyle = 'rgba(0, 0, 0, 0.7)'; + ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); + ctx.fillStyle = '#fff'; + ctx.font = '40px Arial'; + ctx.textAlign = 'center'; + ctx.fillText('PAUSED', CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2); + } + } + + function drawGameOver() { + ctx.fillStyle = 'rgba(0, 0, 0, 0.8)'; + ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); + + ctx.fillStyle = '#f00'; + ctx.font = 'bold 50px Arial'; + ctx.textAlign = 'center'; + ctx.fillText('GAME OVER', CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2 - 60); + + ctx.fillStyle = '#fff'; + ctx.font = '24px Arial'; + ctx.fillText('Score: ' + score, CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2 - 10); + + if (score === highScore && score > 0) { + ctx.fillStyle = '#FFD700'; + ctx.font = 'bold 20px Arial'; + ctx.fillText('πŸ† NEW HIGH SCORE! πŸ†', CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2 + 25); + } + + ctx.fillStyle = '#aaa'; + ctx.font = '20px Arial'; + ctx.fillText('Press Start to play again', CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2 + 60); + } + + function updateScore() { + document.getElementById('snake-score').textContent = score; + document.getElementById('snake-highscore').textContent = highScore; + document.getElementById('snake-length').textContent = snake.length; + document.getElementById('snake-speed').textContent = speed; + + // Update mobile + const scoreMobile = document.getElementById('snake-score-mobile'); + const highscoreMobile = document.getElementById('snake-highscore-mobile'); + const lengthMobile = document.getElementById('snake-length-mobile'); + const speedMobile = document.getElementById('snake-speed-mobile'); + + if (scoreMobile) scoreMobile.textContent = score; + if (highscoreMobile) highscoreMobile.textContent = highScore; + if (lengthMobile) lengthMobile.textContent = snake.length; + if (speedMobile) speedMobile.textContent = speed; + } + + function togglePause() { + if (isGameOver || !gameLoop) { + return; + } + isPaused = !isPaused; + } + + function stopGame() { + if (gameLoop) { + cancelAnimationFrame(gameLoop); + gameLoop = null; + } + + snake = []; + isPaused = false; + isGameOver = true; + + // Draw stopped screen + ctx.fillStyle = '#1a1a1a'; + ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); + + ctx.fillStyle = 'rgba(0, 0, 0, 0.8)'; + ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); + + ctx.fillStyle = '#ff9800'; + ctx.font = 'bold 50px Arial'; + ctx.textAlign = 'center'; + ctx.fillText('STOPPED', CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2 - 20); + + ctx.fillStyle = '#fff'; + ctx.font = '20px Arial'; + ctx.fillText('Press Start to play again', CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2 + 30); + } + + return { + startGame: startGame, + togglePause: togglePause, + stopGame: stopGame + }; +})(); + diff --git a/Community Events Games/spaceinvaders.js b/Community Events Games/spaceinvaders.js new file mode 100644 index 00000000..b71b720b --- /dev/null +++ b/Community Events Games/spaceinvaders.js @@ -0,0 +1,708 @@ +// Space Invaders Game Implementation +window.spaceInvaders = (function() { + let canvas, ctx; + let gameLoop = null; + let lastTime = 0; + let isPaused = false; + let isGameOver = false; + + // Game state + let score = 0; + let lives = 3; + let level = 1; + let kills = 0; + let combo = 0; + let maxCombo = 0; + let comboTimer = 0; + const comboTimeout = 100; + + // Player + let player = { + x: 0, + y: 0, + width: 40, + height: 30, + speed: 6, + color: '#00ff00', + invulnerable: 0 + }; + + // Bullets + let bullets = []; + const bulletSpeed = 8; + const bulletWidth = 4; + const bulletHeight = 15; + let canShoot = true; + let shootCooldown = 0; + const shootCooldownMax = 10; + + // Shields + let shields = []; + + // Aliens + let aliens = []; + let alienDirection = 1; + let alienSpeed = 2; // Increased base speed + let alienDropDistance = 20; + let shouldDropAliens = false; + + // Alien bullets + let alienBullets = []; + const alienBulletSpeed = 3; + let alienShootChance = 0.001; + + // Controls + let keys = {}; + + // Canvas dimensions + const CANVAS_WIDTH = 600; + const CANVAS_HEIGHT = 600; + + function init() { + canvas = document.getElementById('spaceInvadersCanvas'); + if (!canvas) { + console.error('Canvas element not found'); + return false; + } + ctx = canvas.getContext('2d'); + + // Setup keyboard controls + document.addEventListener('keydown', (e) => { + keys[e.key] = true; + + if (e.key === 'p' || e.key === 'P') { + togglePause(); + e.preventDefault(); + } + + if (e.key === ' ' && !isPaused && !isGameOver) { + shoot(); + e.preventDefault(); + } + }); + + document.addEventListener('keyup', (e) => { + keys[e.key] = false; + }); + + return true; + } + + function createShields() { + shields = []; + const shieldWidth = 80; + const shieldHeight = 60; + const shieldY = CANVAS_HEIGHT - 150; + const spacing = (CANVAS_WIDTH - 4 * shieldWidth) / 5; + + for (let i = 0; i < 4; i++) { + const shield = { + x: spacing + i * (shieldWidth + spacing), + y: shieldY, + width: shieldWidth, + height: shieldHeight, + blocks: [] + }; + + // Create shield blocks (8x6 grid) + for (let row = 0; row < 6; row++) { + for (let col = 0; col < 8; col++) { + // Skip corners for classic shield shape + if ((row < 2 && (col < 2 || col > 5)) || + (row === 5 && col > 2 && col < 5)) { + continue; + } + shield.blocks.push({ + x: col * 10, + y: row * 10, + active: true + }); + } + } + shields.push(shield); + } + } + + function startGame() { + if (!init()) { + setTimeout(startGame, 100); + return; + } + + // Reset game state + score = 0; + lives = 3; + level = 1; + kills = 0; + combo = 0; + maxCombo = 0; + comboTimer = 0; + isPaused = false; + isGameOver = false; + bullets = []; + alienBullets = []; + canShoot = true; + shootCooldown = 0; + + // Initialize player + player.x = CANVAS_WIDTH / 2 - player.width / 2; + player.y = CANVAS_HEIGHT - player.height - 40; + player.invulnerable = 0; + + // Create aliens and shields + createAliens(); + createShields(); + + updateScore(); + + if (gameLoop) { + cancelAnimationFrame(gameLoop); + } + + lastTime = 0; + gameLoop = requestAnimationFrame(update); + } + + function createAliens() { + aliens = []; + const rows = 4 + Math.floor(level / 2); + const cols = 8; + const alienWidth = 40; + const alienHeight = 30; + const padding = 10; + const offsetX = 80; + const offsetY = 60; + + // Faster speed progression + alienSpeed = 2 + (level - 1) * 0.5; + alienShootChance = 0.001 + (level - 1) * 0.0002; + + for (let row = 0; row < rows; row++) { + for (let col = 0; col < cols; col++) { + aliens.push({ + x: offsetX + col * (alienWidth + padding), + y: offsetY + row * (alienHeight + padding), + width: alienWidth, + height: alienHeight, + alive: true, + type: row % 3 + }); + } + } + } + + function update(time = 0) { + if (isGameOver) { + drawGameOver(); + return; + } + + const deltaTime = time - lastTime; + lastTime = time; + + if (!isPaused) { + // Update timers + if (player.invulnerable > 0) player.invulnerable--; + if (shootCooldown > 0) shootCooldown--; + if (comboTimer > 0) { + comboTimer--; + } else if (combo > 0) { + combo = 0; + } + + // Move player + if (keys['ArrowLeft'] && player.x > 0) { + player.x -= player.speed; + } + if (keys['ArrowRight'] && player.x < CANVAS_WIDTH - player.width) { + player.x += player.speed; + } + + // Move bullets + for (let i = bullets.length - 1; i >= 0; i--) { + bullets[i].y -= bulletSpeed; + if (bullets[i].y < 0) { + bullets.splice(i, 1); + } + } + + // Move alien bullets + for (let i = alienBullets.length - 1; i >= 0; i--) { + alienBullets[i].y += alienBulletSpeed; + if (alienBullets[i].y > CANVAS_HEIGHT) { + alienBullets.splice(i, 1); + } + } + + // Move aliens (smooth movement every frame) + moveAliens(); + + // Aliens shoot (only from bottom row aliens) + const aliveAliens = aliens.filter(a => a.alive); + const bottomAliens = aliveAliens.filter(alien => { + return !aliveAliens.some(other => + other.alive && + Math.abs(other.x - alien.x) < 30 && + other.y > alien.y + ); + }); + + bottomAliens.forEach(alien => { + if (Math.random() < alienShootChance) { + alienBullets.push({ + x: alien.x + alien.width / 2, + y: alien.y + alien.height, + width: 4, + height: 12 + }); + } + }); + + // Check collisions + checkCollisions(); + + // Check win condition + if (aliens.every(alien => !alien.alive)) { + level++; + kills = 0; + score += level * 100; // Bonus for completing level + createAliens(); + createShields(); // Regenerate shields + } + + // Check game over + if (lives <= 0) { + isGameOver = true; + } + + // Check if aliens reached player + aliens.forEach(alien => { + if (alien.alive && alien.y + alien.height >= player.y) { + lives = 0; + isGameOver = true; + } + }); + } + + draw(); + updateScore(); + gameLoop = requestAnimationFrame(update); + } + + function moveAliens() { + // Check if any alien will hit the edge in the next move + let willHitEdge = false; + + aliens.forEach(alien => { + if (alien.alive) { + const nextX = alien.x + alienDirection * alienSpeed; + if ((alienDirection > 0 && nextX + alien.width >= CANVAS_WIDTH - 10) || + (alienDirection < 0 && nextX <= 10)) { + willHitEdge = true; + } + } + }); + + if (willHitEdge) { + // Change direction and drop + alienDirection *= -1; + aliens.forEach(alien => { + if (alien.alive) { + alien.y += alienDropDistance; + } + }); + } + + // Move aliens horizontally (smooth continuous movement) + aliens.forEach(alien => { + if (alien.alive) { + alien.x += alienDirection * alienSpeed; + } + }); + } + + function shoot() { + if (shootCooldown > 0 || isPaused || isGameOver) { + return; + } + + bullets.push({ + x: player.x + player.width / 2 - bulletWidth / 2, + y: player.y, + width: bulletWidth, + height: bulletHeight + }); + + shootCooldown = shootCooldownMax; + } + + function checkCollisions() { + // Bullet vs Aliens + for (let bulletIndex = bullets.length - 1; bulletIndex >= 0; bulletIndex--) { + const bullet = bullets[bulletIndex]; + let bulletDestroyed = false; + + // Check shields + for (let shield of shields) { + if (bulletDestroyed) break; + for (let i = shield.blocks.length - 1; i >= 0; i--) { + const block = shield.blocks[i]; + if (!block.active) continue; + + const blockX = shield.x + block.x; + const blockY = shield.y + block.y; + + if (bullet.x < blockX + 10 && + bullet.x + bullet.width > blockX && + bullet.y < blockY + 10 && + bullet.y + bullet.height > blockY) { + + block.active = false; + bullets.splice(bulletIndex, 1); + bulletDestroyed = true; + break; + } + } + } + + if (bulletDestroyed) continue; + + // Check aliens + for (let alienIndex = 0; alienIndex < aliens.length; alienIndex++) { + const alien = aliens[alienIndex]; + if (!alien.alive) continue; + + if (bullet.x < alien.x + alien.width && + bullet.x + bullet.width > alien.x && + bullet.y < alien.y + alien.height && + bullet.y + bullet.height > alien.y) { + + alien.alive = false; + bullets.splice(bulletIndex, 1); + + // Combo system + combo++; + comboTimer = comboTimeout; + if (combo > maxCombo) maxCombo = combo; + + const baseScore = (3 - alien.type) * 10 * level; + const comboBonus = Math.floor(baseScore * (combo - 1) * 0.5); + score += baseScore + comboBonus; + kills++; + break; + } + } + } + + // Alien bullets vs Shields + for (let bulletIndex = alienBullets.length - 1; bulletIndex >= 0; bulletIndex--) { + const bullet = alienBullets[bulletIndex]; + let bulletDestroyed = false; + + for (let shield of shields) { + if (bulletDestroyed) break; + for (let i = shield.blocks.length - 1; i >= 0; i--) { + const block = shield.blocks[i]; + if (!block.active) continue; + + const blockX = shield.x + block.x; + const blockY = shield.y + block.y; + + if (bullet.x < blockX + 10 && + bullet.x + bullet.width > blockX && + bullet.y < blockY + 10 && + bullet.y + bullet.height > blockY) { + + block.active = false; + alienBullets.splice(bulletIndex, 1); + bulletDestroyed = true; + break; + } + } + } + + if (bulletDestroyed) continue; + + // Check player + if (player.invulnerable === 0 && + bullet.x < player.x + player.width && + bullet.x + bullet.width > player.x && + bullet.y < player.y + player.height && + bullet.y + bullet.height > player.y) { + + alienBullets.splice(bulletIndex, 1); + lives--; + player.invulnerable = 60; // 1 second invulnerability + combo = 0; // Reset combo on hit + } + } + } + + function draw() { + // Clear canvas + ctx.fillStyle = '#000'; + ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); + + // Draw animated stars background + ctx.fillStyle = '#fff'; + for (let i = 0; i < 100; i++) { + const x = (i * 123 + Date.now() / 100) % CANVAS_WIDTH; + const y = (i * 456) % CANVAS_HEIGHT; + const size = (i % 3) + 1; + ctx.fillRect(x, y, size, size); + } + + // Draw shields + shields.forEach(shield => { + shield.blocks.forEach(block => { + if (block.active) { + ctx.fillStyle = '#00ff00'; + ctx.fillRect( + shield.x + block.x, + shield.y + block.y, + 10, + 10 + ); + } + }); + }); + + // Draw player (with invulnerability flash) + if (player.invulnerable === 0 || Math.floor(player.invulnerable / 5) % 2 === 0) { + drawPlayer(); + } + + // Draw bullets with glow + bullets.forEach(bullet => { + // Glow + ctx.fillStyle = 'rgba(255, 255, 0, 0.3)'; + ctx.fillRect(bullet.x - 2, bullet.y - 2, bullet.width + 4, bullet.height + 4); + // Bullet + ctx.fillStyle = '#ffff00'; + ctx.fillRect(bullet.x, bullet.y, bullet.width, bullet.height); + }); + + // Draw alien bullets with glow + alienBullets.forEach(bullet => { + // Glow + ctx.fillStyle = 'rgba(255, 0, 0, 0.3)'; + ctx.fillRect(bullet.x - 2, bullet.y - 2, bullet.width + 4, bullet.height + 4); + // Bullet + ctx.fillStyle = '#ff0000'; + ctx.fillRect(bullet.x, bullet.y, bullet.width, bullet.height); + }); + + // Draw aliens + aliens.forEach(alien => { + if (alien.alive) { + drawAlien(alien); + } + }); + + // Draw combo indicator + if (combo > 1) { + ctx.fillStyle = '#FFD700'; + ctx.font = 'bold 24px Arial'; + ctx.textAlign = 'left'; + ctx.fillText(`COMBO x${combo}!`, 10, 30); + + // Combo bar + const barWidth = 200; + const barHeight = 10; + const fillWidth = (comboTimer / comboTimeout) * barWidth; + + ctx.fillStyle = 'rgba(0, 0, 0, 0.5)'; + ctx.fillRect(10, 40, barWidth, barHeight); + ctx.fillStyle = '#FFD700'; + ctx.fillRect(10, 40, fillWidth, barHeight); + } + + // Draw shoot cooldown indicator + if (shootCooldown > 0) { + const barWidth = 40; + const barHeight = 5; + const fillWidth = (shootCooldown / shootCooldownMax) * barWidth; + + ctx.fillStyle = 'rgba(255, 0, 0, 0.5)'; + ctx.fillRect(player.x, player.y - 10, barWidth, barHeight); + ctx.fillStyle = '#00ff00'; + ctx.fillRect(player.x, player.y - 10, barWidth - fillWidth, barHeight); + } + + // Draw pause overlay + if (isPaused) { + ctx.fillStyle = 'rgba(0, 0, 0, 0.7)'; + ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); + ctx.fillStyle = '#fff'; + ctx.font = '40px Arial'; + ctx.textAlign = 'center'; + ctx.fillText('PAUSED', CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2); + } + } + + function drawPlayer() { + // Draw glow effect + ctx.fillStyle = 'rgba(0, 255, 0, 0.2)'; + ctx.beginPath(); + ctx.arc(player.x + player.width / 2, player.y + player.height / 2, player.width / 1.5, 0, Math.PI * 2); + ctx.fill(); + + // Draw spaceship body + ctx.fillStyle = player.color; + ctx.beginPath(); + ctx.moveTo(player.x + player.width / 2, player.y); + ctx.lineTo(player.x + player.width, player.y + player.height); + ctx.lineTo(player.x, player.y + player.height); + ctx.closePath(); + ctx.fill(); + + // Draw wings + ctx.fillStyle = '#00cc00'; + ctx.fillRect(player.x, player.y + player.height - 10, 10, 10); + ctx.fillRect(player.x + player.width - 10, player.y + player.height - 10, 10, 10); + + // Draw cockpit + ctx.fillStyle = '#00ffff'; + ctx.beginPath(); + ctx.arc(player.x + player.width / 2, player.y + 12, 6, 0, Math.PI * 2); + ctx.fill(); + + // Draw engines + const engineGlow = Math.sin(Date.now() / 50) * 5 + 10; + ctx.fillStyle = `rgba(255, 100, 0, 0.8)`; + ctx.fillRect(player.x + 8, player.y + player.height, 8, engineGlow); + ctx.fillRect(player.x + player.width - 16, player.y + player.height, 8, engineGlow); + } + + function drawAlien(alien) { + const colors = ['#ff00ff', '#00ffff', '#ffff00']; + const glowColors = ['rgba(255, 0, 255, 0.3)', 'rgba(0, 255, 255, 0.3)', 'rgba(255, 255, 0, 0.3)']; + + // Draw glow + ctx.fillStyle = glowColors[alien.type]; + ctx.beginPath(); + ctx.arc(alien.x + alien.width / 2, alien.y + alien.height / 2, alien.width / 1.8, 0, Math.PI * 2); + ctx.fill(); + + // Draw alien body with animation + const wobble = Math.sin(Date.now() / 200 + alien.x) * 2; + ctx.fillStyle = colors[alien.type]; + ctx.fillRect(alien.x + 5, alien.y + 5 + wobble, alien.width - 10, alien.height - 10); + + // Draw alien eyes (animated) + const eyeBlink = Math.floor(Date.now() / 2000) % 10 === 0 ? 2 : 6; + ctx.fillStyle = '#fff'; + ctx.fillRect(alien.x + 10, alien.y + 10, 6, eyeBlink); + ctx.fillRect(alien.x + alien.width - 16, alien.y + 10, 6, eyeBlink); + + // Draw pupils + if (eyeBlink === 6) { + ctx.fillStyle = '#000'; + ctx.fillRect(alien.x + 12, alien.y + 12, 2, 2); + ctx.fillRect(alien.x + alien.width - 14, alien.y + 12, 2, 2); + } + + // Draw alien antennae with animated tips + const antennaeGlow = Math.sin(Date.now() / 100 + alien.x) * 2; + ctx.strokeStyle = colors[alien.type]; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.moveTo(alien.x + 10, alien.y + 5); + ctx.lineTo(alien.x + 5, alien.y); + ctx.stroke(); + ctx.beginPath(); + ctx.moveTo(alien.x + alien.width - 10, alien.y + 5); + ctx.lineTo(alien.x + alien.width - 5, alien.y); + ctx.stroke(); + + // Antennae tips + ctx.fillStyle = colors[alien.type]; + ctx.beginPath(); + ctx.arc(alien.x + 5, alien.y - antennaeGlow, 3, 0, Math.PI * 2); + ctx.fill(); + ctx.beginPath(); + ctx.arc(alien.x + alien.width - 5, alien.y - antennaeGlow, 3, 0, Math.PI * 2); + ctx.fill(); + } + + function drawGameOver() { + ctx.fillStyle = 'rgba(0, 0, 0, 0.8)'; + ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); + + ctx.fillStyle = '#f00'; + ctx.font = 'bold 50px Arial'; + ctx.textAlign = 'center'; + ctx.fillText('GAME OVER', CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2 - 40); + + ctx.fillStyle = '#fff'; + ctx.font = '24px Arial'; + ctx.fillText('Final Score: ' + score, CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2 + 10); + ctx.font = '20px Arial'; + ctx.fillText('Press Start to play again', CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2 + 50); + } + + function updateScore() { + document.getElementById('si-score').textContent = score; + document.getElementById('si-level').textContent = level; + document.getElementById('si-lives').textContent = lives; + document.getElementById('si-kills').textContent = kills; + document.getElementById('si-combo').textContent = maxCombo; + + // Update mobile + const scoreMobile = document.getElementById('si-score-mobile'); + const levelMobile = document.getElementById('si-level-mobile'); + const livesMobile = document.getElementById('si-lives-mobile'); + const killsMobile = document.getElementById('si-kills-mobile'); + const comboMobile = document.getElementById('si-combo-mobile'); + + if (scoreMobile) scoreMobile.textContent = score; + if (levelMobile) levelMobile.textContent = level; + if (livesMobile) livesMobile.textContent = lives; + if (killsMobile) killsMobile.textContent = kills; + if (comboMobile) comboMobile.textContent = maxCombo; + } + + function togglePause() { + if (isGameOver || !gameLoop) { + return; + } + isPaused = !isPaused; + } + + function stopGame() { + if (gameLoop) { + cancelAnimationFrame(gameLoop); + gameLoop = null; + } + + aliens = []; + bullets = []; + alienBullets = []; + shields = []; + isPaused = false; + isGameOver = true; + + // Draw stopped screen + ctx.fillStyle = '#000'; + ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); + + ctx.fillStyle = 'rgba(0, 0, 0, 0.8)'; + ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); + + ctx.fillStyle = '#ff9800'; + ctx.font = 'bold 50px Arial'; + ctx.textAlign = 'center'; + ctx.fillText('STOPPED', CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2 - 20); + + ctx.fillStyle = '#fff'; + ctx.font = '20px Arial'; + ctx.fillText('Press Start to play again', CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2 + 30); + } + + return { + startGame: startGame, + togglePause: togglePause, + stopGame: stopGame + }; +})(); + diff --git a/Community Events Games/tetris.js b/Community Events Games/tetris.js new file mode 100644 index 00000000..f6f6cce1 --- /dev/null +++ b/Community Events Games/tetris.js @@ -0,0 +1,392 @@ +// Tetris Game Implementation +window.tetris = (function() { + let canvas, ctx; + let gameBoard = []; + let currentPiece = null; + let currentX = 0; + let currentY = 0; + let score = 0; + let level = 1; + let lines = 0; + let gameLoop = null; + let dropCounter = 0; + let dropInterval = 1000; + let lastTime = 0; + let isPaused = false; + let isGameOver = false; + + const COLS = 10; + const ROWS = 20; + const BLOCK_SIZE = 30; + + // Tetromino shapes + const SHAPES = { + I: [[1, 1, 1, 1]], + O: [[1, 1], [1, 1]], + T: [[0, 1, 0], [1, 1, 1]], + S: [[0, 1, 1], [1, 1, 0]], + Z: [[1, 1, 0], [0, 1, 1]], + J: [[1, 0, 0], [1, 1, 1]], + L: [[0, 0, 1], [1, 1, 1]] + }; + + const COLORS = { + I: '#00f0f0', + O: '#f0f000', + T: '#a000f0', + S: '#00f000', + Z: '#f00000', + J: '#0000f0', + L: '#f0a000' + }; + + function init() { + canvas = document.getElementById('tetrisCanvas'); + if (!canvas) { + console.error('Canvas element not found'); + return false; + } + ctx = canvas.getContext('2d'); + + // Initialize game board + gameBoard = Array(ROWS).fill().map(() => Array(COLS).fill(0)); + + // Setup keyboard controls + document.addEventListener('keydown', handleKeyPress); + + return true; + } + + function handleKeyPress(e) { + if (!currentPiece || isGameOver) return; + + if (e.key === 'p' || e.key === 'P') { + isPaused = !isPaused; + return; + } + + if (isPaused) return; + + switch(e.key) { + case 'ArrowLeft': + movePiece(-1, 0); + e.preventDefault(); + break; + case 'ArrowRight': + movePiece(1, 0); + e.preventDefault(); + break; + case 'ArrowDown': + movePiece(0, 1); + dropCounter = 0; + e.preventDefault(); + break; + case 'ArrowUp': + rotatePiece(); + e.preventDefault(); + break; + case ' ': + hardDrop(); + e.preventDefault(); + break; + } + } + + function startGame() { + if (!init()) { + setTimeout(startGame, 100); + return; + } + + // Reset game state + gameBoard = Array(ROWS).fill().map(() => Array(COLS).fill(0)); + score = 0; + level = 1; + lines = 0; + isPaused = false; + isGameOver = false; + dropInterval = 1000; + + updateScore(); + spawnPiece(); + + if (gameLoop) { + cancelAnimationFrame(gameLoop); + } + + lastTime = 0; + gameLoop = requestAnimationFrame(update); + } + + function update(time = 0) { + if (isGameOver) { + drawGameOver(); + return; + } + + const deltaTime = time - lastTime; + lastTime = time; + + if (!isPaused) { + dropCounter += deltaTime; + + if (dropCounter > dropInterval) { + if (!movePiece(0, 1)) { + lockPiece(); + clearLines(); + spawnPiece(); + + if (checkCollision(currentPiece.shape, currentX, currentY)) { + isGameOver = true; + } + } + dropCounter = 0; + } + } + + draw(); + gameLoop = requestAnimationFrame(update); + } + + function spawnPiece() { + const shapes = Object.keys(SHAPES); + const randomShape = shapes[Math.floor(Math.random() * shapes.length)]; + + currentPiece = { + shape: SHAPES[randomShape], + color: COLORS[randomShape], + type: randomShape + }; + + currentX = Math.floor(COLS / 2) - Math.floor(currentPiece.shape[0].length / 2); + currentY = 0; + } + + function movePiece(dx, dy) { + const newX = currentX + dx; + const newY = currentY + dy; + + if (!checkCollision(currentPiece.shape, newX, newY)) { + currentX = newX; + currentY = newY; + return true; + } + return false; + } + + function rotatePiece() { + const rotated = currentPiece.shape[0].map((_, i) => + currentPiece.shape.map(row => row[i]).reverse() + ); + + if (!checkCollision(rotated, currentX, currentY)) { + currentPiece.shape = rotated; + } else { + // Wall kick - try moving left or right + if (!checkCollision(rotated, currentX - 1, currentY)) { + currentPiece.shape = rotated; + currentX--; + } else if (!checkCollision(rotated, currentX + 1, currentY)) { + currentPiece.shape = rotated; + currentX++; + } + } + } + + function hardDrop() { + while (movePiece(0, 1)) { + score += 2; + } + lockPiece(); + clearLines(); + spawnPiece(); + updateScore(); + } + + function checkCollision(shape, x, y) { + for (let row = 0; row < shape.length; row++) { + for (let col = 0; col < shape[row].length; col++) { + if (shape[row][col]) { + const newX = x + col; + const newY = y + row; + + if (newX < 0 || newX >= COLS || newY >= ROWS) { + return true; + } + + if (newY >= 0 && gameBoard[newY][newX]) { + return true; + } + } + } + } + return false; + } + + function lockPiece() { + for (let row = 0; row < currentPiece.shape.length; row++) { + for (let col = 0; col < currentPiece.shape[row].length; col++) { + if (currentPiece.shape[row][col]) { + const y = currentY + row; + const x = currentX + col; + if (y >= 0) { + gameBoard[y][x] = currentPiece.color; + } + } + } + } + } + + function clearLines() { + let linesCleared = 0; + + for (let row = ROWS - 1; row >= 0; row--) { + if (gameBoard[row].every(cell => cell !== 0)) { + gameBoard.splice(row, 1); + gameBoard.unshift(Array(COLS).fill(0)); + linesCleared++; + row++; // Check the same row again + } + } + + if (linesCleared > 0) { + lines += linesCleared; + + // Scoring system + const lineScores = [0, 100, 300, 500, 800]; + score += lineScores[linesCleared] * level; + + // Level up every 10 lines + level = Math.floor(lines / 10) + 1; + dropInterval = Math.max(100, 1000 - (level - 1) * 100); + + updateScore(); + } + } + + function updateScore() { + // Update desktop score + document.getElementById('score').textContent = score; + document.getElementById('level').textContent = level; + document.getElementById('lines').textContent = lines; + + // Update mobile score if elements exist + const scoreMobile = document.getElementById('score-mobile'); + const levelMobile = document.getElementById('level-mobile'); + const linesMobile = document.getElementById('lines-mobile'); + + if (scoreMobile) scoreMobile.textContent = score; + if (levelMobile) levelMobile.textContent = level; + if (linesMobile) linesMobile.textContent = lines; + } + + function draw() { + // Clear canvas + ctx.fillStyle = '#000'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + // Draw grid + ctx.strokeStyle = '#333'; + ctx.lineWidth = 1; + for (let row = 0; row < ROWS; row++) { + for (let col = 0; col < COLS; col++) { + ctx.strokeRect(col * BLOCK_SIZE, row * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE); + } + } + + // Draw locked pieces + for (let row = 0; row < ROWS; row++) { + for (let col = 0; col < COLS; col++) { + if (gameBoard[row][col]) { + drawBlock(col, row, gameBoard[row][col]); + } + } + } + + // Draw current piece + if (currentPiece) { + for (let row = 0; row < currentPiece.shape.length; row++) { + for (let col = 0; col < currentPiece.shape[row].length; col++) { + if (currentPiece.shape[row][col]) { + drawBlock(currentX + col, currentY + row, currentPiece.color); + } + } + } + } + + // Draw pause overlay + if (isPaused) { + ctx.fillStyle = 'rgba(0, 0, 0, 0.7)'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + ctx.fillStyle = '#fff'; + ctx.font = '30px Arial'; + ctx.textAlign = 'center'; + ctx.fillText('PAUSED', canvas.width / 2, canvas.height / 2); + } + } + + function drawBlock(x, y, color) { + ctx.fillStyle = color; + ctx.fillRect(x * BLOCK_SIZE + 1, y * BLOCK_SIZE + 1, BLOCK_SIZE - 2, BLOCK_SIZE - 2); + + // Add some shading for 3D effect + ctx.fillStyle = 'rgba(255, 255, 255, 0.3)'; + ctx.fillRect(x * BLOCK_SIZE + 1, y * BLOCK_SIZE + 1, BLOCK_SIZE - 2, 3); + ctx.fillRect(x * BLOCK_SIZE + 1, y * BLOCK_SIZE + 1, 3, BLOCK_SIZE - 2); + } + + function drawGameOver() { + ctx.fillStyle = 'rgba(0, 0, 0, 0.8)'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + ctx.fillStyle = '#f00'; + ctx.font = 'bold 40px Arial'; + ctx.textAlign = 'center'; + ctx.fillText('GAME OVER', canvas.width / 2, canvas.height / 2 - 20); + + ctx.fillStyle = '#fff'; + ctx.font = '20px Arial'; + ctx.fillText('Press Start to play again', canvas.width / 2, canvas.height / 2 + 20); + } + + function togglePause() { + if (isGameOver || !currentPiece) { + return; + } + isPaused = !isPaused; + } + + function stopGame() { + if (gameLoop) { + cancelAnimationFrame(gameLoop); + gameLoop = null; + } + + // Clear the game board + gameBoard = Array(ROWS).fill().map(() => Array(COLS).fill(0)); + currentPiece = null; + isPaused = false; + isGameOver = true; + + // Draw empty board with "Stopped" message + draw(); + ctx.fillStyle = 'rgba(0, 0, 0, 0.8)'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + ctx.fillStyle = '#ff9800'; + ctx.font = 'bold 40px Arial'; + ctx.textAlign = 'center'; + ctx.fillText('STOPPED', canvas.width / 2, canvas.height / 2 - 20); + + ctx.fillStyle = '#fff'; + ctx.font = '20px Arial'; + ctx.fillText('Press Start to play again', canvas.width / 2, canvas.height / 2 + 20); + } + + return { + startGame: startGame, + togglePause: togglePause, + stopGame: stopGame + }; +})(); + diff --git a/NetLock RMM Agent Comm/Application_Settings.cs b/NetLock RMM Agent Comm/Application_Settings.cs index 69ce3808..3665e9cc 100644 --- a/NetLock RMM Agent Comm/Application_Settings.cs +++ b/NetLock RMM Agent Comm/Application_Settings.cs @@ -13,4 +13,4 @@ internal class Application_Settings public static string NetLock_Events_Database_String = @"Data Source=" + Application_Paths.program_data_netlock_events_database + ";"; public static string NetLock_Local_Encryption_Key = "()TZ%/N)NZTG$/()4i59du4)"; } -} \ No newline at end of file +} \ No newline at end of file diff --git a/NetLock RMM Agent Comm/Global/Device_Information/Hardware.cs b/NetLock RMM Agent Comm/Global/Device_Information/Hardware.cs index 6105999e..88f84705 100644 --- a/NetLock RMM Agent Comm/Global/Device_Information/Hardware.cs +++ b/NetLock RMM Agent Comm/Global/Device_Information/Hardware.cs @@ -70,7 +70,7 @@ public static string CPU_Name() { string cpu_name = string.Empty; // Read the CPU information from the system_profiler command - string systemProfilerOutput = MacOS.Helper.Zsh.Execute_Script("CPU_Name", false, "system_profiler SPHardwareDataType"); + string systemProfilerOutput = MacOS.Helper.Zsh.Execute_Script("CPU_Name", false, "system_profiler SPHardwareDataType", 0); if (!string.IsNullOrEmpty(systemProfilerOutput)) { // Search for the "Processor Name" line @@ -283,11 +283,11 @@ public static string CPU_Information() cpu_name = CPU_Name(); // CPU Cores and Threads - cpu_cores = MacOS.Helper.Zsh.Execute_Script("CPU_Information", false, "sysctl -n hw.physicalcpu").Trim(); - cpu_threads = MacOS.Helper.Zsh.Execute_Script("CPU_Information", false, "sysctl -n hw.logicalcpu").Trim(); + cpu_cores = MacOS.Helper.Zsh.Execute_Script("CPU_Information", false, "sysctl -n hw.physicalcpu", 0).Trim(); + cpu_threads = MacOS.Helper.Zsh.Execute_Script("CPU_Information", false, "sysctl -n hw.logicalcpu", 0).Trim(); // CPU Speed - string cpu_speed_hz = MacOS.Helper.Zsh.Execute_Script("CPU_Information", false, "sysctl -n hw.cpufrequency").Trim(); + string cpu_speed_hz = MacOS.Helper.Zsh.Execute_Script("CPU_Information", false, "sysctl -n hw.cpufrequency", 0).Trim(); if (long.TryParse(cpu_speed_hz, out long cpuSpeedHz)) { double cpuSpeedGHz = cpuSpeedHz / 1_000_000_000.0; // Convert to GHz @@ -295,7 +295,7 @@ public static string CPU_Information() } // CPU Cache Size (L3) - cpu_cache = MacOS.Helper.Zsh.Execute_Script("CPU_Information", false, "sysctl -n hw.l3cachesize").Trim(); + cpu_cache = MacOS.Helper.Zsh.Execute_Script("CPU_Information", false, "sysctl -n hw.l3cachesize", 0).Trim(); if (long.TryParse(cpu_cache, out long cacheBytes)) { double cacheMB = cacheBytes / 1_048_576.0; // Convert to MB @@ -492,7 +492,7 @@ public static int CPU_Usage() try { // Execute the 'top' command to fetch CPU and memory usage - string topOutput = MacOS.Helper.Zsh.Execute_Script("cpu usage", false, "top -l 1 | grep -E \"^CPU|^Phys\""); + string topOutput = MacOS.Helper.Zsh.Execute_Script("cpu usage", false, "top -l 1 | grep -E \"^CPU|^Phys\"", 0); if (!string.IsNullOrWhiteSpace(topOutput)) { @@ -796,7 +796,7 @@ public static string RAM_Information() string hardware_reserved = "N/A"; // Execute vm_stat command to gather memory statistics - string vmStatOutput = MacOS.Helper.Zsh.Execute_Script("RAM_Information", false, "vm_stat"); + string vmStatOutput = MacOS.Helper.Zsh.Execute_Script("RAM_Information", false, "vm_stat", 0); if (!string.IsNullOrEmpty(vmStatOutput)) { // Parse vm_stat output @@ -823,7 +823,7 @@ public static string RAM_Information() } // Use sysctl to gather additional memory information - string sysctlOutput = MacOS.Helper.Zsh.Execute_Script("RAM_Information", false, "sysctl hw.memsize"); + string sysctlOutput = MacOS.Helper.Zsh.Execute_Script("RAM_Information", false, "sysctl hw.memsize", 0); if (!string.IsNullOrEmpty(sysctlOutput)) { // Extract total memory size @@ -949,7 +949,7 @@ public static int RAM_Usage() else if (OperatingSystem.IsLinux()) { // RAM utilisation under Linux - string ramUsage = Linux.Helper.Bash.Execute_Script("RAM_Usage", false, "free | grep Mem | awk '{print $3/$2 * 100}'"); + string ramUsage = Linux.Helper.Bash.Execute_Script("RAM_Usage", false, "free | grep Mem | awk '{print $3/$2 * 100}'", 0); // Parse the result into a double to ensure proper rounding if (double.TryParse(ramUsage, out double usageValue)) @@ -968,7 +968,8 @@ public static int RAM_Usage() string vmStatOutput = MacOS.Helper.Zsh.Execute_Script( "RAM_Usage", false, - "vm_stat | perl -ne '/page size of (\\d+)/ and $size=$1; /Pages\\s+([^:]+)[^\\d]+(\\d+)/ and printf(\"%-16s %16.2f Mi\\n\", \"$1:\", $2 * $size / 1048576);'" + "vm_stat | perl -ne '/page size of (\\d+)/ and $size=$1; /Pages\\s+([^:]+)[^\\d]+(\\d+)/ and printf(\"%-16s %16.2f Mi\\n\", \"$1:\", $2 * $size / 1048576);'", + 0 ); if (string.IsNullOrEmpty(vmStatOutput)) @@ -1480,12 +1481,12 @@ public static string RAM_Total() } else if (OperatingSystem.IsLinux()) { - _ram = Linux.Helper.Bash.Execute_Script("RAM_Total", false, "grep MemTotal /proc/meminfo | awk '{print $2}'"); + _ram = Linux.Helper.Bash.Execute_Script("RAM_Total", false, "grep MemTotal /proc/meminfo | awk '{print $2}'", 0); _ram = Math.Round(Convert.ToDouble(_ram) / 1024 / 1024).ToString(); } else if (OperatingSystem.IsMacOS()) { - string systemProfilerOutput = MacOS.Helper.Zsh.Execute_Script("RAM_Total", false, "system_profiler SPHardwareDataType"); + string systemProfilerOutput = MacOS.Helper.Zsh.Execute_Script("RAM_Total", false, "system_profiler SPHardwareDataType",0); if (!string.IsNullOrEmpty(systemProfilerOutput)) { foreach (string line in systemProfilerOutput.Split('\n')) @@ -1775,7 +1776,7 @@ public static string Mainboard_Name() { if (File.Exists(path)) { - _mainboard = Linux.Helper.Bash.Execute_Script("Mainboard_Name", false, $"cat {path}").Trim(); + _mainboard = Linux.Helper.Bash.Execute_Script("Mainboard_Name", false, $"cat {path}", 0).Trim(); break; } } @@ -1785,7 +1786,7 @@ public static string Mainboard_Name() { if (File.Exists(path)) { - mainboard_manufacturer = Linux.Helper.Bash.Execute_Script("Mainboard_Name", false, $"cat {path}").Trim(); + mainboard_manufacturer = Linux.Helper.Bash.Execute_Script("Mainboard_Name", false, $"cat {path}",0).Trim(); break; } } @@ -1825,7 +1826,7 @@ public static string GPU_Name() } else if (OperatingSystem.IsLinux()) { - string output = Linux.Helper.Bash.Execute_Script("GPU_Name", false, "lshw -C display"); + string output = Linux.Helper.Bash.Execute_Script("GPU_Name", false, "lshw -C display", 0); if (!string.IsNullOrWhiteSpace(output)) { string product = null; diff --git a/NetLock RMM Agent Comm/Global/Device_Information/Network.cs b/NetLock RMM Agent Comm/Global/Device_Information/Network.cs index c180df5b..a2d95d26 100644 --- a/NetLock RMM Agent Comm/Global/Device_Information/Network.cs +++ b/NetLock RMM Agent Comm/Global/Device_Information/Network.cs @@ -123,7 +123,7 @@ public static string Network_Adapter_Information() List network_adapterJsonList = new List(); // Run the 'ip link' command to get a list of network adapters - string output = Linux.Helper.Bash.Execute_Script("Network_Adapter_Information", false, "ip link show"); + string output = Linux.Helper.Bash.Execute_Script("Network_Adapter_Information", false, "ip link show", 0); // Split the output into individual network adapters var networkAdapters = output.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); @@ -134,7 +134,7 @@ public static string Network_Adapter_Information() string adapterName = adapterDetails[1].Trim(); // Run the 'ip' command to get IP address and other details for each adapter - string output2 = Linux.Helper.Bash.Execute_Script("", false, $"ip addr show {adapterName}"); + string output2 = Linux.Helper.Bash.Execute_Script("", false, $"ip addr show {adapterName}", 0); // Parse the necessary information from the second command output string ipv4Address = "N/A"; @@ -205,7 +205,7 @@ public static string Network_Adapter_Information() List network_adapterJsonList = new List(); // Execute the 'ifconfig' command to get the network adapter information - string output = MacOS.Helper.Zsh.Execute_Script("Network_Adapter_Information", false, "ifconfig -a"); + string output = MacOS.Helper.Zsh.Execute_Script("Network_Adapter_Information", false, "ifconfig -a", 0); // Divide the output into individual adapter sections var networkAdapters = output.Split(new[] { "\n\n" }, StringSplitOptions.RemoveEmptyEntries); @@ -221,7 +221,7 @@ public static string Network_Adapter_Information() try { - string networksetupOutput = MacOS.Helper.Zsh.Execute_Script("Network_Adapter_Information", false, "networksetup -listallhardwareports"); + string networksetupOutput = MacOS.Helper.Zsh.Execute_Script("Network_Adapter_Information", false, "networksetup -listallhardwareports", 0); var match = Regex.Match(networksetupOutput, $"Hardware Port: (.+?)\\s+Device: {adapterName}"); if (match.Success) { @@ -267,7 +267,7 @@ public static string Network_Adapter_Information() // DHCP status can be checked using ipconfig (if available) string dhcp_enabled = "N/A"; - var dhcpMatch = Zsh.Execute_Script("Network_Adapter_Information", false, $"ipconfig getpacket {adapterName}"); + var dhcpMatch = Zsh.Execute_Script("Network_Adapter_Information", false, $"ipconfig getpacket {adapterName}", 0); if (!string.IsNullOrEmpty(dhcpMatch) && dhcpMatch.Contains("dhcp")) dhcp_enabled = "True"; else @@ -275,7 +275,7 @@ public static string Network_Adapter_Information() // Subnet mask extraction (ifconfig example) string subnet_mask = "N/A"; - var subnetMatch = Zsh.Execute_Script("Network_Adapter_Information", false, $"ifconfig {adapterName}"); + var subnetMatch = Zsh.Execute_Script("Network_Adapter_Information", false, $"ifconfig {adapterName}", 0); var subnetMaskMatch = Regex.Match(subnetMatch, @"netmask\s+([a-fA-F0-9:]+)"); if (subnetMaskMatch.Success) @@ -381,7 +381,7 @@ public static bool Firewall_Status() if (OperatingSystem.IsLinux()) { // Check whether UFW is installed - string ufwCheck = Bash.Execute_Script("Check_UFW_Installed", false, "command -v ufw"); + string ufwCheck = Bash.Execute_Script("Check_UFW_Installed", false, "command -v ufw", 0); if (string.IsNullOrWhiteSpace(ufwCheck)) { @@ -391,7 +391,7 @@ public static bool Firewall_Status() } // Check the status of the firewall - string firewallStatus = Bash.Execute_Script("Check_Firewall_Status", false, "ufw status"); + string firewallStatus = Bash.Execute_Script("Check_Firewall_Status", false, "ufw status", 0); // Check whether UFW is active if (firewallStatus.Contains("active")) @@ -411,7 +411,7 @@ public static bool Firewall_Status() // 2 = On for essential services // Check if the firewall is enabled - string firewallStatus = Zsh.Execute_Script("Firewall_Status", false, "defaults read /Library/Preferences/com.apple.alf globalstate"); + string firewallStatus = Zsh.Execute_Script("Firewall_Status", false, "defaults read /Library/Preferences/com.apple.alf globalstate", 0); if (firewallStatus.Contains("1")) { diff --git a/NetLock RMM Agent Comm/Global/Device_Information/OS.cs b/NetLock RMM Agent Comm/Global/Device_Information/OS.cs index 52b06982..fb3f5685 100644 --- a/NetLock RMM Agent Comm/Global/Device_Information/OS.cs +++ b/NetLock RMM Agent Comm/Global/Device_Information/OS.cs @@ -89,7 +89,7 @@ public static string Version() try { // macOS Systeminformationen abrufen - string systemInfo = MacOS.Helper.Zsh.Execute_Script("Version", false, "sw_vers"); + string systemInfo = MacOS.Helper.Zsh.Execute_Script("Version", false, "sw_vers", 0); string productName = "-"; string productVersion = "-"; string buildVersion = "-"; @@ -326,7 +326,7 @@ public static string Get_Last_Boot_Time() else if (OperatingSystem.IsMacOS()) { // Get the last boot time from the system - string rawBootTime = MacOS.Helper.Zsh.Execute_Script("Get_Last_Boot_Time", false, "sysctl kern.boottime"); + string rawBootTime = MacOS.Helper.Zsh.Execute_Script("Get_Last_Boot_Time", false, "sysctl kern.boottime", 0); // Extract the seconds part using Regex var match = System.Text.RegularExpressions.Regex.Match(rawBootTime, @"sec\s*=\s*(\d+)"); @@ -384,12 +384,12 @@ public static string Get_Last_Active_User() else if (OperatingSystem.IsLinux()) { // Get the last active user from who command including ip - lastLoggedOnUser = Linux.Helper.Bash.Execute_Script("Get_Last_Active_User", false, "who | awk '{print $1, $5}' | head -n 1"); + lastLoggedOnUser = Linux.Helper.Bash.Execute_Script("Get_Last_Active_User", false, "who | awk '{print $1, $5}' | head -n 1", 0); } else if (OperatingSystem.IsMacOS()) { // Trim any whitespace and return the user - lastLoggedOnUser = MacOS.Helper.Zsh.Execute_Script("Get_Last_Active_User", false, "whoami"); + lastLoggedOnUser = MacOS.Helper.Zsh.Execute_Script("Get_Last_Active_User", false, "whoami", 0); } return lastLoggedOnUser; diff --git a/NetLock RMM Agent Comm/Global/Device_Information/Processes.cs b/NetLock RMM Agent Comm/Global/Device_Information/Processes.cs index f6b1daaf..9aca991e 100644 --- a/NetLock RMM Agent Comm/Global/Device_Information/Processes.cs +++ b/NetLock RMM Agent Comm/Global/Device_Information/Processes.cs @@ -207,7 +207,7 @@ public static string Collect() { int processorCount = Environment.ProcessorCount; - string processes_string = Linux.Helper.Bash.Execute_Script("Collect Application", false, "ps -eo pid,ppid,comm,user,pcpu,pmem,etime,cmd --sort=-pcpu -w"); + string processes_string = Linux.Helper.Bash.Execute_Script("Collect Application", false, "ps -eo pid,ppid,comm,user,pcpu,pmem,etime,cmd --sort=-pcpu -w", 0); Logging.Device_Information("Device_Information.Process_List.Collect", "ps output", processes_string); @@ -248,7 +248,7 @@ public static string Collect() string commandline = string.Join(" ", process_info.Skip(7)); // Get process path - string processPath = Linux.Helper.Bash.Execute_Script("Collect Applications", false, $"readlink -f /proc/{pid}/exe"); + string processPath = Linux.Helper.Bash.Execute_Script("Collect Applications", false, $"readlink -f /proc/{pid}/exe", 0); Process_Data processInfo = new Process_Data { @@ -281,7 +281,7 @@ public static string Collect() int processorCount = Environment.ProcessorCount; // macOS-kompatibler ps-Befehl - string processes_string = MacOS.Helper.Zsh.Execute_Script("Collect", false, "ps -Ao pid,ppid,comm,user,%cpu,%mem,etime,command -r"); + string processes_string = MacOS.Helper.Zsh.Execute_Script("Collect", false, "ps -Ao pid,ppid,comm,user,%cpu,%mem,etime,command -r", 0); Logging.Device_Information("Device_Information.Process_List.Collect", "ps output", processes_string); @@ -466,7 +466,7 @@ public static int Get_CPU_Usage_By_ID(int process_id) { // Execute `ps` command to get process information string command = $"ps -p {process_id} -o %cpu"; - string output = MacOS.Helper.Zsh.Execute_Script("Get_CPU_Usage_By_ID", false, command); + string output = MacOS.Helper.Zsh.Execute_Script("Get_CPU_Usage_By_ID", false, command, 0); // Parse the output var lines = output.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); @@ -633,7 +633,7 @@ public static int Get_RAM_Usage_By_ID(int process_id, bool percentage) // percen try { // Execute `ps` command to get memory usage percentage - string output = MacOS.Helper.Zsh.Execute_Script("Get_RAM_Usage_By_ID", false, $"ps -p {process_id} -o %mem"); + string output = MacOS.Helper.Zsh.Execute_Script("Get_RAM_Usage_By_ID", false, $"ps -p {process_id} -o %mem", 0); // Parse the output var lines = output.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); @@ -653,7 +653,7 @@ public static int Get_RAM_Usage_By_ID(int process_id, bool percentage) // percen if (!percentage) // Return memory usage in MB { // Get total RAM in bytes - string totalRamOutput = MacOS.Helper.Zsh.Execute_Script("Get_RAM_Usage_By_ID", false, "sysctl hw.memsize"); + string totalRamOutput = MacOS.Helper.Zsh.Execute_Script("Get_RAM_Usage_By_ID", false, "sysctl hw.memsize", 0); // Parse total RAM size var totalRamParts = totalRamOutput.Split(':', StringSplitOptions.RemoveEmptyEntries); @@ -840,7 +840,7 @@ private static string GetMacOSProcessOwner(Process process) { // Execute `ps` command to get process information string command = $"ps -p {process.Id} -o user"; - string output = MacOS.Helper.Zsh.Execute_Script("Get_Process_Owner", false, command); + string output = MacOS.Helper.Zsh.Execute_Script("Get_Process_Owner", false, command, 0); // Parse the output var lines = output.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); if (lines.Length < 2) @@ -868,7 +868,7 @@ private static string GetUsernameFromUid(string uid) try { // Use "getent passwd" to fetch username from UID - string result = Linux.Helper.Bash.Execute_Script("GetUsernameFromUid", false, $"getent passwd {uid}"); + string result = Linux.Helper.Bash.Execute_Script("GetUsernameFromUid", false, $"getent passwd {uid}", 0); if (!string.IsNullOrEmpty(result)) { string[] parts = result.Split(':'); diff --git a/NetLock RMM Agent Comm/Global/Device_Information/Software.cs b/NetLock RMM Agent Comm/Global/Device_Information/Software.cs index a1960bf3..475fd77b 100644 --- a/NetLock RMM Agent Comm/Global/Device_Information/Software.cs +++ b/NetLock RMM Agent Comm/Global/Device_Information/Software.cs @@ -245,27 +245,27 @@ public static string Applications_Installed() switch (packageManager.ToLower()) { case "apt": - installedPackages = Linux.Helper.Bash.Execute_Script("Applications_Installed", false, "apt list --installed 2>/dev/null"); + installedPackages = Linux.Helper.Bash.Execute_Script("Applications_Installed", false, "apt list --installed 2>/dev/null",0); Linux.Helper.Package_Manager.ParseAptPackages(installedPackages, applications_installedJsonList, currentApplications); break; case "yum": - installedPackages = Linux.Helper.Bash.Execute_Script("Applications_Installed", false, "yum list installed 2>/dev/null"); + installedPackages = Linux.Helper.Bash.Execute_Script("Applications_Installed", false, "yum list installed 2>/dev/null",0); Linux.Helper.Package_Manager.ParseYumPackages(installedPackages, applications_installedJsonList, currentApplications, "yum"); break; case "dnf": - installedPackages = Linux.Helper.Bash.Execute_Script("Applications_Installed", false, "dnf list installed 2>/dev/null"); + installedPackages = Linux.Helper.Bash.Execute_Script("Applications_Installed", false, "dnf list installed 2>/dev/null",0); Linux.Helper.Package_Manager.ParseYumPackages(installedPackages, applications_installedJsonList, currentApplications, "dnf"); break; case "zypper": - installedPackages = Linux.Helper.Bash.Execute_Script("Applications_Installed", false, "zypper se --installed-only 2>/dev/null"); + installedPackages = Linux.Helper.Bash.Execute_Script("Applications_Installed", false, "zypper se --installed-only 2>/dev/null",0); Linux.Helper.Package_Manager.ParseZypperPackages(installedPackages, applications_installedJsonList, currentApplications); break; case "pacman": - installedPackages = Linux.Helper.Bash.Execute_Script("Applications_Installed", false, "pacman -Q 2>/dev/null"); + installedPackages = Linux.Helper.Bash.Execute_Script("Applications_Installed", false, "pacman -Q 2>/dev/null",0); Linux.Helper.Package_Manager.ParsePacmanPackages(installedPackages, applications_installedJsonList, currentApplications); break; @@ -298,7 +298,7 @@ public static string Applications_Installed() try { // Execute the command to get the list of installed applications - var installedApplications = MacOS.Helper.Zsh.Execute_Script("Applications", false, "system_profiler SPApplicationsDataType -json"); + var installedApplications = MacOS.Helper.Zsh.Execute_Script("Applications", false, "system_profiler SPApplicationsDataType -json",0); // Parse the JSON output from system_profiler var jsonOutput = JsonDocument.Parse(installedApplications); @@ -791,7 +791,7 @@ public static string Applications_Services() List applications_servicesJsonList = new List(); // Execute the systemctl command to list services - string output = Linux.Helper.Bash.Execute_Script("Applications_Services", false, "systemctl list-units --type=service --all --no-pager"); + string output = Linux.Helper.Bash.Execute_Script("Applications_Services", false, "systemctl list-units --type=service --all --no-pager",0); // Split the output into lines (each line corresponds to a service) var lines = output.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); @@ -854,7 +854,7 @@ public static string Applications_Services() List applications_servicesJsonList = new List(); // Execute the launchctl command to list services - string output = MacOS.Helper.Zsh.Execute_Script("Services", false, "launchctl list"); + string output = MacOS.Helper.Zsh.Execute_Script("Services", false, "launchctl list",0); // Split the output into lines (each line corresponds to a service) var lines = output.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); @@ -873,7 +873,7 @@ public static string Applications_Services() // Extract more details from the plist file string plistPath = $"/Library/LaunchDaemons/{label}.plist"; - string plistContent = MacOS.Helper.Zsh.Execute_Script("Plist", false, $"plutil -p \"{plistPath}\""); + string plistContent = MacOS.Helper.Zsh.Execute_Script("Plist", false, $"plutil -p \"{plistPath}\"",0); string startType = plistContent.Contains("KeepAlive") ? "Automatic" : "Manual"; string loginAs = plistContent.Contains("UserName") ? "User Defined" : "root"; @@ -1177,7 +1177,7 @@ public static string Cronjobs() try { // Use `awk` or better parsing command - string output = Linux.Helper.Bash.Execute_Script("Cronjobs", false, "systemctl list-timers --all --no-pager | awk 'NR>1 {for(i=1;i<=NF;i++) printf \"%s|\", $i; printf \"\\n\"}'"); + string output = Linux.Helper.Bash.Execute_Script("Cronjobs", false, "systemctl list-timers --all --no-pager | awk 'NR>1 {for(i=1;i<=NF;i++) printf \"%s|\", $i; printf \"\\n\"}'",0); // Split output into lines var lines = output.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); diff --git a/NetLock RMM Agent Comm/Global/Events/Sender.cs b/NetLock RMM Agent Comm/Global/Events/Sender.cs index 3e6edc3e..53d29998 100644 --- a/NetLock RMM Agent Comm/Global/Events/Sender.cs +++ b/NetLock RMM Agent Comm/Global/Events/Sender.cs @@ -152,8 +152,9 @@ public class Event_Entity authorized = false, }, new JsonSerializerOptions { WriteIndented = true }); - // Write the new server config JSON to the file - File.WriteAllText(Application_Paths.program_data_server_config_json, new_server_config_json); + // Write the new server config JSON to the file (encrypted) + Initialization.Server_Config.SaveEncryptedConfig(new_server_config_json); + Initialization.Server_Config.InvalidateCache(); Device_Worker.authorized = false; } diff --git a/NetLock RMM Agent Comm/Global/Helper/Encryption.cs b/NetLock RMM Agent Comm/Global/Helper/Encryption.cs index 30be26ef..146c352e 100644 --- a/NetLock RMM Agent Comm/Global/Helper/Encryption.cs +++ b/NetLock RMM Agent Comm/Global/Helper/Encryption.cs @@ -10,5 +10,5 @@ namespace Global.Encryption { - //OSSCH_START c0e4508b-1875-4920-bb91-8283de68cd9a //OSSCH_END + //OSSCH_START a6bd90bb-ab23-4aad-9d2b-4c66b9006a34 //OSSCH_END } diff --git a/NetLock RMM Agent Comm/Global/Initialization/Health.cs b/NetLock RMM Agent Comm/Global/Initialization/Health.cs index d4bc715b..4cd32ee7 100644 --- a/NetLock RMM Agent Comm/Global/Initialization/Health.cs +++ b/NetLock RMM Agent Comm/Global/Initialization/Health.cs @@ -196,13 +196,12 @@ public static void User_Processes() // Delete old NetLock RMM User Agent from the registry, if it exists Registry.HKLM_Delete_Value(Application_Paths.hklm_run_directory_reg_path, "NetLock RMM User Process"); - // Write the NetLock RMM User Process to the registry, if it does not exist - Logging.Debug("Initialization.Health.User_Process", "Write to registry", "NetLock RMM User Agent"); - Registry.HKLM_Write_Value(Application_Paths.hklm_run_directory_reg_path, "NetLock RMM User Agent", Application_Paths.netlock_user_process_uac_exe); + // Delete the NetLock RMM User Agent from the registry, if it exists (is now started interactively by the remote agent) + Registry.HKLM_Delete_Value(Application_Paths.hklm_run_directory_reg_path, "NetLock RMM User Agent"); - // Write the NetLock RMM Tray Icon to the registry, if it does not exist + // Delete old NetLock RMM Tray Icon from the registry, if it exists (is now started interactively by the remote agent) Logging.Debug("Initialization.Health.User_Process", "Write to registry", "NetLock RMM Tray Icon"); - Registry.HKLM_Write_Value(Application_Paths.hklm_run_directory_reg_path, "NetLock RMM Tray Icon", Application_Paths.tray_icon_icon_exe); + Registry.HKLM_Delete_Value(Application_Paths.hklm_run_directory_reg_path, "NetLock RMM Tray Icon"); } else if (OperatingSystem.IsLinux()) { diff --git a/NetLock RMM Agent Comm/Global/Initialization/Server_Config.cs b/NetLock RMM Agent Comm/Global/Initialization/Server_Config.cs index 71f0703a..f7f4e143 100644 --- a/NetLock RMM Agent Comm/Global/Initialization/Server_Config.cs +++ b/NetLock RMM Agent Comm/Global/Initialization/Server_Config.cs @@ -7,6 +7,7 @@ using System.IO; using System.Security.Principal; using Global.Helper; +using Global.Encryption; using NetLock_RMM_Agent_Comm; using System.ComponentModel; using System.Runtime.Intrinsics.Wasm; @@ -17,11 +18,144 @@ namespace Global.Initialization { internal class Server_Config { + // Cache for the decrypted JSON to avoid repeated decryption + private static string _cachedDecryptedJson = null; + private static DateTime _cacheTimestamp = DateTime.MinValue; + private static readonly TimeSpan CacheExpiration = TimeSpan.FromMinutes(5); + + /// + /// Loads and decrypts the server config JSON. If the file is unencrypted, it will be converted and encrypted. + /// + private static string LoadAndDecryptConfig() + { + try + { + // Check cache validity + if (_cachedDecryptedJson != null && (DateTime.Now - _cacheTimestamp) < CacheExpiration) + { + return _cachedDecryptedJson; + } + + // Check if file exists + if (!File.Exists(Application_Paths.program_data_server_config_json)) + { + Logging.Error("Server_Config", "LoadAndDecryptConfig", "Server config file does not exist."); + return null; + } + + string fileContent = File.ReadAllText(Application_Paths.program_data_server_config_json); + + // Check if the content is already encrypted (Base64 string without JSON structure) + bool isEncrypted = !fileContent.TrimStart().StartsWith("{"); + + if (isEncrypted) + { + // File is encrypted, decrypt it + Logging.Debug("Server_Config", "LoadAndDecryptConfig", "Config file is encrypted. Decrypting..."); + string decryptedJson = String_Encryption.Decrypt(fileContent, Application_Settings.NetLock_Local_Encryption_Key); + + // Update cache + _cachedDecryptedJson = decryptedJson; + _cacheTimestamp = DateTime.Now; + + return decryptedJson; + } + else + { + // File is unencrypted (legacy format), convert it + Logging.Debug("Server_Config", "LoadAndDecryptConfig", "Config file is unencrypted. Converting to encrypted format..."); + + // Validate that it's valid JSON + using (JsonDocument document = JsonDocument.Parse(fileContent)) + { + // JSON is valid, encrypt and save it + string encryptedContent = String_Encryption.Encrypt(fileContent, Application_Settings.NetLock_Local_Encryption_Key); + File.WriteAllText(Application_Paths.program_data_server_config_json, encryptedContent); + + Logging.Debug("Server_Config", "LoadAndDecryptConfig", "Config file successfully encrypted and saved."); + + // Update cache + _cachedDecryptedJson = fileContent; + _cacheTimestamp = DateTime.Now; + + return fileContent; + } + } + } + catch (Exception ex) + { + Logging.Error("Server_Config", "LoadAndDecryptConfig", ex.ToString()); + return null; + } + } + + /// + /// Saves the server config JSON in encrypted format. + /// + public static bool SaveEncryptedConfig(string jsonContent) + { + try + { + string encryptedContent = String_Encryption.Encrypt(jsonContent, Application_Settings.NetLock_Local_Encryption_Key); + File.WriteAllText(Application_Paths.program_data_server_config_json, encryptedContent); + + // Update cache + _cachedDecryptedJson = jsonContent; + _cacheTimestamp = DateTime.Now; + + Logging.Debug("Server_Config", "SaveEncryptedConfig", "Config file successfully encrypted and saved."); + return true; + } + catch (Exception ex) + { + Logging.Error("Server_Config", "SaveEncryptedConfig", ex.ToString()); + return false; + } + } + + /// + /// Invalidates the cache, forcing a reload on next access. + /// + public static void InvalidateCache() + { + _cachedDecryptedJson = null; + _cacheTimestamp = DateTime.MinValue; + } + + /// + /// Saves the Health Agent server config JSON in encrypted format. + /// + public static bool SaveEncryptedHealthAgentConfig(string jsonContent) + { + try + { + //string encryptedContent = String_Encryption.Encrypt(jsonContent, Application_Settings.NetLock_Local_Encryption_Key); + //File.WriteAllText(Application_Paths.program_data_health_agent_server_config, encryptedContent); + + // Older installations are not able to read the encrypted config. Thats why we still keep it unencrypted for now and will provide a upgrade script for the health agent later. + File.WriteAllText(Application_Paths.program_data_health_agent_server_config, jsonContent); + + Logging.Debug("Server_Config", "SaveEncryptedHealthAgentConfig", "Health Agent config file successfully encrypted and saved."); + return true; + } + catch (Exception ex) + { + Logging.Error("Server_Config", "SaveEncryptedHealthAgentConfig", ex.ToString()); + return false; + } + } + public static string Ssl() { try { - string server_config_json = File.ReadAllText(Application_Paths.program_data_server_config_json); + string server_config_json = LoadAndDecryptConfig(); + + if (server_config_json == null) + { + Logging.Error("Server_Config_Handler", "Ssl", "Failed to load server config."); + return false.ToString(); + } // Parse the JSON using (JsonDocument document = JsonDocument.Parse(server_config_json)) @@ -51,7 +185,13 @@ public static string Package_Guid() { try { - string server_config_json = File.ReadAllText(Application_Paths.program_data_server_config_json); + string server_config_json = LoadAndDecryptConfig(); + + if (server_config_json == null) + { + Logging.Error("Server_Config_Handler", "Package_Guid", "Failed to load server config."); + return false.ToString(); + } // Parse the JSON using (JsonDocument document = JsonDocument.Parse(server_config_json)) @@ -73,7 +213,14 @@ public static string Communication_Servers() { try { - string server_config_json = File.ReadAllText(Application_Paths.program_data_server_config_json); + string server_config_json = LoadAndDecryptConfig(); + + if (server_config_json == null) + { + Logging.Error("Server_Config_Handler", "Communication_Servers", "Failed to load server config."); + return "error"; + } + Logging.Debug("Server_Config_Handler", "Server_Config_Handler.Load (server_config_json)", server_config_json); // Parse the JSON using (JsonDocument document = JsonDocument.Parse(server_config_json)) @@ -94,7 +241,14 @@ public static string Remote_Servers() { try { - string server_config_json = File.ReadAllText(Application_Paths.program_data_server_config_json); + string server_config_json = LoadAndDecryptConfig(); + + if (server_config_json == null) + { + Logging.Error("Server_Config_Handler", "Remote_Servers", "Failed to load server config."); + return "error"; + } + Logging.Debug("Server_Config_Handler", "Server_Config_Handler.Load (server_config_json)", server_config_json); // Parse the JSON using (JsonDocument document = JsonDocument.Parse(server_config_json)) @@ -115,7 +269,14 @@ public static string Update_Servers() { try { - string server_config_json = File.ReadAllText(Application_Paths.program_data_server_config_json); + string server_config_json = LoadAndDecryptConfig(); + + if (server_config_json == null) + { + Logging.Error("Server_Config_Handler", "Update_Servers", "Failed to load server config."); + return "error"; + } + Logging.Debug("Server_Config_Handler", "Server_Config_Handler.Load (server_config_json)", server_config_json); // Parse the JSON using (JsonDocument document = JsonDocument.Parse(server_config_json)) @@ -136,7 +297,14 @@ public static string Trust_Servers() { try { - string server_config_json = File.ReadAllText(Application_Paths.program_data_server_config_json); + string server_config_json = LoadAndDecryptConfig(); + + if (server_config_json == null) + { + Logging.Error("Server_Config_Handler", "Trust_Servers", "Failed to load server config."); + return "error"; + } + Logging.Debug("Server_Config_Handler", "Server_Config_Handler.Load (server_config_json)", server_config_json); // Parse the JSON using (JsonDocument document = JsonDocument.Parse(server_config_json)) @@ -157,7 +325,14 @@ public static string File_Servers() { try { - string server_config_json = File.ReadAllText(Application_Paths.program_data_server_config_json); + string server_config_json = LoadAndDecryptConfig(); + + if (server_config_json == null) + { + Logging.Error("Server_Config_Handler", "File_Servers", "Failed to load server config."); + return "error"; + } + Logging.Debug("Server_Config_Handler", "Server_Config_Handler.Load (server_config_json)", server_config_json); // Parse the JSON using (JsonDocument document = JsonDocument.Parse(server_config_json)) @@ -178,7 +353,14 @@ public static string Tenant_Guid() { try { - string server_config_json = File.ReadAllText(Application_Paths.program_data_server_config_json); + string server_config_json = LoadAndDecryptConfig(); + + if (server_config_json == null) + { + Logging.Error("Server_Config_Handler", "Tenant_Guid", "Failed to load server config."); + return "error"; + } + Logging.Debug("Server_Config_Handler", "Server_Config_Handler.Load (server_config_json)", server_config_json); // Parse the JSON using (JsonDocument document = JsonDocument.Parse(server_config_json)) @@ -199,7 +381,14 @@ public static string Location_Guid() { try { - string server_config_json = File.ReadAllText(Application_Paths.program_data_server_config_json); + string server_config_json = LoadAndDecryptConfig(); + + if (server_config_json == null) + { + Logging.Error("Server_Config_Handler", "Location_Guid", "Failed to load server config."); + return "error"; + } + Logging.Debug("Server_Config_Handler", "Server_Config_Handler.Load (server_config_json)", server_config_json); // Parse the JSON using (JsonDocument document = JsonDocument.Parse(server_config_json)) @@ -220,7 +409,14 @@ public static string Language() { try { - string server_config_json = File.ReadAllText(Application_Paths.program_data_server_config_json); + string server_config_json = LoadAndDecryptConfig(); + + if (server_config_json == null) + { + Logging.Error("Server_Config_Handler", "Language", "Failed to load server config."); + return "error"; + } + Logging.Debug("Server_Config_Handler", "Server_Config_Handler.Load (server_config_json)", server_config_json); // Parse the JSON using (JsonDocument document = JsonDocument.Parse(server_config_json)) @@ -241,7 +437,14 @@ public static bool Authorized() { try { - string serverConfigJson = File.ReadAllText(Application_Paths.program_data_server_config_json); + string serverConfigJson = LoadAndDecryptConfig(); + + if (serverConfigJson == null) + { + Logging.Error("Server_Config_Handler", "Authorized", "Failed to load server config."); + return false; + } + Logging.Debug("Server_Config_Handler", "Authorized", serverConfigJson); // Parse the JSON @@ -274,7 +477,14 @@ public static string Access_Key() { string access_key = String.Empty; - string server_config_json = File.ReadAllText(Application_Paths.program_data_server_config_json); + string server_config_json = LoadAndDecryptConfig(); + + if (server_config_json == null) + { + Logging.Error("Server_Config_Handler", "Access_Key", "Failed to load server config."); + return String.Empty; + } + Logging.Debug("Server_Config_Handler", "Server_Config_Handler.Load (server_config_json)", server_config_json); // Parse the JSON using (JsonDocument document = JsonDocument.Parse(server_config_json)) @@ -314,8 +524,8 @@ public static string Access_Key() string json = JsonSerializer.Serialize(jsonObject, new JsonSerializerOptions { WriteIndented = true }); Logging.Debug("Online_Mode.Handler.Update_Device_Information", "json", json); - // Write the new server config JSON to the file - File.WriteAllText(Application_Paths.program_data_server_config_json, json); + // Write the new server config JSON to the file (encrypted) + SaveEncryptedConfig(json); } else { diff --git a/NetLock RMM Agent Comm/Global/Initialization/Version.cs b/NetLock RMM Agent Comm/Global/Initialization/Version.cs index b018b6fd..877f1a02 100644 --- a/NetLock RMM Agent Comm/Global/Initialization/Version.cs +++ b/NetLock RMM Agent Comm/Global/Initialization/Version.cs @@ -215,9 +215,9 @@ public static async Task Update() // Set permissions on linux & macos if (OperatingSystem.IsLinux()) - Bash.Execute_Script("Installer Permissions", false, $"chmod +x \"{Application_Paths.c_temp_installer_path}\""); + Bash.Execute_Script("Installer Permissions", false, $"chmod +x \"{Application_Paths.c_temp_installer_path}\"",0); else if (OperatingSystem.IsMacOS()) - Bash.Execute_Script("Installer Permissions", false, $"chmod +x \"{Application_Paths.c_temp_installer_path}\""); + Bash.Execute_Script("Installer Permissions", false, $"chmod +x \"{Application_Paths.c_temp_installer_path}\"",0); // Run the installer if (OperatingSystem.IsWindows()) diff --git a/NetLock RMM Agent Comm/Global/Jobs/Time_Scheduler.cs b/NetLock RMM Agent Comm/Global/Jobs/Time_Scheduler.cs index 8e201808..8e92cc61 100644 --- a/NetLock RMM Agent Comm/Global/Jobs/Time_Scheduler.cs +++ b/NetLock RMM Agent Comm/Global/Jobs/Time_Scheduler.cs @@ -25,6 +25,7 @@ public class Job public string platform { get; set; } public string type { get; set; } public string script { get; set; } + public int? timeout { get; set; } // Nullable to handle null values in JSON public int time_scheduler_type { get; set; } public int time_scheduler_seconds { get; set; } @@ -41,6 +42,55 @@ public class Job public bool time_scheduler_sunday { get; set; } } + // Helper method to check if job should run today based on weekday settings + private static bool ShouldRunToday(Job job) + { + try + { + switch (DateTime.Now.DayOfWeek) + { + case DayOfWeek.Monday: + return job.time_scheduler_monday; + case DayOfWeek.Tuesday: + return job.time_scheduler_tuesday; + case DayOfWeek.Wednesday: + return job.time_scheduler_wednesday; + case DayOfWeek.Thursday: + return job.time_scheduler_thursday; + case DayOfWeek.Friday: + return job.time_scheduler_friday; + case DayOfWeek.Saturday: + return job.time_scheduler_saturday; + case DayOfWeek.Sunday: + return job.time_scheduler_sunday; + default: + return false; + } + } + catch (Exception e) + { + Logging.Error("Sensors.Time_Scheduler.ShouldRunToday", "Check if sensor should run today", + "Sensor id: " + job.id + " Exception: " + e.ToString()); + + return false; + } + } + + // Helper method to write encrypted job to disk + private static void WriteEncryptedJob(string filePath, Job job) + { + try + { + string job_json = JsonSerializer.Serialize(job); + string encrypted_json = Encryption.String_Encryption.Encrypt(job_json, Application_Settings.NetLock_Local_Encryption_Key); + File.WriteAllText(filePath, encrypted_json); + } + catch (Exception e) + { + Logging.Error("Jobs.Time_Scheduler.WriteEncryptedJob", "Error writing encrypted job", e.ToString()); + } + } + public static void Check_Execution() { Logging.Jobs("Jobs.Time_Scheduler.Check_Execution", "Check job execution", "Start"); @@ -56,34 +106,62 @@ public static void Check_Execution() // Write each job to disk if not already exists, check if script has changed foreach (var job in job_items) { - // Check if job is for the current platform - if (OperatingSystem.IsWindows() && job.platform != "Windows") - continue; - else if (OperatingSystem.IsLinux() && job.platform != "Linux") - continue; - else if (OperatingSystem.IsMacOS() && job.platform != "MacOS") - continue; - - Logging.Jobs("Jobs.Time_Scheduler.Check_Execution", "Check if job exists on disk", "Job: " + job.name + " Job id: " + job.id); - - string job_json = JsonSerializer.Serialize(job); - string job_path = Path.Combine(Application_Paths.program_data_jobs, job.id + ".json"); - - if (!File.Exists(job_path)) + try { - Logging.Jobs("Jobs.Time_Scheduler.Check_Execution", "Check if job exists on disk", "false"); - File.WriteAllText(job_path, job_json); - } + // Check if job is for the current platform + if (OperatingSystem.IsWindows() && job.platform != "Windows") + continue; + else if (OperatingSystem.IsLinux() && job.platform != "Linux") + continue; + else if (OperatingSystem.IsMacOS() && job.platform != "MacOS") + continue; + + Logging.Jobs("Jobs.Time_Scheduler.Check_Execution", "Check if job exists on disk", "Job: " + job.name + " Job id: " + job.id); + + string job_json = JsonSerializer.Serialize(job); + string job_path = Path.Combine(Application_Paths.program_data_jobs, job.id + ".json"); + + if (!File.Exists(job_path)) + { + Logging.Jobs("Jobs.Time_Scheduler.Check_Execution", "Check if job exists on disk", "false"); + // Encrypt job JSON before writing + string encrypted_job_json = Encryption.String_Encryption.Encrypt(job_json, Application_Settings.NetLock_Local_Encryption_Key); + File.WriteAllText(job_path, encrypted_job_json); + } - // Check if script has changed - if (File.Exists(job_path)) + // Check if script has changed + if (File.Exists(job_path)) + { + // Decrypt job JSON after reading + string encrypted_existing_job_json = File.ReadAllText(job_path); + string existing_job_json = Encryption.String_Encryption.Decrypt(encrypted_existing_job_json, Application_Settings.NetLock_Local_Encryption_Key); + + Job existing_job = JsonSerializer.Deserialize(existing_job_json); + + if (existing_job.script != job.script) + { + Logging.Jobs("Jobs.Time_Scheduler.Check_Execution", "Job script has changed. Updating it.", "Job: " + job.name + " Job id: " + job.id); + // Encrypt job JSON before writing + string encrypted_job_json = Encryption.String_Encryption.Encrypt(job_json, Application_Settings.NetLock_Local_Encryption_Key); + File.WriteAllText(job_path, encrypted_job_json); + } + } + } + catch (Exception e) { - string existing_job_json = File.ReadAllText(job_path); - Job existing_job = JsonSerializer.Deserialize(existing_job_json); - if (existing_job.script != job.script) + Logging.Error("Jobs.Time_Scheduler.Check_Execution", "Error processing job before execution check", "Job id: " + job.id + " Exception: " + e.ToString()); + + // Delete corrupted file so it will be recreated + string job_path = Path.Combine(Application_Paths.program_data_jobs, job.id + ".json"); + + try + { + File.Delete(job_path); + Logging.Jobs("Jobs.Time_Scheduler.Check_Execution", "Deleted corrupted job file (pre-execution check)", "Job file: " + job_path); + } + catch (Exception deleteEx) { - Logging.Jobs("Jobs.Time_Scheduler.Check_Execution", "Job script has changed. Updating it.", "Job: " + job.name + " Job id: " + job.id); - File.WriteAllText(job_path, job_json); + Logging.Error("Jobs.Time_Scheduler.Check_Execution", "Failed to delete corrupted job file (pre-execution check)", "Job file: " + job_path + " Error: " + deleteEx.Message); } } } @@ -91,364 +169,362 @@ public static void Check_Execution() // Clean up old jobs not existing anymore foreach (string file in Directory.GetFiles(Application_Paths.program_data_jobs)) { - Logging.Jobs("Jobs.Time_Scheduler.Check_Execution", "Clean old jobs", "Job: " + file); + try + { + Logging.Jobs("Jobs.Time_Scheduler.Check_Execution", "Clean old jobs", "Job: " + file); - string file_name = Path.GetFileName(file); - string file_id = file_name.Replace(".json", ""); + string file_name = Path.GetFileName(file); + string file_id = file_name.Replace(".json", ""); - bool found = false; + bool found = false; - foreach (var job in job_items) - { - if (job.id == file_id) + foreach (var job in job_items) { - found = true; - break; + if (job.id == file_id) + { + found = true; + break; + } } - } - if (!found) + if (!found) + { + Logging.Jobs("Jobs.Time_Scheduler.Check_Execution", "Clean old jobs", "Delete job: " + file); + File.Delete(file); + } + } + catch (Exception e) { - Logging.Jobs("Jobs.Time_Scheduler.Check_Execution", "Clean old jobs", "Delete job: " + file); - File.Delete(file); + Logging.Error("Jobs.Time_Scheduler.Check_Execution", "Error during cleanup of old jobs", "Job file: " + file + " Exception: " + e.ToString()); } } // Now read & consume each job foreach (var job in Directory.GetFiles(Application_Paths.program_data_jobs)) { - string job_json = File.ReadAllText(job); - Job job_item = JsonSerializer.Deserialize(job_json); - - Logging.Jobs("Jobs.Time_Scheduler.Check_Execution", "Check job execution", "Job: " + job_item.name + " time_scheduler_type: " + job_item.time_scheduler_type); - - // Check enabled - /*if (!job_item.enabled) - { - Logging.Handler.Jobs("Jobs.Time_Scheduler.Check_Execution", "Check job execution", "Job disabled"); - - continue; - }*/ - - bool execute = false; - - if (job_item.time_scheduler_type == 0) // system boot + Job job_item = null; + + try { - Logging.Jobs("Jobs.Time_Scheduler.Check_Execution", "System boot", "name: " + job_item.name + " id: " + job_item.id + " last_run: " + DateTime.Parse(job_item.last_run ?? DateTime.Now.ToString()) + " Last boot: " + os_up_time.ToString()); + // Decrypt job JSON after reading + string encrypted_job_json = File.ReadAllText(job); + string job_json = Encryption.String_Encryption.Decrypt(encrypted_job_json, Application_Settings.NetLock_Local_Encryption_Key); + job_item = JsonSerializer.Deserialize(job_json); - // Check if last run is empty, if so set it to now - if (String.IsNullOrEmpty(job_item.last_run)) + // Null-check after deserialization + if (job_item == null) { - job_item.last_run = DateTime.Now.ToString(); - string updated_job_json = JsonSerializer.Serialize(job_item); - File.WriteAllText(job, updated_job_json); + Logging.Error("Jobs.Time_Scheduler.Check_Execution", "Failed to deserialize job", "Job file: " + job); + + // Delete corrupted file so it will be recreated + File.Delete(job); + Logging.Jobs("Jobs.Time_Scheduler.Check_Execution", "Deleted corrupted job file (job was null)", "Job file: " + job); + + continue; } + + Logging.Jobs("Jobs.Time_Scheduler.Check_Execution", "Check job execution", "Job: " + job_item.name + " time_scheduler_type: " + job_item.time_scheduler_type); - if (DateTime.Parse(job_item.last_run) < os_up_time) - execute = true; - } - else if (job_item.time_scheduler_type == 1) // date & time - { - Logging.Jobs("Jobs.Time_Scheduler.Check_Execution", "date & time", "name: " + job_item.name + " id: " + job_item.id + " last_run: " + DateTime.Parse(job_item.last_run ?? DateTime.Now.ToString())); - - DateTime scheduledDateTime = DateTime.ParseExact($"{job_item.time_scheduler_date.Split(' ')[0]} {job_item.time_scheduler_time}", "dd.MM.yyyy HH:mm:ss", CultureInfo.InvariantCulture); - - // Check if last run is empty, if so, subsract 24 hours from scheduled time to trigger the execution - if (String.IsNullOrEmpty(job_item.last_run)) + // Check enabled + /*if (!job_item.enabled) { - job_item.last_run = (scheduledDateTime - TimeSpan.FromHours(24)).ToString(); - string updated_job_json = JsonSerializer.Serialize(job_item); - File.WriteAllText(job, updated_job_json); - } - - DateTime lastRunDateTime = DateTime.Parse(job_item.last_run); + Logging.Handler.Jobs("Jobs.Time_Scheduler.Check_Execution", "Check job execution", "Job disabled"); - Logging.Jobs("Jobs.Time_Scheduler.Check_Execution", "date & time", "name: " + job_item.name + " id: " + job_item.id + " last_run: " + DateTime.Parse(job_item.last_run) + " scheduledDateTime: " + scheduledDateTime.ToString() + " execute: " + execute.ToString()); + continue; + }*/ - if (DateTime.Now.Date >= scheduledDateTime.Date && DateTime.Now.TimeOfDay >= scheduledDateTime.TimeOfDay && lastRunDateTime < scheduledDateTime) - execute = true; - } - else if (job_item.time_scheduler_type == 2) // all x seconds - { - Logging.Jobs("Jobs.Time_Scheduler.Check_Execution", "all x seconds", "name: " + job_item.name + " id: " + job_item.id + " last_run: " + DateTime.Parse(job_item.last_run ?? DateTime.Now.ToString())); + bool execute = false; - // Check if last run is empty, if so set it to now - if (String.IsNullOrEmpty(job_item.last_run)) + if (job_item.time_scheduler_type == 0) // system boot { - job_item.last_run = DateTime.Now.ToString(); - string updated_job_json = JsonSerializer.Serialize(job_item); - File.WriteAllText(job, updated_job_json); - } + Logging.Jobs("Jobs.Time_Scheduler.Check_Execution", "System boot", "name: " + job_item.name + " id: " + job_item.id + " last_run: " + DateTime.Parse(job_item.last_run ?? DateTime.Now.ToString()) + " Last boot: " + os_up_time.ToString()); - if (DateTime.Parse(job_item.last_run) <= DateTime.Now - TimeSpan.FromSeconds(job_item.time_scheduler_seconds)) - execute = true; + // Check if last run is empty, if so set it to now + if (String.IsNullOrEmpty(job_item.last_run)) + { + job_item.last_run = DateTime.Now.ToString(); + WriteEncryptedJob(job, job_item); + } - Logging.Jobs("Jobs.Time_Scheduler.Check_Execution", "all x seconds", "name: " + job_item.name + " id: " + job_item.id + " last_run: " + DateTime.Parse(job_item.last_run) + " execute: " + execute.ToString()); - } - else if (job_item.time_scheduler_type == 3) // all x minutes - { - Logging.Jobs("Jobs.Time_Scheduler.Check_Execution", "all x minutes", "name: " + job_item.name + " id: " + job_item.id + " last_run: " + DateTime.Parse(job_item.last_run ?? DateTime.Now.ToString())); - - // Check if last run is empty, if so set it to now - if (String.IsNullOrEmpty(job_item.last_run)) - { - job_item.last_run = DateTime.Now.ToString(); - string updated_job_json = JsonSerializer.Serialize(job_item); - File.WriteAllText(job, updated_job_json); + if (DateTime.Parse(job_item.last_run) < os_up_time) + execute = true; } - - if (DateTime.Parse(job_item.last_run) < DateTime.Now - TimeSpan.FromMinutes(job_item.time_scheduler_minutes)) - execute = true; - - Logging.Jobs("Jobs.Time_Scheduler.Check_Execution", "all x minutes", "name: " + job_item.name + " id: " + job_item.id + " last_run: " + DateTime.Parse(job_item.last_run) + " execute: " + execute.ToString()); - } - else if (job_item.time_scheduler_type == 4) // all x hours - { - Logging.Jobs("Jobs.Time_Scheduler.Check_Execution", "all x hours", "name: " + job_item.name + " id: " + job_item.id + " last_run: " + DateTime.Parse(job_item.last_run ?? DateTime.Now.ToString())); - - // Check if last run is empty, if so set it to now - if (String.IsNullOrEmpty(job_item.last_run)) + else if (job_item.time_scheduler_type == 1) // date & time { - job_item.last_run = DateTime.Now.ToString(); - string updated_job_json = JsonSerializer.Serialize(job_item); - File.WriteAllText(job, updated_job_json); - } - - if (DateTime.Parse(job_item.last_run) < DateTime.Now - TimeSpan.FromHours(job_item.time_scheduler_hours)) - execute = true; + Logging.Jobs("Jobs.Time_Scheduler.Check_Execution", "date & time", "name: " + job_item.name + " id: " + job_item.id + " last_run: " + DateTime.Parse(job_item.last_run ?? DateTime.Now.ToString())); - Logging.Jobs("Jobs.Time_Scheduler.Check_Execution", "all x hours", "name: " + job_item.name + " id: " + job_item.id + " last_run: " + DateTime.Parse(job_item.last_run) + " execute: " + execute.ToString()); - } - else if (job_item.time_scheduler_type == 5) // date, all x seconds - { - Logging.Jobs("Jobs.Time_Scheduler.Check_Execution", "date, all x seconds", "name: " + job_item.name + " id: " + job_item.id + " last_run: " + DateTime.Parse(job_item.last_run ?? DateTime.Now.ToString())); + DateTime scheduledDateTime = DateTime.ParseExact($"{job_item.time_scheduler_date.Split(' ')[0]} {job_item.time_scheduler_time}", "dd.MM.yyyy HH:mm:ss", CultureInfo.InvariantCulture); - // Check if last run is empty, if so set it to now - if (String.IsNullOrEmpty(job_item.last_run)) - { - job_item.last_run = DateTime.Now.ToString(); - string updated_job_json = JsonSerializer.Serialize(job_item); - File.WriteAllText(job, updated_job_json); - } + // Check if last run is empty, if so, subsract 24 hours from scheduled time to trigger the execution + if (String.IsNullOrEmpty(job_item.last_run)) + { + job_item.last_run = (scheduledDateTime - TimeSpan.FromHours(24)).ToString(); + WriteEncryptedJob(job, job_item); + } - if (DateTime.Now.Date == DateTime.Parse(job_item.time_scheduler_date).Date && DateTime.Parse(job_item.last_run) <= DateTime.Now - TimeSpan.FromSeconds(job_item.time_scheduler_seconds)) - execute = true; + DateTime lastRunDateTime = DateTime.Parse(job_item.last_run); - Logging.Jobs("Jobs.Time_Scheduler.Check_Execution", "date, all x seconds", "name: " + job_item.name + " id: " + job_item.id + " last_run: " + DateTime.Parse(job_item.last_run) + " execute: " + execute.ToString()); - } - else if (job_item.time_scheduler_type == 6) // date, all x minutes - { - Logging.Jobs("Jobs.Time_Scheduler.Check_Execution", "date, all x minutes", "name: " + job_item.name + " id: " + job_item.id + " last_run: " + DateTime.Parse(job_item.last_run ?? DateTime.Now.ToString())); + Logging.Jobs("Jobs.Time_Scheduler.Check_Execution", "date & time", "name: " + job_item.name + " id: " + job_item.id + " last_run: " + DateTime.Parse(job_item.last_run) + " scheduledDateTime: " + scheduledDateTime.ToString() + " execute: " + execute.ToString()); - // Check if last run is empty, if so set it to now - if (String.IsNullOrEmpty(job_item.last_run)) - { - job_item.last_run = DateTime.Now.ToString(); - string updated_job_json = JsonSerializer.Serialize(job_item); - File.WriteAllText(job, updated_job_json); + if (DateTime.Now.Date >= scheduledDateTime.Date && DateTime.Now.TimeOfDay >= scheduledDateTime.TimeOfDay && lastRunDateTime < scheduledDateTime) + execute = true; } - - if (DateTime.Now.Date == DateTime.Parse(job_item.time_scheduler_date).Date && DateTime.Parse(job_item.last_run) < DateTime.Now - TimeSpan.FromMinutes(job_item.time_scheduler_minutes)) - execute = true; - - Logging.Jobs("Jobs.Time_Scheduler.Check_Execution", "date, all x minutes", "name: " + job_item.name + " id: " + job_item.id + " last_run: " + DateTime.Parse(job_item.last_run) + " execute: " + execute.ToString()); - } - else if (job_item.time_scheduler_type == 7) // date, all x hours - { - Logging.Jobs("Jobs.Time_Scheduler.Check_Execution", "date, all x hours", "name: " + job_item.name + " id: " + job_item.id + " last_run: " + DateTime.Parse(job_item.last_run ?? DateTime.Now.ToString())); - - // Check if last run is empty, if so set it to now - if (String.IsNullOrEmpty(job_item.last_run)) + else if (job_item.time_scheduler_type == 2) // all x seconds { - job_item.last_run = DateTime.Now.ToString(); - string updated_job_json = JsonSerializer.Serialize(job_item); - File.WriteAllText(job, updated_job_json); - } - - if (DateTime.Now.Date == DateTime.Parse(job_item.time_scheduler_date).Date && DateTime.Parse(job_item.last_run) < DateTime.Now - TimeSpan.FromHours(job_item.time_scheduler_hours)) - execute = true; + Logging.Jobs("Jobs.Time_Scheduler.Check_Execution", "all x seconds", "name: " + job_item.name + " id: " + job_item.id + " last_run: " + DateTime.Parse(job_item.last_run ?? DateTime.Now.ToString())); - Logging.Jobs("Jobs.Time_Scheduler.Check_Execution", "date, all x hours", "name: " + job_item.name + " id: " + job_item.id + " last_run: " + DateTime.Parse(job_item.last_run) + " execute: " + execute.ToString()); - } - else if (job_item.time_scheduler_type == 8) // following days at X time - { - Logging.Jobs("Jobs.Time_Scheduler.Check_Execution", "following days at X time", "name: " + job_item.name + " id: " + job_item.id + " last_run: " + DateTime.Parse(job_item.last_run ?? DateTime.Now.ToString())); + // Check if last run is empty, if so set it to now + if (String.IsNullOrEmpty(job_item.last_run)) + { + job_item.last_run = DateTime.Now.ToString(); + WriteEncryptedJob(job, job_item); + } - DateTime scheduledDateTime = DateTime.ParseExact($"{job_item.time_scheduler_date.Split(' ')[0]} {job_item.time_scheduler_time}", "dd.MM.yyyy HH:mm:ss", CultureInfo.InvariantCulture); + if (DateTime.Parse(job_item.last_run) <= DateTime.Now - TimeSpan.FromSeconds(job_item.time_scheduler_seconds)) + execute = true; - // Check if last run is empty, if so, subsract 24 hours from scheduled time to trigger the execution - if (String.IsNullOrEmpty(job_item.last_run)) - { - job_item.last_run = (scheduledDateTime - TimeSpan.FromHours(24)).ToString(); - string updated_job_json = JsonSerializer.Serialize(job_item); - File.WriteAllText(job, updated_job_json); + Logging.Jobs("Jobs.Time_Scheduler.Check_Execution", "all x seconds", "name: " + job_item.name + " id: " + job_item.id + " last_run: " + DateTime.Parse(job_item.last_run) + " execute: " + execute.ToString()); } + else if (job_item.time_scheduler_type == 3) // all x minutes + { + Logging.Jobs("Jobs.Time_Scheduler.Check_Execution", "all x minutes", "name: " + job_item.name + " id: " + job_item.id + " last_run: " + (job_item.last_run ?? DateTime.Now.ToString(CultureInfo.InvariantCulture))); - DateTime lastRunDateTime = DateTime.Parse(job_item.last_run); - - if (DateTime.Now.DayOfWeek.ToString() == "Monday" && job_item.time_scheduler_monday && DateTime.Now.TimeOfDay >= scheduledDateTime.TimeOfDay && lastRunDateTime < scheduledDateTime) - execute = true; + // Check if last run is empty, if so set it to now + if (String.IsNullOrEmpty(job_item.last_run)) + { + job_item.last_run = DateTime.Now.ToString(CultureInfo.InvariantCulture); + WriteEncryptedJob(job, job_item); + } - if (DateTime.Now.DayOfWeek.ToString() == "Tuesday" && job_item.time_scheduler_tuesday && DateTime.Now.TimeOfDay >= scheduledDateTime.TimeOfDay && lastRunDateTime < scheduledDateTime) - execute = true; + DateTime lastRun = DateTime.Parse(job_item.last_run, CultureInfo.InvariantCulture); + if (lastRun <= DateTime.Now - TimeSpan.FromMinutes(job_item.time_scheduler_minutes)) + execute = true; - if (DateTime.Now.DayOfWeek.ToString() == "Wednesday" && job_item.time_scheduler_wednesday && DateTime.Now.TimeOfDay >= scheduledDateTime.TimeOfDay && lastRunDateTime < scheduledDateTime) - execute = true; + Logging.Jobs("Jobs.Time_Scheduler.Check_Execution", "all x minutes", "name: " + job_item.name + " id: " + job_item.id + " last_run: " + lastRun + " execute: " + execute.ToString()); + } + else if (job_item.time_scheduler_type == 4) // all x hours + { + Logging.Jobs("Sensors.Time_Scheduler.Check_Execution", "all x hours", "name: " + job_item.name + " id: " + job_item.id + " last_run: " + (job_item.last_run ?? DateTime.Now.ToString(CultureInfo.InvariantCulture))); - if (DateTime.Now.DayOfWeek.ToString() == "Thursday" && job_item.time_scheduler_thursday && DateTime.Now.TimeOfDay >= scheduledDateTime.TimeOfDay && lastRunDateTime < scheduledDateTime) - execute = true; + // Check if last run is empty, if so set it to now + if (String.IsNullOrEmpty(job_item.last_run)) + { + job_item.last_run = DateTime.Now.ToString(CultureInfo.InvariantCulture); + WriteEncryptedJob(job, job_item); + } - if (DateTime.Now.DayOfWeek.ToString() == "Friday" && job_item.time_scheduler_friday && DateTime.Now.TimeOfDay >= scheduledDateTime.TimeOfDay && lastRunDateTime < scheduledDateTime) - execute = true; + DateTime lastRun = DateTime.Parse(job_item.last_run, CultureInfo.InvariantCulture); + if (lastRun <= DateTime.Now - TimeSpan.FromHours(job_item.time_scheduler_hours)) + execute = true; - if (DateTime.Now.DayOfWeek.ToString() == "Saturday" && job_item.time_scheduler_saturday && DateTime.Now.TimeOfDay >= scheduledDateTime.TimeOfDay && lastRunDateTime < scheduledDateTime) - execute = true; + Logging.Jobs("Jobs.Time_Scheduler.Check_Execution", "all x hours", "name: " + job_item.name + " id: " + job_item.id + " last_run: " + lastRun + " execute: " + execute.ToString()); + } + else if (job_item.time_scheduler_type == 5) // date, all x seconds + { + Logging.Jobs("Jobs.Time_Scheduler.Check_Execution", "date, all x seconds", "name: " + job_item.name + " id: " + job_item.id + " last_run: " + DateTime.Parse(job_item.last_run ?? DateTime.Now.ToString())); - if (DateTime.Now.DayOfWeek.ToString() == "Sunday" && job_item.time_scheduler_sunday && DateTime.Now.TimeOfDay >= scheduledDateTime.TimeOfDay && lastRunDateTime < scheduledDateTime) - execute = true; + // Check if last run is empty, if so set it to now + if (String.IsNullOrEmpty(job_item.last_run)) + { + job_item.last_run = DateTime.Now.ToString(); + WriteEncryptedJob(job, job_item); + } - Logging.Jobs("Jobs.Time_Scheduler.Check_Execution", "following days at X time", "name: " + job_item.name + " id: " + job_item.id + " last_run: " + DateTime.Parse(job_item.last_run) + " execute: " + execute.ToString()); - } - else if (job_item.time_scheduler_type == 9) // following days, x seconds - { - Logging.Jobs("Jobs.Time_Scheduler.Check_Execution", "following days, x seconds", "name: " + job_item.name + " id: " + job_item.id + " last_run: " + DateTime.Parse(job_item.last_run ?? DateTime.Now.ToString())); + if (DateTime.Now.Date == DateTime.Parse(job_item.time_scheduler_date).Date && DateTime.Parse(job_item.last_run) <= DateTime.Now - TimeSpan.FromSeconds(job_item.time_scheduler_seconds)) + execute = true; - // Check if last run is empty, if so set it to now - if (String.IsNullOrEmpty(job_item.last_run)) - { - job_item.last_run = DateTime.Now.ToString(); - string updated_job_json = JsonSerializer.Serialize(job_item); - File.WriteAllText(job, updated_job_json); + Logging.Jobs("Jobs.Time_Scheduler.Check_Execution", "date, all x seconds", "name: " + job_item.name + " id: " + job_item.id + " last_run: " + DateTime.Parse(job_item.last_run) + " execute: " + execute.ToString()); } + else if (job_item.time_scheduler_type == 6) // date, all x minutes + { + Logging.Jobs("Jobs.Time_Scheduler.Check_Execution", "date, all x minutes", "name: " + job_item.name + " id: " + job_item.id + " last_run: " + DateTime.Parse(job_item.last_run ?? DateTime.Now.ToString())); - if (DateTime.Now.DayOfWeek.ToString() == "Monday" && job_item.time_scheduler_monday && DateTime.Parse(job_item.last_run) <= DateTime.Now - TimeSpan.FromSeconds(job_item.time_scheduler_seconds)) - execute = true; + // Check if last run is empty, if so set it to now + if (String.IsNullOrEmpty(job_item.last_run)) + { + job_item.last_run = DateTime.Now.ToString(); + WriteEncryptedJob(job, job_item); + } - if (DateTime.Now.DayOfWeek.ToString() == "Tuesday" && job_item.time_scheduler_tuesday && DateTime.Parse(job_item.last_run) <= DateTime.Now - TimeSpan.FromSeconds(job_item.time_scheduler_seconds)) - execute = true; + if (DateTime.Now.Date == DateTime.Parse(job_item.time_scheduler_date).Date && DateTime.Parse(job_item.last_run) < DateTime.Now - TimeSpan.FromMinutes(job_item.time_scheduler_minutes)) + execute = true; - if (DateTime.Now.DayOfWeek.ToString() == "Wednesday" && job_item.time_scheduler_wednesday && DateTime.Parse(job_item.last_run) <= DateTime.Now - TimeSpan.FromSeconds(job_item.time_scheduler_seconds)) - execute = true; + Logging.Jobs("Jobs.Time_Scheduler.Check_Execution", "date, all x minutes", "name: " + job_item.name + " id: " + job_item.id + " last_run: " + DateTime.Parse(job_item.last_run) + " execute: " + execute.ToString()); + } + else if (job_item.time_scheduler_type == 7) // date, all x hours + { + Logging.Jobs("Jobs.Time_Scheduler.Check_Execution", "date, all x hours", "name: " + job_item.name + " id: " + job_item.id + " last_run: " + DateTime.Parse(job_item.last_run ?? DateTime.Now.ToString())); - if (DateTime.Now.DayOfWeek.ToString() == "Thursday" && job_item.time_scheduler_thursday && DateTime.Parse(job_item.last_run) <= DateTime.Now - TimeSpan.FromSeconds(job_item.time_scheduler_seconds)) - execute = true; + // Check if last run is empty, if so set it to now + if (String.IsNullOrEmpty(job_item.last_run)) + { + job_item.last_run = DateTime.Now.ToString(); + WriteEncryptedJob(job, job_item); + } - if (DateTime.Now.DayOfWeek.ToString() == "Friday" && job_item.time_scheduler_friday && DateTime.Parse(job_item.last_run) <= DateTime.Now - TimeSpan.FromSeconds(job_item.time_scheduler_seconds)) - execute = true; + if (DateTime.Now.Date == DateTime.Parse(job_item.time_scheduler_date).Date && DateTime.Parse(job_item.last_run) < DateTime.Now - TimeSpan.FromHours(job_item.time_scheduler_hours)) + execute = true; - if (DateTime.Now.DayOfWeek.ToString() == "Saturday" && job_item.time_scheduler_saturday && DateTime.Parse(job_item.last_run) <= DateTime.Now - TimeSpan.FromSeconds(job_item.time_scheduler_seconds)) - execute = true; + Logging.Jobs("Jobs.Time_Scheduler.Check_Execution", "date, all x hours", "name: " + job_item.name + " id: " + job_item.id + " last_run: " + DateTime.Parse(job_item.last_run) + " execute: " + execute.ToString()); + } + else if (job_item.time_scheduler_type == 8) // following days at X time + { + Logging.Jobs("Jobs.Time_Scheduler.Check_Execution", "following days at X time", "name: " + job_item.name + " id: " + job_item.id + " last_run: " + (job_item.last_run ?? DateTime.Now.ToString(CultureInfo.InvariantCulture))); - if (DateTime.Now.DayOfWeek.ToString() == "Sunday" && job_item.time_scheduler_sunday && DateTime.Parse(job_item.last_run) <= DateTime.Now - TimeSpan.FromSeconds(job_item.time_scheduler_seconds)) - execute = true; + DateTime scheduledTime = DateTime.ParseExact(job_item.time_scheduler_time, "HH:mm:ss", CultureInfo.InvariantCulture); - Logging.Jobs("Jobs.Time_Scheduler.Check_Execution", "following days, x seconds", "name: " + job_item.name + " id: " + job_item.id + " last_run: " + DateTime.Parse(job_item.last_run) + " execute: " + execute.ToString()); - } - else if (job_item.time_scheduler_type == 10) // following days, x minutes - { - // Check if last run is empty, if so set it to now - if (String.IsNullOrEmpty(job_item.last_run)) - job_item.last_run = DateTime.Now.ToString(); + // Check if last run is empty, if so set it to a time in the past to trigger initial execution + if (String.IsNullOrEmpty(job_item.last_run)) + { + job_item.last_run = DateTime.Now.AddDays(-1).ToString(CultureInfo.InvariantCulture); + WriteEncryptedJob(job, job_item); + } - if (DateTime.Now.DayOfWeek.ToString() == "Monday" && job_item.time_scheduler_monday && DateTime.Parse(job_item.last_run) < DateTime.Now - TimeSpan.FromMinutes(job_item.time_scheduler_minutes)) - execute = true; + DateTime lastRunDateTime = DateTime.Parse(job_item.last_run, CultureInfo.InvariantCulture); - if (DateTime.Now.DayOfWeek.ToString() == "Tuesday" && job_item.time_scheduler_tuesday && DateTime.Parse(job_item.last_run) < DateTime.Now - TimeSpan.FromMinutes(job_item.time_scheduler_minutes)) - execute = true; + // Check if current time is past the scheduled time and we haven't run today yet + bool shouldRunToday = DateTime.Now.TimeOfDay >= scheduledTime.TimeOfDay && lastRunDateTime.Date < DateTime.Now.Date; - if (DateTime.Now.DayOfWeek.ToString() == "Wednesday" && job_item.time_scheduler_wednesday && DateTime.Parse(job_item.last_run) < DateTime.Now - TimeSpan.FromMinutes(job_item.time_scheduler_minutes)) - execute = true; + // Use helper method to check weekday + if (ShouldRunToday(job_item) && shouldRunToday) + execute = true; - if (DateTime.Now.DayOfWeek.ToString() == "Thursday" && job_item.time_scheduler_thursday && DateTime.Parse(job_item.last_run) < DateTime.Now - TimeSpan.FromMinutes(job_item.time_scheduler_minutes)) - execute = true; + Logging.Jobs("Jobs.Time_Scheduler.Check_Execution", "following days at X time", "name: " + job_item.name + " id: " + job_item.id + " last_run: " + lastRunDateTime + " execute: " + execute.ToString()); + } + else if (job_item.time_scheduler_type == 9) // following days, x seconds + { + Logging.Jobs("Jobs.Time_Scheduler.Check_Execution", "following days, x seconds", "name: " + job_item.name + " id: " + job_item.id + " last_run: " + (job_item.last_run ?? DateTime.Now.ToString(CultureInfo.InvariantCulture))); - if (DateTime.Now.DayOfWeek.ToString() == "Friday" && job_item.time_scheduler_friday && DateTime.Parse(job_item.last_run) < DateTime.Now - TimeSpan.FromMinutes(job_item.time_scheduler_minutes)) - execute = true; + // Check if last run is empty, if so set it to now + if (String.IsNullOrEmpty(job_item.last_run)) + { + job_item.last_run = DateTime.Now.ToString(CultureInfo.InvariantCulture); + WriteEncryptedJob(job, job_item); + } - if (DateTime.Now.DayOfWeek.ToString() == "Saturday" && job_item.time_scheduler_saturday && DateTime.Parse(job_item.last_run) < DateTime.Now - TimeSpan.FromMinutes(job_item.time_scheduler_minutes)) - execute = true; + DateTime lastRun = DateTime.Parse(job_item.last_run, CultureInfo.InvariantCulture); - if (DateTime.Now.DayOfWeek.ToString() == "Sunday" && job_item.time_scheduler_sunday && DateTime.Parse(job_item.last_run) < DateTime.Now - TimeSpan.FromMinutes(job_item.time_scheduler_minutes)) - execute = true; + // Check if it's a valid day AND the interval has passed + if (ShouldRunToday(job_item) && lastRun <= DateTime.Now - TimeSpan.FromSeconds(job_item.time_scheduler_seconds)) + execute = true; - Logging.Jobs("Jobs.Time_Scheduler.Check_Execution", "following days, x minutes", "name: " + job_item.name + " id: " + job_item.id + " last_run: " + DateTime.Parse(job_item.last_run) + " execute: " + execute.ToString()); - } - else if (job_item.time_scheduler_type == 11) // following days, x hours - { - DateTime scheduledDateTime = DateTime.ParseExact($"{job_item.time_scheduler_date.Split(' ')[0]} {job_item.time_scheduler_time}", "dd.MM.yyyy HH:mm:ss", CultureInfo.InvariantCulture); - - // Check if last run is empty, if so, subsract 24 hours from scheduled time to trigger the execution - if (String.IsNullOrEmpty(job_item.last_run)) - { - job_item.last_run = (scheduledDateTime - TimeSpan.FromHours(24)).ToString(); - string updated_job_json = JsonSerializer.Serialize(job_item); - File.WriteAllText(job, updated_job_json); + Logging.Jobs("Jobs.Time_Scheduler.Check_Execution", "following days, x seconds", "name: " + job_item.name + " id: " + job_item.id + " last_run: " + lastRun + " execute: " + execute.ToString()); } + else if (job_item.time_scheduler_type == 10) // following days, x minutes + { + Logging.Jobs("Jobs.Time_Scheduler.Check_Execution", "following days, x minutes", "name: " + job_item.name + " id: " + job_item.id + " last_run: " + (job_item.last_run ?? DateTime.Now.ToString(CultureInfo.InvariantCulture))); - DateTime lastRunDateTime = DateTime.Parse(job_item.last_run); + // Check if last run is empty, if so set it to now + if (String.IsNullOrEmpty(job_item.last_run)) + { + job_item.last_run = DateTime.Now.ToString(CultureInfo.InvariantCulture); + WriteEncryptedJob(job, job_item); + } - if (DateTime.Now.DayOfWeek.ToString() == "Monday" && job_item.time_scheduler_monday && DateTime.Parse(job_item.last_run) < DateTime.Now - TimeSpan.FromHours(job_item.time_scheduler_hours)) - execute = true; + DateTime lastRun = DateTime.Parse(job_item.last_run, CultureInfo.InvariantCulture); - if (DateTime.Now.DayOfWeek.ToString() == "Tuesday" && job_item.time_scheduler_tuesday && DateTime.Parse(job_item.last_run) < DateTime.Now - TimeSpan.FromHours(job_item.time_scheduler_hours)) - execute = true; + // Check if it's a valid day AND the interval has passed + if (ShouldRunToday(job_item) && lastRun <= DateTime.Now - TimeSpan.FromMinutes(job_item.time_scheduler_minutes)) + execute = true; - if (DateTime.Now.DayOfWeek.ToString() == "Wednesday" && job_item.time_scheduler_wednesday && DateTime.Parse(job_item.last_run) < DateTime.Now - TimeSpan.FromHours(job_item.time_scheduler_hours)) - execute = true; + Logging.Jobs("Jobs.Time_Scheduler.Check_Execution", "following days, x minutes", "name: " + job_item.name + " id: " + job_item.id + " last_run: " + lastRun + " execute: " + execute.ToString()); + } + else if (job_item.time_scheduler_type == 11) // following days, x hours + { + Logging.Jobs("Jobs.Time_Scheduler.Check_Execution", "following days, x hours", "name: " + job_item.name + " id: " + job_item.id + " last_run: " + (job_item.last_run ?? DateTime.Now.ToString(CultureInfo.InvariantCulture))); - if (DateTime.Now.DayOfWeek.ToString() == "Thursday" && job_item.time_scheduler_thursday && DateTime.Parse(job_item.last_run) < DateTime.Now - TimeSpan.FromHours(job_item.time_scheduler_hours)) - execute = true; + // Check if last run is empty, if so set it to now + if (String.IsNullOrEmpty(job_item.last_run)) + { + job_item.last_run = DateTime.Now.ToString(CultureInfo.InvariantCulture); + WriteEncryptedJob(job, job_item); + } - if (DateTime.Now.DayOfWeek.ToString() == "Friday" && job_item.time_scheduler_friday && DateTime.Parse(job_item.last_run) < DateTime.Now - TimeSpan.FromHours(job_item.time_scheduler_hours)) - execute = true; + DateTime lastRun = DateTime.Parse(job_item.last_run, CultureInfo.InvariantCulture); - if (DateTime.Now.DayOfWeek.ToString() == "Saturday" && job_item.time_scheduler_saturday && DateTime.Parse(job_item.last_run) < DateTime.Now - TimeSpan.FromHours(job_item.time_scheduler_hours)) - execute = true; + // Check if it's a valid day AND the interval has passed + if (ShouldRunToday(job_item) && lastRun <= DateTime.Now - TimeSpan.FromHours(job_item.time_scheduler_hours)) + execute = true; - if (DateTime.Now.DayOfWeek.ToString() == "Sunday" && job_item.time_scheduler_sunday && DateTime.Parse(job_item.last_run) < DateTime.Now - TimeSpan.FromHours(job_item.time_scheduler_hours)) - execute = true; + Logging.Jobs("Jobs.Time_Scheduler.Check_Execution", "following days, x hours", "name: " + job_item.name + " id: " + job_item.id + " last_run: " + lastRun + " execute: " + execute.ToString()); + } - Logging.Jobs("Jobs.Time_Scheduler.Check_Execution", "following days, x hours", "name: " + job_item.name + " id: " + job_item.id + " last_run: " + DateTime.Parse(job_item.last_run) + " execute: " + execute.ToString()); + // Execute if needed + if (execute) + { + // Store the old last_run value in case we need to rollback + string previous_last_run = job_item.last_run; + + // Update last run IMMEDIATELY to prevent race conditions (before executing the job) + job_item.last_run = DateTime.Now.ToString(CultureInfo.InvariantCulture); + WriteEncryptedJob(job, job_item); + + string result = String.Empty; + + try + { + Logging.Jobs("Jobs.Time_Scheduler.Check_Execution", "Execute job", "name: " + job_item.name + " id: " + job_item.id); + + // Use null-coalescing operator to provide default timeout of 0 (which means 60 minutes default) + int timeoutValue = job_item.timeout ?? 0; + + //Execute job + if (OperatingSystem.IsWindows()) + result = Windows.Helper.PowerShell.Execute_Script("Jobs.Time_Scheduler.Check_Execution (execute job) " + job_item.name, job_item.script, timeoutValue); + else if (OperatingSystem.IsLinux()) + result = Linux.Helper.Bash.Execute_Script("Jobs.Time_Scheduler.Check_Execution (execute job) " + job_item.name, true, job_item.script, timeoutValue); + else if (OperatingSystem.IsMacOS()) + result = MacOS.Helper.Zsh.Execute_Script("Jobs.Time_Scheduler.Check_Execution (execute job) " + job_item.name, true, job_item.script, timeoutValue); + + // Insert event + Logging.Jobs("Jobs.Time_Scheduler.Check_Execution", "Job executed", "name: " + job_item.name + " id: " + job_item.id + " result: " + result); + + // Check if job description is empty + if (String.IsNullOrEmpty(job_item.description) && Configuration.Agent.language == "en-US") + job_item.description = "No description"; + else if (String.IsNullOrEmpty(job_item.description) && Configuration.Agent.language == "de-DE") + job_item.description = "Keine Beschreibung"; + + if (Configuration.Agent.language == "en-US") + Events.Logger.Insert_Event("0", "Job", job_item.name + " completed", "Job: " + job_item.name + " (" + job_item.description + ") " + Environment.NewLine + Environment.NewLine + "Result: " + Environment.NewLine + result, String.Empty, 1, 0); + else if (Configuration.Agent.language == "de-DE") + Events.Logger.Insert_Event("0", "Job", job_item.name + " fertiggestellt.", "Job: " + job_item.name + " (" + job_item.description + ") " + Environment.NewLine + Environment.NewLine + "Ergebnis: " + Environment.NewLine + result, String.Empty, 1, 1); + + Logging.Jobs("Jobs.Time_Scheduler.Check_Execution", "Execution finished successfully", "name: " + job_item.name + " id: " + job_item.id); + } + catch (Exception ex) + { + // Job failed - rollback last_run to allow retry on next scheduler run + job_item.last_run = previous_last_run; + WriteEncryptedJob(job, job_item); + + Logging.Error("Jobs.Time_Scheduler.Check_Execution", "Job execution failed (rolled back last_run)", "name: " + job_item.name + " id: " + job_item.id + " error: " + ex.ToString()); + + // Insert error event + if (Configuration.Agent.language == "en-US") + Events.Logger.Insert_Event("2", "Job", job_item.name + " failed", "Job: " + job_item.name + " (" + job_item.description + ") " + Environment.NewLine + Environment.NewLine + "Error: " + Environment.NewLine + ex.ToString(), String.Empty, 1, 0); + else if (Configuration.Agent.language == "de-DE") + Events.Logger.Insert_Event("2", "Job", job_item.name + " fehlgeschlagen", "Job: " + job_item.name + " (" + job_item.description + ") " + Environment.NewLine + Environment.NewLine + "Fehler: " + Environment.NewLine + ex.ToString(), String.Empty, 1, 1); + } + } + else + Logging.Jobs("Jobs.Time_Scheduler.Check_Execution", "Job will not be executed", "name: " + job_item.name + " id: " + job_item.id); } - - // Execute if needed - if (execute) + catch (Exception ex) { - string result = String.Empty; - - Logging.Jobs("Jobs.Time_Scheduler.Check_Execution", "Execute job", "name: " + job_item.name + " id: " + job_item.id); - - //Execute job - if (OperatingSystem.IsWindows()) - result = Windows.Helper.PowerShell.Execute_Script("Jobs.Time_Scheduler.Check_Execution (execute job) " + job_item.name, job_item.script); - else if (OperatingSystem.IsLinux()) - result = Linux.Helper.Bash.Execute_Script("Jobs.Time_Scheduler.Check_Execution (execute job) " + job_item.name, true, job_item.script); - else if (OperatingSystem.IsMacOS()) - result = MacOS.Helper.Zsh.Execute_Script("Jobs.Time_Scheduler.Check_Execution (execute job) " + job_item.name, true, job_item.script); - - // Insert event - Logging.Jobs("Jobs.Time_Scheduler.Check_Execution", "Job executed", "name: " + job_item.name + " id: " + job_item.id + " result: " + result); - - // Check if job description is empty - if (String.IsNullOrEmpty(job_item.description) && Configuration.Agent.language == "en-US") - job_item.description = "No description"; - else if (String.IsNullOrEmpty(job_item.description) && Configuration.Agent.language == "de-DE") - job_item.description = "Keine Beschreibung"; - - if (Configuration.Agent.language == "en-US") - Events.Logger.Insert_Event("0", "Job", job_item.name + " completed", "Job: " + job_item.name + " (" + job_item.description + ") " + Environment.NewLine + Environment.NewLine + "Result: " + Environment.NewLine + result, String.Empty, 1, 0); - else if (Configuration.Agent.language == "de-DE") - Events.Logger.Insert_Event("0", "Job", job_item.name + " fertiggestellt.", "Job: " + job_item.name + " (" + job_item.description + ") " + Environment.NewLine + Environment.NewLine + "Ergebnis: " + Environment.NewLine + result, String.Empty, 1, 1); - - // Update last run - job_item.last_run = DateTime.Now.ToString(); - string updated_job_json = JsonSerializer.Serialize(job_item); - File.WriteAllText(job, updated_job_json); - - Logging.Jobs("Jobs.Time_Scheduler.Check_Execution", "Execution finished", "name: " + job_item.name + " id: " + job_item.id); + Logging.Error("Jobs.Time_Scheduler.Check_Execution", "Failed to read or decrypt job file", "Job file: " + job + " Error: " + ex.Message); + + // Delete corrupted file so it will be recreated on next sync + try + { + File.Delete(job); + Logging.Jobs("Jobs.Time_Scheduler.Check_Execution", "Deleted corrupted job file", "Job file: " + job); + } + catch (Exception deleteEx) + { + Logging.Error("Jobs.Time_Scheduler.Check_Execution", "Failed to delete corrupted job file", "Job file: " + job + " Error: " + deleteEx.Message); + } } - else - Logging.Jobs("Jobs.Time_Scheduler.Check_Execution", "Job will not be executed", "name: " + job_item.name + " id: " + job_item.id); } Logging.Jobs("Jobs.Time_Scheduler.Check_Execution", "Check job execution", "Stop"); @@ -459,4 +535,4 @@ public static void Check_Execution() } } } -} +} \ No newline at end of file diff --git a/NetLock RMM Agent Comm/Global/Online_Mode/Handler.cs b/NetLock RMM Agent Comm/Global/Online_Mode/Handler.cs index 1bb629aa..e5b37e97 100644 --- a/NetLock RMM Agent Comm/Global/Online_Mode/Handler.cs +++ b/NetLock RMM Agent Comm/Global/Online_Mode/Handler.cs @@ -465,9 +465,10 @@ public static async Task Authenticate() string new_server_config_json = JsonSerializer.Serialize(new_server_config, new JsonSerializerOptions { WriteIndented = true }); - // Write the new server config JSON to the file - await File.WriteAllTextAsync(Application_Paths.program_data_server_config_json, new_server_config_json); - await File.WriteAllTextAsync(Application_Paths.program_data_health_agent_server_config, new_server_config_json); + // Write the new server config JSON to the file (encrypted) + Initialization.Server_Config.SaveEncryptedConfig(new_server_config_json); + Initialization.Server_Config.InvalidateCache(); + Initialization.Server_Config.SaveEncryptedHealthAgentConfig(new_server_config_json); Device_Worker.authorized = true; } @@ -495,9 +496,10 @@ public static async Task Authenticate() string new_server_config_json = JsonSerializer.Serialize(new_server_config, new JsonSerializerOptions { WriteIndented = true }); - // Write the new server config JSON to the file - await File.WriteAllTextAsync(Application_Paths.program_data_server_config_json, new_server_config_json); - await File.WriteAllTextAsync(Application_Paths.program_data_health_agent_server_config, new_server_config_json); + // Write the new server config JSON to the file (encrypted) + Initialization.Server_Config.SaveEncryptedConfig(new_server_config_json); + Initialization.Server_Config.InvalidateCache(); + Initialization.Server_Config.SaveEncryptedHealthAgentConfig(new_server_config_json); // Clear local information Device_Worker.processesJson = String.Empty; @@ -666,8 +668,9 @@ public static async Task Update_Device_Information() string new_server_config_json = JsonSerializer.Serialize(new_server_config, new JsonSerializerOptions { WriteIndented = true }); - // Write the new server config JSON to the file - File.WriteAllText(Application_Paths.program_data_server_config_json, new_server_config_json); + // Write the new server config JSON to the file (encrypted) + Initialization.Server_Config.SaveEncryptedConfig(new_server_config_json); + Initialization.Server_Config.InvalidateCache(); Device_Worker.authorized = true; } @@ -695,9 +698,10 @@ public static async Task Update_Device_Information() string new_server_config_json = JsonSerializer.Serialize(new_server_config, new JsonSerializerOptions { WriteIndented = true }); - // Write the new server config JSON to the file - await File.WriteAllTextAsync(Application_Paths.program_data_server_config_json, new_server_config_json); - await File.WriteAllTextAsync(Application_Paths.program_data_health_agent_server_config, new_server_config_json); + // Write the new server config JSON to the file (encrypted) + Initialization.Server_Config.SaveEncryptedConfig(new_server_config_json); + Initialization.Server_Config.InvalidateCache(); + Initialization.Server_Config.SaveEncryptedHealthAgentConfig(new_server_config_json); // Clear local information Device_Worker.processesJson = String.Empty; @@ -818,9 +822,10 @@ public static async Task Policy() string new_server_config_json = JsonSerializer.Serialize(new_server_config, new JsonSerializerOptions { WriteIndented = true }); - // Write the new server config JSON to the file - await File.WriteAllTextAsync(Application_Paths.program_data_server_config_json, new_server_config_json); - await File.WriteAllTextAsync(Application_Paths.program_data_health_agent_server_config, new_server_config_json); + // Write the new server config JSON to the file (encrypted) + Initialization.Server_Config.SaveEncryptedConfig(new_server_config_json); + Initialization.Server_Config.InvalidateCache(); + Initialization.Server_Config.SaveEncryptedHealthAgentConfig(new_server_config_json); // Clear local information Device_Worker.processesJson = String.Empty; diff --git a/NetLock RMM Agent Comm/Global/Sensors/Time_Scheduler.cs b/NetLock RMM Agent Comm/Global/Sensors/Time_Scheduler.cs index 45cdcf42..f373a8bb 100644 --- a/NetLock RMM Agent Comm/Global/Sensors/Time_Scheduler.cs +++ b/NetLock RMM Agent Comm/Global/Sensors/Time_Scheduler.cs @@ -28,70 +28,73 @@ internal class Time_Scheduler { public class Sensor { - public string id { get; set; } - public string name { get; set; } - public string date { get; set; } - public string last_run { get; set; } - public string author { get; set; } - public string description { get; set; } - public string platform { get; set; } - public int severity { get; set; } - public int category { get; set; } - public int sub_category { get; set; } - public int utilization_category { get; set; } - public int notification_treshold_count { get; set; } - public int notification_treshold_max { get; set; } - public string notification_history { get; set; } - public int action_treshold_count { get; set; } - public int action_treshold_max { get; set; } - - public string action_history { get; set; } - public bool auto_reset { get; set; } - public string script { get; set; } - public string script_action { get; set; } - public int cpu_usage { get; set; } - public string process_name { get; set; } - public int ram_usage { get; set; } - public int disk_usage { get; set; } - public int disk_minimum_capacity { get; set; } - public int disk_category { get; set; } - public string disk_letters { get; set; } - public bool disk_include_network_disks { get; set; } - public bool disk_include_removable_disks { get; set; } - public string eventlog { get; set; } - public string eventlog_event_id { get; set; } - public string expected_result { get; set; } + public string id { get; set; } = string.Empty; + public string name { get; set; } = string.Empty; + public string date { get; set; } = string.Empty; + public string last_run { get; set; } = string.Empty; + public string author { get; set; } = string.Empty; + public string description { get; set; } = string.Empty; + public string platform { get; set; } = string.Empty; + public int severity { get; set; } = 0; + public int category { get; set; } = 0; + public int sub_category { get; set; } = 0; + public int utilization_category { get; set; } = 0; + public int notification_treshold_count { get; set; } = 0; + public int notification_treshold_max { get; set; } = 0; + public string notification_history { get; set; } = string.Empty; + public int action_treshold_count { get; set; } = 0; + public int action_treshold_max { get; set; } = 0; + + public string action_history { get; set; } = string.Empty; + public bool auto_reset { get; set; } = false; + public string script { get; set; } = string.Empty; + public string script_action { get; set; } = string.Empty; + public int cpu_usage { get; set; } = 0; + public string process_name { get; set; } = string.Empty; + public int ram_usage { get; set; } = 0; + public int disk_usage { get; set; } = 0; + public int disk_minimum_capacity { get; set; } = 0; + public int disk_category { get; set; } = 0; + public string disk_letters { get; set; } = string.Empty; + public bool disk_include_network_disks { get; set; } = false; + public bool disk_include_removable_disks { get; set; } = false; + public string eventlog { get; set; } = string.Empty; + public string eventlog_event_id { get; set; } = string.Empty; + public string expected_result { get; set; } = string.Empty; //service sensor - public string service_name { get; set; } - public int service_condition { get; set; } - public int service_action { get; set; } + public string service_name { get; set; } = string.Empty; + public int service_condition { get; set; } = 0; + public int service_action { get; set; } = 0; //ping sensor - public string ping_address { get; set; } - public int ping_timeout { get; set; } - public int ping_condition { get; set; } + public string ping_address { get; set; } = string.Empty; + public int ping_timeout { get; set; } = 0; + public int ping_condition { get; set; } = 0; //time schedule - public int time_scheduler_type { get; set; } - public int time_scheduler_seconds { get; set; } - public int time_scheduler_minutes { get; set; } - public int time_scheduler_hours { get; set; } - public string time_scheduler_time { get; set; } - public string time_scheduler_date { get; set; } - public bool time_scheduler_monday { get; set; } - public bool time_scheduler_tuesday { get; set; } - public bool time_scheduler_wednesday { get; set; } - public bool time_scheduler_thursday { get; set; } - public bool time_scheduler_friday { get; set; } - public bool time_scheduler_saturday { get; set; } - public bool time_scheduler_sunday { get; set; } + public int time_scheduler_type { get; set; } = 0; + public int time_scheduler_seconds { get; set; } = 0; + public int time_scheduler_minutes { get; set; } = 0; + public int time_scheduler_hours { get; set; } = 0; + public string time_scheduler_time { get; set; } = string.Empty; + public string time_scheduler_date { get; set; } = string.Empty; + public bool time_scheduler_monday { get; set; } = true; + public bool time_scheduler_tuesday { get; set; } = true; + public bool time_scheduler_wednesday { get; set; } = true; + public bool time_scheduler_thursday { get; set; } = true; + public bool time_scheduler_friday { get; set; } = true; + public bool time_scheduler_saturday { get; set; } = true; + public bool time_scheduler_sunday { get; set; } = true; // NetLock notifications - public bool notifications_mail { get; set; } - public bool notifications_microsoft_teams { get; set; } - public bool notifications_telegram { get; set; } - public bool notifications_ntfy_sh { get; set; } + public bool already_notified { get; set; } = false; // tracks if notification was sent (for spam prevention & resolved notifications) + public bool suppress_notification { get; set; } = false; // controls whether already_notified is used for spam prevention + public bool resolved_notification { get; set; } = false; + public bool notifications_mail { get; set; } = false; + public bool notifications_microsoft_teams { get; set; } = false; + public bool notifications_telegram { get; set; } = false; + public bool notifications_ntfy_sh { get; set; } = false; } public class Notifications @@ -114,6 +117,196 @@ public class Process_Information public string cmd { get; set; } } + // Helper method to check if sensor should run today based on weekday settings + private static bool ShouldRunToday(Sensor sensor) + { + try + { + switch (DateTime.Now.DayOfWeek) + { + case DayOfWeek.Monday: + return sensor.time_scheduler_monday; + case DayOfWeek.Tuesday: + return sensor.time_scheduler_tuesday; + case DayOfWeek.Wednesday: + return sensor.time_scheduler_wednesday; + case DayOfWeek.Thursday: + return sensor.time_scheduler_thursday; + case DayOfWeek.Friday: + return sensor.time_scheduler_friday; + case DayOfWeek.Saturday: + return sensor.time_scheduler_saturday; + case DayOfWeek.Sunday: + return sensor.time_scheduler_sunday; + default: + return false; + } + } + catch (Exception e) + { + Logging.Error("Sensors.Time_Scheduler.ShouldRunToday", "Check if sensor should run today", + "Sensor id: " + sensor.id + " Exception: " + e.ToString()); + + return false; + } + } + + // Helper method to write encrypted sensor to disk + private static void WriteEncryptedSensor(string filePath, Sensor sensor) + { + try + { + string sensor_json = JsonSerializer.Serialize(sensor); + string encrypted_json = Encryption.String_Encryption.Encrypt(sensor_json, Application_Settings.NetLock_Local_Encryption_Key); + File.WriteAllText(filePath, encrypted_json); + } + catch (Exception e) + { + Logging.Error("Sensors.Time_Scheduler.WriteEncryptedSensor", "Write encrypted sensor to disk", + "Sensor id: " + sensor.id + " Exception: " + e.ToString()); + } + } + + // Helper method to safely deserialize string list (for action_history and notification_history) + private static List SafeDeserializeStringList(string json) + { + if (string.IsNullOrEmpty(json)) + return new List(); + + try + { + var list = JsonSerializer.Deserialize>(json); + return list ?? new List(); + } + catch (Exception) + { + return new List(); + } + } + + // Helper method to mark sensor as notified and optionally add to history (for spam prevention when suppress_notification is enabled) + private static void AddNotificationIfNeeded(Sensor sensor_item, string sensor_path, string details) + { + try + { + // Debug logging + Logging.Sensors("Sensors.Time_Scheduler.AddNotificationIfNeeded", "Called", + $"Sensor: {sensor_item.name}, already_notified: {sensor_item.already_notified}, suppress_notification: {sensor_item.suppress_notification}"); + + // Always mark as notified (for resolved notifications) + if (!sensor_item.already_notified) + { + sensor_item.already_notified = true; + Logging.Sensors("Sensors.Time_Scheduler.AddNotificationIfNeeded", "Setting already_notified = true", "Sensor: " + sensor_item.name); + + // Only add to notification history if suppress_notification is enabled + if (sensor_item.suppress_notification) + { + if (String.IsNullOrEmpty(sensor_item.notification_history)) + { + List notification_history_list = new List { details }; + sensor_item.notification_history = JsonSerializer.Serialize(notification_history_list); + } + else + { + List notification_history_list = SafeDeserializeStringList(sensor_item.notification_history); + notification_history_list.Add(details); + sensor_item.notification_history = JsonSerializer.Serialize(notification_history_list); + } + } + + WriteEncryptedSensor(sensor_path, sensor_item); + } + } + catch (Exception e) + { + Logging.Error("Sensors.Time_Scheduler.AddNotificationIfNeeded", "Add notification if needed", + "Sensor id: " + sensor_item.id + " Exception: " + e.ToString()); + } + } + + // Helper method to reset notification flag when sensor is no longer triggered (always resets for resolved notifications) + private static void ResetNotificationFlagIfNeeded(Sensor sensor_item, string sensor_path) + { + try + { + // Always reset the flag (for resolved notifications to work) + if (sensor_item.already_notified) + { + sensor_item.already_notified = false; + WriteEncryptedSensor(sensor_path, sensor_item); + } + } + catch (Exception e) + { + Logging.Error("Sensors.Time_Scheduler.ResetNotificationFlagIfNeeded", "Reset notification flag if needed", + "Sensor id: " + sensor_item.id + " Exception: " + e.ToString()); + } + } + + // Helper method to send resolved notification when problem is fixed + private static void SendResolvedNotificationIfNeeded(Sensor sensor_item, string sensor_path) + { + try + { + // Debug logging + Logging.Sensors("Sensors.Time_Scheduler.SendResolvedNotificationIfNeeded", "Check conditions", + $"Sensor: {sensor_item.name}, already_notified: {sensor_item.already_notified}, resolved_notification: {sensor_item.resolved_notification}"); + + // Only send resolved notification if both conditions are met: + // 1. already_notified = true (notification was sent before) + // 2. resolved_notification = true (resolved feature is enabled) + if (sensor_item.already_notified && sensor_item.resolved_notification) + { + Logging.Sensors("Sensors.Time_Scheduler.SendResolvedNotificationIfNeeded", "Sending resolved notification", "Sensor: " + sensor_item.name); + + string details = String.Empty; + + // Create resolved message based on language + if (Configuration.Agent.language == "en-US") + { + details = $"The issue with sensor '{sensor_item.name}' has been resolved." + Environment.NewLine + Environment.NewLine + + "Sensor name: " + sensor_item.name + Environment.NewLine + + "Description: " + sensor_item.description + Environment.NewLine + + "Time resolved: " + DateTime.Now + Environment.NewLine + + "Status: Problem resolved, monitoring continues."; + } + else if (Configuration.Agent.language == "de-DE") + { + details = $"Das Problem mit Sensor '{sensor_item.name}' wurde behoben." + Environment.NewLine + Environment.NewLine + + "Sensor Name: " + sensor_item.name + Environment.NewLine + + "Beschreibung: " + sensor_item.description + Environment.NewLine + + "Behoben am: " + DateTime.Now + Environment.NewLine + + "Status: Problem behoben, Überwachung lΓ€uft weiter."; + } + + // Create notification_json + Notifications notifications = new Notifications + { + mail = sensor_item.notifications_mail, + microsoft_teams = sensor_item.notifications_microsoft_teams, + telegram = sensor_item.notifications_telegram, + ntfy_sh = sensor_item.notifications_ntfy_sh + }; + + string notifications_json = JsonSerializer.Serialize(notifications, new JsonSerializerOptions { WriteIndented = true }); + + // Send resolved event + if (Configuration.Agent.language == "en-US") + Events.Logger.Insert_Event("0", "Sensor", "Sensor Resolved: " + sensor_item.name, details, notifications_json, 2, 0); + else if (Configuration.Agent.language == "de-DE") + Events.Logger.Insert_Event("0", "Sensor", "Sensor Behoben: " + sensor_item.name, details, notifications_json, 2, 1); + + Logging.Sensors("Sensors.Time_Scheduler.SendResolvedNotificationIfNeeded", "Resolved notification sent", "Sensor: " + sensor_item.name); + } + } + catch (Exception ex) + { + Logging.Error("Sensors.Time_Scheduler.SendResolvedNotificationIfNeeded", "Error sending resolved notification", + "Sensor: " + sensor_item.name + " Exception: " + ex.ToString()); + } + } + public static void Check_Execution() { Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Check sensor execution", "Start"); @@ -131,35 +324,59 @@ public static void Check_Execution() // Write each sensor to disk if not already exists foreach (var sensor in sensor_items) { - // Check if job is for the current platform - if (OperatingSystem.IsWindows() && sensor.platform != "Windows") - continue; - else if (OperatingSystem.IsLinux() && sensor.platform != "Linux") - continue; - else if (OperatingSystem.IsMacOS() && sensor.platform != "MacOS") - continue; + try + { + // Check if job is for the current platform + if (OperatingSystem.IsWindows() && sensor.platform != "Windows") + continue; + else if (OperatingSystem.IsLinux() && sensor.platform != "Linux") + continue; + else if (OperatingSystem.IsMacOS() && sensor.platform != "MacOS") + continue; - Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Check if sensor exists on disk", "Sensor: " + sensor.name + " Sensor id: " + sensor.id); + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Check if sensor exists on disk", "Sensor: " + sensor.name + " Sensor id: " + sensor.id); - string sensor_json = JsonSerializer.Serialize(sensor); - string sensor_path = Path.Combine(Application_Paths.program_data_sensors, sensor.id + ".json"); + string sensor_json = JsonSerializer.Serialize(sensor); + string sensor_path = Path.Combine(Application_Paths.program_data_sensors, sensor.id + ".json"); - if (!File.Exists(sensor_path)) - { - Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Check if sensor exists on disk", "false"); - File.WriteAllText(sensor_path, sensor_json); - } + if (!File.Exists(sensor_path)) + { + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Check if sensor exists on disk", "false"); + // Encrypt sensor JSON before writing + string encrypted_sensor_json = Encryption.String_Encryption.Encrypt(sensor_json, Application_Settings.NetLock_Local_Encryption_Key); + File.WriteAllText(sensor_path, encrypted_sensor_json); + } - // Check if script has changed, if so update it - if (File.Exists(sensor_path)) + // Check if script has changed, if so update it + if (File.Exists(sensor_path)) + { + // Decrypt sensor JSON after reading + string encrypted_existing_sensor_json = File.ReadAllText(sensor_path); + string existing_sensor_json = Encryption.String_Encryption.Decrypt(encrypted_existing_sensor_json, Application_Settings.NetLock_Local_Encryption_Key); + Sensor existing_sensor = JsonSerializer.Deserialize(existing_sensor_json); + + if (existing_sensor.script != sensor.script) + { + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Sensor script has changed. Updating it.", "Sensor: " + sensor.name + " Sensor id: " + sensor.id); + + // Encrypt sensor JSON before writing + string encrypted_sensor_json = Encryption.String_Encryption.Encrypt(sensor_json, Application_Settings.NetLock_Local_Encryption_Key); + File.WriteAllText(sensor_path, encrypted_sensor_json); + } + } + } + catch (Exception e) { - string existing_sensor_json = File.ReadAllText(sensor_path); - Sensor existing_sensor = JsonSerializer.Deserialize(existing_sensor_json); + Logging.Error("Sensors.Time_Scheduler.Check_Execution", "Error processing sensor before execution check", + "Sensor id: " + sensor.id + " Exception: " + e.ToString()); - if (existing_sensor.script != sensor.script) + // Delete corrupted sensor file if exists + string sensor_path = Path.Combine(Application_Paths.program_data_sensors, sensor.id + ".json"); + + if (File.Exists(sensor_path)) { - Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Sensor script has changed. Updating it.", "Sensor: " + sensor.name + " Sensor id: " + sensor.id); - File.WriteAllText(sensor_path, sensor_json); + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Delete corrupted sensor file", "Sensor id: " + sensor.id); + File.Delete(sensor_path); } } } @@ -167,413 +384,374 @@ public static void Check_Execution() // Clean up old sensors not existing anymore foreach (string file in Directory.GetFiles(Application_Paths.program_data_sensors)) { - Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Clean old sensors", "Sensor: " + file); + try + { + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Clean old sensors", "Sensor: " + file); - string file_name = Path.GetFileName(file); - string file_id = file_name.Replace(".json", ""); + string file_name = Path.GetFileName(file); + string file_id = file_name.Replace(".json", ""); - bool found = false; + bool found = false; - foreach (var sensor in sensor_items) - { - if (sensor.id == file_id) + foreach (var sensor in sensor_items) { - found = true; - break; + if (sensor.id == file_id) + { + found = true; + break; + } } - } - if (!found) + if (!found) + { + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Clean old sensors", "Delete sensor: " + file); + File.Delete(file); + } + } + catch (Exception e) { - Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Clean old sensors", "Delete sensor: " + file); - File.Delete(file); + Logging.Error("Sensors.Time_Scheduler.Check_Execution", "Error during cleanup of old sensors", + "Sensor file: " + file + " Exception: " + e.ToString()); } } // Now read & consume each sensor foreach (var sensor in Directory.GetFiles(Application_Paths.program_data_sensors)) { - string sensor_json = File.ReadAllText(sensor); - Sensor sensor_item = JsonSerializer.Deserialize(sensor_json); - sensor_id = sensor_item.id; // needed for logging - - Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Check sensor execution", "Sensor: " + sensor_item.name + " time_scheduler_type: " + sensor_item.time_scheduler_type); - - // Check thresholds - // Check notification treshold - if (string.IsNullOrEmpty(sensor_item.notification_treshold_count.ToString())) - { - sensor_item.notification_treshold_count = 0; - string updated_sensor_json = JsonSerializer.Serialize(sensor_item); - File.WriteAllText(sensor, updated_sensor_json); - } - - // Check action treshold - if (string.IsNullOrEmpty(sensor_item.action_treshold_count.ToString())) + try { - sensor_item.action_treshold_count = 0; - string updated_sensor_json = JsonSerializer.Serialize(sensor_item); - File.WriteAllText(sensor, updated_sensor_json); - } - - // Check enabled - /*if (!sensor_item.enabled) - { - Logging.Handler.Sensors("Sensors.Time_Scheduler.Check_Execution", "Check sensor execution", "Sensor disabled"); - - continue; - }*/ + // Decrypt sensor JSON after reading + string encrypted_sensor_json = File.ReadAllText(sensor); + string sensor_json = Encryption.String_Encryption.Decrypt(encrypted_sensor_json, Application_Settings.NetLock_Local_Encryption_Key); + Sensor sensor_item = JsonSerializer.Deserialize(sensor_json); - bool execute = false; - - if (sensor_item.time_scheduler_type == 0) // system boot - { - Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "System boot", "name: " + sensor_item.name + " id: " + sensor_item.id + " last_run: " + DateTime.Parse(sensor_item.last_run ?? DateTime.Now.ToString()) + " Last boot: " + os_up_time.ToString()); - - // Check if last run is empty, if so set it to now - if (String.IsNullOrEmpty(sensor_item.last_run)) + // Null-check after deserialization + if (sensor_item == null) { - sensor_item.last_run = DateTime.Now.ToString(); - string updated_sensor_json = JsonSerializer.Serialize(sensor_item); - File.WriteAllText(sensor, updated_sensor_json); + Logging.Error("Sensors.Time_Scheduler.Check_Execution", "Failed to deserialize sensor", "Sensor file: " + sensor); + continue; } - if (DateTime.Parse(sensor_item.last_run) < os_up_time) - execute = true; - } - else if (sensor_item.time_scheduler_type == 1) // date & time - { - Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "date & time", "name: " + sensor_item.name + " id: " + sensor_item.id + " last_run: " + DateTime.Parse(sensor_item.last_run ?? DateTime.Now.ToString())); + sensor_id = sensor_item.id; // needed for logging - DateTime scheduledDateTime = DateTime.ParseExact($"{sensor_item.time_scheduler_date.Split(' ')[0]} {sensor_item.time_scheduler_time}", "dd.MM.yyyy HH:mm:ss", CultureInfo.InvariantCulture); + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Check sensor execution", "Sensor: " + sensor_item.name + " time_scheduler_type: " + sensor_item.time_scheduler_type); - // Check if last run is empty, if so, subsract 24 hours from scheduled time to trigger the execution - if (String.IsNullOrEmpty(sensor_item.last_run)) + // Check thresholds + // Check notification treshold + if (string.IsNullOrEmpty(sensor_item.notification_treshold_count.ToString())) { - sensor_item.last_run = (scheduledDateTime - TimeSpan.FromHours(24)).ToString(); - string updated_sensor_json = JsonSerializer.Serialize(sensor_item); - File.WriteAllText(sensor, updated_sensor_json); + sensor_item.notification_treshold_count = 0; + WriteEncryptedSensor(sensor, sensor_item); } - DateTime lastRunDateTime = DateTime.Parse(sensor_item.last_run); - - Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "date & time", "name: " + sensor_item.name + " id: " + sensor_item.id + " last_run: " + DateTime.Parse(sensor_item.last_run) + " scheduledDateTime: " + scheduledDateTime.ToString() + " execute: " + execute.ToString()); - - if (DateTime.Now.Date >= scheduledDateTime.Date && DateTime.Now.TimeOfDay >= scheduledDateTime.TimeOfDay && lastRunDateTime < scheduledDateTime) - execute = true; - } - else if (sensor_item.time_scheduler_type == 2) // all x seconds - { - Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "all x seconds", "name: " + sensor_item.name + " id: " + sensor_item.id + " last_run: " + DateTime.Parse(sensor_item.last_run ?? DateTime.Now.ToString())); - - // Check if last run is empty, if so set it to now - if (String.IsNullOrEmpty(sensor_item.last_run)) + // Check action treshold + if (string.IsNullOrEmpty(sensor_item.action_treshold_count.ToString())) { - sensor_item.last_run = DateTime.Now.ToString(); - string updated_sensor_json = JsonSerializer.Serialize(sensor_item); - File.WriteAllText(sensor, updated_sensor_json); + sensor_item.action_treshold_count = 0; + WriteEncryptedSensor(sensor, sensor_item); } - if (DateTime.Parse(sensor_item.last_run) <= DateTime.Now - TimeSpan.FromSeconds(sensor_item.time_scheduler_seconds)) - execute = true; - - Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "all x seconds", "name: " + sensor_item.name + " id: " + sensor_item.id + " last_run: " + DateTime.Parse(sensor_item.last_run) + " execute: " + execute.ToString()); - } - else if (sensor_item.time_scheduler_type == 3) // all x minutes - { - Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "all x minutes", "name: " + sensor_item.name + " id: " + sensor_item.id + " last_run: " + DateTime.Parse(sensor_item.last_run ?? DateTime.Now.ToString())); - - // Check if last run is empty, if so set it to now - if (String.IsNullOrEmpty(sensor_item.last_run)) + // Check enabled + /*if (!sensor_item.enabled) { - sensor_item.last_run = DateTime.Now.ToString(); - string updated_sensor_json = JsonSerializer.Serialize(sensor_item); - File.WriteAllText(sensor, updated_sensor_json); - } + Logging.Handler.Sensors("Sensors.Time_Scheduler.Check_Execution", "Check sensor execution", "Sensor disabled"); - if (DateTime.Parse(sensor_item.last_run) < DateTime.Now - TimeSpan.FromMinutes(sensor_item.time_scheduler_minutes)) - execute = true; + continue; + }*/ - Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "all x minutes", "name: " + sensor_item.name + " id: " + sensor_item.id + " last_run: " + DateTime.Parse(sensor_item.last_run) + " execute: " + execute.ToString()); - } - else if (sensor_item.time_scheduler_type == 4) // all x hours - { - Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "all x hours", "name: " + sensor_item.name + " id: " + sensor_item.id + " last_run: " + DateTime.Parse(sensor_item.last_run ?? DateTime.Now.ToString())); + bool execute = false; - // Check if last run is empty, if so set it to now - if (String.IsNullOrEmpty(sensor_item.last_run)) + if (sensor_item.time_scheduler_type == 0) // system boot { - sensor_item.last_run = DateTime.Now.ToString(); - string updated_sensor_json = JsonSerializer.Serialize(sensor_item); - File.WriteAllText(sensor, updated_sensor_json); - } + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "System boot", "name: " + sensor_item.name + " id: " + sensor_item.id + " last_run: " + DateTime.Parse(sensor_item.last_run ?? DateTime.Now.ToString()) + " Last boot: " + os_up_time.ToString()); - if (DateTime.Parse(sensor_item.last_run) < DateTime.Now - TimeSpan.FromHours(sensor_item.time_scheduler_hours)) - execute = true; - - Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "all x hours", "name: " + sensor_item.name + " id: " + sensor_item.id + " last_run: " + DateTime.Parse(sensor_item.last_run) + " execute: " + execute.ToString()); - } - else if (sensor_item.time_scheduler_type == 5) // date, all x seconds - { - Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "date, all x seconds", "name: " + sensor_item.name + " id: " + sensor_item.id + " last_run: " + DateTime.Parse(sensor_item.last_run ?? DateTime.Now.ToString())); + // Check if last run is empty, if so set it to now + if (String.IsNullOrEmpty(sensor_item.last_run)) + { + sensor_item.last_run = DateTime.Now.ToString(); + WriteEncryptedSensor(sensor, sensor_item); + } - // Check if last run is empty, if so set it to now - if (String.IsNullOrEmpty(sensor_item.last_run)) - { - sensor_item.last_run = DateTime.Now.ToString(); - string updated_sensor_json = JsonSerializer.Serialize(sensor_item); - File.WriteAllText(sensor, updated_sensor_json); + if (DateTime.Parse(sensor_item.last_run) < os_up_time) + execute = true; } + else if (sensor_item.time_scheduler_type == 1) // date & time + { + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "date & time", "name: " + sensor_item.name + " id: " + sensor_item.id + " last_run: " + DateTime.Parse(sensor_item.last_run ?? DateTime.Now.ToString())); - if (DateTime.Now.Date == DateTime.Parse(sensor_item.time_scheduler_date).Date && DateTime.Parse(sensor_item.last_run) <= DateTime.Now - TimeSpan.FromSeconds(sensor_item.time_scheduler_seconds)) - execute = true; - - Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "date, all x seconds", "name: " + sensor_item.name + " id: " + sensor_item.id + " last_run: " + DateTime.Parse(sensor_item.last_run) + " execute: " + execute.ToString()); - } - else if (sensor_item.time_scheduler_type == 6) // date, all x minutes - { - Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "date, all x minutes", "name: " + sensor_item.name + " id: " + sensor_item.id + " last_run: " + DateTime.Parse(sensor_item.last_run ?? DateTime.Now.ToString())); + DateTime scheduledDateTime = DateTime.ParseExact($"{sensor_item.time_scheduler_date.Split(' ')[0]} {sensor_item.time_scheduler_time}", "dd.MM.yyyy HH:mm:ss", CultureInfo.InvariantCulture); - // Check if last run is empty, if so set it to now - if (String.IsNullOrEmpty(sensor_item.last_run)) - { - sensor_item.last_run = DateTime.Now.ToString(); - string updated_sensor_json = JsonSerializer.Serialize(sensor_item); - File.WriteAllText(sensor, updated_sensor_json); - } + // Check if last run is empty, if so, subsract 24 hours from scheduled time to trigger the execution + if (String.IsNullOrEmpty(sensor_item.last_run)) + { + sensor_item.last_run = (scheduledDateTime - TimeSpan.FromHours(24)).ToString(); + WriteEncryptedSensor(sensor, sensor_item); + } - if (DateTime.Now.Date == DateTime.Parse(sensor_item.time_scheduler_date).Date && DateTime.Parse(sensor_item.last_run) < DateTime.Now - TimeSpan.FromMinutes(sensor_item.time_scheduler_minutes)) - execute = true; + DateTime lastRunDateTime = DateTime.Parse(sensor_item.last_run); - Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "date, all x minutes", "name: " + sensor_item.name + " id: " + sensor_item.id + " last_run: " + DateTime.Parse(sensor_item.last_run) + " execute: " + execute.ToString()); - } - else if (sensor_item.time_scheduler_type == 7) // date, all x hours - { - Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "date, all x hours", "name: " + sensor_item.name + " id: " + sensor_item.id + " last_run: " + DateTime.Parse(sensor_item.last_run ?? DateTime.Now.ToString())); + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "date & time", "name: " + sensor_item.name + " id: " + sensor_item.id + " last_run: " + DateTime.Parse(sensor_item.last_run) + " scheduledDateTime: " + scheduledDateTime.ToString() + " execute: " + execute.ToString()); - // Check if last run is empty, if so set it to now - if (String.IsNullOrEmpty(sensor_item.last_run)) - { - sensor_item.last_run = DateTime.Now.ToString(); - string updated_sensor_json = JsonSerializer.Serialize(sensor_item); - File.WriteAllText(sensor, updated_sensor_json); + if (DateTime.Now.Date >= scheduledDateTime.Date && DateTime.Now.TimeOfDay >= scheduledDateTime.TimeOfDay && lastRunDateTime < scheduledDateTime) + execute = true; } + else if (sensor_item.time_scheduler_type == 2) // all x seconds + { + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "all x seconds", "name: " + sensor_item.name + " id: " + sensor_item.id + " last_run: " + DateTime.Parse(sensor_item.last_run ?? DateTime.Now.ToString())); - if (DateTime.Now.Date == DateTime.Parse(sensor_item.time_scheduler_date).Date && DateTime.Parse(sensor_item.last_run) < DateTime.Now - TimeSpan.FromHours(sensor_item.time_scheduler_hours)) - execute = true; - - Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "date, all x hours", "name: " + sensor_item.name + " id: " + sensor_item.id + " last_run: " + DateTime.Parse(sensor_item.last_run) + " execute: " + execute.ToString()); - } - else if (sensor_item.time_scheduler_type == 8) // following days at X time - { - Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "following days at X time", "name: " + sensor_item.name + " id: " + sensor_item.id + " last_run: " + DateTime.Parse(sensor_item.last_run ?? DateTime.Now.ToString())); + // Check if last run is empty, if so set it to now + if (String.IsNullOrEmpty(sensor_item.last_run)) + { + sensor_item.last_run = DateTime.Now.ToString(); + WriteEncryptedSensor(sensor, sensor_item); + } - DateTime scheduledDateTime = DateTime.ParseExact($"{sensor_item.time_scheduler_date.Split(' ')[0]} {sensor_item.time_scheduler_time}", "dd.MM.yyyy HH:mm:ss", CultureInfo.InvariantCulture); + if (DateTime.Parse(sensor_item.last_run) <= DateTime.Now - TimeSpan.FromSeconds(sensor_item.time_scheduler_seconds)) + execute = true; - // Check if last run is empty, if so, subsract 24 hours from scheduled time to trigger the execution - if (String.IsNullOrEmpty(sensor_item.last_run)) - { - sensor_item.last_run = (scheduledDateTime - TimeSpan.FromHours(24)).ToString(); - string updated_sensor_json = JsonSerializer.Serialize(sensor_item); - File.WriteAllText(sensor, updated_sensor_json); + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "all x seconds", "name: " + sensor_item.name + " id: " + sensor_item.id + " last_run: " + DateTime.Parse(sensor_item.last_run) + " execute: " + execute.ToString()); } + else if (sensor_item.time_scheduler_type == 3) // all x minutes + { + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "all x minutes", "name: " + sensor_item.name + " id: " + sensor_item.id + " last_run: " + (sensor_item.last_run ?? DateTime.Now.ToString(CultureInfo.InvariantCulture))); - DateTime lastRunDateTime = DateTime.Parse(sensor_item.last_run); - - if (DateTime.Now.DayOfWeek.ToString() == "Monday" && sensor_item.time_scheduler_monday && DateTime.Now.TimeOfDay >= scheduledDateTime.TimeOfDay && lastRunDateTime < scheduledDateTime) - execute = true; + // Check if last run is empty, if so set it to now + if (String.IsNullOrEmpty(sensor_item.last_run)) + { + sensor_item.last_run = DateTime.Now.ToString(CultureInfo.InvariantCulture); + WriteEncryptedSensor(sensor, sensor_item); + } - if (DateTime.Now.DayOfWeek.ToString() == "Tuesday" && sensor_item.time_scheduler_tuesday && DateTime.Now.TimeOfDay >= scheduledDateTime.TimeOfDay && lastRunDateTime < scheduledDateTime) - execute = true; + DateTime lastRun = DateTime.Parse(sensor_item.last_run, CultureInfo.InvariantCulture); + if (lastRun <= DateTime.Now - TimeSpan.FromMinutes(sensor_item.time_scheduler_minutes)) + execute = true; - if (DateTime.Now.DayOfWeek.ToString() == "Wednesday" && sensor_item.time_scheduler_wednesday && DateTime.Now.TimeOfDay >= scheduledDateTime.TimeOfDay && lastRunDateTime < scheduledDateTime) - execute = true; + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "all x minutes", "name: " + sensor_item.name + " id: " + sensor_item.id + " last_run: " + lastRun + " execute: " + execute.ToString()); + } + else if (sensor_item.time_scheduler_type == 4) // all x hours + { + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "all x hours", "name: " + sensor_item.name + " id: " + sensor_item.id + " last_run: " + (sensor_item.last_run ?? DateTime.Now.ToString(CultureInfo.InvariantCulture))); - if (DateTime.Now.DayOfWeek.ToString() == "Thursday" && sensor_item.time_scheduler_thursday && DateTime.Now.TimeOfDay >= scheduledDateTime.TimeOfDay && lastRunDateTime < scheduledDateTime) - execute = true; + // Check if last run is empty, if so set it to now + if (String.IsNullOrEmpty(sensor_item.last_run)) + { + sensor_item.last_run = DateTime.Now.ToString(CultureInfo.InvariantCulture); + WriteEncryptedSensor(sensor, sensor_item); + } - if (DateTime.Now.DayOfWeek.ToString() == "Friday" && sensor_item.time_scheduler_friday && DateTime.Now.TimeOfDay >= scheduledDateTime.TimeOfDay && lastRunDateTime < scheduledDateTime) - execute = true; + DateTime lastRun = DateTime.Parse(sensor_item.last_run, CultureInfo.InvariantCulture); + if (lastRun <= DateTime.Now - TimeSpan.FromHours(sensor_item.time_scheduler_hours)) + execute = true; - if (DateTime.Now.DayOfWeek.ToString() == "Saturday" && sensor_item.time_scheduler_saturday && DateTime.Now.TimeOfDay >= scheduledDateTime.TimeOfDay && lastRunDateTime < scheduledDateTime) - execute = true; + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "all x hours", "name: " + sensor_item.name + " id: " + sensor_item.id + " last_run: " + lastRun + " execute: " + execute.ToString()); + } + else if (sensor_item.time_scheduler_type == 5) // date, all x seconds + { + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "date, all x seconds", "name: " + sensor_item.name + " id: " + sensor_item.id + " last_run: " + DateTime.Parse(sensor_item.last_run ?? DateTime.Now.ToString())); - if (DateTime.Now.DayOfWeek.ToString() == "Sunday" && sensor_item.time_scheduler_sunday && DateTime.Now.TimeOfDay >= scheduledDateTime.TimeOfDay && lastRunDateTime < scheduledDateTime) - execute = true; + // Check if last run is empty, if so set it to now + if (String.IsNullOrEmpty(sensor_item.last_run)) + { + sensor_item.last_run = DateTime.Now.ToString(); + WriteEncryptedSensor(sensor, sensor_item); + } - Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "following days at X time", "name: " + sensor_item.name + " id: " + sensor_item.id + " last_run: " + DateTime.Parse(sensor_item.last_run) + " execute: " + execute.ToString()); - } - else if (sensor_item.time_scheduler_type == 9) // following days, x seconds - { - Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "following days, x seconds", "name: " + sensor_item.name + " id: " + sensor_item.id + " last_run: " + DateTime.Parse(sensor_item.last_run ?? DateTime.Now.ToString())); + if (DateTime.Now.Date == DateTime.Parse(sensor_item.time_scheduler_date).Date && DateTime.Parse(sensor_item.last_run) <= DateTime.Now - TimeSpan.FromSeconds(sensor_item.time_scheduler_seconds)) + execute = true; - // Check if last run is empty, if so set it to now - if (String.IsNullOrEmpty(sensor_item.last_run)) - { - sensor_item.last_run = DateTime.Now.ToString(); - string updated_sensor_json = JsonSerializer.Serialize(sensor_item); - File.WriteAllText(sensor, updated_sensor_json); + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "date, all x seconds", "name: " + sensor_item.name + " id: " + sensor_item.id + " last_run: " + DateTime.Parse(sensor_item.last_run) + " execute: " + execute.ToString()); } + else if (sensor_item.time_scheduler_type == 6) // date, all x minutes + { + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "date, all x minutes", "name: " + sensor_item.name + " id: " + sensor_item.id + " last_run: " + DateTime.Parse(sensor_item.last_run ?? DateTime.Now.ToString())); - if (DateTime.Now.DayOfWeek.ToString() == "Monday" && sensor_item.time_scheduler_monday && DateTime.Parse(sensor_item.last_run) <= DateTime.Now - TimeSpan.FromSeconds(sensor_item.time_scheduler_seconds)) - execute = true; - - if (DateTime.Now.DayOfWeek.ToString() == "Tuesday" && sensor_item.time_scheduler_tuesday && DateTime.Parse(sensor_item.last_run) <= DateTime.Now - TimeSpan.FromSeconds(sensor_item.time_scheduler_seconds)) - execute = true; + // Check if last run is empty, if so set it to now + if (String.IsNullOrEmpty(sensor_item.last_run)) + { + sensor_item.last_run = DateTime.Now.ToString(); + WriteEncryptedSensor(sensor, sensor_item); + } - if (DateTime.Now.DayOfWeek.ToString() == "Wednesday" && sensor_item.time_scheduler_wednesday && DateTime.Parse(sensor_item.last_run) <= DateTime.Now - TimeSpan.FromSeconds(sensor_item.time_scheduler_seconds)) - execute = true; + if (DateTime.Now.Date == DateTime.Parse(sensor_item.time_scheduler_date).Date && DateTime.Parse(sensor_item.last_run) < DateTime.Now - TimeSpan.FromMinutes(sensor_item.time_scheduler_minutes)) + execute = true; - if (DateTime.Now.DayOfWeek.ToString() == "Thursday" && sensor_item.time_scheduler_thursday && DateTime.Parse(sensor_item.last_run) <= DateTime.Now - TimeSpan.FromSeconds(sensor_item.time_scheduler_seconds)) - execute = true; + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "date, all x minutes", "name: " + sensor_item.name + " id: " + sensor_item.id + " last_run: " + DateTime.Parse(sensor_item.last_run) + " execute: " + execute.ToString()); + } + else if (sensor_item.time_scheduler_type == 7) // date, all x hours + { + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "date, all x hours", "name: " + sensor_item.name + " id: " + sensor_item.id + " last_run: " + DateTime.Parse(sensor_item.last_run ?? DateTime.Now.ToString())); - if (DateTime.Now.DayOfWeek.ToString() == "Friday" && sensor_item.time_scheduler_friday && DateTime.Parse(sensor_item.last_run) <= DateTime.Now - TimeSpan.FromSeconds(sensor_item.time_scheduler_seconds)) - execute = true; + // Check if last run is empty, if so set it to now + if (String.IsNullOrEmpty(sensor_item.last_run)) + { + sensor_item.last_run = DateTime.Now.ToString(); + WriteEncryptedSensor(sensor, sensor_item); + } - if (DateTime.Now.DayOfWeek.ToString() == "Saturday" && sensor_item.time_scheduler_saturday && DateTime.Parse(sensor_item.last_run) <= DateTime.Now - TimeSpan.FromSeconds(sensor_item.time_scheduler_seconds)) - execute = true; + if (DateTime.Now.Date == DateTime.Parse(sensor_item.time_scheduler_date).Date && DateTime.Parse(sensor_item.last_run) < DateTime.Now - TimeSpan.FromHours(sensor_item.time_scheduler_hours)) + execute = true; - if (DateTime.Now.DayOfWeek.ToString() == "Sunday" && sensor_item.time_scheduler_sunday && DateTime.Parse(sensor_item.last_run) <= DateTime.Now - TimeSpan.FromSeconds(sensor_item.time_scheduler_seconds)) - execute = true; + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "date, all x hours", "name: " + sensor_item.name + " id: " + sensor_item.id + " last_run: " + DateTime.Parse(sensor_item.last_run) + " execute: " + execute.ToString()); + } + else if (sensor_item.time_scheduler_type == 8) // following days at X time + { + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "following days at X time", "name: " + sensor_item.name + " id: " + sensor_item.id + " last_run: " + (sensor_item.last_run ?? DateTime.Now.ToString(CultureInfo.InvariantCulture))); - Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "following days, x seconds", "name: " + sensor_item.name + " id: " + sensor_item.id + " last_run: " + DateTime.Parse(sensor_item.last_run) + " execute: " + execute.ToString()); - } - else if (sensor_item.time_scheduler_type == 10) // following days, x minutes - { - // Check if last run is empty, if so set it to now - if (String.IsNullOrEmpty(sensor_item.last_run)) - sensor_item.last_run = DateTime.Now.ToString(); + DateTime scheduledTime = DateTime.ParseExact(sensor_item.time_scheduler_time, "HH:mm:ss", CultureInfo.InvariantCulture); - if (DateTime.Now.DayOfWeek.ToString() == "Monday" && sensor_item.time_scheduler_monday && DateTime.Parse(sensor_item.last_run) < DateTime.Now - TimeSpan.FromMinutes(sensor_item.time_scheduler_minutes)) - execute = true; + // Check if last run is empty, if so set it to a time in the past to trigger initial execution + if (String.IsNullOrEmpty(sensor_item.last_run)) + { + sensor_item.last_run = DateTime.Now.AddDays(-1).ToString(CultureInfo.InvariantCulture); + WriteEncryptedSensor(sensor, sensor_item); + } - if (DateTime.Now.DayOfWeek.ToString() == "Tuesday" && sensor_item.time_scheduler_tuesday && DateTime.Parse(sensor_item.last_run) < DateTime.Now - TimeSpan.FromMinutes(sensor_item.time_scheduler_minutes)) - execute = true; + DateTime lastRunDateTime = DateTime.Parse(sensor_item.last_run, CultureInfo.InvariantCulture); - if (DateTime.Now.DayOfWeek.ToString() == "Wednesday" && sensor_item.time_scheduler_wednesday && DateTime.Parse(sensor_item.last_run) < DateTime.Now - TimeSpan.FromMinutes(sensor_item.time_scheduler_minutes)) - execute = true; + // Check if current time is past the scheduled time and we haven't run today yet + bool shouldRunToday = DateTime.Now.TimeOfDay >= scheduledTime.TimeOfDay && lastRunDateTime.Date < DateTime.Now.Date; - if (DateTime.Now.DayOfWeek.ToString() == "Thursday" && sensor_item.time_scheduler_thursday && DateTime.Parse(sensor_item.last_run) < DateTime.Now - TimeSpan.FromMinutes(sensor_item.time_scheduler_minutes)) - execute = true; + // Use helper method to check weekday + if (ShouldRunToday(sensor_item) && shouldRunToday) + execute = true; - if (DateTime.Now.DayOfWeek.ToString() == "Friday" && sensor_item.time_scheduler_friday && DateTime.Parse(sensor_item.last_run) < DateTime.Now - TimeSpan.FromMinutes(sensor_item.time_scheduler_minutes)) - execute = true; + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "following days at X time", "name: " + sensor_item.name + " id: " + sensor_item.id + " last_run: " + lastRunDateTime + " execute: " + execute.ToString()); + } + else if (sensor_item.time_scheduler_type == 9) // following days, x seconds + { + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "following days, x seconds", "name: " + sensor_item.name + " id: " + sensor_item.id + " last_run: " + (sensor_item.last_run ?? DateTime.Now.ToString(CultureInfo.InvariantCulture))); - if (DateTime.Now.DayOfWeek.ToString() == "Saturday" && sensor_item.time_scheduler_saturday && DateTime.Parse(sensor_item.last_run) < DateTime.Now - TimeSpan.FromMinutes(sensor_item.time_scheduler_minutes)) - execute = true; + // Check if last run is empty, if so set it to now + if (String.IsNullOrEmpty(sensor_item.last_run)) + { + sensor_item.last_run = DateTime.Now.ToString(CultureInfo.InvariantCulture); + WriteEncryptedSensor(sensor, sensor_item); + } - if (DateTime.Now.DayOfWeek.ToString() == "Sunday" && sensor_item.time_scheduler_sunday && DateTime.Parse(sensor_item.last_run) < DateTime.Now - TimeSpan.FromMinutes(sensor_item.time_scheduler_minutes)) - execute = true; + DateTime lastRun = DateTime.Parse(sensor_item.last_run, CultureInfo.InvariantCulture); - Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "following days, x minutes", "name: " + sensor_item.name + " id: " + sensor_item.id + " last_run: " + DateTime.Parse(sensor_item.last_run) + " execute: " + execute.ToString()); - } - else if (sensor_item.time_scheduler_type == 11) // following days, x hours - { - DateTime scheduledDateTime = DateTime.ParseExact($"{sensor_item.time_scheduler_date.Split(' ')[0]} {sensor_item.time_scheduler_time}", "dd.MM.yyyy HH:mm:ss", CultureInfo.InvariantCulture); + // Check if it's a valid day AND the interval has passed + if (ShouldRunToday(sensor_item) && lastRun <= DateTime.Now - TimeSpan.FromSeconds(sensor_item.time_scheduler_seconds)) + execute = true; - // Check if last run is empty, if so, subsract 24 hours from scheduled time to trigger the execution - if (String.IsNullOrEmpty(sensor_item.last_run)) - { - sensor_item.last_run = (scheduledDateTime - TimeSpan.FromHours(24)).ToString(); - string updated_sensor_json = JsonSerializer.Serialize(sensor_item); - File.WriteAllText(sensor, updated_sensor_json); + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "following days, x seconds", "name: " + sensor_item.name + " id: " + sensor_item.id + " last_run: " + lastRun + " execute: " + execute.ToString()); } + else if (sensor_item.time_scheduler_type == 10) // following days, x minutes + { + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "following days, x minutes", "name: " + sensor_item.name + " id: " + sensor_item.id + " last_run: " + (sensor_item.last_run ?? DateTime.Now.ToString(CultureInfo.InvariantCulture))); - DateTime lastRunDateTime = DateTime.Parse(sensor_item.last_run); - - if (DateTime.Now.DayOfWeek.ToString() == "Monday" && sensor_item.time_scheduler_monday && DateTime.Parse(sensor_item.last_run) < DateTime.Now - TimeSpan.FromHours(sensor_item.time_scheduler_hours)) - execute = true; + // Check if last run is empty, if so set it to now + if (String.IsNullOrEmpty(sensor_item.last_run)) + { + sensor_item.last_run = DateTime.Now.ToString(CultureInfo.InvariantCulture); + WriteEncryptedSensor(sensor, sensor_item); + } - if (DateTime.Now.DayOfWeek.ToString() == "Tuesday" && sensor_item.time_scheduler_tuesday && DateTime.Parse(sensor_item.last_run) < DateTime.Now - TimeSpan.FromHours(sensor_item.time_scheduler_hours)) - execute = true; + DateTime lastRun = DateTime.Parse(sensor_item.last_run, CultureInfo.InvariantCulture); - if (DateTime.Now.DayOfWeek.ToString() == "Wednesday" && sensor_item.time_scheduler_wednesday && DateTime.Parse(sensor_item.last_run) < DateTime.Now - TimeSpan.FromHours(sensor_item.time_scheduler_hours)) - execute = true; + // Check if it's a valid day AND the interval has passed + if (ShouldRunToday(sensor_item) && lastRun <= DateTime.Now - TimeSpan.FromMinutes(sensor_item.time_scheduler_minutes)) + execute = true; - if (DateTime.Now.DayOfWeek.ToString() == "Thursday" && sensor_item.time_scheduler_thursday && DateTime.Parse(sensor_item.last_run) < DateTime.Now - TimeSpan.FromHours(sensor_item.time_scheduler_hours)) - execute = true; + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "following days, x minutes", "name: " + sensor_item.name + " id: " + sensor_item.id + " last_run: " + lastRun + " execute: " + execute.ToString()); + } + else if (sensor_item.time_scheduler_type == 11) // following days, x hours + { + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "following days, x hours", "name: " + sensor_item.name + " id: " + sensor_item.id + " last_run: " + (sensor_item.last_run ?? DateTime.Now.ToString(CultureInfo.InvariantCulture))); - if (DateTime.Now.DayOfWeek.ToString() == "Friday" && sensor_item.time_scheduler_friday && DateTime.Parse(sensor_item.last_run) < DateTime.Now - TimeSpan.FromHours(sensor_item.time_scheduler_hours)) - execute = true; + // Check if last run is empty, if so set it to now + if (String.IsNullOrEmpty(sensor_item.last_run)) + { + sensor_item.last_run = DateTime.Now.ToString(CultureInfo.InvariantCulture); + WriteEncryptedSensor(sensor, sensor_item); + } - if (DateTime.Now.DayOfWeek.ToString() == "Saturday" && sensor_item.time_scheduler_saturday && DateTime.Parse(sensor_item.last_run) < DateTime.Now - TimeSpan.FromHours(sensor_item.time_scheduler_hours)) - execute = true; + DateTime lastRun = DateTime.Parse(sensor_item.last_run, CultureInfo.InvariantCulture); - if (DateTime.Now.DayOfWeek.ToString() == "Sunday" && sensor_item.time_scheduler_sunday && DateTime.Parse(sensor_item.last_run) < DateTime.Now - TimeSpan.FromHours(sensor_item.time_scheduler_hours)) - execute = true; + // Check if it's a valid day AND the interval has passed + if (ShouldRunToday(sensor_item) && lastRun <= DateTime.Now - TimeSpan.FromHours(sensor_item.time_scheduler_hours)) + execute = true; - Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "following days, x hours", "name: " + sensor_item.name + " id: " + sensor_item.id + " last_run: " + DateTime.Parse(sensor_item.last_run) + " execute: " + execute.ToString()); - } + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "following days, x hours", "name: " + sensor_item.name + " id: " + sensor_item.id + " last_run: " + lastRun + " execute: " + execute.ToString()); + } - // Execute if needed - if (execute) - { - bool triggered = false; - var endTime = DateTime.Now; + // Execute if needed + if (execute) + { + // Store the old last_run value in case we need to rollback + string previous_last_run = sensor_item.last_run; + + // Update last run IMMEDIATELY to prevent race conditions (before executing the sensor) + var startTime = DateTime.Now; + sensor_item.last_run = startTime.ToString(CultureInfo.InvariantCulture); + WriteEncryptedSensor(sensor, sensor_item); - string action_result = String.Empty; + bool triggered = false; + var endTime = DateTime.Now; - if (sensor_item.action_treshold_max != 1) - action_result = "[" + DateTime.Now.ToString() + "]"; + string action_result = String.Empty; - string details = String.Empty; - string additional_details = String.Empty; - string notification_history = String.Empty; - string action_history = String.Empty; + if (sensor_item.action_treshold_max != 1) + action_result = "[" + DateTime.Now.ToString(CultureInfo.InvariantCulture) + "]"; - List process_information_list = new List(); + string details = String.Empty; + string additional_details = String.Empty; + string notification_history = String.Empty; + string action_history = String.Empty; - int resource_usage = 0; + List process_information_list = new List(); - Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Execute sensor", "name: " + sensor_item.name + " id: " + sensor_item.id); + int resource_usage = 0; - if (sensor_item.category == 0) // utilization - { - if (sensor_item.sub_category == 0) // cpu + try { - resource_usage = Device_Information.Hardware.CPU_Usage(); + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Execute sensor", "name: " + sensor_item.name + " id: " + sensor_item.id); - if (sensor_item.cpu_usage < resource_usage) // Check if CPU utilization is higher than the treshold + if (sensor_item.category == 0) // utilization { - triggered = true; - - // if action treshold is reached, execute the action and reset the counter - if (sensor_item.action_treshold_count >= sensor_item.action_treshold_max) + if (sensor_item.sub_category == 0) // cpu { - if (OperatingSystem.IsWindows()) - action_result += Environment.NewLine + Environment.NewLine + " [" + DateTime.Now.ToString() + "]" + Environment.NewLine + Windows.Helper.PowerShell.Execute_Script("Sensors.Time_Scheduler.Check_Execution (execute action) " + sensor_item.name, sensor_item.script_action); - else if (OperatingSystem.IsLinux()) - action_result += Environment.NewLine + Environment.NewLine + " [" + DateTime.Now.ToString() + "]" + Environment.NewLine + Linux.Helper.Bash.Execute_Script("Sensors.Time_Scheduler.Check_Execution (execute action) " + sensor_item.name, true, sensor_item.script_action); - else if (OperatingSystem.IsMacOS()) - action_result += Environment.NewLine + Environment.NewLine + " [" + DateTime.Now.ToString() + "]" + Environment.NewLine + MacOS.Helper.Zsh.Execute_Script("Sensors.Time_Scheduler.Check_Execution (execute action) " + sensor_item.name, true, sensor_item.script_action); - - // Create action history if not exists - if (String.IsNullOrEmpty(sensor_item.action_history)) + resource_usage = Device_Information.Hardware.CPU_Usage(); + + if (sensor_item.cpu_usage < resource_usage) // Check if CPU utilization is higher than the treshold { - List action_history_list = new List + triggered = true; + + // if action treshold is reached, execute the action and reset the counter + if (sensor_item.action_treshold_count >= sensor_item.action_treshold_max) { - action_result - }; + if (OperatingSystem.IsWindows()) + action_result += Environment.NewLine + Environment.NewLine + " [" + DateTime.Now.ToString() + "]" + Environment.NewLine + Windows.Helper.PowerShell.Execute_Script("Sensors.Time_Scheduler.Check_Execution (execute action) " + sensor_item.name, sensor_item.script_action); + else if (OperatingSystem.IsLinux()) + action_result += Environment.NewLine + Environment.NewLine + " [" + DateTime.Now.ToString() + "]" + Environment.NewLine + Linux.Helper.Bash.Execute_Script("Sensors.Time_Scheduler.Check_Execution (execute action) " + sensor_item.name, true, sensor_item.script_action); + else if (OperatingSystem.IsMacOS()) + action_result += Environment.NewLine + Environment.NewLine + " [" + DateTime.Now.ToString() + "]" + Environment.NewLine + MacOS.Helper.Zsh.Execute_Script("Sensors.Time_Scheduler.Check_Execution (execute action) " + sensor_item.name, true, sensor_item.script_action); - sensor_item.action_history = JsonSerializer.Serialize(action_history_list); - } - else // if exists, add the result to the list - { - List action_history_list = JsonSerializer.Deserialize>(sensor_item.action_history); - action_history_list.Add(action_result); + // Create action history if not exists + if (String.IsNullOrEmpty(sensor_item.action_history)) + { + List action_history_list = new List + { + action_result + }; + + sensor_item.action_history = JsonSerializer.Serialize(action_history_list); + } + else // if exists, add the result to the list + { + List action_history_list = JsonSerializer.Deserialize>(sensor_item.action_history); + action_history_list.Add(action_result); sensor_item.action_history = JsonSerializer.Serialize(action_history_list); } // Reset the counter sensor_item.action_treshold_count = 0; - string action_treshold_updated_sensor_json = JsonSerializer.Serialize(sensor_item); - File.WriteAllText(sensor, action_treshold_updated_sensor_json); + WriteEncryptedSensor(sensor, sensor_item); } else // if not, increment the counter { sensor_item.action_treshold_count++; - string action_treshold_updated_sensor_json = JsonSerializer.Serialize(sensor_item); - File.WriteAllText(sensor, action_treshold_updated_sensor_json); + WriteEncryptedSensor(sensor, sensor_item); } // Create event @@ -581,201 +759,508 @@ public static void Check_Execution() { details = $"The processor utilization exceeds the threshold value. The current utilization is {resource_usage}%. The defined limit is {sensor_item.cpu_usage}%." + Environment.NewLine + Environment.NewLine + - "Sensor name: " + sensor_item.name + Environment.NewLine + - "Description: " + sensor_item.description + Environment.NewLine + - "Type: Processor" + Environment.NewLine + - "Time: " + DateTime.Now + Environment.NewLine + - "Selected limit: " + sensor_item.cpu_usage + " (%)" + Environment.NewLine + - "In usage: " + resource_usage + " (%)" + Environment.NewLine + - "Action result: " + Environment.NewLine + action_result; + "Sensor name: " + sensor_item.name + Environment.NewLine + + "Description: " + sensor_item.description + Environment.NewLine + + "Type: Processor" + Environment.NewLine + + "Time: " + DateTime.Now + Environment.NewLine + + "Selected limit: " + sensor_item.cpu_usage + " (%)" + Environment.NewLine + + "In usage: " + resource_usage + " (%)" + Environment.NewLine + + "Action result: " + Environment.NewLine + action_result; + } + else if (Configuration.Agent.language == "de-DE") + { + details = + $"Die Prozessor-Auslastung ΓΌberschreitet den Schwellenwert. Aktuell betrΓ€gt die Auslastung {resource_usage}%. Das festgelegte Limit ist {sensor_item.cpu_usage}%." + Environment.NewLine + Environment.NewLine + + "Sensor Name: " + sensor_item.name + Environment.NewLine + + "Beschreibung: " + sensor_item.description + Environment.NewLine + + "Typ: Prozessor" + Environment.NewLine + + "Uhrzeit: " + DateTime.Now + Environment.NewLine + + "Festgelegtes Limit: " + sensor_item.cpu_usage + " (%)" + Environment.NewLine + + "In Verwendung: " + resource_usage + " (%)" + Environment.NewLine + + "Ergebnis der Aktion: " + Environment.NewLine + action_result; + } + } } - else if (Configuration.Agent.language == "de-DE") + else if (sensor_item.sub_category == 1) // RAM { - details = - $"Die Prozessor-Auslastung ΓΌberschreitet den Schwellenwert. Aktuell betrΓ€gt die Auslastung {resource_usage}%. Das festgelegte Limit ist {sensor_item.cpu_usage}%." + Environment.NewLine + Environment.NewLine + - "Sensor Name: " + sensor_item.name + Environment.NewLine + - "Beschreibung: " + sensor_item.description + Environment.NewLine + - "Typ: Prozessor" + Environment.NewLine + - "Uhrzeit: " + DateTime.Now + Environment.NewLine + - "Festgelegtes Limit: " + sensor_item.cpu_usage + " (%)" + Environment.NewLine + - "In Verwendung: " + resource_usage + " (%)" + Environment.NewLine + - "Ergebnis der Aktion: " + Environment.NewLine + action_result; - } + int ram_usage = Device_Information.Hardware.RAM_Usage(); + + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "RAM Check", $"Current RAM usage: {ram_usage}%, Threshold: {sensor_item.ram_usage}%, Triggered: {sensor_item.ram_usage < ram_usage}"); - // Create notification history if not exists - if (String.IsNullOrEmpty(sensor_item.notification_history)) - { - List notification_history_list = new List + if (sensor_item.ram_usage < ram_usage) { - details - }; - - sensor_item.notification_history = JsonSerializer.Serialize(notification_history_list); - } - else // if exists, add the result to the list - { - List notification_history_list = JsonSerializer.Deserialize>(sensor_item.notification_history); - notification_history_list.Add(details); - sensor_item.notification_history = JsonSerializer.Serialize(notification_history_list); - } - } - else - continue; - } - else if (sensor_item.sub_category == 1) // RAM - { - int ram_usage = Device_Information.Hardware.RAM_Usage(); - - if (sensor_item.ram_usage < ram_usage) - { - triggered = true; + triggered = true; - // if action treshold is reached, execute the action and reset the counter - if (sensor_item.action_treshold_count >= sensor_item.action_treshold_max) - { - if (OperatingSystem.IsWindows()) - action_result += Environment.NewLine + Environment.NewLine + " [" + DateTime.Now.ToString() + "]" + Environment.NewLine + Windows.Helper.PowerShell.Execute_Script("Sensors.Time_Scheduler.Check_Execution (execute action) " + sensor_item.name, sensor_item.script_action); - else if (OperatingSystem.IsLinux()) - action_result += Environment.NewLine + Environment.NewLine + " [" + DateTime.Now.ToString() + "]" + Environment.NewLine + Linux.Helper.Bash.Execute_Script("Sensors.Time_Scheduler.Check_Execution (execute action) " + sensor_item.name, true, sensor_item.script_action); - else if (OperatingSystem.IsMacOS()) - action_result += Environment.NewLine + Environment.NewLine + " [" + DateTime.Now.ToString() + "]" + Environment.NewLine + MacOS.Helper.Zsh.Execute_Script("Sensors.Time_Scheduler.Check_Execution (execute action) " + sensor_item.name, true, sensor_item.script_action); - - // Create action history if not exists - if (String.IsNullOrEmpty(sensor_item.action_history)) - { - List action_history_list = new List + // if action treshold is reached, execute the action and reset the counter + if (sensor_item.action_treshold_count >= sensor_item.action_treshold_max) { - action_result - }; + if (OperatingSystem.IsWindows()) + action_result += Environment.NewLine + Environment.NewLine + " [" + DateTime.Now.ToString() + "]" + Environment.NewLine + Windows.Helper.PowerShell.Execute_Script("Sensors.Time_Scheduler.Check_Execution (execute action) " + sensor_item.name, sensor_item.script_action); + else if (OperatingSystem.IsLinux()) + action_result += Environment.NewLine + Environment.NewLine + " [" + DateTime.Now.ToString() + "]" + Environment.NewLine + Linux.Helper.Bash.Execute_Script("Sensors.Time_Scheduler.Check_Execution (execute action) " + sensor_item.name, true, sensor_item.script_action); + else if (OperatingSystem.IsMacOS()) + action_result += Environment.NewLine + Environment.NewLine + " [" + DateTime.Now.ToString() + "]" + Environment.NewLine + MacOS.Helper.Zsh.Execute_Script("Sensors.Time_Scheduler.Check_Execution (execute action) " + sensor_item.name, true, sensor_item.script_action); - sensor_item.action_history = JsonSerializer.Serialize(action_history_list); - } - else // if exists, add the result to the list - { - List action_history_list = JsonSerializer.Deserialize>(sensor_item.action_history); - action_history_list.Add(action_result); - sensor_item.action_history = JsonSerializer.Serialize(action_history_list); - } + // Create action history if not exists + if (String.IsNullOrEmpty(sensor_item.action_history)) + { + List action_history_list = new List + { + action_result + }; - // Reset the counter - sensor_item.action_treshold_count = 0; - string action_treshold_updated_sensor_json = JsonSerializer.Serialize(sensor_item); - File.WriteAllText(sensor, action_treshold_updated_sensor_json); - } - else // if not, increment the counter - { - sensor_item.action_treshold_count++; - string action_treshold_updated_sensor_json = JsonSerializer.Serialize(sensor_item); - File.WriteAllText(sensor, action_treshold_updated_sensor_json); - } + sensor_item.action_history = JsonSerializer.Serialize(action_history_list); + } + else // if exists, add the result to the list + { + List action_history_list = JsonSerializer.Deserialize>(sensor_item.action_history); + action_history_list.Add(action_result); + sensor_item.action_history = JsonSerializer.Serialize(action_history_list); + } + + // Reset the counter + sensor_item.action_treshold_count = 0; + WriteEncryptedSensor(sensor, sensor_item); + } + else // if not, increment the counter + { + sensor_item.action_treshold_count++; + WriteEncryptedSensor(sensor, sensor_item); + } // Create event if (Configuration.Agent.language == "en-US") { details = $"Memory utilization exceeds the threshold. The current utilization is {ram_usage}%. The defined limit is {sensor_item.ram_usage}%." + Environment.NewLine + Environment.NewLine + - "Sensor name: " + sensor_item.name + Environment.NewLine + - "Description: " + sensor_item.description + Environment.NewLine + - "Type: RAM" + Environment.NewLine + - "Time: " + DateTime.Now + Environment.NewLine + - "Selected limit: " + sensor_item.ram_usage + " (%)" + Environment.NewLine + - "In usage: " + ram_usage + " (%)" + Environment.NewLine + - "Action result: " + Environment.NewLine + action_result; + "Sensor name: " + sensor_item.name + Environment.NewLine + + "Description: " + sensor_item.description + Environment.NewLine + + "Type: RAM" + Environment.NewLine + + "Time: " + DateTime.Now + Environment.NewLine + + "Selected limit: " + sensor_item.ram_usage + " (%)" + Environment.NewLine + + "In usage: " + ram_usage + " (%)" + Environment.NewLine + + "Action result: " + Environment.NewLine + action_result; + } + else if (Configuration.Agent.language == "de-DE") + { + details = + $"Die Arbeitsspeicherauslastung ΓΌberschreitet den Schwellenwert. Aktuell betrΓ€gt die Auslastung {ram_usage}%. Das festgelegte Limit ist {sensor_item.ram_usage}%." + Environment.NewLine + Environment.NewLine + + "Sensor Name: " + sensor_item.name + Environment.NewLine + + "Beschreibung: " + sensor_item.description + Environment.NewLine + + "Typ: Arbeitsspeicher" + Environment.NewLine + + "Uhrzeit: " + DateTime.Now + Environment.NewLine + + "Festgelegtes Limit: " + sensor_item.ram_usage + " (%)" + Environment.NewLine + + "In Verwendung: " + ram_usage + " (%)" + Environment.NewLine + + "Ergebnis der Aktion: " + Environment.NewLine + action_result; + } + } } - else if (Configuration.Agent.language == "de-DE") + else if (sensor_item.sub_category == 2) // Drives { - details = - $"Die Arbeitsspeicherauslastung ΓΌberschreitet den Schwellenwert. Aktuell betrΓ€gt die Auslastung {ram_usage}%. Das festgelegte Limit ist {sensor_item.ram_usage}%." + Environment.NewLine + Environment.NewLine + - "Sensor Name: " + sensor_item.name + Environment.NewLine + - "Beschreibung: " + sensor_item.description + Environment.NewLine + - "Typ: Arbeitsspeicher" + Environment.NewLine + - "Uhrzeit: " + DateTime.Now + Environment.NewLine + - "Festgelegtes Limit: " + sensor_item.ram_usage + " (%)" + Environment.NewLine + - "In Verwendung: " + ram_usage + " (%)" + Environment.NewLine + - "Ergebnis der Aktion: " + Environment.NewLine + action_result; - } + // Get all drives + List drives = DriveInfo.GetDrives().ToList(); - // Create notification history if not exists - if (String.IsNullOrEmpty(sensor_item.notification_history)) - { - List notification_history_list = new List + List drive_letters = sensor_item.disk_letters.Split(',') + .Select(letter => letter.Trim()) + .Where(letter => !string.IsNullOrEmpty(letter)) // Removes empty entries + .ToList(); + + foreach (var drive in drives) { - details - }; + // Extract the drive letter on Windows; use the full name on Linux + string drive_name = OperatingSystem.IsWindows() + ? drive.Name.Replace(":\\", "") // Windows: "C:\\" => "C" + : (drive.Name.EndsWith("/") && drive.Name.Count(c => c == '/') > 1 + ? drive.Name.TrimEnd('/') // Only trim if there is more than one slash + : drive.Name); // Otherwise leave the path unchanged - sensor_item.notification_history = JsonSerializer.Serialize(notification_history_list); - } - else // if exists, add the result to the list - { - List notification_history_list = JsonSerializer.Deserialize>(sensor_item.notification_history); - notification_history_list.Add(details); - sensor_item.notification_history = JsonSerializer.Serialize(notification_history_list); - } - } - else - continue; - } - else if (sensor_item.sub_category == 2) // Drives - { - // Get all drives - List drives = DriveInfo.GetDrives().ToList(); + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "foreach drive", "name: " + drive_name + " " + true.ToString()); + + // Check if the disk is included in the drives list that should be checked + if (drive_letters.Contains(drive_name) || drive_letters.Count == 0) + { + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "disk_included", "name: " + drive_name + " " + true.ToString()); + + // Check if the disk is a network disk or a removable disk, if so & they should not be scanned, skip it + if (drive.DriveType == DriveType.Network && !sensor_item.disk_include_network_disks) + continue; + + if (drive.DriveType == DriveType.Removable && !sensor_item.disk_include_removable_disks) + continue; + + // Get specification + string specification = String.Empty; + // if type 0 = gb + if (sensor_item.disk_category == 0 || sensor_item.disk_category == 1) + specification = "(GB)"; + else if (sensor_item.disk_category == 2 || sensor_item.disk_category == 3) + specification = "(%)"; + + // Check disk usage + int drive_total_space_gb = Device_Information.Hardware.Drive_Size_GB(drive_name); + int drive_free_space_gb = Device_Information.Hardware.Drive_Free_Space_GB(drive_name); + int drive_usage = 0; // If disk_category is 0 or 1, just calculate the usage in GB. If not, calculate the usage in percentage and respect that drive usage should not be seen as drive usage but as drive free space instead. This can cause confusion if not known + + // If disk_category is 0 or 1, just calculate the usage in GB + if (sensor_item.disk_category == 0 || sensor_item.disk_category == 1) + drive_usage = drive_total_space_gb - drive_free_space_gb; + else + drive_usage = Device_Information.Hardware.Drive_Usage(sensor_item.disk_category, drive_name); + + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "disk_specification", $"Drive: {drive_name}, Total: {drive_total_space_gb} GB, Free: {drive_free_space_gb} GB, Usage: {drive_usage} {specification}"); + + // 0 = More than X GB occupied, 1 = Less than X GB free, 2 = More than X percent occupied, 3 = Less than X percent free + if (sensor_item.disk_category == 0) // 0 = More than X GB occupied + { + if (drive_usage > sensor_item.disk_usage && drive_total_space_gb > sensor_item.disk_minimum_capacity) + { + triggered = true; - List drive_letters = sensor_item.disk_letters.Split(',') - .Select(letter => letter.Trim()) - .Where(letter => !string.IsNullOrEmpty(letter)) // Removes empty entries - .ToList(); + // if action treshold is reached, execute the action and reset the counter + if (sensor_item.action_treshold_count >= sensor_item.action_treshold_max) + { + if (OperatingSystem.IsWindows()) + action_result += Environment.NewLine + Environment.NewLine + " [" + DateTime.Now.ToString() + "]" + Environment.NewLine + Windows.Helper.PowerShell.Execute_Script("Sensors.Time_Scheduler.Check_Execution (execute action) " + sensor_item.name, sensor_item.script_action); + else if (OperatingSystem.IsLinux()) + action_result += Environment.NewLine + Environment.NewLine + " [" + DateTime.Now.ToString() + "]" + Environment.NewLine + Linux.Helper.Bash.Execute_Script("Sensors.Time_Scheduler.Check_Execution (execute action) " + sensor_item.name, true, sensor_item.script_action); + else if (OperatingSystem.IsMacOS()) + action_result += Environment.NewLine + Environment.NewLine + " [" + DateTime.Now.ToString() + "]" + Environment.NewLine + MacOS.Helper.Zsh.Execute_Script("Sensors.Time_Scheduler.Check_Execution (execute action) " + sensor_item.name, true, sensor_item.script_action); + + // Create action history if not exists + if (String.IsNullOrEmpty(sensor_item.action_history)) + { + List action_history_list = new List + { + action_result + }; + + sensor_item.action_history = JsonSerializer.Serialize(action_history_list); + } + else // if exists, add the result to the list + { + List action_history_list = JsonSerializer.Deserialize>(sensor_item.action_history); + action_history_list.Add(action_result); + sensor_item.action_history = JsonSerializer.Serialize(action_history_list); + } + + // Reset the counter + sensor_item.action_treshold_count = 0; + WriteEncryptedSensor(sensor, sensor_item); + } + else // if not, increment the counter + { + sensor_item.action_treshold_count++; + WriteEncryptedSensor(sensor, sensor_item); + } - foreach (var drive in drives) - { - // Extract the drive letter on Windows; use the full name on Linux - string drive_name = OperatingSystem.IsWindows() - ? drive.Name.Replace(":\\", "") // Windows: "C:\\" => "C" - : (drive.Name.EndsWith("/") && drive.Name.Count(c => c == '/') > 1 - ? drive.Name.TrimEnd('/') // Only trim if there is more than one slash - : drive.Name); // Otherwise leave the path unchanged + // Create event + if (Configuration.Agent.language == "en-US") + { + details = + $"More than the limit of {sensor_item.disk_usage} GB of storage space is being used. Currently, {drive_usage} GB is occupied, leaving {drive_free_space_gb} GB of free space remaining." + Environment.NewLine + Environment.NewLine + + "Sensor name: " + sensor_item.name + Environment.NewLine + + "Description: " + sensor_item.description + Environment.NewLine + + "Type: Drive (more than X GB occupied)" + Environment.NewLine + + "Time: " + DateTime.Now + Environment.NewLine + + "Drive: " + drive.Name + Environment.NewLine + + "Drive size: " + drive_total_space_gb + " (GB)" + Environment.NewLine + + "Drive free space: " + drive_free_space_gb + " (GB)" + Environment.NewLine + + "Selected limit: " + sensor_item.disk_usage + $" {specification}" + Environment.NewLine + + "In usage: " + drive_usage + $" {specification}" + Environment.NewLine + + "Action result: " + Environment.NewLine + action_result; + } + else if (Configuration.Agent.language == "de-DE") + { + details = + $"Es wird mehr als das festgelegte Limit von {sensor_item.disk_usage} GB Speicherplatz genutzt. Aktuell sind {drive_usage} GB belegt, sodass noch {drive_free_space_gb} GB verfΓΌgbar sind." + Environment.NewLine + Environment.NewLine + + "Sensor Name: " + sensor_item.name + Environment.NewLine + + "Beschreibung: " + sensor_item.description + Environment.NewLine + + "Typ: Laufwerk (mehr als X GB belegt)" + Environment.NewLine + + "Uhrzeit: " + DateTime.Now + Environment.NewLine + + "Laufwerk: " + drive.Name + Environment.NewLine + + "Laufwerksgrâße: " + drive_total_space_gb + " (GB)" + Environment.NewLine + + "Freier Laufwerksspeicher: " + drive_free_space_gb + " (GB)" + Environment.NewLine + + "Festgelegtes Limit: " + sensor_item.disk_usage + $" {specification}" + Environment.NewLine + + "In Verwendung: " + drive_usage + $" {specification}" + Environment.NewLine + + "Ergebnis der Aktion: " + Environment.NewLine + action_result; + } + } + } + else if (sensor_item.disk_category == 1) // 1 = Less than X GB free + { + if (drive_usage < sensor_item.disk_usage && drive_total_space_gb > sensor_item.disk_minimum_capacity) + { + triggered = true; + + // if action treshold is reached, execute the action and reset the counter + if (sensor_item.action_treshold_count >= sensor_item.action_treshold_max) + { + if (OperatingSystem.IsWindows()) + action_result += Environment.NewLine + Environment.NewLine + " [" + DateTime.Now.ToString() + "]" + Environment.NewLine + Windows.Helper.PowerShell.Execute_Script("Sensors.Time_Scheduler.Check_Execution (execute action) " + sensor_item.name, sensor_item.script_action); + else if (OperatingSystem.IsLinux()) + action_result += Environment.NewLine + Environment.NewLine + " [" + DateTime.Now.ToString() + "]" + Environment.NewLine + Linux.Helper.Bash.Execute_Script("Sensors.Time_Scheduler.Check_Execution (execute action) " + sensor_item.name, true, sensor_item.script_action); + else if (OperatingSystem.IsMacOS()) + action_result += Environment.NewLine + Environment.NewLine + " [" + DateTime.Now.ToString() + "]" + Environment.NewLine + MacOS.Helper.Zsh.Execute_Script("Sensors.Time_Scheduler.Check_Execution (execute action) " + sensor_item.name, true, sensor_item.script_action); + + // Create action history if not exists + if (String.IsNullOrEmpty(sensor_item.action_history)) + { + List action_history_list = new List + { + action_result + }; + + sensor_item.action_history = JsonSerializer.Serialize(action_history_list); + } + else // if exists, add the result to the list + { + List action_history_list = JsonSerializer.Deserialize>(sensor_item.action_history); + action_history_list.Add(action_result); + sensor_item.action_history = JsonSerializer.Serialize(action_history_list); + } + + // Reset the counter + sensor_item.action_treshold_count = 0; + WriteEncryptedSensor(sensor, sensor_item); + } + else // if not, increment the counter + { + sensor_item.action_treshold_count++; + WriteEncryptedSensor(sensor, sensor_item); + } + + // Create event + if (Configuration.Agent.language == "en-US") + { + details = + $"Less than the limit of {sensor_item.disk_usage} GB of storage space is available. Currently, {drive_usage} GB is occupied, leaving {drive_free_space_gb} GB of free space remaining." + Environment.NewLine + Environment.NewLine + + "Sensor name: " + sensor_item.name + Environment.NewLine + + "Description: " + sensor_item.description + Environment.NewLine + + "Type: Drive (less than X GB free)" + Environment.NewLine + + "Time: " + DateTime.Now + Environment.NewLine + + "Drive: " + drive.Name + Environment.NewLine + + "Drive size: " + drive_total_space_gb + " (GB)" + Environment.NewLine + + "Drive free space: " + drive_free_space_gb + " (GB)" + Environment.NewLine + + "Selected limit: " + sensor_item.disk_usage + $" {specification}" + Environment.NewLine + + "Free: " + drive_usage + $" {specification}" + Environment.NewLine + + "Action result: " + Environment.NewLine + action_result; + } + else if (Configuration.Agent.language == "de-DE") + { + details = + $"Weniger als der Grenzwert von {sensor_item.disk_usage} GB an Speicherplatz ist verfΓΌgbar. Aktuell sind {drive_usage} GB belegt, sodass noch {drive_free_space_gb} GB verfΓΌgbar sind." + Environment.NewLine + Environment.NewLine + + "Sensor Name: " + sensor_item.name + Environment.NewLine + + "Beschreibung: " + sensor_item.description + Environment.NewLine + + "Typ: Laufwerk (weniger als X GB frei)" + Environment.NewLine + + "Uhrzeit: " + DateTime.Now + Environment.NewLine + + "Laufwerk: " + drive.Name + Environment.NewLine + + "Laufwerksgrâße: " + drive_total_space_gb + " (GB)" + Environment.NewLine + + "Freier Platz auf dem Laufwerk: " + drive_free_space_gb + " (GB)" + Environment.NewLine + + "Festgelegtes Limit: " + sensor_item.disk_usage + $" {specification}" + Environment.NewLine + + "Frei: " + drive_usage + $" {specification}" + Environment.NewLine + + "Ergebnis der Aktion: " + Environment.NewLine + action_result; + } + } + } + else if (sensor_item.disk_category == 2) // 2 = More than X percent occupied + { + if (drive_usage > sensor_item.disk_usage && drive_total_space_gb > sensor_item.disk_minimum_capacity) + { + triggered = true; - Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "foreach drive", "name: " + drive_name + " " + true.ToString()); + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "disk_category_2", "name: " + drive_name + " triggered: " + true.ToString()); - // Check if the disk is included in the drives list that should be checked - if (drive_letters.Contains(drive_name) || drive_letters.Count == 0) + // if action treshold is reached, execute the action and reset the counter + if (sensor_item.action_treshold_count >= sensor_item.action_treshold_max) + { + if (OperatingSystem.IsWindows()) + action_result += Environment.NewLine + Environment.NewLine + " [" + DateTime.Now.ToString() + "]" + Environment.NewLine + Windows.Helper.PowerShell.Execute_Script("Sensors.Time_Scheduler.Check_Execution (execute action) " + sensor_item.name, sensor_item.script_action); + else if (OperatingSystem.IsLinux()) + action_result += Environment.NewLine + Environment.NewLine + " [" + DateTime.Now.ToString() + "]" + Environment.NewLine + Linux.Helper.Bash.Execute_Script("Sensors.Time_Scheduler.Check_Execution (execute action) " + sensor_item.name, true, sensor_item.script_action); + else if (OperatingSystem.IsMacOS()) + action_result += Environment.NewLine + Environment.NewLine + " [" + DateTime.Now.ToString() + "]" + Environment.NewLine + MacOS.Helper.Zsh.Execute_Script("Sensors.Time_Scheduler.Check_Execution (execute action) " + sensor_item.name, true, sensor_item.script_action); + + // Create action history if not exists + if (String.IsNullOrEmpty(sensor_item.action_history)) + { + List action_history_list = new List + { + action_result + }; + + sensor_item.action_history = JsonSerializer.Serialize(action_history_list); + } + else // if exists, add the result to the list + { + List action_history_list = JsonSerializer.Deserialize>(sensor_item.action_history); + action_history_list.Add(action_result); + sensor_item.action_history = JsonSerializer.Serialize(action_history_list); + } + + // Reset the counter + sensor_item.action_treshold_count = 0; + WriteEncryptedSensor(sensor, sensor_item); + } + else // if not, increment the counter + { + sensor_item.action_treshold_count++; + WriteEncryptedSensor(sensor, sensor_item); + } + + // Create event + if (Configuration.Agent.language == "en-US") + { + details = + $"More than the limit of {sensor_item.disk_usage}% of storage space is being used. Currently, {drive_usage}% is occupied, leaving {drive_free_space_gb} GB of free space remaining." + Environment.NewLine + Environment.NewLine + + "Sensor name: " + sensor_item.name + Environment.NewLine + + "Description: " + sensor_item.description + Environment.NewLine + + "Type: Drive (more than X percent occupied)" + Environment.NewLine + + "Time: " + DateTime.Now + Environment.NewLine + + "Drive: " + drive.Name + Environment.NewLine + + "Drive size: " + drive_total_space_gb + " (GB)" + Environment.NewLine + + "Drive free space: " + drive_free_space_gb + " (GB)" + Environment.NewLine + + "Selected limit: " + sensor_item.disk_usage + $" {specification}" + Environment.NewLine + + "In usage: " + drive_usage + $" {specification}" + Environment.NewLine + + "Action result: " + Environment.NewLine + action_result; + } + else if (Configuration.Agent.language == "de-DE") + { + details = + $"Es wird mehr als das festgelegte Limit von {sensor_item.disk_usage}% Speicherplatz genutzt. Aktuell sind {drive_usage}% belegt, sodass noch {drive_free_space_gb} GB verfΓΌgbar sind." + Environment.NewLine + Environment.NewLine + + "Sensor Name: " + sensor_item.name + Environment.NewLine + + "Beschreibung: " + sensor_item.description + Environment.NewLine + + "Typ: Laufwerk (mehr als X Prozent belegt)" + Environment.NewLine + + "Uhrzeit: " + DateTime.Now + Environment.NewLine + + "Laufwerk: " + drive.Name + Environment.NewLine + + "Laufwerksgrâße: " + drive_total_space_gb + " (GB)" + Environment.NewLine + + "Freier Platz auf dem Laufwerk: " + drive_free_space_gb + " (GB)" + Environment.NewLine + + "Festgelegtes Limit: " + sensor_item.disk_usage + $" {specification}" + Environment.NewLine + + "In Verwendung: " + drive_usage + $" {specification}" + Environment.NewLine + + "Ergebnis der Aktion: " + Environment.NewLine + action_result; + } + } + } + else if (sensor_item.disk_category == 3) // 3 = Less than X percent free + { + if (drive_usage < sensor_item.disk_usage && drive_total_space_gb > sensor_item.disk_minimum_capacity) + { + triggered = true; + + // if action treshold is reached, execute the action and reset the counter + if (sensor_item.action_treshold_count >= sensor_item.action_treshold_max) + { + if (OperatingSystem.IsWindows()) + action_result += Environment.NewLine + Environment.NewLine + " [" + DateTime.Now.ToString() + "]" + Environment.NewLine + Windows.Helper.PowerShell.Execute_Script("Sensors.Time_Scheduler.Check_Execution (execute action) " + sensor_item.name, sensor_item.script_action); + else if (OperatingSystem.IsLinux()) + action_result += Environment.NewLine + Environment.NewLine + " [" + DateTime.Now.ToString() + "]" + Environment.NewLine + Linux.Helper.Bash.Execute_Script("Sensors.Time_Scheduler.Check_Execution (execute action) " + sensor_item.name, true, sensor_item.script_action); + else if (OperatingSystem.IsMacOS()) + action_result += Environment.NewLine + Environment.NewLine + " [" + DateTime.Now.ToString() + "]" + Environment.NewLine + MacOS.Helper.Zsh.Execute_Script("Sensors.Time_Scheduler.Check_Execution (execute action) " + sensor_item.name, true, sensor_item.script_action); + + // Create action history if not exists + if (String.IsNullOrEmpty(sensor_item.action_history)) + { + List action_history_list = new List + { + action_result + }; + + sensor_item.action_history = JsonSerializer.Serialize(action_history_list); + } + else // if exists, add the result to the list + { + List action_history_list = JsonSerializer.Deserialize>(sensor_item.action_history); + action_history_list.Add(action_result); + sensor_item.action_history = JsonSerializer.Serialize(action_history_list); + } + + // Reset the counter + sensor_item.action_treshold_count = 0; + WriteEncryptedSensor(sensor, sensor_item); + } + else // if not, increment the counter + { + sensor_item.action_treshold_count++; + WriteEncryptedSensor(sensor, sensor_item); + } + + + // Create event + if (Configuration.Agent.language == "en-US") + { + details = + $"Less than the limit of {sensor_item.disk_usage}% of storage space is available. Leaving {drive_free_space_gb} GB of free space remaining." + Environment.NewLine + Environment.NewLine + + "Sensor name: " + sensor_item.name + Environment.NewLine + + "Description: " + sensor_item.description + Environment.NewLine + + "Type: Drive (less than X percent free)" + Environment.NewLine + + "Time: " + DateTime.Now + Environment.NewLine + + "Drive: " + drive.Name + Environment.NewLine + + "Drive size: " + drive_total_space_gb + " (GB)" + Environment.NewLine + + "Drive free space: " + drive_free_space_gb + " (GB)" + Environment.NewLine + + "Selected limit: " + sensor_item.disk_usage + $" {specification}" + Environment.NewLine + + "Free: " + drive_usage + $" {specification}" + Environment.NewLine + + "Action result: " + Environment.NewLine + action_result; + } + else if (Configuration.Agent.language == "de-DE") + { + details = + $"Weniger als der Grenzwert von {sensor_item.disk_usage}% an Speicherplatz ist verfΓΌgbar. Sodass noch {drive_free_space_gb} GB verfΓΌgbar sind." + Environment.NewLine + Environment.NewLine + + "Sensor Name: " + sensor_item.name + Environment.NewLine + + "Beschreibung: " + sensor_item.description + Environment.NewLine + + "Typ: Laufwerk (weniger als X Prozent frei)" + Environment.NewLine + + "Uhrzeit: " + DateTime.Now + Environment.NewLine + + "Laufwerk: " + drive.Name + Environment.NewLine + + "Laufwerksgrâße: " + drive_total_space_gb + " (GB)" + Environment.NewLine + + "Freier Platz auf dem Laufwerk: " + drive_free_space_gb + " (GB)" + Environment.NewLine + + "Festgelegtes Limit: " + sensor_item.disk_usage + $" {specification}" + Environment.NewLine + + "Frei: " + drive_usage + $" {specification}" + Environment.NewLine + + "Ergebnis der Aktion: " + Environment.NewLine + action_result; + } + } + } + } + } + } + if (sensor_item.sub_category == 3) // Process cpu utilization (%) { - Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "disk_included", "name: " + drive_name + " " + true.ToString()); - - // Check if the disk is a network disk or a removable disk, if so & they should not be scanned, skip it - if (drive.DriveType == DriveType.Network && !sensor_item.disk_include_network_disks) - continue; - - if (drive.DriveType == DriveType.Removable && !sensor_item.disk_include_removable_disks) - continue; - - // Get specification - string specification = String.Empty; - // if type 0 = gb - if (sensor_item.disk_category == 0 || sensor_item.disk_category == 1) - specification = "(GB)"; - else if (sensor_item.disk_category == 2 || sensor_item.disk_category == 3) - specification = "(%)"; - - // Check disk usage - int drive_total_space_gb = Device_Information.Hardware.Drive_Size_GB(drive_name); - int drive_free_space_gb = Device_Information.Hardware.Drive_Free_Space_GB(drive_name); - int drive_usage = 0; // If disk_category is 0 or 1, just calculate the usage in GB. If not, calculate the usage in percentage and respect that drive usage should not be seen as drive usage but as drive free space instead. This can cause confusion if not known - - // If disk_category is 0 or 1, just calculate the usage in GB - if (sensor_item.disk_category == 0 || sensor_item.disk_category == 1) - drive_usage = drive_total_space_gb - drive_free_space_gb; - else - drive_usage = Device_Information.Hardware.Drive_Usage(sensor_item.disk_category, drive_name); - - Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "disk_specification", $"Drive: {drive_name}, Total: {drive_total_space_gb} GB, Free: {drive_free_space_gb} GB, Usage: {drive_usage} {specification}"); - - // 0 = More than X GB occupied, 1 = Less than X GB free, 2 = More than X percent occupied, 3 = Less than X percent free - if (sensor_item.disk_category == 0) // 0 = More than X GB occupied + //foreach process + foreach (Process process in Process.GetProcesses()) { - if (drive_usage > sensor_item.disk_usage && drive_total_space_gb > sensor_item.disk_minimum_capacity) + if (process.ProcessName.ToLower() != sensor_item.process_name.Replace(".exe", "").ToLower()) // Check if the process name is the same, replace .exe to catch user fails + continue; + + resource_usage = Device_Information.Processes.Get_CPU_Usage_By_ID(process.Id); + + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "process cpu utilization", "name: " + sensor_item.name + " id: " + sensor_item.id); + + if (resource_usage > sensor_item.cpu_usage) { triggered = true; + //Logging.Handler.Sensors("Sensors.Time_Scheduler.Check_Execution", "process cpu utilization sensor triggered", "name: " + sensor_item.name + " id: " + sensor_item.id); + + int ram = Device_Information.Processes.Get_RAM_Usage_By_ID(process.Id, false); + string user = Device_Information.Processes.Process_Owner(process); + string created = process.StartTime.ToString(); + string path = process.MainModule.FileName; + string cmd = "-"; + + if (OperatingSystem.IsWindows()) + cmd = Windows.Helper.WMI.Search("root\\cimv2", "SELECT * FROM Win32_Process WHERE ProcessId = " + process.Id, "CommandLine"); + + Process_Information proc_info = new Process_Information + { + id = process.Id, + name = process.ProcessName, + cpu = resource_usage.ToString(), + ram = ram.ToString(), + user = user, + created = created, + path = path, + cmd = cmd + }; + + process_information_list.Add(proc_info); + // if action treshold is reached, execute the action and reset the counter if (sensor_item.action_treshold_count >= sensor_item.action_treshold_max) { @@ -803,74 +1288,92 @@ public static void Check_Execution() sensor_item.action_history = JsonSerializer.Serialize(action_history_list); } - // Reset the counter - sensor_item.action_treshold_count = 0; - string action_treshold_updated_sensor_json = JsonSerializer.Serialize(sensor_item); - File.WriteAllText(sensor, action_treshold_updated_sensor_json); - } - else // if not, increment the counter - { - sensor_item.action_treshold_count++; - string action_treshold_updated_sensor_json = JsonSerializer.Serialize(sensor_item); - File.WriteAllText(sensor, action_treshold_updated_sensor_json); - } + // Reset the counter + sensor_item.action_treshold_count = 0; + WriteEncryptedSensor(sensor, sensor_item); + } + else // if not, increment the counter + { + sensor_item.action_treshold_count++; + WriteEncryptedSensor(sensor, sensor_item); + } - // Create event - if (Configuration.Agent.language == "en-US") - { - details = - $"More than the limit of {sensor_item.disk_usage} GB of storage space is being used. Currently, {drive_usage} GB is occupied, leaving {drive_free_space_gb} GB of free space remaining." + Environment.NewLine + Environment.NewLine + + // Create event + if (Configuration.Agent.language == "en-US") + { + details = + $"The process utilization of {sensor_item.process_name} exceeds the threshold value. The current utilization is {resource_usage}%. The defined limit is {sensor_item.cpu_usage}%. The ram usage is {ram} (MB) & the owner of the process is {user}." + Environment.NewLine + Environment.NewLine + "Sensor name: " + sensor_item.name + Environment.NewLine + "Description: " + sensor_item.description + Environment.NewLine + - "Type: Drive (more than X GB occupied)" + Environment.NewLine + + "Type: Process CPU usage (%)" + Environment.NewLine + "Time: " + DateTime.Now + Environment.NewLine + - "Drive: " + drive.Name + Environment.NewLine + - "Drive size: " + drive_total_space_gb + " (GB)" + Environment.NewLine + - "Drive free space: " + drive_free_space_gb + " (GB)" + Environment.NewLine + - "Selected limit: " + sensor_item.disk_usage + $" {specification}" + Environment.NewLine + - "In usage: " + drive_usage + $" {specification}" + Environment.NewLine + + "Process name: " + sensor_item.process_name + " (" + process.Id + ")" + Environment.NewLine + + "Selected limit: " + sensor_item.cpu_usage + " (%)" + Environment.NewLine + + "In usage: " + resource_usage + " (%)" + Environment.NewLine + + "Ram usage: " + ram + " (MB)" + Environment.NewLine + + "User: " + user + Environment.NewLine + + "Commandline: " + cmd + Environment.NewLine + "Action result: " + Environment.NewLine + action_result; } else if (Configuration.Agent.language == "de-DE") { details = - $"Es wird mehr als das festgelegte Limit von {sensor_item.disk_usage} GB Speicherplatz genutzt. Aktuell sind {drive_usage} GB belegt, sodass noch {drive_free_space_gb} GB verfΓΌgbar sind." + Environment.NewLine + Environment.NewLine + + $"Die Prozessauslastung von {sensor_item.process_name} ΓΌberschreitet den Schwellenwert. Die aktuelle Auslastung betrΓ€gt {resource_usage}%. Der definierte Grenzwert ist {sensor_item.cpu_usage}%. Die Ram-Auslastung betrΓ€gt {ram} (MB) & der Besitzer des Prozesses ist {user}." + Environment.NewLine + Environment.NewLine + "Sensor Name: " + sensor_item.name + Environment.NewLine + "Beschreibung: " + sensor_item.description + Environment.NewLine + - "Typ: Laufwerk (mehr als X GB belegt)" + Environment.NewLine + + "Typ: Prozess-CPU-Nutzung" + Environment.NewLine + "Uhrzeit: " + DateTime.Now + Environment.NewLine + - "Laufwerk: " + drive.Name + Environment.NewLine + - "Laufwerksgrâße: " + drive_total_space_gb + " (GB)" + Environment.NewLine + - "Freier Laufwerksspeicher: " + drive_free_space_gb + " (GB)" + Environment.NewLine + - "Festgelegtes Limit: " + sensor_item.disk_usage + $" {specification}" + Environment.NewLine + - "In Verwendung: " + drive_usage + $" {specification}" + Environment.NewLine + + "Prozess Name: " + sensor_item.process_name + " (%)" + Environment.NewLine + + "Festgelegtes Limit: " + sensor_item.cpu_usage + " (%)" + Environment.NewLine + + "In Verwendung: " + resource_usage + " (%)" + Environment.NewLine + + "Ram Nutzung: " + ram + " (MB)" + Environment.NewLine + + "Benutzer: " + user + Environment.NewLine + + "Commandline: " + cmd + Environment.NewLine + "Ergebnis der Aktion: " + Environment.NewLine + action_result; } - - // Create notification history if not exists - if (String.IsNullOrEmpty(sensor_item.notification_history)) - { - List notification_history_list = new List - { - details - }; - - sensor_item.notification_history = JsonSerializer.Serialize(notification_history_list); - } - else // if exists, add the result to the list - { - List notification_history_list = JsonSerializer.Deserialize>(sensor_item.notification_history); - notification_history_list.Add(details); - sensor_item.notification_history = JsonSerializer.Serialize(notification_history_list); - } } } - else if (sensor_item.disk_category == 1) // 1 = Less than X GB free + } + else if (sensor_item.sub_category == 4) // Process ram utilization (%) + { + //foreach process + foreach (Process process in Process.GetProcesses()) { - if (drive_usage < sensor_item.disk_usage && drive_total_space_gb > sensor_item.disk_minimum_capacity) + if (process.ProcessName.ToLower() != sensor_item.process_name.Replace(".exe", "").ToLower()) // Check if the process name is the same, replace .exe to catch user fails + continue; + + resource_usage = Device_Information.Processes.Get_RAM_Usage_By_ID(process.Id, true); + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "process cpu utilization", "name: " + sensor_item.name + " id: " + sensor_item.id); + + if (resource_usage > sensor_item.ram_usage) { triggered = true; + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "process cpu utilization sensor triggered", "name: " + sensor_item.name + " id: " + sensor_item.id); + + int ram = Device_Information.Processes.Get_RAM_Usage_By_ID(process.Id, false); + string user = Device_Information.Processes.Process_Owner(process); + string created = process.StartTime.ToString(); + string path = process.MainModule.FileName; + string cmd = "-"; + + if (OperatingSystem.IsWindows()) + cmd = Windows.Helper.WMI.Search("root\\cimv2", "SELECT * FROM Win32_Process WHERE ProcessId = " + process.Id, "CommandLine"); + + Process_Information proc_info = new Process_Information + { + id = process.Id, + name = process.ProcessName, + cpu = resource_usage.ToString(), + ram = ram.ToString(), + user = user, + created = created, + path = path, + cmd = cmd + }; + + process_information_list.Add(proc_info); + // if action treshold is reached, execute the action and reset the counter if (sensor_item.action_treshold_count >= sensor_item.action_treshold_max) { @@ -898,75 +1401,91 @@ public static void Check_Execution() sensor_item.action_history = JsonSerializer.Serialize(action_history_list); } - // Reset the counter - sensor_item.action_treshold_count = 0; - string action_treshold_updated_sensor_json = JsonSerializer.Serialize(sensor_item); - File.WriteAllText(sensor, action_treshold_updated_sensor_json); - } - else // if not, increment the counter - { - sensor_item.action_treshold_count++; - string action_treshold_updated_sensor_json = JsonSerializer.Serialize(sensor_item); - File.WriteAllText(sensor, action_treshold_updated_sensor_json); - } + // Reset the counter + sensor_item.action_treshold_count = 0; + WriteEncryptedSensor(sensor, sensor_item); + } + else // if not, increment the counter + { + sensor_item.action_treshold_count++; + WriteEncryptedSensor(sensor, sensor_item); + } - // Create event - if (Configuration.Agent.language == "en-US") - { - details = - $"Less than the limit of {sensor_item.disk_usage} GB of storage space is available. Currently, {drive_usage} GB is occupied, leaving {drive_free_space_gb} GB of free space remaining." + Environment.NewLine + Environment.NewLine + + // Create event + if (Configuration.Agent.language == "en-US") + { + details = + $"The memory utilization of {sensor_item.process_name} exceeds the threshold value. The current utilization is {resource_usage}%. The defined limit is {sensor_item.ram_usage}%. The ram usage is {ram} (MB) & the owner of the process is {user}." + Environment.NewLine + Environment.NewLine + "Sensor name: " + sensor_item.name + Environment.NewLine + "Description: " + sensor_item.description + Environment.NewLine + - "Type: Drive (less than X GB free)" + Environment.NewLine + + "Type: Process RAM usage (%)" + Environment.NewLine + "Time: " + DateTime.Now + Environment.NewLine + - "Drive: " + drive.Name + Environment.NewLine + - "Drive size: " + drive_total_space_gb + " (GB)" + Environment.NewLine + - "Drive free space: " + drive_free_space_gb + " (GB)" + Environment.NewLine + - "Selected limit: " + sensor_item.disk_usage + $" {specification}" + Environment.NewLine + - "Free: " + drive_usage + $" {specification}" + Environment.NewLine + + "Process name: " + sensor_item.process_name + " (" + process.Id + ")" + Environment.NewLine + + "Selected limit: " + sensor_item.ram_usage + " (%)" + Environment.NewLine + + "In usage: " + resource_usage + " (%)" + Environment.NewLine + + "Ram usage: " + ram + " (MB)" + Environment.NewLine + + "User: " + user + Environment.NewLine + + "Commandline: " + cmd + Environment.NewLine + "Action result: " + Environment.NewLine + action_result; } else if (Configuration.Agent.language == "de-DE") { details = - $"Weniger als der Grenzwert von {sensor_item.disk_usage} GB an Speicherplatz ist verfΓΌgbar. Aktuell sind {drive_usage} GB belegt, sodass noch {drive_free_space_gb} GB verfΓΌgbar sind." + Environment.NewLine + Environment.NewLine + + $"Die Speicherauslastung von {sensor_item.process_name} ΓΌberschreitet den Schwellenwert. Die aktuelle Auslastung betrΓ€gt {resource_usage}%. Der definierte Grenzwert ist {sensor_item.ram_usage}%. Die Ram-Auslastung betrΓ€gt {ram} (MB) & der Besitzer des Prozesses ist {user}." + Environment.NewLine + Environment.NewLine + "Sensor Name: " + sensor_item.name + Environment.NewLine + "Beschreibung: " + sensor_item.description + Environment.NewLine + - "Typ: Laufwerk (weniger als X GB frei)" + Environment.NewLine + + "Typ: Prozess-RAM-Nutzung (%)" + Environment.NewLine + "Uhrzeit: " + DateTime.Now + Environment.NewLine + - "Laufwerk: " + drive.Name + Environment.NewLine + - "Laufwerksgrâße: " + drive_total_space_gb + " (GB)" + Environment.NewLine + - "Freier Platz auf dem Laufwerk: " + drive_free_space_gb + " (GB)" + Environment.NewLine + - "Festgelegtes Limit: " + sensor_item.disk_usage + $" {specification}" + Environment.NewLine + - "Frei: " + drive_usage + $" {specification}" + Environment.NewLine + + "Prozess Name: " + sensor_item.process_name + " (%)" + Environment.NewLine + + "Festgelegtes Limit: " + sensor_item.ram_usage + " (%)" + Environment.NewLine + + "In Verwendung: " + resource_usage + " (%)" + Environment.NewLine + + "Ram Nutzung: " + ram + " (MB)" + Environment.NewLine + + "Benutzer: " + user + Environment.NewLine + + "Commandline: " + cmd + Environment.NewLine + "Ergebnis der Aktion: " + Environment.NewLine + action_result; } - - // Create notification history if not exists - if (String.IsNullOrEmpty(sensor_item.notification_history)) - { - List notification_history_list = new List - { - details - }; - - sensor_item.notification_history = JsonSerializer.Serialize(notification_history_list); - } - else // if exists, add the result to the list - { - List notification_history_list = JsonSerializer.Deserialize>(sensor_item.notification_history); - notification_history_list.Add(details); - sensor_item.notification_history = JsonSerializer.Serialize(notification_history_list); - } } } - else if (sensor_item.disk_category == 2) // 2 = More than X percent occupied + } + else if (sensor_item.sub_category == 5) // Process ram utilization (MB) + { + //foreach process + foreach (Process process in Process.GetProcesses()) { - if (drive_usage > sensor_item.disk_usage && drive_total_space_gb > sensor_item.disk_minimum_capacity) + if (process.ProcessName.ToLower() != sensor_item.process_name.Replace(".exe", "").ToLower()) // Check if the process name is the same, replace .exe to catch user fails + continue; + + resource_usage = Device_Information.Processes.Get_RAM_Usage_By_ID(process.Id, false); + //Logging.Handler.Sensors("Sensors.Time_Scheduler.Check_Execution", "process cpu utilization", "name: " + sensor_item.name + " id: " + sensor_item.id); + + if (resource_usage > sensor_item.ram_usage) { triggered = true; - Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "disk_category_2", "name: " + drive_name + " triggered: " + true.ToString()); + //Logging.Handler.Sensors("Sensors.Time_Scheduler.Check_Execution", "process cpu utilization sensor triggered", "name: " + sensor_item.name + " id: " + sensor_item.id); + + int ram = resource_usage; + string user = Device_Information.Processes.Process_Owner(process); + string created = process.StartTime.ToString(); + string path = process.MainModule.FileName; + string cmd = "-"; + + if (OperatingSystem.IsWindows()) + cmd = Windows.Helper.WMI.Search("root\\cimv2", "SELECT * FROM Win32_Process WHERE ProcessId = " + process.Id, "CommandLine"); + + Process_Information proc_info = new Process_Information + { + id = process.Id, + name = process.ProcessName, + cpu = resource_usage.ToString(), + ram = ram.ToString(), + user = user, + created = created, + path = path, + cmd = cmd + }; + + process_information_list.Add(proc_info); // if action treshold is reached, execute the action and reset the counter if (sensor_item.action_treshold_count >= sensor_item.action_treshold_max) @@ -995,83 +1514,130 @@ public static void Check_Execution() sensor_item.action_history = JsonSerializer.Serialize(action_history_list); } - // Reset the counter - sensor_item.action_treshold_count = 0; - string action_treshold_updated_sensor_json = JsonSerializer.Serialize(sensor_item); - File.WriteAllText(sensor, action_treshold_updated_sensor_json); - } - else // if not, increment the counter - { - sensor_item.action_treshold_count++; - string action_treshold_updated_sensor_json = JsonSerializer.Serialize(sensor_item); - File.WriteAllText(sensor, action_treshold_updated_sensor_json); - } + // Reset the counter + sensor_item.action_treshold_count = 0; + WriteEncryptedSensor(sensor, sensor_item); + } + else // if not, increment the counter + { + sensor_item.action_treshold_count++; + WriteEncryptedSensor(sensor, sensor_item); + } - // Create event - if (Configuration.Agent.language == "en-US") - { - details = - $"More than the limit of {sensor_item.disk_usage}% of storage space is being used. Currently, {drive_usage}% is occupied, leaving {drive_free_space_gb} GB of free space remaining." + Environment.NewLine + Environment.NewLine + + // Create event + if (Configuration.Agent.language == "en-US") + { + details = + $"The memory utilization of {sensor_item.process_name} exceeds the threshold value. The current utilization is {resource_usage} (MB). The defined limit is {sensor_item.ram_usage} (MB). The owner of the process is {user}." + Environment.NewLine + Environment.NewLine + "Sensor name: " + sensor_item.name + Environment.NewLine + "Description: " + sensor_item.description + Environment.NewLine + - "Type: Drive (more than X percent occupied)" + Environment.NewLine + + "Type: Process RAM usage (MB)" + Environment.NewLine + "Time: " + DateTime.Now + Environment.NewLine + - "Drive: " + drive.Name + Environment.NewLine + - "Drive size: " + drive_total_space_gb + " (GB)" + Environment.NewLine + - "Drive free space: " + drive_free_space_gb + " (GB)" + Environment.NewLine + - "Selected limit: " + sensor_item.disk_usage + $" {specification}" + Environment.NewLine + - "In usage: " + drive_usage + $" {specification}" + Environment.NewLine + + "Process name: " + sensor_item.process_name + " (" + process.Id + ")" + Environment.NewLine + + "Selected limit: " + sensor_item.ram_usage + " (MB)" + Environment.NewLine + + "In usage: " + resource_usage + " (MB)" + Environment.NewLine + + "User: " + user + Environment.NewLine + + "Commandline: " + cmd + Environment.NewLine + "Action result: " + Environment.NewLine + action_result; } else if (Configuration.Agent.language == "de-DE") { details = - $"Es wird mehr als das festgelegte Limit von {sensor_item.disk_usage}% Speicherplatz genutzt. Aktuell sind {drive_usage}% belegt, sodass noch {drive_free_space_gb} GB verfΓΌgbar sind." + Environment.NewLine + Environment.NewLine + + $"Die Speicherauslastung von {sensor_item.process_name} ΓΌberschreitet den Schwellenwert. Die aktuelle Auslastung betrΓ€gt {resource_usage} (MB). Der definierte Grenzwert ist {sensor_item.ram_usage} (MB). Der Besitzer des Prozesses ist {user}." + Environment.NewLine + Environment.NewLine + "Sensor Name: " + sensor_item.name + Environment.NewLine + "Beschreibung: " + sensor_item.description + Environment.NewLine + - "Typ: Laufwerk (mehr als X Prozent belegt)" + Environment.NewLine + + "Typ: Prozess-RAM-Nutzung (MB)" + Environment.NewLine + "Uhrzeit: " + DateTime.Now + Environment.NewLine + - "Laufwerk: " + drive.Name + Environment.NewLine + - "Laufwerksgrâße: " + drive_total_space_gb + " (GB)" + Environment.NewLine + - "Freier Platz auf dem Laufwerk: " + drive_free_space_gb + " (GB)" + Environment.NewLine + - "Festgelegtes Limit: " + sensor_item.disk_usage + $" {specification}" + Environment.NewLine + - "In Verwendung: " + drive_usage + $" {specification}" + Environment.NewLine + + "Prozess Name: " + sensor_item.process_name + " (%)" + Environment.NewLine + + "Festgelegtes Limit: " + sensor_item.ram_usage + " (MB)" + Environment.NewLine + + "In Verwendung: " + resource_usage + " (MB)" + Environment.NewLine + + "Benutzer: " + user + Environment.NewLine + + "Commandline: " + cmd + Environment.NewLine + "Ergebnis der Aktion: " + Environment.NewLine + action_result; } + } + } + } + } + else if (sensor_item.category == 1) // Windows Eventlog + { + bool event_log_existing = false; + bool action_already_executed = false; // prevents the action from being executed multiple times + + EventLogQuery query; + EventLogReader reader = null; + EventRecord eventRecord; + + try + { + // Filter by time range and event ID + query = new EventLogQuery(sensor_item.eventlog, PathType.LogName, string.Format("*[System[(EventID={0}) and TimeCreated[@SystemTime >= '{1}'] and TimeCreated[@SystemTime <= '{2}']]]", sensor_item.eventlog_event_id, startTime.ToUniversalTime().ToString("o"), endTime.ToUniversalTime().ToString("o"))); - // Create notification history if not exists - if (String.IsNullOrEmpty(sensor_item.notification_history)) + reader = new EventLogReader(query); + + event_log_existing = true; + + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Check event log existence (" + sensor_item.eventlog + ")", event_log_existing.ToString()); + + // Read each event from event logs + while ((eventRecord = reader.ReadEvent()) != null) + { + //Logging.Handler.Sensors("Sensors.Time_Scheduler.Check_Execution", "Found events", eventRecord.Id.ToString()); + + if (DateTime.Parse(sensor_item.last_run) > eventRecord.TimeCreated) + { + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Check event time (" + eventRecord.TimeCreated + ")", "Last scan is newer than last event log."); + } + else + { + // Print current scanning event log + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Scan event", "EventID: " + eventRecord.Id.ToString() + " Timestamp: " + eventRecord.TimeCreated.Value.ToString() + " lastscan: " + sensor_item.last_run); + + string result = "-"; + string content = eventRecord.FormatDescription(); + bool regex_match = false; + + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "event log content", content); + + if (String.IsNullOrEmpty(content)) { - List notification_history_list = new List - { - details - }; + content = eventRecord.ToXml(); + + // code below is not reliable enough in current state. Maybe we can use it in the future. + //XmlDocument doc = new XmlDocument(); + //doc.LoadXml(eventRecord.ToXml()); - sensor_item.notification_history = JsonSerializer.Serialize(notification_history_list); + //XmlNode messageNode = doc.SelectSingleNode("//Event/EventData/Data[@Name='Message']"); + //content = messageNode?.InnerText ?? "N/A"; + + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "event log content xml extracted", content); } - else // if exists, add the result to the list + + if (String.IsNullOrEmpty(sensor_item.expected_result)) { - List notification_history_list = JsonSerializer.Deserialize>(sensor_item.notification_history); - notification_history_list.Add(details); - sensor_item.notification_history = JsonSerializer.Serialize(notification_history_list); + // Increment notification counter for each hit + sensor_item.notification_treshold_count++; + + triggered = true; + } + else if (Regex.IsMatch(content, sensor_item.expected_result)) + { + // Increment notification counter for each hit + sensor_item.notification_treshold_count++; + + triggered = true; } - } - } - else if (sensor_item.disk_category == 3) // 3 = Less than X percent free - { - if (drive_usage < sensor_item.disk_usage && drive_total_space_gb > sensor_item.disk_minimum_capacity) - { - triggered = true; // if action treshold is reached, execute the action and reset the counter if (sensor_item.action_treshold_count >= sensor_item.action_treshold_max) { - if (OperatingSystem.IsWindows()) - action_result += Environment.NewLine + Environment.NewLine + " [" + DateTime.Now.ToString() + "]" + Environment.NewLine + Windows.Helper.PowerShell.Execute_Script("Sensors.Time_Scheduler.Check_Execution (execute action) " + sensor_item.name, sensor_item.script_action); - else if (OperatingSystem.IsLinux()) - action_result += Environment.NewLine + Environment.NewLine + " [" + DateTime.Now.ToString() + "]" + Environment.NewLine + Linux.Helper.Bash.Execute_Script("Sensors.Time_Scheduler.Check_Execution (execute action) " + sensor_item.name, true, sensor_item.script_action); - else if (OperatingSystem.IsMacOS()) - action_result += Environment.NewLine + Environment.NewLine + " [" + DateTime.Now.ToString() + "]" + Environment.NewLine + MacOS.Helper.Zsh.Execute_Script("Sensors.Time_Scheduler.Check_Execution (execute action) " + sensor_item.name, true, sensor_item.script_action); + if (!action_already_executed) + { + if (OperatingSystem.IsWindows()) + action_result += Environment.NewLine + Environment.NewLine + " [" + DateTime.Now.ToString() + "]" + Environment.NewLine + Windows.Helper.PowerShell.Execute_Script("Sensors.Time_Scheduler.Check_Execution (execute action) " + sensor_item.name, sensor_item.script_action); + + action_already_executed = true; + } // Create action history if not exists if (String.IsNullOrEmpty(sensor_item.action_history)) @@ -1090,113 +1656,86 @@ public static void Check_Execution() sensor_item.action_history = JsonSerializer.Serialize(action_history_list); } - // Reset the counter - sensor_item.action_treshold_count = 0; - string action_treshold_updated_sensor_json = JsonSerializer.Serialize(sensor_item); - File.WriteAllText(sensor, action_treshold_updated_sensor_json); - } - else // if not, increment the counter - { - sensor_item.action_treshold_count++; - string action_treshold_updated_sensor_json = JsonSerializer.Serialize(sensor_item); - File.WriteAllText(sensor, action_treshold_updated_sensor_json); - } - + // Reset the counter + sensor_item.action_treshold_count = 0; + WriteEncryptedSensor(sensor, sensor_item); + } + else // if not, increment the counter + { + sensor_item.action_treshold_count++; + WriteEncryptedSensor(sensor, sensor_item); + } - // Create event - if (Configuration.Agent.language == "en-US") - { - details = - $"Less than the limit of {sensor_item.disk_usage}% of storage space is available. Leaving {drive_free_space_gb} GB of free space remaining." + Environment.NewLine + Environment.NewLine + + // Create event + if (Configuration.Agent.language == "en-US") + { + details = + $"The check of event log {sensor_item.eventlog} by event ID {sensor_item.eventlog_event_id} resulted in a hit for the expected result {sensor_item.expected_result}." + Environment.NewLine + Environment.NewLine + "Sensor name: " + sensor_item.name + Environment.NewLine + "Description: " + sensor_item.description + Environment.NewLine + - "Type: Drive (less than X percent free)" + Environment.NewLine + + "Type: Windows Eventlog" + Environment.NewLine + "Time: " + DateTime.Now + Environment.NewLine + - "Drive: " + drive.Name + Environment.NewLine + - "Drive size: " + drive_total_space_gb + " (GB)" + Environment.NewLine + - "Drive free space: " + drive_free_space_gb + " (GB)" + Environment.NewLine + - "Selected limit: " + sensor_item.disk_usage + $" {specification}" + Environment.NewLine + - "Free: " + drive_usage + $" {specification}" + Environment.NewLine + + "Eventlog: " + sensor_item.eventlog + Environment.NewLine + + "Event id: " + sensor_item.eventlog_event_id + Environment.NewLine + + "Expected result: " + sensor_item.expected_result + Environment.NewLine + + "Content: " + content + Environment.NewLine + + "Level: " + eventRecord.Level + Environment.NewLine + + "Process id: " + eventRecord.ProcessId + Environment.NewLine + + "Created: " + eventRecord.TimeCreated + Environment.NewLine + + "User id: " + eventRecord.UserId + Environment.NewLine + + "Version: " + eventRecord.Version + Environment.NewLine + "Action result: " + Environment.NewLine + action_result; } else if (Configuration.Agent.language == "de-DE") { details = - $"Weniger als der Grenzwert von {sensor_item.disk_usage}% an Speicherplatz ist verfΓΌgbar. Sodass noch {drive_free_space_gb} GB verfΓΌgbar sind." + Environment.NewLine + Environment.NewLine + + $"Die PrΓΌfung von Eventlog {sensor_item.eventlog} nach Event ID {sensor_item.eventlog_event_id} ergab einen Treffer fΓΌr das erwartete Ergebnis {sensor_item.expected_result}." + Environment.NewLine + Environment.NewLine + "Sensor Name: " + sensor_item.name + Environment.NewLine + "Beschreibung: " + sensor_item.description + Environment.NewLine + - "Typ: Laufwerk (weniger als X Prozent frei)" + Environment.NewLine + + "Typ: Windows Eventlog" + Environment.NewLine + "Uhrzeit: " + DateTime.Now + Environment.NewLine + - "Laufwerk: " + drive.Name + Environment.NewLine + - "Laufwerksgrâße: " + drive_total_space_gb + " (GB)" + Environment.NewLine + - "Freier Platz auf dem Laufwerk: " + drive_free_space_gb + " (GB)" + Environment.NewLine + - "Festgelegtes Limit: " + sensor_item.disk_usage + $" {specification}" + Environment.NewLine + - "Frei: " + drive_usage + $" {specification}" + Environment.NewLine + + "Eventlog: " + sensor_item.eventlog + Environment.NewLine + + "Event ID: " + sensor_item.eventlog_event_id + Environment.NewLine + + "Erwartetes Ergebnis: " + sensor_item.expected_result + Environment.NewLine + + "Inhalt: " + content + Environment.NewLine + + "Level: " + eventRecord.Level + Environment.NewLine + + "Prozess ID: " + eventRecord.ProcessId + Environment.NewLine + + "Erstellt: " + eventRecord.TimeCreated + Environment.NewLine + + "Benutzer ID: " + eventRecord.UserId + Environment.NewLine + + "Version: " + eventRecord.Version + Environment.NewLine + "Ergebnis der Aktion: " + Environment.NewLine + action_result; } - // Create notification history if not exists - if (String.IsNullOrEmpty(sensor_item.notification_history)) - { - List notification_history_list = new List - { - details - }; - sensor_item.notification_history = JsonSerializer.Serialize(notification_history_list); - } - else // if exists, add the result to the list + //Trigger incident response | NetLock legacy code + /*if (sensor["incident_response_ruleset"].ToString() != "LQ==") { - List notification_history_list = JsonSerializer.Deserialize>(sensor_item.notification_history); - notification_history_list.Add(details); - sensor_item.notification_history = JsonSerializer.Serialize(notification_history_list); - } + //Trigger it + Incident_Response.Handler.Get_Incident_Response_Ruleset(sensor["incident_response_ruleset"].ToString()); + }*/ } } } + catch (Exception ex) + { + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Check event log existence (" + sensor_item.eventlog + ")", event_log_existing.ToString() + " error: " + ex.ToString()); + } } - } - if (sensor_item.sub_category == 3) // Process cpu utilization (%) - { - //foreach process - foreach (Process process in Process.GetProcesses()) + else if (sensor_item.category == 2 || sensor_item.category == 5 || sensor_item.category == 6) // PowerShell, Linux Bash or MacOS Zsh { - if (process.ProcessName.ToLower() != sensor_item.process_name.Replace(".exe", "").ToLower()) // Check if the process name is the same, replace .exe to catch user fails - continue; + string result = "-"; - resource_usage = Device_Information.Processes.Get_CPU_Usage_By_ID(process.Id); + if (sensor_item.category == 2) + result = Windows.Helper.PowerShell.Execute_Script("Sensors.Time_Scheduler.Check_Execution (execute action) " + sensor_item.name, sensor_item.script); + else if (sensor_item.category == 5) + result = Linux.Helper.Bash.Execute_Script("Sensors.Time_Scheduler.Check_Execution (execute action) " + sensor_item.name, true, sensor_item.script); + else if (sensor_item.category == 6) + result = MacOS.Helper.Zsh.Execute_Script("Sensors.Time_Scheduler.Check_Execution (execute action) " + sensor_item.name, true, sensor_item.script); - Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "process cpu utilization", "name: " + sensor_item.name + " id: " + sensor_item.id); - - if (resource_usage > sensor_item.cpu_usage) + if (Regex.IsMatch(result, sensor_item.expected_result)) { triggered = true; - //Logging.Handler.Sensors("Sensors.Time_Scheduler.Check_Execution", "process cpu utilization sensor triggered", "name: " + sensor_item.name + " id: " + sensor_item.id); - - int ram = Device_Information.Processes.Get_RAM_Usage_By_ID(process.Id, false); - string user = Device_Information.Processes.Process_Owner(process); - string created = process.StartTime.ToString(); - string path = process.MainModule.FileName; - string cmd = "-"; - - if (OperatingSystem.IsWindows()) - cmd = Windows.Helper.WMI.Search("root\\cimv2", "SELECT * FROM Win32_Process WHERE ProcessId = " + process.Id, "CommandLine"); - - Process_Information proc_info = new Process_Information - { - id = process.Id, - name = process.ProcessName, - cpu = resource_usage.ToString(), - ram = ram.ToString(), - user = user, - created = created, - path = path, - cmd = cmd - }; - - process_information_list.Add(proc_info); - // if action treshold is reached, execute the action and reset the counter if (sensor_item.action_treshold_count >= sensor_item.action_treshold_max) { @@ -1224,243 +1763,242 @@ public static void Check_Execution() sensor_item.action_history = JsonSerializer.Serialize(action_history_list); } - // Reset the counter - sensor_item.action_treshold_count = 0; - string action_treshold_updated_sensor_json = JsonSerializer.Serialize(sensor_item); - File.WriteAllText(sensor, action_treshold_updated_sensor_json); - } - else // if not, increment the counter - { - sensor_item.action_treshold_count++; - string action_treshold_updated_sensor_json = JsonSerializer.Serialize(sensor_item); - File.WriteAllText(sensor, action_treshold_updated_sensor_json); - } + // Reset the counter + sensor_item.action_treshold_count = 0; + WriteEncryptedSensor(sensor, sensor_item); + } + else // if not, increment the counter + { + sensor_item.action_treshold_count++; + WriteEncryptedSensor(sensor, sensor_item); + } + + string details_type_specification = String.Empty; + + if (sensor_item.category == 2) + details_type_specification = "PowerShell"; + else if (sensor_item.category == 5) + details_type_specification = "Bash"; + else if (sensor_item.category == 6) + details_type_specification = "Zsh"; // Create event if (Configuration.Agent.language == "en-US") { details = - $"The process utilization of {sensor_item.process_name} exceeds the threshold value. The current utilization is {resource_usage}%. The defined limit is {sensor_item.cpu_usage}%. The ram usage is {ram} (MB) & the owner of the process is {user}." + Environment.NewLine + Environment.NewLine + + $"The script execution of {sensor_item.name} resulted in a hit for the expected result {sensor_item.expected_result}." + Environment.NewLine + Environment.NewLine + "Sensor name: " + sensor_item.name + Environment.NewLine + "Description: " + sensor_item.description + Environment.NewLine + - "Type: Process CPU usage (%)" + Environment.NewLine + + $"Type: {details_type_specification}" + Environment.NewLine + "Time: " + DateTime.Now + Environment.NewLine + - "Process name: " + sensor_item.process_name + " (" + process.Id + ")" + Environment.NewLine + - "Selected limit: " + sensor_item.cpu_usage + " (%)" + Environment.NewLine + - "In usage: " + resource_usage + " (%)" + Environment.NewLine + - "Ram usage: " + ram + " (MB)" + Environment.NewLine + - "User: " + user + Environment.NewLine + - "Commandline: " + cmd + Environment.NewLine + + "Script: " + sensor_item.script + Environment.NewLine + + "Pattern: " + sensor_item.expected_result + Environment.NewLine + + "Result: " + result + Environment.NewLine + "Action result: " + Environment.NewLine + action_result; } else if (Configuration.Agent.language == "de-DE") { details = - $"Die Prozessauslastung von {sensor_item.process_name} ΓΌberschreitet den Schwellenwert. Die aktuelle Auslastung betrΓ€gt {resource_usage}%. Der definierte Grenzwert ist {sensor_item.cpu_usage}%. Die Ram-Auslastung betrΓ€gt {ram} (MB) & der Besitzer des Prozesses ist {user}." + Environment.NewLine + Environment.NewLine + + $"Die SkriptausfΓΌhrung von {sensor_item.name} ergab einen Treffer fΓΌr das erwartete Ergebnis {sensor_item.expected_result}." + Environment.NewLine + Environment.NewLine + "Sensor Name: " + sensor_item.name + Environment.NewLine + "Beschreibung: " + sensor_item.description + Environment.NewLine + - "Typ: Prozess-CPU-Nutzung" + Environment.NewLine + + $"Typ: {details_type_specification}" + Environment.NewLine + "Uhrzeit: " + DateTime.Now + Environment.NewLine + - "Prozess Name: " + sensor_item.process_name + " (%)" + Environment.NewLine + - "Festgelegtes Limit: " + sensor_item.cpu_usage + " (%)" + Environment.NewLine + - "In Verwendung: " + resource_usage + " (%)" + Environment.NewLine + - "Ram Nutzung: " + ram + " (MB)" + Environment.NewLine + - "Benutzer: " + user + Environment.NewLine + - "Commandline: " + cmd + Environment.NewLine + + "Skript: " + sensor_item.script + Environment.NewLine + + "Pattern: " + sensor_item.expected_result + Environment.NewLine + + "Ergebnis: " + result + Environment.NewLine + "Ergebnis der Aktion: " + Environment.NewLine + action_result; } + } + } + else if (sensor_item.category == 3) // Service + { + bool service_start_failed = false; + string service_error_message = String.Empty; + string service_status = String.Empty; - // Create notification history if not exists - if (String.IsNullOrEmpty(sensor_item.notification_history)) + try + { + if (OperatingSystem.IsWindows()) { - List notification_history_list = new List + ServiceController sc = new ServiceController(sensor_item.service_name); + service_status = sc.Status.Equals(ServiceControllerStatus.Paused).ToString(); + + if (sensor_item.service_condition == 0 && sc.Status.Equals(ServiceControllerStatus.Running)) // if service is running and condition is 0 = running { - details - }; + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Service is running", sensor_item.service_name + " " + sc.Status.ToString()); + + triggered = true; + + if (sensor_item.service_action == 1) // stop the service if it's running and the action is 1 = stop + sc.Stop(); + } + else if (sensor_item.service_condition == 1 && sc.Status.Equals(ServiceControllerStatus.Paused)) // if service is paused and condition is 1 = paused + { + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Service is paused", sensor_item.service_name + " " + sc.Status.ToString()); + + triggered = true; + + if (sensor_item.service_action == 2) // restart the service if it's paused and the action is 2 = restart + { + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Service is paused, restarting", sensor_item.service_name + " " + sc.Status.ToString()); + + sc.Stop(); + sc.WaitForStatus(ServiceControllerStatus.Stopped); + sc.Start(); + } + } + else if (sensor_item.service_condition == 2 && sc.Status.Equals(ServiceControllerStatus.Stopped)) // if service is stopped and condition is 2 = stopped + { + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Service is stopped", sensor_item.service_name + " " + sc.Status.ToString()); + + triggered = true; - sensor_item.notification_history = JsonSerializer.Serialize(notification_history_list); + if (sensor_item.service_action == 0) // start the service if it's stopped and the action is 0 = start + sc.Start(); + } } - else // if exists, add the result to the list + else if (OperatingSystem.IsLinux()) // marked for refactoring for cleaner code { - List notification_history_list = JsonSerializer.Deserialize>(sensor_item.notification_history); - notification_history_list.Add(details); - sensor_item.notification_history = JsonSerializer.Serialize(notification_history_list); - } - } - } - } - else if (sensor_item.sub_category == 4) // Process ram utilization (%) - { - //foreach process - foreach (Process process in Process.GetProcesses()) - { - if (process.ProcessName.ToLower() != sensor_item.process_name.Replace(".exe", "").ToLower()) // Check if the process name is the same, replace .exe to catch user fails - continue; + sensor_item.service_name = sensor_item.service_name.Replace(".service", ""); // remove .service from the service name + string serviceCommand = $"systemctl list-units --type=service --all | grep -w {sensor_item.service_name}.service"; - resource_usage = Device_Information.Processes.Get_RAM_Usage_By_ID(process.Id, true); - Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "process cpu utilization", "name: " + sensor_item.name + " id: " + sensor_item.id); + // Execute bash script and save the output + string output = Linux.Helper.Bash.Execute_Script("Service Sensor", false, serviceCommand); - if (resource_usage > sensor_item.ram_usage) - { - triggered = true; + if (string.IsNullOrWhiteSpace(output)) + { + //Console.WriteLine($"Service {sensor_item.service_name} not found or no output received."); + continue; + } - Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "process cpu utilization sensor triggered", "name: " + sensor_item.name + " id: " + sensor_item.id); + bool isServiceRunning = false; - int ram = Device_Information.Processes.Get_RAM_Usage_By_ID(process.Id, false); - string user = Device_Information.Processes.Process_Owner(process); - string created = process.StartTime.ToString(); - string path = process.MainModule.FileName; - string cmd = "-"; + // Check status + // Use Regex to match running status + var match = Regex.Match(output, $@"running", RegexOptions.Multiline); - if (OperatingSystem.IsWindows()) - cmd = Windows.Helper.WMI.Search("root\\cimv2", "SELECT * FROM Win32_Process WHERE ProcessId = " + process.Id, "CommandLine"); + if (match.Success) + isServiceRunning = true; - Process_Information proc_info = new Process_Information - { - id = process.Id, - name = process.ProcessName, - cpu = resource_usage.ToString(), - ram = ram.ToString(), - user = user, - created = created, - path = path, - cmd = cmd - }; - - process_information_list.Add(proc_info); + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Service status", sensor_item.service_name + " " + isServiceRunning.ToString()); - // if action treshold is reached, execute the action and reset the counter - if (sensor_item.action_treshold_count >= sensor_item.action_treshold_max) - { - if (OperatingSystem.IsWindows()) - action_result += Environment.NewLine + Environment.NewLine + " [" + DateTime.Now.ToString() + "]" + Environment.NewLine + Windows.Helper.PowerShell.Execute_Script("Sensors.Time_Scheduler.Check_Execution (execute action) " + sensor_item.name, sensor_item.script_action); - else if (OperatingSystem.IsLinux()) - action_result += Environment.NewLine + Environment.NewLine + " [" + DateTime.Now.ToString() + "]" + Environment.NewLine + Linux.Helper.Bash.Execute_Script("Sensors.Time_Scheduler.Check_Execution (execute action) " + sensor_item.name, true, sensor_item.script_action); - else if (OperatingSystem.IsMacOS()) - action_result += Environment.NewLine + Environment.NewLine + " [" + DateTime.Now.ToString() + "]" + Environment.NewLine + MacOS.Helper.Zsh.Execute_Script("Sensors.Time_Scheduler.Check_Execution (execute action) " + sensor_item.name, true, sensor_item.script_action); - - // Create action history if not exists - if (String.IsNullOrEmpty(sensor_item.action_history)) + if (sensor_item.service_condition == 0 && isServiceRunning) // if service is running and condition is 0 = running { - List action_history_list = new List + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Service is running", sensor_item.service_name + " " + isServiceRunning.ToString()); + + triggered = true; + + if (sensor_item.service_action == 1) // stop the service if it's running and the action is 1 = stop { - action_result - }; + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Service is running, stopping", sensor_item.service_name + " " + isServiceRunning.ToString()); - sensor_item.action_history = JsonSerializer.Serialize(action_history_list); + // Stoppe den Dienst + Linux.Helper.Bash.Execute_Script("", false, $"systemctl stop {sensor_item.service_name}"); + } } - else // if exists, add the result to the list + else if (sensor_item.service_condition == 1 && !isServiceRunning) // if service is stopped and condition is 1 = paused (simuliert als gestoppt) { - List action_history_list = JsonSerializer.Deserialize>(sensor_item.action_history); - action_history_list.Add(action_result); - sensor_item.action_history = JsonSerializer.Serialize(action_history_list); + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Service is stopped", sensor_item.service_name + " " + isServiceRunning.ToString()); + + triggered = true; + + if (sensor_item.service_action == 2) // restart the service if it's stopped and the action is 2 = restart + { + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Service is stopped, restarting", sensor_item.service_name + " " + isServiceRunning.ToString()); + + // Starte den Dienst + Linux.Helper.Bash.Execute_Script("Sensor", false, $"systemctl start {sensor_item.service_name}"); + } } + else if (sensor_item.service_condition == 2 && !isServiceRunning) // if service is stopped and condition is 2 = stopped + { + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Service is stopped", sensor_item.service_name + " " + isServiceRunning.ToString()); - // Reset the counter - sensor_item.action_treshold_count = 0; - string action_treshold_updated_sensor_json = JsonSerializer.Serialize(sensor_item); - File.WriteAllText(sensor, action_treshold_updated_sensor_json); - } - else // if not, increment the counter - { - sensor_item.action_treshold_count++; - string action_treshold_updated_sensor_json = JsonSerializer.Serialize(sensor_item); - File.WriteAllText(sensor, action_treshold_updated_sensor_json); - } + triggered = true; - // Create event - if (Configuration.Agent.language == "en-US") - { - details = - $"The memory utilization of {sensor_item.process_name} exceeds the threshold value. The current utilization is {resource_usage}%. The defined limit is {sensor_item.ram_usage}%. The ram usage is {ram} (MB) & the owner of the process is {user}." + Environment.NewLine + Environment.NewLine + - "Sensor name: " + sensor_item.name + Environment.NewLine + - "Description: " + sensor_item.description + Environment.NewLine + - "Type: Process RAM usage (%)" + Environment.NewLine + - "Time: " + DateTime.Now + Environment.NewLine + - "Process name: " + sensor_item.process_name + " (" + process.Id + ")" + Environment.NewLine + - "Selected limit: " + sensor_item.ram_usage + " (%)" + Environment.NewLine + - "In usage: " + resource_usage + " (%)" + Environment.NewLine + - "Ram usage: " + ram + " (MB)" + Environment.NewLine + - "User: " + user + Environment.NewLine + - "Commandline: " + cmd + Environment.NewLine + - "Action result: " + Environment.NewLine + action_result; + if (sensor_item.service_action == 0) // start the service if it's stopped and the action is 0 = start + { + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Service is stopped, starting", sensor_item.service_name + " " + isServiceRunning.ToString()); + + // Starte den Dienst + Linux.Helper.Bash.Execute_Script("Sensor", false, $"systemctl start {sensor_item.service_name}"); + } + } } - else if (Configuration.Agent.language == "de-DE") + else if (OperatingSystem.IsMacOS()) // Currently only supports system wide services { - details = - $"Die Speicherauslastung von {sensor_item.process_name} ΓΌberschreitet den Schwellenwert. Die aktuelle Auslastung betrΓ€gt {resource_usage}%. Der definierte Grenzwert ist {sensor_item.ram_usage}%. Die Ram-Auslastung betrΓ€gt {ram} (MB) & der Besitzer des Prozesses ist {user}." + Environment.NewLine + Environment.NewLine + - "Sensor Name: " + sensor_item.name + Environment.NewLine + - "Beschreibung: " + sensor_item.description + Environment.NewLine + - "Typ: Prozess-RAM-Nutzung (%)" + Environment.NewLine + - "Uhrzeit: " + DateTime.Now + Environment.NewLine + - "Prozess Name: " + sensor_item.process_name + " (%)" + Environment.NewLine + - "Festgelegtes Limit: " + sensor_item.ram_usage + " (%)" + Environment.NewLine + - "In Verwendung: " + resource_usage + " (%)" + Environment.NewLine + - "Ram Nutzung: " + ram + " (MB)" + Environment.NewLine + - "Benutzer: " + user + Environment.NewLine + - "Commandline: " + cmd + Environment.NewLine + - "Ergebnis der Aktion: " + Environment.NewLine + action_result; - } + string output = MacOS.Helper.Zsh.Execute_Script("Service Sensor", false, $"launchctl list | grep {sensor_item.service_name}"); - // Create notification history if not exists - if (String.IsNullOrEmpty(sensor_item.notification_history)) - { - List notification_history_list = new List + // Regex, um nur die Zeile fΓΌr den spezifischen Dienst zu extrahieren + string pattern = $@"^\S+\s+\S+\s+{sensor_item.service_name}$"; + var match = Regex.Match(output, pattern, RegexOptions.Multiline); + + bool isServiceRunning = false; + + if (match.Success) { - details - }; + // Extrahiere den PID-Wert oder das `-` am Anfang der Zeile + string[] parts = match.Value.Split(new[] { '\t' }, StringSplitOptions.RemoveEmptyEntries); + isServiceRunning = parts[0] != "-"; // "-" bedeutet, der Dienst lΓ€uft nicht + } - sensor_item.notification_history = JsonSerializer.Serialize(notification_history_list); - } - else // if exists, add the result to the list - { - List notification_history_list = JsonSerializer.Deserialize>(sensor_item.notification_history); - notification_history_list.Add(details); - sensor_item.notification_history = JsonSerializer.Serialize(notification_history_list); - } - } - } - } - else if (sensor_item.sub_category == 5) // Process ram utilization (MB) - { - //foreach process - foreach (Process process in Process.GetProcesses()) - { - if (process.ProcessName.ToLower() != sensor_item.process_name.Replace(".exe", "").ToLower()) // Check if the process name is the same, replace .exe to catch user fails - continue; + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Service status", sensor_item.service_name + " " + isServiceRunning.ToString()); - resource_usage = Device_Information.Processes.Get_RAM_Usage_By_ID(process.Id, false); - //Logging.Handler.Sensors("Sensors.Time_Scheduler.Check_Execution", "process cpu utilization", "name: " + sensor_item.name + " id: " + sensor_item.id); + if (sensor_item.service_condition == 0 && isServiceRunning) // Wenn der Dienst lΓ€uft und die Bedingung 0 ist (sollte laufen) + { + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Service is running", sensor_item.service_name + " " + isServiceRunning.ToString()); - if (resource_usage > sensor_item.ram_usage) - { - triggered = true; + triggered = true; - //Logging.Handler.Sensors("Sensors.Time_Scheduler.Check_Execution", "process cpu utilization sensor triggered", "name: " + sensor_item.name + " id: " + sensor_item.id); + if (sensor_item.service_action == 1) // Stoppe den Dienst, wenn die Aktion 1 ist (soll gestoppt werden) + { + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Service is running, stopping", sensor_item.service_name + " " + isServiceRunning.ToString()); - int ram = resource_usage; - string user = Device_Information.Processes.Process_Owner(process); - string created = process.StartTime.ToString(); - string path = process.MainModule.FileName; - string cmd = "-"; + // Stoppe den Dienst + MacOS.Helper.Zsh.Execute_Script("Service Sensor", false, $"launchctl stop {sensor_item.service_name}.plist"); + } + } + else if (sensor_item.service_condition == 1 && !isServiceRunning) // Wenn der Dienst gestoppt ist und die Bedingung 1 ist (simuliert als pausiert) + { + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Service is stopped", sensor_item.service_name + " " + isServiceRunning.ToString()); - if (OperatingSystem.IsWindows()) - cmd = Windows.Helper.WMI.Search("root\\cimv2", "SELECT * FROM Win32_Process WHERE ProcessId = " + process.Id, "CommandLine"); + triggered = true; - Process_Information proc_info = new Process_Information - { - id = process.Id, - name = process.ProcessName, - cpu = resource_usage.ToString(), - ram = ram.ToString(), - user = user, - created = created, - path = path, - cmd = cmd - }; - - process_information_list.Add(proc_info); + if (sensor_item.service_action == 2) // Starte den Dienst neu, wenn die Aktion 2 ist + { + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Service is stopped, restarting", sensor_item.service_name + " " + isServiceRunning.ToString()); + // Starte den Dienst neu + MacOS.Helper.Zsh.Execute_Script("Service Sensor", false, $"launchctl stop {sensor_item.service_name}.plist"); + MacOS.Helper.Zsh.Execute_Script("Service Sensor", false, $"launchctl start {sensor_item.service_name}.plist"); + } + } + else if (sensor_item.service_condition == 2 && !isServiceRunning) // Wenn der Dienst gestoppt ist und die Bedingung 2 ist (sollte gestoppt sein) + { + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Service is stopped", sensor_item.service_name + " " + isServiceRunning.ToString()); + + triggered = true; + + if (sensor_item.service_action == 0) // Starte den Dienst, wenn die Aktion 0 ist (soll gestartet werden) + { + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Service is stopped, starting", sensor_item.service_name + " " + isServiceRunning.ToString()); + + // Starte den Dienst + MacOS.Helper.Zsh.Execute_Script("Service Sensor", false, $"launchctl start {sensor_item.service_name}.plist"); + } + } + } + } + catch (Exception ex) + { + service_start_failed = true; + service_error_message = ex.Message; + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Checking service state, or performing action failed", ex.ToString()); + } + + if (triggered) + { // if action treshold is reached, execute the action and reset the counter if (sensor_item.action_treshold_count >= sensor_item.action_treshold_max) { @@ -1488,150 +2026,135 @@ public static void Check_Execution() sensor_item.action_history = JsonSerializer.Serialize(action_history_list); } - // Reset the counter - sensor_item.action_treshold_count = 0; - string action_treshold_updated_sensor_json = JsonSerializer.Serialize(sensor_item); - File.WriteAllText(sensor, action_treshold_updated_sensor_json); - } - else // if not, increment the counter - { - sensor_item.action_treshold_count++; - string action_treshold_updated_sensor_json = JsonSerializer.Serialize(sensor_item); - File.WriteAllText(sensor, action_treshold_updated_sensor_json); - } + // Reset the counter + sensor_item.action_treshold_count = 0; + WriteEncryptedSensor(sensor, sensor_item); + } + else // if not, increment the counter + { + sensor_item.action_treshold_count++; + WriteEncryptedSensor(sensor, sensor_item); + } - // Create event - if (Configuration.Agent.language == "en-US") - { - details = - $"The memory utilization of {sensor_item.process_name} exceeds the threshold value. The current utilization is {resource_usage} (MB). The defined limit is {sensor_item.ram_usage} (MB). The owner of the process is {user}." + Environment.NewLine + Environment.NewLine + - "Sensor name: " + sensor_item.name + Environment.NewLine + - "Description: " + sensor_item.description + Environment.NewLine + - "Type: Process RAM usage (MB)" + Environment.NewLine + - "Time: " + DateTime.Now + Environment.NewLine + - "Process name: " + sensor_item.process_name + " (" + process.Id + ")" + Environment.NewLine + - "Selected limit: " + sensor_item.ram_usage + " (MB)" + Environment.NewLine + - "In usage: " + resource_usage + " (MB)" + Environment.NewLine + - "User: " + user + Environment.NewLine + - "Commandline: " + cmd + Environment.NewLine + - "Action result: " + Environment.NewLine + action_result; + // Create event + if (Configuration.Agent.language == "en-US") + { + // Convert the service condition to human readable text + string service_condition = String.Empty; + + if (sensor_item.service_condition == 0) + service_condition = "running"; + else if (sensor_item.service_condition == 1) + service_condition = "paused"; + else if (sensor_item.service_condition == 2) + service_condition = "stopped"; + + // Convert the service action to human readable text + string service_action = String.Empty; + + if (sensor_item.service_action == 0) + service_action = "start"; + else if (sensor_item.service_action == 1) + service_action = "stop"; + else if (sensor_item.service_action == 2) + service_action = "restart"; + + if (service_start_failed) + { + details = + $"The service {sensor_item.service_name} was {service_condition}. The service action ({service_action}) could not be performed." + Environment.NewLine + Environment.NewLine + + "Sensor name: " + sensor_item.name + Environment.NewLine + + "Description: " + sensor_item.description + Environment.NewLine + + "Type: Service" + Environment.NewLine + + "Time: " + DateTime.Now + Environment.NewLine + + "Service: " + sensor_item.service_name + Environment.NewLine + + "Result: The requested service action could not be performed." + Environment.NewLine + + "Error: " + service_error_message + Environment.NewLine + + "Action result: " + Environment.NewLine + action_result; + } + else + { + details = + $"The service {sensor_item.service_name} was {service_condition}. The service action ({service_action}) was successfully executed." + Environment.NewLine + Environment.NewLine + + "Sensor name: " + sensor_item.name + Environment.NewLine + + "Description: " + sensor_item.description + Environment.NewLine + + "Type: Service" + Environment.NewLine + + "Time: " + DateTime.Now + Environment.NewLine + + "Service: " + sensor_item.service_name + Environment.NewLine + + "Result: The requested service action was successfully executed." + Environment.NewLine + + "Action result: " + Environment.NewLine + action_result; + } } else if (Configuration.Agent.language == "de-DE") { - details = - $"Die Speicherauslastung von {sensor_item.process_name} ΓΌberschreitet den Schwellenwert. Die aktuelle Auslastung betrΓ€gt {resource_usage} (MB). Der definierte Grenzwert ist {sensor_item.ram_usage} (MB). Der Besitzer des Prozesses ist {user}." + Environment.NewLine + Environment.NewLine + - "Sensor Name: " + sensor_item.name + Environment.NewLine + - "Beschreibung: " + sensor_item.description + Environment.NewLine + - "Typ: Prozess-RAM-Nutzung (MB)" + Environment.NewLine + - "Uhrzeit: " + DateTime.Now + Environment.NewLine + - "Prozess Name: " + sensor_item.process_name + " (%)" + Environment.NewLine + - "Festgelegtes Limit: " + sensor_item.ram_usage + " (MB)" + Environment.NewLine + - "In Verwendung: " + resource_usage + " (MB)" + Environment.NewLine + - "Benutzer: " + user + Environment.NewLine + - "Commandline: " + cmd + Environment.NewLine + - "Ergebnis der Aktion: " + Environment.NewLine + action_result; - } - - // Create notification history if not exists - if (String.IsNullOrEmpty(sensor_item.notification_history)) - { - List notification_history_list = new List + // Convert the service condition to human readable text + string service_condition = String.Empty; + + if (sensor_item.service_condition == 0) + service_condition = "lΓ€uft"; + else if (sensor_item.service_condition == 1) + service_condition = "pausiert"; + else if (sensor_item.service_condition == 2) + service_condition = "gestoppt"; + + // Convert the service action to human readable text + string service_action = String.Empty; + + if (sensor_item.service_action == 0) + service_action = "starten"; + else if (sensor_item.service_action == 1) + service_action = "stoppen"; + else if (sensor_item.service_action == 2) + service_action = "neu starten"; + + if (service_start_failed) { - details - }; - - sensor_item.notification_history = JsonSerializer.Serialize(notification_history_list); - } - else // if exists, add the result to the list - { - List notification_history_list = JsonSerializer.Deserialize>(sensor_item.notification_history); - notification_history_list.Add(details); - sensor_item.notification_history = JsonSerializer.Serialize(notification_history_list); + details = + $"Der Dienst {sensor_item.service_name} war {service_condition}. Die Dienstaktion ({service_action}) konnte nicht ausgefΓΌhrt werden." + Environment.NewLine + + "Sensor Name: " + sensor_item.name + Environment.NewLine + + "Beschreibung: " + sensor_item.description + Environment.NewLine + + "Typ: Dienst" + Environment.NewLine + + "Uhrzeit: " + DateTime.Now + Environment.NewLine + + "Dienst: " + sensor_item.service_name + Environment.NewLine + + "Ergebnis: The requested service action could not be performed." + Environment.NewLine + + "Fehler: " + service_error_message + Environment.NewLine + + "Ergebnis der Aktion: " + Environment.NewLine + action_result; + } + else + { + details = + $"Der Dienst {sensor_item.service_name} war {service_condition}. Die Dienstaktion ({service_action}) wurde erfolgreich ausgefΓΌhrt." + Environment.NewLine + + "Sensor Name: " + sensor_item.name + Environment.NewLine + + "Beschreibung: " + sensor_item.description + Environment.NewLine + + "Typ: Dienst" + Environment.NewLine + + "Uhrzeit: " + DateTime.Now + Environment.NewLine + + "Dienst: " + sensor_item.service_name + Environment.NewLine + + "Ergebnis: Die gewΓΌnschte Dienst Aktion wurde erfolgreich ausgefΓΌhrt." + Environment.NewLine + + "Ergebnis der Aktion: " + Environment.NewLine + action_result; + } } } } - } - } - else if (sensor_item.category == 1) // Windows Eventlog - { - DateTime startTime = DateTime.Parse(sensor_item.last_run); - bool event_log_existing = false; - bool action_already_executed = false; // prevents the action from being executed multiple times - - EventLogQuery query; - EventLogReader reader = null; - EventRecord eventRecord; - - try - { - // Filter by time range and event ID - query = new EventLogQuery(sensor_item.eventlog, PathType.LogName, string.Format("*[System[(EventID={0}) and TimeCreated[@SystemTime >= '{1}'] and TimeCreated[@SystemTime <= '{2}']]]", sensor_item.eventlog_event_id, startTime.ToUniversalTime().ToString("o"), endTime.ToUniversalTime().ToString("o"))); - - reader = new EventLogReader(query); - - event_log_existing = true; - - Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Check event log existence (" + sensor_item.eventlog + ")", event_log_existing.ToString()); - - // Read each event from event logs - while ((eventRecord = reader.ReadEvent()) != null) + else if (sensor_item.category == 4) // Ping { - //Logging.Handler.Sensors("Sensors.Time_Scheduler.Check_Execution", "Found events", eventRecord.Id.ToString()); - - if (DateTime.Parse(sensor_item.last_run) > eventRecord.TimeCreated) - { - Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Check event time (" + eventRecord.TimeCreated + ")", "Last scan is newer than last event log."); - } - else - { - // Print current scanning event log - Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Scan event", "EventID: " + eventRecord.Id.ToString() + " Timestamp: " + eventRecord.TimeCreated.Value.ToString() + " lastscan: " + sensor_item.last_run); - - string result = "-"; - string content = eventRecord.FormatDescription(); - bool regex_match = false; - - Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "event log content", content); - - if (String.IsNullOrEmpty(content)) - { - content = eventRecord.ToXml(); - - // code below is not reliable enough in current state. Maybe we can use it in the future. - //XmlDocument doc = new XmlDocument(); - //doc.LoadXml(eventRecord.ToXml()); - - //XmlNode messageNode = doc.SelectSingleNode("//Event/EventData/Data[@Name='Message']"); - //content = messageNode?.InnerText ?? "N/A"; + bool ping_status = Device_Information.Network.Ping(sensor_item.ping_address, sensor_item.ping_timeout); - Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "event log content xml extracted", content); - } - - if (String.IsNullOrEmpty(sensor_item.expected_result)) - { - // Increment notification counter for each hit - sensor_item.notification_treshold_count++; - - triggered = true; - } - else if (Regex.IsMatch(content, sensor_item.expected_result)) - { - // Increment notification counter for each hit - sensor_item.notification_treshold_count++; - - triggered = true; - } + if (ping_status && sensor_item.ping_condition == 0) + triggered = true; + else if (!ping_status && sensor_item.ping_condition == 1) + triggered = true; + if (triggered) + { // if action treshold is reached, execute the action and reset the counter if (sensor_item.action_treshold_count >= sensor_item.action_treshold_max) { - if (!action_already_executed) - { - if (OperatingSystem.IsWindows()) - action_result += Environment.NewLine + Environment.NewLine + " [" + DateTime.Now.ToString() + "]" + Environment.NewLine + Windows.Helper.PowerShell.Execute_Script("Sensors.Time_Scheduler.Check_Execution (execute action) " + sensor_item.name, sensor_item.script_action); - - action_already_executed = true; - } + if (OperatingSystem.IsWindows()) + action_result += Environment.NewLine + Environment.NewLine + " [" + DateTime.Now.ToString() + "]" + Environment.NewLine + Windows.Helper.PowerShell.Execute_Script("Sensors.Time_Scheduler.Check_Execution (execute action) " + sensor_item.name, sensor_item.script_action, 0); + else if (OperatingSystem.IsLinux()) + action_result += Environment.NewLine + Environment.NewLine + " [" + DateTime.Now.ToString() + "]" + Environment.NewLine + Linux.Helper.Bash.Execute_Script("Sensors.Time_Scheduler.Check_Execution (execute action) " + sensor_item.name, true, sensor_item.script_action, 0); + else if (OperatingSystem.IsMacOS()) + action_result += Environment.NewLine + Environment.NewLine + " [" + DateTime.Now.ToString() + "]" + Environment.NewLine + MacOS.Helper.Zsh.Execute_Script("Sensors.Time_Scheduler.Check_Execution (execute action) " + sensor_item.name, true, sensor_item.script_action, 0); // Create action history if not exists if (String.IsNullOrEmpty(sensor_item.action_history)) @@ -1650,856 +2173,297 @@ public static void Check_Execution() sensor_item.action_history = JsonSerializer.Serialize(action_history_list); } - // Reset the counter - sensor_item.action_treshold_count = 0; - string action_treshold_updated_sensor_json = JsonSerializer.Serialize(sensor_item); - File.WriteAllText(sensor, action_treshold_updated_sensor_json); - } - else // if not, increment the counter - { - sensor_item.action_treshold_count++; - string action_treshold_updated_sensor_json = JsonSerializer.Serialize(sensor_item); - File.WriteAllText(sensor, action_treshold_updated_sensor_json); - } + // Reset the counter + sensor_item.action_treshold_count = 0; + WriteEncryptedSensor(sensor, sensor_item); + } + else // if not, increment the counter + { + sensor_item.action_treshold_count++; + WriteEncryptedSensor(sensor, sensor_item); + } + + // Create event + string ping_result = String.Empty; - // Create event if (Configuration.Agent.language == "en-US") { + if (sensor_item.ping_condition == 0) + ping_result = "Successful"; + else if (sensor_item.ping_condition == 1) + ping_result = "Failed"; + details = - $"The check of event log {sensor_item.eventlog} by event ID {sensor_item.eventlog_event_id} resulted in a hit for the expected result {sensor_item.expected_result}." + Environment.NewLine + Environment.NewLine + + $"The ping check of {sensor_item.ping_address} resulted in a hit for the expected result {ping_result}." + Environment.NewLine + Environment.NewLine + "Sensor name: " + sensor_item.name + Environment.NewLine + "Description: " + sensor_item.description + Environment.NewLine + - "Type: Windows Eventlog" + Environment.NewLine + + "Type: Ping" + Environment.NewLine + "Time: " + DateTime.Now + Environment.NewLine + - "Eventlog: " + sensor_item.eventlog + Environment.NewLine + - "Event id: " + sensor_item.eventlog_event_id + Environment.NewLine + - "Expected result: " + sensor_item.expected_result + Environment.NewLine + - "Content: " + content + Environment.NewLine + - "Level: " + eventRecord.Level + Environment.NewLine + - "Process id: " + eventRecord.ProcessId + Environment.NewLine + - "Created: " + eventRecord.TimeCreated + Environment.NewLine + - "User id: " + eventRecord.UserId + Environment.NewLine + - "Version: " + eventRecord.Version + Environment.NewLine + + "Address: " + sensor_item.ping_address + Environment.NewLine + + "Timeout: " + sensor_item.ping_timeout + Environment.NewLine + + "Result: " + ping_result + Environment.NewLine + "Action result: " + Environment.NewLine + action_result; } else if (Configuration.Agent.language == "de-DE") { + if (sensor_item.ping_condition == 0) + ping_result = "Erfolgreich"; + else if (sensor_item.ping_condition == 1) + ping_result = "Fehlgeschlagen"; + details = - $"Die PrΓΌfung von Eventlog {sensor_item.eventlog} nach Event ID {sensor_item.eventlog_event_id} ergab einen Treffer fΓΌr das erwartete Ergebnis {sensor_item.expected_result}." + Environment.NewLine + Environment.NewLine + + $"Der Ping-Check von {sensor_item.ping_address} ergab einen Treffer fΓΌr das erwartete Ergebnis {ping_result}." + Environment.NewLine + Environment.NewLine + "Sensor Name: " + sensor_item.name + Environment.NewLine + "Beschreibung: " + sensor_item.description + Environment.NewLine + - "Typ: Windows Eventlog" + Environment.NewLine + + "Typ: Ping" + Environment.NewLine + "Uhrzeit: " + DateTime.Now + Environment.NewLine + - "Eventlog: " + sensor_item.eventlog + Environment.NewLine + - "Event ID: " + sensor_item.eventlog_event_id + Environment.NewLine + - "Erwartetes Ergebnis: " + sensor_item.expected_result + Environment.NewLine + - "Inhalt: " + content + Environment.NewLine + - "Level: " + eventRecord.Level + Environment.NewLine + - "Prozess ID: " + eventRecord.ProcessId + Environment.NewLine + - "Erstellt: " + eventRecord.TimeCreated + Environment.NewLine + - "Benutzer ID: " + eventRecord.UserId + Environment.NewLine + - "Version: " + eventRecord.Version + Environment.NewLine + + "Adresse: " + sensor_item.ping_address + Environment.NewLine + + "Timeout: " + sensor_item.ping_timeout + Environment.NewLine + + "Ergebnis: " + ping_result + Environment.NewLine + "Ergebnis der Aktion: " + Environment.NewLine + action_result; } - - // Create notification history if not exists - if (String.IsNullOrEmpty(sensor_item.notification_history)) - { - List notification_history_list = new List - { - details - }; - - sensor_item.notification_history = JsonSerializer.Serialize(notification_history_list); - } - else // if exists, add the result to the list - { - List notification_history_list = JsonSerializer.Deserialize>(sensor_item.notification_history); - notification_history_list.Add(details); - sensor_item.notification_history = JsonSerializer.Serialize(notification_history_list); - } - - //Trigger incident response | NetLock legacy code - /*if (sensor["incident_response_ruleset"].ToString() != "LQ==") - { - //Trigger it - Incident_Response.Handler.Get_Incident_Response_Ruleset(sensor["incident_response_ruleset"].ToString()); - }*/ - } - } - } - catch (Exception ex) - { - Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Check event log existence (" + sensor_item.eventlog + ")", event_log_existing.ToString() + " error: " + ex.ToString()); - } - } - else if (sensor_item.category == 2 || sensor_item.category == 5 || sensor_item.category == 6) // PowerShell, Linux Bash or MacOS Zsh - { - string result = "-"; - - if (sensor_item.category == 2) - result = Windows.Helper.PowerShell.Execute_Script("Sensors.Time_Scheduler.Check_Execution (execute action) " + sensor_item.name, sensor_item.script); - else if (sensor_item.category == 5) - result = Linux.Helper.Bash.Execute_Script("Sensors.Time_Scheduler.Check_Execution (execute action) " + sensor_item.name, true, sensor_item.script); - else if (sensor_item.category == 6) - result = MacOS.Helper.Zsh.Execute_Script("Sensors.Time_Scheduler.Check_Execution (execute action) " + sensor_item.name, true, sensor_item.script); - - if (Regex.IsMatch(result, sensor_item.expected_result)) - { - triggered = true; - - // if action treshold is reached, execute the action and reset the counter - if (sensor_item.action_treshold_count >= sensor_item.action_treshold_max) - { - if (OperatingSystem.IsWindows()) - action_result += Environment.NewLine + Environment.NewLine + " [" + DateTime.Now.ToString() + "]" + Environment.NewLine + Windows.Helper.PowerShell.Execute_Script("Sensors.Time_Scheduler.Check_Execution (execute action) " + sensor_item.name, sensor_item.script_action); - else if (OperatingSystem.IsLinux()) - action_result += Environment.NewLine + Environment.NewLine + " [" + DateTime.Now.ToString() + "]" + Environment.NewLine + Linux.Helper.Bash.Execute_Script("Sensors.Time_Scheduler.Check_Execution (execute action) " + sensor_item.name, true, sensor_item.script_action); - else if (OperatingSystem.IsMacOS()) - action_result += Environment.NewLine + Environment.NewLine + " [" + DateTime.Now.ToString() + "]" + Environment.NewLine + MacOS.Helper.Zsh.Execute_Script("Sensors.Time_Scheduler.Check_Execution (execute action) " + sensor_item.name, true, sensor_item.script_action); - - // Create action history if not exists - if (String.IsNullOrEmpty(sensor_item.action_history)) - { - List action_history_list = new List - { - action_result - }; - - sensor_item.action_history = JsonSerializer.Serialize(action_history_list); - } - else // if exists, add the result to the list - { - List action_history_list = JsonSerializer.Deserialize>(sensor_item.action_history); - action_history_list.Add(action_result); - sensor_item.action_history = JsonSerializer.Serialize(action_history_list); } - - // Reset the counter - sensor_item.action_treshold_count = 0; - string action_treshold_updated_sensor_json = JsonSerializer.Serialize(sensor_item); - File.WriteAllText(sensor, action_treshold_updated_sensor_json); - } - else // if not, increment the counter - { - sensor_item.action_treshold_count++; - string action_treshold_updated_sensor_json = JsonSerializer.Serialize(sensor_item); - File.WriteAllText(sensor, action_treshold_updated_sensor_json); } - string details_type_specification = String.Empty; + // Execution finished, set last run time + endTime = DateTime.Now; // set end time for the next scan - if (sensor_item.category == 2) - details_type_specification = "PowerShell"; - else if (sensor_item.category == 5) - details_type_specification = "Bash"; - else if (sensor_item.category == 6) - details_type_specification = "Zsh"; + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Sensor executed", "name: " + sensor_item.name + " id: " + sensor_item.id); - // Create event - if (Configuration.Agent.language == "en-US") - { - details = - $"The script execution of {sensor_item.name} resulted in a hit for the expected result {sensor_item.expected_result}." + Environment.NewLine + Environment.NewLine + - "Sensor name: " + sensor_item.name + Environment.NewLine + - "Description: " + sensor_item.description + Environment.NewLine + - $"Type: {details_type_specification}" + Environment.NewLine + - "Time: " + DateTime.Now + Environment.NewLine + - "Script: " + sensor_item.script + Environment.NewLine + - "Pattern: " + sensor_item.expected_result + Environment.NewLine + - "Result: " + result + Environment.NewLine + - "Action result: " + Environment.NewLine + action_result; - } - else if (Configuration.Agent.language == "de-DE") + // Build additional details string + foreach (var process in process_information_list) { - details = - $"Die SkriptausfΓΌhrung von {sensor_item.name} ergab einen Treffer fΓΌr das erwartete Ergebnis {sensor_item.expected_result}." + Environment.NewLine + Environment.NewLine + - "Sensor Name: " + sensor_item.name + Environment.NewLine + - "Beschreibung: " + sensor_item.description + Environment.NewLine + - $"Typ: {details_type_specification}" + Environment.NewLine + - "Uhrzeit: " + DateTime.Now + Environment.NewLine + - "Skript: " + sensor_item.script + Environment.NewLine + - "Pattern: " + sensor_item.expected_result + Environment.NewLine + - "Ergebnis: " + result + Environment.NewLine + - "Ergebnis der Aktion: " + Environment.NewLine + action_result; + if (Configuration.Agent.language == "en-US") + additional_details += Environment.NewLine + "Process ID: " + process.id + Environment.NewLine + "Process name: " + process.name + Environment.NewLine + "Usage: " + process.cpu + " (%)" + Environment.NewLine + "RAM usage: " + process.ram + " (MB)" + Environment.NewLine + "User: " + process.user + Environment.NewLine + "Created: " + process.created + Environment.NewLine + "Path: " + process.path + Environment.NewLine + "Commandline: " + process.cmd + Environment.NewLine; + else if (Configuration.Agent.language == "de-DE") + additional_details += Environment.NewLine + "Prozess ID: " + process.id + Environment.NewLine + "Prozess Name: " + process.name + Environment.NewLine + "Nutzung: " + process.cpu + " (%)" + Environment.NewLine + "RAM Nutzung: " + process.ram + " (MB)" + Environment.NewLine + "Benutzer: " + process.user + Environment.NewLine + "Erstellt: " + process.created + Environment.NewLine + "Pfad: " + process.path + Environment.NewLine + "Commandline: " + process.cmd + Environment.NewLine; } - // Create notification history if not exists - if (String.IsNullOrEmpty(sensor_item.notification_history)) + // Insert event + // Check already_notified only if suppress_notification is enabled + + // Debug logging + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Notification decision", + $"Sensor: {sensor_item.name}, triggered: {triggered}, already_notified: {sensor_item.already_notified}, suppress_notification: {sensor_item.suppress_notification}, resolved_notification: {sensor_item.resolved_notification}"); + + if (triggered) { - List notification_history_list = new List + bool should_notify = !sensor_item.suppress_notification || !sensor_item.already_notified; + + if (should_notify) { - details - }; - - sensor_item.notification_history = JsonSerializer.Serialize(notification_history_list); - } - else // if exists, add the result to the list - { - List notification_history_list = JsonSerializer.Deserialize>(sensor_item.notification_history); - notification_history_list.Add(details); - sensor_item.notification_history = JsonSerializer.Serialize(notification_history_list); - } - } - } - else if (sensor_item.category == 3) // Service - { - bool service_start_failed = false; - string service_error_message = String.Empty; - string service_status = String.Empty; + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Triggered (id)", triggered.ToString() + " (" + sensor_item.id + ")"); + + // Check if job description is empty + if (String.IsNullOrEmpty(sensor_item.description) && Configuration.Agent.language == "en-US") + sensor_item.description = "No description"; + else if (String.IsNullOrEmpty(sensor_item.description) && Configuration.Agent.language == "de-DE") + sensor_item.description = "Keine Beschreibung"; + + // if notification treshold is reached, insert event and reset the counter + if (sensor_item.notification_treshold_count >= sensor_item.notification_treshold_max) + { + // Create action history, if treshold is not 1 + if (sensor_item.notification_treshold_max != 1) + { + List action_history_list = JsonSerializer.Deserialize>(sensor_item.action_history); - try - { - if (OperatingSystem.IsWindows()) - { - ServiceController sc = new ServiceController(sensor_item.service_name); - service_status = sc.Status.Equals(ServiceControllerStatus.Paused).ToString(); + foreach (var action_history_item in action_history_list) + action_history += Environment.NewLine + action_history_item + Environment.NewLine; - if (sensor_item.service_condition == 0 && sc.Status.Equals(ServiceControllerStatus.Running)) // if service is running and condition is 0 = running - { - Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Service is running", sensor_item.service_name + " " + sc.Status.ToString()); + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "action_history", action_history + " (" + sensor_item.id + ")"); - triggered = true; + // Clear action history + action_history_list.Clear(); + sensor_item.action_history = null; + WriteEncryptedSensor(sensor, sensor_item); + } - if (sensor_item.service_action == 1) // stop the service if it's running and the action is 1 = stop - sc.Stop(); - } - else if (sensor_item.service_condition == 1 && sc.Status.Equals(ServiceControllerStatus.Paused)) // if service is paused and condition is 1 = paused - { - Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Service is paused", sensor_item.service_name + " " + sc.Status.ToString()); + // Create notification_json + Notifications notifications = new Notifications + { + mail = sensor_item.notifications_mail, + microsoft_teams = sensor_item.notifications_microsoft_teams, + telegram = sensor_item.notifications_telegram, + ntfy_sh = sensor_item.notifications_ntfy_sh + }; - triggered = true; - - if (sensor_item.service_action == 2) // restart the service if it's paused and the action is 2 = restart - { - Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Service is paused, restarting", sensor_item.service_name + " " + sc.Status.ToString()); - - sc.Stop(); - sc.WaitForStatus(ServiceControllerStatus.Stopped); - sc.Start(); - } - } - else if (sensor_item.service_condition == 2 && sc.Status.Equals(ServiceControllerStatus.Stopped)) // if service is stopped and condition is 2 = stopped - { - Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Service is stopped", sensor_item.service_name + " " + sc.Status.ToString()); - - triggered = true; - - if (sensor_item.service_action == 0) // start the service if it's stopped and the action is 0 = start - sc.Start(); - } - } - else if (OperatingSystem.IsLinux()) // marked for refactoring for cleaner code - { - sensor_item.service_name = sensor_item.service_name.Replace(".service", ""); // remove .service from the service name - string serviceCommand = $"systemctl list-units --type=service --all | grep -w {sensor_item.service_name}.service"; - - // Execute bash script and save the output - string output = Linux.Helper.Bash.Execute_Script("Service Sensor", false, serviceCommand); - - if (string.IsNullOrWhiteSpace(output)) - { - //Console.WriteLine($"Service {sensor_item.service_name} not found or no output received."); - continue; - } - - bool isServiceRunning = false; - - // Check status - // Use Regex to match running status - var match = Regex.Match(output, $@"running", RegexOptions.Multiline); - - if (match.Success) - isServiceRunning = true; - - Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Service status", sensor_item.service_name + " " + isServiceRunning.ToString()); - - if (sensor_item.service_condition == 0 && isServiceRunning) // if service is running and condition is 0 = running - { - Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Service is running", sensor_item.service_name + " " + isServiceRunning.ToString()); - - triggered = true; + // Serializing the extracted properties to JSON + string notifications_json = JsonSerializer.Serialize(notifications, new JsonSerializerOptions { WriteIndented = true }); + + if (sensor_item.category == 0) //utilization + { + if (sensor_item.sub_category == 0) + { + // CPU usage + if (Configuration.Agent.language == "en-US") + Events.Logger.Insert_Event(sensor_item.severity.ToString(), "Sensor", "Sensor CPU (" + sensor_item.name + ")", details + Environment.NewLine + Environment.NewLine + "History of actions" + Environment.NewLine + action_history, notifications_json, 2, 0); // type is 2 = sensor + else if (Configuration.Agent.language == "de-DE") + Events.Logger.Insert_Event(sensor_item.severity.ToString(), "Sensor", "Sensor CPU (" + sensor_item.name + ")", details + Environment.NewLine + Environment.NewLine + "Historie der Aktionen" + Environment.NewLine + action_history, notifications_json, 2, 1); + } + else if (sensor_item.sub_category == 1) // RAM usage + { + if (Configuration.Agent.language == "en-US") + Events.Logger.Insert_Event(sensor_item.severity.ToString(), "Sensor", "Sensor RAM (" + sensor_item.name + ")", details + Environment.NewLine + Environment.NewLine + "History of actions" + Environment.NewLine + action_history, notifications_json, 2, 0); + else if (Configuration.Agent.language == "de-DE") + Events.Logger.Insert_Event(sensor_item.severity.ToString(), "Sensor", "Sensor RAM (" + sensor_item.name + ")", details + Environment.NewLine + Environment.NewLine + "Historie der Aktionen" + Environment.NewLine + action_history, notifications_json, 2, 1); + } + else if (sensor_item.sub_category == 2) // Disks + { + if (Configuration.Agent.language == "en-US") + Events.Logger.Insert_Event(sensor_item.severity.ToString(), "Sensor", "Sensor Drive (" + sensor_item.name + ")", details + Environment.NewLine + Environment.NewLine + "History of actions" + Environment.NewLine + action_history, notifications_json, 2, 0); + else if (Configuration.Agent.language == "de-DE") + Events.Logger.Insert_Event(sensor_item.severity.ToString(), "Sensor", "Sensor Laufwerk (" + sensor_item.name + ")", details + Environment.NewLine + Environment.NewLine + "Historie der Aktionen" + Environment.NewLine + action_history, notifications_json, 2, 1); + } + else if (sensor_item.sub_category == 3) // CPU process usage + { + if (Configuration.Agent.language == "en-US") + Events.Logger.Insert_Event(sensor_item.severity.ToString(), "Sensor", "Sensor Process CPU usage (" + sensor_item.name + ")", details + Environment.NewLine + Environment.NewLine + "Further information" + additional_details + Environment.NewLine + Environment.NewLine + "History of actions" + Environment.NewLine + action_history, notifications_json, 2, 0); + else if (Configuration.Agent.language == "de-DE") + Events.Logger.Insert_Event(sensor_item.severity.ToString(), "Sensor", "Sensor Prozess-CPU-Nutzung (" + sensor_item.name + ")", details + Environment.NewLine + Environment.NewLine + "Weitere Informationen" + additional_details + Environment.NewLine + Environment.NewLine + "Historie der Aktionen" + Environment.NewLine + action_history, notifications_json, 2, 1); + } + else if (sensor_item.sub_category == 4) // RAM process usage in % + { + if (Configuration.Agent.language == "en-US") + Events.Logger.Insert_Event(sensor_item.severity.ToString(), "Sensor", "Sensor Process RAM usage (%) (" + sensor_item.name + ")", details + Environment.NewLine + Environment.NewLine + "Further information" + additional_details + Environment.NewLine + Environment.NewLine + "History of actions" + Environment.NewLine + action_history, notifications_json, 2, 0); + else if (Configuration.Agent.language == "de-DE") + Events.Logger.Insert_Event(sensor_item.severity.ToString(), "Sensor", "Sensor Prozess-RAM-Nutzung (%) (" + sensor_item.name + ")", details + Environment.NewLine + Environment.NewLine + "Weitere Informationen" + additional_details + Environment.NewLine + Environment.NewLine + "Historie der Aktionen" + Environment.NewLine + action_history, notifications_json, 2, 1); + } + else if (sensor_item.sub_category == 5) // RAM process usage in MB + { + if (Configuration.Agent.language == "en-US") + Events.Logger.Insert_Event(sensor_item.severity.ToString(), "Sensor", "Sensor Process RAM usage (MB) (" + sensor_item.name + ")", details + Environment.NewLine + Environment.NewLine + "Further information" + additional_details + Environment.NewLine + Environment.NewLine + "History of actions" + Environment.NewLine + action_history, notifications_json, 2, 0); + else if (Configuration.Agent.language == "de-DE") + Events.Logger.Insert_Event(sensor_item.severity.ToString(), "Sensor", "Sensor Prozess-RAM-Nutzung (MB) (" + sensor_item.name + ")", details + Environment.NewLine + Environment.NewLine + "Weitere Informationen" + additional_details + Environment.NewLine + Environment.NewLine + "Historie der Aktionen" + Environment.NewLine + action_history, notifications_json, 2, 1); + } + } + else if (sensor_item.category == 1) // Windows Eventlog + { + if (Configuration.Agent.language == "en-US") + Events.Logger.Insert_Event(sensor_item.severity.ToString(), "Sensor", "Windows Eventlog (" + sensor_item.name + ")", details + Environment.NewLine + Environment.NewLine + "History of actions" + Environment.NewLine + action_history, notifications_json, 2, 0); + else if (Configuration.Agent.language == "de-DE") + Events.Logger.Insert_Event(sensor_item.severity.ToString(), "Sensor", "Windows Eventlog (" + sensor_item.name + ")", details + Environment.NewLine + Environment.NewLine + "Historie der Aktionen" + Environment.NewLine + action_history, notifications_json, 2, 1); + } + else if (sensor_item.category == 2) // PowerShell + { + if (Configuration.Agent.language == "en-US") + Events.Logger.Insert_Event(sensor_item.severity.ToString(), "Sensor", "PowerShell (" + sensor_item.name + ")", details + Environment.NewLine + Environment.NewLine + "History of actions" + Environment.NewLine + action_history, notifications_json, 2, 0); + else if (Configuration.Agent.language == "de-DE") + Events.Logger.Insert_Event(sensor_item.severity.ToString(), "Sensor", "PowerShell (" + sensor_item.name + ")", details + Environment.NewLine + Environment.NewLine + "Historie der Aktionen" + Environment.NewLine + action_history, notifications_json, 2, 1); + } + else if (sensor_item.category == 3) // Service + { + if (Configuration.Agent.language == "en-US") + Events.Logger.Insert_Event(sensor_item.severity.ToString(), "Sensor", "Service (" + sensor_item.name + ")", details + Environment.NewLine + Environment.NewLine + "History of actions" + Environment.NewLine + action_history, notifications_json, 2, 0); + else if (Configuration.Agent.language == "de-DE") + Events.Logger.Insert_Event(sensor_item.severity.ToString(), "Sensor", "Dienst (" + sensor_item.name + ")", details + Environment.NewLine + Environment.NewLine + "Historie der Aktionen" + Environment.NewLine + action_history, notifications_json, 2, 1); + } + else if (sensor_item.category == 4) // Ping + { + if (Configuration.Agent.language == "en-US") + Events.Logger.Insert_Event(sensor_item.severity.ToString(), "Sensor", "Ping (" + sensor_item.name + ")", details + Environment.NewLine + Environment.NewLine + "History of actions" + Environment.NewLine + action_history, notifications_json, 2, 0); + else if (Configuration.Agent.language == "de-DE") + Events.Logger.Insert_Event(sensor_item.severity.ToString(), "Sensor", "Ping (" + sensor_item.name + ")", details + Environment.NewLine + Environment.NewLine + "Historie der Aktionen" + Environment.NewLine + action_history, notifications_json, 2, 1); + } + else if (sensor_item.category == 5) + { + if (Configuration.Agent.language == "en-US") + Events.Logger.Insert_Event(sensor_item.severity.ToString(), "Sensor", "Bash (" + sensor_item.name + ")", details + Environment.NewLine + Environment.NewLine + "History of actions" + Environment.NewLine + action_history, notifications_json, 2, 0); + else if (Configuration.Agent.language == "de-DE") + Events.Logger.Insert_Event(sensor_item.severity.ToString(), "Sensor", "Bash (" + sensor_item.name + ")", details + Environment.NewLine + Environment.NewLine + "Historie der Aktionen" + Environment.NewLine + action_history, notifications_json, 2, 1); + } + else if (sensor_item.category == 6) + { + if (Configuration.Agent.language == "en-US") + Events.Logger.Insert_Event(sensor_item.severity.ToString(), "Sensor", "Zsh (" + sensor_item.name + ")", details + Environment.NewLine + Environment.NewLine + "History of actions" + Environment.NewLine + action_history, notifications_json, 2, 0); + else if (Configuration.Agent.language == "de-DE") + Events.Logger.Insert_Event(sensor_item.severity.ToString(), "Sensor", "Zsh (" + sensor_item.name + ")", details + Environment.NewLine + Environment.NewLine + "Historie der Aktionen" + Environment.NewLine + action_history, notifications_json, 2, 1); + } - if (sensor_item.service_action == 1) // stop the service if it's running and the action is 1 = stop - { - Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Service is running, stopping", sensor_item.service_name + " " + isServiceRunning.ToString()); + // Mark as notified to prevent spam (only after actually sending the event) + AddNotificationIfNeeded(sensor_item, sensor, details); - // Stoppe den Dienst - Linux.Helper.Bash.Execute_Script("", false, $"systemctl stop {sensor_item.service_name}"); + sensor_item.notification_treshold_count = 0; + WriteEncryptedSensor(sensor, sensor_item); } - } - else if (sensor_item.service_condition == 1 && !isServiceRunning) // if service is stopped and condition is 1 = paused (simuliert als gestoppt) - { - Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Service is stopped", sensor_item.service_name + " " + isServiceRunning.ToString()); - - triggered = true; - - if (sensor_item.service_action == 2) // restart the service if it's stopped and the action is 2 = restart + else // if not, increment the counter { - Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Service is stopped, restarting", sensor_item.service_name + " " + isServiceRunning.ToString()); - - // Starte den Dienst - Linux.Helper.Bash.Execute_Script("Sensor", false, $"systemctl start {sensor_item.service_name}"); + sensor_item.notification_treshold_count++; + WriteEncryptedSensor(sensor, sensor_item); } - } - else if (sensor_item.service_condition == 2 && !isServiceRunning) // if service is stopped and condition is 2 = stopped + } // end of should_notify + else { - Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Service is stopped", sensor_item.service_name + " " + isServiceRunning.ToString()); - - triggered = true; - - if (sensor_item.service_action == 0) // start the service if it's stopped and the action is 0 = start - { - Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Service is stopped, starting", sensor_item.service_name + " " + isServiceRunning.ToString()); - - // Starte den Dienst - Linux.Helper.Bash.Execute_Script("Sensor", false, $"systemctl start {sensor_item.service_name}"); - } + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Sensor triggered but notification suppressed", $"name: {sensor_item.name}, id: {sensor_item.id}, category: {sensor_item.category}, sub_category: {sensor_item.sub_category}"); } - } - else if (OperatingSystem.IsMacOS()) // Currently only supports system wide services + } // end of triggered + else // not triggered - sensor is now resolved { - string output = MacOS.Helper.Zsh.Execute_Script("Service Sensor", false, $"launchctl list | grep {sensor_item.service_name}"); - - // Regex, um nur die Zeile fΓΌr den spezifischen Dienst zu extrahieren - string pattern = $@"^\S+\s+\S+\s+{sensor_item.service_name}$"; - var match = Regex.Match(output, pattern, RegexOptions.Multiline); - - bool isServiceRunning = false; - - if (match.Success) - { - // Extrahiere den PID-Wert oder das `-` am Anfang der Zeile - string[] parts = match.Value.Split(new[] { '\t' }, StringSplitOptions.RemoveEmptyEntries); - isServiceRunning = parts[0] != "-"; // "-" bedeutet, der Dienst lΓ€uft nicht - } + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Sensor not triggered", $"name: {sensor_item.name}, id: {sensor_item.id}, category: {sensor_item.category}, sub_category: {sensor_item.sub_category}"); - Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Service status", sensor_item.service_name + " " + isServiceRunning.ToString()); + // Send resolved notification if needed (before resetting flags) + SendResolvedNotificationIfNeeded(sensor_item, sensor); - if (sensor_item.service_condition == 0 && isServiceRunning) // Wenn der Dienst lΓ€uft und die Bedingung 0 ist (sollte laufen) - { - Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Service is running", sensor_item.service_name + " " + isServiceRunning.ToString()); - - triggered = true; + // Reset notification flag if sensor is no longer triggered (problem resolved) + ResetNotificationFlagIfNeeded(sensor_item, sensor); - if (sensor_item.service_action == 1) // Stoppe den Dienst, wenn die Aktion 1 ist (soll gestoppt werden) - { - Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Service is running, stopping", sensor_item.service_name + " " + isServiceRunning.ToString()); - - // Stoppe den Dienst - MacOS.Helper.Zsh.Execute_Script("Service Sensor", false, $"launchctl stop {sensor_item.service_name}.plist"); - } - } - else if (sensor_item.service_condition == 1 && !isServiceRunning) // Wenn der Dienst gestoppt ist und die Bedingung 1 ist (simuliert als pausiert) - { - Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Service is stopped", sensor_item.service_name + " " + isServiceRunning.ToString()); - - triggered = true; - - if (sensor_item.service_action == 2) // Starte den Dienst neu, wenn die Aktion 2 ist - { - Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Service is stopped, restarting", sensor_item.service_name + " " + isServiceRunning.ToString()); - - // Starte den Dienst neu - MacOS.Helper.Zsh.Execute_Script("Service Sensor", false, $"launchctl stop {sensor_item.service_name}.plist"); - MacOS.Helper.Zsh.Execute_Script("Service Sensor", false, $"launchctl start {sensor_item.service_name}.plist"); - } - } - else if (sensor_item.service_condition == 2 && !isServiceRunning) // Wenn der Dienst gestoppt ist und die Bedingung 2 ist (sollte gestoppt sein) + // if auto reset is enabled, reset the counters + if (sensor_item.auto_reset) { - Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Service is stopped", sensor_item.service_name + " " + isServiceRunning.ToString()); + // Reset notification counter + sensor_item.notification_treshold_count = 0; + WriteEncryptedSensor(sensor, sensor_item); - triggered = true; - - if (sensor_item.service_action == 0) // Starte den Dienst, wenn die Aktion 0 ist (soll gestartet werden) - { - Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Service is stopped, starting", sensor_item.service_name + " " + isServiceRunning.ToString()); - - // Starte den Dienst - MacOS.Helper.Zsh.Execute_Script("Service Sensor", false, $"launchctl start {sensor_item.service_name}.plist"); - } - } - } - } - catch (Exception ex) - { - service_start_failed = true; - service_error_message = ex.Message; - Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Checking service state, or performing action failed", ex.ToString()); - } - - if (triggered) - { - // if action treshold is reached, execute the action and reset the counter - if (sensor_item.action_treshold_count >= sensor_item.action_treshold_max) - { - if (OperatingSystem.IsWindows()) - action_result += Environment.NewLine + Environment.NewLine + " [" + DateTime.Now.ToString() + "]" + Environment.NewLine + Windows.Helper.PowerShell.Execute_Script("Sensors.Time_Scheduler.Check_Execution (execute action) " + sensor_item.name, sensor_item.script_action); - else if (OperatingSystem.IsLinux()) - action_result += Environment.NewLine + Environment.NewLine + " [" + DateTime.Now.ToString() + "]" + Environment.NewLine + Linux.Helper.Bash.Execute_Script("Sensors.Time_Scheduler.Check_Execution (execute action) " + sensor_item.name, true, sensor_item.script_action); - else if (OperatingSystem.IsMacOS()) - action_result += Environment.NewLine + Environment.NewLine + " [" + DateTime.Now.ToString() + "]" + Environment.NewLine + MacOS.Helper.Zsh.Execute_Script("Sensors.Time_Scheduler.Check_Execution (execute action) " + sensor_item.name, true, sensor_item.script_action); - - // Create action history if not exists - if (String.IsNullOrEmpty(sensor_item.action_history)) - { - List action_history_list = new List - { - action_result - }; - - sensor_item.action_history = JsonSerializer.Serialize(action_history_list); - } - else // if exists, add the result to the list - { - List action_history_list = JsonSerializer.Deserialize>(sensor_item.action_history); - action_history_list.Add(action_result); - sensor_item.action_history = JsonSerializer.Serialize(action_history_list); - } - - // Reset the counter - sensor_item.action_treshold_count = 0; - string action_treshold_updated_sensor_json = JsonSerializer.Serialize(sensor_item); - File.WriteAllText(sensor, action_treshold_updated_sensor_json); - } - else // if not, increment the counter - { - sensor_item.action_treshold_count++; - string action_treshold_updated_sensor_json = JsonSerializer.Serialize(sensor_item); - File.WriteAllText(sensor, action_treshold_updated_sensor_json); - } - - // Create event - if (Configuration.Agent.language == "en-US") - { - // Convert the service condition to human readable text - string service_condition = String.Empty; - - if (sensor_item.service_condition == 0) - service_condition = "running"; - else if (sensor_item.service_condition == 1) - service_condition = "paused"; - else if (sensor_item.service_condition == 2) - service_condition = "stopped"; - - // Convert the service action to human readable text - string service_action = String.Empty; - - if (sensor_item.service_action == 0) - service_action = "start"; - else if (sensor_item.service_action == 1) - service_action = "stop"; - else if (sensor_item.service_action == 2) - service_action = "restart"; - - if (service_start_failed) - { - details = - $"The service {sensor_item.service_name} was {service_condition}. The service action ({service_action}) could not be performed." + Environment.NewLine + Environment.NewLine + - "Sensor name: " + sensor_item.name + Environment.NewLine + - "Description: " + sensor_item.description + Environment.NewLine + - "Type: Service" + Environment.NewLine + - "Time: " + DateTime.Now + Environment.NewLine + - "Service: " + sensor_item.service_name + Environment.NewLine + - "Result: The requested service action could not be performed." + Environment.NewLine + - "Error: " + service_error_message + Environment.NewLine + - "Action result: " + Environment.NewLine + action_result; - } - else - { - details = - $"The service {sensor_item.service_name} was {service_condition}. The service action ({service_action}) was successfully executed." + Environment.NewLine + Environment.NewLine + - "Sensor name: " + sensor_item.name + Environment.NewLine + - "Description: " + sensor_item.description + Environment.NewLine + - "Type: Service" + Environment.NewLine + - "Time: " + DateTime.Now + Environment.NewLine + - "Service: " + sensor_item.service_name + Environment.NewLine + - "Result: The requested service action was successfully executed." + Environment.NewLine + - "Action result: " + Environment.NewLine + action_result; - } - } - else if (Configuration.Agent.language == "de-DE") - { - // Convert the service condition to human readable text - string service_condition = String.Empty; - - if (sensor_item.service_condition == 0) - service_condition = "lΓ€uft"; - else if (sensor_item.service_condition == 1) - service_condition = "pausiert"; - else if (sensor_item.service_condition == 2) - service_condition = "gestoppt"; - - // Convert the service action to human readable text - string service_action = String.Empty; - - if (sensor_item.service_action == 0) - service_action = "starten"; - else if (sensor_item.service_action == 1) - service_action = "stoppen"; - else if (sensor_item.service_action == 2) - service_action = "neu starten"; - - if (service_start_failed) - { - details = - $"Der Dienst {sensor_item.service_name} war {service_condition}. Die Dienstaktion ({service_action}) konnte nicht ausgefΓΌhrt werden." + Environment.NewLine + - "Sensor Name: " + sensor_item.name + Environment.NewLine + - "Beschreibung: " + sensor_item.description + Environment.NewLine + - "Typ: Dienst" + Environment.NewLine + - "Uhrzeit: " + DateTime.Now + Environment.NewLine + - "Dienst: " + sensor_item.service_name + Environment.NewLine + - "Ergebnis: The requested service action could not be performed." + Environment.NewLine + - "Fehler: " + service_error_message + Environment.NewLine + - "Ergebnis der Aktion: " + Environment.NewLine + action_result; - } - else - { - details = - $"Der Dienst {sensor_item.service_name} war {service_condition}. Die Dienstaktion ({service_action}) wurde erfolgreich ausgefΓΌhrt." + Environment.NewLine + - "Sensor Name: " + sensor_item.name + Environment.NewLine + - "Beschreibung: " + sensor_item.description + Environment.NewLine + - "Typ: Dienst" + Environment.NewLine + - "Uhrzeit: " + DateTime.Now + Environment.NewLine + - "Dienst: " + sensor_item.service_name + Environment.NewLine + - "Ergebnis: Die gewΓΌnschte Dienst Aktion wurde erfolgreich ausgefΓΌhrt." + Environment.NewLine + - "Ergebnis der Aktion: " + Environment.NewLine + action_result; + // Reset action counter + sensor_item.action_treshold_count = 0; + WriteEncryptedSensor(sensor, sensor_item); } } - // Create notification history if not exists - if (String.IsNullOrEmpty(sensor_item.notification_history)) - { - List notification_history_list = new List - { - details - }; - - sensor_item.notification_history = JsonSerializer.Serialize(notification_history_list); - } - else // if exists, add the result to the list - { - List notification_history_list = JsonSerializer.Deserialize>(sensor_item.notification_history); - notification_history_list.Add(details); - sensor_item.notification_history = JsonSerializer.Serialize(notification_history_list); - } + // Sensor execution successful + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Execution finished successfully", "name: " + sensor_item.name + " id: " + sensor_item.id); } - } - else if (sensor_item.category == 4) // Ping - { - bool ping_status = Device_Information.Network.Ping(sensor_item.ping_address, sensor_item.ping_timeout); - - if (ping_status && sensor_item.ping_condition == 0) - triggered = true; - else if (!ping_status && sensor_item.ping_condition == 1) - triggered = true; - - if (triggered) + catch (Exception ex) { - // if action treshold is reached, execute the action and reset the counter - if (sensor_item.action_treshold_count >= sensor_item.action_treshold_max) - { - if (OperatingSystem.IsWindows()) - action_result += Environment.NewLine + Environment.NewLine + " [" + DateTime.Now.ToString() + "]" + Environment.NewLine + Windows.Helper.PowerShell.Execute_Script("Sensors.Time_Scheduler.Check_Execution (execute action) " + sensor_item.name, sensor_item.script_action); - else if (OperatingSystem.IsLinux()) - action_result += Environment.NewLine + Environment.NewLine + " [" + DateTime.Now.ToString() + "]" + Environment.NewLine + Linux.Helper.Bash.Execute_Script("Sensors.Time_Scheduler.Check_Execution (execute action) " + sensor_item.name, true, sensor_item.script_action); - else if (OperatingSystem.IsMacOS()) - action_result += Environment.NewLine + Environment.NewLine + " [" + DateTime.Now.ToString() + "]" + Environment.NewLine + MacOS.Helper.Zsh.Execute_Script("Sensors.Time_Scheduler.Check_Execution (execute action) " + sensor_item.name, true, sensor_item.script_action); - - // Create action history if not exists - if (String.IsNullOrEmpty(sensor_item.action_history)) - { - List action_history_list = new List - { - action_result - }; + // Sensor failed - rollback last_run to allow retry on next scheduler run + sensor_item.last_run = previous_last_run; + WriteEncryptedSensor(sensor, sensor_item); - sensor_item.action_history = JsonSerializer.Serialize(action_history_list); - } - else // if exists, add the result to the list - { - List action_history_list = JsonSerializer.Deserialize>(sensor_item.action_history); - action_history_list.Add(action_result); - sensor_item.action_history = JsonSerializer.Serialize(action_history_list); - } - - // Reset the counter - sensor_item.action_treshold_count = 0; - string action_treshold_updated_sensor_json = JsonSerializer.Serialize(sensor_item); - File.WriteAllText(sensor, action_treshold_updated_sensor_json); - } - else // if not, increment the counter - { - sensor_item.action_treshold_count++; - string action_treshold_updated_sensor_json = JsonSerializer.Serialize(sensor_item); - File.WriteAllText(sensor, action_treshold_updated_sensor_json); - } - - // Create event - string ping_result = String.Empty; + Logging.Error("Sensors.Time_Scheduler.Check_Execution", "Sensor execution failed (rolled back last_run)", "name: " + sensor_item.name + " id: " + sensor_item.id + " error: " + ex.ToString()); + // Insert error event if (Configuration.Agent.language == "en-US") - { - if (sensor_item.ping_condition == 0) - ping_result = "Successful"; - else if (sensor_item.ping_condition == 1) - ping_result = "Failed"; - - details = - $"The ping check of {sensor_item.ping_address} resulted in a hit for the expected result {ping_result}." + Environment.NewLine + Environment.NewLine + - "Sensor name: " + sensor_item.name + Environment.NewLine + - "Description: " + sensor_item.description + Environment.NewLine + - "Type: Ping" + Environment.NewLine + - "Time: " + DateTime.Now + Environment.NewLine + - "Address: " + sensor_item.ping_address + Environment.NewLine + - "Timeout: " + sensor_item.ping_timeout + Environment.NewLine + - "Result: " + ping_result + Environment.NewLine + - "Action result: " + Environment.NewLine + action_result; - } + Events.Logger.Insert_Event("2", "Sensor", sensor_item.name + " failed", "Sensor: " + sensor_item.name + " (" + sensor_item.description + ") " + Environment.NewLine + Environment.NewLine + "Error: " + Environment.NewLine + ex.ToString(), String.Empty, 1, 0); else if (Configuration.Agent.language == "de-DE") + Events.Logger.Insert_Event("2", "Sensor", sensor_item.name + " fehlgeschlagen", "Sensor: " + sensor_item.name + " (" + sensor_item.description + ") " + Environment.NewLine + Environment.NewLine + "Fehler: " + Environment.NewLine + ex.ToString(), String.Empty, 1, 1); + + // Delete corrupted sensor file if exists + string sensor_path = Path.Combine(Application_Paths.program_data_sensors, sensor_item.id + ".json"); + + if (File.Exists(sensor_path)) { - if (sensor_item.ping_condition == 0) - ping_result = "Erfolgreich"; - else if (sensor_item.ping_condition == 1) - ping_result = "Fehlgeschlagen"; - - details = - $"Der Ping-Check von {sensor_item.ping_address} ergab einen Treffer fΓΌr das erwartete Ergebnis {ping_result}." + Environment.NewLine + Environment.NewLine + - "Sensor Name: " + sensor_item.name + Environment.NewLine + - "Beschreibung: " + sensor_item.description + Environment.NewLine + - "Typ: Ping" + Environment.NewLine + - "Uhrzeit: " + DateTime.Now + Environment.NewLine + - "Adresse: " + sensor_item.ping_address + Environment.NewLine + - "Timeout: " + sensor_item.ping_timeout + Environment.NewLine + - "Ergebnis: " + ping_result + Environment.NewLine + - "Ergebnis der Aktion: " + Environment.NewLine + action_result; - } - - // Create notification history if not exists - if (String.IsNullOrEmpty(sensor_item.notification_history)) - { - List notification_history_list = new List - { - details - }; - - sensor_item.notification_history = JsonSerializer.Serialize(notification_history_list); - } - else // if exists, add the result to the list - { - List notification_history_list = JsonSerializer.Deserialize>(sensor_item.notification_history); - notification_history_list.Add(details); - sensor_item.notification_history = JsonSerializer.Serialize(notification_history_list); - } - } - } - - // Execution finished, set last run time - endTime = DateTime.Now; // set end time for the next scan - - Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Sensor executed", "name: " + sensor_item.name + " id: " + sensor_item.id); - - // Build additional details string - foreach (var process in process_information_list) - { - if (Configuration.Agent.language == "en-US") - additional_details += Environment.NewLine + "Process ID: " + process.id + Environment.NewLine + "Process name: " + process.name + Environment.NewLine + "Usage: " + process.cpu + " (%)" + Environment.NewLine + "RAM usage: " + process.ram + " (MB)" + Environment.NewLine + "User: " + process.user + Environment.NewLine + "Created: " + process.created + Environment.NewLine + "Path: " + process.path + Environment.NewLine + "Commandline: " + process.cmd + Environment.NewLine; - else if (Configuration.Agent.language == "de-DE") - additional_details += Environment.NewLine + "Prozess ID: " + process.id + Environment.NewLine + "Prozess Name: " + process.name + Environment.NewLine + "Nutzung: " + process.cpu + " (%)" + Environment.NewLine + "RAM Nutzung: " + process.ram + " (MB)" + Environment.NewLine + "Benutzer: " + process.user + Environment.NewLine + "Erstellt: " + process.created + Environment.NewLine + "Pfad: " + process.path + Environment.NewLine + "Commandline: " + process.cmd + Environment.NewLine; - } - - // Insert event - if (triggered) - { - Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Triggered (id)", triggered.ToString() + " (" + sensor_item.id + ")"); - - // Check if job description is empty - if (String.IsNullOrEmpty(sensor_item.description) && Configuration.Agent.language == "en-US") - sensor_item.description = "No description"; - else if (String.IsNullOrEmpty(sensor_item.description) && Configuration.Agent.language == "de-DE") - sensor_item.description = "Keine Beschreibung"; - - // if notification treshold is reached, insert event and reset the counter - if (sensor_item.notification_treshold_count >= sensor_item.notification_treshold_max) - { - // Create action history, if treshold is not 1 - if (sensor_item.notification_treshold_max != 1) - { - List action_history_list = JsonSerializer.Deserialize>(sensor_item.action_history); - - foreach (var action_history_item in action_history_list) - action_history += Environment.NewLine + action_history_item + Environment.NewLine; - - Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "action_history", action_history + " (" + sensor_item.id + ")"); - - // Clear action history - action_history_list.Clear(); - sensor_item.action_history = null; - string action_history_updated_sensor_json = JsonSerializer.Serialize(sensor_item); - File.WriteAllText(sensor, action_history_updated_sensor_json); - } - - // Create notification history - if (!String.IsNullOrEmpty(sensor_item.notification_history)) - { - List notification_history_list = JsonSerializer.Deserialize>(sensor_item.notification_history); - - foreach (var notification_history_item in notification_history_list) - notification_history += Environment.NewLine + notification_history_item + Environment.NewLine; - - Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "notification_history", notification_history + " (" + sensor_item.id + ")"); - - // Clear notification history - notification_history_list.Clear(); - sensor_item.notification_history = null; - string notification_history_updated_sensor_json = JsonSerializer.Serialize(sensor_item); - File.WriteAllText(sensor, notification_history_updated_sensor_json); - } - - // Create notification_json - Notifications notifications = new Notifications - { - mail = sensor_item.notifications_mail, - microsoft_teams = sensor_item.notifications_microsoft_teams, - telegram = sensor_item.notifications_telegram, - ntfy_sh = sensor_item.notifications_ntfy_sh - }; - - // Serializing the extracted properties to JSON - string notifications_json = JsonSerializer.Serialize(notifications, new JsonSerializerOptions { WriteIndented = true }); - - // Remove all empty characters & lines until the first character - notification_history = notification_history.TrimStart(); - - // Create event based on category and sub category - if (sensor_item.category == 0) //utilization - { - if (sensor_item.sub_category == 0) - { - // CPU usage - if (Configuration.Agent.language == "en-US") - Events.Logger.Insert_Event(sensor_item.severity.ToString(), "Sensor", "Sensor CPU (" + sensor_item.name + ")", notification_history + "History of actions" + Environment.NewLine + action_history, notifications_json, 2, 0); // type is 2 = sensor - else if (Configuration.Agent.language == "de-DE") - Events.Logger.Insert_Event(sensor_item.severity.ToString(), "Sensor", "Sensor CPU (" + sensor_item.name + ")", notification_history + "Historie der Aktionen" + Environment.NewLine + action_history, notifications_json, 2, 1); - } - else if (sensor_item.sub_category == 1) // RAM usage - { - if (Configuration.Agent.language == "en-US") - Events.Logger.Insert_Event(sensor_item.severity.ToString(), "Sensor", "Sensor RAM (" + sensor_item.name + ")", notification_history + "History of actions" + Environment.NewLine + action_history, notifications_json, 2, 0); - else if (Configuration.Agent.language == "de-DE") - Events.Logger.Insert_Event(sensor_item.severity.ToString(), "Sensor", "Sensor RAM (" + sensor_item.name + ")", notification_history + "Historie der Aktionen" + Environment.NewLine + action_history, notifications_json, 2, 1); - } - else if (sensor_item.sub_category == 2) // Disks - { - if (Configuration.Agent.language == "en-US") - Events.Logger.Insert_Event(sensor_item.severity.ToString(), "Sensor", "Sensor Drive (" + sensor_item.name + ")", notification_history + "History of actions" + Environment.NewLine + action_history, notifications_json, 2, 0); - else if (Configuration.Agent.language == "de-DE") - Events.Logger.Insert_Event(sensor_item.severity.ToString(), "Sensor", "Sensor Laufwerk (" + sensor_item.name + ")", notification_history + "Historie der Aktionen" + Environment.NewLine + action_history, notifications_json, 2, 1); - } - else if (sensor_item.sub_category == 3) // CPU process usage - { - if (Configuration.Agent.language == "en-US") - Events.Logger.Insert_Event(sensor_item.severity.ToString(), "Sensor", "Sensor Process CPU usage (" + sensor_item.name + ")", notification_history + Environment.NewLine + Environment.NewLine + "Further information" + additional_details + Environment.NewLine + Environment.NewLine + "History of actions" + Environment.NewLine + action_history, notifications_json, 2, 0); - else if (Configuration.Agent.language == "de-DE") - Events.Logger.Insert_Event(sensor_item.severity.ToString(), "Sensor", "Sensor Prozess-CPU-Nutzung (" + sensor_item.name + ")", notification_history + Environment.NewLine + Environment.NewLine + "Weitere Informationen" + additional_details + Environment.NewLine + Environment.NewLine + "Historie der Aktionen" + Environment.NewLine + action_history, notifications_json, 2, 1); - } - else if (sensor_item.sub_category == 4) // RAM process usage in % - { - if (Configuration.Agent.language == "en-US") - Events.Logger.Insert_Event(sensor_item.severity.ToString(), "Sensor", "Sensor Process RAM usage (%) (" + sensor_item.name + ")", notification_history + Environment.NewLine + Environment.NewLine + "Further information" + additional_details + Environment.NewLine + Environment.NewLine + "History of actions" + Environment.NewLine + action_history, notifications_json, 2, 0); - else if (Configuration.Agent.language == "de-DE") - Events.Logger.Insert_Event(sensor_item.severity.ToString(), "Sensor", "Sensor Prozess-RAM-Nutzung (%) (" + sensor_item.name + ")", notification_history + Environment.NewLine + Environment.NewLine + "Weitere Informationen" + additional_details + Environment.NewLine + Environment.NewLine + "Historie der Aktionen" + Environment.NewLine + action_history, notifications_json, 2, 1); - } - else if (sensor_item.sub_category == 5) // RAM process usage in MB - { - if (Configuration.Agent.language == "en-US") - Events.Logger.Insert_Event(sensor_item.severity.ToString(), "Sensor", "Sensor Process RAM usage (MB) (" + sensor_item.name + ")", notification_history + Environment.NewLine + Environment.NewLine + "Further information" + additional_details + Environment.NewLine + Environment.NewLine + "History of actions" + Environment.NewLine + action_history, notifications_json, 2, 0); - else if (Configuration.Agent.language == "de-DE") - Events.Logger.Insert_Event(sensor_item.severity.ToString(), "Sensor", "Sensor Prozess-RAM-Nutzung (MB) (" + sensor_item.name + ")", notification_history + Environment.NewLine + Environment.NewLine + "Weitere Informationen" + additional_details + Environment.NewLine + Environment.NewLine + "Historie der Aktionen" + Environment.NewLine + action_history, notifications_json, 2, 1); - } - } - else if (sensor_item.category == 1) // Windows Eventlog - { - if (Configuration.Agent.language == "en-US") - Events.Logger.Insert_Event(sensor_item.severity.ToString(), "Sensor", "Windows Eventlog (" + sensor_item.name + ")", notification_history + Environment.NewLine + Environment.NewLine + "History of actions" + Environment.NewLine + action_history, notifications_json, 2, 0); - else if (Configuration.Agent.language == "de-DE") - Events.Logger.Insert_Event(sensor_item.severity.ToString(), "Sensor", "Windows Eventlog (" + sensor_item.name + ")", notification_history + Environment.NewLine + Environment.NewLine + "Historie der Aktionen" + Environment.NewLine + action_history, notifications_json, 2, 1); - } - else if (sensor_item.category == 2) // PowerShell - { - if (Configuration.Agent.language == "en-US") - Events.Logger.Insert_Event(sensor_item.severity.ToString(), "Sensor", "PowerShell (" + sensor_item.name + ")", notification_history + Environment.NewLine + Environment.NewLine + "History of actions" + Environment.NewLine + action_history, notifications_json, 2, 0); - else if (Configuration.Agent.language == "de-DE") - Events.Logger.Insert_Event(sensor_item.severity.ToString(), "Sensor", "PowerShell (" + sensor_item.name + ")", notification_history + Environment.NewLine + Environment.NewLine + "Historie der Aktionen" + Environment.NewLine + action_history, notifications_json, 2, 1); - } - else if (sensor_item.category == 3) // Service - { - if (Configuration.Agent.language == "en-US") - Events.Logger.Insert_Event(sensor_item.severity.ToString(), "Sensor", "Service (" + sensor_item.name + ")", notification_history + Environment.NewLine + Environment.NewLine + "History of actions" + Environment.NewLine + action_history, notifications_json, 2, 0); - else if (Configuration.Agent.language == "de-DE") - Events.Logger.Insert_Event(sensor_item.severity.ToString(), "Sensor", "Dienst (" + sensor_item.name + ")", notification_history + Environment.NewLine + Environment.NewLine + "Historie der Aktionen" + Environment.NewLine + action_history, notifications_json, 2, 1); - } - else if (sensor_item.category == 4) // Ping - { - if (Configuration.Agent.language == "en-US") - Events.Logger.Insert_Event(sensor_item.severity.ToString(), "Sensor", "Ping (" + sensor_item.name + ")", notification_history + Environment.NewLine + Environment.NewLine + "History of actions" + Environment.NewLine + action_history, notifications_json, 2, 0); - else if (Configuration.Agent.language == "de-DE") - Events.Logger.Insert_Event(sensor_item.severity.ToString(), "Sensor", "Ping (" + sensor_item.name + ")", notification_history + Environment.NewLine + Environment.NewLine + "Historie der Aktionen" + Environment.NewLine + action_history, notifications_json, 2, 1); - } - else if (sensor_item.category == 5) - { - if (Configuration.Agent.language == "en-US") - Events.Logger.Insert_Event(sensor_item.severity.ToString(), "Sensor", "Bash (" + sensor_item.name + ")", notification_history + Environment.NewLine + Environment.NewLine + "History of actions" + Environment.NewLine + action_history, notifications_json, 2, 0); - else if (Configuration.Agent.language == "de-DE") - Events.Logger.Insert_Event(sensor_item.severity.ToString(), "Sensor", "Bash (" + sensor_item.name + ")", notification_history + Environment.NewLine + Environment.NewLine + "Historie der Aktionen" + Environment.NewLine + action_history, notifications_json, 2, 1); - } - else if (sensor_item.category == 6) - { - if (Configuration.Agent.language == "en-US") - Events.Logger.Insert_Event(sensor_item.severity.ToString(), "Sensor", "Zsh (" + sensor_item.name + ")", notification_history + Environment.NewLine + Environment.NewLine + "History of actions" + Environment.NewLine + action_history, notifications_json, 2, 0); - else if (Configuration.Agent.language == "de-DE") - Events.Logger.Insert_Event(sensor_item.severity.ToString(), "Sensor", "Zsh (" + sensor_item.name + ")", notification_history + Environment.NewLine + Environment.NewLine + "Historie der Aktionen" + Environment.NewLine + action_history, notifications_json, 2, 1); + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Delete corrupted sensor file", "Sensor id: " + sensor_item.id); + File.Delete(sensor_path); } - - sensor_item.notification_treshold_count = 0; - string notification_treshold_updated_sensor_json = JsonSerializer.Serialize(sensor_item); - File.WriteAllText(sensor, notification_treshold_updated_sensor_json); - } - else // if not, increment the counter - { - sensor_item.notification_treshold_count++; - string notification_treshold_updated_sensor_json = JsonSerializer.Serialize(sensor_item); - File.WriteAllText(sensor, notification_treshold_updated_sensor_json); } } - else //not triggered + else + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Sensor will not be executed", "name: " + sensor_item.name + " id: " + sensor_item.id); + } + catch (Exception e) + { + Logging.Error("Sensors.Time_Scheduler.Check_Execution", "Error processing sensor during execution check", + "Sensor file: " + sensor + " Exception: " + e.ToString()); + + // Delete corrupted sensor file if exists + string sensor_path = Path.Combine(Application_Paths.program_data_sensors, sensor_id + ".json"); + + if (File.Exists(sensor_path)) { - Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Triggered", false.ToString()); - - // if auto reset is enabled, reset the counters - if (sensor_item.auto_reset) - { - // Reset notification counter - sensor_item.notification_treshold_count = 0; - string notification_treshold_updated_sensor_json = JsonSerializer.Serialize(sensor_item); - File.WriteAllText(sensor, notification_treshold_updated_sensor_json); - - // Reset action counter - sensor_item.action_treshold_count = 0; - string action_treshold_updated_sensor_json = JsonSerializer.Serialize(sensor_item); - File.WriteAllText(sensor, action_treshold_updated_sensor_json); - } + Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Delete corrupted sensor file", "Sensor id: " + sensor_id); + File.Delete(sensor_path); } - - // Update last run - sensor_item.last_run = endTime.ToString(); - string updated_sensor_json = JsonSerializer.Serialize(sensor_item); - File.WriteAllText(sensor, updated_sensor_json); - - Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Execution finished", "name: " + sensor_item.name + " id: " + sensor_item.id); } - else - Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Sensor will not be executed", "name: " + sensor_item.name + " id: " + sensor_item.id); } Logging.Sensors("Sensors.Time_Scheduler.Check_Execution", "Check sensor execution", "Stop"); @@ -2510,4 +2474,4 @@ public static void Check_Execution() } } } -} +} \ No newline at end of file diff --git a/NetLock RMM Agent Comm/Linux/Helper/Bash.cs b/NetLock RMM Agent Comm/Linux/Helper/Bash.cs index e33f9aac..71540a61 100644 --- a/NetLock RMM Agent Comm/Linux/Helper/Bash.cs +++ b/NetLock RMM Agent Comm/Linux/Helper/Bash.cs @@ -11,23 +11,48 @@ namespace Linux.Helper { internal class Bash { - public static string Execute_Script(string type, bool decode, string script) + public static string Execute_Script(string type, bool decode, string script, int timeout = 0) // 0 = 60 minutes, timeout in minutes { + Process process = null; + try { Logging.Debug("Linux.Helper.Bash.Execute_Script", "Executing script", $"type: {type}, script length: {script.Length}"); + // Set timeout to 60 minutes if no timeout is set, otherwise convert minutes to milliseconds + if (timeout == 0) + timeout = 3600000; // 60 minutes in milliseconds + else + timeout = timeout * 60 * 1000; // Convert minutes to milliseconds + if (String.IsNullOrEmpty(script)) { Logging.Error("Linux.Helper.Bash.Execute_Script", "Script is empty", ""); - return "-"; + return "Error: Script is empty"; } // Decode the script from Base64 if (decode) { - byte[] script_data = Convert.FromBase64String(script); - string decoded_script = Encoding.UTF8.GetString(script_data); + byte[] script_data; + string decoded_script; + + try + { + script_data = Convert.FromBase64String(script); + decoded_script = Encoding.UTF8.GetString(script_data); + } + catch (FormatException ex) + { + Logging.Error("Linux.Helper.Bash.Execute_Script", "Invalid Base64 script", ex.Message); + return "Error: Invalid Base64 encoding"; + } + + if (String.IsNullOrWhiteSpace(decoded_script)) + { + Logging.Error("Linux.Helper.Bash.Execute_Script", "Decoded script is empty", String.Empty); + return "Error: Decoded script is empty"; + } // Convert Windows line endings (\r\n) to Unix line endings (\n) script = decoded_script.Replace("\r\n", "\n"); @@ -36,48 +61,105 @@ public static string Execute_Script(string type, bool decode, string script) } // Create a new process - using (Process process = new Process()) + process = new Process(); + process.StartInfo.FileName = "/bin/bash"; + process.StartInfo.Arguments = "-s"; // Read script from standard input + process.StartInfo.UseShellExecute = false; + process.StartInfo.RedirectStandardInput = true; + process.StartInfo.RedirectStandardOutput = true; + process.StartInfo.RedirectStandardError = true; + process.StartInfo.CreateNoWindow = true; + + // Use StringBuilder for better performance when reading large outputs + StringBuilder output = new StringBuilder(); + StringBuilder error = new StringBuilder(); + + // Asynchronous output reading to avoid deadlocks + process.OutputDataReceived += (sender, e) => { - process.StartInfo.FileName = "/bin/bash"; - process.StartInfo.Arguments = "-s"; // Read script from standard input - process.StartInfo.UseShellExecute = false; - process.StartInfo.RedirectStandardInput = true; - process.StartInfo.RedirectStandardOutput = true; - process.StartInfo.RedirectStandardError = true; - process.StartInfo.CreateNoWindow = true; - - // Start the process - process.Start(); - - // Write the cleaned script to the process's standard input - using (StreamWriter writer = process.StandardInput) - { - writer.Write(script); - } + if (e.Data != null) + output.AppendLine(e.Data); + }; - // Read the output and error - string output = process.StandardOutput.ReadToEnd(); - string error = process.StandardError.ReadToEnd(); + process.ErrorDataReceived += (sender, e) => + { + if (e.Data != null) + error.AppendLine(e.Data); + }; - // Wait for the process to exit, with a timeout of 2 minutes - process.WaitForExit(120000); + // Start the process + process.Start(); - // Check for errors - if (!string.IsNullOrEmpty(error)) - { - Logging.Error("Linux.Helper.Bash.Execute_Script", "Error executing script", error); - return "Output: " + Environment.NewLine + output + Environment.NewLine + Environment.NewLine + "Error output: " + Environment.NewLine + error; - } - else - { - return output; - } + // Write the script to the process's standard input + using (StreamWriter writer = process.StandardInput) + { + writer.Write(script); } + + // Begin asynchronous reading + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + + // Wait for the process to exit, with a timeout + bool exited = process.WaitForExit(timeout); + + if (!exited) + { + // Timeout occurred - kill the process + process.Kill(true); + process.WaitForExit(); // Ensure async streams are flushed + string timeoutMessage = $"Error: Script execution timed out after {timeout / 60000} minutes."; + Logging.Error("Linux.Helper.Bash.Execute_Script", "Script execution timed out", $"Timeout: {timeout}ms"); + return timeoutMessage; + } + + // Wait for async output reading to complete + process.WaitForExit(); + + string result = output.ToString(); + string errorOutput = error.ToString(); + + if (!String.IsNullOrWhiteSpace(errorOutput)) + { + Logging.Error("Linux.Helper.Bash.Execute_Script", "Script produced error output", errorOutput); + result += Environment.NewLine + "STDERR: " + errorOutput; + } + + int exitCode = process.ExitCode; + if (exitCode != 0) + { + Logging.Error("Linux.Helper.Bash.Execute_Script", $"Script exited with code {exitCode}", errorOutput); + result += Environment.NewLine + $"Exit Code: {exitCode}"; + } + else + { + Logging.Debug("Linux.Helper.Bash.Execute_Script", "Script execution successful", $"Exit code: {exitCode}"); + } + + return result; } catch (Exception ex) { Logging.Error("Linux.Helper.Bash.Execute_Script", "Error executing script", ex.ToString()); - return ex.Message; + return "Error: " + ex.Message; + } + finally + { + // Ensure process is disposed + if (process != null) + { + try + { + if (!process.HasExited) + process.Kill(true); + + process.Dispose(); + } + catch + { + // Ignore disposal errors + } + } } } } diff --git a/NetLock RMM Agent Comm/MacOS/Helper/Zsh.cs b/NetLock RMM Agent Comm/MacOS/Helper/Zsh.cs index 4c7a63b1..edd95f37 100644 --- a/NetLock RMM Agent Comm/MacOS/Helper/Zsh.cs +++ b/NetLock RMM Agent Comm/MacOS/Helper/Zsh.cs @@ -10,24 +10,49 @@ namespace MacOS.Helper { internal class Zsh { - public static string Execute_Script(string type, bool decode, string script) + public static string Execute_Script(string type, bool decode, string script, int timeout = 0) // 0 = 60 minutes, timeout in minutes { + Process process = null; + try { Logging.Debug("MacOS.Helper.Zsh.Execute_Script", "Executing script", $"type: {type}, script length: {script.Length}"); + // Set timeout to 60 minutes if no timeout is set, otherwise convert minutes to milliseconds + if (timeout == 0) + timeout = 3600000; // 60 minutes in milliseconds + else + timeout = timeout * 60 * 1000; // Convert minutes to milliseconds + if (String.IsNullOrEmpty(script)) { Logging.Error("MacOS.Helper.Zsh.Execute_Script", "Script is empty", ""); - return "-"; + return "Error: Script is empty"; } // Decode the script from Base64 if (decode) { - byte[] script_data = Convert.FromBase64String(script); - string decoded_script = Encoding.UTF8.GetString(script_data); - + byte[] script_data; + string decoded_script; + + try + { + script_data = Convert.FromBase64String(script); + decoded_script = Encoding.UTF8.GetString(script_data); + } + catch (FormatException ex) + { + Logging.Error("MacOS.Helper.Zsh.Execute_Script", "Invalid Base64 script", ex.Message); + return "Error: Invalid Base64 encoding"; + } + + if (String.IsNullOrWhiteSpace(decoded_script)) + { + Logging.Error("MacOS.Helper.Zsh.Execute_Script", "Decoded script is empty", String.Empty); + return "Error: Decoded script is empty"; + } + // Convert Windows line endings (\r\n) to Unix line endings (\n) script = decoded_script.Replace("\r\n", "\n"); @@ -35,48 +60,105 @@ public static string Execute_Script(string type, bool decode, string script) } // Create a new process - using (Process process = new Process()) + process = new Process(); + process.StartInfo.FileName = "/bin/zsh"; + process.StartInfo.Arguments = "-s"; // Read script from standard input + process.StartInfo.UseShellExecute = false; + process.StartInfo.RedirectStandardInput = true; + process.StartInfo.RedirectStandardOutput = true; + process.StartInfo.RedirectStandardError = true; + process.StartInfo.CreateNoWindow = true; + + // Use StringBuilder for better performance when reading large outputs + StringBuilder output = new StringBuilder(); + StringBuilder error = new StringBuilder(); + + // Asynchronous output reading to avoid deadlocks + process.OutputDataReceived += (sender, e) => { - process.StartInfo.FileName = "/bin/zsh"; - process.StartInfo.Arguments = "-s"; // Read script from standard input - process.StartInfo.UseShellExecute = false; - process.StartInfo.RedirectStandardInput = true; - process.StartInfo.RedirectStandardOutput = true; - process.StartInfo.RedirectStandardError = true; - process.StartInfo.CreateNoWindow = true; - - // Start the process - process.Start(); - - // Write the cleaned script to the process's standard input - using (StreamWriter writer = process.StandardInput) - { - writer.Write(script); - } + if (e.Data != null) + output.AppendLine(e.Data); + }; - // Read the output and error - string output = process.StandardOutput.ReadToEnd(); - string error = process.StandardError.ReadToEnd(); + process.ErrorDataReceived += (sender, e) => + { + if (e.Data != null) + error.AppendLine(e.Data); + }; - // Wait for the process to exit, with a timeout of 2 minutes - process.WaitForExit(120000); + // Start the process + process.Start(); - // Check for errors - if (!string.IsNullOrEmpty(error)) - { - Logging.Error("MacOS.Helper.Zsh.Execute_Script", "Error executing script", error); - return "Output: " + Environment.NewLine + output + Environment.NewLine + Environment.NewLine + "Error output: " + Environment.NewLine + error; - } - else - { - return output; - } + // Write the script to the process's standard input + using (StreamWriter writer = process.StandardInput) + { + writer.Write(script); + } + + // Begin asynchronous reading + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + + // Wait for the process to exit, with a timeout + bool exited = process.WaitForExit(timeout); + + if (!exited) + { + // Timeout occurred - kill the process + process.Kill(true); + process.WaitForExit(); // Ensure async streams are flushed + string timeoutMessage = $"Error: Script execution timed out after {timeout / 60000} minutes."; + Logging.Error("MacOS.Helper.Zsh.Execute_Script", "Script execution timed out", $"Timeout: {timeout}ms"); + return timeoutMessage; + } + + // Wait for async output reading to complete + process.WaitForExit(); + + string result = output.ToString(); + string errorOutput = error.ToString(); + + if (!String.IsNullOrWhiteSpace(errorOutput)) + { + Logging.Error("MacOS.Helper.Zsh.Execute_Script", "Script produced error output", errorOutput); + result += Environment.NewLine + "STDERR: " + errorOutput; + } + + int exitCode = process.ExitCode; + if (exitCode != 0) + { + Logging.Error("MacOS.Helper.Zsh.Execute_Script", $"Script exited with code {exitCode}", errorOutput); + result += Environment.NewLine + $"Exit Code: {exitCode}"; } + else + { + Logging.Debug("MacOS.Helper.Zsh.Execute_Script", "Script execution successful", $"Exit code: {exitCode}"); + } + + return result; } catch (Exception ex) { Logging.Error("MacOS.Helper.Zsh.Execute_Script", "Error executing script", ex.ToString()); - return ex.Message; + return "Error: " + ex.Message; + } + finally + { + // Ensure process is disposed + if (process != null) + { + try + { + if (!process.HasExited) + process.Kill(true); + + process.Dispose(); + } + catch + { + // Ignore disposal errors + } + } } } } diff --git a/NetLock RMM Agent Comm/Windows/Helper/PowerShell.cs b/NetLock RMM Agent Comm/Windows/Helper/PowerShell.cs index 3845bf54..05281656 100644 --- a/NetLock RMM Agent Comm/Windows/Helper/PowerShell.cs +++ b/NetLock RMM Agent Comm/Windows/Helper/PowerShell.cs @@ -42,52 +42,156 @@ public static string Execute_Command(string type, string command, int timeout) / } } - public static string Execute_Script(string type, string script) + public static string Execute_Script(string type, string script, int timeout = 0) // timeout in minutes { string path = String.Empty; + Process cmd_process = null; try { Global.Initialization.Health.Check_Directories(); - Logging.PowerShell("Helper.Powershell.Execute_Script", "Trying to execute command", type + "script:" + Environment.NewLine + script); + Logging.PowerShell("Helper.Powershell.Execute_Script", "Trying to execute script", type); + // Set timeout to 60 minutes if no timeout is set, otherwise convert minutes to milliseconds + if (timeout == 0) + timeout = 3600000; // 60 minutes in milliseconds + else + timeout = timeout * 60 * 1000; // Convert minutes to milliseconds + if (String.IsNullOrEmpty(script)) { - Logging.Error("Helper.Powershell.Execute_Script", "Script is empty", ""); - return "-"; + Logging.Error("Helper.Powershell.Execute_Script", "Script is empty", String.Empty); + return "Error: Script is empty"; } - path = Application_Paths.program_data_scripts + @"\" + Guid.NewGuid() + ".ps1"; + // Validate base64 + byte[] script_data; + string decoded_script; + + try + { + script_data = Convert.FromBase64String(script); + decoded_script = Encoding.UTF8.GetString(script_data); + } + catch (FormatException ex) + { + Logging.Error("Helper.Powershell.Execute_Script", "Invalid Base64 script", ex.Message); + return "Error: Invalid Base64 encoding"; + } - //Decode script - byte[] script_data = Convert.FromBase64String(script); - string decoded_script = Encoding.UTF8.GetString(script_data); + if (String.IsNullOrWhiteSpace(decoded_script)) + { + Logging.Error("Helper.Powershell.Execute_Script", "Decoded script is empty", String.Empty); + return "Error: Decoded script is empty"; + } + // Create temporary script file + path = Path.Combine(Application_Paths.program_data_scripts, Guid.NewGuid() + ".ps1"); File.WriteAllText(path, decoded_script); - Process cmd_process = new Process(); + // Execute script + cmd_process = new Process(); cmd_process.StartInfo.UseShellExecute = false; cmd_process.StartInfo.RedirectStandardOutput = true; + cmd_process.StartInfo.RedirectStandardError = true; cmd_process.StartInfo.CreateNoWindow = true; cmd_process.StartInfo.FileName = "powershell.exe"; - cmd_process.StartInfo.Arguments = "-executionpolicy bypass -file \"" + path + "\""; + cmd_process.StartInfo.Arguments = "-NoProfile -ExecutionPolicy Bypass -File \"" + path + "\""; + + // Use StringBuilder for better performance when reading large outputs + StringBuilder output = new StringBuilder(); + StringBuilder error = new StringBuilder(); + + // Asynchronous output reading to avoid deadlocks + cmd_process.OutputDataReceived += (sender, e) => + { + if (e.Data != null) + output.AppendLine(e.Data); + }; + + cmd_process.ErrorDataReceived += (sender, e) => + { + if (e.Data != null) + error.AppendLine(e.Data); + }; + cmd_process.Start(); - string result = cmd_process.StandardOutput.ReadToEnd(); - cmd_process.WaitForExit(120000); + cmd_process.BeginOutputReadLine(); + cmd_process.BeginErrorReadLine(); + + // Wait for process to exit with timeout + bool exited = cmd_process.WaitForExit(timeout); + + if (!exited) + { + cmd_process.Kill(true); + cmd_process.WaitForExit(); // Ensure async streams are flushed + string timeoutMessage = $"Error: Script execution timed out after {timeout / 60000} minutes."; + Logging.Error("Helper.Powershell.Execute_Script", "Script execution timed out", $"Timeout: {timeout}ms"); + return timeoutMessage; + } + + // Wait for async output reading to complete + cmd_process.WaitForExit(); + + string result = output.ToString(); + string errorOutput = error.ToString(); + + if (!String.IsNullOrWhiteSpace(errorOutput)) + { + Logging.Error("Helper.Powershell.Execute_Script", "Script produced error output", errorOutput); + result += Environment.NewLine + "STDERR: " + errorOutput; + } + + int exitCode = cmd_process.ExitCode; + if (exitCode != 0) + { + Logging.Error("Helper.Powershell.Execute_Script", $"Script exited with code {exitCode}", errorOutput); + result += Environment.NewLine + $"Exit Code: {exitCode}"; + } + else + { + Logging.PowerShell("Helper.Powershell.Execute_Script", "Script execution successful", $"Exit code: {exitCode}"); + } - Logging.PowerShell("Helper.Powershell.Execute_Script", "Command execution successfully", Environment.NewLine + " Result:" + result); return result; } catch (Exception ex) { - Logging.Error("Helper.Powershell.Execute_Script", "Failed executing script. Type: " + type + " Script: " + script, ex.ToString()); - return "Error: " + ex.ToString(); + Logging.Error("Helper.Powershell.Execute_Script", "Failed executing script. Type: " + type, ex.ToString()); + return "Error: " + ex.Message; } finally { - if (File.Exists(path)) - File.Delete(path); + // Ensure process is disposed + if (cmd_process != null) + { + try + { + if (!cmd_process.HasExited) + cmd_process.Kill(true); + + cmd_process.Dispose(); + } + catch + { + // Ignore disposal errors + } + } + + // Clean up temporary script file + if (!String.IsNullOrWhiteSpace(path) && File.Exists(path)) + { + try + { + File.Delete(path); + } + catch (Exception deleteEx) + { + Logging.Error("Helper.Powershell.Execute_Script", "Failed to delete temporary script file", deleteEx.ToString()); + } + } } } } diff --git a/NetLock RMM Agent Comm/Windows/Microsoft_Defender_Antivirus/Eventlog_Crawler.cs b/NetLock RMM Agent Comm/Windows/Microsoft_Defender_Antivirus/Eventlog_Crawler.cs index 61404d36..4212dd4d 100644 --- a/NetLock RMM Agent Comm/Windows/Microsoft_Defender_Antivirus/Eventlog_Crawler.cs +++ b/NetLock RMM Agent Comm/Windows/Microsoft_Defender_Antivirus/Eventlog_Crawler.cs @@ -19,6 +19,31 @@ namespace Windows.Microsoft_Defender_Antivirus { internal class Eventlog_Crawler { + // Helper method to check if a notification is enabled in the policy + private static bool Is_Notification_Enabled(string notificationKey) + { + try + { + if (String.IsNullOrEmpty(Windows_Worker.policy_antivirus_settings_json)) + return false; + + using (JsonDocument document = JsonDocument.Parse(Windows_Worker.policy_antivirus_settings_json)) + { + if (document.RootElement.TryGetProperty(notificationKey, out JsonElement element)) + { + return element.GetBoolean(); + } + } + + return false; + } + catch (Exception ex) + { + Logging.Error("Microsoft_Defender_AntiVirus.Eventlog_Crawler.Is_Notification_Enabled", $"Failed to check notification: {notificationKey}", ex.ToString()); + return false; + } + } + public static void Do() { Logging.Microsoft_Defender_Antivirus("Microsoft_Defender_AntiVirus.Eventlog_Crawler.Do", "Start", ""); @@ -34,44 +59,120 @@ public static void Do() Check_Registry(); - MALWAREPROTECTION_RTP_ENABLED(); - MALWAREPROTECTION_RTP_DISABLED(); - MALWAREPROTECTION_SIGNATURE_UPDATED(); - MALWAREPROTECTION_STATE_MALWARE_DETECTED(); - MALWAREPROTECTION_MALWARE_ACTION_TAKEN(); - MALWAREPROTECTION_MALWARE_DETECTED(); - MALWAREPROTECTION_SCAN_COMPLETED(); - MALWAREPROTECTION_SCAN_CANCELLED(); - MALWAREPROTECTION_SCAN_PAUSED(); - MALWAREPROTECTION_SCAN_FAILED(); - MALWAREPROTECTION_MALWARE_ACTION_FAILED_00_MALWAREPROTECTION_STATE_MALWARE_ACTION_FAILED_00_MALWAREPROTECTION_STATE_MALWARE_ACTION_CRITICALLY_FAILED(); - MALWAREPROTECTION_QUARANTINE_RESTORE(); - MALWAREPROTECTION_QUARANTINE_DELETE(); + // Only call event methods if the corresponding notification is enabled in the policy + if (Is_Notification_Enabled("notifications_malwareprotection_rtp_enabled")) + MALWAREPROTECTION_RTP_ENABLED(); + + if (Is_Notification_Enabled("notifications_malwareprotection_rtp_disabled")) + MALWAREPROTECTION_RTP_DISABLED(); + + if (Is_Notification_Enabled("notifications_malwareprotection_signature_updated")) + MALWAREPROTECTION_SIGNATURE_UPDATED(); + + if (Is_Notification_Enabled("notifications_malwareprotection_state_malware_detected")) + MALWAREPROTECTION_STATE_MALWARE_DETECTED(); + + if (Is_Notification_Enabled("notifications_malwareprotection_malware_action_taken")) + MALWAREPROTECTION_MALWARE_ACTION_TAKEN(); + + if (Is_Notification_Enabled("notifications_malwareprotection_malware_detected")) + MALWAREPROTECTION_MALWARE_DETECTED(); + + if (Is_Notification_Enabled("notifications_malwareprotection_scan_completed")) + MALWAREPROTECTION_SCAN_COMPLETED(); + + if (Is_Notification_Enabled("notifications_malwareprotection_scan_cancelled")) + MALWAREPROTECTION_SCAN_CANCELLED(); + + if (Is_Notification_Enabled("notifications_malwareprotection_scan_paused")) + MALWAREPROTECTION_SCAN_PAUSED(); + + if (Is_Notification_Enabled("notifications_malwareprotection_scan_failed")) + MALWAREPROTECTION_SCAN_FAILED(); + + if (Is_Notification_Enabled("notifications_malwareprotection_malware_action_failed_00_malwareprotection_state_malware_action_failed_00_malwareprotection_state_malware_action_critically_failed")) + MALWAREPROTECTION_MALWARE_ACTION_FAILED_00_MALWAREPROTECTION_STATE_MALWARE_ACTION_FAILED_00_MALWAREPROTECTION_STATE_MALWARE_ACTION_CRITICALLY_FAILED(); + + if (Is_Notification_Enabled("notifications_malwareprotection_quarantine_restore")) + MALWAREPROTECTION_QUARANTINE_RESTORE(); + + if (Is_Notification_Enabled("notifications_malwareprotection_quarantine_delete")) + MALWAREPROTECTION_QUARANTINE_DELETE(); + + // MALWAREPROTECTION_MALWARE_HISTORY_DELETE - no notification setting, always run MALWAREPROTECTION_MALWARE_HISTORY_DELETE(); - MALWAREPROTECTION_BEHAVIOR_DETECTED(); - MALWAREPROTECTION_STATE_MALWARE_ACTION_TAKEN(); - MALWAREPROTECTION_FOLDER_GUARD_SECTOR_BLOCK(); - MALWAREPROTECTION_SIGNATURE_UPDATE_FAILED(); - MALWAREPROTECTION_SIGNATURE_REVERSION(); - MALWAREPROTECTION_ENGINE_UPDATE_PLATFORMOUTOFDATE(); - MALWAREPROTECTION_PLATFORM_UPDATE_FAILED(); - MALWAREPROTECTION_PLATFORM_ALMOSTOUTOFDATE(); - MALWAREPROTECTION_OS_EXPIRING(); - MALWAREPROTECTION_OS_EOL(); - MALWAREPROTECTION_PROTECTION_EOL(); - MALWAREPROTECTION_RTP_FEATURE_FAILURE(); - MALWAREPROTECTION_RTP_FEATURE_RECOVERED(); - MALWAREPROTECTION_RTP_FEATURE_CONFIGURED(); + + if (Is_Notification_Enabled("notifications_malwareprotection_behavior_detected")) + MALWAREPROTECTION_BEHAVIOR_DETECTED(); + + if (Is_Notification_Enabled("notifications_malwareprotection_state_malware_action_taken")) + MALWAREPROTECTION_STATE_MALWARE_ACTION_TAKEN(); + + if (Is_Notification_Enabled("notifications_malwareprotection_folder_guard_sector_block")) + MALWAREPROTECTION_FOLDER_GUARD_SECTOR_BLOCK(); + + if (Is_Notification_Enabled("notifications_malwareprotection_signature_update_failed")) + MALWAREPROTECTION_SIGNATURE_UPDATE_FAILED(); + + if (Is_Notification_Enabled("notifications_malwareprotection_signature_reversion")) + MALWAREPROTECTION_SIGNATURE_REVERSION(); + + if (Is_Notification_Enabled("notifications_malwareprotection_engine_update_platformoutofdate")) + MALWAREPROTECTION_ENGINE_UPDATE_PLATFORMOUTOFDATE(); + + if (Is_Notification_Enabled("notifications_malwareprotection_platform_update_failed")) + MALWAREPROTECTION_PLATFORM_UPDATE_FAILED(); + + if (Is_Notification_Enabled("notifications_malwareprotection_platform_almostoutofdate")) + MALWAREPROTECTION_PLATFORM_ALMOSTOUTOFDATE(); + + if (Is_Notification_Enabled("notifications_malwareprotection_os_expiring")) + MALWAREPROTECTION_OS_EXPIRING(); + + if (Is_Notification_Enabled("notifications_malwareprotection_os_eol")) + MALWAREPROTECTION_OS_EOL(); + + if (Is_Notification_Enabled("notifications_malwareprotection_protection_eol")) + MALWAREPROTECTION_PROTECTION_EOL(); + + if (Is_Notification_Enabled("notifications_malwareprotection_rtp_feature_failure")) + MALWAREPROTECTION_RTP_FEATURE_FAILURE(); + + if (Is_Notification_Enabled("notifications_malwareprotection_rtp_feature_recovered")) + MALWAREPROTECTION_RTP_FEATURE_RECOVERED(); + + if (Is_Notification_Enabled("notifications_malwareprotection_rtp_feature_configured")) + MALWAREPROTECTION_RTP_FEATURE_CONFIGURED(); + //MALWAREPROTECTION_CONFIG_CHANGED(); Spams config - MALWAREPROTECTION_ENGINE_FAILURE(); - MALWAREPROTECTION_ANTISPYWARE_ENABLED(); - MALWAREPROTECTION_ANTISPYWARE_DISABLED(); - MALWAREPROTECTION_ANTIVIRUS_ENABLED(); - MALWAREPROTECTION_ANTIVIRUS_DISABLED(); - TAMPER_PROTECTION_BLOCKED_CHANGES(); - MALWAREPROTECTION_EXPIRATION_WARNING_STATE(); - MALWAREPROTECTION_DISABLED_EXPIRED_STATE(); - CONTROLLED_FOLDER_ACTIONS_BLOCKED_ACTION(); + + if (Is_Notification_Enabled("notifications_malwareprotection_engine_failure")) + MALWAREPROTECTION_ENGINE_FAILURE(); + + if (Is_Notification_Enabled("notifications_malwareprotection_antispyware_enabled")) + MALWAREPROTECTION_ANTISPYWARE_ENABLED(); + + if (Is_Notification_Enabled("notifications_malwareprotection_antispyware_disabled")) + MALWAREPROTECTION_ANTISPYWARE_DISABLED(); + + if (Is_Notification_Enabled("notifications_malwareprotection_antivirus_enabled")) + MALWAREPROTECTION_ANTIVIRUS_ENABLED(); + + if (Is_Notification_Enabled("notifications_malwareprotection_antivirus_disabled")) + MALWAREPROTECTION_ANTIVIRUS_DISABLED(); + + if (Is_Notification_Enabled("notifications_tamper_protection_blocked_changes")) + TAMPER_PROTECTION_BLOCKED_CHANGES(); + + if (Is_Notification_Enabled("notifications_malwareprotection_expiration_warning_state")) + MALWAREPROTECTION_EXPIRATION_WARNING_STATE(); + + if (Is_Notification_Enabled("notifications_malwareprotection_disabled_expired_state")) + MALWAREPROTECTION_DISABLED_EXPIRED_STATE(); + + if (Is_Notification_Enabled("notifications_controlled_folder_actions_blocked_action")) + CONTROLLED_FOLDER_ACTIONS_BLOCKED_ACTION(); + //Delete_Eventlog(); //Service.microsoft_defender_antivirus_events_crawling = false; @@ -527,6 +628,7 @@ public static void MALWAREPROTECTION_RTP_ENABLED() else if (Global.Configuration.Agent.language == "de-DE") Global.Events.Logger.Insert_Event("0", "Microsoft Defender Antivirus", "Echtzeitschutz aktiviert.", "Timestamp: " + eventRecord.TimeCreated + Environment.NewLine + "Sensor: MALWAREPROTECTION_RTP_ENABLED" + Environment.NewLine + Environment.NewLine + eventRecord.FormatDescription(), Windows_Worker.microsoft_defender_antivirus_notifications_json, 0, 1); + //Trigger trayicon notification | NetLock legacy code. Will be worked on in the future. /*if (NetLock_Agent_Service.tray_icon_notifications_antivirus) if (NetLock_Agent_Service.os_language != 0) @@ -570,6 +672,7 @@ public static void MALWAREPROTECTION_RTP_DISABLED() else if (Global.Configuration.Agent.language == "de-DE") Global.Events.Logger.Insert_Event("3", "Microsoft Defender Antivirus", "Echtzeitschutz deaktiviert.", details, Windows_Worker.microsoft_defender_antivirus_notifications_json, 0, 1); + //Trigger trayicon notification | NetLock legacy code. Will be worked on in the future. /* if (NetLock_Agent_Service.tray_icon_notifications_antivirus) diff --git a/NetLock RMM Agent Comm/Windows/Microsoft_Defender_Antivirus/Handler.cs b/NetLock RMM Agent Comm/Windows/Microsoft_Defender_Antivirus/Handler.cs index 723c7e2b..3d8672a3 100644 --- a/NetLock RMM Agent Comm/Windows/Microsoft_Defender_Antivirus/Handler.cs +++ b/NetLock RMM Agent Comm/Windows/Microsoft_Defender_Antivirus/Handler.cs @@ -26,11 +26,11 @@ public static void Initalization() using (JsonDocument document = JsonDocument.Parse(Windows_Worker.policy_antivirus_settings_json)) { - JsonElement enabled_element = document.RootElement.GetProperty("enabled"); - enabled = Convert.ToBoolean(enabled_element.ToString()); + if (document.RootElement.TryGetProperty("enabled", out JsonElement enabled_element)) + enabled = enabled_element.GetBoolean(); - JsonElement security_center_tray_element = document.RootElement.GetProperty("security_center_tray"); - security_center_tray = Convert.ToBoolean(security_center_tray_element.ToString()); + if (document.RootElement.TryGetProperty("security_center_tray", out JsonElement security_center_tray_element)) + security_center_tray = security_center_tray_element.GetBoolean(); } if (enabled) @@ -39,7 +39,6 @@ public static void Initalization() if (!security_center_tray) Kill_Security_Center_Tray_Icon(); - Windows_Worker.microsoft_defender_antivirus_notifications_json = Get_Notifications_Json(); Set_Settings.Do(); @@ -129,8 +128,8 @@ public static void Check_Hourly_Sig_Updates() using (JsonDocument document = JsonDocument.Parse(Windows_Worker.policy_antivirus_settings_json)) { - JsonElement check_hourly_signatures_element = document.RootElement.GetProperty("check_hourly_signatures"); - check_hourly_signatures = Convert.ToBoolean(check_hourly_signatures_element.ToString()); + if (document.RootElement.TryGetProperty("check_hourly_signatures", out JsonElement check_hourly_signatures_element)) + check_hourly_signatures = check_hourly_signatures_element.GetBoolean(); } if (check_hourly_signatures) @@ -179,20 +178,20 @@ public static string Get_Notifications_Json() using (JsonDocument document = JsonDocument.Parse(Windows_Worker.policy_antivirus_settings_json)) { - JsonElement notifications_netlock_mail_element = document.RootElement.GetProperty("notifications_netlock_mail"); - notifications_netlock_mail = notifications_netlock_mail_element.GetBoolean(); + if (document.RootElement.TryGetProperty("notifications_netlock_mail", out JsonElement notifications_netlock_mail_element)) + notifications_netlock_mail = notifications_netlock_mail_element.GetBoolean(); - JsonElement notifications_netlock_microsoft_teams_element = document.RootElement.GetProperty("notifications_netlock_microsoft_teams"); - notifications_netlock_microsoft_teams = notifications_netlock_microsoft_teams_element.GetBoolean(); + if (document.RootElement.TryGetProperty("notifications_netlock_microsoft_teams", out JsonElement notifications_netlock_microsoft_teams_element)) + notifications_netlock_microsoft_teams = notifications_netlock_microsoft_teams_element.GetBoolean(); - JsonElement notifications_netlock_telegram_element = document.RootElement.GetProperty("notifications_netlock_telegram"); - notifications_netlock_telegram = notifications_netlock_telegram_element.GetBoolean(); + if (document.RootElement.TryGetProperty("notifications_netlock_telegram", out JsonElement notifications_netlock_telegram_element)) + notifications_netlock_telegram = notifications_netlock_telegram_element.GetBoolean(); - JsonElement notifications_netlock_ntfy_sh_element = document.RootElement.GetProperty("notifications_netlock_ntfy_sh"); - notifications_netlock_ntfy_sh = notifications_netlock_ntfy_sh_element.GetBoolean(); + if (document.RootElement.TryGetProperty("notifications_netlock_ntfy_sh", out JsonElement notifications_netlock_ntfy_sh_element)) + notifications_netlock_ntfy_sh = notifications_netlock_ntfy_sh_element.GetBoolean(); - JsonElement notifications_netlock_webhook_element = document.RootElement.GetProperty("notifications_netlock_webhook"); - notifications_netlock_webhook = notifications_netlock_webhook_element.GetBoolean(); + if (document.RootElement.TryGetProperty("notifications_netlock_webhook", out JsonElement notifications_netlock_webhook_element)) + notifications_netlock_webhook = notifications_netlock_webhook_element.GetBoolean(); } // Create notifications_json diff --git a/NetLock RMM Agent Health/Application_Settings.cs b/NetLock RMM Agent Health/Application_Settings.cs new file mode 100644 index 00000000..8ccc0e71 --- /dev/null +++ b/NetLock RMM Agent Health/Application_Settings.cs @@ -0,0 +1,8 @@ +namespace NetLock_RMM_Agent_Health; + +internal class Application_Settings +{ + // Encryption key for local configuration files + // This key is used to encrypt sensitive configuration data at rest + public static string NetLock_Local_Encryption_Key = "()TZ%/N)NZTG$/()4i59du4)"; +} \ No newline at end of file diff --git a/NetLock RMM Agent Health/Global/Helper/Encryption.cs b/NetLock RMM Agent Health/Global/Helper/Encryption.cs new file mode 100644 index 00000000..16ba5470 --- /dev/null +++ b/NetLock RMM Agent Health/Global/Helper/Encryption.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.IO; +using Microsoft.Win32; +using System.Security.Cryptography; +using Global.Helper; + +namespace Global.Encryption +{ + //OSSCH_START 5dfcf5de-52eb-4345-a402-9859ac5b159d //OSSCH_END +} diff --git a/NetLock RMM Agent Health/Initialization/Server_Config.cs b/NetLock RMM Agent Health/Initialization/Server_Config.cs index f1cf09c7..867767ba 100644 --- a/NetLock RMM Agent Health/Initialization/Server_Config.cs +++ b/NetLock RMM Agent Health/Initialization/Server_Config.cs @@ -7,6 +7,7 @@ using System.IO; using System.Security.Principal; using Global.Helper; +using Global.Encryption; using NetLock_RMM_Agent_Health; using System.ComponentModel; using System.Runtime.Intrinsics.Wasm; @@ -17,11 +18,121 @@ namespace Global.Initialization { internal class Server_Config { + // Cache for the decrypted JSON to avoid repeated decryption + private static string _cachedDecryptedJson = null; + private static DateTime _cacheTimestamp = DateTime.MinValue; + private static readonly TimeSpan CacheExpiration = TimeSpan.FromMinutes(5); + + /// + /// Loads and decrypts the server config JSON. If the file is unencrypted, it will be converted and encrypted. + /// + private static string LoadAndDecryptConfig() + { + try + { + // Check cache validity + if (_cachedDecryptedJson != null && (DateTime.Now - _cacheTimestamp) < CacheExpiration) + { + return _cachedDecryptedJson; + } + + // Check if file exists + if (!File.Exists(Application_Paths.program_data_server_config_json)) + { + Logging.Error("Server_Config", "LoadAndDecryptConfig", "Server config file does not exist."); + return null; + } + + string fileContent = File.ReadAllText(Application_Paths.program_data_server_config_json); + + // Check if the content is already encrypted (Base64 string without JSON structure) + bool isEncrypted = !fileContent.TrimStart().StartsWith("{"); + + if (isEncrypted) + { + // File is encrypted, decrypt it + Logging.Debug("Server_Config", "LoadAndDecryptConfig", "Config file is encrypted. Decrypting..."); + string decryptedJson = String_Encryption.Decrypt(fileContent, Application_Settings.NetLock_Local_Encryption_Key); + + // Update cache + _cachedDecryptedJson = decryptedJson; + _cacheTimestamp = DateTime.Now; + + return decryptedJson; + } + else + { + // File is unencrypted (legacy format), convert it + Logging.Debug("Server_Config", "LoadAndDecryptConfig", "Config file is unencrypted. Converting to encrypted format..."); + + // Validate that it's valid JSON + using (JsonDocument document = JsonDocument.Parse(fileContent)) + { + // JSON is valid, encrypt and save it + string encryptedContent = String_Encryption.Encrypt(fileContent, Application_Settings.NetLock_Local_Encryption_Key); + File.WriteAllText(Application_Paths.program_data_server_config_json, encryptedContent); + + Logging.Debug("Server_Config", "LoadAndDecryptConfig", "Config file successfully encrypted and saved."); + + // Update cache + _cachedDecryptedJson = fileContent; + _cacheTimestamp = DateTime.Now; + + return fileContent; + } + } + } + catch (Exception ex) + { + Logging.Error("Server_Config", "LoadAndDecryptConfig", ex.ToString()); + return null; + } + } + + /// + /// Saves the server config JSON in encrypted format. + /// + public static bool SaveEncryptedConfig(string jsonContent) + { + try + { + string encryptedContent = String_Encryption.Encrypt(jsonContent, Application_Settings.NetLock_Local_Encryption_Key); + File.WriteAllText(Application_Paths.program_data_server_config_json, encryptedContent); + + // Update cache + _cachedDecryptedJson = jsonContent; + _cacheTimestamp = DateTime.Now; + + Logging.Debug("Server_Config", "SaveEncryptedConfig", "Config file successfully encrypted and saved."); + return true; + } + catch (Exception ex) + { + Logging.Error("Server_Config", "SaveEncryptedConfig", ex.ToString()); + return false; + } + } + + /// + /// Invalidates the cache, forcing a reload on next access. + /// + public static void InvalidateCache() + { + _cachedDecryptedJson = null; + _cacheTimestamp = DateTime.MinValue; + } + public static string Ssl() { try { - string server_config_json = File.ReadAllText(Application_Paths.program_data_server_config_json); + string server_config_json = LoadAndDecryptConfig(); + + if (server_config_json == null) + { + Logging.Error("Server_Config_Handler", "Ssl", "Failed to load server config."); + return false.ToString(); + } // Parse the JSON using (JsonDocument document = JsonDocument.Parse(server_config_json)) @@ -51,7 +162,13 @@ public static string Package_Guid() { try { - string server_config_json = File.ReadAllText(Application_Paths.program_data_server_config_json); + string server_config_json = LoadAndDecryptConfig(); + + if (server_config_json == null) + { + Logging.Error("Server_Config_Handler", "Package_Guid", "Failed to load server config."); + return false.ToString(); + } // Parse the JSON using (JsonDocument document = JsonDocument.Parse(server_config_json)) @@ -73,7 +190,14 @@ public static string Communication_Servers() { try { - string server_config_json = File.ReadAllText(Application_Paths.program_data_server_config_json); + string server_config_json = LoadAndDecryptConfig(); + + if (server_config_json == null) + { + Logging.Error("Server_Config_Handler", "Communication_Servers", "Failed to load server config."); + return "error"; + } + Logging.Debug("Server_Config_Handler", "Server_Config_Handler.Load (server_config_json)", server_config_json); // Parse the JSON using (JsonDocument document = JsonDocument.Parse(server_config_json)) @@ -94,7 +218,14 @@ public static string Remote_Servers() { try { - string server_config_json = File.ReadAllText(Application_Paths.program_data_server_config_json); + string server_config_json = LoadAndDecryptConfig(); + + if (server_config_json == null) + { + Logging.Error("Server_Config_Handler", "Remote_Servers", "Failed to load server config."); + return "error"; + } + Logging.Debug("Server_Config_Handler", "Server_Config_Handler.Load (server_config_json)", server_config_json); // Parse the JSON using (JsonDocument document = JsonDocument.Parse(server_config_json)) @@ -115,7 +246,14 @@ public static string Update_Servers() { try { - string server_config_json = File.ReadAllText(Application_Paths.program_data_server_config_json); + string server_config_json = LoadAndDecryptConfig(); + + if (server_config_json == null) + { + Logging.Error("Server_Config_Handler", "Update_Servers", "Failed to load server config."); + return "error"; + } + Logging.Debug("Server_Config_Handler", "Server_Config_Handler.Load (server_config_json)", server_config_json); // Parse the JSON using (JsonDocument document = JsonDocument.Parse(server_config_json)) @@ -136,7 +274,14 @@ public static string Trust_Servers() { try { - string server_config_json = File.ReadAllText(Application_Paths.program_data_server_config_json); + string server_config_json = LoadAndDecryptConfig(); + + if (server_config_json == null) + { + Logging.Error("Server_Config_Handler", "Trust_Servers", "Failed to load server config."); + return "error"; + } + Logging.Debug("Server_Config_Handler", "Server_Config_Handler.Load (server_config_json)", server_config_json); // Parse the JSON using (JsonDocument document = JsonDocument.Parse(server_config_json)) @@ -157,7 +302,14 @@ public static string File_Servers() { try { - string server_config_json = File.ReadAllText(Application_Paths.program_data_server_config_json); + string server_config_json = LoadAndDecryptConfig(); + + if (server_config_json == null) + { + Logging.Error("Server_Config_Handler", "File_Servers", "Failed to load server config."); + return "error"; + } + Logging.Debug("Server_Config_Handler", "Server_Config_Handler.Load (server_config_json)", server_config_json); // Parse the JSON using (JsonDocument document = JsonDocument.Parse(server_config_json)) @@ -178,7 +330,14 @@ public static string Tenant_Guid() { try { - string server_config_json = File.ReadAllText(Application_Paths.program_data_server_config_json); + string server_config_json = LoadAndDecryptConfig(); + + if (server_config_json == null) + { + Logging.Error("Server_Config_Handler", "Tenant_Guid", "Failed to load server config."); + return "error"; + } + Logging.Debug("Server_Config_Handler", "Server_Config_Handler.Load (server_config_json)", server_config_json); // Parse the JSON using (JsonDocument document = JsonDocument.Parse(server_config_json)) @@ -199,7 +358,14 @@ public static string Location_Guid() { try { - string server_config_json = File.ReadAllText(Application_Paths.program_data_server_config_json); + string server_config_json = LoadAndDecryptConfig(); + + if (server_config_json == null) + { + Logging.Error("Server_Config_Handler", "Location_Guid", "Failed to load server config."); + return "error"; + } + Logging.Debug("Server_Config_Handler", "Server_Config_Handler.Load (server_config_json)", server_config_json); // Parse the JSON using (JsonDocument document = JsonDocument.Parse(server_config_json)) @@ -220,7 +386,14 @@ public static string Language() { try { - string server_config_json = File.ReadAllText(Application_Paths.program_data_server_config_json); + string server_config_json = LoadAndDecryptConfig(); + + if (server_config_json == null) + { + Logging.Error("Server_Config_Handler", "Language", "Failed to load server config."); + return "error"; + } + Logging.Debug("Server_Config_Handler", "Server_Config_Handler.Load (server_config_json)", server_config_json); // Parse the JSON using (JsonDocument document = JsonDocument.Parse(server_config_json)) @@ -241,7 +414,14 @@ public static bool Authorized() { try { - string serverConfigJson = File.ReadAllText(Application_Paths.program_data_server_config_json); + string serverConfigJson = LoadAndDecryptConfig(); + + if (serverConfigJson == null) + { + Logging.Error("Server_Config_Handler", "Authorized", "Failed to load server config."); + return false; + } + Logging.Debug("Server_Config_Handler", "Authorized", serverConfigJson); // Parse the JSON @@ -274,7 +454,14 @@ public static string Access_Key() { string access_key = String.Empty; - string server_config_json = File.ReadAllText(Application_Paths.program_data_server_config_json); + string server_config_json = LoadAndDecryptConfig(); + + if (server_config_json == null) + { + Logging.Error("Server_Config_Handler", "Access_Key", "Failed to load server config."); + return String.Empty; + } + Logging.Debug("Server_Config_Handler", "Server_Config_Handler.Load (server_config_json)", server_config_json); // Parse the JSON using (JsonDocument document = JsonDocument.Parse(server_config_json)) @@ -314,8 +501,8 @@ public static string Access_Key() string json = JsonSerializer.Serialize(jsonObject, new JsonSerializerOptions { WriteIndented = true }); Logging.Debug("Online_Mode.Handler.Update_Device_Information", "json", json); - // Write the new server config JSON to the file - File.WriteAllText(Application_Paths.program_data_server_config_json, json); + // Write the new server config JSON to the file (encrypted) + SaveEncryptedConfig(json); } else { @@ -332,3 +519,4 @@ public static string Access_Key() } } } + diff --git a/NetLock RMM Agent Remote/Global/Helper/Encryption.cs b/NetLock RMM Agent Remote/Global/Helper/Encryption.cs index aa4f52b8..0ed38735 100644 --- a/NetLock RMM Agent Remote/Global/Helper/Encryption.cs +++ b/NetLock RMM Agent Remote/Global/Helper/Encryption.cs @@ -8,4 +8,4 @@ using System.Security.Cryptography; using Global.Helper; -//OSSCH_START 5a2597e6-eec8-4e05-89a8-b5904dc2d896 //OSSCH_END \ No newline at end of file +//OSSCH_START 83bfe57c-a18d-4501-91eb-c5a463d29f63 //OSSCH_END \ No newline at end of file diff --git a/NetLock RMM Agent Remote/Initialization/Server_Config.cs b/NetLock RMM Agent Remote/Initialization/Server_Config.cs index e1c5c990..0c7b2e0c 100644 --- a/NetLock RMM Agent Remote/Initialization/Server_Config.cs +++ b/NetLock RMM Agent Remote/Initialization/Server_Config.cs @@ -7,6 +7,7 @@ using System.IO; using System.Security.Principal; using Global.Helper; +using Global.Encryption; using NetLock_RMM_Agent_Remote; using System.ComponentModel; using System.Runtime.Intrinsics.Wasm; @@ -17,11 +18,108 @@ namespace Global.Initialization { internal class Server_Config { + // Cache for the decrypted JSON to avoid repeated decryption + private static string _cachedDecryptedJson = null; + private static DateTime _cacheTimestamp = DateTime.MinValue; + private static readonly TimeSpan CacheExpiration = TimeSpan.FromMinutes(5); + + /// + /// Loads and decrypts the server config JSON. Retries on failure with delay. + /// + private static string LoadAndDecryptConfig() + { + try + { + // Check cache validity + if (_cachedDecryptedJson != null && (DateTime.Now - _cacheTimestamp) < CacheExpiration) + { + return _cachedDecryptedJson; + } + + string fileContent = File.ReadAllText(Application_Paths.program_data_server_config_json); + + // Check if the content is already encrypted (Base64 string without JSON structure) + bool isEncrypted = !fileContent.TrimStart().StartsWith("{"); + + if (isEncrypted) + { + // File is encrypted, decrypt it + Logging.Debug("Server_Config", "LoadAndDecryptConfig", "Config file is encrypted. Decrypting..."); + string decryptedJson = String_Encryption.Decrypt(fileContent, Application_Settings.NetLock_Local_Encryption_Key); + + // Update cache + _cachedDecryptedJson = decryptedJson; + _cacheTimestamp = DateTime.Now; + + return decryptedJson; + } + else + { + // File is unencrypted - just return it + Logging.Debug("Server_Config", "LoadAndDecryptConfig", "Config file is unencrypted. Loading..."); + + // Validate that it's valid JSON + using (JsonDocument document = JsonDocument.Parse(fileContent)) + { + // Update cache + _cachedDecryptedJson = fileContent; + _cacheTimestamp = DateTime.Now; + + return fileContent; + } + } + } + catch (Exception ex) + { + Logging.Error("Server_Config", "LoadAndDecryptConfig", ex.ToString()); + return null; + } + } + + /// + /// Saves the server config JSON in encrypted format. + /// + public static bool SaveEncryptedConfig(string jsonContent) + { + try + { + string encryptedContent = String_Encryption.Encrypt(jsonContent, Application_Settings.NetLock_Local_Encryption_Key); + File.WriteAllText(Application_Paths.program_data_server_config_json, encryptedContent); + + // Update cache + _cachedDecryptedJson = jsonContent; + _cacheTimestamp = DateTime.Now; + + Logging.Debug("Server_Config", "SaveEncryptedConfig", "Config file successfully encrypted and saved."); + return true; + } + catch (Exception ex) + { + Logging.Error("Server_Config", "SaveEncryptedConfig", ex.ToString()); + return false; + } + } + + /// + /// Invalidates the cache, forcing a reload on next access. + /// + public static void InvalidateCache() + { + _cachedDecryptedJson = null; + _cacheTimestamp = DateTime.MinValue; + } + public static string Ssl() { try { - string server_config_json = File.ReadAllText(Application_Paths.program_data_server_config_json); + string server_config_json = LoadAndDecryptConfig(); + + if (server_config_json == null) + { + Logging.Error("Server_Config_Handler", "Ssl", "Failed to load server config."); + return false.ToString(); + } // Parse the JSON using (JsonDocument document = JsonDocument.Parse(server_config_json)) @@ -51,7 +149,13 @@ public static string Package_Guid() { try { - string server_config_json = File.ReadAllText(Application_Paths.program_data_server_config_json); + string server_config_json = LoadAndDecryptConfig(); + + if (server_config_json == null) + { + Logging.Error("Server_Config_Handler", "Package_Guid", "Failed to load server config."); + return false.ToString(); + } // Parse the JSON using (JsonDocument document = JsonDocument.Parse(server_config_json)) @@ -73,7 +177,14 @@ public static string Communication_Servers() { try { - string server_config_json = File.ReadAllText(Application_Paths.program_data_server_config_json); + string server_config_json = LoadAndDecryptConfig(); + + if (server_config_json == null) + { + Logging.Error("Server_Config_Handler", "Communication_Servers", "Failed to load server config."); + return "error"; + } + Logging.Debug("Server_Config_Handler", "Server_Config_Handler.Load (server_config_json)", server_config_json); // Parse the JSON using (JsonDocument document = JsonDocument.Parse(server_config_json)) @@ -94,7 +205,14 @@ public static string Remote_Servers() { try { - string server_config_json = File.ReadAllText(Application_Paths.program_data_server_config_json); + string server_config_json = LoadAndDecryptConfig(); + + if (server_config_json == null) + { + Logging.Error("Server_Config_Handler", "Remote_Servers", "Failed to load server config."); + return "error"; + } + Logging.Debug("Server_Config_Handler", "Server_Config_Handler.Load (server_config_json)", server_config_json); // Parse the JSON using (JsonDocument document = JsonDocument.Parse(server_config_json)) @@ -115,7 +233,14 @@ public static string Update_Servers() { try { - string server_config_json = File.ReadAllText(Application_Paths.program_data_server_config_json); + string server_config_json = LoadAndDecryptConfig(); + + if (server_config_json == null) + { + Logging.Error("Server_Config_Handler", "Update_Servers", "Failed to load server config."); + return "error"; + } + Logging.Debug("Server_Config_Handler", "Server_Config_Handler.Load (server_config_json)", server_config_json); // Parse the JSON using (JsonDocument document = JsonDocument.Parse(server_config_json)) @@ -136,7 +261,14 @@ public static string Trust_Servers() { try { - string server_config_json = File.ReadAllText(Application_Paths.program_data_server_config_json); + string server_config_json = LoadAndDecryptConfig(); + + if (server_config_json == null) + { + Logging.Error("Server_Config_Handler", "Trust_Servers", "Failed to load server config."); + return "error"; + } + Logging.Debug("Server_Config_Handler", "Server_Config_Handler.Load (server_config_json)", server_config_json); // Parse the JSON using (JsonDocument document = JsonDocument.Parse(server_config_json)) @@ -157,7 +289,14 @@ public static string File_Servers() { try { - string server_config_json = File.ReadAllText(Application_Paths.program_data_server_config_json); + string server_config_json = LoadAndDecryptConfig(); + + if (server_config_json == null) + { + Logging.Error("Server_Config_Handler", "File_Servers", "Failed to load server config."); + return "error"; + } + Logging.Debug("Server_Config_Handler", "Server_Config_Handler.Load (server_config_json)", server_config_json); // Parse the JSON using (JsonDocument document = JsonDocument.Parse(server_config_json)) @@ -178,7 +317,14 @@ public static string Tenant_Guid() { try { - string server_config_json = File.ReadAllText(Application_Paths.program_data_server_config_json); + string server_config_json = LoadAndDecryptConfig(); + + if (server_config_json == null) + { + Logging.Error("Server_Config_Handler", "Tenant_Guid", "Failed to load server config."); + return "error"; + } + Logging.Debug("Server_Config_Handler", "Server_Config_Handler.Load (server_config_json)", server_config_json); // Parse the JSON using (JsonDocument document = JsonDocument.Parse(server_config_json)) @@ -199,7 +345,14 @@ public static string Location_Guid() { try { - string server_config_json = File.ReadAllText(Application_Paths.program_data_server_config_json); + string server_config_json = LoadAndDecryptConfig(); + + if (server_config_json == null) + { + Logging.Error("Server_Config_Handler", "Location_Guid", "Failed to load server config."); + return "error"; + } + Logging.Debug("Server_Config_Handler", "Server_Config_Handler.Load (server_config_json)", server_config_json); // Parse the JSON using (JsonDocument document = JsonDocument.Parse(server_config_json)) @@ -220,7 +373,14 @@ public static string Language() { try { - string server_config_json = File.ReadAllText(Application_Paths.program_data_server_config_json); + string server_config_json = LoadAndDecryptConfig(); + + if (server_config_json == null) + { + Logging.Error("Server_Config_Handler", "Language", "Failed to load server config."); + return "error"; + } + Logging.Debug("Server_Config_Handler", "Server_Config_Handler.Load (server_config_json)", server_config_json); // Parse the JSON using (JsonDocument document = JsonDocument.Parse(server_config_json)) @@ -241,7 +401,14 @@ public static bool Authorized() { try { - string serverConfigJson = File.ReadAllText(Application_Paths.program_data_server_config_json); + string serverConfigJson = LoadAndDecryptConfig(); + + if (serverConfigJson == null) + { + Logging.Error("Server_Config_Handler", "Authorized", "Failed to load server config."); + return false; + } + Logging.Debug("Server_Config_Handler", "Authorized", serverConfigJson); // Parse the JSON @@ -267,14 +434,20 @@ public static bool Authorized() } } - public static string Access_Key() { try { string access_key = String.Empty; - string server_config_json = File.ReadAllText(Application_Paths.program_data_server_config_json); + string server_config_json = LoadAndDecryptConfig(); + + if (server_config_json == null) + { + Logging.Error("Server_Config_Handler", "Access_Key", "Failed to load server config."); + return String.Empty; + } + Logging.Debug("Server_Config_Handler", "Server_Config_Handler.Load (server_config_json)", server_config_json); // Parse the JSON using (JsonDocument document = JsonDocument.Parse(server_config_json)) @@ -314,8 +487,8 @@ public static string Access_Key() string json = JsonSerializer.Serialize(jsonObject, new JsonSerializerOptions { WriteIndented = true }); Logging.Debug("Online_Mode.Handler.Update_Device_Information", "json", json); - // Write the new server config JSON to the file - File.WriteAllText(Application_Paths.program_data_server_config_json, json); + // Write the new server config JSON to the file (encrypted) + SaveEncryptedConfig(json); } else { @@ -332,3 +505,4 @@ public static string Access_Key() } } } + diff --git a/NetLock RMM Agent Remote/Linux/Helper/Bash.cs b/NetLock RMM Agent Remote/Linux/Helper/Bash.cs index 19db333e..00dfd8b6 100644 --- a/NetLock RMM Agent Remote/Linux/Helper/Bash.cs +++ b/NetLock RMM Agent Remote/Linux/Helper/Bash.cs @@ -11,25 +11,42 @@ namespace Linux.Helper { internal class Bash { - public static string Execute_Script(string type, bool decode, string script) + public static string Execute_Script(string type, bool decode, string script, int timeout = 0) // timeout in minutes { + Process process = null; + try { Health.Check_Directories(); Logging.Debug("Linux.Helper.Bash.Execute_Script", "Executing script", $"type: {type}, script length: {script.Length}"); + // Set timeout to 60 minutes if no timeout is set, otherwise convert minutes to milliseconds + if (timeout == 0) + timeout = 3600000; // 60 minutes in milliseconds + else + timeout = timeout * 60 * 1000; // Convert minutes to milliseconds + if (String.IsNullOrEmpty(script)) { - Logging.Error("Linux.Helper.Bash.Execute_Script", "Script is empty", ""); - return "-"; + Logging.Error("Linux.Helper.Bash.Execute_Script", "Script is empty", String.Empty); + return "Error: Script is empty"; } // Decode the script from Base64 + string decoded_script; if (decode) { - byte[] script_data = Convert.FromBase64String(script); - string decoded_script = Encoding.UTF8.GetString(script_data); + try + { + byte[] script_data = Convert.FromBase64String(script); + decoded_script = Encoding.UTF8.GetString(script_data); + } + catch (FormatException ex) + { + Logging.Error("Linux.Helper.Bash.Execute_Script", "Invalid Base64 script", ex.Message); + return "Error: Invalid Base64 encoding"; + } // Convert Windows line endings (\r\n) to Unix line endings (\n) script = decoded_script.Replace("\r\n", "\n"); @@ -37,103 +54,111 @@ public static string Execute_Script(string type, bool decode, string script) Logging.Debug("Linux.Helper.Bash.Execute_Script", "Decoded script", script); } + if (String.IsNullOrWhiteSpace(script)) + { + Logging.Error("Linux.Helper.Bash.Execute_Script", "Script is empty after decoding", String.Empty); + return "Error: Decoded script is empty"; + } + // Create a new process - using (Process process = new Process()) + process = new Process(); + process.StartInfo.FileName = "/bin/bash"; + process.StartInfo.Arguments = "-s"; // Read script from standard input + process.StartInfo.UseShellExecute = false; + process.StartInfo.RedirectStandardInput = true; + process.StartInfo.RedirectStandardOutput = true; + process.StartInfo.RedirectStandardError = true; + process.StartInfo.CreateNoWindow = true; + + // Use StringBuilder for better performance when reading large outputs + StringBuilder output = new StringBuilder(); + StringBuilder error = new StringBuilder(); + + // Asynchronous output reading to avoid deadlocks + process.OutputDataReceived += (sender, e) => { - process.StartInfo.FileName = "/bin/bash"; - process.StartInfo.Arguments = "-s"; // Read script from standard input - process.StartInfo.UseShellExecute = false; - process.StartInfo.RedirectStandardInput = true; - process.StartInfo.RedirectStandardOutput = true; - process.StartInfo.RedirectStandardError = true; - process.StartInfo.CreateNoWindow = true; - - // Start the process - process.Start(); - - // Write the cleaned script to the process's standard input and close it immediately - using (StreamWriter writer = process.StandardInput) - { - writer.Write(script); - } - // StandardInput is automatically closed when the StreamWriter is disposed + if (e.Data != null) + output.AppendLine(e.Data); + }; - // Capture the streams before they can be disposed - var stdout = process.StandardOutput; - var stderr = process.StandardError; + process.ErrorDataReceived += (sender, e) => + { + if (e.Data != null) + error.AppendLine(e.Data); + }; - // Use async reading with timeout to prevent hanging - var outputTask = Task.Run(() => stdout.ReadToEnd()); - var errorTask = Task.Run(() => stderr.ReadToEnd()); + // Start the process + process.Start(); + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); - // Wait for process to exit with timeout (5 minutes instead of 1 day) - const int timeoutMs = 300000; // 5 minutes - bool processExited = process.WaitForExit(timeoutMs); + // Write the cleaned script to the process's standard input and close it immediately + using (StreamWriter writer = process.StandardInput) + { + writer.Write(script); + } + // StandardInput is automatically closed when the StreamWriter is disposed - string output = ""; - string error = ""; + // Wait for process to exit with timeout + bool exited = process.WaitForExit(timeout); - if (processExited) - { - // Process exited normally, get the results - try - { - if (outputTask.Wait(5000)) // Wait max 5 seconds for output reading - output = outputTask.Result; - else - output = "Output reading timed out"; - } - catch (Exception) - { - output = "Error reading output"; - } - - try - { - if (errorTask.Wait(5000)) // Wait max 5 seconds for error reading - error = errorTask.Result; - else - error = "Error reading timed out"; - } - catch (Exception) - { - error = "Error reading stderr"; - } - } - else - { - // Process didn't exit within timeout, kill it - try - { - process.Kill(); - process.WaitForExit(5000); // Give it 5 seconds to clean up - } - catch (Exception) - { - // Ignore errors when killing the process - } - - return "Error: Script execution timed out (5 minutes). The script may have been waiting for user input or was stuck in an infinite loop."; - } + if (!exited) + { + process.Kill(true); + process.WaitForExit(); // Ensure async streams are flushed + string timeoutMessage = $"Error: Script execution timed out after {timeout / 60000} minutes."; + Logging.Error("Linux.Helper.Bash.Execute_Script", "Script execution timed out", $"Timeout: {timeout}ms"); + return timeoutMessage; + } - // Log the output and error - if (!string.IsNullOrEmpty(error)) + // Wait for async output reading to complete + process.WaitForExit(); + + string result = output.ToString(); + string errorOutput = error.ToString(); + + if (!String.IsNullOrWhiteSpace(errorOutput)) + { + Logging.Error("Linux.Helper.Bash.Execute_Script", "Script produced error output", errorOutput); + result += Environment.NewLine + "STDERR: " + errorOutput; + } + + int exitCode = process.ExitCode; + if (exitCode != 0) + { + Logging.Error("Linux.Helper.Bash.Execute_Script", $"Script exited with code {exitCode}", errorOutput); + result += Environment.NewLine + $"Exit Code: {exitCode}"; + } + else + { + Logging.PowerShell("Linux.Helper.Bash.Execute_Script", "Script execution successful", $"Exit code: {exitCode}"); + } + + return result; + } + catch (Exception ex) + { + Logging.Error("Linux.Helper.Bash.Execute_Script", "Failed executing script. Type: " + type, ex.ToString()); + return "Error: " + ex.Message; + } + finally + { + // Ensure process is disposed + if (process != null) + { + try { - Logging.PowerShell("Linux.Helper.Bash.Execute_Script", "Script error output", error); - return "Output: " + Environment.NewLine + output + Environment.NewLine + Environment.NewLine + "More output: " + Environment.NewLine + error; + if (!process.HasExited) + process.Kill(true); + + process.Dispose(); } - else + catch { - Logging.PowerShell("Linux.Helper.Bash.Execute_Script", "Command executed successfully", Environment.NewLine + "Result:" + output); - return output; + // Ignore disposal errors } } } - catch (Exception ex) - { - Logging.Error("Linux.Helper.Bash.Execute_Script", "Error executing script", ex.ToString()); - return ex.Message; - } } } } diff --git a/NetLock RMM Agent Remote/MacOS/Helper/Zsh.cs b/NetLock RMM Agent Remote/MacOS/Helper/Zsh.cs index fbd3e249..4713b17f 100644 --- a/NetLock RMM Agent Remote/MacOS/Helper/Zsh.cs +++ b/NetLock RMM Agent Remote/MacOS/Helper/Zsh.cs @@ -1,4 +1,4 @@ -ο»Ώusing Global.Helper; +ο»Ώο»Ώusing Global.Helper; using System; using System.Collections.Generic; using System.Diagnostics; @@ -10,23 +10,40 @@ namespace MacOS.Helper { internal class Zsh { - public static string Execute_Script(string type, bool decode, string script) + public static string Execute_Script(string type, bool decode, string script, int timeout = 0) // timeout in minutes { + Process process = null; + try { Logging.Debug("MacOS.Helper.Zsh.Execute_Script", "Executing script", $"type: {type}, script length: {script.Length}"); + // Set timeout to 60 minutes if no timeout is set, otherwise convert minutes to milliseconds + if (timeout == 0) + timeout = 3600000; // 60 minutes in milliseconds + else + timeout = timeout * 60 * 1000; // Convert minutes to milliseconds + if (String.IsNullOrEmpty(script)) { - Logging.Error("MacOS.Helper.Zsh.Execute_Script", "Script is empty", ""); - return "-"; + Logging.Error("MacOS.Helper.Zsh.Execute_Script", "Script is empty", String.Empty); + return "Error: Script is empty"; } // Decode the script from Base64 + string decoded_script; if (decode) { - byte[] script_data = Convert.FromBase64String(script); - string decoded_script = Encoding.UTF8.GetString(script_data); + try + { + byte[] script_data = Convert.FromBase64String(script); + decoded_script = Encoding.UTF8.GetString(script_data); + } + catch (FormatException ex) + { + Logging.Error("MacOS.Helper.Zsh.Execute_Script", "Invalid Base64 script", ex.Message); + return "Error: Invalid Base64 encoding"; + } // Convert Windows line endings (\r\n) to Unix line endings (\n) script = decoded_script.Replace("\r\n", "\n"); @@ -34,103 +51,111 @@ public static string Execute_Script(string type, bool decode, string script) Logging.Debug("MacOS.Helper.Zsh.Execute_Script", "Decoded script", script); } + if (String.IsNullOrWhiteSpace(script)) + { + Logging.Error("MacOS.Helper.Zsh.Execute_Script", "Script is empty after decoding", String.Empty); + return "Error: Decoded script is empty"; + } + // Create a new process - using (Process process = new Process()) + process = new Process(); + process.StartInfo.FileName = "/bin/zsh"; + process.StartInfo.Arguments = "-s"; // Read script from standard input + process.StartInfo.UseShellExecute = false; + process.StartInfo.RedirectStandardInput = true; + process.StartInfo.RedirectStandardOutput = true; + process.StartInfo.RedirectStandardError = true; + process.StartInfo.CreateNoWindow = true; + + // Use StringBuilder for better performance when reading large outputs + StringBuilder output = new StringBuilder(); + StringBuilder error = new StringBuilder(); + + // Asynchronous output reading to avoid deadlocks + process.OutputDataReceived += (sender, e) => { - process.StartInfo.FileName = "/bin/zsh"; - process.StartInfo.Arguments = "-s"; // Read script from standard input - process.StartInfo.UseShellExecute = false; - process.StartInfo.RedirectStandardInput = true; - process.StartInfo.RedirectStandardOutput = true; - process.StartInfo.RedirectStandardError = true; - process.StartInfo.CreateNoWindow = true; - - // Start the process - process.Start(); - - // Write the cleaned script to the process's standard input and close it immediately - using (StreamWriter writer = process.StandardInput) - { - writer.Write(script); - } - // StandardInput is automatically closed when the StreamWriter is disposed + if (e.Data != null) + output.AppendLine(e.Data); + }; - // Capture the streams before they can be disposed - var stdout = process.StandardOutput; - var stderr = process.StandardError; + process.ErrorDataReceived += (sender, e) => + { + if (e.Data != null) + error.AppendLine(e.Data); + }; - // Use async reading with timeout to prevent hanging - var outputTask = Task.Run(() => stdout.ReadToEnd()); - var errorTask = Task.Run(() => stderr.ReadToEnd()); + // Start the process + process.Start(); + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); - // Wait for process to exit with timeout (5 minutes instead of 1 day) - const int timeoutMs = 300000; // 5 minutes - bool processExited = process.WaitForExit(timeoutMs); + // Write the cleaned script to the process's standard input and close it immediately + using (StreamWriter writer = process.StandardInput) + { + writer.Write(script); + } + // StandardInput is automatically closed when the StreamWriter is disposed - string output = ""; - string error = ""; + // Wait for process to exit with timeout + bool exited = process.WaitForExit(timeout); - if (processExited) - { - // Process exited normally, get the results - try - { - if (outputTask.Wait(5000)) // Wait max 5 seconds for output reading - output = outputTask.Result; - else - output = "Output reading timed out"; - } - catch (Exception) - { - output = "Error reading output"; - } - - try - { - if (errorTask.Wait(5000)) // Wait max 5 seconds for error reading - error = errorTask.Result; - else - error = "Error reading timed out"; - } - catch (Exception) - { - error = "Error reading stderr"; - } - } - else - { - // Process didn't exit within timeout, kill it - try - { - process.Kill(); - process.WaitForExit(5000); // Give it 5 seconds to clean up - } - catch (Exception) - { - // Ignore errors when killing the process - } - - return "Error: Script execution timed out (5 minutes). The script may have been waiting for user input or was stuck in an infinite loop."; - } + if (!exited) + { + process.Kill(true); + process.WaitForExit(); // Ensure async streams are flushed + string timeoutMessage = $"Error: Script execution timed out after {timeout / 60000} minutes."; + Logging.Error("MacOS.Helper.Zsh.Execute_Script", "Script execution timed out", $"Timeout: {timeout}ms"); + return timeoutMessage; + } + + // Wait for async output reading to complete + process.WaitForExit(); + + string result = output.ToString(); + string errorOutput = error.ToString(); + + if (!String.IsNullOrWhiteSpace(errorOutput)) + { + Logging.Error("MacOS.Helper.Zsh.Execute_Script", "Script produced error output", errorOutput); + result += Environment.NewLine + "STDERR: " + errorOutput; + } + + int exitCode = process.ExitCode; + if (exitCode != 0) + { + Logging.Error("MacOS.Helper.Zsh.Execute_Script", $"Script exited with code {exitCode}", errorOutput); + result += Environment.NewLine + $"Exit Code: {exitCode}"; + } + else + { + Logging.PowerShell("MacOS.Helper.Zsh.Execute_Script", "Script execution successful", $"Exit code: {exitCode}"); + } - // Log the output and error - if (!string.IsNullOrEmpty(error)) + return result; + } + catch (Exception ex) + { + Logging.Error("MacOS.Helper.Zsh.Execute_Script", "Failed executing script. Type: " + type, ex.ToString()); + return "Error: " + ex.Message; + } + finally + { + // Ensure process is disposed + if (process != null) + { + try { - Logging.PowerShell("MacOS.Helper.Zsh.Execute_Script", "Script error output", error); - return "Output: " + Environment.NewLine + output + Environment.NewLine + Environment.NewLine + "More output: " + Environment.NewLine + error; + if (!process.HasExited) + process.Kill(true); + + process.Dispose(); } - else + catch { - Logging.PowerShell("MacOS.Helper.Zsh.Execute_Script", "Command executed successfully", Environment.NewLine + "Result:" + output); - return output; + // Ignore disposal errors } } } - catch (Exception ex) - { - Logging.Error("MacOS.Helper.Zsh.Execute_Script", "Error executing script", ex.ToString()); - return ex.Message; - } } } } diff --git a/NetLock RMM Agent Remote/Remote_Worker.cs b/NetLock RMM Agent Remote/Remote_Worker.cs index 19b4c6cb..1a7592b8 100644 --- a/NetLock RMM Agent Remote/Remote_Worker.cs +++ b/NetLock RMM Agent Remote/Remote_Worker.cs @@ -12,6 +12,7 @@ using System.Globalization; using Windows.Helper.ScreenControl; using System.Runtime.InteropServices; +using Windows.Helper; using Global.Configuration; using Global.Encryption; @@ -66,6 +67,9 @@ public class Remote_Worker : BackgroundService private bool _agentSettingsRemoteScreenControlUnattendedAccess = false; private bool _remoteScreenControlAccessGranted = false; private List _remoteScreenControlGrantedUsers = new List(); + + // Process monitoring flags + private bool _isCheckingUserProcesses = false; public class Device_Identity { @@ -113,7 +117,7 @@ public class Command_Entity public string remote_control_mouse_xyz { get; set; } public string remote_control_keyboard_input { get; set; } public string remote_control_keyboard_content { get; set; } - public string command { get; set; } // used for service, task manager, screen capture. A command can either be a quick command like "list" or a json string with parameters, a number or json string + public string command { get; set; } // used for service, task manager, screen capture. A command can either be a quick command like "list" or a json string with parameters, a number or json string. Can also be used to transfer command details for other command types } protected override async Task ExecuteAsync(CancellationToken stoppingToken) @@ -125,15 +129,14 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) if (firstRun) { firstRun = false; -if (Agent.debug_mode) - Logging.Debug("Service.ExecuteAsync", "Service is starting...", "Information"); - + + if (Agent.debug_mode) + Logging.Debug("Service.ExecuteAsync", "Service is starting...", "Information"); try { -if (Agent.debug_mode) - Logging.Debug("Service.OnStart", "Service started", "Information"); - + if (Agent.debug_mode) + Logging.Debug("Service.OnStart", "Service started", "Information"); await LoadServerConfig(); @@ -149,7 +152,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) // Start the timer to check the user process status every 1 minute user_process_monitoringCheckTimer = new Timer(async (e) => await CheckUserProcessStatus(), null, - TimeSpan.Zero, TimeSpan.FromMinutes(1)); + TimeSpan.Zero, TimeSpan.FromSeconds(30)); // Start the timer to check the tray icon process status every 1 minute //tray_icon_process_monitoringCheckTimer = new Timer(async (e) => await CheckTrayIconProcessStatus(), null, @@ -165,14 +168,17 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) // Establishing a connection to the local server _ = Task.Run(async () => await Local_Server_Connect()); // LΓ€uft im Hintergrund - // Check user process status as early as possible without blocking. This helps with device reboot scenarios - _ = Task.Run(async () => await CheckUserProcessStatus()); + // Check user process status with a small delay to allow Windows sessions to stabilize during fast login + _ = Task.Run(async () => + { + await Task.Delay(15000); + await CheckUserProcessStatus(); + }); } catch (Exception ex) { -if (Agent.debug_mode) - Logging.Error("Service.OnStart", "Error during service startup.", ex.ToString()); - + if (Agent.debug_mode) + Logging.Error("Service.OnStart", "Error during service startup.", ex.ToString()); } } @@ -369,17 +375,17 @@ public async Task Setup_SignalR() // Check if the device_identity is empty, if so, return if (String.IsNullOrEmpty(device_identity_json)) { -if (Agent.debug_mode) - Logging.Error("Service.Setup_SignalR", "Device identity is empty.", ""); + if (Agent.debug_mode) + Logging.Error("Service.Setup_SignalR", "Device identity is empty.", ""); return; } else Logging.Debug("Service.Setup_SignalR", "Device identity is not empty. Preparing remote connection.", ""); -if (Agent.debug_mode) - Logging.Debug("Service.Setup_SignalR", "Device identity JSON", device_identity_json); - + + if (Agent.debug_mode) + Logging.Debug("Service.Setup_SignalR", "Device identity JSON", device_identity_json); // Deserialise device identity var jsonDocument = JsonDocument.Parse(device_identity_json); @@ -390,8 +396,8 @@ public async Task Setup_SignalR() if (remote_server_client != null) { -if (Agent.debug_mode) - Logging.Debug("Service.Setup_SignalR", "Disposing existing remote server client.", ""); + if (Agent.debug_mode) + Logging.Debug("Service.Setup_SignalR", "Disposing existing remote server client.", ""); await remote_server_client.StopAsync(); await remote_server_client.DisposeAsync(); @@ -410,16 +416,14 @@ public async Task Setup_SignalR() }).ConfigureLogging(logging => { if (OperatingSystem.IsWindows()) -if (Agent.debug_mode) - logging.AddEventLog(); - -if (Agent.debug_mode) - - logging.AddConsole(); + if (Agent.debug_mode) + logging.AddEventLog(); -if (Agent.debug_mode) + if (Agent.debug_mode) + logging.AddConsole(); - logging.SetMinimumLevel(LogLevel.Warning); + if (Agent.debug_mode) + logging.SetMinimumLevel(LogLevel.Warning); }) .WithAutomaticReconnect() @@ -428,8 +432,8 @@ public async Task Setup_SignalR() // Handle ConnectionEstablished event from server remote_server_client.On("ConnectionEstablished", (message) => { -if (Agent.debug_mode) - Logging.Debug("Service.Setup_SignalR", "ConnectionEstablished with message", message); + if (Agent.debug_mode) + Logging.Debug("Service.Setup_SignalR", "ConnectionEstablished with message", message); // Connection established, no further action needed }); @@ -437,18 +441,17 @@ public async Task Setup_SignalR() // Handle ConnectionEstablished event from server - without parameter remote_server_client.On("ConnectionEstablished", () => { -if (Agent.debug_mode) - Logging.Debug("Service.Setup_SignalR", "ConnectionEstablished without message", ""); + if (Agent.debug_mode) + Logging.Debug("Service.Setup_SignalR", "ConnectionEstablished without message", ""); // Connection established, no further action needed }); remote_server_client.On("SendMessageToClient", async (command) => { -if (Agent.debug_mode) - Logging.Debug("Service.Setup_SignalR", "SendMessageToClient", command); - - + if (Agent.debug_mode) + Logging.Debug("Service.Setup_SignalR", "SendMessageToClient", command); + // Deserialisation of the entire JSON string Command_Entity command_object = JsonSerializer.Deserialize(command); @@ -562,10 +565,10 @@ public async Task Setup_SignalR() // Convert the object into a JSON string string json = JsonSerializer.Serialize(jsonObject, new JsonSerializerOptions { WriteIndented = true }); -if (Agent.debug_mode) - Logging.Debug("Service.Setup_SignalR", "Remote Control json", json); - - + + if (Agent.debug_mode) + Logging.Debug("Service.Setup_SignalR", "Remote Control json", json); + // Send through local server to tray icon user process await SendToClient(command_object.remote_control_username + "tray", json); @@ -578,9 +581,8 @@ public async Task Setup_SignalR() } catch (Exception ex) { -if (Agent.debug_mode) - Logging.Error("Service.Setup_SignalR", "Failed to deserialize command object.", ex.ToString()); - + if (Agent.debug_mode) + Logging.Error("Service.Setup_SignalR", "Failed to deserialize command object.", ex.ToString()); } // Example: If the command is "sync", send a message to the local server to force a sync with the remote server @@ -591,9 +593,8 @@ public async Task Setup_SignalR() // Receive a message from the remote server, process the command and send a response back to the remote server remote_server_client.On("SendMessageToClientAndWaitForResponse", async (command) => { -if (Agent.debug_mode) - Logging.Debug("Service.Setup_SignalR", "SendMessageToClientAndWaitForResponse", command); - + if (Agent.debug_mode) + Logging.Debug("Service.Setup_SignalR", "SendMessageToClientAndWaitForResponse", command); // Deserialisation of the entire JSON string Command_Entity command_object = JsonSerializer.Deserialize(command); @@ -607,15 +608,16 @@ public async Task Setup_SignalR() { if (OperatingSystem.IsWindows()) result = Windows.Helper.PowerShell.Execute_Script(command_object.type.ToString(), - command_object.powershell_code); + command_object.powershell_code, Convert.ToInt32(command_object.command)); else if (OperatingSystem.IsLinux()) result = Linux.Helper.Bash.Execute_Script("Remote Shell", true, - command_object.powershell_code); + command_object.powershell_code, Convert.ToInt32(command_object.command)); else if (OperatingSystem.IsMacOS()) result = MacOS.Helper.Zsh.Execute_Script("Remote Shell", true, - command_object.powershell_code); -if (Agent.debug_mode) - Logging.Debug("Client", "PowerShell executed", result); + command_object.powershell_code, Convert.ToInt32(command_object.command)); + + if (Agent.debug_mode) + Logging.Debug("Client", "PowerShell executed", result); } else if (command_object.type == 1 && _agentSettingsRemoteFileBrowserEnabled) // File Browser @@ -688,15 +690,15 @@ public async Task Setup_SignalR() device_identity_object.device_name + "&access_key=" + device_identity_object.access_key + "&hwid=" + device_identity_object.hwid; -if (Agent.debug_mode) - Logging.Debug("Service.Setup_SignalR", "Download URL", download_url); - - + + if (Agent.debug_mode) + Logging.Debug("Service.Setup_SignalR", "Download URL", download_url); + result = await Http.DownloadFileAsync(Global.Configuration.Agent.ssl, download_url, command_object.file_browser_path, device_identity_object.package_guid); -if (Agent.debug_mode) - Logging.Debug("Service.Setup_SignalR", "File downloaded", result); - + + if (Agent.debug_mode) + Logging.Debug("Service.Setup_SignalR", "File downloaded", result); } else if (command_object.file_browser_command == 11) // upload file { @@ -709,9 +711,9 @@ public async Task Setup_SignalR() device_identity_object.device_name + "&access_key=" + device_identity_object.access_key + "&hwid=" + device_identity_object.hwid; -if (Agent.debug_mode) - Logging.Debug("Service.Setup_SignalR", "Upload URL", upload_url); + if (Agent.debug_mode) + Logging.Debug("Service.Setup_SignalR", "Upload URL", upload_url); // Upload the file to the server result = await Http.UploadFileAsync(Global.Configuration.Agent.ssl, upload_url, command_object.file_browser_path, @@ -721,10 +723,10 @@ public async Task Setup_SignalR() else if (command_object.type == 2 && _agentSettingsRemoteServiceManagerEnabled) // Service { // Deserialise the command_object.command json, using json document (action, name) -if (Agent.debug_mode) - Logging.Debug("Service.Setup_SignalR", "Service command", command_object.command); - - + + if (Agent.debug_mode) + Logging.Debug("Service.Setup_SignalR", "Service command", command_object.command); + string action = String.Empty; string name = String.Empty; @@ -850,9 +852,8 @@ public async Task Setup_SignalR() } else if (command_object.type == 6 && _agentSettingsRemoteScreenControlEnabled) // Tray Icon - Show chat window { -if (Agent.debug_mode) - Logging.Debug("Service.Setup_SignalR", "Tray icon command", command_object.command); - + if (Agent.debug_mode) + Logging.Debug("Service.Setup_SignalR", "Tray icon command", command_object.command); // Create the JSON object var jsonObject = new @@ -864,9 +865,9 @@ public async Task Setup_SignalR() // Convert the object into a JSON string string json = JsonSerializer.Serialize(jsonObject, new JsonSerializerOptions { WriteIndented = true }); -if (Agent.debug_mode) - Logging.Debug("Service.Setup_SignalR", "Remote Control json", json); - + + if (Agent.debug_mode) + Logging.Debug("Service.Setup_SignalR", "Remote Control json", json); // Send through local server to tray icon user process await SendToClient(command_object.remote_control_username + "tray", json); @@ -890,10 +891,28 @@ public async Task Setup_SignalR() { _remoteScreenControlGrantedUsers.Add(command_object.remote_control_username); result = "accepted"; + + // Disable windows fast logon to prevent issues with cached credentials + if (OperatingSystem.IsWindows()) + { + // HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Winlogon + + // SyncForegroundPolicy = 1 (disables async logon) + Registry.HKLM_Write_Value(@"Software\Microsoft\Windows NT\CurrentVersion\Winlogon", "SyncForegroundPolicy", "1"); + } } else { - + // If windows fast logon is disabled, enable it back to previous state + if (OperatingSystem.IsWindows()) + { + // HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Winlogon + + // SyncForegroundPolicy = 0 or delete (enables async logon) + Registry.HKLM_Delete_Value(@"Software\Microsoft\Windows NT\CurrentVersion\Winlogon", "SyncForegroundPolicy"); + } + + // Send request to tray icon to show chat window var jsonObject = new { response_id = command_object.response_id, @@ -902,9 +921,9 @@ public async Task Setup_SignalR() }; string json = JsonSerializer.Serialize(jsonObject, new JsonSerializerOptions { WriteIndented = true }); -if (Agent.debug_mode) - Logging.Debug("Service.Setup_SignalR", "Remote Control access request json", json); - + + if (Agent.debug_mode) + Logging.Debug("Service.Setup_SignalR", "Remote Control access request json", json); await SendToClient(command_object.remote_control_username + "tray", json); @@ -948,6 +967,219 @@ await remote_server_client.InvokeAsync("ReceiveClientResponse", command_object.r return; // Return here to prevent sending a second response } + else if (command_object.type == 10) // Event Log - Simple Commands (Windows only) + { + if (!OperatingSystem.IsWindows()) + { + result = JsonSerializer.Serialize(new + { + success = false, + error = "Event Log is only available on Windows", + timestamp = DateTime.UtcNow.ToString("o") + }, new JsonSerializerOptions { WriteIndented = true }); + } + else + { + try + { + // command_object.command contains the event log command as a simple string ("1", "3", "4") + int eventLogCommand = Convert.ToInt32(command_object.command); + + if (Agent.debug_mode) + Logging.Debug("Service.Setup_SignalR", "Event Log command", eventLogCommand.ToString()); + + if (eventLogCommand == 1) // Get available event logs + { + result = Eventlog.GetAvailableEventLogs(); + } + else if (eventLogCommand == 3) // Get event log stats (requires log_name in future) + { + // Default to Application log for now + string logName = "Application"; + result = Eventlog.GetEventLogStats(logName); + } + else if (eventLogCommand == 4) // Clear event log (requires log_name in future) + { + // Default to Application log for now + string logName = "Application"; + result = Eventlog.ClearEventLog(logName); + } + else + { + result = JsonSerializer.Serialize(new + { + success = false, + error = $"Unknown event log command: {eventLogCommand}. Use Type 11 for reading event logs.", + timestamp = DateTime.UtcNow.ToString("o") + }, new JsonSerializerOptions { WriteIndented = true }); + } + + if (Agent.debug_mode) + Logging.Debug("Service.Setup_SignalR", "Event Log result", result); + } + catch (Exception ex) + { + Logging.Error("Service.Setup_SignalR", "Failed to execute event log command", ex.ToString()); + result = JsonSerializer.Serialize(new + { + success = false, + error = ex.Message, + timestamp = DateTime.UtcNow.ToString("o") + }, new JsonSerializerOptions { WriteIndented = true }); + } + } + } + else if (command_object.type == 11) // Event Log - Read Event Log with Parameters (Windows only) + { + if (!OperatingSystem.IsWindows()) + { + result = JsonSerializer.Serialize(new + { + success = false, + error = "Event Log is only available on Windows", + timestamp = DateTime.UtcNow.ToString("o") + }, new JsonSerializerOptions { WriteIndented = true }); + } + else + { + try + { + // Parse the command JSON to extract parameters for reading event logs + using (JsonDocument doc = JsonDocument.Parse(command_object.command)) + { + JsonElement root = doc.RootElement; + + string logName = root.TryGetProperty("log_name", out var logNameElement) + ? logNameElement.GetString() ?? "Application" + : "Application"; + + int maxEntries = root.TryGetProperty("max_entries", out var maxEntriesElement) + ? maxEntriesElement.GetInt32() + : 100; + + byte? level = null; + if (root.TryGetProperty("level", out var levelElement) && levelElement.ValueKind != JsonValueKind.Null) + level = levelElement.GetByte(); + + int? eventId = null; + if (root.TryGetProperty("event_id", out var eventIdElement) && eventIdElement.ValueKind != JsonValueKind.Null) + eventId = eventIdElement.GetInt32(); + + DateTime? startTime = null; + if (root.TryGetProperty("start_time", out var startTimeElement) && startTimeElement.ValueKind != JsonValueKind.Null) + { + string startTimeStr = startTimeElement.GetString() ?? string.Empty; + if (!string.IsNullOrEmpty(startTimeStr)) + { + // Support both ISO 8601 and "yyyy-MM-dd HH:mm:ss" formats + if (DateTime.TryParse(startTimeStr, out DateTime parsedStartTime)) + startTime = parsedStartTime; + } + } + + DateTime? endTime = null; + if (root.TryGetProperty("end_time", out var endTimeElement) && endTimeElement.ValueKind != JsonValueKind.Null) + { + string endTimeStr = endTimeElement.GetString() ?? string.Empty; + if (!string.IsNullOrEmpty(endTimeStr)) + { + // Support both ISO 8601 and "yyyy-MM-dd HH:mm:ss" formats + if (DateTime.TryParse(endTimeStr, out DateTime parsedEndTime)) + endTime = parsedEndTime; + } + } + + string? providerName = null; + if (root.TryGetProperty("provider_name", out var providerNameElement) && providerNameElement.ValueKind != JsonValueKind.Null) + { + providerName = providerNameElement.GetString(); + // Treat empty string as null + if (string.IsNullOrWhiteSpace(providerName)) + providerName = null; + } + + if (Agent.debug_mode) + Logging.Debug("Service.Setup_SignalR", "Reading Event Log", + $"LogName: {logName}, MaxEntries: {maxEntries}, Level: {level}, EventId: {eventId}, StartTime: {startTime}, EndTime: {endTime}, Provider: {providerName}"); + + result = Eventlog.ReadEventLog(logName, maxEntries, level, eventId, startTime, endTime, providerName); + } + + if (Agent.debug_mode) + Logging.Debug("Service.Setup_SignalR", "Event Log read result", result); + } + catch (Exception ex) + { + Logging.Error("Service.Setup_SignalR", "Failed to read event log", ex.ToString()); + result = JsonSerializer.Serialize(new + { + success = false, + error = ex.Message, + timestamp = DateTime.UtcNow.ToString("o") + }, new JsonSerializerOptions { WriteIndented = true }); + } + } + } + else if (command_object.type == 12) // Get eventlog stats + { + if (!OperatingSystem.IsWindows()) + { + result = JsonSerializer.Serialize(new + { + success = false, + error = "Event Log is only available on Windows", + timestamp = DateTime.UtcNow.ToString("o") + }, new JsonSerializerOptions { WriteIndented = true }); + } + else + { + try + { + string logName = command_object.command ?? "Application"; + result = Eventlog.GetEventLogStats(logName); + } + catch (Exception ex) + { + Logging.Error("Service.Setup_SignalR", "Failed to get event log stats", ex.ToString()); + result = JsonSerializer.Serialize(new + { + success = false, + error = ex.Message, + timestamp = DateTime.UtcNow.ToString("o") + }, new JsonSerializerOptions { WriteIndented = true }); + } + } + } + else if (command_object.type == 13) // Clear eventlogs + { + if (!OperatingSystem.IsWindows()) + { + result = JsonSerializer.Serialize(new + { + success = false, + error = "Event Log is only available on Windows", + timestamp = DateTime.UtcNow.ToString("o") + }, new JsonSerializerOptions { WriteIndented = true }); + } + else + { + try + { + string logName = command_object.command ?? "Application"; + result = Eventlog.ClearEventLog(logName); + } + catch (Exception ex) + { + Logging.Error("Service.Setup_SignalR", "Failed to clear event log", ex.ToString()); + result = JsonSerializer.Serialize(new + { + success = false, + error = ex.Message, + timestamp = DateTime.UtcNow.ToString("o") + }, new JsonSerializerOptions { WriteIndented = true }); + } + } + } } catch (Exception ex) { @@ -996,43 +1228,205 @@ private async Task CheckUserProcessStatus() { if (!OperatingSystem.IsWindows()) return; - try + // Prevent multiple simultaneous executions + if (_isCheckingUserProcesses) { - bool processIsRunning = false; + if (Agent.debug_mode) + Logging.Debug("Service.CheckUserProcess", "Already checking processes, skipping", ""); + return; + } - var allProcesses = Process.GetProcessesByName("NetLock_RMM_User_Process"); + _isCheckingUserProcesses = true; - // Check if the user process is running - foreach (var process in allProcesses) + try + { + // Hole alle aktiven Sessions (inkl. Anmeldebildschirm) + var activeSessions = Windows.Helper.ScreenControl.WindowsSession.GetActiveSessions(); + + if (Agent.debug_mode) + Logging.Debug("Service.CheckUserProcess", "Active sessions found", $"Count: {activeSessions.Count}"); + + // PrΓΌfe und starte NetLock_RMM_User_Process (System-Kontext) - nur einmal, unabhΓ€ngig von Session + // Dieser Prozess lΓ€uft als SYSTEM und sollte nur einmal existieren + bool systemProcessRunning = false; + var systemProcesses = Process.GetProcessesByName("NetLock_RMM_User_Process"); + + if (Agent.debug_mode) + Logging.Debug("Service.CheckUserProcess", + "Checking system process", + $"Found {systemProcesses.Length} NetLock_RMM_User_Process instances"); + + foreach (var process in systemProcesses) { if (process != null && !process.HasExited) { - processIsRunning = true; + // PrΓΌfe Session-ID - SYSTEM Prozess sollte in Session 0 oder 1 laufen + uint processSessionId = Windows.Helper.ScreenControl.WindowsSession.GetProcessSessionId(process.Id); if (Agent.debug_mode) - Logging.Debug("Service.CheckUserProcess", "User process is running.", $"PID: {process.Id}"); - - break; // User process is running, no action needed + Logging.Debug("Service.CheckUserProcess", + "Found system process instance", + $"PID: {process.Id}, SessionId: {processSessionId}, ProcessName: NetLock_RMM_User_Process"); + + // Wenn wir mindestens einen SYSTEM-Prozess gefunden haben, ist er schon aktiv + systemProcessRunning = true; + break; } } - if (!processIsRunning) + if (!systemProcessRunning) { + if (Agent.debug_mode) + Logging.Debug("Service.CheckUserProcess", + "Starting system process", + $"ProcessName: NetLock_RMM_User_Process, Path: {Application_Paths.netlock_rmm_user_agent_path}"); + bool success = Windows.Helper.ScreenControl.Win32Interop.CreateInteractiveSystemProcess( commandLine: Application_Paths.netlock_rmm_user_agent_path, - targetSessionId: 0, + targetSessionId: 0, // System session hiddenWindow: false, out var procInfo ); + + if (Agent.debug_mode) + Logging.Debug("Service.CheckUserProcess", + "System process start result", + $"Success: {success}, ProcessName: NetLock_RMM_User_Process"); + + // Warte kurz damit der Prozess Zeit hat zu starten + if (success) + await Task.Delay(500); + } + else if (Agent.debug_mode) + { + Logging.Debug("Service.CheckUserProcess", + "System process already running, skipping start", + $"ProcessName: NetLock_RMM_User_Process"); + } + + // PrΓΌfe und starte User-Prozesse fΓΌr jede angemeldete Session + foreach (var sessionId in activeSessions) + { + // PrΓΌfe ob ein Benutzer angemeldet ist + bool isUserLoggedIn = Windows.Helper.ScreenControl.WindowsSession.IsUserLoggedIntoSession(sessionId); + + // Nur fΓΌr angemeldete User (nicht fΓΌr Login-Bildschirm/Session 0) + if (!isUserLoggedIn || sessionId == 0) + continue; + + // FΓΌr angemeldete User: Starte NetLock_RMM_User_Process_UAC im User-Kontext + string processName = "NetLock_RMM_User_Process_UAC"; + string processPath = Application_Paths.netlock_rmm_user_agent_uac_path; + + bool processIsRunning = false; + var allProcesses = Process.GetProcessesByName(processName); + + // PrΓΌfe, ob der Prozess bereits in dieser Session lΓ€uft + foreach (var process in allProcesses) + { + if (process != null && !process.HasExited) + { + uint processSessionId = Windows.Helper.ScreenControl.WindowsSession.GetProcessSessionId(process.Id); + + if (processSessionId == sessionId) + { + processIsRunning = true; + + if (Agent.debug_mode) + Logging.Debug("Service.CheckUserProcess", + "User process is running.", + $"PID: {process.Id}, SessionId: {sessionId}, ProcessName: {processName}"); + break; + } + } + } + + // Starte Prozess nur wenn er in dieser Session nicht lΓ€uft + if (!processIsRunning) + { + if (Agent.debug_mode) + Logging.Debug("Service.CheckUserProcess", + "Starting user process in session", + $"SessionId: {sessionId}, ProcessName: {processName}, Path: {processPath}"); + + // FΓΌr angemeldete User: Starte im User-Kontext + bool success = Windows.Helper.ScreenControl.Win32Interop.CreateProcessInUserSession( + commandLine: processPath, + targetSessionId: (int)sessionId, + hiddenWindow: false, + out var procInfo + ); + + if (Agent.debug_mode) + Logging.Debug("Service.CheckUserProcess", + "Process start result", + $"Success: {success}, SessionId: {sessionId}, ProcessName: {processName}"); + + // Warte kurz damit der Prozess Zeit hat zu starten + if (success) + await Task.Delay(500); + } + + // PrΓΌfe und starte Tray Icon fΓΌr diese User-Session + bool trayIconRunning = false; + var trayIconProcesses = Process.GetProcessesByName("NetLock_RMM_Tray_Icon"); + + foreach (var process in trayIconProcesses) + { + if (process != null && !process.HasExited) + { + uint traySessionId = Windows.Helper.ScreenControl.WindowsSession.GetProcessSessionId(process.Id); + + if (traySessionId == sessionId) + { + trayIconRunning = true; + + if (Agent.debug_mode) + Logging.Debug("Service.CheckUserProcess", + "Tray icon is running.", + $"PID: {process.Id}, SessionId: {sessionId}"); + break; + } + } + } + + if (!trayIconRunning) + { + if (Agent.debug_mode) + Logging.Debug("Service.CheckUserProcess", + "Starting tray icon in session", + $"SessionId: {sessionId}, Path: {Application_Paths.program_files_tray_icon_path}"); + + bool traySuccess = Windows.Helper.ScreenControl.Win32Interop.CreateProcessInUserSession( + commandLine: Application_Paths.program_files_tray_icon_path, + targetSessionId: (int)sessionId, + hiddenWindow: false, + out var trayProcInfo + ); + + if (Agent.debug_mode) + Logging.Debug("Service.CheckUserProcess", + "Tray icon start result", + $"Success: {traySuccess}, SessionId: {sessionId}"); + + if (traySuccess) + await Task.Delay(500); + } } } catch (Exception ex) { - Logging.Error("Service.CheckUserProcess", "Exception while checking or starting user processes.", + Logging.Error("Service.CheckUserProcess", + "Exception while checking or starting user processes.", ex.ToString()); } + finally + { + _isCheckingUserProcesses = false; + } } + #endregion #region Tray Icon Process Monitoring (Windows, Linux & MacOS) @@ -1614,23 +2008,23 @@ private async Task LoadServerConfig() _agentSettingsRemoteServiceManagerEnabled = agentSettings.RemoteServiceManagerEnabled; _agentSettingsRemoteScreenControlEnabled = agentSettings.RemoteScreenControlEnabled; _agentSettingsRemoteScreenControlUnattendedAccess = agentSettings.RemoteScreenControlUnattendedAccess; - + Logging.Debug("Service.LoadServerConfig", "Agent settings loaded", $"RemoteServiceEnabled: {_agentSettingsRemoteServiceEnabled}, RemoteShellEnabled: {_agentSettingsRemoteShellEnabled}, RemoteFileBrowserEnabled: {_agentSettingsRemoteFileBrowserEnabled}, RemoteTaskManagerEnabled: {_agentSettingsRemoteTaskManagerEnabled}, RemoteServiceManagerEnabled: {_agentSettingsRemoteServiceManagerEnabled}, RemoteScreenControlEnabled: {_agentSettingsRemoteScreenControlEnabled}"); } - + // Get access key & authorized state access_key = Global.Initialization.Server_Config.Access_Key(); authorized = Global.Initialization.Server_Config.Authorized(); - + // Read device_identity.json file & decrypt string jsonString = await File.ReadAllTextAsync(Application_Paths.device_identity_json_path); device_identity_json = String_Encryption.Decrypt(jsonString, Application_Settings.NetLock_Local_Encryption_Key); if (Agent.debug_mode) Logging.Debug("Service.LoadServerConfig", "Device identity loaded", device_identity_json); - - // Check servers | We do not want to spam the server with requests here. + + // Check servers | We do not want to spam the server with requests here. if (authorized && !remote_server_status || !file_server_status) await Global.Initialization.Check_Connection.Check_Servers(); } diff --git a/NetLock RMM Agent Remote/Windows/Helper/Eventlog.cs b/NetLock RMM Agent Remote/Windows/Helper/Eventlog.cs new file mode 100644 index 00000000..7f3a5d36 --- /dev/null +++ b/NetLock RMM Agent Remote/Windows/Helper/Eventlog.cs @@ -0,0 +1,587 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.Eventing.Reader; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; +using Global.Helper; + +namespace Windows.Helper; + +public class Eventlog +{ + /// + /// Event Log Entry Model for JSON serialization + /// + public class EventLogEntry + { + [JsonPropertyName("index")] + public long Index { get; set; } + + [JsonPropertyName("time_created")] + public string TimeCreated { get; set; } + + [JsonPropertyName("event_id")] + public int? EventId { get; set; } + + [JsonPropertyName("level")] + public string Level { get; set; } + + [JsonPropertyName("level_value")] + public byte? LevelValue { get; set; } + + [JsonPropertyName("provider_name")] + public string ProviderName { get; set; } + + [JsonPropertyName("source")] + public string Source { get; set; } + + [JsonPropertyName("computer")] + public string Computer { get; set; } + + [JsonPropertyName("user")] + public string User { get; set; } + + [JsonPropertyName("task_category")] + public string TaskCategory { get; set; } + + [JsonPropertyName("task_display_name")] + public string TaskDisplayName { get; set; } + + [JsonPropertyName("opcode")] + public string Opcode { get; set; } + + [JsonPropertyName("opcode_display_name")] + public string OpcodeDisplayName { get; set; } + + [JsonPropertyName("keywords")] + public List Keywords { get; set; } + + [JsonPropertyName("keywords_display_names")] + public List KeywordsDisplayNames { get; set; } + + [JsonPropertyName("message")] + public string Message { get; set; } + + [JsonPropertyName("process_id")] + public int? ProcessId { get; set; } + + [JsonPropertyName("thread_id")] + public int? ThreadId { get; set; } + + [JsonPropertyName("activity_id")] + public string ActivityId { get; set; } + + [JsonPropertyName("related_activity_id")] + public string RelatedActivityId { get; set; } + + [JsonPropertyName("record_id")] + public long? RecordId { get; set; } + + [JsonPropertyName("log_name")] + public string LogName { get; set; } + } + + /// + /// Response model for event log queries + /// + public class EventLogResponse + { + [JsonPropertyName("success")] + public bool Success { get; set; } + + [JsonPropertyName("log_name")] + public string LogName { get; set; } + + [JsonPropertyName("total_entries")] + public int TotalEntries { get; set; } + + [JsonPropertyName("entries")] + public List Entries { get; set; } + + [JsonPropertyName("error")] + public string Error { get; set; } + + [JsonPropertyName("timestamp")] + public string Timestamp { get; set; } + } + + /// + /// Response model for available event logs list + /// + public class AvailableLogsResponse + { + [JsonPropertyName("success")] + public bool Success { get; set; } + + [JsonPropertyName("total_logs")] + public int TotalLogs { get; set; } + + [JsonPropertyName("log_names")] + public List LogNames { get; set; } + + [JsonPropertyName("error")] + public string Error { get; set; } + + [JsonPropertyName("timestamp")] + public string Timestamp { get; set; } + } + + /// + /// Get available event log names on the system + /// + public static string GetAvailableEventLogs() + { + AvailableLogsResponse response = new AvailableLogsResponse + { + LogNames = new List(), + Timestamp = DateTime.UtcNow.ToString("o") + }; + + try + { + Logging.Debug("Windows.Helper.Eventlog", "GetAvailableEventLogs", "Getting available event logs"); + + using (EventLogSession session = new EventLogSession()) + { + var allLogNames = session.GetLogNames().OrderBy(name => name).ToList(); + var availableLogs = new List(); + + // Filter out disabled or unavailable logs + foreach (var logName in allLogNames) + { + try + { + EventLogConfiguration config = new EventLogConfiguration(logName, session); + + // Only include logs that are enabled and not empty + if (config.IsEnabled) + { + availableLogs.Add(logName); + } + } + catch (EventLogException) + { + // Skip logs that can't be accessed or configured + continue; + } + catch (UnauthorizedAccessException) + { + // Skip logs we don't have permission to access + continue; + } + } + + response.LogNames = availableLogs; + response.Success = true; + response.TotalLogs = response.LogNames.Count; + + Logging.Debug("Windows.Helper.Eventlog", "GetAvailableEventLogs", + $"Found {response.TotalLogs} available event logs (out of {allLogNames.Count} total)"); + } + } + catch (Exception ex) + { + Logging.Error("Windows.Helper.Eventlog", "GetAvailableEventLogs", ex.ToString()); + response.Success = false; + response.Error = ex.Message; + } + + return JsonSerializer.Serialize(response, new JsonSerializerOptions { WriteIndented = true }); + } + + /// + /// Read event log entries from a specific log + /// + /// Name of the event log (e.g., "Application", "System", "Security") + /// Maximum number of entries to return (default: 100, max: 10000) + /// Filter by level: 0=LogAlways, 1=Critical, 2=Error, 3=Warning, 4=Informational, 5=Verbose (null = all) + /// Filter by specific Event ID (null = all) + /// Filter events after this time (null = no filter) + /// Filter events before this time (null = no filter) + /// Filter by provider/source name (null = all) + /// JSON string with event log entries + public static string ReadEventLog( + string logName, + int maxEntries = 100, + byte? level = null, + int? eventId = null, + DateTime? startTime = null, + DateTime? endTime = null, + string providerName = null) + { + EventLogResponse response = new EventLogResponse + { + LogName = logName, + Entries = new List(), + Timestamp = DateTime.UtcNow.ToString("o") + }; + + try + { + // Validate maxEntries + if (maxEntries < 1) maxEntries = 100; + if (maxEntries > 10000) maxEntries = 10000; + + Logging.Debug("Windows.Helper.Eventlog", "ReadEventLog", + $"Reading log: {logName}, maxEntries: {maxEntries}, level: {level}, eventId: {eventId}"); + + // Build XPath query + string query = "*"; + List conditions = new List(); + + if (level.HasValue) + conditions.Add($"Level={level.Value}"); + + if (eventId.HasValue) + conditions.Add($"EventID={eventId.Value}"); + + if (startTime.HasValue) + conditions.Add($"TimeCreated[@SystemTime>='{startTime.Value.ToUniversalTime():o}']"); + + if (endTime.HasValue) + conditions.Add($"TimeCreated[@SystemTime<='{endTime.Value.ToUniversalTime():o}']"); + + if (!string.IsNullOrWhiteSpace(providerName)) + conditions.Add($"@Name='{providerName}'"); + + if (conditions.Count > 0) + query = $"*[System[{string.Join(" and ", conditions)}]]"; + + Logging.Debug("Windows.Helper.Eventlog", "ReadEventLog", $"XPath Query: {query}"); + + // Create query + EventLogQuery eventLogQuery = new EventLogQuery(logName, PathType.LogName, query) + { + ReverseDirection = true // Get newest entries first + }; + + EventLogReader reader = null; + + try + { + // Try to create the reader - this will fail if the log is disabled or not available + reader = new EventLogReader(eventLogQuery); + } + catch (EventLogException ex) + { + // Check for specific error conditions + if (ex.Message.Contains("not supported") || ex.Message.Contains("disabled")) + { + response.Success = false; + response.Error = $"Event log '{logName}' is disabled or not available. Please enable it in Event Viewer or choose a different log."; + Logging.Error("Windows.Helper.Eventlog", "ReadEventLog", response.Error); + return JsonSerializer.Serialize(response, new JsonSerializerOptions { WriteIndented = true }); + } + + // Re-throw other EventLogExceptions to be caught by outer catch + throw; + } + + // Read events + using (reader) + { + EventRecord eventRecord; + long index = 0; + + while ((eventRecord = reader.ReadEvent()) != null && index < maxEntries) + { + try + { + EventLogEntry entry = new EventLogEntry + { + Index = index++, + RecordId = eventRecord.RecordId, + LogName = eventRecord.LogName, + TimeCreated = eventRecord.TimeCreated?.ToUniversalTime().ToString("o") ?? "N/A", + EventId = eventRecord.Id, + Level = GetLevelName(eventRecord.Level), + LevelValue = eventRecord.Level, + ProviderName = eventRecord.ProviderName, + Source = eventRecord.ProviderName, // Source is same as ProviderName + Computer = eventRecord.MachineName, + User = eventRecord.UserId?.Value ?? "N/A", + TaskCategory = eventRecord.Task?.ToString() ?? "N/A", + TaskDisplayName = eventRecord.TaskDisplayName ?? "N/A", + Opcode = eventRecord.Opcode?.ToString() ?? "N/A", + OpcodeDisplayName = eventRecord.OpcodeDisplayName ?? "N/A", + ProcessId = eventRecord.ProcessId, + ThreadId = eventRecord.ThreadId, + ActivityId = eventRecord.ActivityId?.ToString() ?? null, + RelatedActivityId = eventRecord.RelatedActivityId?.ToString() ?? null, + Message = GetEventMessage(eventRecord), + Keywords = GetKeywords(eventRecord.Keywords), + KeywordsDisplayNames = GetKeywordsDisplayNames(eventRecord.KeywordsDisplayNames) + }; + + response.Entries.Add(entry); + } + catch (Exception entryEx) + { + Logging.Error("Windows.Helper.Eventlog", "ReadEventLog", + $"Error reading event entry: {entryEx.Message}"); + } + finally + { + eventRecord.Dispose(); + } + } + } + + response.Success = true; + response.TotalEntries = response.Entries.Count; + + Logging.Debug("Windows.Helper.Eventlog", "ReadEventLog", + $"Successfully read {response.TotalEntries} entries from {logName}"); + } + catch (EventLogNotFoundException ex) + { + response.Success = false; + response.Error = $"Event log '{logName}' not found: {ex.Message}"; + Logging.Error("Windows.Helper.Eventlog", "ReadEventLog", response.Error); + } + catch (UnauthorizedAccessException ex) + { + response.Success = false; + response.Error = $"Access denied to event log '{logName}': {ex.Message}"; + Logging.Error("Windows.Helper.Eventlog", "ReadEventLog", response.Error); + } + catch (Exception ex) + { + response.Success = false; + response.Error = $"Error reading event log: {ex.Message}"; + Logging.Error("Windows.Helper.Eventlog", "ReadEventLog", ex.ToString()); + } + + return JsonSerializer.Serialize(response, new JsonSerializerOptions { WriteIndented = true }); + } + + /// + /// Get human-readable level name from level value + /// + private static string GetLevelName(byte? level) + { + if (!level.HasValue) return "Unknown"; + + return level.Value switch + { + 0 => "LogAlways", + 1 => "Critical", + 2 => "Error", + 3 => "Warning", + 4 => "Information", + 5 => "Verbose", + _ => $"Unknown({level.Value})" + }; + } + + /// + /// Safely get the event message + /// + private static string GetEventMessage(EventRecord eventRecord) + { + try + { + return eventRecord.FormatDescription() ?? "No description available"; + } + catch (EventLogException) + { + // Message template not found, try to construct from properties + try + { + if (eventRecord.Properties != null && eventRecord.Properties.Count > 0) + { + var properties = eventRecord.Properties + .Select(p => p.Value?.ToString() ?? "null") + .ToList(); + return $"Event data: {string.Join(", ", properties)}"; + } + } + catch { } + + return "Description not available (message template missing)"; + } + catch (Exception ex) + { + return $"Error retrieving message: {ex.Message}"; + } + } + + /// + /// Convert keywords bitmask to list of keyword values + /// + private static List GetKeywords(long? keywords) + { + if (!keywords.HasValue || keywords.Value == 0) + return new List(); + + List keywordList = new List(); + long value = keywords.Value; + + // Check each bit + for (int i = 0; i < 64; i++) + { + long bit = 1L << i; + if ((value & bit) != 0) + { + keywordList.Add($"0x{bit:X}"); + } + } + + return keywordList; + } + + /// + /// Get keywords display names as list + /// + private static List GetKeywordsDisplayNames(IEnumerable keywordsDisplayNames) + { + if (keywordsDisplayNames == null) + return new List(); + + return keywordsDisplayNames.Where(k => !string.IsNullOrWhiteSpace(k)).ToList(); + } + + /// + /// Clear an event log (requires administrator privileges) + /// + /// Name of the event log to clear + /// JSON string with result + public static string ClearEventLog(string logName) + { + try + { + Logging.Debug("Windows.Helper.Eventlog", "ClearEventLog", $"Attempting to clear log: {logName}"); + + using (EventLogSession session = new EventLogSession()) + { + session.ClearLog(logName); + + var response = new + { + success = true, + log_name = logName, + message = $"Event log '{logName}' cleared successfully", + timestamp = DateTime.UtcNow.ToString("o") + }; + + Logging.Debug("Windows.Helper.Eventlog", "ClearEventLog", $"Successfully cleared log: {logName}"); + return JsonSerializer.Serialize(response, new JsonSerializerOptions { WriteIndented = true }); + } + } + catch (UnauthorizedAccessException ex) + { + var errorResponse = new + { + success = false, + log_name = logName, + error = $"Access denied. Administrator privileges required: {ex.Message}", + timestamp = DateTime.UtcNow.ToString("o") + }; + Logging.Error("Windows.Helper.Eventlog", "ClearEventLog", errorResponse.error); + return JsonSerializer.Serialize(errorResponse, new JsonSerializerOptions { WriteIndented = true }); + } + catch (Exception ex) + { + var errorResponse = new + { + success = false, + log_name = logName, + error = ex.Message, + timestamp = DateTime.UtcNow.ToString("o") + }; + Logging.Error("Windows.Helper.Eventlog", "ClearEventLog", ex.ToString()); + return JsonSerializer.Serialize(errorResponse, new JsonSerializerOptions { WriteIndented = true }); + } + } + + /// + /// Get statistics about an event log + /// + /// Name of the event log + /// JSON string with log statistics + public static string GetEventLogStats(string logName) + { + try + { + Logging.Debug("Windows.Helper.Eventlog", "GetEventLogStats", $"Getting stats for log: {logName}"); + + using (EventLogSession session = new EventLogSession()) + { + EventLogConfiguration logConfig = new EventLogConfiguration(logName, session); + + // Count entries by level + Dictionary levelCounts = new Dictionary + { + { "Critical", 0 }, + { "Error", 0 }, + { "Warning", 0 }, + { "Information", 0 }, + { "Verbose", 0 } + }; + + DateTime? oldestEntry = null; + DateTime? newestEntry = null; + long totalEntries = 0; + + // Read through events to gather statistics + EventLogQuery query = new EventLogQuery(logName, PathType.LogName, "*"); + using (EventLogReader reader = new EventLogReader(query)) + { + EventRecord eventRecord; + while ((eventRecord = reader.ReadEvent()) != null) + { + totalEntries++; + + if (eventRecord.TimeCreated.HasValue) + { + if (!oldestEntry.HasValue || eventRecord.TimeCreated.Value < oldestEntry.Value) + oldestEntry = eventRecord.TimeCreated.Value; + + if (!newestEntry.HasValue || eventRecord.TimeCreated.Value > newestEntry.Value) + newestEntry = eventRecord.TimeCreated.Value; + } + + string levelName = GetLevelName(eventRecord.Level); + if (levelCounts.ContainsKey(levelName)) + levelCounts[levelName]++; + + eventRecord.Dispose(); + } + } + + var response = new + { + success = true, + log_name = logName, + is_enabled = logConfig.IsEnabled, + log_type = logConfig.LogType.ToString(), + log_mode = logConfig.LogMode.ToString(), + maximum_size_bytes = logConfig.MaximumSizeInBytes, + log_file_path = logConfig.LogFilePath, + total_entries = totalEntries, + oldest_entry = oldestEntry?.ToUniversalTime().ToString("o"), + newest_entry = newestEntry?.ToUniversalTime().ToString("o"), + level_counts = levelCounts, + timestamp = DateTime.UtcNow.ToString("o") + }; + + Logging.Debug("Windows.Helper.Eventlog", "GetEventLogStats", + $"Stats for {logName}: {totalEntries} entries"); + return JsonSerializer.Serialize(response, new JsonSerializerOptions { WriteIndented = true }); + } + } + catch (Exception ex) + { + var errorResponse = new + { + success = false, + log_name = logName, + error = ex.Message, + timestamp = DateTime.UtcNow.ToString("o") + }; + Logging.Error("Windows.Helper.Eventlog", "GetEventLogStats", ex.ToString()); + return JsonSerializer.Serialize(errorResponse, new JsonSerializerOptions { WriteIndented = true }); + } + } +} \ No newline at end of file diff --git a/NetLock RMM Agent Remote/Windows/Helper/PowerShell.cs b/NetLock RMM Agent Remote/Windows/Helper/PowerShell.cs index 696cc01f..ce486e46 100644 --- a/NetLock RMM Agent Remote/Windows/Helper/PowerShell.cs +++ b/NetLock RMM Agent Remote/Windows/Helper/PowerShell.cs @@ -42,7 +42,7 @@ public static string Execute_Command(string type, string command, int timeout) / } } - public static string Execute_Script(string type, string script, int timeout = 360) // timeout in minutes + public static string Execute_Script(string type, string script, int timeout = 0) // timeout in minutes { string path = String.Empty; Process cmd_process = null; @@ -53,9 +53,15 @@ public static string Execute_Script(string type, string script, int timeout = 36 Logging.PowerShell("Helper.Powershell.Execute_Script", "Trying to execute script", type); + // Set timeout to 60 minutes if no timeout is set, otherwise convert minutes to milliseconds + if (timeout == 0) + timeout = 3600000; // 60 minutes in milliseconds + else + timeout = timeout * 60 * 1000; // Convert minutes to milliseconds + if (String.IsNullOrEmpty(script)) { - Logging.Error("Helper.Powershell.Execute_Script", "Script is empty", ""); + Logging.Error("Helper.Powershell.Execute_Script", "Script is empty", String.Empty); return "Error: Script is empty"; } @@ -76,7 +82,7 @@ public static string Execute_Script(string type, string script, int timeout = 36 if (String.IsNullOrWhiteSpace(decoded_script)) { - Logging.Error("Helper.Powershell.Execute_Script", "Decoded script is empty", ""); + Logging.Error("Helper.Powershell.Execute_Script", "Decoded script is empty", String.Empty); return "Error: Decoded script is empty"; } @@ -113,27 +119,19 @@ public static string Execute_Script(string type, string script, int timeout = 36 cmd_process.Start(); cmd_process.BeginOutputReadLine(); cmd_process.BeginErrorReadLine(); - - // Handle timeout - timeout = timeout * 1000 * 60; // Convert minutes to milliseconds + // Wait for process to exit with timeout bool exited = cmd_process.WaitForExit(timeout); - + if (!exited) { - // Process didn't finish in time, kill it - try - { - cmd_process.Kill(true); // Kill entire process tree - Logging.Error("Helper.Powershell.Execute_Script", "Script execution timed out", $"Timeout: {timeout}ms"); - return $"Error: Script execution timed out after {timeout}ms"; - } - catch (Exception killEx) - { - Logging.Error("Helper.Powershell.Execute_Script", "Failed to kill timed out process", killEx.ToString()); - } + cmd_process.Kill(true); + cmd_process.WaitForExit(); // Ensure async streams are flushed + string timeoutMessage = $"Error: Script execution timed out after {timeout / 60000} minutes."; + Logging.Error("Helper.Powershell.Execute_Script", "Script execution timed out", $"Timeout: {timeout}ms"); + return timeoutMessage; } - + // Wait for async output reading to complete cmd_process.WaitForExit(); @@ -183,7 +181,7 @@ public static string Execute_Script(string type, string script, int timeout = 36 } // Clean up temporary script file - if (!String.IsNullOrEmpty(path) && File.Exists(path)) + if (!String.IsNullOrWhiteSpace(path) && File.Exists(path)) { try { diff --git a/NetLock RMM Agent Remote/Windows/Helper/ScreenControl/Kernel32.cs b/NetLock RMM Agent Remote/Windows/Helper/ScreenControl/Kernel32.cs index e5e31b6d..ade2498f 100644 --- a/NetLock RMM Agent Remote/Windows/Helper/ScreenControl/Kernel32.cs +++ b/NetLock RMM Agent Remote/Windows/Helper/ScreenControl/Kernel32.cs @@ -27,6 +27,37 @@ public static class Kernel32 [DllImport("kernel32.dll")] public static extern uint WTSGetActiveConsoleSessionId(); + [DllImport("kernel32.dll")] + public static extern nint GetCurrentProcess(); + + [DllImport("advapi32.dll", SetLastError = true)] + public static extern bool SetTokenInformation( + nint TokenHandle, + TOKEN_INFORMATION_CLASS TokenInformationClass, + ref uint TokenInformation, + uint TokenInformationLength); + + public enum TOKEN_INFORMATION_CLASS + { + TokenUser = 1, + TokenGroups, + TokenPrivileges, + TokenOwner, + TokenPrimaryGroup, + TokenDefaultDacl, + TokenSource, + TokenType, + TokenImpersonationLevel, + TokenStatistics, + TokenRestrictedSids, + TokenSessionId, + TokenGroupsAndPrivileges, + TokenSessionReference, + TokenSandBoxInert, + TokenAuditPolicy, + TokenOrigin + } + /// /// contains information about the current state of both physical and virtual memory, including extended memory /// diff --git a/NetLock RMM Agent Remote/Windows/Helper/ScreenControl/Win32Interop.cs b/NetLock RMM Agent Remote/Windows/Helper/ScreenControl/Win32Interop.cs index 0bc5cf97..76a3de49 100644 --- a/NetLock RMM Agent Remote/Windows/Helper/ScreenControl/Win32Interop.cs +++ b/NetLock RMM Agent Remote/Windows/Helper/ScreenControl/Win32Interop.cs @@ -3,6 +3,7 @@ using System.Runtime.InteropServices; using System.Text; using Global.Helper; +using Global.Configuration; using static Windows.Helper.ScreenControl.ADVAPI32; using static Windows.Helper.ScreenControl.User32; @@ -112,6 +113,10 @@ public static bool CreateInteractiveSystemProcess( var dwSessionId = ResolveWindowsSession(targetSessionId); + if (Global.Configuration.Agent.debug_mode) + Logging.Debug("Win32Interop.cs", "CreateInteractiveSystemProcess", + $"Resolved SessionId: {dwSessionId}, CommandLine: {commandLine}"); + // Obtain the process ID of the winlogon process that is running within the currently active session. var processes = Process.GetProcessesByName("winlogon"); foreach (Process p in processes) @@ -119,17 +124,52 @@ public static bool CreateInteractiveSystemProcess( if ((uint)p.SessionId == dwSessionId) { winlogonPid = (uint)p.Id; + if (Global.Configuration.Agent.debug_mode) + Logging.Debug("Win32Interop.cs", "CreateInteractiveSystemProcess", + $"Found winlogon PID: {winlogonPid} in session {dwSessionId}"); } } - // Obtain a handle to the winlogon process. - hProcess = Kernel32.OpenProcess(MAXIMUM_ALLOWED, false, winlogonPid); - - // Obtain a handle to the access token of the winlogon process. - if (!OpenProcessToken(hProcess, TOKEN_DUPLICATE, ref hPToken)) + // If no winlogon found, use the current process token (we're running as SYSTEM service) + if (winlogonPid == 0) { - Kernel32.CloseHandle(hProcess); - return false; + if (Global.Configuration.Agent.debug_mode) + Logging.Debug("Win32Interop.cs", "CreateInteractiveSystemProcess", + $"No winlogon found for session {dwSessionId}, using current process token"); + + // Get the current process token + hProcess = Kernel32.GetCurrentProcess(); + + if (!OpenProcessToken(hProcess, TOKEN_DUPLICATE, ref hPToken)) + { + int error = Marshal.GetLastWin32Error(); + Logging.Error("Win32Interop.cs", "CreateInteractiveSystemProcess", + $"Failed to open current process token. Error: {error}"); + return false; + } + } + else + { + // Obtain a handle to the winlogon process. + hProcess = Kernel32.OpenProcess(MAXIMUM_ALLOWED, false, winlogonPid); + + if (hProcess == nint.Zero) + { + int error = Marshal.GetLastWin32Error(); + Logging.Error("Win32Interop.cs", "CreateInteractiveSystemProcess", + $"Failed to open winlogon process {winlogonPid}. Error: {error}"); + return false; + } + + // Obtain a handle to the access token of the winlogon process. + if (!OpenProcessToken(hProcess, TOKEN_DUPLICATE, ref hPToken)) + { + int error = Marshal.GetLastWin32Error(); + Logging.Error("Win32Interop.cs", "CreateInteractiveSystemProcess", + $"Failed to open process token. Error: {error}"); + Kernel32.CloseHandle(hProcess); + return false; + } } // Security attibute structure used in DuplicateTokenEx and CreateProcessAsUser. @@ -139,11 +179,24 @@ public static bool CreateInteractiveSystemProcess( // Copy the access token of the winlogon process; the newly created token will be a primary token. if (!DuplicateTokenEx(hPToken, MAXIMUM_ALLOWED, ref sa, SECURITY_IMPERSONATION_LEVEL.SecurityIdentification, TOKEN_TYPE.TokenPrimary, out hUserTokenDup)) { + int error = Marshal.GetLastWin32Error(); + Logging.Error("Win32Interop.cs", "CreateInteractiveSystemProcess", + $"Failed to duplicate token. Error: {error}"); Kernel32.CloseHandle(hProcess); Kernel32.CloseHandle(hPToken); return false; } + // Set the session ID in the token to the target session + if (!Kernel32.SetTokenInformation(hUserTokenDup, Kernel32.TOKEN_INFORMATION_CLASS.TokenSessionId, + ref dwSessionId, (uint)Marshal.SizeOf(dwSessionId))) + { + int error = Marshal.GetLastWin32Error(); + if (Global.Configuration.Agent.debug_mode) + Logging.Debug("Win32Interop.cs", "CreateInteractiveSystemProcess", + $"SetTokenInformation failed (may not be critical). Error: {error}"); + } + // By default, CreateProcessAsUser creates a process on a non-interactive window station, meaning // the window station has a desktop that is invisible and the process is incapable of receiving // user input. To remedy this we set the lpDesktop parameter to indicate we want to enable user @@ -179,6 +232,18 @@ public static bool CreateInteractiveSystemProcess( ref si, out procInfo); + if (!result) + { + int error = Marshal.GetLastWin32Error(); + Logging.Error("Win32Interop.cs", "CreateInteractiveSystemProcess", + $"CreateProcessAsUser failed. Error: {error}, Desktop: {si.lpDesktop}, CommandLine: {commandLine}"); + } + else if (Global.Configuration.Agent.debug_mode) + { + Logging.Debug("Win32Interop.cs", "CreateInteractiveSystemProcess", + $"Process created successfully. PID: {procInfo.dwProcessId}, Desktop: {si.lpDesktop}"); + } + // Invalidate the handles. Kernel32.CloseHandle(hProcess); Kernel32.CloseHandle(hPToken); @@ -194,6 +259,111 @@ public static bool CreateInteractiveSystemProcess( } } + /// + /// Creates a process in the user's session context (not SYSTEM) + /// Uses WTSQueryUserToken to get the actual user token + /// + public static bool CreateProcessInUserSession( + string commandLine, + int targetSessionId, + bool hiddenWindow, + out PROCESS_INFORMATION procInfo) + { + try + { + var hUserToken = nint.Zero; + var hUserTokenDup = nint.Zero; + + procInfo = new PROCESS_INFORMATION(); + + var dwSessionId = ResolveWindowsSession(targetSessionId); + + if (Global.Configuration.Agent.debug_mode) + Logging.Debug("Win32Interop.cs", "CreateProcessInUserSession", + $"Resolved SessionId: {dwSessionId}, CommandLine: {commandLine}"); + + // Get the user token for this session + if (!WTSAPI32.WTSQueryUserToken(dwSessionId, out hUserToken)) + { + int error = Marshal.GetLastWin32Error(); + Logging.Error("Win32Interop.cs", "CreateProcessInUserSession", + $"WTSQueryUserToken failed for session {dwSessionId}. Error: {error}"); + return false; + } + + // Security attribute structure + var sa = new SECURITY_ATTRIBUTES(); + sa.Length = Marshal.SizeOf(sa); + + // Duplicate the token + if (!DuplicateTokenEx(hUserToken, MAXIMUM_ALLOWED, ref sa, + SECURITY_IMPERSONATION_LEVEL.SecurityIdentification, TOKEN_TYPE.TokenPrimary, out hUserTokenDup)) + { + int error = Marshal.GetLastWin32Error(); + Logging.Error("Win32Interop.cs", "CreateProcessInUserSession", + $"Failed to duplicate token. Error: {error}"); + Kernel32.CloseHandle(hUserToken); + return false; + } + + // Setup startup info + var si = new STARTUPINFO(); + si.cb = Marshal.SizeOf(si); + si.lpDesktop = @"winsta0\Default"; // User sessions always use Default desktop + + // Flags for process creation + uint dwCreationFlags; + if (hiddenWindow) + { + dwCreationFlags = NORMAL_PRIORITY_CLASS | CREATE_UNICODE_ENVIRONMENT | CREATE_NO_WINDOW; + si.dwFlags = STARTF_USESHOWWINDOW; + si.wShowWindow = 0; + } + else + { + dwCreationFlags = NORMAL_PRIORITY_CLASS | CREATE_UNICODE_ENVIRONMENT | CREATE_NEW_CONSOLE; + } + + // Create the process in the user's context + var result = CreateProcessAsUser( + hUserTokenDup, + null, + commandLine, + ref sa, + ref sa, + false, + dwCreationFlags, + nint.Zero, + null, + ref si, + out procInfo); + + if (!result) + { + int error = Marshal.GetLastWin32Error(); + Logging.Error("Win32Interop.cs", "CreateProcessInUserSession", + $"CreateProcessAsUser failed. Error: {error}, Desktop: {si.lpDesktop}, CommandLine: {commandLine}"); + } + else if (Global.Configuration.Agent.debug_mode) + { + Logging.Debug("Win32Interop.cs", "CreateProcessInUserSession", + $"Process created successfully in user context. PID: {procInfo.dwProcessId}, SessionId: {dwSessionId}"); + } + + // Clean up handles + Kernel32.CloseHandle(hUserToken); + Kernel32.CloseHandle(hUserTokenDup); + + return result; + } + catch (Exception ex) + { + Logging.Error("Win32Interop.cs", "CreateProcessInUserSession", $"Error: {ex.ToString()}"); + procInfo = new PROCESS_INFORMATION(); + return false; + } + } + public static string ResolveDesktopName(uint targetSessionId) { try diff --git a/NetLock RMM Agent Remote/Windows/Helper/ScreenControl/WindowsSession.cs b/NetLock RMM Agent Remote/Windows/Helper/ScreenControl/WindowsSession.cs index 7d95043f..ee5219ea 100644 --- a/NetLock RMM Agent Remote/Windows/Helper/ScreenControl/WindowsSession.cs +++ b/NetLock RMM Agent Remote/Windows/Helper/ScreenControl/WindowsSession.cs @@ -1,4 +1,7 @@ using System.Runtime.Serialization; +using System.Runtime.InteropServices; +using Global.Helper; +using Global.Configuration; namespace Windows.Helper.ScreenControl; @@ -20,4 +23,130 @@ public class WindowsSession public WindowsSessionType Type { get; set; } [DataMember(Name = "Username")] public string Username { get; set; } = string.Empty; + + /// + /// Gets all active Windows sessions (including login screen) + /// + public static List GetActiveSessions() + { + try + { + var sessions = new List(); + + nint pSessionInfo = nint.Zero; + int sessionCount = 0; + + if (WTSAPI32.WTSEnumerateSessions(nint.Zero, 0, 1, ref pSessionInfo, ref sessionCount) != 0) + { + nint current = pSessionInfo; + + for (int i = 0; i < sessionCount; i++) + { + var sessionInfo = Marshal.PtrToStructure(current); + + // Only active sessions and console (including login screen) + if (sessionInfo.State == WTSAPI32.WTS_CONNECTSTATE_CLASS.WTSActive || + sessionInfo.State == WTSAPI32.WTS_CONNECTSTATE_CLASS.WTSConnected) + { + sessions.Add(sessionInfo.SessionID); + + if (Agent.debug_mode) + Logging.Debug("WindowsSession.GetActiveSessions", + "Found session", + $"SessionId: {sessionInfo.SessionID}, State: {sessionInfo.State}, Station: {sessionInfo.pWinStationName}"); + } + + current = nint.Add(current, Marshal.SizeOf()); + } + + WTSAPI32.WTSFreeMemory(pSessionInfo); + } + + return sessions; + } + catch (Exception e) + { + Console.WriteLine(e); + Logging.Error("WindowsSession.GetActiveSessions", "Error enumerating sessions", e.ToString()); + return new List(); + } + } + + /// + /// Gets the session ID for a specific process + /// + public static uint GetProcessSessionId(int processId) + { + uint sessionId = 0; + Kernel32.ProcessIdToSessionId((uint)processId, ref sessionId); + return sessionId; + } + + /// + /// Checks if a user is logged into a specific session (not just login screen) + /// Session 0 = Services/System, Session 1+ with WTSActive = Logged in user + /// + public static bool IsUserLoggedIntoSession(uint sessionId) + { + nint buffer = nint.Zero; + uint bytesReturned = 0; + + try + { + // Session 0 is always the system/services session (login screen) + if (sessionId == 0) + { + if (Agent.debug_mode) + Logging.Debug("WindowsSession.IsUserLoggedIntoSession", + "Session check", + $"SessionId: {sessionId}, IsLoggedIn: False (System session)"); + return false; + } + + // Check the connect state for this session + if (WTSAPI32.WTSQuerySessionInformation(nint.Zero, sessionId, + WTSAPI32.WTS_INFO_CLASS.WTSConnectState, out buffer, out bytesReturned)) + { + if (buffer != nint.Zero) + { + int connectState = Marshal.ReadInt32(buffer); + WTSAPI32.WTS_CONNECTSTATE_CLASS state = (WTSAPI32.WTS_CONNECTSTATE_CLASS)connectState; + + // WTSActive means a user is actively logged in and working + // WTSDisconnected means a user was logged in but is now disconnected (still logged in) + bool isLoggedIn = (state == WTSAPI32.WTS_CONNECTSTATE_CLASS.WTSActive || + state == WTSAPI32.WTS_CONNECTSTATE_CLASS.WTSDisconnected); + + if (Agent.debug_mode) + Logging.Debug("WindowsSession.IsUserLoggedIntoSession", + "Session check", + $"SessionId: {sessionId}, State: {state}, IsLoggedIn: {isLoggedIn}"); + + return isLoggedIn; + } + } + } + catch (Exception ex) + { + if (Agent.debug_mode) + Logging.Error("WindowsSession.IsUserLoggedIntoSession", + "Error checking session login status", + ex.ToString()); + } + finally + { + if (buffer != nint.Zero) + WTSAPI32.WTSFreeMemory(buffer); + } + + // Default: Sessions > 0 are usually user sessions + bool defaultIsLoggedIn = sessionId > 0; + + if (Agent.debug_mode) + Logging.Debug("WindowsSession.IsUserLoggedIntoSession", + "Session check (fallback)", + $"SessionId: {sessionId}, IsLoggedIn: {defaultIsLoggedIn} (default based on session ID)"); + + return defaultIsLoggedIn; + } } diff --git a/NetLock RMM Tray Icon/Global/Helper/Encryption.cs b/NetLock RMM Tray Icon/Global/Helper/Encryption.cs index 678f06c8..0906635c 100644 --- a/NetLock RMM Tray Icon/Global/Helper/Encryption.cs +++ b/NetLock RMM Tray Icon/Global/Helper/Encryption.cs @@ -7,4 +7,4 @@ using Microsoft.Win32; using System.Security.Cryptography; using Global.Helper; -//OSSCH_START d9fb06e8-d8dc-4dd0-aee7-b5a8bfd7a69b //OSSCH_END \ No newline at end of file +//OSSCH_START 92bf281d-2a2a-4c23-b709-9dfcff0ca891 //OSSCH_END \ No newline at end of file diff --git a/NetLock-RMM-Server/Agent/Windows/Authentification.cs b/NetLock-RMM-Server/Agent/Windows/Authentification.cs index 51159f76..d7422723 100644 --- a/NetLock-RMM-Server/Agent/Windows/Authentification.cs +++ b/NetLock-RMM-Server/Agent/Windows/Authentification.cs @@ -69,13 +69,11 @@ public static async Task Verify_Device(string json, string ip_address_ex await conn.OpenAsync(); - string reader_query = "SELECT * FROM `devices` WHERE device_name = @device_name AND location_id = @location_id AND tenant_id = @tenant_id;"; + string reader_query = "SELECT * FROM `devices` WHERE access_key = @access_key;"; Logging.Handler.Debug("Modules.Authentification.Verify_Device", "MySQL_Query", reader_query); MySqlCommand command = new MySqlCommand(reader_query, conn); - command.Parameters.AddWithValue("@tenant_id", tenant_id); - command.Parameters.AddWithValue("@location_id", location_id); - command.Parameters.AddWithValue("@device_name", device_identity.device_name); + command.Parameters.AddWithValue("@access_key", device_identity.access_key); DbDataReader reader = await command.ExecuteReaderAsync(); @@ -107,7 +105,33 @@ public static async Task Verify_Device(string json, string ip_address_ex { // log state with additional details Logging.Handler.Debug("Modules.Authentification.Verify_Device", "Device not authorized", "Device is not authorized. //access key & hwid correct, but not authorized"); - authentification_result = "unauthorized"; + + // Check if package has auto_authorize_until set and if it's still valid + bool shouldAutoAuthorize = await Check_Auto_Authorization(device_identity.package_guid); + + if (shouldAutoAuthorize) + { + Logging.Handler.Debug("Modules.Authentification.Verify_Device", "Auto-Authorization", "Auto-authorizing device based on package configuration."); + + // Auto-authorize the device + bool autoAuthorized = await Auto_Authorize_Device(device_identity.access_key); + + if (autoAuthorized) + { + Logging.Handler.Debug("Modules.Authentification.Verify_Device", "Auto-Authorization", "Device was successfully auto-authorized."); + authentification_result = "not_synced"; + authorized = "1"; + } + else + { + Logging.Handler.Error("Modules.Authentification.Verify_Device", "Auto-Authorization", "Failed to auto-authorize device."); + authentification_result = "unauthorized"; + } + } + else + { + authentification_result = "unauthorized"; + } } else if (device_identity.access_key != reader["access_key"].ToString() && device_identity.hwid == reader["hwid"].ToString()) //access key is not correct, but hwid is. Deauthorize the device, set new access key & set not synced { @@ -139,12 +163,9 @@ public static async Task Verify_Device(string json, string ip_address_ex // Deauthorize the device if access key is not correct, but hwid is if (deauthorize) { - string execute_query = "UPDATE `devices` SET access_key = @access_key, hwid = @hwid, authorized = 0, synced = 0 WHERE device_name = @device_name AND location_id = @location_id AND tenant_id = @tenant_id"; + string execute_query = "UPDATE `devices` SET hwid = @hwid, authorized = 0, synced = 0 WHERE access_key = @access_key"; MySqlCommand cmd = new MySqlCommand(execute_query, conn); - cmd.Parameters.AddWithValue("@tenant_id", tenant_id); - cmd.Parameters.AddWithValue("@location_id", location_id); - cmd.Parameters.AddWithValue("@device_name", device_identity.device_name); cmd.Parameters.AddWithValue("@access_key", device_identity.access_key); cmd.Parameters.AddWithValue("@hwid", device_identity.hwid); cmd.ExecuteNonQuery(); @@ -251,6 +272,28 @@ public static async Task Verify_Device(string json, string ip_address_ex authentification_result = "unauthorized"; device_exists = false; + + // Check if package has auto_authorize_until set and if it's still valid + bool shouldAutoAuthorize = await Check_Auto_Authorization(device_identity.package_guid); + + if (shouldAutoAuthorize) + { + Logging.Handler.Debug("Modules.Authentification.Verify_Device", "Auto-Authorization New Device", "Auto-authorizing new device based on package configuration."); + + // Auto-authorize the device + bool autoAuthorized = await Auto_Authorize_Device(device_identity.access_key); + + if (autoAuthorized) + { + Logging.Handler.Debug("Modules.Authentification.Verify_Device", "Auto-Authorization New Device", "New device was successfully auto-authorized."); + authentification_result = "not_synced"; + authorized = "1"; + } + else + { + Logging.Handler.Error("Modules.Authentification.Verify_Device", "Auto-Authorization New Device", "Failed to auto-authorize new device."); + } + } } //Update device data if authorized, not synced or synced, and device exists, and update is true @@ -272,7 +315,6 @@ public static async Task Verify_Device(string json, string ip_address_ex "`tenant_id` = @tenant_id, " + "`location_id` = @location_id, " + "`device_name` = @device_name, " + - "`access_key` = @access_key, " + "`platform` = @platform, " + "`authorized` = @authorized, " + "`last_access` = @last_access, " + @@ -295,7 +337,7 @@ public static async Task Verify_Device(string json, string ip_address_ex "`environment_variables` = @environment_variables, " + "`synced` = @synced, " + "`last_active_user` = @last_active_user " + - "WHERE device_name = @device_name AND location_id = @location_id AND tenant_id = @tenant_id"; + "WHERE access_key = @access_key"; MySqlCommand cmd = new MySqlCommand(execute_query, conn); @@ -331,12 +373,10 @@ public static async Task Verify_Device(string json, string ip_address_ex } else if (authentification_result == "unauthorized") // if unauthorized, update last access { - string execute_query = "UPDATE `devices` SET last_access = @last_access WHERE device_name = @device_name AND location_id = @location_id AND tenant_id = @tenant_id"; + string execute_query = "UPDATE `devices` SET last_access = @last_access WHERE access_key = @access_key"; MySqlCommand cmd = new MySqlCommand(execute_query, conn); - cmd.Parameters.AddWithValue("@tenant_id", tenant_id); - cmd.Parameters.AddWithValue("@location_id", location_id); - cmd.Parameters.AddWithValue("@device_name", device_identity.device_name); + cmd.Parameters.AddWithValue("@access_key", device_identity.access_key); cmd.Parameters.AddWithValue("@last_access", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); cmd.ExecuteNonQuery(); @@ -426,6 +466,73 @@ public static async Task Verify_NetLock_Package_Configurations_Guid(string } } + public static async Task Check_Auto_Authorization(string package_guid) + { + MySqlConnection conn = new MySqlConnection(Configuration.MySQL.Connection_String); + + try + { + await conn.OpenAsync(); + + MySqlCommand cmd = new MySqlCommand("SELECT auto_authorize_until FROM agent_package_configurations WHERE guid = @guid;", conn); + cmd.Parameters.AddWithValue("@guid", package_guid); + + var result = await cmd.ExecuteScalarAsync(); + + if (result != null && result != DBNull.Value) + { + DateTime autoAuthorizeUntil = Convert.ToDateTime(result); + bool shouldAutoAuthorize = autoAuthorizeUntil > DateTime.Now; + + Logging.Handler.Debug("NetLock_RMM_Server.Modules.Authentification.Check_Auto_Authorization", "auto_authorize_until", autoAuthorizeUntil.ToString("yyyy-MM-dd HH:mm:ss")); + Logging.Handler.Debug("NetLock_RMM_Server.Modules.Authentification.Check_Auto_Authorization", "shouldAutoAuthorize", shouldAutoAuthorize.ToString()); + + return shouldAutoAuthorize; + } + + Logging.Handler.Debug("NetLock_RMM_Server.Modules.Authentification.Check_Auto_Authorization", "auto_authorize_until", "NULL or not set"); + return false; + } + catch (Exception ex) + { + Logging.Handler.Error("NetLock_RMM_Server.Modules.Authentification.Check_Auto_Authorization", "General error", ex.ToString()); + return false; + } + finally + { + await conn.CloseAsync(); + } + } + + public static async Task Auto_Authorize_Device(string access_key) + { + MySqlConnection conn = new MySqlConnection(Configuration.MySQL.Connection_String); + + try + { + await conn.OpenAsync(); + + string execute_query = "UPDATE `devices` SET authorized = 1 WHERE access_key = @access_key"; + MySqlCommand cmd = new MySqlCommand(execute_query, conn); + cmd.Parameters.AddWithValue("@access_key", access_key); + + int rowsAffected = await cmd.ExecuteNonQueryAsync(); + + Logging.Handler.Debug("NetLock_RMM_Server.Modules.Authentification.Auto_Authorize_Device", "rowsAffected", rowsAffected.ToString()); + + return rowsAffected > 0; + } + catch (Exception ex) + { + Logging.Handler.Error("NetLock_RMM_Server.Modules.Authentification.Auto_Authorize_Device", "General error", ex.ToString()); + return false; + } + finally + { + await conn.CloseAsync(); + } + } + public class JsonAuthMiddleware { private readonly RequestDelegate _next; @@ -514,18 +621,13 @@ public async Task InvokeAsync(HttpContext context) // Verarbeiten Sie die Device-Identity Logging.Handler.Debug("Agent.Windows.Authentification.InvokeAsync", "device_identity", $"Device name: {device_identity.device_name}"); - // Get the tenant id & location id with tenant_guid & location_guid - (int tenant_id, int location_id) = await Helper.Get_Tenant_Location_Id(device_identity.tenant_guid, device_identity.location_guid); - await conn.OpenAsync(); - string reader_query = "SELECT * FROM `devices` WHERE device_name = @device_name AND location_id = @location_id AND tenant_id = @tenant_id;"; + string reader_query = "SELECT * FROM `devices` WHERE access_key = @access_key;"; Logging.Handler.Debug("Modules.Authentification.InvokeAsync", "MySQL_Query", reader_query); MySqlCommand command = new MySqlCommand(reader_query, conn); - command.Parameters.AddWithValue("@device_name", device_identity.device_name); - command.Parameters.AddWithValue("@location_id", location_id); - command.Parameters.AddWithValue("@tenant_id", tenant_id); + command.Parameters.AddWithValue("@access_key", device_identity.access_key); DbDataReader reader = await command.ExecuteReaderAsync(); @@ -546,7 +648,37 @@ public async Task InvokeAsync(HttpContext context) } else if (device_identity.access_key == reader["access_key"].ToString() && device_identity.hwid == reader["hwid"].ToString() && reader["authorized"].ToString() == "0") //access key & hwid correct, but not authorized { - authentification_result = "unauthorized"; + // Close the reader before calling other database operations + await reader.CloseAsync(); + + // Check if package has auto_authorize_until set and if it's still valid + bool shouldAutoAuthorize = await Check_Auto_Authorization(device_identity.package_guid); + + if (shouldAutoAuthorize) + { + Logging.Handler.Debug("Agent.Windows.Authentification.InvokeAsync", "Auto-Authorization", "Auto-authorizing device based on package configuration."); + + // Auto-authorize the device + bool autoAuthorized = await Auto_Authorize_Device(device_identity.access_key); + + if (autoAuthorized) + { + Logging.Handler.Debug("Agent.Windows.Authentification.InvokeAsync", "Auto-Authorization", "Device was successfully auto-authorized."); + authentification_result = "not_synced"; + } + else + { + Logging.Handler.Error("Agent.Windows.Authentification.InvokeAsync", "Auto-Authorization", "Failed to auto-authorize device."); + authentification_result = "unauthorized"; + } + } + else + { + authentification_result = "unauthorized"; + } + + // Reader already closed, skip the close at the end + break; } else if (device_identity.access_key != reader["access_key"].ToString() && device_identity.hwid == reader["hwid"].ToString()) //access key is not correct, but hwid is. Deauthorize the device, set new access key & set not synced { @@ -558,7 +690,8 @@ public async Task InvokeAsync(HttpContext context) } } - await reader.CloseAsync(); + if (!reader.IsClosed) + await reader.CloseAsync(); } else //device not existing, create authentification_result = "unauthorized"; diff --git a/NetLock-RMM-Server/Agent/Windows/Device_Handler.cs b/NetLock-RMM-Server/Agent/Windows/Device_Handler.cs index b1020890..46dad850 100644 --- a/NetLock-RMM-Server/Agent/Windows/Device_Handler.cs +++ b/NetLock-RMM-Server/Agent/Windows/Device_Handler.cs @@ -112,8 +112,8 @@ public static async Task Update_Device_Information(string json) // Get the tenant id & location id with tenant_guid & location_guid (int tenant_id, int location_id) = await Helper.Get_Tenant_Location_Id(device_identity.tenant_guid, device_identity.location_guid); - // Get device id with device name, tenant id & location id - int device_id = await Helper.Get_Device_Id(device_identity.device_name, tenant_id, location_id); + // Get device id with access_key + int device_id = await Helper.Get_Device_Id_By_Access_Key(device_identity.access_key); await conn.OpenAsync(); @@ -237,13 +237,11 @@ public static async Task Update_Device_Information(string json) try { - string device_information_general_history_reader_query = $"SELECT * FROM `devices` WHERE device_name = @device_name AND location_id = @location_id AND tenant_id = @tenant_id;"; + string device_information_general_history_reader_query = $"SELECT * FROM `devices` WHERE access_key = @access_key;"; Logging.Handler.Debug("Modules.Authentification.Verify_Device", "MySQL_Query", device_information_general_history_reader_query); MySqlCommand device_information_general_history_command = new MySqlCommand(device_information_general_history_reader_query, conn); - device_information_general_history_command.Parameters.AddWithValue("@device_name", device_identity.device_name); - device_information_general_history_command.Parameters.AddWithValue("@location_id", location_id); - device_information_general_history_command.Parameters.AddWithValue("@tenant_id", tenant_id); + device_information_general_history_command.Parameters.AddWithValue("@access_key", device_identity.access_key); DbDataReader device_information_general_history_reader = await device_information_general_history_command.ExecuteReaderAsync(); diff --git a/NetLock-RMM-Server/Agent/Windows/Event_Handler.cs b/NetLock-RMM-Server/Agent/Windows/Event_Handler.cs index ec22922b..67461ec2 100644 --- a/NetLock-RMM-Server/Agent/Windows/Event_Handler.cs +++ b/NetLock-RMM-Server/Agent/Windows/Event_Handler.cs @@ -77,7 +77,7 @@ public static async Task Consume(string json) (int tenant_id, int location_id) = await Helper.Get_Tenant_Location_Id(device_identity.tenant_guid, device_identity.location_guid); // Get device_id - int device_id = await Helper.Get_Device_Id(device_identity.device_name, tenant_id, location_id); + int device_id = await Helper.Get_Device_Id_By_Access_Key(device_identity.access_key); // Get tenant_name & location_name (string tenant_name, string location_name) = await Helper.Get_Tenant_Location_Name(tenant_id, location_id); diff --git a/NetLock-RMM-Server/Agent/Windows/Helper.cs b/NetLock-RMM-Server/Agent/Windows/Helper.cs index f3ab5b61..d9b0f13f 100644 --- a/NetLock-RMM-Server/Agent/Windows/Helper.cs +++ b/NetLock-RMM-Server/Agent/Windows/Helper.cs @@ -143,6 +143,42 @@ public static async Task Get_Device_Id(string device_name, int tenant_id, i } } + // Get device id with access_key + public static async Task Get_Device_Id_By_Access_Key(string access_key) + { + MySqlConnection conn = new MySqlConnection(Configuration.MySQL.Connection_String); + + try + { + await conn.OpenAsync(); + + // Abfrage fΓΌr device_id + string deviceIdQuery = "SELECT id FROM devices WHERE access_key = @access_key;"; + MySqlCommand deviceCmd = new MySqlCommand(deviceIdQuery, conn); + deviceCmd.Parameters.AddWithValue("@access_key", access_key); + + int device_id = 0; + using (MySqlDataReader deviceReader = await deviceCmd.ExecuteReaderAsync()) + { + if (deviceReader.HasRows && await deviceReader.ReadAsync()) + { + device_id = deviceReader.GetInt32("id"); + } + } + + return device_id; + } + catch (Exception ex) + { + Logging.Handler.Error("NetLock_RMM_Server.Modules.Helper.Get_Device_Id_By_Access_Key", "General error", ex.ToString()); + return 0; + } + finally + { + await conn.CloseAsync(); + } + } + // Get role_remote from appsettings.json file public static async Task Get_Role_Status(string role) { diff --git a/NetLock-RMM-Server/Agent/Windows/Policy_Handler.cs b/NetLock-RMM-Server/Agent/Windows/Policy_Handler.cs index d7e2959e..69741330 100644 --- a/NetLock-RMM-Server/Agent/Windows/Policy_Handler.cs +++ b/NetLock-RMM-Server/Agent/Windows/Policy_Handler.cs @@ -1,4 +1,4 @@ -ο»Ώusing MySqlConnector; +ο»Ώο»Ώusing MySqlConnector; using System.Data.Common; using Microsoft.AspNetCore.Identity; using System; @@ -119,6 +119,8 @@ public class Sensors_Entity public bool? time_scheduler_sunday { get; set; } // NetLock notifications + public bool? suppress_notification { get; set; } + public bool? resolved_notification { get; set; } public bool? notifications_mail { get; set; } public bool? notifications_microsoft_teams { get; set; } public bool? notifications_telegram { get; set; } @@ -140,6 +142,7 @@ public class Jobs_Entity public string? platform { get; set; } public string? type { get; set; } public string? script { get; set; } + public int? timeout { get; set; } public int? time_scheduler_type { get; set; } public int? time_scheduler_seconds { get; set; } @@ -173,7 +176,7 @@ public static async Task Get_Policy(string json, string external_ip_addr (int tenant_id, int location_id) = await Helper.Get_Tenant_Location_Id(device_identity.tenant_guid, device_identity.location_guid); // Get device_id - int device_id = await Helper.Get_Device_Id(device_identity.device_name, tenant_id, location_id); + int device_id = await Helper.Get_Device_Id_By_Access_Key(device_identity.access_key); // Get tenant_name & location_name (string tenant_name, string location_name) = await Helper.Get_Tenant_Location_Name(tenant_id, location_id); @@ -204,12 +207,10 @@ public static async Task Get_Policy(string json, string external_ip_addr // Get device group try { - string query = "SELECT * FROM devices WHERE device_name = @device_name AND location_id = @location_id AND tenant_id = @tenant_id;"; + string query = "SELECT * FROM devices WHERE access_key = @access_key;"; MySqlCommand command = new MySqlCommand(query, conn); - command.Parameters.AddWithValue("@tenant_id", tenant_id); - command.Parameters.AddWithValue("@location_id", location_id); - command.Parameters.AddWithValue("@device_name", device_name); + command.Parameters.AddWithValue("@access_key", device_identity.access_key); Logging.Handler.Debug("Agent.Windows.Policy_Handler.Get_Policy (group_name)", "MySQL_Prepared_Query", query); @@ -538,12 +539,10 @@ public static async Task Get_Policy(string json, string external_ip_addr // Set synced = 1 try { - string execute_query = "UPDATE devices SET synced = 1 WHERE device_name = @device_name AND location_id = @location_id AND tenant_id = @tenant_id;"; + string execute_query = "UPDATE devices SET synced = 1 WHERE access_key = @access_key;"; MySqlCommand command = new MySqlCommand(execute_query, conn); - command.Parameters.AddWithValue("@tenant_id", tenant_id); - command.Parameters.AddWithValue("@location_id", location_id); - command.Parameters.AddWithValue("@device_name", device_name); + command.Parameters.AddWithValue("@access_key", device_identity.access_key); command.ExecuteNonQuery(); } diff --git a/NetLock-RMM-Server/Agent/Windows/Version_Handler.cs b/NetLock-RMM-Server/Agent/Windows/Version_Handler.cs index dad6387f..699026d0 100644 --- a/NetLock-RMM-Server/Agent/Windows/Version_Handler.cs +++ b/NetLock-RMM-Server/Agent/Windows/Version_Handler.cs @@ -145,21 +145,16 @@ public static async Task Check_Version(string json) return "identical"; // Return identical if the number of pending updates exceeds the maximum allowed to prevent flooding the update process } - // Get the location_id and tenant_id from the device_identity - (int tenantId, int locationId) = await Windows.Helper.Get_Tenant_Location_Id(device_identity.tenant_guid, device_identity.location_guid); - MySqlConnection conn = new MySqlConnection(Configuration.MySQL.Connection_String); - string query = "UPDATE devices SET update_pending = 1, update_started = @update_started WHERE device_name = @device_name AND location_id = @location_id AND tenant_id = @tenant_id;"; + string query = "UPDATE devices SET update_pending = 1, update_started = @update_started WHERE access_key = @access_key;"; try { await conn.OpenAsync(); MySqlCommand cmd = new MySqlCommand(query, conn); - cmd.Parameters.AddWithValue("@device_name", device_identity.device_name); - cmd.Parameters.AddWithValue("@location_id", locationId); - cmd.Parameters.AddWithValue("@tenant_id", tenantId); + cmd.Parameters.AddWithValue("@access_key", device_identity.access_key); cmd.Parameters.AddWithValue("@update_started", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); Logging.Handler.Debug("Agent.Windows.Version_Handler", "MySQL_Prepared_Query", query); diff --git a/NetLock-RMM-Server/Application_Paths.cs b/NetLock-RMM-Server/Application_Paths.cs index 5a9d092b..2bb4fc06 100644 --- a/NetLock-RMM-Server/Application_Paths.cs +++ b/NetLock-RMM-Server/Application_Paths.cs @@ -25,7 +25,7 @@ public class Application_Paths public static string internal_dir = Path.Combine(GetCurrentDirectory(), "internal"); public static string internal_temp_dir = Path.Combine(GetCurrentDirectory(), "internal", "temp"); - //OSSCH_START 07e84799-47a8-49f0-9f45-9976cc931ee5 //OSSCH_END + //OSSCH_START 84a26b09-edf1-4f9b-9381-d1acec7a78a4 //OSSCH_END // Lets Encrypt path public static string certificates_path = Path.Combine(GetCurrentDirectory(), "certificates"); diff --git a/NetLock-RMM-Server/Application_Settings.cs b/NetLock-RMM-Server/Application_Settings.cs index 23308f38..1c602e66 100644 --- a/NetLock-RMM-Server/Application_Settings.cs +++ b/NetLock-RMM-Server/Application_Settings.cs @@ -5,6 +5,6 @@ public class Application_Settings public static string server_version = "2.5.3.4b"; public static string agent_version = "2.5.3.4"; - //OSSCH_START 90a9ae32-6698-40e3-b707-3efbdc944f57 //OSSCH_END + //OSSCH_START e835ab4e-bde3-440f-813b-31590d69f3c9 //OSSCH_END } } diff --git a/NetLock-RMM-Server/Files/Handler.cs b/NetLock-RMM-Server/Files/Handler.cs index 4b12342d..1854b8c1 100644 --- a/NetLock-RMM-Server/Files/Handler.cs +++ b/NetLock-RMM-Server/Files/Handler.cs @@ -39,6 +39,9 @@ public static async Task Verify_Api_Key(string files_api_key) { bool api_key_exists = false; + if (string.IsNullOrEmpty(files_api_key)) + return false; + MySqlConnection conn = new MySqlConnection(Configuration.MySQL.Connection_String); try @@ -66,7 +69,7 @@ public static async Task Verify_Api_Key(string files_api_key) } finally { - conn.Close(); + await conn.CloseAsync(); } return api_key_exists; diff --git a/NetLock-RMM-Server/Helper/Encryption.cs b/NetLock-RMM-Server/Helper/Encryption.cs index 46a31448..d3548295 100644 --- a/NetLock-RMM-Server/Helper/Encryption.cs +++ b/NetLock-RMM-Server/Helper/Encryption.cs @@ -9,5 +9,5 @@ namespace Encryption { - //OSSCH_START f8fd017e-5dd1-4aa9-a292-7ecb5ce49c05 //OSSCH_END + //OSSCH_START 4fe3fddd-3db6-450b-b415-ec06d1b05a7d //OSSCH_END } diff --git a/NetLock-RMM-Server/Members_Portal/Config.cs b/NetLock-RMM-Server/Members_Portal/Config.cs index ed716e97..decd38ae 100644 --- a/NetLock-RMM-Server/Members_Portal/Config.cs +++ b/NetLock-RMM-Server/Members_Portal/Config.cs @@ -1 +1 @@ -//OSSCH_START 61a7ae61-89f2-4812-9839-2cd78cf8687a //OSSCH_END \ No newline at end of file +//OSSCH_START 83d04f36-ffab-4e2d-b16b-fec9ee0678e7 //OSSCH_END \ No newline at end of file diff --git a/NetLock-RMM-Server/Members_Portal/Handler.cs b/NetLock-RMM-Server/Members_Portal/Handler.cs index 155663a9..23b467ee 100644 --- a/NetLock-RMM-Server/Members_Portal/Handler.cs +++ b/NetLock-RMM-Server/Members_Portal/Handler.cs @@ -8,4 +8,4 @@ using System.IO.Compression; using System; -//OSSCH_START 668ba00b-154e-4055-9425-8f894621130f //OSSCH_END \ No newline at end of file +//OSSCH_START 949edc4f-56a8-471b-ad45-40feadc581f0 //OSSCH_END \ No newline at end of file diff --git a/NetLock-RMM-Server/Members_Portal/Members_Portal.cs b/NetLock-RMM-Server/Members_Portal/Members_Portal.cs index aafdf335..77572945 100644 --- a/NetLock-RMM-Server/Members_Portal/Members_Portal.cs +++ b/NetLock-RMM-Server/Members_Portal/Members_Portal.cs @@ -1 +1 @@ -//OSSCH_START ab6ebce4-35b8-4b1a-9300-a437a7d2a84f //OSSCH_END \ No newline at end of file +//OSSCH_START 665db98d-f333-43e2-a49d-3b0af7213267 //OSSCH_END \ No newline at end of file diff --git a/NetLock-RMM-Server/Members_Portal/Members_Portal_License_Service.cs b/NetLock-RMM-Server/Members_Portal/Members_Portal_License_Service.cs index 56bf5485..45f35b45 100644 --- a/NetLock-RMM-Server/Members_Portal/Members_Portal_License_Service.cs +++ b/NetLock-RMM-Server/Members_Portal/Members_Portal_License_Service.cs @@ -3,4 +3,4 @@ using System; using System.Threading; using System.Threading.Tasks; -//OSSCH_START 269c075b-6668-4b54-9a2f-9837071d0aa0 //OSSCH_END \ No newline at end of file +//OSSCH_START 7a152120-6afd-4008-b1a7-f8081d3bed13 //OSSCH_END \ No newline at end of file diff --git a/NetLock-RMM-Server/Members_Portal/Obfuscation.cs b/NetLock-RMM-Server/Members_Portal/Obfuscation.cs index 5cdb07d5..e10c23f7 100644 --- a/NetLock-RMM-Server/Members_Portal/Obfuscation.cs +++ b/NetLock-RMM-Server/Members_Portal/Obfuscation.cs @@ -4,4 +4,4 @@ using System.Text.Json; using Helper; -//OSSCH_START 06e6ac48-d03d-4b1b-9912-c6f31f3b256d //OSSCH_END \ No newline at end of file +//OSSCH_START 45f54c29-ca6e-4c95-92b4-705720183ccb //OSSCH_END \ No newline at end of file diff --git a/NetLock-RMM-Server/Members_Portal/Package_Provider.cs b/NetLock-RMM-Server/Members_Portal/Package_Provider.cs index 1d881d07..728fb290 100644 --- a/NetLock-RMM-Server/Members_Portal/Package_Provider.cs +++ b/NetLock-RMM-Server/Members_Portal/Package_Provider.cs @@ -10,4 +10,4 @@ using System.Net.Http; using System.Globalization; -//OSSCH_START 3e8067d0-1990-4430-be12-f310f5afd974 //OSSCH_END \ No newline at end of file +//OSSCH_START 502e5f36-2178-4b00-8740-8ae174cda870 //OSSCH_END \ No newline at end of file diff --git a/NetLock-RMM-Server/Program.cs b/NetLock-RMM-Server/Program.cs index 1016a92b..731030a9 100644 --- a/NetLock-RMM-Server/Program.cs +++ b/NetLock-RMM-Server/Program.cs @@ -88,6 +88,9 @@ SemaphoreSlim _maxConcurrentNetLockPackageDownloadsSemaphore = new SemaphoreSlim(5, 5); +// IP Whitelist will be loaded from database after MySQL connection is established +List allowedIps = new List(); + Roles.Comm = role_comm; Roles.Update = role_update; Roles.Trust = role_trust; @@ -270,6 +273,38 @@ Console.WriteLine("Members Portal API key loaded from database: " + Members_Portal.ApiKey); } + + // Load IP Whitelist from database + try + { + string ipWhitelistJson = await NetLock_RMM_Server.MySQL.Handler.Quick_Reader("SELECT * FROM settings;", "ip_whitelist_backend"); + + if (!string.IsNullOrEmpty(ipWhitelistJson)) + { + var ipList = System.Text.Json.JsonSerializer.Deserialize>(ipWhitelistJson); + if (ipList != null && ipList.Count > 0) + { + allowedIps = ipList; + Console.WriteLine($"[Agent Backend] IP Whitelist loaded from database: {string.Join(", ", allowedIps)}"); + } + else + { + Console.WriteLine("[Agent Backend] IP Whitelist is empty in database. All IPs will be allowed."); + } + } + else + { + Console.WriteLine("[Agent Backend] No IP Whitelist configured in database. All IPs will be allowed."); + } + } + catch (Exception ex) + { + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine($"[Agent Backend] Warning: Could not load IP Whitelist from database: {ex.Message}"); + Console.WriteLine("[Agent Backend] All IPs will be allowed."); + Console.ResetColor(); + Logging.Handler.Error("Program.cs", "Load IP Whitelist Agent Backend", ex.ToString()); + } } Console.WriteLine(Environment.NewLine); @@ -279,7 +314,7 @@ { if (Members_Portal.IsApiEnabled) { - //OSSCH_START dcae5642-41c6-4f9f-ad0f-a74727c6e611 //OSSCH_END + //OSSCH_START 00b50243-73a9-4b2b-a0c6-35b202f1c714 //OSSCH_END Console.WriteLine("----------------------------------------"); } } @@ -325,6 +360,37 @@ app.UseRouting(); +// IP Whitelist Middleware for Agent Backend +if (allowedIps != null && allowedIps.Count > 0) +{ + Logging.Handler.Debug("Middleware", "IP Whitelisting Agent Backend", "IP whitelisting enabled. Whitelisted IPs: " + string.Join(", ", allowedIps)); + Console.WriteLine("[Agent Backend] IP whitelisting enabled. Whitelisted IPs: " + string.Join(", ", allowedIps)); + + app.Use(async (context, next) => + { + var remoteIp = context.Request.Headers.TryGetValue("X-Forwarded-For", out var headerValue) + ? headerValue.ToString().Split(',')[0].Trim() + : context.Connection.RemoteIpAddress?.ToString(); + + Logging.Handler.Debug("Middleware Agent Backend", "Checking IP", remoteIp); + + if (!allowedIps.Contains(remoteIp)) + { + Logging.Handler.Error("Middleware Agent Backend", "IP Whitelisting", $"IP {remoteIp} is not whitelisted."); + context.Response.StatusCode = 403; + await context.Response.WriteAsync("Access denied. Your IP: " + remoteIp); + return; + } + + await next(); + }); +} +else +{ + Logging.Handler.Debug("Middleware", "IP Whitelisting Agent Backend", "No IP addresses are whitelisted. All IPs will be allowed."); + Console.WriteLine("[Agent Backend] No IP addresses are whitelisted. All IPs will be allowed."); +} + // Only use the middleware for the commandHub, to verify the signalR connection app.UseWhen(context => context.Request.Path.StartsWithSegments("/commandHub"), appBuilder => { @@ -349,13 +415,13 @@ // Members Portal Api Cloud Version Endpoints if (Members_Portal.IsApiEnabled && Members_Portal.IsCloudEnabled) { - //OSSCH_START 7e183b1d-197e-4427-94f8-de63f17f7c54 //OSSCH_END + //OSSCH_START d410d003-d633-45db-beae-85b28e729fda //OSSCH_END } if (Members_Portal.IsApiEnabled && Members_Portal.IsCloudEnabled) { // Credentials update endpoint - //OSSCH_START f91b0363-be54-4d47-956b-93ed7ba40390 //OSSCH_END + //OSSCH_START ec26330e-75a8-4f97-9e42-0b48d1115853 //OSSCH_END } //Check Version @@ -733,6 +799,24 @@ { try { + Logging.Handler.Debug("/admin/files", "Request received.", path); + + // Add security header + context.Response.Headers.Add("Content-Security-Policy", "default-src 'self'"); + + // Determine external IP address (if available) + string ipAddressExternal = context.Request.Headers.TryGetValue("X-Forwarded-For", out var headerValue) + ? headerValue.ToString() + : context.Connection.RemoteIpAddress?.ToString() ?? "Unknown"; + + // Verify API key + if (!context.Request.Headers.TryGetValue("x-api-key", out StringValues apiKey) || !await NetLock_RMM_Server.Files.Handler.Verify_Api_Key(apiKey)) + { + context.Response.StatusCode = 401; + await context.Response.WriteAsync("Unauthorized."); + return; + } + // Check whether the path is null or empty if (String.IsNullOrWhiteSpace(path)) { @@ -761,25 +845,7 @@ return; } } - - Logging.Handler.Debug("/admin/files", "Request received.", path); - - // Add security header - context.Response.Headers.Add("Content-Security-Policy", "default-src 'self'"); - - // Determine external IP address (if available) - string ipAddressExternal = context.Request.Headers.TryGetValue("X-Forwarded-For", out var headerValue) - ? headerValue.ToString() - : context.Connection.RemoteIpAddress?.ToString() ?? "Unknown"; - - // Verify API key - if (!context.Request.Headers.TryGetValue("x-api-key", out StringValues apiKey) || !await NetLock_RMM_Server.Files.Handler.Verify_Api_Key(apiKey)) - { - context.Response.StatusCode = 401; - await context.Response.WriteAsync("Unauthorized."); - return; - } - + // Check directory var fullPath = Path.Combine(Application_Paths._private_files, path); @@ -1314,7 +1380,7 @@ // NetLock files download private - GUID, used for update server & trust server if (role_update || role_trust) { - //OSSCH_START 89a9374b-8b49-433b-ad67-9b154fe3bb7c //OSSCH_END + //OSSCH_START aa463c1d-ad0a-4971-bf4e-9bc23fabc9a2 //OSSCH_END } /* @@ -1549,6 +1615,71 @@ await CommandHubSingleton.Instance.HubContext.Clients.Client(admin_client_id) }); } +// Web console get connected devices +if (role_file) +{ + app.MapGet("/admin/devices/connected", async (HttpContext context) => + { + try + { + Logging.Handler.Debug("/admin/devices/connected", "Request received.", ""); + + // Add security header + context.Response.Headers.Add("Content-Security-Policy", "default-src 'self'"); + + // Determine external IP address (if available) + string ipAddressExternal = context.Request.Headers.TryGetValue("X-Forwarded-For", out var headerValue) + ? headerValue.ToString() + : context.Connection.RemoteIpAddress?.ToString() ?? "Unknown"; + + // Verify API key + if (!context.Request.Headers.TryGetValue("x-api-key", out StringValues apiKey) || !await NetLock_RMM_Server.Files.Handler.Verify_Api_Key(apiKey)) + { + context.Response.StatusCode = 401; + await context.Response.WriteAsync("Unauthorized."); + return; + } + + // Get connected devices access keys from CommandHubSingleton + var connectedAccessKeys = new List(); + foreach (var identityJson in NetLock_RMM_Server.SignalR.CommandHubSingleton.Instance._clientConnections.Values) + { + try + { + var identityDoc = JsonDocument.Parse(identityJson); + if (identityDoc.RootElement.TryGetProperty("device_identity", out var deviceIdentity) && + deviceIdentity.TryGetProperty("access_key", out var accessKeyElement)) + { + string accessKey = accessKeyElement.GetString(); + if (!string.IsNullOrEmpty(accessKey) && !connectedAccessKeys.Contains(accessKey)) + { + connectedAccessKeys.Add(accessKey); + } + } + } + catch (Exception ex) + { + Logging.Handler.Error("/admin/devices/connected", "Failed to parse identity JSON", ex.ToString()); + } + } + + // Create JSON response + var jsonObject = new { access_keys = connectedAccessKeys }; + string json = JsonSerializer.Serialize(jsonObject, new JsonSerializerOptions { WriteIndented = true }); + Logging.Handler.Debug("/admin/devices/connected", "Connected devices", json); + + context.Response.ContentType = "application/json"; + await context.Response.WriteAsync(json); + } + catch (Exception ex) + { + Logging.Handler.Error("/admin/devices/connected/index", "General error", ex.ToString()); + context.Response.StatusCode = 500; + await context.Response.WriteAsync("An error occurred while processing the request."); + } + }); +} + // Add a middleware to handle exceptions globally and return a 500 status code with a message to the client in case of an unexpected error app.UseExceptionHandler(errorApp => { diff --git a/NetLock-RMM-Server/SignalR/CommandHub.cs b/NetLock-RMM-Server/SignalR/CommandHub.cs index dbf9187a..860e6c36 100644 --- a/NetLock-RMM-Server/SignalR/CommandHub.cs +++ b/NetLock-RMM-Server/SignalR/CommandHub.cs @@ -126,18 +126,19 @@ public override async Task OnConnectedAsync() } // Verbesserte Verbindungslogik: PrΓΌfe auf existierende Verbindungen - string deviceClientId = await Get_Device_ClientId(deviceIdentity.device_name, deviceIdentity.location_guid, deviceIdentity.tenant_guid); + // Device identifiziert sich nur per access_key + string deviceClientId = await Get_Device_ClientId_By_Access_Key(deviceIdentity.access_key); // Wenn eine alte Verbindung existiert, entferne sie if (!String.IsNullOrEmpty(deviceClientId)) { - Logging.Handler.Debug("SignalR CommandHub", "OnConnectedAsync", $"Device {deviceIdentity.device_name} already connected with ID {deviceClientId}. Replacing connection."); + Logging.Handler.Debug("SignalR CommandHub", "OnConnectedAsync", $"Device with access_key already connected with ID {deviceClientId}. Replacing connection."); // Protokolliere Verbindungswechsel mit mehr Informationen Logging.Handler.Debug("SignalR CommandHub", "OnConnectedAsync", - $"Connection replacement for device {deviceIdentity.device_name}: Old ID={deviceClientId}, New ID={clientId}"); + $"Connection replacement: Old ID={deviceClientId}, New ID={clientId}"); - // Entferne alte Verbindung + // Entferne alte Verbindung CommandHubSingleton.Instance.RemoveClientConnection(deviceClientId); } @@ -215,6 +216,52 @@ public override async Task OnDisconnectedAsync(Exception exception) await base.OnDisconnectedAsync(exception); } + // Get device client id by access_key (used when device connects) + public async Task Get_Device_ClientId_By_Access_Key(string access_key) + { + try + { + Logging.Handler.Debug("SignalR CommandHub", "Get_Device_ClientID_By_Access_Key", $"Access Key: {access_key}"); + + // Optimierte Suche durch EinschrΓ€nkung der Logausgabe + // Nur bei niedrigerem Log-Level alle Clients auflisten + if (Logging.Handler.IsDebugVerboseEnabled()) + { + foreach (var client in CommandHubSingleton.Instance._clientConnections) + { + Logging.Handler.Debug("SignalR CommandHub", "Get_Device_ClientID_By_Access_Key", $"Connected client: {client.Key}, {client.Value}"); + } + } + + var clientId = CommandHubSingleton.Instance._clientConnections.FirstOrDefault(x => + { + try + { + var rootData = JsonSerializer.Deserialize(x.Value); + return rootData?.device_identity != null && + rootData.device_identity.access_key == access_key; + } + catch (JsonException) + { + return false; + } + }).Key; + + if (string.IsNullOrEmpty(clientId)) + { + Logging.Handler.Debug("SignalR CommandHub", "Get_Device_ClientID_By_Access_Key", "Client ID not found."); + } + + return clientId; + } + catch (Exception ex) + { + Logging.Handler.Error("SignalR CommandHub", "Get_Device_ClientID_By_Access_Key", ex.ToString()); + return null; + } + } + + // Get device client id by device info (used when admin sends command via webconsole) public async Task Get_Device_ClientId(string device_name, string location_guid, string tenant_guid) { try @@ -435,7 +482,7 @@ public async Task ReceiveClientResponse(string responseId, string response, bool Logging.Handler.Debug("SignalR CommandHub", "ReceiveClientResponse", $"Admin client ID: {admin_client_id} type: {type}"); - // insert result into history table + // insert result into history table and add device_name for remote shell commands response if (type == 0) // remote shell { // Verbesserte Datenbankverbindung mit using-Statement fΓΌr automatisches Schließen @@ -465,6 +512,9 @@ public async Task ReceiveClientResponse(string responseId, string response, bool } // Kein finally-Block notwendig, da using-Statement } + + // Add device_id to the response for remote shell commands to identify the device in the webconsole response for bulk executin view + response = device_id + ">>nlocksep<<" + response; } // Check if the admin client ID is empty or null and return if it is @@ -535,6 +585,8 @@ private string GetResponseMethodName(int type, int file_browser_command, string return "ReceiveClientResponseRemoteControlAccessRequest"; else if (type == 9) // Power Management Action return "ReceiveClientResponsePowerManagementAction"; + else if (type == 10 || type == 11 || type == 12 || type == 13) // Remote Eventlog Viewer - Get Eventlogs + return "ReceiveClientResponseRemoteEventlogViewer"; return "ReceiveClientResponse"; // Fallback } @@ -687,4 +739,3 @@ private async Task TrySendToClientWithRetry(string clientId, string method } } } - diff --git a/NetLock-RMM-Web-Console/Application_Paths.cs b/NetLock-RMM-Web-Console/Application_Paths.cs index c491c771..558e185e 100644 --- a/NetLock-RMM-Web-Console/Application_Paths.cs +++ b/NetLock-RMM-Web-Console/Application_Paths.cs @@ -14,7 +14,7 @@ public class Application_Paths public static string internal_recordings_dir = Path.Combine(GetCurrentDirectory(), "internal", "recordings"); - //OSSCH_START d34d8f75-ef2b-4364-90a7-596c02e78e75 //OSSCH_END + //OSSCH_START e17a2268-4486-4c71-a6c4-4c0959a17405 //OSSCH_END public static string certificates_path = Path.Combine(GetCurrentDirectory(), "certificates"); diff --git a/NetLock-RMM-Web-Console/Application_Settings.cs b/NetLock-RMM-Web-Console/Application_Settings.cs index 4774c94c..68840aba 100644 --- a/NetLock-RMM-Web-Console/Application_Settings.cs +++ b/NetLock-RMM-Web-Console/Application_Settings.cs @@ -7,7 +7,7 @@ public class Application_Settings public static string versionUrl = "https://blog.netlockrmm.com/2025/10/26/netlock-rmm-2-5-3-0-whitelabel-tray-icon/"; public static string Local_Encryption_Key = "01234567890123456789012345678901"; - //OSSCH_START 1494daa2-84e9-4bc1-a000-c078f5add955 //OSSCH_END + //OSSCH_START 638e7df6-7e73-4dc7-88cc-5559932f37be //OSSCH_END public static string onlyPro = "This feature is exclusive to Pro & Cloud users. Please ensure you have an active paid membership, or your changes will not take effect."; } diff --git a/NetLock-RMM-Web-Console/Classes/Authentication/AuthProviderRegistrar.cs b/NetLock-RMM-Web-Console/Classes/Authentication/AuthProviderRegistrar.cs index 252b7678..84953836 100644 --- a/NetLock-RMM-Web-Console/Classes/Authentication/AuthProviderRegistrar.cs +++ b/NetLock-RMM-Web-Console/Classes/Authentication/AuthProviderRegistrar.cs @@ -5,761 +5,5 @@ using Microsoft.Identity.Web; using Microsoft.AspNetCore.Authentication; using System; - -namespace NetLock_RMM_Web_Console.Classes.Authentication -{ - /// - /// Centralized SSO provider registration to support multiple authentication providers - /// - public static class AuthProviderRegistrar - { - /// - /// Registers all enabled SSO providers from configuration - /// - public static void RegisterSsoProviders(IServiceCollection services, IConfiguration configuration) - { - try - { - bool anyProviderRegistered = false; - - // Try to register Azure AD - var azureSection = configuration.GetSection("Authentication:AzureAd"); - if (azureSection.Exists() && azureSection.GetValue("Enabled", false)) - { - RegisterAzureAd(services, azureSection); - anyProviderRegistered = true; - Console.WriteLine("SSO: Azure AD (Microsoft Entra ID) provider registered."); - } - - // Try to register Keycloak - var keycloakSection = configuration.GetSection("Authentication:Keycloak"); - if (keycloakSection.Exists() && keycloakSection.GetValue("Enabled", false)) - { - RegisterKeycloak(services, keycloakSection); - anyProviderRegistered = true; - Console.WriteLine("SSO: Keycloak provider registered."); - } - - // Try to register Google Workspace / Google Identity - var googleSection = configuration.GetSection("Authentication:GoogleIdentity"); - if (googleSection.Exists() && googleSection.GetValue("Enabled", false)) - { - RegisterGoogleIdentity(services, googleSection); - anyProviderRegistered = true; - Console.WriteLine("SSO: Google Workspace / Google Identity provider registered."); - } - - // Try to register Okta - var oktaSection = configuration.GetSection("Authentication:Okta"); - if (oktaSection.Exists() && oktaSection.GetValue("Enabled", false)) - { - RegisterOkta(services, oktaSection); - anyProviderRegistered = true; - Console.WriteLine("SSO: Okta provider registered."); - } - - // Try to register Auth0 - var auth0Section = configuration.GetSection("Authentication:Auth0"); - if (auth0Section.Exists() && auth0Section.GetValue("Enabled", false)) - { - RegisterAuth0(services, auth0Section); - anyProviderRegistered = true; - Console.WriteLine("SSO: Auth0 provider registered."); - } - - if (!anyProviderRegistered) - { - Console.WriteLine("SSO: No SSO providers enabled. Using default authentication."); - } - } - catch (Exception e) - { - Logging.Handler.Error("SSO Provider Registration", "Initialization Failed", e.Message); - Console.WriteLine("SSO Provider Registration", "Initialization Failed", e.Message); - throw; - } - } - - /// - /// Registers Azure AD / Microsoft Entra ID authentication - /// - private static void RegisterAzureAd(IServiceCollection services, IConfigurationSection section) - { - try - { - // AddMicrosoftIdentityWebApp already registers Cookie authentication internally - // So we don't need to call AddCookie separately - services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) - .AddMicrosoftIdentityWebApp(options => - { - section.Bind(options); - - // Configure sign-out redirect - where to go after Azure AD logout - options.SignedOutRedirectUri = "/?logout=true"; - options.SignedOutCallbackPath = "/signout-callback-oidc"; - - // Don't save tokens in cookie (security best practice) - options.SaveTokens = false; - - // Configure OIDC events - options.Events = new OpenIdConnectEvents - { - OnTokenValidated = async context => - { - // After successful authentication, check if user is authorized - Console.WriteLine("Azure AD SSO: Token validated successfully"); - - // Try to find email in various Azure AD claim types - var email = context.Principal?.FindFirst(System.Security.Claims.ClaimTypes.Email)?.Value - ?? context.Principal?.FindFirst("preferred_username")?.Value - ?? context.Principal?.FindFirst("upn")?.Value - ?? context.Principal?.FindFirst("email")?.Value; - - if (!string.IsNullOrEmpty(email)) - { - Console.WriteLine($"Azure AD SSO: User email identified as: {email}"); - - // Check if user exists in database with SSO role - bool userAuthorized = await NetLock_RMM_Web_Console.Classes.Authentication.User.CheckSsoUserAuthorization(email); - - if (!userAuthorized) - { - Console.WriteLine($"Azure AD SSO: User {email} not authorized or does not have SSO role"); - Logging.Handler.Debug("Azure AD SSO", "Authorization Failed", $"User {email} not authorized for SSO login"); - - // Reject the authentication - context.Fail("User is not authorized for SSO login. Please contact your administrator."); - return; - } - - Console.WriteLine($"Azure AD SSO: User {email} authorized for SSO login"); - Logging.Handler.Debug("Azure AD SSO", "Authorization Success", $"User {email} authorized"); - } - else - { - Console.WriteLine("Azure AD SSO: Warning - Could not find email claim in token"); - // Log all claims for debugging - foreach (var claim in context.Principal?.Claims ?? Enumerable.Empty()) - { - Console.WriteLine($" Claim: {claim.Type} = {claim.Value}"); - } - - // Reject authentication if no email found - context.Fail("Email claim not found in authentication token."); - } - }, - OnAuthenticationFailed = context => - { - Console.WriteLine($"Azure AD SSO: Authentication failed - {context.Exception?.Message}"); - context.Response.Redirect("/"); - context.HandleResponse(); - return Task.CompletedTask; - }, - OnRemoteFailure = context => - { - Console.WriteLine($"Azure AD SSO: Remote failure - {context.Failure?.Message}"); - context.Response.Redirect("/"); - context.HandleResponse(); - return Task.CompletedTask; - }, - OnRedirectToIdentityProviderForSignOut = context => - { - Console.WriteLine("Azure AD SSO: Redirecting to Azure AD for sign-out"); - Console.WriteLine($"Azure AD SSO: Post logout redirect URI: {context.Properties.RedirectUri}"); - return Task.CompletedTask; - }, - OnSignedOutCallbackRedirect = context => - { - Console.WriteLine("Azure AD SSO: Signed out callback redirect"); - Console.WriteLine($"Azure AD SSO: Redirecting to: {context.Properties?.RedirectUri ?? "default"}"); - return Task.CompletedTask; - } - }; - }); - - // Add Authorization services (without FallbackPolicy to avoid redirect loops) - // Pages should use @attribute [Authorize] to require authentication - services.AddAuthorization(); - } - catch (Exception ex) - { - Console.WriteLine($"Error configuring Azure AD SSO: {ex.Message}"); - throw; - } - } - - /// - /// Registers Keycloak authentication - /// - private static void RegisterKeycloak(IServiceCollection services, IConfigurationSection section) - { - try - { - var authority = section.GetValue("Authority"); - var clientId = section.GetValue("ClientId"); - var clientSecret = section.GetValue("ClientSecret"); - var callbackPath = section.GetValue("CallbackPath") ?? "/signin-keycloak"; - var signedOutCallbackPath = section.GetValue("SignedOutCallbackPath") ?? "/signout-callback-keycloak"; - var responseType = section.GetValue("ResponseType") ?? "code"; - var saveTokens = section.GetValue("SaveTokens", false); - var getClaimsFromUserInfoEndpoint = section.GetValue("GetClaimsFromUserInfoEndpoint", true); - var requireHttpsMetadata = section.GetValue("RequireHttpsMetadata", true); - - services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) - .AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options => - { - options.Authority = authority; - options.ClientId = clientId; - options.ClientSecret = clientSecret; - options.ResponseType = responseType; - options.SaveTokens = saveTokens; - options.GetClaimsFromUserInfoEndpoint = getClaimsFromUserInfoEndpoint; - options.RequireHttpsMetadata = requireHttpsMetadata; - options.CallbackPath = callbackPath; - options.SignedOutCallbackPath = signedOutCallbackPath; - - // Configure sign-out redirect - options.SignedOutRedirectUri = "/?logout=true"; - - // Request standard OIDC scopes - options.Scope.Clear(); - options.Scope.Add("openid"); - options.Scope.Add("profile"); - options.Scope.Add("email"); - - // Map Keycloak claims to standard claims - options.TokenValidationParameters.NameClaimType = "preferred_username"; - options.TokenValidationParameters.RoleClaimType = "roles"; - - // Configure OIDC events - options.Events = new OpenIdConnectEvents - { - OnTokenValidated = async context => - { - Console.WriteLine("Keycloak SSO: Token validated successfully"); - - // Try to find email in various claim types - var email = context.Principal?.FindFirst(System.Security.Claims.ClaimTypes.Email)?.Value - ?? context.Principal?.FindFirst("email")?.Value - ?? context.Principal?.FindFirst("preferred_username")?.Value - ?? context.Principal?.FindFirst("upn")?.Value; - - if (!string.IsNullOrEmpty(email)) - { - Console.WriteLine($"Keycloak SSO: User email identified as: {email}"); - - // Check if user exists in database with SSO role - bool userAuthorized = await NetLock_RMM_Web_Console.Classes.Authentication.User.CheckSsoUserAuthorization(email); - - if (!userAuthorized) - { - Console.WriteLine($"Keycloak SSO: User {email} not authorized or does not have SSO role"); - Logging.Handler.Debug("Keycloak SSO", "Authorization Failed", $"User {email} not authorized for SSO login"); - - // Reject the authentication - context.Fail("User is not authorized for SSO login. Please contact your administrator."); - return; - } - - Console.WriteLine($"Keycloak SSO: User {email} authorized for SSO login"); - Logging.Handler.Debug("Keycloak SSO", "Authorization Success", $"User {email} authorized"); - } - else - { - Console.WriteLine("Keycloak SSO: Warning - Could not find email claim in token"); - // Log all claims for debugging - foreach (var claim in context.Principal?.Claims ?? Enumerable.Empty()) - { - Console.WriteLine($" Claim: {claim.Type} = {claim.Value}"); - } - - // Reject authentication if no email found - context.Fail("Email claim not found in authentication token."); - } - }, - OnAuthenticationFailed = context => - { - Console.WriteLine($"Keycloak SSO: Authentication failed - {context.Exception?.Message}"); - context.Response.Redirect("/"); - context.HandleResponse(); - return Task.CompletedTask; - }, - OnRemoteFailure = context => - { - Console.WriteLine($"Keycloak SSO: Remote failure - {context.Failure?.Message}"); - context.Response.Redirect("/"); - context.HandleResponse(); - return Task.CompletedTask; - }, - OnRedirectToIdentityProviderForSignOut = context => - { - Console.WriteLine("Keycloak SSO: Redirecting to Keycloak for sign-out"); - Console.WriteLine($"Keycloak SSO: Post logout redirect URI: {context.Properties.RedirectUri}"); - return Task.CompletedTask; - }, - OnSignedOutCallbackRedirect = context => - { - Console.WriteLine("Keycloak SSO: Signed out callback redirect"); - Console.WriteLine($"Keycloak SSO: Redirecting to: {context.Properties?.RedirectUri ?? "default"}"); - return Task.CompletedTask; - } - }; - }); - - // Add Authorization services - services.AddAuthorization(); - } - catch (Exception ex) - { - Console.WriteLine($"Error configuring Keycloak SSO: {ex.Message}"); - throw; - } - } - - /// - /// Registers Google Workspace / Google Identity authentication - /// - private static void RegisterGoogleIdentity(IServiceCollection services, IConfigurationSection section) - { - try - { - var clientId = section.GetValue("ClientId"); - var clientSecret = section.GetValue("ClientSecret"); - var callbackPath = section.GetValue("CallbackPath") ?? "/signin-google"; - var signedOutCallbackPath = section.GetValue("SignedOutCallbackPath") ?? "/signout-callback-google"; - var saveTokens = section.GetValue("SaveTokens", false); - var hostedDomain = section.GetValue("HostedDomain"); // Optional: Restrict to specific Google Workspace domain - - services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) - .AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options => - { - // Google's OpenID Connect endpoints - options.Authority = "https://accounts.google.com"; - options.ClientId = clientId; - options.ClientSecret = clientSecret; - options.ResponseType = "code"; - options.SaveTokens = saveTokens; - options.GetClaimsFromUserInfoEndpoint = true; - options.CallbackPath = callbackPath; - options.SignedOutCallbackPath = signedOutCallbackPath; - - // Configure sign-out redirect - options.SignedOutRedirectUri = "/?logout=true"; - - // Request standard OIDC scopes - options.Scope.Clear(); - options.Scope.Add("openid"); - options.Scope.Add("profile"); - options.Scope.Add("email"); - - // Configure OIDC events - options.Events = new OpenIdConnectEvents - { - OnRedirectToIdentityProvider = context => - { - // If hosted domain is specified, add hd parameter to restrict to that domain - if (!string.IsNullOrEmpty(hostedDomain)) - { - context.ProtocolMessage.SetParameter("hd", hostedDomain); - } - return Task.CompletedTask; - }, - OnTokenValidated = async context => - { - await ValidateGoogleIdentityToken(context, hostedDomain); - }, - OnAuthenticationFailed = context => - { - Console.WriteLine($"Google SSO: Authentication failed - {context.Exception?.Message}"); - context.Response.Redirect("/"); - context.HandleResponse(); - return Task.CompletedTask; - }, - OnRemoteFailure = context => - { - Console.WriteLine($"Google SSO: Remote failure - {context.Failure?.Message}"); - context.Response.Redirect("/"); - context.HandleResponse(); - return Task.CompletedTask; - }, - OnRedirectToIdentityProviderForSignOut = context => - { - Console.WriteLine("Google SSO: Redirecting to Google for sign-out"); - Console.WriteLine($"Google SSO: Post logout redirect URI: {context.Properties.RedirectUri}"); - return Task.CompletedTask; - }, - OnSignedOutCallbackRedirect = context => - { - Console.WriteLine("Google SSO: Signed out callback redirect"); - Console.WriteLine($"Google SSO: Redirecting to: {context.Properties?.RedirectUri ?? "default"}"); - return Task.CompletedTask; - } - }; - - // Map Google claims to standard claims - options.TokenValidationParameters.NameClaimType = "name"; - options.TokenValidationParameters.RoleClaimType = "role"; - }); - - // Add Authorization services - services.AddAuthorization(); - } - catch (Exception ex) - { - Console.WriteLine($"Error configuring Google SSO: {ex.Message}"); - throw; - } - } - - /// - /// Validates Google token and checks user authorization - /// - private static async Task ValidateGoogleIdentityToken(TokenValidatedContext context, string? hostedDomain) - { - try - { - Console.WriteLine("Google SSO: Token validated successfully"); - - // Try to find email in various claim types - var email = context.Principal?.FindFirst(System.Security.Claims.ClaimTypes.Email)?.Value - ?? context.Principal?.FindFirst("email")?.Value; - - if (!string.IsNullOrEmpty(email)) - { - Console.WriteLine($"Google SSO: User email identified as: {email}"); - - // If hosted domain is specified, verify the email domain matches - if (!string.IsNullOrEmpty(hostedDomain)) - { - var emailDomain = email.Split('@').LastOrDefault(); - if (!string.Equals(emailDomain, hostedDomain, StringComparison.OrdinalIgnoreCase)) - { - Console.WriteLine($"Google SSO: Email domain {emailDomain} does not match required domain {hostedDomain}"); - Logging.Handler.Debug("Google SSO", "Domain Validation Failed", - $"User {email} does not belong to required domain {hostedDomain}"); - context.Fail($"Email must belong to {hostedDomain} domain."); - return; - } - } - - // Check if user exists in database with SSO role - bool userAuthorized = await NetLock_RMM_Web_Console.Classes.Authentication.User.CheckSsoUserAuthorization(email); - - if (!userAuthorized) - { - Console.WriteLine($"Google SSO: User {email} not authorized or does not have SSO role"); - Logging.Handler.Debug("Google SSO", "Authorization Failed", - $"User {email} not authorized for SSO login"); - - // Reject the authentication - context.Fail("User is not authorized for SSO login. Please contact your administrator."); - return; - } - - Console.WriteLine($"Google SSO: User {email} authorized for SSO login"); - Logging.Handler.Debug("Google SSO", "Authorization Success", $"User {email} authorized"); - } - else - { - Console.WriteLine("Google SSO: Warning - Could not find email claim in token"); - // Log all claims for debugging - foreach (var claim in context.Principal?.Claims ?? Enumerable.Empty()) - { - Console.WriteLine($" Claim: {claim.Type} = {claim.Value}"); - } - - // Reject authentication if no email found - context.Fail("Email claim not found in authentication token."); - } - } - catch (Exception e) - { - Console.WriteLine($"Google SSO: Error during token validation - {e.Message}"); - context.Fail("Error during token validation."); - } - } - - /// - /// Registers Okta authentication - /// - private static void RegisterOkta(IServiceCollection services, IConfigurationSection section) - { - try - { - var domain = section.GetValue("Domain"); - var clientId = section.GetValue("ClientId"); - var clientSecret = section.GetValue("ClientSecret"); - var callbackPath = section.GetValue("CallbackPath") ?? "/signin-okta"; - var signedOutCallbackPath = section.GetValue("SignedOutCallbackPath") ?? "/signout-callback-okta"; - var saveTokens = section.GetValue("SaveTokens", false); - var getClaimsFromUserInfoEndpoint = section.GetValue("GetClaimsFromUserInfoEndpoint", true); - var authorizationServerId = section.GetValue("AuthorizationServerId"); - - // Build Okta authority URL - // If authorization server ID is provided, use it (e.g., "default" or custom) - // Otherwise use the org authorization server - var authority = string.IsNullOrEmpty(authorizationServerId) - ? $"https://{domain}/oauth2" - : $"https://{domain}/oauth2/{authorizationServerId}"; - - services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) - .AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options => - { - options.Authority = authority; - options.ClientId = clientId; - options.ClientSecret = clientSecret; - options.ResponseType = "code"; - options.SaveTokens = saveTokens; - options.GetClaimsFromUserInfoEndpoint = getClaimsFromUserInfoEndpoint; - options.CallbackPath = callbackPath; - options.SignedOutCallbackPath = signedOutCallbackPath; - - // Configure sign-out redirect - options.SignedOutRedirectUri = "/?logout=true"; - - // Request standard OIDC scopes - options.Scope.Clear(); - options.Scope.Add("openid"); - options.Scope.Add("profile"); - options.Scope.Add("email"); - - // Map Okta claims to standard claims - options.TokenValidationParameters.NameClaimType = "name"; - options.TokenValidationParameters.RoleClaimType = "groups"; - - // Configure OIDC events - options.Events = new OpenIdConnectEvents - { - OnTokenValidated = async context => - { - Console.WriteLine("Okta SSO: Token validated successfully"); - - // Try to find email in various claim types - var email = context.Principal?.FindFirst(System.Security.Claims.ClaimTypes.Email)?.Value - ?? context.Principal?.FindFirst("email")?.Value - ?? context.Principal?.FindFirst("preferred_username")?.Value; - - if (!string.IsNullOrEmpty(email)) - { - Console.WriteLine($"Okta SSO: User email identified as: {email}"); - - // Check if user exists in database with SSO role - bool userAuthorized = await NetLock_RMM_Web_Console.Classes.Authentication.User.CheckSsoUserAuthorization(email); - - if (!userAuthorized) - { - Console.WriteLine($"Okta SSO: User {email} not authorized or does not have SSO role"); - Logging.Handler.Debug("Okta SSO", "Authorization Failed", - $"User {email} not authorized for SSO login"); - - // Reject the authentication - context.Fail("User is not authorized for SSO login. Please contact your administrator."); - return; - } - - Console.WriteLine($"Okta SSO: User {email} authorized for SSO login"); - Logging.Handler.Debug("Okta SSO", "Authorization Success", $"User {email} authorized"); - } - else - { - Console.WriteLine("Okta SSO: Warning - Could not find email claim in token"); - // Log all claims for debugging - foreach (var claim in context.Principal?.Claims ?? Enumerable.Empty()) - { - Console.WriteLine($" Claim: {claim.Type} = {claim.Value}"); - } - - // Reject authentication if no email found - context.Fail("Email claim not found in authentication token."); - } - }, - OnAuthenticationFailed = context => - { - Console.WriteLine($"Okta SSO: Authentication failed - {context.Exception?.Message}"); - context.Response.Redirect("/"); - context.HandleResponse(); - return Task.CompletedTask; - }, - OnRemoteFailure = context => - { - Console.WriteLine($"Okta SSO: Remote failure - {context.Failure?.Message}"); - context.Response.Redirect("/"); - context.HandleResponse(); - return Task.CompletedTask; - }, - OnRedirectToIdentityProviderForSignOut = context => - { - Console.WriteLine("Okta SSO: Redirecting to Okta for sign-out"); - Console.WriteLine($"Okta SSO: Post logout redirect URI: {context.Properties.RedirectUri}"); - return Task.CompletedTask; - }, - OnSignedOutCallbackRedirect = context => - { - Console.WriteLine("Okta SSO: Signed out callback redirect"); - Console.WriteLine($"Okta SSO: Redirecting to: {context.Properties?.RedirectUri ?? "default"}"); - return Task.CompletedTask; - } - }; - }); - - // Add Authorization services - services.AddAuthorization(); - } - catch (Exception ex) - { - Console.WriteLine($"Error configuring Okta SSO: {ex.Message}"); - throw; - } - } - - /// - /// Registers Auth0 authentication - /// - private static void RegisterAuth0(IServiceCollection services, IConfigurationSection section) - { - try - { - var domain = section.GetValue("Domain"); - var clientId = section.GetValue("ClientId"); - var clientSecret = section.GetValue("ClientSecret"); - var callbackPath = section.GetValue("CallbackPath") ?? "/signin-auth0"; - var signedOutCallbackPath = section.GetValue("SignedOutCallbackPath") ?? "/signout-callback-auth0"; - var saveTokens = section.GetValue("SaveTokens", false); - var audience = section.GetValue("Audience"); - - // Build Auth0 authority URL - var authority = $"https://{domain}"; - - services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) - .AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options => - { - options.Authority = authority; - options.ClientId = clientId; - options.ClientSecret = clientSecret; - options.ResponseType = "code"; - options.SaveTokens = saveTokens; - options.GetClaimsFromUserInfoEndpoint = true; - options.CallbackPath = callbackPath; - options.SignedOutCallbackPath = signedOutCallbackPath; - - // Configure sign-out redirect - options.SignedOutRedirectUri = "/?logout=true"; - - // Request standard OIDC scopes - options.Scope.Clear(); - options.Scope.Add("openid"); - options.Scope.Add("profile"); - options.Scope.Add("email"); - - // If audience is specified, add it (for API access) - if (!string.IsNullOrEmpty(audience)) - { - options.TokenValidationParameters.ValidAudiences = new[] { clientId, audience }; - } - - // Map Auth0 claims to standard claims - options.TokenValidationParameters.NameClaimType = "name"; - options.TokenValidationParameters.RoleClaimType = "https://schemas.auth0.com/roles"; - - // Configure OIDC events - options.Events = new OpenIdConnectEvents - { - OnTokenValidated = async context => - { - Console.WriteLine("Auth0 SSO: Token validated successfully"); - - // Try to find email in various claim types - var email = context.Principal?.FindFirst(System.Security.Claims.ClaimTypes.Email)?.Value - ?? context.Principal?.FindFirst("email")?.Value - ?? context.Principal?.FindFirst("preferred_username")?.Value; - - if (!string.IsNullOrEmpty(email)) - { - Console.WriteLine($"Auth0 SSO: User email identified as: {email}"); - - // Check if user exists in database with SSO role - bool userAuthorized = await NetLock_RMM_Web_Console.Classes.Authentication.User.CheckSsoUserAuthorization(email); - - if (!userAuthorized) - { - Console.WriteLine($"Auth0 SSO: User {email} not authorized or does not have SSO role"); - Logging.Handler.Debug("Auth0 SSO", "Authorization Failed", - $"User {email} not authorized for SSO login"); - - // Reject the authentication - context.Fail("User is not authorized for SSO login. Please contact your administrator."); - return; - } - - Console.WriteLine($"Auth0 SSO: User {email} authorized for SSO login"); - Logging.Handler.Debug("Auth0 SSO", "Authorization Success", $"User {email} authorized"); - } - else - { - Console.WriteLine("Auth0 SSO: Warning - Could not find email claim in token"); - // Log all claims for debugging - foreach (var claim in context.Principal?.Claims ?? Enumerable.Empty()) - { - Console.WriteLine($" Claim: {claim.Type} = {claim.Value}"); - } - - // Reject authentication if no email found - context.Fail("Email claim not found in authentication token."); - } - }, - OnAuthenticationFailed = context => - { - Console.WriteLine($"Auth0 SSO: Authentication failed - {context.Exception?.Message}"); - context.Response.Redirect("/"); - context.HandleResponse(); - return Task.CompletedTask; - }, - OnRemoteFailure = context => - { - Console.WriteLine($"Auth0 SSO: Remote failure - {context.Failure?.Message}"); - context.Response.Redirect("/"); - context.HandleResponse(); - return Task.CompletedTask; - }, - OnRedirectToIdentityProviderForSignOut = context => - { - Console.WriteLine("Auth0 SSO: Redirecting to Auth0 for sign-out"); - - // Auth0 logout URL format: https://YOUR_DOMAIN/v2/logout?client_id=YOUR_CLIENT_ID&returnTo=RETURN_URL - var logoutUri = $"https://{domain}/v2/logout?client_id={clientId}"; - var returnUri = context.Properties?.RedirectUri ?? "/?logout=true"; - - // Build full return URL - var request = context.Request; - var returnUrl = $"{request.Scheme}://{request.Host}{returnUri}"; - - logoutUri += $"&returnTo={Uri.EscapeDataString(returnUrl)}"; - - Console.WriteLine($"Auth0 SSO: Logout URI: {logoutUri}"); - - context.Response.Redirect(logoutUri); - context.HandleResponse(); - - return Task.CompletedTask; - }, - OnSignedOutCallbackRedirect = context => - { - Console.WriteLine("Auth0 SSO: Signed out callback redirect"); - Console.WriteLine($"Auth0 SSO: Redirecting to: {context.Properties?.RedirectUri ?? "default"}"); - return Task.CompletedTask; - } - }; - }); - - // Add Authorization services - services.AddAuthorization(); - } - catch (Exception ex) - { - Console.WriteLine($"Error configuring Auth0 SSO: {ex.Message}"); - throw; - } - } - } -} - +// Used for SSO +//OSSCH_START 79ea1465-aa2d-41d1-9654-6b5e18bd0ba4 //OSSCH_END \ No newline at end of file diff --git a/NetLock-RMM-Web-Console/Classes/Authentication/SsoConfig.cs b/NetLock-RMM-Web-Console/Classes/Authentication/SsoConfig.cs index da6b054c..b2a6200d 100644 --- a/NetLock-RMM-Web-Console/Classes/Authentication/SsoConfig.cs +++ b/NetLock-RMM-Web-Console/Classes/Authentication/SsoConfig.cs @@ -1,82 +1 @@ -namespace NetLock_RMM_Web_Console.Classes.Authentication -{ - /// - /// Configuration models for SSO providers - /// - - public class SsoConfig - { - public bool Enabled { get; set; } - public AzureAdConfig? AzureAd { get; set; } - public KeycloakConfig? Keycloak { get; set; } - public GoogleIdentityConfig? GoogleIdentity { get; set; } - public OktaConfig? Okta { get; set; } - public Auth0Config? Auth0 { get; set; } - } - - public class AzureAdConfig - { - public bool Enabled { get; set; } - public string Instance { get; set; } = string.Empty; - public string Domain { get; set; } = string.Empty; - public string TenantId { get; set; } = string.Empty; - public string ClientId { get; set; } = string.Empty; - public string CallbackPath { get; set; } = string.Empty; - public string ClientSecret { get; set; } = string.Empty; - public string SignedOutCallbackPath { get; set; } = string.Empty; - public string ResponseType { get; set; } = string.Empty; - public bool SaveTokens { get; set; } - } - - public class KeycloakConfig - { - public bool Enabled { get; set; } - public string Authority { get; set; } = string.Empty; - public string Realm { get; set; } = string.Empty; - public string ClientId { get; set; } = string.Empty; - public string ClientSecret { get; set; } = string.Empty; - public string CallbackPath { get; set; } = string.Empty; - public string SignedOutCallbackPath { get; set; } = string.Empty; - public string ResponseType { get; set; } = string.Empty; - public bool SaveTokens { get; set; } - public bool GetClaimsFromUserInfoEndpoint { get; set; } - public bool RequireHttpsMetadata { get; set; } - } - - public class GoogleIdentityConfig - { - public bool Enabled { get; set; } - public string ClientId { get; set; } = string.Empty; - public string ClientSecret { get; set; } = string.Empty; - public string CallbackPath { get; set; } = string.Empty; - public string SignedOutCallbackPath { get; set; } = string.Empty; - public bool SaveTokens { get; set; } - public string? HostedDomain { get; set; } // Optional: Restrict to specific Google Workspace domain - } - - public class OktaConfig - { - public bool Enabled { get; set; } - public string Domain { get; set; } = string.Empty; // e.g., dev-12345.okta.com - public string ClientId { get; set; } = string.Empty; - public string ClientSecret { get; set; } = string.Empty; - public string CallbackPath { get; set; } = string.Empty; - public string SignedOutCallbackPath { get; set; } = string.Empty; - public bool SaveTokens { get; set; } - public bool GetClaimsFromUserInfoEndpoint { get; set; } - public string? AuthorizationServerId { get; set; } // Optional: default or custom authorization server ID - } - - public class Auth0Config - { - public bool Enabled { get; set; } - public string Domain { get; set; } = string.Empty; // e.g., your-tenant.auth0.com or your-tenant.eu.auth0.com - public string ClientId { get; set; } = string.Empty; - public string ClientSecret { get; set; } = string.Empty; - public string CallbackPath { get; set; } = string.Empty; - public string SignedOutCallbackPath { get; set; } = string.Empty; - public bool SaveTokens { get; set; } - public string? Audience { get; set; } // Optional: API Identifier for custom API - } -} - +//OSSCH_START f0bdbf1d-57af-4f76-b378-a90932d99cea //OSSCH_END \ No newline at end of file diff --git a/NetLock-RMM-Web-Console/Classes/Helper/Encryption.cs b/NetLock-RMM-Web-Console/Classes/Helper/Encryption.cs index 89b628ff..731d5ef1 100644 --- a/NetLock-RMM-Web-Console/Classes/Helper/Encryption.cs +++ b/NetLock-RMM-Web-Console/Classes/Helper/Encryption.cs @@ -9,5 +9,5 @@ namespace Encryption { - //OSSCH_START de3c6ec8-bcb5-4684-b418-e1b95b55cd21 //OSSCH_END + //OSSCH_START 6b185bb2-c7f7-49f1-8081-7af9795377bc //OSSCH_END } diff --git a/NetLock-RMM-Web-Console/Classes/Helper/Http.cs b/NetLock-RMM-Web-Console/Classes/Helper/Http.cs index 4f1369f9..d73cf730 100644 --- a/NetLock-RMM-Web-Console/Classes/Helper/Http.cs +++ b/NetLock-RMM-Web-Console/Classes/Helper/Http.cs @@ -6,11 +6,11 @@ namespace NetLock_RMM_Web_Console.Classes.Helper { public class Http { - public static async Task Get_Request_With_Api_Key(string url) + public static async Task Get_Request_With_Api_Key(string url, bool membersPortal) { try { - string api_key = await Classes.MySQL.Handler.Get_Api_Key(); + string api_key = await Classes.MySQL.Handler.Get_Api_Key(membersPortal); using (var httpClient = new HttpClient()) { @@ -19,7 +19,7 @@ public static async Task Get_Request_With_Api_Key(string url) // GET Request absenden var response = await httpClient.GetAsync(url); - + if (response.IsSuccessStatusCode) { // Read response @@ -38,16 +38,17 @@ public static async Task Get_Request_With_Api_Key(string url) } catch (Exception ex) { + Console.WriteLine(ex.ToString()); Logging.Handler.Error("Online_Mode.Handler.Get_Request_With_Api_Key", "General error", ex.ToString()); return String.Empty; } } - public static async Task POST_Request_Json_With_Api_Key(string url, string json) + public static async Task POST_Request_Json_With_Api_Key(string url, string json, bool membersPortal) { try { - string api_key = await Classes.MySQL.Handler.Get_Api_Key(); + string api_key = await Classes.MySQL.Handler.Get_Api_Key(membersPortal); using (var httpClient = new HttpClient()) { diff --git a/NetLock-RMM-Web-Console/Classes/Helper/IO.cs b/NetLock-RMM-Web-Console/Classes/Helper/IO.cs index f9c85c78..dd3000e8 100644 --- a/NetLock-RMM-Web-Console/Classes/Helper/IO.cs +++ b/NetLock-RMM-Web-Console/Classes/Helper/IO.cs @@ -4,6 +4,6 @@ namespace NetLock_RMM_Web_Console.Classes.Helper { public class IO { - //OSSCH_START 838f8336-9a92-4a72-90b6-487b9cee5551 //OSSCH_END + //OSSCH_START 33e8fe61-93b9-4ec7-976b-f14bf2a96620 //OSSCH_END } } diff --git a/NetLock-RMM-Web-Console/Classes/Helper/Notifications/WebConsole.cs b/NetLock-RMM-Web-Console/Classes/Helper/Notifications/WebConsole.cs new file mode 100644 index 00000000..7b89b7c6 --- /dev/null +++ b/NetLock-RMM-Web-Console/Classes/Helper/Notifications/WebConsole.cs @@ -0,0 +1,133 @@ +using MudBlazor; + +namespace NetLock_RMM_Web_Console.Classes.Helper.Notifications +{ + /// + /// Helper class for displaying beautiful notifications in the Web Console + /// Uses MudBlazor Snackbar to show toast notifications in the bottom-right corner + /// + public static class WebConsole + { + /// + /// Shows a success notification (green) + /// + /// MudBlazor ISnackbar instance + /// Message to display + /// Duration in milliseconds (default: 3000ms) + public static void Success(ISnackbar snackbar, string message, int duration = 3000) + { + snackbar.Configuration.PositionClass = Defaults.Classes.Position.BottomRight; + snackbar.Configuration.SnackbarVariant = Variant.Filled; + snackbar.Configuration.VisibleStateDuration = duration; + snackbar.Configuration.ShowCloseIcon = true; + snackbar.Configuration.MaxDisplayedSnackbars = 5; + + snackbar.Add(message, Severity.Success); + } + + /// + /// Shows an info notification (blue) + /// + /// MudBlazor ISnackbar instance + /// Message to display + /// Duration in milliseconds (default: 3000ms) + public static void Info(ISnackbar snackbar, string message, int duration = 3000) + { + snackbar.Configuration.PositionClass = Defaults.Classes.Position.BottomRight; + snackbar.Configuration.SnackbarVariant = Variant.Filled; + snackbar.Configuration.VisibleStateDuration = duration; + snackbar.Configuration.ShowCloseIcon = true; + snackbar.Configuration.MaxDisplayedSnackbars = 5; + + snackbar.Add(message, Severity.Info); + } + + /// + /// Shows a warning notification (orange) + /// + /// MudBlazor ISnackbar instance + /// Message to display + /// Duration in milliseconds (default: 4000ms) + public static void Warning(ISnackbar snackbar, string message, int duration = 4000) + { + snackbar.Configuration.PositionClass = Defaults.Classes.Position.BottomRight; + snackbar.Configuration.SnackbarVariant = Variant.Filled; + snackbar.Configuration.VisibleStateDuration = duration; + snackbar.Configuration.ShowCloseIcon = true; + snackbar.Configuration.MaxDisplayedSnackbars = 5; + + snackbar.Add(message, Severity.Warning); + } + + /// + /// Shows an error notification (red) + /// + /// MudBlazor ISnackbar instance + /// Message to display + /// Duration in milliseconds (default: 5000ms) + public static void Error(ISnackbar snackbar, string message, int duration = 5000) + { + snackbar.Configuration.PositionClass = Defaults.Classes.Position.BottomRight; + snackbar.Configuration.SnackbarVariant = Variant.Filled; + snackbar.Configuration.VisibleStateDuration = duration; + snackbar.Configuration.ShowCloseIcon = true; + snackbar.Configuration.MaxDisplayedSnackbars = 5; + + snackbar.Add(message, Severity.Error); + } + + /// + /// Shows a normal notification (default gray) + /// + /// MudBlazor ISnackbar instance + /// Message to display + /// Duration in milliseconds (default: 3000ms) + public static void Normal(ISnackbar snackbar, string message, int duration = 3000) + { + snackbar.Configuration.PositionClass = Defaults.Classes.Position.BottomRight; + snackbar.Configuration.SnackbarVariant = Variant.Filled; + snackbar.Configuration.VisibleStateDuration = duration; + snackbar.Configuration.ShowCloseIcon = true; + snackbar.Configuration.MaxDisplayedSnackbars = 5; + + snackbar.Add(message, Severity.Normal); + } + + /// + /// Shows a custom notification with emoji + /// + /// MudBlazor ISnackbar instance + /// Message to display + /// Emoji to add (e.g., "βœ…", "πŸš€", "πŸ””") + /// Severity level + /// Duration in milliseconds (default: 3000ms) + public static void Custom(ISnackbar snackbar, string message, string emoji, Severity severity = Severity.Normal, int duration = 3000) + { + snackbar.Configuration.PositionClass = Defaults.Classes.Position.BottomRight; + snackbar.Configuration.SnackbarVariant = Variant.Filled; + snackbar.Configuration.VisibleStateDuration = duration; + snackbar.Configuration.ShowCloseIcon = true; + snackbar.Configuration.MaxDisplayedSnackbars = 5; + + snackbar.Add($"{emoji} {message}", severity); + } + + /// + /// Shows an auto-refresh notification (for background updates) + /// + /// MudBlazor ISnackbar instance + /// Type of items refreshed (e.g., "Events", "Devices") + /// Number of items refreshed + public static void AutoRefresh(ISnackbar snackbar, string itemType, int count) + { + snackbar.Configuration.PositionClass = Defaults.Classes.Position.BottomRight; + snackbar.Configuration.SnackbarVariant = Variant.Filled; + snackbar.Configuration.VisibleStateDuration = 2000; + snackbar.Configuration.ShowCloseIcon = false; + snackbar.Configuration.MaxDisplayedSnackbars = 5; + + snackbar.Add($"πŸ”„ {itemType} refreshed ({count})", Severity.Info); + } + } +} + diff --git a/NetLock-RMM-Web-Console/Classes/Members_Portal/Config.cs b/NetLock-RMM-Web-Console/Classes/Members_Portal/Config.cs index 7b0ddd8f..0488900d 100644 --- a/NetLock-RMM-Web-Console/Classes/Members_Portal/Config.cs +++ b/NetLock-RMM-Web-Console/Classes/Members_Portal/Config.cs @@ -1,4 +1,4 @@ namespace NetLock_RMM_Web_Console.Classes.Members_Portal { - //OSSCH_START 435b840e-0f23-466b-a10d-5b42e624855a //OSSCH_END + //OSSCH_START 6ad8d965-03b0-4dae-9ff2-9bc4283ed15a //OSSCH_END } diff --git a/NetLock-RMM-Web-Console/Classes/Members_Portal/Handler.cs b/NetLock-RMM-Web-Console/Classes/Members_Portal/Handler.cs index 0c10d469..e7913361 100644 --- a/NetLock-RMM-Web-Console/Classes/Members_Portal/Handler.cs +++ b/NetLock-RMM-Web-Console/Classes/Members_Portal/Handler.cs @@ -3,5 +3,5 @@ using System.Globalization; using System.Text.Json; -//OSSCH_START 6a0395e0-2c72-4972-a26b-b5b0c5b0fd11 //OSSCH_END +//OSSCH_START c0baa66a-dcc3-434d-876b-1c66807a78c6 //OSSCH_END diff --git a/NetLock-RMM-Web-Console/Classes/MySQL/Database.cs b/NetLock-RMM-Web-Console/Classes/MySQL/Database.cs index 8d46680f..a04afe58 100644 --- a/NetLock-RMM-Web-Console/Classes/MySQL/Database.cs +++ b/NetLock-RMM-Web-Console/Classes/MySQL/Database.cs @@ -616,7 +616,6 @@ public static async Task Execute_Update_Scripts() scripts.Add(upgrade_script_2_5_2_2c_to_2_5_2_7); scripts.Add(upgrade_script_2_5_2_7_to_2_5_3_0); scripts.Add(upgrade_script_2_5_3_4_to_2_5_3_4b); - // Disabled due to testing... /*if (db_version == "2.0.0.0") @@ -790,9 +789,9 @@ public static async Task Fix_Settings() } } - //OSSCH_START c08671e5-704e-4cb5-a1ee-40f703ffdaf7 //OSSCH_END + //OSSCH_START 76be5105-3025-49ed-8811-cadfd6ce4fc8 //OSSCH_END // Reset database - //OSSCH_START 23cab9c1-8b7b-4fda-b8d1-3cb99bc80862 //OSSCH_END + //OSSCH_START ae6d6d4c-5a28-478b-8020-18dd15285ad1 //OSSCH_END } } diff --git a/NetLock-RMM-Web-Console/Classes/MySQL/Handler.cs b/NetLock-RMM-Web-Console/Classes/MySQL/Handler.cs index 085ecbc6..aa3f2183 100644 --- a/NetLock-RMM-Web-Console/Classes/MySQL/Handler.cs +++ b/NetLock-RMM-Web-Console/Classes/MySQL/Handler.cs @@ -1,4 +1,4 @@ -ο»Ώusing MySqlConnector; +using MySqlConnector; using System.Data.Common; using System; using System.Data; @@ -353,9 +353,12 @@ public static async Task Get_Session_Guid_By_Username(string username) } // Get api_key from settings table - public static async Task Get_Api_Key() + public static async Task Get_Api_Key(bool membersPortal) { - return await Quick_Reader("SELECT members_portal_api_key FROM settings;", "members_portal_api_key"); + if (membersPortal) + return await Quick_Reader("SELECT members_portal_api_key FROM settings;", "members_portal_api_key"); + else + return await Quick_Reader("SELECT * FROM settings;", "files_api_key"); } // Get device platform @@ -597,7 +600,7 @@ public static async Task GetAssignedDevicePolicyByDeviceId(string device return policy_name; } - private static async Task GetDeviceNameById(string device_id) + public static async Task GetDeviceNameById(string device_id) { MySqlConnection conn = new MySqlConnection(Configuration.MySQL.Connection_String); @@ -607,10 +610,12 @@ private static async Task GetDeviceNameById(string device_id) MySqlCommand command = new MySqlCommand(query, conn); command.Parameters.AddWithValue("@device_id", device_id); - + Logging.Handler.Debug("Agent.Windows.Policy_Handler.Get_Policy (automations)", "MySQL_Prepared_Query", query); + await conn.OpenAsync(); + using (DbDataReader reader = await command.ExecuteReaderAsync()) { if (reader.HasRows) @@ -717,5 +722,86 @@ public static async Task Get_Location_Name_By_Device_Id(string device_id await conn.CloseAsync(); } } + + public static async Task<(bool, bool, bool, bool, bool, bool, bool)> GetPolicyRemoteFeatureSettings(string device_id) + { + string policyName = await GetAssignedDevicePolicyByDeviceId(device_id); + + MySqlConnection conn = new MySqlConnection(Configuration.MySQL.Connection_String); + + try + { + await conn.OpenAsync(); + + string query = "SELECT * FROM policies WHERE name = @policy_name;"; + + MySqlCommand cmd = new MySqlCommand(query, conn); + cmd.Parameters.AddWithValue("@policy_name", policyName); + + string agentSettingsJson = String.Empty; + + bool remoteServiceEnabled = false; + bool remoteShellEnabled = false; + bool remoteFileBrowserEnabled = false; + bool remoteTaskManagerEnabled = false; + bool remoteServiceManagerEnabled = false; + bool remoteScreenControlEnabled = false; + bool remoteScreenControlUnattendedAccess = false; + + using (DbDataReader reader = await cmd.ExecuteReaderAsync()) + { + if (reader.HasRows) + { + while (await reader.ReadAsync()) + { + agentSettingsJson = reader["agent_settings"].ToString() ?? String.Empty; + } + } + } + + // { + // "AutoUpdateEnabled": true, + // "SyncInterval": 5, + // "RemoteServiceEnabled": true, + // "RemoteShellEnabled": true, + // "RemoteFileBrowserEnabled": true, + // "RemoteTaskManagerEnabled": true, + // "RemoteServiceManagerEnabled": true, + // "RemoteScreenControlEnabled": true, + // "RemoteScreenControlUnattendedAccess": true + // } + + // Parse agentSettingsJson + if (!string.IsNullOrEmpty(agentSettingsJson)) + { + dynamic agentSettings = JsonConvert.DeserializeObject(agentSettingsJson); + remoteServiceEnabled = agentSettings.RemoteServiceEnabled; + remoteShellEnabled = agentSettings.RemoteShellEnabled; + remoteFileBrowserEnabled = agentSettings.RemoteFileBrowserEnabled; + remoteTaskManagerEnabled = agentSettings.RemoteTaskManagerEnabled; + remoteServiceManagerEnabled = agentSettings.RemoteServiceManagerEnabled; + remoteScreenControlEnabled = agentSettings.RemoteScreenControlEnabled; + remoteScreenControlUnattendedAccess = agentSettings.RemoteScreenControlUnattendedAccess; + } + + Logging.Handler.Debug("Classes.MySQL.Handler.GetPolicyRemoteFeatureSettings", "Query", query); + + return (remoteServiceEnabled, remoteShellEnabled, remoteFileBrowserEnabled, remoteTaskManagerEnabled, remoteServiceManagerEnabled, remoteScreenControlEnabled, remoteScreenControlUnattendedAccess); + } + catch (Exception ex) + { + Logging.Handler.Error("Classes.MySQL.Handler.GetPolicyRemoteFeatureSettings", "Query: ", ex.ToString()); + return (false, false, false, false, false, false, false); + } + finally + { + await conn.CloseAsync(); + } + } + + // Get SSO Configuration from database + //OSSCH_START 7831177c-ff42-45ad-ac05-18ad422c2c67 //OSSCH_END + + } } diff --git a/NetLock-RMM-Web-Console/Classes/Theme/ThemePalette.cs b/NetLock-RMM-Web-Console/Classes/Theme/ThemePalette.cs new file mode 100644 index 00000000..ed49dd7d --- /dev/null +++ b/NetLock-RMM-Web-Console/Classes/Theme/ThemePalette.cs @@ -0,0 +1,3 @@ +using MudBlazor; + +//OSSCH_START 1eb89cb6-6c93-454f-8324-cdd2219bd0e2 //OSSCH_END \ No newline at end of file diff --git a/NetLock-RMM-Web-Console/Components/App.razor b/NetLock-RMM-Web-Console/Components/App.razor index 1f5c1c3c..59be939e 100644 --- a/NetLock-RMM-Web-Console/Components/App.razor +++ b/NetLock-RMM-Web-Console/Components/App.razor @@ -9,7 +9,8 @@ - + + diff --git a/NetLock-RMM-Web-Console/Components/Layout/Agent_Download/Agent_Download_Dialog.razor b/NetLock-RMM-Web-Console/Components/Layout/Agent_Download/Agent_Download_Dialog.razor index 512b0005..c569b40e 100644 --- a/NetLock-RMM-Web-Console/Components/Layout/Agent_Download/Agent_Download_Dialog.razor +++ b/NetLock-RMM-Web-Console/Components/Layout/Agent_Download/Agent_Download_Dialog.razor @@ -43,7 +43,7 @@ { - +
@Localizer["target_server"] @@ -86,6 +86,8 @@ + + @Localizer["download_installer"] @@ -101,7 +103,7 @@
- @Localizer["download_installer"] + @Localizer["download_installer"] @@ -123,6 +125,41 @@ @if (!String.IsNullOrEmpty(installer_url) && !creating_installer) { Url: @installer_url + +
+ @if (architecture.StartsWith("win-")) + { + + Download Windows installation script (.ps1) + + + + + + } + + @if (architecture.StartsWith("linux-")) + { + + Download Linux installation script (.sh) + + + + + + } + + @if (architecture.StartsWith("osx-")) + { + + Download macOS installation script (.sh) + + + + + + } +
} @@ -163,6 +200,7 @@ private string location_id = String.Empty; private string language = String.Empty; private string architecture = "win-x64"; + private DateTime? autoAuthorizeUntilDateTime = DateTime.Now.AddDays(30); protected override async Task OnInitializedAsync() { @@ -181,95 +219,6 @@ StateHasChanged(); } - // Update ports - private async Task Update_Ports() - { - if (ssl) - { - if (communication_servers.Contains(":")) - { - string[] communication_servers_split = communication_servers.Split(':'); - - if (communication_servers_split[1] != "7443") - communication_servers = communication_servers_split[0] + ":7443"; - } - - if (remote_servers.Contains(":")) - { - string[] remote_servers_split = remote_servers.Split(':'); - - if (remote_servers_split[1] != "7443") - remote_servers = remote_servers_split[0] + ":7443"; - } - - if (update_servers.Contains(":")) - { - string[] update_servers_split = update_servers.Split(':'); - - if (update_servers_split[1] != "7443") - update_servers = update_servers_split[0] + ":7443"; - } - - if (trust_servers.Contains(":")) - { - string[] trust_servers_split = trust_servers.Split(':'); - - if (trust_servers_split[1] != "7443") - trust_servers = trust_servers_split[0] + ":7443"; - } - - if (file_servers.Contains(":")) - { - string[] file_servers_split = file_servers.Split(':'); - - if (file_servers_split[1] != "7443") - file_servers = file_servers_split[0] + ":7443"; - } - } - else - { - if (communication_servers.Contains(":")) - { - string[] communication_servers_split = communication_servers.Split(':'); - - if (communication_servers_split[1] != "7080") - communication_servers = communication_servers_split[0] + ":7080"; - } - - if (remote_servers.Contains(":")) - { - string[] remote_servers_split = remote_servers.Split(':'); - - if (remote_servers_split[1] != "7080") - remote_servers = remote_servers_split[0] + ":7080"; - } - - if (update_servers.Contains(":")) - { - string[] update_servers_split = update_servers.Split(':'); - - if (update_servers_split[1] != "7080") - update_servers = update_servers_split[0] + ":7080"; - } - - if (trust_servers.Contains(":")) - { - string[] trust_servers_split = trust_servers.Split(':'); - - if (trust_servers_split[1] != "7080") - trust_servers = trust_servers_split[0] + ":7080"; - } - - if (file_servers.Contains(":")) - { - string[] file_servers_split = file_servers.Split(':'); - - if (file_servers_split[1] != "7080") - file_servers = file_servers_split[0] + ":7080"; - } - } - } - private async Task CleanInput() { bool protocolRemoved = false; @@ -530,6 +479,7 @@ language = reader["language"].ToString() ?? String.Empty; ssl = reader["ssl"].ToString() == "1"; package_guid = reader["guid"].ToString() ?? String.Empty; + autoAuthorizeUntilDateTime = reader["auto_authorize_until"] != DBNull.Value ? Convert.ToDateTime(reader["auto_authorize_until"]) : DateTime.Now.AddDays(30); } } } @@ -668,7 +618,7 @@ { await conn.OpenAsync(); - string query = "UPDATE agent_package_configurations SET `name` = @name, `ssl` = @ssl, communication_servers = @communication_servers, remote_servers = @remote_servers, update_servers = @update_servers, trust_servers = @trust_servers, file_servers = @file_servers, tenant_id = @tenant_id, location_id = @location_id, language = @language WHERE id = @id;"; + string query = "UPDATE agent_package_configurations SET `name` = @name, `ssl` = @ssl, communication_servers = @communication_servers, remote_servers = @remote_servers, update_servers = @update_servers, trust_servers = @trust_servers, file_servers = @file_servers, tenant_id = @tenant_id, location_id = @location_id, language = @language, auto_authorize_until = @auto_authorize_until WHERE id = @id;"; MySqlCommand cmd = new MySqlCommand(query, conn); cmd.Parameters.AddWithValue("@id", id); @@ -682,6 +632,7 @@ cmd.Parameters.AddWithValue("@location_id", location_id); cmd.Parameters.AddWithValue("@language", language); cmd.Parameters.AddWithValue("@ssl", ssl ? 1 : 0); + cmd.Parameters.AddWithValue("@auto_authorize_until", autoAuthorizeUntilDateTime.HasValue ? autoAuthorizeUntilDateTime.Value : DBNull.Value); cmd.ExecuteNonQuery(); @@ -690,6 +641,7 @@ catch (Exception ex) { Logging.Handler.Error("/MainLayout -> Update_Configuration", "Result", ex.ToString()); + this.Snackbar.Add(Localizer["error_occurred"], Severity.Error); } finally { @@ -755,7 +707,7 @@ await Get_Configurations(); } - //OSSCH_START c83b5301-be4d-4a67-8ae1-6740b6c32c6b //OSSCH_END + //OSSCH_START 28649cca-2e9a-469f-8c17-167da62e8462 //OSSCH_END private async Task GetCloudPreset() { @@ -776,5 +728,323 @@ await JSRuntime.InvokeVoidAsync("window.open", installer_url, "_blank"); } + private async Task Download_Installation_Script_Windows() + { + try + { + string scriptContent = $@"# NetLock RMM Agent Installation Script for Windows +# This script downloads and installs the NetLock RMM Agent +$packageUrl = ""{installer_url}"" + +# Check if the service NetLock_RMM_Agent_Comm exists +$serviceName = ""NetLock_RMM_Agent_Comm"" +$service = Get-Service -Name $serviceName -ErrorAction SilentlyContinue + +if ($service) {{ + Write-Host ""Service '$serviceName' already exists. Installation will not proceed."" -ForegroundColor Yellow + exit 0 +}} + +Write-Host ""Service '$serviceName' not found. Proceeding with installation..."" -ForegroundColor Green + +# Package download URL +$tempFolder = ""C:\temp"" +$zipFile = ""$tempFolder\NetLock_Agent.zip"" +$extractFolder = ""$tempFolder\NetLock_Agent"" + +# Create temp folder if it doesn't exist +if (-not (Test-Path -Path $tempFolder)) {{ + Write-Host ""Creating folder: $tempFolder"" -ForegroundColor Cyan + New-Item -ItemType Directory -Path $tempFolder -Force | Out-Null +}} + +# Download the ZIP file +Write-Host ""Downloading NetLock RMM Agent package..."" -ForegroundColor Cyan +try {{ + Invoke-WebRequest -Uri $packageUrl -OutFile $zipFile -UseBasicParsing + Write-Host ""Download completed successfully."" -ForegroundColor Green +}} catch {{ + Write-Host ""Error downloading package: $_"" -ForegroundColor Red + exit 1 +}} + +# Unzip the package +Write-Host ""Extracting package to $extractFolder..."" -ForegroundColor Cyan +try {{ + # Remove existing extract folder if it exists + if (Test-Path -Path $extractFolder) {{ + Remove-Item -Path $extractFolder -Recurse -Force + }} + + Expand-Archive -Path $zipFile -DestinationPath $extractFolder -Force + Write-Host ""Extraction completed successfully."" -ForegroundColor Green +}} catch {{ + Write-Host ""Error extracting package: $_"" -ForegroundColor Red + exit 1 +}} + +# Find and execute the installer +$installerPath = Get-ChildItem -Path $extractFolder -Filter ""NetLock_RMM_Agent_Installer.exe"" -Recurse | Select-Object -First 1 + +if ($installerPath) {{ + Write-Host ""Running installer: $($installerPath.FullName)"" -ForegroundColor Cyan + try {{ + Start-Process -FilePath $installerPath.FullName -Wait -NoNewWindow + Write-Host ""Installation completed successfully."" -ForegroundColor Green + }} catch {{ + Write-Host ""Error running installer: $_"" -ForegroundColor Red + exit 1 + }} +}} else {{ + Write-Host ""Installer executable 'NetLock_RMM_Agent_Installer.exe' not found in the extracted package."" -ForegroundColor Red + exit 1 +}} + +# Cleanup +Write-Host ""Cleaning up temporary files..."" -ForegroundColor Cyan +try {{ + Remove-Item -Path $zipFile -Force -ErrorAction SilentlyContinue + Remove-Item -Path $extractFolder -Recurse -Force -ErrorAction SilentlyContinue + Write-Host ""Cleanup completed."" -ForegroundColor Green +}} catch {{ + Write-Host ""Warning: Could not clean up temporary files."" -ForegroundColor Yellow +}} + +Write-Host ""NetLock RMM Agent installation process completed!"" -ForegroundColor Green"; + + await JSRuntime.InvokeVoidAsync("exportToTxt", "Install-NetLockAgent.ps1", scriptContent); + + Snackbar.Configuration.PositionClass = Defaults.Classes.Position.BottomRight; + Snackbar.Add("Windows installation script downloaded successfully.", Severity.Success, config => { config.ShowCloseIcon = true; }); + } + catch (Exception ex) + { + Logging.Handler.Error("/MainLayout -> Download_Installation_Script_Windows", "Result", ex.ToString()); + Snackbar.Configuration.PositionClass = Defaults.Classes.Position.BottomRight; + Snackbar.Add("Error creating installation script. Please check the logs.", Severity.Error, config => { config.ShowCloseIcon = true; }); + } + } + + private async Task Download_Installation_Script_Linux() + { + try + { + string scriptContent = $@"#!/bin/bash +# NetLock RMM Agent Installation Script for Linux +# This script downloads and installs the NetLock RMM Agent + +PACKAGE_URL=""{installer_url}"" +SERVICE_NAME=""netlock-rmm-agent-comm"" + +# Check if running as root +if [ ""$EUID"" -ne 0 ]; then + echo ""Error: This script must be run as root (use sudo)"" + exit 1 +fi + +# Check if the service already exists +if systemctl list-units --full --all | grep -Fq ""$SERVICE_NAME.service""; then + echo ""Service '$SERVICE_NAME' already exists. Installation will not proceed."" + exit 0 +fi + +echo ""Service '$SERVICE_NAME' not found. Proceeding with installation..."" + +# Package download settings +TEMP_FOLDER=""/tmp/netlock_agent"" +ZIP_FILE=""$TEMP_FOLDER/NetLock_Agent.zip"" +EXTRACT_FOLDER=""$TEMP_FOLDER/NetLock_Agent"" + +# Create temp folder if it doesn't exist +if [ ! -d ""$TEMP_FOLDER"" ]; then + echo ""Creating folder: $TEMP_FOLDER"" + mkdir -p ""$TEMP_FOLDER"" +fi + +# Download the ZIP file +echo ""Downloading NetLock RMM Agent package..."" +if command -v wget &> /dev/null; then + wget -q --show-progress ""$PACKAGE_URL"" -O ""$ZIP_FILE"" +elif command -v curl &> /dev/null; then + curl -L ""$PACKAGE_URL"" -o ""$ZIP_FILE"" +else + echo ""Error: Neither wget nor curl is available. Please install one of them."" + exit 1 +fi + +if [ $? -eq 0 ]; then + echo ""Download completed successfully."" +else + echo ""Error downloading package."" + exit 1 +fi + +# Unzip the package +echo ""Extracting package to $EXTRACT_FOLDER..."" + +# Check if unzip is available +if ! command -v unzip &> /dev/null; then + echo ""Error: unzip is not installed. Please install unzip first."" + exit 1 +fi + +# Remove existing extract folder if it exists +if [ -d ""$EXTRACT_FOLDER"" ]; then + rm -rf ""$EXTRACT_FOLDER"" +fi + +unzip -q ""$ZIP_FILE"" -d ""$EXTRACT_FOLDER"" + +if [ $? -eq 0 ]; then + echo ""Extraction completed successfully."" +else + echo ""Error extracting package."" + exit 1 +fi + +# Find and execute the installer +INSTALLER_PATH=$(find ""$EXTRACT_FOLDER"" -name ""NetLock_RMM_Agent_Installer"" -type f | head -n 1) + +if [ -n ""$INSTALLER_PATH"" ]; then + echo ""Running installer: $INSTALLER_PATH"" + chmod +x ""$INSTALLER_PATH"" + ""$INSTALLER_PATH"" + + if [ $? -eq 0 ]; then + echo ""Installation completed successfully."" + else + echo ""Error running installer."" + exit 1 + fi +else + echo ""Error: Installer executable 'NetLock_RMM_Agent_Installer' not found in the extracted package."" + exit 1 +fi + +# Cleanup +echo ""Cleaning up temporary files..."" +rm -f ""$ZIP_FILE"" +rm -rf ""$EXTRACT_FOLDER"" +echo ""Cleanup completed."" + +echo ""NetLock RMM Agent installation process completed!"""; + + await JSRuntime.InvokeVoidAsync("exportToTxt", "Install-NetLockAgent.sh", scriptContent); + + Snackbar.Configuration.PositionClass = Defaults.Classes.Position.BottomRight; + Snackbar.Add("Linux installation script downloaded successfully.", Severity.Success, config => { config.ShowCloseIcon = true; }); + } + catch (Exception ex) + { + Logging.Handler.Error("/MainLayout -> Download_Installation_Script_Linux", "Result", ex.ToString()); + Snackbar.Configuration.PositionClass = Defaults.Classes.Position.BottomRight; + Snackbar.Add("Error creating installation script. Please check the logs.", Severity.Error, config => { config.ShowCloseIcon = true; }); + } + } + + private async Task Download_Installation_Script_MacOS() + { + try + { + string scriptContent = $@"#!/bin/bash +# NetLock RMM Agent Installation Script for macOS +# This script downloads and installs the NetLock RMM Agent + +PACKAGE_URL=""{installer_url}"" +SERVICE_NAME=""netlock-rmm-agent-comm"" + +# Check if running as root +if [ ""$EUID"" -ne 0 ]; then + echo ""Error: This script must be run as root (use sudo)"" + exit 1 +fi + +# Check if the service already exists (launchd) +if launchctl list | grep -q ""$SERVICE_NAME""; then + echo ""Service '$SERVICE_NAME' already exists. Installation will not proceed."" + exit 0 +fi + +echo ""Service '$SERVICE_NAME' not found. Proceeding with installation..."" + +# Package download settings +TEMP_FOLDER=""/tmp/netlock_agent"" +ZIP_FILE=""$TEMP_FOLDER/NetLock_Agent.zip"" +EXTRACT_FOLDER=""$TEMP_FOLDER/NetLock_Agent"" + +# Create temp folder if it doesn't exist +if [ ! -d ""$TEMP_FOLDER"" ]; then + echo ""Creating folder: $TEMP_FOLDER"" + mkdir -p ""$TEMP_FOLDER"" +fi + +# Download the ZIP file +echo ""Downloading NetLock RMM Agent package..."" +curl -L ""$PACKAGE_URL"" -o ""$ZIP_FILE"" + +if [ $? -eq 0 ]; then + echo ""Download completed successfully."" +else + echo ""Error downloading package."" + exit 1 +fi + +# Unzip the package +echo ""Extracting package to $EXTRACT_FOLDER..."" + +# Remove existing extract folder if it exists +if [ -d ""$EXTRACT_FOLDER"" ]; then + rm -rf ""$EXTRACT_FOLDER"" +fi + +unzip -q ""$ZIP_FILE"" -d ""$EXTRACT_FOLDER"" + +if [ $? -eq 0 ]; then + echo ""Extraction completed successfully."" +else + echo ""Error extracting package."" + exit 1 +fi + +# Find and execute the installer +INSTALLER_PATH=$(find ""$EXTRACT_FOLDER"" -name ""NetLock_RMM_Agent_Installer"" -type f | head -n 1) + +if [ -n ""$INSTALLER_PATH"" ]; then + echo ""Running installer: $INSTALLER_PATH"" + chmod +x ""$INSTALLER_PATH"" + ""$INSTALLER_PATH"" + + if [ $? -eq 0 ]; then + echo ""Installation completed successfully."" + else + echo ""Error running installer."" + exit 1 + fi +else + echo ""Error: Installer executable 'NetLock_RMM_Agent_Installer' not found in the extracted package."" + exit 1 +fi + +# Cleanup +echo ""Cleaning up temporary files..."" +rm -f ""$ZIP_FILE"" +rm -rf ""$EXTRACT_FOLDER"" +echo ""Cleanup completed."" + +echo ""NetLock RMM Agent installation process completed!"""; + + await JSRuntime.InvokeVoidAsync("exportToTxt", "Install-NetLockAgent.sh", scriptContent); + + Snackbar.Configuration.PositionClass = Defaults.Classes.Position.BottomRight; + Snackbar.Add("macOS installation script downloaded successfully.", Severity.Success, config => { config.ShowCloseIcon = true; }); + } + catch (Exception ex) + { + Logging.Handler.Error("/MainLayout -> Download_Installation_Script_MacOS", "Result", ex.ToString()); + Snackbar.Configuration.PositionClass = Defaults.Classes.Position.BottomRight; + Snackbar.Add("Error creating installation script. Please check the logs.", Severity.Error, config => { config.ShowCloseIcon = true; }); + } + } + private void Cancel() => MudDialog.Cancel(); -} \ No newline at end of file +} diff --git a/NetLock-RMM-Web-Console/Components/Layout/MainLayout.razor b/NetLock-RMM-Web-Console/Components/Layout/MainLayout.razor index 246d36da..47cb37b3 100644 --- a/NetLock-RMM-Web-Console/Components/Layout/MainLayout.razor +++ b/NetLock-RMM-Web-Console/Components/Layout/MainLayout.razor @@ -1,4 +1,4 @@ -ο»Ώ@using MySqlConnector; +@using MySqlConnector; @using System.Data.Common; @using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage @using Microsoft.AspNetCore.DataProtection @@ -13,6 +13,523 @@ @inject IDataProtectionProvider DataProtectionProvider @inject ProtectedSessionStorage SessionStorage + + @@ -23,8 +540,73 @@ - - + + + @if (_currentSeasonalEvent != SeasonalEvent.None) + { +
+ @switch (_currentSeasonalEvent) + { + case SeasonalEvent.WinterSnowfall: + @for (int i = 0; i < 10; i++) + { +
❄
+ } + break; + + case SeasonalEvent.NewYearFireworks: + @for (int i = 0; i < 8; i++) + { +
πŸŽ†
+ } + break; + + case SeasonalEvent.Halloween: + @for (int i = 0; i < 5; i++) + { +
@(i % 2 == 0 ? "πŸ‘»" : "πŸ¦‡")
+ } + break; + + case SeasonalEvent.Easter: + @for (int i = 0; i < 7; i++) + { +
πŸ₯š
+ } + break; + + case SeasonalEvent.Spring: + @for (int i = 0; i < 6; i++) + { +
@(new[] { "🌸", "🌺", "🌼", "🌻", "🌷", "🌹" }[i])
+ } + break; + + case SeasonalEvent.Autumn: + @for (int i = 0; i < 6; i++) + { +
πŸ‚
+ } + break; + + case SeasonalEvent.Valentine: + @for (int i = 0; i < 4; i++) + { +
❀️
+ } + break; + } +
+ } + + @if (!string.IsNullOrEmpty(NetLock_RMM_Web_Console.Configuration.Web_Console.logoBase64)) + { + + } + else + { + + } @@ -47,6 +629,7 @@ +
@@ -88,17 +671,58 @@ private bool _isDarkMode = false; private MudTheme? _theme = null; private MudThemeProvider _mudThemeProvider; + private SeasonalEvent _currentSeasonalEvent = SeasonalEvent.None; - protected override void OnInitialized() + // Seasonal Event Enum + private enum SeasonalEvent { - base.OnInitialized(); + None, + WinterSnowfall, // December 10 - February 28 + NewYearFireworks, // December 28 - January 3 (Silvester/New Year) + Valentine, // February 10-14 + Easter, // Easter week (calculated) + Spring, // March 20 - June 20 + Halloween, // October 15 - November 1 + Autumn // September 22 - December 9 + } - _theme = new() - { - PaletteLight = _lightPalette, - PaletteDark = _darkPalette, - LayoutProperties = new LayoutProperties() - }; + //OSSCH_START 33d8cb08-b78e-4071-aca7-6f45eac040f5 //OSSCH_END + + private SeasonalEvent DetectSeasonalEvent() + { + var now = DateTime.Now; + int month = now.Month; + int day = now.Day; + + // New Year / Silvester (highest priority) + if ((month == 12 && day >= 28) || (month == 1 && day <= 3)) + return SeasonalEvent.NewYearFireworks; + + // Valentine's Day + if (month == 2 && day >= 10 && day <= 14) + return SeasonalEvent.Valentine; + + // Easter (around Easter Sunday - simplified: April) + if (month == 4 && day >= 10 && day <= 20) + return SeasonalEvent.Easter; + + // Halloween + if ((month == 10 && day >= 15) || (month == 11 && day == 1)) + return SeasonalEvent.Halloween; + + // Winter Snowfall (December-February, but not during New Year) + if (month == 12 || month == 1 || month == 2) + return SeasonalEvent.WinterSnowfall; + + // Spring + if (month >= 3 && month <= 5) + return SeasonalEvent.Spring; + + // Autumn + if (month >= 9 && month <= 11) + return SeasonalEvent.Autumn; + + return SeasonalEvent.None; } protected override async Task OnAfterRenderAsync(bool firstRender) @@ -124,47 +748,145 @@ } private readonly PaletteLight _lightPalette = new() - { - Primary = "#910012", - Black = "#110e2d", - AppbarText = "#424242", - AppbarBackground = "rgba(255,255,255,0.8)", - DrawerBackground = "#ffffff", - GrayLight = "#e8e8e8", - GrayLighter = "#f9f9f9", - }; + { + // Primary Colors + Primary = "#910012", // NetLock RMM Primary Red + PrimaryContrastText = "#FFFFFF", // White text on primary + Secondary = "#424242", // Dark gray for secondary elements + SecondaryContrastText = "#FFFFFF",// White text on secondary + Tertiary = "#910012", // Same as primary for consistency + TertiaryContrastText = "#FFFFFF", // White text on tertiary + + // Background & Surface + Background = "#F5F5F5", // Light gray background + BackgroundGray = "#FAFAFA", // Slightly lighter gray + Surface = "#FFFFFF", // White surface for cards + + // AppBar & Drawer + AppbarBackground = "rgba(255,255,255,0.8)", // Semi-transparent white + AppbarText = "#424242", // Dark gray text + DrawerBackground = "#FFFFFF", // White drawer + DrawerText = "#424242", // Dark gray drawer text + DrawerIcon = "#424242", // Dark gray drawer icons + + // Text Colors + TextPrimary = "rgba(0,0,0,0.87)", // Standard dark text + TextSecondary = "rgba(0,0,0,0.6)",// Secondary dark text + TextDisabled = "rgba(0,0,0,0.38)",// Disabled text + + // Action Colors + ActionDefault = "rgba(0,0,0,0.54)", // Default action color + ActionDisabled = "rgba(0,0,0,0.26)", // Disabled actions + ActionDisabledBackground = "rgba(0,0,0,0.12)", // Disabled background + + // Status Colors + Info = "#0C80DF", // Blue for information + InfoContrastText = "#FFFFFF", // White text on info + Success = "#00A344", // Green for success + SuccessContrastText = "#FFFFFF", // White text on success + Warning = "#D68100", // Orange for warnings + WarningContrastText = "#FFFFFF", // White text on warnings + Error = "#DC3545", // Red for errors + ErrorContrastText = "#FFFFFF", // White text on errors + Dark = "#424242", // Dark color + DarkContrastText = "#FFFFFF", // White text on dark + + // Lines & Dividers + LinesDefault = "rgba(0,0,0,0.12)",// Light gray lines + LinesInputs = "rgba(0,0,0,0.42)", // Darker lines for inputs + Divider = "rgba(0,0,0,0.12)", // Divider color + DividerLight = "rgba(0,0,0,0.06)",// Light divider + TableLines = "rgba(0,0,0,0.12)", // Table border lines + TableStriped = "rgba(0,0,0,0.02)",// Striped table rows + TableHover = "rgba(0,0,0,0.04)", // Table row hover + + // Gray Scale + Black = "#000000", // Pure black + White = "#FFFFFF", // Pure white + GrayDefault = "#9E9E9E", // Mid gray + GrayLight = "#E0E0E0", // Light gray + GrayLighter = "#F5F5F5", // Very light gray + GrayDark = "#616161", // Dark gray + GrayDarker = "#424242", // Darker gray + + // Overlay + OverlayDark = "rgba(33,33,33,0.4)", // Dark overlay for modals + OverlayLight = "rgba(255,255,255,0.4)", // Light overlay + + // Hover Effects + HoverOpacity = 0.06, // Hover opacity + }; private readonly PaletteDark _darkPalette = new() - { - Primary = "#ED0D32", // Helles Rot als PrimÀrfarbe - Surface = "#2B2B2B", // Hintergrundfarbe für OberflÀchen wie Karten (dunkles Grau) - Background = "#141414", // Hintergrundfarbe für die gesamte Seite (sehr dunkles Grau) - BackgroundGray = "#2B2B2B", // Leicht heller Hintergrund für SekundÀrflÀchen (dunkles Grau) - AppbarText = "#FFFFFF", // Textfarbe in der AppBar (weiß) - AppbarBackground = "3a3a3a", // Transparente schwarze Hintergrundfarbe für AppBar - DrawerBackground = "#3a3a3a", // Hintergrundfarbe für das Drawer-Menü (dunkles Grau) - ActionDefault = "#FFFFFF", // Standardfarbe für Aktionen wie SchaltflÀchen (weiß) - ActionDisabled = "#9999994d", // Farbe für deaktivierte Aktionen (grau mit Transparenz) - ActionDisabledBackground = "#605f6d4d", // Hintergrundfarbe für deaktivierte Aktionen - TextPrimary = "#FFFFFF", // PrimÀre Textfarbe für bessere Lesbarkeit (weiß) - TextSecondary = "#FFFFFF", // SekundÀre Textfarbe für weniger wichtige Informationen (weiß) - TextDisabled = "#FFFFFF", // Farbe für deaktivierten Text (weiß mit Transparenz) - DrawerIcon = "#FFFFFF", // Farbe für Icons im Drawer-Menü (weiß) - DrawerText = "#FFFFFF", // Farbe für Text im Drawer-Menü (weiß) - GrayLight = "#383838", // Helle Grautâne für SekundÀre FlÀchen - GrayLighter = "#2B2B2B", // Noch hellerer Grauton für Hintergründe - Info = "rgb(12,128,223)", // Farbe für Informationsmeldungen (blau) - Success = "#00A344", // Erfolgsfarbe (grün) - Warning = "#D68100", // Warnfarbe (orange) - Error = "#ff3f5f", // Fehlerfarbe (rot) - LinesDefault = "#2B2B2B", // Farbe für Linien und Rahmen (dunkles Grau) - TableLines = "#2B2B2B", // Farbe für Tabellenlinien (dunkles Grau) - Divider = "#2B2B2B", // Farbe für Trenner (dunkles Grau) - OverlayLight = "#14141480", // Transparente Überlagerung für modale Fenster oder Popups - }; - - - + { + // Primary Colors + Primary = "#ED0D32", // NetLock RMM Bright Red + PrimaryContrastText = "#FFFFFF", // White text on primary + Secondary = "#757575", // Light gray for secondary + SecondaryContrastText = "#FFFFFF",// White text on secondary + Tertiary = "#ED0D32", // Same as primary + TertiaryContrastText = "#FFFFFF", // White text on tertiary + + // Background & Surface + Background = "#0A0A0A", // Fast schwarz + BackgroundGray = "#121212", // Sehr dunkel + Surface = "#1A1A1A", // Dunkelgrau für Cards + + // AppBar & Drawer + AppbarBackground = "#1A1A1A", // Dunkelgrau + AppbarText = "#FFFFFF", // White text + DrawerBackground = "#1A1A1A", // Dunkelgrau + DrawerText = "#FFFFFF", // White drawer text + DrawerIcon = "#FFFFFF", // White drawer icons + + // Text Colors + TextPrimary = "rgba(255,255,255,0.95)", // Bright white text + TextSecondary = "rgba(255,255,255,0.7)",// Dimmed white text + TextDisabled = "rgba(255,255,255,0.38)",// Disabled text + + // Action Colors + ActionDefault = "rgba(255,255,255,0.87)", // Default action color + ActionDisabled = "rgba(255,255,255,0.26)", // Disabled actions + ActionDisabledBackground = "rgba(255,255,255,0.12)", // Disabled background + + // Status Colors + Info = "#0C80DF", // Blue for information + InfoContrastText = "#FFFFFF", // White text on info + Success = "#00A344", // Green for success + SuccessContrastText = "#FFFFFF", // White text on success + Warning = "#D68100", // Orange for warnings + WarningContrastText = "#FFFFFF", // White text on warnings + Error = "#FF3F5F", // Red for errors + ErrorContrastText = "#FFFFFF", // White text on errors + Dark = "#1A1A1A", // Darker color + DarkContrastText = "#FFFFFF", // White text on dark + + // Lines & Dividers + LinesDefault = "rgba(255,255,255,0.12)", // Subtle white lines + LinesInputs = "rgba(255,255,255,0.3)", // Brighter lines for inputs + Divider = "rgba(255,255,255,0.12)", // Divider color + DividerLight = "rgba(255,255,255,0.06)", // Light divider + TableLines = "rgba(255,255,255,0.12)", // Table border lines + TableStriped = "rgba(255,255,255,0.02)", // Striped table rows + TableHover = "rgba(255,255,255,0.04)", // Table row hover + + // Gray Scale + Black = "#000000", // Pure black + White = "#FFFFFF", // Pure white + GrayDefault = "#757575", // Mid gray + GrayLight = "#616161", // Light gray (darker in dark mode) + GrayLighter = "#383838", // Dunkler für besseren Kontrast + GrayDark = "#E0E0E0", // Dark gray (lighter in dark mode) + GrayDarker = "#F5F5F5", // Darker gray + + // Overlay + OverlayDark = "rgba(33,33,33,0.5)", // Dark overlay for modals + OverlayLight = "rgba(20,20,20,0.5)", // Dark mode light overlay + + // Hover Effects + HoverOpacity = 0.08, // Hover opacity (slightly higher for dark mode) + }; + public string DarkLightModeButtonIcon => _isDarkMode switch { true => Icons.Material.Rounded.LightMode, @@ -308,8 +1030,124 @@ } finally { - conn.Close(); + await conn.CloseAsync(); + } + } + + private async Task OpenGameDialog() + { + var options = new DialogOptions + { + CloseButton = true, + FullWidth = true, + MaxWidth = MaxWidth.ExtraExtraLarge, + BackgroundClass = "dialog-blurring", + }; + + var parameters = new DialogParameters + { + }; + + DialogService.Show(String.Empty, parameters, options); + } + + // Helper method to get color string from MudColor or fallback + private string GetColorString(MudBlazor.Utilities.MudColor? color) + { + if (color == null || string.IsNullOrEmpty(color.Value)) + { + // Return default based on current mode + return _isDarkMode ? "#ED0D32" : "#910012"; + } + return color.Value; + } + + // Helper method to convert HEX to RGB for CSS variables + private string HexToRgb(string hex) + { + if (string.IsNullOrEmpty(hex)) + return "145, 0, 18"; // Default fallback + + hex = hex.Replace("#", ""); + + // Handle rgba() format + if (hex.StartsWith("rgba(")) + { + var parts = hex.Replace("rgba(", "").Replace(")", "").Split(','); + if (parts.Length >= 3) + return $"{parts[0].Trim()}, {parts[1].Trim()}, {parts[2].Trim()}"; + } + + // Handle rgb() format + if (hex.StartsWith("rgb(")) + { + return hex.Replace("rgb(", "").Replace(")", ""); + } + + try + { + if (hex.Length == 6) + { + int r = Convert.ToInt32(hex.Substring(0, 2), 16); + int g = Convert.ToInt32(hex.Substring(2, 2), 16); + int b = Convert.ToInt32(hex.Substring(4, 2), 16); + return $"{r}, {g}, {b}"; + } + else if (hex.Length == 3) + { + int r = Convert.ToInt32(hex.Substring(0, 1) + hex.Substring(0, 1), 16); + int g = Convert.ToInt32(hex.Substring(1, 1) + hex.Substring(1, 1), 16); + int b = Convert.ToInt32(hex.Substring(2, 1) + hex.Substring(2, 1), 16); + return $"{r}, {g}, {b}"; + } + } + catch + { + return "145, 0, 18"; // Fallback on error } + + return "145, 0, 18"; // Default fallback + } + + // Helper method to darken a color for gradient effects + private string DarkenColor(string hex) + { + if (string.IsNullOrEmpty(hex)) + return "#b30015"; // Default fallback + + // If already rgba/rgb, return darker version + if (hex.StartsWith("rgba(") || hex.StartsWith("rgb(")) + return hex; // Can't easily darken rgba/rgb strings, return as-is + + hex = hex.Replace("#", ""); + + try + { + if (hex.Length == 6) + { + int r = Convert.ToInt32(hex.Substring(0, 2), 16); + int g = Convert.ToInt32(hex.Substring(2, 2), 16); + int b = Convert.ToInt32(hex.Substring(4, 2), 16); + + // Darken by 20% + r = (int)(r * 1.2); + g = (int)(g * 1.2); + b = (int)(b * 1.2); + + // Clamp values + r = Math.Min(255, r); + g = Math.Min(255, g); + b = Math.Min(255, b); + + return $"#{r:X2}{g:X2}{b:X2}"; + } + } + catch + { + return "#b30015"; // Fallback on error + } + + return "#b30015"; // Default fallback } } diff --git a/NetLock-RMM-Web-Console/Components/Layout/NavMenu.razor b/NetLock-RMM-Web-Console/Components/Layout/NavMenu.razor index 93ce3014..cbead705 100644 --- a/NetLock-RMM-Web-Console/Components/Layout/NavMenu.razor +++ b/NetLock-RMM-Web-Console/Components/Layout/NavMenu.razor @@ -35,6 +35,20 @@ } } + +
+ +
+ @{ if (tenants_full_access) @@ -59,7 +73,7 @@ @{ HashSet processedTenants = new HashSet(); - @foreach (var tenant in tenant_list) + @foreach (var tenant in filteredTenantList) { // Überprüfe, ob dieser tenant_name bereits verarbeitet wurde if (!processedTenants.Contains(tenant.guid)) @@ -282,6 +296,8 @@ public static List tenant_list = new List { }; public static List locations_list = new List { }; + private List filteredTenantList = new List { }; + private string tenantSearchQuery = string.Empty; //private List tenant_list = new List { }; public DataTable clients_table = new DataTable(); @@ -518,6 +534,9 @@ } await reader.CloseAsync(); + + // Initialize filtered list with all tenants + filteredTenantList = tenant_list.ToList(); } catch (Exception ex) { @@ -667,6 +686,21 @@ } } + private void FilterTenants(string searchValue) + { + if (string.IsNullOrWhiteSpace(searchValue)) + { + filteredTenantList = tenant_list.ToList(); + } + else + { + filteredTenantList = tenant_list + .Where(t => t.name.Contains(searchValue, StringComparison.OrdinalIgnoreCase)) + .ToList(); + } + StateHasChanged(); + } + private void Redirect(string path) { NavigationManager.NavigateTo(Application_Paths.redirect_path); diff --git a/NetLock-RMM-Web-Console/Components/Pages/Account/Login.razor b/NetLock-RMM-Web-Console/Components/Pages/Account/Login.razor index c0b3f500..c5c21dab 100644 --- a/NetLock-RMM-Web-Console/Components/Pages/Account/Login.razor +++ b/NetLock-RMM-Web-Console/Components/Pages/Account/Login.razor @@ -1,6 +1,8 @@ @page "/" @page "/login" +@implements IDisposable + @using Microsoft.AspNetCore.Components.Authorization; @using Microsoft.AspNetCore.Authentication; @using Microsoft.AspNetCore.Authentication.Cookies; @@ -37,49 +39,604 @@ @inject IHttpContextAccessor _httpContextAccessor @inject AuthenticationStateProvider AuthenticationStateProvider + + + @@ -142,163 +705,299 @@ + + + + @code { - private string netlock_username = String.Empty; private bool permissions_dashboard_enabled = false; public static List permissions_tenants_list = new List { }; @@ -357,6 +1056,56 @@ { if (firstRender) { + // Initialize blood particles animation with retry + try + { + await JSRuntime.InvokeVoidAsync("eval", @" + (function() { + console.log('Starting blood particles initialization...'); + + function tryInit() { + const canvas = document.getElementById('blood-particles-canvas'); + console.log('Canvas element:', canvas); + + if (!canvas) { + console.warn('Canvas not found, retrying...'); + setTimeout(tryInit, 100); + return; + } + + if (typeof window.initBloodParticles === 'function') { + console.log('initBloodParticles found, calling it...'); + window.initBloodParticles(); + } else { + console.log('Loading blood-particles.js...'); + const script = document.createElement('script'); + script.src = '/js/blood-particles.js?v=' + Date.now(); + script.onload = function() { + console.log('Script loaded, initializing...'); + setTimeout(() => { + if (typeof window.initBloodParticles === 'function') { + window.initBloodParticles(); + } else { + console.error('initBloodParticles not found after script load'); + } + }, 50); + }; + script.onerror = function(e) { + console.error('Failed to load blood-particles.js:', e); + }; + document.head.appendChild(script); + } + } + + tryInit(); + })(); + "); + } + catch (Exception ex) + { + Logging.Handler.Error("/login", "BloodParticles", ex.ToString()); + } + await AfterInitializedAsync(); } } @@ -390,7 +1139,7 @@ Logging.Handler.Debug("/login", "State", "Logout confirmed, clearing URL flag"); // Clear the logout flag from URL without reload (user is properly logged out) - NavigationManager.NavigateTo("/", false); + NavigationManager.NavigateTo("/", true); } else { @@ -401,7 +1150,7 @@ if (user?.Identity is { IsAuthenticated: true }) { Logging.Handler.Debug("/login", "State", "User already authenticated, redirecting to dashboard"); - NavigationManager.NavigateTo("/dashboard", true); + NavigationManager.NavigateTo("/dashboard", false); return; } } @@ -410,11 +1159,49 @@ StateHasChanged(); funFact = await Classes.Miscellaneous.FunFacts.GetRandomFact(); + + // Start Fun Fact rotation timer + StartFunFactRotation(); loading_overlay = false; StateHasChanged(); } + private System.Threading.Timer funFactTimer; + private string funFact = String.Empty; + private string funFactFadeClass = ""; + private int funFactKey = 0; + + private void StartFunFactRotation() + { + // Rotate fun facts every 10 seconds + funFactTimer = new System.Threading.Timer(async _ => + { + await InvokeAsync(async () => + { + // Add fade-out class + funFactFadeClass = "fade-out"; + StateHasChanged(); + + // Wait for fade-out animation (1 second) + await Task.Delay(1000); + + // Get new fun fact + funFact = await Classes.Miscellaneous.FunFacts.GetRandomFact(); + funFactKey++; // Change key to force re-render with new animation + + // Remove fade-out class (will trigger fade-in) + funFactFadeClass = ""; + StateHasChanged(); + }); + }, null, TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(10)); + } + + public void Dispose() + { + funFactTimer?.Dispose(); + } + private bool loading_overlay = false; private string username = String.Empty; @@ -430,6 +1217,20 @@ private string password_new = String.Empty; private string password_confirm_new = String.Empty; + // Password visibility toggles + private bool showPassword = false; + private bool showPasswordConfirm = false; + + private void TogglePasswordVisibility() + { + showPassword = !showPassword; + } + + private void TogglePasswordConfirmVisibility() + { + showPasswordConfirm = !showPasswordConfirm; + } + //2factor private TwoFactorAuthenticator tfa = new TwoFactorAuthenticator(); @@ -444,7 +1245,6 @@ private int invalid_count = 0; - private string funFact = String.Empty; private async Task Enter(KeyboardEventArgs e) { @@ -622,9 +1422,9 @@ await Permissions(); if (permissions_dashboard_enabled) - NavigationManager.NavigateTo("/dashboard", true); + NavigationManager.NavigateTo("/dashboard", false); else if (!permissions_dashboard_enabled) - NavigationManager.NavigateTo("/home", true); + NavigationManager.NavigateTo("/home", false); } catch (Exception ex) { @@ -688,7 +1488,7 @@ two_factor_qrCodeImageUrl = setupInfo.QrCodeSetupImageUrl; two_factor_manualEntrySetupCode = setupInfo.ManualEntryKey; - // The code is expected after the setup + // The code is expected after the setup message = "Please scan the QR code or manually enter the setup code, then enter the verification code."; } diff --git a/NetLock-RMM-Web-Console/Components/Pages/Account/Logout.razor b/NetLock-RMM-Web-Console/Components/Pages/Account/Logout.razor index f79ad90a..a6c83e91 100644 --- a/NetLock-RMM-Web-Console/Components/Pages/Account/Logout.razor +++ b/NetLock-RMM-Web-Console/Components/Pages/Account/Logout.razor @@ -79,17 +79,28 @@ await customAuthStateProvider.UpdateAuthentificationState(null, true); Logging.Handler.Debug("/logout", "CustomAuthState", "Reset and cache cleared"); - if (isSsoUser) + // Check if SSO is currently enabled + bool ssoEnabled = NetLock_RMM_Web_Console.Configuration.Sso.IsEnabled; + + if (isSsoUser && ssoEnabled) { - // For SSO users: redirect to SSO logout handler + // For SSO users when SSO is enabled: redirect to SSO logout handler // This endpoint will properly initiate the OIDC signout flow Logging.Handler.Debug("/logout", "Action", "Redirecting to SSO logout handler"); NavigationManager.NavigateTo("/sso-signout", true); } else { - // For normal users: redirect to login page with logout flag - Logging.Handler.Debug("/logout", "Action", "Redirecting to login page"); + // For normal users or when SSO is disabled: redirect to login page with logout flag + if (isSsoUser && !ssoEnabled) + { + Logging.Handler.Debug("/logout", "Action", "SSO user but SSO is disabled, redirecting to login page"); + } + else + { + Logging.Handler.Debug("/logout", "Action", "Redirecting to login page"); + } + NavigationManager.NavigateTo("/?logout=true", true); } } diff --git a/NetLock-RMM-Web-Console/Components/Pages/Account/SsoCallback.razor b/NetLock-RMM-Web-Console/Components/Pages/Account/SsoCallback.razor index 8d9f491b..c1620176 100644 --- a/NetLock-RMM-Web-Console/Components/Pages/Account/SsoCallback.razor +++ b/NetLock-RMM-Web-Console/Components/Pages/Account/SsoCallback.razor @@ -52,6 +52,7 @@ animation: slide 2s infinite ease-in-out; } + /* Light Mode */ .sso-card { background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(10px); @@ -61,6 +62,20 @@ max-width: 500px; text-align: center; } + + /* Dark Mode */ + @@media (prefers-color-scheme: dark) { + .sso-card { + background: rgba(30, 30, 30, 0.95); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5); + } + } + + /* MudBlazor Dark Mode Detection */ + .mud-theme-dark .sso-card { + background: rgba(30, 30, 30, 0.95); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5); + } @@ -70,59 +85,28 @@ 🐧
- + SSO Authentication - + Completing your secure login... - + You'll be redirected shortly - + Powered by NetLock RMM @code { - protected override async Task OnAfterRenderAsync(bool firstRender) - { - if (firstRender) - { - try - { - // Give the SSO authentication time to complete - await Task.Delay(500); - - // Get the current user from the authentication state - var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync(); - var user = authState.User; - - // Check if user is authenticated - if (user?.Identity is { IsAuthenticated: true }) - { - // User is authenticated via SSO, redirect to dashboard - NavigationManager.NavigateTo("/dashboard", true); - } - else - { - // Authentication failed, redirect back to login - NavigationManager.NavigateTo("/", true); - } - } - catch (Exception e) - { - Console.WriteLine(e); - NavigationManager.NavigateTo("/", true); - } - } - } + //OSSCH_START 13d00d3e-287a-406c-a981-84015ba55ca9 //OSSCH_END } diff --git a/NetLock-RMM-Web-Console/Components/Pages/Collections/Jobs/Dialogs/Add_Job_Dialog.razor b/NetLock-RMM-Web-Console/Components/Pages/Collections/Jobs/Dialogs/Add_Job_Dialog.razor index 1958946a..80358fe5 100644 --- a/NetLock-RMM-Web-Console/Components/Pages/Collections/Jobs/Dialogs/Add_Job_Dialog.razor +++ b/NetLock-RMM-Web-Console/Components/Pages/Collections/Jobs/Dialogs/Add_Job_Dialog.razor @@ -23,6 +23,16 @@ + + @@ -205,7 +215,7 @@ await Get_Scripts(); } - public List + + + + + + + + + πŸ›‘οΈ Virus Defense + + + + + + + + + Statistics + + + + Resources: 200 + + + System Health: 20 + + + Attack Wave: 0 + + + Score: 0 + + + Viruses Removed: 0 + + + + + + + + Deploy Security Tools + + + + + πŸ›‘οΈ Firewall (50G) + + + πŸ” IDS (75G) + + + 🦠 Antivirus (100G) + + + πŸ”’ Quarantine (150G) + + + + Uninstall Tool + + + + + + + + + How to Play + + + πŸ›‘οΈ Select a security tool + πŸ–±οΈ Click on grid to deploy + ⬆️ Click tool to patch/upgrade (Max Lvl 5) + πŸ’Ž Hover tool to see upgrade cost + πŸ—‘οΈ Use uninstall mode to remove tools + πŸ’° Earn resources by removing viruses + ⚠️ Don't let viruses reach the end! + + + + + + + Security Tools + + + πŸ›‘οΈ Firewall: Blocks threats + πŸ” IDS: Fast detection + 🦠 Antivirus: High damage scan + πŸ”’ Quarantine: Isolates multiple + + + πŸ’‘ Upgrade Cost = Base Cost Γ— 50% Γ— Level + + + πŸ’° Refund Value = 70% of total invested + + + + + + +
+ + + + + + + New Game + + + + + Pause + + + + + +
+ + Statistics + Resources: 200 + System Health: 20 + Attack Wave: 0 + Score: 0 + Viruses Removed: 0 + + + + Deploy Security Tools + + + πŸ›‘οΈ Firewall (50G) + + + πŸ” IDS (75G) + + + 🦠 Antivirus (100G) + + + πŸ”’ Quarantine (150G) + + + + Uninstall + + + + + + How to Play: + Select tool β†’ Click grid to deploy + Click tool to upgrade (Max Lvl 5) + Hover tool to see upgrade cost + Earn resources by removing viruses + + + + Security Tools: + πŸ›‘οΈ Firewall: Blocks threats + πŸ” IDS: Fast detection + 🦠 Antivirus: High damage + πŸ”’ Quarantine: Isolates multiple + +
+
+
+
+
+ + Close + +
+ +
+
+ +@code { + + [CascadingParameter] IMudDialogInstance MudDialog { get; set; } + + void Close() + { + MudDialog.Close(); + } + + private string netlock_username = String.Empty; + + private async Task Permissions() + { + try + { + bool logout = false; + + // Get the current user from the authentication state + var user = (await AuthenticationStateProvider.GetAuthenticationStateAsync()).User; + + // Check if user is authenticated + if (user?.Identity is not { IsAuthenticated: true }) + logout = true; + + netlock_username = user.FindFirst(ClaimTypes.Email)?.Value; + + if (logout) // Redirect to the login page + { + NavigationManager.NavigateTo("/logout", true); + return false; + } + + // All fine? Nice. + return true; + } + catch (Exception ex) + { + Logging.Handler.Error("/dashboard -> Permissions", "Error", ex.ToString()); + return false; + } + } + + protected override async Task OnInitializedAsync() + { + await base.OnInitializedAsync(); + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + await AfterInitializedAsync(); + } + } + + private async Task AfterInitializedAsync() + { + if (!await Permissions()) + return; + + StateHasChanged(); + } +} \ No newline at end of file diff --git a/NetLock-RMM-Web-Console/Components/Pages/Dashboard/Dashboard.razor b/NetLock-RMM-Web-Console/Components/Pages/Dashboard/Dashboard.razor index aeb9bb74..63e7e009 100644 --- a/NetLock-RMM-Web-Console/Components/Pages/Dashboard/Dashboard.razor +++ b/NetLock-RMM-Web-Console/Components/Pages/Dashboard/Dashboard.razor @@ -19,6 +19,8 @@ @inject IStringLocalizer Localizer @inject AuthenticationStateProvider AuthenticationStateProvider +@implements IDisposable + @@ -105,12 +121,7 @@ { @Localizer["authorized_devices"] @group_name_displayed - - if (group_name != Localizer["all_devices"] && permissions_devices_move) - { - @Localizer["move_devices"] - } - + @@ -118,7 +129,65 @@ - + + if (permissions_devices_move) + { + if (!moveDevices) + { + + Move devices + + } + else if (selectedDeviceIds.Count() == 0) + { + + Cancel moving + + } + else + { + + Confirm move (@selectedDeviceIds.Count() devices) + + + Cancel + + } + } + + if (permissions_devices_remote_shell) + { + if (!bulkRemoteShell) + { + + Bulk Remote Shell + + } + else if (selectedDeviceIds.Count() == 0) + { + + Cancel selection + + } + else + { + + Execute on @selectedDeviceIds.Count() device(s) + + + Cancel + + } + } + if (remote_server_client_setup) { @@ -137,8 +206,20 @@ + @if (moveDevices || bulkRemoteShell) + { + +
+ + + +
+
+ } + Name - Label + Label Last active user @Localizer["tenant"] @Localizer["location"] @@ -155,21 +236,44 @@ Disconnection alert -
+ + - + @{ - DateTime lastAccess; - var isValidDate = DateTime.TryParse(row.last_access, out lastAccess); - var isOlderThan10Minutes = isValidDate && (DateTime.UtcNow - lastAccess).TotalMinutes > 30; - var iconColor = isValidDate && (DateTime.UtcNow - lastAccess).TotalMinutes > 30 ? "color: grey;" : "color: green;"; + if (moveDevices || bulkRemoteShell) + { + + + + + + } + + var iconColor = "color: grey;"; + var iconClass = ""; + + if (row.connected) + { + // Device has active remote connection - show green icon with pulsing blue animation + iconColor = "color: green;"; + iconClass = "remote-active"; + } + else + { + // No active remote connection - check if device is online based on last_access + DateTime lastAccess; + var isValidDate = DateTime.TryParse(row.last_access, out lastAccess); + // If last access within 5 minutes, show green (online), otherwise grey (offline) + iconColor = isValidDate && (DateTime.UtcNow - lastAccess).TotalMinutes <= 5 ? "color: green;" : "color: grey;"; + } } @if (row.platform == "Windows") { -  @row.device_name +  @row.device_name } @@ -177,7 +281,7 @@ { -  @row.device_name +  @row.device_name } @@ -185,7 +289,7 @@ { -  @row.device_name +  @row.device_name } @@ -355,35 +459,35 @@ { @Localizer["unauthorize"] - @if (permissions_devices_move) - { - @Localizer["move"] - } - @if (permissions_devices_remote_control) { - + - + } @if (permissions_devices_remote_shell) { - Remote Shell + Shell } @if (permissions_devices_remote_file_browser) { - @Localizer["file_browser"] + @Localizer["file_browser"] } + @if (permissions_devices_remote_eventlog_viewer && platform == "Windows") + { + Event Logs + } + @if (permissions_devices_remote_control && platform == "Windows") { - Remote Screen Control + Screen Control } } @@ -910,7 +1014,7 @@ ChartOptions="@cpu_usage_history_graph_options" AxisChartOptions="@cpu_usage_history_graph_axisChartOptions" CanHideSeries="false" - TimeLabelSpacing="TimeSpan.FromMinutes(5)" + TimeLabelSpacing="CpuHistoryTimeLabelSpacing" TimeLabelSpacingRounding="true" TimeLabelSpacingRoundingPadSeries="true" DataMarkerTooltipTimeLabelFormat="yyyy MMM dd HH:mm:ss" @@ -1234,6 +1338,7 @@ ChartOptions="@ram_usage_history_graph_options" AxisChartOptions="@ram_usage_history_graph_axisChartOptions" CanHideSeries="false" + TimeLabelSpacing="RamHistoryTimeLabelSpacing" TimeLabelSpacingRounding="true" TimeLabelSpacingRoundingPadSeries="true" DataMarkerTooltipTimeLabelFormat="yyyy MMM dd HH:mm:ss" /> @@ -3414,46 +3519,279 @@ { - - - Antimalware -  @Localizer["antivirus_information_amserviceenabled"]: @antivirus_information_amserviceenabled_display -  @Localizer["antivirus_information_amengineversion"]: @antivirus_information_amengineversion -  @Localizer["antivirus_information_amproductversion"]: @antivirus_information_amproductversion -  @Localizer["antivirus_information_amserviceversion"]: @antivirus_information_amserviceversion - - Antispyware -  Status: @antivirus_information_antispywareenabled_display -  @Localizer["antivirus_information_antispywaresignaturelastupdated"]: @antivirus_information_antispywaresignaturelastupdated -  @Localizer["antivirus_information_antispywaresignatureversion"]: @antivirus_information_antispywaresignatureversion - - Antivirus -  Status: @antivirus_information_antivirusenabled_display -  @Localizer["antivirus_information_antivirussignaturelastupdated"]: @antivirus_information_antivirussignaturelastupdated -  @Localizer["antivirus_information_antivirussignatureversion"]: @antivirus_information_antivirussignatureversion - - @Localizer["antivirus_information_nis"] -  Status: @antivirus_information_nisenabled_display -  @Localizer["antivirus_information_nisengineversion"]: @antivirus_information_nisengineversion -  @Localizer["antivirus_information_nissignaturelastupdated"]: @antivirus_information_nissignaturelastupdated -  @Localizer["antivirus_information_nissignatureversion"]: @antivirus_information_nissignatureversion - - @Localizer["real_time_protection"] -  Status: @antivirus_information_realtimetprotectionenabled_display - - @Localizer["on_access_protection"] -  Status: @antivirus_information_onaccessprotectionenabled_display - - @Localizer["behavior_monitoring"] -  Status: @antivirus_information_behaviormonitorenabled_display + + + @* Row 1: Protection Status Cards *@ + + + + + + + + + + @Localizer["real_time_protection"] + + + + + @antivirus_information_realtimetprotectionenabled_display + + + + + + + + + + + + + + + @Localizer["behavior_monitoring"] + + + + + @antivirus_information_behaviormonitorenabled_display + + + + + + + + + + + + + + + @Localizer["tamper_protection"] + + + + + @antivirus_information_istamperprotected_display + + + + + + @* Row 2 *@ + + + + + + + + + + @Localizer["on_access_protection"] + + + + + @antivirus_information_onaccessprotectionenabled_display + + + + - @Localizer["ioav"] -  Status: @antivirus_information_ioavprotectionenabled_display + + + + + + + + + + @Localizer["ioav"] + + + + + @antivirus_information_ioavprotectionenabled_display + + + + + + @* Antimalware Card *@ + + + + + + + + + + Antimalware + + @antivirus_information_amserviceenabled_display + + + + + + + + + @Localizer["antivirus_information_amengineversion"] + + @antivirus_information_amengineversion + + + + + + @Localizer["antivirus_information_amproductversion"] + + @antivirus_information_amproductversion + + + + + + @Localizer["antivirus_information_amserviceversion"] + + @antivirus_information_amserviceversion + + + + + + + @* Antivirus Card *@ + + + + + + + + + + Antivirus + + @antivirus_information_antivirusenabled_display + + + + + + + + + @Localizer["antivirus_information_antivirussignaturelastupdated"] + + @antivirus_information_antivirussignaturelastupdated + + + + + + @Localizer["antivirus_information_antivirussignatureversion"] + + @antivirus_information_antivirussignatureversion + + + + + + + @* Antispyware Card *@ + + + + + + + + + + Antispyware + + @antivirus_information_antispywareenabled_display + + + + + + + + + @Localizer["antivirus_information_antispywaresignaturelastupdated"] + + @antivirus_information_antispywaresignaturelastupdated + + + + + + @Localizer["antivirus_information_antispywaresignatureversion"] + + @antivirus_information_antispywaresignatureversion + + + + + + + @* NIS Card *@ + + + + + + + + + + @Localizer["antivirus_information_nis"] + + @antivirus_information_nisenabled_display + + + + + + + + + @Localizer["antivirus_information_nisengineversion"] + + @antivirus_information_nisengineversion + + + + + + @Localizer["antivirus_information_nissignaturelastupdated"] + + @antivirus_information_nissignaturelastupdated + + + + + + @Localizer["antivirus_information_nissignatureversion"] + + @antivirus_information_nissignatureversion + + + + + - @Localizer["tamper_protection"] -  Status: @antivirus_information_istamperprotected_display - - + } diff --git a/NetLock-RMM-Web-Console/Components/Pages/Devices/Devices.razor.cs b/NetLock-RMM-Web-Console/Components/Pages/Devices/Devices.razor.cs index 37adcc93..f1506e29 100644 --- a/NetLock-RMM-Web-Console/Components/Pages/Devices/Devices.razor.cs +++ b/NetLock-RMM-Web-Console/Components/Pages/Devices/Devices.razor.cs @@ -5,6 +5,7 @@ using System.Text; using System.Text.Json; using System.Text.Json.Nodes; +using System.Timers; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage; using Microsoft.AspNetCore.SignalR.Client; @@ -18,7 +19,7 @@ namespace NetLock_RMM_Web_Console.Components.Pages.Devices { - public partial class Devices + public partial class Devices : IDisposable { #region Permissions System @@ -36,6 +37,7 @@ public partial class Devices private bool permissions_devices_remote_shell = false; private bool permissions_devices_remote_file_browser = false; private bool permissions_devices_remote_control = false; + private bool permissions_devices_remote_eventlog_viewer = false; private bool permissions_devices_deauthorize = false; private bool permissions_devices_move = false; public static List permissions_tenants_list = new List { }; @@ -67,6 +69,7 @@ private async Task Permissions() permissions_devices_remote_shell = await Classes.Authentication.Permissions.Verify_Permission(netlock_username, "devices_remote_shell"); permissions_devices_remote_file_browser = await Classes.Authentication.Permissions.Verify_Permission(netlock_username, "devices_remote_file_browser"); permissions_devices_remote_control = await Classes.Authentication.Permissions.Verify_Permission(netlock_username, "devices_remote_control"); + permissions_devices_remote_eventlog_viewer = await Classes.Authentication.Permissions.Verify_Permission(netlock_username, "devices_remote_eventlog_viewer"); permissions_devices_deauthorize = await Classes.Authentication.Permissions.Verify_Permission(netlock_username, "devices_deauthorize"); permissions_devices_move = await Classes.Authentication.Permissions.Verify_Permission(netlock_username, "devices_move"); permissions_tenants_list = await Classes.Authentication.Permissions.Get_Tenants(netlock_username, true); @@ -111,6 +114,8 @@ private async Task Permissions() #endregion private bool _isDarkMode = false; + private System.Timers.Timer? _autoRefreshTimer; + private bool _isRefreshing = false; protected override async Task OnInitializedAsync() { @@ -160,6 +165,9 @@ private async Task AfterInitializedAsync() await Remote_Setup_SignalR(); + // Start auto-refresh timer (30 seconds) + StartAutoRefreshTimer(); + loading_overlay = false; StateHasChanged(); } @@ -174,9 +182,6 @@ private async Task Get_Members_Portal_License_Limit() try { members_portal_license_limit_reached = await Classes.Members_Portal.Handler.Check_License_Limit_Reached(); - - members_portal_license_count = Convert.ToInt32(await Classes.MySQL.Handler.Quick_Reader("SELECT COUNT(*) FROM devices WHERE authorized = '1';", "COUNT(*)")); - members_portal_license_limit = Convert.ToInt32(await Classes.MySQL.Handler.Quick_Reader("SELECT members_portal_licenses_max FROM settings;", "members_portal_licenses_max")); } catch (Exception ex) { @@ -200,7 +205,11 @@ private async Task Get_Members_Portal_License_Limit() private string location_guid = String.Empty; private string group_id = String.Empty; + private bool moveDevices = false; + private bool bulkRemoteShell = false; + //Device information + private bool deviceConnected = false; public string agent_version = String.Empty; public string operating_system = String.Empty; public string architecture = String.Empty; @@ -234,7 +243,16 @@ private async Task Get_Members_Portal_License_Limit() public string applications_services = String.Empty; public string applications_drivers = String.Empty; public string policy_name = String.Empty; - + + // Device policy config + private bool policyRemoteServiceEnabled = false; + private bool policyRemoteShellEnabled = false; + private bool policyRemoteFileBrowserEnabled = false; + private bool policyRemoteTaskManagerEnabled = false; + private bool policyRemoteServiceManagerEnabled = false; + private bool policyRemoteScreenControlEnabled = false; + private bool policyRemoteScreenControlUnattendedAccess = false; + #region Device Table private int devicesTableRowsPerPage = 25; @@ -308,7 +326,19 @@ private async void Devices_RowClickHandler(MySQL_Entity row) notes_expanded = false; else notes_expanded = true; - + + // Get remote connected state + deviceConnected = row.connected; + + // Get policy remote features + (policyRemoteServiceEnabled, + policyRemoteShellEnabled, + policyRemoteFileBrowserEnabled, + policyRemoteTaskManagerEnabled, + policyRemoteServiceManagerEnabled, + policyRemoteScreenControlEnabled, + policyRemoteScreenControlUnattendedAccess) = await Classes.MySQL.Handler.GetPolicyRemoteFeatureSettings(row.device_id); + loading_overlay = false; StateHasChanged(); @@ -404,6 +434,95 @@ private async Task Deauthorize_Device(string device_id) await AfterInitializedAsync(); } + #region Auto-Refresh Timer + + private void StartAutoRefreshTimer() + { + try + { + // Create timer with 30 second interval + _autoRefreshTimer = new System.Timers.Timer(30000); + _autoRefreshTimer.Elapsed += OnAutoRefreshTimer; + _autoRefreshTimer.AutoReset = true; + _autoRefreshTimer.Enabled = true; + + Logging.Handler.Debug("/devices -> StartAutoRefreshTimer", "Timer", "Auto-refresh timer started (30 seconds)"); + } + catch (Exception ex) + { + Logging.Handler.Error("/devices -> StartAutoRefreshTimer", "Error", ex.ToString()); + } + } + + private async void OnAutoRefreshTimer(object? sender, ElapsedEventArgs e) + { + try + { + // Prevent multiple simultaneous refreshes + if (_isRefreshing) + { + Logging.Handler.Debug("/devices -> OnAutoRefreshTimer", "Skip", "Already refreshing"); + return; + } + + _isRefreshing = true; + + Logging.Handler.Debug("/devices -> OnAutoRefreshTimer", "Refresh", "Starting silent background refresh"); + + // Silent refresh without showing loading overlay + await InvokeAsync(async () => + { + try + { + // Refresh device list silently + await Get_Clients_OverviewAsync(silent: true); + + // Update UI without disturbing user focus + StateHasChanged(); + } + catch (Exception ex) + { + Logging.Handler.Error("/devices -> OnAutoRefreshTimer", "Refresh error", ex.ToString()); + } + }); + } + catch (Exception ex) + { + Logging.Handler.Error("/devices -> OnAutoRefreshTimer", "Error", ex.ToString()); + } + finally + { + _isRefreshing = false; + } + } + + private void StopAutoRefreshTimer() + { + try + { + if (_autoRefreshTimer != null) + { + _autoRefreshTimer.Stop(); + _autoRefreshTimer.Elapsed -= OnAutoRefreshTimer; + _autoRefreshTimer.Dispose(); + _autoRefreshTimer = null; + + Logging.Handler.Debug("/devices -> StopAutoRefreshTimer", "Timer", "Auto-refresh timer stopped"); + } + } + catch (Exception ex) + { + Logging.Handler.Error("/devices -> StopAutoRefreshTimer", "Error", ex.ToString()); + } + } + + public void Dispose() + { + StopAutoRefreshTimer(); + } + + #endregion + private void Device_Information_Expansion_Status() { devices_table_view_port = expanded ? "35vh" : "70vh"; @@ -657,6 +776,26 @@ public class Device_Information_CPU_History_Entity private MudDateRangePicker device_information_cpu_history_table_picker; private DateRange device_information_cpu_history_table_dateRange = new DateRange(DateTime.Now.Date.AddDays(-1), DateTime.Now.Date.AddDays(1)); + private TimeSpan CpuHistoryTimeLabelSpacing + { + get + { + if (device_information_cpu_history_table_dateRange?.Start == null || device_information_cpu_history_table_dateRange?.End == null) + return TimeSpan.FromMinutes(30); + + var timeSpan = device_information_cpu_history_table_dateRange.End.Value - device_information_cpu_history_table_dateRange.Start.Value; + var totalHours = timeSpan.TotalHours; + + // Dynamische Berechnung basierend auf Zeitspanne + if (totalHours <= 6) return TimeSpan.FromMinutes(15); // <= 6 Stunden: alle 15 Minuten + if (totalHours <= 24) return TimeSpan.FromMinutes(30); // <= 1 Tag: alle 30 Minuten + if (totalHours <= 72) return TimeSpan.FromHours(2); // <= 3 Tage: alle 2 Stunden + if (totalHours <= 168) return TimeSpan.FromHours(6); // <= 1 Woche: alle 6 Stunden + if (totalHours <= 720) return TimeSpan.FromHours(24); // <= 1 Monat: alle 24 Stunden + return TimeSpan.FromDays(7); // > 1 Monat: alle 7 Tage + } + } + private async Task Device_Information_CPU_History_Table_Submit_Picker() { await device_information_cpu_history_table_picker.CloseAsync(); @@ -1039,6 +1178,26 @@ public class RAM_History_Entity private MudDateRangePicker ram_history_table_picker; private DateRange ram_history_table_dateRange = new DateRange(DateTime.Now.Date.AddDays(-1), DateTime.Now.Date.AddDays(1)); + private TimeSpan RamHistoryTimeLabelSpacing + { + get + { + if (ram_history_table_dateRange?.Start == null || ram_history_table_dateRange?.End == null) + return TimeSpan.FromMinutes(30); + + var timeSpan = ram_history_table_dateRange.End.Value - ram_history_table_dateRange.Start.Value; + var totalHours = timeSpan.TotalHours; + + // Dynamische Berechnung basierend auf Zeitspanne + if (totalHours <= 6) return TimeSpan.FromMinutes(15); // <= 6 Stunden: alle 15 Minuten + if (totalHours <= 24) return TimeSpan.FromMinutes(30); // <= 1 Tag: alle 30 Minuten + if (totalHours <= 72) return TimeSpan.FromHours(2); // <= 3 Tage: alle 2 Stunden + if (totalHours <= 168) return TimeSpan.FromHours(6); // <= 1 Woche: alle 6 Stunden + if (totalHours <= 720) return TimeSpan.FromHours(24); // <= 1 Monat: alle 24 Stunden + return TimeSpan.FromDays(7); // > 1 Monat: alle 7 Tage + } + } + private async Task RAM_History_Table_Submit_Picker() { ram_history_table_picker.CloseAsync(); @@ -3090,87 +3249,6 @@ private async Task Export_Antivirus_Products_History_Table_Dialog() private string group_name = null; private string group_name_displayed = String.Empty; - private bool move_devices_dialog_open = false; - - private async Task Move_Devices_Dialog() - { - if (move_devices_dialog_open) - return; - - await Get_Tenant_Location_Group_ID(); - - var options = new DialogOptions - { - CloseButton = true, - FullWidth = true, - MaxWidth = MaxWidth.Small, - BackgroundClass = "dialog-blurring", - }; - - DialogParameters parameters = new DialogParameters(); - parameters.Add("tenant_id", tenant_id); - parameters.Add("location_id", location_id); - parameters.Add("group_id", group_id); - - if (group_name == "all") - parameters.Add("grouped", false); - else - parameters.Add("grouped", true); - - move_devices_dialog_open = true; - - var result = await DialogService.Show(string.Empty, parameters, options).Result; - - move_devices_dialog_open = false; - - if (result.Canceled) - { - await Get_Clients_OverviewAsync(); - return; - } - - Logging.Handler.Debug("/devices -> Move_Devices_Dialog", "Result", result.Data.ToString() ?? String.Empty); - - if (String.IsNullOrEmpty(result.Data.ToString()) == false && result.Data.ToString() != "error") - { - await Get_Clients_OverviewAsync(); - } - } - - private bool move_device_dialog_open = false; - - private async Task Move_Device_Dialog() - { - var options = new DialogOptions - { - CloseButton = true, - FullWidth = true, - MaxWidth = MaxWidth.Small, - BackgroundClass = "dialog-blurring", - }; - - DialogParameters parameters = new DialogParameters(); - parameters.Add("tenant_id", notes_tenant_id); - parameters.Add("location_id", notes_location_id); - parameters.Add("device_id", notes_device_id); - - move_device_dialog_open = false; - - var result = await DialogService.Show(string.Empty, parameters, options).Result; - - move_device_dialog_open = true; - - if (result.Canceled) - return; - - Logging.Handler.Debug("/devices -> Move_Device_Dialog", "Result", result.Data.ToString() ?? String.Empty); - - if (String.IsNullOrEmpty(result.Data.ToString()) == false && result.Data.ToString() != "error") - { - await Get_Clients_OverviewAsync(); - } - } - // Get tenant & location & group id from database using guid private async Task Get_Tenant_Location_Group_ID() { @@ -3252,7 +3330,10 @@ private async Task Get_Tenant_Location_Group_ID() public class MySQL_Entity { + public bool isChecked { get; set; } = false; public string device_id { get; set; } = "Empty"; + public bool connected { get; set; } = false; + public string access_key { get; set; } = "Empty"; public string device_name { get; set; } = "Empty"; public string label { get; set; } = "Empty"; public string tenant_name { get; set; } = "Empty"; @@ -3274,20 +3355,28 @@ public class MySQL_Entity } public List mysql_data; + + public static string connectedDevicesAccessKeys = String.Empty; - private async Task Get_Clients_OverviewAsync() + private async Task Get_Clients_OverviewAsync(bool silent = false) { string tenant_name = await localStorage.GetItemAsync("tenant_name"); string group_name = await localStorage.GetItemAsync("group_name"); string location_name = await localStorage.GetItemAsync("location_name"); string query = null; - mysql_data = new List(); - + // Only create new list if not in silent mode (to preserve references) + var tempMysqlData = silent ? new List() : null; + if (!silent) + mysql_data = new List(); + MySqlConnection conn = new MySqlConnection(Configuration.MySQL.Connection_String); try { + // Get list of remote connected devices from the server backend + string tempConnectedDevicesAccessKeys = await NetLock_RMM_Web_Console.Classes.Helper.Http.Get_Request_With_Api_Key(Configuration.Web_Console.publicOverrideUrl + "/admin/devices/connected", false); + await conn.OpenAsync(); MySqlCommand command; @@ -3334,6 +3423,7 @@ private async Task Get_Clients_OverviewAsync() MySQL_Entity entity = new MySQL_Entity { device_id = reader["id"].ToString() ?? String.Empty, + access_key = reader["access_key"].ToString() ?? String.Empty, device_name = reader["device_name"].ToString() ?? String.Empty, label = reader["label"].ToString() ?? String.Empty, tenant_name = reader["tenant_name"].ToString() ?? String.Empty, @@ -3354,10 +3444,43 @@ private async Task Get_Clients_OverviewAsync() policy = await Handler.GetAssignedDevicePolicyByDeviceId(reader["id"].ToString() ?? String.Empty), }; - mysql_data.Add(entity); + if (silent) + tempMysqlData.Add(entity); + else + mysql_data.Add(entity); } } } + + // Now iterate through the data and set the connected status based on the access keys + var dataToUpdate = silent ? tempMysqlData : mysql_data; + foreach (var device in dataToUpdate) + { + try + { + if (tempConnectedDevicesAccessKeys.Contains(device.access_key)) + device.connected = true; + else + device.connected = false; + } + catch (Exception ex) + { + Logging.Handler.Error("/devices -> Get_Clients_OverviewAsync", "Set_Connected_Status", + ex.ToString()); + } + } + + // In silent mode, update the main data only after successful completion + if (silent) + { + mysql_data = tempMysqlData; + connectedDevicesAccessKeys = tempConnectedDevicesAccessKeys; + Logging.Handler.Debug("/devices -> Get_Clients_OverviewAsync", "Silent update", $"Updated {mysql_data.Count} devices"); + } + else + { + connectedDevicesAccessKeys = tempConnectedDevicesAccessKeys; + } } catch (Exception ex) { @@ -3376,14 +3499,14 @@ private async Task SearchDeviceByName() await Get_Clients_OverviewAsync(); return; } - + string tenant_name = await localStorage.GetItemAsync("tenant_name"); string group_name = await localStorage.GetItemAsync("group_name"); string location_name = await localStorage.GetItemAsync("location_name"); string query = null; mysql_data = new List(); - + MySqlConnection conn = new MySqlConnection(Configuration.MySQL.Connection_String); try @@ -3427,7 +3550,7 @@ private async Task SearchDeviceByName() command.Parameters.AddWithValue("@tenant_name", tenant_name); command.Parameters.AddWithValue("@deviceName", deviceNameFilter); } - + Logging.Handler.Debug("/devices -> SearchDeviceByName", "MySQL_Query", query); using (DbDataReader reader = await command.ExecuteReaderAsync()) @@ -5152,6 +5275,43 @@ private async Task Remote_Shell_Dialog() #endregion + #region Remote EventLog + + private bool remote_eventlog_dialog_open = false; + + private async Task Remote_EventLog_Dialog() + { + if (remote_eventlog_dialog_open) + return; + + var options = new DialogOptions + { + CloseButton = true, + FullWidth = true, + MaxWidth = MaxWidth.ExtraLarge, + BackgroundClass = "dialog-blurring", + }; + + DialogParameters parameters = new DialogParameters(); + parameters.Add("device_id", notes_device_id); + parameters.Add("device_name", notes_device_name); + parameters.Add("tenant_guid", tenant_guid); + parameters.Add("location_guid", location_guid); + + remote_eventlog_dialog_open = true; + + var result = await DialogService.Show(string.Empty, parameters, options).Result; + + remote_eventlog_dialog_open = false; + + if (result.Canceled) + return; + + Logging.Handler.Debug("/devices -> Remote_EventLog_Dialog", "Result", result.Data?.ToString() ?? "Dialog closed"); + } + + #endregion + #region Remote Shell History private List device_information_remote_shell_history_mysql_data; @@ -5415,6 +5575,116 @@ private async Task Remote_Service_Action(string action, string name) #endregion + #region Remote Action Availability Check + + private async Task CheckRemoteActionAvailability(Func action, bool policyFeatureEnabled) + { + // Refresh device connection status before checking + await RefreshSingleDeviceConnectionStatus(); + + // Check if no policy is assigned + if (policy_name == "-") + { + await DialogService.ShowMessageBox( + "No Policy Assigned", + "This device does not have a policy assigned yet. Please assign a policy to enable remote features. See here: https://docs.netlockrmm.com/en/how-to/enabling-remote-features", + yesText: "OK", + options: new DialogOptions() { FullWidth = true, MaxWidth = MaxWidth.Small, BackgroundClass = "dialog-blurring" } + ); + return; + } + + // Check if device is not connected + if (!deviceConnected) + { + // Check if policy is assigned but device not connected + if (policyFeatureEnabled) + { + await DialogService.ShowMessageBox( + "Device Not Connected", + "The device is currently not connected. The policy may not have been synchronized yet, or the device is offline. Please ensure the device is online and connected.", + yesText: "OK", + options: new DialogOptions() { FullWidth = true, MaxWidth = MaxWidth.Medium, BackgroundClass = "dialog-blurring" } + ); + } + else + { + await DialogService.ShowMessageBox( + "Device Not Connected", + "The device is currently not connected or offline. Please ensure the device is online and connected.", + yesText: "OK", + options: new DialogOptions() { FullWidth = true, MaxWidth = MaxWidth.Medium, BackgroundClass = "dialog-blurring" } + ); + } + return; + } + + // Check if feature is disabled in policy + if (!policyFeatureEnabled) + { + await DialogService.ShowMessageBox( + "Feature Disabled in Policy", + "This feature is disabled in the assigned policy. Please enable the feature in the policy settings or assign a different policy to use this functionality. See here: https://docs.netlockrmm.com/en/how-to/enabling-remote-features", + yesText: "OK", + options: new DialogOptions() { FullWidth = true, MaxWidth = MaxWidth.Medium, BackgroundClass = "dialog-blurring" } + ); + return; + } + + // If all checks pass, execute the action + await action.Invoke(); + } + + /// + /// Refreshes the connection status for the currently selected device + /// + private async Task RefreshSingleDeviceConnectionStatus() + { + try + { + // Get list of connected device access keys + string connectedDevicesAccessKeys = await NetLock_RMM_Web_Console.Classes.Helper.Http.Get_Request_With_Api_Key( + Configuration.Web_Console.publicOverrideUrl + "/admin/devices/connected", false); + + if (string.IsNullOrEmpty(connectedDevicesAccessKeys)) + { + deviceConnected = false; + return; + } + + // Find the current device in mysql_data + var currentDevice = mysql_data?.FirstOrDefault(d => d.device_id == notes_device_id); + + if (currentDevice != null) + { + // Check if the device's access key is in the connected devices list + bool isConnected = connectedDevicesAccessKeys.Contains(currentDevice.access_key); + + // Update deviceConnected variable + deviceConnected = isConnected; + + // Update the device in mysql_data table + currentDevice.connected = isConnected; + + // Trigger UI update + await InvokeAsync(StateHasChanged); + + Logging.Handler.Debug("/devices -> RefreshSingleDeviceConnectionStatus", + "Device connection status updated", + $"Device: {notes_device_name}, Access Key: {currentDevice.access_key}, Connected: {isConnected}"); + } + } + catch (Exception ex) + { + Logging.Handler.Error("/devices -> RefreshSingleDeviceConnectionStatus", + "Failed to refresh device connection status", + ex.ToString()); + // Don't throw - just log the error and continue with existing status + } + } + + #endregion + #region Shutdown Device private async Task Remote_Shutdown_Device() @@ -5627,6 +5897,215 @@ private async Task Remote_Control_Dialog() #endregion + #region Move Devices + + private bool allDevicesChecked = false; + private List selectedDeviceIds = new List(); + + private async Task MoveDevicesSwitch() + { + if (moveDevices) + { + // Deactivate move mode and reset selections + moveDevices = false; + selectedDeviceIds.Clear(); + allDevicesChecked = false; + + // Reset all checkboxes + foreach (var device in mysql_data) + { + device.isChecked = false; + } + } + else + { + // Activate move mode + moveDevices = true; + } + } + + private async Task BulkRemoteShellSwitch() + { + if (bulkRemoteShell) + { + // Deactivate bulk remote shell mode and reset selections + bulkRemoteShell = false; + selectedDeviceIds.Clear(); + allDevicesChecked = false; + + // Reset all checkboxes + foreach (var device in mysql_data) + { + device.isChecked = false; + } + } + else + { + // Activate bulk remote shell mode + bulkRemoteShell = true; + } + } + + private async Task ToggleAllDevicesSelection() + { + if (allDevicesChecked) + { + // Check all devices + selectedDeviceIds = mysql_data.Select(d => d.device_id).ToList(); + foreach (var device in mysql_data) + { + device.isChecked = true; + } + } + else + { + // Uncheck all devices + selectedDeviceIds.Clear(); + foreach (var device in mysql_data) + { + device.isChecked = false; + } + } + } + + private void OnDeviceCheckboxChanged(MySQL_Entity device) + { + if (device.isChecked) + { + // Add to selection if not already present + if (!selectedDeviceIds.Contains(device.device_id)) + { + selectedDeviceIds.Add(device.device_id); + } + } + else + { + // Remove from selection + selectedDeviceIds.Remove(device.device_id); + allDevicesChecked = false; // Uncheck "select all" if any individual item is unchecked + } + + // Update "select all" checkbox state + if (selectedDeviceIds.Count == mysql_data.Count && mysql_data.Count > 0) + { + allDevicesChecked = true; + } + } + + private bool move_devices_dialog_open = false; + + private async Task Move_Devices_Dialog() + { + if (move_devices_dialog_open) + return; + + // Check if devices are selected + if (selectedDeviceIds == null || selectedDeviceIds.Count == 0) + { + Snackbar.Add(Localizer["no_devices_selected"], Severity.Warning); + return; + } + + var options = new DialogOptions + { + CloseButton = true, + FullWidth = true, + MaxWidth = MaxWidth.Medium, + BackgroundClass = "dialog-blurring", + }; + + DialogParameters parameters = new DialogParameters(); + parameters.Add("SelectedDeviceIds", selectedDeviceIds.ToList()); + + move_devices_dialog_open = true; + + var result = await DialogService.Show(string.Empty, parameters, options).Result; + + move_devices_dialog_open = false; + + if (result.Canceled) + { + return; + } + + Logging.Handler.Debug("/devices -> Move_Devices_Dialog", "Result", result.Data?.ToString() ?? "null"); + + if (result.Data != null && (bool)result.Data == true) + { + // Reset move mode after successful move + moveDevices = false; + selectedDeviceIds.Clear(); + allDevicesChecked = false; + + foreach (var device in mysql_data) + { + device.isChecked = false; + } + + await Get_Clients_OverviewAsync(); + } + } + + #endregion + + #region Bulk Remote Shell + + private bool bulk_remote_shell_dialog_open = false; + + private async Task Bulk_Remote_Shell_Dialog() + { + if (bulk_remote_shell_dialog_open) + return; + + // Check if devices are selected + if (selectedDeviceIds == null || selectedDeviceIds.Count == 0) + { + Snackbar.Add("No devices selected", Severity.Warning); + return; + } + + var options = new DialogOptions + { + CloseButton = true, + FullWidth = true, + MaxWidth = MaxWidth.ExtraLarge, + BackgroundClass = "dialog-blurring", + }; + + DialogParameters parameters = new DialogParameters(); + parameters.Add("device_ids", selectedDeviceIds.ToList()); + parameters.Add("tenant_guid", tenant_guid); + parameters.Add("location_guid", location_guid); + + bulk_remote_shell_dialog_open = true; + + var result = await DialogService.Show(string.Empty, parameters, options).Result; + + bulk_remote_shell_dialog_open = false; + + if (result.Canceled) + { + return; + } + + Logging.Handler.Debug("/devices -> Bulk_Remote_Shell_Dialog", "Result", result.Data?.ToString() ?? "null"); + + if (result.Data != null && (bool)result.Data == true) + { + // Reset bulk remote shell mode after successful execution + bulkRemoteShell = false; + selectedDeviceIds.Clear(); + allDevicesChecked = false; + + foreach (var device in mysql_data) + { + device.isChecked = false; + } + } + } + + #endregion + #region Data_Export private bool show_export_table_dialog_open = false; diff --git a/NetLock-RMM-Web-Console/Components/Pages/Devices/Dialogs/Bulk_Remote_Shell/Bulk_Remote_Shell_Dialog.razor b/NetLock-RMM-Web-Console/Components/Pages/Devices/Dialogs/Bulk_Remote_Shell/Bulk_Remote_Shell_Dialog.razor new file mode 100644 index 00000000..a2f772a7 --- /dev/null +++ b/NetLock-RMM-Web-Console/Components/Pages/Devices/Dialogs/Bulk_Remote_Shell/Bulk_Remote_Shell_Dialog.razor @@ -0,0 +1,799 @@ +@using MySqlConnector +@using System.Data.Common +@using System.Text.Json +@using System.Xml.Serialization +@using System.Text +@using System.Text.RegularExpressions +@using System.Text.Json.Nodes +@using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage +@using Microsoft.AspNetCore.DataProtection +@using System.Globalization +@using Microsoft.AspNetCore.SignalR.Client +@using System.Net.Http +@using System.Security.Claims + +@inject NavigationManager NavigationManager +@inject ILocalStorageService localStorage +@inject IDialogService DialogService +@inject ISnackbar Snackbar +@inject IJSRuntime JSRuntime +@inject IDataProtectionProvider DataProtectionProvider +@inject IStringLocalizer Localizer +@inject AuthenticationStateProvider AuthenticationStateProvider + + + + + + + Bulk Remote Shell (@device_ids.Count device(s)) + + + + @{ + if (!remote_server_client_setup) + { + Not connected to the NetLock RMM server backend. + + Reconnect. + } + } + + Execution Results (@device_ids.Count device(s)) + + + + Device + Platform + Status + Result + + + +
+ + @device.Name +
+
+ @device.Platform + + @if (deviceTimedOut.ContainsKey(device.Id) && deviceTimedOut[device.Id]) + { + Timeout + } + else if (deviceHasError.ContainsKey(device.Id) && deviceHasError[device.Id]) + { + Failed + } + else if (deviceCompleted.ContainsKey(device.Id) && deviceCompleted[device.Id]) + { + Completed + } + else if (loading_overlay) + { + Executing... + } + else + { + Pending + } + + + @if (deviceOutputs.ContainsKey(device.Id) && !string.IsNullOrEmpty(deviceOutputs[device.Id])) + { + + View Output + + } + else + { + No output yet + } + +
+
+ + Command Input + + @if (!expanded) + { + + } + + + @foreach (var s in scripts) + { + + } + + + + +
+ + @if (expanded) + { + + } + else + { + + } + + Expand +
+
+ + + + +
+ + @{ + if (loading_overlay) + { + + Execution in progress... (@executionProgress / @device_ids.Count) + + } + } + +
+ +
+ +
+ + Community Scripts + +
+ + + + + Execute on @device_ids.Count device(s) +
+ + Close +
+
+ +@code { + + [CascadingParameter] IMudDialogInstance MudDialog { get; set; } + [Parameter] public List device_ids { get; set; } + [Parameter] public string tenant_guid { get; set; } + [Parameter] public string location_guid { get; set; } + + private string netlock_username = String.Empty; + private string token = String.Empty; + private bool loading_overlay = false; + private bool expanded = false; + private int executionProgress = 0; + private int timeout = 1; + + private class DeviceInfo + { + public string Id { get; set; } + public string Name { get; set; } + public string Platform { get; set; } + } + + private List deviceInfos = new List(); + private Dictionary deviceOutputs = new Dictionary(); + private Dictionary deviceCompleted = new Dictionary(); + private Dictionary deviceHasError = new Dictionary(); + private Dictionary deviceTimedOut = new Dictionary(); + private Dictionary deviceTimeoutTimers = new Dictionary(); + + protected override async Task OnInitializedAsync() + { + // Get the current user from the authentication state + var user = (await AuthenticationStateProvider.GetAuthenticationStateAsync()).User; + + // Check if user is authenticated + if (user?.Identity is not { IsAuthenticated: true }) + { + NavigationManager.NavigateTo("/login", true); + return; + } + + // Retrieve username from claims + netlock_username = user.FindFirst(ClaimTypes.Email)?.Value; + + token = await Classes.Authentication.User.Get_Remote_Session_Token(netlock_username); + + await Remote_Setup_SignalR(); + + // Load device information + await LoadDeviceInfos(); + + await Get_Scripts(); + } + + private async Task LoadDeviceInfos() + { + foreach (var device_id in device_ids) + { + var name = await Classes.MySQL.Handler.GetDeviceNameById(device_id); + var platform = await Classes.MySQL.Handler.Get_Device_Platform(device_id); + + deviceInfos.Add(new DeviceInfo + { + Id = device_id, + Name = name, + Platform = platform + }); + + // Initialize output tracking for each device + deviceOutputs[device_id] = ""; + deviceCompleted[device_id] = false; + deviceHasError[device_id] = false; + deviceTimedOut[device_id] = false; + } + } + + private string GetPlatformIcon(string platform) + { + return platform switch + { + "Windows" => Icons.Custom.Brands.MicrosoftWindows, + "Linux" => Icons.Custom.Brands.Linux, + "MacOS" => Icons.Custom.Brands.Apple, + _ => Icons.Material.Filled.Computer + }; + } + + private async Task ShowDeviceOutput(DeviceInfo device) + { + var parameters = new DialogParameters + { + ["DeviceName"] = device.Name, + ["Output"] = deviceOutputs.ContainsKey(device.Id) ? deviceOutputs[device.Id] : "No output available", + ["HasError"] = deviceHasError.ContainsKey(device.Id) && deviceHasError[device.Id] + }; + + var options = new DialogOptions + { + CloseButton = true, + MaxWidth = MaxWidth.Large, + FullWidth = true + }; + + await DialogService.ShowAsync("Device Output", parameters, options); + } + + #region Editors + + private StandaloneCodeEditor remote_shell_command_editor = null!; + + private StandaloneEditorConstructionOptions Remote_Shell_Editor_Construction_Options(StandaloneCodeEditor editor) + { + return new StandaloneEditorConstructionOptions + { + AutomaticLayout = true, + Theme = "vs-dark", + Language = "powershell", + ReadOnly = false, + }; + } + + private async Task Remote_Shell_Editor_Handle_Input(ModelContentChangedEvent e) + { + remote_shell_powershell_code = await remote_shell_command_editor.GetValue(); + } + + #endregion + + #region Scripts + + private List scripts = new List(); + + private string collection_script_name = String.Empty; + private string collection_script = String.Empty; + + private async Task Get_Scripts() + { + // Get scripts for all platforms (or we could filter by the most common platform) + MySqlConnection conn = new MySqlConnection(Configuration.MySQL.Connection_String); + + try + { + await conn.OpenAsync(); + + string query = "SELECT DISTINCT name FROM scripts;"; + + MySqlCommand cmd = new MySqlCommand(query, conn); + + using (DbDataReader reader = await cmd.ExecuteReaderAsync()) + { + while (await reader.ReadAsync()) + { + scripts.Add(reader["name"].ToString() ?? String.Empty); + } + } + } + catch (Exception ex) + { + Logging.Handler.Error("/bulk_remote_shell -> Get_Scripts", "Result", ex.ToString()); + } + finally + { + await conn.CloseAsync(); + } + } + + private async Task Get_Script(string name) + { + MySqlConnection conn = new MySqlConnection(Configuration.MySQL.Connection_String); + + try + { + await conn.OpenAsync(); + + string query = "SELECT * FROM scripts WHERE name = @name LIMIT 1;"; + + MySqlCommand cmd = new MySqlCommand(query, conn); + cmd.Parameters.AddWithValue("@name", name); + + using (DbDataReader reader = await cmd.ExecuteReaderAsync()) + { + while (await reader.ReadAsync()) + { + Collections.Scripts.Script _object = JsonSerializer.Deserialize(reader["json"].ToString() ?? String.Empty); + timeout = _object.timeout; + + await remote_shell_command_editor.SetValue(_object.script);} + } + } + catch (Exception ex) + { + Logging.Handler.Error("/bulk_remote_shell -> Get_Script", "Result", ex.ToString()); + } + finally + { + await conn.CloseAsync(); + } + } + + #endregion + + #region Remote + + private async Task Remote_Keyboard_Enter(BlazorMonaco.KeyboardEvent e) + { + if (remote_server_client_setup) + { + if (e.CtrlKey && e.Code == "Enter" && !String.IsNullOrEmpty(remote_shell_powershell_code) && !Regex.IsMatch(remote_shell_powershell_code, @"^\s*$")) + await Bulk_Remote_Shell_Send_Command(remote_shell_powershell_code, remote_shell_wait_response); + } + } + + private async Task Enter(KeyboardEventArgs e) + { + if (remote_server_client_setup) + { + if (e.Code == "Enter" && !String.IsNullOrEmpty(remote_shell_powershell_code) && !Regex.IsMatch(remote_shell_powershell_code, @"^\s*$")) + await Bulk_Remote_Shell_Send_Command(remote_shell_powershell_code, remote_shell_wait_response); + } + } + + public class Remote_Admin_Identity + { + public string token { get; set; } + } + + public class Remote_Target_Device + { + public string device_id { get; set; } + public string device_name { get; set; } + public string location_guid { get; set; } + public string tenant_guid { get; set; } + } + + public class Remote_Command + { + public int type { get; set; } + public bool wait_response { get; set; } + public string powershell_code { get; set; } + public int file_browser_command { get; set; } + public string file_browser_path { get; set; } + public string file_browser_path_move { get; set; } + public string file_browser_file_content { get; set; } + } + + public class Remote_Root_Object + { + public Remote_Admin_Identity admin_identity { get; set; } + public Remote_Target_Device target_device { get; set; } + public Remote_Command command { get; set; } + } + + // Remote Server + private HubConnection remote_server_client; + private System.Threading.Timer remote_server_clientCheckTimer; + private bool remote_server_client_setup = false; + private string remote_admin_identity = String.Empty; + + // Remote Shell + private string remote_shell_powershell_code = String.Empty; + private bool remote_shell_wait_response = true; + + public async Task Remote_Setup_SignalR() + { + this.Snackbar.Configuration.ShowCloseIcon = true; + this.Snackbar.Configuration.PositionClass = Defaults.Classes.Position.BottomRight; + + try + { + Remote_Admin_Identity identity = new Remote_Admin_Identity + { + token = token + }; + + // Create the object that contains the device_identity object + var jsonObject = new { admin_identity = identity }; + + // Serialize the object to a JSON string + string json = JsonSerializer.Serialize(jsonObject, new JsonSerializerOptions { WriteIndented = true }); + + remote_admin_identity = json; + + remote_server_client = new HubConnectionBuilder() + .WithUrl(Configuration.Remote_Server.Connection_String + "/commandHub", options => + { + options.Headers.Add("Admin-Identity", Uri.EscapeDataString(remote_admin_identity)); + }) + .Build(); + + // Remote Shell - Response mit Device-Zuordnung ΓΌber >>nlocksep<< Separator + remote_server_client.On("ReceiveClientResponseRemoteShell", async (response) => + { + Logging.Handler.Debug("/bulk_remote_shell -> Remote_Setup_SignalR", "ReceiveClientResponseRemoteShell", response); + + // Use InvokeAsync to reflect changes on UI immediately + await InvokeAsync(() => + { + try + { + // Parse Response: device_id + ">>nlocksep<<" + response + string deviceId = ""; + string output = response; + + if (response.Contains(">>nlocksep<<")) + { + var parts = response.Split(new[] { ">>nlocksep<<" }, StringSplitOptions.None); + if (parts.Length >= 2) + { + deviceId = parts[0].Trim(); + output = parts[1]; + } + } + + // Ordne Output dem richtigen GerΓ€t zu + if (!string.IsNullOrEmpty(deviceId) && deviceOutputs.ContainsKey(deviceId)) + { + // Stop the timeout timer for this device + if (deviceTimeoutTimers.ContainsKey(deviceId)) + { + deviceTimeoutTimers[deviceId]?.Dispose(); + deviceTimeoutTimers.Remove(deviceId); + } + + string timestamp = DateTime.Now.ToString("dd.MM.yyyy HH:mm:ss"); + + // Append output to existing output + if (string.IsNullOrEmpty(deviceOutputs[deviceId])) + { + deviceOutputs[deviceId] = $"[{timestamp}]\n{output}"; + } + else + { + deviceOutputs[deviceId] = $"{deviceOutputs[deviceId]}\n\n--------------\n\n[{timestamp}]\n{output}"; + } + + deviceCompleted[deviceId] = true; + executionProgress++; + + Logging.Handler.Debug("/bulk_remote_shell", "Response assigned to device", $"{deviceId} - Progress: {executionProgress}/{device_ids.Count}"); + } + else + { + // Fallback: Wenn keine device_id erkannt wurde + Logging.Handler.Debug("/bulk_remote_shell", "Could not parse device_id from response", response.Substring(0, Math.Min(100, response.Length))); + + // Zeige bei erstem wartenden GerΓ€t + var waitingDevice = deviceInfos.FirstOrDefault(d => !deviceCompleted[d.Id]); + if (waitingDevice != null) + { + string timestamp = DateTime.Now.ToString("dd.MM.yyyy HH:mm:ss"); + deviceOutputs[waitingDevice.Id] = $"[{timestamp}] [Warning: Could not identify device]\n{response}"; + deviceCompleted[waitingDevice.Id] = true; + executionProgress++; + } + } + + if (executionProgress >= device_ids.Count) + { + loading_overlay = false; + } + + StateHasChanged(); + } + catch (Exception ex) + { + Logging.Handler.Error("/bulk_remote_shell", "Error parsing response", ex.Message); + deviceHasError[deviceInfos.First().Id] = true; + StateHasChanged(); + } + }); + }); + + // Start the connection + await remote_server_client.StartAsync(); + + remote_server_client_setup = true; + + Logging.Handler.Debug("/bulk_remote_shell -> Remote_Setup_SignalR", "Connected to the remote server.", remote_server_client_setup.ToString()); + } + catch (Exception ex) + { + Logging.Handler.Error("/bulk_remote_shell -> Remote_Setup_SignalR", "General error", ex.ToString()); + } + } + + #endregion + + #region Bulk Remote Shell + private async Task Bulk_Remote_Shell_Send_Command(string powershell_code, bool wait_response) + { + try + { + string encoded_powershell_code = await Base64.Handler.Encode(powershell_code); + + loading_overlay = true; + executionProgress = 0; + + // Dispose old timers + foreach (var timer in deviceTimeoutTimers.Values) + { + timer?.Dispose(); + } + deviceTimeoutTimers.Clear(); + + // Reset status for all devices + foreach (var device in deviceInfos) + { + deviceCompleted[device.Id] = false; + deviceHasError[device.Id] = false; + deviceTimedOut[device.Id] = false; + } + + StateHasChanged(); + + // Send command to each device + foreach (var deviceInfo in deviceInfos) + { + try + { + // Start timeout timer for this device + int timeoutMilliseconds = timeout * 60 * 1000; // Convert minutes to milliseconds + var timer = new System.Threading.Timer(async _ => + { + await InvokeAsync(() => + { + if (!deviceCompleted[deviceInfo.Id]) + { + deviceTimedOut[deviceInfo.Id] = true; + deviceCompleted[deviceInfo.Id] = true; + deviceHasError[deviceInfo.Id] = false; // Not an error, just timeout + + string timestamp = DateTime.Now.ToString("dd.MM.yyyy HH:mm:ss"); + deviceOutputs[deviceInfo.Id] = $"[{timestamp}] [TIMEOUT] Script execution exceeded the timeout limit of {timeout} minute(s).\n{deviceOutputs[deviceInfo.Id]}"; + + executionProgress++; + + if (executionProgress >= device_ids.Count) + { + loading_overlay = false; + } + + StateHasChanged(); + } + }); + }, null, timeoutMilliseconds, System.Threading.Timeout.Infinite); + + deviceTimeoutTimers[deviceInfo.Id] = timer; + // Create the object + var adminIdentity = new Remote_Admin_Identity + { + token = token + }; + + var targetDevice = new Remote_Target_Device + { + device_id = deviceInfo.Id, + device_name = deviceInfo.Name, + tenant_guid = tenant_guid, + location_guid = location_guid + }; + + var command = new Remote_Command + { + type = 0, + wait_response = wait_response, + powershell_code = encoded_powershell_code + }; + + var rootObject = new Remote_Root_Object + { + admin_identity = adminIdentity, + target_device = targetDevice, + command = command + }; + + // Serialization of the object + string json = JsonSerializer.Serialize(rootObject, new JsonSerializerOptions { WriteIndented = true }); + + if (remote_server_client_setup) + { + Logging.Handler.Debug("/bulk_remote_shell -> Bulk_Remote_Shell_Send_Command", "Sending to device", deviceInfo.Name); + + await remote_server_client.SendAsync("MessageReceivedFromWebconsole", json); + + // Small delay between commands to avoid overwhelming the server + await Task.Delay(100); + } + else + { + Logging.Handler.Error("/bulk_remote_shell -> Bulk_Remote_Shell_Send_Command", "Remote server not setup.", ""); + } + } + catch (Exception ex) + { + Logging.Handler.Error("/bulk_remote_shell -> Bulk_Remote_Shell_Send_Command", $"Error sending to device {deviceInfo.Name}", ex.Message); + + await InvokeAsync(() => + { + string timestamp = DateTime.Now.ToString("dd.MM.yyyy HH:mm:ss"); + deviceOutputs[deviceInfo.Id] = $"[{timestamp}] [ERROR] Failed to send command: {ex.Message}\n{deviceOutputs[deviceInfo.Id]}"; + deviceHasError[deviceInfo.Id] = true; + deviceCompleted[deviceInfo.Id] = true; + executionProgress++; + StateHasChanged(); + }); + } + } + + // If not waiting for response, mark as complete + if (!wait_response) + { + foreach (var device in deviceInfos) + { + if (!deviceCompleted[device.Id]) + { + deviceOutputs[device.Id] = $"{DateTime.Now.ToString("dd.MM.yyyy HH:mm:ss")}\nCommand sent successfully (no response expected)\n{deviceOutputs[device.Id]}"; + deviceCompleted[device.Id] = true; + } + } + loading_overlay = false; + StateHasChanged(); + } + } + catch (Exception ex) + { + Logging.Handler.Error("/bulk_remote_shell -> Bulk_Remote_Shell_Send_Command", "General error", ex.Message); + loading_overlay = false; + StateHasChanged(); + } + } + + #endregion + + private async Task OK() + { + this.Snackbar.Configuration.ShowCloseIcon = true; + this.Snackbar.Configuration.PositionClass = Defaults.Classes.Position.BottomRight; + + bool success = true; + + if (success) + { + this.MudDialog.Close(DialogResult.Ok(true)); + } + else + { + this.Snackbar.Add("An error occurred", Severity.Error); + this.MudDialog.Close(DialogResult.Ok(false)); + } + } + + private async Task Cancel() + { + // Dispose all timeout timers + foreach (var timer in deviceTimeoutTimers.Values) + { + timer?.Dispose(); + } + deviceTimeoutTimers.Clear(); + + // Stop the remote server client + if (remote_server_client_setup) + { + await remote_server_client.StopAsync(); + remote_server_client_setup = false; + }; + + this.MudDialog.Cancel(); + } + + private bool community_scripts_dialog_open = false; + + private async Task Community_Scripts_Dialog() + { + this.Snackbar.Configuration.ShowCloseIcon = true; + this.Snackbar.Configuration.PositionClass = Defaults.Classes.Position.BottomRight; + + try + { + if (community_scripts_dialog_open) + return; + + var options = new DialogOptions + { + CloseButton = true, + FullWidth = true, + MaxWidth = MaxWidth.ExtraLarge, + BackgroundClass = "dialog-blurring", + }; + + DialogParameters parameters = new DialogParameters(); + parameters.Add("editor", "true"); + + community_scripts_dialog_open = true; + + var result = await this.DialogService.Show(string.Empty, parameters, options).Result; + + // Handle the result if needed + if (!result.Canceled) + { + var resultData = result.Data; + + if (resultData != null) + { + string script = await remote_shell_command_editor.GetValue(); + script += await Base64.Handler.Decode(resultData.ToString()); + + await remote_shell_command_editor.SetValue(script); + } + } + } + catch (Exception ex) + { + Logging.Handler.Error("/bulk_remote_shell -> Community_Scripts_Dialog", "Community_Scripts_Dialog", ex.ToString()); + } + finally + { + community_scripts_dialog_open = false; + } + } +} + diff --git a/NetLock-RMM-Web-Console/Components/Pages/Devices/Dialogs/Bulk_Remote_Shell/Bulk_Remote_Shell_Result_Dialog.razor b/NetLock-RMM-Web-Console/Components/Pages/Devices/Dialogs/Bulk_Remote_Shell/Bulk_Remote_Shell_Result_Dialog.razor new file mode 100644 index 00000000..11db60f3 --- /dev/null +++ b/NetLock-RMM-Web-Console/Components/Pages/Devices/Dialogs/Bulk_Remote_Shell/Bulk_Remote_Shell_Result_Dialog.razor @@ -0,0 +1,61 @@ +@inject ISnackbar Snackbar +@inject IJSRuntime JSRuntime + + + + + + @DeviceName - Output + + + + @if (HasError) + { + + Execution failed or returned an error + + } + + +
@Output
+
+ + + Copy to Clipboard + +
+ + Close + +
+ +@code { + [CascadingParameter] IMudDialogInstance MudDialog { get; set; } + + [Parameter] public string DeviceName { get; set; } + [Parameter] public string Output { get; set; } + [Parameter] public bool HasError { get; set; } + + private async Task CopyToClipboard() + { + try + { + await JSRuntime.InvokeVoidAsync("navigator.clipboard.writeText", Output); + Snackbar.Add("Output copied to clipboard", Severity.Success); + } + catch (Exception) + { + Snackbar.Add("Failed to copy to clipboard", Severity.Error); + } + } + + private void Close() + { + MudDialog.Close(); + } +} + diff --git a/NetLock-RMM-Web-Console/Components/Pages/Devices/Dialogs/Move_Devices_Dialog.razor b/NetLock-RMM-Web-Console/Components/Pages/Devices/Dialogs/Move_Devices_Dialog.razor index c2ac34a4..a8ce7ccc 100644 --- a/NetLock-RMM-Web-Console/Components/Pages/Devices/Dialogs/Move_Devices_Dialog.razor +++ b/NetLock-RMM-Web-Console/Components/Pages/Devices/Dialogs/Move_Devices_Dialog.razor @@ -1,6 +1,7 @@ -ο»Ώ@using MySqlConnector; +ο»Ώο»Ώ@using MySqlConnector; @using System.Data.Common; @using System.Security.Claims +@using MySqlConnector @inject NavigationManager NavigationManager @inject ILocalStorageService localStorage @@ -20,32 +21,92 @@ - @Localizer["title"] - + + @Localizer["title"] + + - @Localizer["text"] + @Localizer["text"] + + + Selected devices: @selectedDeviceCount + + + + + + + @foreach (var tenant in tenants_list) + { + + } + - - @foreach (var device in device_list) + + + @foreach (var location in locations_list) { - + } - + + + - @foreach (var group in groups_list) { } + @if (!String.IsNullOrEmpty(error_message)) + { + @error_message + } + - @Localizer["cancel"] - @Localizer["move"] - + + @Localizer["cancel"] + + + Confirm + + @@ -53,18 +114,20 @@ @code { - private string target_device_name = null; - private string target_device_id = null; - private string target_group_name = null; - private string target_group_id = null; - private List device_list = new List { }; - private List groups_list = new List { }; + private string target_tenant_id; + private string target_location_id; + private string target_group_name; + private string target_group_id; + + private List tenants_list = new List(); + private List locations_list = new List(); + private List groups_list = new List(); + + private int selectedDeviceCount = 0; + private string error_message; [CascadingParameter] IMudDialogInstance MudDialog { get; set; } - [Parameter] public string tenant_id { get; set; } - [Parameter] public string location_id { get; set; } - [Parameter] public string group_id { get; set; } - [Parameter] public bool grouped { get; set; } + [Parameter] public List SelectedDeviceIds { get; set; } protected override async Task OnInitializedAsync() { @@ -78,24 +141,99 @@ return; } - Logging.Handler.Debug("Move_Devices_Dialog", "tenant_id", tenant_id); - Logging.Handler.Debug("Move_Devices_Dialog", "location_id", location_id); - Logging.Handler.Debug("Move_Devices_Dialog", "group_id", group_id); - Logging.Handler.Debug("Move_Devices_Dialog", "grouped", grouped.ToString()); + selectedDeviceCount = SelectedDeviceIds?.Count ?? 0; + Logging.Handler.Debug("Move_Devices_Dialog", "selectedDeviceCount", selectedDeviceCount.ToString()); - await Get_Devices(); - await Get_Groups(); + await Get_Tenants(); StateHasChanged(); } - private async Task Get_Devices() + // Watch for tenant selection change + private string _target_tenant_name; + private string target_tenant_name { - string query = String.Empty; + get => _target_tenant_name; + set + { + if (_target_tenant_name != value) + { + _target_tenant_name = value; + target_location_name = null; + target_group_name = null; + locations_list.Clear(); + groups_list.Clear(); + if (!String.IsNullOrEmpty(value)) + { + _ = Get_Locations(); + } + } + } + } - if (grouped) - query = "SELECT * FROM devices WHERE authorized = '1' AND group_id = @group_id AND location_id = @location_id AND tenant_id = @tenant_id;"; - else - query = "SELECT * FROM devices WHERE authorized = '1';"; + // Watch for location selection change + private string _target_location_name; + private string target_location_name + { + get => _target_location_name; + set + { + if (_target_location_name != value) + { + _target_location_name = value; + target_group_name = null; + groups_list.Clear(); + if (!String.IsNullOrEmpty(value)) + { + _ = Get_Groups(); + } + } + } + } + + private async Task Get_Tenants() + { + string query = "SELECT * FROM tenants ORDER BY name ASC;"; + + MySqlConnection conn = new MySqlConnection(Configuration.MySQL.Connection_String); + + try + { + await conn.OpenAsync(); + + MySqlCommand command = new MySqlCommand(query, conn); + + using (DbDataReader reader = await command.ExecuteReaderAsync()) + { + if (reader.HasRows) + { + while (await reader.ReadAsync()) + tenants_list.Add(reader["name"].ToString() ?? ""); + } + } + } + catch (Exception ex) + { + Logging.Handler.Error("Move_Devices_Dialog", "Get_Tenants", ex.Message); + } + finally + { + conn.Close(); + } + + StateHasChanged(); + } + + private async Task Get_Locations() + { + locations_list.Clear(); + + // Get tenant_id first + target_tenant_id = await Get_Tenant_Id(target_tenant_name); + + if (String.IsNullOrEmpty(target_tenant_id)) + return; + + string query = "SELECT * FROM `locations` WHERE tenant_id = @tenant_id ORDER BY name ASC;"; MySqlConnection conn = new MySqlConnection(Configuration.MySQL.Connection_String); @@ -104,32 +242,40 @@ await conn.OpenAsync(); MySqlCommand command = new MySqlCommand(query, conn); - command.Parameters.AddWithValue("@tenant_id", tenant_id); - command.Parameters.AddWithValue("@location_id", location_id); - command.Parameters.AddWithValue("@group_id", group_id); + command.Parameters.AddWithValue("@tenant_id", target_tenant_id); using (DbDataReader reader = await command.ExecuteReaderAsync()) { if (reader.HasRows) { while (await reader.ReadAsync()) - device_list.Add(reader["device_name"].ToString() ?? ""); + locations_list.Add(reader["name"].ToString() ?? ""); } } } catch (Exception ex) { - Logging.Handler.Error("class", "Get_Devices", ex.Message); + Logging.Handler.Error("Move_Devices_Dialog", "Get_Locations", ex.Message); } finally { conn.Close(); } + + StateHasChanged(); } private async Task Get_Groups() { - string query = "SELECT * FROM `groups` WHERE location_id = @location_id AND tenant_id = @tenant_id;"; + groups_list.Clear(); + + // Get location_id first + target_location_id = await Get_Location_Id(target_location_name, target_tenant_id); + + if (String.IsNullOrEmpty(target_location_id)) + return; + + string query = "SELECT * FROM `groups` WHERE location_id = @location_id AND tenant_id = @tenant_id ORDER BY name ASC;"; MySqlConnection conn = new MySqlConnection(Configuration.MySQL.Connection_String); @@ -138,8 +284,8 @@ await conn.OpenAsync(); MySqlCommand command = new MySqlCommand(query, conn); - command.Parameters.AddWithValue("@tenant_id", tenant_id); - command.Parameters.AddWithValue("@location_id", location_id); + command.Parameters.AddWithValue("@tenant_id", target_tenant_id); + command.Parameters.AddWithValue("@location_id", target_location_id); using (DbDataReader reader = await command.ExecuteReaderAsync()) { @@ -153,19 +299,20 @@ } catch (Exception ex) { - Logging.Handler.Error("class", "Get_Groups", ex.Message); + Logging.Handler.Error("Move_Devices_Dialog", "Get_Groups", ex.Message); } finally { conn.Close(); } + + StateHasChanged(); } - // Get group id from groups table and device_id from device table from database - private async Task Get_Group_Device_ID() + private async Task Get_Tenant_Id(string tenantName) { - string groupQuery = "SELECT id FROM `groups` WHERE name = @group_name AND location_id = @location_id AND tenant_id = @tenant_id;"; - string deviceQuery = "SELECT id FROM devices WHERE device_name = @device_name AND location_id = @location_id AND tenant_id = @tenant_id;"; + string result = null; + string query = "SELECT id FROM tenants WHERE name = @tenant_name;"; MySqlConnection conn = new MySqlConnection(Configuration.MySQL.Connection_String); @@ -173,102 +320,220 @@ { await conn.OpenAsync(); - MySqlCommand groupCommand = new MySqlCommand(groupQuery, conn); - groupCommand.Parameters.AddWithValue("@tenant_id", tenant_id); - groupCommand.Parameters.AddWithValue("@location_id", location_id); - groupCommand.Parameters.AddWithValue("@group_name", target_group_name); + MySqlCommand command = new MySqlCommand(query, conn); + command.Parameters.AddWithValue("@tenant_name", tenantName); - using (DbDataReader groupReader = await groupCommand.ExecuteReaderAsync()) + using (DbDataReader reader = await command.ExecuteReaderAsync()) { - if (groupReader.HasRows) + if (reader.HasRows && await reader.ReadAsync()) { - while (await groupReader.ReadAsync()) - target_group_id = groupReader["id"].ToString() ?? ""; + result = reader["id"].ToString() ?? ""; } } + } + catch (Exception ex) + { + Logging.Handler.Error("Move_Devices_Dialog", "Get_Tenant_Id", ex.Message); + } + finally + { + conn.Close(); + } + + return result; + } + + private async Task Get_Location_Id(string locationName, string tenantId) + { + string result = null; + string query = "SELECT id FROM `locations` WHERE name = @location_name AND tenant_id = @tenant_id;"; - MySqlCommand deviceCommand = new MySqlCommand(deviceQuery, conn); - deviceCommand.Parameters.AddWithValue("@tenant_id", tenant_id); - deviceCommand.Parameters.AddWithValue("@location_id", location_id); - deviceCommand.Parameters.AddWithValue("@device_name", target_device_name); + MySqlConnection conn = new MySqlConnection(Configuration.MySQL.Connection_String); - using (DbDataReader deviceReader = await deviceCommand.ExecuteReaderAsync()) + try + { + await conn.OpenAsync(); + + MySqlCommand command = new MySqlCommand(query, conn); + command.Parameters.AddWithValue("@location_name", locationName); + command.Parameters.AddWithValue("@tenant_id", tenantId); + + using (DbDataReader reader = await command.ExecuteReaderAsync()) { - if (deviceReader.HasRows) + if (reader.HasRows && await reader.ReadAsync()) { - while (await deviceReader.ReadAsync()) - target_device_id = deviceReader["id"].ToString() ?? ""; + result = reader["id"].ToString() ?? ""; } } } catch (Exception ex) { - Logging.Handler.Error("class", "Get_Group_Device_ID", ex.Message); + Logging.Handler.Error("Move_Devices_Dialog", "Get_Location_Id", ex.Message); } finally { conn.Close(); } + + return result; } - private async Task Move_Device() + private async Task Get_Group_Id(string groupName, string locationId, string tenantId) { - await Get_Group_Device_ID(); + string result = null; + string query = "SELECT id FROM `groups` WHERE name = @group_name AND location_id = @location_id AND tenant_id = @tenant_id;"; + + MySqlConnection conn = new MySqlConnection(Configuration.MySQL.Connection_String); + + try + { + await conn.OpenAsync(); + + MySqlCommand command = new MySqlCommand(query, conn); + command.Parameters.AddWithValue("@group_name", groupName); + command.Parameters.AddWithValue("@location_id", locationId); + command.Parameters.AddWithValue("@tenant_id", tenantId); + using (DbDataReader reader = await command.ExecuteReaderAsync()) + { + if (reader.HasRows && await reader.ReadAsync()) + { + result = reader["id"].ToString() ?? ""; + } + } + } + catch (Exception ex) + { + Logging.Handler.Error("Move_Devices_Dialog", "Get_Group_Id", ex.Message); + } + finally + { + conn.Close(); + } + + return result; + } + + private async Task Move_Devices() + { this.Snackbar.Configuration.ShowCloseIcon = true; this.Snackbar.Configuration.PositionClass = Defaults.Classes.Position.BottomRight; - if (String.IsNullOrEmpty(target_group_name)) - this.Snackbar.Add("UngΓΌltige Eingabe.", Severity.Error); - else + // Validate selection + if (String.IsNullOrEmpty(target_tenant_name) || String.IsNullOrEmpty(target_location_name)) { - bool success = false; + error_message = Localizer["please_select_tenant_and_location"]; + return; + } - MySqlConnection conn = new MySqlConnection(Configuration.MySQL.Connection_String); + if (SelectedDeviceIds == null || SelectedDeviceIds.Count == 0) + { + error_message = Localizer["no_devices_selected"]; + return; + } - try - { - await conn.OpenAsync(); + // Get IDs + if (String.IsNullOrEmpty(target_tenant_id)) + target_tenant_id = await Get_Tenant_Id(target_tenant_name); + + if (String.IsNullOrEmpty(target_location_id)) + target_location_id = await Get_Location_Id(target_location_name, target_tenant_id); - string execute_query = "UPDATE devices SET group_id = @group_id, group_name = @group_name WHERE id = @device_id AND location_id = @location_id AND tenant_id = @tenant_id;"; + // Get group ID if group is selected + if (!String.IsNullOrEmpty(target_group_name)) + { + target_group_id = await Get_Group_Id(target_group_name, target_location_id, target_tenant_id); + } - MySqlCommand cmd = new MySqlCommand(execute_query, conn); - cmd.Parameters.AddWithValue("@tenant_id", tenant_id); - cmd.Parameters.AddWithValue("@location_id", location_id); - cmd.Parameters.AddWithValue("@device_id", target_device_id); - cmd.Parameters.AddWithValue("@group_name", target_group_name); - cmd.Parameters.AddWithValue("@group_id", target_group_id); - cmd.ExecuteNonQuery(); + int successCount = 0; + int failCount = 0; - success = true; - } - catch (Exception ex) - { - Logging.Handler.Error("/location_settings -> Add_Policy_Dialog.OK", "Result", ex.Message); - } - finally - { - await conn.CloseAsync(); - } + MySqlConnection conn = new MySqlConnection(Configuration.MySQL.Connection_String); - if (success) - { - device_list.Remove(target_device_name); - - if (device_list.Count() > 0) - target_device_name = device_list[0]; - else - target_device_name = String.Empty; - - this.Snackbar.Add("Device moved to target group.", Severity.Success); - //this.MudDialog.Close(DialogResult.Ok("success")); - } - else + try + { + await conn.OpenAsync(); + + foreach (var deviceId in SelectedDeviceIds) { - this.Snackbar.Add(Localizer["error_occurred"], Severity.Error); - //this.MudDialog.Close(DialogResult.Ok("error")); + try + { + string execute_query; + MySqlCommand cmd; + + if (String.IsNullOrEmpty(target_group_name)) + { + // Move to location without group + execute_query = @"UPDATE devices + SET tenant_id = @tenant_id, + tenant_name = @tenant_name, + location_id = @location_id, + location_name = @location_name, + group_id = NULL, + group_name = '-' + WHERE id = @device_id;"; + cmd = new MySqlCommand(execute_query, conn); + cmd.Parameters.AddWithValue("@tenant_id", target_tenant_id); + cmd.Parameters.AddWithValue("@tenant_name", target_tenant_name); + cmd.Parameters.AddWithValue("@location_id", target_location_id); + cmd.Parameters.AddWithValue("@location_name", target_location_name); + cmd.Parameters.AddWithValue("@device_id", deviceId); + } + else + { + // Move to location with group + execute_query = @"UPDATE devices + SET tenant_id = @tenant_id, + tenant_name = @tenant_name, + location_id = @location_id, + location_name = @location_name, + group_id = @group_id, + group_name = @group_name + WHERE id = @device_id;"; + cmd = new MySqlCommand(execute_query, conn); + cmd.Parameters.AddWithValue("@tenant_id", target_tenant_id); + cmd.Parameters.AddWithValue("@tenant_name", target_tenant_name); + cmd.Parameters.AddWithValue("@location_id", target_location_id); + cmd.Parameters.AddWithValue("@location_name", target_location_name); + cmd.Parameters.AddWithValue("@group_id", target_group_id); + cmd.Parameters.AddWithValue("@group_name", target_group_name); + cmd.Parameters.AddWithValue("@device_id", deviceId); + } + + await cmd.ExecuteNonQueryAsync(); + successCount++; + } + catch (Exception ex) + { + Logging.Handler.Error("Move_Devices_Dialog", "Move_Device_" + deviceId, ex.Message); + failCount++; + } } } + catch (Exception ex) + { + Logging.Handler.Error("Move_Devices_Dialog", "Move_Devices", ex.Message); + error_message = Localizer["error_occurred"] + ": " + ex.Message; + } + finally + { + await conn.CloseAsync(); + } + + // Show result + if (successCount > 0) + { + string message = String.Format("Moved devices:", successCount); + if (failCount > 0) + message += " " + String.Format("Failed moving devices", failCount); + + this.Snackbar.Add(message, Severity.Success); + this.MudDialog.Close(DialogResult.Ok(true)); + } + else + { + this.Snackbar.Add(Localizer["error_moving_devices"], Severity.Error); + } } private void Cancel() => MudDialog.Cancel(); diff --git a/NetLock-RMM-Web-Console/Components/Pages/Devices/Dialogs/Remote_Control/Code_Signing_Warning_Dialog.razor b/NetLock-RMM-Web-Console/Components/Pages/Devices/Dialogs/Remote_Control/Code_Signing_Warning_Dialog.razor new file mode 100644 index 00000000..ef889b43 --- /dev/null +++ b/NetLock-RMM-Web-Console/Components/Pages/Devices/Dialogs/Remote_Control/Code_Signing_Warning_Dialog.razor @@ -0,0 +1,51 @@ + + + + Code Signing Not Available + + Your Windows agents are not digitally signed. This may cause issues with unattended access, UAC dialogs, + or applications running with administrator privileges. These limitations are enforced by Microsoft as a + security measure when code signing is missing. + + + + + If you experience such issues, consider upgrading to one of our paid plans, which include code signing. + + + + + + + Available from Tier 1 for professional deployments + + + + Code signed Windows agents + No UAC warnings + Trusted by Windows + Full administrator privileges + + + + Continue Anyway + + View Pricing + + + + +@code { + [CascadingParameter] IMudDialogInstance MudDialog { get; set; } = null!; + + private void Cancel() + { + MudDialog.Close(DialogResult.Ok(false)); + } + + private void ViewPricing() + { + MudDialog.Close(DialogResult.Ok(true)); + } +} + diff --git a/NetLock-RMM-Web-Console/Components/Pages/Devices/Dialogs/Remote_Control/Remote_Control_Dialog.razor b/NetLock-RMM-Web-Console/Components/Pages/Devices/Dialogs/Remote_Control/Remote_Control_Dialog.razor index f7f7d8da..3b3a6d9f 100644 --- a/NetLock-RMM-Web-Console/Components/Pages/Devices/Dialogs/Remote_Control/Remote_Control_Dialog.razor +++ b/NetLock-RMM-Web-Console/Components/Pages/Devices/Dialogs/Remote_Control/Remote_Control_Dialog.razor @@ -27,6 +27,83 @@ @implements IDisposable @@ -174,57 +360,76 @@ @if (!connected) { - - The device is not connected to the NetLock RMM backend. Waiting for it to come back. - +
+ + The device is not connected to the NetLock RMM backend. Waiting for it to come back. + +
+ } + else if (waitingForRemoteProcess) + { +
+ + Device is online. Waiting for remote control process to start... + + + Retry +
} else if (!users.Any()) { - - Waiting for device. - +
+ + Waiting for device. + +
} else if (String.IsNullOrEmpty(user)) { - No user selected. Please select a user. +
+ No user selected. Please select a user. - + - @{ - foreach (string user in users) - { - @user + @{ + foreach (string user in users) + { + @user + } } - } - - - Fetch user sessions + + + Fetch user sessions +
} else if (connected && !rendered && !connectionAllowed) { - - Screen access not granted. Awaiting system or user approval. - - - +
+ + Screen access not granted. Awaiting system or user approval. + + + - @{ - foreach (string user in users) - { - @user + @{ + foreach (string user in users) + { + @user + } } - } - - - Fetch user sessions - Request screen access from user (again) + + + Fetch user sessions + Request screen access from user (again) +
} else if (connected && !rendered && connectionAllowed) { - - No screen output from user. Attempting auto detect in the background. Please wait. +
+ + No screen output from user. Attempting auto detect in the background. Please wait. Fetch users @@ -239,11 +444,12 @@ } +
} else { -
+ await InvokeAsync(async () => { try { + // Update last response time + lastResponseTime = DateTime.Now; + if (command == "Remote device is not connected with the NetLock RMM backend. Make sure your target device is connected.") { connected = false; rendered = false; - Remote_Check_Connection(); + + // Start connection check timer if not already running + if (!connection_check_timer_running) + { + await Connection_Check_Timer_Setup(); + } } else if (command == "Remote device is connected with the NetLock RMM backend.") { connected = true; - Reconnect(); + + // Stop connection check timer + if (connection_check_timer_running) + { + await Connection_Check_Timer_Stop(); + } + + // Get users to continue + await Remote_Get_Users(); } StateHasChanged(); @@ -739,12 +966,33 @@ { try { - if (connected && command == "Remote device is not connected with the NetLock RMM backend. Make sure your target device is connected.") + if (command == "Remote device is not connected with the NetLock RMM backend. Make sure your target device is connected.") { - connected = false; - rendered = false; - StateHasChanged(); - await Remote_Check_Connection(); + if (connected) + { + // Device went offline - reset dialog to initial state + connected = false; + rendered = false; + waitingForRemoteProcess = false; + connectionAllowed = false; + user = String.Empty; + users.Clear(); + previous_users.Clear(); + screens.Clear(); + screen_index = "0"; + + // Stop screen refresh timer + if (refresh_screen_timer_running) + { + await Remote_Screen_Refresh_Timer_Stop(); + } + + // Start connection check timer if not already running + if (!connection_check_timer_running) + { + await Connection_Check_Timer_Setup(); + } + } } StateHasChanged(); @@ -765,6 +1013,8 @@ { try { + // Update last response time + lastResponseTime = DateTime.Now; // Debug: Check first bytes if (Web_Console.loggingEnabled && imageBytes.Length > 20) { @@ -818,7 +1068,7 @@ await JSRuntime.InvokeVoidAsync("updateImageBlob", actualImageBytes); // Check if recording is enabled - //OSSCH_START ab4b401f-42ce-41c2-8163-3205d5db7a05 //OSSCH_END + //OSSCH_START 53b19447-c6af-4c11-8cd7-ceb2e8e7a542 //OSSCH_END // Prüfung auf leeres Bild if (actualImageBytes.Length < 1000) // Anpassen je nach minimaler Bildgrâße @@ -855,6 +1105,7 @@ { connected = true; rendered = true; + waitingForRemoteProcess = false; remoteScreenFetchTries = 0; } @@ -873,41 +1124,131 @@ Logging.Handler.Debug("ReceiveClientResponseRemoteControlUsers", "command", command); // Use InvokeAsync to reflect changes on UI immediately - await InvokeAsync(() => + await InvokeAsync(async () => { try { - users.Clear(); - users.AddRange(command.Split(',').Select(user => user)); - - if (users.Count() == 0) + // Update last response time + lastResponseTime = DateTime.Now; + + // Check if remote process is not running + if (command == "Command executed. No result returned.") { + // Device is online but remote process is not running - reset dialog to initial state + connected = true; + rendered = false; + waitingForRemoteProcess = true; + connectionAllowed = false; user = String.Empty; + users.Clear(); + previous_users.Clear(); + screens.Clear(); + screen_index = "0"; + + // Stop all timers + if (connection_check_timer_running) + { + await Connection_Check_Timer_Stop(); + } + + if (refresh_screen_timer_running) + { + await Remote_Screen_Refresh_Timer_Stop(); + } + + // Start fetch users timer to keep retrying + if (!fetch_users_timer_running) + { + await Fetch_Users_Timer_Setup(); + } + + StateHasChanged(); + return; } - else if (users.Count() == 1) + + // Save previous users + previous_users = new List(users); + + users.Clear(); + users.AddRange(command.Split(',').Select(user => user)); + + // Check for new users - but don't show dialog if we were waiting for remote process + var newUsers = users.Except(previous_users).ToList(); + + if (newUsers.Any() && previous_users.Any() && !waitingForRemoteProcess) { - user = users[0]; // no other users logged in, switch to session 0 + // New user detected - show popup (only if not waiting for remote process) + foreach (var newUser in newUsers) + { + var result = await DialogService.ShowMessageBox( + "New user detected", + $"A new user '{newUser}' has been detected. Do you want to switch to this user's session?", + yesText: "Yes, switch", + cancelText: "No" + ); + + if (result == true) + { + // Switch user and reset connection state + user = newUser; + connectionAllowed = false; + rendered = false; + screen_index = "0"; + + // Get screen indexes for the new user + await Remote_Get_Screen_Indexes(); + + // Request access for the new user session + await Remote_Request_Access(); + } + } } - else if (users.Count() > 1) + else if (users.Count() > 0 && String.IsNullOrEmpty(user)) { - user = users[1]; // other users are present. Switch to first user session + // Initial user selection - set first user + if (users.Count() == 1) + { + user = users[0]; // no other users logged in, switch to session 0 + } + else if (users.Count() > 1) + { + user = users[1]; // other users are present. Switch to first user session + } } if (users.Count() > 0) { connected = true; - Remote_Request_Access(); + waitingForRemoteProcess = false; + + if (String.IsNullOrEmpty(user)) + { + // No user selected yet - will be handled by UI + } + else if (!previous_users.Any()) + { + // First time getting users after reset - request access and start screen sharing + await Remote_Request_Access(); + + // Start screen refresh timer + if (!refresh_screen_timer_running) + { + await Remote_Screen_Refresh_Timer_Setup(); + } + } + + // Start fetch users timer if not already running + if (!fetch_users_timer_running) + { + await Fetch_Users_Timer_Setup(); + } } else { connected = false; } - - // if more users are available do not select the device name StateHasChanged(); - - //Remote_Get_Screen_Indexes(); } catch (Exception ex) { @@ -926,6 +1267,9 @@ { try { + // Update last response time + lastResponseTime = DateTime.Now; + screens.Clear(); screens = new List(); @@ -953,6 +1297,8 @@ { try { + // Update last response time + lastResponseTime = DateTime.Now; // Set the clipboard content if (!String.IsNullOrEmpty(command) && command.Contains("clipboard_content%")) @@ -989,6 +1335,8 @@ { try { + // Update last response time + lastResponseTime = DateTime.Now; string name = String.Empty; string message = string.Empty; @@ -1029,7 +1377,7 @@ // Use InvokeAsync to reflect changes on UI immediately - await InvokeAsync(() => + await InvokeAsync(async () => { try { @@ -1037,6 +1385,9 @@ if (command == "accepted") { connectionAllowed = true; + + // Immediately trigger a screen refresh to start getting images + await Remote_Refresh_Screen(); } else if (command == "declined") { @@ -1102,6 +1453,12 @@ //await Remote_Refresh_Screen(); await Remote_Screen_Refresh_Timer_Setup(); + + // Start connection check timer to monitor device status + if (!connection_check_timer_running) + { + await Connection_Check_Timer_Setup(); + } } #endregion @@ -1112,15 +1469,29 @@ private bool refresh_screen_timer_running = false; private bool rendered = false; private bool connectionAllowed = false; + private bool waitingForRemoteProcess = false; private string screen_index = "0"; private List screens = new List(); private string user = String.Empty; private List users = new List(); + private List previous_users = new List(); // Loop to refresh the screen private System.Threading.Timer remote_screen_refresh_timer; private int remote_screen_refresh_ms = 400; private int _quality = 0; + + // User fetch timer + private System.Threading.Timer fetch_users_timer; + private bool fetch_users_timer_running = false; + + // Connection check timer + private System.Threading.Timer connection_check_timer; + private bool connection_check_timer_running = false; + + // Track last response to detect offline devices + private DateTime lastResponseTime = DateTime.Now; + private const int ResponseTimeoutSeconds = 15; // Consider offline after 15 seconds of no response private async Task Remote_Screen_Refresh_Timer_Setup() { @@ -1154,6 +1525,66 @@ StateHasChanged(); } + + private async Task Fetch_Users_Timer_Setup() + { + fetch_users_timer_running = true; + + fetch_users_timer = new System.Threading.Timer(async (e) => + { + await Remote_Get_Users(); + }, null, 0, 15000); // 15 Sekunden + } + + private async Task Fetch_Users_Timer_Stop() + { + fetch_users_timer_running = false; + + if (fetch_users_timer != null) + { + fetch_users_timer.Dispose(); + fetch_users_timer = null; + } + } + + private async Task Connection_Check_Timer_Setup() + { + connection_check_timer_running = true; + + connection_check_timer = new System.Threading.Timer(async (e) => + { + // Check if we haven't received a response in a while + if ((DateTime.Now - lastResponseTime).TotalSeconds > ResponseTimeoutSeconds) + { + await InvokeAsync(async () => + { + if (connected) + { + connected = false; + rendered = false; + StateHasChanged(); + } + }); + } + + // Always check connection when offline + if (!connected) + { + await Remote_Check_Connection(); + } + }, null, 0, 30000); // Check every 30 seconds + } + + private async Task Connection_Check_Timer_Stop() + { + connection_check_timer_running = false; + + if (connection_check_timer != null) + { + connection_check_timer.Dispose(); + connection_check_timer = null; + } + } private async Task Remote_Check_Connection() { @@ -1364,6 +1795,20 @@ } } + private async Task Handle_User_Change() + { + // Reset connection state when user changes + connectionAllowed = false; + rendered = false; + screen_index = "0"; + + // Get screen indexes for the new user + await Remote_Get_Screen_Indexes(); + + // Request access for the new user session + await Remote_Request_Access(); + } + private async Task Remote_Get_Screen_Indexes() { @@ -2668,6 +3113,26 @@ #endregion + private async Task ShowCodeSigningWarning() + { + var options = new DialogOptions + { + CloseButton = true, + FullWidth = true, + MaxWidth = MaxWidth.Medium, + BackgroundClass = "dialog-blurring", + }; + + var dialog = await DialogService.ShowAsync("Code Signing Warning", options); + var result = await dialog.Result; + + if (!result.Canceled && result.Data is bool viewPricing && viewPricing) + { + // User clicked "View Pricing" button + NavigationManager.NavigateTo("https://netlockrmm.com/pricing.html", true); + } + } + public void Dispose() { try @@ -2678,6 +3143,12 @@ remote_screen_refresh_timer?.Dispose(); refresh_screen_timer_running = false; + + fetch_users_timer?.Dispose(); + fetch_users_timer_running = false; + + connection_check_timer?.Dispose(); + connection_check_timer_running = false; if (remote_server_client is not null) { @@ -2709,6 +3180,7 @@ public async Task DisposeOnUnload() { await Remote_Screen_Refresh_Timer_Stop(); + await Fetch_Users_Timer_Stop(); await Remote_End_Remote_Session(); if (remote_server_client is not null) diff --git a/NetLock-RMM-Web-Console/Components/Pages/Devices/Dialogs/Remote_EventLog/EventLogEntry.cs b/NetLock-RMM-Web-Console/Components/Pages/Devices/Dialogs/Remote_EventLog/EventLogEntry.cs new file mode 100644 index 00000000..0c5f6d45 --- /dev/null +++ b/NetLock-RMM-Web-Console/Components/Pages/Devices/Dialogs/Remote_EventLog/EventLogEntry.cs @@ -0,0 +1,96 @@ +using System.Text.Json.Serialization; + +namespace NetLock_RMM_Web_Console.Components.Pages.Devices.Dialogs.Remote_EventLog +{ + public class EventLogEntry + { + [JsonPropertyName("index")] + public long Index { get; set; } + + [JsonPropertyName("time_created")] + public string TimeCreated { get; set; } + + [JsonPropertyName("event_id")] + public int? EventId { get; set; } + + [JsonPropertyName("level")] + public string Level { get; set; } + + [JsonPropertyName("level_value")] + public byte? LevelValue { get; set; } + + [JsonPropertyName("provider_name")] + public string ProviderName { get; set; } + + [JsonPropertyName("source")] + public string Source { get; set; } + + [JsonPropertyName("computer")] + public string Computer { get; set; } + + [JsonPropertyName("user")] + public string User { get; set; } + + [JsonPropertyName("task_category")] + public string TaskCategory { get; set; } + + [JsonPropertyName("task_display_name")] + public string TaskDisplayName { get; set; } + + [JsonPropertyName("opcode")] + public string Opcode { get; set; } + + [JsonPropertyName("opcode_display_name")] + public string OpcodeDisplayName { get; set; } + + [JsonPropertyName("keywords")] + public List Keywords { get; set; } + + [JsonPropertyName("keywords_display_names")] + public List KeywordsDisplayNames { get; set; } + + [JsonPropertyName("message")] + public string Message { get; set; } + + [JsonPropertyName("process_id")] + public int? ProcessId { get; set; } + + [JsonPropertyName("thread_id")] + public int? ThreadId { get; set; } + + [JsonPropertyName("activity_id")] + public string ActivityId { get; set; } + + [JsonPropertyName("related_activity_id")] + public string RelatedActivityId { get; set; } + + [JsonPropertyName("record_id")] + public long? RecordId { get; set; } + + [JsonPropertyName("log_name")] + public string LogName { get; set; } + + /// + /// Get formatted time created string + /// + public string GetFormattedTimeCreated() + { + if (string.IsNullOrEmpty(TimeCreated)) + return "N/A"; + + try + { + if (DateTime.TryParse(TimeCreated, out DateTime dt)) + { + return dt.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss"); + } + return TimeCreated; + } + catch + { + return TimeCreated; + } + } + } +} + diff --git a/NetLock-RMM-Web-Console/Components/Pages/Devices/Dialogs/Remote_EventLog/EventLogStatsResponse.cs b/NetLock-RMM-Web-Console/Components/Pages/Devices/Dialogs/Remote_EventLog/EventLogStatsResponse.cs new file mode 100644 index 00000000..bdc8b386 --- /dev/null +++ b/NetLock-RMM-Web-Console/Components/Pages/Devices/Dialogs/Remote_EventLog/EventLogStatsResponse.cs @@ -0,0 +1,47 @@ +using System.Text.Json.Serialization; + +namespace NetLock_RMM_Web_Console.Components.Pages.Devices.Dialogs.Remote_EventLog +{ + public class EventLogStatsResponse + { + [JsonPropertyName("success")] + public bool Success { get; set; } + + [JsonPropertyName("log_name")] + public string LogName { get; set; } + + [JsonPropertyName("is_enabled")] + public bool IsEnabled { get; set; } + + [JsonPropertyName("log_type")] + public string LogType { get; set; } + + [JsonPropertyName("log_mode")] + public string LogMode { get; set; } + + [JsonPropertyName("maximum_size_bytes")] + public long MaximumSizeBytes { get; set; } + + [JsonPropertyName("log_file_path")] + public string LogFilePath { get; set; } + + [JsonPropertyName("total_entries")] + public long TotalEntries { get; set; } + + [JsonPropertyName("oldest_entry")] + public string OldestEntry { get; set; } + + [JsonPropertyName("newest_entry")] + public string NewestEntry { get; set; } + + [JsonPropertyName("level_counts")] + public Dictionary LevelCounts { get; set; } + + [JsonPropertyName("error")] + public string Error { get; set; } + + [JsonPropertyName("timestamp")] + public string Timestamp { get; set; } + } +} + diff --git a/NetLock-RMM-Web-Console/Components/Pages/Devices/Dialogs/Remote_EventLog/EventLog_Details_Dialog.razor b/NetLock-RMM-Web-Console/Components/Pages/Devices/Dialogs/Remote_EventLog/EventLog_Details_Dialog.razor new file mode 100644 index 00000000..155ebf5c --- /dev/null +++ b/NetLock-RMM-Web-Console/Components/Pages/Devices/Dialogs/Remote_EventLog/EventLog_Details_Dialog.razor @@ -0,0 +1,156 @@ +@using System.Text.Json +@using System.Text.Json.Serialization + + + + Event Details - Event ID: @EventEntry.EventId + + + + + Record ID: + @EventEntry.RecordId + + + + Time Created: + @EventEntry.GetFormattedTimeCreated() + + + + Level: + @EventEntry.Level (@EventEntry.LevelValue) + + + + Event ID: + @EventEntry.EventId + + + + Log Name: + @EventEntry.LogName + + + + Source: + @EventEntry.Source + + + + Provider Name: + @EventEntry.ProviderName + + + + Computer: + @EventEntry.Computer + + + + User: + @EventEntry.User + + + + Task Category: + @EventEntry.TaskCategory + + + + Task Display Name: + @EventEntry.TaskDisplayName + + + + Opcode: + @EventEntry.Opcode + + + + Opcode Display Name: + @EventEntry.OpcodeDisplayName + + + + Process ID: + @EventEntry.ProcessId + + + + Thread ID: + @EventEntry.ThreadId + + + @if (!string.IsNullOrEmpty(EventEntry.ActivityId)) + { + + Activity ID: + @EventEntry.ActivityId + + } + + @if (!string.IsNullOrEmpty(EventEntry.RelatedActivityId)) + { + + Related Activity ID: + @EventEntry.RelatedActivityId + + } + + @if (EventEntry.Keywords != null && EventEntry.Keywords.Count > 0) + { + + Keywords: + @string.Join(", ", EventEntry.Keywords) + + } + + @if (EventEntry.KeywordsDisplayNames != null && EventEntry.KeywordsDisplayNames.Count > 0) + { + + Keywords Display Names: + @string.Join(", ", EventEntry.KeywordsDisplayNames) + + } + + + + Message: + + @EventEntry.Message + + + + + + Close + + + +@code { + [CascadingParameter] IMudDialogInstance MudDialog { get; set; } + [Parameter] public EventLogEntry EventEntry { get; set; } + + + private Color GetLevelColor(byte? level) + { + if (!level.HasValue) return Color.Default; + + return level.Value switch + { + 1 => Color.Error, // Critical + 2 => Color.Error, // Error + 3 => Color.Warning, // Warning + 4 => Color.Info, // Information + 5 => Color.Default, // Verbose + _ => Color.Default + }; + } + + private void Close() + { + MudDialog.Close(DialogResult.Ok(true)); + } +} + diff --git a/NetLock-RMM-Web-Console/Components/Pages/Devices/Dialogs/Remote_EventLog/EventLog_Stats_Dialog.razor b/NetLock-RMM-Web-Console/Components/Pages/Devices/Dialogs/Remote_EventLog/EventLog_Stats_Dialog.razor new file mode 100644 index 00000000..662769da --- /dev/null +++ b/NetLock-RMM-Web-Console/Components/Pages/Devices/Dialogs/Remote_EventLog/EventLog_Stats_Dialog.razor @@ -0,0 +1,189 @@ +@using System.Text.Json.Serialization + + + + Event Log Statistics - @Stats.LogName + + + + + Status: + + @(Stats.IsEnabled ? "Enabled" : "Disabled") + + + + + Total Entries: + @Stats.TotalEntries.ToString("N0") + + + + Log Type: + @Stats.LogType + + + + Log Mode: + @Stats.LogMode + + + + Maximum Size: + @FormatBytes(Stats.MaximumSizeBytes) + + + + Log File Path: + @Stats.LogFilePath + + + + Oldest Entry: + @FormatDateTime(Stats.OldestEntry) + + + + Newest Entry: + @FormatDateTime(Stats.NewestEntry) + + + @if (Stats.LevelCounts != null && Stats.LevelCounts.Count > 0) + { + + + Event Levels Distribution + + + + + + @foreach (var level in Stats.LevelCounts.OrderByDescending(x => x.Value)) + { + + @level.Key + @level.Value.ToString("N0") + + } + + + + + + + + } + + + + Close + + + +@code { + [CascadingParameter] IMudDialogInstance MudDialog { get; set; } + [Parameter] public EventLogStatsResponse Stats { get; set; } + + + private string FormatBytes(long bytes) + { + string[] sizes = { "B", "KB", "MB", "GB", "TB" }; + double len = bytes; + int order = 0; + while (len >= 1024 && order < sizes.Length - 1) + { + order++; + len = len / 1024; + } + return $"{len:0.##} {sizes[order]}"; + } + + private string FormatDateTime(string dateTimeString) + { + if (string.IsNullOrEmpty(dateTimeString)) + return "N/A"; + + try + { + if (DateTime.TryParse(dateTimeString, out DateTime dt)) + { + return dt.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss"); + } + return dateTimeString; + } + catch + { + return dateTimeString; + } + } + + private double[] GetChartData() + { + if (Stats.LevelCounts == null || Stats.LevelCounts.Count == 0) + return Array.Empty(); + + return Stats.LevelCounts.Values.Select(v => (double)v).ToArray(); + } + + private string[] GetChartLabels() + { + if (Stats.LevelCounts == null || Stats.LevelCounts.Count == 0) + return Array.Empty(); + + return Stats.LevelCounts.Keys.ToArray(); + } + + private ChartOptions GetChartOptions() + { + var colors = new List(); + + if (Stats.LevelCounts != null && Stats.LevelCounts.Count > 0) + { + foreach (var levelName in Stats.LevelCounts.Keys) + { + colors.Add(GetColorHex(levelName)); + } + } + + return new ChartOptions + { + ChartPalette = colors.ToArray() + }; + } + + private string GetColorHex(string levelName) + { + return levelName switch + { + "Critical" => "#f44336", // Red - Error color + "Error" => "#ff9800", // Orange - Error/Warning mix + "Warning" => "#ff9800", // Orange - Warning color + "Information" => "#2196f3", // Blue - Info color + "Verbose" => "#9e9e9e", // Grey - Default color + _ => "#9e9e9e" // Grey - Default + }; + } + + private Color GetLevelColor(string levelName) + { + return levelName switch + { + "Critical" => Color.Error, + "Error" => Color.Error, + "Warning" => Color.Warning, + "Information" => Color.Info, + "Verbose" => Color.Default, + _ => Color.Default + }; + } + + private void Close() + { + MudDialog.Close(DialogResult.Ok(true)); + } +} diff --git a/NetLock-RMM-Web-Console/Components/Pages/Devices/Dialogs/Remote_EventLog/Remote_EventLog_Dialog.razor b/NetLock-RMM-Web-Console/Components/Pages/Devices/Dialogs/Remote_EventLog/Remote_EventLog_Dialog.razor new file mode 100644 index 00000000..e4f0c03c --- /dev/null +++ b/NetLock-RMM-Web-Console/Components/Pages/Devices/Dialogs/Remote_EventLog/Remote_EventLog_Dialog.razor @@ -0,0 +1,793 @@ +@using MySqlConnector +@using System.Data.Common +@using System.Text.Json +@using System.Xml.Serialization +@using System.Text +@using System.Text.RegularExpressions +@using System.Text.Json.Nodes +@using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage +@using Microsoft.AspNetCore.DataProtection +@using System.Globalization +@using Microsoft.AspNetCore.SignalR.Client +@using System.Net.Http +@using System.Security.Claims +@using System.Text.Json.Serialization + +@inject NavigationManager NavigationManager +@inject ILocalStorageService localStorage +@inject IDialogService DialogService +@inject ISnackbar Snackbar +@inject IJSRuntime JSRuntime +@inject IDataProtectionProvider DataProtectionProvider +@inject AuthenticationStateProvider AuthenticationStateProvider + + + + Event Log Viewer (@device_name) + + + + @{ + if (!remote_server_client_setup) + { + Not connected to the NetLock RMM server backend. + Reconnect + } + } + + + + + + + + + + + + + All Levels + Critical + Error + Warning + Information + Verbose + + + + + + + + + + + + + + + + + + + + + @{ + if (loading_overlay) + { + + Loading... + + } + } + + @if (event_log_entries != null && event_log_entries.Count > 0) + { + Results (@event_log_entries.Count) + + + + + + + Time Created + Level + Event ID + Source + Message + + + @context.GetFormattedTimeCreated() + + @context.Level + + @context.EventId + @context.Source + @(context.Message?.Length > 100 ? context.Message.Substring(0, 100) + "..." : context.Message) + + + + + + } + else if (has_searched && (event_log_entries == null || event_log_entries.Count == 0)) + { + + No event log entries found. Try adjusting your search criteria or select a different log. + + } + else if (!string.IsNullOrEmpty(last_error_message)) + { + + @last_error_message + + } + + + + Stats + Clear Log + + Search + Close + + + +@code { + [CascadingParameter] IMudDialogInstance MudDialog { get; set; } + [Parameter] public string device_id { get; set; } + [Parameter] public string device_name { get; set; } + [Parameter] public string tenant_guid { get; set; } + [Parameter] public string location_guid { get; set; } + + private string netlock_username = String.Empty; + private string token = String.Empty; + private bool loading_overlay = false; + private string search_string = string.Empty; + private EventLogEntry selected_event_entry; + private bool has_searched = false; + private string last_error_message = string.Empty; + + // EventLog parameters + private List available_logs = new List(); + private string selected_log_name = "Application"; + private int max_entries = 100; + private byte? selected_level = null; + private int? event_id = null; + private DateTime? start_time = null; + private DateTime? end_time = null; + private string provider_name = string.Empty; + + private List event_log_entries = new List(); + private EventLogStatsResponse current_log_stats = null; + + protected override async Task OnInitializedAsync() + { + // Get the current user from the authentication state + var user = (await AuthenticationStateProvider.GetAuthenticationStateAsync()).User; + + // Check if user is authenticated + if (user?.Identity is not { IsAuthenticated: true }) + { + NavigationManager.NavigateTo("/login", true); + return; + } + + // Retrieve username from claims + netlock_username = user.FindFirst(ClaimTypes.Email)?.Value; + + token = await Classes.Authentication.User.Get_Remote_Session_Token(netlock_username); + + await Remote_Setup_SignalR(); + await Load_Available_Logs(); + } + + #region Remote Communication Classes + + public class Remote_Admin_Identity + { + public string token { get; set; } + } + + public class Remote_Target_Device + { + public string device_id { get; set; } + public string device_name { get; set; } + public string location_guid { get; set; } + public string tenant_guid { get; set; } + } + + public class Remote_Command + { + public int type { get; set; } + public bool wait_response { get; set; } + public string powershell_code { get; set; } + public int file_browser_command { get; set; } + public string file_browser_path { get; set; } + public string file_browser_path_move { get; set; } + public string file_browser_file_content { get; set; } + public string command { get; set; } + } + + public class Remote_Root_Object + { + public Remote_Admin_Identity admin_identity { get; set; } + public Remote_Target_Device target_device { get; set; } + public Remote_Command command { get; set; } + } + + #endregion + + #region EventLog Models + + + public class EventLogResponse + { + [JsonPropertyName("success")] + public bool Success { get; set; } + + [JsonPropertyName("log_name")] + public string LogName { get; set; } + + [JsonPropertyName("total_entries")] + public int TotalEntries { get; set; } + + [JsonPropertyName("entries")] + public List Entries { get; set; } + + [JsonPropertyName("error")] + public string Error { get; set; } + + [JsonPropertyName("timestamp")] + public string Timestamp { get; set; } + } + + public class AvailableLogsResponse + { + [JsonPropertyName("success")] + public bool Success { get; set; } + + [JsonPropertyName("total_logs")] + public int TotalLogs { get; set; } + + [JsonPropertyName("log_names")] + public List LogNames { get; set; } + + [JsonPropertyName("error")] + public string Error { get; set; } + + [JsonPropertyName("timestamp")] + public string Timestamp { get; set; } + } + + + public class ClearEventLogResponse + { + [JsonPropertyName("success")] + public bool Success { get; set; } + + [JsonPropertyName("log_name")] + public string LogName { get; set; } + + [JsonPropertyName("message")] + public string Message { get; set; } + + [JsonPropertyName("error")] + public string Error { get; set; } + + [JsonPropertyName("timestamp")] + public string Timestamp { get; set; } + } + + #endregion + + #region Remote Server Setup + + private HubConnection remote_server_client; + private bool remote_server_client_setup = false; + private string remote_admin_identity = String.Empty; + + public async Task Remote_Setup_SignalR() + { + this.Snackbar.Configuration.ShowCloseIcon = true; + this.Snackbar.Configuration.PositionClass = Defaults.Classes.Position.BottomRight; + + try + { + Remote_Admin_Identity identity = new Remote_Admin_Identity + { + token = token + }; + + var jsonObject = new { admin_identity = identity }; + string json = JsonSerializer.Serialize(jsonObject, new JsonSerializerOptions { WriteIndented = true }); + remote_admin_identity = json; + + remote_server_client = new HubConnectionBuilder() + .WithUrl(Configuration.Remote_Server.Connection_String + "/commandHub", options => + { + options.Headers.Add("Admin-Identity", Uri.EscapeDataString(remote_admin_identity)); + }) + .Build(); + + // Remote Shell response handler for EventLog + remote_server_client.On("ReceiveClientResponseRemoteEventlogViewer", async (command) => + { + Logging.Handler.Debug("Remote_EventLog_Dialog -> Remote_Setup_SignalR", "ReceiveClientResponseRemoteShell", command); + + await InvokeAsync(() => + { + try + { + Console.WriteLine("Received command: " + command); + string jsonResponse = command; + + // Try to parse as EventLogResponse + if (jsonResponse.Contains("\"entries\"")) + { + var response = JsonSerializer.Deserialize(jsonResponse); + if (response != null && response.Success) + { + event_log_entries = response.Entries ?? new List(); + last_error_message = string.Empty; + } + else + { + last_error_message = response?.Error ?? "Error loading entries"; + event_log_entries = new List(); + } + has_searched = true; + } + // Try to parse as EventLogStatsResponse + else if (jsonResponse.Contains("\"level_counts\"")) + { + var response = JsonSerializer.Deserialize(jsonResponse); + if (response != null && response.Success) + { + current_log_stats = response; + InvokeAsync(async () => await ShowStatsDialog(response)); + } + else + { + last_error_message = response?.Error ?? "Error loading stats"; + } + } + // Try to parse as ClearEventLogResponse + else if (jsonResponse.Contains("\"message\"") && jsonResponse.Contains("cleared")) + { + var response = JsonSerializer.Deserialize(jsonResponse); + if (response != null && response.Success) + { + Snackbar.Add(response.Message ?? "Event log cleared successfully", Severity.Success); + // Refresh the entries after clearing + event_log_entries.Clear(); + has_searched = false; + } + else + { + last_error_message = response?.Error ?? "Error clearing log"; + } + } + // Try to parse as AvailableLogsResponse + else if (jsonResponse.Contains("\"log_names\"")) + { + var response = JsonSerializer.Deserialize(jsonResponse); + if (response != null && response.Success) + { + available_logs = response.LogNames ?? new List(); + if (available_logs.Count > 0 && string.IsNullOrEmpty(selected_log_name)) + { + selected_log_name = available_logs.FirstOrDefault(l => l == "Application") ?? available_logs[0]; + } + } + else + { + last_error_message = response?.Error ?? "Error loading logs"; + } + + available_logs.Add("Application"); + available_logs.Add("System"); + available_logs.Add("Security"); + available_logs = available_logs.Distinct().ToList(); + } + } + catch (Exception ex) + { + Logging.Handler.Error("Remote_EventLog_Dialog", "Response parsing", ex.ToString()); + last_error_message = "Error parsing response: " + ex.Message; + } + finally + { + loading_overlay = false; + StateHasChanged(); + } + }); + }); + + await remote_server_client.StartAsync(); + remote_server_client_setup = true; + + Logging.Handler.Debug("Remote_EventLog_Dialog -> Remote_Setup_SignalR", "Connected to the remote server.", remote_server_client_setup.ToString()); + } + catch (Exception ex) + { + Logging.Handler.Error("Remote_EventLog_Dialog -> Remote_Setup_SignalR", "General error", ex.ToString()); + Snackbar.Add("Error connecting to remote server", Severity.Error); + } + } + + #endregion + + #region EventLog Operations + + private async Task Load_Available_Logs() + { + try + { + loading_overlay = true; + StateHasChanged(); + + var adminIdentity = new Remote_Admin_Identity { token = token }; + var targetDevice = new Remote_Target_Device + { + device_id = device_id, + device_name = device_name, + tenant_guid = tenant_guid, + location_guid = location_guid + }; + + var command = new Remote_Command + { + type = 10, + wait_response = true, + command = "1" // Command "1" for getting available logs + }; + + var rootObject = new Remote_Root_Object + { + admin_identity = adminIdentity, + target_device = targetDevice, + command = command + }; + + string json = JsonSerializer.Serialize(rootObject, new JsonSerializerOptions { WriteIndented = true }); + + if (remote_server_client_setup) + { + await remote_server_client.SendAsync("MessageReceivedFromWebconsole", json); + } + } + catch (Exception ex) + { + Logging.Handler.Error("Remote_EventLog_Dialog", "Load_Available_Logs", ex.ToString()); + last_error_message = "Error loading logs: " + ex.Message; + } + finally + { + loading_overlay = false; + StateHasChanged(); + } + } + + private async Task Load_EventLog_Entries() + { + try + { + if (string.IsNullOrEmpty(selected_log_name)) + { + last_error_message = "Please select a log name"; + return; + } + + loading_overlay = true; + has_searched = false; + last_error_message = string.Empty; + StateHasChanged(); + + // Build JSON command with all parameters + var eventLogParameters = new + { + command = 2, + log_name = selected_log_name, + max_entries = max_entries, + level = selected_level, + event_id = event_id, + start_time = start_time?.ToString("yyyy-MM-dd HH:mm:ss"), + end_time = end_time?.ToString("yyyy-MM-dd HH:mm:ss"), + provider_name = string.IsNullOrWhiteSpace(provider_name) ? null : provider_name + }; + + string commandJson = JsonSerializer.Serialize(eventLogParameters); + + var adminIdentity = new Remote_Admin_Identity { token = token }; + var targetDevice = new Remote_Target_Device + { + device_id = device_id, + device_name = device_name, + tenant_guid = tenant_guid, + location_guid = location_guid + }; + + var command = new Remote_Command + { + type = 11, + wait_response = true, + command = commandJson + }; + + var rootObject = new Remote_Root_Object + { + admin_identity = adminIdentity, + target_device = targetDevice, + command = command + }; + + string json = JsonSerializer.Serialize(rootObject, new JsonSerializerOptions { WriteIndented = true }); + + if (remote_server_client_setup) + { + await remote_server_client.SendAsync("MessageReceivedFromWebconsole", json); + } + } + catch (Exception ex) + { + Logging.Handler.Error("Remote_EventLog_Dialog", "Load_EventLog_Entries", ex.ToString()); + last_error_message = "Error loading entries: " + ex.Message; + has_searched = true; + loading_overlay = false; + StateHasChanged(); + } + } + + private async Task Load_EventLog_Stats() + { + try + { + if (string.IsNullOrEmpty(selected_log_name)) + { + last_error_message = "Please select a log name"; + return; + } + + loading_overlay = true; + last_error_message = string.Empty; + StateHasChanged(); + + var adminIdentity = new Remote_Admin_Identity { token = token }; + var targetDevice = new Remote_Target_Device + { + device_id = device_id, + device_name = device_name, + tenant_guid = tenant_guid, + location_guid = location_guid + }; + + var command = new Remote_Command + { + type = 12, + wait_response = true, + command = selected_log_name + }; + + var rootObject = new Remote_Root_Object + { + admin_identity = adminIdentity, + target_device = targetDevice, + command = command + }; + + string json = JsonSerializer.Serialize(rootObject, new JsonSerializerOptions { WriteIndented = true }); + + if (remote_server_client_setup) + { + await remote_server_client.SendAsync("MessageReceivedFromWebconsole", json); + } + } + catch (Exception ex) + { + Logging.Handler.Error("Remote_EventLog_Dialog", "Load_EventLog_Stats", ex.ToString()); + last_error_message = "Error loading stats: " + ex.Message; + } + finally + { + loading_overlay = false; + StateHasChanged(); + } + } + + private async Task Clear_EventLog() + { + try + { + if (string.IsNullOrEmpty(selected_log_name)) + { + last_error_message = "Please select a log name"; + return; + } + + bool? confirmed = await DialogService.ShowMessageBox( + "Clear Event Log", + $"Are you sure you want to clear the '{selected_log_name}' event log? This action cannot be undone!", + yesText: "Clear", cancelText: "Cancel", + options: new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small }); + + if (confirmed != true) + return; + + loading_overlay = true; + last_error_message = string.Empty; + StateHasChanged(); + + var adminIdentity = new Remote_Admin_Identity { token = token }; + var targetDevice = new Remote_Target_Device + { + device_id = device_id, + device_name = device_name, + tenant_guid = tenant_guid, + location_guid = location_guid + }; + + var command = new Remote_Command + { + type = 13, + wait_response = true, + command = selected_log_name + }; + + var rootObject = new Remote_Root_Object + { + admin_identity = adminIdentity, + target_device = targetDevice, + command = command + }; + + string json = JsonSerializer.Serialize(rootObject, new JsonSerializerOptions { WriteIndented = true }); + + if (remote_server_client_setup) + { + await remote_server_client.SendAsync("MessageReceivedFromWebconsole", json); + } + } + catch (Exception ex) + { + Logging.Handler.Error("Remote_EventLog_Dialog", "Clear_EventLog", ex.ToString()); + last_error_message = "Error clearing log: " + ex.Message; + } + finally + { + loading_overlay = false; + StateHasChanged(); + } + } + + #endregion + + #region Helper Methods + + private Task> SearchLogs(string value, CancellationToken cancellationToken) + { + // If the search value is empty, return all logs + if (string.IsNullOrEmpty(value)) + return Task.FromResult(available_logs.AsEnumerable()); + + // Filter logs based on the search value (case-insensitive) + return Task.FromResult(available_logs + .Where(log => log.Contains(value, StringComparison.InvariantCultureIgnoreCase)) + .AsEnumerable()); + } + + private bool FilterFunc(EventLogEntry entry) + { + if (string.IsNullOrWhiteSpace(search_string)) + return true; + + if (entry.EventId?.ToString().Contains(search_string, StringComparison.OrdinalIgnoreCase) == true) + return true; + if (entry.Level?.Contains(search_string, StringComparison.OrdinalIgnoreCase) == true) + return true; + if (entry.Source?.Contains(search_string, StringComparison.OrdinalIgnoreCase) == true) + return true; + if (entry.Message?.Contains(search_string, StringComparison.OrdinalIgnoreCase) == true) + return true; + if (entry.TimeCreated?.Contains(search_string, StringComparison.OrdinalIgnoreCase) == true) + return true; + if (entry.ProviderName?.Contains(search_string, StringComparison.OrdinalIgnoreCase) == true) + return true; + if (entry.Computer?.Contains(search_string, StringComparison.OrdinalIgnoreCase) == true) + return true; + + return false; + } + + private Color GetLevelColor(byte? level) + { + if (!level.HasValue) return Color.Default; + + return level.Value switch + { + 1 => Color.Error, // Critical + 2 => Color.Error, // Error + 3 => Color.Warning, // Warning + 4 => Color.Info, // Information + 5 => Color.Default, // Verbose + _ => Color.Default + }; + } + + private async Task ShowEventDetails(EventLogEntry entry) + { + try + { + var options = new DialogOptions + { + CloseButton = true, + MaxWidth = MaxWidth.Large, + FullWidth = true, + BackgroundClass = "dialog-blurring" + }; + + var parameters = new DialogParameters + { + { "EventEntry", entry } + }; + + await DialogService.ShowAsync("Event Details", parameters, options); + } + catch (Exception e) + { + Logging.Handler.Error("Remote_EventLog_Dialog -> ShowEventDetails", "General error", e.ToString()); + } + } + + private async Task ShowStatsDialog(EventLogStatsResponse stats) + { + try + { + var options = new DialogOptions + { + CloseButton = true, + MaxWidth = MaxWidth.Medium, + FullWidth = true, + BackgroundClass = "dialog-blurring" + }; + + var parameters = new DialogParameters + { + { "Stats", stats } + }; + + await DialogService.ShowAsync("Event Log Statistics", parameters, options); + } + catch (Exception e) + { + Logging.Handler.Error("Remote_EventLog_Dialog -> ShowStatsDialog", "General error", e.ToString()); + } + } + + #endregion + + private async Task Cancel() + { + if (remote_server_client_setup) + { + await remote_server_client.StopAsync(); + remote_server_client_setup = false; + } + + MudDialog.Cancel(); + } +} diff --git a/NetLock-RMM-Web-Console/Components/Pages/Devices/Dialogs/Remote_Shell/Remote_Shell_Dialog.razor b/NetLock-RMM-Web-Console/Components/Pages/Devices/Dialogs/Remote_Shell/Remote_Shell_Dialog.razor index 474b84b3..4e6ba360 100644 --- a/NetLock-RMM-Web-Console/Components/Pages/Devices/Dialogs/Remote_Shell/Remote_Shell_Dialog.razor +++ b/NetLock-RMM-Web-Console/Components/Pages/Devices/Dialogs/Remote_Shell/Remote_Shell_Dialog.razor @@ -98,6 +98,19 @@ + +
+ +
+ Community Scripts
@@ -125,6 +138,7 @@ private bool loading_overlay = false; private bool expanded = false; private string platform = String.Empty; + private int timeout = 1; protected override async Task OnInitializedAsync() { @@ -248,7 +262,10 @@ { while (await reader.ReadAsync()) { - await remote_shell_command_editor.SetValue(reader["script"].ToString() ?? String.Empty); + Collections.Scripts.Script _object = JsonSerializer.Deserialize(reader["json"].ToString() ?? String.Empty); + timeout = _object.timeout; + + await remote_shell_command_editor.SetValue(_object.script); } } } @@ -306,6 +323,7 @@ public string file_browser_path { get; set; } public string file_browser_path_move { get; set; } public string file_browser_file_content { get; set; } + public string command { get; set; } } public class Remote_Root_Object @@ -366,6 +384,14 @@ await InvokeAsync(() => { remote_shell_command_output = DateTime.Now.ToString("dd.MM.yyyy hh:mm:ss") + System.Environment.NewLine + command + System.Environment.NewLine + "--------------END--------------" + System.Environment.NewLine + remote_shell_command_output; + + // if nlock seperator, remove everything before it and including it + if (remote_shell_command_output.Contains(">>nlocksep<<")) + { + int index = remote_shell_command_output.IndexOf(">>nlocksep<<"); + remote_shell_command_output = remote_shell_command_output.Substring(index + ">>nlocksep<<".Length).TrimStart(); + } + remote_shell_command_output_editor.SetValue(remote_shell_command_output); loading_overlay = false; @@ -416,7 +442,8 @@ { type = 0, wait_response = wait_repsonse, - powershell_code = powershell_code + powershell_code = powershell_code, + command = timeout.ToString(), }; var rootObject = new Remote_Root_Object diff --git a/NetLock-RMM-Web-Console/Components/Pages/Events/Manage_Events.razor b/NetLock-RMM-Web-Console/Components/Pages/Events/Manage_Events.razor index da1f863e..9503ac6c 100644 --- a/NetLock-RMM-Web-Console/Components/Pages/Events/Manage_Events.razor +++ b/NetLock-RMM-Web-Console/Components/Pages/Events/Manage_Events.razor @@ -18,6 +18,8 @@ @inject IStringLocalizer Localizer @inject AuthenticationStateProvider AuthenticationStateProvider +@implements IDisposable + - - -
- - - Please wait... 🐧 - -
-
- - - - - - @if (Configuration.Members_Portal.IsCloudEnabled) - { - This section is administered by the cloud team. The view is limited. :-) - } - - @if (permissions_settings_enabled && permissions_settings_system_enabled) - { - - - @if (!Configuration.Members_Portal.IsCloudEnabled) - { - - - - - @Localizer["MySQL"] - - - - - - - @if (mysqlConnectionStatus) - { -
- - @Localizer["connected"] -
- - - Version: @mysqlVersion - @Localizer["uptime"]: @mysqlUptime (seconds) - @Localizer["active connections"]: @mysqlConnectedUsers - @Localizer["database size"]: @mysqlDatabaseSize - - - @Localizer["max connections"]: @mysqlMaxConnections - @Localizer["failed logins"]: @mysqlFailedLogins - } - else - { -
- - @Localizer["not connected"] -
- } -
-
-
-
- - @Localizer["active queries"] - - - - - - - Id - User - Host - Database - Command - Time - State - Info - - - - - - -  @row.Id - - - - - -  @row.User - - - - - -  @row.Host - - - - - -  @row.Database - - - - - -  @row.Command - - - - - -  @row.Time - - - - - -  @row.State - - - - - -  @row.Info - - - - - - - - - -
- - - @Localizer["Web Console"] - - - - - - - @Localizer["cpu usage"]: @cpuUsage% - - - - - - - - - - @Localizer["ram usage"]: @ramUsage% - - - - - - - - - - @Localizer["disk usage"]: @diskUsage% - - - - - - - - @foreach (var server in servers) - { - DateTime lastHeartbeat; - bool isRecent = false; - - try - { - // Try to parse the heartbeat timestamp - lastHeartbeat = DateTime.Parse(server.hearthbeat); - isRecent = (DateTime.Now - lastHeartbeat).TotalMinutes <= 10; - } - catch (Exception ex) - { - // Error handling for invalid or missing date - Logging.Handler.Error("Heartbeat-Parsing", "Error parsing server heartbeat", ex.ToString()); - } - - - @server.name (@server.os) @(server.docker == "1" ? " (Docker)" : String.Empty) - IP: @server.ip_address | Domain: @server.domain - - @Localizer["last hearthbeat"]: @server.hearthbeat - - - - - - - @Localizer["cpu usage"]: @server.cpu_usage% - - - - - - - - - - @Localizer["ram usage"]: @server.ram_usage% - - - - - - - - - - @Localizer["disk usage"]: @server.disk_usage% - - - - - - - - -
- - @Localizer["appsettings.json"] -
-
- - - - - -
-
- } - - - - - @Localizer["NetLock RMM File Server"] (@Configuration.File_Server.Connection_String) - - - - - - -
- @if (fileServerConnectionStatus) - { - - @Localizer["connected"] - } - else - { - - @Localizer["not connected"] - } -
- -
-
-
-
- -
- - - @Localizer["NetLock RMM Remote Server"] (@Configuration.Remote_Server.Connection_String) - - - - - - - -
- - @if (remoteServerConnectionStatus) - { - - @Localizer["connected"] - } - else - { - - @Localizer["not connected"] - } - -
-
-
-
-
- - @if (!_dnsWarning && !Configuration.Members_Portal.IsCloudEnabled && !remoteServerConnectionStatus && !fileServerConnectionStatus) - { - PTR record mismatch detected! Your provided hostname resolves to: @_dnsWarningText but it should resolve to: @Configuration.Remote_Server.Hostname instead, according to your provided configuration. More details: https://docs.netlockrmm.com/en/troubleshooting/ptr-mismatch - } - - @Localizer["Check again"] - -
- -
- - } - @if (!Configuration.Members_Portal.IsCloudEnabled) - { - - - - - @Localizer["Members Portal Api"] - - @if (!Configuration.Members_Portal.IsApiEnabled) - { - @Localizer["members portal api is not enabled. Please enable it in the appsettings.json"] - } - else - { - - @Localizer["save"] - - @Localizer["license status"] - - @if (members_portal_status == "Active") - { -
- - @Localizer["active"] -
- } - else - { -
- - @Localizer["expired"] -
- } - - Name: @members_portal_license_name - @Localizer["licenses used"]: @members_portal_licenses_used / @members_portal_licenses_max - @Localizer["hard_limit"]: @members_portal_licenses_hard_limit - @Localizer["code_signed"]: @members_portal_code_signed - - @Localizer["refresh license information"] - } - -
- -
- } - - - Automatic agent updates - Windows - Linux - MacOS - -
- - - - - -
- - Save -
-
- @if (!Configuration.Members_Portal.IsCloudEnabled) - { - - - Clean Up & Optimize Database - - Automatic cleanup of the database. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Save - Optimize Database - - - } - - - @if (!Configuration.Members_Portal.IsCodeSigned) - { - @Application_Settings.onlyPro - } - - Remote Screen Control Recording - - When enabled, all remote screen control sessions will be recorded by the server. - - - - @if (Configuration.Members_Portal.IsCloudEnabled) - { - Your recordings will be kept for seven days. - } - else - { - - } - - Save - - - -
- } - -
- -
- -@code { - - #region Permissions System - - private string permissions_json = String.Empty; - - private bool permissions_settings_enabled = false; - private bool permissions_settings_system_enabled = false; - - private async Task Permissions() - { - try - { - bool logout = false; - - // Get the current user from the authentication state - var user = (await AuthenticationStateProvider.GetAuthenticationStateAsync()).User; - - // Check if user is authenticated - if (user?.Identity is not { IsAuthenticated: true }) - logout = true; - - string netlock_username = user.FindFirst(ClaimTypes.Email)?.Value; - - permissions_settings_enabled = await Classes.Authentication.Permissions.Verify_Permission(netlock_username, "settings_enabled"); - permissions_settings_system_enabled = await Classes.Authentication.Permissions.Verify_Permission(netlock_username, "settings_system_enabled"); - - if (!permissions_settings_enabled) - logout = true; - - if (logout) // Redirect to the login page - { - NavigationManager.NavigateTo("/logout", true); - return false; - } - - // All fine? Nice. - return true; - } - catch (Exception ex) - { - Logging.Handler.Error("/dashboard -> Permissions", "Error", ex.ToString()); - return false; - } - } - - #endregion - - private bool loading_overlay = false; - - private bool _isDarkMode = false; - private bool _dnsWarning = false; - private string _dnsWarningText = String.Empty; - - protected override async Task OnInitializedAsync() - { - await base.OnInitializedAsync(); - } - - protected override async Task OnAfterRenderAsync(bool firstRender) - { - if (firstRender) - { - await AfterInitializedAsync(); - } - } - - private async Task AfterInitializedAsync() - { - if (!await Permissions()) - return; - - loading_overlay = true; - StateHasChanged(); - - _isDarkMode = await JSRuntime.InvokeAsync("isDarkMode"); - - await MySQL_Status(); - await Fix_Settings(); // this is a hotfix code to verify if a bug is present in the database - await Get_Servers(); - await Get_Agent_Updates(); - - if (Configuration.Members_Portal.IsApiEnabled) - await Get_Members_Portal_License_Status(); - - // Get CPU Usage - cpuUsage = await Classes.System_Information.Handler.Get_CPU_Usage(); - - // Get RAM Usage - ramUsage = await Classes.System_Information.Handler.Get_RAM_Usage(); - - // Get Disk Usage - diskUsage = await Classes.System_Information.Handler.Get_Disk_Usage(); - - fileServerConnectionStatus = await Classes.Helper.Networking.Test_Connection(Configuration.File_Server.Connection_String + "/test"); - - remoteServerConnectionStatus = await Classes.Helper.Networking.Test_Connection(Configuration.Remote_Server.Connection_String + "/test"); - - await Check_DNS(); - - await Get_Cleanup_Settings(); - - loading_overlay = false; - - StateHasChanged(); - } - - private bool mysqlConnectionStatus = false; - private bool fileServerConnectionStatus = false; - private bool remoteServerConnectionStatus = false; - - private int cpuUsage = 0; - private int ramUsage = 0; - private int diskUsage = 0; - - private string mysqlVersion = String.Empty; - private string mysqlUptime = String.Empty; - private string mysqlConnectedUsers = String.Empty; - private string mysqlDatabaseSize = String.Empty; - private List mysqlActiveQueries = new List(); - private bool mysqlSSLEnabled = false; - private string mysqlFailedLogins = String.Empty; - private string mysqlPrivilegedUsers = String.Empty; - private string mysqlMaxConnections = String.Empty; - - // Agent Update Settings - private bool agent_updates_windows_enabled = false; - private bool agent_updates_linux_enabled = false; - private bool agent_updates_macos_enabled = false; - private int agent_updates_max_concurrent_updates = 0; - - // Members Portal - private string members_portal_api_key = String.Empty; - private string members_portal_status = String.Empty; - private string members_portal_license_name = String.Empty; - private int members_portal_licenses_used = 0; - private int members_portal_licenses_max = 0; - private bool members_portal_licenses_hard_limit = false; - private bool members_portal_code_signed = false; - - private string package_url = String.Empty; - - // Remote Screen Control - private bool remoteScreenSessionRecordingForced = false; - private bool remoteScreenSessionRecordingAutoClean = false; - private int remoteScreenSessionRecordingForcedDays = 0; - - public class MySQL_Active_Queries - { - public int Id { get; set; } - public string User { get; set; } - public string Host { get; set; } - public string Database { get; set; } - public string Command { get; set; } - public int Time { get; set; } - public string State { get; set; } - public string Info { get; set; } - } - - private bool MySQL_Active_Queries_Table_Filter_Func(MySQL_Active_Queries row) - { - try - { - if (row == null) return false; // Überprüfe, ob row null ist - - if (string.IsNullOrEmpty(mysql_active_queries_table_search_string)) - return true; - - return row.Id.ToString().Contains(mysql_active_queries_table_search_string, StringComparison.OrdinalIgnoreCase) || - (row.User?.Contains(mysql_active_queries_table_search_string, StringComparison.OrdinalIgnoreCase) ?? false) || - (row.Host?.Contains(mysql_active_queries_table_search_string, StringComparison.OrdinalIgnoreCase) ?? false) || - (row.Database?.Contains(mysql_active_queries_table_search_string, StringComparison.OrdinalIgnoreCase) ?? false) || - (row.Command?.Contains(mysql_active_queries_table_search_string, StringComparison.OrdinalIgnoreCase) ?? false) || - row.Time.ToString().Contains(mysql_active_queries_table_search_string, StringComparison.OrdinalIgnoreCase) || - (row.State?.Contains(mysql_active_queries_table_search_string, StringComparison.OrdinalIgnoreCase) ?? false) || - (row.Info?.Contains(mysql_active_queries_table_search_string, StringComparison.OrdinalIgnoreCase) ?? false); - } - catch (Exception ex) - { - Logging.Handler.Error("/system -> MySQL_Active_Queries_Table_Filter_Func", "general_error", ex.ToString()); - return false; - } - } - - private string mysql_active_queries_table_sorted_column; - private string mysql_active_queries_table_search_string = String.Empty; - private string mysql_active_queries_selectedRowContent = ""; // Saving content of selected row - - // Executes on row click - private void mysql_active_queries_RowClickHandler(MySQL_Active_Queries row) - { - mysql_active_queries_selectedRowContent = row.Id.ToString(); - } - - private string mysql_active_queries_GetRowClass(MySQL_Active_Queries row) - { - return row.Id.ToString() == mysql_active_queries_selectedRowContent ? "selected-row" : ""; - } - - // MySQL - - private string GetCommandColor(string command, int time) - { - switch (command) - { - case "Daemon": - return "blue"; - case "Sleep": - return "orange"; - case "Query": - return time > 60 ? "red" : "green"; // Red for long queries, green for short - default: - return "inherit"; // Standard colour - } - } - - private async Task MySQL_Status() - { - try - { - if (await Classes.MySQL.Database.Check_Connection()) - { - mysqlConnectionStatus = true; - - // Get MySQL Version - mysqlVersion = await Classes.MySQL.Database.Get_Version(); - - // Get MySQL Uptime - mysqlUptime = await Classes.MySQL.Database.Get_Uptime(); - - // Get MySQL Connected Users - mysqlConnectedUsers = await Classes.MySQL.Database.Get_Connected_Users(); - - // Get MySQL Active Queries - string mysqlActiveQueriesJson = await Classes.MySQL.Database.Get_Active_Queries(); - Logging.Handler.Debug("/system -> MySQL_Status", "mysqlActiveQueriesJson", mysqlActiveQueriesJson); - mysqlActiveQueries = JsonSerializer.Deserialize>(mysqlActiveQueriesJson); - - // Get MySQL Failed Logins - mysqlFailedLogins = await Classes.MySQL.Database.Get_Failed_Logins(); - - // Get MySQL Max Connections - mysqlMaxConnections = await Classes.MySQL.Database.Get_Max_Connections(); - - // Get MySQL Database Size - mysqlDatabaseSize = await Classes.MySQL.Database.Get_Database_Size(); - } - else - { - mysqlConnectionStatus = false; - } - } - catch (Exception ex) - { - Logging.Handler.Error("/system -> MySQL_Status", "general_error", ex.ToString()); - } - - } - - // Servers overview - private List servers = new List(); - - public class Servers - { - public string id { get; set; } - public string name { get; set; } - public string ip_address { get; set; } - public string domain { get; set; } - public string os { get; set; } - public string hearthbeat { get; set; } - public string appsettings { get; set; } - public string cpu_usage { get; set; } - public string ram_usage { get; set; } - public string disk_usage { get; set; } - public string docker { get; set; } - } - - // Get servers from database - private async Task Get_Servers() - { - servers.Clear(); - - MySqlConnection conn = new MySqlConnection(Configuration.MySQL.Connection_String); - - try - { - await conn.OpenAsync(); - - MySqlCommand command = new MySqlCommand("SELECT * FROM servers;", conn); - using (DbDataReader reader = await command.ExecuteReaderAsync()) - { - if (reader.HasRows) - { - while (await reader.ReadAsync()) - { - servers.Add(new Servers - { - id = reader["id"].ToString() ?? String.Empty, - name = reader["name"].ToString() ?? String.Empty, - ip_address = reader["ip_address"].ToString() ?? String.Empty, - domain = reader["domain"].ToString() ?? String.Empty, - os = reader["os"].ToString() ?? String.Empty, - hearthbeat = reader["hearthbeat"].ToString() ?? String.Empty, - appsettings = reader["appsettings"].ToString() ?? String.Empty, - cpu_usage = reader["cpu_usage"].ToString() ?? String.Empty, - ram_usage = reader["ram_usage"].ToString() ?? String.Empty, - disk_usage = reader["disk_usage"].ToString() ?? String.Empty, - docker = reader["docker"].ToString() ?? String.Empty - }); - } - } - } - } - catch (Exception ex) - { - Logging.Handler.Error("/system -> Get_Servers", "general_error", ex.ToString()); - } - finally - { - conn.Close(); - } - } - - // Save api key for members portal - //OSSCH_START b80b76e9-8f16-46b1-9f7b-bac4b3f86a3f //OSSCH_END - - // Update members portal license information - //OSSCH_START b413a2c3-80f1-467c-999d-45cef69c0ee1 //OSSCH_END - - // Get members portal license status - //OSSCH_START cfaaeafd-56f2-4e21-9608-ee6d736c3949 //OSSCH_END - - // Get agent updates settings - private async Task Get_Agent_Updates() - { - string windows_updates_enabled = await Classes.MySQL.Handler.Quick_Reader("SELECT * FROM settings", "agent_updates_windows_enabled"); - string linux_updates_enabled = await Classes.MySQL.Handler.Quick_Reader("SELECT * FROM settings", "agent_updates_linux_enabled"); - string macos_updates_enabled = await Classes.MySQL.Handler.Quick_Reader("SELECT * FROM settings", "agent_updates_macos_enabled"); - - if (windows_updates_enabled == "1") - agent_updates_windows_enabled = true; - else - agent_updates_windows_enabled = false; - - if (linux_updates_enabled == "1") - agent_updates_linux_enabled = true; - else - agent_updates_linux_enabled = false; - - if (macos_updates_enabled == "1") - agent_updates_macos_enabled = true; - else - agent_updates_macos_enabled = false; - - agent_updates_max_concurrent_updates = Convert.ToInt32(await Classes.MySQL.Handler.Quick_Reader("SELECT * FROM settings", "agent_updates_max_concurrent_updates")); - } - - // Set agent updates settings - private async Task SaveAgentUpdates() - { - Snackbar.Configuration.ShowCloseIcon = true; - Snackbar.Configuration.PositionClass = Defaults.Classes.Position.BottomRight; - - if (agent_updates_windows_enabled) - { - Logging.Handler.Debug("/system -> Set_Agent_Updates", "agent_updates_windows_enabled", "true"); - await Classes.MySQL.Handler.Execute_Command("UPDATE settings SET agent_updates_windows_enabled = 1;"); - } - else - { - Logging.Handler.Debug("/system -> Set_Agent_Updates", "agent_updates_windows_enabled", "false"); - await Classes.MySQL.Handler.Execute_Command("UPDATE settings SET agent_updates_windows_enabled = 0;"); - } - - if (agent_updates_linux_enabled) - { - Logging.Handler.Debug("/system -> Set_Agent_Updates", "agent_updates_linux_enabled", "true"); - await Classes.MySQL.Handler.Execute_Command("UPDATE settings SET agent_updates_linux_enabled = 1;"); - } - else - { - Logging.Handler.Debug("/system -> Set_Agent_Updates", "agent_updates_linux_enabled", "false"); - await Classes.MySQL.Handler.Execute_Command("UPDATE settings SET agent_updates_linux_enabled = 0;"); - } - - if (agent_updates_macos_enabled) - { - Logging.Handler.Debug("/system -> Set_Agent_Updates", "agent_updates_macos_enabled", "true"); - await Classes.MySQL.Handler.Execute_Command("UPDATE settings SET agent_updates_macos_enabled = 1;"); - } - else - { - Logging.Handler.Debug("/system -> Set_Agent_Updates", "agent_updates_macos_enabled", "false"); - await Classes.MySQL.Handler.Execute_Command("UPDATE settings SET agent_updates_macos_enabled = 0;"); - } - - MySqlConnection conn = new MySqlConnection(Configuration.MySQL.Connection_String); - - string query = "UPDATE settings SET agent_updates_max_concurrent_updates = @agent_updates_max_concurrent_updates;"; - - try - { - await conn.OpenAsync(); - - MySqlCommand cmd = new MySqlCommand(query, conn); - cmd.Parameters.AddWithValue("@agent_updates_max_concurrent_updates", agent_updates_max_concurrent_updates.ToString()); - - Logging.Handler.Debug("SaveAgentUpdates", "MySQL_Prepared_Query", query); - - await cmd.ExecuteNonQueryAsync(); - } - catch (Exception ex) - { - Logging.Handler.Error("SaveAgentUpdates", "MySQL_Query", ex.ToString()); - } - finally - { - await conn.CloseAsync(); - } - - Snackbar.Add("Saved.", Severity.Success); - } - - // Check the dns ptr record - private async Task Check_DNS() - { - (_dnsWarning, _dnsWarningText) = await Classes.Setup.DNS.Check_Dns_Forward_Reverse(Configuration.Remote_Server.Hostname); - } - - // Check connection - private async Task Test_Remote_File_Server_Connection() - { - Snackbar.Configuration.ShowCloseIcon = true; - Snackbar.Configuration.PositionClass = Defaults.Classes.Position.BottomRight; - - loading_overlay = true; - fileServerConnectionStatus = await Classes.Helper.Networking.Test_Connection(Configuration.File_Server.Connection_String + "/test"); - - remoteServerConnectionStatus = await Classes.Helper.Networking.Test_Connection(Configuration.Remote_Server.Connection_String + "/test"); - - await Check_DNS(); - - loading_overlay = false; - - StateHasChanged(); - - if (fileServerConnectionStatus && remoteServerConnectionStatus) - { - Snackbar.Add(Localizer["connected"] + ".", Severity.Success); - } - else - { - Snackbar.Add(Localizer["not connected"] + ".", Severity.Error); - } - } - - // Automatic cleanup - private bool cleanup_applications_drivers_history_enabled = false; - private int cleanup_applications_drivers_history_days = 0; - - private bool cleanup_applications_installed_history_enabled = false; - private int cleanup_applications_installed_history_days = 0; - - private bool cleanup_applications_logon_history_enabled = false; - private int cleanup_applications_logon_history_days = 0; - - private bool cleanup_applications_scheduled_tasks_history_enabled = false; - private int cleanup_applications_scheduled_tasks_history_days = 0; - - private bool cleanup_applications_services_history_enabled = false; - private int cleanup_applications_services_history_days = 0; - - private bool cleanup_device_information_antivirus_products_history_enabled = false; - private int cleanup_device_information_antivirus_products_history_days = 0; - - private bool cleanup_device_information_cpu_history_enabled = false; - private int cleanup_device_information_cpu_history_days = 0; - - private bool cleanup_device_information_cronjobs_history_enabled = false; - private int cleanup_device_information_cronjobs_history_days = 0; - - private bool cleanup_device_information_disks_history_enabled = false; - private int cleanup_device_information_disks_history_days = 0; - - private bool cleanup_device_information_general_history_enabled = false; - private int cleanup_device_information_general_history_days = 0; - - private bool cleanup_device_information_history_enabled = false; - private int cleanup_device_information_history_days = 0; - - private bool cleanup_device_information_network_adapters_history_enabled = false; - private int cleanup_device_information_network_adapters_history_days = 0; - - private bool cleanup_device_information_ram_history_enabled = false; - private int cleanup_device_information_ram_history_days = 0; - - private bool cleanup_device_information_task_manager_history_enabled = false; - private int cleanup_device_information_task_manager_history_days = 0; - - private bool cleanup_events_history_enabled = false; - private int cleanup_events_history_days = 0; - - // Get cleanup settings from database - private async Task Get_Cleanup_Settings() - { - MySqlConnection conn = new MySqlConnection(Configuration.MySQL.Connection_String); - - try - { - await conn.OpenAsync(); - - string query = "SELECT * FROM settings;"; - - MySqlCommand cmd = new MySqlCommand(query, conn); - - Logging.Handler.Debug("Example", "MySQL_Prepared_Query", query); - - using (DbDataReader reader = await cmd.ExecuteReaderAsync()) - { - if (reader.HasRows) - { - while (await reader.ReadAsync()) - { - cleanup_applications_drivers_history_enabled = reader.GetBoolean(reader.GetOrdinal("cleanup_applications_drivers_history_enabled")); - cleanup_applications_drivers_history_days = reader.GetInt32(reader.GetOrdinal("cleanup_applications_drivers_history_days")); - - cleanup_applications_installed_history_enabled = reader.GetBoolean(reader.GetOrdinal("cleanup_applications_installed_history_enabled")); - cleanup_applications_installed_history_days = reader.GetInt32(reader.GetOrdinal("cleanup_applications_installed_history_days")); - - cleanup_applications_logon_history_enabled = reader.GetBoolean(reader.GetOrdinal("cleanup_applications_logon_history_enabled")); - cleanup_applications_logon_history_days = reader.GetInt32(reader.GetOrdinal("cleanup_applications_logon_history_days")); - - cleanup_applications_scheduled_tasks_history_enabled = reader.GetBoolean(reader.GetOrdinal("cleanup_applications_scheduled_tasks_history_enabled")); - cleanup_applications_scheduled_tasks_history_days = reader.GetInt32(reader.GetOrdinal("cleanup_applications_scheduled_tasks_history_days")); - - cleanup_applications_services_history_enabled = reader.GetBoolean(reader.GetOrdinal("cleanup_applications_services_history_enabled")); - cleanup_applications_services_history_days = reader.GetInt32(reader.GetOrdinal("cleanup_applications_services_history_days")); - - cleanup_device_information_antivirus_products_history_enabled = reader.GetBoolean(reader.GetOrdinal("cleanup_device_information_antivirus_products_history_enabled")); - cleanup_device_information_antivirus_products_history_days = reader.GetInt32(reader.GetOrdinal("cleanup_device_information_antivirus_products_history_days")); - - cleanup_device_information_cpu_history_enabled = reader.GetBoolean(reader.GetOrdinal("cleanup_device_information_cpu_history_enabled")); - cleanup_device_information_cpu_history_days = reader.GetInt32(reader.GetOrdinal("cleanup_device_information_cpu_history_days")); - - cleanup_device_information_cronjobs_history_enabled = reader.GetBoolean(reader.GetOrdinal("cleanup_device_information_cronjobs_history_enabled")); - cleanup_device_information_cronjobs_history_days = reader.GetInt32(reader.GetOrdinal("cleanup_device_information_cronjobs_history_days")); - - cleanup_device_information_disks_history_enabled = reader.GetBoolean(reader.GetOrdinal("cleanup_device_information_disks_history_enabled")); - cleanup_device_information_disks_history_days = reader.GetInt32(reader.GetOrdinal("cleanup_device_information_disks_history_days")); - - cleanup_device_information_general_history_enabled = reader.GetBoolean(reader.GetOrdinal("cleanup_device_information_general_history_enabled")); - cleanup_device_information_general_history_days = reader.GetInt32(reader.GetOrdinal("cleanup_device_information_general_history_days")); - - cleanup_device_information_history_enabled = reader.GetBoolean(reader.GetOrdinal("cleanup_device_information_history_enabled")); - cleanup_device_information_history_days = reader.GetInt32(reader.GetOrdinal("cleanup_device_information_history_days")); - - cleanup_device_information_network_adapters_history_enabled = reader.GetBoolean(reader.GetOrdinal("cleanup_device_information_network_adapters_history_enabled")); - cleanup_device_information_network_adapters_history_days = reader.GetInt32(reader.GetOrdinal("cleanup_device_information_network_adapters_history_days")); - - cleanup_device_information_ram_history_enabled = reader.GetBoolean(reader.GetOrdinal("cleanup_device_information_ram_history_enabled")); - cleanup_device_information_ram_history_days = reader.GetInt32(reader.GetOrdinal("cleanup_device_information_ram_history_days")); - - cleanup_device_information_task_manager_history_enabled = reader.GetBoolean(reader.GetOrdinal("cleanup_device_information_task_manager_history_enabled")); - cleanup_device_information_task_manager_history_days = reader.GetInt32(reader.GetOrdinal("cleanup_device_information_task_manager_history_days")); - - cleanup_events_history_enabled = reader.GetBoolean(reader.GetOrdinal("cleanup_events_history_enabled")); - cleanup_events_history_days = reader.GetInt32(reader.GetOrdinal("cleanup_events_history_days")); - - remoteScreenSessionRecordingForced = reader.GetBoolean(reader.GetOrdinal("remote_screen_session_recording_forced_enabled")); - remoteScreenSessionRecordingAutoClean = reader.GetBoolean(reader.GetOrdinal("remote_screen_session_recording_auto_clean_enabled")); - remoteScreenSessionRecordingForcedDays = reader.GetInt32(reader.GetOrdinal("remote_screen_session_recording_forced_days")); - } - } - } - } - catch (Exception ex) - { - Logging.Handler.Error("Settings -> Get_Cleanup_Settings", "MySQL_Query", ex.ToString()); - } - finally - { - await conn.CloseAsync(); - } - } - - // Save cleanup settings to database (settings) - private async Task Save_Cleanup_Settings() - { - Snackbar.Configuration.ShowCloseIcon = true; - Snackbar.Configuration.PositionClass = Defaults.Classes.Position.BottomRight; - - // Save cleanup settings to database - MySqlConnection conn = new MySqlConnection(Configuration.MySQL.Connection_String); - - string query = @"UPDATE settings SET -cleanup_applications_drivers_history_enabled = @cleanup_applications_drivers_history_enabled, -cleanup_applications_drivers_history_days = @cleanup_applications_drivers_history_days, -cleanup_applications_installed_history_enabled = @cleanup_applications_installed_history_enabled, -cleanup_applications_installed_history_days = @cleanup_applications_installed_history_days, -cleanup_applications_logon_history_enabled = @cleanup_applications_logon_history_enabled, -cleanup_applications_logon_history_days = @cleanup_applications_logon_history_days, -cleanup_applications_scheduled_tasks_history_enabled = @cleanup_applications_scheduled_tasks_history_enabled, -cleanup_applications_scheduled_tasks_history_days = @cleanup_applications_scheduled_tasks_history_days, -cleanup_applications_services_history_enabled = @cleanup_applications_services_history_enabled, -cleanup_applications_services_history_days = @cleanup_applications_services_history_days, -cleanup_device_information_antivirus_products_history_enabled = @cleanup_device_information_antivirus_products_history_enabled, -cleanup_device_information_antivirus_products_history_days = @cleanup_device_information_antivirus_products_history_days, -cleanup_device_information_cpu_history_enabled = @cleanup_device_information_cpu_history_enabled, -cleanup_device_information_cpu_history_days = @cleanup_device_information_cpu_history_days, -cleanup_device_information_cronjobs_history_enabled = @cleanup_device_information_cronjobs_history_enabled, -cleanup_device_information_cronjobs_history_days = @cleanup_device_information_cronjobs_history_days, -cleanup_device_information_disks_history_enabled = @cleanup_device_information_disks_history_enabled, -cleanup_device_information_disks_history_days = @cleanup_device_information_disks_history_days, -cleanup_device_information_general_history_enabled = @cleanup_device_information_general_history_enabled, -cleanup_device_information_general_history_days = @cleanup_device_information_general_history_days, -cleanup_device_information_history_enabled = @cleanup_device_information_history_enabled, -cleanup_device_information_history_days = @cleanup_device_information_history_days, -cleanup_device_information_network_adapters_history_enabled = @cleanup_device_information_network_adapters_history_enabled, -cleanup_device_information_network_adapters_history_days = @cleanup_device_information_network_adapters_history_days, -cleanup_device_information_ram_history_enabled = @cleanup_device_information_ram_history_enabled, -cleanup_device_information_ram_history_days = @cleanup_device_information_ram_history_days, -cleanup_device_information_task_manager_history_enabled = @cleanup_device_information_task_manager_history_enabled, -cleanup_device_information_task_manager_history_days = @cleanup_device_information_task_manager_history_days, -cleanup_events_history_enabled = @cleanup_events_history_enabled, -cleanup_events_history_days = @cleanup_events_history_days;"; - - try - { - await conn.OpenAsync(); - - MySqlCommand cmd = new MySqlCommand(query, conn); - cmd.Parameters.AddWithValue("@cleanup_applications_drivers_history_enabled", Convert.ToInt32(cleanup_applications_drivers_history_enabled)); - cmd.Parameters.AddWithValue("@cleanup_applications_drivers_history_days", cleanup_applications_drivers_history_days); - cmd.Parameters.AddWithValue("@cleanup_applications_installed_history_enabled", Convert.ToInt32(cleanup_applications_installed_history_enabled)); - cmd.Parameters.AddWithValue("@cleanup_applications_installed_history_days", cleanup_applications_installed_history_days); - cmd.Parameters.AddWithValue("@cleanup_applications_logon_history_enabled", Convert.ToInt32(cleanup_applications_logon_history_enabled)); - cmd.Parameters.AddWithValue("@cleanup_applications_logon_history_days", cleanup_applications_logon_history_days); - cmd.Parameters.AddWithValue("@cleanup_applications_scheduled_tasks_history_enabled", Convert.ToInt32(cleanup_applications_scheduled_tasks_history_enabled)); - cmd.Parameters.AddWithValue("@cleanup_applications_scheduled_tasks_history_days", cleanup_applications_scheduled_tasks_history_days); - cmd.Parameters.AddWithValue("@cleanup_applications_services_history_enabled", Convert.ToInt32(cleanup_applications_services_history_enabled)); - cmd.Parameters.AddWithValue("@cleanup_applications_services_history_days", cleanup_applications_services_history_days); - cmd.Parameters.AddWithValue("@cleanup_device_information_antivirus_products_history_enabled", Convert.ToInt32(cleanup_device_information_antivirus_products_history_enabled)); - cmd.Parameters.AddWithValue("@cleanup_device_information_antivirus_products_history_days", cleanup_device_information_antivirus_products_history_days); - cmd.Parameters.AddWithValue("@cleanup_device_information_cpu_history_enabled", Convert.ToInt32(cleanup_device_information_cpu_history_enabled)); - cmd.Parameters.AddWithValue("@cleanup_device_information_cpu_history_days", cleanup_device_information_cpu_history_days); - cmd.Parameters.AddWithValue("@cleanup_device_information_cronjobs_history_enabled", Convert.ToInt32(cleanup_device_information_cronjobs_history_enabled)); - cmd.Parameters.AddWithValue("@cleanup_device_information_cronjobs_history_days", cleanup_device_information_cronjobs_history_days); - cmd.Parameters.AddWithValue("@cleanup_device_information_disks_history_enabled", Convert.ToInt32(cleanup_device_information_disks_history_enabled)); - cmd.Parameters.AddWithValue("@cleanup_device_information_disks_history_days", cleanup_device_information_disks_history_days); - cmd.Parameters.AddWithValue("@cleanup_device_information_general_history_enabled", Convert.ToInt32(cleanup_device_information_general_history_enabled)); - cmd.Parameters.AddWithValue("@cleanup_device_information_general_history_days", cleanup_device_information_general_history_days); - cmd.Parameters.AddWithValue("@cleanup_device_information_history_enabled", Convert.ToInt32(cleanup_device_information_history_enabled)); - cmd.Parameters.AddWithValue("@cleanup_device_information_history_days", cleanup_device_information_history_days); - cmd.Parameters.AddWithValue("@cleanup_device_information_network_adapters_history_enabled", Convert.ToInt32(cleanup_device_information_network_adapters_history_enabled)); - cmd.Parameters.AddWithValue("@cleanup_device_information_network_adapters_history_days", cleanup_device_information_network_adapters_history_days); - cmd.Parameters.AddWithValue("@cleanup_device_information_ram_history_enabled", Convert.ToInt32(cleanup_device_information_ram_history_enabled)); - cmd.Parameters.AddWithValue("@cleanup_device_information_ram_history_days", cleanup_device_information_ram_history_days); - cmd.Parameters.AddWithValue("@cleanup_device_information_task_manager_history_enabled", Convert.ToInt32(cleanup_device_information_task_manager_history_enabled)); - cmd.Parameters.AddWithValue("@cleanup_device_information_task_manager_history_days", cleanup_device_information_task_manager_history_days); - cmd.Parameters.AddWithValue("@cleanup_events_history_enabled", Convert.ToInt32(cleanup_events_history_enabled)); - cmd.Parameters.AddWithValue("@cleanup_events_history_days", cleanup_events_history_days); - - Logging.Handler.Debug("Settings -> Save_Cleanup_Setting", "MySQL_Prepared_Query", query); - - await cmd.ExecuteNonQueryAsync(); - - Snackbar.Add("Cleanup settings saved!", Severity.Success); - } - catch (Exception ex) - { - Logging.Handler.Error("Settings -> Save_Cleanup_Settings", "MySQL_Query", ex.ToString()); - } - finally - { - await conn.CloseAsync(); - } - } - - private async Task Optimize_Database() - { - Snackbar.Configuration.ShowCloseIcon = true; - Snackbar.Configuration.PositionClass = Defaults.Classes.Position.BottomRight; - - loading_overlay = true; - StateHasChanged(); - - MySqlConnection conn = new MySqlConnection(Configuration.MySQL.Connection_String); - - string query = @"OPTIMIZE TABLE `accounts`; -OPTIMIZE TABLE agent_package_configurations; -OPTIMIZE TABLE antivirus_controlled_folder_access_rulesets; -OPTIMIZE TABLE applications_drivers_history; -OPTIMIZE TABLE applications_installed_history; -OPTIMIZE TABLE applications_logon_history; -OPTIMIZE TABLE applications_scheduled_tasks_history; -OPTIMIZE TABLE applications_services_history; -OPTIMIZE TABLE automations; -OPTIMIZE TABLE device_information_antivirus_products_history; -OPTIMIZE TABLE device_information_cpu_history; -OPTIMIZE TABLE device_information_cronjobs_history; -OPTIMIZE TABLE device_information_disks_history; -OPTIMIZE TABLE device_information_general_history; -OPTIMIZE TABLE device_information_history; -OPTIMIZE TABLE device_information_network_adapters_history; -OPTIMIZE TABLE device_information_notes_history; -OPTIMIZE TABLE device_information_ram_history; -OPTIMIZE TABLE device_information_remote_shell_history; -OPTIMIZE TABLE device_information_task_manager_history; -OPTIMIZE TABLE devices; -OPTIMIZE TABLE `events`; -OPTIMIZE TABLE files; -OPTIMIZE TABLE `groups`; -OPTIMIZE TABLE infrastructure_events; -OPTIMIZE TABLE jobs; -OPTIMIZE TABLE locations; -OPTIMIZE TABLE mail_notifications; -OPTIMIZE TABLE microsoft_teams_notifications; -OPTIMIZE TABLE ntfy_sh_notifications; -OPTIMIZE TABLE performance_monitoring_ressources; -OPTIMIZE TABLE policies; -OPTIMIZE TABLE scripts; -OPTIMIZE TABLE sensors; -OPTIMIZE TABLE servers; -OPTIMIZE TABLE `settings`; -OPTIMIZE TABLE support_history; -OPTIMIZE TABLE telegram_notifications; -OPTIMIZE TABLE tenants;"; - - try - { - await conn.OpenAsync(); - - MySqlCommand cmd = new MySqlCommand(query, conn); - - Logging.Handler.Debug("Settings -> Optimize_Database", "MySQL_Prepared_Query", query); - - await cmd.ExecuteNonQueryAsync(); - } - catch (Exception ex) - { - Logging.Handler.Error("Settings -> Optimize_Database", "MySQL_Query", ex.ToString()); - } - finally - { - await conn.CloseAsync(); - } - - loading_overlay = false; - StateHasChanged(); - - Snackbar.Add("Database optimized!", Severity.Success); - } - - private async Task Fix_Settings() - { - try - { - // smtp - string smtp = String.Empty; - smtp = await Classes.MySQL.Handler.Quick_Reader("SELECT * FROM settings;", "smtp"); - - // files api key - string files_api_key = String.Empty; - files_api_key = await Classes.MySQL.Handler.Quick_Reader("SELECT * FROM settings;", "files_api_key"); - - if (String.IsNullOrEmpty(files_api_key)) - { - // Generate random files api key if empty - if (String.IsNullOrEmpty(files_api_key)) - files_api_key = Guid.NewGuid().ToString() + "-" + Guid.NewGuid().ToString(); - - // Delete old settings - await Classes.MySQL.Handler.Execute_Command("DELETE FROM settings;"); - - // Add new settings - await Classes.MySQL.Handler.Execute_Command("INSERT INTO settings (db_version, files_api_key, smtp) VALUES ('" + Application_Settings.db_version + "', '" + files_api_key + "', '" + smtp + "');"); - } - } - catch (Exception ex) - { - Logging.Handler.Error("Settings -> Fix_Settings", "MySQL_Query", ex.ToString()); - } - } - - // Remote Screen Control - - private async Task SaveRemoteScreenSessionRecordingSettings() - { - Snackbar.Configuration.ShowCloseIcon = true; - Snackbar.Configuration.PositionClass = Defaults.Classes.Position.BottomRight; - - int remoteScreenSessionRecordingForcedDaysValue = remoteScreenSessionRecordingForcedDays; - - - // Save cleanup settings to database - MySqlConnection conn = new MySqlConnection(Configuration.MySQL.Connection_String); - - string query = @"UPDATE settings SET remote_screen_session_recording_forced_enabled = @remote_screen_session_recording_forced_enabled, remote_screen_session_recording_auto_clean_enabled = @remote_screen_session_recording_auto_clean_enabled, remote_screen_session_recording_forced_days = @remote_screen_session_recording_forced_days;"; - - try - { - await conn.OpenAsync(); - - MySqlCommand cmd = new MySqlCommand(query, conn); - cmd.Parameters.AddWithValue("@remote_screen_session_recording_forced_enabled", Convert.ToInt32(remoteScreenSessionRecordingForced)); - cmd.Parameters.AddWithValue("@remote_screen_session_recording_auto_clean_enabled", Convert.ToInt32(remoteScreenSessionRecordingAutoClean)); - cmd.Parameters.AddWithValue("@remote_screen_session_recording_forced_days", remoteScreenSessionRecordingForcedDays); - - Logging.Handler.Debug("Settings -> SaveRemoteScreenSessionRecordingSettings", "MySQL_Prepared_Query", query); - - await cmd.ExecuteNonQueryAsync(); - - Snackbar.Add("Remote screen control cleanup settings saved!", Severity.Success); - } - catch (Exception ex) - { - Logging.Handler.Error("Settings -> SaveRemoteScreenSessionRecordingSettings", "MySQL_Query", ex.ToString()); - } - finally - { - await conn.CloseAsync(); - } - } -} \ No newline at end of file +@page "/system" + +@using MySqlConnector; +@using System.Data.Common; +@using System.Text.Json; +@using System.Xml.Serialization; +@using System.Text; +@using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage; +@using Microsoft.AspNetCore.DataProtection; +@using System.Net.Http; +@using System.Net.Http.Headers; +@using System.Net; +@using System.Security.Claims +@using NetLock_RMM_Web_Console.Classes.Authentication + +@inject NavigationManager NavigationManager +@inject ILocalStorageService localStorage +@inject IDialogService DialogService +@inject ISnackbar Snackbar +@inject IJSRuntime JSRuntime +@inject IDataProtectionProvider DataProtectionProvider +@inject IStringLocalizer Localizer +@inject AuthenticationStateProvider AuthenticationStateProvider + + + + +
+ + + Please wait... 🐧 + +
+
+ + + + + + @if (Configuration.Members_Portal.IsCloudEnabled) + { + This section is administered by the cloud team. The view is limited. :-) + } + + @if (permissions_settings_enabled && permissions_settings_system_enabled) + { + + + @if (!Configuration.Members_Portal.IsCloudEnabled) + { + + + + + @Localizer["MySQL"] + + + + + + + @if (mysqlConnectionStatus) + { +
+ + @Localizer["connected"] +
+ + + Version: @mysqlVersion + @Localizer["uptime"]: @mysqlUptime (seconds) + @Localizer["active connections"]: @mysqlConnectedUsers + @Localizer["database size"]: @mysqlDatabaseSize + + + @Localizer["max connections"]: @mysqlMaxConnections + @Localizer["failed logins"]: @mysqlFailedLogins + } + else + { +
+ + @Localizer["not connected"] +
+ } +
+
+
+
+ + @Localizer["active queries"] + + + + + + + Id + User + Host + Database + Command + Time + State + Info + + + + + + +  @row.Id + + + + + +  @row.User + + + + + +  @row.Host + + + + + +  @row.Database + + + + + +  @row.Command + + + + + +  @row.Time + + + + + +  @row.State + + + + + +  @row.Info + + + + + + + + + +
+ + + @Localizer["Web Console"] + + + + + + + @Localizer["cpu usage"]: @cpuUsage% + + + + + + + + + + @Localizer["ram usage"]: @ramUsage% + + + + + + + + + + @Localizer["disk usage"]: @diskUsage% + + + + + + + + @*@foreach (var server in servers) + { + DateTime lastHeartbeat; + bool isRecent = false; + + try + { + // Try to parse the heartbeat timestamp + lastHeartbeat = DateTime.Parse(server.hearthbeat); + isRecent = (DateTime.Now - lastHeartbeat).TotalMinutes <= 10; + } + catch (Exception ex) + { + // Error handling for invalid or missing date + Logging.Handler.Error("Heartbeat-Parsing", "Error parsing server heartbeat", ex.ToString()); + } + + + @server.name (@server.os) @(server.docker == "1" ? " (Docker)" : String.Empty) + IP: @server.ip_address | Domain: @server.domain + + @Localizer["last hearthbeat"]: @server.hearthbeat + + + + + + + @Localizer["cpu usage"]: @server.cpu_usage% + + + + + + + + + + @Localizer["ram usage"]: @server.ram_usage% + + + + + + + + + + @Localizer["disk usage"]: @server.disk_usage% + + + + + + + + +
+ + @Localizer["appsettings.json"] +
+
+ + + + + +
+
+ } + *@ + + + + + @Localizer["NetLock RMM File Server"] (@Configuration.File_Server.Connection_String) + + + + + + +
+ @if (fileServerConnectionStatus) + { + + @Localizer["connected"] + } + else + { + + @Localizer["not connected"] + } +
+ +
+
+
+
+ +
+ + + @Localizer["NetLock RMM Remote Server"] (@Configuration.Remote_Server.Connection_String) + + + + + + + +
+ + @if (remoteServerConnectionStatus) + { + + @Localizer["connected"] + } + else + { + + @Localizer["not connected"] + } + +
+
+
+
+
+ + @if (!_dnsWarning && !Configuration.Members_Portal.IsCloudEnabled && !remoteServerConnectionStatus && !fileServerConnectionStatus) + { + PTR record mismatch detected! Your provided hostname resolves to: @_dnsWarningText but it should resolve to: @Configuration.Remote_Server.Hostname instead, according to your provided configuration. More details: https://docs.netlockrmm.com/en/troubleshooting/ptr-mismatch + } + + @Localizer["Check again"] + +
+ +
+ + } + @if (!Configuration.Members_Portal.IsCloudEnabled) + { + + + + + + + @Localizer["Members Portal Api"] + + + @if (!Configuration.Members_Portal.IsApiEnabled) + { + + @Localizer["members portal api is not enabled. Please enable it in the appsettings.json"] + + } + else + { + + + + API Konfiguration + + + The API key can only be changed in the appsettings.json. + + + + + + + + @Localizer["license status"] + + + + @(members_portal_status == "Active" ? Localizer["active"] : Localizer["expired"]) + + + + + + + + + Name: @members_portal_license_name + + + + + + +
+ @if (members_portal_code_signed) + { + +
+ + Code signing enabled + + + Your Windows agents are code signed + +
+ } + else + { + +
+ + Code Signing not available + + + Available from Tier 1 for professional deployments + + + + Upgrade + +
+ } +
+
+
+
+ + + + @Localizer["licenses used"]: + @members_portal_licenses_used / @members_portal_licenses_max + + + +
+ + + @Localizer["refresh license information"] + +
+
+ } + +
+
+ } + + + Automatic Agent Updates + + + Global Master Switches: These settings act as global master switches for automatic agent updates. +
β€’ When enabled here, you must also enable agent updates in the policy assigned to each device. +
β€’ Both the global switch (here) AND the policy setting must be enabled for updates to work. +
β€’ If disabled here, no automatic updates will occur regardless of policy settings. +
+ + Platform-specific Master Switches: + Windows + Linux + MacOS + + + + Concurrent Limit: + + + NetLock State-of-the-Art High-Performance Backend: +
β€’ NetLock uses a cutting-edge high-performance backend that loads and caches installation packages in RAM +
β€’ This state-of-the-art architecture means the primary limitation is your available network bandwidth, not CPU or memory +
β€’ Massive rollouts are possible with minimal resource impact on the backend server +
β€’ The concurrent limit mainly controls how many simultaneous network connections are used +
β€’ Perfect for enterprise-scale deployments with thousands of agents +
+ + + Controls concurrent agent installations & updates: +
β€’ Applies to both automatic agent updates AND initial agent installations via installer +
β€’ Limits how many agents can be installed/updated simultaneously by the backend +
β€’ Choose a value appropriate for your available bandwidth +
β€’ Lower values prevent bandwidth saturation and network congestion +
β€’ Higher values allow faster rollout if you have sufficient bandwidth available +
β€’ Note: Consider your MySQL database server resources, as each installation/update process writes to the database in the background +
+ + + + Save +
+
+ @if (!Configuration.Members_Portal.IsCloudEnabled) + { + + + Clean Up & Optimize Database + + Automatic cleanup of historical database entries to manage storage space and optimize performance. + + + + Important: Data Storage Considerations +
β€’ With many devices and short sync intervals (configured in policies), the database can grow to very large sizes +
β€’ Historical data accumulates rapidly and can consume significant disk storage +
β€’ Regular cleanup is essential to prevent database performance degradation and storage issues +
β€’ Consider your available disk space when setting retention periods +
+ + + + + + + Very high volume data. Historical data of installed drivers on devices. Recommended: 30-60 days. + + + + + + + + + + Very high volume data. Historical data of installed applications. Useful for tracking software changes. Recommended: 30-60 days. + + + + + + + + + + Very high volume data. User logon history and session data. Recommended: 30-60 days. + + + + + + + + + + Very high volume data. Historical data of scheduled tasks changes. Recommended: 30-60 days. + + + + + + + + + + Very high volume data. Historical data of Windows/Linux/macOS services. Recommended: 30-60 days. + + + + + + + + + + Very high volume data. Antivirus product status and configuration history. Recommended: 60-90 days. + + + + + + + + + + CPU usage metrics. High volume data with short sync intervals. Recommended: 30-90 days. + + + + + + + + + + Cronjob configuration history (Linux/macOS). Recommended: 90-180 days. + + + + + + + + + + Disk space usage metrics. High volume data. Recommended: 30-90 days. + + + + + + + + + + Very high volume data. General device information changes (hostname, OS version, etc.). Recommended: 60-90 days. + + + + + + + + + + Device configuration history snapshots. Recommended: 180-365 days. + + + + + + + + + + Network adapter configuration history. Recommended: 90-180 days. + + + + + + + + + + RAM usage metrics. High volume data with short sync intervals. Recommended: 30-90 days. + + + + + + + + + + Extremely high volume data! Running processes history. Can grow rapidly with short sync intervals. Recommended: 7-30 days. + + + + + + + + + + Critical: Sensor events, job execution logs, and system events. Minimum recommended: 365 days for compliance and audit purposes. + + + + + + + + Save + Optimize Database +
+
+ } + + + + Remote Screen Control Recording + + When enabled, all remote screen control sessions will be recorded by the server. + + + + @if (Configuration.Members_Portal.IsCloudEnabled) + { + Your recordings will be kept for seven days. + } + else + { + + } + + Save + + + + + + + + + + IP Whitelist for Web Console + + + Configure IP addresses that are allowed to access the Web Console. Enter IP addresses separated by commas.
+ Leave empty to allow access from all IPs. The application will restart after saving. +
+ + + + @if (!string.IsNullOrEmpty(ipWhitelistWebConsoleValidationError)) + { + + @ipWhitelistWebConsoleValidationError + + } + + Save & Restart +
+
+ + + + + IP Whitelist for Backend + + + Configure IP addresses that are allowed to access the Backend services. Enter IP addresses separated by commas.
+ Leave empty to allow access from all IPs. This configuration will be applied when the backend services restart. +
+ + + + @if (!string.IsNullOrEmpty(ipWhitelistBackendValidationError)) + { + + @ipWhitelistBackendValidationError + + } + + Save +
+
+ +
+
+ + + + Single Sign-On Configuration + + @if (Configuration.Sso.ConfigurationError) + { + + SSO Configuration Error!
+ The SSO configuration could not be loaded during application startup and SSO is currently DISABLED.
+
+ Error: @Configuration.Sso.ConfigurationErrorMessage
+
+ Please verify your SSO configuration below and save it. The application will automatically restart after saving. +
+ } + + + You can only enable one SSO provider at a time. Enabling a new provider will automatically disable the others. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Save +
+
+ + + + + + + + + Configure the title that will be displayed in the web console.
+ Changes will apply after the web console application has been restarted. +
+ + + + @if (!string.IsNullOrEmpty(webConsoleTitleValidationError)) + { + + @webConsoleTitleValidationError + + } + + Save & Restart +
+
+ + + + + + Upload a custom logo for the web console. Recommended size: 200x50 pixels (PNG or JPG).
+ Changes will apply after the web console application has been restarted. +
+ + + @if (!string.IsNullOrEmpty(webConsoleLogoBase64)) + { + Current Logo: + + Current Logo + + } + + + + + + Choose Logo File + + + + + @if (!string.IsNullOrEmpty(selectedLogoFileName)) + { + Selected file: @selectedLogoFileName + } + + + @if (!string.IsNullOrEmpty(webConsoleLogoPreviewBase64)) + { + Preview: + + Logo Preview + + } + + @if (!string.IsNullOrEmpty(webConsoleLogoValidationError)) + { + + @webConsoleLogoValidationError + + } + + + + Save & Restart + + + @if (!string.IsNullOrEmpty(webConsoleLogoBase64)) + { + + Remove Logo & Restart + + } + +
+
+ + + + + + Customize the color palette for both light and dark modes. Use hex color codes (e.g., #FF0000 for red).
+ Changes will apply after the web console application has been restarted. +
+ + + + + + + + Primary Colors + Main brand colors used for buttons, links, and primary UI elements. Contrast text colors ensure readability. + + + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+
+ + + + Background & Surface + Background colors for the main page and surface colors for cards, dialogs, and elevated components. + + + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+
+ + + + AppBar & Drawer + Colors for the top app bar and navigation drawer (sidebar). Controls the background, text, and icon colors. + + + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+
+ + + + Text Colors + Text colors for different hierarchy levels. Primary for main content, secondary for less important text, disabled for inactive elements. + + + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+
+ + + + Action Colors + Colors for interactive elements like icons and buttons in their default and disabled states. + + + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+
+ + + + Status Colors + Semantic colors for alerts, notifications, and status indicators. Info (blue), Success (green), Warning (orange), Error (red), and Dark mode accent. + + + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+
+ + + + Lines & Dividers + Border colors for input fields, dividers, and table elements. Controls the visual separation between UI components. + + + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+
+ + + + Gray Scale + Grayscale palette from black to white with various gray shades. Used for neutral UI elements and subtle backgrounds. + + + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+
+ + + + Overlay & Hover + Overlay colors for modals and dialogs. Hover opacity controls the transparency of hover effects on interactive elements. + + + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+ + + +
+
+
+ + + + + + Primary Colors + Main brand colors used for buttons, links, and primary UI elements. Contrast text colors ensure readability. + + + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+
+ + + + Background & Surface + Background colors for the main page and surface colors for cards, dialogs, and elevated components. + + + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+
+ + + + AppBar & Drawer + Colors for the top app bar and navigation drawer (sidebar). Controls the background, text, and icon colors. + + + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+
+ + + + Text Colors + Text colors for different hierarchy levels. Primary for main content, secondary for less important text, disabled for inactive elements. + + + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+
+ + + + Action Colors + Colors for interactive elements like icons and buttons in their default and disabled states. + + + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+
+ + + + Status Colors + Semantic colors for alerts, notifications, and status indicators. Info (blue), Success (green), Warning (orange), Error (red), and Dark mode accent. + + + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+
+ + + + Lines & Dividers + Border colors for input fields, dividers, and table elements. Controls the visual separation between UI components. + + + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+
+ + + + Gray Scale + Grayscale palette from black to white with various gray shades. Used for neutral UI elements and subtle backgrounds. + + + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+
+ + + + Overlay & Hover + Overlay colors for modals and dialogs. Hover opacity controls the transparency of hover effects on interactive elements. + + + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+ + + +
+
+
+ +
+ + @if (!string.IsNullOrEmpty(themePaletteValidationError)) + { + + @themePaletteValidationError + + } + + + + Save & Restart + + + + Reset to Default + + +
+
+ +
+
+ +
+ } + +
+ +
+ +@code { + + #region Permissions System + + private string permissions_json = String.Empty; + + private bool permissions_settings_enabled = false; + private bool permissions_settings_system_enabled = false; + + private async Task Permissions() + { + try + { + bool logout = false; + + // Get the current user from the authentication state + var user = (await AuthenticationStateProvider.GetAuthenticationStateAsync()).User; + + // Check if user is authenticated + if (user?.Identity is not { IsAuthenticated: true }) + logout = true; + + string netlock_username = user.FindFirst(ClaimTypes.Email)?.Value; + + permissions_settings_enabled = await Classes.Authentication.Permissions.Verify_Permission(netlock_username, "settings_enabled"); + permissions_settings_system_enabled = await Classes.Authentication.Permissions.Verify_Permission(netlock_username, "settings_system_enabled"); + + if (!permissions_settings_enabled) + logout = true; + + if (logout) // Redirect to the login page + { + NavigationManager.NavigateTo("/logout", true); + return false; + } + + // All fine? Nice. + return true; + } + catch (Exception ex) + { + Logging.Handler.Error("/dashboard -> Permissions", "Error", ex.ToString()); + return false; + } + } + + #endregion + + private bool loading_overlay = false; + + private bool _isDarkMode = false; + private bool _dnsWarning = false; + private string _dnsWarningText = String.Empty; + + protected override async Task OnInitializedAsync() + { + await base.OnInitializedAsync(); + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + await AfterInitializedAsync(); + } + } + + private async Task AfterInitializedAsync() + { + if (!await Permissions()) + return; + + loading_overlay = true; + StateHasChanged(); + + _isDarkMode = await JSRuntime.InvokeAsync("isDarkMode"); + + await MySQL_Status(); + await Fix_Settings(); // this is a hotfix code to verify if a bug is present in the database + //await Get_Servers(); + await Get_Agent_Updates(); + + if (Configuration.Members_Portal.IsApiEnabled) + await Get_Members_Portal_License_Status(); + + // Get CPU Usage + cpuUsage = await Classes.System_Information.Handler.Get_CPU_Usage(); + + // Get RAM Usage + ramUsage = await Classes.System_Information.Handler.Get_RAM_Usage(); + + // Get Disk Usage + diskUsage = await Classes.System_Information.Handler.Get_Disk_Usage(); + + fileServerConnectionStatus = await Classes.Helper.Networking.Test_Connection(Configuration.File_Server.Connection_String + "/test"); + + remoteServerConnectionStatus = await Classes.Helper.Networking.Test_Connection(Configuration.Remote_Server.Connection_String + "/test"); + + await Check_DNS(); + + await Get_Cleanup_Settings(); + + await Get_Sso_Settings(); + + await Get_Ip_Whitelist(); + + await Get_Whitelabeling_Settings(); + + loading_overlay = false; + + StateHasChanged(); + } + + private bool mysqlConnectionStatus = false; + private bool fileServerConnectionStatus = false; + private bool remoteServerConnectionStatus = false; + + private int cpuUsage = 0; + private int ramUsage = 0; + private int diskUsage = 0; + + private string mysqlVersion = String.Empty; + private string mysqlUptime = String.Empty; + private string mysqlConnectedUsers = String.Empty; + private string mysqlDatabaseSize = String.Empty; + private List mysqlActiveQueries = new List(); + private bool mysqlSSLEnabled = false; + private string mysqlFailedLogins = String.Empty; + private string mysqlPrivilegedUsers = String.Empty; + private string mysqlMaxConnections = String.Empty; + + // Agent Update Settings + private bool agent_updates_windows_enabled = false; + private bool agent_updates_linux_enabled = false; + private bool agent_updates_macos_enabled = false; + private int agent_updates_max_concurrent_updates = 0; + + // Members Portal + private string members_portal_api_key = String.Empty; + private string members_portal_status = String.Empty; + private string members_portal_license_name = String.Empty; + private int members_portal_licenses_used = 0; + private int members_portal_licenses_max = 0; + private bool members_portal_licenses_hard_limit = false; + private bool members_portal_code_signed = false; + + private string package_url = String.Empty; + + // Remote Screen Control + private bool remoteScreenSessionRecordingForced = false; + private bool remoteScreenSessionRecordingAutoClean = false; + private int remoteScreenSessionRecordingForcedDays = 0; + + // SSO Configuration + // Azure AD + private bool azureAdEnabled = false; + private string azureAdInstance = string.Empty; + private string azureAdDomain = string.Empty; + private string azureAdTenantId = string.Empty; + private string azureAdClientId = string.Empty; + private string azureAdClientSecret = string.Empty; + private string azureAdCallbackPath = "/signin-oidc"; + private string azureAdSignedOutCallbackPath = "/signout-callback-oidc"; + private string azureAdResponseType = "code"; + private bool azureAdSaveTokens = false; + + // Keycloak + private bool keycloakEnabled = false; + private string keycloakAuthority = string.Empty; + private string keycloakRealm = string.Empty; + private string keycloakClientId = string.Empty; + private string keycloakClientSecret = string.Empty; + private string keycloakCallbackPath = "/signin-keycloak"; + private string keycloakSignedOutCallbackPath = "/signout-callback-keycloak"; + private string keycloakResponseType = "code"; + private bool keycloakSaveTokens = false; + private bool keycloakGetClaimsFromUserInfoEndpoint = true; + private bool keycloakRequireHttpsMetadata = true; + + // Google Identity + private bool googleEnabled = false; + private string googleClientId = string.Empty; + private string googleClientSecret = string.Empty; + private string googleCallbackPath = "/signin-google"; + private string googleSignedOutCallbackPath = "/signout-callback-google"; + private string googleHostedDomain = string.Empty; + private bool googleSaveTokens = false; + + // Okta + private bool oktaEnabled = false; + private string oktaDomain = string.Empty; + private string oktaClientId = string.Empty; + private string oktaClientSecret = string.Empty; + private string oktaCallbackPath = "/signin-okta"; + private string oktaSignedOutCallbackPath = "/signout-callback-okta"; + private string oktaAuthorizationServerId = "default"; + private bool oktaSaveTokens = false; + private bool oktaGetClaimsFromUserInfoEndpoint = true; + + // Auth0 + private bool auth0Enabled = false; + private string auth0Domain = string.Empty; + private string auth0ClientId = string.Empty; + private string auth0ClientSecret = string.Empty; + private string auth0CallbackPath = "/signin-auth0"; + private string auth0SignedOutCallbackPath = "/signout-callback-auth0"; + private string auth0Audience = string.Empty; + private bool auth0SaveTokens = false; + + // IP Whitelist + private string ipWhitelistWebConsoleInput = string.Empty; + private string ipWhitelistWebConsoleValidationError = string.Empty; + private string ipWhitelistBackendInput = string.Empty; + private string ipWhitelistBackendValidationError = string.Empty; + + // Whitelabeling + private string webConsoleTitleInput = string.Empty; + private string webConsoleTitleValidationError = string.Empty; + + private string webConsoleLogoBase64 = string.Empty; + private string webConsoleLogoPreviewBase64 = string.Empty; + private string webConsoleLogoValidationError = string.Empty; + private string selectedLogoFileName = string.Empty; + private IBrowserFile? selectedLogoFile = null; + + // Theme Palette + private Classes.Theme.ThemePaletteConfig themePaletteConfig = new(); + private string themePaletteValidationError = string.Empty; + + public class MySQL_Active_Queries + { + public int Id { get; set; } + public string User { get; set; } + public string Host { get; set; } + public string Database { get; set; } + public string Command { get; set; } + public int Time { get; set; } + public string State { get; set; } + public string Info { get; set; } + } + + private bool MySQL_Active_Queries_Table_Filter_Func(MySQL_Active_Queries row) + { + try + { + if (row == null) return false; // ÜberprΓΌfe, ob row null ist + + if (string.IsNullOrEmpty(mysql_active_queries_table_search_string)) + return true; + + return row.Id.ToString().Contains(mysql_active_queries_table_search_string, StringComparison.OrdinalIgnoreCase) || + (row.User?.Contains(mysql_active_queries_table_search_string, StringComparison.OrdinalIgnoreCase) ?? false) || + (row.Host?.Contains(mysql_active_queries_table_search_string, StringComparison.OrdinalIgnoreCase) ?? false) || + (row.Database?.Contains(mysql_active_queries_table_search_string, StringComparison.OrdinalIgnoreCase) ?? false) || + (row.Command?.Contains(mysql_active_queries_table_search_string, StringComparison.OrdinalIgnoreCase) ?? false) || + row.Time.ToString().Contains(mysql_active_queries_table_search_string, StringComparison.OrdinalIgnoreCase) || + (row.State?.Contains(mysql_active_queries_table_search_string, StringComparison.OrdinalIgnoreCase) ?? false) || + (row.Info?.Contains(mysql_active_queries_table_search_string, StringComparison.OrdinalIgnoreCase) ?? false); + } + catch (Exception ex) + { + Logging.Handler.Error("/system -> MySQL_Active_Queries_Table_Filter_Func", "general_error", ex.ToString()); + return false; + } + } + + private string mysql_active_queries_table_sorted_column; + private string mysql_active_queries_table_search_string = String.Empty; + private string mysql_active_queries_selectedRowContent = ""; // Saving content of selected row + + // Executes on row click + private void mysql_active_queries_RowClickHandler(MySQL_Active_Queries row) + { + mysql_active_queries_selectedRowContent = row.Id.ToString(); + } + + private string mysql_active_queries_GetRowClass(MySQL_Active_Queries row) + { + return row.Id.ToString() == mysql_active_queries_selectedRowContent ? "selected-row" : ""; + } + + // MySQL + + private string GetCommandColor(string command, int time) + { + switch (command) + { + case "Daemon": + return "blue"; + case "Sleep": + return "orange"; + case "Query": + return time > 60 ? "red" : "green"; // Red for long queries, green for short + default: + return "inherit"; // Standard colour + } + } + + private async Task MySQL_Status() + { + try + { + if (await Classes.MySQL.Database.Check_Connection()) + { + mysqlConnectionStatus = true; + + // Get MySQL Version + mysqlVersion = await Classes.MySQL.Database.Get_Version(); + + // Get MySQL Uptime + mysqlUptime = await Classes.MySQL.Database.Get_Uptime(); + + // Get MySQL Connected Users + mysqlConnectedUsers = await Classes.MySQL.Database.Get_Connected_Users(); + + // Get MySQL Active Queries + string mysqlActiveQueriesJson = await Classes.MySQL.Database.Get_Active_Queries(); + Logging.Handler.Debug("/system -> MySQL_Status", "mysqlActiveQueriesJson", mysqlActiveQueriesJson); + mysqlActiveQueries = JsonSerializer.Deserialize>(mysqlActiveQueriesJson); + + // Get MySQL Failed Logins + mysqlFailedLogins = await Classes.MySQL.Database.Get_Failed_Logins(); + + // Get MySQL Max Connections + mysqlMaxConnections = await Classes.MySQL.Database.Get_Max_Connections(); + + // Get MySQL Database Size + mysqlDatabaseSize = await Classes.MySQL.Database.Get_Database_Size(); + } + else + { + mysqlConnectionStatus = false; + } + } + catch (Exception ex) + { + Logging.Handler.Error("/system -> MySQL_Status", "general_error", ex.ToString()); + } + + } + + // Servers overview + /*private List servers = new List(); + + public class Servers + { + public string id { get; set; } + public string name { get; set; } + public string ip_address { get; set; } + public string domain { get; set; } + public string os { get; set; } + public string hearthbeat { get; set; } + public string appsettings { get; set; } + public string cpu_usage { get; set; } + public string ram_usage { get; set; } + public string disk_usage { get; set; } + public string docker { get; set; } + } + + // Get servers from database + private async Task Get_Servers() + { + servers.Clear(); + + MySqlConnection conn = new MySqlConnection(Configuration.MySQL.Connection_String); + + try + { + await conn.OpenAsync(); + + MySqlCommand command = new MySqlCommand("SELECT * FROM servers;", conn); + using (DbDataReader reader = await command.ExecuteReaderAsync()) + { + if (reader.HasRows) + { + while (await reader.ReadAsync()) + { + servers.Add(new Servers + { + id = reader["id"].ToString() ?? String.Empty, + name = reader["name"].ToString() ?? String.Empty, + ip_address = reader["ip_address"].ToString() ?? String.Empty, + domain = reader["domain"].ToString() ?? String.Empty, + os = reader["os"].ToString() ?? String.Empty, + hearthbeat = reader["hearthbeat"].ToString() ?? String.Empty, + appsettings = reader["appsettings"].ToString() ?? String.Empty, + cpu_usage = reader["cpu_usage"].ToString() ?? String.Empty, + ram_usage = reader["ram_usage"].ToString() ?? String.Empty, + disk_usage = reader["disk_usage"].ToString() ?? String.Empty, + docker = reader["docker"].ToString() ?? String.Empty + }); + } + } + } + } + catch (Exception ex) + { + Logging.Handler.Error("/system -> Get_Servers", "general_error", ex.ToString()); + } + finally + { + conn.Close(); + } + } + */ + + // Save api key for members portal + //OSSCH_START 241c58a5-a702-4e08-b9a4-e6ef92d78eb6 //OSSCH_END + + // Update members portal license information + //OSSCH_START 1ef1c6ff-2e03-4f9e-a5c1-6ff0f46b48c4 //OSSCH_END + + // Get members portal license status + //OSSCH_START b373b616-f894-410a-af37-ed9eee36115f //OSSCH_END + + // Get agent updates settings + private async Task Get_Agent_Updates() + { + string windows_updates_enabled = await Classes.MySQL.Handler.Quick_Reader("SELECT * FROM settings", "agent_updates_windows_enabled"); + string linux_updates_enabled = await Classes.MySQL.Handler.Quick_Reader("SELECT * FROM settings", "agent_updates_linux_enabled"); + string macos_updates_enabled = await Classes.MySQL.Handler.Quick_Reader("SELECT * FROM settings", "agent_updates_macos_enabled"); + + if (windows_updates_enabled == "1") + agent_updates_windows_enabled = true; + else + agent_updates_windows_enabled = false; + + if (linux_updates_enabled == "1") + agent_updates_linux_enabled = true; + else + agent_updates_linux_enabled = false; + + if (macos_updates_enabled == "1") + agent_updates_macos_enabled = true; + else + agent_updates_macos_enabled = false; + + agent_updates_max_concurrent_updates = Convert.ToInt32(await Classes.MySQL.Handler.Quick_Reader("SELECT * FROM settings", "agent_updates_max_concurrent_updates")); + } + + // Set agent updates settings + private async Task SaveAgentUpdates() + { + Snackbar.Configuration.ShowCloseIcon = true; + Snackbar.Configuration.PositionClass = Defaults.Classes.Position.BottomRight; + + if (agent_updates_windows_enabled) + { + Logging.Handler.Debug("/system -> Set_Agent_Updates", "agent_updates_windows_enabled", "true"); + await Classes.MySQL.Handler.Execute_Command("UPDATE settings SET agent_updates_windows_enabled = 1;"); + } + else + { + Logging.Handler.Debug("/system -> Set_Agent_Updates", "agent_updates_windows_enabled", "false"); + await Classes.MySQL.Handler.Execute_Command("UPDATE settings SET agent_updates_windows_enabled = 0;"); + } + + if (agent_updates_linux_enabled) + { + Logging.Handler.Debug("/system -> Set_Agent_Updates", "agent_updates_linux_enabled", "true"); + await Classes.MySQL.Handler.Execute_Command("UPDATE settings SET agent_updates_linux_enabled = 1;"); + } + else + { + Logging.Handler.Debug("/system -> Set_Agent_Updates", "agent_updates_linux_enabled", "false"); + await Classes.MySQL.Handler.Execute_Command("UPDATE settings SET agent_updates_linux_enabled = 0;"); + } + + if (agent_updates_macos_enabled) + { + Logging.Handler.Debug("/system -> Set_Agent_Updates", "agent_updates_macos_enabled", "true"); + await Classes.MySQL.Handler.Execute_Command("UPDATE settings SET agent_updates_macos_enabled = 1;"); + } + else + { + Logging.Handler.Debug("/system -> Set_Agent_Updates", "agent_updates_macos_enabled", "false"); + await Classes.MySQL.Handler.Execute_Command("UPDATE settings SET agent_updates_macos_enabled = 0;"); + } + + MySqlConnection conn = new MySqlConnection(Configuration.MySQL.Connection_String); + + string query = "UPDATE settings SET agent_updates_max_concurrent_updates = @agent_updates_max_concurrent_updates;"; + + try + { + await conn.OpenAsync(); + + MySqlCommand cmd = new MySqlCommand(query, conn); + cmd.Parameters.AddWithValue("@agent_updates_max_concurrent_updates", agent_updates_max_concurrent_updates.ToString()); + + Logging.Handler.Debug("SaveAgentUpdates", "MySQL_Prepared_Query", query); + + await cmd.ExecuteNonQueryAsync(); + } + catch (Exception ex) + { + Logging.Handler.Error("SaveAgentUpdates", "MySQL_Query", ex.ToString()); + } + finally + { + await conn.CloseAsync(); + } + + Snackbar.Add("Saved.", Severity.Success); + } + + // Check the dns ptr record + private async Task Check_DNS() + { + (_dnsWarning, _dnsWarningText) = await Classes.Setup.DNS.Check_Dns_Forward_Reverse(Configuration.Remote_Server.Hostname); + } + + // Check connection + private async Task Test_Remote_File_Server_Connection() + { + Snackbar.Configuration.ShowCloseIcon = true; + Snackbar.Configuration.PositionClass = Defaults.Classes.Position.BottomRight; + + loading_overlay = true; + fileServerConnectionStatus = await Classes.Helper.Networking.Test_Connection(Configuration.File_Server.Connection_String + "/test"); + + remoteServerConnectionStatus = await Classes.Helper.Networking.Test_Connection(Configuration.Remote_Server.Connection_String + "/test"); + + await Check_DNS(); + + loading_overlay = false; + + StateHasChanged(); + + if (fileServerConnectionStatus && remoteServerConnectionStatus) + { + Snackbar.Add(Localizer["connected"] + ".", Severity.Success); + } + else + { + Snackbar.Add(Localizer["not connected"] + ".", Severity.Error); + } + } + + // Automatic cleanup + private bool cleanup_applications_drivers_history_enabled = false; + private int cleanup_applications_drivers_history_days = 0; + + private bool cleanup_applications_installed_history_enabled = false; + private int cleanup_applications_installed_history_days = 0; + + private bool cleanup_applications_logon_history_enabled = false; + private int cleanup_applications_logon_history_days = 0; + + private bool cleanup_applications_scheduled_tasks_history_enabled = false; + private int cleanup_applications_scheduled_tasks_history_days = 0; + + private bool cleanup_applications_services_history_enabled = false; + private int cleanup_applications_services_history_days = 0; + + private bool cleanup_device_information_antivirus_products_history_enabled = false; + private int cleanup_device_information_antivirus_products_history_days = 0; + + private bool cleanup_device_information_cpu_history_enabled = false; + private int cleanup_device_information_cpu_history_days = 0; + + private bool cleanup_device_information_cronjobs_history_enabled = false; + private int cleanup_device_information_cronjobs_history_days = 0; + + private bool cleanup_device_information_disks_history_enabled = false; + private int cleanup_device_information_disks_history_days = 0; + + private bool cleanup_device_information_general_history_enabled = false; + private int cleanup_device_information_general_history_days = 0; + + private bool cleanup_device_information_history_enabled = false; + private int cleanup_device_information_history_days = 0; + + private bool cleanup_device_information_network_adapters_history_enabled = false; + private int cleanup_device_information_network_adapters_history_days = 0; + + private bool cleanup_device_information_ram_history_enabled = false; + private int cleanup_device_information_ram_history_days = 0; + + private bool cleanup_device_information_task_manager_history_enabled = false; + private int cleanup_device_information_task_manager_history_days = 0; + + private bool cleanup_events_history_enabled = false; + private int cleanup_events_history_days = 0; + + // Get cleanup settings from database + private async Task Get_Cleanup_Settings() + { + MySqlConnection conn = new MySqlConnection(Configuration.MySQL.Connection_String); + + try + { + await conn.OpenAsync(); + + string query = "SELECT * FROM settings;"; + + MySqlCommand cmd = new MySqlCommand(query, conn); + + Logging.Handler.Debug("Example", "MySQL_Prepared_Query", query); + + using (DbDataReader reader = await cmd.ExecuteReaderAsync()) + { + if (reader.HasRows) + { + while (await reader.ReadAsync()) + { + cleanup_applications_drivers_history_enabled = reader.GetBoolean(reader.GetOrdinal("cleanup_applications_drivers_history_enabled")); + cleanup_applications_drivers_history_days = reader.GetInt32(reader.GetOrdinal("cleanup_applications_drivers_history_days")); + + cleanup_applications_installed_history_enabled = reader.GetBoolean(reader.GetOrdinal("cleanup_applications_installed_history_enabled")); + cleanup_applications_installed_history_days = reader.GetInt32(reader.GetOrdinal("cleanup_applications_installed_history_days")); + + cleanup_applications_logon_history_enabled = reader.GetBoolean(reader.GetOrdinal("cleanup_applications_logon_history_enabled")); + cleanup_applications_logon_history_days = reader.GetInt32(reader.GetOrdinal("cleanup_applications_logon_history_days")); + + cleanup_applications_scheduled_tasks_history_enabled = reader.GetBoolean(reader.GetOrdinal("cleanup_applications_scheduled_tasks_history_enabled")); + cleanup_applications_scheduled_tasks_history_days = reader.GetInt32(reader.GetOrdinal("cleanup_applications_scheduled_tasks_history_days")); + + cleanup_applications_services_history_enabled = reader.GetBoolean(reader.GetOrdinal("cleanup_applications_services_history_enabled")); + cleanup_applications_services_history_days = reader.GetInt32(reader.GetOrdinal("cleanup_applications_services_history_days")); + + cleanup_device_information_antivirus_products_history_enabled = reader.GetBoolean(reader.GetOrdinal("cleanup_device_information_antivirus_products_history_enabled")); + cleanup_device_information_antivirus_products_history_days = reader.GetInt32(reader.GetOrdinal("cleanup_device_information_antivirus_products_history_days")); + + cleanup_device_information_cpu_history_enabled = reader.GetBoolean(reader.GetOrdinal("cleanup_device_information_cpu_history_enabled")); + cleanup_device_information_cpu_history_days = reader.GetInt32(reader.GetOrdinal("cleanup_device_information_cpu_history_days")); + + cleanup_device_information_cronjobs_history_enabled = reader.GetBoolean(reader.GetOrdinal("cleanup_device_information_cronjobs_history_enabled")); + cleanup_device_information_cronjobs_history_days = reader.GetInt32(reader.GetOrdinal("cleanup_device_information_cronjobs_history_days")); + + cleanup_device_information_disks_history_enabled = reader.GetBoolean(reader.GetOrdinal("cleanup_device_information_disks_history_enabled")); + cleanup_device_information_disks_history_days = reader.GetInt32(reader.GetOrdinal("cleanup_device_information_disks_history_days")); + + cleanup_device_information_general_history_enabled = reader.GetBoolean(reader.GetOrdinal("cleanup_device_information_general_history_enabled")); + cleanup_device_information_general_history_days = reader.GetInt32(reader.GetOrdinal("cleanup_device_information_general_history_days")); + + cleanup_device_information_history_enabled = reader.GetBoolean(reader.GetOrdinal("cleanup_device_information_history_enabled")); + cleanup_device_information_history_days = reader.GetInt32(reader.GetOrdinal("cleanup_device_information_history_days")); + + cleanup_device_information_network_adapters_history_enabled = reader.GetBoolean(reader.GetOrdinal("cleanup_device_information_network_adapters_history_enabled")); + cleanup_device_information_network_adapters_history_days = reader.GetInt32(reader.GetOrdinal("cleanup_device_information_network_adapters_history_days")); + + cleanup_device_information_ram_history_enabled = reader.GetBoolean(reader.GetOrdinal("cleanup_device_information_ram_history_enabled")); + cleanup_device_information_ram_history_days = reader.GetInt32(reader.GetOrdinal("cleanup_device_information_ram_history_days")); + + cleanup_device_information_task_manager_history_enabled = reader.GetBoolean(reader.GetOrdinal("cleanup_device_information_task_manager_history_enabled")); + cleanup_device_information_task_manager_history_days = reader.GetInt32(reader.GetOrdinal("cleanup_device_information_task_manager_history_days")); + + cleanup_events_history_enabled = reader.GetBoolean(reader.GetOrdinal("cleanup_events_history_enabled")); + cleanup_events_history_days = reader.GetInt32(reader.GetOrdinal("cleanup_events_history_days")); + + remoteScreenSessionRecordingForced = reader.GetBoolean(reader.GetOrdinal("remote_screen_session_recording_forced_enabled")); + remoteScreenSessionRecordingAutoClean = reader.GetBoolean(reader.GetOrdinal("remote_screen_session_recording_auto_clean_enabled")); + remoteScreenSessionRecordingForcedDays = reader.GetInt32(reader.GetOrdinal("remote_screen_session_recording_forced_days")); + } + } + } + } + catch (Exception ex) + { + Logging.Handler.Error("Settings -> Get_Cleanup_Settings", "MySQL_Query", ex.ToString()); + } + finally + { + await conn.CloseAsync(); + } + } + + // Save cleanup settings to database (settings) + private async Task Save_Cleanup_Settings() + { + Snackbar.Configuration.ShowCloseIcon = true; + Snackbar.Configuration.PositionClass = Defaults.Classes.Position.BottomRight; + + // Save cleanup settings to database + MySqlConnection conn = new MySqlConnection(Configuration.MySQL.Connection_String); + + string query = @"UPDATE settings SET +cleanup_applications_drivers_history_enabled = @cleanup_applications_drivers_history_enabled, +cleanup_applications_drivers_history_days = @cleanup_applications_drivers_history_days, +cleanup_applications_installed_history_enabled = @cleanup_applications_installed_history_enabled, +cleanup_applications_installed_history_days = @cleanup_applications_installed_history_days, +cleanup_applications_logon_history_enabled = @cleanup_applications_logon_history_enabled, +cleanup_applications_logon_history_days = @cleanup_applications_logon_history_days, +cleanup_applications_scheduled_tasks_history_enabled = @cleanup_applications_scheduled_tasks_history_enabled, +cleanup_applications_scheduled_tasks_history_days = @cleanup_applications_scheduled_tasks_history_days, +cleanup_applications_services_history_enabled = @cleanup_applications_services_history_enabled, +cleanup_applications_services_history_days = @cleanup_applications_services_history_days, +cleanup_device_information_antivirus_products_history_enabled = @cleanup_device_information_antivirus_products_history_enabled, +cleanup_device_information_antivirus_products_history_days = @cleanup_device_information_antivirus_products_history_days, +cleanup_device_information_cpu_history_enabled = @cleanup_device_information_cpu_history_enabled, +cleanup_device_information_cpu_history_days = @cleanup_device_information_cpu_history_days, +cleanup_device_information_cronjobs_history_enabled = @cleanup_device_information_cronjobs_history_enabled, +cleanup_device_information_cronjobs_history_days = @cleanup_device_information_cronjobs_history_days, +cleanup_device_information_disks_history_enabled = @cleanup_device_information_disks_history_enabled, +cleanup_device_information_disks_history_days = @cleanup_device_information_disks_history_days, +cleanup_device_information_general_history_enabled = @cleanup_device_information_general_history_enabled, +cleanup_device_information_general_history_days = @cleanup_device_information_general_history_days, +cleanup_device_information_history_enabled = @cleanup_device_information_history_enabled, +cleanup_device_information_history_days = @cleanup_device_information_history_days, +cleanup_device_information_network_adapters_history_enabled = @cleanup_device_information_network_adapters_history_enabled, +cleanup_device_information_network_adapters_history_days = @cleanup_device_information_network_adapters_history_days, +cleanup_device_information_ram_history_enabled = @cleanup_device_information_ram_history_enabled, +cleanup_device_information_ram_history_days = @cleanup_device_information_ram_history_days, +cleanup_device_information_task_manager_history_enabled = @cleanup_device_information_task_manager_history_enabled, +cleanup_device_information_task_manager_history_days = @cleanup_device_information_task_manager_history_days, +cleanup_events_history_enabled = @cleanup_events_history_enabled, +cleanup_events_history_days = @cleanup_events_history_days;"; + + try + { + await conn.OpenAsync(); + + MySqlCommand cmd = new MySqlCommand(query, conn); + cmd.Parameters.AddWithValue("@cleanup_applications_drivers_history_enabled", Convert.ToInt32(cleanup_applications_drivers_history_enabled)); + cmd.Parameters.AddWithValue("@cleanup_applications_drivers_history_days", cleanup_applications_drivers_history_days); + cmd.Parameters.AddWithValue("@cleanup_applications_installed_history_enabled", Convert.ToInt32(cleanup_applications_installed_history_enabled)); + cmd.Parameters.AddWithValue("@cleanup_applications_installed_history_days", cleanup_applications_installed_history_days); + cmd.Parameters.AddWithValue("@cleanup_applications_logon_history_enabled", Convert.ToInt32(cleanup_applications_logon_history_enabled)); + cmd.Parameters.AddWithValue("@cleanup_applications_logon_history_days", cleanup_applications_logon_history_days); + cmd.Parameters.AddWithValue("@cleanup_applications_scheduled_tasks_history_enabled", Convert.ToInt32(cleanup_applications_scheduled_tasks_history_enabled)); + cmd.Parameters.AddWithValue("@cleanup_applications_scheduled_tasks_history_days", cleanup_applications_scheduled_tasks_history_days); + cmd.Parameters.AddWithValue("@cleanup_applications_services_history_enabled", Convert.ToInt32(cleanup_applications_services_history_enabled)); + cmd.Parameters.AddWithValue("@cleanup_applications_services_history_days", cleanup_applications_services_history_days); + cmd.Parameters.AddWithValue("@cleanup_device_information_antivirus_products_history_enabled", Convert.ToInt32(cleanup_device_information_antivirus_products_history_enabled)); + cmd.Parameters.AddWithValue("@cleanup_device_information_antivirus_products_history_days", cleanup_device_information_antivirus_products_history_days); + cmd.Parameters.AddWithValue("@cleanup_device_information_cpu_history_enabled", Convert.ToInt32(cleanup_device_information_cpu_history_enabled)); + cmd.Parameters.AddWithValue("@cleanup_device_information_cpu_history_days", cleanup_device_information_cpu_history_days); + cmd.Parameters.AddWithValue("@cleanup_device_information_cronjobs_history_enabled", Convert.ToInt32(cleanup_device_information_cronjobs_history_enabled)); + cmd.Parameters.AddWithValue("@cleanup_device_information_cronjobs_history_days", cleanup_device_information_cronjobs_history_days); + cmd.Parameters.AddWithValue("@cleanup_device_information_disks_history_enabled", Convert.ToInt32(cleanup_device_information_disks_history_enabled)); + cmd.Parameters.AddWithValue("@cleanup_device_information_disks_history_days", cleanup_device_information_disks_history_days); + cmd.Parameters.AddWithValue("@cleanup_device_information_general_history_enabled", Convert.ToInt32(cleanup_device_information_general_history_enabled)); + cmd.Parameters.AddWithValue("@cleanup_device_information_general_history_days", cleanup_device_information_general_history_days); + cmd.Parameters.AddWithValue("@cleanup_device_information_history_enabled", Convert.ToInt32(cleanup_device_information_history_enabled)); + cmd.Parameters.AddWithValue("@cleanup_device_information_history_days", cleanup_device_information_history_days); + cmd.Parameters.AddWithValue("@cleanup_device_information_network_adapters_history_enabled", Convert.ToInt32(cleanup_device_information_network_adapters_history_enabled)); + cmd.Parameters.AddWithValue("@cleanup_device_information_network_adapters_history_days", cleanup_device_information_network_adapters_history_days); + cmd.Parameters.AddWithValue("@cleanup_device_information_ram_history_enabled", Convert.ToInt32(cleanup_device_information_ram_history_enabled)); + cmd.Parameters.AddWithValue("@cleanup_device_information_ram_history_days", cleanup_device_information_ram_history_days); + cmd.Parameters.AddWithValue("@cleanup_device_information_task_manager_history_enabled", Convert.ToInt32(cleanup_device_information_task_manager_history_enabled)); + cmd.Parameters.AddWithValue("@cleanup_device_information_task_manager_history_days", cleanup_device_information_task_manager_history_days); + cmd.Parameters.AddWithValue("@cleanup_events_history_enabled", Convert.ToInt32(cleanup_events_history_enabled)); + cmd.Parameters.AddWithValue("@cleanup_events_history_days", cleanup_events_history_days); + + Logging.Handler.Debug("Settings -> Save_Cleanup_Setting", "MySQL_Prepared_Query", query); + + await cmd.ExecuteNonQueryAsync(); + + Snackbar.Add("Cleanup settings saved!", Severity.Success); + } + catch (Exception ex) + { + Logging.Handler.Error("Settings -> Save_Cleanup_Settings", "MySQL_Query", ex.ToString()); + } + finally + { + await conn.CloseAsync(); + } + } + + private async Task Optimize_Database() + { + Snackbar.Configuration.ShowCloseIcon = true; + Snackbar.Configuration.PositionClass = Defaults.Classes.Position.BottomRight; + + loading_overlay = true; + StateHasChanged(); + + MySqlConnection conn = new MySqlConnection(Configuration.MySQL.Connection_String); + + string query = @"OPTIMIZE TABLE `accounts`; +OPTIMIZE TABLE agent_package_configurations; +OPTIMIZE TABLE antivirus_controlled_folder_access_rulesets; +OPTIMIZE TABLE applications_drivers_history; +OPTIMIZE TABLE applications_installed_history; +OPTIMIZE TABLE applications_logon_history; +OPTIMIZE TABLE applications_scheduled_tasks_history; +OPTIMIZE TABLE applications_services_history; +OPTIMIZE TABLE automations; +OPTIMIZE TABLE device_information_antivirus_products_history; +OPTIMIZE TABLE device_information_cpu_history; +OPTIMIZE TABLE device_information_cronjobs_history; +OPTIMIZE TABLE device_information_disks_history; +OPTIMIZE TABLE device_information_general_history; +OPTIMIZE TABLE device_information_history; +OPTIMIZE TABLE device_information_network_adapters_history; +OPTIMIZE TABLE device_information_notes_history; +OPTIMIZE TABLE device_information_ram_history; +OPTIMIZE TABLE device_information_remote_shell_history; +OPTIMIZE TABLE device_information_task_manager_history; +OPTIMIZE TABLE devices; +OPTIMIZE TABLE `events`; +OPTIMIZE TABLE files; +OPTIMIZE TABLE `groups`; +OPTIMIZE TABLE infrastructure_events; +OPTIMIZE TABLE jobs; +OPTIMIZE TABLE locations; +OPTIMIZE TABLE mail_notifications; +OPTIMIZE TABLE microsoft_teams_notifications; +OPTIMIZE TABLE ntfy_sh_notifications; +OPTIMIZE TABLE performance_monitoring_ressources; +OPTIMIZE TABLE policies; +OPTIMIZE TABLE scripts; +OPTIMIZE TABLE sensors; +OPTIMIZE TABLE servers; +OPTIMIZE TABLE `settings`; +OPTIMIZE TABLE support_history; +OPTIMIZE TABLE telegram_notifications; +OPTIMIZE TABLE tenants;"; + + try + { + await conn.OpenAsync(); + + MySqlCommand cmd = new MySqlCommand(query, conn); + + Logging.Handler.Debug("Settings -> Optimize_Database", "MySQL_Prepared_Query", query); + + await cmd.ExecuteNonQueryAsync(); + } + catch (Exception ex) + { + Logging.Handler.Error("Settings -> Optimize_Database", "MySQL_Query", ex.ToString()); + } + finally + { + await conn.CloseAsync(); + } + + loading_overlay = false; + StateHasChanged(); + + Snackbar.Add("Database optimized!", Severity.Success); + } + + private async Task Fix_Settings() + { + try + { + // smtp + string smtp = String.Empty; + smtp = await Classes.MySQL.Handler.Quick_Reader("SELECT * FROM settings;", "smtp"); + + // files api key + string files_api_key = String.Empty; + files_api_key = await Classes.MySQL.Handler.Quick_Reader("SELECT * FROM settings;", "files_api_key"); + + if (String.IsNullOrEmpty(files_api_key)) + { + // Generate random files api key if empty + if (String.IsNullOrEmpty(files_api_key)) + files_api_key = Guid.NewGuid().ToString() + "-" + Guid.NewGuid().ToString(); + + // Delete old settings + await Classes.MySQL.Handler.Execute_Command("DELETE FROM settings;"); + + // Add new settings + await Classes.MySQL.Handler.Execute_Command("INSERT INTO settings (db_version, files_api_key, smtp) VALUES ('" + Application_Settings.db_version + "', '" + files_api_key + "', '" + smtp + "');"); + } + } + catch (Exception ex) + { + Logging.Handler.Error("Settings -> Fix_Settings", "MySQL_Query", ex.ToString()); + } + } + + // Remote Screen Control + + private async Task SaveRemoteScreenSessionRecordingSettings() + { + // Check if paid user + if (!Configuration.Members_Portal.IsCodeSigned) + { + // Open membership reminder dialog + var membershipReminderDialogOptions = new DialogOptions + { + CloseButton = true, + FullWidth = true, + MaxWidth = MaxWidth.Small, + BackgroundClass = "dialog-blurring", + }; + + DialogService.Show(Localizer["membership required"], membershipReminderDialogOptions); + return; + } + + Snackbar.Configuration.ShowCloseIcon = true; + Snackbar.Configuration.PositionClass = Defaults.Classes.Position.BottomRight; + + int remoteScreenSessionRecordingForcedDaysValue = remoteScreenSessionRecordingForcedDays; + + // Save cleanup settings to database + MySqlConnection conn = new MySqlConnection(Configuration.MySQL.Connection_String); + + string query = @"UPDATE settings SET remote_screen_session_recording_forced_enabled = @remote_screen_session_recording_forced_enabled, remote_screen_session_recording_auto_clean_enabled = @remote_screen_session_recording_auto_clean_enabled, remote_screen_session_recording_forced_days = @remote_screen_session_recording_forced_days;"; + + try + { + await conn.OpenAsync(); + + MySqlCommand cmd = new MySqlCommand(query, conn); + cmd.Parameters.AddWithValue("@remote_screen_session_recording_forced_enabled", Convert.ToInt32(remoteScreenSessionRecordingForced)); + cmd.Parameters.AddWithValue("@remote_screen_session_recording_auto_clean_enabled", Convert.ToInt32(remoteScreenSessionRecordingAutoClean)); + cmd.Parameters.AddWithValue("@remote_screen_session_recording_forced_days", remoteScreenSessionRecordingForcedDays); + + Logging.Handler.Debug("Settings -> SaveRemoteScreenSessionRecordingSettings", "MySQL_Prepared_Query", query); + + await cmd.ExecuteNonQueryAsync(); + + Snackbar.Add("Remote screen control cleanup settings saved!", Severity.Success); + } + catch (Exception ex) + { + Logging.Handler.Error("Settings -> SaveRemoteScreenSessionRecordingSettings", "MySQL_Query", ex.ToString()); + } + finally + { + await conn.CloseAsync(); + } + } + + // SSO Settings + //OSSCH_START ef647980-40af-44bc-8a8b-f3f4a649d48b //OSSCH_END + + // IP Whitelist Settings + private async Task Get_Ip_Whitelist() + { + try + { + // Load Web Console IP Whitelist + string ipWhitelistWebConsoleJson = await Classes.MySQL.Handler.Quick_Reader("SELECT * FROM settings", "ip_whitelist_web_console"); + + if (!string.IsNullOrEmpty(ipWhitelistWebConsoleJson)) + { + var ipList = JsonSerializer.Deserialize>(ipWhitelistWebConsoleJson); + if (ipList != null && ipList.Count > 0) + { + ipWhitelistWebConsoleInput = string.Join(", ", ipList); + } + } + + // Load Backend IP Whitelist + string ipWhitelistBackendJson = await Classes.MySQL.Handler.Quick_Reader("SELECT * FROM settings", "ip_whitelist_backend"); + + if (!string.IsNullOrEmpty(ipWhitelistBackendJson)) + { + var ipList = JsonSerializer.Deserialize>(ipWhitelistBackendJson); + if (ipList != null && ipList.Count > 0) + { + ipWhitelistBackendInput = string.Join(", ", ipList); + } + } + } + catch (Exception ex) + { + Logging.Handler.Error("Settings -> Get_Ip_Whitelist", "Error", ex.ToString()); + } + } + + private bool ValidateIpAddresses(string input, out List validIps, ref string validationError) + { + validIps = new List(); + validationError = string.Empty; + + if (string.IsNullOrWhiteSpace(input)) + { + return true; // Empty is valid (no whitelist) + } + + var ips = input.Split(',', StringSplitOptions.RemoveEmptyEntries) + .Select(ip => ip.Trim()) + .Where(ip => !string.IsNullOrWhiteSpace(ip)) + .ToList(); + + foreach (var ip in ips) + { + if (System.Net.IPAddress.TryParse(ip, out _)) + { + validIps.Add(ip); + } + else + { + validationError = $"Invalid IP address: {ip}"; + return false; + } + } + + return true; + } + + private async Task SaveIpWhitelistWebConsole() + { + // Validate IP addresses first + if (!ValidateIpAddresses(ipWhitelistWebConsoleInput, out List validIps, ref ipWhitelistWebConsoleValidationError)) + { + Snackbar.Configuration.ShowCloseIcon = true; + Snackbar.Configuration.PositionClass = Defaults.Classes.Position.BottomRight; + Snackbar.Add(ipWhitelistWebConsoleValidationError, Severity.Error); + StateHasChanged(); + return; + } + + // Show confirmation dialog with warning about automatic restart + var options = new DialogOptions + { + CloseButton = true, + FullWidth = true, + MaxWidth = MaxWidth.Medium, + BackgroundClass = "dialog-blurring", + }; + + bool? dialog_result = await DialogService.ShowMessageBox( + "Warning", + "The web console will automatically restart after saving the IP whitelist configuration. All active user sessions will be terminated. Do you want to continue?", + yesText: Localizer["confirm"], + cancelText: Localizer["cancel"], + options: options); + + bool state = Convert.ToBoolean(dialog_result == null ? "false" : "true"); + + if (!state) + return; + + try + { + Snackbar.Configuration.ShowCloseIcon = true; + Snackbar.Configuration.PositionClass = Defaults.Classes.Position.BottomRight; + + loading_overlay = true; + StateHasChanged(); + + // Serialize to JSON + string ipWhitelistJson = string.Empty; + if (validIps.Count > 0) + { + ipWhitelistJson = JsonSerializer.Serialize(validIps); + } + + // Save to database + MySqlConnection conn = new MySqlConnection(Configuration.MySQL.Connection_String); + string query = "UPDATE settings SET ip_whitelist_web_console = @ip_whitelist_web_console;"; + + await conn.OpenAsync(); + MySqlCommand cmd = new MySqlCommand(query, conn); + + if (string.IsNullOrEmpty(ipWhitelistJson)) + cmd.Parameters.AddWithValue("@ip_whitelist_web_console", DBNull.Value); + else + cmd.Parameters.AddWithValue("@ip_whitelist_web_console", ipWhitelistJson); + + Logging.Handler.Debug("Settings -> SaveIpWhitelistWebConsole", "MySQL_Prepared_Query", query); + + await cmd.ExecuteNonQueryAsync(); + await conn.CloseAsync(); + + Snackbar.Add("Web Console IP Whitelist saved successfully! Application is restarting...", Severity.Success); + + // Wait briefly so the user can see the success message + await Task.Delay(2000); + + // Restart the application + System.Environment.Exit(0); + } + catch (Exception ex) + { + Logging.Handler.Error("Settings -> SaveIpWhitelistWebConsole", "Error", ex.ToString()); + Snackbar.Add("Error saving Web Console IP Whitelist: " + ex.Message, Severity.Error); + loading_overlay = false; + StateHasChanged(); + } + } + + private async Task SaveIpWhitelistBackend() + { + // Validate IP addresses first + if (!ValidateIpAddresses(ipWhitelistBackendInput, out List validIps, ref ipWhitelistBackendValidationError)) + { + Snackbar.Configuration.ShowCloseIcon = true; + Snackbar.Configuration.PositionClass = Defaults.Classes.Position.BottomRight; + Snackbar.Add(ipWhitelistBackendValidationError, Severity.Error); + StateHasChanged(); + return; + } + + try + { + Snackbar.Configuration.ShowCloseIcon = true; + Snackbar.Configuration.PositionClass = Defaults.Classes.Position.BottomRight; + + loading_overlay = true; + StateHasChanged(); + + // Serialize to JSON + string ipWhitelistJson = string.Empty; + if (validIps.Count > 0) + { + ipWhitelistJson = JsonSerializer.Serialize(validIps); + } + + // Save to database + MySqlConnection conn = new MySqlConnection(Configuration.MySQL.Connection_String); + string query = "UPDATE settings SET ip_whitelist_backend = @ip_whitelist_backend;"; + + await conn.OpenAsync(); + MySqlCommand cmd = new MySqlCommand(query, conn); + + if (string.IsNullOrEmpty(ipWhitelistJson)) + cmd.Parameters.AddWithValue("@ip_whitelist_backend", DBNull.Value); + else + cmd.Parameters.AddWithValue("@ip_whitelist_backend", ipWhitelistJson); + + Logging.Handler.Debug("Settings -> SaveIpWhitelistBackend", "MySQL_Prepared_Query", query); + + await cmd.ExecuteNonQueryAsync(); + await conn.CloseAsync(); + + Snackbar.Add("Backend IP Whitelist saved successfully!", Severity.Success); + + loading_overlay = false; + StateHasChanged(); + } + catch (Exception ex) + { + Logging.Handler.Error("Settings -> SaveIpWhitelistBackend", "Error", ex.ToString()); + Snackbar.Add("Error saving Backend IP Whitelist: " + ex.Message, Severity.Error); + loading_overlay = false; + StateHasChanged(); + } + } + + // Whitelabeling Settings + //OSSCH_START 6d973178-ba80-4110-ac88-b95488e8155b //OSSCH_END +} + diff --git a/NetLock-RMM-Web-Console/Components/Pages/Users/User_Settings.razor b/NetLock-RMM-Web-Console/Components/Pages/Users/User_Settings.razor index c29e173a..416356ec 100644 --- a/NetLock-RMM-Web-Console/Components/Pages/Users/User_Settings.razor +++ b/NetLock-RMM-Web-Console/Components/Pages/Users/User_Settings.razor @@ -201,6 +201,7 @@ + @@ -463,6 +464,7 @@ private bool devices_remote_shell = false; private bool devices_remote_file_browser = false; private bool devices_remote_control = false; + private bool devices_remote_eventlog_viewer = false; private bool devices_deauthorize = false; private bool devices_move = false; @@ -604,6 +606,7 @@ public bool devices_remote_shell { get; set; } public bool devices_remote_file_browser { get; set; } public bool devices_remote_control { get; set; } + public bool devices_remote_eventlog_viewer { get; set; } public bool devices_deauthorize { get; set; } public bool devices_move { get; set; } @@ -803,6 +806,7 @@ devices_remote_shell = devices_remote_shell, devices_remote_file_browser = devices_remote_file_browser, devices_remote_control = devices_remote_control, + devices_remote_eventlog_viewer = devices_remote_eventlog_viewer, devices_deauthorize = devices_deauthorize, devices_move = devices_move, @@ -1028,6 +1032,24 @@ } } + private bool GetBoolProperty(JsonElement element, string propertyName, bool defaultValue = false) + { + try + { + if (element.TryGetProperty(propertyName, out JsonElement property)) + { + return property.GetBoolean(); + } + Logging.Handler.Debug("/user_settings -> GetBoolProperty", "Missing property", $"{propertyName} not found, using default: {defaultValue}"); + return defaultValue; + } + catch (Exception ex) + { + Logging.Handler.Error("/user_settings -> GetBoolProperty", propertyName, ex.Message); + return defaultValue; + } + } + private async Task Get_Permissions() { //Extract permissions_json @@ -1036,145 +1058,145 @@ Logging.Handler.Debug("/user_settings -> Get_Permissions (Extract)", "permissions_json", permissions_json); JsonDocument jsonDocument = JsonDocument.Parse(permissions_json); + JsonElement root = jsonDocument.RootElement; //dashboard - dashboard_enabled = jsonDocument.RootElement.GetProperty("dashboard_enabled").GetBoolean(); + dashboard_enabled = GetBoolProperty(root, "dashboard_enabled"); //permissions: devices - devices_authorized_enabled = jsonDocument.RootElement.GetProperty("devices_authorized_enabled").GetBoolean(); - devices_general = jsonDocument.RootElement.GetProperty("devices_general").GetBoolean(); - devices_software = jsonDocument.RootElement.GetProperty("devices_software").GetBoolean(); - devices_task_manager = jsonDocument.RootElement.GetProperty("devices_task_manager").GetBoolean(); - devices_antivirus = jsonDocument.RootElement.GetProperty("devices_antivirus").GetBoolean(); - devices_events = jsonDocument.RootElement.GetProperty("devices_events").GetBoolean(); - devices_remote_shell = jsonDocument.RootElement.GetProperty("devices_remote_shell").GetBoolean(); - devices_remote_file_browser = jsonDocument.RootElement.GetProperty("devices_remote_file_browser").GetBoolean(); - devices_remote_control = jsonDocument.RootElement.GetProperty("devices_remote_control").GetBoolean(); - devices_deauthorize = jsonDocument.RootElement.GetProperty("devices_deauthorize").GetBoolean(); - devices_move = jsonDocument.RootElement.GetProperty("devices_move").GetBoolean(); - - devices_unauthorized_enabled = jsonDocument.RootElement.GetProperty("devices_unauthorized_enabled").GetBoolean(); - devices_unauthorized_authorize = jsonDocument.RootElement.GetProperty("devices_unauthorized_authorize").GetBoolean(); + devices_authorized_enabled = GetBoolProperty(root, "devices_authorized_enabled"); + devices_general = GetBoolProperty(root, "devices_general"); + devices_software = GetBoolProperty(root, "devices_software"); + devices_task_manager = GetBoolProperty(root, "devices_task_manager"); + devices_antivirus = GetBoolProperty(root, "devices_antivirus"); + devices_events = GetBoolProperty(root, "devices_events"); + devices_remote_shell = GetBoolProperty(root, "devices_remote_shell"); + devices_remote_file_browser = GetBoolProperty(root, "devices_remote_file_browser"); + devices_remote_control = GetBoolProperty(root, "devices_remote_control"); + devices_remote_eventlog_viewer = GetBoolProperty(root, "devices_remote_eventlog_viewer"); + devices_deauthorize = GetBoolProperty(root, "devices_deauthorize"); + devices_move = GetBoolProperty(root, "devices_move"); + + devices_unauthorized_enabled = GetBoolProperty(root, "devices_unauthorized_enabled"); + devices_unauthorized_authorize = GetBoolProperty(root, "devices_unauthorized_authorize"); //permissions: tenants - tenants_enabled = jsonDocument.RootElement.GetProperty("tenants_enabled").GetBoolean(); - tenants_add = jsonDocument.RootElement.GetProperty("tenants_add").GetBoolean(); - tenants_manage = jsonDocument.RootElement.GetProperty("tenants_manage").GetBoolean(); - tenants_edit = jsonDocument.RootElement.GetProperty("tenants_edit").GetBoolean(); - tenants_delete = jsonDocument.RootElement.GetProperty("tenants_delete").GetBoolean(); - tenants_locations_add = jsonDocument.RootElement.GetProperty("tenants_locations_add").GetBoolean(); - tenants_locations_manage = jsonDocument.RootElement.GetProperty("tenants_locations_manage").GetBoolean(); - tenants_locations_edit = jsonDocument.RootElement.GetProperty("tenants_locations_edit").GetBoolean(); - tenants_locations_delete = jsonDocument.RootElement.GetProperty("tenants_locations_delete").GetBoolean(); - tenants_groups_add = jsonDocument.RootElement.GetProperty("tenants_groups_add").GetBoolean(); - tenants_groups_edit = jsonDocument.RootElement.GetProperty("tenants_groups_edit").GetBoolean(); - tenants_groups_delete = jsonDocument.RootElement.GetProperty("tenants_groups_delete").GetBoolean(); + tenants_enabled = GetBoolProperty(root, "tenants_enabled"); + tenants_add = GetBoolProperty(root, "tenants_add"); + tenants_manage = GetBoolProperty(root, "tenants_manage"); + tenants_edit = GetBoolProperty(root, "tenants_edit"); + tenants_delete = GetBoolProperty(root, "tenants_delete"); + tenants_locations_add = GetBoolProperty(root, "tenants_locations_add"); + tenants_locations_manage = GetBoolProperty(root, "tenants_locations_manage"); + tenants_locations_edit = GetBoolProperty(root, "tenants_locations_edit"); + tenants_locations_delete = GetBoolProperty(root, "tenants_locations_delete"); + tenants_groups_add = GetBoolProperty(root, "tenants_groups_add"); + tenants_groups_edit = GetBoolProperty(root, "tenants_groups_edit"); + tenants_groups_delete = GetBoolProperty(root, "tenants_groups_delete"); //permissions: automation - automation_enabled = jsonDocument.RootElement.GetProperty("automation_enabled").GetBoolean(); - automation_add = jsonDocument.RootElement.GetProperty("automation_add").GetBoolean(); - automation_edit = jsonDocument.RootElement.GetProperty("automation_edit").GetBoolean(); - automation_delete = jsonDocument.RootElement.GetProperty("automation_delete").GetBoolean(); + automation_enabled = GetBoolProperty(root, "automation_enabled"); + automation_add = GetBoolProperty(root, "automation_add"); + automation_edit = GetBoolProperty(root, "automation_edit"); + automation_delete = GetBoolProperty(root, "automation_delete"); //permissions: policies - policies_enabled = jsonDocument.RootElement.GetProperty("policies_enabled").GetBoolean(); - policies_add = jsonDocument.RootElement.GetProperty("policies_add").GetBoolean(); - policies_manage = jsonDocument.RootElement.GetProperty("policies_manage").GetBoolean(); - policies_edit = jsonDocument.RootElement.GetProperty("policies_edit").GetBoolean(); - policies_delete = jsonDocument.RootElement.GetProperty("policies_delete").GetBoolean(); + policies_enabled = GetBoolProperty(root, "policies_enabled"); + policies_add = GetBoolProperty(root, "policies_add"); + policies_manage = GetBoolProperty(root, "policies_manage"); + policies_edit = GetBoolProperty(root, "policies_edit"); + policies_delete = GetBoolProperty(root, "policies_delete"); //permissions: collections - collections_enabled = jsonDocument.RootElement.GetProperty("collections_enabled").GetBoolean(); + collections_enabled = GetBoolProperty(root, "collections_enabled"); //permissions: collections -> acfa - collections_antivirus_controlled_folder_access_enabled = jsonDocument.RootElement.GetProperty("collections_antivirus_controlled_folder_access_enabled").GetBoolean(); - collections_antivirus_controlled_folder_access_add = jsonDocument.RootElement.GetProperty("collections_antivirus_controlled_folder_access_add").GetBoolean(); - collections_antivirus_controlled_folder_access_manage = jsonDocument.RootElement.GetProperty("collections_antivirus_controlled_folder_access_manage").GetBoolean(); - collections_antivirus_controlled_folder_access_edit = jsonDocument.RootElement.GetProperty("collections_antivirus_controlled_folder_access_edit").GetBoolean(); - collections_antivirus_controlled_folder_access_delete = jsonDocument.RootElement.GetProperty("collections_antivirus_controlled_folder_access_delete").GetBoolean(); - collections_antivirus_controlled_folder_access_processes_add = jsonDocument.RootElement.GetProperty("collections_antivirus_controlled_folder_access_processes_add").GetBoolean(); - collections_antivirus_controlled_folder_access_processes_edit = jsonDocument.RootElement.GetProperty("collections_antivirus_controlled_folder_access_processes_edit").GetBoolean(); - collections_antivirus_controlled_folder_access_processes_delete = jsonDocument.RootElement.GetProperty("collections_antivirus_controlled_folder_access_processes_delete").GetBoolean(); + collections_antivirus_controlled_folder_access_enabled = GetBoolProperty(root, "collections_antivirus_controlled_folder_access_enabled"); + collections_antivirus_controlled_folder_access_add = GetBoolProperty(root, "collections_antivirus_controlled_folder_access_add"); + collections_antivirus_controlled_folder_access_manage = GetBoolProperty(root, "collections_antivirus_controlled_folder_access_manage"); + collections_antivirus_controlled_folder_access_edit = GetBoolProperty(root, "collections_antivirus_controlled_folder_access_edit"); + collections_antivirus_controlled_folder_access_delete = GetBoolProperty(root, "collections_antivirus_controlled_folder_access_delete"); + collections_antivirus_controlled_folder_access_processes_add = GetBoolProperty(root, "collections_antivirus_controlled_folder_access_processes_add"); + collections_antivirus_controlled_folder_access_processes_edit = GetBoolProperty(root, "collections_antivirus_controlled_folder_access_processes_edit"); + collections_antivirus_controlled_folder_access_processes_delete = GetBoolProperty(root, "collections_antivirus_controlled_folder_access_processes_delete"); //permissions: collections -> sensors - collections_sensors_enabled = jsonDocument.RootElement.GetProperty("collections_sensors_enabled").GetBoolean(); - collections_sensors_add = jsonDocument.RootElement.GetProperty("collections_sensors_add").GetBoolean(); - collections_sensors_edit = jsonDocument.RootElement.GetProperty("collections_sensors_edit").GetBoolean(); - collections_sensors_delete = jsonDocument.RootElement.GetProperty("collections_sensors_delete").GetBoolean(); + collections_sensors_enabled = GetBoolProperty(root, "collections_sensors_enabled"); + collections_sensors_add = GetBoolProperty(root, "collections_sensors_add"); + collections_sensors_edit = GetBoolProperty(root, "collections_sensors_edit"); + collections_sensors_delete = GetBoolProperty(root, "collections_sensors_delete"); //permissions: collections -> scripts - collections_scripts_enabled = jsonDocument.RootElement.GetProperty("collections_scripts_enabled").GetBoolean(); - collections_scripts_add = jsonDocument.RootElement.GetProperty("collections_scripts_add").GetBoolean(); - collections_scripts_edit = jsonDocument.RootElement.GetProperty("collections_scripts_edit").GetBoolean(); - collections_scripts_delete = jsonDocument.RootElement.GetProperty("collections_scripts_delete").GetBoolean(); + collections_scripts_enabled = GetBoolProperty(root, "collections_scripts_enabled"); + collections_scripts_add = GetBoolProperty(root, "collections_scripts_add"); + collections_scripts_edit = GetBoolProperty(root, "collections_scripts_edit"); + collections_scripts_delete = GetBoolProperty(root, "collections_scripts_delete"); //permissions: collections -> jobs - collections_jobs_enabled = jsonDocument.RootElement.GetProperty("collections_jobs_enabled").GetBoolean(); - collections_jobs_add = jsonDocument.RootElement.GetProperty("collections_jobs_add").GetBoolean(); - collections_jobs_edit = jsonDocument.RootElement.GetProperty("collections_jobs_edit").GetBoolean(); - collections_jobs_delete = jsonDocument.RootElement.GetProperty("collections_jobs_delete").GetBoolean(); + collections_jobs_enabled = GetBoolProperty(root, "collections_jobs_enabled"); + collections_jobs_add = GetBoolProperty(root, "collections_jobs_add"); + collections_jobs_edit = GetBoolProperty(root, "collections_jobs_edit"); + collections_jobs_delete = GetBoolProperty(root, "collections_jobs_delete"); //permissions: collections -> files - collections_files_enabled = jsonDocument.RootElement.GetProperty("collections_files_enabled").GetBoolean(); - collections_files_add = jsonDocument.RootElement.GetProperty("collections_files_add").GetBoolean(); - collections_files_edit = jsonDocument.RootElement.GetProperty("collections_files_edit").GetBoolean(); - collections_files_delete = jsonDocument.RootElement.GetProperty("collections_files_delete").GetBoolean(); - collections_files_netlock = jsonDocument.RootElement.GetProperty("collections_files_netlock").GetBoolean(); + collections_files_enabled = GetBoolProperty(root, "collections_files_enabled"); + collections_files_add = GetBoolProperty(root, "collections_files_add"); + collections_files_edit = GetBoolProperty(root, "collections_files_edit"); //permissions: events - events_enabled = jsonDocument.RootElement.GetProperty("events_enabled").GetBoolean(); + events_enabled = GetBoolProperty(root, "events_enabled"); //permissions: users - users_enabled = jsonDocument.RootElement.GetProperty("users_enabled").GetBoolean(); - users_add = jsonDocument.RootElement.GetProperty("users_add").GetBoolean(); - users_manage = jsonDocument.RootElement.GetProperty("users_manage").GetBoolean(); - users_edit = jsonDocument.RootElement.GetProperty("users_edit").GetBoolean(); - users_delete = jsonDocument.RootElement.GetProperty("users_delete").GetBoolean(); + users_enabled = GetBoolProperty(root, "users_enabled"); + users_add = GetBoolProperty(root, "users_add"); + users_manage = GetBoolProperty(root, "users_manage"); + users_edit = GetBoolProperty(root, "users_edit"); + users_delete = GetBoolProperty(root, "users_delete"); //permissions: settings - settings_enabled = jsonDocument.RootElement.GetProperty("settings_enabled").GetBoolean(); + settings_enabled = GetBoolProperty(root, "settings_enabled"); //permissions: settings -> notifications - settings_notifications_enabled = jsonDocument.RootElement.GetProperty("settings_notifications_enabled").GetBoolean(); + settings_notifications_enabled = GetBoolProperty(root, "settings_notifications_enabled"); //permissions: settings -> notifications -> mail - settings_notifications_mail_enabled = jsonDocument.RootElement.GetProperty("settings_notifications_mail_enabled").GetBoolean(); - settings_notifications_mail_add = jsonDocument.RootElement.GetProperty("settings_notifications_mail_add").GetBoolean(); - settings_notifications_mail_smtp = jsonDocument.RootElement.GetProperty("settings_notifications_mail_smtp").GetBoolean(); - settings_notifications_mail_test = jsonDocument.RootElement.GetProperty("settings_notifications_mail_test").GetBoolean(); - settings_notifications_mail_edit = jsonDocument.RootElement.GetProperty("settings_notifications_mail_edit").GetBoolean(); - settings_notifications_mail_delete = jsonDocument.RootElement.GetProperty("settings_notifications_mail_delete").GetBoolean(); + settings_notifications_mail_enabled = GetBoolProperty(root, "settings_notifications_mail_enabled"); + settings_notifications_mail_add = GetBoolProperty(root, "settings_notifications_mail_add"); + settings_notifications_mail_smtp = GetBoolProperty(root, "settings_notifications_mail_smtp"); + settings_notifications_mail_test = GetBoolProperty(root, "settings_notifications_mail_test"); + settings_notifications_mail_edit = GetBoolProperty(root, "settings_notifications_mail_edit"); + settings_notifications_mail_delete = GetBoolProperty(root, "settings_notifications_mail_delete"); //permissions: settings -> notifications -> ms teams - settings_notifications_microsoft_teams_enabled = jsonDocument.RootElement.GetProperty("settings_notifications_microsoft_teams_enabled").GetBoolean(); - settings_notifications_microsoft_teams_add = jsonDocument.RootElement.GetProperty("settings_notifications_microsoft_teams_add").GetBoolean(); - settings_notifications_microsoft_teams_test = jsonDocument.RootElement.GetProperty("settings_notifications_microsoft_teams_test").GetBoolean(); - settings_notifications_microsoft_teams_edit = jsonDocument.RootElement.GetProperty("settings_notifications_microsoft_teams_edit").GetBoolean(); - settings_notifications_microsoft_teams_delete = jsonDocument.RootElement.GetProperty("settings_notifications_microsoft_teams_delete").GetBoolean(); + settings_notifications_microsoft_teams_enabled = GetBoolProperty(root, "settings_notifications_microsoft_teams_enabled"); + settings_notifications_microsoft_teams_add = GetBoolProperty(root, "settings_notifications_microsoft_teams_add"); + settings_notifications_microsoft_teams_test = GetBoolProperty(root, "settings_notifications_microsoft_teams_test"); + settings_notifications_microsoft_teams_edit = GetBoolProperty(root, "settings_notifications_microsoft_teams_edit"); + settings_notifications_microsoft_teams_delete = GetBoolProperty(root, "settings_notifications_microsoft_teams_delete"); //permissions: settings -> notifications -> telegram - settings_notifications_telegram_enabled = jsonDocument.RootElement.GetProperty("settings_notifications_telegram_enabled").GetBoolean(); - settings_notifications_telegram_add = jsonDocument.RootElement.GetProperty("settings_notifications_telegram_add").GetBoolean(); - settings_notifications_telegram_test = jsonDocument.RootElement.GetProperty("settings_notifications_telegram_test").GetBoolean(); - settings_notifications_telegram_edit = jsonDocument.RootElement.GetProperty("settings_notifications_telegram_edit").GetBoolean(); - settings_notifications_telegram_delete = jsonDocument.RootElement.GetProperty("settings_notifications_telegram_delete").GetBoolean(); + settings_notifications_telegram_enabled = GetBoolProperty(root, "settings_notifications_telegram_enabled"); + settings_notifications_telegram_add = GetBoolProperty(root, "settings_notifications_telegram_add"); + settings_notifications_telegram_test = GetBoolProperty(root, "settings_notifications_telegram_test"); + settings_notifications_telegram_edit = GetBoolProperty(root, "settings_notifications_telegram_edit"); + settings_notifications_telegram_delete = GetBoolProperty(root, "settings_notifications_telegram_delete"); //permissions: settings -> notifications -> ntfysh - settings_notifications_ntfysh_enabled = jsonDocument.RootElement.GetProperty("settings_notifications_ntfysh_enabled").GetBoolean(); - settings_notifications_ntfysh_add = jsonDocument.RootElement.GetProperty("settings_notifications_ntfysh_add").GetBoolean(); - settings_notifications_ntfysh_test = jsonDocument.RootElement.GetProperty("settings_notifications_ntfysh_test").GetBoolean(); - settings_notifications_ntfysh_edit = jsonDocument.RootElement.GetProperty("settings_notifications_ntfysh_edit").GetBoolean(); - settings_notifications_ntfysh_delete = jsonDocument.RootElement.GetProperty("settings_notifications_ntfysh_delete").GetBoolean(); + settings_notifications_ntfysh_enabled = GetBoolProperty(root, "settings_notifications_ntfysh_enabled"); + settings_notifications_ntfysh_add = GetBoolProperty(root, "settings_notifications_ntfysh_add"); + settings_notifications_ntfysh_test = GetBoolProperty(root, "settings_notifications_ntfysh_test"); + settings_notifications_ntfysh_edit = GetBoolProperty(root, "settings_notifications_ntfysh_edit"); + settings_notifications_ntfysh_delete = GetBoolProperty(root, "settings_notifications_ntfysh_delete"); //permissions: settings -> notifications -> webhook - settings_notifications_webhook_enabled = jsonDocument.RootElement.GetProperty("settings_notifications_webhook_enabled").GetBoolean(); - settings_notifications_webhook_add = jsonDocument.RootElement.GetProperty("settings_notifications_webhook_add").GetBoolean(); - settings_notifications_webhook_test = jsonDocument.RootElement.GetProperty("settings_notifications_webhook_test").GetBoolean(); - settings_notifications_webhook_edit = jsonDocument.RootElement.GetProperty("settings_notifications_webhook_edit").GetBoolean(); - settings_notifications_webhook_delete = jsonDocument.RootElement.GetProperty("settings_notifications_webhook_delete").GetBoolean(); - - settings_system_enabled = jsonDocument.RootElement.GetProperty("settings_system_enabled").GetBoolean(); - settings_protocols_enabled = jsonDocument.RootElement.GetProperty("settings_protocols_enabled").GetBoolean(); + settings_notifications_webhook_enabled = GetBoolProperty(root, "settings_notifications_webhook_enabled"); + settings_notifications_webhook_add = GetBoolProperty(root, "settings_notifications_webhook_add"); + settings_notifications_webhook_test = GetBoolProperty(root, "settings_notifications_webhook_test"); + settings_notifications_webhook_edit = GetBoolProperty(root, "settings_notifications_webhook_edit"); + settings_notifications_webhook_delete = GetBoolProperty(root, "settings_notifications_webhook_delete"); + + settings_system_enabled = GetBoolProperty(root, "settings_system_enabled"); + settings_protocols_enabled = GetBoolProperty(root, "settings_protocols_enabled"); } catch (Exception ex) { diff --git a/NetLock-RMM-Web-Console/Components/Shared/Membership_Reminder_Dialog.razor b/NetLock-RMM-Web-Console/Components/Shared/Membership_Reminder_Dialog.razor index 61a52f5a..32ec34bf 100644 --- a/NetLock-RMM-Web-Console/Components/Shared/Membership_Reminder_Dialog.razor +++ b/NetLock-RMM-Web-Console/Components/Shared/Membership_Reminder_Dialog.razor @@ -11,67 +11,53 @@ - @Localizer["title"] + + + Pro Feature – Tier 1+ Required + - - @if (Configuration.Web_Console.language == "en-US") - { + - NetLock RMM is an open-source RMM solution that I, Nico, have been developing full-time as a solo developer. My vision is to create a robust, state-of-the-art RMM that meets the needs of both enterprises and small businesses, with transparency and independence at its core. + This feature is available for Pro Users with at least Tier 1 membership. - - - Since late 2023, I’ve dedicated myself to building the entire foundation of NetLock RMM. Released on October 25, 2024, this remains my full-time job and passion. While I explored funding options, I chose not to involve investors to ensure NetLock RMM stays independent. + + + Tier 1 and higher memberships unlock premium features like: - - - Hosting and maintaining this service requires significant time and resources/money. I hope that companies benefiting from NetLock RMM will support the project through memberships or cloud instances. - - - πŸ’‘ Open Source β‰  Free - - Nothing in this world is truly free. That's the first thing I learned in life. Companies that benefit from this project should be willing to contribute their fair share instead of seeing open source as just "free." If no one pays, the project cannot be sustained in the long runβ€”eventually, thats what happened to a lot of big projects. Companies then have to switch to a commercial solution, which is often more expensive and not as transparent. Or features need to be put behind a paywall. I want to avoid that. - - - 🀝 A Shared Responsibility - - It's a simple principle of give and take. I believe that, with your support, we can build a competitive, open-source RMM capable of standing alongside the larger closed-source solutions and even surpassing them. I am committed to transparency, independence, and sustainability. Don't forget. With more ressources (money = employees) I can even speed up the development and add more features. - - - πŸ™ Thank You for Being Part of This Journey! - } - else if (Configuration.Web_Console.language == "de-DE") - { - - NetLock RMM ist eine Open-Source-RMM-LΓΆsung, die ich, Nico, als Einzelentwickler in Vollzeit entwickle. Meine Vision ist es, ein leistungsstarkes, hochmodernes RMM zu schaffen, das sowohl die Anforderungen von Unternehmen als auch von kleineren Betrieben erfΓΌllt – mit Transparenz und UnabhΓ€ngigkeit als zentrale Werte. - - - - Seit Ende 2023 widme ich mich vollstΓ€ndig dem Aufbau von NetLock RMM. VerΓΆffentlicht am 25. Oktober 2024, ist es weiterhin mein Vollzeitjob und meine Leidenschaft. WΓ€hrend ich verschiedene FinanzierungsmΓΆglichkeiten geprΓΌft habe, habe ich mich bewusst gegen Investoren entschieden, um die UnabhΓ€ngigkeit von NetLock RMM zu bewahren. - - - - Das Hosten und die Wartung dieses Dienstes erfordern erhebliche Zeit und Ressourcen/Geld. Ich hoffe, dass Unternehmen, die von NetLock RMM profitieren, das Projekt durch Mitgliedschaften oder Cloud-Instanzen unterstΓΌtzen. - - - πŸ’‘ Open Source β‰  Kostenlos - - Nichts auf dieser Welt ist wirklich kostenlos – das habe ich frΓΌh gelernt. Unternehmen, die von diesem Projekt profitieren, sollten bereit sein, ihren fairen Beitrag zu leisten, anstatt Open Source nur als β€žkostenlosβ€œ zu betrachten. Wenn niemand zahlt, kann das Projekt auf lange Sicht nicht bestehen – genau das ist vielen großen Projekten passiert. Am Ende mΓΌssen Unternehmen dann auf kommerzielle LΓΆsungen umsteigen, die oft teurer und weniger transparent sind. Oder Funktionen werden hinter einer Paywall versteckt – und genau das mΓΆchte ich vermeiden. - - - 🀝 Eine gemeinsame Verantwortung - - Es ist ein einfaches Prinzip von Geben und Nehmen. Ich bin ΓΌberzeugt, dass wir mit eurer UnterstΓΌtzung ein wettbewerbsfΓ€higes Open-Source-RMM schaffen kΓΆnnen, das mit großen Closed-Source-LΓΆsungen mithalten und sie sogar ΓΌbertreffen kann. Ich stehe fΓΌr Transparenz, UnabhΓ€ngigkeit und Nachhaltigkeit. Vergesst nicht: Mit mehr Ressourcen (*Geld = Mitarbeiter*) kann die Entwicklung schneller vorangehen und neue Funktionen kΓΆnnen schneller hinzugefΓΌgt werden. + + + + Code-signed Windows agents + + + Single Sign-On (SSO) + + + Whitelabeling options + + + Professional support + + + + + + + Your support helps us maintain and improve NetLock RMM for everyone. By upgrading, you're not just unlocking features – you're investing in the future of this open-source project. - - πŸ™ Danke, dass du Teil dieser Reise bist! - } - + + + Starting at just 55€/month or 550€/year, Tier 1 gives you access to premium features and professional support. + + - @Localizer["dismiss"] + I have understood the message + + View Pricing + @@ -80,6 +66,72 @@ [CascadingParameter] IMudDialogInstance MudDialog { get; set; } + protected override async Task OnInitializedAsync() + { + // Check if user has at least Tier 1 - if yes, close dialog automatically + if (await HasMinimumTier()) + { + MudDialog.Close(DialogResult.Ok(true)); + } + } + + private async Task HasMinimumTier() + { + try + { + // Check if license info file exists + if (!File.Exists(Application_Paths.internal_license_info_json_path)) + return false; + + // Get license info + var (validUntil, packageName, licensesMax, licensesHardLimit, codeSigned) = + await Classes.Members_Portal.Handler.Get_License_Info(); + + // Check if license is valid + bool isValid = await Classes.Members_Portal.Handler.Check_License_Valid_Status(false); + + if (!isValid) + return false; + + // Check if package name indicates Tier 1 or higher + // Package names: "Open Source", "Tier 1", "Tier 2", etc. + if (string.IsNullOrEmpty(packageName) || packageName.Equals("Open Source", StringComparison.OrdinalIgnoreCase) || + packageName.Equals("Invalid", StringComparison.OrdinalIgnoreCase) || + packageName.Equals("Error", StringComparison.OrdinalIgnoreCase) || + packageName.Equals("Unknown", StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + // If it's a Tier package, extract the tier number + if (packageName.StartsWith("Tier ", StringComparison.OrdinalIgnoreCase)) + { + var tierNumberStr = packageName.Substring(5).Trim(); + if (int.TryParse(tierNumberStr, out int tierNumber)) + { + return tierNumber >= 1; + } + } + + // For cloud packages (Starter, Professional, Advanced, Enterprise), they all have premium features + if (packageName.Equals("Starter", StringComparison.OrdinalIgnoreCase) || + packageName.Equals("Professional", StringComparison.OrdinalIgnoreCase) || + packageName.Equals("Advanced", StringComparison.OrdinalIgnoreCase) || + packageName.Equals("Enterprise", StringComparison.OrdinalIgnoreCase) || + packageName.Equals("Custom", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + return false; + } + catch (Exception ex) + { + Logging.Handler.Error("Membership_Reminder_Dialog -> HasMinimumTier", "Error", ex.ToString()); + return false; + } + } + private void OK() { try @@ -92,7 +144,7 @@ // Update the Membership_Reminder value if (jsonObject != null && jsonObject["Webinterface"]?["Membership_Reminder"] != null) { - jsonObject["Webinterface"]["Membership_Reminder"] = false; // Set to true or false as desired + jsonObject["Webinterface"]["Membership_Reminder"] = false; // Serialize the JSON with updated values and save back to the file var options = new JsonSerializerOptions { WriteIndented = true }; @@ -101,7 +153,7 @@ } catch (Exception ex) { - Logging.Handler.Error("/dashboard -> Check_Member_Ship_Reminder", "Error", ex.ToString()); + Logging.Handler.Error("Membership_Reminder_Dialog -> OK", "Error", ex.ToString()); } MudDialog.Close(DialogResult.Ok(true)); diff --git a/NetLock-RMM-Web-Console/Configuration/Members_Portal.cs b/NetLock-RMM-Web-Console/Configuration/Members_Portal.cs index b17590b2..d41ad936 100644 --- a/NetLock-RMM-Web-Console/Configuration/Members_Portal.cs +++ b/NetLock-RMM-Web-Console/Configuration/Members_Portal.cs @@ -1,4 +1,4 @@ namespace NetLock_RMM_Web_Console.Configuration { - //OSSCH_START 5f175dc5-6430-442b-a3e3-e53754435bb2 //OSSCH_END + //OSSCH_START 58a9b8cd-ffba-45ab-8968-2ee9f89f03e1 //OSSCH_END } diff --git a/NetLock-RMM-Web-Console/Configuration/SSO.cs b/NetLock-RMM-Web-Console/Configuration/SSO.cs index c353f73e..b26c9711 100644 --- a/NetLock-RMM-Web-Console/Configuration/SSO.cs +++ b/NetLock-RMM-Web-Console/Configuration/SSO.cs @@ -1,13 +1 @@ -namespace NetLock_RMM_Web_Console.Configuration -{ - public class Sso - { - public static bool IsEnabled = false; - public static bool IsAzureAdEnabled = false; - public static bool IsKeycloakEnabled = false; - public static bool isGoogleIdentityEnabled = false; - public static bool IsOktaEnabled = false; - public static bool IsAuth0Enabled = false; - } -} - +//OSSCH_START b3c7c455-97be-44b6-bcc0-87c0be17b561 //OSSCH_END \ No newline at end of file diff --git a/NetLock-RMM-Web-Console/Configuration/Web_Console.cs b/NetLock-RMM-Web-Console/Configuration/Web_Console.cs index f704ea0c..51c904e3 100644 --- a/NetLock-RMM-Web-Console/Configuration/Web_Console.cs +++ b/NetLock-RMM-Web-Console/Configuration/Web_Console.cs @@ -1,12 +1,14 @@ -ο»Ώnamespace NetLock_RMM_Web_Console.Configuration +ο»Ώο»Ώnamespace NetLock_RMM_Web_Console.Configuration { public class Web_Console { public static string title = String.Empty; + public static string logoBase64 = String.Empty; public static string language = "en-US"; public static bool loggingEnabled = false; public static string token_service_secret_key = String.Empty; // generated on startup public static string publicOverrideUrl = String.Empty; // used to override the public URL for the web console, useful for reverse proxies or load balancers - public static string agentConfigurationConnectionString = String.Empty; // used for cloud instances to + public static string agentConfigurationConnectionString = String.Empty; // used for cloud instances to + public static Classes.Theme.ThemePaletteConfig? customThemePalette = null; // custom theme palette loaded from database } } diff --git a/NetLock-RMM-Web-Console/Program.cs b/NetLock-RMM-Web-Console/Program.cs index 25188812..079ab6b7 100644 --- a/NetLock-RMM-Web-Console/Program.cs +++ b/NetLock-RMM-Web-Console/Program.cs @@ -45,7 +45,10 @@ var loggingEnabled = builder.Configuration.GetValue("Logging:Custom:Enabled", true); var publicOverrideUrlRaw = builder.Configuration.GetValue("Webinterface:publicOverrideUrl", string.Empty); var publicOverrideUrl = publicOverrideUrlRaw.TrimEnd('/'); -var allowedIps = builder.Configuration.GetSection("Kestrel:IpWhitelist").Get>() ?? new List(); + +// IP Whitelist will be loaded from database after MySQL connection is established +List allowedIps = new List(); + var knownProxies = builder.Configuration.GetSection("Kestrel:KnownProxies").Get>() ?? new List(); Web_Console.loggingEnabled = loggingEnabled; @@ -80,16 +83,10 @@ if (!String.IsNullOrEmpty(publicOverrideUrl)) Web_Console.publicOverrideUrl = publicOverrideUrl; -// Title -Web_Console.title = builder.Configuration.GetValue("Webinterface:Title", "NetLock RMM"); - -if (Web_Console.title == "Your company name") - Web_Console.title = "NetLock RMM"; // Default title if not set - var language = builder.Configuration["Webinterface:Language"]; // Check members portal parts -//OSSCH_START 3eb7e221-4f4f-437a-b306-765953894a92 //OSSCH_END +//OSSCH_START 383bddbe-dfb8-42f4-9393-a59e2207abd8 //OSSCH_END Console.WriteLine("---------Loader_End----------"); // Output OS @@ -115,7 +112,7 @@ Console.WriteLine($"Https (force): {https_force}"); Console.WriteLine($"Hsts: {hsts}"); Console.WriteLine($"Hsts Max Age: {hsts_max_age}"); -Console.WriteLine($"Allowed IPs: {string.Join(", ", allowedIps)}"); +Console.WriteLine($"Allowed IPs: (loaded from database)"); Console.WriteLine($"Known Proxies: {string.Join(", ", knownProxies)}"); Console.WriteLine($"Custom Certificate Path: {cert_path}"); @@ -269,7 +266,7 @@ // Get api key if (String.IsNullOrEmpty(Members_Portal.ApiKey)) { - Members_Portal.ApiKey = await NetLock_RMM_Web_Console.Classes.MySQL.Handler.Get_Api_Key(); + Members_Portal.ApiKey = await NetLock_RMM_Web_Console.Classes.MySQL.Handler.Get_Api_Key(true); Console.WriteLine("Members Portal API key loaded from database: " + Members_Portal.ApiKey); } @@ -300,6 +297,40 @@ // Update license info await NetLock_RMM_Web_Console.Classes.Members_Portal.Handler.Request_License_Info_Json(Members_Portal.ApiKey); } + + //OSSCH_START 54f54f01-0242-4acd-a29f-5e24ddcc6ab1 //OSSCH_END + + // Load IP Whitelist from database + try + { + string ipWhitelistJson = await NetLock_RMM_Web_Console.Classes.MySQL.Handler.Quick_Reader("SELECT * FROM settings", "ip_whitelist_web_console"); + + if (!string.IsNullOrEmpty(ipWhitelistJson)) + { + var ipList = System.Text.Json.JsonSerializer.Deserialize>(ipWhitelistJson); + if (ipList != null && ipList.Count > 0) + { + allowedIps = ipList; + Console.WriteLine($"IP Whitelist loaded from database: {string.Join(", ", allowedIps)}"); + } + else + { + Console.WriteLine("IP Whitelist is empty in database. All IPs will be allowed."); + } + } + else + { + Console.WriteLine("No IP Whitelist configured in database. All IPs will be allowed."); + } + } + catch (Exception ex) + { + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine($"Warning: Could not load IP Whitelist from database: {ex.Message}"); + Console.WriteLine("All IPs will be allowed."); + Console.ResetColor(); + Logging.Handler.Error("Program.cs", "Load IP Whitelist", ex.ToString()); + } } } @@ -313,9 +344,22 @@ // SSO Configuration Console.WriteLine(Environment.NewLine); +//OSSCH_START Console.WriteLine("[SSO Configuration]"); +Console.WriteLine("Loading SSO configuration from database..."); + +// Load SSO configuration from database instead of appsettings.json +var ssoConfig = await NetLock_RMM_Web_Console.Classes.MySQL.Handler.Get_Sso_Config(); -var ssoConfig = builder.Configuration.GetSection("Authentication").Get() ?? new NetLock_RMM_Web_Console.Classes.Authentication.SsoConfig(); +if (ssoConfig == null) +{ + Console.WriteLine("No SSO configuration found in database. SSO is disabled."); + ssoConfig = new NetLock_RMM_Web_Console.Classes.Authentication.SsoConfig { Enabled = false }; +} +else +{ + Console.WriteLine("SSO configuration loaded from database successfully."); +} if (ssoConfig.Enabled) { @@ -354,8 +398,48 @@ if (Sso.IsAzureAdEnabled || Sso.IsKeycloakEnabled || Sso.isGoogleIdentityEnabled || Sso.IsOktaEnabled || Sso.IsAuth0Enabled) { - Console.WriteLine("SSO is enabled. Registering SSO authentication providers..."); - AuthProviderRegistrar.RegisterSsoProviders(builder.Services, builder.Configuration); + try + { + Console.WriteLine("SSO is enabled. Registering SSO authentication providers..."); + AuthProviderRegistrar.RegisterSsoProviders(builder.Services, ssoConfig); + } + catch (Exception ex) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine("========================================"); + Console.WriteLine("ERROR: Failed to register SSO providers!"); + Console.WriteLine("========================================"); + Console.WriteLine($"Error: {ex.Message}"); + Console.WriteLine(Environment.NewLine); + Console.WriteLine("SSO configuration appears to be invalid or incomplete."); + Console.WriteLine("Common issues:"); + Console.WriteLine(" - Missing required fields (ClientId, ClientSecret, Domain, etc.)"); + Console.WriteLine(" - Invalid URLs or endpoints"); + Console.WriteLine(" - Incorrect certificate configuration"); + Console.WriteLine(Environment.NewLine); + Console.WriteLine("The application will continue running with SSO DISABLED."); + Console.WriteLine("Please check your SSO configuration in the database and restart the application."); + Console.WriteLine("========================================"); + Console.ResetColor(); + + // Log the error + Logging.Handler.Error("SSO Configuration", "Provider Registration Failed", ex.ToString()); + + // Set error flag and message + Sso.ConfigurationError = true; + Sso.ConfigurationErrorMessage = ex.Message; + + // Disable SSO flags + Sso.IsEnabled = false; + Sso.IsAzureAdEnabled = false; + Sso.IsKeycloakEnabled = false; + Sso.isGoogleIdentityEnabled = false; + Sso.IsOktaEnabled = false; + Sso.IsAuth0Enabled = false; + + // Fall back to default authentication + builder.Services.AddAuthenticationCore(); + } } else { @@ -369,6 +453,7 @@ builder.Services.AddAuthenticationCore(); } Console.WriteLine(Environment.NewLine); +//OSSCH_END // Blazor and core services builder.Services.AddBlazoredLocalStorage(); @@ -489,7 +574,7 @@ { Logging.Handler.Error("Middleware", "IP Whitelisting", $"IP {remoteIp} is not whitelisted."); context.Response.StatusCode = 403; - await context.Response.WriteAsync("Your IP is unknown."); + await context.Response.WriteAsync("Your IP is unknown. Your ip: " + remoteIp); return; } @@ -528,7 +613,7 @@ app.UseAntiforgery(); app.MapRazorComponents().AddInteractiveServerRenderMode(); -//OSSCH_START 269aeefa-6750-41e1-96c1-f2fa9a018564 //OSSCH_END +//OSSCH_START 8da1e227-8ff8-42be-96d2-51f5cf5407f5 //OSSCH_END Console.WriteLine("---------Loader_End----------"); @@ -536,412 +621,9 @@ Console.WriteLine("Server started."); // SSO Challenge Endpoints -if (Sso.IsEnabled) +if (Sso.IsEnabled && Members_Portal.IsCodeSigned) { - Console.WriteLine("SSO endpoints registered:"); - - // Azure AD / Microsoft Entra ID Endpoints - if (Sso.IsAzureAdEnabled) - { - // Azure AD Challenge - app.MapGet("/challenge/azuread", async context => - { - await context.ChallengeAsync(Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectDefaults.AuthenticationScheme, - new Microsoft.AspNetCore.Authentication.AuthenticationProperties - { - RedirectUri = "/sso-callback" - }); - }); - - // Azure AD Signout Callback - Handles the return from Azure AD after logout (POST) - app.MapPost("/signout-callback-oidc", async context => - { - Console.WriteLine("SSO: Signout callback received from Azure AD (POST)"); - Logging.Handler.Debug("SSO", "Signout Callback", "Returned from Azure AD logout (POST)"); - - try - { - // Sign out from ALL authentication schemes to ensure complete logout - await context.SignOutAsync(Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationDefaults.AuthenticationScheme); - await context.SignOutAsync(Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectDefaults.AuthenticationScheme); - - Console.WriteLine("SSO: All local authentication schemes cleared"); - Logging.Handler.Debug("SSO", "Signout Callback", "Local sessions cleared"); - } - catch (Exception ex) - { - Console.WriteLine($"SSO: Error clearing local sessions: {ex.Message}"); - Logging.Handler.Error("SSO", "Signout Callback Error", ex.ToString()); - } - - Console.WriteLine("SSO: Redirecting to login with logout flag and forcing reload"); - // Add cache control headers to force browser to reload and not use cached version - context.Response.Headers["Cache-Control"] = "no-cache, no-store, must-revalidate"; - context.Response.Headers["Pragma"] = "no-cache"; - context.Response.Headers["Expires"] = "0"; - context.Response.Redirect("/?logout=true"); - }); - - // Azure AD Signout Callback - Also handle GET requests (some IdPs use GET instead of POST) - app.MapGet("/signout-callback-oidc", async context => - { - Console.WriteLine("SSO: Signout callback received from Azure AD (GET)"); - Logging.Handler.Debug("SSO", "Signout Callback", "Returned from Azure AD logout (GET)"); - - try - { - // Sign out from ALL authentication schemes to ensure complete logout - await context.SignOutAsync(Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationDefaults.AuthenticationScheme); - await context.SignOutAsync(Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectDefaults.AuthenticationScheme); - - Console.WriteLine("SSO: All local authentication schemes cleared"); - Logging.Handler.Debug("SSO", "Signout Callback", "Local sessions cleared"); - } - catch (Exception ex) - { - Console.WriteLine($"SSO: Error clearing local sessions: {ex.Message}"); - Logging.Handler.Error("SSO", "Signout Callback Error", ex.ToString()); - } - - Console.WriteLine("SSO: Redirecting to login with logout flag and forcing reload"); - // Add cache control headers to force browser to reload and not use cached version - context.Response.Headers["Cache-Control"] = "no-cache, no-store, must-revalidate"; - context.Response.Headers["Pragma"] = "no-cache"; - context.Response.Headers["Expires"] = "0"; - context.Response.Redirect("/?logout=true"); - }); - - Console.WriteLine(" - /challenge/azuread (Azure AD / Microsoft Entra ID login)"); - Console.WriteLine(" - /signout-callback-oidc (Azure AD logout callback)"); - } - - // Keycloak Endpoints - if (Sso.IsKeycloakEnabled) - { - // Keycloak Challenge - app.MapGet("/challenge/keycloak", async context => - { - await context.ChallengeAsync(Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectDefaults.AuthenticationScheme, - new Microsoft.AspNetCore.Authentication.AuthenticationProperties - { - RedirectUri = "/sso-callback" - }); - }); - - // Keycloak Signout Callback (POST) - app.MapPost("/signout-callback-keycloak", async context => - { - Console.WriteLine("SSO: Keycloak signout callback received (POST)"); - Logging.Handler.Debug("SSO", "Keycloak Signout Callback", "Returned from Keycloak logout (POST)"); - - try - { - await context.SignOutAsync(Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationDefaults.AuthenticationScheme); - await context.SignOutAsync(Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectDefaults.AuthenticationScheme); - - Console.WriteLine("SSO: Keycloak - All local authentication schemes cleared"); - Logging.Handler.Debug("SSO", "Keycloak Signout Callback", "Local sessions cleared"); - } - catch (Exception ex) - { - Console.WriteLine($"SSO: Keycloak - Error clearing local sessions: {ex.Message}"); - Logging.Handler.Error("SSO", "Keycloak Signout Callback Error", ex.ToString()); - } - - context.Response.Headers["Cache-Control"] = "no-cache, no-store, must-revalidate"; - context.Response.Headers["Pragma"] = "no-cache"; - context.Response.Headers["Expires"] = "0"; - context.Response.Redirect("/?logout=true"); - }); - - // Keycloak Signout Callback (GET) - app.MapGet("/signout-callback-keycloak", async context => - { - Console.WriteLine("SSO: Keycloak signout callback received (GET)"); - Logging.Handler.Debug("SSO", "Keycloak Signout Callback", "Returned from Keycloak logout (GET)"); - - try - { - await context.SignOutAsync(Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationDefaults.AuthenticationScheme); - await context.SignOutAsync(Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectDefaults.AuthenticationScheme); - - Console.WriteLine("SSO: Keycloak - All local authentication schemes cleared"); - Logging.Handler.Debug("SSO", "Keycloak Signout Callback", "Local sessions cleared"); - } - catch (Exception ex) - { - Console.WriteLine($"SSO: Keycloak - Error clearing local sessions: {ex.Message}"); - Logging.Handler.Error("SSO", "Keycloak Signout Callback Error", ex.ToString()); - } - - context.Response.Headers["Cache-Control"] = "no-cache, no-store, must-revalidate"; - context.Response.Headers["Pragma"] = "no-cache"; - context.Response.Headers["Expires"] = "0"; - context.Response.Redirect("/?logout=true"); - }); - - Console.WriteLine(" - /challenge/keycloak (Keycloak login)"); - Console.WriteLine(" - /signout-callback-keycloak (Keycloak logout callback)"); - } - - // Google Workspace / Google Identity Endpoints - if (Sso.isGoogleIdentityEnabled) - { - // Google Challenge - app.MapGet("/challenge/google", async context => - { - await context.ChallengeAsync(Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectDefaults.AuthenticationScheme, - new Microsoft.AspNetCore.Authentication.AuthenticationProperties - { - RedirectUri = "/sso-callback" - }); - }); - - // Google Signout Callback (POST) - app.MapPost("/signout-callback-google", async context => - { - Console.WriteLine("SSO: Google signout callback received (POST)"); - Logging.Handler.Debug("SSO", "Google Signout Callback", "Returned from Google logout (POST)"); - - try - { - await context.SignOutAsync(Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationDefaults.AuthenticationScheme); - await context.SignOutAsync(Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectDefaults.AuthenticationScheme); - - Console.WriteLine("SSO: Google - All local authentication schemes cleared"); - Logging.Handler.Debug("SSO", "Google Signout Callback", "Local sessions cleared"); - } - catch (Exception ex) - { - Console.WriteLine($"SSO: Google - Error clearing local sessions: {ex.Message}"); - Logging.Handler.Error("SSO", "Google Signout Callback Error", ex.ToString()); - } - - context.Response.Headers["Cache-Control"] = "no-cache, no-store, must-revalidate"; - context.Response.Headers["Pragma"] = "no-cache"; - context.Response.Headers["Expires"] = "0"; - context.Response.Redirect("/?logout=true"); - }); - - // Google Signout Callback (GET) - app.MapGet("/signout-callback-google", async context => - { - Console.WriteLine("SSO: Google signout callback received (GET)"); - Logging.Handler.Debug("SSO", "Google Signout Callback", "Returned from Google logout (GET)"); - - try - { - await context.SignOutAsync(Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationDefaults.AuthenticationScheme); - await context.SignOutAsync(Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectDefaults.AuthenticationScheme); - - Console.WriteLine("SSO: Google - All local authentication schemes cleared"); - Logging.Handler.Debug("SSO", "Google Signout Callback", "Local sessions cleared"); - } - catch (Exception ex) - { - Console.WriteLine($"SSO: Google - Error clearing local sessions: {ex.Message}"); - Logging.Handler.Error("SSO", "Google Signout Callback Error", ex.ToString()); - } - - context.Response.Headers["Cache-Control"] = "no-cache, no-store, must-revalidate"; - context.Response.Headers["Pragma"] = "no-cache"; - context.Response.Headers["Expires"] = "0"; - context.Response.Redirect("/?logout=true"); - }); - - Console.WriteLine(" - /challenge/google (Google Workspace / Google Identity login)"); - Console.WriteLine(" - /signout-callback-google (Google logout callback)"); - } - - // Okta Endpoints - if (Sso.IsOktaEnabled) - { - // Okta Challenge - app.MapGet("/challenge/okta", async context => - { - await context.ChallengeAsync(Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectDefaults.AuthenticationScheme, - new Microsoft.AspNetCore.Authentication.AuthenticationProperties - { - RedirectUri = "/sso-callback" - }); - }); - - // Okta Signout Callback (POST) - app.MapPost("/signout-callback-okta", async context => - { - Console.WriteLine("SSO: Okta signout callback received (POST)"); - Logging.Handler.Debug("SSO", "Okta Signout Callback", "Returned from Okta logout (POST)"); - - try - { - await context.SignOutAsync(Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationDefaults.AuthenticationScheme); - await context.SignOutAsync(Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectDefaults.AuthenticationScheme); - - Console.WriteLine("SSO: Okta - All local authentication schemes cleared"); - Logging.Handler.Debug("SSO", "Okta Signout Callback", "Local sessions cleared"); - } - catch (Exception ex) - { - Console.WriteLine($"SSO: Okta - Error clearing local sessions: {ex.Message}"); - Logging.Handler.Error("SSO", "Okta Signout Callback Error", ex.ToString()); - } - - context.Response.Headers["Cache-Control"] = "no-cache, no-store, must-revalidate"; - context.Response.Headers["Pragma"] = "no-cache"; - context.Response.Headers["Expires"] = "0"; - context.Response.Redirect("/?logout=true"); - }); - - // Okta Signout Callback (GET) - app.MapGet("/signout-callback-okta", async context => - { - Console.WriteLine("SSO: Okta signout callback received (GET)"); - Logging.Handler.Debug("SSO", "Okta Signout Callback", "Returned from Okta logout (GET)"); - - try - { - await context.SignOutAsync(Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationDefaults.AuthenticationScheme); - await context.SignOutAsync(Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectDefaults.AuthenticationScheme); - - Console.WriteLine("SSO: Okta - All local authentication schemes cleared"); - Logging.Handler.Debug("SSO", "Okta Signout Callback", "Local sessions cleared"); - } - catch (Exception ex) - { - Console.WriteLine($"SSO: Okta - Error clearing local sessions: {ex.Message}"); - Logging.Handler.Error("SSO", "Okta Signout Callback Error", ex.ToString()); - } - - context.Response.Headers["Cache-Control"] = "no-cache, no-store, must-revalidate"; - context.Response.Headers["Pragma"] = "no-cache"; - context.Response.Headers["Expires"] = "0"; - context.Response.Redirect("/?logout=true"); - }); - - Console.WriteLine(" - /challenge/okta (Okta login)"); - Console.WriteLine(" - /signout-callback-okta (Okta logout callback)"); - } - - // Auth0 Endpoints - if (Sso.IsAuth0Enabled) - { - // Auth0 Challenge - app.MapGet("/challenge/auth0", async context => - { - await context.ChallengeAsync(Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectDefaults.AuthenticationScheme, - new Microsoft.AspNetCore.Authentication.AuthenticationProperties - { - RedirectUri = "/sso-callback" - }); - }); - - // Auth0 Signout Callback (POST) - app.MapPost("/signout-callback-auth0", async context => - { - Console.WriteLine("SSO: Auth0 signout callback received (POST)"); - Logging.Handler.Debug("SSO", "Auth0 Signout Callback", "Returned from Auth0 logout (POST)"); - - try - { - await context.SignOutAsync(Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationDefaults.AuthenticationScheme); - await context.SignOutAsync(Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectDefaults.AuthenticationScheme); - - Console.WriteLine("SSO: Auth0 - All local authentication schemes cleared"); - Logging.Handler.Debug("SSO", "Auth0 Signout Callback", "Local sessions cleared"); - } - catch (Exception ex) - { - Console.WriteLine($"SSO: Auth0 - Error clearing local sessions: {ex.Message}"); - Logging.Handler.Error("SSO", "Auth0 Signout Callback Error", ex.ToString()); - } - - context.Response.Headers["Cache-Control"] = "no-cache, no-store, must-revalidate"; - context.Response.Headers["Pragma"] = "no-cache"; - context.Response.Headers["Expires"] = "0"; - context.Response.Redirect("/?logout=true"); - }); - - // Auth0 Signout Callback (GET) - app.MapGet("/signout-callback-auth0", async context => - { - Console.WriteLine("SSO: Auth0 signout callback received (GET)"); - Logging.Handler.Debug("SSO", "Auth0 Signout Callback", "Returned from Auth0 logout (GET)"); - - try - { - await context.SignOutAsync(Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationDefaults.AuthenticationScheme); - await context.SignOutAsync(Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectDefaults.AuthenticationScheme); - - Console.WriteLine("SSO: Auth0 - All local authentication schemes cleared"); - Logging.Handler.Debug("SSO", "Auth0 Signout Callback", "Local sessions cleared"); - } - catch (Exception ex) - { - Console.WriteLine($"SSO: Auth0 - Error clearing local sessions: {ex.Message}"); - Logging.Handler.Error("SSO", "Auth0 Signout Callback Error", ex.ToString()); - } - - context.Response.Headers["Cache-Control"] = "no-cache, no-store, must-revalidate"; - context.Response.Headers["Pragma"] = "no-cache"; - context.Response.Headers["Expires"] = "0"; - context.Response.Redirect("/?logout=true"); - }); - - Console.WriteLine(" - /challenge/auth0 (Auth0 login)"); - Console.WriteLine(" - /signout-callback-auth0 (Auth0 logout callback)"); - } - - // Common SSO Signout Handler - Used by all providers - if (Sso.IsAzureAdEnabled || Sso.IsKeycloakEnabled || Sso.isGoogleIdentityEnabled || Sso.IsOktaEnabled || Sso.IsAuth0Enabled) - { - app.MapGet("/sso-signout", async context => - { - Console.WriteLine("SSO: Signout handler called"); - Logging.Handler.Debug("SSO", "Signout", "Initiating SSO logout process"); - - try - { - // Check if user is authenticated - if (context.User?.Identity?.IsAuthenticated == true) - { - Console.WriteLine($"SSO: Signing out user: {context.User.Identity.Name}"); - Logging.Handler.Debug("SSO", "Signout User", context.User.Identity.Name ?? "unknown"); - - // IMPORTANT: Sign out from Cookie scheme FIRST to clear local session immediately - // This prevents automatic re-login when user returns - await context.SignOutAsync(Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationDefaults.AuthenticationScheme); - Console.WriteLine("SSO: Local cookie session cleared"); - Logging.Handler.Debug("SSO", "Signout", "Local cookie session cleared"); - - // Then sign out from OIDC scheme to logout from the identity provider - // This will redirect to the IdP logout page and then back to our app - await context.SignOutAsync(Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectDefaults.AuthenticationScheme, - new Microsoft.AspNetCore.Authentication.AuthenticationProperties - { - RedirectUri = "/?logout=true" - }); - - Console.WriteLine("SSO: OIDC signout initiated, redirecting to identity provider"); - Logging.Handler.Debug("SSO", "Signout", "OIDC signout flow started"); - } - else - { - Console.WriteLine("SSO: No authenticated user found, redirecting to login"); - Logging.Handler.Debug("SSO", "Signout", "No authenticated user, redirecting"); - context.Response.Redirect("/?logout=true"); - } - } - catch (Exception ex) - { - Console.WriteLine($"SSO: Error during signout: {ex.Message}"); - Logging.Handler.Error("SSO", "Signout Error", ex.ToString()); - context.Response.Redirect("/?logout=true"); - } - }); - - Console.WriteLine(" - /sso-signout (Initiate SSO logout)"); - } -} + //OSSCH_START 005eb6a8-4538-41a8-8cc1-924dcd0f9075 //OSSCH_END // Test endpoint app.MapGet("/test", async context => @@ -953,7 +635,7 @@ await context.SignOutAsync(Microsoft.AspNetCore.Authentication.OpenIdConnect.Ope // Members Portal Api Cloud Version Endpoints if (Members_Portal.IsApiEnabled && Members_Portal.IsCloudEnabled) { - //OSSCH_START 5c023aad-e525-48fe-bc13-00db8f83b187 //OSSCH_END + //OSSCH_START c5623f1f-d791-43d2-816e-e8a7393ec95b //OSSCH_END } // Start server diff --git a/NetLock-RMM-Web-Console/appsettings.json b/NetLock-RMM-Web-Console/appsettings.json index f88f28b9..bd744e48 100644 --- a/NetLock-RMM-Web-Console/appsettings.json +++ b/NetLock-RMM-Web-Console/appsettings.json @@ -1,134 +1,64 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Warning", - "Microsoft": "Error", - "Microsoft.Hosting.Lifetime": "Warning" - }, - "Custom": { - "Enabled": true - } - }, - "AllowedHosts": "*", - "Kestrel": { - "Endpoint": { - "Http": { - "Enabled": true, - "Port": 5000 - }, - "Https": { - "Enabled": false, - "Port": 5001, - "Force": false, - "Hsts": { - "Enabled": false - }, - "Certificate": { - "Path": "C:\\temp\\localhost.pfx", - "Password": "" - } - } - }, - "IpWhitelist": [ ], - "KnownProxies": [ ] - }, - "NetLock_Remote_Server": { - "Server": "127.0.0.1", - "Port": 7080, - "UseSSL": false - }, - "NetLock_File_Server": { - "Server": "127.0.0.1", - "Port": 7080, - "UseSSL": false - }, - "MySQL": { - "Server": "127.0.0.1", - "Port": 3306, - "Database": "netlock", - "User": "root", - "Password": "Bochum234", - "SslMode": "None", - "AdditionalConnectionParameters": "AllowPublicKeyRetrieval=True;" - }, - "LettuceEncrypt": { - "Enabled": false, - "AcceptTermsOfService": true, - "DomainNames": [ - "demo.netlockrmm.com" - ], - "EmailAddress": "nico.mak@0x101-cyber-security.de", - "AllowedChallengeTypes": "Http01", - "CertificateStoredPfxPassword": "Password123" - }, - "Webinterface": { - "Title": "Your company name", - "Language": "en-US", - "Membership_Reminder": false, - "PublicOverrideUrl": "http://localhost:7080" - }, - "Members_Portal_Api": { - "Enabled": true, - "Cloud": true, - "ApiKeyOverride": "2b77005b-61a3-4fa8-83c9-5b9f3621f758-ba910728-1b53-4cb5-9159-3eb4140a2eed", - "ServerGuid": "Test-Guid" - }, - "Authentication": { - "Enabled": true, - "AzureAd": { - "Enabled": true, - "Instance": "https://login.microsoftonline.com/", - "Domain": "your-domain.onmicrosoft.com", - "TenantId": "your-tenant-id", - "ClientId": "your-client-id", - "CallbackPath": "/signin-oidc", - "ClientSecret": "your-client-secret", - "SignedOutCallbackPath": "/signout-callback-oidc", - "ResponseType": "code", - "SaveTokens": true - }, - "Keycloak": { - "Enabled": false, - "Authority": "https://keycloak.example.com/realms/your-realm", - "Realm": "your-realm", - "ClientId": "netlock-rmm", - "ClientSecret": "your-client-secret", - "CallbackPath": "/signin-keycloak", - "SignedOutCallbackPath": "/signout-callback-keycloak", - "ResponseType": "code", - "SaveTokens": false, - "GetClaimsFromUserInfoEndpoint": true, - "RequireHttpsMetadata": true - }, - "GoogleIdentity": { - "Enabled": false, - "ClientId": "your-client-id.apps.googleusercontent.com", - "ClientSecret": "your-client-secret", - "CallbackPath": "/signin-google", - "SignedOutCallbackPath": "/signout-callback-google", - "SaveTokens": false, - "HostedDomain": "" - }, - "Okta": { - "Enabled": false, - "Domain": "dev-12345.okta.com", - "ClientId": "your-client-id", - "ClientSecret": "your-client-secret", - "CallbackPath": "/signin-okta", - "SignedOutCallbackPath": "/signout-callback-okta", - "SaveTokens": false, - "GetClaimsFromUserInfoEndpoint": true, - "AuthorizationServerId": "default" - }, - "Auth0": { - "Enabled": false, - "Domain": "your-tenant.auth0.com", - "ClientId": "your-client-id", - "ClientSecret": "your-client-secret", - "CallbackPath": "/signin-auth0", - "SignedOutCallbackPath": "/signout-callback-auth0", - "SaveTokens": false, - "Audience": "" - } - } +{ + "Logging": { + "LogLevel": { + "Default": "Warning", + "Microsoft": "Error", + "Microsoft.Hosting.Lifetime": "Warning" + }, + "Custom": { + "Enabled": true + } + }, + "AllowedHosts": "*", + "Kestrel": { + "Endpoint": { + "Http": { + "Enabled": true, + "Port": 5000 + }, + "Https": { + "Enabled": false, + "Port": 5001, + "Force": false, + "Hsts": { + "Enabled": false + }, + "Certificate": { + "Path": "C:\\temp\\localhost.pfx", + "Password": "" + } + } + }, + "KnownProxies": [] + }, + "NetLock_Remote_Server": { + "Server": "127.0.0.1", + "Port": 7080, + "UseSSL": false + }, + "NetLock_File_Server": { + "Server": "127.0.0.1", + "Port": 7080, + "UseSSL": false + }, + "MySQL": { + "Server": "127.0.0.1", + "Port": 3306, + "Database": "netlock", + "User": "root", + "Password": "Bochum234", + "SslMode": "None", + "AdditionalConnectionParameters": "AllowPublicKeyRetrieval=True;" + }, + "Webinterface": { + "Language": "en-US", + "Membership_Reminder": false, + "PublicOverrideUrl": "http://localhost:7080" + }, + "Members_Portal_Api": { + "Enabled": true, + "Cloud": false, + "ApiKeyOverride": "2b77005b-61a3-4fa8-83c9-5b9f3621f758-ba910728-1b53-4cb5-9159-3eb4140a2eed", + "ServerGuid": "Test-Guid" + } } \ No newline at end of file diff --git a/NetLock-RMM-Web-Console/wwwroot/fonts/Poppins-Bold.ttf b/NetLock-RMM-Web-Console/wwwroot/fonts/Poppins-Bold.ttf new file mode 100644 index 00000000..1982f38a Binary files /dev/null and b/NetLock-RMM-Web-Console/wwwroot/fonts/Poppins-Bold.ttf differ diff --git a/NetLock-RMM-Web-Console/wwwroot/fonts/Poppins-Light.ttf b/NetLock-RMM-Web-Console/wwwroot/fonts/Poppins-Light.ttf new file mode 100644 index 00000000..931ae8fd Binary files /dev/null and b/NetLock-RMM-Web-Console/wwwroot/fonts/Poppins-Light.ttf differ diff --git a/NetLock-RMM-Web-Console/wwwroot/fonts/Poppins-Medium.ttf b/NetLock-RMM-Web-Console/wwwroot/fonts/Poppins-Medium.ttf new file mode 100644 index 00000000..a590f5c3 Binary files /dev/null and b/NetLock-RMM-Web-Console/wwwroot/fonts/Poppins-Medium.ttf differ diff --git a/NetLock-RMM-Web-Console/wwwroot/fonts/Poppins-Regular.ttf b/NetLock-RMM-Web-Console/wwwroot/fonts/Poppins-Regular.ttf new file mode 100644 index 00000000..0bda228a Binary files /dev/null and b/NetLock-RMM-Web-Console/wwwroot/fonts/Poppins-Regular.ttf differ diff --git a/NetLock-RMM-Web-Console/wwwroot/fonts/Poppins-SemiBold.ttf b/NetLock-RMM-Web-Console/wwwroot/fonts/Poppins-SemiBold.ttf new file mode 100644 index 00000000..c30ad104 Binary files /dev/null and b/NetLock-RMM-Web-Console/wwwroot/fonts/Poppins-SemiBold.ttf differ diff --git a/NetLock-RMM-Web-Console/wwwroot/fonts/Roboto-Bold.ttf b/NetLock-RMM-Web-Console/wwwroot/fonts/Roboto-Bold.ttf new file mode 100644 index 00000000..8869666f Binary files /dev/null and b/NetLock-RMM-Web-Console/wwwroot/fonts/Roboto-Bold.ttf differ diff --git a/NetLock-RMM-Web-Console/wwwroot/fonts/Roboto-Light.ttf b/NetLock-RMM-Web-Console/wwwroot/fonts/Roboto-Light.ttf new file mode 100644 index 00000000..e19094a8 Binary files /dev/null and b/NetLock-RMM-Web-Console/wwwroot/fonts/Roboto-Light.ttf differ diff --git a/NetLock-RMM-Web-Console/wwwroot/fonts/Roboto-Medium.ttf b/NetLock-RMM-Web-Console/wwwroot/fonts/Roboto-Medium.ttf new file mode 100644 index 00000000..6a951337 Binary files /dev/null and b/NetLock-RMM-Web-Console/wwwroot/fonts/Roboto-Medium.ttf differ diff --git a/NetLock-RMM-Web-Console/wwwroot/fonts/Roboto-Regular.ttf b/NetLock-RMM-Web-Console/wwwroot/fonts/Roboto-Regular.ttf new file mode 100644 index 00000000..ddee473e Binary files /dev/null and b/NetLock-RMM-Web-Console/wwwroot/fonts/Roboto-Regular.ttf differ diff --git a/NetLock-RMM-Web-Console/wwwroot/fonts/poppins.css b/NetLock-RMM-Web-Console/wwwroot/fonts/poppins.css new file mode 100644 index 00000000..d19fae46 --- /dev/null +++ b/NetLock-RMM-Web-Console/wwwroot/fonts/poppins.css @@ -0,0 +1,42 @@ +/* Poppins Font - Local Version (GDPR compliant) */ + +@font-face { + font-family: 'Poppins'; + font-style: normal; + font-weight: 300; + font-display: swap; + src: url('Poppins-Light.ttf') format('truetype'); +} + +@font-face { + font-family: 'Poppins'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('Poppins-Regular.ttf') format('truetype'); +} + +@font-face { + font-family: 'Poppins'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url('Poppins-Medium.ttf') format('truetype'); +} + +@font-face { + font-family: 'Poppins'; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url('Poppins-SemiBold.ttf') format('truetype'); +} + +@font-face { + font-family: 'Poppins'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url('Poppins-Bold.ttf') format('truetype'); +} + diff --git a/NetLock-RMM-Web-Console/wwwroot/fonts/roboto.css b/NetLock-RMM-Web-Console/wwwroot/fonts/roboto.css new file mode 100644 index 00000000..735c8017 --- /dev/null +++ b/NetLock-RMM-Web-Console/wwwroot/fonts/roboto.css @@ -0,0 +1,34 @@ +/* Roboto Font - Local Version (GDPR compliant) */ + +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 300; + font-display: swap; + src: url('Roboto-Light.ttf') format('truetype'); +} + +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('Roboto-Regular.ttf') format('truetype'); +} + +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url('Roboto-Medium.ttf') format('truetype'); +} + +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url('Roboto-Bold.ttf') format('truetype'); +} + diff --git a/NetLock-RMM-Web-Console/wwwroot/js/blood-particles.js b/NetLock-RMM-Web-Console/wwwroot/js/blood-particles.js new file mode 100644 index 00000000..6477c62f --- /dev/null +++ b/NetLock-RMM-Web-Console/wwwroot/js/blood-particles.js @@ -0,0 +1,167 @@ +// Blood Particles Animation for Login Page - Enhanced Version +class BloodParticle { + constructor(canvas) { + this.canvas = canvas; + this.trail = []; + this.maxTrailLength = 8; + this.glowPhase = Math.random() * Math.PI * 2; + this.rotation = Math.random() * Math.PI * 2; + this.rotationSpeed = (Math.random() - 0.5) * 0.05; + this.reset(); + } + reset() { + this.x = Math.random() * this.canvas.width; + this.y = -20 - Math.random() * 100; + // More size variation - small droplets to big splatters + const sizeType = Math.random(); + if (sizeType < 0.7) { + this.size = Math.random() * 2 + 1; // Small droplets (1-3px) + } else if (sizeType < 0.9) { + this.size = Math.random() * 4 + 3; // Medium drops (3-7px) + } else { + this.size = Math.random() * 6 + 5; // Large splatters (5-11px) + } + this.speedY = Math.random() * 1.5 + 0.8; + this.speedX = (Math.random() - 0.5) * 0.8; + this.baseOpacity = Math.random() * 0.4 + 0.5; + this.opacity = this.baseOpacity; + // Enhanced blood color variations + const redVariations = [ + { r: 139, g: 0, b: 0 }, + { r: 165, g: 0, b: 0 }, + { r: 178, g: 34, b: 34 }, + { r: 200, g: 20, b: 40 }, + { r: 220, g: 20, b: 60 }, + { r: 180, g: 0, b: 20 }, + { r: 128, g: 0, b: 0 }, + { r: 150, g: 10, b: 10 }, + ]; + this.colorObj = redVariations[Math.floor(Math.random() * redVariations.length)]; + this.color = `rgba(${this.colorObj.r}, ${this.colorObj.g}, ${this.colorObj.b}, `; + this.trail = []; + this.willSplash = Math.random() > 0.7; + this.hasSplashed = false; + } + update() { + // Store trail position + if (this.trail.length < this.maxTrailLength) { + this.trail.push({ x: this.x, y: this.y, opacity: this.opacity }); + } else { + this.trail.shift(); + this.trail.push({ x: this.x, y: this.y, opacity: this.opacity }); + } + this.y += this.speedY; + this.x += this.speedX; + this.x += Math.sin(this.y * 0.015) * 0.4; + this.rotation += this.rotationSpeed; + // Pulsating glow + this.glowPhase += 0.03; + this.opacity = this.baseOpacity + Math.sin(this.glowPhase) * 0.15; + // Splash effect + if (this.y > this.canvas.height - 20 && this.willSplash && !this.hasSplashed) { + this.speedY *= 0.3; + this.opacity *= 0.7; + this.hasSplashed = true; + } + if (this.y > this.canvas.height + 30) { + this.reset(); + } + if (this.x < -20) this.x = this.canvas.width + 20; + if (this.x > this.canvas.width + 20) this.x = -20; + } + draw(ctx) { + // Draw trail + for (let i = 0; i < this.trail.length; i++) { + const trailPoint = this.trail[i]; + const trailOpacity = (i / this.trail.length) * trailPoint.opacity * 0.6; + const trailSize = this.size * (0.3 + (i / this.trail.length) * 0.7); + ctx.fillStyle = this.color + trailOpacity + ')'; + ctx.beginPath(); + ctx.arc(trailPoint.x, trailPoint.y, trailSize, 0, Math.PI * 2); + ctx.fill(); + } + ctx.save(); + ctx.translate(this.x, this.y); + ctx.rotate(this.rotation); + const glowIntensity = 1 + Math.sin(this.glowPhase) * 0.3; + ctx.shadowBlur = this.size * 3 * glowIntensity; + ctx.shadowColor = this.color + (this.opacity * 0.8) + ')'; + ctx.fillStyle = this.color + this.opacity + ')'; + ctx.beginPath(); + if (this.size > 4) { + const points = 8; + for (let i = 0; i < points; i++) { + const angle = (i / points) * Math.PI * 2; + const radius = this.size * (0.8 + Math.random() * 0.4); + const x = Math.cos(angle) * radius; + const y = Math.sin(angle) * radius; + if (i === 0) { + ctx.moveTo(x, y); + } else { + ctx.lineTo(x, y); + } + } + ctx.closePath(); + } else { + ctx.arc(0, 0, this.size, 0, Math.PI * 2); + } + ctx.fill(); + // Inner highlight + ctx.shadowBlur = 0; + ctx.fillStyle = `rgba(${this.colorObj.r + 30}, ${this.colorObj.g + 10}, ${this.colorObj.b + 10}, ${this.opacity * 0.4})`; + ctx.beginPath(); + ctx.arc(-this.size * 0.2, -this.size * 0.2, this.size * 0.3, 0, Math.PI * 2); + ctx.fill(); + ctx.restore(); + } +} +let bloodParticlesCanvas = null; +let bloodParticlesCtx = null; +let bloodParticles = []; +let animationFrameId = null; +function initBloodParticles() { + console.log('🩸 Initializing enhanced blood particles...'); + bloodParticlesCanvas = document.getElementById('blood-particles-canvas'); + if (!bloodParticlesCanvas) { + console.warn('Blood particles canvas not found'); + return; + } + bloodParticlesCtx = bloodParticlesCanvas.getContext('2d'); + resizeCanvas(); + window.addEventListener('resize', resizeCanvas); + const particleCount = 80; + bloodParticles = []; + for (let i = 0; i < particleCount; i++) { + bloodParticles.push(new BloodParticle(bloodParticlesCanvas)); + bloodParticles[i].y = Math.random() * bloodParticlesCanvas.height - 100; + } + console.log(`✨ Created ${particleCount} enhanced blood particles`); + animateBloodParticles(); +} +function resizeCanvas() { + if (!bloodParticlesCanvas) return; + bloodParticlesCanvas.width = window.innerWidth; + bloodParticlesCanvas.height = window.innerHeight; +} +function animateBloodParticles() { + if (!bloodParticlesCanvas || !bloodParticlesCtx) return; + bloodParticlesCtx.fillStyle = 'rgba(0, 0, 0, 0.02)'; + bloodParticlesCtx.fillRect(0, 0, bloodParticlesCanvas.width, bloodParticlesCanvas.height); + bloodParticles.forEach(particle => { + particle.update(); + particle.draw(bloodParticlesCtx); + }); + animationFrameId = requestAnimationFrame(animateBloodParticles); +} +function stopBloodParticles() { + if (animationFrameId) { + cancelAnimationFrame(animationFrameId); + animationFrameId = null; + } + if (bloodParticlesCtx && bloodParticlesCanvas) { + bloodParticlesCtx.clearRect(0, 0, bloodParticlesCanvas.width, bloodParticlesCanvas.height); + } +} +window.initBloodParticles = initBloodParticles; +window.stopBloodParticles = stopBloodParticles; +console.log('🩸 Enhanced blood particles script loaded'); diff --git a/NetLock-RMM-Web-Console/wwwroot/js/towerdefense.js b/NetLock-RMM-Web-Console/wwwroot/js/towerdefense.js new file mode 100644 index 00000000..dff6d6e8 --- /dev/null +++ b/NetLock-RMM-Web-Console/wwwroot/js/towerdefense.js @@ -0,0 +1,1024 @@ +// Computer Virus Defense Game +(function() { + let canvas; + let ctx; + let gameRunning = false; + let gamePaused = false; + let animationId; + + // Game state + let gold = 200; + let lives = 20; + let wave = 0; + let score = 0; + let enemiesKilled = 0; + + // Grid settings + const GRID_SIZE = 40; + const COLS = 20; + const ROWS = 15; + + // Game objects + let towers = []; + let enemies = []; + let projectiles = []; + let particles = []; + + // Selected tower type for placement + let selectedTowerType = null; + let hoveredCell = { x: -1, y: -1 }; + let demolishMode = false; + + // Path (Simplified network path) + const path = [ + { x: 0, y: 7 }, + { x: 5, y: 7 }, + { x: 5, y: 3 }, + { x: 10, y: 3 }, + { x: 10, y: 10 }, + { x: 15, y: 10 }, + { x: 15, y: 5 }, + { x: 20, y: 5 } + ]; + + // Security Tool types (towers) + const towerTypes = { + basic: { + name: 'Firewall', + cost: 50, + damage: 12, + range: 120, + fireRate: 1000, + color: '#1565C0', + projectileColor: '#64B5F6', + description: 'Blocks incoming threats' + }, + sniper: { + name: 'Antivirus', + cost: 100, + damage: 45, + range: 200, + fireRate: 2000, + color: '#2E7D32', + projectileColor: '#81C784', + description: 'Detects and removes viruses' + }, + cannon: { + name: 'Quarantine', + cost: 150, + damage: 30, + range: 100, + fireRate: 1500, + color: '#6A1B9A', + projectileColor: '#BA68C8', + splash: true, + splashRadius: 40, + description: 'Isolates multiple threats' + }, + rapid: { + name: 'IDS', + cost: 75, + damage: 6, + range: 100, + fireRate: 300, + color: '#FF8F00', + projectileColor: '#FFD54F', + description: 'Intrusion Detection System' + } + }; + + // Virus types (Enemies) + const enemyTypes = { + basic: { + health: 50, + speed: 1, + reward: 10, + color: '#E53935', + name: 'Worm', + description: 'Simple network worm' + }, + fast: { + health: 30, + speed: 2, + reward: 15, + color: '#8E24AA', + name: 'Trojan', + description: 'Fast trojan' + }, + tank: { + health: 150, + speed: 0.5, + reward: 30, + color: '#3949AB', + name: 'Rootkit', + description: 'Persistent rootkit' + }, + boss: { + health: 500, + speed: 0.3, + reward: 100, + color: '#FF7043', + name: 'Ransomware', + description: 'Dangerous ransomware' + } + }; + + class Tower { + constructor(gridX, gridY, type) { + this.gridX = gridX; + this.gridY = gridY; + this.x = gridX * GRID_SIZE + GRID_SIZE / 2; + this.y = gridY * GRID_SIZE + GRID_SIZE / 2; + this.type = type; + this.lastFireTime = 0; + this.target = null; + this.level = 1; + this.maxLevel = 5; + } + + getUpgradeCost() { + return Math.floor(towerTypes[this.type].cost * 0.5 * this.level); + } + + canUpgrade() { + return this.level < this.maxLevel; + } + + getRefundValue() { + // Refund 70% of total invested (base cost + all upgrades) + let totalCost = towerTypes[this.type].cost; + for (let i = 1; i < this.level; i++) { + totalCost += Math.floor(towerTypes[this.type].cost * 0.5 * i); + } + return Math.floor(totalCost * 0.7); + } + + update(timestamp) { + // Find target + if (!this.target || this.target.dead || !this.isInRange(this.target)) { + this.findTarget(); + } + + // Fire at target + if (this.target && timestamp - this.lastFireTime >= towerTypes[this.type].fireRate) { + this.fire(); + this.lastFireTime = timestamp; + } + } + + findTarget() { + this.target = null; + let closestDistance = Infinity; + + for (let enemy of enemies) { + if (enemy.dead) continue; + + const distance = this.getDistance(enemy); + if (distance <= towerTypes[this.type].range && distance < closestDistance) { + this.target = enemy; + closestDistance = distance; + } + } + } + + isInRange(enemy) { + return this.getDistance(enemy) <= towerTypes[this.type].range; + } + + getDistance(enemy) { + const dx = this.x - enemy.x; + const dy = this.y - enemy.y; + return Math.sqrt(dx * dx + dy * dy); + } + + fire() { + if (!this.target) return; + + projectiles.push(new Projectile( + this.x, + this.y, + this.target, + towerTypes[this.type].damage * this.level, + towerTypes[this.type].projectileColor, + towerTypes[this.type].splash, + towerTypes[this.type].splashRadius + )); + } + + draw() { + // Draw range (when hovered) + if (hoveredCell.x === this.gridX && hoveredCell.y === this.gridY) { + ctx.beginPath(); + ctx.arc(this.x, this.y, towerTypes[this.type].range, 0, Math.PI * 2); + ctx.fillStyle = 'rgba(255, 255, 255, 0.06)'; + ctx.fill(); + ctx.strokeStyle = 'rgba(255, 255, 255, 0.2)'; + ctx.lineWidth = 2; + ctx.stroke(); + + // Draw info when hovered + ctx.fillStyle = 'rgba(0, 0, 0, 0.85)'; + ctx.fillRect(this.x - 60, this.y - 75, 120, 55); + + ctx.fillStyle = '#FFF'; + ctx.font = 'bold 13px Arial'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + + if (demolishMode) { + // Show demolish info + const refund = this.getRefundValue(); + ctx.fillText(`${towerTypes[this.type].name}`, this.x, this.y - 55); + ctx.fillStyle = '#FF5252'; + ctx.font = 'bold 11px Arial'; + ctx.fillText(`Uninstall`, this.x, this.y - 40); + ctx.fillStyle = '#FFD700'; + ctx.fillText(`Refund: ${refund}G`, this.x, this.y - 27); + } else { + // Show upgrade info + const upgradeCost = this.getUpgradeCost(); + const canUpgrade = this.canUpgrade(); + + ctx.fillText(`${towerTypes[this.type].name}`, this.x, this.y - 58); + ctx.font = '10px Arial'; + ctx.fillStyle = '#AAA'; + ctx.fillText(`Level ${this.level}/${this.maxLevel}`, this.x, this.y - 44); + + if (canUpgrade) { + ctx.font = 'bold 11px Arial'; + ctx.fillStyle = gold >= upgradeCost ? '#4CAF50' : '#F44336'; + ctx.fillText(`Upgrade: ${upgradeCost}G`, this.x, this.y - 28); + } else { + ctx.font = 'bold 11px Arial'; + ctx.fillStyle = '#FFD700'; + ctx.fillText(`MAX LEVEL`, this.x, this.y - 28); + } + } + } + + // Draw tower + ctx.fillStyle = towerTypes[this.type].color; + ctx.fillRect( + this.gridX * GRID_SIZE + 5, + this.gridY * GRID_SIZE + 5, + GRID_SIZE - 10, + GRID_SIZE - 10 + ); + + // Draw red overlay in demolish mode when hovered + if (demolishMode && hoveredCell.x === this.gridX && hoveredCell.y === this.gridY) { + ctx.fillStyle = 'rgba(244, 67, 54, 0.5)'; + ctx.fillRect( + this.gridX * GRID_SIZE + 5, + this.gridY * GRID_SIZE + 5, + GRID_SIZE - 10, + GRID_SIZE - 10 + ); + } + + // Draw level indicator + ctx.fillStyle = '#FFF'; + ctx.font = 'bold 14px Arial'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText(this.level, this.x, this.y); + + // Draw max level indicator (star) + if (this.level >= this.maxLevel) { + ctx.fillStyle = '#FFD700'; + ctx.font = '16px Arial'; + ctx.fillText('β˜…', this.x, this.y - 15); + } + + // Draw barrel pointing at target + if (this.target) { + const angle = Math.atan2(this.target.y - this.y, this.target.x - this.x); + ctx.save(); + ctx.translate(this.x, this.y); + ctx.rotate(angle); + ctx.fillStyle = '#333'; + ctx.fillRect(0, -3, 15, 6); + ctx.restore(); + } + } + } + + class Enemy { + constructor(type, waveMultiplier = 1) { + this.type = type; + this.maxHealth = Math.round(enemyTypes[type].health * waveMultiplier); + this.health = this.maxHealth; + // Speed increases slightly with waves (max 50% faster at wave 20) + const speedMultiplier = 1 + Math.min(wave * 0.025, 0.5); + this.speed = enemyTypes[type].speed * speedMultiplier; + this.reward = Math.round(enemyTypes[type].reward * waveMultiplier); + this.pathIndex = 0; + this.progress = 0; + this.x = path[0].x * GRID_SIZE; + this.y = path[0].y * GRID_SIZE; + this.dead = false; + this.reachedEnd = false; + } + + update(deltaTime) { + if (this.dead || this.reachedEnd) return; + + const currentPoint = path[this.pathIndex]; + const nextPoint = path[this.pathIndex + 1]; + + if (!nextPoint) { + this.reachedEnd = true; + lives--; + updateStats(); + return; + } + + const dx = nextPoint.x * GRID_SIZE - currentPoint.x * GRID_SIZE; + const dy = nextPoint.y * GRID_SIZE - currentPoint.y * GRID_SIZE; + const distance = Math.sqrt(dx * dx + dy * dy); + + this.progress += this.speed * deltaTime / 16.67; + + if (this.progress >= distance) { + this.progress = 0; + this.pathIndex++; + if (this.pathIndex >= path.length - 1) { + this.reachedEnd = true; + lives--; + updateStats(); + } + } else { + this.x = currentPoint.x * GRID_SIZE + (dx / distance) * this.progress; + this.y = currentPoint.y * GRID_SIZE + (dy / distance) * this.progress; + } + } + + takeDamage(damage) { + this.health -= damage; + if (this.health <= 0) { + this.dead = true; + gold += this.reward; + score += this.reward; + enemiesKilled++; + + // Spawn particles + for (let i = 0; i < 10; i++) { + particles.push(new Particle(this.x, this.y, enemyTypes[this.type].color)); + } + + updateStats(); + } + } + + draw() { + if (this.dead || this.reachedEnd) return; + + // Draw virus (mit Spikes fΓΌr gefΓ€hrlicheres Aussehen) + ctx.fillStyle = enemyTypes[this.type].color; + ctx.beginPath(); + ctx.arc(this.x, this.y, 12, 0, Math.PI * 2); + ctx.fill(); + + // Draw spikes (fΓΌr Virus-Look) + ctx.fillStyle = enemyTypes[this.type].color; + for (let i = 0; i < 8; i++) { + const angle = (i / 8) * Math.PI * 2; + ctx.beginPath(); + ctx.moveTo(this.x + Math.cos(angle) * 10, this.y + Math.sin(angle) * 10); + ctx.lineTo(this.x + Math.cos(angle) * 16, this.y + Math.sin(angle) * 16); + ctx.lineWidth = 2; + ctx.strokeStyle = enemyTypes[this.type].color; + ctx.stroke(); + } + + // Draw health bar + const barWidth = 24; + const barHeight = 4; + ctx.fillStyle = '#1a1a1a'; + ctx.fillRect(this.x - barWidth / 2, this.y - 22, barWidth, barHeight); + ctx.fillStyle = this.health / this.maxHealth > 0.3 ? '#4CAF50' : '#F44336'; + ctx.fillRect(this.x - barWidth / 2, this.y - 22, barWidth * (this.health / this.maxHealth), barHeight); + + // Draw virus type label + ctx.fillStyle = '#FFF'; + ctx.font = 'bold 9px Arial'; + ctx.textAlign = 'center'; + ctx.shadowColor = '#000'; + ctx.shadowBlur = 3; + ctx.fillText(enemyTypes[this.type].name, this.x, this.y + 24); + ctx.shadowBlur = 0; + } + } + + class Projectile { + constructor(x, y, target, damage, color, splash = false, splashRadius = 0) { + this.x = x; + this.y = y; + this.target = target; + this.damage = damage; + this.color = color; + this.speed = 5; + this.dead = false; + this.splash = splash; + this.splashRadius = splashRadius; + } + + update() { + if (this.dead || this.target.dead || this.target.reachedEnd) { + this.dead = true; + return; + } + + const dx = this.target.x - this.x; + const dy = this.target.y - this.y; + const distance = Math.sqrt(dx * dx + dy * dy); + + if (distance < this.speed) { + if (this.splash) { + // Splash damage + for (let enemy of enemies) { + if (enemy.dead || enemy.reachedEnd) continue; + const edx = enemy.x - this.target.x; + const edy = enemy.y - this.target.y; + const enemyDistance = Math.sqrt(edx * edx + edy * edy); + if (enemyDistance <= this.splashRadius) { + enemy.takeDamage(this.damage); + } + } + } else { + this.target.takeDamage(this.damage); + } + this.dead = true; + } else { + this.x += (dx / distance) * this.speed; + this.y += (dy / distance) * this.speed; + } + } + + draw() { + if (this.dead) return; + + ctx.fillStyle = this.color; + ctx.beginPath(); + ctx.arc(this.x, this.y, 4, 0, Math.PI * 2); + ctx.fill(); + } + } + + class Particle { + constructor(x, y, color) { + this.x = x; + this.y = y; + this.color = color; + this.vx = (Math.random() - 0.5) * 4; + this.vy = (Math.random() - 0.5) * 4; + this.life = 30; + this.dead = false; + } + + update() { + this.x += this.vx; + this.y += this.vy; + this.life--; + if (this.life <= 0) { + this.dead = true; + } + } + + draw() { + if (this.dead) return; + + ctx.globalAlpha = this.life / 30; + ctx.fillStyle = this.color; + ctx.fillRect(this.x - 2, this.y - 2, 4, 4); + ctx.globalAlpha = 1; + } + } + + function drawGrid() { + // Draw path + ctx.strokeStyle = '#444'; + ctx.lineWidth = GRID_SIZE; + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + ctx.beginPath(); + ctx.moveTo(path[0].x * GRID_SIZE, path[0].y * GRID_SIZE); + for (let i = 1; i < path.length; i++) { + ctx.lineTo(path[i].x * GRID_SIZE, path[i].y * GRID_SIZE); + } + ctx.stroke(); + + // Draw grid lines + ctx.strokeStyle = '#222'; + ctx.lineWidth = 1; + for (let i = 0; i <= COLS; i++) { + ctx.beginPath(); + ctx.moveTo(i * GRID_SIZE, 0); + ctx.lineTo(i * GRID_SIZE, ROWS * GRID_SIZE); + ctx.stroke(); + } + for (let i = 0; i <= ROWS; i++) { + ctx.beginPath(); + ctx.moveTo(0, i * GRID_SIZE); + ctx.lineTo(COLS * GRID_SIZE, i * GRID_SIZE); + ctx.stroke(); + } + + // Highlight hovered cell + if (hoveredCell.x >= 0 && hoveredCell.y >= 0 && selectedTowerType) { + const canPlace = canPlaceTower(hoveredCell.x, hoveredCell.y); + ctx.fillStyle = canPlace ? 'rgba(76, 175, 80, 0.3)' : 'rgba(244, 67, 54, 0.3)'; + ctx.fillRect(hoveredCell.x * GRID_SIZE, hoveredCell.y * GRID_SIZE, GRID_SIZE, GRID_SIZE); + + // Draw range preview + if (canPlace) { + ctx.beginPath(); + ctx.arc( + hoveredCell.x * GRID_SIZE + GRID_SIZE / 2, + hoveredCell.y * GRID_SIZE + GRID_SIZE / 2, + towerTypes[selectedTowerType].range, + 0, + Math.PI * 2 + ); + ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)'; + ctx.lineWidth = 2; + ctx.stroke(); + } + } + } + + function canPlaceTower(gridX, gridY) { + // Check if on path + for (let i = 0; i < path.length - 1; i++) { + const p1 = path[i]; + const p2 = path[i + 1]; + + if (p1.x === p2.x) { // Vertical segment + const minY = Math.min(p1.y, p2.y); + const maxY = Math.max(p1.y, p2.y); + if (gridX === p1.x && gridY >= minY && gridY <= maxY) { + return false; + } + } else { // Horizontal segment + const minX = Math.min(p1.x, p2.x); + const maxX = Math.max(p1.x, p2.x); + if (gridY === p1.y && gridX >= minX && gridX <= maxX) { + return false; + } + } + } + + // Check if tower already exists + for (let tower of towers) { + if (tower.gridX === gridX && tower.gridY === gridY) { + return false; + } + } + + return true; + } + + function spawnWave() { + wave++; + // Exponential difficulty increase: health and damage scale faster + const waveMultiplier = 1 + (wave - 1) * 0.3 + Math.pow(wave - 1, 1.2) * 0.05; + + // More enemies per wave (exponential growth) + let enemyCount = Math.floor(5 + wave * 2.5 + Math.pow(wave, 1.3) * 0.5); + + // Faster spawning at higher waves + let baseSpawnDelay = Math.max(300, 800 - wave * 15); + let spawnDelay = 0; + + for (let i = 0; i < enemyCount; i++) { + setTimeout(() => { + if (!gameRunning) return; + + let enemyType; + // Difficulty-based enemy distribution + const rand = Math.random(); + + if (wave >= 15) { + // Wave 15+: Lots of bosses and tanks + if (rand < 0.25) { + enemyType = 'boss'; + } else if (rand < 0.55) { + enemyType = 'tank'; + } else if (rand < 0.80) { + enemyType = 'fast'; + } else { + enemyType = 'basic'; + } + } else if (wave >= 10) { + // Wave 10-14: More bosses appear + if (rand < 0.15) { + enemyType = 'boss'; + } else if (rand < 0.45) { + enemyType = 'tank'; + } else if (rand < 0.70) { + enemyType = 'fast'; + } else { + enemyType = 'basic'; + } + } else if (wave >= 5) { + // Wave 5-9: Tanks and fast enemies + if (rand < 0.05) { + enemyType = 'boss'; + } else if (rand < 0.30) { + enemyType = 'tank'; + } else if (rand < 0.60) { + enemyType = 'fast'; + } else { + enemyType = 'basic'; + } + } else if (wave >= 3) { + // Wave 3-4: Some fast enemies + if (rand < 0.40) { + enemyType = 'fast'; + } else { + enemyType = 'basic'; + } + } else { + // Wave 1-2: Mostly basic + if (rand < 0.20) { + enemyType = 'fast'; + } else { + enemyType = 'basic'; + } + } + + enemies.push(new Enemy(enemyType, waveMultiplier)); + }, spawnDelay); + + spawnDelay += baseSpawnDelay; + } + + updateStats(); + } + + function updateStats() { + // Round values to prevent floating-point precision errors + gold = Math.round(gold); + score = Math.round(score); + + updateElement('td-gold', gold); + updateElement('td-lives', lives); + updateElement('td-wave', wave); + updateElement('td-score', score); + updateElement('td-kills', enemiesKilled); + + // Update mobile stats too + updateElement('td-gold-mobile', gold); + updateElement('td-lives-mobile', lives); + updateElement('td-wave-mobile', wave); + updateElement('td-score-mobile', score); + updateElement('td-kills-mobile', enemiesKilled); + + if (lives <= 0) { + gameOver(); + } + } + + function updateElement(id, value) { + const element = document.getElementById(id); + if (element) { + element.textContent = value; + } + } + + function gameOver() { + gameRunning = false; + cancelAnimationFrame(animationId); + + ctx.fillStyle = 'rgba(0, 0, 0, 0.9)'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + // Draw red warning effect + ctx.fillStyle = 'rgba(211, 47, 47, 0.3)'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + ctx.fillStyle = '#FFF'; + ctx.font = 'bold 48px Arial'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.shadowColor = '#F44336'; + ctx.shadowBlur = 15; + ctx.fillText('SYSTEM COMPROMISED', canvas.width / 2, canvas.height / 2 - 40); + ctx.shadowBlur = 0; + + ctx.font = '20px Arial'; + ctx.fillStyle = '#FF5252'; + ctx.fillText('Your network has been infected!', canvas.width / 2, canvas.height / 2 + 10); + + ctx.fillStyle = '#FFF'; + ctx.font = '24px Arial'; + ctx.fillText(`Final Score: ${score}`, canvas.width / 2, canvas.height / 2 + 50); + ctx.fillText(`Wave: ${wave}`, canvas.width / 2, canvas.height / 2 + 85); + ctx.fillText(`Viruses Removed: ${enemiesKilled}`, canvas.width / 2, canvas.height / 2 + 120); + } + + let lastTimestamp = 0; + function gameLoop(timestamp) { + if (!gameRunning) return; + + if (gamePaused) { + animationId = requestAnimationFrame(gameLoop); + return; + } + + const deltaTime = timestamp - lastTimestamp; + lastTimestamp = timestamp; + + // Clear canvas + ctx.fillStyle = '#1a1a1a'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + // Draw grid + drawGrid(); + + // Update and draw towers + for (let tower of towers) { + tower.update(timestamp); + tower.draw(); + } + + // Update and draw enemies + enemies = enemies.filter(enemy => !enemy.dead && !enemy.reachedEnd); + for (let enemy of enemies) { + enemy.update(deltaTime); + enemy.draw(); + } + + // Update and draw projectiles + projectiles = projectiles.filter(projectile => !projectile.dead); + for (let projectile of projectiles) { + projectile.update(); + projectile.draw(); + } + + // Update and draw particles + particles = particles.filter(particle => !particle.dead); + for (let particle of particles) { + particle.update(); + particle.draw(); + } + + // Check if wave is complete + if (enemies.length === 0 && gameRunning) { + setTimeout(() => { + if (gameRunning && enemies.length === 0) { + spawnWave(); + } + }, 3000); + } + + animationId = requestAnimationFrame(gameLoop); + } + + function handleCanvasClick(event) { + if (!gameRunning || gamePaused || !event) return; + + try { + const rect = canvas.getBoundingClientRect(); + const x = event.clientX - rect.left; + const y = event.clientY - rect.top; + + // Validate coordinates are within canvas bounds + if (x < 0 || x > canvas.width || y < 0 || y > canvas.height) { + return; + } + + const gridX = Math.floor(x / GRID_SIZE); + const gridY = Math.floor(y / GRID_SIZE); + + // Validate grid coordinates + if (gridX < 0 || gridX >= COLS || gridY < 0 || gridY >= ROWS) { + return; + } + + if (demolishMode) { + // Check if clicking on tower to demolish + for (let i = 0; i < towers.length; i++) { + const tower = towers[i]; + if (tower.gridX === gridX && tower.gridY === gridY) { + // Get refund + const refund = tower.getRefundValue(); + gold = Math.round(gold + refund); + + // Show demolish feedback + for (let j = 0; j < 20; j++) { + particles.push(new Particle(tower.x, tower.y, '#F44336')); + } + + // Remove tower + towers.splice(i, 1); + updateStats(); + break; + } + } + } else if (selectedTowerType && canPlaceTower(gridX, gridY)) { + // Validate tower type exists + if (!towerTypes.hasOwnProperty(selectedTowerType)) { + console.error('Invalid tower type:', selectedTowerType); + return; + } + + // Try to place tower + const cost = towerTypes[selectedTowerType].cost; + if (gold >= cost && cost > 0) { + towers.push(new Tower(gridX, gridY, selectedTowerType)); + gold = Math.round(gold - cost); + updateStats(); + } + } else { + // Check if clicking on existing tower to upgrade + for (let tower of towers) { + if (tower.gridX === gridX && tower.gridY === gridY) { + if (tower.canUpgrade()) { + const upgradeCost = tower.getUpgradeCost(); + if (gold >= upgradeCost && upgradeCost > 0) { + tower.level++; + gold = Math.round(gold - upgradeCost); + updateStats(); + + // Show upgrade feedback + for (let i = 0; i < 15; i++) { + particles.push(new Particle(tower.x, tower.y, '#FFD700')); + } + } + } + break; + } + } + } + } catch (error) { + console.error('Tower Defense: Error handling click:', error); + } + } + + function handleCanvasMove(event) { + if (!gameRunning || !event) return; + + try { + const rect = canvas.getBoundingClientRect(); + const x = event.clientX - rect.left; + const y = event.clientY - rect.top; + + // Validate coordinates + if (x < 0 || x > canvas.width || y < 0 || y > canvas.height) { + hoveredCell.x = -1; + hoveredCell.y = -1; + return; + } + + const gridX = Math.floor(x / GRID_SIZE); + const gridY = Math.floor(y / GRID_SIZE); + + // Validate grid coordinates + if (gridX >= 0 && gridX < COLS && gridY >= 0 && gridY < ROWS) { + hoveredCell.x = gridX; + hoveredCell.y = gridY; + } else { + hoveredCell.x = -1; + hoveredCell.y = -1; + } + } catch (error) { + console.error('Tower Defense: Error handling mouse move:', error); + } + } + + // Public API + window.towerdefense = { + startGame: function() { + try { + // Stop any running game first + if (gameRunning) { + gameRunning = false; + gamePaused = false; + if (animationId) { + cancelAnimationFrame(animationId); + } + } + + canvas = document.getElementById('towerdefenseCanvas'); + if (!canvas || !canvas.getContext) { + console.error('Tower Defense: Canvas not found or not supported'); + return; + } + + ctx = canvas.getContext('2d'); + if (!ctx) { + console.error('Tower Defense: Could not get 2D context'); + return; + } + + // Remove old event listeners before adding new ones + canvas.removeEventListener('click', handleCanvasClick); + canvas.removeEventListener('mousemove', handleCanvasMove); + + // Reset game state to safe default values (validated) + gold = Math.max(0, 200); + lives = Math.max(0, 20); + wave = 0; + score = 0; + enemiesKilled = 0; + towers = []; + enemies = []; + projectiles = []; + particles = []; + selectedTowerType = null; + hoveredCell = { x: -1, y: -1 }; + demolishMode = false; + + gameRunning = true; + gamePaused = false; + lastTimestamp = 0; + + updateStats(); + + // Add event listeners + canvas.addEventListener('click', handleCanvasClick); + canvas.addEventListener('mousemove', handleCanvasMove); + + // Start first wave after 2 seconds + setTimeout(() => { + if (gameRunning) { + spawnWave(); + } + }, 2000); + + animationId = requestAnimationFrame(gameLoop); + } catch (error) { + console.error('Tower Defense: Error starting game:', error); + gameRunning = false; + } + }, + + + togglePause: function() { + try { + if (!gameRunning || !canvas || !ctx) return; + + const wasPaused = gamePaused; + gamePaused = !gamePaused; + + if (gamePaused) { + // Draw pause overlay + ctx.fillStyle = 'rgba(0, 0, 0, 0.7)'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + ctx.fillStyle = '#FFF'; + ctx.font = 'bold 48px Arial'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.shadowColor = '#000'; + ctx.shadowBlur = 10; + ctx.fillText('SYSTEM PAUSED', canvas.width / 2, canvas.height / 2); + ctx.shadowBlur = 0; + ctx.font = '20px Arial'; + ctx.fillStyle = '#AAA'; + ctx.fillText('Press Pause to continue', canvas.width / 2, canvas.height / 2 + 40); + } else if (wasPaused) { + // Resume - reset timestamp to prevent large deltaTime jump + lastTimestamp = performance.now(); + } + } catch (error) { + console.error('Tower Defense: Error toggling pause:', error); + } + }, + + selectTower: function(type) { + try { + // Validate tower type to prevent injection + if (typeof type !== 'string') { + console.error('Tower Defense: Invalid tower type (not a string)'); + return; + } + + // Whitelist validation - only allow valid tower types + if (!towerTypes.hasOwnProperty(type)) { + console.error('Tower Defense: Invalid tower type:', type); + return; + } + + selectedTowerType = type; + demolishMode = false; + } catch (error) { + console.error('Tower Defense: Error selecting tower:', error); + } + }, + + toggleDemolish: function() { + try { + demolishMode = !demolishMode; + if (demolishMode) { + selectedTowerType = null; + } + return demolishMode; + } catch (error) { + console.error('Tower Defense: Error toggling demolish:', error); + return false; + } + } + }; +})(); + diff --git a/NetLock-RMM-Web-Console/wwwroot/media/images/NetLock-Logo-Dark-Transparent.png b/NetLock-RMM-Web-Console/wwwroot/media/images/NetLock-Logo-Dark-Transparent.png new file mode 100644 index 00000000..1ad5ef16 Binary files /dev/null and b/NetLock-RMM-Web-Console/wwwroot/media/images/NetLock-Logo-Dark-Transparent.png differ