From e981b7be841d2947a71a6346cec459b7adc06e88 Mon Sep 17 00:00:00 2001 From: Tim Lee Date: Tue, 12 Dec 2023 07:58:47 -0700 Subject: [PATCH 1/5] hotfix: fix return codes on enterprise tests (#15578) --- server/service/integration_enterprise_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/service/integration_enterprise_test.go b/server/service/integration_enterprise_test.go index 5d15d97125..9ee2883fb5 100644 --- a/server/service/integration_enterprise_test.go +++ b/server/service/integration_enterprise_test.go @@ -4356,7 +4356,7 @@ func (s *integrationEnterpriseTestSuite) TestRunHostScript() { // create a valid sync script execution request, fails because the // request will time-out waiting for a result. runSyncResp = runScriptSyncResponse{} - s.DoJSON("POST", "/api/latest/fleet/scripts/run/sync", fleet.HostScriptRequestPayload{HostID: host.ID, ScriptContents: "echo"}, http.StatusGatewayTimeout, &runSyncResp) + s.DoJSON("POST", "/api/latest/fleet/scripts/run/sync", fleet.HostScriptRequestPayload{HostID: host.ID, ScriptContents: "echo"}, http.StatusRequestTimeout, &runSyncResp) require.Equal(t, host.ID, runSyncResp.HostID) require.NotEmpty(t, runSyncResp.ExecutionID) require.True(t, runSyncResp.HostTimeout) @@ -4574,7 +4574,7 @@ func (s *integrationEnterpriseTestSuite) TestRunHostSavedScript() { // create a valid sync script execution request, fails because the // request will time-out waiting for a result. var runSyncResp runScriptSyncResponse - s.DoJSON("POST", "/api/latest/fleet/scripts/run/sync", fleet.HostScriptRequestPayload{HostID: host.ID, ScriptID: &savedNoTmScript.ID}, http.StatusGatewayTimeout, &runSyncResp) + s.DoJSON("POST", "/api/latest/fleet/scripts/run/sync", fleet.HostScriptRequestPayload{HostID: host.ID, ScriptID: &savedNoTmScript.ID}, http.StatusRequestTimeout, &runSyncResp) require.Equal(t, host.ID, runSyncResp.HostID) require.NotEmpty(t, runSyncResp.ExecutionID) require.NotNil(t, runSyncResp.ScriptID) From 8c548afe3188007aa7485c04e4cd8b508b98c935 Mon Sep 17 00:00:00 2001 From: Luke Heath Date: Tue, 12 Dec 2023 12:04:23 -0600 Subject: [PATCH 2/5] Add Windows scripts and set scripts table width --- .../hosts/details/cards/Scripts/_styles.scss | 12 +++ .../mdm/windows/windows-change-password.ps1 | 52 +++++++++++++ .../windows/windows-disable-administrator.ps1 | 8 ++ .../windows/windows-enable-administrator.ps1 | 29 +++++++ scripts/mdm/windows/windows-lock.ps1 | 35 +++++++++ scripts/mdm/windows/windows-unlock.ps1 | 14 ++++ scripts/mdm/windows/windows-wipe.ps1 | 76 +++++++++++++++++++ 7 files changed, 226 insertions(+) create mode 100644 scripts/mdm/windows/windows-change-password.ps1 create mode 100644 scripts/mdm/windows/windows-disable-administrator.ps1 create mode 100644 scripts/mdm/windows/windows-enable-administrator.ps1 create mode 100644 scripts/mdm/windows/windows-lock.ps1 create mode 100644 scripts/mdm/windows/windows-unlock.ps1 create mode 100644 scripts/mdm/windows/windows-wipe.ps1 diff --git a/frontend/pages/hosts/details/cards/Scripts/_styles.scss b/frontend/pages/hosts/details/cards/Scripts/_styles.scss index 0c4f9ccfc3..0bdc86c03c 100644 --- a/frontend/pages/hosts/details/cards/Scripts/_styles.scss +++ b/frontend/pages/hosts/details/cards/Scripts/_styles.scss @@ -9,6 +9,18 @@ line-height: 1.5; } + .table-container { + .name__header { + width: 50%; + } + .last_execution__header { + width: 25%; + } + .actions__header { + width: 25%; + } + } + .table-container__header-left { display: block; } diff --git a/scripts/mdm/windows/windows-change-password.ps1 b/scripts/mdm/windows/windows-change-password.ps1 new file mode 100644 index 0000000000..43cca1128e --- /dev/null +++ b/scripts/mdm/windows/windows-change-password.ps1 @@ -0,0 +1,52 @@ +# PowerShell script to log off all users and change their passwords + +# Function to generate a random password +function Generate-Password { + param ( + [int]$length = 12 + ) + $chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_-+=<>?/" + $password = -join ((1..$length) | ForEach-Object { Get-Random -Maximum $chars.length } | ForEach-Object { $chars[$_]} ) + return $password +} + +# Log off all non-administrative users +$loggedOffUsers = @{} +Get-WmiObject -Class Win32_UserProfile | Where-Object { $_.Special -eq $false } | ForEach-Object { + $username = $_.LocalPath.Split('\')[-1] + if ($username -ne "Administrator" -and $username -ne $env:USERNAME -and -not $loggedOffUsers.ContainsKey($username)) { + try { + $userSessions = query user | Where-Object { $_ -match "\b$username\b" } + foreach ($session in $userSessions) { + if ($session -match "\s+(\d+)\s+Disc\s+") { + # Disconnected sessions can't be logged off + continue + } + elseif ($session -match "\s+(\d+)\s+") { + $sessionID = $matches[1] + logoff $sessionID + $loggedOffUsers[$username] = $true + Write-Host "Logged out user: $username" + } + } + } catch { + Write-Host "Could not log off user: $username. Error: $($_.Exception.Message)" + } + } +} + +# Get all local user accounts except built-in accounts like 'Administrator' +$users = Get-LocalUser | Where-Object { $_.Name -notlike "Administrator" -and $_.PrincipalSource -eq "Local" } + +# Change password for each user and output the new password +foreach ($user in $users) { + $newPassword = Generate-Password -length 12 + $securePassword = ConvertTo-SecureString $newPassword -AsPlainText -Force + + try { + Set-LocalUser -Name $user.Name -Password $securePassword + Write-Host "Password for user $($user.Name) changed successfully. New Password: $newPassword" + } catch { + Write-Host "Failed to change password for user $($user.Name)" + } +} diff --git a/scripts/mdm/windows/windows-disable-administrator.ps1 b/scripts/mdm/windows/windows-disable-administrator.ps1 new file mode 100644 index 0000000000..de66080e7b --- /dev/null +++ b/scripts/mdm/windows/windows-disable-administrator.ps1 @@ -0,0 +1,8 @@ +# PowerShell script to disable the Administrator account + +# Run this script as an administrator + +# Disable the Administrator account +Disable-LocalUser -Name "Administrator" + +Write-Host "Administrator account has been disabled." diff --git a/scripts/mdm/windows/windows-enable-administrator.ps1 b/scripts/mdm/windows/windows-enable-administrator.ps1 new file mode 100644 index 0000000000..13c48b04fe --- /dev/null +++ b/scripts/mdm/windows/windows-enable-administrator.ps1 @@ -0,0 +1,29 @@ +# PowerShell script to enable the Administrator account and set a random, secure password + +# Run this script as an administrator + +# Function to generate a random password +function Generate-Password { + param ( + [int]$length = 12 + ) + $chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_-+=<>?/" + $password = -join ((1..$length) | ForEach-Object { Get-Random -Maximum $chars.length } | ForEach-Object { $chars[$_]} ) + return $password +} + +# Generate a random password +$password = Generate-Password -length 12 + +# Convert the password to a SecureString +$securePassword = ConvertTo-SecureString $password -AsPlainText -Force + +# Enable the Administrator account +Enable-LocalUser -Name "Administrator" + +# Set the generated password for the Administrator account +Set-LocalUser -Name "Administrator" -Password $securePassword + +# Output the password +Write-Host "Administrator account has been enabled." +Write-Host "Generated Password: $password" diff --git a/scripts/mdm/windows/windows-lock.ps1 b/scripts/mdm/windows/windows-lock.ps1 new file mode 100644 index 0000000000..e4d9809fee --- /dev/null +++ b/scripts/mdm/windows/windows-lock.ps1 @@ -0,0 +1,35 @@ +# PowerShell script to log off all non-administrative users and disable their accounts + +# Log off all non-administrative users +$loggedOffUsers = @{} +Get-WmiObject -Class Win32_UserProfile | Where-Object { $_.Special -eq $false } | ForEach-Object { + $username = $_.LocalPath.Split('\')[-1] + if ($username -ne "Administrator" -and $username -ne $env:USERNAME -and -not $loggedOffUsers.ContainsKey($username)) { + try { + $userSessions = query user | Where-Object { $_ -match "\b$username\b" } + foreach ($session in $userSessions) { + if ($session -match "\s+(\d+)\s+Disc\s+") { + # Disconnected sessions can't be logged off + continue + } + elseif ($session -match "\s+(\d+)\s+") { + $sessionID = $matches[1] + logoff $sessionID + $loggedOffUsers[$username] = $true + Write-Host "Logged out user: $username" + } + } + } catch { + Write-Host "Could not log off user: $username. Error: $($_.Exception.Message)" + } + } +} + +# Disable all non-administrative local user accounts +Get-LocalUser | Where-Object { $_.Enabled -eq $true -and $_.Name -ne "Administrator" } | ForEach-Object { + $username = $_.Name + Disable-LocalUser -Name $username + Write-Host "Disabled account for $username" +} + +Write-Host "All non-administrative users have been logged out and their accounts disabled." diff --git a/scripts/mdm/windows/windows-unlock.ps1 b/scripts/mdm/windows/windows-unlock.ps1 new file mode 100644 index 0000000000..6a10c00fb3 --- /dev/null +++ b/scripts/mdm/windows/windows-unlock.ps1 @@ -0,0 +1,14 @@ +# PowerShell script to enable all disabled local user accounts + +# Get all local user accounts +$localUsers = Get-LocalUser + +# Enable each disabled user account +foreach ($user in $localUsers) { + if ($user.Enabled -eq $false) { + Enable-LocalUser -Name $user.Name + Write-Host "Enabled user account: $($user.Name)" + } +} + +Write-Host "All disabled user accounts have been enabled." diff --git a/scripts/mdm/windows/windows-wipe.ps1 b/scripts/mdm/windows/windows-wipe.ps1 new file mode 100644 index 0000000000..aa27fc52bb --- /dev/null +++ b/scripts/mdm/windows/windows-wipe.ps1 @@ -0,0 +1,76 @@ +# PowerShell script to wipe user data and then make the Windows system inoperable + +# Function to delete user data +function Wipe-UserData { + $userFolders = Get-ChildItem C:\Users -Directory + + foreach ($folder in $userFolders) { + if ($folder.Name -notlike "Public" -and $folder.Name -notlike "Default*" -and $folder.Name -notlike "Administrator") { + $path = $folder.FullName + Write-Host "Wiping user data in $path" + Remove-Item -Path $path -Recurse -Force + } + } +} + +# Function to delete critical system files and directories +function Wipe-SystemFiles { + $criticalPaths = @( + "C:\Program Files", + "C:\Program Files (x86)", + "C:\Windows\System32", + "C:\Windows\SysWOW64" + # Add other critical paths as necessary + ) + + foreach ($path in $criticalPaths) { + if (Test-Path $path) { + try { + Takeown /f $path /r /d y + Icacls $path /grant administrators:F /t + Remove-Item -Path $path -Recurse -Force + Write-Host "Wiped $path" + } catch { + Write-Host "Failed to wipe $path" + } + } + } +} + +# Log off all non-administrative users +$loggedOffUsers = @{} +Get-WmiObject -Class Win32_UserProfile | Where-Object { $_.Special -eq $false } | ForEach-Object { + $username = $_.LocalPath.Split('\')[-1] + if ($username -ne "Administrator" -and $username -ne $env:USERNAME -and -not $loggedOffUsers.ContainsKey($username)) { + try { + $userSessions = query user | Where-Object { $_ -match "\b$username\b" } + foreach ($session in $userSessions) { + if ($session -match "\s+(\d+)\s+Disc\s+") { + # Disconnected sessions can't be logged off + continue + } + elseif ($session -match "\s+(\d+)\s+") { + $sessionID = $matches[1] + logoff $sessionID + $loggedOffUsers[$username] = $true + Write-Host "Logged out user: $username" + } + } + } catch { + Write-Host "Could not log off user: $username. Error: $($_.Exception.Message)" + } + } +} + +# Disable all non-administrative local user accounts +Get-LocalUser | Where-Object { $_.Enabled -eq $true -and $_.Name -ne "Administrator" } | ForEach-Object { + $username = $_.Name + Disable-LocalUser -Name $username + Write-Host "Disabled account for $username" +} + +# Start the wiping process +Wipe-UserData +Wipe-SystemFiles + +Write-Host "Wiping process completed." From 47e8e57d5ac5fa122b3446d26417296568bd9778 Mon Sep 17 00:00:00 2001 From: Luke Heath Date: Tue, 12 Dec 2023 12:20:41 -0600 Subject: [PATCH 3/5] Revert "Add Windows scripts and set scripts table width" (#15590) --- .../hosts/details/cards/Scripts/_styles.scss | 12 --- .../mdm/windows/windows-change-password.ps1 | 52 ------------- .../windows/windows-disable-administrator.ps1 | 8 -- .../windows/windows-enable-administrator.ps1 | 29 ------- scripts/mdm/windows/windows-lock.ps1 | 35 --------- scripts/mdm/windows/windows-unlock.ps1 | 14 ---- scripts/mdm/windows/windows-wipe.ps1 | 76 ------------------- 7 files changed, 226 deletions(-) delete mode 100644 scripts/mdm/windows/windows-change-password.ps1 delete mode 100644 scripts/mdm/windows/windows-disable-administrator.ps1 delete mode 100644 scripts/mdm/windows/windows-enable-administrator.ps1 delete mode 100644 scripts/mdm/windows/windows-lock.ps1 delete mode 100644 scripts/mdm/windows/windows-unlock.ps1 delete mode 100644 scripts/mdm/windows/windows-wipe.ps1 diff --git a/frontend/pages/hosts/details/cards/Scripts/_styles.scss b/frontend/pages/hosts/details/cards/Scripts/_styles.scss index 0bdc86c03c..0c4f9ccfc3 100644 --- a/frontend/pages/hosts/details/cards/Scripts/_styles.scss +++ b/frontend/pages/hosts/details/cards/Scripts/_styles.scss @@ -9,18 +9,6 @@ line-height: 1.5; } - .table-container { - .name__header { - width: 50%; - } - .last_execution__header { - width: 25%; - } - .actions__header { - width: 25%; - } - } - .table-container__header-left { display: block; } diff --git a/scripts/mdm/windows/windows-change-password.ps1 b/scripts/mdm/windows/windows-change-password.ps1 deleted file mode 100644 index 43cca1128e..0000000000 --- a/scripts/mdm/windows/windows-change-password.ps1 +++ /dev/null @@ -1,52 +0,0 @@ -# PowerShell script to log off all users and change their passwords - -# Function to generate a random password -function Generate-Password { - param ( - [int]$length = 12 - ) - $chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_-+=<>?/" - $password = -join ((1..$length) | ForEach-Object { Get-Random -Maximum $chars.length } | ForEach-Object { $chars[$_]} ) - return $password -} - -# Log off all non-administrative users -$loggedOffUsers = @{} -Get-WmiObject -Class Win32_UserProfile | Where-Object { $_.Special -eq $false } | ForEach-Object { - $username = $_.LocalPath.Split('\')[-1] - if ($username -ne "Administrator" -and $username -ne $env:USERNAME -and -not $loggedOffUsers.ContainsKey($username)) { - try { - $userSessions = query user | Where-Object { $_ -match "\b$username\b" } - foreach ($session in $userSessions) { - if ($session -match "\s+(\d+)\s+Disc\s+") { - # Disconnected sessions can't be logged off - continue - } - elseif ($session -match "\s+(\d+)\s+") { - $sessionID = $matches[1] - logoff $sessionID - $loggedOffUsers[$username] = $true - Write-Host "Logged out user: $username" - } - } - } catch { - Write-Host "Could not log off user: $username. Error: $($_.Exception.Message)" - } - } -} - -# Get all local user accounts except built-in accounts like 'Administrator' -$users = Get-LocalUser | Where-Object { $_.Name -notlike "Administrator" -and $_.PrincipalSource -eq "Local" } - -# Change password for each user and output the new password -foreach ($user in $users) { - $newPassword = Generate-Password -length 12 - $securePassword = ConvertTo-SecureString $newPassword -AsPlainText -Force - - try { - Set-LocalUser -Name $user.Name -Password $securePassword - Write-Host "Password for user $($user.Name) changed successfully. New Password: $newPassword" - } catch { - Write-Host "Failed to change password for user $($user.Name)" - } -} diff --git a/scripts/mdm/windows/windows-disable-administrator.ps1 b/scripts/mdm/windows/windows-disable-administrator.ps1 deleted file mode 100644 index de66080e7b..0000000000 --- a/scripts/mdm/windows/windows-disable-administrator.ps1 +++ /dev/null @@ -1,8 +0,0 @@ -# PowerShell script to disable the Administrator account - -# Run this script as an administrator - -# Disable the Administrator account -Disable-LocalUser -Name "Administrator" - -Write-Host "Administrator account has been disabled." diff --git a/scripts/mdm/windows/windows-enable-administrator.ps1 b/scripts/mdm/windows/windows-enable-administrator.ps1 deleted file mode 100644 index 13c48b04fe..0000000000 --- a/scripts/mdm/windows/windows-enable-administrator.ps1 +++ /dev/null @@ -1,29 +0,0 @@ -# PowerShell script to enable the Administrator account and set a random, secure password - -# Run this script as an administrator - -# Function to generate a random password -function Generate-Password { - param ( - [int]$length = 12 - ) - $chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_-+=<>?/" - $password = -join ((1..$length) | ForEach-Object { Get-Random -Maximum $chars.length } | ForEach-Object { $chars[$_]} ) - return $password -} - -# Generate a random password -$password = Generate-Password -length 12 - -# Convert the password to a SecureString -$securePassword = ConvertTo-SecureString $password -AsPlainText -Force - -# Enable the Administrator account -Enable-LocalUser -Name "Administrator" - -# Set the generated password for the Administrator account -Set-LocalUser -Name "Administrator" -Password $securePassword - -# Output the password -Write-Host "Administrator account has been enabled." -Write-Host "Generated Password: $password" diff --git a/scripts/mdm/windows/windows-lock.ps1 b/scripts/mdm/windows/windows-lock.ps1 deleted file mode 100644 index e4d9809fee..0000000000 --- a/scripts/mdm/windows/windows-lock.ps1 +++ /dev/null @@ -1,35 +0,0 @@ -# PowerShell script to log off all non-administrative users and disable their accounts - -# Log off all non-administrative users -$loggedOffUsers = @{} -Get-WmiObject -Class Win32_UserProfile | Where-Object { $_.Special -eq $false } | ForEach-Object { - $username = $_.LocalPath.Split('\')[-1] - if ($username -ne "Administrator" -and $username -ne $env:USERNAME -and -not $loggedOffUsers.ContainsKey($username)) { - try { - $userSessions = query user | Where-Object { $_ -match "\b$username\b" } - foreach ($session in $userSessions) { - if ($session -match "\s+(\d+)\s+Disc\s+") { - # Disconnected sessions can't be logged off - continue - } - elseif ($session -match "\s+(\d+)\s+") { - $sessionID = $matches[1] - logoff $sessionID - $loggedOffUsers[$username] = $true - Write-Host "Logged out user: $username" - } - } - } catch { - Write-Host "Could not log off user: $username. Error: $($_.Exception.Message)" - } - } -} - -# Disable all non-administrative local user accounts -Get-LocalUser | Where-Object { $_.Enabled -eq $true -and $_.Name -ne "Administrator" } | ForEach-Object { - $username = $_.Name - Disable-LocalUser -Name $username - Write-Host "Disabled account for $username" -} - -Write-Host "All non-administrative users have been logged out and their accounts disabled." diff --git a/scripts/mdm/windows/windows-unlock.ps1 b/scripts/mdm/windows/windows-unlock.ps1 deleted file mode 100644 index 6a10c00fb3..0000000000 --- a/scripts/mdm/windows/windows-unlock.ps1 +++ /dev/null @@ -1,14 +0,0 @@ -# PowerShell script to enable all disabled local user accounts - -# Get all local user accounts -$localUsers = Get-LocalUser - -# Enable each disabled user account -foreach ($user in $localUsers) { - if ($user.Enabled -eq $false) { - Enable-LocalUser -Name $user.Name - Write-Host "Enabled user account: $($user.Name)" - } -} - -Write-Host "All disabled user accounts have been enabled." diff --git a/scripts/mdm/windows/windows-wipe.ps1 b/scripts/mdm/windows/windows-wipe.ps1 deleted file mode 100644 index aa27fc52bb..0000000000 --- a/scripts/mdm/windows/windows-wipe.ps1 +++ /dev/null @@ -1,76 +0,0 @@ -# PowerShell script to wipe user data and then make the Windows system inoperable - -# Function to delete user data -function Wipe-UserData { - $userFolders = Get-ChildItem C:\Users -Directory - - foreach ($folder in $userFolders) { - if ($folder.Name -notlike "Public" -and $folder.Name -notlike "Default*" -and $folder.Name -notlike "Administrator") { - $path = $folder.FullName - Write-Host "Wiping user data in $path" - Remove-Item -Path $path -Recurse -Force - } - } -} - -# Function to delete critical system files and directories -function Wipe-SystemFiles { - $criticalPaths = @( - "C:\Program Files", - "C:\Program Files (x86)", - "C:\Windows\System32", - "C:\Windows\SysWOW64" - # Add other critical paths as necessary - ) - - foreach ($path in $criticalPaths) { - if (Test-Path $path) { - try { - Takeown /f $path /r /d y - Icacls $path /grant administrators:F /t - Remove-Item -Path $path -Recurse -Force - Write-Host "Wiped $path" - } catch { - Write-Host "Failed to wipe $path" - } - } - } -} - -# Log off all non-administrative users -$loggedOffUsers = @{} -Get-WmiObject -Class Win32_UserProfile | Where-Object { $_.Special -eq $false } | ForEach-Object { - $username = $_.LocalPath.Split('\')[-1] - if ($username -ne "Administrator" -and $username -ne $env:USERNAME -and -not $loggedOffUsers.ContainsKey($username)) { - try { - $userSessions = query user | Where-Object { $_ -match "\b$username\b" } - foreach ($session in $userSessions) { - if ($session -match "\s+(\d+)\s+Disc\s+") { - # Disconnected sessions can't be logged off - continue - } - elseif ($session -match "\s+(\d+)\s+") { - $sessionID = $matches[1] - logoff $sessionID - $loggedOffUsers[$username] = $true - Write-Host "Logged out user: $username" - } - } - } catch { - Write-Host "Could not log off user: $username. Error: $($_.Exception.Message)" - } - } -} - -# Disable all non-administrative local user accounts -Get-LocalUser | Where-Object { $_.Enabled -eq $true -and $_.Name -ne "Administrator" } | ForEach-Object { - $username = $_.Name - Disable-LocalUser -Name $username - Write-Host "Disabled account for $username" -} - -# Start the wiping process -Wipe-UserData -Wipe-SystemFiles - -Write-Host "Wiping process completed." From 2ed30268198d8016f9293b2be24f3ceeae6551ca Mon Sep 17 00:00:00 2001 From: Roberto Dip Date: Tue, 12 Dec 2023 15:24:20 -0300 Subject: [PATCH 4/5] Add pagination meta to software versions endpoint (#15550) --- cmd/fleetctl/get_test.go | 4 +-- ee/server/service/software.go | 2 +- server/datastore/mysql/mysql.go | 8 +++++- server/datastore/mysql/software.go | 22 +++++++++++++-- server/datastore/mysql/software_test.go | 36 +++++++++++++++++++------ server/fleet/datastore.go | 2 +- server/fleet/service.go | 2 +- server/mock/datastore_mock.go | 4 +-- server/service/integration_core_test.go | 8 ++++++ server/service/software.go | 30 ++++++++++++--------- server/service/software_test.go | 16 +++++------ 11 files changed, 96 insertions(+), 38 deletions(-) diff --git a/cmd/fleetctl/get_test.go b/cmd/fleetctl/get_test.go index 5f300cbcb4..ac7eadd9f8 100644 --- a/cmd/fleetctl/get_test.go +++ b/cmd/fleetctl/get_test.go @@ -774,9 +774,9 @@ func TestGetSoftwareVersions(t *testing.T) { var gotTeamID *uint - ds.ListSoftwareFunc = func(ctx context.Context, opt fleet.SoftwareListOptions) ([]fleet.Software, error) { + ds.ListSoftwareFunc = func(ctx context.Context, opt fleet.SoftwareListOptions) ([]fleet.Software, *fleet.PaginationMetadata, error) { gotTeamID = opt.TeamID - return []fleet.Software{foo001, foo002, foo003, bar003}, nil + return []fleet.Software{foo001, foo002, foo003, bar003}, &fleet.PaginationMetadata{}, nil } ds.CountSoftwareFunc = func(ctx context.Context, opt fleet.SoftwareListOptions) (int, error) { diff --git a/ee/server/service/software.go b/ee/server/service/software.go index 469fbfd27d..0e63e02bce 100644 --- a/ee/server/service/software.go +++ b/ee/server/service/software.go @@ -6,7 +6,7 @@ import ( "github.com/fleetdm/fleet/v4/server/fleet" ) -func (svc *Service) ListSoftware(ctx context.Context, opts fleet.SoftwareListOptions) ([]fleet.Software, error) { +func (svc *Service) ListSoftware(ctx context.Context, opts fleet.SoftwareListOptions) ([]fleet.Software, *fleet.PaginationMetadata, error) { // reuse ListSoftware, but include cve scores in premium version opts.IncludeCVEScores = true return svc.Service.ListSoftware(ctx, opts) diff --git a/server/datastore/mysql/mysql.go b/server/datastore/mysql/mysql.go index 3769bee35e..956c6bcea6 100644 --- a/server/datastore/mysql/mysql.go +++ b/server/datastore/mysql/mysql.go @@ -787,12 +787,18 @@ func appendLimitOffsetToSelect(ds *goqu.SelectDataset, opts fleet.ListOptions) * if perPage == 0 { perPage = defaultSelectLimit } - ds = ds.Limit(perPage) offset := perPage * opts.Page if offset > 0 { ds = ds.Offset(offset) } + + if opts.IncludeMetadata { + perPage++ + } + + ds = ds.Limit(perPage) + return ds } diff --git a/server/datastore/mysql/software.go b/server/datastore/mysql/software.go index 0e7cea5af0..f9a99427c4 100644 --- a/server/datastore/mysql/software.go +++ b/server/datastore/mysql/software.go @@ -1036,8 +1036,26 @@ func (ds *Datastore) ListSoftwareCPEs(ctx context.Context) ([]fleet.SoftwareCPE, return result, nil } -func (ds *Datastore) ListSoftware(ctx context.Context, opt fleet.SoftwareListOptions) ([]fleet.Software, error) { - return listSoftwareDB(ctx, ds.reader(ctx), opt) +func (ds *Datastore) ListSoftware(ctx context.Context, opt fleet.SoftwareListOptions) ([]fleet.Software, *fleet.PaginationMetadata, error) { + software, err := listSoftwareDB(ctx, ds.reader(ctx), opt) + if err != nil { + return nil, nil, err + } + + perPage := opt.ListOptions.PerPage + var metaData *fleet.PaginationMetadata + if opt.ListOptions.IncludeMetadata { + if perPage <= 0 { + perPage = defaultSelectLimit + } + metaData = &fleet.PaginationMetadata{HasPreviousResults: opt.ListOptions.Page > 0} + if len(software) > int(perPage) { + metaData.HasNextResults = true + software = software[:len(software)-1] + } + } + + return software, metaData, nil } func (ds *Datastore) CountSoftware(ctx context.Context, opt fleet.SoftwareListOptions) (int, error) { diff --git a/server/datastore/mysql/software_test.go b/server/datastore/mysql/software_test.go index 4c5e3f2c31..80256c70b5 100644 --- a/server/datastore/mysql/software_test.go +++ b/server/datastore/mysql/software_test.go @@ -657,9 +657,10 @@ func testSoftwareList(t *testing.T, ds *Datastore) { t.Run("paginates", func(t *testing.T) { opts := fleet.SoftwareListOptions{ ListOptions: fleet.ListOptions{ - Page: 1, - PerPage: 1, - OrderKey: "version", + Page: 1, + PerPage: 1, + OrderKey: "version", + IncludeMetadata: true, }, IncludeCVEScores: true, } @@ -704,9 +705,10 @@ func testSoftwareList(t *testing.T, ds *Datastore) { opts := fleet.SoftwareListOptions{ ListOptions: fleet.ListOptions{ - PerPage: 1, - Page: 1, - OrderKey: "id", + PerPage: 1, + Page: 1, + OrderKey: "id", + IncludeMetadata: true, }, TeamID: &team1.ID, } @@ -882,12 +884,30 @@ func testSoftwareList(t *testing.T, ds *Datastore) { } func listSoftwareCheckCount(t *testing.T, ds *Datastore, expectedListCount int, expectedFullCount int, opts fleet.SoftwareListOptions, returnSorted bool) []fleet.Software { - software, err := ds.ListSoftware(context.Background(), opts) + software, meta, err := ds.ListSoftware(context.Background(), opts) require.NoError(t, err) require.Len(t, software, expectedListCount) count, err := ds.CountSoftware(context.Background(), opts) require.NoError(t, err) require.Equal(t, expectedFullCount, count) + + if opts.ListOptions.IncludeMetadata { + require.NotNil(t, meta) + if expectedListCount == expectedFullCount { + require.False(t, meta.HasPreviousResults) + require.True(t, meta.HasNextResults) + } + if expectedFullCount > expectedListCount { + shouldHavePrevious := opts.ListOptions.Page > 0 + require.Equal(t, shouldHavePrevious, meta.HasPreviousResults) + + shouldHaveNext := uint(expectedFullCount) > (opts.ListOptions.Page+1)*opts.ListOptions.PerPage // page is 0-indexed + require.Equal(t, shouldHaveNext, meta.HasNextResults) + } + } else { + require.Nil(t, meta) + } + for _, s := range software { sort.Slice(s.Vulnerabilities, func(i, j int) bool { return s.Vulnerabilities[i].CVE < s.Vulnerabilities[j].CVE @@ -1372,7 +1392,7 @@ func testHostVulnSummariesBySoftwareIDs(t *testing.T, ds *Datastore) { insertVulnSoftwareForTest(t, ds) - allSoftware, err := ds.ListSoftware(ctx, fleet.SoftwareListOptions{}) + allSoftware, _, err := ds.ListSoftware(ctx, fleet.SoftwareListOptions{}) require.NoError(t, err) var fooRpm fleet.Software diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go index 27241d83bf..cccaf822e4 100644 --- a/server/fleet/datastore.go +++ b/server/fleet/datastore.go @@ -558,7 +558,7 @@ type Datastore interface { // MigrationStatus returns nil if migrations are complete, and an error if migrations need to be run. MigrationStatus(ctx context.Context) (*MigrationStatus, error) - ListSoftware(ctx context.Context, opt SoftwareListOptions) ([]Software, error) + ListSoftware(ctx context.Context, opt SoftwareListOptions) ([]Software, *PaginationMetadata, error) CountSoftware(ctx context.Context, opt SoftwareListOptions) (int, error) // DeleteVulnerabilities deletes the given list of vulnerabilities identified by CPE+CVE. DeleteSoftwareVulnerabilities(ctx context.Context, vulnerabilities []SoftwareVulnerability) error diff --git a/server/fleet/service.go b/server/fleet/service.go index 816a96d2e1..8c07b63155 100644 --- a/server/fleet/service.go +++ b/server/fleet/service.go @@ -564,7 +564,7 @@ type Service interface { // ///////////////////////////////////////////////////////////////////////////// // Software - ListSoftware(ctx context.Context, opt SoftwareListOptions) ([]Software, error) + ListSoftware(ctx context.Context, opt SoftwareListOptions) ([]Software, *PaginationMetadata, error) SoftwareByID(ctx context.Context, id uint, includeCVEScores bool) (*Software, error) CountSoftware(ctx context.Context, opt SoftwareListOptions) (int, error) diff --git a/server/mock/datastore_mock.go b/server/mock/datastore_mock.go index adc53e31d0..041184b842 100644 --- a/server/mock/datastore_mock.go +++ b/server/mock/datastore_mock.go @@ -414,7 +414,7 @@ type MigrateDataFunc func(ctx context.Context) error type MigrationStatusFunc func(ctx context.Context) (*fleet.MigrationStatus, error) -type ListSoftwareFunc func(ctx context.Context, opt fleet.SoftwareListOptions) ([]fleet.Software, error) +type ListSoftwareFunc func(ctx context.Context, opt fleet.SoftwareListOptions) ([]fleet.Software, *fleet.PaginationMetadata, error) type CountSoftwareFunc func(ctx context.Context, opt fleet.SoftwareListOptions) (int, error) @@ -3293,7 +3293,7 @@ func (s *DataStore) MigrationStatus(ctx context.Context) (*fleet.MigrationStatus return s.MigrationStatusFunc(ctx) } -func (s *DataStore) ListSoftware(ctx context.Context, opt fleet.SoftwareListOptions) ([]fleet.Software, error) { +func (s *DataStore) ListSoftware(ctx context.Context, opt fleet.SoftwareListOptions) ([]fleet.Software, *fleet.PaginationMetadata, error) { s.mu.Lock() s.ListSoftwareFuncInvoked = true s.mu.Unlock() diff --git a/server/service/integration_core_test.go b/server/service/integration_core_test.go index 6f4f57f353..f76d730a9d 100644 --- a/server/service/integration_core_test.go +++ b/server/service/integration_core_test.go @@ -5757,6 +5757,8 @@ func (s *integrationTestSuite) TestListSoftwareAndSoftwareDetails() { versResp = listSoftwareVersionsResponse{} s.DoJSON("GET", "/api/latest/fleet/software/versions", nil, http.StatusOK, &versResp, "per_page", "5", "page", "0", "order_key", "hosts_count", "order_direction", "desc") assertVersionsResp(versResp, []fleet.Software{sws[19], sws[18], sws[17], sws[16], sws[15]}, hostsCountTs, len(sws), 20, 19, 18, 17, 16) + require.False(t, versResp.Meta.HasPreviousResults) + require.True(t, versResp.Meta.HasNextResults) // second page (page=1) lsResp = listSoftwareResponse{} @@ -5765,6 +5767,8 @@ func (s *integrationTestSuite) TestListSoftwareAndSoftwareDetails() { versResp = listSoftwareVersionsResponse{} s.DoJSON("GET", "/api/latest/fleet/software/versions", nil, http.StatusOK, &versResp, "per_page", "5", "page", "1", "order_key", "hosts_count", "order_direction", "desc") assertVersionsResp(versResp, []fleet.Software{sws[14], sws[13], sws[12], sws[11], sws[10]}, hostsCountTs, len(sws), 15, 14, 13, 12, 11) + require.True(t, versResp.Meta.HasPreviousResults) + require.True(t, versResp.Meta.HasNextResults) // third page (page=2) lsResp = listSoftwareResponse{} @@ -5773,6 +5777,8 @@ func (s *integrationTestSuite) TestListSoftwareAndSoftwareDetails() { versResp = listSoftwareVersionsResponse{} s.DoJSON("GET", "/api/latest/fleet/software/versions", nil, http.StatusOK, &versResp, "per_page", "5", "page", "2", "order_key", "hosts_count", "order_direction", "desc") assertVersionsResp(versResp, []fleet.Software{sws[9], sws[8], sws[7], sws[6], sws[5]}, hostsCountTs, len(sws), 10, 9, 8, 7, 6) + require.True(t, versResp.Meta.HasPreviousResults) + require.True(t, versResp.Meta.HasNextResults) // last page (page=3) lsResp = listSoftwareResponse{} @@ -5781,6 +5787,8 @@ func (s *integrationTestSuite) TestListSoftwareAndSoftwareDetails() { versResp = listSoftwareVersionsResponse{} s.DoJSON("GET", "/api/latest/fleet/software/versions", nil, http.StatusOK, &versResp, "per_page", "5", "page", "3", "order_key", "hosts_count", "order_direction", "desc") assertVersionsResp(versResp, []fleet.Software{sws[4], sws[3], sws[2], sws[1], sws[0]}, hostsCountTs, len(sws), 5, 4, 3, 2, 1) + require.True(t, versResp.Meta.HasPreviousResults) + require.False(t, versResp.Meta.HasNextResults) // past the end lsResp = listSoftwareResponse{} diff --git a/server/service/software.go b/server/service/software.go index 06148dd222..46e1fa51e3 100644 --- a/server/service/software.go +++ b/server/service/software.go @@ -29,7 +29,7 @@ func (r listSoftwareResponse) error() error { return r.Err } // DEPRECATED: use listSoftwareVersionsEndpoint instead func listSoftwareEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { req := request.(*listSoftwareRequest) - resp, err := svc.ListSoftware(ctx, req.SoftwareListOptions) + resp, _, err := svc.ListSoftware(ctx, req.SoftwareListOptions) if err != nil { return listSoftwareResponse{Err: err}, nil } @@ -50,17 +50,23 @@ func listSoftwareEndpoint(ctx context.Context, request interface{}, svc fleet.Se } type listSoftwareVersionsResponse struct { - Count int `json:"count"` - CountsUpdatedAt *time.Time `json:"counts_updated_at"` - Software []fleet.Software `json:"software,omitempty"` - Err error `json:"error,omitempty"` + Count int `json:"count"` + CountsUpdatedAt *time.Time `json:"counts_updated_at"` + Software []fleet.Software `json:"software,omitempty"` + Meta *fleet.PaginationMetadata `json:"meta"` + Err error `json:"error,omitempty"` } func (r listSoftwareVersionsResponse) error() error { return r.Err } func listSoftwareVersionsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { req := request.(*listSoftwareRequest) - resp, err := svc.ListSoftware(ctx, req.SoftwareListOptions) + + // always include pagination for new software versions endpoint (not included by default in + // legacy endpoint for backwards compatibility) + req.SoftwareListOptions.ListOptions.IncludeMetadata = true + + resp, meta, err := svc.ListSoftware(ctx, req.SoftwareListOptions) if err != nil { return listSoftwareVersionsResponse{Err: err}, nil } @@ -72,7 +78,7 @@ func listSoftwareVersionsEndpoint(ctx context.Context, request interface{}, svc latest = sw.CountsUpdatedAt } } - listResp := listSoftwareVersionsResponse{Software: resp} + listResp := listSoftwareVersionsResponse{Software: resp, Meta: meta} if !latest.IsZero() { listResp.CountsUpdatedAt = &latest } @@ -86,11 +92,11 @@ func listSoftwareVersionsEndpoint(ctx context.Context, request interface{}, svc return listResp, nil } -func (svc *Service) ListSoftware(ctx context.Context, opt fleet.SoftwareListOptions) ([]fleet.Software, error) { +func (svc *Service) ListSoftware(ctx context.Context, opt fleet.SoftwareListOptions) ([]fleet.Software, *fleet.PaginationMetadata, error) { if err := svc.authz.Authorize(ctx, &fleet.AuthzSoftwareInventory{ TeamID: opt.TeamID, }, fleet.ActionRead); err != nil { - return nil, err + return nil, nil, err } // default sort order to hosts_count descending @@ -100,12 +106,12 @@ func (svc *Service) ListSoftware(ctx context.Context, opt fleet.SoftwareListOpti } opt.WithHostCounts = true - softwares, err := svc.ds.ListSoftware(ctx, opt) + softwares, meta, err := svc.ds.ListSoftware(ctx, opt) if err != nil { - return nil, err + return nil, nil, err } - return softwares, nil + return softwares, meta, nil } ///////////////////////////////////////////////////////////////////////////////// diff --git a/server/service/software_test.go b/server/service/software_test.go index 49478ffdda..6695267670 100644 --- a/server/service/software_test.go +++ b/server/service/software_test.go @@ -17,10 +17,10 @@ func TestService_ListSoftware(t *testing.T) { var calledWithTeamID *uint var calledWithOpt fleet.SoftwareListOptions - ds.ListSoftwareFunc = func(ctx context.Context, opt fleet.SoftwareListOptions) ([]fleet.Software, error) { + ds.ListSoftwareFunc = func(ctx context.Context, opt fleet.SoftwareListOptions) ([]fleet.Software, *fleet.PaginationMetadata, error) { calledWithTeamID = opt.TeamID calledWithOpt = opt - return []fleet.Software{}, nil + return []fleet.Software{}, &fleet.PaginationMetadata{}, nil } user := &fleet.User{ @@ -32,7 +32,7 @@ func TestService_ListSoftware(t *testing.T) { svc, ctx := newTestService(t, ds, nil, nil) ctx = viewer.NewContext(ctx, viewer.Viewer{User: user}) - _, err := svc.ListSoftware(ctx, fleet.SoftwareListOptions{TeamID: ptr.Uint(42), ListOptions: fleet.ListOptions{PerPage: 77, Page: 4}}) + _, _, err := svc.ListSoftware(ctx, fleet.SoftwareListOptions{TeamID: ptr.Uint(42), ListOptions: fleet.ListOptions{PerPage: 77, Page: 4}}) require.NoError(t, err) assert.True(t, ds.ListSoftwareFuncInvoked) @@ -43,7 +43,7 @@ func TestService_ListSoftware(t *testing.T) { // call again, this time with an explicit sort ds.ListSoftwareFuncInvoked = false - _, err = svc.ListSoftware(ctx, fleet.SoftwareListOptions{TeamID: nil, ListOptions: fleet.ListOptions{PerPage: 11, Page: 2, OrderKey: "id", OrderDirection: fleet.OrderAscending}}) + _, _, err = svc.ListSoftware(ctx, fleet.SoftwareListOptions{TeamID: nil, ListOptions: fleet.ListOptions{PerPage: 11, Page: 2, OrderKey: "id", OrderDirection: fleet.OrderAscending}}) require.NoError(t, err) assert.True(t, ds.ListSoftwareFuncInvoked) @@ -55,8 +55,8 @@ func TestService_ListSoftware(t *testing.T) { func TestServiceSoftwareInventoryAuth(t *testing.T) { ds := new(mock.Store) - ds.ListSoftwareFunc = func(ctx context.Context, opt fleet.SoftwareListOptions) ([]fleet.Software, error) { - return []fleet.Software{}, nil + ds.ListSoftwareFunc = func(ctx context.Context, opt fleet.SoftwareListOptions) ([]fleet.Software, *fleet.PaginationMetadata, error) { + return []fleet.Software{}, &fleet.PaginationMetadata{}, nil } ds.CountSoftwareFunc = func(ctx context.Context, opt fleet.SoftwareListOptions) (int, error) { return 0, nil @@ -173,7 +173,7 @@ func TestServiceSoftwareInventoryAuth(t *testing.T) { ctx := viewer.NewContext(ctx, viewer.Viewer{User: tc.user}) // List all software. - _, err := svc.ListSoftware(ctx, fleet.SoftwareListOptions{}) + _, _, err := svc.ListSoftware(ctx, fleet.SoftwareListOptions{}) checkAuthErr(t, tc.shouldFailGlobalRead, err) // Count all software. @@ -181,7 +181,7 @@ func TestServiceSoftwareInventoryAuth(t *testing.T) { checkAuthErr(t, tc.shouldFailGlobalRead, err) // List software for a team. - _, err = svc.ListSoftware(ctx, fleet.SoftwareListOptions{ + _, _, err = svc.ListSoftware(ctx, fleet.SoftwareListOptions{ TeamID: ptr.Uint(1), }) checkAuthErr(t, tc.shouldFailTeamRead, err) From e1eb0172499ca0045bd285d33379d590f7509d07 Mon Sep 17 00:00:00 2001 From: Jahziel Villasana-Espinoza Date: Tue, 12 Dec 2023 13:36:33 -0500 Subject: [PATCH 5/5] fix: send back queries but ignore them on the FE (#15507) > #15009 # Checklist for submitter If some of the following don't apply, delete the relevant line. - [x] Changes file added for user-visible changes in `changes/` or `orbit/changes/`. See [Changes files](https://fleetdm.com/docs/contributing/committing-changes#changes-files) for more information. - [x] Added/updated tests - [x] Manual QA for all new/changed functionality --- changes/15009-queries-observer | 1 + .../pages/queries/ManageQueriesPage/ManageQueriesPage.tsx | 7 +++++++ server/service/queries.go | 5 ++++- server/service/queries_test.go | 4 ++-- 4 files changed, 14 insertions(+), 3 deletions(-) create mode 100644 changes/15009-queries-observer diff --git a/changes/15009-queries-observer b/changes/15009-queries-observer new file mode 100644 index 0000000000..d92ecc41c5 --- /dev/null +++ b/changes/15009-queries-observer @@ -0,0 +1 @@ +- Fixes bug where Global Observers were not able to list all queries through the API. \ No newline at end of file diff --git a/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx b/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx index c7318ab266..80f6caabb5 100644 --- a/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx +++ b/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx @@ -90,6 +90,7 @@ const ManageQueriesPage = ({ filteredQueriesPath, isPremiumTier, isSandboxMode, + isGlobalObserver, config, } = useContext(AppContext); const { setLastEditedQueryBody, setSelectedQueryTargetsByType } = useContext( @@ -137,6 +138,12 @@ const ManageQueriesPage = ({ [{ scope: "queries", teamId: teamIdForApi }], ({ queryKey: [{ teamId }] }) => queriesAPI.loadAll(teamId).then(({ queries }) => { + if (isGlobalObserver) { + return queries + .filter((q: ISchedulableQuery) => q.observer_can_run) + .map(enhanceQuery); + } + return queries.map(enhanceQuery); }), { diff --git a/server/service/queries.go b/server/service/queries.go index 47429b7923..094a8519ce 100644 --- a/server/service/queries.go +++ b/server/service/queries.go @@ -114,7 +114,10 @@ func (svc *Service) ListQueries(ctx context.Context, opt fleet.ListOptions, team func onlyShowObserverCanRunQueries(user *fleet.User, teamID *uint) bool { if user.GlobalRole != nil && *user.GlobalRole == fleet.RoleObserver { - return true + // Return false here because Global Observers should be able to access all queries via API. + // However, the UI will only show queries that have "observer can run" set to true. + // See the user permissions matrix: https://fleetdm.com/docs/using-fleet/manage-access#user-permissions + return false } return teamID != nil && user.TeamMembership(func(ut fleet.UserTeam) bool { diff --git a/server/service/queries_test.go b/server/service/queries_test.go index cd929ad153..5d1815d703 100644 --- a/server/service/queries_test.go +++ b/server/service/queries_test.go @@ -14,7 +14,7 @@ import ( func TestFilterQueriesForObserver(t *testing.T) { t.Run("global role", func(t *testing.T) { - require.True(t, onlyShowObserverCanRunQueries(&fleet.User{ + require.False(t, onlyShowObserverCanRunQueries(&fleet.User{ GlobalRole: ptr.String(fleet.RoleObserver), }, nil)) @@ -89,7 +89,7 @@ func TestListQueries(t *testing.T) { { title: "global observer", user: &fleet.User{GlobalRole: ptr.String(fleet.RoleObserver)}, - expectedOpts: fleet.ListQueryOptions{OnlyObserverCanRun: true}, + expectedOpts: fleet.ListQueryOptions{OnlyObserverCanRun: false}, }, { title: "team maintainer",