From 13a5f3c20524389594f3954e7982da7a4d76a543 Mon Sep 17 00:00:00 2001
From: Sarah Gillespie <73313222+gillespi314@users.noreply.github.com>
Date: Tue, 12 Dec 2023 13:12:50 -0600
Subject: [PATCH 1/5] Fix button text wrapping in UI for Settings >
Integrations > MDM. (#15594)
---
changes/14641-ui-mdm-button-wrap | 1 +
.../components/MacOSMdmCard/_styles.scss | 12 +++++++++++-
.../components/WindowsMdmCard/_styles.scss | 13 ++++++++++++-
3 files changed, 24 insertions(+), 2 deletions(-)
create mode 100644 changes/14641-ui-mdm-button-wrap
diff --git a/changes/14641-ui-mdm-button-wrap b/changes/14641-ui-mdm-button-wrap
new file mode 100644
index 0000000000..4830405da3
--- /dev/null
+++ b/changes/14641-ui-mdm-button-wrap
@@ -0,0 +1 @@
+- Fixed button text wrapping in UI for Settings > Integrations > MDM.
diff --git a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/MacOSMdmCard/_styles.scss b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/MacOSMdmCard/_styles.scss
index 302a4beb18..53ee9f0ab9 100644
--- a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/MacOSMdmCard/_styles.scss
+++ b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/MacOSMdmCard/_styles.scss
@@ -10,6 +10,16 @@
display: flex;
justify-content: space-between;
align-items: center;
+
+ p {
+ margin-right: $pad-small;
+ }
+
+ button {
+ .children-wrapper {
+ text-wrap: nowrap;
+ }
+ }
}
&__turn-on-mac-os {
@@ -25,7 +35,7 @@
}
&__turn-off-mac-os {
- >div {
+ > div {
display: flex;
align-items: center;
}
diff --git a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/WindowsMdmCard/_styles.scss b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/WindowsMdmCard/_styles.scss
index f157bb1178..79bc9b91b4 100644
--- a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/WindowsMdmCard/_styles.scss
+++ b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/WindowsMdmCard/_styles.scss
@@ -5,10 +5,21 @@
margin: 0;
}
- &__turn-on-windows, &__turn-off-windows {
+ &__turn-on-windows,
+ &__turn-off-windows {
display: flex;
justify-content: space-between;
align-items: center;
+
+ p {
+ margin-right: $pad-small;
+ }
+
+ button {
+ .children-wrapper {
+ text-wrap: nowrap;
+ }
+ }
}
&__turn-on-windows {
From 65f5404565d60a51da4dca11b9816f0776dba48b Mon Sep 17 00:00:00 2001
From: Noah Talerman <47070608+noahtalerman@users.noreply.github.com>
Date: Tue, 12 Dec 2023 14:31:20 -0500
Subject: [PATCH 2/5] Fix link on Windows automatic enrollment page (#15580)
For this bug: #15566
- Add redirect so that we can change the link later w/o breaking it
---
.../WindowsAutomaticEnrollmentPage.tsx | 2 +-
website/config/routes.js | 1 +
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/frontend/pages/admin/IntegrationsPage/cards/AutomaticEnrollment/WindowsAutomaticEnrollmentPage/WindowsAutomaticEnrollmentPage.tsx b/frontend/pages/admin/IntegrationsPage/cards/AutomaticEnrollment/WindowsAutomaticEnrollmentPage/WindowsAutomaticEnrollmentPage.tsx
index 80743fe5ba..ec8e50b7b1 100644
--- a/frontend/pages/admin/IntegrationsPage/cards/AutomaticEnrollment/WindowsAutomaticEnrollmentPage/WindowsAutomaticEnrollmentPage.tsx
+++ b/frontend/pages/admin/IntegrationsPage/cards/AutomaticEnrollment/WindowsAutomaticEnrollmentPage/WindowsAutomaticEnrollmentPage.tsx
@@ -50,7 +50,7 @@ const WindowsAutomaticEnrollmentPage = () => {
diff --git a/website/config/routes.js b/website/config/routes.js
index a90dbf80c0..7209aab9e1 100644
--- a/website/config/routes.js
+++ b/website/config/routes.js
@@ -511,6 +511,7 @@ module.exports.routes = {
// These are external links not maintained by Fleet. We can point the Fleet UI to redirects here instead of the
// original sources to help avoid broken links.
'GET /learn-more-about/chromeos-updates': 'https://support.google.com/chrome/a/answer/6220366',
+ 'GET /sign-in-to/microsoft-automatic-enrollment-tool': 'https://portal.azure.com',
// Sitemap
// =============================================================================================================
From 7a1797f621551bbb6ea8ee193849cfaad61b86f0 Mon Sep 17 00:00:00 2001
From: Luke Heath
Date: Tue, 12 Dec 2023 14:52:27 -0600
Subject: [PATCH 3/5] Linux and Windows MDM scripts (#15501)
---
.../hosts/details/cards/Scripts/_styles.scss | 12 +++++
scripts/mdm/linux/linux-change-password.sh | 34 ++++++++++++
scripts/mdm/linux/linux-lock.sh | 32 ++++++++++++
scripts/mdm/linux/linux-unlock.sh | 11 ++++
scripts/mdm/linux/linux-wipe.sh | 46 ++++++++++++++++
.../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 +++++
10 files changed, 273 insertions(+)
create mode 100644 scripts/mdm/linux/linux-change-password.sh
create mode 100644 scripts/mdm/linux/linux-lock.sh
create mode 100644 scripts/mdm/linux/linux-unlock.sh
create mode 100644 scripts/mdm/linux/linux-wipe.sh
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
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/linux/linux-change-password.sh b/scripts/mdm/linux/linux-change-password.sh
new file mode 100644
index 0000000000..dfe4abf0b2
--- /dev/null
+++ b/scripts/mdm/linux/linux-change-password.sh
@@ -0,0 +1,34 @@
+#!/bin/sh
+
+# Disable automatic login for common display managers
+disable_autologin() {
+ # GDM (GNOME Display Manager)
+ if [ -f /etc/gdm3/custom.conf ]; then
+ sed -i '/^AutomaticLoginEnable/s/^/#/' /etc/gdm3/custom.conf
+ sed -i '/^AutomaticLogin/s/^/#/' /etc/gdm3/custom.conf
+ fi
+
+ # LightDM
+ if [ -f /etc/lightdm/lightdm.conf ]; then
+ sed -i '/^autologin-user=/s/^/#/' /etc/lightdm/lightdm.conf
+ fi
+
+ # Add similar cases for other display managers if needed
+}
+
+# Disable automatic login
+disable_autologin
+
+# Loop through all users in /etc/passwd
+awk -F':' '{ if ($3 >= 1000 && $3 < 60000) print $1 }' /etc/passwd | while read user
+do
+ if [ "$user" != "root" ]; then
+ echo "Logging out $user"
+ pkill -KILL -u "$user" # Kill user processes. This will log out logged-in users.
+ password=$(openssl rand -base64 9)
+ echo "$user:$password" | chpasswd
+ echo "$user: new password is $password"
+ fi
+done
+
+echo "All non-root users have been logged out and their passwords changed."
diff --git a/scripts/mdm/linux/linux-lock.sh b/scripts/mdm/linux/linux-lock.sh
new file mode 100644
index 0000000000..6c7e317240
--- /dev/null
+++ b/scripts/mdm/linux/linux-lock.sh
@@ -0,0 +1,32 @@
+#!/bin/sh
+
+# Disable automatic login for common display managers
+disable_autologin() {
+ # GDM (GNOME Display Manager)
+ if [ -f /etc/gdm3/custom.conf ]; then
+ sed -i '/^AutomaticLoginEnable/s/^/#/' /etc/gdm3/custom.conf
+ sed -i '/^AutomaticLogin/s/^/#/' /etc/gdm3/custom.conf
+ fi
+
+ # LightDM
+ if [ -f /etc/lightdm/lightdm.conf ]; then
+ sed -i '/^autologin-user=/s/^/#/' /etc/lightdm/lightdm.conf
+ fi
+
+ # Add similar cases for other display managers if needed
+}
+
+# Disable automatic login
+disable_autologin
+
+# Loop through all users in /etc/passwd
+awk -F':' '{ if ($3 >= 1000 && $3 < 60000) print $1 }' /etc/passwd | while read user
+do
+ if [ "$user" != "root" ]; then
+ echo "Logging out $user"
+ pkill -KILL -u "$user" # Kill user processes. This will log out logged-in users.
+ passwd -l "$user" # Lock the user account
+ fi
+done
+
+echo "All non-root users have been logged out and their accounts locked."
diff --git a/scripts/mdm/linux/linux-unlock.sh b/scripts/mdm/linux/linux-unlock.sh
new file mode 100644
index 0000000000..2122fb837b
--- /dev/null
+++ b/scripts/mdm/linux/linux-unlock.sh
@@ -0,0 +1,11 @@
+#!/bin/sh
+
+# Unlock password for all non-root users
+awk -F':' '{ if ($3 >= 1000 && $3 < 60000) print $1 }' /etc/passwd | while read user
+do
+ echo "$user"
+ if [ "$user" != "root" ]; then
+ echo "Unlocking password for $user"
+ passwd -u $user
+ fi
+done
diff --git a/scripts/mdm/linux/linux-wipe.sh b/scripts/mdm/linux/linux-wipe.sh
new file mode 100644
index 0000000000..69a78b1235
--- /dev/null
+++ b/scripts/mdm/linux/linux-wipe.sh
@@ -0,0 +1,46 @@
+#!/bin/sh
+
+# Function to log out all users and lock their passwords except root
+logout_users() {
+ for user in $(who | awk '{print $1}' | sort | uniq)
+ do
+ if [ "$user" != "root" ]; then
+ echo "Logging out $user"
+ pkill -KILL -u "$user"
+ passwd -l "$user"
+ fi
+ done
+}
+
+# Function to wipe non-essential data
+wipe_non_essential_data() {
+ # Define non-essential paths
+ non_essential_paths="/home/* /tmp /var/tmp /var/log /home/*/.cache /var/cache /home/*/.local/share/Trash"
+
+ for path in $non_essential_paths
+ do
+ if [ -e "$path" ]; then
+ echo "Wiping $path"
+ rm -rf "$path"
+ fi
+ done
+}
+
+# Function to wipe system files - Warning: This will render the system inoperable
+wipe_system_files() {
+ # Define essential system paths
+ essential_system_paths="/bin /sbin /usr /lib"
+
+ for path in $essential_system_paths
+ do
+ echo "Wiping $path"
+ rm -rf "$path"
+ done
+}
+
+# Start the wiping process
+logout_users
+wipe_non_essential_data
+wipe_system_files
+
+echo "Wiping process completed."
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."
From 053582fd881b347102868c55ab567d29b5151ef3 Mon Sep 17 00:00:00 2001
From: Noah Talerman <47070608+noahtalerman@users.noreply.github.com>
Date: Tue, 12 Dec 2023 15:58:26 -0500
Subject: [PATCH 4/5] Update disk encryption docs (#15496)
- Associated w/ this story: #15600
- Update docs now that disk encryption enforcement is cross platform
(Windows story here: #12577)
- Remove section about resetting a password w/ disk encryption key to
reduce doc content. Remove this link from the UI
---
articles/fleet-4.33.0.md | 2 +-
docs/Using Fleet/MDM-disk-encryption.md | 62 ++++++-------------
docs/Using Fleet/Scripts.md | 2 +-
.../DiskEncryptionKeyModal.tsx | 12 +---
4 files changed, 23 insertions(+), 55 deletions(-)
diff --git a/articles/fleet-4.33.0.md b/articles/fleet-4.33.0.md
index 2a51ef8087..d14e40809d 100644
--- a/articles/fleet-4.33.0.md
+++ b/articles/fleet-4.33.0.md
@@ -32,7 +32,7 @@ of compliant devices. This reflects our commitment to creating user-friendly sys
empathy we share for our users' experience and their need for efficient, straightforward tools.
Learn more about [Fleet's "Verified"
-status](https://fleetdm.com/docs/using-fleet/mdm-disk-encryption#step-3-confirm-disk-encryption-is-enforced-and-fleet-is-storing-the-disk-encryption-key).
+status](https://fleetdm.com/docs/using-fleet/mdm-disk-encryption#disk-encryption-status).

diff --git a/docs/Using Fleet/MDM-disk-encryption.md b/docs/Using Fleet/MDM-disk-encryption.md
index 0e8b59b70b..db70ad46c2 100644
--- a/docs/Using Fleet/MDM-disk-encryption.md
+++ b/docs/Using Fleet/MDM-disk-encryption.md
@@ -2,30 +2,26 @@
_Available in Fleet Premium_
-In Fleet, you can enforce disk encryption on your macOS hosts. Apple calls this [FileVault](https://support.apple.com/en-us/HT204837). If turned on, hosts’ disk encryption keys will be stored in Fleet.
+In Fleet, you can enforce disk encryption for your macOS and Windows hosts.
-You can also enforce custom macOS settings. Learn how [here](./MDM-custom-macOS-settings.md).
+> Apple calls this [FileVault](https://support.apple.com/en-us/HT204837) and Microsoft calls this [BitLocker](https://learn.microsoft.com/en-us/windows/security/operating-system-security/data-protection/bitlocker/).
+
+When disk encryption is enforced, hosts’ disk encryption keys will be stored in Fleet.
## Enforce disk encryption
-To enforce disk encryption and have Fleet collect the disk encryption key, we will do the following steps:
-
-1. Enforce disk encryption
-2. Share migrations with end users
-2. Confirm disk encryption is enforced and Fleet is storing the disk encryption key
-
-### Step 1: enforce disk encryption
-
-To enforce disk encryption, choose the "Fleet UI" or "fleetctl" method and follow the steps below.
+You can enforce disk encryption in the Fleet UI, with Fleet API, or with the fleetctl command-line interface (CLI).
Fleet UI:
-1. In the Fleet UI, head to the **Controls > macOS settings > Disk encryption** page. Users with the maintainer and admin roles can access the settings pages.
+1. In Fleet, head to the **Controls > OS settings > Disk encryption** page.
-2. Choose which team you want to enforce disk encryption on by selecting the desired team in the teams dropdown in the upper left corner. Teams are available in Fleet Premium.
+2. Choose which team you want to enforce disk encryption on by selecting the desired team in the teams dropdown in the upper left corner.
3. Check the box next to **Turn on** and select **Save**.
+Fleet API: API documentation is [here](../REST%20API/rest-api.md#update-disk-encryption-enforcement)
+
`fleetctl` CLI:
1. Choose which team you want to enforce disk encryption on.
@@ -41,8 +37,7 @@ spec:
team:
name: Workstations (canary)
mdm:
- macos_settings:
- enable_disk_encryption: true
+ enable_disk_encryption: true
...
```
@@ -53,28 +48,19 @@ apiVersion: v1
kind: config
spec:
mdm:
- macos_settings:
- enable_disk_encryption: true
+ enable_disk_encryption: true
...
```
-Learn more about configuration options for hosts that aren't assigned to a team [here](./configuration-files/README.md#organization-settings).
-
-3. Set the `mdm.macos_settings.enable_disk_encryption` configuration option to `true`.
+3. Set the `mdm.enable_disk_encryption` configuration option to `true`.
4. Run the `fleetctl apply -f workstations-canary-config.yml` command.
> Fleet auto-configures `DeferForceAtUserLoginMaxBypassAttempts` to `1`, ensuring mandatory disk encryption during new Mac setup.
-### Step 2: share migration instructions with your end users
+### Disk encryption status
-In order to complete the process of encrypting the hard drive and escrowing the key in Fleet, your end users must take action. If the host already had disk encryption turned on, the user will need to input their password. If the host did not already have disk encryption turned on, the user will need to log out or restart their computer.
-
-Share [these guided instructions](./MDM-migration-guide.md#how-to-turn-on-disk-encryption) with your end users.
-
-### Step 3: confirm disk encryption is enforced and Fleet is storing the disk encryption key
-
-In the Fleet UI, head to the **Controls > macOS settings > Disk encryption** tab. You will see a table that shows the status of disk encryption on your hosts.
+In the Fleet UI, head to the **Controls > OS settings > Disk encryption** tab. You will see a table that shows the status of disk encryption on your hosts.
* Verified: the host turned disk encryption on and sent their key to Fleet. Fleet verified with osquery. See instructions for viewing the disk encryption key [here](#view-disk-encryption-key).
@@ -94,31 +80,23 @@ You can click each status to view the list of hosts for that status.
## View disk encryption key
-The disk encryption key allows you to reset a macOS host's password if you don't know it. This way, if you plan to prepare a host for a new employee, you can login to it and erase all its content and settings.
-
-The key can be accessed by Fleet admin, maintainers, and observers. An event is tracked in the activity feed when a user views the key in Fleet.
-
How to view the disk encryption key:
1. Select a host on the **Hosts** page.
2. On the **Host details** page, select **Actions > Show disk encryption key**.
-> Whenever a disk encryption key is viewed, an activity will be logged. To view activity in the Fleet UI, click on the Fleet icon in the top navigation bar and make sure **All teams** is selected in the teams dropdown.
+## Migrate macOS hosts
-## Reset a macOS host's password using the disk encryption key
+When migrating macOS hosts another MDM solution, in order to complete the process of encrypting the hard drive and escrowing the key in Fleet, your end users must take action.
-How to reset a macOS host's password using the disk encryption key:
+If the host already had disk encryption turned on, the user will need to input their password.
-1. Restart the host. If you just unlocked a host that was locked remotely, the host will automatically restart.
+If the host did not already have disk encryption turned on, the user will need to log out or restart their computer.
-2. On the Mac's login screen, enter the incorrect password three times. After the third failed login attempt, the Mac will display a prompt below the password field with the following message: "If you forgot your password, you can reset it using your Recovery Key." Select the right facing arrow at the end of this prompt.
-
-3. Enter the disk encryption key. Note that Apple calls this "Recovery key." Learn how to find a host's disk encryption key [here](#view-disk-encryption-key).
-
-4. The Mac will display a prompt to reset the password. Reset the password and save this password somewhere safe. If you plan to prepare this Mac for a new employee, you'll need this password to erase all content and settings on the Mac.
+Share [these guided instructions](./MDM-migration-guide.md#how-to-turn-on-disk-encryption) with your end users.
-
+
diff --git a/docs/Using Fleet/Scripts.md b/docs/Using Fleet/Scripts.md
index c80fa4f48b..2e7c30ad86 100644
--- a/docs/Using Fleet/Scripts.md
+++ b/docs/Using Fleet/Scripts.md
@@ -36,7 +36,7 @@ Fleet UI:
> Currently, you can only run scripts on macOS and Windows hosts in the Fleet UI. To run a script on a Linux host, use the Fleet API or fleetctl CLI.
-Fleet API: API documentation is [here](https://fleetdm.com/docs/rest-api/rest-api#run-script)
+Fleet API: API documentation is [here](../REST%20API/rest-api.md#run-script)
fleetctl CLI:
diff --git a/frontend/pages/hosts/details/HostDetailsPage/modals/DiskEncryptionKeyModal/DiskEncryptionKeyModal.tsx b/frontend/pages/hosts/details/HostDetailsPage/modals/DiskEncryptionKeyModal/DiskEncryptionKeyModal.tsx
index 1046822673..b453ebc83c 100644
--- a/frontend/pages/hosts/details/HostDetailsPage/modals/DiskEncryptionKeyModal/DiskEncryptionKeyModal.tsx
+++ b/frontend/pages/hosts/details/HostDetailsPage/modals/DiskEncryptionKeyModal/DiskEncryptionKeyModal.tsx
@@ -58,9 +58,6 @@ const DiskEncryptionKeyModal = ({
const recoveryText = isMacOS
? "Use this key to log in to the host if you forgot the password."
: "Use this key to unlock the encrypted drive.";
- const recoveryUrl = isMacOS
- ? "https://fleetdm.com/docs/using-fleet/mdm-disk-encryption#reset-a-macos-hosts-password-using-the-disk-encryption-key"
- : "https://fleetdm.com/docs/using-fleet/mdm-disk-encryption#unlock-a-windows-hosts-drive-using-the-disk-encryption-key";
return (
@@ -70,14 +67,7 @@ const DiskEncryptionKeyModal = ({
<>
{descriptionText}
-
- {recoveryText}{" "}
-
-
+ {recoveryText}
Done
From 9d65a2dc8c12c7aef9c0df130944cdee6974e8ce Mon Sep 17 00:00:00 2001
From: Gabriel Hernandez
Date: Tue, 12 Dec 2023 21:03:33 +0000
Subject: [PATCH 5/5] Implement UI for new software pages (#15579)
---
changes/15226-hosts-filter-software | 1 +
...e-15224-15225-implement-new-software-pages | 2 +
cmd/fleetctl/get_test.go | 4 +-
frontend/__mocks__/softwareMock.ts | 135 +++-
.../top_nav/SiteTopNav/SiteTopNav.tsx | 4 -
.../components/top_nav/SiteTopNav/navItems.ts | 2 +-
frontend/interfaces/software.ts | 51 +-
frontend/interfaces/vulnerability.ts | 16 -
.../pages/DashboardPage/DashboardPage.tsx | 4 +-
.../DashboardPage/cards/Software/Software.tsx | 2 +-
frontend/pages/SoftwarePage/SoftwarePage.tsx | 380 +++++++++
.../SoftwareTitleDetailsPage.tsx | 82 ++
.../SoftwareTitleDetailsTable.tsx | 70 ++
.../SoftwareTitleDetailsTableConfig.tsx | 111 +++
.../SoftwareTitleDetailsTable/_styles.scss | 13 +
.../SoftwareTitleDetailsTable/index.ts | 1 +
.../SoftwareTitleDetailsPage/_styles.scss | 33 +
.../SoftwareTitleDetailsPage/index.ts | 1 +
.../SoftwareTitles/SoftwareTitles.tsx | 303 ++++++++
.../SoftwareTitlesTableConfig.tsx | 185 +++++
.../SoftwarePage/SoftwareTitles/_styles.scss | 165 ++++
.../SoftwarePage/SoftwareTitles/index.ts | 1 +
.../SoftwareVersionDetailsPage.tsx | 125 +++
.../SoftwareVersionDetailsTableConfig.tsx} | 16 +-
.../SoftwareVersionDetailsPage/_styles.scss | 25 +
.../SoftwareVersionDetailsPage/index.ts | 1 +
.../SoftwareVersions/SoftwareVersions.tsx | 318 ++++++++
.../SoftwareVersionsTableConfig.tsx | 160 ++++
.../SoftwareVersions/_styles.scss | 166 ++++
.../SoftwarePage/SoftwareVersions/index.ts | 1 +
frontend/pages/SoftwarePage/_styles.scss | 59 ++
.../EmptySoftwareTable.tsx | 3 +-
.../components/EmptySoftwareTable/index.ts | 1 +
.../ManageAutomationsModal.tsx | 0
.../ManageAutomationsModal/_styles.scss | 0
.../ManageAutomationsModal/index.ts | 0
.../PreviewPayloadModal.tsx | 19 +-
.../PreviewPayloadModal/_styles.scss | 0
.../components/PreviewPayloadModal/index.ts | 0
.../PreviewTicketModal/PreviewTicketModal.tsx | 8 +-
.../PreviewTicketModal/_styles.scss | 0
.../components/PreviewTicketModal/index.ts | 0
.../SoftwareDetailsSummary.tsx | 69 ++
.../SoftwareDetailsSummary/_styles.scss | 40 +
.../SoftwareDetailsSummary/index.ts | 1 +
.../components/VersionCell/VersionCell.tsx | 67 ++
.../components/VersionCell/_styles.scss | 10 +
.../components/VersionCell/index.ts | 1 +
.../VulnerabilitiesCell.tsx | 114 +++
.../VulnerabilitiesCell/_styles.scss | 12 +
.../components/VulnerabilitiesCell/index.ts | 1 +
.../components/icons/AcrobatReader.tsx | 17 +
.../SoftwarePage/components/icons/Chrome.tsx | 71 ++
.../SoftwarePage/components/icons/Excel.tsx | 70 ++
.../components/icons/Extension.tsx | 20 +
.../SoftwarePage/components/icons/Firefox.tsx | 292 +++++++
.../SoftwarePage/components/icons/MacApp.tsx | 20 +
.../SoftwarePage/components/icons/Package.tsx | 20 +
.../SoftwarePage/components/icons/Safari.tsx | 584 ++++++++++++++
.../SoftwarePage/components/icons/Slack.tsx | 37 +
.../icons/SoftwareIcon/SoftwareIcon.tsx | 64 ++
.../components/icons/SoftwareIcon/index.ts | 1 +
.../SoftwarePage/components/icons/Teams.tsx | 84 ++
.../components/icons/VisualStudioCode.tsx | 37 +
.../components/icons/WindowsApp.tsx | 20 +
.../SoftwarePage/components/icons/Word.tsx | 94 +++
.../SoftwarePage/components/icons/Zoom.tsx | 30 +
.../SoftwarePage/components/icons/index.ts | 60 ++
frontend/pages/SoftwarePage/helpers.ts | 0
frontend/pages/SoftwarePage/index.ts | 1 +
.../hosts/ManageHostsPage/ManageHostsPage.tsx | 35 +-
.../HostsFilterBlock/HostsFilterBlock.tsx | 42 +-
.../hosts/details/cards/Software/Software.tsx | 2 +-
.../cards/Software/SoftwareTableConfig.tsx | 4 +-
.../ManageSoftwarePage/ManageSoftwarePage.tsx | 728 ------------------
.../SoftwareTableConfig.tsx | 271 -------
.../software/ManageSoftwarePage/_styles.scss | 222 ------
.../software/ManageSoftwarePage/index.ts | 1 -
.../SoftwareDetailsPage.tsx | 142 ----
.../software/SoftwareDetailsPage/_styles.scss | 80 --
.../Vulnerabilities/Vulnerabilities.tests.tsx | 105 ---
.../Vulnerabilities/Vulnerabilities.tsx | 79 --
.../components/Vulnerabilities/_styles.scss | 60 --
.../components/Vulnerabilities/index.ts | 1 -
.../software/SoftwareDetailsPage/index.ts | 1 -
frontend/router/index.tsx | 17 +-
frontend/router/paths.ts | 15 +-
frontend/services/entities/host_count.ts | 6 +
frontend/services/entities/hosts.ts | 17 +-
frontend/services/entities/software.ts | 58 +-
frontend/utilities/endpoints.ts | 10 +-
frontend/utilities/url/index.ts | 9 +-
server/fleet/software.go | 2 +-
93 files changed, 4442 insertions(+), 1770 deletions(-)
create mode 100644 changes/15226-hosts-filter-software
create mode 100644 changes/issue-15224-15225-implement-new-software-pages
create mode 100644 frontend/pages/SoftwarePage/SoftwarePage.tsx
create mode 100644 frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsPage.tsx
create mode 100644 frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsTable/SoftwareTitleDetailsTable.tsx
create mode 100644 frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsTable/SoftwareTitleDetailsTableConfig.tsx
create mode 100644 frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsTable/_styles.scss
create mode 100644 frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsTable/index.ts
create mode 100644 frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/_styles.scss
create mode 100644 frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/index.ts
create mode 100644 frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTitles.tsx
create mode 100644 frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTitlesTableConfig.tsx
create mode 100644 frontend/pages/SoftwarePage/SoftwareTitles/_styles.scss
create mode 100644 frontend/pages/SoftwarePage/SoftwareTitles/index.ts
create mode 100644 frontend/pages/SoftwarePage/SoftwareVersionDetailsPage/SoftwareVersionDetailsPage.tsx
rename frontend/pages/{software/SoftwareDetailsPage/components/Vulnerabilities/VulnTableConfig.tsx => SoftwarePage/SoftwareVersionDetailsPage/SoftwareVersionDetailsTableConfig.tsx} (96%)
create mode 100644 frontend/pages/SoftwarePage/SoftwareVersionDetailsPage/_styles.scss
create mode 100644 frontend/pages/SoftwarePage/SoftwareVersionDetailsPage/index.ts
create mode 100644 frontend/pages/SoftwarePage/SoftwareVersions/SoftwareVersions.tsx
create mode 100644 frontend/pages/SoftwarePage/SoftwareVersions/SoftwareVersionsTableConfig.tsx
create mode 100644 frontend/pages/SoftwarePage/SoftwareVersions/_styles.scss
create mode 100644 frontend/pages/SoftwarePage/SoftwareVersions/index.ts
create mode 100644 frontend/pages/SoftwarePage/_styles.scss
rename frontend/pages/{software/components => SoftwarePage/components/EmptySoftwareTable}/EmptySoftwareTable.tsx (94%)
create mode 100644 frontend/pages/SoftwarePage/components/EmptySoftwareTable/index.ts
rename frontend/pages/{software/ManageSoftwarePage => SoftwarePage}/components/ManageAutomationsModal/ManageAutomationsModal.tsx (100%)
rename frontend/pages/{software/ManageSoftwarePage => SoftwarePage}/components/ManageAutomationsModal/_styles.scss (100%)
rename frontend/pages/{software/ManageSoftwarePage => SoftwarePage}/components/ManageAutomationsModal/index.ts (100%)
rename frontend/pages/{software/ManageSoftwarePage => SoftwarePage}/components/PreviewPayloadModal/PreviewPayloadModal.tsx (88%)
rename frontend/pages/{software/ManageSoftwarePage => SoftwarePage}/components/PreviewPayloadModal/_styles.scss (100%)
rename frontend/pages/{software/ManageSoftwarePage => SoftwarePage}/components/PreviewPayloadModal/index.ts (100%)
rename frontend/pages/{software/ManageSoftwarePage => SoftwarePage}/components/PreviewTicketModal/PreviewTicketModal.tsx (79%)
rename frontend/pages/{software/ManageSoftwarePage => SoftwarePage}/components/PreviewTicketModal/_styles.scss (100%)
rename frontend/pages/{software/ManageSoftwarePage => SoftwarePage}/components/PreviewTicketModal/index.ts (100%)
create mode 100644 frontend/pages/SoftwarePage/components/SoftwareDetailsSummary/SoftwareDetailsSummary.tsx
create mode 100644 frontend/pages/SoftwarePage/components/SoftwareDetailsSummary/_styles.scss
create mode 100644 frontend/pages/SoftwarePage/components/SoftwareDetailsSummary/index.ts
create mode 100644 frontend/pages/SoftwarePage/components/VersionCell/VersionCell.tsx
create mode 100644 frontend/pages/SoftwarePage/components/VersionCell/_styles.scss
create mode 100644 frontend/pages/SoftwarePage/components/VersionCell/index.ts
create mode 100644 frontend/pages/SoftwarePage/components/VulnerabilitiesCell/VulnerabilitiesCell.tsx
create mode 100644 frontend/pages/SoftwarePage/components/VulnerabilitiesCell/_styles.scss
create mode 100644 frontend/pages/SoftwarePage/components/VulnerabilitiesCell/index.ts
create mode 100644 frontend/pages/SoftwarePage/components/icons/AcrobatReader.tsx
create mode 100644 frontend/pages/SoftwarePage/components/icons/Chrome.tsx
create mode 100644 frontend/pages/SoftwarePage/components/icons/Excel.tsx
create mode 100644 frontend/pages/SoftwarePage/components/icons/Extension.tsx
create mode 100644 frontend/pages/SoftwarePage/components/icons/Firefox.tsx
create mode 100644 frontend/pages/SoftwarePage/components/icons/MacApp.tsx
create mode 100644 frontend/pages/SoftwarePage/components/icons/Package.tsx
create mode 100644 frontend/pages/SoftwarePage/components/icons/Safari.tsx
create mode 100644 frontend/pages/SoftwarePage/components/icons/Slack.tsx
create mode 100644 frontend/pages/SoftwarePage/components/icons/SoftwareIcon/SoftwareIcon.tsx
create mode 100644 frontend/pages/SoftwarePage/components/icons/SoftwareIcon/index.ts
create mode 100644 frontend/pages/SoftwarePage/components/icons/Teams.tsx
create mode 100644 frontend/pages/SoftwarePage/components/icons/VisualStudioCode.tsx
create mode 100644 frontend/pages/SoftwarePage/components/icons/WindowsApp.tsx
create mode 100644 frontend/pages/SoftwarePage/components/icons/Word.tsx
create mode 100644 frontend/pages/SoftwarePage/components/icons/Zoom.tsx
create mode 100644 frontend/pages/SoftwarePage/components/icons/index.ts
create mode 100644 frontend/pages/SoftwarePage/helpers.ts
create mode 100644 frontend/pages/SoftwarePage/index.ts
delete mode 100644 frontend/pages/software/ManageSoftwarePage/ManageSoftwarePage.tsx
delete mode 100644 frontend/pages/software/ManageSoftwarePage/SoftwareTableConfig.tsx
delete mode 100644 frontend/pages/software/ManageSoftwarePage/_styles.scss
delete mode 100644 frontend/pages/software/ManageSoftwarePage/index.ts
delete mode 100644 frontend/pages/software/SoftwareDetailsPage/SoftwareDetailsPage.tsx
delete mode 100644 frontend/pages/software/SoftwareDetailsPage/_styles.scss
delete mode 100644 frontend/pages/software/SoftwareDetailsPage/components/Vulnerabilities/Vulnerabilities.tests.tsx
delete mode 100644 frontend/pages/software/SoftwareDetailsPage/components/Vulnerabilities/Vulnerabilities.tsx
delete mode 100644 frontend/pages/software/SoftwareDetailsPage/components/Vulnerabilities/_styles.scss
delete mode 100644 frontend/pages/software/SoftwareDetailsPage/components/Vulnerabilities/index.ts
delete mode 100644 frontend/pages/software/SoftwareDetailsPage/index.ts
diff --git a/changes/15226-hosts-filter-software b/changes/15226-hosts-filter-software
new file mode 100644
index 0000000000..19cf0805f3
--- /dev/null
+++ b/changes/15226-hosts-filter-software
@@ -0,0 +1 @@
+- Updated manage hosts UI to filter hosts by `software_version_id` and `software_title_id`.
diff --git a/changes/issue-15224-15225-implement-new-software-pages b/changes/issue-15224-15225-implement-new-software-pages
new file mode 100644
index 0000000000..a376105647
--- /dev/null
+++ b/changes/issue-15224-15225-implement-new-software-pages
@@ -0,0 +1,2 @@
+- add new software pages to fleet UI. Includes software titles, software versions, software title
+ details and software version details.
diff --git a/cmd/fleetctl/get_test.go b/cmd/fleetctl/get_test.go
index ac7eadd9f8..8c647ac1b4 100644
--- a/cmd/fleetctl/get_test.go
+++ b/cmd/fleetctl/get_test.go
@@ -691,6 +691,7 @@ spec:
versions:
- id: 0
version: 0.0.3
+ vulnerabilities: null
versions_count: 1
`
@@ -741,7 +742,8 @@ spec:
"versions": [
{
"id": 0,
- "version": "0.0.3"
+ "version": "0.0.3",
+ "vulnerabilities": null
}
]
}
diff --git a/frontend/__mocks__/softwareMock.ts b/frontend/__mocks__/softwareMock.ts
index 7bd61786e0..4067212381 100644
--- a/frontend/__mocks__/softwareMock.ts
+++ b/frontend/__mocks__/softwareMock.ts
@@ -1,4 +1,16 @@
-import { ISoftware } from "interfaces/software";
+import {
+ ISoftware,
+ ISoftwareVersion,
+ ISoftwareTitle,
+ ISoftwareVulnerability,
+ ISoftwareTitleVersion,
+} from "interfaces/software";
+import {
+ ISoftwareTitlesResponse,
+ ISoftwareTitleResponse,
+ ISoftwareVersionsResponse,
+ ISoftwareVersionResponse,
+} from "services/entities/software";
const DEFAULT_SOFTWARE_MOCK: ISoftware = {
hosts_count: 1,
@@ -12,8 +24,125 @@ const DEFAULT_SOFTWARE_MOCK: ISoftware = {
bundle_identifier: "com.app.mock",
};
-const createMockSoftware = (overrides?: Partial): ISoftware => {
+export const createMockSoftware = (
+ overrides?: Partial
+): ISoftware => {
return { ...DEFAULT_SOFTWARE_MOCK, ...overrides };
};
-export default createMockSoftware;
+const DEFAULT_SOFTWARE_TITLE_VERSION_MOCK = {
+ id: 1,
+ version: "1.0.0",
+ vulnerabilities: ["CVE-2020-0001"],
+};
+
+export const createMockSoftwareTitleVersion = (
+ overrides?: Partial
+): ISoftwareTitleVersion => {
+ return { ...DEFAULT_SOFTWARE_TITLE_VERSION_MOCK, ...overrides };
+};
+
+const DEFAULT_SOFTWARE_TITLE_MOCK: ISoftwareTitle = {
+ id: 1,
+ name: "mock software 1.app",
+ versions_count: 1,
+ source: "apps",
+ hosts_count: 1,
+ browser: "chrome",
+ versions: [createMockSoftwareTitleVersion()],
+};
+
+export const createMockSoftwareTitle = (
+ overrides?: Partial
+): ISoftwareTitle => {
+ return { ...DEFAULT_SOFTWARE_TITLE_MOCK, ...overrides };
+};
+
+const DEFAULT_SOFTWARE_TITLES_RESPONSE_MOCK: ISoftwareTitlesResponse = {
+ counts_updated_at: "2020-01-01T00:00:00.000Z",
+ count: 1,
+ software_titles: [createMockSoftwareTitle()],
+ meta: {
+ has_next_results: false,
+ has_previous_results: false,
+ },
+};
+
+export const createMockSoftwareTitlesReponse = (
+ overrides?: Partial
+): ISoftwareTitlesResponse => {
+ return { ...DEFAULT_SOFTWARE_TITLES_RESPONSE_MOCK, ...overrides };
+};
+
+const DEFAULT_SOFTWARE_VULNERABILITY_MOCK = {
+ cve: "CVE-2020-0001",
+ details_link: "https://test.com",
+ cvss_score: 9,
+ epss_probability: 0.8,
+ cisa_known_exploit: false,
+ cve_published: "2020-01-01T00:00:00.000Z",
+ cve_description: "test description",
+ resolved_in_version: "1.2.3",
+};
+
+export const createMockSoftwareVulnerability = (
+ overrides?: Partial
+): ISoftwareVulnerability => {
+ return { ...DEFAULT_SOFTWARE_VULNERABILITY_MOCK, ...overrides };
+};
+
+const DEFAULT_SOFTWARE_VERSION_MOCK: ISoftwareVersion = {
+ id: 1,
+ name: "test.app",
+ version: "1.2.3",
+ bundle_identifier: "com.test.Desktop",
+ source: "test_package",
+ release: "1",
+ vendor: "test_vendor",
+ arch: "x86_64",
+ generated_cpe: "cpe:test:app:1.2.3",
+ vulnerabilities: [createMockSoftwareVulnerability()],
+ hosts_count: 1,
+};
+
+export const createMockSoftwareVersion = (
+ overrides?: Partial
+): ISoftwareVersion => {
+ return { ...DEFAULT_SOFTWARE_VERSION_MOCK, ...overrides };
+};
+
+const DEFAULT_SOFTWARE_VERSIONS_RESPONSE_MOCK: ISoftwareVersionsResponse = {
+ counts_updated_at: "2020-01-01T00:00:00.000Z",
+ count: 1,
+ software: [createMockSoftwareVersion()],
+ meta: {
+ has_next_results: false,
+ has_previous_results: false,
+ },
+};
+
+export const createMockSoftwareVersionsReponse = (
+ overrides?: Partial
+): ISoftwareVersionsResponse => {
+ return { ...DEFAULT_SOFTWARE_VERSIONS_RESPONSE_MOCK, ...overrides };
+};
+
+const DEFAULT_SOFTWARE_TITLE_RESPONSE = {
+ software_title: createMockSoftwareTitle(),
+};
+
+export const createMockSoftwareTitleResponse = (
+ overrides?: Partial
+): ISoftwareTitleResponse => {
+ return { ...DEFAULT_SOFTWARE_TITLE_RESPONSE, ...overrides };
+};
+
+const DEFAULT_SOFTWARE_VERSION_RESPONSE = {
+ software: createMockSoftwareVersion(),
+};
+
+export const createMockSoftwareVersionResponse = (
+ overrides?: Partial
+): ISoftwareVersionResponse => {
+ return { ...DEFAULT_SOFTWARE_VERSION_RESPONSE, ...overrides };
+};
diff --git a/frontend/components/top_nav/SiteTopNav/SiteTopNav.tsx b/frontend/components/top_nav/SiteTopNav/SiteTopNav.tsx
index 534253ea02..0437a3aed0 100644
--- a/frontend/components/top_nav/SiteTopNav/SiteTopNav.tsx
+++ b/frontend/components/top_nav/SiteTopNav/SiteTopNav.tsx
@@ -95,7 +95,6 @@ const SiteTopNav = ({
isGlobalMaintainer,
isAnyTeamMaintainer,
isNoAccess,
- isMdmEnabledAndConfigured, // TODO: confirm
isSandboxMode,
} = useContext(AppContext);
@@ -160,9 +159,6 @@ const SiteTopNav = ({
{name}
- {/*
- {name}
-
*/}
);
}
diff --git a/frontend/components/top_nav/SiteTopNav/navItems.ts b/frontend/components/top_nav/SiteTopNav/navItems.ts
index 4b828087ed..22738ecccb 100644
--- a/frontend/components/top_nav/SiteTopNav/navItems.ts
+++ b/frontend/components/top_nav/SiteTopNav/navItems.ts
@@ -67,7 +67,7 @@ export default (
name: "Software",
location: {
regex: new RegExp(`^${URL_PREFIX}/software/`),
- pathname: PATHS.MANAGE_SOFTWARE,
+ pathname: PATHS.SOFTWARE_TITLES,
},
withParams: { type: "query", names: ["team_id"] },
},
diff --git a/frontend/interfaces/software.ts b/frontend/interfaces/software.ts
index 7b285004a9..88f5c856f6 100644
--- a/frontend/interfaces/software.ts
+++ b/frontend/interfaces/software.ts
@@ -1,5 +1,5 @@
import PropTypes from "prop-types";
-import vulnerabilityInterface, { IVulnerability } from "./vulnerability";
+import vulnerabilityInterface from "./vulnerability";
export default PropTypes.shape({
type: PropTypes.string,
@@ -23,6 +23,8 @@ export interface IGetSoftwareByIdResponse {
software: ISoftware;
}
+// TODO: old software interface. replaced with ISoftwareVersion
+// check to see if we still need this.
export interface ISoftware {
id: number;
name: string; // e.g., "Figma.app"
@@ -30,12 +32,54 @@ export interface ISoftware {
bundle_identifier?: string | null; // e.g., "com.figma.Desktop"
source: string; // e.g., "apps"
generated_cpe: string;
- vulnerabilities: IVulnerability[] | null;
+ vulnerabilities: ISoftwareVulnerability[] | null;
hosts_count?: number;
last_opened_at?: string | null; // e.g., "2021-08-18T15:11:35Z”
installed_paths?: string[];
}
+export interface ISoftwareTitleVersion {
+ id: number;
+ version: string;
+ vulnerabilities: string[] | null; // TODO: does this return null or is it omitted?
+ hosts_count?: number;
+}
+
+export interface ISoftwareTitle {
+ id: number;
+ name: string;
+ versions_count: number;
+ source: string;
+ hosts_count: number;
+ versions: ISoftwareTitleVersion[];
+ browser: string;
+}
+
+export interface ISoftwareVulnerability {
+ cve: string;
+ details_link: string;
+ cvss_score?: number | null;
+ epss_probability?: number | null;
+ cisa_known_exploit?: boolean | null;
+ cve_published?: string | null;
+ cve_description?: string | null;
+ resolved_in_version?: string | null;
+}
+
+export interface ISoftwareVersion {
+ id: number;
+ name: string; // e.g., "Figma.app"
+ version: string; // e.g., "2.1.11"
+ bundle_identifier?: string; // e.g., "com.figma.Desktop"
+ source: string; // e.g., "apps"
+ release: string; // TODO: on software/verions/:id?
+ vendor: string;
+ arch: string; // e.g., "x86_64" // TODO: on software/verions/:id?
+ generated_cpe: string;
+ vulnerabilities: ISoftwareVulnerability[] | null;
+ hosts_count?: number;
+}
+
export const TYPE_CONVERSION: Record = {
apt_sources: "Package (APT)",
deb_packages: "Package (deb)",
@@ -56,7 +100,8 @@ export const TYPE_CONVERSION: Record = {
pkg_packages: "Package (pkg)",
} as const;
-export const formatSoftwareType = (source: string): string => {
+// TODO: update with new software types
+export const formatSoftwareType = (source: string) => {
const DICT = TYPE_CONVERSION;
return DICT[source] || "Unknown";
};
diff --git a/frontend/interfaces/vulnerability.ts b/frontend/interfaces/vulnerability.ts
index 168a3dda24..3806f61326 100644
--- a/frontend/interfaces/vulnerability.ts
+++ b/frontend/interfaces/vulnerability.ts
@@ -4,19 +4,3 @@ export default PropTypes.shape({
cve: PropTypes.string,
details_link: PropTypes.string,
});
-
-export interface IHostsAffected {
- id: number;
- display_name: string;
- url: string;
- software_installed_paths?: string[];
-}
-export interface IVulnerability {
- cve: string;
- details_link: string;
- cvss_score?: number;
- epss_probability?: number;
- cisa_known_exploit?: boolean;
- cve_published?: string;
- hosts_affected?: IHostsAffected[];
-}
diff --git a/frontend/pages/DashboardPage/DashboardPage.tsx b/frontend/pages/DashboardPage/DashboardPage.tsx
index d4e0d4b228..3a224d8b63 100644
--- a/frontend/pages/DashboardPage/DashboardPage.tsx
+++ b/frontend/pages/DashboardPage/DashboardPage.tsx
@@ -465,11 +465,11 @@ const DashboardPage = ({ router, location }: IDashboardProps): JSX.Element => {
};
const onSoftwareTabChange = (index: number) => {
- const { MANAGE_SOFTWARE } = paths;
+ const { SOFTWARE_TITLES } = paths;
setSoftwareNavTabIndex(index);
setSoftwareActionUrl &&
setSoftwareActionUrl(
- index === 1 ? `${MANAGE_SOFTWARE}?vulnerable=true` : MANAGE_SOFTWARE
+ index === 1 ? `${SOFTWARE_TITLES}?vulnerable=true` : SOFTWARE_TITLES
);
};
diff --git a/frontend/pages/DashboardPage/cards/Software/Software.tsx b/frontend/pages/DashboardPage/cards/Software/Software.tsx
index 41a9e9cd5b..734b3850d3 100644
--- a/frontend/pages/DashboardPage/cards/Software/Software.tsx
+++ b/frontend/pages/DashboardPage/cards/Software/Software.tsx
@@ -11,7 +11,7 @@ import TabsWrapper from "components/TabsWrapper";
import TableContainer from "components/TableContainer";
import TableDataError from "components/DataError";
import Spinner from "components/Spinner";
-import EmptySoftwareTable from "pages/software/components/EmptySoftwareTable";
+import EmptySoftwareTable from "pages/SoftwarePage/components/EmptySoftwareTable";
import generateTableHeaders from "./SoftwareTableConfig";
diff --git a/frontend/pages/SoftwarePage/SoftwarePage.tsx b/frontend/pages/SoftwarePage/SoftwarePage.tsx
new file mode 100644
index 0000000000..1113a58c45
--- /dev/null
+++ b/frontend/pages/SoftwarePage/SoftwarePage.tsx
@@ -0,0 +1,380 @@
+import React, { useCallback, useContext, useState } from "react";
+import { InjectedRouter } from "react-router";
+import { useQuery } from "react-query";
+import { Tab, TabList, Tabs } from "react-tabs";
+
+import PATHS from "router/paths";
+import {
+ IConfig,
+ CONFIG_DEFAULT_RECENT_VULNERABILITY_MAX_AGE_IN_DAYS,
+} from "interfaces/config";
+import {
+ IJiraIntegration,
+ IZendeskIntegration,
+ IIntegrations,
+} from "interfaces/integration";
+import { ITeamConfig } from "interfaces/team";
+import { IWebhookSoftwareVulnerabilities } from "interfaces/webhook";
+import configAPI from "services/entities/config";
+import teamsAPI, { ILoadTeamResponse } from "services/entities/teams";
+import { AppContext } from "context/app";
+import { NotificationContext } from "context/notification";
+import useTeamIdParam from "hooks/useTeamIdParam";
+
+import Button from "components/buttons/Button";
+import MainContent from "components/MainContent";
+import TeamsDropdown from "components/TeamsDropdown";
+import TabsWrapper from "components/TabsWrapper";
+
+import ManageAutomationsModal from "./components/ManageAutomationsModal";
+
+interface ISoftwareSubNavItem {
+ name: string;
+ pathname: string;
+}
+
+const softwareSubNav: ISoftwareSubNavItem[] = [
+ {
+ name: "Software",
+ pathname: PATHS.SOFTWARE_TITLES,
+ },
+ {
+ name: "Versions",
+ pathname: PATHS.SOFTWARE_VERSIONS,
+ },
+];
+
+const getTabIndex = (path: string): number => {
+ return softwareSubNav.findIndex((navItem) => {
+ // tab stays highlighted for paths that start with same pathname
+ return path.startsWith(navItem.pathname);
+ });
+};
+
+// default values for query params used on this page if not provided
+const DEFAULT_SORT_DIRECTION = "desc";
+const DEFAULT_SORT_HEADER = "hosts_count";
+const DEFAULT_PAGE_SIZE = 20;
+const DEFAULT_PAGE = 0;
+
+const baseClass = "software-page";
+
+interface ISoftwareAutomations {
+ webhook_settings: {
+ vulnerabilities_webhook: IWebhookSoftwareVulnerabilities;
+ };
+ integrations: {
+ jira: IJiraIntegration[];
+ zendesk: IZendeskIntegration[];
+ };
+}
+
+interface ISoftwareConfigQueryKey {
+ scope: string;
+ teamId?: number;
+}
+
+interface ISoftwarePageProps {
+ children: JSX.Element;
+ location: {
+ pathname: string;
+ search: string;
+ query: {
+ team_id?: string;
+ vulnerable?: string;
+ page?: string;
+ query?: string;
+ order_key?: string;
+ order_direction?: "asc" | "desc";
+ };
+ hash?: string;
+ };
+ router: InjectedRouter; // v3
+}
+
+const SoftwarePage = ({ children, router, location }: ISoftwarePageProps) => {
+ const {
+ config: globalConfig,
+ isFreeTier,
+ isGlobalAdmin,
+ isGlobalMaintainer,
+ isOnGlobalTeam,
+ isPremiumTier,
+ isSandboxMode,
+ } = useContext(AppContext);
+ const { renderFlash } = useContext(NotificationContext);
+
+ const queryParams = location.query;
+
+ // initial values for query params used on this page
+ const query = queryParams && queryParams.query ? queryParams.query : "";
+ const sortHeader =
+ queryParams && queryParams.order_key
+ ? queryParams.order_key
+ : DEFAULT_SORT_HEADER;
+ const sortDirection =
+ queryParams?.order_direction === undefined
+ ? DEFAULT_SORT_DIRECTION
+ : queryParams.order_direction;
+ const page =
+ queryParams && queryParams.page
+ ? parseInt(queryParams.page, 10)
+ : DEFAULT_PAGE;
+ const showVulnerableSoftware =
+ queryParams !== undefined && queryParams.vulnerable === "true";
+
+ const [showManageAutomationsModal, setShowManageAutomationsModal] = useState(
+ false
+ );
+ const [showPreviewPayloadModal, setShowPreviewPayloadModal] = useState(false);
+ const [showPreviewTicketModal, setShowPreviewTicketModal] = useState(false);
+
+ const {
+ currentTeamId,
+ isAnyTeamSelected,
+ isRouteOk,
+ teamIdForApi,
+ userTeams,
+ handleTeamChange,
+ } = useTeamIdParam({
+ location,
+ router,
+ includeAllTeams: true,
+ includeNoTeam: false,
+ });
+
+ // softwareConfig is either the global config or the team config of the
+ // currently selected team depending on the page team context selected
+ // by the user.
+ const {
+ data: softwareConfig,
+ error: softwareConfigError,
+ isFetching: isFetchingSoftwareConfig,
+ refetch: refetchSoftwareConfig,
+ } = useQuery<
+ IConfig | ILoadTeamResponse,
+ Error,
+ IConfig | ITeamConfig,
+ ISoftwareConfigQueryKey[]
+ >(
+ [{ scope: "softwareConfig", teamId: teamIdForApi }],
+ ({ queryKey }) => {
+ const { teamId } = queryKey[0];
+ return teamId ? teamsAPI.load(teamId) : configAPI.loadAll();
+ },
+ {
+ enabled: isRouteOk,
+ select: (data) => ("team" in data ? data.team : data),
+ }
+ );
+
+ // TODO: move into manage automations modal
+ const vulnWebhookSettings =
+ softwareConfig?.webhook_settings?.vulnerabilities_webhook;
+ const isVulnWebhookEnabled = !!vulnWebhookSettings?.enable_vulnerabilities_webhook;
+ const isVulnIntegrationEnabled = (integrations?: IIntegrations) => {
+ return (
+ !!integrations?.jira?.some((j) => j.enable_software_vulnerabilities) ||
+ !!integrations?.zendesk?.some((z) => z.enable_software_vulnerabilities)
+ );
+ };
+
+ // TODO: move into manage automations modal
+ const isAnyVulnAutomationEnabled =
+ isVulnWebhookEnabled ||
+ isVulnIntegrationEnabled(softwareConfig?.integrations);
+
+ // TODO: move into manage automations modal
+ const recentVulnerabilityMaxAge = (() => {
+ let maxAgeInNanoseconds: number | undefined;
+ if (softwareConfig && "vulnerabilities" in softwareConfig) {
+ maxAgeInNanoseconds =
+ softwareConfig.vulnerabilities.recent_vulnerability_max_age;
+ } else {
+ maxAgeInNanoseconds =
+ globalConfig?.vulnerabilities.recent_vulnerability_max_age;
+ }
+ return maxAgeInNanoseconds
+ ? Math.round(maxAgeInNanoseconds / 86400000000000) // convert from nanoseconds to days
+ : CONFIG_DEFAULT_RECENT_VULNERABILITY_MAX_AGE_IN_DAYS;
+ })();
+
+ const isSoftwareConfigLoaded =
+ !isFetchingSoftwareConfig && !softwareConfigError && !!softwareConfig;
+
+ const canManageAutomations =
+ isGlobalAdmin && (!isPremiumTier || !isAnyTeamSelected);
+
+ const toggleManageAutomationsModal = useCallback(() => {
+ setShowManageAutomationsModal(!showManageAutomationsModal);
+ }, [setShowManageAutomationsModal, showManageAutomationsModal]);
+
+ const togglePreviewPayloadModal = useCallback(() => {
+ setShowPreviewPayloadModal(!showPreviewPayloadModal);
+ }, [setShowPreviewPayloadModal, showPreviewPayloadModal]);
+
+ const togglePreviewTicketModal = useCallback(() => {
+ setShowPreviewTicketModal(!showPreviewTicketModal);
+ }, [setShowPreviewTicketModal, showPreviewTicketModal]);
+
+ // TODO: move into manage automations modal
+ const onCreateWebhookSubmit = async (
+ configSoftwareAutomations: ISoftwareAutomations
+ ) => {
+ try {
+ const request = configAPI.update(configSoftwareAutomations);
+ await request.then(() => {
+ renderFlash(
+ "success",
+ "Successfully updated vulnerability automations."
+ );
+ refetchSoftwareConfig();
+ });
+ } catch {
+ renderFlash(
+ "error",
+ "Could not update vulnerability automations. Please try again."
+ );
+ } finally {
+ toggleManageAutomationsModal();
+ }
+ };
+
+ const onTeamChange = useCallback(
+ (teamId: number) => {
+ handleTeamChange(teamId);
+ // TODO: reset page to 0 when changing teams
+ },
+ [handleTeamChange]
+ );
+
+ const navigateToNav = useCallback(
+ (i: number): void => {
+ const navPath = softwareSubNav[i].pathname;
+ router.replace(
+ navPath.concat(location?.search || "").concat(location?.hash || "")
+ );
+ },
+ [location, router]
+ );
+
+ const renderTitle = () => {
+ return (
+ <>
+ {isFreeTier && Software }
+ {isPremiumTier &&
+ userTeams &&
+ (userTeams.length > 1 || isOnGlobalTeam) && (
+
+ )}
+ {isPremiumTier &&
+ !isOnGlobalTeam &&
+ userTeams &&
+ userTeams.length === 1 && {userTeams[0].name} }
+ >
+ );
+ };
+
+ const renderHeaderDescription = () => {
+ return (
+
+ Search for installed software{" "}
+ {(isGlobalAdmin || isGlobalMaintainer) &&
+ (!isPremiumTier || !isAnyTeamSelected) &&
+ "and manage automations for detected vulnerabilities (CVEs)"}{" "}
+ on{" "}
+
+ {isPremiumTier && isAnyTeamSelected
+ ? "all hosts assigned to this team"
+ : "all of your hosts"}
+
+ .
+
+ );
+ };
+
+ const renderBody = () => {
+ return (
+
+
+
+
+ {softwareSubNav.map((navItem) => {
+ return (
+
+ {navItem.name}
+
+ );
+ })}
+
+
+
+ {React.cloneElement(children, {
+ router,
+ isSoftwareEnabled: Boolean(
+ softwareConfig?.features?.enable_software_inventory
+ ),
+ query,
+ // NOTE: may move this lower in tree if we need different values for different pages
+ perPage: DEFAULT_PAGE_SIZE,
+ orderDirection: sortDirection,
+ orderKey: sortHeader,
+ showVulnerableSoftware,
+ currentPage: page,
+ teamId: teamIdForApi,
+ })}
+
+ );
+ };
+
+ return (
+
+
+
+
+ {canManageAutomations && isSoftwareConfigLoaded && (
+
+ Manage automations
+
+ )}
+
+
+ {renderHeaderDescription()}
+
+ {renderBody()}
+ {showManageAutomationsModal && (
+
+ )}
+
+
+ );
+};
+
+export default SoftwarePage;
diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsPage.tsx b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsPage.tsx
new file mode 100644
index 0000000000..26eaffecfa
--- /dev/null
+++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsPage.tsx
@@ -0,0 +1,82 @@
+import React, { useContext } from "react";
+import { RouteComponentProps } from "react-router";
+import { useQuery } from "react-query";
+
+import { AppContext } from "context/app";
+import { ISoftwareTitle } from "interfaces/software";
+import softwareAPI, {
+ ISoftwareTitleResponse,
+} from "services/entities/software";
+
+import MainContent from "components/MainContent";
+import TableDataError from "components/DataError";
+
+import SoftwareDetailsSummary from "../components/SoftwareDetailsSummary";
+import SoftwareTitleDetailsTable from "./SoftwareTitleDetailsTable";
+
+const baseClass = "software-title-details-page";
+
+interface ISoftwareTitleDetailsRouteParams {
+ id: string;
+}
+
+type ISoftwareTitleDetailsPageProps = RouteComponentProps<
+ undefined,
+ ISoftwareTitleDetailsRouteParams
+>;
+
+const SoftwareTitleDetailsPage = ({
+ router,
+ routeParams,
+}: ISoftwareTitleDetailsPageProps) => {
+ // TODO: handle non integer values
+ const softwareId = parseInt(routeParams.id, 10);
+
+ const {
+ data: softwareTitle,
+ isLoading: isSoftwareTitleLoading,
+ isError: isSoftwareTitleError,
+ } = useQuery(
+ ["softwareById", softwareId],
+ () => softwareAPI.getSoftwareTitle(softwareId),
+ {
+ select: (data) => data.software_title,
+ }
+ );
+
+ if (!softwareTitle) {
+ return null;
+ }
+
+ return (
+
+ {isSoftwareTitleError ? (
+
+ ) : (
+ <>
+
+ {/* TODO: can we use Card here for card styles */}
+
+
Versions
+
+
+ >
+ )}
+
+ );
+};
+
+export default SoftwareTitleDetailsPage;
diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsTable/SoftwareTitleDetailsTable.tsx b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsTable/SoftwareTitleDetailsTable.tsx
new file mode 100644
index 0000000000..e42d6decfc
--- /dev/null
+++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsTable/SoftwareTitleDetailsTable.tsx
@@ -0,0 +1,70 @@
+import React, { useMemo } from "react";
+import { InjectedRouter } from "react-router";
+
+import { ISoftwareTitleVersion } from "interfaces/software";
+import { GITHUB_NEW_ISSUE_LINK } from "utilities/constants";
+
+import TableContainer from "components/TableContainer";
+import EmptyTable from "components/EmptyTable";
+import CustomLink from "components/CustomLink";
+
+import generateSoftwareTitleDetailsTableConfig from "./SoftwareTitleDetailsTableConfig";
+
+const DEFAULT_SORT_HEADER = "hosts_count";
+const DEFAULT_SORT_DIRECTION = "desc";
+
+const baseClass = "software-title-details-table";
+
+const NoVersionsDetected = (): JSX.Element => {
+ return (
+
+ Expecting to see versions?{" "}
+
+ >
+ }
+ />
+ );
+};
+
+interface ISoftwareTitleDetailsTableProps {
+ router: InjectedRouter;
+ data: ISoftwareTitleVersion[];
+ isLoading: boolean;
+}
+
+const SoftwareTitleDetailsTable = ({
+ router,
+ data,
+ isLoading,
+}: ISoftwareTitleDetailsTableProps) => {
+ const softwareTableHeaders = useMemo(
+ () => generateSoftwareTitleDetailsTableConfig(router),
+ [router]
+ );
+
+ return (
+
+ );
+};
+
+export default SoftwareTitleDetailsTable;
diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsTable/SoftwareTitleDetailsTableConfig.tsx b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsTable/SoftwareTitleDetailsTableConfig.tsx
new file mode 100644
index 0000000000..6e3e9c80bf
--- /dev/null
+++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsTable/SoftwareTitleDetailsTableConfig.tsx
@@ -0,0 +1,111 @@
+import React from "react";
+import { InjectedRouter } from "react-router";
+
+import {
+ ISoftwareTitleVersion,
+ ISoftwareVulnerability,
+} from "interfaces/software";
+import PATHS from "router/paths";
+
+import TextCell from "components/TableContainer/DataTable/TextCell";
+import ViewAllHostsLink from "components/ViewAllHostsLink";
+import LinkCell from "components/TableContainer/DataTable/LinkCell";
+
+import VulnerabilitiesCell from "../../components/VulnerabilitiesCell";
+
+interface ICellProps {
+ cell: {
+ value: number | string | ISoftwareVulnerability[];
+ };
+ row: {
+ original: ISoftwareTitleVersion;
+ };
+}
+
+interface IVersionCellProps extends ICellProps {
+ cell: {
+ value: string;
+ };
+}
+
+interface INumberCellProps extends ICellProps {
+ cell: {
+ value: number;
+ };
+}
+
+interface IVulnCellProps extends ICellProps {
+ cell: {
+ value: ISoftwareVulnerability[];
+ };
+}
+
+const generateSoftwareTitleDetailsTableConfig = (router: InjectedRouter) => {
+ const tableHeaders = [
+ {
+ title: "Version",
+ Header: "Version",
+ disableSortBy: true,
+ accessor: "version",
+ Cell: (cellProps: IVersionCellProps): JSX.Element => {
+ const { id } = cellProps.row.original;
+ const onClickSoftware = (e: React.MouseEvent) => {
+ // Allows for button to be clickable in a clickable row
+ e.stopPropagation();
+ router?.push(PATHS.SOFTWARE_VERSION_DETAILS(id.toString()));
+ };
+
+ // TODO: make only text clickable
+ return (
+
+ );
+ },
+ },
+ {
+ title: "Vulnerabilities",
+ Header: "Vulnerabilities",
+ disableSortBy: true,
+ // the "vulnerabilities" accessor is used but the data is actually coming
+ // from the version attribute. We do this as we already have a "versions"
+ // attribute used for the "Version" column and we cannot reuse. This is a
+ // limitation of react-table.
+ // With the versions data, we can sum up the vulnerabilities to get the
+ // total number of vulnerabilities for the software title
+ accessor: "vulnerabilities",
+ Cell: (cellProps: IVulnCellProps): JSX.Element => (
+
+ // TODO: tooltip
+ ),
+ },
+ {
+ title: "Hosts",
+ Header: "Hosts",
+ disableSortBy: true,
+ accessor: "hosts_count",
+ Cell: (cellProps: INumberCellProps): JSX.Element => (
+
+
+
+
+
+
+
+
+ ),
+ },
+ ];
+
+ return tableHeaders;
+};
+
+export default generateSoftwareTitleDetailsTableConfig;
diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsTable/_styles.scss b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsTable/_styles.scss
new file mode 100644
index 0000000000..4226d6790f
--- /dev/null
+++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsTable/_styles.scss
@@ -0,0 +1,13 @@
+.software-title-details-table {
+
+ .hosts-cell__wrapper {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+
+ .hosts-cell__link {
+ display: flex;
+ white-space: nowrap;
+ }
+ }
+}
diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsTable/index.ts b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsTable/index.ts
new file mode 100644
index 0000000000..2e2c71c6e0
--- /dev/null
+++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsTable/index.ts
@@ -0,0 +1 @@
+export { default } from "./SoftwareTitleDetailsTable";
diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/_styles.scss b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/_styles.scss
new file mode 100644
index 0000000000..4a0f501b35
--- /dev/null
+++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/_styles.scss
@@ -0,0 +1,33 @@
+.software-title-details-page {
+ background-color: $ui-off-white;
+ display: flex;
+ flex-direction: column;
+ gap: $pad-medium;
+
+ &__versions-section {
+ background-color: $core-white;
+ padding: $pad-xxlarge;
+ border: 1px solid $ui-fleet-black-10;
+ border-radius: $border-radius-xxlarge;
+ box-shadow: $box-shadow;
+
+ h2 {
+ margin: 0;
+ font-size: $medium;
+ }
+ }
+
+ // for showing and hiding software link on hover
+ tr {
+ .software-link {
+ opacity: 0;
+ transition: opacity 250ms;
+ }
+
+ &:hover {
+ .software-link {
+ opacity: 1;
+ }
+ }
+ }
+}
diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/index.ts b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/index.ts
new file mode 100644
index 0000000000..a071356328
--- /dev/null
+++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/index.ts
@@ -0,0 +1 @@
+export { default } from "./SoftwareTitleDetailsPage";
diff --git a/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTitles.tsx b/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTitles.tsx
new file mode 100644
index 0000000000..df84248aa7
--- /dev/null
+++ b/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTitles.tsx
@@ -0,0 +1,303 @@
+import React, { useCallback, useContext, useMemo } from "react";
+import { InjectedRouter } from "react-router";
+import { useQuery } from "react-query";
+import { Row } from "react-table";
+
+import PATHS from "router/paths";
+import softwareAPI, {
+ ISoftwareApiParams,
+ ISoftwareTitlesResponse,
+} from "services/entities/software";
+import { AppContext } from "context/app";
+import {
+ GITHUB_NEW_ISSUE_LINK,
+ VULNERABLE_DROPDOWN_OPTIONS,
+} from "utilities/constants";
+import { getNextLocationPath } from "utilities/helpers";
+import { buildQueryStringFromParams } from "utilities/url";
+
+// @ts-ignore
+import Dropdown from "components/forms/fields/Dropdown";
+import TableDataError from "components/DataError";
+import TableContainer from "components/TableContainer";
+import CustomLink from "components/CustomLink";
+import LastUpdatedText from "components/LastUpdatedText";
+import { ITableQueryData } from "components/TableContainer/TableContainer";
+
+import EmptySoftwareTable from "../components/EmptySoftwareTable";
+
+import generateSoftwareTitlesTableHeaders from "./SoftwareTitlesTableConfig";
+
+const baseClass = "software-titles";
+
+interface IRowProps extends Row {
+ original: {
+ id?: number;
+ };
+}
+
+interface ISoftwareTitlesQueryKey extends ISoftwareApiParams {
+ scope: "software-titles";
+}
+
+interface ISoftwareTitlesProps {
+ router: InjectedRouter;
+ isSoftwareEnabled: boolean;
+ query: string;
+ perPage: number;
+ orderDirection: "asc" | "desc";
+ orderKey: string;
+ showVulnerableSoftware: boolean;
+ currentPage: number;
+ teamId?: number;
+}
+
+const SoftwareTitles = ({
+ router,
+ isSoftwareEnabled,
+ query,
+ perPage,
+ orderDirection,
+ orderKey,
+ showVulnerableSoftware,
+ currentPage,
+ teamId,
+}: ISoftwareTitlesProps) => {
+ const { isSandboxMode, noSandboxHosts } = useContext(AppContext);
+
+ // request to get software data
+ const {
+ data: softwareData,
+ isLoading: isSoftwareLoading,
+ isError: isSoftwareError,
+ } = useQuery<
+ ISoftwareTitlesResponse,
+ Error,
+ ISoftwareTitlesResponse,
+ ISoftwareTitlesQueryKey[]
+ >(
+ [
+ {
+ scope: "software-titles",
+ page: currentPage,
+ perPage,
+ query,
+ orderDirection,
+ orderKey,
+ teamId,
+ vulnerable: showVulnerableSoftware,
+ },
+ ],
+ ({ queryKey }) => softwareAPI.getSoftwareTitles(queryKey[0]),
+ {
+ // stale time can be adjusted if fresher data is desired based on
+ // software inventory interval
+ staleTime: 30000,
+ }
+ );
+
+ // determines if a user be able to search in the table
+ const searchable =
+ isSoftwareEnabled &&
+ (!!softwareData?.software_titles || query !== "" || showVulnerableSoftware);
+
+ const softwareTableHeaders = useMemo(
+ () => generateSoftwareTitlesTableHeaders(router, teamId),
+ [router, teamId]
+ );
+
+ const handleVulnFilterDropdownChange = (isFilterVulnerable: string) => {
+ router.replace(
+ getNextLocationPath({
+ pathPrefix: PATHS.SOFTWARE_TITLES,
+ routeTemplate: "",
+ queryParams: {
+ query,
+ teamId,
+ orderDirection,
+ orderKey,
+ vulnerable: isFilterVulnerable,
+ page: 0, // resets page index
+ },
+ })
+ );
+ };
+
+ const handleRowSelect = (row: IRowProps) => {
+ const hostsBySoftwareParams = {
+ software_title_id: row.original.id,
+ team_id: teamId,
+ };
+
+ const path = `${PATHS.MANAGE_HOSTS}?${buildQueryStringFromParams(
+ hostsBySoftwareParams
+ )}`;
+
+ router.push(path);
+ };
+
+ const determineQueryParamChange = useCallback(
+ (newTableQuery: ITableQueryData) => {
+ const changedEntry = Object.entries(newTableQuery).find(([key, val]) => {
+ switch (key) {
+ case "searchQuery":
+ return val !== query;
+ case "sortDirection":
+ return val !== orderDirection;
+ case "sortHeader":
+ return val !== orderKey;
+ case "vulnerable":
+ return val !== showVulnerableSoftware.toString();
+ case "pageIndex":
+ return val !== currentPage;
+ default:
+ return false;
+ }
+ });
+ return changedEntry?.[0] ?? "";
+ },
+ [currentPage, orderDirection, orderKey, query, showVulnerableSoftware]
+ );
+
+ const generateNewQueryParams = useCallback(
+ (newTableQuery: ITableQueryData, changedParam: string) => {
+ return {
+ query: newTableQuery.searchQuery,
+ team_id: teamId,
+ order_direction: newTableQuery.sortDirection,
+ order_key: newTableQuery.sortHeader,
+ vulnerable: showVulnerableSoftware.toString(),
+ page: changedParam === "pageIndex" ? newTableQuery.pageIndex : 0,
+ };
+ },
+ [showVulnerableSoftware, teamId]
+ );
+
+ // NOTE: this is called once on initial render and every time the query changes
+ const onQueryChange = useCallback(
+ (newTableQuery: ITableQueryData) => {
+ // we want to determine which query param has changed in order to
+ // reset the page index to 0 if any other param has changed.
+ const changedParam = determineQueryParamChange(newTableQuery);
+
+ // if nothing has changed, don't update the route. this can happen when
+ // this handler is called on the inital render.
+ if (changedParam === "") return;
+
+ const newRoute = getNextLocationPath({
+ pathPrefix: PATHS.SOFTWARE_TITLES,
+ routeTemplate: "",
+ queryParams: generateNewQueryParams(newTableQuery, changedParam),
+ });
+
+ router.replace(newRoute);
+ },
+ [determineQueryParamChange, generateNewQueryParams, router]
+ );
+
+ const getItemsCountText = () => {
+ const count = softwareData?.count;
+ if (!softwareData || !count) return "";
+
+ return count === 1 ? `${count} item` : `${count} items`;
+ };
+
+ const getLastUpdatedText = () => {
+ if (!softwareData || !softwareData.counts_updated_at) return "";
+ return (
+
+ );
+ };
+
+ const renderSoftwareCount = () => {
+ const itemText = getItemsCountText();
+ const lastUpdatedText = getLastUpdatedText();
+
+ if (!itemText) return null;
+
+ return (
+
+ {itemText}
+ {lastUpdatedText}
+
+ );
+ };
+
+ const renderVulnFilterDropdown = () => {
+ return (
+
+ );
+ };
+
+ const renderTableFooter = () => {
+ return (
+
+ Seeing unexpected software or vulnerabilities?{" "}
+
+
+ );
+ };
+
+ if (isSoftwareError) {
+ return ;
+ }
+
+ return (
+
+
(
+
+ )}
+ defaultSortHeader={orderKey}
+ defaultSortDirection={orderDirection}
+ defaultPageIndex={currentPage}
+ defaultSearchQuery={query}
+ manualSortBy
+ pageSize={perPage}
+ showMarkAllPages={false}
+ isAllPagesSelected={false}
+ disableNextPage={!softwareData?.meta.has_next_results}
+ searchable={searchable}
+ inputPlaceHolder="Search by name or vulnerabilities (CVEs)"
+ onQueryChange={onQueryChange}
+ // additionalQueries serves as a trigger for the useDeepEffect hook
+ // to fire onQueryChange for events happeing outside of
+ // the TableContainer.
+ additionalQueries={showVulnerableSoftware ? "vulnerable" : ""}
+ customControl={searchable ? renderVulnFilterDropdown : undefined}
+ stackControls
+ renderCount={renderSoftwareCount}
+ renderFooter={renderTableFooter}
+ disableMultiRowSelect
+ onSelectSingleRow={handleRowSelect}
+ />
+
+ );
+};
+
+export default SoftwareTitles;
diff --git a/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTitlesTableConfig.tsx b/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTitlesTableConfig.tsx
new file mode 100644
index 0000000000..3e48b76a55
--- /dev/null
+++ b/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTitlesTableConfig.tsx
@@ -0,0 +1,185 @@
+import React from "react";
+import { Column } from "react-table";
+import { InjectedRouter } from "react-router";
+
+import {
+ ISoftwareTitleVersion,
+ ISoftwareTitle,
+ formatSoftwareType,
+} from "interfaces/software";
+import PATHS from "router/paths";
+
+import HeaderCell from "components/TableContainer/DataTable/HeaderCell";
+import TextCell from "components/TableContainer/DataTable/TextCell";
+import LinkCell from "components/TableContainer/DataTable/LinkCell/LinkCell";
+import ViewAllHostsLink from "components/ViewAllHostsLink";
+import VersionCell from "../components/VersionCell";
+import VulnerabilitiesCell from "../components/VulnerabilitiesCell";
+import SoftwareIcon from "../components/icons/SoftwareIcon";
+
+// NOTE: cellProps come from react-table
+// more info here https://react-table.tanstack.com/docs/api/useTable#cell-properties
+interface ICellProps {
+ cell: {
+ value: number | string | ISoftwareTitleVersion[];
+ };
+ row: {
+ original: ISoftwareTitle;
+ };
+}
+interface IStringCellProps extends ICellProps {
+ cell: {
+ value: string;
+ };
+}
+
+interface IVersionCellProps extends ICellProps {
+ cell: {
+ value: ISoftwareTitleVersion[];
+ };
+ row: {
+ original: ISoftwareTitle;
+ };
+}
+
+interface INumberCellProps extends ICellProps {
+ cell: {
+ value: number;
+ };
+}
+
+interface IVulnCellProps extends ICellProps {
+ cell: {
+ value: ISoftwareTitleVersion[];
+ };
+}
+interface IHeaderProps {
+ column: {
+ title: string;
+ isSortedDesc: boolean;
+ };
+}
+
+const getVulnerabilities = (versions: ISoftwareTitleVersion[]) => {
+ const vulnerabilities = versions.reduce((acc: string[], currentVersion) => {
+ if (
+ currentVersion.vulnerabilities &&
+ currentVersion.vulnerabilities.length !== 0
+ ) {
+ acc.push(...currentVersion.vulnerabilities);
+ }
+ return acc;
+ }, []);
+ return vulnerabilities;
+};
+
+const generateTableHeaders = (
+ router: InjectedRouter,
+ teamId?: number
+): Column[] => {
+ const softwareTableHeaders = [
+ {
+ title: "Name",
+ Header: (cellProps: IHeaderProps): JSX.Element => (
+
+ ),
+ disableSortBy: false,
+ accessor: "name",
+ Cell: (cellProps: IStringCellProps): JSX.Element => {
+ const { id, name, source } = cellProps.row.original;
+
+ const onClickSoftware = (e: React.MouseEvent) => {
+ // Allows for button to be clickable in a clickable row
+ e.stopPropagation();
+
+ router?.push(PATHS.SOFTWARE_TITLE_DETAILS(id.toString()));
+ };
+
+ return (
+
+
+ {name}
+ >
+ }
+ />
+ );
+ },
+ sortType: "caseInsensitive",
+ },
+ {
+ title: "Version",
+ Header: "Version",
+ disableSortBy: true,
+ accessor: "versions",
+ Cell: (cellProps: IVersionCellProps): JSX.Element => (
+
+ ),
+ },
+ {
+ title: "Type",
+ Header: "Type",
+ disableSortBy: true,
+ accessor: "source",
+ Cell: (cellProps: IStringCellProps): JSX.Element => (
+
+ ),
+ },
+ // the "vulnerabilities" accessor is used but the data is actually coming
+ // from the version attribute. We do this as we already have a "versions"
+ // attribute used for the "Version" column and we cannot reuse. This is a
+ // limitation of react-table.
+ // With the versions data, we can sum up the vulnerabilities to get the
+ // total number of vulnerabilities for the software title
+ {
+ title: "Vulnerabilities",
+ Header: "Vulnerabilities",
+ disableSortBy: true,
+ accessor: "vulnerabilities",
+ Cell: (cellProps: IVulnCellProps): JSX.Element => {
+ const vulnerabilities = getVulnerabilities(
+ cellProps.row.original.versions
+ );
+ return ;
+ },
+ },
+ {
+ title: "Hosts",
+ Header: (cellProps: IHeaderProps): JSX.Element => (
+
+ ),
+ disableSortBy: false,
+ accessor: "hosts_count",
+ Cell: (cellProps: INumberCellProps): JSX.Element => (
+
+
+
+
+
+
+
+
+ ),
+ },
+ ];
+
+ return softwareTableHeaders;
+};
+
+export default generateTableHeaders;
diff --git a/frontend/pages/SoftwarePage/SoftwareTitles/_styles.scss b/frontend/pages/SoftwarePage/SoftwareTitles/_styles.scss
new file mode 100644
index 0000000000..ee2569f458
--- /dev/null
+++ b/frontend/pages/SoftwarePage/SoftwareTitles/_styles.scss
@@ -0,0 +1,165 @@
+.software-titles {
+ margin-top: $pad-xxlarge;
+
+ &__count {
+ display: flex;
+ gap: 12px;
+ }
+
+ &__vuln_dropdown {
+ .Select-menu-outer {
+ width: 250px;
+ max-height: 310px;
+
+ .Select-menu {
+ max-height: none;
+ }
+ }
+
+ .Select-value {
+ padding-left: $pad-medium;
+ padding-right: $pad-medium;
+ }
+
+ .dropdown__custom-value-label {
+ width: 155px; // Override 105px for longer text options
+ }
+ }
+
+ .table-container {
+ &__header {
+ flex-direction: column-reverse; // Search bar on top
+ margin-bottom: $pad-medium;
+
+ @media (min-width: $break-md) {
+ flex-direction: row;
+ }
+ }
+
+ &__header-left {
+ flex-direction: row; // Filter dropdown aligned with count
+
+ .controls {
+ .form-field--dropdown {
+ margin: 0;
+ }
+ }
+ }
+
+ &__search-input,
+ &__search {
+ width: 100%; // Search bar across entire table
+
+ .input-icon-field__input {
+ width: 100%;
+ }
+
+ @media (min-width: $break-md) {
+ width: auto;
+
+ .input-icon-field__input {
+ width: 375px;
+ }
+ }
+ }
+
+ &__data-table-block {
+ .data-table-block {
+ .data-table__table {
+
+ // for showing and hiding "view all hosts" link on hover
+ tr {
+ .software-link {
+ opacity: 0;
+ transition: opacity 250ms;
+ }
+
+ &:hover {
+ .software-link {
+ opacity: 1;
+ }
+ }
+ }
+
+ thead {
+ .name__header {
+ width: $col-md;
+ }
+
+ .hosts_count__header {
+ width: auto;
+ border-right: 0;
+ }
+
+ @media (min-width: $break-lg) {
+ // expand the width of version header at larger screen sizes
+ .versions__header {
+ width: $col-md;
+ }
+ }
+ }
+
+ tbody {
+ .name__cell {
+ max-width: $col-md;
+
+ // Tooltip does not get cut off
+ .children-wrapper {
+ overflow: initial;
+ }
+
+ // ellipsis for software name
+ .software-name {
+ overflow: hidden;
+ text-wrap: nowrap;
+ text-overflow: ellipsis;
+ }
+ }
+
+ .link-cell {
+ display: flex;
+ align-items: center;
+ gap: $pad-small;
+ }
+
+ .hosts_count__cell {
+ .hosts-cell__wrapper {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+
+ .hosts-cell__link {
+ display: flex;
+ white-space: nowrap;
+ }
+ }
+ }
+
+ @media (min-width: $break-sm) {
+ .name__cell {
+ max-width: $col-lg;
+ }
+ }
+
+ @media (min-width: $break-lg) {
+ .versions__cell {
+ width: $col-md;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // needed to handle overflow of the table data on small screens
+ .data-table {
+ &__wrapper {
+ overflow-x: auto;
+ }
+ }
+
+ &__table-error {
+ margin-top: $pad-xxxlarge;
+ }
+}
diff --git a/frontend/pages/SoftwarePage/SoftwareTitles/index.ts b/frontend/pages/SoftwarePage/SoftwareTitles/index.ts
new file mode 100644
index 0000000000..36fd17a897
--- /dev/null
+++ b/frontend/pages/SoftwarePage/SoftwareTitles/index.ts
@@ -0,0 +1 @@
+export { default } from "./SoftwareTitles";
diff --git a/frontend/pages/SoftwarePage/SoftwareVersionDetailsPage/SoftwareVersionDetailsPage.tsx b/frontend/pages/SoftwarePage/SoftwareVersionDetailsPage/SoftwareVersionDetailsPage.tsx
new file mode 100644
index 0000000000..e8860641e3
--- /dev/null
+++ b/frontend/pages/SoftwarePage/SoftwareVersionDetailsPage/SoftwareVersionDetailsPage.tsx
@@ -0,0 +1,125 @@
+import React, { useContext, useMemo } from "react";
+import { useQuery } from "react-query";
+import { RouteComponentProps } from "react-router";
+
+import softwareAPI, {
+ ISoftwareVersionResponse,
+} from "services/entities/software";
+import { ISoftwareVersion } from "interfaces/software";
+import { GITHUB_NEW_ISSUE_LINK } from "utilities/constants";
+import { AppContext } from "context/app";
+
+import MainContent from "components/MainContent";
+import TableContainer from "components/TableContainer";
+import CustomLink from "components/CustomLink";
+import EmptyTable from "components/EmptyTable";
+import TableDataError from "components/DataError";
+
+import generateSoftwareVersionDetailsTableConfig from "./SoftwareVersionDetailsTableConfig";
+import SoftwareDetailsSummary from "../components/SoftwareDetailsSummary";
+
+const baseClass = "software-version-details-page";
+
+interface ISoftwareVersionDetailsRouteParams {
+ id: string;
+}
+
+type ISoftwareTitleDetailsPageProps = RouteComponentProps<
+ undefined,
+ ISoftwareVersionDetailsRouteParams
+>;
+
+const NoVulnsDetected = (): JSX.Element => {
+ return (
+
+ Expecting to see vulnerabilities?{" "}
+
+ >
+ }
+ />
+ );
+};
+
+const SoftwareVersionDetailsPage = ({
+ routeParams,
+}: ISoftwareTitleDetailsPageProps) => {
+ const versionId = parseInt(routeParams.id, 10);
+ const { isPremiumTier, isSandboxMode, filteredSoftwarePath } = useContext(
+ AppContext
+ );
+
+ const {
+ data: softwareVersion,
+ isLoading: isSoftwareVersionLoading,
+ isError: isSoftwareVersionError,
+ } = useQuery(
+ ["software-version", versionId],
+ () => softwareAPI.getSoftwareVersion(versionId),
+ {
+ select: (data) => data.software,
+ }
+ );
+
+ const tableHeaders = useMemo(
+ () =>
+ generateSoftwareVersionDetailsTableConfig(
+ Boolean(isPremiumTier),
+ Boolean(isSandboxMode)
+ ),
+ [isPremiumTier, isSandboxMode]
+ );
+
+ if (!softwareVersion) {
+ return null;
+ }
+
+ return (
+
+ {isSoftwareVersionError ? (
+
+ ) : (
+ <>
+
+
+
Vulnerabilities
+ {softwareVersion?.vulnerabilities?.length ? (
+
+ ) : (
+
+ )}
+
+ >
+ )}
+
+ );
+};
+export default SoftwareVersionDetailsPage;
diff --git a/frontend/pages/software/SoftwareDetailsPage/components/Vulnerabilities/VulnTableConfig.tsx b/frontend/pages/SoftwarePage/SoftwareVersionDetailsPage/SoftwareVersionDetailsTableConfig.tsx
similarity index 96%
rename from frontend/pages/software/SoftwareDetailsPage/components/Vulnerabilities/VulnTableConfig.tsx
rename to frontend/pages/SoftwarePage/SoftwareVersionDetailsPage/SoftwareVersionDetailsTableConfig.tsx
index 518b8f58b5..1d3b036694 100644
--- a/frontend/pages/software/SoftwareDetailsPage/components/Vulnerabilities/VulnTableConfig.tsx
+++ b/frontend/pages/SoftwarePage/SoftwareVersionDetailsPage/SoftwareVersionDetailsTableConfig.tsx
@@ -1,6 +1,5 @@
import React from "react";
-import { IVulnerability } from "interfaces/vulnerability";
import { formatFloatAsPercentage } from "utilities/helpers";
import { DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants";
@@ -10,6 +9,7 @@ import TooltipWrapper from "components/TooltipWrapper";
import CustomLink from "components/CustomLink";
import { HumanTimeDiffWithDateTip } from "components/HumanTimeDiffWithDateTip";
import PremiumFeatureIconWithTooltip from "components/PremiumFeatureIconWithTooltip";
+import { ISoftwareVulnerability } from "interfaces/software";
interface IHeaderProps {
column: {
@@ -22,7 +22,7 @@ interface ICellProps {
value: number | string | string[];
};
row: {
- original: IVulnerability;
+ original: ISoftwareVulnerability;
index: number;
};
}
@@ -62,7 +62,7 @@ const formatSeverity = (float: number | null) => {
return `${severity} (${float.toFixed(1)})`;
};
-const generateVulnTableHeaders = (
+const generateSoftwareVersionDetailsTableConfig = (
isPremiumTier: boolean,
isSandboxMode: boolean
): IDataColumn[] => {
@@ -106,11 +106,11 @@ const generateVulnTableHeaders = (
);
return (
<>
- {isSandboxMode && }
+ {isSandboxMode && }
>
);
},
@@ -140,11 +140,11 @@ const generateVulnTableHeaders = (
);
return (
<>
- {isSandboxMode && }
+ {isSandboxMode && }
>
);
},
@@ -173,11 +173,11 @@ const generateVulnTableHeaders = (
);
return (
<>
- {isSandboxMode && }
+ {isSandboxMode && }
>
);
},
@@ -205,11 +205,11 @@ const generateVulnTableHeaders = (
);
return (
<>
- {isSandboxMode && }
+ {isSandboxMode && }
>
);
},
@@ -228,4 +228,4 @@ const generateVulnTableHeaders = (
return isPremiumTier ? tableHeaders.concat(premiumHeaders) : tableHeaders;
};
-export default generateVulnTableHeaders;
+export default generateSoftwareVersionDetailsTableConfig;
diff --git a/frontend/pages/SoftwarePage/SoftwareVersionDetailsPage/_styles.scss b/frontend/pages/SoftwarePage/SoftwareVersionDetailsPage/_styles.scss
new file mode 100644
index 0000000000..55c572cad2
--- /dev/null
+++ b/frontend/pages/SoftwarePage/SoftwareVersionDetailsPage/_styles.scss
@@ -0,0 +1,25 @@
+.software-version-details-page {
+ background-color: $ui-off-white;
+ display: flex;
+ flex-direction: column;
+ gap: $pad-medium;
+
+ &__vulnerabilities-section {
+ background-color: $core-white;
+ padding: $pad-xxlarge;
+ border: 1px solid $ui-fleet-black-10;
+ border-radius: $border-radius-xxlarge;
+ box-shadow: $box-shadow;
+
+ h2 {
+ margin: 0;
+ font-size: $medium;
+ }
+ }
+
+ // used to position header text with premium icon correctly
+ .column-header {
+ display: flex;
+ gap: $pad-small;
+ }
+}
diff --git a/frontend/pages/SoftwarePage/SoftwareVersionDetailsPage/index.ts b/frontend/pages/SoftwarePage/SoftwareVersionDetailsPage/index.ts
new file mode 100644
index 0000000000..f2db109234
--- /dev/null
+++ b/frontend/pages/SoftwarePage/SoftwareVersionDetailsPage/index.ts
@@ -0,0 +1 @@
+export { default } from "./SoftwareVersionDetailsPage";
diff --git a/frontend/pages/SoftwarePage/SoftwareVersions/SoftwareVersions.tsx b/frontend/pages/SoftwarePage/SoftwareVersions/SoftwareVersions.tsx
new file mode 100644
index 0000000000..1d1aeb3e86
--- /dev/null
+++ b/frontend/pages/SoftwarePage/SoftwareVersions/SoftwareVersions.tsx
@@ -0,0 +1,318 @@
+import React, { useCallback, useContext, useMemo } from "react";
+import { InjectedRouter } from "react-router";
+import { useQuery } from "react-query";
+import { Row } from "react-table";
+
+import PATHS from "router/paths";
+import softwareAPI, {
+ ISoftwareApiParams,
+ ISoftwareVersionsResponse,
+} from "services/entities/software";
+import { AppContext } from "context/app";
+import {
+ GITHUB_NEW_ISSUE_LINK,
+ VULNERABLE_DROPDOWN_OPTIONS,
+} from "utilities/constants";
+import { getNextLocationPath } from "utilities/helpers";
+import { buildQueryStringFromParams } from "utilities/url";
+
+// @ts-ignore
+import Dropdown from "components/forms/fields/Dropdown";
+import TableDataError from "components/DataError";
+import TableContainer from "components/TableContainer";
+import CustomLink from "components/CustomLink";
+import LastUpdatedText from "components/LastUpdatedText";
+import { ITableQueryData } from "components/TableContainer/TableContainer";
+
+import EmptySoftwareTable from "../components/EmptySoftwareTable";
+
+import generateSoftwareVersionsTableHeaders from "./SoftwareVersionsTableConfig";
+
+const baseClass = "software-versions";
+
+interface IRowProps extends Row {
+ original: {
+ id?: number;
+ };
+}
+
+interface ISoftwareVersionsQueryKey extends ISoftwareApiParams {
+ scope: "software-versions";
+}
+
+interface ISoftwareVersionsProps {
+ router: InjectedRouter;
+ isSoftwareEnabled: boolean;
+ query: string;
+ perPage: number;
+ orderDirection: "asc" | "desc";
+ orderKey: string;
+ showVulnerableSoftware: boolean;
+ currentPage: number;
+ teamId?: number;
+}
+
+const SoftwareVersions = ({
+ router,
+ isSoftwareEnabled,
+ query,
+ perPage,
+ orderDirection,
+ orderKey,
+ showVulnerableSoftware,
+ currentPage,
+ teamId,
+}: ISoftwareVersionsProps) => {
+ const { isSandboxMode, noSandboxHosts, isPremiumTier } = useContext(
+ AppContext
+ );
+
+ // request to get software versions data
+ const {
+ data: softwareVersionsData,
+ isLoading: isSoftwareVersionsLoading,
+ isError: isSoftwareVersionsError,
+ } = useQuery<
+ ISoftwareVersionsResponse,
+ Error,
+ ISoftwareVersionsResponse,
+ ISoftwareVersionsQueryKey[]
+ >(
+ [
+ {
+ scope: "software-versions",
+ page: currentPage,
+ perPage,
+ query,
+ orderDirection,
+ orderKey,
+ teamId,
+ vulnerable: showVulnerableSoftware,
+ },
+ ],
+ ({ queryKey }) => softwareAPI.getSoftwareVersions(queryKey[0]),
+ {
+ keepPreviousData: true,
+ // stale time can be adjusted if fresher data is desired based on
+ // software inventory interval
+ staleTime: 30000,
+ }
+ );
+
+ // determines if a user be able to search in the table
+ const searchable =
+ isSoftwareEnabled &&
+ (!!softwareVersionsData?.software ||
+ query !== "" ||
+ showVulnerableSoftware);
+
+ const softwareTableHeaders = useMemo(
+ () =>
+ generateSoftwareVersionsTableHeaders(
+ router,
+ isPremiumTier,
+ isSandboxMode,
+ teamId
+ ),
+ [isPremiumTier, isSandboxMode, router, teamId]
+ );
+
+ // TODO: figure out why this is not working
+ const handleVulnFilterDropdownChange = (isFilterVulnerable: string) => {
+ router.replace(
+ getNextLocationPath({
+ pathPrefix: PATHS.SOFTWARE_VERSIONS,
+ routeTemplate: "",
+ queryParams: {
+ query,
+ teamId,
+ orderDirection,
+ orderKey,
+ vulnerable: isFilterVulnerable,
+ page: 0, // resets page index
+ },
+ })
+ );
+ };
+
+ const handleRowSelect = (row: IRowProps) => {
+ const hostsBySoftwareParams = {
+ software_version_id: row.original.id,
+ team_id: teamId,
+ };
+
+ const path = `${PATHS.MANAGE_HOSTS}?${buildQueryStringFromParams(
+ hostsBySoftwareParams
+ )}`;
+
+ router.push(path);
+ };
+
+ const determineQueryParamChange = useCallback(
+ (newTableQuery: ITableQueryData) => {
+ const changedEntry = Object.entries(newTableQuery).find(([key, val]) => {
+ switch (key) {
+ case "searchQuery":
+ return val !== query;
+ case "sortDirection":
+ return val !== orderDirection;
+ case "sortHeader":
+ return val !== orderKey;
+ case "vulnerable":
+ return val !== showVulnerableSoftware.toString();
+ case "pageIndex":
+ return val !== currentPage;
+ default:
+ return false;
+ }
+ });
+ return changedEntry?.[0] ?? "";
+ },
+ [currentPage, orderDirection, orderKey, query, showVulnerableSoftware]
+ );
+
+ const generateNewQueryParams = useCallback(
+ (newTableQuery: ITableQueryData) => {
+ return {
+ query: newTableQuery.searchQuery,
+ team_id: teamId,
+ order_direction: newTableQuery.sortDirection,
+ order_key: newTableQuery.sortHeader,
+ vulnerable: showVulnerableSoftware.toString(),
+ page: newTableQuery.pageIndex,
+ };
+ },
+ [showVulnerableSoftware, teamId]
+ );
+
+ // NOTE: this is called once on initial render and every time the query changes
+ const onQueryChange = useCallback(
+ (newTableQuery: ITableQueryData) => {
+ // we want to determine which query param has changed in order to
+ // reset the page index to 0 if any other param has changed.
+ const changedParam = determineQueryParamChange(newTableQuery);
+
+ // if nothing has changed, don't update the route. this can happen when
+ // this handler is called on the inital render.
+ if (changedParam === "") return;
+
+ const newRoute = getNextLocationPath({
+ pathPrefix: PATHS.SOFTWARE_VERSIONS,
+ routeTemplate: "",
+ queryParams: generateNewQueryParams(newTableQuery),
+ });
+
+ router.replace(newRoute);
+ },
+ [determineQueryParamChange, generateNewQueryParams, router]
+ );
+
+ const getItemsCountText = () => {
+ const count = softwareVersionsData?.count;
+ if (!softwareVersionsData || !count) return "";
+
+ return count === 1 ? `${count} item` : `${count} items`;
+ };
+
+ const getLastUpdatedText = () => {
+ if (!softwareVersionsData || !softwareVersionsData.counts_updated_at)
+ return "";
+ return (
+
+ );
+ };
+
+ const renderSoftwareCount = () => {
+ const itemText = getItemsCountText();
+ const lastUpdatedText = getLastUpdatedText();
+
+ if (!itemText) return null;
+
+ return (
+
+ {itemText}
+ {lastUpdatedText}
+
+ );
+ };
+
+ const renderVulnFilterDropdown = () => {
+ return (
+
+ );
+ };
+
+ const renderTableFooter = () => {
+ return (
+
+ Seeing unexpected software or vulnerabilities?{" "}
+
+
+ );
+ };
+
+ if (isSoftwareVersionsError) {
+ return ;
+ }
+
+ return (
+
+
+
(
+
+ )}
+ defaultSortHeader={orderKey}
+ defaultSortDirection={orderDirection}
+ defaultPageIndex={currentPage}
+ defaultSearchQuery={query}
+ manualSortBy
+ pageSize={perPage}
+ showMarkAllPages={false}
+ isAllPagesSelected={false}
+ disableNextPage={!softwareVersionsData?.meta.has_next_results}
+ searchable={searchable}
+ inputPlaceHolder="Search by name or vulnerabilities (CVEs)"
+ onQueryChange={onQueryChange}
+ // additionalQueries serves as a trigger for the useDeepEffect hook
+ // to fire onQueryChange for events happeing outside of
+ // the TableContainer.
+ additionalQueries={showVulnerableSoftware ? "vulnerable" : ""}
+ customControl={searchable ? renderVulnFilterDropdown : undefined}
+ stackControls
+ renderCount={renderSoftwareCount}
+ renderFooter={renderTableFooter}
+ disableMultiRowSelect
+ onSelectSingleRow={handleRowSelect}
+ />
+
+
+ );
+};
+
+export default SoftwareVersions;
diff --git a/frontend/pages/SoftwarePage/SoftwareVersions/SoftwareVersionsTableConfig.tsx b/frontend/pages/SoftwarePage/SoftwareVersions/SoftwareVersionsTableConfig.tsx
new file mode 100644
index 0000000000..28ef2b9bf0
--- /dev/null
+++ b/frontend/pages/SoftwarePage/SoftwareVersions/SoftwareVersionsTableConfig.tsx
@@ -0,0 +1,160 @@
+import React from "react";
+import { Column } from "react-table";
+import { InjectedRouter } from "react-router";
+
+import {
+ formatSoftwareType,
+ ISoftwareVersion,
+ ISoftwareVulnerability,
+} from "interfaces/software";
+import PATHS from "router/paths";
+
+import HeaderCell from "components/TableContainer/DataTable/HeaderCell";
+import TextCell from "components/TableContainer/DataTable/TextCell";
+import LinkCell from "components/TableContainer/DataTable/LinkCell/LinkCell";
+import ViewAllHostsLink from "components/ViewAllHostsLink";
+import VulnerabilitiesCell from "../components/VulnerabilitiesCell";
+import SoftwareIcon from "../components/icons/SoftwareIcon";
+
+// NOTE: cellProps come from react-table
+// more info here https://react-table.tanstack.com/docs/api/useTable#cell-properties
+interface ICellProps {
+ cell: {
+ value: number | string | ISoftwareVulnerability[];
+ };
+ row: {
+ original: ISoftwareVersion;
+ };
+}
+interface IStringCellProps extends ICellProps {
+ cell: {
+ value: string;
+ };
+}
+
+interface IVersionCellProps extends ICellProps {
+ cell: {
+ value: string;
+ };
+}
+
+interface INumberCellProps extends ICellProps {
+ cell: {
+ value: number;
+ };
+}
+
+interface IVulnCellProps extends ICellProps {
+ cell: {
+ value: ISoftwareVulnerability[];
+ };
+}
+interface IHeaderProps {
+ column: {
+ title: string;
+ isSortedDesc: boolean;
+ };
+}
+
+const generateTableHeaders = (
+ router: InjectedRouter,
+ isPremiumTier?: boolean,
+ isSandboxMode?: boolean,
+ teamId?: number
+): Column[] => {
+ const softwareTableHeaders = [
+ {
+ title: "Name",
+ Header: (cellProps: IHeaderProps): JSX.Element => (
+
+ ),
+ disableSortBy: false,
+ accessor: "name",
+ Cell: (cellProps: IStringCellProps): JSX.Element => {
+ const { id, name, source } = cellProps.row.original;
+
+ const onClickSoftware = (e: React.MouseEvent) => {
+ // Allows for button to be clickable in a clickable row
+ e.stopPropagation();
+ router?.push(PATHS.SOFTWARE_VERSION_DETAILS(id.toString()));
+ };
+
+ return (
+
+
+ {name}
+ >
+ }
+ />
+ );
+ },
+ sortType: "caseInsensitive",
+ },
+ {
+ title: "Version",
+ Header: "Version",
+ disableSortBy: true,
+ accessor: "version",
+ Cell: (cellProps: IVersionCellProps): JSX.Element => (
+
+ ),
+ },
+ {
+ title: "Type",
+ Header: "Type",
+ disableSortBy: true,
+ accessor: "source",
+ Cell: (cellProps: IStringCellProps): JSX.Element => (
+
+ ),
+ },
+ {
+ title: "Vulnerabilities",
+ Header: "Vulnerabilities",
+ disableSortBy: true,
+ accessor: "vulnerabilities",
+ Cell: (cellProps: IVulnCellProps): JSX.Element => (
+
+ ),
+ },
+ {
+ title: "Hosts",
+ Header: (cellProps: IHeaderProps): JSX.Element => (
+
+ ),
+ disableSortBy: false,
+ accessor: "hosts_count",
+ Cell: (cellProps: INumberCellProps): JSX.Element => (
+
+
+
+
+
+
+
+
+ ),
+ },
+ ];
+
+ return softwareTableHeaders;
+};
+
+export default generateTableHeaders;
diff --git a/frontend/pages/SoftwarePage/SoftwareVersions/_styles.scss b/frontend/pages/SoftwarePage/SoftwareVersions/_styles.scss
new file mode 100644
index 0000000000..f7087cfcca
--- /dev/null
+++ b/frontend/pages/SoftwarePage/SoftwareVersions/_styles.scss
@@ -0,0 +1,166 @@
+.software-versions {
+ margin-top: $pad-xxlarge;
+
+ &__count {
+ display: flex;
+ gap: 12px;
+ }
+
+ &__vuln_dropdown {
+ .Select-menu-outer {
+ width: 250px;
+ max-height: 310px;
+
+ .Select-menu {
+ max-height: none;
+ }
+ }
+
+ .Select-value {
+ padding-left: $pad-medium;
+ padding-right: $pad-medium;
+ }
+
+ .dropdown__custom-value-label {
+ width: 155px; // Override 105px for longer text options
+ }
+ }
+
+ .table-container {
+ &__header {
+ flex-direction: column-reverse; // Search bar on top
+ margin-bottom: $pad-medium;
+
+ @media (min-width: $break-md) {
+ flex-direction: row;
+ }
+ }
+
+ &__header-left {
+ flex-direction: row; // Filter dropdown aligned with count
+
+ .controls {
+ .form-field--dropdown {
+ margin: 0;
+ }
+ }
+ }
+
+ &__search-input,
+ &__search {
+ width: 100%; // Search bar across entire table
+
+ .input-icon-field__input {
+ width: 100%;
+ }
+
+ @media (min-width: $break-md) {
+ width: auto;
+
+ .input-icon-field__input {
+ width: 375px;
+ }
+ }
+ }
+
+ &__data-table-block {
+ .data-table-block {
+ .data-table__table {
+
+ // for showing and hiding "view all hosts" link on hover
+ tr {
+ .software-link {
+ opacity: 0;
+ transition: opacity 250ms;
+ }
+
+ &:hover {
+ .software-link {
+ opacity: 1;
+ }
+ }
+ }
+
+ thead {
+ .name__header {
+ width: $col-md;
+ }
+
+ .hosts_count__header {
+ width: auto;
+ border-right: 0;
+ }
+
+ @media (min-width: $break-lg) {
+ // expand the width of version header at larger screen sizes
+ .version__header {
+ width: $col-md;
+ }
+ }
+ }
+
+ tbody {
+ .name__cell {
+ max-width: $col-md;
+
+ // Tooltip does not get cut off
+ .children-wrapper {
+ overflow: initial;
+ }
+
+ // ellipsis for software name
+ .software-name {
+ overflow: hidden;
+ text-wrap: nowrap;
+ text-overflow: ellipsis;
+ }
+ }
+
+ .link-cell {
+ display: flex;
+ align-items: center;
+ gap: $pad-small;
+ }
+
+
+ .hosts_count__cell {
+ .hosts-cell__wrapper {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+
+ .hosts-cell__link {
+ display: flex;
+ white-space: nowrap;
+ }
+ }
+ }
+
+ @media (min-width: $break-sm) {
+ .name__cell {
+ max-width: $col-lg;
+ }
+ }
+
+ @media (min-width: $break-lg) {
+ .version__cell {
+ width: $col-md;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // needed to handle overflow of the table data on small screens
+ .data-table {
+ &__wrapper {
+ overflow-x: auto;
+ }
+ }
+
+ &__table-error {
+ margin-top: $pad-xxxlarge;
+ }
+}
diff --git a/frontend/pages/SoftwarePage/SoftwareVersions/index.ts b/frontend/pages/SoftwarePage/SoftwareVersions/index.ts
new file mode 100644
index 0000000000..cdd1fa3a9c
--- /dev/null
+++ b/frontend/pages/SoftwarePage/SoftwareVersions/index.ts
@@ -0,0 +1 @@
+export { default } from "./SoftwareVersions";
diff --git a/frontend/pages/SoftwarePage/_styles.scss b/frontend/pages/SoftwarePage/_styles.scss
new file mode 100644
index 0000000000..0f9fe85494
--- /dev/null
+++ b/frontend/pages/SoftwarePage/_styles.scss
@@ -0,0 +1,59 @@
+.software-page {
+ &__header-wrap {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ height: 38px;
+
+ .button-wrap {
+ display: flex;
+ justify-content: flex-end;
+ min-width: 266px;
+ }
+ }
+
+ &__manage-automations {
+ padding: $pad-small $pad-medium;
+ }
+
+ &__header {
+ display: flex;
+ align-items: center;
+
+ .form-field {
+ margin-bottom: 0;
+ }
+ }
+
+ &__text {
+ margin-right: $pad-large;
+ }
+
+ &__title {
+ font-size: $large;
+ }
+
+ &__description {
+ margin: 0;
+ margin-bottom: $pad-large;
+ max-width: 75%;
+
+ @media (min-width: $break-md) {
+ max-width: none;
+ }
+
+ h2 {
+ text-transform: uppercase;
+ color: $core-fleet-black;
+ font-weight: $regular;
+ font-size: $small;
+ }
+
+ p {
+ color: $ui-fleet-black-75;
+ margin: 0;
+ font-size: $x-small;
+ font-style: italic;
+ }
+ }
+}
diff --git a/frontend/pages/software/components/EmptySoftwareTable.tsx b/frontend/pages/SoftwarePage/components/EmptySoftwareTable/EmptySoftwareTable.tsx
similarity index 94%
rename from frontend/pages/software/components/EmptySoftwareTable.tsx
rename to frontend/pages/SoftwarePage/components/EmptySoftwareTable/EmptySoftwareTable.tsx
index 8caf3b8bc2..1596ea750b 100644
--- a/frontend/pages/software/components/EmptySoftwareTable.tsx
+++ b/frontend/pages/SoftwarePage/components/EmptySoftwareTable/EmptySoftwareTable.tsx
@@ -1,4 +1,5 @@
-// This component is used on DashboardPage.tsx > Software.tsx, Host Details/Device User > Software.tsx, and ManageSoftwarePage.tsx
+// This component is used on DashboardPage.tsx > Software.tsx,
+// Host Details / Device User > Software.tsx, and SoftwarePage.tsx
import React from "react";
diff --git a/frontend/pages/SoftwarePage/components/EmptySoftwareTable/index.ts b/frontend/pages/SoftwarePage/components/EmptySoftwareTable/index.ts
new file mode 100644
index 0000000000..17fa4e965d
--- /dev/null
+++ b/frontend/pages/SoftwarePage/components/EmptySoftwareTable/index.ts
@@ -0,0 +1 @@
+export { default } from "./EmptySoftwareTable";
diff --git a/frontend/pages/software/ManageSoftwarePage/components/ManageAutomationsModal/ManageAutomationsModal.tsx b/frontend/pages/SoftwarePage/components/ManageAutomationsModal/ManageAutomationsModal.tsx
similarity index 100%
rename from frontend/pages/software/ManageSoftwarePage/components/ManageAutomationsModal/ManageAutomationsModal.tsx
rename to frontend/pages/SoftwarePage/components/ManageAutomationsModal/ManageAutomationsModal.tsx
diff --git a/frontend/pages/software/ManageSoftwarePage/components/ManageAutomationsModal/_styles.scss b/frontend/pages/SoftwarePage/components/ManageAutomationsModal/_styles.scss
similarity index 100%
rename from frontend/pages/software/ManageSoftwarePage/components/ManageAutomationsModal/_styles.scss
rename to frontend/pages/SoftwarePage/components/ManageAutomationsModal/_styles.scss
diff --git a/frontend/pages/software/ManageSoftwarePage/components/ManageAutomationsModal/index.ts b/frontend/pages/SoftwarePage/components/ManageAutomationsModal/index.ts
similarity index 100%
rename from frontend/pages/software/ManageSoftwarePage/components/ManageAutomationsModal/index.ts
rename to frontend/pages/SoftwarePage/components/ManageAutomationsModal/index.ts
diff --git a/frontend/pages/software/ManageSoftwarePage/components/PreviewPayloadModal/PreviewPayloadModal.tsx b/frontend/pages/SoftwarePage/components/PreviewPayloadModal/PreviewPayloadModal.tsx
similarity index 88%
rename from frontend/pages/software/ManageSoftwarePage/components/PreviewPayloadModal/PreviewPayloadModal.tsx
rename to frontend/pages/SoftwarePage/components/PreviewPayloadModal/PreviewPayloadModal.tsx
index 30b4a5fb08..eeb5a96537 100644
--- a/frontend/pages/software/ManageSoftwarePage/components/PreviewPayloadModal/PreviewPayloadModal.tsx
+++ b/frontend/pages/SoftwarePage/components/PreviewPayloadModal/PreviewPayloadModal.tsx
@@ -1,8 +1,9 @@
import React, { useContext } from "react";
-import { syntaxHighlight } from "utilities/helpers";
import { AppContext } from "context/app";
-import { IVulnerability } from "interfaces/vulnerability";
+import { syntaxHighlight } from "utilities/helpers";
+import { ISoftwareVulnerability } from "interfaces/software";
+
import Modal from "components/Modal";
import Button from "components/buttons/Button";
import CustomLink from "components/CustomLink";
@@ -12,9 +13,21 @@ const baseClass = "preview-data-modal";
interface IPreviewPayloadModalProps {
onCancel: () => void;
}
+
+interface IHostsAffected {
+ id: number;
+ display_name: string;
+ url: string;
+ software_installed_paths?: string[];
+}
+
+type IWebhookPayload = {
+ hosts_affected?: IHostsAffected[] | null;
+} & ISoftwareVulnerability;
+
interface IJsonPayload {
timestamp: string;
- vulnerability: IVulnerability;
+ vulnerability: IWebhookPayload;
}
const PreviewPayloadModal = ({
diff --git a/frontend/pages/software/ManageSoftwarePage/components/PreviewPayloadModal/_styles.scss b/frontend/pages/SoftwarePage/components/PreviewPayloadModal/_styles.scss
similarity index 100%
rename from frontend/pages/software/ManageSoftwarePage/components/PreviewPayloadModal/_styles.scss
rename to frontend/pages/SoftwarePage/components/PreviewPayloadModal/_styles.scss
diff --git a/frontend/pages/software/ManageSoftwarePage/components/PreviewPayloadModal/index.ts b/frontend/pages/SoftwarePage/components/PreviewPayloadModal/index.ts
similarity index 100%
rename from frontend/pages/software/ManageSoftwarePage/components/PreviewPayloadModal/index.ts
rename to frontend/pages/SoftwarePage/components/PreviewPayloadModal/index.ts
diff --git a/frontend/pages/software/ManageSoftwarePage/components/PreviewTicketModal/PreviewTicketModal.tsx b/frontend/pages/SoftwarePage/components/PreviewTicketModal/PreviewTicketModal.tsx
similarity index 79%
rename from frontend/pages/software/ManageSoftwarePage/components/PreviewTicketModal/PreviewTicketModal.tsx
rename to frontend/pages/SoftwarePage/components/PreviewTicketModal/PreviewTicketModal.tsx
index f531d2023d..4197119b64 100644
--- a/frontend/pages/software/ManageSoftwarePage/components/PreviewTicketModal/PreviewTicketModal.tsx
+++ b/frontend/pages/SoftwarePage/components/PreviewTicketModal/PreviewTicketModal.tsx
@@ -6,10 +6,10 @@ import Modal from "components/Modal";
import Button from "components/buttons/Button";
import CustomLink from "components/CustomLink";
-import JiraPreview from "../../../../../../assets/images/jira-vuln-software-preview-400x517@2x.png";
-import ZendeskPreview from "../../../../../../assets/images/zendesk-vuln-software-preview-400x455@2x.png";
-import JiraPreviewPremium from "../../../../../../assets/images/jira-vuln-software-preview-premium-400x517@2x.png";
-import ZendeskPreviewPremium from "../../../../../../assets/images/zendesk-vuln-software-preview-premium-400x455@2x.png";
+import JiraPreview from "../../../../../assets/images/jira-vuln-software-preview-400x517@2x.png";
+import ZendeskPreview from "../../../../../assets/images/zendesk-vuln-software-preview-400x455@2x.png";
+import JiraPreviewPremium from "../../../../../assets/images/jira-vuln-software-preview-premium-400x517@2x.png";
+import ZendeskPreviewPremium from "../../../../../assets/images/zendesk-vuln-software-preview-premium-400x455@2x.png";
const baseClass = "preview-ticket-modal";
diff --git a/frontend/pages/software/ManageSoftwarePage/components/PreviewTicketModal/_styles.scss b/frontend/pages/SoftwarePage/components/PreviewTicketModal/_styles.scss
similarity index 100%
rename from frontend/pages/software/ManageSoftwarePage/components/PreviewTicketModal/_styles.scss
rename to frontend/pages/SoftwarePage/components/PreviewTicketModal/_styles.scss
diff --git a/frontend/pages/software/ManageSoftwarePage/components/PreviewTicketModal/index.ts b/frontend/pages/SoftwarePage/components/PreviewTicketModal/index.ts
similarity index 100%
rename from frontend/pages/software/ManageSoftwarePage/components/PreviewTicketModal/index.ts
rename to frontend/pages/SoftwarePage/components/PreviewTicketModal/index.ts
diff --git a/frontend/pages/SoftwarePage/components/SoftwareDetailsSummary/SoftwareDetailsSummary.tsx b/frontend/pages/SoftwarePage/components/SoftwareDetailsSummary/SoftwareDetailsSummary.tsx
new file mode 100644
index 0000000000..f1b5a39aa1
--- /dev/null
+++ b/frontend/pages/SoftwarePage/components/SoftwareDetailsSummary/SoftwareDetailsSummary.tsx
@@ -0,0 +1,69 @@
+import ViewAllHostsLink from "components/ViewAllHostsLink";
+import React from "react";
+import SoftwareIcon from "../icons/SoftwareIcon";
+
+const baseClass = "software-details-summary";
+
+interface IDescriptionSetProps {
+ title: string;
+ value: React.ReactNode;
+}
+
+// TODO: move to frontend/components
+const DataSet = ({ title, value }: IDescriptionSetProps) => {
+ return (
+
+
{title}
+ {value}
+
+ );
+};
+
+interface ISoftwareDetailsSummaryProps {
+ id: number;
+ title: string;
+ type: string;
+ hosts: number;
+ /** The query param name that will be added when user clicks on "View all hosts" link */
+ queryParam: string;
+ name?: string;
+ source?: string;
+ versions?: number;
+}
+
+const SoftwareDetailsSummary = ({
+ id,
+ title,
+ type,
+ hosts,
+ queryParam,
+ name,
+ source,
+ versions,
+}: ISoftwareDetailsSummaryProps) => {
+ return (
+
+
+
+ {title}
+
+
+ {versions && }
+
+
+
+
+
+
+
+ );
+};
+
+export default SoftwareDetailsSummary;
diff --git a/frontend/pages/SoftwarePage/components/SoftwareDetailsSummary/_styles.scss b/frontend/pages/SoftwarePage/components/SoftwareDetailsSummary/_styles.scss
new file mode 100644
index 0000000000..a0456417b1
--- /dev/null
+++ b/frontend/pages/SoftwarePage/components/SoftwareDetailsSummary/_styles.scss
@@ -0,0 +1,40 @@
+.software-details-summary {
+ background-color: $core-white;
+ padding: $pad-xxlarge;
+ border: 1px solid $ui-fleet-black-10;
+ border-radius: $border-radius-xxlarge;
+ box-shadow: $box-shadow;
+ display: flex;
+ gap: $pad-medium;
+
+ &__icon {
+ width: 96px;
+ height: 96px;
+ border: 1px solid #E2E4EA;
+ border-radius: 8px;
+ }
+
+ &__info {
+ flex-grow: 1;
+ }
+
+ h1 {
+ font-size: $pad-large;
+ font-weight: bold;
+ margin-bottom: $pad-medium;
+ }
+
+
+ &__description-list {
+ display: flex;
+ gap: $pad-xxlarge;
+ }
+
+ &__data-set {
+ font-size: $x-small;
+
+ dt {
+ font-weight: $bold;
+ }
+ }
+}
diff --git a/frontend/pages/SoftwarePage/components/SoftwareDetailsSummary/index.ts b/frontend/pages/SoftwarePage/components/SoftwareDetailsSummary/index.ts
new file mode 100644
index 0000000000..0fe352aa8f
--- /dev/null
+++ b/frontend/pages/SoftwarePage/components/SoftwareDetailsSummary/index.ts
@@ -0,0 +1 @@
+export { default } from "./SoftwareDetailsSummary";
diff --git a/frontend/pages/SoftwarePage/components/VersionCell/VersionCell.tsx b/frontend/pages/SoftwarePage/components/VersionCell/VersionCell.tsx
new file mode 100644
index 0000000000..db8f4370c9
--- /dev/null
+++ b/frontend/pages/SoftwarePage/components/VersionCell/VersionCell.tsx
@@ -0,0 +1,67 @@
+import React from "react";
+import { uniqueId } from "lodash";
+
+import { ISoftwareTitleVersion } from "interfaces/software";
+
+import TextCell from "components/TableContainer/DataTable/TextCell";
+import ReactTooltip from "react-tooltip";
+
+const baseClass = "version-cell";
+
+const generateText = (versions: ISoftwareTitleVersion[]) => {
+ const text =
+ versions.length !== 1 ? `${versions.length} versions` : versions[0].version;
+ return ;
+};
+
+const generateTooltip = (
+ versions: ISoftwareTitleVersion[],
+ tooltipId: string
+) => {
+ if (versions.length <= 1) {
+ return null;
+ }
+
+ const versionNames = versions.map((version) => version.version);
+
+ return (
+
+ {versionNames.join(", ")}
+
+ );
+};
+
+interface IVersionCellProps {
+ versions: ISoftwareTitleVersion[];
+}
+
+const VersionCell = ({ versions }: IVersionCellProps) => {
+ const tooltipId = uniqueId();
+
+ // only one version, no need for tooltip
+ const cellText = generateText(versions);
+ if (versions.length <= 1) {
+ return <>{cellText}>;
+ }
+
+ const versionTooltip = generateTooltip(versions, tooltipId);
+ return (
+ <>
+
+ {cellText}
+
+ {versionTooltip}
+ >
+ );
+};
+
+export default VersionCell;
diff --git a/frontend/pages/SoftwarePage/components/VersionCell/_styles.scss b/frontend/pages/SoftwarePage/components/VersionCell/_styles.scss
new file mode 100644
index 0000000000..2f0275a50d
--- /dev/null
+++ b/frontend/pages/SoftwarePage/components/VersionCell/_styles.scss
@@ -0,0 +1,10 @@
+.version-cell {
+
+ &__version-text-with-tooltip {
+ display: inline-block;
+ }
+
+ &__versions {
+ margin: 0;
+ }
+}
diff --git a/frontend/pages/SoftwarePage/components/VersionCell/index.ts b/frontend/pages/SoftwarePage/components/VersionCell/index.ts
new file mode 100644
index 0000000000..bd8a77e877
--- /dev/null
+++ b/frontend/pages/SoftwarePage/components/VersionCell/index.ts
@@ -0,0 +1 @@
+export { default } from "./VersionCell";
diff --git a/frontend/pages/SoftwarePage/components/VulnerabilitiesCell/VulnerabilitiesCell.tsx b/frontend/pages/SoftwarePage/components/VulnerabilitiesCell/VulnerabilitiesCell.tsx
new file mode 100644
index 0000000000..51183cacda
--- /dev/null
+++ b/frontend/pages/SoftwarePage/components/VulnerabilitiesCell/VulnerabilitiesCell.tsx
@@ -0,0 +1,114 @@
+import React from "react";
+import { uniqueId } from "lodash";
+import ReactTooltip from "react-tooltip";
+
+import TextCell from "components/TableContainer/DataTable/TextCell";
+import { ISoftwareVulnerability } from "interfaces/software";
+
+const NUM_VULNERABILITIES_IN_TOOLTIP = 3;
+
+const baseClass = "vulnerabilities-cell";
+
+const generateCell = (
+ vulnerabilities: ISoftwareVulnerability[] | string[] | null
+) => {
+ if (vulnerabilities === null) {
+ return ;
+ }
+
+ let text = "";
+ let isGrayed = true;
+ if (vulnerabilities.length === 0) {
+ text = "---";
+ } else if (vulnerabilities.length === 1) {
+ isGrayed = false;
+ text =
+ typeof vulnerabilities[0] === "string"
+ ? vulnerabilities[0]
+ : vulnerabilities[0].cve;
+ } else {
+ text = `${vulnerabilities.length} vulnerabilities`;
+ }
+
+ return ;
+};
+
+const getName = (vulnerabiltiy: ISoftwareVulnerability | string) => {
+ return typeof vulnerabiltiy === "string" ? vulnerabiltiy : vulnerabiltiy.cve;
+};
+
+const condenseVulnerabilities = (
+ vulnerabilities: ISoftwareVulnerability[] | string[]
+) => {
+ const condensed =
+ (vulnerabilities?.length &&
+ vulnerabilities
+ .slice(-NUM_VULNERABILITIES_IN_TOOLTIP)
+ .map(getName)
+ .reverse()) ||
+ [];
+
+ return vulnerabilities.length > NUM_VULNERABILITIES_IN_TOOLTIP
+ ? condensed.concat(
+ `+${vulnerabilities.length - NUM_VULNERABILITIES_IN_TOOLTIP} more`
+ )
+ : condensed;
+};
+
+const generateTooltip = (
+ vulnerabilities: ISoftwareVulnerability[] | string[],
+ tooltipId: string
+) => {
+ if (vulnerabilities.length <= 1) {
+ return null;
+ }
+
+ const condensedVulnerabilties = condenseVulnerabilities(vulnerabilities);
+
+ return (
+
+
+ {condensedVulnerabilties.map((vulnerability) => {
+ return • {vulnerability} ;
+ })}
+
+
+ );
+};
+interface IVulnerabilitiesCellProps {
+ vulnerabilities: ISoftwareVulnerability[] | string[] | null;
+}
+
+const VulnerabilitiesCell = ({
+ vulnerabilities,
+}: IVulnerabilitiesCellProps) => {
+ const tooltipId = uniqueId();
+
+ // only one vulnerability, no need for tooltip
+ const cell = generateCell(vulnerabilities);
+ if (vulnerabilities === null || vulnerabilities.length <= 1) {
+ return <>{cell}>;
+ }
+
+ const vulnerabilityTooltip = generateTooltip(vulnerabilities, tooltipId);
+
+ return (
+ <>
+
+ {cell}
+
+ {vulnerabilityTooltip}
+ >
+ );
+};
+
+export default VulnerabilitiesCell;
diff --git a/frontend/pages/SoftwarePage/components/VulnerabilitiesCell/_styles.scss b/frontend/pages/SoftwarePage/components/VulnerabilitiesCell/_styles.scss
new file mode 100644
index 0000000000..a1f3c87066
--- /dev/null
+++ b/frontend/pages/SoftwarePage/components/VulnerabilitiesCell/_styles.scss
@@ -0,0 +1,12 @@
+.vulnerabilities-cell {
+ &__vulnerability-text-with-tooltip {
+ display: inline-block;
+ }
+
+ &__vulnerability-list {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+ text-align: left;
+ }
+}
diff --git a/frontend/pages/SoftwarePage/components/VulnerabilitiesCell/index.ts b/frontend/pages/SoftwarePage/components/VulnerabilitiesCell/index.ts
new file mode 100644
index 0000000000..82f8dfb1aa
--- /dev/null
+++ b/frontend/pages/SoftwarePage/components/VulnerabilitiesCell/index.ts
@@ -0,0 +1 @@
+export { default } from "./VulnerabilitiesCell";
diff --git a/frontend/pages/SoftwarePage/components/icons/AcrobatReader.tsx b/frontend/pages/SoftwarePage/components/icons/AcrobatReader.tsx
new file mode 100644
index 0000000000..440db6f583
--- /dev/null
+++ b/frontend/pages/SoftwarePage/components/icons/AcrobatReader.tsx
@@ -0,0 +1,17 @@
+import React from "react";
+
+import type { SVGProps } from "react";
+
+const AcrobatReader = (props: SVGProps) => (
+
+
+
+
+);
+export default AcrobatReader;
diff --git a/frontend/pages/SoftwarePage/components/icons/Chrome.tsx b/frontend/pages/SoftwarePage/components/icons/Chrome.tsx
new file mode 100644
index 0000000000..4a4df76d39
--- /dev/null
+++ b/frontend/pages/SoftwarePage/components/icons/Chrome.tsx
@@ -0,0 +1,71 @@
+import React from "react";
+
+import type { SVGProps } from "react";
+
+const Chrome = (props: SVGProps) => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+);
+export default Chrome;
diff --git a/frontend/pages/SoftwarePage/components/icons/Excel.tsx b/frontend/pages/SoftwarePage/components/icons/Excel.tsx
new file mode 100644
index 0000000000..5a46cfbc23
--- /dev/null
+++ b/frontend/pages/SoftwarePage/components/icons/Excel.tsx
@@ -0,0 +1,70 @@
+import React from "react";
+
+import type { SVGProps } from "react";
+
+const Excel = (props: SVGProps) => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+);
+export default Excel;
diff --git a/frontend/pages/SoftwarePage/components/icons/Extension.tsx b/frontend/pages/SoftwarePage/components/icons/Extension.tsx
new file mode 100644
index 0000000000..d456c8724f
--- /dev/null
+++ b/frontend/pages/SoftwarePage/components/icons/Extension.tsx
@@ -0,0 +1,20 @@
+import React from "react";
+
+import type { SVGProps } from "react";
+
+const Extension = (props: SVGProps) => (
+
+
+
+
+);
+export default Extension;
diff --git a/frontend/pages/SoftwarePage/components/icons/Firefox.tsx b/frontend/pages/SoftwarePage/components/icons/Firefox.tsx
new file mode 100644
index 0000000000..be5594c4e6
--- /dev/null
+++ b/frontend/pages/SoftwarePage/components/icons/Firefox.tsx
@@ -0,0 +1,292 @@
+import React from "react";
+
+import type { SVGProps } from "react";
+
+const Firefox = (props: SVGProps) => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+);
+export default Firefox;
diff --git a/frontend/pages/SoftwarePage/components/icons/MacApp.tsx b/frontend/pages/SoftwarePage/components/icons/MacApp.tsx
new file mode 100644
index 0000000000..5119e03fb1
--- /dev/null
+++ b/frontend/pages/SoftwarePage/components/icons/MacApp.tsx
@@ -0,0 +1,20 @@
+import React from "react";
+
+import type { SVGProps } from "react";
+
+const MacApp = (props: SVGProps) => (
+
+
+
+
+);
+export default MacApp;
diff --git a/frontend/pages/SoftwarePage/components/icons/Package.tsx b/frontend/pages/SoftwarePage/components/icons/Package.tsx
new file mode 100644
index 0000000000..8a2aae8db9
--- /dev/null
+++ b/frontend/pages/SoftwarePage/components/icons/Package.tsx
@@ -0,0 +1,20 @@
+import React from "react";
+
+import type { SVGProps } from "react";
+
+const Package = (props: SVGProps) => (
+
+
+
+
+);
+export default Package;
diff --git a/frontend/pages/SoftwarePage/components/icons/Safari.tsx b/frontend/pages/SoftwarePage/components/icons/Safari.tsx
new file mode 100644
index 0000000000..c0c28e3a8c
--- /dev/null
+++ b/frontend/pages/SoftwarePage/components/icons/Safari.tsx
@@ -0,0 +1,584 @@
+import React from "react";
+
+import type { SVGProps } from "react";
+
+const Safari = (props: SVGProps) => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+);
+export default Safari;
diff --git a/frontend/pages/SoftwarePage/components/icons/Slack.tsx b/frontend/pages/SoftwarePage/components/icons/Slack.tsx
new file mode 100644
index 0000000000..3b6aac63d7
--- /dev/null
+++ b/frontend/pages/SoftwarePage/components/icons/Slack.tsx
@@ -0,0 +1,37 @@
+import React from "react";
+
+import type { SVGProps } from "react";
+
+const Slack = (props: SVGProps) => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+);
+export default Slack;
diff --git a/frontend/pages/SoftwarePage/components/icons/SoftwareIcon/SoftwareIcon.tsx b/frontend/pages/SoftwarePage/components/icons/SoftwareIcon/SoftwareIcon.tsx
new file mode 100644
index 0000000000..e99b23e262
--- /dev/null
+++ b/frontend/pages/SoftwarePage/components/icons/SoftwareIcon/SoftwareIcon.tsx
@@ -0,0 +1,64 @@
+import React, { ComponentType, SVGProps } from "react";
+import {
+ SOFTWARE_NAME_TO_ICON_MAP,
+ SOFTWARE_SOURCE_TO_ICON_MAP,
+ SOFTWARE_ICON_SIZES,
+ SoftwareIconSizes,
+} from "../";
+
+const baseClass = "software-icon";
+
+interface ISoftwareIconProps {
+ name?: string;
+ source?: string;
+ size?: SoftwareIconSizes;
+}
+
+const matchInMap = (
+ map: Record>>,
+ potentialKey?: string
+) => {
+ if (!potentialKey) {
+ return null;
+ }
+
+ const sanitizedKey = potentialKey.trim().toLowerCase();
+ const match = Object.entries(map).find(([namePrefix, icon]) => {
+ if (sanitizedKey.startsWith(namePrefix)) {
+ return icon;
+ }
+ return null;
+ });
+
+ return match ? match[1] : null;
+};
+
+const SoftwareIcon = ({
+ name,
+ source,
+ size = "medium",
+}: ISoftwareIconProps) => {
+ // try to find a match for name
+ let MatchedIcon = matchInMap(SOFTWARE_NAME_TO_ICON_MAP, name);
+
+ // otherwise, try to find a match for source
+ if (!MatchedIcon) {
+ MatchedIcon = matchInMap(SOFTWARE_SOURCE_TO_ICON_MAP, source);
+ }
+
+ // default to 'package'
+ if (!MatchedIcon) {
+ MatchedIcon = SOFTWARE_SOURCE_TO_ICON_MAP.package;
+ }
+
+ return (
+
+ );
+};
+
+export default SoftwareIcon;
diff --git a/frontend/pages/SoftwarePage/components/icons/SoftwareIcon/index.ts b/frontend/pages/SoftwarePage/components/icons/SoftwareIcon/index.ts
new file mode 100644
index 0000000000..202b202e40
--- /dev/null
+++ b/frontend/pages/SoftwarePage/components/icons/SoftwareIcon/index.ts
@@ -0,0 +1 @@
+export { default } from "./SoftwareIcon";
diff --git a/frontend/pages/SoftwarePage/components/icons/Teams.tsx b/frontend/pages/SoftwarePage/components/icons/Teams.tsx
new file mode 100644
index 0000000000..dfb64170cb
--- /dev/null
+++ b/frontend/pages/SoftwarePage/components/icons/Teams.tsx
@@ -0,0 +1,84 @@
+import React from "react";
+
+import type { SVGProps } from "react";
+
+const Teams = (props: SVGProps) => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+);
+export default Teams;
diff --git a/frontend/pages/SoftwarePage/components/icons/VisualStudioCode.tsx b/frontend/pages/SoftwarePage/components/icons/VisualStudioCode.tsx
new file mode 100644
index 0000000000..ac089803e2
--- /dev/null
+++ b/frontend/pages/SoftwarePage/components/icons/VisualStudioCode.tsx
@@ -0,0 +1,37 @@
+import React from "react";
+
+import type { SVGProps } from "react";
+
+const VisualStudioCode = (props: SVGProps) => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+);
+export default VisualStudioCode;
diff --git a/frontend/pages/SoftwarePage/components/icons/WindowsApp.tsx b/frontend/pages/SoftwarePage/components/icons/WindowsApp.tsx
new file mode 100644
index 0000000000..6f0fad5a90
--- /dev/null
+++ b/frontend/pages/SoftwarePage/components/icons/WindowsApp.tsx
@@ -0,0 +1,20 @@
+import React from "react";
+
+import type { SVGProps } from "react";
+
+const WindowsApp = (props: SVGProps) => (
+
+
+
+
+);
+export default WindowsApp;
diff --git a/frontend/pages/SoftwarePage/components/icons/Word.tsx b/frontend/pages/SoftwarePage/components/icons/Word.tsx
new file mode 100644
index 0000000000..4a654107b2
--- /dev/null
+++ b/frontend/pages/SoftwarePage/components/icons/Word.tsx
@@ -0,0 +1,94 @@
+import React from "react";
+
+import type { SVGProps } from "react";
+
+const Word = (props: SVGProps) => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+);
+export default Word;
diff --git a/frontend/pages/SoftwarePage/components/icons/Zoom.tsx b/frontend/pages/SoftwarePage/components/icons/Zoom.tsx
new file mode 100644
index 0000000000..8b9512751d
--- /dev/null
+++ b/frontend/pages/SoftwarePage/components/icons/Zoom.tsx
@@ -0,0 +1,30 @@
+import React from "react";
+
+import type { SVGProps } from "react";
+
+const Zoom = (props: SVGProps) => (
+
+
+
+
+
+
+
+
+
+
+);
+export default Zoom;
diff --git a/frontend/pages/SoftwarePage/components/icons/index.ts b/frontend/pages/SoftwarePage/components/icons/index.ts
new file mode 100644
index 0000000000..0965c78651
--- /dev/null
+++ b/frontend/pages/SoftwarePage/components/icons/index.ts
@@ -0,0 +1,60 @@
+import AcrobatReader from "./AcrobatReader";
+import Chrome from "./Chrome";
+import Excel from "./Excel";
+import Extension from "./Extension";
+import Firefox from "./Firefox";
+import MacApp from "./MacApp";
+import Package from "./Package";
+import Safari from "./Safari";
+import Slack from "./Slack";
+import Teams from "./Teams";
+import VisualStudioCode from "./VisualStudioCode";
+import WindowsApp from "./WindowsApp";
+import Word from "./Word";
+import Zoom from "./Zoom";
+
+// SOFTWARE_NAME_TO_ICON_MAP list "special" applications that have a defined
+// icon for them, keys refer to application names, and are intended to be fuzzy
+// matched in the application logic.
+export const SOFTWARE_NAME_TO_ICON_MAP = {
+ "adobe acrobat reader": AcrobatReader,
+ "google chrome": Chrome,
+ "microsoft excel": Excel,
+ firefox: Firefox,
+ package: Package,
+ safari: Safari,
+ slack: Slack,
+ "microsoft teams": Teams,
+ "visual studio code": VisualStudioCode,
+ "microsoft word": Word,
+ zoom: Zoom,
+} as const;
+
+// SOFTWARE_SOURCE_TO_ICON_MAP maps different software sources to a defined
+// icon.
+export const SOFTWARE_SOURCE_TO_ICON_MAP = {
+ package: Package,
+ apt_sources: Package,
+ deb_packages: Package,
+ rpm_packages: Package,
+ yum_sources: Package,
+ npm_packages: Package,
+ atom_packages: Package,
+ python_packages: Package,
+ homebrew_packages: Package,
+ apps: MacApp,
+ programs: WindowsApp,
+ chrome_extensions: Extension,
+ safari_extensions: Extension,
+ firefox_addons: Extension,
+ ie_extensions: Extension,
+ chocolatey_packages: Package,
+ pkg_packages: Package,
+} as const;
+
+export const SOFTWARE_ICON_SIZES: Record = {
+ medium: "24",
+ large: "96",
+} as const;
+
+export type SoftwareIconSizes = keyof typeof SOFTWARE_ICON_SIZES;
diff --git a/frontend/pages/SoftwarePage/helpers.ts b/frontend/pages/SoftwarePage/helpers.ts
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frontend/pages/SoftwarePage/index.ts b/frontend/pages/SoftwarePage/index.ts
new file mode 100644
index 0000000000..980539c402
--- /dev/null
+++ b/frontend/pages/SoftwarePage/index.ts
@@ -0,0 +1 @@
+export { default } from "./SoftwarePage";
diff --git a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx
index 480175995c..577aa24f01 100644
--- a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx
+++ b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx
@@ -219,6 +219,14 @@ const ManageHostsPage = ({
queryParams?.software_id !== undefined
? parseInt(queryParams.software_id, 10)
: undefined;
+ const softwareVersionId =
+ queryParams?.software_version_id !== undefined
+ ? parseInt(queryParams.software_version_id, 10)
+ : undefined;
+ const softwareTitleId =
+ queryParams?.software_title_id !== undefined
+ ? parseInt(queryParams.software_title_id, 10)
+ : undefined;
const status = isAcceptableStatus(queryParams?.status)
? queryParams?.status
: undefined;
@@ -360,6 +368,8 @@ const ManageHostsPage = ({
policyId,
policyResponse,
softwareId,
+ softwareTitleId,
+ softwareVersionId,
status,
mdmId,
mdmEnrollmentStatus,
@@ -400,6 +410,8 @@ const ManageHostsPage = ({
policyId,
policyResponse,
softwareId,
+ softwareTitleId,
+ softwareVersionId,
status,
mdmId,
mdmEnrollmentStatus,
@@ -491,7 +503,13 @@ const ManageHostsPage = ({
// TODO: cleanup this effect
useEffect(() => {
- if (location.search.includes("software_id")) {
+ if (
+ location.search.match(
+ /software_id|software_version_id|software_title_id/gi
+ )
+ ) {
+ // regex matches any of "software_id", "software_version_id", or "software_title_id"
+ // so we don't set the filtered hosts path in those cases
return;
}
const path = location.pathname + location.search;
@@ -520,6 +538,8 @@ const ManageHostsPage = ({
"policy_id",
"policy_response",
"software_id",
+ "software_version_id",
+ "software_title_id",
]);
}
@@ -783,6 +803,10 @@ const ManageHostsPage = ({
newQueryParams.macos_settings = macSettingsStatus;
} else if (softwareId) {
newQueryParams.software_id = softwareId;
+ } else if (softwareVersionId) {
+ newQueryParams.software_version_id = softwareVersionId;
+ } else if (softwareTitleId) {
+ newQueryParams.software_title_id = softwareTitleId;
} else if (mdmId) {
newQueryParams.mdm_id = mdmId;
} else if (mdmEnrollmentStatus) {
@@ -828,6 +852,8 @@ const ManageHostsPage = ({
policyResponse,
macSettingsStatus,
softwareId,
+ softwareVersionId,
+ softwareTitleId,
mdmId,
mdmEnrollmentStatus,
munkiIssueId,
@@ -1244,6 +1270,8 @@ const ManageHostsPage = ({
policyResponse,
macSettingsStatus,
softwareId,
+ softwareTitleId,
+ softwareVersionId,
status,
mdmId,
mdmEnrollmentStatus,
@@ -1557,6 +1585,8 @@ const ManageHostsPage = ({
policy,
macSettingsStatus,
softwareId,
+ softwareTitleId,
+ softwareVersionId,
mdmId,
mdmEnrollmentStatus,
lowDiskSpaceHosts,
@@ -1566,7 +1596,8 @@ const ManageHostsPage = ({
osVersions,
munkiIssueId,
munkiIssueDetails: hostsData?.munki_issue || null,
- softwareDetails: hostsData?.software || null,
+ softwareDetails:
+ hostsData?.software || hostsData?.software_title || null,
mdmSolutionDetails:
hostsData?.mobile_device_management_solution || null,
osSettingsStatus,
diff --git a/frontend/pages/hosts/ManageHostsPage/components/HostsFilterBlock/HostsFilterBlock.tsx b/frontend/pages/hosts/ManageHostsPage/components/HostsFilterBlock/HostsFilterBlock.tsx
index 1c45076031..f504b6556d 100644
--- a/frontend/pages/hosts/ManageHostsPage/components/HostsFilterBlock/HostsFilterBlock.tsx
+++ b/frontend/pages/hosts/ManageHostsPage/components/HostsFilterBlock/HostsFilterBlock.tsx
@@ -53,7 +53,9 @@ interface IHostsFilterBlockProps {
policyId?: any;
policy?: IPolicy;
macSettingsStatus?: any;
- softwareId?: any;
+ softwareId?: number;
+ softwareTitleId?: number;
+ softwareVersionId?: number;
mdmId?: number;
mdmEnrollmentStatus?: any;
lowDiskSpaceHosts?: number;
@@ -62,7 +64,7 @@ interface IHostsFilterBlockProps {
osVersion?: any;
munkiIssueId?: number;
osVersions?: IOperatingSystemVersion[];
- softwareDetails: ISoftware | null;
+ softwareDetails: { name: string; version?: string } | null;
mdmSolutionDetails: IMdmSolution | null;
osSettingsStatus?: MdmProfileStatus;
diskEncryptionStatus?: DiskEncryptionStatus;
@@ -95,6 +97,8 @@ const HostsFilterBlock = ({
policyId,
macSettingsStatus,
softwareId,
+ softwareTitleId,
+ softwareVersionId,
mdmId,
mdmEnrollmentStatus,
lowDiskSpaceHosts,
@@ -235,21 +239,31 @@ const HostsFilterBlock = ({
if (!softwareDetails) return null;
const { name, version } = softwareDetails;
- const label = `${name || "Unknown software"} ${version || ""}`;
+ let label = name;
+ if (version) {
+ label += ` ${version}`;
+ }
+ label = label.trim() || "Unknown software";
- const TooltipDescription = (
-
- Hosts with {name || "Unknown software"},
-
- {version || "version unknown"} installed
-
- );
+ // const TooltipDescription = (
+ //
+ // Hosts with {name || "Unknown software"},
+ //
+ // {version || "version unknown"} installed
+ //
+ // );
return (
handleClearFilter(["software_id"])}
- tooltipDescription={TooltipDescription}
+ onClear={() =>
+ handleClearFilter([
+ "software_id",
+ "software_version_id",
+ "software_title_id",
+ ])
+ }
+ // tooltipDescription={TooltipDescription}
/>
);
};
@@ -433,6 +447,8 @@ const HostsFilterBlock = ({
policyId ||
macSettingsStatus ||
softwareId ||
+ softwareTitleId ||
+ softwareVersionId ||
mdmId ||
mdmEnrollmentStatus ||
lowDiskSpaceHosts ||
@@ -472,7 +488,7 @@ const HostsFilterBlock = ({
return renderPoliciesFilterBlock();
case !!macSettingsStatus:
return renderMacSettingsStatusFilterBlock();
- case !!softwareId:
+ case !!softwareId || !!softwareVersionId || !!softwareTitleId:
return renderSoftwareFilterBlock();
case !!mdmId:
return renderMDMSolutionFilterBlock();
diff --git a/frontend/pages/hosts/details/cards/Software/Software.tsx b/frontend/pages/hosts/details/cards/Software/Software.tsx
index bb695fb725..05e3cf8a3a 100644
--- a/frontend/pages/hosts/details/cards/Software/Software.tsx
+++ b/frontend/pages/hosts/details/cards/Software/Software.tsx
@@ -13,7 +13,7 @@ import { buildQueryStringFromParams } from "utilities/url";
import Dropdown from "components/forms/fields/Dropdown";
import TableContainer from "components/TableContainer";
import { ITableQueryData } from "components/TableContainer/TableContainer";
-import EmptySoftwareTable from "pages/software/components/EmptySoftwareTable";
+import EmptySoftwareTable from "pages/SoftwarePage/components/EmptySoftwareTable";
import { getNextLocationPath } from "utilities/helpers";
import SoftwareVulnCount from "./SoftwareVulnCount";
diff --git a/frontend/pages/hosts/details/cards/Software/SoftwareTableConfig.tsx b/frontend/pages/hosts/details/cards/Software/SoftwareTableConfig.tsx
index b95f38b3f8..8ce0179574 100644
--- a/frontend/pages/hosts/details/cards/Software/SoftwareTableConfig.tsx
+++ b/frontend/pages/hosts/details/cards/Software/SoftwareTableConfig.tsx
@@ -211,12 +211,12 @@ export const generateSoftwareTableHeaders = ({
// Allows for button to be clickable in a clickable row
e.stopPropagation();
setFilteredSoftwarePath(pathname);
- router?.push(PATHS.SOFTWARE_DETAILS(id.toString()));
+ router?.push(PATHS.SOFTWARE_VERSION_DETAILS(id.toString()));
};
return (
{
- const routeTemplate = route?.path ?? "";
- const queryParams = location.query;
- const {
- config: globalConfig,
- isFreeTier,
- isGlobalAdmin,
- isGlobalMaintainer,
- isOnGlobalTeam,
- isPremiumTier,
- isSandboxMode,
- noSandboxHosts,
- filteredSoftwarePath,
- setFilteredSoftwarePath,
- } = useContext(AppContext);
- const { renderFlash } = useContext(NotificationContext);
-
- const {
- currentTeamId,
- isAnyTeamSelected,
- isRouteOk,
- teamIdForApi,
- userTeams,
- handleTeamChange,
- } = useTeamIdParam({
- location,
- router,
- includeAllTeams: true,
- includeNoTeam: false,
- });
-
- const canManageAutomations =
- isGlobalAdmin && (!isPremiumTier || !isAnyTeamSelected);
-
- const DEFAULT_SORT_HEADER = isPremiumTier ? "vulnerabilities" : "hosts_count";
-
- const initialQuery = (() => {
- let query = "";
-
- if (queryParams && queryParams.query) {
- query = queryParams.query;
- }
-
- return query;
- })();
-
- const initialSortHeader = (() => {
- let sortHeader = isPremiumTier ? "vulnerabilities" : "hosts_count";
-
- if (queryParams && queryParams.order_key) {
- sortHeader = queryParams.order_key;
- }
-
- return sortHeader;
- })();
-
- const initialSortDirection = ((): "asc" | "desc" | undefined => {
- let sortDirection = "desc";
-
- if (queryParams && queryParams.order_direction) {
- sortDirection = queryParams.order_direction;
- }
-
- return sortDirection as "asc" | "desc" | undefined;
- })();
-
- const initialPage = (() => {
- let page = 0;
-
- if (queryParams && queryParams.page) {
- page = parseInt(queryParams.page, 10);
- }
-
- return page;
- })();
-
- const initialVulnFilter = (() => {
- let isFilteredByVulnerabilities = false;
-
- if (queryParams && queryParams.vulnerable === "true") {
- isFilteredByVulnerabilities = true;
- }
-
- return isFilteredByVulnerabilities;
- })();
-
- const [filterVuln, setFilterVuln] = useState(initialVulnFilter);
- const [searchQuery, setSearchQuery] = useState(initialQuery);
- const [sortDirection, setSortDirection] = useState<
- "asc" | "desc" | undefined
- >(initialSortDirection);
- const [sortHeader, setSortHeader] = useState(initialSortHeader);
- const [page, setPage] = useState(initialPage);
- const [tableQueryData, setTableQueryData] = useState();
- const [resetPageIndex, setResetPageIndex] = useState(false);
- const [showManageAutomationsModal, setShowManageAutomationsModal] = useState(
- false
- );
- const [showPreviewPayloadModal, setShowPreviewPayloadModal] = useState(false);
- const [showPreviewTicketModal, setShowPreviewTicketModal] = useState(false);
-
- useEffect(() => {
- setFilterVuln(initialVulnFilter);
- setPage(initialPage);
- setSearchQuery(initialQuery);
- // TODO: handle invalid values for params
- }, [location]);
-
- useEffect(() => {
- const path = location.pathname + location.search;
- if (filteredSoftwarePath !== path) {
- setFilteredSoftwarePath(path);
- }
- }, [filteredSoftwarePath, location, setFilteredSoftwarePath]);
-
- // softwareConfig is either the global config or the team config of the currently selected team
- const {
- data: softwareConfig,
- error: softwareConfigError,
- isFetching: isFetchingSoftwareConfig,
- refetch: refetchSoftwareConfig,
- } = useQuery<
- IConfig | ILoadTeamResponse,
- Error,
- IConfig | ITeamConfig,
- ISoftwareConfigQueryKey[]
- >(
- [{ scope: "softwareConfig", teamId: teamIdForApi }],
- ({ queryKey }) => {
- const { teamId } = queryKey[0];
- return teamId ? teamsAPI.load(teamId) : configAPI.loadAll();
- },
- {
- enabled: isRouteOk,
- select: (data) => ("team" in data ? data.team : data),
- }
- );
-
- const isSoftwareConfigLoaded =
- !isFetchingSoftwareConfig && !softwareConfigError && !!softwareConfig;
-
- const isSoftwareEnabled = !!softwareConfig?.features
- ?.enable_software_inventory;
-
- const vulnWebhookSettings =
- softwareConfig?.webhook_settings?.vulnerabilities_webhook;
-
- const isVulnWebhookEnabled = !!vulnWebhookSettings?.enable_vulnerabilities_webhook;
-
- const isVulnIntegrationEnabled = (integrations?: IIntegrations) => {
- return (
- !!integrations?.jira?.some((j) => j.enable_software_vulnerabilities) ||
- !!integrations?.zendesk?.some((z) => z.enable_software_vulnerabilities)
- );
- };
-
- const isAnyVulnAutomationEnabled =
- isVulnWebhookEnabled ||
- isVulnIntegrationEnabled(softwareConfig?.integrations);
-
- const recentVulnerabilityMaxAge = (() => {
- let maxAgeInNanoseconds: number | undefined;
- if (softwareConfig && "vulnerabilities" in softwareConfig) {
- maxAgeInNanoseconds =
- softwareConfig.vulnerabilities.recent_vulnerability_max_age;
- } else {
- maxAgeInNanoseconds =
- globalConfig?.vulnerabilities.recent_vulnerability_max_age;
- }
- return maxAgeInNanoseconds
- ? Math.round(maxAgeInNanoseconds / 86400000000000) // convert from nanoseconds to days
- : CONFIG_DEFAULT_RECENT_VULNERABILITY_MAX_AGE_IN_DAYS;
- })();
-
- const {
- data: software,
- error: softwareError,
- isFetching: isFetchingSoftware,
- } = useQuery<
- ISoftwareResponse,
- Error,
- ISoftwareResponse,
- ISoftwareQueryKey[]
- >(
- [
- {
- scope: "software",
- page: tableQueryData?.pageIndex,
- perPage: DEFAULT_PAGE_SIZE,
- query: searchQuery,
- orderDirection: sortDirection,
- // API expects "epss_probability" rather than "vulnerabilities"
- orderKey:
- isPremiumTier && sortHeader === "vulnerabilities"
- ? "epss_probability"
- : sortHeader,
- teamId: teamIdForApi,
- vulnerable: filterVuln,
- },
- ],
- ({ queryKey }) => softwareAPI.load(queryKey[0]),
- {
- enabled: isRouteOk && isSoftwareConfigLoaded,
- keepPreviousData: true,
- staleTime: 30000, // stale time can be adjusted if fresher data is desired based on software inventory interval
- }
- );
-
- const {
- data: softwareCount,
- error: softwareCountError,
- isFetching: isFetchingCount,
- } = useQuery(
- [
- {
- scope: "softwareCount",
- query: searchQuery,
- vulnerable: filterVuln,
- teamId: teamIdForApi,
- },
- ],
- ({ queryKey }) => softwareAPI.getCount(queryKey[0]),
- {
- enabled: isRouteOk && isSoftwareConfigLoaded,
- keepPreviousData: true,
- staleTime: 30000, // stale time can be adjusted if fresher data is desired based on software inventory interval
- refetchOnWindowFocus: false,
- retry: 1,
- select: (data) => data.count,
- }
- );
-
- // NOTE: this is called once on initial render and every time the query changes
- const onQueryChange = useCallback(
- async (newTableQuery: ITableQueryData) => {
- if (!isRouteOk || isEqual(newTableQuery, tableQueryData)) {
- return;
- }
-
- setTableQueryData({ ...newTableQuery });
-
- const {
- pageIndex,
- searchQuery: newSearchQuery,
- sortDirection: newSortDirection,
- } = newTableQuery;
- let { sortHeader: newSortHeader } = newTableQuery;
-
- pageIndex !== page && setPage(pageIndex);
- searchQuery !== newSearchQuery && setSearchQuery(newSearchQuery);
- sortDirection !== newSortDirection &&
- setSortDirection(
- newSortDirection === "asc" || newSortDirection === "desc"
- ? newSortDirection
- : DEFAULT_SORT_DIRECTION
- );
-
- if (isPremiumTier && newSortHeader === "vulnerabilities") {
- newSortHeader = "epss_probability";
- }
- sortHeader !== newSortHeader && setSortHeader(newSortHeader);
-
- // Rebuild queryParams to dispatch new browser location to react-router
- const newQueryParams: { [key: string]: string | number | undefined } = {};
- if (!isEmpty(newSearchQuery)) {
- newQueryParams.query = newSearchQuery;
- }
- newQueryParams.page = pageIndex;
- newQueryParams.order_key = newSortHeader || DEFAULT_SORT_HEADER;
- newQueryParams.order_direction =
- newSortDirection || DEFAULT_SORT_DIRECTION;
-
- newQueryParams.vulnerable = filterVuln ? "true" : undefined;
-
- if (teamIdForApi !== undefined) {
- newQueryParams.team_id = teamIdForApi;
- }
-
- const locationPath = getNextLocationPath({
- pathPrefix: PATHS.MANAGE_SOFTWARE,
- routeTemplate,
- queryParams: newQueryParams,
- });
- router.replace(locationPath);
- },
- [
- isRouteOk,
- teamIdForApi,
- tableQueryData,
- page,
- searchQuery,
- sortDirection,
- isPremiumTier,
- sortHeader,
- DEFAULT_SORT_HEADER,
- filterVuln,
- routeTemplate,
- router,
- ]
- );
-
- const toggleManageAutomationsModal = useCallback(() => {
- setShowManageAutomationsModal(!showManageAutomationsModal);
- }, [setShowManageAutomationsModal, showManageAutomationsModal]);
-
- const togglePreviewPayloadModal = useCallback(() => {
- setShowPreviewPayloadModal(!showPreviewPayloadModal);
- }, [setShowPreviewPayloadModal, showPreviewPayloadModal]);
-
- const togglePreviewTicketModal = useCallback(() => {
- setShowPreviewTicketModal(!showPreviewTicketModal);
- }, [setShowPreviewTicketModal, showPreviewTicketModal]);
-
- const onCreateWebhookSubmit = async (
- configSoftwareAutomations: ISoftwareAutomations
- ) => {
- try {
- const request = configAPI.update(configSoftwareAutomations);
- await request.then(() => {
- renderFlash(
- "success",
- "Successfully updated vulnerability automations."
- );
- refetchSoftwareConfig();
- });
- } catch {
- renderFlash(
- "error",
- "Could not update vulnerability automations. Please try again."
- );
- } finally {
- toggleManageAutomationsModal();
- }
- };
-
- const onTeamChange = useCallback(
- (teamId: number) => {
- handleTeamChange(teamId);
- setPage(0);
- },
- [handleTeamChange]
- );
-
- // NOTE: used to reset page number to 0 when modifying filters
- const handleResetPageIndex = () => {
- setTableQueryData(
- (prevState) =>
- ({
- ...prevState,
- pageIndex: 0,
- } as ITableQueryData)
- );
- setResetPageIndex(true);
- };
-
- // NOTE: used to reset page number to 0 when modifying filters
- useEffect(() => {
- // TODO: cleanup this effect
- setResetPageIndex(false);
- }, [queryParams]);
-
- const renderHeaderDescription = () => {
- return (
-
- Search for installed software{" "}
- {(isGlobalAdmin || isGlobalMaintainer) &&
- (!isPremiumTier || !isAnyTeamSelected) &&
- "and manage automations for detected vulnerabilities (CVEs)"}{" "}
- on{" "}
-
- {isPremiumTier && isAnyTeamSelected
- ? "all hosts assigned to this team"
- : "all of your hosts"}
-
- .
-
- );
- };
-
- const renderSoftwareCount = useCallback(() => {
- const count = softwareCount;
- const lastUpdatedAt = software?.counts_updated_at;
-
- if (!isSoftwareEnabled || !lastUpdatedAt) {
- return null;
- }
-
- if (softwareCountError && !isFetchingCount) {
- return (
-
- Failed to load software count
-
- );
- }
-
- if (count) {
- return (
-
- {`${count} software item${count === 1 ? "" : "s"}`}
-
-
- );
- }
-
- return null;
- }, [
- isFetchingCount,
- software,
- softwareCountError,
- softwareCount,
- isSoftwareEnabled,
- ]);
-
- const handleVulnFilterDropdownChange = (isFilterVulnerable: string) => {
- handleResetPageIndex();
-
- router.replace(
- getNextLocationPath({
- pathPrefix: PATHS.MANAGE_SOFTWARE,
- routeTemplate,
- queryParams: {
- ...queryParams,
- vulnerable: isFilterVulnerable,
- page: 0, // resets page index
- },
- })
- );
- };
-
- const renderVulnFilterDropdown = () => {
- return (
-
- );
- };
-
- const renderTableFooter = () => {
- return (
-
- Seeing unexpected software or vulnerabilities?{" "}
-
-
- );
- };
-
- // TODO: Rework after backend is adjusted to differentiate empty search/filter results from
- // collecting inventory
- const isCollectingInventory =
- !searchQuery &&
- !filterVuln &&
- page === 0 &&
- !software?.software &&
- software?.counts_updated_at === null;
-
- const isLastPage =
- tableQueryData &&
- !!softwareCount &&
- DEFAULT_PAGE_SIZE * page + (software?.software?.length || 0) >=
- softwareCount;
-
- const softwareTableHeaders = useMemo(
- () =>
- generateSoftwareTableHeaders(
- router,
- isPremiumTier,
- isSandboxMode,
- currentTeamId
- ),
- [isPremiumTier, isSandboxMode, router, currentTeamId]
- );
- const handleRowSelect = (row: IRowProps) => {
- const hostsBySoftwareParams = {
- software_id: row.original.id,
- team_id: currentTeamId,
- };
-
- const path = hostsBySoftwareParams
- ? `${PATHS.MANAGE_HOSTS}?${buildQueryStringFromParams(
- hostsBySoftwareParams
- )}`
- : PATHS.MANAGE_HOSTS;
-
- router.push(path);
- };
-
- const searchable =
- isSoftwareEnabled &&
- (!!software?.software ||
- searchQuery !== "" ||
- queryParams.vulnerable === "true");
-
- const renderSoftwareTable = () => {
- if (
- (softwareError && !isFetchingSoftware) ||
- (softwareConfigError && !isFetchingSoftwareConfig)
- ) {
- return ;
- }
- return (
- (
-
- )}
- defaultSortHeader={sortHeader || DEFAULT_SORT_HEADER}
- defaultSortDirection={sortDirection || DEFAULT_SORT_DIRECTION}
- defaultPageIndex={page || 0}
- defaultSearchQuery={searchQuery}
- manualSortBy
- pageSize={DEFAULT_PAGE_SIZE}
- showMarkAllPages={false}
- isAllPagesSelected={false}
- resetPageIndex={resetPageIndex}
- disableNextPage={isLastPage}
- searchable={searchable}
- inputPlaceHolder="Search by name or vulnerabilities (CVEs)"
- onQueryChange={onQueryChange}
- additionalQueries={filterVuln ? "vulnerable" : ""} // additionalQueries serves as a trigger
- // for the useDeepEffect hook to fire onQueryChange for events happeing outside of
- // the TableContainer
- customControl={searchable ? renderVulnFilterDropdown : undefined}
- stackControls
- renderCount={renderSoftwareCount}
- renderFooter={renderTableFooter}
- disableMultiRowSelect
- onSelectSingleRow={handleRowSelect}
- />
- );
- };
-
- return (
-
-
-
-
-
-
- {isFreeTier &&
Software }
- {isPremiumTier &&
- ((userTeams && userTeams.length > 1) || isOnGlobalTeam) && (
-
- )}
- {isPremiumTier &&
- !isOnGlobalTeam &&
- userTeams &&
- userTeams.length === 1 && {userTeams[0].name} }
-
-
-
- {canManageAutomations && !softwareError && isSoftwareConfigLoaded && (
-
- Manage automations
-
- )}
-
-
- {renderHeaderDescription()}
-
-
{renderSoftwareTable()}
- {showManageAutomationsModal && (
-
- )}
-
-
- );
-};
-
-export default ManageSoftwarePage;
diff --git a/frontend/pages/software/ManageSoftwarePage/SoftwareTableConfig.tsx b/frontend/pages/software/ManageSoftwarePage/SoftwareTableConfig.tsx
deleted file mode 100644
index 531479d2eb..0000000000
--- a/frontend/pages/software/ManageSoftwarePage/SoftwareTableConfig.tsx
+++ /dev/null
@@ -1,271 +0,0 @@
-import React from "react";
-import { Column } from "react-table";
-import { InjectedRouter } from "react-router";
-import ReactTooltip from "react-tooltip";
-
-import { formatSoftwareType, ISoftware } from "interfaces/software";
-import { IVulnerability } from "interfaces/vulnerability";
-import PATHS from "router/paths";
-import {
- formatFloatAsPercentage,
- getSoftwareBundleTooltipJSX,
-} from "utilities/helpers";
-import { DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants";
-
-import HeaderCell from "components/TableContainer/DataTable/HeaderCell";
-import TextCell from "components/TableContainer/DataTable/TextCell";
-import LinkCell from "components/TableContainer/DataTable/LinkCell/LinkCell";
-import TooltipWrapper from "components/TooltipWrapper";
-import ViewAllHostsLink from "components/ViewAllHostsLink";
-import PremiumFeatureIconWithTooltip from "components/PremiumFeatureIconWithTooltip";
-
-// NOTE: cellProps come from react-table
-// more info here https://react-table.tanstack.com/docs/api/useTable#cell-properties
-interface ICellProps {
- cell: {
- value: number | string | IVulnerability[];
- };
- row: {
- original: ISoftware;
- };
-}
-interface IStringCellProps extends ICellProps {
- cell: {
- value: string;
- };
-}
-
-interface INumberCellProps extends ICellProps {
- cell: {
- value: number;
- };
-}
-
-interface IVulnCellProps extends ICellProps {
- cell: {
- value: IVulnerability[];
- };
-}
-interface IHeaderProps {
- column: {
- title: string;
- isSortedDesc: boolean;
- };
-}
-
-const condenseVulnerabilities = (
- vulnerabilities: IVulnerability[]
-): string[] => {
- const condensed =
- (vulnerabilities?.length &&
- vulnerabilities
- .slice(-3)
- .map((v) => v.cve)
- .reverse()) ||
- [];
- return vulnerabilities.length > 3
- ? condensed.concat(`+${vulnerabilities.length - 3} more`)
- : condensed;
-};
-
-const getMaxProbability = (vulns: IVulnerability[]) =>
- vulns.reduce(
- (max, { epss_probability }) => Math.max(max, epss_probability || 0),
- 0
- );
-
-const generateEPSSColumnHeader = (isSandboxMode = false) => {
- return {
- Header: (headerProps: IHeaderProps): JSX.Element => {
- const titleWithToolTip = (
-
- The probability that this software will be exploited
-
- in the next 30 days (EPSS probability). This data is
-
- reported by FIRST.org.
- >
- }
- >
- Probability of exploit
-
- );
- return (
- <>
- {isSandboxMode && }
-
- >
- );
- },
- disableSortBy: false,
- accessor: "vulnerabilities",
- Cell: (cellProps: IVulnCellProps): JSX.Element => {
- const vulns = cellProps.cell.value || [];
- const maxProbability = (!!vulns.length && getMaxProbability(vulns)) || 0;
- const displayValue =
- (maxProbability && formatFloatAsPercentage(maxProbability)) ||
- DEFAULT_EMPTY_CELL_VALUE;
-
- return (
-
- {displayValue}
-
- );
- },
- };
-};
-
-const generateVulnColumnHeader = () => {
- return {
- title: "Vulnerabilities",
- Header: "Vulnerabilities",
- disableSortBy: true,
- accessor: "vulnerabilities",
- Cell: (cellProps: IVulnCellProps): JSX.Element => {
- const vulnerabilities = cellProps.cell.value || [];
- const tooltipText = condenseVulnerabilities(vulnerabilities)?.map(
- (value) => {
- return (
-
- {value}
-
-
- );
- }
- );
-
- if (!vulnerabilities?.length) {
- return --- ;
- }
- return (
- <>
- 1 ? "text-muted tooltip" : ""
- }`}
- data-tip
- data-for={`vulnerabilities__${cellProps.row.original.id}`}
- data-tip-disable={vulnerabilities.length <= 1}
- >
- {vulnerabilities.length === 1
- ? vulnerabilities[0].cve
- : `${vulnerabilities.length} vulnerabilities`}
-
-
-
- {tooltipText}
-
-
- >
- );
- },
- };
-};
-
-const generateTableHeaders = (
- router: InjectedRouter,
- isPremiumTier?: boolean,
- isSandboxMode?: boolean,
- teamId?: number
-): Column[] => {
- const softwareTableHeaders = [
- {
- title: "Name",
- Header: (cellProps: IHeaderProps): JSX.Element => (
-
- ),
- disableSortBy: false,
- accessor: "name",
- Cell: (cellProps: IStringCellProps): JSX.Element => {
- const { id, name, bundle_identifier: bundle } = cellProps.row.original;
-
- const onClickSoftware = (e: React.MouseEvent) => {
- // Allows for button to be clickable in a clickable row
- e.stopPropagation();
-
- router?.push(PATHS.SOFTWARE_DETAILS(id.toString()));
- };
-
- return (
-
- );
- },
- sortType: "caseInsensitive",
- },
- {
- title: "Version",
- Header: "Version",
- disableSortBy: true,
- accessor: "version",
- Cell: (cellProps: IStringCellProps): JSX.Element => (
-
- ),
- },
- {
- title: "Type",
- Header: "Type",
- disableSortBy: true,
- accessor: "source",
- Cell: (cellProps: IStringCellProps): JSX.Element => (
-
- ),
- },
- isPremiumTier
- ? generateEPSSColumnHeader(isSandboxMode)
- : generateVulnColumnHeader(),
- {
- title: "Hosts",
- Header: (cellProps: IHeaderProps): JSX.Element => (
-
- ),
- disableSortBy: false,
- accessor: "hosts_count",
- Cell: (cellProps: INumberCellProps): JSX.Element => (
-
-
-
-
-
-
-
-
- ),
- },
- ];
-
- return softwareTableHeaders;
-};
-
-export default generateTableHeaders;
diff --git a/frontend/pages/software/ManageSoftwarePage/_styles.scss b/frontend/pages/software/ManageSoftwarePage/_styles.scss
deleted file mode 100644
index ad2d32f106..0000000000
--- a/frontend/pages/software/ManageSoftwarePage/_styles.scss
+++ /dev/null
@@ -1,222 +0,0 @@
-.manage-software-page {
- &__header-wrap {
- display: flex;
- align-items: center;
- justify-content: space-between;
- height: 38px;
-
- .button-wrap {
- display: flex;
- justify-content: flex-end;
- min-width: 266px;
- }
- }
-
- &__manage-automations {
- padding: $pad-small $pad-medium;
- }
-
- &__count {
- display: flex;
- gap: 12px;
- }
-
- &__header {
- display: flex;
- align-items: center;
-
- .form-field {
- margin-bottom: 0;
- }
- }
-
- &__text {
- margin-right: $pad-large;
- }
-
- &__title {
- font-size: $large;
- }
-
- &__description {
- margin: 0;
- margin-bottom: $pad-large;
- max-width: 75%;
- @media (min-width: $break-md) {
- max-width: none;
- }
-
- h2 {
- text-transform: uppercase;
- color: $core-fleet-black;
- font-weight: $regular;
- font-size: $small;
- }
-
- p {
- color: $ui-fleet-black-75;
- margin: 0;
- font-size: $x-small;
- font-style: italic;
- }
- }
-
- &__table {
- .table-container {
- &__header {
- flex-direction: column-reverse; // Search bar on top
-
- @media (min-width: $break-md) {
- flex-direction: row;
- }
- }
-
- &__header-left {
- flex-direction: row; // Filter dropdown aligned with count
- .controls {
- .form-field--dropdown {
- margin: 0;
- }
- .manage-software-page__vuln_dropdown {
- .Select-menu-outer {
- width: 250px;
- max-height: 310px;
- .Select-menu {
- max-height: none;
- }
- }
- .Select-value {
- padding-left: $pad-medium;
- padding-right: $pad-medium;
- }
- .dropdown__custom-value-label {
- width: 155px; // Override 105px for longer text options
- }
- }
- }
- }
- &__search-input,
- &__search {
- width: 100%; // Search bar across entire table
- .input-icon-field__input {
- width: 100%;
- }
- @media (min-width: $break-md) {
- width: auto;
- .input-icon-field__input {
- width: 375px;
- }
- }
- }
- &__data-table-block {
- .data-table-block {
- .data-table__table {
- tr {
- .software-link {
- opacity: 0;
- transition: opacity 250ms;
- }
-
- &:hover {
- .software-link {
- opacity: 1;
- }
- }
- }
-
- thead {
- .name__header {
- width: $col-md;
- }
- .version__header {
- width: 0;
- }
- .vulnerabilities__header {
- display: none;
- width: 0;
- }
- .source__header {
- display: none;
- width: 0;
- }
- .hosts_count__header {
- width: auto;
- border-right: 0;
- }
- @media (min-width: $break-md) {
- .vulnerabilities__header {
- display: table-cell;
- }
- }
- @media (min-width: $break-lg) {
- .version__header {
- width: $col-md;
- }
- .source__header {
- display: table-cell;
- }
- }
- }
-
- tbody {
- .name__cell {
- max-width: $col-md;
- // Tooltip does not get cut off
- .children-wrapper {
- overflow: initial;
- }
- }
- .version__cell {
- width: 0;
- }
- .source__cell {
- display: none;
- width: 0;
- }
- .vulnerabilities__cell {
- display: none;
- width: 0;
- span {
- display: inline;
- }
- .text-muted {
- color: $ui-fleet-black-50;
- }
- }
- .hosts_count__cell {
- width: auto;
- .hosts-cell__wrapper {
- display: flex;
- align-items: center;
- justify-content: space-between;
- .hosts-cell__link {
- display: flex;
- white-space: nowrap;
- }
- }
- }
- @media (min-width: $break-sm) {
- .name__cell {
- max-width: $col-lg;
- }
- }
- @media (min-width: $break-md) {
- .vulnerabilities__cell {
- display: table-cell;
- }
- }
- @media (min-width: $break-lg) {
- .version_cell {
- width: $col-md;
- }
- .source__cell {
- display: table-cell;
- }
- }
- }
- }
- }
- }
- }
- }
-}
diff --git a/frontend/pages/software/ManageSoftwarePage/index.ts b/frontend/pages/software/ManageSoftwarePage/index.ts
deleted file mode 100644
index a10fa5ef7a..0000000000
--- a/frontend/pages/software/ManageSoftwarePage/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from "./ManageSoftwarePage";
diff --git a/frontend/pages/software/SoftwareDetailsPage/SoftwareDetailsPage.tsx b/frontend/pages/software/SoftwareDetailsPage/SoftwareDetailsPage.tsx
deleted file mode 100644
index 7f174f258e..0000000000
--- a/frontend/pages/software/SoftwareDetailsPage/SoftwareDetailsPage.tsx
+++ /dev/null
@@ -1,142 +0,0 @@
-import React, { useContext, useEffect } from "react";
-import { useErrorHandler } from "react-error-boundary";
-import { useQuery } from "react-query";
-import PATHS from "router/paths";
-
-import { AppContext } from "context/app";
-import {
- formatSoftwareType,
- ISoftware,
- IGetSoftwareByIdResponse,
-} from "interfaces/software";
-import { APP_CONTEXT_NO_TEAM_ID } from "interfaces/team";
-import softwareAPI from "services/entities/software";
-import hostCountAPI from "services/entities/host_count";
-
-import { DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants";
-import Spinner from "components/Spinner";
-import BackLink from "components/BackLink";
-import MainContent from "components/MainContent";
-import ViewAllHostsLink from "components/ViewAllHostsLink";
-import Vulnerabilities from "./components/Vulnerabilities";
-
-const baseClass = "software-details-page";
-
-interface ISoftwareDetailsProps {
- params: {
- software_id: string;
- };
-}
-
-const SoftwareDetailsPage = ({
- params: { software_id },
-}: ISoftwareDetailsProps): JSX.Element => {
- const {
- isPremiumTier,
- isSandboxMode,
- currentTeam,
- filteredSoftwarePath,
- } = useContext(AppContext);
-
- const handlePageError = useErrorHandler();
-
- const { data: software, isFetching: isFetchingSoftware } = useQuery<
- IGetSoftwareByIdResponse,
- Error,
- ISoftware
- >(
- ["softwareById", software_id],
- () => softwareAPI.getSoftwareById(software_id),
- {
- select: (data) => data.software,
- onError: (err) => handlePageError(err),
- }
- );
-
- const { data: hostCount } = useQuery<{ count: number }, Error, number>(
- ["hostCountBySoftwareId", software_id],
- () => hostCountAPI.load({ softwareId: parseInt(software_id, 10) }),
- { select: (data) => data.count }
- );
-
- const renderName = (sw: ISoftware) => {
- const { name, version } = sw;
- if (!name) {
- return "--";
- }
- if (!version) {
- return name;
- }
-
- return `${name}, ${version}`;
- };
-
- // Updates title that shows up on browser tabs
- useEffect(() => {
- // e.g., Software horizon, 5.2.0 details | Fleet for osquery
- document.title = `Software details | ${
- software && renderName(software)
- } | Fleet for osquery`;
- }, [location.pathname, software]);
-
- if (!software || isPremiumTier === undefined) {
- return ;
- }
-
- // Function instead of constant eliminates race condition with filteredSoftwarePath
- const backToSoftwarePath = () => {
- if (filteredSoftwarePath) {
- return filteredSoftwarePath;
- }
- return currentTeam && currentTeam?.id > APP_CONTEXT_NO_TEAM_ID
- ? `${PATHS.MANAGE_SOFTWARE}?team_id=${currentTeam?.id}`
- : PATHS.MANAGE_SOFTWARE;
- };
-
- return (
-
-
-
-
-
-
-
-
-
{renderName(software)}
-
-
-
-
-
-
-
-
- Type
-
- {formatSoftwareType(software.source)}
-
-
-
- Hosts
-
- {hostCount || DEFAULT_EMPTY_CELL_VALUE}
-
-
-
-
-
-
-
-
- );
-};
-
-export default SoftwareDetailsPage;
diff --git a/frontend/pages/software/SoftwareDetailsPage/_styles.scss b/frontend/pages/software/SoftwareDetailsPage/_styles.scss
deleted file mode 100644
index 3d8a569222..0000000000
--- a/frontend/pages/software/SoftwareDetailsPage/_styles.scss
+++ /dev/null
@@ -1,80 +0,0 @@
-.software-details-page {
- background-color: $ui-off-white;
-
- &__wrapper {
- display: grid;
- gap: $pad-medium;
- }
-
- .header {
- flex: 100%;
- display: flex;
- flex-direction: column;
- }
-
- .section {
- flex: 100%;
- display: flex;
- flex-direction: column;
- background-color: $core-white;
- border-radius: 16px;
- border: 1px solid $ui-fleet-black-10;
- padding: $pad-xxlarge;
- box-shadow: 0px 3px 0px rgba(226, 228, 234, 0.4);
-
- &__header {
- font-size: $medium;
- font-weight: $bold;
- margin: 0 0 $pad-large 0;
- }
-
- .info-flex {
- display: flex;
- flex-wrap: wrap;
-
- .info-flex__item--title {
- margin-bottom: 2.5rem;
- }
-
- &__item {
- font-size: $x-small;
- display: flex;
- flex-direction: column;
- white-space: nowrap;
-
- &--title {
- margin-right: $pad-xxlarge;
-
- .info-flex__data {
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- }
- }
- }
-
- &__header {
- color: $core-fleet-black;
- font-weight: $bold;
- }
- }
- }
-
- .title,
- .info {
- flex-direction: row;
- justify-content: space-between;
- margin: 0;
- padding-bottom: 0;
- }
-
- .name-container {
- display: flex;
- align-items: center;
- }
-
- .name {
- font-size: $large;
- font-weight: $bold;
- }
-}
diff --git a/frontend/pages/software/SoftwareDetailsPage/components/Vulnerabilities/Vulnerabilities.tests.tsx b/frontend/pages/software/SoftwareDetailsPage/components/Vulnerabilities/Vulnerabilities.tests.tsx
deleted file mode 100644
index bd98579dfe..0000000000
--- a/frontend/pages/software/SoftwareDetailsPage/components/Vulnerabilities/Vulnerabilities.tests.tsx
+++ /dev/null
@@ -1,105 +0,0 @@
-import React from "react";
-import { render, screen } from "@testing-library/react";
-
-import createMockSoftware from "__mocks__/softwareMock";
-
-import Vulnerabilities from "./Vulnerabilities";
-
-describe("Vulnerabilities", () => {
- const [mockSoftwareWithVuln, mockSoftwareNoVulns] = [
- createMockSoftware({
- vulnerabilities: [
- {
- cve: "CVE_333",
- details_link: "https://its.really.bad",
- cvss_score: 9.5,
- epss_probability: 1,
- cisa_known_exploit: false,
- cve_published: "2023-02-14T20:15:00Z",
- },
- ],
- }),
- createMockSoftware(),
- ];
-
- it("renders the empty state when no vulnerabilities are provided", () => {
- render(
-
- );
-
- // Empty state
- expect(
- screen.getByText("No vulnerabilities detected for this software item.")
- ).toBeInTheDocument();
- expect(
- screen.getByText("Expecting to see vulnerabilities?")
- ).toBeInTheDocument();
- expect(screen.getByText("File an issue on GitHub")).toBeInTheDocument();
- });
-
- it("correctly renders a table when 1 vulnerability is provided, Premium tier", () => {
- render(
-
- );
-
- // Rendered table
- expect(screen.getByText("Vulnerability")).toBeInTheDocument();
- expect(screen.getByText("Probability of exploit")).toBeInTheDocument();
- expect(screen.getByText("Severity")).toBeInTheDocument();
- expect(screen.getByText("Known exploit")).toBeInTheDocument();
- expect(screen.getByText("Published")).toBeInTheDocument();
- expect(screen.getByText("CVE_333")).toBeInTheDocument();
- expect(screen.getByText("100%")).toBeInTheDocument();
- expect(screen.getByText("Critical", { exact: false })).toBeInTheDocument();
- expect(screen.getByText("ago", { exact: false })).toBeInTheDocument();
- });
-
- it("Only renders the 'Vulnerability' column when 1 vulnerability is provided on Free tier", () => {
- render(
-
- );
-
- // Rendered table
- expect(screen.getByText("Vulnerability")).toBeInTheDocument();
-
- // No premium-only columns
- expect(screen.queryByText("Probability of exploit")).toBeNull();
- expect(screen.queryByText("Severity")).toBeNull();
- expect(screen.queryByText("Known exploit")).toBeNull();
- expect(screen.queryByText("Published")).toBeNull();
-
- // Row data
- expect(screen.getByText("CVE_333")).toBeInTheDocument();
- expect(screen.queryByText("100%")).toBeNull();
- expect(screen.queryByText("Critical", { exact: false })).toBeNull();
- expect(screen.queryByText("ago", { exact: false })).toBeNull();
- });
-
- // Test for premium icons on column headers in Sandbox mode
- it("Renders 4 'Premium feature' tooltips when in premium tier Sandbox mode", () => {
- render(
-
- );
-
- expect(
- screen.getAllByText("This is a Fleet Premium feature.", { exact: false })
- ).toHaveLength(4);
- });
-});
diff --git a/frontend/pages/software/SoftwareDetailsPage/components/Vulnerabilities/Vulnerabilities.tsx b/frontend/pages/software/SoftwareDetailsPage/components/Vulnerabilities/Vulnerabilities.tsx
deleted file mode 100644
index 0446c6f20d..0000000000
--- a/frontend/pages/software/SoftwareDetailsPage/components/Vulnerabilities/Vulnerabilities.tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-import React, { useMemo } from "react";
-
-import { ISoftware } from "interfaces/software";
-import { GITHUB_NEW_ISSUE_LINK } from "utilities/constants";
-
-import TableContainer from "components/TableContainer";
-import CustomLink from "components/CustomLink";
-import EmptyTable from "components/EmptyTable";
-
-import generateVulnTableHeaders from "./VulnTableConfig";
-
-const baseClass = "vulnerabilities";
-
-interface IVulnerabilitiesProps {
- isLoading: boolean;
- isPremiumTier: boolean;
- isSandboxMode?: boolean;
- software: ISoftware;
-}
-
-const NoVulnsDetected = (): JSX.Element => {
- return (
-
- Expecting to see vulnerabilities?{" "}
-
- >
- }
- />
- );
-};
-
-const Vulnerabilities = ({
- isLoading,
- isPremiumTier,
- isSandboxMode = false,
- software,
-}: IVulnerabilitiesProps): JSX.Element => {
- const tableHeaders = useMemo(
- () => generateVulnTableHeaders(isPremiumTier, isSandboxMode),
- [isPremiumTier, isSandboxMode]
- );
-
- return (
-
-
Vulnerabilities
- {software?.vulnerabilities?.length ? (
- <>
- {software && (
-
- )}
- >
- ) : (
-
- )}
-
- );
-};
-export default Vulnerabilities;
diff --git a/frontend/pages/software/SoftwareDetailsPage/components/Vulnerabilities/_styles.scss b/frontend/pages/software/SoftwareDetailsPage/components/Vulnerabilities/_styles.scss
deleted file mode 100644
index a42e892e85..0000000000
--- a/frontend/pages/software/SoftwareDetailsPage/components/Vulnerabilities/_styles.scss
+++ /dev/null
@@ -1,60 +0,0 @@
-.software-details-page {
- &__hosts-link {
- display: flex;
- align-items: center;
- padding-bottom: $pad-small;
- height: 20px;
- font-size: $x-small;
- color: $core-vibrant-blue;
- font-weight: $bold;
- text-decoration: none;
- }
-
- #right-chevron {
- width: 16px;
- height: 16px;
- margin-left: $pad-small;
- }
-
- .section--vulnerabilities {
- .component__tooltip-wrapper__tip-text {
- max-width: $col-md;
- white-space: normal;
- }
-
- .data-table-block {
- .data-table__table {
- thead {
- .cve__header {
- width: $col-md;
- }
- .epss_probability__header {
- width: $col-sm;
- }
- .cvss_score__header {
- width: $col-sm;
- }
-
- @media (max-width: $tooltip-break-md) {
- .cisa_known_exploit__header {
- .component__tooltip-wrapper__tip-text {
- max-width: 200px; // Prevents horizontal scrolling off viewport
- white-space: normal;
- }
- }
- }
- }
-
- tr {
- .text-link {
- img {
- height: 12px;
- width: auto;
- padding-left: $pad-small;
- }
- }
- }
- }
- }
- }
-}
diff --git a/frontend/pages/software/SoftwareDetailsPage/components/Vulnerabilities/index.ts b/frontend/pages/software/SoftwareDetailsPage/components/Vulnerabilities/index.ts
deleted file mode 100644
index 184d8bd8da..0000000000
--- a/frontend/pages/software/SoftwareDetailsPage/components/Vulnerabilities/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from "./Vulnerabilities";
diff --git a/frontend/pages/software/SoftwareDetailsPage/index.ts b/frontend/pages/software/SoftwareDetailsPage/index.ts
deleted file mode 100644
index 0de53678fa..0000000000
--- a/frontend/pages/software/SoftwareDetailsPage/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from "./SoftwareDetailsPage";
diff --git a/frontend/router/index.tsx b/frontend/router/index.tsx
index 645c3616d2..5d947a7fb4 100644
--- a/frontend/router/index.tsx
+++ b/frontend/router/index.tsx
@@ -29,7 +29,6 @@ import LabelPage from "pages/LabelPage";
import LoginPage, { LoginPreviewPage } from "pages/LoginPage";
import LogoutPage from "pages/LogoutPage";
import ManageHostsPage from "pages/hosts/ManageHostsPage";
-import ManageSoftwarePage from "pages/software/ManageSoftwarePage";
import ManageQueriesPage from "pages/queries/ManageQueriesPage";
import ManagePacksPage from "pages/packs/ManagePacksPage";
import ManagePoliciesPage from "pages/policies/ManagePoliciesPage";
@@ -43,7 +42,6 @@ import RegistrationPage from "pages/RegistrationPage";
import ResetPasswordPage from "pages/ResetPasswordPage";
import MDMAppleSSOPage from "pages/MDMAppleSSOPage";
import MDMAppleSSOCallbackPage from "pages/MDMAppleSSOCallbackPage";
-import SoftwareDetailsPage from "pages/software/SoftwareDetailsPage";
import ApiOnlyUser from "pages/ApiOnlyUser";
import Fleet403 from "pages/errors/Fleet403";
import Fleet404 from "pages/errors/Fleet404";
@@ -59,6 +57,11 @@ import WindowsMdmPage from "pages/admin/IntegrationsPage/cards/MdmSettings/Windo
import MacOSMdmPage from "pages/admin/IntegrationsPage/cards/MdmSettings/MacOSMdmPage";
import Scripts from "pages/ManageControlsPage/Scripts/Scripts";
import WindowsAutomaticEnrollmentPage from "pages/admin/IntegrationsPage/cards/AutomaticEnrollment/WindowsAutomaticEnrollmentPage";
+import SoftwarePage from "pages/SoftwarePage";
+import SoftwareTitles from "pages/SoftwarePage/SoftwareTitles";
+import SoftwareVersions from "pages/SoftwarePage/SoftwareVersions";
+import SoftwareTitleDetailsPage from "pages/SoftwarePage/SoftwareTitleDetailsPage";
+import SoftwareVersionDetailsPage from "pages/SoftwarePage/SoftwareVersionDetailsPage";
import PATHS from "router/paths";
@@ -206,9 +209,13 @@ const routes = (
-
-
-
+
+
+
+
+
+
+
diff --git a/frontend/router/paths.ts b/frontend/router/paths.ts
index 6babce6bba..217a951694 100644
--- a/frontend/router/paths.ts
+++ b/frontend/router/paths.ts
@@ -43,6 +43,16 @@ export default {
ADMIN_ORGANIZATION_ADVANCED: `${URL_PREFIX}/settings/organization/advanced`,
ADMIN_ORGANIZATION_FLEET_DESKTOP: `${URL_PREFIX}/settings/organization/fleet-desktop`,
+ // Software pages
+ SOFTWARE_TITLES: `${URL_PREFIX}/software/titles`,
+ SOFTWARE_VERSIONS: `${URL_PREFIX}/software/versions`,
+ SOFTWARE_TITLE_DETAILS: (id: string): string => {
+ return `${URL_PREFIX}/software/titles/${id}`;
+ },
+ SOFTWARE_VERSION_DETAILS: (id: string): string => {
+ return `${URL_PREFIX}/software/versions/${id}`;
+ },
+
EDIT_PACK: (packId: number): string => {
return `${URL_PREFIX}/packs/${packId}/edit`;
},
@@ -107,10 +117,7 @@ export default {
DEVICE_USER_DETAILS_POLICIES: (deviceAuthToken: string): string => {
return `${URL_PREFIX}/device/${deviceAuthToken}/policies`;
},
- MANAGE_SOFTWARE: `${URL_PREFIX}/software/manage`,
- SOFTWARE_DETAILS: (id: string): string => {
- return `${URL_PREFIX}/software/${id}`;
- },
+
TEAM_DETAILS_MEMBERS: (teamId?: number): string => {
if (teamId !== undefined && teamId > 0) {
return `${URL_PREFIX}/settings/teams/members?team_id=${teamId}`;
diff --git a/frontend/services/entities/host_count.ts b/frontend/services/entities/host_count.ts
index 7c30d17b35..fb04978c07 100644
--- a/frontend/services/entities/host_count.ts
+++ b/frontend/services/entities/host_count.ts
@@ -40,6 +40,8 @@ export interface IHostCountLoadOptions {
policyResponse?: string;
macSettingsStatus?: MacSettingsStatusQueryParam;
softwareId?: number;
+ softwareTitleId?: number;
+ softwareVersionId?: number;
lowDiskSpaceHosts?: number;
mdmId?: number;
mdmEnrollmentStatus?: string;
@@ -62,6 +64,8 @@ export default {
const globalFilter = options?.globalFilter || "";
const teamId = options?.teamId;
const softwareId = options?.softwareId;
+ const softwareTitleId = options?.softwareTitleId;
+ const softwareVersionId = options?.softwareVersionId;
const macSettingsStatus = options?.macSettingsStatus;
const status = options?.status;
const mdmId = options?.mdmId;
@@ -91,6 +95,8 @@ export default {
mdmEnrollmentStatus,
munkiIssueId,
softwareId,
+ softwareTitleId,
+ softwareVersionId,
lowDiskSpaceHosts,
osName,
osId,
diff --git a/frontend/services/entities/hosts.ts b/frontend/services/entities/hosts.ts
index 65f8b1b241..f9ebde9697 100644
--- a/frontend/services/entities/hosts.ts
+++ b/frontend/services/entities/hosts.ts
@@ -9,7 +9,7 @@ import {
reconcileMutuallyInclusiveHostParams,
} from "utilities/url";
import { SelectedPlatform } from "interfaces/platform";
-import { ISoftware } from "interfaces/software";
+import { ISoftwareTitle, ISoftware } from "interfaces/software";
import {
DiskEncryptionStatus,
BootstrapPackageStatus,
@@ -25,7 +25,8 @@ export interface ISortOption {
export interface ILoadHostsResponse {
hosts: IHost[];
- software: ISoftware;
+ software: ISoftware | undefined;
+ software_title: ISoftwareTitle | undefined;
munki_issue: IMunkiIssuesAggregate;
mobile_device_management_solution: IMdmSolution;
}
@@ -55,6 +56,8 @@ export interface ILoadHostsOptions {
policyResponse?: string;
macSettingsStatus?: MacSettingsStatusQueryParam;
softwareId?: number;
+ softwareTitleId?: number;
+ softwareVersionId?: number;
status?: HostStatus;
mdmId?: number;
mdmEnrollmentStatus?: string;
@@ -82,6 +85,8 @@ export interface IExportHostsOptions {
policyResponse?: string;
macSettingsStatus?: MacSettingsStatusQueryParam;
softwareId?: number;
+ softwareTitleId?: number;
+ softwareVersionId?: number;
status?: HostStatus;
mdmId?: number;
munkiIssueId?: number;
@@ -177,6 +182,8 @@ export default {
const policyId = options?.policyId;
const policyResponse = options?.policyResponse || "passing";
const softwareId = options?.softwareId;
+ const softwareTitleId = options?.softwareTitleId;
+ const softwareVersionId = options?.softwareVersionId;
const macSettingsStatus = options?.macSettingsStatus;
const status = options?.status;
const mdmId = options?.mdmId;
@@ -209,6 +216,8 @@ export default {
mdmEnrollmentStatus,
munkiIssueId,
softwareId,
+ softwareTitleId,
+ softwareVersionId,
lowDiskSpaceHosts,
osSettings,
diskEncryptionStatus,
@@ -234,6 +243,8 @@ export default {
policyResponse = "passing",
macSettingsStatus,
softwareId,
+ softwareTitleId,
+ softwareVersionId,
status,
mdmId,
mdmEnrollmentStatus,
@@ -273,6 +284,8 @@ export default {
mdmEnrollmentStatus,
munkiIssueId,
softwareId,
+ softwareTitleId,
+ softwareVersionId,
lowDiskSpaceHosts,
osId,
osName,
diff --git a/frontend/services/entities/software.ts b/frontend/services/entities/software.ts
index 54f829171d..66bfd74161 100644
--- a/frontend/services/entities/software.ts
+++ b/frontend/services/entities/software.ts
@@ -6,10 +6,12 @@ import {
ISoftwareResponse,
ISoftwareCountResponse,
IGetSoftwareByIdResponse,
+ ISoftwareVersion,
+ ISoftwareTitle,
} from "interfaces/software";
import { buildQueryStringFromParams, QueryParams } from "utilities/url";
-interface ISoftwareApiParams {
+export interface ISoftwareApiParams {
page?: number;
perPage?: number;
orderKey?: string;
@@ -19,6 +21,34 @@ interface ISoftwareApiParams {
teamId?: number;
}
+export interface ISoftwareTitlesResponse {
+ counts_updated_at: string | null;
+ count: number;
+ software_titles: ISoftwareTitle[];
+ meta: {
+ has_next_results: boolean;
+ has_previous_results: boolean;
+ };
+}
+
+export interface ISoftwareVersionsResponse {
+ counts_updated_at: string | null;
+ count: number;
+ software: ISoftwareVersion[];
+ meta: {
+ has_next_results: boolean;
+ has_previous_results: boolean;
+ };
+}
+
+export interface ISoftwareTitleResponse {
+ software_title: ISoftwareTitle;
+}
+
+export interface ISoftwareVersionResponse {
+ software: ISoftwareVersion;
+}
+
export interface ISoftwareQueryKey extends ISoftwareApiParams {
scope: "software";
}
@@ -103,4 +133,30 @@ export default {
return sendRequest("GET", path);
},
+
+ getSoftwareTitles: (params: ISoftwareApiParams) => {
+ const { SOFTWARE_TITLES } = endpoints;
+ const snakeCaseParams = convertParamsToSnakeCase(params);
+ const queryString = buildQueryStringFromParams(snakeCaseParams);
+ const path = `${SOFTWARE_TITLES}?${queryString}`;
+ return sendRequest("GET", path);
+ },
+
+ getSoftwareTitle: (id: number) => {
+ const { SOFTWARE_TITLE } = endpoints;
+ return sendRequest("GET", SOFTWARE_TITLE(id));
+ },
+
+ getSoftwareVersions: (params: ISoftwareApiParams) => {
+ const { SOFTWARE_VERSIONS } = endpoints;
+ const snakeCaseParams = convertParamsToSnakeCase(params);
+ const queryString = buildQueryStringFromParams(snakeCaseParams);
+ const path = `${SOFTWARE_VERSIONS}?${queryString}`;
+ return sendRequest("GET", path);
+ },
+
+ getSoftwareVersion: (id: number) => {
+ const { SOFTWARE_VERSION } = endpoints;
+ return sendRequest("GET", SOFTWARE_VERSION(id));
+ },
};
diff --git a/frontend/utilities/endpoints.ts b/frontend/utilities/endpoints.ts
index f93487b2a2..545ed0d666 100644
--- a/frontend/utilities/endpoints.ts
+++ b/frontend/utilities/endpoints.ts
@@ -91,7 +91,15 @@ export default {
return `/${API_VERSION}/fleet/packs/${packId}/scheduled`;
},
SETUP: `/v1/setup`, // not a typo - hasn't been updated yet
+
+ // Software endpoints
SOFTWARE: `/${API_VERSION}/fleet/software`,
+ SOFTWARE_TITLES: `/${API_VERSION}/fleet/software/titles`,
+ SOFTWARE_TITLE: (id: number) => `/${API_VERSION}/fleet/software/titles/${id}`,
+ SOFTWARE_VERSIONS: `/${API_VERSION}/fleet/software/versions`,
+ SOFTWARE_VERSION: (id: number) =>
+ `/${API_VERSION}/fleet/software/versions/${id}`,
+
SSO: `/v1/fleet/sso`,
STATUS_LABEL_COUNTS: `/${API_VERSION}/fleet/host_summary`,
STATUS_LIVE_QUERY: `/${API_VERSION}/fleet/status/live_query`,
@@ -126,7 +134,7 @@ export default {
USERS_ADMIN: `/${API_VERSION}/fleet/users/admin`,
VERSION: `/${API_VERSION}/fleet/version`,
- // SCRIPTS
+ // Script endpoints
HOST_SCRIPTS: (id: number) => `/${API_VERSION}/fleet/hosts/${id}/scripts`,
SCRIPTS: `/${API_VERSION}/fleet/scripts`,
SCRIPT: (id: number) => `/${API_VERSION}/fleet/scripts/${id}`,
diff --git a/frontend/utilities/url/index.ts b/frontend/utilities/url/index.ts
index 5fcf792bbc..0f4c9a408f 100644
--- a/frontend/utilities/url/index.ts
+++ b/frontend/utilities/url/index.ts
@@ -30,6 +30,8 @@ interface IMutuallyExclusiveHostParams {
munkiIssueId?: number;
lowDiskSpaceHosts?: number;
softwareId?: number;
+ softwareVersionId?: number;
+ softwareTitleId?: number;
osId?: number;
osName?: string;
osVersion?: string;
@@ -100,6 +102,8 @@ export const reconcileMutuallyExclusiveHostParams = ({
munkiIssueId,
lowDiskSpaceHosts,
softwareId,
+ softwareVersionId,
+ softwareTitleId,
osId,
osName,
osVersion,
@@ -131,6 +135,10 @@ export const reconcileMutuallyExclusiveHostParams = ({
return { mdm_enrollment_status: mdmEnrollmentStatus };
case !!munkiIssueId:
return { munki_issue_id: munkiIssueId };
+ case !!softwareTitleId:
+ return { software_title_id: softwareTitleId };
+ case !!softwareVersionId:
+ return { software_version_id: softwareVersionId };
case !!softwareId:
return { software_id: softwareId };
case !!osId:
@@ -141,7 +149,6 @@ export const reconcileMutuallyExclusiveHostParams = ({
return { low_disk_space: lowDiskSpaceHosts };
case !!osSettings:
return { [HOSTS_QUERY_PARAMS.OS_SETTINGS]: osSettings };
-
case !!diskEncryptionStatus:
return { [HOSTS_QUERY_PARAMS.DISK_ENCRYPTION]: diskEncryptionStatus };
case !!bootstrapPackageStatus:
diff --git a/server/fleet/software.go b/server/fleet/software.go
index 089ea808f6..da30d346e6 100644
--- a/server/fleet/software.go
+++ b/server/fleet/software.go
@@ -120,7 +120,7 @@ type SoftwareVersion struct {
// Version is the version string we grab for this specific software.
Version string `db:"version" json:"version"`
// Vulnerabilities is the list of CVE names for vulnerabilities found for this version.
- Vulnerabilities *SliceString `db:"vulnerabilities" json:"vulnerabilities,omitempty"`
+ Vulnerabilities *SliceString `db:"vulnerabilities" json:"vulnerabilities"`
// HostsCount is the number of hosts that use this software version.
HostsCount *uint `db:"hosts_count" json:"hosts_count,omitempty"`