diff --git a/.github/ISSUE_TEMPLATE/story.md b/.github/ISSUE_TEMPLATE/story.md index 3c6d4cbaee9e..a67793a470b1 100644 --- a/.github/ISSUE_TEMPLATE/story.md +++ b/.github/ISSUE_TEMPLATE/story.md @@ -19,11 +19,21 @@ It is [planned and ready](https://fleetdm.com/handbook/company/development-group | I want to _________________________________________ | so that I can _________________________________________. +## Context +- Requestor(s): _________________________ +- Product designer: _________________________ + + + ## Changes ### Product - [ ] UI changes: TODO -- [ ] CLI usage changes: TODO +- [ ] CLI usage changes: TODO - [ ] REST API changes: TODO - [ ] Permissions changes: TODO - [ ] Outdated documentation changes: TODO @@ -35,14 +45,6 @@ It is [planned and ready](https://fleetdm.com/handbook/company/development-group > ℹ️  Please read this issue carefully and understand it. Pay [special attention](https://fleetdm.com/handbook/company/development-groups#developing-from-wireframes) to UI wireframes, especially "dev notes". -## Context -- Requestor(s): _________________________ - - ## QA ### Risk assessment diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 5d97bc6d97c7..37e1fd98506d 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -47,6 +47,11 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + + - name: Set up Go + uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 + with: + go-version: ${{ vars.GO_VERSION }} # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/goreleaser-orbit.yaml b/.github/workflows/goreleaser-orbit.yaml index 613b5fea0789..1efd3faec1cb 100644 --- a/.github/workflows/goreleaser-orbit.yaml +++ b/.github/workflows/goreleaser-orbit.yaml @@ -66,7 +66,7 @@ jobs: uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v2 with: name: orbit-macos - path: dist + path: dist/orbit-macos_darwin_all/orbit goreleaser-linux: runs-on: ubuntu-20.04 @@ -94,7 +94,7 @@ jobs: uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v2 with: name: orbit-linux - path: dist + path: dist/orbit_linux_amd64_v1/orbit goreleaser-windows: runs-on: windows-2022 @@ -122,4 +122,4 @@ jobs: uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v2 with: name: orbit-windows - path: dist + path: dist/orbit_windows_amd64_v1/orbit.exe diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a50c0c809bf..66937e9c8ba2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,68 @@ +## Fleet 4.45.1 (Feb 23, 2024) + +### Bug fixes + +* Fixed a bug that caused macOS ADE enrollments gated behind SSO to get a "method not allowed" error. +* Fixed a bug where the "Done" button on the add hosts modal for plain osquery could be covered. + +## Fleet 4.45.0 (Feb 20, 2024) + +### Changes + +* **Endpoint operations**: + - Added two new API endpoints for running provided live query SQL on a single host. + - Added `fleetctl gitops` command for GitOps workflow synchronization. + - Added capabilities to the `gitops` role to support reading queries/policies and writing scripts. + - Updated policy names to be unique per team. + - Updated fleetd-chrome to use the latest wa-sqlite v0.9.11. + - Updated "Add hosts" modal UI to dynamically include the `--enable-scripts` flag. + - Added count of upcoming activities to host vitals UI. + - Updated UI to include upcoming activity counts in host vitals. + - Updated 405 response for `POST` requests on the root path to highlight misconfigured osquery instances. + +* **Device management (MDM)**: + - Added MDM command payloads to the response of `GET /api/_version_/fleet/mdm/commandresults`. + - Changed several MDM-related endpoints to be platform-agnostic. + - Added script capabilities to UI for Linux hosts. + - Added UI for locking and unlocking hosts managed by Fleet MDM. + - Added `fleetctl mdm lock` and `fleetctl mdm unlock` commands. + - Added validation to reject script enqueue requests for hosts without fleetd. + - Added the `host_mdm_actions` DB table for MDM lock and wipe functionality. + - Updated backend MDM migration flow and added logging. + - Updated UI text for disk encryption to reflect cross-platform functionality. + - Renamed and updated fields in MDM configuration profiles for clarity. + - Improved validation of Windows profiles to prevent delivery errors. + - Improved Windows MDM profile error tooltip messages. + - Fixed MDM unlock flow and updated lock/unlock functionality for Windows and Linux. + - Fixed a bug that would cause OS Settings verification to fail with MySQL's `only_full_group_by` mode enabled. + +* **Vulnerability management**: + - Windows OS Vulnerabilities now include a `resolved_in_version` in the `/os_versions` API response. + - Fixed an issue where software from a Parallels VM would incorrectly appear as the host's software. + - Implemented permission checks for software and software titles. + - Fixed software title aggregation when triggering vulnerability scans. + +### Bug fixes and improvements + - Updated text and style across the app for consistency and clarity. + - Improved UI for the view disk encryption key, host details activity card, and "Add hosts" modal. + - Addressed a bug where updating the search field caused unwanted loss of focus. + - Corrected alignment bugs on empty table states for software details. + - Updated URL query parameters to reset when switching tabs. + - Fixed device page showing invalid date for the last restarted. + - Fixed visual display issues with chevron right icons on Chrome. + - Fixed Windows vulnerabilities without exploit/severity from crashing the software page. + - Fixed issues with checkboxes in hidden modals and long enroll secrets overlapping action buttons. + - Fixed a bug with built-in platform labels. + - Fixed enroll secret error messaging showing secret in cleartext. + - Fixed various UI bugs including disk encryption key input icons, alignment issues, and dropdown menus. + - Fixed dropdown behavior in administrative settings and software title/version tables. + - Fixed various UI and style bugs, including issues with long OS names causing table render issues. + - Fixed a bug where checkboxes within a hidden modal were not correctly hidden. + - Fixed vulnerable software dropdown from switching back to all teams. + - Fixed wall_time to report in milliseconds for consistency with other query performance stats. + - Fixed generating duplicate activities when locking or unlocking a host with scripts disabled. + - Fixed how errors are reported to APM to avoid duplicates and improve stack trace accuracy. + ## Fleet 4.44.1 (Feb 13, 2024) ### Bug fixes diff --git a/Dockerfile-desktop-linux b/Dockerfile-desktop-linux index 63fd08aebd4b..eb3828939b83 100644 --- a/Dockerfile-desktop-linux +++ b/Dockerfile-desktop-linux @@ -1,4 +1,4 @@ -FROM --platform=linux/amd64 golang:1.21.6-bullseye@sha256:fa52abd182d334cfcdffdcc934e21fcfbc71c3cde568e606193ae7db045b1b8d +FROM --platform=linux/amd64 golang:1.21.7-bullseye@sha256:447afe790df28e0bc19d782a9f776a105ce3b8417cdd21f33affc4ed6d38f9d5 LABEL maintainer="Fleet Developers" RUN apt-get update && apt-get install -y \ diff --git a/articles/fleet-4.26.0.md b/articles/fleet-4.26.0.md index 92cf01190ab1..20d7522c494e 100644 --- a/articles/fleet-4.26.0.md +++ b/articles/fleet-4.26.0.md @@ -48,8 +48,6 @@ You already have a lot of raw data to sift through in your data lake, especially Fleet 4.26.0 reduces the number of calls you have to make to pull software data with the REST API. Each time a host has software added, updated, or deleted, a `host_software_updated_at` timestamp gets updated for that host. The `host_software_updated_at` timestamp is exposed through the API. This lets you send the latest software data to your data lake, so you can avoid drowning in outdated information. - - ## Fleet MDM **MDM features are not ready for production and are currently in development. These features are disabled by default.** diff --git a/articles/fleet-4.27.0.md b/articles/fleet-4.27.0.md index 6638c2cf05b6..8ecccd7ffcd6 100644 --- a/articles/fleet-4.27.0.md +++ b/articles/fleet-4.27.0.md @@ -21,8 +21,6 @@ In the UI an account administrator will see the following information: If you pair this new login activity with the audit improvements from [release 4.26](https://fleetdm.com/releases/fleet-4.26.0) you can now set up an alert if multiple failed login attempts occur. - - ## Better search filters on the ‘Select Targets’ screen in Fleet **Available in Fleet Free and Fleet Premium** diff --git a/articles/fleet-4.28.0.md b/articles/fleet-4.28.0.md index 3392e0526d95..26fd4ea6105e 100644 --- a/articles/fleet-4.28.0.md +++ b/articles/fleet-4.28.0.md @@ -32,8 +32,6 @@ Premium and Ultimate Fleet plans have the ability to import the CIS benchmarks i For more information on adding CIS Benchmarks, check out the [documentation here](https://fleetdm.com/docs/using-fleet/cis-benchmarks#how-to-add-cis-benchmarks). - - ## Reduced false negatives from MS Office products related to vulnerabilities reported in the NVD A false negative occurs when a policy reports there is not a vulnerability, but there actually is a vulnerability. Even if a policy reports zero vulnerabilities, that does not imply there are no vulnerabilities present. Both of these types of errors can cause problems when trying to identify vulnerabilities that need attention. @@ -69,8 +67,6 @@ For more information on enabling this functionality, check out the [documentati * Enabled installation and auto-updates of Nudge via Orbit. * Added support for providing macos\_settings.custom\_settings profiles for team (with Fleet Premium) and no-team levels via fleetctl apply. - - #### List of other features * Added --policies-team flag to fleetctl apply to easily import a group of policies into a team. diff --git a/articles/fleet-4.29.0.md b/articles/fleet-4.29.0.md index 39b9d7be60a0..9c13562ac389 100644 --- a/articles/fleet-4.29.0.md +++ b/articles/fleet-4.29.0.md @@ -27,8 +27,6 @@ Users created via JIT provisioning can be assigned Fleet roles using SAML custom Learn more about [JIT user role setting](https://fleetdm.com/docs/deploying/configuration#just-in-time-jit-user-provisioning). - - ## CIS benchmarks manual intervention _Available in Fleet Premium and Fleet Ultimate_ @@ -65,8 +63,6 @@ Fleet updated translation rules to provide better 🟢 Results and avoid false p * Added MDM profiles status filter to hosts endpoints. * Added indicators of aggregate host count for each possible status of MDM-enforced mac settings (hidden until 4.30.0). - - #### List of other features * As part of JIT provisioning, read user roles from SAML custom attributes. diff --git a/articles/fleet-4.45.0.md b/articles/fleet-4.45.0.md new file mode 100644 index 000000000000..6574a26a2cd5 --- /dev/null +++ b/articles/fleet-4.45.0.md @@ -0,0 +1,120 @@ +# Fleet 4.45.0 | Remote lock, Linux script library, osquery storage location. + +![Fleet 4.45.0](../website/assets/images/articles/fleet-4.45.0-1600x900@2x.png) + +Fleet 4.45.0 is live. Check out the full [changelog](https://github.com/fleetdm/fleet/releases/tag/fleet-v4.45.0) or continue reading to get the highlights. +For upgrade instructions, see our [upgrade guide](https://fleetdm.com/docs/deploying/upgrading-fleet) in the Fleet docs. + +## Highlights + +* Remote lock for macOS, Windows, and Linux +* Linux script library +* Customizable osquery data storage location + + +### Remote lock for macOS, Windows, and Linux + +Fleet expands its device management capabilities with remote lock functionalities for macOS, Windows, and Linux systems. This development allows administrators to enhance security protocols and respond swiftly to potential security breaches by either locking a device remotely. This feature is particularly crucial in scenarios involving lost or stolen devices or when a device is suspected to be compromised. By integrating these remote actions, Fleet empowers IT and security teams with robust tools to protect organizational data and maintain device security. This update aligns with Fleet's values of ownership and results, as it offers users more control over their device fleet while ensuring effective response measures are in place for critical security incidents. + + +### Linux script library + +A script library specifically designed for Linux hosts has been added. This complements Fleet's existing script execution functionalities and script libraries for macOS and Windows. The script library for Linux allows administrators to store, manage, and execute scripts efficiently using the Fleet UI or API, facilitating streamlined operations and maintenance tasks on Linux-based systems. This addition underscores Fleet's commitment to adaptability and inclusiveness, ensuring users can leverage the platform's full potential regardless of their operating system environment. By providing a dedicated script library for Linux, Fleet reinforces its dedication to delivering versatile and user-centric solutions that cater to the diverse needs of IT and security professionals. + + +### Customizable osquery data storage location + +Fleet introduces a new `--osquery-db` flag to the `fleetctl` package command, catering to a unique requirement for virtual machine (VM) environments. This feature allows users to specify or update the osquery database directory for `fleetd` at the time of packaging or through an environment variable. By enabling the customization of the osquery data storage location, users can direct `fleetd` to utilize directories with more available space, optimizing resource use in VM setups. This enhancement demonstrates Fleet's commitment to ownership by giving users greater control over their Fleet configuration and results and facilitating more efficient data management in resource-constrained environments. + + + +## Changes + +* **Endpoint operations**: + - Added two new API endpoints for running provided live query SQL on a single host. + - Added `fleetctl gitops` command for GitOps workflow synchronization. + - Added capabilities to the `gitops` role to support reading queries/policies and writing scripts. + - Updated policy names to be unique per team. + - Updated fleetd-chrome to use the latest wa-sqlite v0.9.11. + - Updated "Add hosts" modal UI to dynamically include the `--enable-scripts` flag. + - Added count of upcoming activities to host vitals UI. + - Updated UI to include upcoming activity counts in host vitals. + - Updated 405 response for `POST` requests on the root path to highlight misconfigured osquery instances. + +* **Device management (MDM)**: + - Added MDM command payloads to the response of `GET /api/_version_/fleet/mdm/commandresults`. + - Changed several MDM-related endpoints to be platform-agnostic. + - Added script capabilities to UI for Linux hosts. + - Added UI for locking and unlocking hosts managed by Fleet MDM. + - Added `fleetctl mdm lock` and `fleetctl mdm unlock` commands. + - Added validation to reject script enqueue requests for hosts without fleetd. + - Added the `host_mdm_actions` DB table for MDM lock and wipe functionality. + - Updated backend MDM migration flow and added logging. + - Updated UI text for disk encryption to reflect cross-platform functionality. + - Renamed and updated fields in MDM configuration profiles for clarity. + - Improved validation of Windows profiles to prevent delivery errors. + - Improved Windows MDM profile error tooltip messages. + - Fixed MDM unlock flow and updated lock/unlock functionality for Windows and Linux. + - Fixed a bug that would cause OS Settings verification to fail with MySQL's `only_full_group_by` mode enabled. + +* **Vulnerability management**: + - Windows OS Vulnerabilities now include a `resolved_in_version` in the `/os_versions` API response. + - Fixed an issue where software from a Parallels VM would incorrectly appear as the host's software. + - Implemented permission checks for software and software titles. + - Fixed software title aggregation when triggering vulnerability scans. + +### Bug fixes and improvements + - Updated text and style across the app for consistency and clarity. + - Improved UI for the view disk encryption key, host details activity card, and "Add hosts" modal. + - Addressed a bug where updating the search field caused unwanted loss of focus. + - Corrected alignment bugs on empty table states for software details. + - Updated URL query parameters to reset when switching tabs. + - Fixed device page showing invalid date for the last restarted. + - Fixed visual display issues with chevron right icons on Chrome. + - Fixed Windows vulnerabilities without exploit/severity from crashing the software page. + - Fixed issues with checkboxes in hidden modals and long enroll secrets overlapping action buttons. + - Fixed a bug with built-in platform labels. + - Fixed enroll secret error messaging showing secret in cleartext. + - Fixed various UI bugs including disk encryption key input icons, alignment issues, and dropdown menus. + - Fixed dropdown behavior in administrative settings and software title/version tables. + - Fixed various UI and style bugs, including issues with long OS names causing table render issues. + - Fixed a bug where checkboxes within a hidden modal were not correctly hidden. + - Fixed vulnerable software dropdown from switching back to all teams. + - Fixed wall_time to report in milliseconds for consistency with other query performance stats. + - Fixed generating duplicate activities when locking or unlocking a host with scripts disabled. + - Fixed how errors are reported to APM to avoid duplicates and improve stack trace accuracy. + +## Fleet 4.44.1 (Feb 13, 2024) + +### Bug fixes + +* Fixed a bug where long enrollment secrets would overlap with the action buttons on top of them. +* Fixed a bug that caused OS Settings to never be verified if the MySQL config of Fleet's database had 'only_full_group_by' mode enabled (enabled by default). +* Ensured policy names are now unique per team, allowing different teams to have policies with the same name. +* Fixed the visual display of chevron right icons on Chrome. +* Renamed the 'mdm_windows_configuration_profiles' and 'mdm_apple_configuration_profiles' 'updated_at' field to 'uploaded_at' and removed the automatic setting of the value, setting it explicitly instead. +* Fixed a small alignment bug in the setup flow. +* Improved the validation of Windows profiles to prevent errors when delivering the profiles to the hosts. If you need to embed a nested XML structure (for example, for Wi-Fi profiles), you can either: + - Escape the XML. + - Use a wrapping `` element. +* Fixed an issue where an inaccurate message was returned after running an asynchronous (queued) script. +* Fixed URL query parameters to reset when switching tabs. +* Fixed the vulnerable software dropdown from switching back to all teams. +* Added fleetctl gitops command: + - Synchronize Fleet configuration with the provided file. This command is intended to be used in a GitOps workflow. +* Updated the response for 'GET /api/v1/fleet/hosts/:id/activities/upcoming' to include the count of all upcoming activities for the host. +* Fixed an issue where software from a Parallels VM on a MacOS host would show up in Fleet as if it were the host's software. +* Removed unnecessary nested database transactions in batch-setting of MDM profiles. +* Added count of upcoming activities to host vitals UI. + + +## Ready to upgrade? + +Visit our [Upgrade guide](https://fleetdm.com/docs/deploying/upgrading-fleet) in the Fleet docs for instructions on updating to Fleet 4.45.0. + + + + + + + diff --git a/articles/introducing-cross-platform-script-execution.md b/articles/introducing-cross-platform-script-execution.md index d822de48ef8c..360e0e458e81 100644 --- a/articles/introducing-cross-platform-script-execution.md +++ b/articles/introducing-cross-platform-script-execution.md @@ -2,7 +2,7 @@ At Fleet, we strive to enhance the functionality of our platform, offering more power and flexibility to IT administrators and security teams. Today, we are thrilled to announce an addition to our toolkit - the ability to [execute a shell script](https://fleetdm.com/docs/using-fleet/scripts) via command line (CLI) and Fleet's API across macOS, Windows, and Linux. -This feature, available exclusively in Fleet Premium, unlocks the potential to execute a script on a specific host, empowering professionals to remediate issues or collect logs quickly. Whether using shell scripts on Mac and Linux or PowerShell for Windows, this functionality offers a streamlined and efficient solution to tackle challenges head-on. +This feature, available exclusively in Fleet, unlocks the potential to execute a script on a specific host, empowering professionals to remediate issues or collect logs quickly. Whether using shell scripts on Mac and Linux or PowerShell for Windows, this functionality offers a streamlined and efficient solution to tackle challenges head-on. Whether you need to address a security vulnerability, investigate an anomaly, or gather information for analysis, this new capability puts the control in your hands right where you need it. Join us as we explore this feature in detail, looking at the requirements, usage, and unique advantages it brings to your device management landscape. diff --git a/articles/using-fleet-and-tines-together.md b/articles/using-fleet-and-tines-together.md index 11169e4318a7..5c2353168b6f 100644 --- a/articles/using-fleet-and-tines-together.md +++ b/articles/using-fleet-and-tines-together.md @@ -74,8 +74,6 @@ The final email with the above definition looks like this: The Fleet API is very flexible, but with the addition of Tines, the options for data transformation are endless. In the above example, we easily connected to the Fleet API and transformed the data response with a single Tines Transform function, and allowed the end user to receive a customized report of vulnerable software on an individual host. - - diff --git a/changes/10476-lock-unlock-api-changes b/changes/10476-lock-unlock-api-changes deleted file mode 100644 index a2f897f8ce6e..000000000000 --- a/changes/10476-lock-unlock-api-changes +++ /dev/null @@ -1 +0,0 @@ -* Added tracking of Windows and Linux' scripts to lock or unlock the host, report the proper current and pending states. diff --git a/changes/13643-fleetctl-gitops b/changes/13643-fleetctl-gitops deleted file mode 100644 index be7855dece47..000000000000 --- a/changes/13643-fleetctl-gitops +++ /dev/null @@ -1,2 +0,0 @@ -Added fleetctl gitops command: -- Synchronize Fleet configuration with provided file. This command is intended to be used in a GitOps workflow. diff --git a/changes/13643-gitops-role b/changes/13643-gitops-role deleted file mode 100644 index d7b4676b6b48..000000000000 --- a/changes/13643-gitops-role +++ /dev/null @@ -1 +0,0 @@ -gitops role can now read queries/policies and write (but not execute) scripts diff --git a/changes/13643-policy-name-uniqueness b/changes/13643-policy-name-uniqueness deleted file mode 100644 index ad220cb29cf7..000000000000 --- a/changes/13643-policy-name-uniqueness +++ /dev/null @@ -1 +0,0 @@ -Policy names are now unique per team -- different teams can have policies with the same name. diff --git a/changes/14444-mdm-migration-debug b/changes/14444-mdm-migration-debug deleted file mode 100644 index e030e3d206b7..000000000000 --- a/changes/14444-mdm-migration-debug +++ /dev/null @@ -1 +0,0 @@ -- Updated backend MDM migration flow and added logging to aid in debugging migration errors. \ No newline at end of file diff --git a/changes/14713-fix-apm-stacktrace-and-duplicates b/changes/14713-fix-apm-stacktrace-and-duplicates deleted file mode 100644 index 46458926727c..000000000000 --- a/changes/14713-fix-apm-stacktrace-and-duplicates +++ /dev/null @@ -1 +0,0 @@ -* Fixed how errors are sent to APM (Elastic) to avoid duplicates, cover more errors in background tasks (cron and worker jobs) and fix the reported stack trace. diff --git a/changes/14850-fix-ui-settings-action-dropdowns b/changes/14850-fix-ui-settings-action-dropdowns deleted file mode 100644 index 22b61a4e22e1..000000000000 --- a/changes/14850-fix-ui-settings-action-dropdowns +++ /dev/null @@ -1 +0,0 @@ -- Fixed UI issues where dropdown menus were not displaying correctly in the administrative settings page. diff --git a/changes/15082-make-endpoints-consistent b/changes/15082-make-endpoints-consistent deleted file mode 100644 index c72bd7b56abb..000000000000 --- a/changes/15082-make-endpoints-consistent +++ /dev/null @@ -1,9 +0,0 @@ -- Changed the following endpoints to be platform-agnostic. The old routes still work but are deprecated. - - POST /mdm/apple/setup/eula was replaced by POST /mdm/setup/eula - - GET /mdm/apple/setup/eula/metadata was replaced by GET /mdm/setup/eula/metadata - - DELETE /mdm/apple/setup/eula/:token was replaced by DELETE /mdm/setup/eula/:token - - GET /mdm/apple/setup/eula/:token was replaced by GET /mdm/setup/eula/:token - - POST /mdm/apple/bootstrap was replaced by POST /mdm/bootstrap - - GET /mdm/apple/bootstrap/:team_id/metadata was replaced by GET /mdm/bootstrap/:team_id/metadata - - DELETE /mdm/apple/bootstrap/:team_id was replaced by DELETE /mdm/bootstrap/:team_id - - GET /mdm/apple/bootstrap/summary was replaced by GET /mdm/bootstrap/summary diff --git a/changes/15283-linux-scripts b/changes/15283-linux-scripts deleted file mode 100644 index cfeb8d85f4d0..000000000000 --- a/changes/15283-linux-scripts +++ /dev/null @@ -1 +0,0 @@ -- Added script capabilities to UI for Linux hosts. \ No newline at end of file diff --git a/changes/15332-scep-renew b/changes/15332-scep-renew new file mode 100644 index 000000000000..c66e9b72603b --- /dev/null +++ b/changes/15332-scep-renew @@ -0,0 +1 @@ +* Automatically renew macOS identity certificates for devices 30 days prior to their expiration. diff --git a/changes/15703-wall_time b/changes/15703-wall_time deleted file mode 100644 index 43eb3a6b221a..000000000000 --- a/changes/15703-wall_time +++ /dev/null @@ -1 +0,0 @@ -wall_time is now reported in milliseconds (as opposed to seconds), consistent with other query performance stats. diff --git a/changes/15855-vm-software b/changes/15855-vm-software deleted file mode 100644 index 7cb935cfe86a..000000000000 --- a/changes/15855-vm-software +++ /dev/null @@ -1,2 +0,0 @@ -- Fixes issue where software from a Parallels VM on a MacOS host would show up in Fleet as if it - were the host's software. \ No newline at end of file diff --git a/changes/15893-team-users b/changes/15893-team-users deleted file mode 100644 index dc8fcd801a97..000000000000 --- a/changes/15893-team-users +++ /dev/null @@ -1 +0,0 @@ -- Change verbiage around team members to users \ No newline at end of file diff --git a/changes/15923-page-descriptions-part-2 b/changes/15923-page-descriptions-part-2 new file mode 100644 index 000000000000..ee2daab2778d --- /dev/null +++ b/changes/15923-page-descriptions-part-2 @@ -0,0 +1 @@ +- Update page descriptions diff --git a/changes/15968-rename-team b/changes/15968-rename-team new file mode 100644 index 000000000000..4d8f29f7d18f --- /dev/null +++ b/changes/15968-rename-team @@ -0,0 +1 @@ +- UI Edit team more properly labeled as rename team diff --git a/changes/16014-add-osquery-db-flag-to-fleetd b/changes/16014-add-osquery-db-flag-to-fleetd deleted file mode 100644 index 0a38db7b0094..000000000000 --- a/changes/16014-add-osquery-db-flag-to-fleetd +++ /dev/null @@ -1 +0,0 @@ -* Add `--osquery-db` flag to `fleetctl package` command to configure a custom directory for osquery's database (`fleetctl package --osquery-db=/path/to/osquery.db`). diff --git a/changes/16025-empty-policy-state b/changes/16025-empty-policy-state new file mode 100644 index 000000000000..f72256971b94 --- /dev/null +++ b/changes/16025-empty-policy-state @@ -0,0 +1 @@ +- Update UI's empty policy states diff --git a/changes/16029-account-page b/changes/16029-account-page new file mode 100644 index 000000000000..ee343e7ed723 --- /dev/null +++ b/changes/16029-account-page @@ -0,0 +1 @@ +- User settings/profile page officially renamed to account page diff --git a/changes/16051-rename-update-timestamp-mdm-profiles b/changes/16051-rename-update-timestamp-mdm-profiles deleted file mode 100644 index 43aa0d53d50f..000000000000 --- a/changes/16051-rename-update-timestamp-mdm-profiles +++ /dev/null @@ -1 +0,0 @@ -* Renamed the `mdm_windows_configuration_profiles` and `mdm_apple_configuration_profiles` `updated_at` field to `uploaded_at` and removed the automatic setting of the value, set explicity instead. diff --git a/changes/16133-icons b/changes/16133-icons deleted file mode 100644 index 27202b9e1ad5..000000000000 --- a/changes/16133-icons +++ /dev/null @@ -1 +0,0 @@ -* Fix visual display of chevron right icons on Chrome diff --git a/changes/16155-enroll-secret-bug b/changes/16155-enroll-secret-bug deleted file mode 100644 index fc66b23e303b..000000000000 --- a/changes/16155-enroll-secret-bug +++ /dev/null @@ -1 +0,0 @@ -- Fix a bug where long enroll enroll secrets would overlap with the action buttons on top of them. diff --git a/changes/16182-fail-post-to-root b/changes/16182-fail-post-to-root deleted file mode 100644 index caa704a3129c..000000000000 --- a/changes/16182-fail-post-to-root +++ /dev/null @@ -1,5 +0,0 @@ -* Return 405 when receiving `POST` requests on the root path. -WARNING: -We found that misconfigured (empty `logger_tls_endpoint`) osquery instances were sending log results (`POST` requests) to the root path and Fleet was incorrectly returning HTTP 200 responses on such root path. -This version will now return HTTP 405 (Method Not Allowed) when receiving `POST` requests on the root path so that this misconfiguration can be detected by administrators. -If you deploy this version of Fleet and there's log traffic on the root path it could cause increased network usage on your infrastructure because osquery will retry sending the logs and these will accumulate (up to a limit configured by logger flags). Thus, before upgrading, make sure there's no osquery traffic (`POST` requests) to Fleet's root path. diff --git a/changes/16232-resolved-in-version-windows b/changes/16232-resolved-in-version-windows deleted file mode 100644 index 25b24841bea6..000000000000 --- a/changes/16232-resolved-in-version-windows +++ /dev/null @@ -1 +0,0 @@ -- Windows OS Vulnerabilities now include a `resolved_in_version` in the `/os_versions` API response \ No newline at end of file diff --git a/changes/16273-remove-nested-transactions b/changes/16273-remove-nested-transactions deleted file mode 100644 index e7fa044306fe..000000000000 --- a/changes/16273-remove-nested-transactions +++ /dev/null @@ -1 +0,0 @@ -* Removed unnecessary nested database transactions in batch-setting of MDM profiles. diff --git a/changes/16316-windows-xml-validation b/changes/16316-windows-xml-validation deleted file mode 100644 index def14d8ae52c..000000000000 --- a/changes/16316-windows-xml-validation +++ /dev/null @@ -1,5 +0,0 @@ -* Improved the validation of Windows profiles to prevent errors when the - profiles are delivered to the hosts. If you need to embed a nested XML - structure (for example for Wi-Fi profiles) you can either: - - Escape the XML - - Use a wrapping `` element diff --git a/changes/16381-add-hosts-modal-enable-scripts b/changes/16381-add-hosts-modal-enable-scripts deleted file mode 100644 index b3e40d4d0020..000000000000 --- a/changes/16381-add-hosts-modal-enable-scripts +++ /dev/null @@ -1,2 +0,0 @@ -- Updated "Add hosts" modal UI to dynamically include the `--enable-scripts` flag unless scripts are - disabled in the server settings. diff --git a/changes/16382-fleetctl-copy b/changes/16382-fleetctl-copy deleted file mode 100644 index 6b0317202d03..000000000000 --- a/changes/16382-fleetctl-copy +++ /dev/null @@ -1 +0,0 @@ -- Updates the copy in `fleetctl`'s output to reference `fleetd`. \ No newline at end of file diff --git a/changes/16383-lock-cli b/changes/16383-lock-cli deleted file mode 100644 index e78fb887c4bf..000000000000 --- a/changes/16383-lock-cli +++ /dev/null @@ -1,2 +0,0 @@ -- Adds the `fleetctl mdm` commands `lock` and `unlock` -- Adds missing functionality for lock/unlock flows for Windows and Linux \ No newline at end of file diff --git a/changes/16386-host-lock-schema b/changes/16386-host-lock-schema deleted file mode 100644 index a36317345037..000000000000 --- a/changes/16386-host-lock-schema +++ /dev/null @@ -1 +0,0 @@ -- Adds the `host_mdm_actions` DB table to support MDM lock and wipe functionality. \ No newline at end of file diff --git a/changes/16394-fleetd-chrome-runtime-error b/changes/16394-fleetd-chrome-runtime-error deleted file mode 100644 index d6c03976d248..000000000000 --- a/changes/16394-fleetd-chrome-runtime-error +++ /dev/null @@ -1 +0,0 @@ -Updated fleetd-chrome to use the latest wa-sqlite v0.9.11 diff --git a/changes/16394-fleetd-chrome-runtime-error-fix b/changes/16394-fleetd-chrome-runtime-error-fix new file mode 100644 index 000000000000..9c9003a68254 --- /dev/null +++ b/changes/16394-fleetd-chrome-runtime-error-fix @@ -0,0 +1 @@ +In fleetd-chrome, fixed RuntimeError seen by some hosts. diff --git a/changes/16416-cmd-debugging b/changes/16416-cmd-debugging deleted file mode 100644 index 9fbfabad1945..000000000000 --- a/changes/16416-cmd-debugging +++ /dev/null @@ -1,4 +0,0 @@ -* Added MDM command payloads to the response of `GET /api/_version_/fleet/mdm/commandresults`. -* Added a new column named "PAYLOAD" to the output of `fleetctl get mdm-command-results` with the request payload. -* Replaced CmdID values in favor of the LocURI for messages for failed profiles. -* Added a new comment over CmdID elements generated by Fleet in Windows profiles and commands to make evident that Fleet is in control of those values. diff --git a/changes/16426-add-upcoming-activity-count b/changes/16426-add-upcoming-activity-count deleted file mode 100644 index 782518c46228..000000000000 --- a/changes/16426-add-upcoming-activity-count +++ /dev/null @@ -1,2 +0,0 @@ -- Updated `GET /api/v1/fleet/hosts/:id/activities/upcoming` response to include the count of all - upcoming activities for the host. diff --git a/changes/16426-host-upcoming-activities-count-ui b/changes/16426-host-upcoming-activities-count-ui deleted file mode 100644 index c82070f23ba0..000000000000 --- a/changes/16426-host-upcoming-activities-count-ui +++ /dev/null @@ -1 +0,0 @@ -- Added count of upcoming activities to host vitals UI. \ No newline at end of file diff --git a/changes/16431-scripts-result-message b/changes/16431-scripts-result-message deleted file mode 100644 index 3d29c82fd130..000000000000 --- a/changes/16431-scripts-result-message +++ /dev/null @@ -1 +0,0 @@ -- Fixes issue where an inaccurate message was returned after running an async (queued) script. \ No newline at end of file diff --git a/changes/16466-transfer-hosts-to-No-team b/changes/16466-transfer-hosts-to-No-team deleted file mode 100644 index d39f7e283bdd..000000000000 --- a/changes/16466-transfer-hosts-to-No-team +++ /dev/null @@ -1 +0,0 @@ -fleetctl can now transfer hosts to No team like: fleetctl hosts transfer --team '' --hosts yourHost diff --git a/changes/16480-fix-capturing-errors-in-sentry b/changes/16480-fix-capturing-errors-in-sentry new file mode 100644 index 000000000000..0638ba66051f --- /dev/null +++ b/changes/16480-fix-capturing-errors-in-sentry @@ -0,0 +1,4 @@ +* Fixed issues with how errors were captured in Sentry: + - The stack trace is now more precise. + - More error paths will now get captured in Sentry. + - **NOTE: Many more entries could be generated in Sentry compared to earlier Fleet versions.** Sentry capacity should be planned accordingly. diff --git a/changes/16506-page-descriptions b/changes/16506-page-descriptions new file mode 100644 index 000000000000..5bdbd499f0ff --- /dev/null +++ b/changes/16506-page-descriptions @@ -0,0 +1 @@ +- Update page description styling diff --git a/changes/16541-create-user-with-bad-team b/changes/16541-create-user-with-bad-team deleted file mode 100644 index 2c92ab6a6063..000000000000 --- a/changes/16541-create-user-with-bad-team +++ /dev/null @@ -1 +0,0 @@ -Improved error message when creating a new user (via API or fleetctl) with a team that does not exist. diff --git a/changes/16569-setup-flow-alignment b/changes/16569-setup-flow-alignment deleted file mode 100644 index 3b8b317bfdef..000000000000 --- a/changes/16569-setup-flow-alignment +++ /dev/null @@ -1 +0,0 @@ -* Fix a small alignment bug in the setup flow diff --git a/changes/16621-obfuscate-enroll-secret b/changes/16621-obfuscate-enroll-secret deleted file mode 100644 index accfff76f3bf..000000000000 --- a/changes/16621-obfuscate-enroll-secret +++ /dev/null @@ -1 +0,0 @@ -When attempting to set an enroll secret which already exists in DB, error message no longer contains the secret in cleartext. diff --git a/changes/16648-windows-mdm-cmd-type b/changes/16648-windows-mdm-cmd-type new file mode 100644 index 000000000000..a4ff45382c2c --- /dev/null +++ b/changes/16648-windows-mdm-cmd-type @@ -0,0 +1,2 @@ +- Fixes issue where the "Type" column was empty for Windows MDM profile commands when running + `fleetctl get mdm-commands` and `fleetctl get mdm-command-results`. \ No newline at end of file diff --git a/changes/16649-ui-activity-disk-encryption b/changes/16649-ui-activity-disk-encryption deleted file mode 100644 index 5ca2a0e06a94..000000000000 --- a/changes/16649-ui-activity-disk-encryption +++ /dev/null @@ -1 +0,0 @@ -- Updated UI text for disk encryption activities to reflect cross-platform functionality. \ No newline at end of file diff --git a/changes/16669-fix-hardcoded-label-bug b/changes/16669-fix-hardcoded-label-bug deleted file mode 100644 index 396dea3ef752..000000000000 --- a/changes/16669-fix-hardcoded-label-bug +++ /dev/null @@ -1 +0,0 @@ -- Fixed built in platform labels bug diff --git a/changes/16672-software-url-states-bug b/changes/16672-software-url-states-bug deleted file mode 100644 index 7fcd985dac0d..000000000000 --- a/changes/16672-software-url-states-bug +++ /dev/null @@ -1,2 +0,0 @@ -- Fix URL query params to reset when switching tabs -- Fix vulnerable software dropdown from switching back to all teams diff --git a/changes/16681-device-last-restarted-bug b/changes/16681-device-last-restarted-bug deleted file mode 100644 index 912291b0316d..000000000000 --- a/changes/16681-device-last-restarted-bug +++ /dev/null @@ -1 +0,0 @@ -- Fix device page showing invalid date for last restarted diff --git a/changes/16700-scripts-disabled-osquery-only b/changes/16700-scripts-disabled-osquery-only deleted file mode 100644 index 3e01f1788712..000000000000 --- a/changes/16700-scripts-disabled-osquery-only +++ /dev/null @@ -1,2 +0,0 @@ -- Added validation to reject requests to enqueue scripts for hosts that do not have fleetd installed - (i.e. plain osquery hosts). diff --git a/changes/16701-move-show-query-button b/changes/16701-move-show-query-button new file mode 100644 index 000000000000..ec0d5868ef58 --- /dev/null +++ b/changes/16701-move-show-query-button @@ -0,0 +1 @@ +- Move show query button so it shows in report page even with no results diff --git a/changes/16724-capitalization-fixes b/changes/16724-capitalization-fixes deleted file mode 100644 index 90d4c6d635e7..000000000000 --- a/changes/16724-capitalization-fixes +++ /dev/null @@ -1 +0,0 @@ -- Fix title case to sentence case and a few other headers diff --git a/changes/16752-blur-on-software-search b/changes/16752-blur-on-software-search deleted file mode 100644 index ef80c99a39cb..000000000000 --- a/changes/16752-blur-on-software-search +++ /dev/null @@ -1,2 +0,0 @@ -- Fix a bug where updating the search field for the Software titles page caused an unwanted loss of - focus from the search field on rerender. diff --git a/changes/16765-windows-software-vuln-crash b/changes/16765-windows-software-vuln-crash deleted file mode 100644 index 5ecbff968979..000000000000 --- a/changes/16765-windows-software-vuln-crash +++ /dev/null @@ -1 +0,0 @@ -- Fix windows vulnerabilities without exploit/severity from crashing the page when rendered diff --git a/changes/16805-new-live-query-on-host-endpoint b/changes/16805-new-live-query-on-host-endpoint deleted file mode 100644 index 84918569b318..000000000000 --- a/changes/16805-new-live-query-on-host-endpoint +++ /dev/null @@ -1 +0,0 @@ -* Add two new API endpoints to run a live query SQL on one host: `POST /api/latest/fleet/hosts/identifier/{identifier}/query` and `POST /api/_version_/fleet/hosts/{id}/query`. diff --git a/changes/16820-loading-state-auto-enroll-ui b/changes/16820-loading-state-auto-enroll-ui new file mode 100644 index 000000000000..4eee73b5f641 --- /dev/null +++ b/changes/16820-loading-state-auto-enroll-ui @@ -0,0 +1 @@ +- Fixed UI styling of loading state for automatic enrollment settings page. diff --git a/changes/16853-show-all-mdm-errs b/changes/16853-show-all-mdm-errs new file mode 100644 index 000000000000..772fb1f6237b --- /dev/null +++ b/changes/16853-show-all-mdm-errs @@ -0,0 +1 @@ +- Fixes an issue where some MDM profile installation errors would not be shown in Fleet. \ No newline at end of file diff --git a/changes/16856-fix-duplicate-activities-lock-unlock-scripts b/changes/16856-fix-duplicate-activities-lock-unlock-scripts deleted file mode 100644 index 0600cc82e2f6..000000000000 --- a/changes/16856-fix-duplicate-activities-lock-unlock-scripts +++ /dev/null @@ -1 +0,0 @@ -* Fixed generating duplicate activities when locking or unlocking a host with scripts disabled. diff --git a/changes/16910-sw-table-breakpoint b/changes/16910-sw-table-breakpoint deleted file mode 100644 index 9bc478cc66a3..000000000000 --- a/changes/16910-sw-table-breakpoint +++ /dev/null @@ -1,2 +0,0 @@ -- Fix a style bug where the controls on the software title and versions table would wrap and bump into - each other. diff --git "a/changes/16912-hide\342\200\223modal-checkboxes" "b/changes/16912-hide\342\200\223modal-checkboxes" deleted file mode 100644 index 100d9ddd869f..000000000000 --- "a/changes/16912-hide\342\200\223modal-checkboxes" +++ /dev/null @@ -1 +0,0 @@ -- Fix a bug where checkboxes within a hidden modal would not be hidden with the rest of the modal content. diff --git a/changes/16941-sw-os-table-overflows b/changes/16941-sw-os-table-overflows deleted file mode 100644 index 502ce6c44f2c..000000000000 --- a/changes/16941-sw-os-table-overflows +++ /dev/null @@ -1 +0,0 @@ -- Fix a bug where long OS names caused the table to render outside its bounds with smaller viewports diff --git a/changes/16942-empty-swversion-swos-details-tables b/changes/16942-empty-swversion-swos-details-tables deleted file mode 100644 index 7b83916600b4..000000000000 --- a/changes/16942-empty-swversion-swos-details-tables +++ /dev/null @@ -1,2 +0,0 @@ -* Fix alignment bugs on the Software > OS > details and Software > Versions > details empty table -states. diff --git a/changes/17029-update-policy-count b/changes/17029-update-policy-count new file mode 100644 index 000000000000..f7a4dce857ed --- /dev/null +++ b/changes/17029-update-policy-count @@ -0,0 +1 @@ +- Deleting a policy updates the policy count diff --git a/changes/17048-updating-policy-name b/changes/17048-updating-policy-name new file mode 100644 index 000000000000..1e250c9928be --- /dev/null +++ b/changes/17048-updating-policy-name @@ -0,0 +1,2 @@ +Fixed bug where updating policy name can result with multiple policies with the same name in a team. +- This bug was introduced in fleet v4.44.1. Any duplicate policy names in the same team will be renamed by adding a number to the end of the policy name. diff --git a/changes/17124-mdm-sso-fix b/changes/17124-mdm-sso-fix new file mode 100644 index 000000000000..cffa451944ab --- /dev/null +++ b/changes/17124-mdm-sso-fix @@ -0,0 +1 @@ +* Fixed a bug that caused macOS ADE enrollments gated behind SSO to get a "method not allowed" error. diff --git a/changes/issue-10477-ui-for-locking-unlocking b/changes/issue-10477-ui-for-locking-unlocking deleted file mode 100644 index 86599a3dd5ec..000000000000 --- a/changes/issue-10477-ui-for-locking-unlocking +++ /dev/null @@ -1 +0,0 @@ -- add UI for locking and unlocking hosts managed by fleet mdm. diff --git a/changes/issue-16052-add-permission-checks-to-software-titles b/changes/issue-16052-add-permission-checks-to-software-titles deleted file mode 100644 index 98c672e5c710..000000000000 --- a/changes/issue-16052-add-permission-checks-to-software-titles +++ /dev/null @@ -1 +0,0 @@ -- Implemented permission checks for endpoints and UI routes related to software and software titles, restricting visibility to team-specific hosts. diff --git a/changes/issue-16417-improve-windows-profile-error-tooltip b/changes/issue-16417-improve-windows-profile-error-tooltip deleted file mode 100644 index 390aae2316c3..000000000000 --- a/changes/issue-16417-improve-windows-profile-error-tooltip +++ /dev/null @@ -1 +0,0 @@ -- improve windows mdm profile error tooltip messages. diff --git a/changes/issue-16747-fix-disk-encryption-key-input b/changes/issue-16747-fix-disk-encryption-key-input deleted file mode 100644 index 32b20e04acae..000000000000 --- a/changes/issue-16747-fix-disk-encryption-key-input +++ /dev/null @@ -1 +0,0 @@ -- fix UI bug for the view disk encryption key input icons diff --git a/changes/issue-16794-update-go-to-1.21.7 b/changes/issue-16794-update-go-to-1.21.7 new file mode 100644 index 000000000000..7eeccbb9c80b --- /dev/null +++ b/changes/issue-16794-update-go-to-1.21.7 @@ -0,0 +1 @@ +- upgrade golang version to 1.21.7 diff --git a/changes/issue-16854-fix-software-version-and-os-loading b/changes/issue-16854-fix-software-version-and-os-loading new file mode 100644 index 000000000000..924833d0eb4f --- /dev/null +++ b/changes/issue-16854-fix-software-version-and-os-loading @@ -0,0 +1 @@ +- fix UI loading state for software versions and os for the inital request. diff --git a/changes/jve-lock-host-auth b/changes/jve-lock-host-auth deleted file mode 100644 index 9d842be43e95..000000000000 --- a/changes/jve-lock-host-auth +++ /dev/null @@ -1 +0,0 @@ -- Adds authorization tests for the MDM lock and unlock features. \ No newline at end of file diff --git a/changes/jve-macos-special-case b/changes/jve-macos-special-case deleted file mode 100644 index 33be35ffbed7..000000000000 --- a/changes/jve-macos-special-case +++ /dev/null @@ -1,2 +0,0 @@ -- Updates the MDM unlock flow to allow the PIN to unlock MacOS machines to be viewed as many times -as needed. \ No newline at end of file diff --git a/changes/lock-perms-docs b/changes/lock-perms-docs deleted file mode 100644 index b326b18431fb..000000000000 --- a/changes/lock-perms-docs +++ /dev/null @@ -1 +0,0 @@ -- Updates the permissions docs to include permissions for lock/unlock/wipe actions on a host. \ No newline at end of file diff --git a/changes/profiles-fix b/changes/profiles-fix deleted file mode 100644 index dfd4ed028786..000000000000 --- a/changes/profiles-fix +++ /dev/null @@ -1 +0,0 @@ -* Fixed a bug that would cause OS Settings to never get verified if the MySQL config of Fleet's database has `only_full_group_by` mode enabled (enabled by default). diff --git a/charts/fleet/Chart.yaml b/charts/fleet/Chart.yaml index a4f768464367..973a09604905 100644 --- a/charts/fleet/Chart.yaml +++ b/charts/fleet/Chart.yaml @@ -8,7 +8,7 @@ version: v6.0.2 home: https://github.com/fleetdm/fleet sources: - https://github.com/fleetdm/fleet.git -appVersion: v4.44.1 +appVersion: v4.45.0 dependencies: - name: mysql condition: mysql.enabled diff --git a/charts/fleet/values.yaml b/charts/fleet/values.yaml index 89dd2fad4cdc..d4e7f8ec2452 100644 --- a/charts/fleet/values.yaml +++ b/charts/fleet/values.yaml @@ -2,7 +2,7 @@ # All settings related to how Fleet is deployed in Kubernetes hostName: fleet.localhost replicas: 3 # The number of Fleet instances to deploy -imageTag: v4.44.1 # Version of Fleet to deploy +imageTag: v4.45.0 # Version of Fleet to deploy podAnnotations: {} # Additional annotations to add to the Fleet pod serviceAccountAnnotations: {} # Additional annotations to add to the Fleet service account resources: diff --git a/cmd/fleet/cron.go b/cmd/fleet/cron.go index 2a17c87e9ec1..4a17dc8b1608 100644 --- a/cmd/fleet/cron.go +++ b/cmd/fleet/cron.go @@ -32,7 +32,6 @@ import ( "github.com/fleetdm/fleet/v4/server/vulnerabilities/utils" "github.com/fleetdm/fleet/v4/server/webhooks" "github.com/fleetdm/fleet/v4/server/worker" - "github.com/getsentry/sentry-go" kitlog "github.com/go-kit/log" "github.com/go-kit/log/level" "github.com/hashicorp/go-multierror" @@ -41,7 +40,6 @@ import ( func errHandler(ctx context.Context, logger kitlog.Logger, msg string, err error) { level.Error(logger).Log("msg", msg, "err", err) - sentry.CaptureException(err) ctxerr.Handle(ctx, err) } @@ -102,6 +100,11 @@ func cronVulnerabilities( if err := scanVulnerabilities(ctx, ds, logger, config, appConfig, vulnPath); err != nil { return fmt.Errorf("scanning vulnerabilities: %w", err) } + + level.Info(logger).Log("msg", "updating vulnerability host counts") + if err := ds.UpdateVulnerabilityHostCounts(ctx); err != nil { + return fmt.Errorf("updating vulnerability host counts: %w", err) + } } return nil @@ -705,6 +708,7 @@ func newCleanupsAndAggregationSchedule( logger kitlog.Logger, enrollHostLimiter fleet.EnrollHostLimiter, config *config.FleetConfig, + commander *apple_mdm.MDMAppleCommander, ) (*schedule.Schedule, error) { const ( name = string(fleet.CronCleanupsThenAggregation) @@ -805,6 +809,12 @@ func newCleanupsAndAggregationSchedule( return verifyDiskEncryptionKeys(ctx, logger, ds, config) }, ), + schedule.WithJob( + "renew_scep_certificates", + func(ctx context.Context) error { + return service.RenewSCEPCertificates(ctx, logger, ds, config, commander) + }, + ), schedule.WithJob("query_results_cleanup", func(ctx context.Context) error { config, err := ds.AppConfig(ctx) if err != nil { diff --git a/cmd/fleet/serve.go b/cmd/fleet/serve.go index 5bbe21337584..9755b97bd396 100644 --- a/cmd/fleet/serve.go +++ b/cmd/fleet/serve.go @@ -46,6 +46,7 @@ import ( "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/push" "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/push/buford" nanomdm_pushsvc "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/push/service" + scep_depot "github.com/fleetdm/fleet/v4/server/mdm/scep/depot" "github.com/fleetdm/fleet/v4/server/pubsub" "github.com/fleetdm/fleet/v4/server/service" "github.com/fleetdm/fleet/v4/server/service/async" @@ -57,7 +58,6 @@ import ( "github.com/go-kit/kit/log/level" kitprometheus "github.com/go-kit/kit/metrics/prometheus" "github.com/go-kit/log" - scep_depot "github.com/micromdm/scep/v2/depot" "github.com/ngrok/sqlmw" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" @@ -681,7 +681,11 @@ the way that the Fleet server works. }() if err := cronSchedules.StartCronSchedule(func() (fleet.CronSchedule, error) { - return newCleanupsAndAggregationSchedule(ctx, instanceID, ds, logger, redisWrapperDS, &config) + var commander *apple_mdm.MDMAppleCommander + if appCfg.MDM.EnabledAndConfigured { + commander = apple_mdm.NewMDMAppleCommander(mdmStorage, mdmPushService) + } + return newCleanupsAndAggregationSchedule(ctx, instanceID, ds, logger, redisWrapperDS, &config, commander) }); err != nil { initFatal(err, "failed to register cleanups_then_aggregations schedule") } diff --git a/cmd/fleetctl/get.go b/cmd/fleetctl/get.go index 438549d220c3..3fbc6274a3aa 100644 --- a/cmd/fleetctl/get.go +++ b/cmd/fleetctl/get.go @@ -1510,10 +1510,14 @@ func getMDMCommandResultsCommand() *cli.Command { } formattedPayload = r.Payload } + reqType := r.RequestType + if len(reqType) == 0 { + reqType = "InstallProfile" + } data = append(data, []string{ r.CommandUUID, r.UpdatedAt.Format(time.RFC3339), - r.RequestType, + reqType, r.Status, r.Hostname, string(formattedPayload), @@ -1561,10 +1565,14 @@ func getMDMCommandsCommand() *cli.Command { // print the results as a table data := [][]string{} for _, r := range results { + reqType := r.RequestType + if len(reqType) == 0 { + reqType = "InstallProfile" + } data = append(data, []string{ r.CommandUUID, r.UpdatedAt.Format(time.RFC3339), - r.RequestType, + reqType, r.Status, r.Hostname, }) diff --git a/cmd/fleetctl/get_test.go b/cmd/fleetctl/get_test.go index 995ab3646406..f1808cbdf9b6 100644 --- a/cmd/fleetctl/get_test.go +++ b/cmd/fleetctl/get_test.go @@ -2365,7 +2365,6 @@ func TestGetMDMCommandResults(t *testing.T) { CommandUUID: commandUUID, Status: "200", UpdatedAt: time.Date(2023, 4, 4, 15, 29, 0, 0, time.UTC), - RequestType: "test", Payload: []byte(winPayloadXML), Result: []byte(winResultXML), }, @@ -2374,7 +2373,6 @@ func TestGetMDMCommandResults(t *testing.T) { CommandUUID: commandUUID, Status: "500", UpdatedAt: time.Date(2023, 4, 4, 15, 29, 0, 0, time.UTC), - RequestType: "test", Payload: []byte(winPayloadXML), Result: []byte(winResultXML), }, @@ -2518,89 +2516,89 @@ func TestGetMDMCommandResults(t *testing.T) { }) t.Run("windows command results", func(t *testing.T) { - expectedOutput := strings.TrimSpace(`+-----------+----------------------+------+--------+----------+---------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------+ -| ID | TIME | TYPE | STATUS | HOSTNAME | PAYLOAD | RESULTS | -+-----------+----------------------+------+--------+----------+---------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------+ -| valid-cmd | 2023-04-04T15:29:00Z | test | 200 | host1 | | | -| | | | | | | | -| | | | | | 90dbfca8-d4ac-40c9-bf57-ba5b8cbf1ce0 | 1.2 | -| | | | | | | DM/1.2 | -| | | | | | | 48 | -| | | | | | 81a141b2-5064-4dc3-a51a-128b8caa5438 | 2 | -| | | | | | | | -| | | | | | | https://roperzh-fleet.ngrok.io/api/mdm/microsoft/management | -| | | | | | ./Device/Vendor/MSFT/Policy/Config/Bluetooth/AllowDiscoverableMode | | -| | | | | | | | -| | | | | | | 1F28CCBDCE02AE44BD2AAC3C0B9AD4DE | -| | | | | | int | | -| | | | | | | | -| | | | | | 1 | | -| | | | | | | | -| | | | | | | 1 | -| | | | | | | 1 | -| | | | | | | 0 | -| | | | | | | SyncHdr | -| | | | | | | 200 | -| | | | | | | | -| | | | | | | | -| | | | | | | 2 | -| | | | | | | 1 | -| | | | | | | 90dbfca8-d4ac-40c9-bf57-ba5b8cbf1ce0 | -| | | | | | | Atomic | -| | | | | | | 200 | -| | | | | | | | -| | | | | | | | -| | | | | | | 3 | -| | | | | | | 1 | -| | | | | | | 81a141b2-5064-4dc3-a51a-128b8caa5438 | -| | | | | | | Replace | -| | | | | | | 200 | -| | | | | | | | -| | | | | | | | -| | | | | | | | -| | | | | | | | -| | | | | | | | -+-----------+----------------------+------+--------+----------+---------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------+ -| valid-cmd | 2023-04-04T15:29:00Z | test | 500 | host2 | | | -| | | | | | | | -| | | | | | 90dbfca8-d4ac-40c9-bf57-ba5b8cbf1ce0 | 1.2 | -| | | | | | | DM/1.2 | -| | | | | | | 48 | -| | | | | | 81a141b2-5064-4dc3-a51a-128b8caa5438 | 2 | -| | | | | | | | -| | | | | | | https://roperzh-fleet.ngrok.io/api/mdm/microsoft/management | -| | | | | | ./Device/Vendor/MSFT/Policy/Config/Bluetooth/AllowDiscoverableMode | | -| | | | | | | | -| | | | | | | 1F28CCBDCE02AE44BD2AAC3C0B9AD4DE | -| | | | | | int | | -| | | | | | | | -| | | | | | 1 | | -| | | | | | | | -| | | | | | | 1 | -| | | | | | | 1 | -| | | | | | | 0 | -| | | | | | | SyncHdr | -| | | | | | | 200 | -| | | | | | | | -| | | | | | | | -| | | | | | | 2 | -| | | | | | | 1 | -| | | | | | | 90dbfca8-d4ac-40c9-bf57-ba5b8cbf1ce0 | -| | | | | | | Atomic | -| | | | | | | 200 | -| | | | | | | | -| | | | | | | | -| | | | | | | 3 | -| | | | | | | 1 | -| | | | | | | 81a141b2-5064-4dc3-a51a-128b8caa5438 | -| | | | | | | Replace | -| | | | | | | 200 | -| | | | | | | | -| | | | | | | | -| | | | | | | | -| | | | | | | | -| | | | | | | | -+-----------+----------------------+------+--------+----------+---------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------+ + expectedOutput := strings.TrimSpace(`+-----------+----------------------+----------------+--------+----------+---------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------+ +| ID | TIME | TYPE | STATUS | HOSTNAME | PAYLOAD | RESULTS | ++-----------+----------------------+----------------+--------+----------+---------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------+ +| valid-cmd | 2023-04-04T15:29:00Z | InstallProfile | 200 | host1 | | | +| | | | | | | | +| | | | | | 90dbfca8-d4ac-40c9-bf57-ba5b8cbf1ce0 | 1.2 | +| | | | | | | DM/1.2 | +| | | | | | | 48 | +| | | | | | 81a141b2-5064-4dc3-a51a-128b8caa5438 | 2 | +| | | | | | | | +| | | | | | | https://roperzh-fleet.ngrok.io/api/mdm/microsoft/management | +| | | | | | ./Device/Vendor/MSFT/Policy/Config/Bluetooth/AllowDiscoverableMode | | +| | | | | | | | +| | | | | | | 1F28CCBDCE02AE44BD2AAC3C0B9AD4DE | +| | | | | | int | | +| | | | | | | | +| | | | | | 1 | | +| | | | | | | | +| | | | | | | 1 | +| | | | | | | 1 | +| | | | | | | 0 | +| | | | | | | SyncHdr | +| | | | | | | 200 | +| | | | | | | | +| | | | | | | | +| | | | | | | 2 | +| | | | | | | 1 | +| | | | | | | 90dbfca8-d4ac-40c9-bf57-ba5b8cbf1ce0 | +| | | | | | | Atomic | +| | | | | | | 200 | +| | | | | | | | +| | | | | | | | +| | | | | | | 3 | +| | | | | | | 1 | +| | | | | | | 81a141b2-5064-4dc3-a51a-128b8caa5438 | +| | | | | | | Replace | +| | | | | | | 200 | +| | | | | | | | +| | | | | | | | +| | | | | | | | +| | | | | | | | +| | | | | | | | ++-----------+----------------------+----------------+--------+----------+---------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------+ +| valid-cmd | 2023-04-04T15:29:00Z | InstallProfile | 500 | host2 | | | +| | | | | | | | +| | | | | | 90dbfca8-d4ac-40c9-bf57-ba5b8cbf1ce0 | 1.2 | +| | | | | | | DM/1.2 | +| | | | | | | 48 | +| | | | | | 81a141b2-5064-4dc3-a51a-128b8caa5438 | 2 | +| | | | | | | | +| | | | | | | https://roperzh-fleet.ngrok.io/api/mdm/microsoft/management | +| | | | | | ./Device/Vendor/MSFT/Policy/Config/Bluetooth/AllowDiscoverableMode | | +| | | | | | | | +| | | | | | | 1F28CCBDCE02AE44BD2AAC3C0B9AD4DE | +| | | | | | int | | +| | | | | | | | +| | | | | | 1 | | +| | | | | | | | +| | | | | | | 1 | +| | | | | | | 1 | +| | | | | | | 0 | +| | | | | | | SyncHdr | +| | | | | | | 200 | +| | | | | | | | +| | | | | | | | +| | | | | | | 2 | +| | | | | | | 1 | +| | | | | | | 90dbfca8-d4ac-40c9-bf57-ba5b8cbf1ce0 | +| | | | | | | Atomic | +| | | | | | | 200 | +| | | | | | | | +| | | | | | | | +| | | | | | | 3 | +| | | | | | | 1 | +| | | | | | | 81a141b2-5064-4dc3-a51a-128b8caa5438 | +| | | | | | | Replace | +| | | | | | | 200 | +| | | | | | | | +| | | | | | | | +| | | | | | | | +| | | | | | | | +| | | | | | | | ++-----------+----------------------+----------------+--------+----------+---------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------+ `) platform = "windows" @@ -2644,6 +2642,14 @@ func TestGetMDMCommands(t *testing.T) { Status: "200", Hostname: "host2", }, + // This represents a command generated by fleet as part of a Windows profile + { + HostUUID: "h2", + CommandUUID: "u3", + UpdatedAt: time.Date(2023, 4, 11, 9, 5, 0, 0, time.UTC), + Status: "200", + Hostname: "host2", + }, }, nil } @@ -2669,6 +2675,8 @@ func TestGetMDMCommands(t *testing.T) { +----+----------------------+---------------------------------------+--------------+----------+ | u2 | 2023-04-11T09:05:00Z | ./Device/Vendor/MSFT/Reboot/RebootNow | 200 | host2 | +----+----------------------+---------------------------------------+--------------+----------+ +| u3 | 2023-04-11T09:05:00Z | InstallProfile | 200 | host2 | ++----+----------------------+---------------------------------------+--------------+----------+ `)) } diff --git a/cmd/osquery-perf/agent.go b/cmd/osquery-perf/agent.go index 3321fc6baad2..ac0a58ca96f1 100644 --- a/cmd/osquery-perf/agent.go +++ b/cmd/osquery-perf/agent.go @@ -629,6 +629,7 @@ func (a *agent) runOrbitLoop() { HardwareSerial: a.SerialNumber, Hostname: a.CachedString("hostname"), }, + nil, ) if err != nil { log.Println("creating orbit client: ", err) diff --git a/docs/01-Using-Fleet/standard-query-library/standard-query-library.yml b/docs/01-Using-Fleet/standard-query-library/standard-query-library.yml index 213a9d05a26a..e5d3ba2d65b4 100644 --- a/docs/01-Using-Fleet/standard-query-library/standard-query-library.yml +++ b/docs/01-Using-Fleet/standard-query-library/standard-query-library.yml @@ -880,12 +880,12 @@ apiVersion: v1 kind: policy spec: name: No 1Password emergency kit stored on desktop or in downloads (macOS) - query: SELECT EXISTS (SELECT 1 FROM file WHERE filename LIKE '%Emergency Kit%.pdf' AND (path LIKE '/Users/%%/Downloads/%%' OR path LIKE '/Users/%%/Desktop/%%')); + query: SELECT 1 FROM file WHERE filename LIKE '%Emergency Kit%.pdf' AND (path LIKE '/Users/%%/Desktop/%%' OR path LIKE '/Users/%%/Documents/%%' OR path LIKE '/Users/%%/Downloads/%%' OR path LIKE '/Users/Shared'); description: "Looks for PDF files with file names typically used by 1Password for emergency recovery kits." resolution: "Delete 1Password emergency kits from your computer, and empty the trash. 1Password emergency kits should only be printed and stored in a physically secure location." platform: darwin tags: compliance, built-in - contributors: GuillaumeRoss + contributors: nonpunctual --- apiVersion: v1 kind: query @@ -1067,3 +1067,15 @@ spec: purpose: Informational tags: inventory contributors: lucasmrod,sharon-fdm,zwass +--- +apiVersion: v1 +kind: query +spec: + name: List osquery table names + platform: darwin, linux, windows + description: List all table names in the schema of the currently installed version of osquery + query: SELECT DISTINCT name FROM osquery_registry; + purpose: Informational + tags: fleet, osquery, table, schema + contributors: nonpunctual +--- diff --git a/docs/Configuration/configuration-files/README.md b/docs/Configuration/configuration-files/README.md index 298b2a7f4f1f..a5f8ced68a1c 100644 --- a/docs/Configuration/configuration-files/README.md +++ b/docs/Configuration/configuration-files/README.md @@ -234,19 +234,30 @@ spec: secrets: - secret: RzTlxPvugG4o4O5IKS/HqEDJUmI1hwBoffff - secret: JZ/C/Z7ucq22dt/zjx2kEuDBN0iLjqfz + host_expiry_settings: + host_expiry_enabled: true + host_expiry_window: 14 mdm: macos_updates: minimum_version: "12.3.1" deadline: "2022-01-04" macos_settings: custom_settings: - - path: path/to/profile1.mobileconfig - - path: path/to/profile2.mobileconfig + - path: '/path/to/profile1.mobileconfig' + labels: + - Label name 1 + - path: '/path/to/profile2.mobileconfig' + - path: '/path/to/profile3.mobileconfig' + labels: + - Label name 2 + - Label name 3 enable_disk_encryption: true windows_settings: custom_settings: - - path: path/to/profile3.xml - - path: path/to/profile4.xml + - path: '/path/to/profile4.xml' + labels: + - Label name 4 + - path: '/path/to/profile5.xml' scripts: - path/to/script1.sh - path/to/script2.sh @@ -456,13 +467,21 @@ spec: deadline: "" macos_settings: custom_settings: - - path: path/to/profile1.mobileconfig - - path: path/to/profile2.mobileconfig + - path: '/path/to/profile1.mobileconfig' + labels: + - Label name 1 + - path: '/path/to/profile2.mobileconfig' + - path: '/path/to/profile3.mobileconfig' + labels: + - Label name 2 + - Label name 3 enable_disk_encryption: true windows_settings: custom_settings: - - path: path/to/profile3.xml - - path: path/to/profile4.xml + - path: '/path/to/profile4.xml' + labels: + - Label name 4 + - path: '/path/to/profile5.xml' ``` ### Settings @@ -1207,8 +1226,14 @@ If you're using Fleet Premium, these profiles apply to all hosts assigned to no mdm: macos_settings: custom_settings: - - path: path/to/profile1.mobileconfig - - path: path/to/profile2.mobileconfig + - path: '/path/to/profile1.mobileconfig' + labels: + - Label name 1 + - path: '/path/to/profile2.mobileconfig' + - path: '/path/to/profile3.mobileconfig' + labels: + - Label name 2 + - Label name 3 ``` ##### mdm.macos_settings.enable_disk_encryption @@ -1247,8 +1272,10 @@ If you're using Fleet Premium, these profiles apply to all hosts assigned to no mdm: windows_settings: custom_settings: - - path: path/to/profile1.xml - - path: path/to/profile2.xml + - path: '/path/to/profile1.xml' + labels: + - Label name 1 + - path: '/path/to/profile2.xml' ``` #### Scripts diff --git a/docs/Configuration/configuration-files/kubernetes/fleet-deployment.yml b/docs/Configuration/configuration-files/kubernetes/fleet-deployment.yml index da6fc2b677f6..e7badbf04f05 100644 --- a/docs/Configuration/configuration-files/kubernetes/fleet-deployment.yml +++ b/docs/Configuration/configuration-files/kubernetes/fleet-deployment.yml @@ -1,4 +1,4 @@ -apiVersion: apps/v1beta2 +apiVersion: apps/v1 kind: Deployment metadata: name: fleet-webserver @@ -20,10 +20,10 @@ spec: secretName: fleet-tls containers: - name: fleet-webserver - image: fleetdm/fleet:4.0.1 + image: fleetdm/fleet:v4.43.3 command: ["fleet", "serve"] ports: - - containerPort: 443 + - containerPort: 8443 volumeMounts: - name: fleet-tls mountPath: "/secrets/fleet-tls" @@ -37,14 +37,14 @@ spec: name: fleet-database-mysql key: mysql-password - name: FLEET_REDIS_ADDRESS - value: fleet-cache-redis:6379 + value: fleet-cache-redis-master:6379 - name: FLEET_REDIS_PASSWORD valueFrom: secretKeyRef: name: fleet-cache-redis key: redis-password - name: FLEET_SERVER_ADDRESS - value: "0.0.0.0:443" + value: "0.0.0.0:8443" - name: FLEET_SERVER_CERT value: "/secrets/fleet-tls/tls.crt" - name: FLEET_SERVER_KEY diff --git a/docs/Configuration/configuration-files/kubernetes/fleet-migrations.yml b/docs/Configuration/configuration-files/kubernetes/fleet-migrations.yml index f6dc7ebfc3d5..8e432b189c0e 100644 --- a/docs/Configuration/configuration-files/kubernetes/fleet-migrations.yml +++ b/docs/Configuration/configuration-files/kubernetes/fleet-migrations.yml @@ -9,7 +9,7 @@ spec: spec: containers: - name: fleet - image: fleetdm/fleet:4.0.1 + image: fleetdm/fleet:v4.43.3 command: ["fleet", "prepare", "db"] env: - name: FLEET_MYSQL_ADDRESS diff --git a/docs/Configuration/configuration-files/kubernetes/fleet-service.yml b/docs/Configuration/configuration-files/kubernetes/fleet-service.yml index 098270f02183..621199dba1aa 100644 --- a/docs/Configuration/configuration-files/kubernetes/fleet-service.yml +++ b/docs/Configuration/configuration-files/kubernetes/fleet-service.yml @@ -9,7 +9,7 @@ spec: ports: - name: proxy-tls port: 443 - targetPort: 443 + targetPort: 8443 protocol: TCP - name: proxy-http port: 80 diff --git a/docs/Contributing/API-for-contributors.md b/docs/Contributing/API-for-contributors.md index b1367effa677..a89269c6c7d2 100644 --- a/docs/Contributing/API-for-contributors.md +++ b/docs/Contributing/API-for-contributors.md @@ -597,7 +597,8 @@ Once base64-decoded, they are PEM-encoded certificate and keys. | team_id | number | query | _Available in Fleet Premium_ The team ID to apply the custom settings to. Only one of `team_name`/`team_id` can be provided. | | team_name | string | query | _Available in Fleet Premium_ The name of the team to apply the custom settings to. Only one of `team_name`/`team_id` can be provided. | | dry_run | bool | query | Validate the provided profiles and return any validation errors, but do not apply the changes. | -| profiles | json | body | An array of strings, the base64-encoded .mobileconfig (macOS) or XML (Windows) files to apply. | +| profiles | json | body | An array of objects, consisting of a `profile` base64-encoded .mobileconfig (macOS) or XML (Windows) file, `labels` array of strings (label names), and `name` display name (only for Windows configuration profiles). | + If no team (id or name) is provided, the profiles are applied for all hosts (for _Fleet Free_) or for hosts that are not assigned to any team (for _Fleet Premium_). After the call, the provided list of `profiles` will be the active profiles for that team (or no team) - that is, any existing profile that is not part of that list will be removed, and an existing profile with the same payload identifier (macOS) as a new profile will be edited. If the list of provided `profiles` is empty, all profiles are removed for that team (or no team). @@ -1280,9 +1281,9 @@ If the `name` is not already associated with an existing team, this API route cr | mdm.macos_updates.minimum_version | string | body | The required minimum operating system version. | | mdm.macos_updates.deadline | string | body | The required installation date for Nudge to enforce the operating system version. | | mdm.macos_settings | object | body | The macOS-specific MDM settings. | -| mdm.macos_settings.custom_settings | list | body | The list of .mobileconfig files (profiles) to apply to hosts that belong to this team. | +| mdm.macos_settings.custom_settings | list | body | The list of objects consists of a `path` to .mobileconfig file and `labels` list of label names. | | mdm.windows_settings | object | body | The Windows-specific MDM settings. | -| mdm.windows_settings.custom_settings | list | body | The list of XML files (profiles) to apply to hosts that belong to this team. | +| mdm.windows_settings.custom_settings | list | body | The list of objects consists of a `path` to XML files and `labels` list of label names. | | scripts | list | body | A list of script files to add to this team so they can be executed at a later time. | | mdm.macos_settings.enable_disk_encryption | bool | body | Whether disk encryption should be enabled for hosts that belong to this team. | | force | bool | query | Force apply the spec even if there are (ignorable) validation errors. Those are unknown keys and agent options-related validations. | @@ -1342,11 +1343,17 @@ If the `name` is not already associated with an existing team, this API route cr "deadline": "2023-12-01" }, "macos_settings": { - "custom_settings": ["path/to/profile1.mobileconfig"], + "custom_settings": { + "path": "path/to/profile1.mobileconfig" + "labels": ["Label 1", "Label 2"] + }, "enable_disk_encryption": true }, "windows_settings": { - "custom_settings": ["path/to/profile1.xml"] + "custom_settings": { + "path": "path/to/profile1.xml" + "labels": ["Label 1", "Label 2"] + }, } }, "scripts": ["path/to/script.sh"], diff --git a/docs/Contributing/File-carving.md b/docs/Contributing/File-carving.md new file mode 100644 index 000000000000..ca14a295ea10 --- /dev/null +++ b/docs/Contributing/File-carving.md @@ -0,0 +1,122 @@ +## File carving + +Fleet supports osquery's file carving functionality as of Fleet 3.3.0. This allows the Fleet server to request files (and sets of files) from osquery agents, returning the full contents to Fleet. + +File carving data can be either stored in Fleet's database or to an external S3 bucket. For information on how to configure the latter, consult the [configuration docs](https://fleetdm.com/docs/deploying/configuration#s-3-file-carving-backend). + +### Configuration + +Given a working flagfile for connecting osquery agents to Fleet, add the following flags to enable carving: + +```sh +--disable_carver=false +--carver_disable_function=false +--carver_start_endpoint=/api/v1/osquery/carve/begin +--carver_continue_endpoint=/api/v1/osquery/carve/block +--carver_block_size=8000000 +``` + +The default flagfile provided in the "Add New Host" dialog also includes this configuration. + +#### Carver block size + +The `carver_block_size` flag should be configured in osquery. + +For the (default) MySQL Backend, the configured value must be less than the value of +`max_allowed_packet` in the MySQL connection, allowing for some overhead. The default for [MySQL 5.7](https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar_max_allowed_packet) +is 4MB and for [MySQL 8](https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html#sysvar_max_allowed_packet) it is 64MB. + +For the S3/Minio backend, this value must be set to at least 5MiB (`5242880`) due to the +[constraints of S3's multipart +uploads](https://docs.aws.amazon.com/AmazonS3/latest/dev/qfacts.html). + +#### Compression + +Compression of the carve contents can be enabled with the `carver_compression` flag in osquery. When used, the carve results will be compressed with [Zstandard](https://facebook.github.io/zstd/) compression. + +### Usage + +File carves are initiated with osquery queries. Issue a query to the `carves` table, providing `carve = 1` along with the desired path(s) as constraints. + +For example, to extract the `/etc/hosts` file on a host with hostname `mac-workstation`: + +```sh +fleetctl query --hosts mac-workstation --query 'SELECT * FROM carves WHERE carve = 1 AND path = "/etc/hosts"' +``` + +The standard osquery file globbing syntax is also supported to carve entire directories or more: + +```sh +fleetctl query --hosts mac-workstation --query 'SELECT * FROM carves WHERE carve = 1 AND path LIKE "/etc/%%"' +``` + +#### Retrieving carves + +List the non-expired (see below) carves with `fleetctl get carves`. Note that carves will not be available through this command until osquery checks in to the Fleet server with the first of the carve contents. This can take some time from initiation of the carve. + +To also retrieve expired carves, use `fleetctl get carves --expired`. + +Contents of carves are returned as .tar archives, and compressed if that option is configured. + +To download the contents of a carve with ID 3, use + +```sh +fleetctl get carve --outfile carve.tar 3 +``` + +It can also be useful to pipe the results directly into the tar command for unarchiving: + +```sh +fleetctl get carve --stdout 3 | tar -x +``` + +#### Expiration + +Carve contents remain available for 24 hours after the first data is provided from the osquery client. After this time, the carve contents are cleaned from the database and the carve is marked as "expired". + +The same is not true if S3 is used as the storage backend. In that scenario, it is suggested to setup a [bucket lifecycle configuration](https://docs.aws.amazon.com/AmazonS3/latest/dev/object-lifecycle-mgmt.html) to avoid retaining data in excess. Fleet, in an "eventual consistent" manner (i.e. by periodically performing comparisons), will keep the metadata relative to the files carves in sync with what it is actually available in the bucket. + +### Alternative carving backends + +#### Minio + +Configure the following: +- `FLEET_S3_ENDPOINT_URL=minio_host:port` +- `FLEET_S3_BUCKET=minio_bucket_name` +- `FLEET_S3_SECRET_ACCESS_KEY=your_secret_access_key` +- `FLEET_S3_ACCESS_KEY_ID=acces_key_id` +- `FLEET_S3_FORCE_S3_PATH_STYLE=true` +- `FLEET_S3_REGION=minio` or any non-empty string otherwise Fleet will attempt to derive the region. + +### Troubleshooting + +#### Check carve status in osquery + +Osquery can report on the status of carves through queries to the `carves` table. + +The details provided by + +```sh +fleetctl query --labels 'All Hosts' --query 'SELECT * FROM carves' +``` + +can be helpful to debug carving problems. + +#### Ensure `carver_block_size` is set appropriately + +`carver_block_size` is an osquery flag that sets the size of each part of a file carve that osquery +sends to the Fleet server. + +When using the MySQL backend (default), this value must be less than the `max_allowed_packet` +setting in MySQL. If it is too large, MySQL will reject the writes. + +When using S3, the value must be at least 5MiB (5242880 bytes), as smaller multipart upload +sizes are rejected. Additionally, [S3 +limits](https://docs.aws.amazon.com/AmazonS3/latest/userguide/qfacts.html) the maximum number of +parts to 10,000. + +The value must be small enough that HTTP requests do not time out. + +Start with a default of 2MiB for MySQL (2097152 bytes), and 5MiB for S3/Minio (5242880 bytes). + + diff --git a/docs/REST API/rest-api.md b/docs/REST API/rest-api.md index d87cb8c7b941..e21bfd158547 100644 --- a/docs/REST API/rest-api.md +++ b/docs/REST API/rest-api.md @@ -1091,9 +1091,9 @@ Modifies the Fleet's configuration with the supplied information. | enable | boolean | body | _mdm.macos_migration settings_. Whether to enable the end user migration workflow for devices migrating from your old MDM solution. **Requires Fleet Premium license** | | mode | string | body | _mdm.macos_migration settings_. The end user migration workflow mode for devices migrating from your old MDM solution. Options are `"voluntary"` or `"forced"`. **Requires Fleet Premium license** | | webhook_url | string | body | _mdm.macos_migration settings_. The webhook url configured to receive requests to unenroll devices migrating from your old MDM solution. **Requires Fleet Premium license** | -| custom_settings | list | body | _mdm.macos_settings settings_. Hosts that belong to no team and are enrolled into Fleet's MDM will have those custom profiles applied. | +| custom_settings | list | body | _mdm.macos_settings settings_. macOS hosts that belong to no team, and are members of specified labels will have custom profiles applied. | | enable_disk_encryption | boolean | body | _mdm.macos_settings settings_. Hosts that belong to no team and are enrolled into Fleet's MDM will have disk encryption enabled if set to true. **Requires Fleet Premium license** | -| custom_settings | list | body | _mdm.windows_settings settings_. Hosts that belong to no team and are enrolled into Fleet's MDM will have those custom profiles applied. | +| custom_settings | list | body | _mdm.windows_settings settings_. Windows hosts that belong to no team, and are members of specified labels will have custom profiles applied. | | scripts | list | body | A list of script files to add so they can be executed at a later time. | | enable_end_user_authentication | boolean | body | _mdm.macos_setup settings_. If set to true, end user authentication will be required during automatic MDM enrollment of new macOS devices. Settings for your IdP provider must also be [configured](https://fleetdm.com/docs/using-fleet/mdm-macos-setup-experience#end-user-authentication-and-eula). **Requires Fleet Premium license** | | additional_queries | boolean | body | Whether or not additional queries are enabled on hosts. | @@ -1189,11 +1189,17 @@ Note that when making changes to the `integrations` object, all integrations mus "grace_period_days": 1 }, "macos_settings": { - "custom_settings": ["path/to/profile1.mobileconfig"], + "custom_settings": { + "path": "path/to/profile1.mobileconfig" + "labels": ["Label 1", "Label 2"] + }, "enable_disk_encryption": true }, "windows_settings": { - "custom_settings": ["path/to/profile2.xml"], + "custom_settings": { + "path": "path/to/profile1.xml" + "labels": ["Label 1", "Label 2"] + } }, "end_user_authentication": { "entity_id": "", @@ -1825,6 +1831,8 @@ None. - [Get host's scripts](#get-hosts-scripts) - [Get hosts report in CSV](#get-hosts-report-in-csv) - [Get host's disk encryption key](#get-hosts-disk-encryption-key) +- [Lock host](#lock-host) +- [Unlock host](#unlock-host) - [Get host's past activity](#get-hosts-past-activity) - [Get host's upcoming activity](#get-hosts-upcoming-activity) - [Live query one host (ad-hoc)](#live-query-one-host-ad-hoc) @@ -2019,7 +2027,9 @@ If `after` is being used with `created_at` or `updated_at`, the table must be sp "encryption_key_available": false, "enrollment_status": null, "name": "", - "server_url": null + "server_url": null, + "device_status": "unlocked", + "pending_action": "" }, "software": [ { @@ -2451,6 +2461,8 @@ Returns the information of the specified host. "enrollment_status": null, "name": "", "server_url": null, + "device_status": "unlocked", + "pending_action": "", "macos_settings": { "disk_encryption": null, "action_required": null @@ -2660,6 +2672,8 @@ Returns the information of the host specified using the `uuid`, `hardware_serial "enrollment_status": null, "name": "", "server_url": null, + "device_status": "unlocked", + "pending_action": "lock", "macos_settings": { "disk_encryption": null, "action_required": null @@ -3758,6 +3772,67 @@ Retrieves a list of the configuration profiles assigned to a host. } ``` +### Lock host + +_Available in Fleet Premium_ + +Sends a command to lock the specified macOS, Linux, or Windows host. The host is locked once it comes online. + +To lock a macOS host, the host must have MDM turned on. To lock a Windows or Linux host, the host must have [scripts enabled](https://fleetdm.com/docs/using-fleet/scripts). + + +`POST /api/v1/fleet/hosts/:id/lock` + +#### Parameters + +| Name | Type | In | Description | +| ---------- | ----------------- | ---- | ----------------------------------------------------------------------------- | +| id | integer | path | **Required**. ID of the host to be locked. | + +#### Example + +`POST /api/v1/fleet/hosts/123/lock` + +##### Default response + +`Status: 204` + +### Unlock host + +_Available in Fleet Premium_ + +Sends a command to unlock the specified Windows or Linux host, or retrieves the unlock PIN for a macOS host. + +To unlock a Windows or Linux host, the host must have [scripts enabled](https://fleetdm.com/docs/using-fleet/scripts). + +`POST /api/v1/fleet/hosts/:id/unlock` + +#### Parameters + +| Name | Type | In | Description | +| ---------- | ----------------- | ---- | ----------------------------------------------------------------------------- | +| id | integer | path | **Required**. ID of the host to be unlocked. | + +#### Example + +`POST /api/v1/fleet/hosts/:id/unlock` + +##### Default response (Windows or Linux hosts) + +`Status: 204` + +##### Default response (macOS hosts) + +`Status: 200` + +```json +{ + "host_id": 8, + "unlock_pin": "123456" +} +``` + + ### Get host's past activity `GET /api/v1/fleet/hosts/:id/activites/past` @@ -4475,6 +4550,7 @@ Add a configuration profile to enforce custom settings on macOS and Windows host | ------------------------- | -------- | ---- | ------------------------------------------------------------------------------------------------------------- | | profile | file | form | **Required.** The .mobileconfig (macOS) or XML (Windows) file containing the profile. | | team_id | string | form | _Available in Fleet Premium_ The team ID for the profile. If specified, the profile is applied to only hosts that are assigned to the specified team. If not specified, the profile is applied to only to hosts that are not assigned to any team. | +| labels | array | form | _Available in Fleet Premium_ An array of labels to filter hosts in a team (or no team) that should get a profile. | #### Example @@ -4499,6 +4575,14 @@ Content-Disposition: form-data; name="team_id" 1 --------------------------f02md47480und42y +Content-Disposition: form-data; name="labels" + +Label name 1 +--------------------------f02md47480und42y +Content-Disposition: form-data; name="labels" + +Label name 2 +--------------------------f02md47480und42y Content-Disposition: form-data; name="profile"; filename="Foo.mobileconfig" Content-Type: application/octet-stream @@ -4562,7 +4646,7 @@ Retrieves the manual enrollment profile for macOS hosts. Install this profile on ### List custom OS settings (configuration profiles) -> [List custom macOS setting](https://github.com/fleetdm/fleet/blob/fleet-v4.40.0/docs/REST%20API/rest-api.md#list-custom-macos-settings-configuration-profiles) (`GET /api/v1/fleet/mdm/apple/profiles`) API endpoint is deprecated as of Fleet 4.41. It is maintained for backwards compatibility. Please use the below API endpoint instead. +> [List custom macOS settings](https://github.com/fleetdm/fleet/blob/fleet-v4.40.0/docs/REST%20API/rest-api.md#list-custom-macos-settings-configuration-profiles) (`GET /api/v1/fleet/mdm/apple/profiles`) API endpoint is deprecated as of Fleet 4.41. It is maintained for backwards compatibility. Please use the below API endpoint instead. Get a list of the configuration profiles in Fleet. @@ -4601,16 +4685,30 @@ List all configuration profiles for macOS and Windows hosts enrolled to Fleet's "identifier": "com.example.profile", "created_at": "2023-03-31T00:00:00Z", "updated_at": "2023-03-31T00:00:00Z", - "checksum": "dGVzdAo=" + "checksum": "dGVzdAo=", + "labels": [ + { + "name": "Label name 1" + } + ] }, { - "profile_uuid": "f5ad01cc-f416-4b5f-88f3-a26da3b56a19", + "profile_uuid": f5ad01cc-f416-4b5f-88f3-a26da3b56a19, "team_id": 0, "name": "Example Windows profile", "platform": "windows", "created_at": "2023-04-31T00:00:00Z", "updated_at": "2023-04-31T00:00:00Z", - "checksum": "aCLemVr)" + "checksum": "aCLemVr)", + "labels": [ + { + "name": "Label name 1", + "broken": true + }, + { + "name": "Label name 2" + } + ] } ], "meta": { @@ -4620,6 +4718,8 @@ List all configuration profiles for macOS and Windows hosts enrolled to Fleet's } ``` +If one or more assigned labels are deleted the profile is considered broken (`broken: true`). It won’t be applied to new hosts. + ### Get or download custom OS setting (configuration profile) > [Download custom macOS setting](https://github.com/fleetdm/fleet/blob/fleet-v4.40.0/docs/REST%20API/rest-api.md#download-custom-macos-setting-configuration-profile) (`GET /api/v1/fleet/mdm/apple/profiles/:profile_id`) API endpoint is deprecated as of Fleet 4.41. It is maintained for backwards compatibility. Please use the API endpoint below instead. @@ -4650,7 +4750,16 @@ List all configuration profiles for macOS and Windows hosts enrolled to Fleet's "identifier": "com.example.profile", "created_at": "2023-03-31T00:00:00Z", "updated_at": "2023-03-31T00:00:00Z", - "checksum": "dGVzdAo=" + "checksum": "dGVzdAo=", + "labels": [ + { + "name": "Label name 1", + "broken": true + }, + { + "name": "Label name 2", + } + ] } ``` @@ -4874,7 +4983,8 @@ This endpoint returns the results for a specific custom MDM command. "updated_at": "2023-04-04:00:00Z", "request_type": "ProfileList", "hostname": "mycomputer", - "result": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCFET0NUWVBFIHBsaXN0IFBVQkxJQyAiLS8vQXBwbGUvL0RURCBQTElTVCAxLjAvL0VOIiAiaHR0cDovL3d3dy5hcHBsZS5jb20vRFREcy9Qcm9wZXJ0eUxpc3QtMS4wLmR0ZCI-CjxwbGlzdCB2ZXJzaW9uPSIxLjAiPgo8ZGljdD4KICAgIDxrZXk-Q29tbWFuZDwva2V5PgogICAgPGRpY3Q-CiAgICAgICAgPGtleT5NYW5hZ2VkT25seTwva2V5PgogICAgICAgIDxmYWxzZS8-CiAgICAgICAgPGtleT5SZXF1ZXN0VHlwZTwva2V5PgogICAgICAgIDxzdHJpbmc-UHJvZmlsZUxpc3Q8L3N0cmluZz4KICAgIDwvZGljdD4KICAgIDxrZXk-Q29tbWFuZFVVSUQ8L2tleT4KICAgIDxzdHJpbmc-MDAwMV9Qcm9maWxlTGlzdDwvc3RyaW5nPgo8L2RpY3Q-CjwvcGxpc3Q-" + "payload": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4NCjwhRE9DVFlQRSBwbGlzdCBQVUJMSUMgIi0vL0FwcGxlLy9EVEQgUExJU1QgMS4wLy9FTiIgImh0dHA6Ly93d3cuYXBwbGUuY29tL0RURHMvUHJvcGVydHlMaXN0LTEuMC5kdGQiPg0KPHBsaXN0IHZlcnNpb249IjEuMCI+DQo8ZGljdD4NCg0KCTxrZXk+UGF5bG9hZERlc2NyaXB0aW9uPC9rZXk+DQoJPHN0cmluZz5UaGlzIHByb2ZpbGUgY29uZmlndXJhdGlvbiBpcyBkZXNpZ25lZCB0byBhcHBseSB0aGUgQ0lTIEJlbmNobWFyayBmb3IgbWFjT1MgMTAuMTQgKHYyLjAuMCksIDEwLjE1ICh2Mi4wLjApLCAxMS4wICh2Mi4wLjApLCBhbmQgMTIuMCAodjEuMC4wKTwvc3RyaW5nPg0KCTxrZXk+UGF5bG9hZERpc3BsYXlOYW1lPC9rZXk+DQoJPHN0cmluZz5EaXNhYmxlIEJsdWV0b290aCBzaGFyaW5nPC9zdHJpbmc+DQoJPGtleT5QYXlsb2FkRW5hYmxlZDwva2V5Pg0KCTx0cnVlLz4NCgk8a2V5PlBheWxvYWRJZGVudGlmaWVyPC9rZXk+DQoJPHN0cmluZz5jaXMubWFjT1NCZW5jaG1hcmsuc2VjdGlvbjIuQmx1ZXRvb3RoU2hhcmluZzwvc3RyaW5nPg0KCTxrZXk+UGF5bG9hZFNjb3BlPC9rZXk+DQoJPHN0cmluZz5TeXN0ZW08L3N0cmluZz4NCgk8a2V5PlBheWxvYWRUeXBlPC9rZXk+DQoJPHN0cmluZz5Db25maWd1cmF0aW9uPC9zdHJpbmc+DQoJPGtleT5QYXlsb2FkVVVJRDwva2V5Pg0KCTxzdHJpbmc+NUNFQkQ3MTItMjhFQi00MzJCLTg0QzctQUEyOEE1QTM4M0Q4PC9zdHJpbmc+DQoJPGtleT5QYXlsb2FkVmVyc2lvbjwva2V5Pg0KCTxpbnRlZ2VyPjE8L2ludGVnZXI+DQogICAgPGtleT5QYXlsb2FkUmVtb3ZhbERpc2FsbG93ZWQ8L2tleT4NCiAgICA8dHJ1ZS8+DQoJPGtleT5QYXlsb2FkQ29udGVudDwva2V5Pg0KCTxhcnJheT4NCgkJPGRpY3Q+DQoJCQk8a2V5PlBheWxvYWRDb250ZW50PC9rZXk+DQoJCQk8ZGljdD4NCgkJCQk8a2V5PmNvbS5hcHBsZS5CbHVldG9vdGg8L2tleT4NCgkJCQk8ZGljdD4NCgkJCQkJPGtleT5Gb3JjZWQ8L2tleT4NCgkJCQkJPGFycmF5Pg0KCQkJCQkJPGRpY3Q+DQoJCQkJCQkJPGtleT5tY3hfcHJlZmVyZW5jZV9zZXR0aW5nczwva2V5Pg0KCQkJCQkJCTxkaWN0Pg0KCQkJCQkJCQk8a2V5PlByZWZLZXlTZXJ2aWNlc0VuYWJsZWQ8L2tleT4NCgkJCQkJCQkJPGZhbHNlLz4NCgkJCQkJCQk8L2RpY3Q+DQoJCQkJCQk8L2RpY3Q+DQoJCQkJCTwvYXJyYXk+DQoJCQkJPC9kaWN0Pg0KCQkJPC9kaWN0Pg0KCQkJPGtleT5QYXlsb2FkRGVzY3JpcHRpb248L2tleT4NCgkJCTxzdHJpbmc+RGlzYWJsZXMgQmx1ZXRvb3RoIFNoYXJpbmc8L3N0cmluZz4NCgkJCTxrZXk+UGF5bG9hZERpc3BsYXlOYW1lPC9rZXk+DQoJCQk8c3RyaW5nPkN1c3RvbTwvc3RyaW5nPg0KCQkJPGtleT5QYXlsb2FkRW5hYmxlZDwva2V5Pg0KCQkJPHRydWUvPg0KCQkJPGtleT5QYXlsb2FkSWRlbnRpZmllcjwva2V5Pg0KCQkJPHN0cmluZz4wMjQwREQxQy03MERDLTQ3NjYtOTAxOC0wNDMyMkJGRUVBRDE8L3N0cmluZz4NCgkJCTxrZXk+UGF5bG9hZFR5cGU8L2tleT4NCgkJCTxzdHJpbmc+Y29tLmFwcGxlLk1hbmFnZWRDbGllbnQucHJlZmVyZW5jZXM8L3N0cmluZz4NCgkJCTxrZXk+UGF5bG9hZFVVSUQ8L2tleT4NCgkJCTxzdHJpbmc+MDI0MEREMUMtNzBEQy00NzY2LTkwMTgtMDQzMjJCRkVFQUQxPC9zdHJpbmc+DQoJCQk8a2V5PlBheWxvYWRWZXJzaW9uPC9rZXk+DQoJCQk8aW50ZWdlcj4xPC9pbnRlZ2VyPg0KCQk8L2RpY3Q+DQoJPC9hcnJheT4NCjwvZGljdD4NCjwvcGxpc3Q+", + "result": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4NCjwhRE9DVFlQRSBwbGlzdCBQVUJMSUMgIi0vL0FwcGxlLy9EVEQgUExJU1QgMS4wLy9FTiIgImh0dHA6Ly93d3cuYXBwbGUuY29tL0RURHMvUHJvcGVydHlMaXN0LTEuMC5kdGQiPg0KPHBsaXN0IHZlcnNpb249IjEuMCI+DQo8ZGljdD4NCiAgICA8a2V5PkNvbW1hbmRVVUlEPC9rZXk+DQogICAgPHN0cmluZz4wMDAxX0luc3RhbGxQcm9maWxlPC9zdHJpbmc+DQogICAgPGtleT5TdGF0dXM8L2tleT4NCiAgICA8c3RyaW5nPkFja25vd2xlZGdlZDwvc3RyaW5nPg0KICAgIDxrZXk+VURJRDwva2V5Pg0KICAgIDxzdHJpbmc+MDAwMDgwMjAtMDAwOTE1MDgzQzgwMDEyRTwvc3RyaW5nPg0KPC9kaWN0Pg0KPC9wbGlzdD4=" } ] } @@ -8319,10 +8429,10 @@ _Available in Fleet Premium_ |     deadline_days | integer | body | Hosts that belong to this team and are enrolled into Fleet's MDM will have this number of days before updates are installed on Windows. | |     grace_period_days | integer | body | Hosts that belong to this team and are enrolled into Fleet's MDM will have this number of days before Windows restarts to install updates. | |   macos_settings | object | body | macOS-specific settings. | -|     custom_settings | list | body | The list of .mobileconfig files to apply to macOS hosts that belong to this team. | +|     custom_settings | list | body | The list of objects where each object includes .mobileconfig file (configuration profile) and label name to apply to macOS hosts that belong to this team and are members of the specified label. | |     enable_disk_encryption | boolean | body | Hosts that belong to this team and are enrolled into Fleet's MDM will have disk encryption enabled if set to true. | |   windows_settings | object | body | Windows-specific settings. | -|     custom_settings | list | body | The list of XML files to apply to Windows hosts that belong to this team. | +|     custom_settings | list | body | The list of objects where each object includes XML file (configuration profile) and label name to apply to Windows hosts that belong to this team and are members of the specified label. | |   macos_setup | object | body | Setup for automatic MDM enrollment of macOS hosts. | |     enable_end_user_authentication | boolean | body | If set to true, end user authentication will be required during automatic MDM enrollment of new macOS hosts. Settings for your IdP provider must also be [configured](https://fleetdm.com/docs/using-fleet/mdm-macos-setup-experience#end-user-authentication-and-eula). | | host_expiry_settings | object | body | Host expiry settings for the team. | diff --git a/docs/Using Fleet/Scripts.md b/docs/Using Fleet/Scripts.md index c80fa4f48b60..dfd8a5429aa3 100644 --- a/docs/Using Fleet/Scripts.md +++ b/docs/Using Fleet/Scripts.md @@ -1,7 +1,5 @@ # Scripts -_Available in Fleet Premium_ - In Fleet you can execute a custom script to remediate an issue on your macOS, Windows, and Linux hosts. Shell scripts are supported on macOS and Linux. All scripts will run in the host's (root) default shell (`/bin/sh`). Other interpreters are not supported yet. @@ -34,9 +32,7 @@ Fleet UI: 3. On your target host's host details page, select the **Scripts** tab and select **Actions** to run the script. -> 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](https://fleetdm.com/docs/rest-api/rest-api#run-script] fleetctl CLI: diff --git a/docs/Using Fleet/enroll-hosts.md b/docs/Using Fleet/enroll-hosts.md index 0a8c0e3df71d..e0b2545f0378 100644 --- a/docs/Using Fleet/enroll-hosts.md +++ b/docs/Using Fleet/enroll-hosts.md @@ -130,6 +130,7 @@ How to unenroll a host from Fleet: - [Specifying update channels](#specifying-update-channels) - [Testing osquery queries locally](#testing-osquery-queries-locally) - [Finding fleetd logs](#finding-fleetd-logs) +- [Using system keystore for enroll secret](#using-system-keystore-for-enroll-secret) - [Generating Windows installers using local WiX toolset](#generating-windows-installers-using-local-wix-toolset) - [Experimental features](#experimental-features) @@ -294,6 +295,14 @@ If the `logger_path` agent configuration is set to `filesystem`, fleetd will sen - macOS: /opt/orbit/osquery_log - Linux: /opt/orbit/osquery_log +### Using system keystore for enroll secret + +On macOS and Windows, fleetd will add the enroll secret to the system keystore (Keychain on macOS, Credential Manager on Windows) on launch. Subsequent launches will retrieve the enroll secret from the keystore. + +System keystore access can be disabled via `--disable-keystore` flag for the `fleetctl package` command. On macOS, subsequent installations of fleetd must be signed by the same organization as the original installation to access the enroll secret in the keychain. + +>**Note:** The keychain is not used on macOS when the enroll secret is provided via MDM profile. Keychain support when passing the enroll secret via MDM profile is coming soon. + ### Generating Windows installers using local WiX toolset `Applies only to Fleet Premium` diff --git a/docs/Using Fleet/fleetctl-CLI.md b/docs/Using Fleet/fleetctl-CLI.md index bf173307dd4a..678dbd565e02 100644 --- a/docs/Using Fleet/fleetctl-CLI.md +++ b/docs/Using Fleet/fleetctl-CLI.md @@ -1,34 +1,52 @@ # fleetctl CLI -Fleetctl (pronounced "Fleet control") is a CLI tool for managing Fleet from the command line. Fleetctl enables a GitOps workflow with Fleet and osquery. With fleetctl, you can manage configurations, queries, generate osquery installers, etc. +fleetctl (pronounced "Fleet control") is a CLI tool for managing Fleet from the command line. fleetctl enables a GitOps workflow with Fleet. -Fleetctl also provides a quick way to work with all the data exposed by Fleet without having to use the Fleet UI or work directly with the Fleet API. - -## Using fleetctl - -To install the latest version of `fleetctl` run `npm install -g fleetctl` or download the binary from [GitHub](https://github.com/fleetdm/fleet/releases). - -You can use `fleetctl` to accomplish many tasks you would typically need to do through the Fleet UI. You can even set up or apply configuration files to the Fleet server. +fleetctl also provides a quick way to work with all the data exposed by Fleet without having to use the Fleet UI or work directly with the Fleet API.
+## Installing fleetctl + +Install fleetctl with npm or download the binary from [GitHub](https://github.com/fleetdm/fleet/releases). + +```sh +npm install -g fleetctl +``` + +### Upgrading fleetctl + +The easiest way to update fleetctl is by running the installation command again. + +```sh +npm install -g fleetctl@latest +``` + +## Usage + + ### Available commands + Much of the functionality available in the Fleet UI is also available in `fleetctl`. You can run queries, add and remove users, generate agent (fleetd) installers to add new hosts, get information about existing hosts, and more! -To see the commands you can run with fleetctl, run the `fleetctl --help` command. +To see the available commands you can run: + +```sh +> fleetctl --help +``` ### Get more info about a command -Each command available to `fleetctl` has a help menu with additional information. To pull up the help menu, run `fleetctl --help`, replacing `` with the command you're looking up: +Each command has a help menu with additional information. To pull up the help menu, run `fleetctl --help`, replacing `` with the command you're looking up: ```sh > fleetctl setup --help ``` -You will see more info about the command, including the usage and information about any additional commands and options (or 'flags') that can be passed with it: +You will see more info about the command, including the usage and information about any additional commands and options (or 'flags'): ```sh NAME: @@ -46,123 +64,43 @@ OPTIONS: --context value Name of fleetctl config context to use (default: "default") [$CONTEXT] --debug Enable debug http request logging (default: false) [$DEBUG] --help, -h show help (default: false) - -``` - -## Setting up Fleet - -This section walks through setting up and configuring Fleet via the CLI. If you already have a running Fleet instance, skip ahead to [Logging in to an existing Fleet instance](#logging-in-to-an-existing-fleet-instance) to configure the `fleetctl` CLI. - -This guide illustrates: - -- A minimal CLI workflow for managing an osquery fleet -- The set of API interactions that are required if you want to perform remote, automated management of a Fleet instance - -### Running Fleet - -For the sake of this tutorial, we will be using the local development Docker Compose infrastructure to run Fleet locally. This is documented in some detail in the [developer documentation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/Building-Fleet.md#development-infrastructure), but the following are the minimal set of commands that you can run from the root of the repository (assuming that you have a working Go/JavaScript toolchain installed along with Docker Compose): - -```sh -docker-compose up -d -make deps -make generate -make -./build/fleet prepare db -./build/fleet serve ``` -The `fleet serve` command will be the long running command that runs the Fleet server. +## Authentication -### Fleetctl config +This section walks you through authentication, assuming you already have a running Fleet instance. To learn how to set up new Fleet instance, check out the [Deploy](https://fleetdm.com/docs/deploy/introduction) section or [Building Fleet locally](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/Building-Fleet.md) docs. -At this point, the MySQL database doesn't have any users in it. Because of this, Fleet is exposing a one-time setup endpoint. Before we can hit that endpoint (by running `fleetctl setup`), we have to first configure the local `fleetctl` context. +### Login -Now, since our Fleet instance is local in this tutorial, we didn't get a valid TLS certificate, so we need to run the following to configure our Fleet context: +To log in to your Fleet instance, run following commands: -```sh -fleetctl config set --address https://localhost:8080 --tls-skip-verify -[+] Set the address config key to "https://localhost:8080" in the "default" context -[+] Set the tls-skip-verify config key to "true" in the "default" context -``` - -Now, if you were connecting to a Fleet instance for real, you wouldn't want to skip TLS certificate verification, so you might run something like: +1. Set the Fleet instance address ```sh -fleetctl config set --address https://fleet.corp.example.com -[+] Set the address config key to "https://fleet.corp.example.com" in the "default" context +> fleetctl config set --address https://fleet.example.com +[+] Set the address config key to "https://fleet.example.com" in the "default" context ``` -### Fleetctl setup - -Now that we've configured our local CLI context, lets go ahead and create our admin account: +2. Log in with your credentials ```sh -fleetctl setup --email zwass@example.com --name 'Zach' --org-name 'Fleet Test' -Password: -[+] Fleet setup successful and context configured! -``` - -It's possible to specify the password via the `--password` flag or the `$PASSWORD` environment variable, but be cautious of the security implications of such an action. For local use, the interactive mode above is the most secure. - -### Query hosts - -To run a simple query against all hosts, you might run something like the following: - -```sh -fleetctl query --query 'SELECT * FROM osquery_info;' --labels='All Hosts' > results.json -⠂ 100% responded (100% online) | 1/1 targeted hosts (1/1 online) -^C -``` - -When the query is done (or you have enough results), CTRL-C and look at the `results.json` file: - -```json -{ - "host": "marpaia", - "rows": [ - { - "build_distro": "10.13", - "build_platform": "darwin", - "config_hash": "d7cafcd183cc50c686b4c128263bd4eace5d89e1", - "config_valid": "1", - "extensions": "active", - "host_hostname": "marpaia", - "host_display_name": "marpaia", - "instance_id": "37840766-7182-4a68-a204-c7f577bd71e1", - "pid": "22984", - "start_time": "1527031727", - "uuid": "B312055D-9209-5C89-9DDB-987299518FF7", - "version": "3.2.3", - "watcher": "-1" - } - ] -} -``` - -## Logging in to an existing Fleet instance - -If you have an existing Fleet instance, run `fleetctl login` (after configuring your local CLI context): - -```sh -fleetctl config set --address https://fleet.corp.example.com -[+] Set the address config key to "https://fleet.corp.example.com" in the "default" context - -fleetctl login +> fleetctl login Log in using the standard Fleet credentials. Email: mike@arpaia.co Password: [+] Fleet login successful and context configured! ``` -Once your local context is configured, you can use the above `fleetctl` normally. See `fleetctl --help` for more information. +Once your local context is configured, you can use `fleetctl` normally. -### Logging in with SAML (SSO) authentication +### Log in with SAML (SSO) authentication Users that authenticate to Fleet via SSO should retrieve their API token from the UI and set it manually in their `fleetctl` configuration (instead of logging in via `fleetctl login`). -1. Go to the "My account" page in Fleet (https://fleet.corp.example.com/profile). Click the "Get API token" button to bring up a modal with the API token. - -2. Set the API token in the `~/.fleet/config` file. The file should look like the following: +**Fleet UI:** +1. Go to the **My account** page (https://fleet.example.com/profile) +2. Select the **Get API token** button to bring up a modal with the API token. +3. Set the API token in the `~/.fleet/config` file. ```yaml contexts: @@ -172,75 +110,50 @@ contexts: token: your_token_here ``` -Note the token can also be set with `fleetctl config set --token`, but this may leak the token into a user's shell history. - -## Using fleetctl to configure Fleet - -A Fleet configuration is defined using one or more declarative "messages" in yaml syntax. - -Fleet configuration can be retrieved and applied using the `fleetctl` tool. - -### Fleetctl get - -The `fleetctl get > .yml` command allows you retrieve the current configuration and create a new file for specified Fleet entity (queries, hosts, etc.) - -### Fleetctl apply - -The `fleetctl apply -f .yml` allows you to apply the current configuration in the specified file. - -When a new configuration is applied, agent options are validated. If any errors are found, you will receive an error message describing the issue and the new configuration will not be applied. You can also verify that your agent options are valid without applying using the `--dry-run` flag. Validation is based on the latest version of osquery. If you don't use the latest version of osquery, you can override validation using the `--force` flag. This will update agent options even if they are invalid. - -Check out the [configuration files](https://fleetdm.com/docs/using-fleet/configuration-files) section of the documentation for example yaml files. +The token can also be set with `fleetctl config set --token`, but this may leak the token into a user's shell history. ## Using fleetctl with an API-only user -When running automated workflows using the Fleet API, we recommend an API-only user's API key rather than the API key of a regular user. A regular user's API key expires frequently for security purposes, requiring routine updates. Meanwhile, an API-only user's key does not expire. +When running automated workflows using the Fleet API, we recommend an API-only user's API key rather than the API key of a regular user. A regular user's API key expires frequently for security purposes, requiring routine updates. Meanwhile, an API-only user's key does not expire. + An API-only user does not have access to the Fleet UI. Instead, it's only purpose is to interact with the API programmatically or from fleetctl. -### Create an API-only user +### Create API-only user -To create your new API-only user, run `fleetctl user create` and pass values for `--name`, `--email`, and `--password`, and include the `--api-only` flag: +To create your new API-only user, use `fleetctl user create`: ```sh fleetctl user create --name "API User" --email api@example.com --password temp@pass123 --api-only ``` -### Creating an API-only user -An API-only user can be given the same permissions as a regular user. The default access level is `Observer`. For more information on permissions, see the [user permissions documentation](https://fleetdm.com/docs/using-fleet/permissions#user-permissions). -If you'd like your API-only user to have a different access level than the default `Observer` role, you can specify what level of access the new user should have using the `--global-role` flag: +To use fleetctl with an API-only user, you will need to log in via `fleetctl`. See [authentication](https://#authentication) above for details. + +#### Permissions + +An API-only user can be given the same permissions as a regular user. The default access level is **Observer**. You can specify what level of access the new user should have using the `--global-role` flag: ```sh -fleetctl user create --name "API User" --email api@example.com --password temp#pass --api-only --global-role admin +fleetctl user create --name "API User" --email api@example.com --password temp@pass123 --api-only --global-role admin ``` -On Fleet Premium, use the `--team` flag setting `team_id:role` to create an API-only user on a team: +On Fleet Premium, use the `--team :` to create an API-only user on a team: ```sh -fleetctl user create --name "API Team Maintainer User" --email apimaintainer@example.com --password temp#pass --team 4:maintainer +fleetctl user create --name "API User" --email api@example.com --password temp@pass123 --api-only --team 4: gitops ``` -Assigning the [GitOps role](https://fleetdm.com/docs/using-fleet/permissions#gitops) to a user is also completed using this method because GitOps is an API-only role. - -### Changing permissions of an API-only user +#### Changing permissions To change roles of a current user, log into the Fleet UI as an admin and navigate to **Settings > Users**. - > Suggestion: To disable/enable a user's access to the UI (converting a regular user to an API-only user or vice versa), create a new user. -### Use fleetctl as an API-only user - -To use fleetctl with an API-only user, you will need to log in with `fleetctl login`. Once done, you'll be able to perform tasks using `fleetctl` as your new API-only user. - -> If you are using a version of Fleet older than `4.13.0`, you will need to [reset the API-only user's password](https://github.com/fleetdm/fleet/blob/a1eba3d5b945cb3339004dd1181526c137dc901c/docs/Using-Fleet/fleetctl-CLI.md#reset-the-password) before running queries. +### Get API token for API-only user -### Get the API token of an API-only user -To get the API key of an API-only user, you need to call the Login API with the credentials supplied during user creation. - -For example, say the credentials provided were `api@example.com` for the email and `foobar12345` for the password. You may call the [Log in API](https://fleetdm.com/docs/using-fleet/rest-api#log-in) like so: +To get the API key of an API-only user, you need to call the [login API](https://fleetdm.com/docs/rest-api/rest-api#log-in) with the credentials supplied during user creation. ```sh -curl --location --request POST 'https://myfleetdomain.com/api/v1/fleet/login' \ +curl --location --request POST 'https://fleet.example.com/api/v1/fleet/login' \ --header 'Content-Type: application/json' \ --data-raw '{ "email": "api@example.com", @@ -286,131 +199,6 @@ Password: Running a command with no context will use the default profile. -## MDM commands - -With fleetctl, you can run MDM commands to take some action on your macOS hosts, like restart the host, remotely. Learn how [here](./MDM-commands.md). - -## File carving - -Fleet supports osquery's file carving functionality as of Fleet 3.3.0. This allows the Fleet server to request files (and sets of files) from osquery agents, returning the full contents to Fleet. - -File carving data can be either stored in Fleet's database or to an external S3 bucket. For information on how to configure the latter, consult the [configuration docs](https://fleetdm.com/docs/deploying/configuration#s-3-file-carving-backend). - -### Configuration - -Given a working flagfile for connecting osquery agents to Fleet, add the following flags to enable carving: - -```sh ---disable_carver=false ---carver_disable_function=false ---carver_start_endpoint=/api/v1/osquery/carve/begin ---carver_continue_endpoint=/api/v1/osquery/carve/block ---carver_block_size=8000000 -``` - -The default flagfile provided in the "Add New Host" dialog also includes this configuration. - -#### Carver block size - -The `carver_block_size` flag should be configured in osquery. - -For the (default) MySQL Backend, the configured value must be less than the value of -`max_allowed_packet` in the MySQL connection, allowing for some overhead. The default for [MySQL 5.7](https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar_max_allowed_packet) -is 4MB and for [MySQL 8](https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html#sysvar_max_allowed_packet) it is 64MB. - -For the S3/Minio backend, this value must be set to at least 5MiB (`5242880`) due to the -[constraints of S3's multipart -uploads](https://docs.aws.amazon.com/AmazonS3/latest/dev/qfacts.html). - -#### Compression - -Compression of the carve contents can be enabled with the `carver_compression` flag in osquery. When used, the carve results will be compressed with [Zstandard](https://facebook.github.io/zstd/) compression. - -### Usage - -File carves are initiated with osquery queries. Issue a query to the `carves` table, providing `carve = 1` along with the desired path(s) as constraints. - -For example, to extract the `/etc/hosts` file on a host with hostname `mac-workstation`: - -```sh -fleetctl query --hosts mac-workstation --query 'SELECT * FROM carves WHERE carve = 1 AND path = "/etc/hosts"' -``` - -The standard osquery file globbing syntax is also supported to carve entire directories or more: - -```sh -fleetctl query --hosts mac-workstation --query 'SELECT * FROM carves WHERE carve = 1 AND path LIKE "/etc/%%"' -``` - -#### Retrieving carves - -List the non-expired (see below) carves with `fleetctl get carves`. Note that carves will not be available through this command until osquery checks in to the Fleet server with the first of the carve contents. This can take some time from initiation of the carve. - -To also retrieve expired carves, use `fleetctl get carves --expired`. - -Contents of carves are returned as .tar archives, and compressed if that option is configured. - -To download the contents of a carve with ID 3, use - -```sh -fleetctl get carve --outfile carve.tar 3 -``` - -It can also be useful to pipe the results directly into the tar command for unarchiving: - -```sh -fleetctl get carve --stdout 3 | tar -x -``` - -#### Expiration - -Carve contents remain available for 24 hours after the first data is provided from the osquery client. After this time, the carve contents are cleaned from the database and the carve is marked as "expired". - -The same is not true if S3 is used as the storage backend. In that scenario, it is suggested to setup a [bucket lifecycle configuration](https://docs.aws.amazon.com/AmazonS3/latest/dev/object-lifecycle-mgmt.html) to avoid retaining data in excess. Fleet, in an "eventual consistent" manner (i.e. by periodically performing comparisons), will keep the metadata relative to the files carves in sync with what it is actually available in the bucket. - -### Alternative carving backends - -#### Minio - -Configure the following: -- `FLEET_S3_ENDPOINT_URL=minio_host:port` -- `FLEET_S3_BUCKET=minio_bucket_name` -- `FLEET_S3_SECRET_ACCESS_KEY=your_secret_access_key` -- `FLEET_S3_ACCESS_KEY_ID=acces_key_id` -- `FLEET_S3_FORCE_S3_PATH_STYLE=true` -- `FLEET_S3_REGION=minio` or any non-empty string otherwise Fleet will attempt to derive the region. - -### Troubleshooting - -#### Check carve status in osquery - -Osquery can report on the status of carves through queries to the `carves` table. - -The details provided by - -```sh -fleetctl query --labels 'All Hosts' --query 'SELECT * FROM carves' -``` - -can be helpful to debug carving problems. - -#### Ensure `carver_block_size` is set appropriately - -`carver_block_size` is an osquery flag that sets the size of each part of a file carve that osquery -sends to the Fleet server. - -When using the MySQL backend (default), this value must be less than the `max_allowed_packet` -setting in MySQL. If it is too large, MySQL will reject the writes. - -When using S3, the value must be at least 5MiB (5242880 bytes), as smaller multipart upload -sizes are rejected. Additionally, [S3 -limits](https://docs.aws.amazon.com/AmazonS3/latest/userguide/qfacts.html) the maximum number of -parts to 10,000. - -The value must be small enough that HTTP requests do not time out. - -Start with a default of 2MiB for MySQL (2097152 bytes), and 5MiB for S3/Minio (5242880 bytes). - ## Debugging Fleet `fleetctl` provides debugging capabilities about the running Fleet server via the `debug` command. To see a complete list of all the options run: @@ -433,4 +221,4 @@ This will generate a `tar.gz` file with: - \ No newline at end of file + diff --git a/ee/fleetd-chrome/package-lock.json b/ee/fleetd-chrome/package-lock.json index 32acbb513178..dfdbb4e32750 100644 --- a/ee/fleetd-chrome/package-lock.json +++ b/ee/fleetd-chrome/package-lock.json @@ -1,12 +1,12 @@ { "name": "fleetd-for-chrome", - "version": "1.1.3", + "version": "1.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "fleetd-for-chrome", - "version": "1.1.3", + "version": "1.2.0", "dependencies": { "dotenv": "^16.0.3", "wa-sqlite": "github:rhashimoto/wa-sqlite#v0.9.11" diff --git a/ee/fleetd-chrome/package.json b/ee/fleetd-chrome/package.json index e1a0d61b7394..ad2d9a08f359 100644 --- a/ee/fleetd-chrome/package.json +++ b/ee/fleetd-chrome/package.json @@ -1,7 +1,7 @@ { "name": "fleetd-for-chrome", "description": "Extension for Fleetd on ChromeOS", - "version": "1.1.3", + "version": "1.2.0", "dependencies": { "dotenv": "^16.0.3", "wa-sqlite": "github:rhashimoto/wa-sqlite#v0.9.11" diff --git a/ee/fleetd-chrome/src/tables/Table.ts b/ee/fleetd-chrome/src/tables/Table.ts index 93deadb6e104..42b163051b1c 100644 --- a/ee/fleetd-chrome/src/tables/Table.ts +++ b/ee/fleetd-chrome/src/tables/Table.ts @@ -14,7 +14,6 @@ const CONCAT_CHROME_WARNINGS = (warnings: ChromeWarning[]): string => { class cursorState { rowIndex: number; rows: Record[]; - error: any; } interface ChromeWarning { @@ -121,10 +120,10 @@ export default abstract class Table implements SQLiteModule { } cursorState.rows = tableDataReturned.data; } catch (err) { - // Throwing here doesn't seem to work as expected in testing (the error doesn't seem to be - // thrown in a way that it can be caught appropriately), so instead we save the error and - // throw in xEof. - cursorState.error = err; + // We cannot throw inside SQLITE function because it may cause the wasm stack to run out of memory. + // See: https://github.com/rhashimoto/wa-sqlite/issues/156#issuecomment-1942477704 + console.warn("Error generating table data: %s", err); + return SQLite.SQLITE_ERROR; } return SQLite.SQLITE_OK; }); @@ -133,6 +132,9 @@ export default abstract class Table implements SQLiteModule { xNext(pCursor: number): number { // Advance the row index for the cursor. const cursorState = this.cursorStates.get(pCursor); + if (!cursorState || !cursorState.rows) { + return SQLite.SQLITE_ERROR; + } cursorState.rowIndex += 1; return SQLite.SQLITE_OK; } @@ -140,10 +142,8 @@ export default abstract class Table implements SQLiteModule { xEof(pCursor: number): number { // Check whether we've returned all rows (cursor index is beyond number of rows). const cursorState = this.cursorStates.get(pCursor); - // Throw any error saved in the cursor state (because throwing in xFilter doesn't seem to work - // correctly with async code). - if (cursorState.error) { - throw cursorState.error; + if (!cursorState || !cursorState.rows) { + return 1; } return Number(cursorState.rowIndex >= cursorState.rows.length); } diff --git a/ee/fleetd-chrome/src/tables/network_interfaces.ts b/ee/fleetd-chrome/src/tables/network_interfaces.ts index 8e57d575f966..2da372dfaff3 100644 --- a/ee/fleetd-chrome/src/tables/network_interfaces.ts +++ b/ee/fleetd-chrome/src/tables/network_interfaces.ts @@ -5,6 +5,18 @@ export default class TableNetworkInterfaces extends Table { columns = ["mac", "ipv4", "ipv6"]; async generate() { + if (!chrome.enterprise) { + return { + data: [], + warnings: [ + { + column: "mac", + error_message: "chrome.enterprise API is not available for network details", + }, + ], + }; + } + // @ts-expect-error @types/chrome doesn't yet have the getNetworkDetails Promise API. const networkDetails = (await chrome.enterprise.networkingAttributes.getNetworkDetails()) as chrome.enterprise.networkingAttributes.NetworkDetails; const ipv4 = networkDetails.ipv4; diff --git a/ee/fleetd-chrome/src/tables/os_version.test.ts b/ee/fleetd-chrome/src/tables/os_version.test.ts index 294883924254..7a954b281b64 100644 --- a/ee/fleetd-chrome/src/tables/os_version.test.ts +++ b/ee/fleetd-chrome/src/tables/os_version.test.ts @@ -39,6 +39,8 @@ describe("os_version", () => { ); const db = await VirtualDatabase.init(); + globalThis.DB = db; + const res = await db.query("select * from os_version"); expect(res).toEqual({ data: [ @@ -55,7 +57,7 @@ describe("os_version", () => { codename: "ChromeOS 13.2.1", }, ], - warnings: null, + warnings: "", }); }); @@ -83,6 +85,7 @@ describe("os_version", () => { console.warn = jest.fn(); const db = await VirtualDatabase.init(); + globalThis.DB = db; const res = await db.query("select * from os_version"); expect(res).toEqual({ data: [ @@ -99,7 +102,7 @@ describe("os_version", () => { codename: "ChromeOS 13.2.1", }, ], - warnings: null, + warnings: "", }); expect(console.warn).toHaveBeenCalledWith( expect.stringContaining("expected 4 segments") @@ -111,18 +114,18 @@ describe("os_version", () => { global.navigator.userAgentData = { getHighEntropyValues: jest.fn(() => Promise.resolve({ - data: { - fullVersionList: [ - { brand: "Not even chrome", version: "110.0.5481.177" }, - ], - }, + fullVersionList: [ + { brand: "Not even Chrome", version: "103.0.5060.134" }, + { brand: "Not chrome", version: "103.0.5060.134" }, + ], }) ), }; const db = await VirtualDatabase.init(); - expect(async () => { - await db.query("select * from os_version"); - }).rejects.toThrow(); + globalThis.DB = db; + + const res = await db.query("select * from os_version"); + expect(res.warnings).toContain("environment does not look like Chrome"); }); }); diff --git a/ee/fleetd-chrome/src/tables/os_version.ts b/ee/fleetd-chrome/src/tables/os_version.ts index 2c35517dbd27..f63b09356056 100644 --- a/ee/fleetd-chrome/src/tables/os_version.ts +++ b/ee/fleetd-chrome/src/tables/os_version.ts @@ -24,49 +24,96 @@ export default class TableOSVersion extends Table { } async generate() { - // @ts-expect-error Typescript doesn't include the userAgentData API yet. - const data = await navigator.userAgentData.getHighEntropyValues([ - "fullVersionList", - "platform", - "platformVersion", - ]); + let warningsArray = []; let version = ""; - for (let entry of data.fullVersionList) { - if (entry.brand === "Google Chrome") { - version = entry.version; - break; + let name = ""; + let codename = ""; + let major = ""; + let minor = ""; + let build = ""; + let patch = ""; + try { + // @ts-expect-error Typescript doesn't include the userAgentData API yet. + const data = await navigator.userAgentData.getHighEntropyValues([ + "fullVersionList", + "platform", + "platformVersion", + ]); + + for (let entry of data.fullVersionList) { + if (entry.brand === "Google Chrome") { + version = entry.version; + break; + } } - } - if (version === "") { - throw new Error("environment does not look like Chrome"); - } + if (version === "") { + throw new Error("environment does not look like Chrome"); + } + name = this.getName(data.platform); + codename = this.getCodename(data.platformVersion); - // Note MAJOR.MINOR.BUILD.PATCH (see https://www.chromium.org/developers/version-numbers/) - const splits = version.split("."); - let major = "", - minor = "", - build = "", - patch = ""; - if (splits.length !== 4) { - console.warn( - `Chrome version ${version} does not have expected 4 segments` - ); - } else { - [major, minor, build, patch] = splits; + // Note MAJOR.MINOR.BUILD.PATCH (see https://www.chromium.org/developers/version-numbers/) + const splits = version.split("."); + if (splits.length !== 4) { + console.warn( + `Chrome version ${version} does not have expected 4 segments` + ); + } else { + [major, minor, build, patch] = splits; + } + } catch (err) { + console.warn("getHighEntropyValues:", err); + warningsArray.push({ + column: "version", + error_message: err.message.toString(), + }); + warningsArray.push({ + column: "major", + error_message: err.message.toString(), + }); + warningsArray.push({ + column: "minor", + error_message: err.message.toString(), + }); + warningsArray.push({ + column: "build", + error_message: err.message.toString(), + }); + warningsArray.push({ + column: "patch", + error_message: err.message.toString(), + }); + warningsArray.push({ + column: "codename", + error_message: err.message.toString(), + }); + warningsArray.push({ + column: "platform_like", + error_message: err.message.toString(), + }); } - // Note we can actually get the platform of Chrome running on non-ChromeOS devices, but instead - // we just hardcode to "chrome" so that Fleet always sees this Chrome extension as a Chrome - // device even when we are doing local dev on a non-ChromeOS machine. - const platformInfo = await chrome.runtime.getPlatformInfo(); - const { arch } = platformInfo; + let arch; + try { + // Note we can actually get the platform of Chrome running on non-ChromeOS devices, but instead + // we just hardcode to "chrome" so that Fleet always sees this Chrome extension as a Chrome + // device even when we are doing local dev on a non-ChromeOS machine. + const platformInfo = await chrome.runtime.getPlatformInfo(); + arch = platformInfo.arch; + } catch (err) { + console.warn("get cpu info:", err); + warningsArray.push({ + column: "arch", + error_message: err.message.toString(), + }); + } // Some of these values won't actually be correct on a non-chromeOS machine. return { data: [ { - name: this.getName(data.platform), + name, platform: "chrome", platform_like: "chrome", version, @@ -74,11 +121,12 @@ export default class TableOSVersion extends Table { minor, build, patch, - codename: this.getCodename(data.platformVersion), + codename, // https://developer.chrome.com/docs/extensions/reference/runtime/#type-PlatformArch - arch: arch, + arch, }, ], + warnings: warningsArray, }; } } diff --git a/ee/fleetd-chrome/updates-beta.xml b/ee/fleetd-chrome/updates-beta.xml index 668c0f64dd5b..03bddca04fd0 100644 --- a/ee/fleetd-chrome/updates-beta.xml +++ b/ee/fleetd-chrome/updates-beta.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/ee/fleetd-chrome/updates.xml b/ee/fleetd-chrome/updates.xml index 9881e9203555..7bf2b24b2ca2 100644 --- a/ee/fleetd-chrome/updates.xml +++ b/ee/fleetd-chrome/updates.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/ee/server/service/software.go b/ee/server/service/software.go index 0e63e02bcead..3b66ae485a30 100644 --- a/ee/server/service/software.go +++ b/ee/server/service/software.go @@ -12,7 +12,7 @@ func (svc *Service) ListSoftware(ctx context.Context, opts fleet.SoftwareListOpt return svc.Service.ListSoftware(ctx, opts) } -func (svc *Service) SoftwareByID(ctx context.Context, id uint, includeCVEScores bool) (*fleet.Software, error) { +func (svc *Service) SoftwareByID(ctx context.Context, id uint, teamID *uint, _ bool) (*fleet.Software, error) { // reuse SoftwareByID, but include cve scores in premium version - return svc.Service.SoftwareByID(ctx, id, true) + return svc.Service.SoftwareByID(ctx, id, teamID, true) } diff --git a/ee/server/service/vulnerabilities.go b/ee/server/service/vulnerabilities.go new file mode 100644 index 000000000000..6c2730f0a4ed --- /dev/null +++ b/ee/server/service/vulnerabilities.go @@ -0,0 +1,26 @@ +package service + +import ( + "context" + + "github.com/fleetdm/fleet/v4/server/fleet" +) + +var eeValidVulnSortColumns = []string{ + "cve", + "hosts_count", + "created_at", + "cvss_score", + "epss_probability", + "published", +} + +func (svc *Service) ListVulnerabilities(ctx context.Context, opt fleet.VulnListOptions) ([]fleet.VulnerabilityWithMetadata, *fleet.PaginationMetadata, error) { + opt.ValidSortColumns = eeValidVulnSortColumns + opt.IsEE = true + return svc.Service.ListVulnerabilities(ctx, opt) +} + +func (svc *Service) Vulnerability(ctx context.Context, cve string, teamID *uint, useCVSScores bool) (*fleet.VulnerabilityWithMetadata, error) { + return svc.Service.Vulnerability(ctx, cve, teamID, true) +} diff --git a/frontend/__mocks__/operatingSystemsMock.ts b/frontend/__mocks__/operatingSystemsMock.ts index 6b376328d57d..45cdb23349dc 100644 --- a/frontend/__mocks__/operatingSystemsMock.ts +++ b/frontend/__mocks__/operatingSystemsMock.ts @@ -9,7 +9,7 @@ const DEFAULT_OS_VERSION: IOperatingSystemVersion = { version: "10.15.7", platform: "darwin", hosts_count: 1, - generated_cpe: "cpe:/o:apple:mac_os_x:10.15.7", + generated_cpes: ["cpe:/o:apple:mac_os_x:10.15.7"], vulnerabilities: [createMockSoftwareVulnerability()], }; diff --git a/frontend/__mocks__/vulnerabilitiesMock.ts b/frontend/__mocks__/vulnerabilitiesMock.ts new file mode 100644 index 000000000000..86adde46c05b --- /dev/null +++ b/frontend/__mocks__/vulnerabilitiesMock.ts @@ -0,0 +1,143 @@ +import { IVulnerability } from "interfaces/vulnerability"; +import { + IVulnerabilitiesResponse, + IVulnerabilityResponse, +} from "services/entities/vulnerabilities"; + +const DEFAULT_VULNERABILITY: IVulnerability = { + cve: "CVE-2022-30190", + created_at: "2022-06-01T00:15:00Z", + hosts_count: 1234, + hosts_count_updated_at: "2023-12-20T15:23:57Z", + details_link: "https://nvd.nist.gov/vuln/detail/CVE-2022-30190", + cvss_score: 7.8, // Available in Fleet Premium + epss_probability: 0.9729, // Available in Fleet Premium + cisa_known_exploit: true, // Available in Fleet Premium + cve_published: "2022-06-01T00:15:00Z", // Available in Fleet Premium + cve_description: + "Microsoft Windows Support Diagnostic Tool (MSDT) Remote Code Execution Vulnerability.", // Available in Fleet Premium + resolved_in_version: "", // Available in Fleet Premium + os_versions: [ + { + os_version_id: 1, + name: "bad version", + name_only: "bad version", + version: "1", + platform: "windows", + hosts_count: 5, + resolved_in_version: "2", + generated_cpes: [], + }, + ], + software: [ + { + id: 1, + name: "bad software", + version: "1.1.1", + bundle_identifier: "com.bad.software", + source: "apps", + generated_cpe: "cpe:/a:bad:software:1.1.1", + hosts_count: 5, + last_opened_at: "2021-08-18T15:11:35Z", + installed_paths: ["/Applications/BadSoftware.app"], + resolved_in_version: "2", + }, + ], +}; + +export const createMockVulnerability = ( + overrides?: Partial +): IVulnerability => { + return { ...DEFAULT_VULNERABILITY, ...overrides }; +}; + +const DEFAULT_VULNERABILITIES_RESPONSE: IVulnerabilitiesResponse = { + count: 6, + counts_updated_at: "2024-02-01T00:00:00Z", + vulnerabilities: [ + createMockVulnerability(), + createMockVulnerability({ + cve: "CVE-2018-16463", + created_at: "2023-06-01T00:15:00Z", + details_link: "https://nvd.nist.gov/vuln/detail/CVE-2018-16463", + hosts_count: 4, + cvss_score: 3.1, + epss_probability: 0.00054, + cisa_known_exploit: false, + cve_published: "2018-10-30T21:29:00Z", + resolved_in_version: "12.0.8", + }), + createMockVulnerability({ + cve: "CVE-2018-16464", + created_at: "2023-12-01T00:15:00Z", + details_link: "https://nvd.nist.gov/vuln/detail/CVE-2018-16464", + hosts_count: 37, + cvss_score: 5.7, + epss_probability: 0, + cisa_known_exploit: false, + cve_published: "2022-10-30T21:29:00Z", + resolved_in_version: "14.0.0", + }), + createMockVulnerability({ + cve: "CVE-2018-16465", + created_at: "2024-01-11T00:15:00Z", + details_link: "https://nvd.nist.gov/vuln/detail/CVE-2018-16465", + hosts_count: 80, + cvss_score: 5.3, + epss_probability: 0, + cisa_known_exploit: true, + cve_published: "2023-10-30T21:29:00Z", + resolved_in_version: "14.0.0", + }), + createMockVulnerability({ + cve: "CVE-2018-16466", + created_at: "2023-11-30T00:15:00Z", + details_link: "https://nvd.nist.gov/vuln/detail/CVE-2018-16466", + hosts_count: 297, + cvss_score: 8.1, + epss_probability: null, + cisa_known_exploit: false, + cve_published: "2021-10-30T21:29:00Z", + resolved_in_version: "12.0.11", + }), + createMockVulnerability({ + cve: "CVE-2018-16467", + created_at: "2023-12-10T00:15:00Z", + details_link: "https://nvd.nist.gov/vuln/detail/CVE-2018-16467", + hosts_count: 9, + cvss_score: 5.3, + epss_probability: 0.00119, + cisa_known_exploit: false, + cve_published: "2024-01-30T21:29:00Z", + resolved_in_version: "14.0.0", + }), + createMockVulnerability({ + cve: "CVE-2018-3761", + created_at: "2024-02-04T00:15:00Z", + details_link: "https://nvd.nist.gov/vuln/detail/CVE-2018-3761", + hosts_count: 1, + cvss_score: 8.1, + epss_probability: 0.00197, + cisa_known_exploit: false, + cve_published: "2018-07-05T16:29:00Z", + resolved_in_version: "12.0.8", + }), + ], + meta: { + has_next_results: true, + has_previous_results: false, + }, +}; + +export const createMockVulnerabilityResponse = ( + overrides?: Partial +): IVulnerabilityResponse => { + return { vulnerability: { ...DEFAULT_VULNERABILITY, ...overrides } }; +}; + +// eslint-disable-next-line import/prefer-default-export +export const createMockVulnerabilitiesResponse = ( + overrides?: Partial +): IVulnerabilitiesResponse => { + return { ...DEFAULT_VULNERABILITIES_RESPONSE, ...overrides }; +}; diff --git a/frontend/components/AddHostsModal/AddHostsModal.tests.tsx b/frontend/components/AddHostsModal/AddHostsModal.tests.tsx index 34a29d618a22..f3368a5140af 100644 --- a/frontend/components/AddHostsModal/AddHostsModal.tests.tsx +++ b/frontend/components/AddHostsModal/AddHostsModal.tests.tsx @@ -130,7 +130,7 @@ describe("AddHostsModal", () => { render( diff --git a/frontend/components/AddHostsModal/PlatformWrapper/PlatformWrapper.tsx b/frontend/components/AddHostsModal/PlatformWrapper/PlatformWrapper.tsx index f590c00b2118..e81d9de24412 100644 --- a/frontend/components/AddHostsModal/PlatformWrapper/PlatformWrapper.tsx +++ b/frontend/components/AddHostsModal/PlatformWrapper/PlatformWrapper.tsx @@ -427,9 +427,9 @@ const PlatformWrapper = ({ setShowPlainOsquery((prev) => !prev)} /> {showPlainOsquery && ( @@ -502,8 +502,8 @@ const PlatformWrapper = ({ "plain-osquery", "osqueryd --flagfile=flagfile.txt --verbose" )} - type={"text"} - value={"osqueryd --flagfile=flagfile.txt --verbose"} + type="text" + value="osqueryd --flagfile=flagfile.txt --verbose" /> diff --git a/frontend/components/Avatar/Avatar.tsx b/frontend/components/Avatar/Avatar.tsx index 1da12fb47457..319696be7bdb 100644 --- a/frontend/components/Avatar/Avatar.tsx +++ b/frontend/components/Avatar/Avatar.tsx @@ -42,7 +42,7 @@ const Avatar = ({ return (
{"User { const { gravatar_url_dark } = user; return ( -
+
{"User = { + title: "Components/DataSet", + component: DataSet, + args: { + title: "Data set", + value: "This is the value", + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Basic: Story = {}; diff --git a/frontend/components/DataSet/DataSet.tsx b/frontend/components/DataSet/DataSet.tsx new file mode 100644 index 000000000000..25a4b78bff01 --- /dev/null +++ b/frontend/components/DataSet/DataSet.tsx @@ -0,0 +1,19 @@ +import React from "react"; + +const baseClass = "data-set"; + +interface IDataSetProps { + title: React.ReactNode; + value: React.ReactNode; +} + +const DataSet = ({ title, value }: IDataSetProps) => { + return ( +
+
{title}
+
{value}
+
+ ); +}; + +export default DataSet; diff --git a/frontend/components/DataSet/_styles.scss b/frontend/components/DataSet/_styles.scss new file mode 100644 index 000000000000..809e7d98c75e --- /dev/null +++ b/frontend/components/DataSet/_styles.scss @@ -0,0 +1,7 @@ +.data-set { + font-size: $x-small; + + dt { + font-weight: $bold; + } +} diff --git a/frontend/components/DataSet/index.ts b/frontend/components/DataSet/index.ts new file mode 100644 index 000000000000..95947aec852b --- /dev/null +++ b/frontend/components/DataSet/index.ts @@ -0,0 +1 @@ +export { default } from "./DataSet"; diff --git a/frontend/components/DiskSpaceGraph/DiskSpaceGraph.tsx b/frontend/components/DiskSpaceGraph/DiskSpaceGraph.tsx index 97eb1a8f9fc1..7f8e355ac4db 100644 --- a/frontend/components/DiskSpaceGraph/DiskSpaceGraph.tsx +++ b/frontend/components/DiskSpaceGraph/DiskSpaceGraph.tsx @@ -67,7 +67,7 @@ const DiskSpaceGraph = ({
{diskSpaceTooltipText && (
diff --git a/frontend/components/EnrollSecrets/EnrollSecretModal/EnrollSecretModal.tsx b/frontend/components/EnrollSecrets/EnrollSecretModal/EnrollSecretModal.tsx index a6c6a691d6f4..cc88ba825af3 100644 --- a/frontend/components/EnrollSecrets/EnrollSecretModal/EnrollSecretModal.tsx +++ b/frontend/components/EnrollSecrets/EnrollSecretModal/EnrollSecretModal.tsx @@ -51,7 +51,7 @@ const EnrollSecretModal = ({
diff --git a/frontend/components/HumanTimeDiffWithDateTip/HumanTimeDiffWithDateTip.tsx b/frontend/components/HumanTimeDiffWithDateTip/HumanTimeDiffWithDateTip.tsx index 6cb0cbadfe37..1b1c93a1d152 100644 --- a/frontend/components/HumanTimeDiffWithDateTip/HumanTimeDiffWithDateTip.tsx +++ b/frontend/components/HumanTimeDiffWithDateTip/HumanTimeDiffWithDateTip.tsx @@ -33,7 +33,7 @@ export const HumanTimeDiffWithDateTip = ({ try { return ( <> - + {humanLastSeen(timeString)} { + // Grey out if API returns null or undefined (but not 0) + if (typeof probabilityOfExploit !== "number") { + return ( + + {DEFAULT_EMPTY_CELL_VALUE} + + ); + } + + const renderExploitedIcon = () => { + const tooltipId = uniqueId(); + return ( + <> + + + + + + + <> + The vulnerability has been actively exploited in the wild. This + data is reported by the Cybersecurity and Infrastructure Security + Agency (CISA). + + + + + ); + }; + + return ( + + {formatFloatAsPercentage(probabilityOfExploit)} + {cisaKnownExploit && renderExploitedIcon()} + + ); +}; + +export default ProbabilityOfExploit; diff --git a/frontend/components/ProbabilityOfExploit/_styles.scss b/frontend/components/ProbabilityOfExploit/_styles.scss new file mode 100644 index 000000000000..d0df6c90fd33 --- /dev/null +++ b/frontend/components/ProbabilityOfExploit/_styles.scss @@ -0,0 +1,10 @@ +.probability-of-exploit { + display: flex; + align-items: center; + gap: 8px; + + &__unknown { + color: $ui-fleet-black-50; + font-style: italic; + } +} diff --git a/frontend/components/ProbabilityOfExploit/index.ts b/frontend/components/ProbabilityOfExploit/index.ts new file mode 100644 index 000000000000..2bacf7669dcb --- /dev/null +++ b/frontend/components/ProbabilityOfExploit/index.ts @@ -0,0 +1 @@ +export { default } from "./ProbabilityOfExploit"; diff --git a/frontend/components/Sandbox/SandboxMessage/SandboxMessage.tsx b/frontend/components/Sandbox/SandboxMessage/SandboxMessage.tsx index e8d77c4f6e40..56087a0ff6e8 100644 --- a/frontend/components/Sandbox/SandboxMessage/SandboxMessage.tsx +++ b/frontend/components/Sandbox/SandboxMessage/SandboxMessage.tsx @@ -25,14 +25,14 @@ const SandboxMessage = ({ demo: ( ), sales: ( ), diff --git a/frontend/components/SectionHeader/SectionHeader.stories.tsx b/frontend/components/SectionHeader/SectionHeader.stories.tsx index fe5ce3022c67..6bcbce1a7ad6 100644 --- a/frontend/components/SectionHeader/SectionHeader.stories.tsx +++ b/frontend/components/SectionHeader/SectionHeader.stories.tsx @@ -22,7 +22,7 @@ export const WithSubTitle: Story = { subTitle: ( ), }, diff --git a/frontend/components/TableContainer/DataTable/DataTable.tests.tsx b/frontend/components/TableContainer/DataTable/DataTable.tests.tsx index dae0653945a6..424f4982b267 100644 --- a/frontend/components/TableContainer/DataTable/DataTable.tests.tsx +++ b/frontend/components/TableContainer/DataTable/DataTable.tests.tsx @@ -22,8 +22,8 @@ describe("DataTable - component", () => { { { name: "foo user", address: "biz address" }, { name: "bar user", address: "daz address" }, ]} - sortHeader={"name"} - sortDirection={"desc"} + sortHeader="name" + sortDirection="desc" isLoading={false} onSort={noop} showMarkAllPages={false} @@ -90,8 +90,8 @@ describe("DataTable - component", () => { { name: "foo user", address: "biz address" }, { name: "bar user", address: "daz address" }, ]} - sortHeader={"address"} - sortDirection={"desc"} + sortHeader="address" + sortDirection="desc" isLoading={false} onSort={noop} showMarkAllPages={false} @@ -124,8 +124,8 @@ describe("DataTable - component", () => { { {isLoading && ( -
+
)} -
+
{Object.keys(selectedRowIds).length !== 0 && ( - + -
{headerGroups[0].headers[0].render("Header")} -
+
+
{renderSelectedCount()} -
+
{secondarySelectActions && renderSecondarySelectActions()}
-
+
{primarySelectAction && renderPrimarySelectAction()}
{toggleAllPagesSelected && renderAreAllSelected()} {shouldRenderToggleAllPages && ( )} -
diff --git a/frontend/components/TableContainer/DataTable/TextCell/TextCell.tests.tsx b/frontend/components/TableContainer/DataTable/TextCell/TextCell.tests.tsx index 853e813d8cd1..c134a44a8212 100644 --- a/frontend/components/TableContainer/DataTable/TextCell/TextCell.tests.tsx +++ b/frontend/components/TableContainer/DataTable/TextCell/TextCell.tests.tsx @@ -10,17 +10,17 @@ describe("TextCell", () => { }); it("renders a default value when `value` is empty", () => { - render(); + render(); expect(screen.getByText(DEFAULT_EMPTY_CELL_VALUE)).toBeInTheDocument(); }); it("renders a default value when `value` is empty after formatting", () => { - render( ""} />); + render( ""} />); expect(screen.getByText(DEFAULT_EMPTY_CELL_VALUE)).toBeInTheDocument(); }); it("uses the provided formatter function", () => { - render( "bar"} />); + render( "bar"} />); expect(screen.getByText("bar")).toBeInTheDocument(); }); }); diff --git a/frontend/components/TableContainer/DataTable/TooltipTruncatedTextCell/TooltipTruncatedTextCell.tsx b/frontend/components/TableContainer/DataTable/TooltipTruncatedTextCell/TooltipTruncatedTextCell.tsx index e52223405957..64dda5d06587 100644 --- a/frontend/components/TableContainer/DataTable/TooltipTruncatedTextCell/TooltipTruncatedTextCell.tsx +++ b/frontend/components/TableContainer/DataTable/TooltipTruncatedTextCell/TooltipTruncatedTextCell.tsx @@ -36,7 +36,7 @@ const TooltipTruncatedTextCell = ({ return (
diff --git a/frontend/components/TableContainer/DataTable/_styles.scss b/frontend/components/TableContainer/DataTable/_styles.scss index 26d2dcf56c78..51cbec675414 100644 --- a/frontend/components/TableContainer/DataTable/_styles.scss +++ b/frontend/components/TableContainer/DataTable/_styles.scss @@ -211,6 +211,10 @@ $shadow-transition-width: 10px; max-width: 500px; word-wrap: break-word; + &.linkToFilteredHosts__cell { + text-align: right; + } + &.selection__cell { width: 0px; padding: 0 $pad-medium; diff --git a/frontend/components/TableContainer/TableContainer.tsx b/frontend/components/TableContainer/TableContainer.tsx index 678d9077ee4b..f96f15cd29c0 100644 --- a/frontend/components/TableContainer/TableContainer.tsx +++ b/frontend/components/TableContainer/TableContainer.tsx @@ -30,6 +30,7 @@ interface IRowProps extends Row { original: { id?: number; os_version_id?: string; // Required for onSelectSingleRow of SoftwareOSTable.tsx + cve?: string; // Required for onSelectSingleRow of SoftwareVulnerabilityTable.tsx }; } @@ -330,7 +331,7 @@ const TableContainer = ({
)} - + {actionButton && !actionButton.hideButton && ( diff --git a/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/DeleteProfileModal/DeleteProfileModal.tsx b/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/DeleteProfileModal/DeleteProfileModal.tsx index 40b2976d1526..71b265f2bc5b 100644 --- a/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/DeleteProfileModal/DeleteProfileModal.tsx +++ b/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/DeleteProfileModal/DeleteProfileModal.tsx @@ -34,7 +34,7 @@ const DeleteProfileModal = ({ return ( onDelete(profileId)} width="large" diff --git a/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/ProfileListItem/ProfileListItem.tsx b/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/ProfileListItem/ProfileListItem.tsx index 8fe3ce132368..15b76767010a 100644 --- a/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/ProfileListItem/ProfileListItem.tsx +++ b/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/ProfileListItem/ProfileListItem.tsx @@ -10,7 +10,7 @@ import Button from "components/buttons/Button"; import Graphic from "components/Graphic"; import Icon from "components/Icon"; -import { pluralize } from "utilities/helpers"; +import strUtils from "utilities/strings"; const baseClass = "profile-list-item"; @@ -22,7 +22,7 @@ const LabelCount = ({ count: number; }) => (
- {`${count} ${pluralize(count, "label", "s", "")}`} + {`${count} ${strUtils.pluralize(count, "label")}`}
); diff --git a/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/ProfileUploader/components/AddProfileGraphic.tsx b/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/ProfileUploader/components/AddProfileGraphic.tsx index 24522265811a..c0460e83dd5f 100644 --- a/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/ProfileUploader/components/AddProfileGraphic.tsx +++ b/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/ProfileUploader/components/AddProfileGraphic.tsx @@ -16,7 +16,7 @@ const ProfileGraphic = ({ {showMessage && ( diff --git a/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/ProfileUploader/components/AddProfileModal.tsx b/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/ProfileUploader/components/AddProfileModal.tsx index 487a070f109d..883479185a49 100644 --- a/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/ProfileUploader/components/AddProfileModal.tsx +++ b/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/ProfileUploader/components/AddProfileModal.tsx @@ -52,7 +52,7 @@ const FileChooser = ({ { @@ -97,20 +97,20 @@ const TargetChooser = ({
Target
diff --git a/frontend/pages/ManageControlsPage/OSSettings/cards/DiskEncryption/components/DiskEncryptionTable/DiskEncryptionTableConfig.tsx b/frontend/pages/ManageControlsPage/OSSettings/cards/DiskEncryption/components/DiskEncryptionTable/DiskEncryptionTableConfig.tsx index 774959a092c2..f709acfcf5b7 100644 --- a/frontend/pages/ManageControlsPage/OSSettings/cards/DiskEncryption/components/DiskEncryptionTable/DiskEncryptionTableConfig.tsx +++ b/frontend/pages/ManageControlsPage/OSSettings/cards/DiskEncryption/components/DiskEncryptionTable/DiskEncryptionTableConfig.tsx @@ -108,21 +108,32 @@ const defaultTableHeaders: IDataColumn[] = [ ), disableSortBy: true, accessor: "windowsHosts", - Cell: ({ - cell: { value: aggregateCount }, - row: { original }, - }: ICellProps) => { + Cell: ({ cell: { value: aggregateCount } }: ICellProps) => { return ( -
- <>{val}} /> - -
+ <>{val}} /> + ); + }, + }, + { + title: "", + Header: "", + accessor: "linkToFilteredHosts", + disableSortBy: true, + Cell: (cellProps: ICellProps) => { + return ( + <> + {cellProps.row.original && ( + + )} + ); }, }, diff --git a/frontend/pages/ManageControlsPage/OSSettings/cards/DiskEncryption/components/DiskEncryptionTable/_styles.scss b/frontend/pages/ManageControlsPage/OSSettings/cards/DiskEncryption/components/DiskEncryptionTable/_styles.scss index c2e35efe6b8b..0a4456453e97 100644 --- a/frontend/pages/ManageControlsPage/OSSettings/cards/DiskEncryption/components/DiskEncryptionTable/_styles.scss +++ b/frontend/pages/ManageControlsPage/OSSettings/cards/DiskEncryption/components/DiskEncryptionTable/_styles.scss @@ -5,25 +5,6 @@ min-width: auto; } - &__aggregate-table-data { - display: flex; - align-items: center; - justify-content: space-between; - } - - tr { - .view-hosts-link { - opacity: 0; - transition: 250ms; - } - - &:hover { - .view-hosts-link { - opacity: 1; - } - } - } - @media (max-width: $break-md) { .view-hosts-link { span { diff --git a/frontend/pages/ManageControlsPage/OSUpdates/_styles.scss b/frontend/pages/ManageControlsPage/OSUpdates/_styles.scss index 93393f26af60..cf1b866d1f27 100644 --- a/frontend/pages/ManageControlsPage/OSUpdates/_styles.scss +++ b/frontend/pages/ManageControlsPage/OSUpdates/_styles.scss @@ -7,7 +7,6 @@ &__content { display: grid; - max-width: $break-xxl; gap: $pad-xxlarge; margin: 0 auto; diff --git a/frontend/pages/ManageControlsPage/OSUpdates/components/CurrentVersionSection/CurrentVersionSection.tsx b/frontend/pages/ManageControlsPage/OSUpdates/components/CurrentVersionSection/CurrentVersionSection.tsx index 921e47e49a24..6253acf923b8 100644 --- a/frontend/pages/ManageControlsPage/OSUpdates/components/CurrentVersionSection/CurrentVersionSection.tsx +++ b/frontend/pages/ManageControlsPage/OSUpdates/components/CurrentVersionSection/CurrentVersionSection.tsx @@ -50,7 +50,7 @@ const CurrentVersionSection = ({ return ( ); }; diff --git a/frontend/pages/ManageControlsPage/OSUpdates/components/OSVersionTable/OSVersionTableConfig.tsx b/frontend/pages/ManageControlsPage/OSUpdates/components/OSVersionTable/OSVersionTableConfig.tsx index 01fa65c8eadb..0b69cca6c1e5 100644 --- a/frontend/pages/ManageControlsPage/OSUpdates/components/OSVersionTable/OSVersionTableConfig.tsx +++ b/frontend/pages/ManageControlsPage/OSUpdates/components/OSVersionTable/OSVersionTableConfig.tsx @@ -61,23 +61,30 @@ export const generateTableHeaders = (teamId: number) => { ), Cell: ({ row }: IHostCellProps): JSX.Element => { const { hosts_count, name_only, version } = row.original; + return ; + }, + }, + { + title: "", + Header: "", + accessor: "linkToFilteredHosts", + disableSortBy: true, + Cell: (cellProps: IOSTypeCellProps) => { return ( - - - - - + <> + {cellProps.row.original && ( - - + )} + ); }, }, diff --git a/frontend/pages/ManageControlsPage/Scripts/components/DeleteScriptModal/DeleteScriptModal.tsx b/frontend/pages/ManageControlsPage/Scripts/components/DeleteScriptModal/DeleteScriptModal.tsx index 385f1e5cbbf7..4d67aea8b69b 100644 --- a/frontend/pages/ManageControlsPage/Scripts/components/DeleteScriptModal/DeleteScriptModal.tsx +++ b/frontend/pages/ManageControlsPage/Scripts/components/DeleteScriptModal/DeleteScriptModal.tsx @@ -36,7 +36,7 @@ const DeleteScriptModal = ({ return ( onClickDelete(scriptId)} > diff --git a/frontend/pages/ManageControlsPage/Scripts/components/RerunScriptModal/RerunScriptModal.tsx b/frontend/pages/ManageControlsPage/Scripts/components/RerunScriptModal/RerunScriptModal.tsx index b8081dbc6615..db3029d8e853 100644 --- a/frontend/pages/ManageControlsPage/Scripts/components/RerunScriptModal/RerunScriptModal.tsx +++ b/frontend/pages/ManageControlsPage/Scripts/components/RerunScriptModal/RerunScriptModal.tsx @@ -34,7 +34,7 @@ const RerunScriptModal = ({ return ( onRerun(scriptId)} > diff --git a/frontend/pages/ManageControlsPage/SetupExperience/cards/BootstrapPackage/components/BootstrapPackageTable/BootstrapPackageTableConfig.tsx b/frontend/pages/ManageControlsPage/SetupExperience/cards/BootstrapPackage/components/BootstrapPackageTable/BootstrapPackageTableConfig.tsx index 73231b331734..b84bbf547ee2 100644 --- a/frontend/pages/ManageControlsPage/SetupExperience/cards/BootstrapPackage/components/BootstrapPackageTable/BootstrapPackageTableConfig.tsx +++ b/frontend/pages/ManageControlsPage/SetupExperience/cards/BootstrapPackage/components/BootstrapPackageTable/BootstrapPackageTableConfig.tsx @@ -82,21 +82,31 @@ export const COLUMN_CONFIGS: IColumnConfig[] = [ /> ), accessor: "hosts", - Cell: ({ - cell: { value: aggregateCount }, - row: { original }, - }: ICellProps) => { + Cell: ({ cell: { value: aggregateCount } }: ICellProps) => { return ( -
- <>{val}} /> - -
+ <>{val}} /> + ); + }, + }, + { + title: "", + Header: "", + accessor: "linkToFilteredHosts", + disableSortBy: true, + Cell: (cellProps: ICellProps) => { + return ( + <> + {cellProps.row.original && ( + + )} + ); }, }, diff --git a/frontend/pages/ManageControlsPage/SetupExperience/cards/BootstrapPackage/components/BootstrapPackageTable/_styles.scss b/frontend/pages/ManageControlsPage/SetupExperience/cards/BootstrapPackage/components/BootstrapPackageTable/_styles.scss index 9d0a64c24ad4..a64ae509ffdd 100644 --- a/frontend/pages/ManageControlsPage/SetupExperience/cards/BootstrapPackage/components/BootstrapPackageTable/_styles.scss +++ b/frontend/pages/ManageControlsPage/SetupExperience/cards/BootstrapPackage/components/BootstrapPackageTable/_styles.scss @@ -10,23 +10,8 @@ min-width: auto; } - &__aggregate-table-data { - display: flex; - align-items: center; - justify-content: space-between; - } - - tr { - .view-hosts-link { - opacity: 0; - transition: 250ms; - } - - &:hover { - .view-hosts-link { - opacity: 1; - } - } + th:nth-last-child(2) { + border-right: 0; } @media (max-width: $break-lg) { diff --git a/frontend/pages/ManageControlsPage/SetupExperience/cards/BootstrapPackage/components/DeletePackageModal/DeletePackageModal.tsx b/frontend/pages/ManageControlsPage/SetupExperience/cards/BootstrapPackage/components/DeletePackageModal/DeletePackageModal.tsx index e29119392c00..a351ddf82040 100644 --- a/frontend/pages/ManageControlsPage/SetupExperience/cards/BootstrapPackage/components/DeletePackageModal/DeletePackageModal.tsx +++ b/frontend/pages/ManageControlsPage/SetupExperience/cards/BootstrapPackage/components/DeletePackageModal/DeletePackageModal.tsx @@ -17,7 +17,7 @@ const DeletePackageModal = ({ return ( onDelete()} > diff --git a/frontend/pages/ManageControlsPage/components/TurnOnMdmMessage/TurnOnMdmMessage.tsx b/frontend/pages/ManageControlsPage/components/TurnOnMdmMessage/TurnOnMdmMessage.tsx index ce052c31e180..f2853ac515c8 100644 --- a/frontend/pages/ManageControlsPage/components/TurnOnMdmMessage/TurnOnMdmMessage.tsx +++ b/frontend/pages/ManageControlsPage/components/TurnOnMdmMessage/TurnOnMdmMessage.tsx @@ -31,7 +31,7 @@ const TurnOnMdmMessage = ({ router }: ITurnOnMdmMessageProps) => { return ( ); diff --git a/frontend/pages/RegistrationPage/Breadcrumbs/Breadcrumbs.jsx b/frontend/pages/RegistrationPage/Breadcrumbs/Breadcrumbs.jsx deleted file mode 100644 index 549c00d6d283..000000000000 --- a/frontend/pages/RegistrationPage/Breadcrumbs/Breadcrumbs.jsx +++ /dev/null @@ -1,84 +0,0 @@ -import React, { Component } from "react"; -import PropTypes from "prop-types"; -import classnames from "classnames"; - -class Breadcrumbs extends Component { - static propTypes = { - onClick: PropTypes.func, - pageProgress: PropTypes.number, - }; - - static defaultProps = { - pageProgress: 1, - }; - - onClick = (page) => { - return (evt) => { - evt.preventDefault(); - - const { onClick: handleClick } = this.props; - - return handleClick(page); - }; - }; - - render() { - const { onClick } = this; - const { pageProgress } = this.props; - const baseClass = "registration-breadcrumbs"; - const pageBaseClass = `${baseClass}__page`; - const page1ClassName = classnames( - pageBaseClass, - `${pageBaseClass}--1`, - "button--unstyled", - { - [`${pageBaseClass}--active`]: pageProgress === 1, - [`${pageBaseClass}--complete`]: pageProgress > 1, - } - ); - const page2TabIndex = pageProgress >= 2 ? 0 : -1; - const page2ClassName = classnames( - pageBaseClass, - `${pageBaseClass}--2`, - "button--unstyled", - { - [`${pageBaseClass}--active`]: pageProgress === 2, - [`${pageBaseClass}--complete`]: pageProgress > 2, - } - ); - const page3TabIndex = pageProgress >= 3 ? 0 : -1; - const page3ClassName = classnames( - pageBaseClass, - `${pageBaseClass}--3`, - "button--unstyled", - { - [`${pageBaseClass}--active`]: pageProgress === 3, - [`${pageBaseClass}--complete`]: pageProgress > 3, - } - ); - - return ( -
- - - -
- ); - } -} - -export default Breadcrumbs; diff --git a/frontend/pages/RegistrationPage/Breadcrumbs/Breadcrumbs.tests.jsx b/frontend/pages/RegistrationPage/Breadcrumbs/Breadcrumbs.tests.jsx deleted file mode 100644 index c94ea3252559..000000000000 --- a/frontend/pages/RegistrationPage/Breadcrumbs/Breadcrumbs.tests.jsx +++ /dev/null @@ -1,60 +0,0 @@ -import React from "react"; -import { fireEvent, render, screen } from "@testing-library/react"; - -import Breadcrumbs from "pages/RegistrationPage/Breadcrumbs"; - -describe("Breadcrumbs - component", () => { - it("renders 3 Button components", () => { - render(); - expect(screen.getAllByRole("button").length).toEqual(3); - }); - - it("renders page 1 Button as active when the page prop is 1", () => { - const { container } = render(); - const page1Btn = container.querySelector( - "button.registration-breadcrumbs__page--1" - ); - const page2Btn = container.querySelector( - "button.registration-breadcrumbs__page--2" - ); - const page3Btn = container.querySelector( - "button.registration-breadcrumbs__page--3" - ); - - expect(page1Btn.className).toContain( - "registration-breadcrumbs__page--active" - ); - expect(page2Btn.className).not.toContain( - "registration-breadcrumbs__page--active" - ); - expect(page3Btn.className).not.toContain( - "registration-breadcrumbs__page--active" - ); - }); - - it("calls the onClick prop with the page number when clicked", () => { - const onClickSpy = jest.fn(); - const { container } = render(); - const page1Btn = container.querySelector( - "button.registration-breadcrumbs__page--1" - ); - const page2Btn = container.querySelector( - "button.registration-breadcrumbs__page--2" - ); - const page3Btn = container.querySelector( - "button.registration-breadcrumbs__page--3" - ); - - fireEvent.click(page1Btn); - - expect(onClickSpy).toHaveBeenCalledWith(1); - - fireEvent.click(page2Btn); - - expect(onClickSpy).toHaveBeenCalledWith(2); - - fireEvent.click(page3Btn); - - expect(onClickSpy).toHaveBeenCalledWith(3); - }); -}); diff --git a/frontend/pages/RegistrationPage/Breadcrumbs/Breadcrumbs.tests.tsx b/frontend/pages/RegistrationPage/Breadcrumbs/Breadcrumbs.tests.tsx new file mode 100644 index 000000000000..74714ba4c260 --- /dev/null +++ b/frontend/pages/RegistrationPage/Breadcrumbs/Breadcrumbs.tests.tsx @@ -0,0 +1,37 @@ +import React from "react"; +import { fireEvent, render, screen } from "@testing-library/react"; +import { noop } from "lodash"; + +import Breadcrumbs from "pages/RegistrationPage/Breadcrumbs"; + +describe("Breadcrumbs - component", () => { + it("renders 3 Button components", () => { + render(); + expect(screen.getAllByRole("button").length).toEqual(3); + }); + + it("renders page 1 Button as active when the current page prop is 1", () => { + const { container } = render( + + ); + const page1Btn = container.querySelector( + "button.registration-breadcrumbs__page--1" + ); + const page2Btn = container.querySelector( + "button.registration-breadcrumbs__page--2" + ); + const page3Btn = container.querySelector( + "button.registration-breadcrumbs__page--3" + ); + + expect(page1Btn?.className).toContain( + "registration-breadcrumbs__page--active" + ); + expect(page2Btn?.className).not.toContain( + "registration-breadcrumbs__page--active" + ); + expect(page3Btn?.className).not.toContain( + "registration-breadcrumbs__page--active" + ); + }); +}); diff --git a/frontend/pages/RegistrationPage/Breadcrumbs/Breadcrumbs.tsx b/frontend/pages/RegistrationPage/Breadcrumbs/Breadcrumbs.tsx new file mode 100644 index 000000000000..edeeef39ba8a --- /dev/null +++ b/frontend/pages/RegistrationPage/Breadcrumbs/Breadcrumbs.tsx @@ -0,0 +1,64 @@ +import React, { MouseEventHandler } from "react"; +import classnames from "classnames"; + +import Button from "components/buttons/Button"; + +interface IBreadcrumbs { + onSetPage: (page: number) => void; + currentPage: number; + pageProgress: number; +} +const baseClass = "registration-breadcrumbs"; + +const Breadcrumbs = ({ + onSetPage, + currentPage = 1, + pageProgress = 1, +}: IBreadcrumbs): JSX.Element => { + const pageBaseClass = `${baseClass}__page`; + const page1ClassName = classnames(pageBaseClass, `${pageBaseClass}--1`, { + [`${pageBaseClass}--active`]: currentPage === 1, + [`${pageBaseClass}--complete`]: pageProgress > 1, + }); + + const page2TabIndex = pageProgress >= 2 ? 0 : -1; + const page2ClassName = classnames(pageBaseClass, `${pageBaseClass}--2`, { + [`${pageBaseClass}--active`]: currentPage === 2, + [`${pageBaseClass}--complete`]: pageProgress > 2, + }); + const page3TabIndex = pageProgress >= 3 ? 0 : -1; + const page3ClassName = classnames(pageBaseClass, `${pageBaseClass}--3`, { + [`${pageBaseClass}--active`]: currentPage === 3, + [`${pageBaseClass}--complete`]: pageProgress > 3, + }); + + return ( +
+ + + +
+ ); +}; + +export default Breadcrumbs; diff --git a/frontend/pages/RegistrationPage/Breadcrumbs/_styles.scss b/frontend/pages/RegistrationPage/Breadcrumbs/_styles.scss index 97b54005d68d..d8fe1f1695f6 100644 --- a/frontend/pages/RegistrationPage/Breadcrumbs/_styles.scss +++ b/frontend/pages/RegistrationPage/Breadcrumbs/_styles.scss @@ -71,7 +71,6 @@ &--active { font-weight: $bold; - color: $core-white; } &--1 { @@ -97,7 +96,7 @@ &.registration-breadcrumbs__page--complete { &::before { - background-color: $core-white; + background: $core-white; background-size: auto; z-index: 2; } diff --git a/frontend/pages/RegistrationPage/Breadcrumbs/index.js b/frontend/pages/RegistrationPage/Breadcrumbs/index.ts similarity index 100% rename from frontend/pages/RegistrationPage/Breadcrumbs/index.js rename to frontend/pages/RegistrationPage/Breadcrumbs/index.ts diff --git a/frontend/pages/RegistrationPage/RegistrationPage.tsx b/frontend/pages/RegistrationPage/RegistrationPage.tsx index 5d0fbd644525..84323d68e995 100644 --- a/frontend/pages/RegistrationPage/RegistrationPage.tsx +++ b/frontend/pages/RegistrationPage/RegistrationPage.tsx @@ -86,8 +86,8 @@ const RegistrationPage = ({ router }: IRegistrationPageProps) => { className={`${baseClass}__logo`} /> ; + } + if (isError) { return ; } diff --git a/frontend/pages/SoftwarePage/SoftwareOS/SoftwareOSTable/SoftwareOSTable.tsx b/frontend/pages/SoftwarePage/SoftwareOS/SoftwareOSTable/SoftwareOSTable.tsx index 4d24e10f8898..d528abd9fb4a 100644 --- a/frontend/pages/SoftwarePage/SoftwareOS/SoftwareOSTable/SoftwareOSTable.tsx +++ b/frontend/pages/SoftwarePage/SoftwareOS/SoftwareOSTable/SoftwareOSTable.tsx @@ -1,3 +1,5 @@ +/** software/os OS tab > Table */ + import React, { useCallback, useContext, useMemo } from "react"; import { InjectedRouter } from "react-router"; import { Row } from "react-table"; @@ -138,7 +140,7 @@ const SoftwareOSTable = ({ return ( ); }; @@ -176,7 +178,7 @@ const SoftwareOSTable = ({ columnConfigs={softwareTableHeaders} data={data?.os_versions ?? []} isLoading={isLoading} - resultsTitle={"items"} + resultsTitle="items" emptyComponent={() => ( { interface ISoftwareOSDetailsRouteParams { id: string; + team_id?: string; } type ISoftwareOSDetailsPageProps = RouteComponentProps< @@ -49,22 +63,62 @@ type ISoftwareOSDetailsPageProps = RouteComponentProps< const SoftwareOSDetailsPage = ({ routeParams, + router, + location, }: ISoftwareOSDetailsPageProps) => { + const { isPremiumTier, isOnGlobalTeam } = useContext(AppContext); + const handlePageError = useErrorHandler(); + const osVersionIdFromURL = parseInt(routeParams.id, 10); - const { data: osVersionDetails, isLoading, isError } = useQuery< + const { + currentTeamId, + teamIdForApi, + userTeams, + handleTeamChange, + } = useTeamIdParam({ + location, + router, + includeAllTeams: true, + includeNoTeam: false, + }); + + const { + data: osVersionDetails, + isLoading, + isError: isOsVersionError, + } = useQuery< IOSVersionResponse, - Error, - IOperatingSystemVersion + AxiosError, + IOperatingSystemVersion, + IGetOsVersionQueryKey[] >( - ["osVersionDetails", osVersionIdFromURL], - () => osVersionsAPI.getOSVersion(osVersionIdFromURL), + [ + { + scope: "osVersionDetails", + os_version_id: osVersionIdFromURL, + teamId: teamIdForApi, + }, + ], + ({ queryKey }) => osVersionsAPI.getOSVersion(queryKey[0]), { enabled: !!osVersionIdFromURL, select: (data) => data.os_version, + onError: (error) => { + if (!ignoreAxiosError(error, [403, 404])) { + handlePageError(error); + } + }, } ); + const onTeamChange = useCallback( + (teamId: number) => { + handleTeamChange(teamId); + }, + [handleTeamChange] + ); + const renderTable = () => { if (!osVersionDetails) { return null; @@ -82,6 +136,8 @@ const SoftwareOSDetailsPage = ({ data={osVersionDetails.vulnerabilities} itemName="version" isLoading={isLoading} + router={router} + teamIdForApi={teamIdForApi} /> ); }; @@ -91,30 +147,49 @@ const SoftwareOSDetailsPage = ({ return ; } - if (isError) { - return ; - } - - if (!osVersionDetails) { + if (!osVersionDetails && !isOsVersionError) { return null; } return ( <> - - {/* TODO: can we use Card here for card styles */} -
-

Vulnerabilities

- {renderTable()} -
+ {isPremiumTier && ( + + )} + {isOsVersionError ? ( + + ) : ( + <> + + +

Vulnerabilities

+ {renderTable()} +
+ + )} ); }; diff --git a/frontend/pages/SoftwarePage/SoftwareOSDetailsPage/_styles.scss b/frontend/pages/SoftwarePage/SoftwareOSDetailsPage/_styles.scss index d460c294643d..afec1d411565 100644 --- a/frontend/pages/SoftwarePage/SoftwareOSDetailsPage/_styles.scss +++ b/frontend/pages/SoftwarePage/SoftwareOSDetailsPage/_styles.scss @@ -4,16 +4,7 @@ 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; - } + h2 { + font-size: $small; } } diff --git a/frontend/pages/SoftwarePage/SoftwarePage.tsx b/frontend/pages/SoftwarePage/SoftwarePage.tsx index 5ae4946970ac..e82eb67fcec9 100644 --- a/frontend/pages/SoftwarePage/SoftwarePage.tsx +++ b/frontend/pages/SoftwarePage/SoftwarePage.tsx @@ -24,7 +24,7 @@ import { buildQueryStringFromParams } from "utilities/url"; import Button from "components/buttons/Button"; import MainContent from "components/MainContent"; -import TeamsDropdown from "components/TeamsDropdown"; +import TeamsHeader from "components/TeamsHeader"; import TabsWrapper from "components/TabsWrapper"; import ManageAutomationsModal from "./components/ManageSoftwareAutomationsModal"; @@ -43,6 +43,10 @@ const softwareSubNav: ISoftwareSubNavItem[] = [ name: "OS", pathname: PATHS.SOFTWARE_OS, }, + { + name: "Vulnerabilities", + pathname: PATHS.SOFTWARE_VULNERABILITIES, + }, ]; const getTabIndex = (path: string): number => { @@ -88,6 +92,7 @@ interface ISoftwarePageProps { query: { team_id?: string; vulnerable?: string; + exploited?: string; page?: string; query?: string; order_key?: string; @@ -129,6 +134,8 @@ const SoftwarePage = ({ children, router, location }: ISoftwarePageProps) => { const query = queryParams && queryParams.query ? queryParams.query : ""; const showVulnerableSoftware = queryParams !== undefined && queryParams.vulnerable === "true"; + const showExploitedVulnerabilitiesOnly = + queryParams !== undefined && queryParams.exploited === "true"; const [showManageAutomationsModal, setShowManageAutomationsModal] = useState( false @@ -273,20 +280,15 @@ const SoftwarePage = ({ children, router, location }: ISoftwarePageProps) => { return ( <> {isFreeTier &&

Software

} - {isPremiumTier && - userTeams && - (userTeams.length > 1 || isOnGlobalTeam) && ( - - )} - {isPremiumTier && - !isOnGlobalTeam && - userTeams && - userTeams.length === 1 &&

{userTeams[0].name}

} + {isPremiumTier && ( + + )} ); }; @@ -299,11 +301,9 @@ const SoftwarePage = ({ children, router, location }: ISoftwarePageProps) => { (!isPremiumTier || !isAnyTeamSelected) && "and manage automations for detected vulnerabilities (CVEs)"}{" "} on{" "} - - {isPremiumTier && isAnyTeamSelected - ? "all hosts assigned to this team" - : "all of your hosts"} - + {isPremiumTier && isAnyTeamSelected + ? "all hosts assigned to this team" + : "all of your hosts"} .

); @@ -341,6 +341,7 @@ const SoftwarePage = ({ children, router, location }: ISoftwarePageProps) => { // TODO: move down into the Software Titles component query, showVulnerableSoftware, + showExploitedVulnerabilitiesOnly, })}
); diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsPage.tsx b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsPage.tsx index 3e9922fb737a..8efee4faa3b0 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsPage.tsx +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsPage.tsx @@ -1,25 +1,36 @@ -import React from "react"; -import { RouteComponentProps } from "react-router"; +/** software/titles/:id */ + +import React, { useCallback, useContext } from "react"; import { useQuery } from "react-query"; -import { AxiosError } from "axios"; import { useErrorHandler } from "react-error-boundary"; +import { RouteComponentProps } from "react-router"; +import { AxiosError } from "axios"; + +import useTeamIdParam from "hooks/useTeamIdParam"; + +import { AppContext } from "context/app"; import { ISoftwareTitle, formatSoftwareType } from "interfaces/software"; +import { ignoreAxiosError } from "interfaces/errors"; import softwareAPI, { ISoftwareTitleResponse, + IGetSoftwareTitleQueryKey, } from "services/entities/software"; -import MainContent from "components/MainContent"; -import TableDataError from "components/DataError"; import Spinner from "components/Spinner"; +import MainContent from "components/MainContent"; +import TeamsHeader from "components/TeamsHeader"; +import Card from "components/Card"; import SoftwareDetailsSummary from "../components/SoftwareDetailsSummary"; import SoftwareTitleDetailsTable from "./SoftwareTitleDetailsTable"; +import DetailsNoHosts from "../components/DetailsNoHosts"; const baseClass = "software-title-details-page"; interface ISoftwareTitleDetailsRouteParams { id: string; + team_id?: string; } type ISoftwareTitleDetailsPageProps = RouteComponentProps< @@ -30,64 +41,110 @@ type ISoftwareTitleDetailsPageProps = RouteComponentProps< const SoftwareTitleDetailsPage = ({ router, routeParams, + location, }: ISoftwareTitleDetailsPageProps) => { + const { isPremiumTier, isOnGlobalTeam } = useContext(AppContext); + const handlePageError = useErrorHandler(); + // TODO: handle non integer values const softwareId = parseInt(routeParams.id, 10); - const handlePageError = useErrorHandler(); + const { + currentTeamId, + teamIdForApi, + userTeams, + handleTeamChange, + } = useTeamIdParam({ + location, + router, + includeAllTeams: true, + includeNoTeam: false, + }); const { data: softwareTitle, isLoading: isSoftwareTitleLoading, isError: isSoftwareTitleError, - } = useQuery( - ["softwareById", softwareId], - () => softwareAPI.getSoftwareTitle(softwareId), + } = useQuery< + ISoftwareTitleResponse, + AxiosError, + ISoftwareTitle, + IGetSoftwareTitleQueryKey[] + >( + [{ scope: "softwareById", softwareId, teamId: teamIdForApi }], + ({ queryKey }) => softwareAPI.getSoftwareTitle(queryKey[0]), { - select: (data) => data.software_title, - retry: false, refetchOnWindowFocus: false, + select: (data) => data.software_title, onError: (error) => { - if (error.status === 403) { - handlePageError({ status: 403 }); + if (!ignoreAxiosError(error, [403, 404])) { + handlePageError(error); } }, } ); + const onTeamChange = useCallback( + (teamId: number) => { + handleTeamChange(teamId); + }, + [handleTeamChange] + ); + const renderContent = () => { if (isSoftwareTitleLoading) { return ; } - if (isSoftwareTitleError) { - return ; - } - - if (!softwareTitle) { + if (!softwareTitle && !isSoftwareTitleError) { return null; } - return ( <> - - {/* TODO: can we use Card here for card styles */} -
-

Versions

- + )} + {isSoftwareTitleError ? ( + -
+ ) : ( + <> + + +

Versions

+ +
+ + )} ); }; diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsTable/SoftwareTitleDetailsTable.tsx b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsTable/SoftwareTitleDetailsTable.tsx index 5f8933fa8afe..43c4685cfe6b 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsTable/SoftwareTitleDetailsTable.tsx +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsTable/SoftwareTitleDetailsTable.tsx @@ -1,8 +1,13 @@ +/** software/titles/:id > Versions section */ + import React, { useMemo } from "react"; import { InjectedRouter } from "react-router"; +import { Row } from "react-table"; +import PATHS from "router/paths"; import { ISoftwareTitleVersion } from "interfaces/software"; import { GITHUB_NEW_ISSUE_LINK } from "utilities/constants"; +import { buildQueryStringFromParams } from "utilities/url"; import TableContainer from "components/TableContainer"; import EmptyTable from "components/EmptyTable"; @@ -37,16 +42,39 @@ interface ISoftwareTitleDetailsTableProps { router: InjectedRouter; data: ISoftwareTitleVersion[]; isLoading: boolean; + teamIdForApi?: number; +} + +interface IRowProps extends Row { + original: { + id?: number; + }; } const SoftwareTitleDetailsTable = ({ router, data, isLoading, + teamIdForApi, }: ISoftwareTitleDetailsTableProps) => { + const handleRowSelect = (row: IRowProps) => { + const hostsBySoftwareParams = { + software_version_id: row.original.id, + }; + + const path = hostsBySoftwareParams + ? `${PATHS.MANAGE_HOSTS}?${buildQueryStringFromParams( + hostsBySoftwareParams + )}` + : PATHS.MANAGE_HOSTS; + + router.push(path); + }; + const softwareTableHeaders = useMemo( - () => generateSoftwareTitleDetailsTableConfig(router), - [router] + () => + generateSoftwareTitleDetailsTableConfig({ router, teamId: teamIdForApi }), + [router, teamIdForApi] ); return ( @@ -62,7 +90,8 @@ const SoftwareTitleDetailsTable = ({ defaultSortHeader={DEFAULT_SORT_HEADER} defaultSortDirection={DEFAULT_SORT_DIRECTION} disablePagination - // TODO: add row click handler + disableMultiRowSelect + onSelectSingleRow={handleRowSelect} /> ); }; diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsTable/SoftwareTitleDetailsTableConfig.tsx b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsTable/SoftwareTitleDetailsTableConfig.tsx index 6e3e9c80bfdb..e5990b58a6c3 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsTable/SoftwareTitleDetailsTableConfig.tsx +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsTable/SoftwareTitleDetailsTableConfig.tsx @@ -6,6 +6,7 @@ import { ISoftwareVulnerability, } from "interfaces/software"; import PATHS from "router/paths"; +import { buildQueryStringFromParams } from "utilities/url"; import TextCell from "components/TableContainer/DataTable/TextCell"; import ViewAllHostsLink from "components/ViewAllHostsLink"; @@ -13,6 +14,10 @@ import LinkCell from "components/TableContainer/DataTable/LinkCell"; import VulnerabilitiesCell from "../../components/VulnerabilitiesCell"; +interface ISoftwareTitleDetailsTableConfigProps { + router: InjectedRouter; + teamId?: number; +} interface ICellProps { cell: { value: number | string | ISoftwareVulnerability[]; @@ -40,7 +45,10 @@ interface IVulnCellProps extends ICellProps { }; } -const generateSoftwareTitleDetailsTableConfig = (router: InjectedRouter) => { +const generateSoftwareTitleDetailsTableConfig = ({ + router, + teamId, +}: ISoftwareTitleDetailsTableConfigProps) => { const tableHeaders = [ { title: "Version", @@ -49,17 +57,22 @@ const generateSoftwareTitleDetailsTableConfig = (router: InjectedRouter) => { accessor: "version", Cell: (cellProps: IVersionCellProps): JSX.Element => { const { id } = cellProps.row.original; + const teamQueryParam = buildQueryStringFromParams({ team_id: teamId }); + const softwareVersionDetailsPath = `${PATHS.SOFTWARE_VERSION_DETAILS( + id.toString() + )}?${teamQueryParam}`; + 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())); + router?.push(softwareVersionDetailsPath); }; // TODO: make only text clickable return ( @@ -88,21 +101,31 @@ const generateSoftwareTitleDetailsTableConfig = (router: InjectedRouter) => { disableSortBy: true, accessor: "hosts_count", Cell: (cellProps: INumberCellProps): JSX.Element => ( - - - - - - - - + ), }, + { + title: "", + Header: "", + accessor: "linkToFilteredHosts", + disableSortBy: true, + Cell: (cellProps: ICellProps) => { + return ( + <> + {cellProps.row.original && ( + + )} + + ); + }, + }, ]; return tableHeaders; diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsTable/_styles.scss b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsTable/_styles.scss index 4226d6790f1d..4bb68fc4394e 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsTable/_styles.scss +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsTable/_styles.scss @@ -1,13 +1,7 @@ .software-title-details-table { - - .hosts-cell__wrapper { - display: flex; - align-items: center; - justify-content: space-between; - - .hosts-cell__link { - display: flex; - white-space: nowrap; + .data-table { + .hosts_count__header { + border-right: 0; } } } diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/_styles.scss b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/_styles.scss index 4a0f501b3569..1e428eb76da7 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/_styles.scss +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/_styles.scss @@ -4,30 +4,7 @@ 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; - } - } + h2 { + font-size: $small; } } diff --git a/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTable/SoftwareTable.tsx b/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTable/SoftwareTable.tsx index 47686816f53b..191e964e3ecf 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTable/SoftwareTable.tsx +++ b/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTable/SoftwareTable.tsx @@ -1,3 +1,8 @@ +/** +software/titles Software tab > Table +software/versions Software tab > Table (version toggle on) +*/ + import React, { useCallback, useContext, useMemo } from "react"; import { InjectedRouter } from "react-router"; import { Row } from "react-table"; @@ -176,7 +181,7 @@ const SoftwareTable = ({ return ( ); }; @@ -292,7 +297,7 @@ const SoftwareTable = ({ columnConfigs={softwareTableHeaders} data={tableData ?? []} isLoading={isLoading} - resultsTitle={"items"} + resultsTitle="items" emptyComponent={() => ( { const { id, name, source } = cellProps.row.original; + const teamQueryParam = buildQueryStringFromParams({ team_id: teamId }); + const softwareTitleDetailsPath = `${PATHS.SOFTWARE_TITLE_DETAILS( + id.toString() + )}?${teamQueryParam}`; + 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())); + router?.push(softwareTitleDetailsPath); }; return ( @@ -144,7 +150,7 @@ const generateTableHeaders = ( accessor: "vulnerabilities", Cell: (cellProps: IVulnCellProps): JSX.Element => { const vulnerabilities = getVulnerabilities( - cellProps.row.original.versions + cellProps.row.original.versions ?? [] ); return ; }, @@ -161,22 +167,27 @@ const generateTableHeaders = ( disableSortBy: false, accessor: "hosts_count", Cell: (cellProps: INumberCellProps): JSX.Element => ( - - - - - - - - + ), }, + { + title: "", + Header: "", + accessor: "linkToFilteredHosts", + disableSortBy: true, + Cell: (cellProps: ICellProps) => { + return ( + + ); + }, + }, ]; return softwareTableHeaders; diff --git a/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTable/SoftwareVersionsTableConfig.tsx b/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTable/SoftwareVersionsTableConfig.tsx index 2cfb1a5d712c..bcf1d85bbec5 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTable/SoftwareVersionsTableConfig.tsx +++ b/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTable/SoftwareVersionsTableConfig.tsx @@ -2,6 +2,7 @@ import React from "react"; import { Column } from "react-table"; import { InjectedRouter } from "react-router"; +import { buildQueryStringFromParams } from "utilities/url"; import { formatSoftwareType, ISoftwareVersion, @@ -74,15 +75,23 @@ const generateTableHeaders = ( Cell: (cellProps: IStringCellProps): JSX.Element => { const { id, name, source } = cellProps.row.original; + const teamQueryParam = buildQueryStringFromParams({ + team_id: teamId, + }); + const softwareVersionDetailsPath = `${PATHS.SOFTWARE_VERSION_DETAILS( + id.toString() + )}?${teamQueryParam}`; + 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())); + + router?.push(softwareVersionDetailsPath); }; return ( @@ -134,22 +143,31 @@ const generateTableHeaders = ( disableSortBy: false, accessor: "hosts_count", Cell: (cellProps: INumberCellProps): JSX.Element => ( - - - - - - - - + ), }, + { + title: "", + Header: "", + accessor: "linkToFilteredHosts", + disableSortBy: true, + Cell: (cellProps: ICellProps) => { + return ( + <> + {cellProps.row.original && ( + + )} + + ); + }, + }, ]; return softwareTableHeaders; diff --git a/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTitles.tsx b/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTitles.tsx index ae35c2eb9881..18b13e555e25 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTitles.tsx +++ b/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTitles.tsx @@ -1,3 +1,8 @@ +/** + software/titles Software tab + software/versions Software tab (version toggle on) + */ + import React from "react"; import { InjectedRouter } from "react-router"; import { useQuery } from "react-query"; @@ -58,6 +63,7 @@ const SoftwareTitles = ({ const { data: titlesData, isFetching: isTitlesFetching, + isLoading: isTitlesLoading, isError: isTitlesError, } = useQuery< ISoftwareTitlesResponse, @@ -88,6 +94,7 @@ const SoftwareTitles = ({ const { data: versionsData, isFetching: isVersionsFetching, + isLoading: isVersionsLoading, isError: isVersionsError, } = useQuery< ISoftwareVersionsResponse, @@ -114,6 +121,10 @@ const SoftwareTitles = ({ } ); + if (isTitlesLoading || isVersionsLoading) { + return ; + } + if (isTitlesError || isVersionsError) { return ; } diff --git a/frontend/pages/SoftwarePage/SoftwareTitles/_styles.scss b/frontend/pages/SoftwarePage/SoftwareTitles/_styles.scss index a42e2240b0cb..6df9667e4d70 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitles/_styles.scss +++ b/frontend/pages/SoftwarePage/SoftwareTitles/_styles.scss @@ -1,6 +1,4 @@ .software-titles { - margin-top: $pad-xxlarge; - &__table-error { margin-top: $pad-xxxlarge; } diff --git a/frontend/pages/SoftwarePage/SoftwareVersionDetailsPage/SoftwareVersionDetailsPage.tsx b/frontend/pages/SoftwarePage/SoftwareVersionDetailsPage/SoftwareVersionDetailsPage.tsx index 59c9ef8bdfa4..678c3bfa702d 100644 --- a/frontend/pages/SoftwarePage/SoftwareVersionDetailsPage/SoftwareVersionDetailsPage.tsx +++ b/frontend/pages/SoftwarePage/SoftwareVersionDetailsPage/SoftwareVersionDetailsPage.tsx @@ -1,29 +1,40 @@ -import React from "react"; +/** software/versions/:id */ + +import React, { useCallback, useContext } from "react"; import { useQuery } from "react-query"; +import { useErrorHandler } from "react-error-boundary"; import { RouteComponentProps } from "react-router"; import { AxiosError } from "axios"; -import { useErrorHandler } from "react-error-boundary"; + +import useTeamIdParam from "hooks/useTeamIdParam"; + +import { AppContext } from "context/app"; import softwareAPI, { ISoftwareVersionResponse, + IGetSoftwareVersionQueryKey, } from "services/entities/software"; import hostsCountAPI, { IHostsCountQueryKey, IHostsCountResponse, } from "services/entities/host_count"; import { ISoftwareVersion, formatSoftwareType } from "interfaces/software"; +import { ignoreAxiosError } from "interfaces/errors"; -import MainContent from "components/MainContent"; -import TableDataError from "components/DataError"; import Spinner from "components/Spinner"; +import MainContent from "components/MainContent"; +import TeamsHeader from "components/TeamsHeader"; +import Card from "components/Card"; import SoftwareDetailsSummary from "../components/SoftwareDetailsSummary"; import SoftwareVulnerabilitiesTable from "../components/SoftwareVulnerabilitiesTable"; +import DetailsNoHosts from "../components/DetailsNoHosts"; const baseClass = "software-version-details-page"; interface ISoftwareVersionDetailsRouteParams { id: string; + team_id?: string; } type ISoftwareTitleDetailsPageProps = RouteComponentProps< @@ -33,24 +44,44 @@ type ISoftwareTitleDetailsPageProps = RouteComponentProps< const SoftwareVersionDetailsPage = ({ routeParams, + router, + location, }: ISoftwareTitleDetailsPageProps) => { - const versionId = parseInt(routeParams.id, 10); + const { isPremiumTier, isOnGlobalTeam } = useContext(AppContext); const handlePageError = useErrorHandler(); + const versionId = parseInt(routeParams.id, 10); + + const { + currentTeamId, + teamIdForApi, + userTeams, + handleTeamChange, + } = useTeamIdParam({ + location, + router, + includeAllTeams: true, + includeNoTeam: false, + }); + const { data: softwareVersion, isLoading: isSoftwareVersionLoading, isError: isSoftwareVersionError, - } = useQuery( - ["software-version", versionId], - () => softwareAPI.getSoftwareVersion(versionId), + } = useQuery< + ISoftwareVersionResponse, + AxiosError, + ISoftwareVersion, + IGetSoftwareVersionQueryKey[] + >( + [{ scope: "softwareVersion", versionId, teamId: teamIdForApi }], + ({ queryKey }) => softwareAPI.getSoftwareVersion(queryKey[0]), { - select: (data) => data.software, - retry: false, refetchOnWindowFocus: false, + select: (data) => data.software, onError: (error) => { - if (error.status === 403) { - handlePageError({ status: 403 }); + if (!ignoreAxiosError(error, [403, 404])) { + handlePageError(error); } }, } @@ -71,37 +102,68 @@ const SoftwareVersionDetailsPage = ({ } ); + const onTeamChange = useCallback( + (teamId: number) => { + handleTeamChange(teamId); + }, + [handleTeamChange] + ); + const renderContent = () => { if (isSoftwareVersionLoading) { return ; } - if (isSoftwareVersionError) { - return ; - } - - if (!softwareVersion) { + if (!softwareVersion && !isSoftwareVersionError) { return null; } return ( <> - -
-

Vulnerabilities

- + )} + {isSoftwareVersionError ? ( + -
+ ) : ( + <> + + +

Vulnerabilities

+ +
+ + )} ); }; diff --git a/frontend/pages/SoftwarePage/SoftwareVersionDetailsPage/_styles.scss b/frontend/pages/SoftwarePage/SoftwareVersionDetailsPage/_styles.scss index 5b993e84fe79..60a88a8254e1 100644 --- a/frontend/pages/SoftwarePage/SoftwareVersionDetailsPage/_styles.scss +++ b/frontend/pages/SoftwarePage/SoftwareVersionDetailsPage/_styles.scss @@ -4,16 +4,7 @@ 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; - } + h2 { + font-size: $small; } } diff --git a/frontend/pages/SoftwarePage/SoftwareVulnerabilities/SoftwareVulnerabilities.tsx b/frontend/pages/SoftwarePage/SoftwareVulnerabilities/SoftwareVulnerabilities.tsx new file mode 100644 index 000000000000..946e4fe1dbe8 --- /dev/null +++ b/frontend/pages/SoftwarePage/SoftwareVulnerabilities/SoftwareVulnerabilities.tsx @@ -0,0 +1,93 @@ +/** software/vulnerabilities Vulnerabilities tab */ + +import React from "react"; +import { useQuery } from "react-query"; +import { InjectedRouter } from "react-router"; +import { + IGetVulnerabilitiesQueryKey, + IVulnerabilitiesResponse, + getVulnerabilities, +} from "services/entities/vulnerabilities"; + +import TableDataError from "components/DataError"; + +import SoftwareVulnerabilitiesTable from "./SoftwareVulnerabilitiesTable"; + +const baseClass = "software-vulnerabilities"; + +interface ISoftwareVulnerabilitiesProps { + router: InjectedRouter; + isSoftwareEnabled: boolean; + perPage: number; + query?: string; + orderDirection: "asc" | "desc"; + orderKey: string; + currentPage: number; + teamId?: number; + showExploitedVulnerabilitiesOnly: boolean; +} + +const SoftwareVulnerabilities = ({ + router, + isSoftwareEnabled, + query, + perPage, + orderDirection, + orderKey, + currentPage, + teamId, + showExploitedVulnerabilitiesOnly, +}: ISoftwareVulnerabilitiesProps) => { + const queryParams = { + page: currentPage, + per_page: perPage, + order_direction: orderDirection, + order_key: orderKey, + teamId, + query, + exploited: showExploitedVulnerabilitiesOnly, + }; + + const { data, isFetching, isError } = useQuery< + IVulnerabilitiesResponse, + Error, + IVulnerabilitiesResponse, + IGetVulnerabilitiesQueryKey[] + >( + [ + { + scope: "software-vulnerabilities", + ...queryParams, + }, + ], + () => getVulnerabilities(queryParams), + { + keepPreviousData: true, + staleTime: 30000, + } + ); + + if (isError) { + return ; + } + + return ( +
+ +
+ ); +}; + +export default SoftwareVulnerabilities; diff --git a/frontend/pages/SoftwarePage/SoftwareVulnerabilities/SoftwareVulnerabilitiesTable/SoftwareVulnerabilitiesTable.tsx b/frontend/pages/SoftwarePage/SoftwareVulnerabilities/SoftwareVulnerabilitiesTable/SoftwareVulnerabilitiesTable.tsx new file mode 100644 index 000000000000..7c319e9c2cb0 --- /dev/null +++ b/frontend/pages/SoftwarePage/SoftwareVulnerabilities/SoftwareVulnerabilitiesTable/SoftwareVulnerabilitiesTable.tsx @@ -0,0 +1,281 @@ +/** software/vulnerabilities Vulnerabilities tab > Table */ + +import React, { useCallback, useContext, useMemo } from "react"; +import { InjectedRouter } from "react-router"; +import { Row } from "react-table"; + +import PATHS from "router/paths"; + +import { AppContext } from "context/app"; +import { + GITHUB_NEW_ISSUE_LINK, + EXPLOITED_VULNERABILITIES_DROPDOWN_OPTIONS, +} from "utilities/constants"; + +// @ts-ignore +import Dropdown from "components/forms/fields/Dropdown"; +import CustomLink from "components/CustomLink"; +import TableContainer from "components/TableContainer"; +import LastUpdatedText from "components/LastUpdatedText"; +import { ITableQueryData } from "components/TableContainer/TableContainer"; + +import EmptySoftwareTable from "pages/SoftwarePage/components/EmptySoftwareTable"; +import { IVulnerabilitiesResponse } from "services/entities/vulnerabilities"; +import { buildQueryStringFromParams } from "utilities/url"; +import { getNextLocationPath } from "utilities/helpers"; + +import generateTableConfig from "./VulnerabilitiesTableConfig"; + +const baseClass = "software-vulnerabilities-table"; + +interface IRowProps extends Row { + original: { + cve?: string; + }; +} + +interface ISoftwareVulnerabilitiesTableProps { + router: InjectedRouter; + isSoftwareEnabled: boolean; + data?: IVulnerabilitiesResponse; + query?: string; + perPage: number; + orderDirection: "asc" | "desc"; + orderKey: string; + showExploitedVulnerabilitiesOnly: boolean; + currentPage: number; + teamId?: number; + isLoading: boolean; +} + +const SoftwareVulnerabilitiesTable = ({ + router, + isSoftwareEnabled, + data, + query, + perPage, + orderDirection, + orderKey, + showExploitedVulnerabilitiesOnly, + currentPage, + teamId, + isLoading, +}: ISoftwareVulnerabilitiesTableProps) => { + const { isPremiumTier, isSandboxMode, noSandboxHosts } = useContext( + AppContext + ); + + const determineQueryParamChange = useCallback( + (newTableQuery: ITableQueryData) => { + const changedEntry = Object.entries(newTableQuery).find(([key, val]) => { + switch (key) { + case "sortDirection": + return val !== orderDirection; + case "sortHeader": + return val !== orderKey; + case "pageIndex": + return val !== currentPage; + case "searchQuery": + return val !== query; + case "exploited": + return val !== showExploitedVulnerabilitiesOnly.toString(); + default: + return false; + } + }); + return changedEntry?.[0] ?? ""; + }, + [ + currentPage, + orderDirection, + orderKey, + query, + showExploitedVulnerabilitiesOnly, + ] + ); + + const generateNewQueryParams = useCallback( + (newTableQuery: ITableQueryData, changedParam: string) => { + return { + team_id: teamId, + exploited: showExploitedVulnerabilitiesOnly.toString(), + query: newTableQuery.searchQuery, + order_direction: newTableQuery.sortDirection, + order_key: newTableQuery.sortHeader, + page: changedParam === "pageIndex" ? newTableQuery.pageIndex : 0, + }; + }, + [teamId, showExploitedVulnerabilitiesOnly] + ); + + 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_VULNERABILITIES, + routeTemplate: "", + queryParams: generateNewQueryParams(newTableQuery, changedParam), + }); + + router.replace(newRoute); + }, + [determineQueryParamChange, generateNewQueryParams, router] + ); + + // determines if a user be able to search in the table + const searchable = + isSoftwareEnabled && + (!!data?.vulnerabilities || + query !== "" || + showExploitedVulnerabilitiesOnly); + + const vulnerabilitiesTableHeaders = useMemo(() => { + if (!data) return []; + return generateTableConfig( + isPremiumTier, + isSandboxMode, + router, + { + includeName: true, + includeVulnerabilities: true, + includeIcon: true, + }, + teamId + ); + }, [data, router, teamId]); + + const handleExploitedVulnFilterDropdownChange = ( + isFilterExploited: boolean + ) => { + router.replace( + getNextLocationPath({ + pathPrefix: PATHS.SOFTWARE_VULNERABILITIES, + routeTemplate: "", + queryParams: { + query, + team_id: teamId, + order_direction: orderDirection, + order_key: orderKey, + exploited: isFilterExploited.toString(), + page: 0, // resets page index + }, + }) + ); + }; + + const handleRowSelect = (row: IRowProps) => { + const hostsByVulnerabilityParams = { + cve: row.original.cve, + team_id: teamId, + }; + + const path = `${PATHS.MANAGE_HOSTS}?${buildQueryStringFromParams( + hostsByVulnerabilityParams + )}`; + + router.push(path); + }; + + const getItemsCountText = () => { + const count = data?.count; + if (!data?.vulnerabilities || !count) return ""; + + return count === 1 ? `${count} vulnerability` : `${count} vulnerabilities`; + }; + + const getLastUpdatedText = () => { + if (!data?.vulnerabilities || !data?.counts_updated_at) return ""; + return ( + + ); + }; + + const renderVulnerabilityCount = () => { + const itemText = getItemsCountText(); + const lastUpdatedText = getLastUpdatedText(); + + if (!itemText) return null; + + return ( +
+ {itemText} + {lastUpdatedText} +
+ ); + }; + + const renderTableFooter = () => { + return ( +
+ Seeing unexpected software or vulnerabilities?{" "} + +
+ ); + }; + + const renderExploitedVulnerabilitiesDropdown = () => { + return ( + + ); + }; + + return ( +
+ ( + + )} + defaultSortHeader={orderKey} + defaultSortDirection={orderDirection} + defaultPageIndex={currentPage} + manualSortBy + pageSize={perPage} + showMarkAllPages={false} + isAllPagesSelected={false} + disableNextPage={!data?.meta.has_next_results} + searchable={searchable} + searchQueryColumn="vulnerability" + inputPlaceHolder="Search by CVE" + onQueryChange={onQueryChange} + customControl={ + searchable ? renderExploitedVulnerabilitiesDropdown : undefined + } + renderCount={renderVulnerabilityCount} + renderFooter={renderTableFooter} + disableMultiRowSelect + onSelectSingleRow={handleRowSelect} + /> +
+ ); +}; + +export default SoftwareVulnerabilitiesTable; diff --git a/frontend/pages/SoftwarePage/SoftwareVulnerabilities/SoftwareVulnerabilitiesTable/VulnerabilitiesTableConfig.tsx b/frontend/pages/SoftwarePage/SoftwareVulnerabilities/SoftwareVulnerabilitiesTable/VulnerabilitiesTableConfig.tsx new file mode 100644 index 000000000000..404fd0b212b8 --- /dev/null +++ b/frontend/pages/SoftwarePage/SoftwareVulnerabilities/SoftwareVulnerabilitiesTable/VulnerabilitiesTableConfig.tsx @@ -0,0 +1,285 @@ +import React from "react"; + +import { InjectedRouter } from "react-router"; + +import PATHS from "router/paths"; +import { formatSeverity } from "utilities/helpers"; +import { buildQueryStringFromParams } from "utilities/url"; +import { formatOperatingSystemDisplayName } from "interfaces/operating_system"; +import { IVulnerability } from "interfaces/vulnerability"; + +import ProbabilityOfExploit from "components/ProbabilityOfExploit/ProbabilityOfExploit"; +import TextCell from "components/TableContainer/DataTable/TextCell"; +import HeaderCell from "components/TableContainer/DataTable/HeaderCell"; +import ViewAllHostsLink from "components/ViewAllHostsLink"; +import LinkCell from "components/TableContainer/DataTable/LinkCell"; +import TooltipWrapper from "components/TooltipWrapper"; +import PremiumFeatureIconWithTooltip from "components/PremiumFeatureIconWithTooltip"; +import { HumanTimeDiffWithDateTip } from "components/HumanTimeDiffWithDateTip"; + +interface ICellProps { + cell: { + value: number | string | IVulnerability[]; + }; + row: { + original: IVulnerability; + }; +} + +interface ITextCellProps extends ICellProps { + cell: { + value: string | number; + }; +} + +interface IHeaderProps { + column: { + title: string; + isSortedDesc: boolean; + }; +} + +interface IDataColumn { + title: string; + Header: ((props: IHeaderProps) => JSX.Element) | string; + accessor: string; + Cell: (props: ITextCellProps) => JSX.Element; + disableHidden?: boolean; + disableSortBy?: boolean; + sortType?: string; +} + +interface IVulnerabilitiesTableConfigOptions { + includeName?: boolean; + includeVulnerabilities?: boolean; + includeIcon?: boolean; +} + +const generateTableHeaders = ( + isPremiumTier?: boolean, + isSandboxMode?: boolean, + router?: InjectedRouter, + configOptions?: IVulnerabilitiesTableConfigOptions, + teamId?: number +): IDataColumn[] => { + const tableHeaders: IDataColumn[] = [ + { + title: "Vulnerability", + Header: "Vulnerability", + disableSortBy: true, + accessor: "cve", + Cell: (cellProps: ITextCellProps) => { + if (!configOptions?.includeIcon) { + return ( + formatOperatingSystemDisplayName(name)} + /> + ); + } + + const { cve } = cellProps.row.original; + + const teamQueryParam = buildQueryStringFromParams({ team_id: teamId }); + const softwareVulnerabilitiesDetailsPath = `${PATHS.SOFTWARE_VULNERABILITY_DETAILS( + cve + )}?${teamQueryParam}`; + + const onClickVulnerability = (e: React.MouseEvent) => { + // Allows for button to be clickable in a clickable row + e.stopPropagation(); + + router?.push(softwareVulnerabilitiesDetailsPath); + }; + + return ( + + ); + }, + }, + { + title: "Severity", + accessor: "cvss_score", + disableSortBy: false, + Header: (headerProps: IHeaderProps): JSX.Element => { + const titleWithToolTip = ( + + The worst case impact across different environments (CVSS base + score). + + } + > + Severity + + ); + return ( + <> + + {isSandboxMode && } + + ); + }, + Cell: ({ cell: { value } }: ITextCellProps): JSX.Element => ( + + ), + }, + { + title: "Probability of exploit", + accessor: "epss_probability", + disableSortBy: false, + Header: (headerProps: IHeaderProps): JSX.Element => { + const titleWithToolTip = ( + + The probability that this vulnerability will be exploited in the + next 30 days (EPSS probability).
+ This data is reported by FIRST.org. + + } + > + Probability of exploit +
+ ); + return ( + <> + + {isSandboxMode && } + + ); + }, + Cell: (cellProps: ICellProps): JSX.Element => ( + + ), + }, + { + title: "Published", + accessor: "cve_published", + disableSortBy: false, + Header: (headerProps: IHeaderProps): JSX.Element => { + const titleWithToolTip = ( + + The date this vulnerability was published in the National + Vulnerability Database (NVD). + + } + > + Published + + ); + return ( + <> + + {isSandboxMode && } + + ); + }, + Cell: ({ cell: { value } }: ITextCellProps): JSX.Element => { + const valString = typeof value === "number" ? value.toString() : value; + return ( + + ); + }, + }, + { + title: "Detected", + accessor: "created_at", + disableSortBy: false, + Header: (headerProps: IHeaderProps): JSX.Element => { + return ( + <> + + {isSandboxMode && } + + ); + }, + Cell: (cellProps: ICellProps): JSX.Element => { + const createdAt = cellProps.row.original.created_at || ""; + + return ( + + ); + }, + }, + { + title: "Hosts", + Header: (cellProps: IHeaderProps): JSX.Element => ( + + ), + disableSortBy: false, + accessor: "hosts_count", + Cell: (cellProps: ITextCellProps): JSX.Element => { + const { hosts_count } = cellProps.row.original; + return ; + }, + }, + { + title: "", + Header: "", + accessor: "linkToFilteredHosts", + disableSortBy: true, + Cell: (cellProps: ICellProps) => { + return ( + <> + {cellProps.row.original && ( + + )} + + ); + }, + }, + ]; + + if (!isPremiumTier) { + return tableHeaders.filter( + (header) => + header.accessor !== "epss_probability" && + header.accessor !== "cve_published" + ); + } + + return tableHeaders; +}; + +export default generateTableHeaders; diff --git a/frontend/pages/SoftwarePage/SoftwareVulnerabilities/SoftwareVulnerabilitiesTable/_styles.scss b/frontend/pages/SoftwarePage/SoftwareVulnerabilities/SoftwareVulnerabilitiesTable/_styles.scss new file mode 100644 index 000000000000..5ab1e1a438bd --- /dev/null +++ b/frontend/pages/SoftwarePage/SoftwareVulnerabilities/SoftwareVulnerabilitiesTable/_styles.scss @@ -0,0 +1,40 @@ +.software-vulnerabilities-table { + &__count { + display: flex; + gap: 12px; + align-items: center; + } + + .hosts-cell__wrapper { + display: flex; + align-items: center; + justify-content: space-between; + } + + .vulnerabilities-hosts-link { + opacity: 0; + transition: 250ms; + } + + tr:hover { + .os-hosts-link { + opacity: 1; + } + } + + .table-container { + &__data-table-block { + .data-table-block { + .data-table__table { + tbody { + .link-cell { + display: flex; + align-items: center; + gap: $pad-small; + } + } + } + } + } + } +} diff --git a/frontend/pages/SoftwarePage/SoftwareVulnerabilities/SoftwareVulnerabilitiesTable/index.ts b/frontend/pages/SoftwarePage/SoftwareVulnerabilities/SoftwareVulnerabilitiesTable/index.ts new file mode 100644 index 000000000000..fcb97b12492e --- /dev/null +++ b/frontend/pages/SoftwarePage/SoftwareVulnerabilities/SoftwareVulnerabilitiesTable/index.ts @@ -0,0 +1 @@ +export { default } from "./SoftwareVulnerabilitiesTable"; diff --git a/frontend/pages/SoftwarePage/SoftwareVulnerabilities/_styles.scss b/frontend/pages/SoftwarePage/SoftwareVulnerabilities/_styles.scss new file mode 100644 index 000000000000..fecd08d5b955 --- /dev/null +++ b/frontend/pages/SoftwarePage/SoftwareVulnerabilities/_styles.scss @@ -0,0 +1,5 @@ +.software-vulnerabilities { + .dropdown__custom-value-label { + width: 260px; // Override 105px for longer text options + } +} diff --git a/frontend/pages/SoftwarePage/SoftwareVulnerabilities/index.ts b/frontend/pages/SoftwarePage/SoftwareVulnerabilities/index.ts new file mode 100644 index 000000000000..2122b774a670 --- /dev/null +++ b/frontend/pages/SoftwarePage/SoftwareVulnerabilities/index.ts @@ -0,0 +1 @@ +export { default } from "./SoftwareVulnerabilities"; diff --git a/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/SoftwareVulnOSVersions/SoftwareVulnOSVersions.tsx b/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/SoftwareVulnOSVersions/SoftwareVulnOSVersions.tsx new file mode 100644 index 000000000000..7cc266421646 --- /dev/null +++ b/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/SoftwareVulnOSVersions/SoftwareVulnOSVersions.tsx @@ -0,0 +1,78 @@ +/** software/vulnerabilities/:cve > Vulnerable OS section */ + +import React, { useCallback, useMemo } from "react"; + +import { InjectedRouter } from "react-router"; + +import PATHS from "router/paths"; +import { IVulnerability } from "interfaces/vulnerability"; +import { buildQueryStringFromParams } from "utilities/url"; +import Card from "components/Card"; +import TableContainer from "components/TableContainer"; + +import generateColumnConfigs from "./SwVulnOSTableConfig"; + +const baseClass = "software-vuln-os-versions"; + +interface ISoftwareVulnOSVersions { + osVersions: IVulnerability["os_versions"]; + isPremiumTier: boolean; + router: InjectedRouter; + teamIdForApi?: number; +} + +const SoftwareVulnOSVersions = ({ + osVersions, + isPremiumTier, + router, + teamIdForApi, +}: ISoftwareVulnOSVersions) => { + const columnConfigs = useMemo( + () => generateColumnConfigs(isPremiumTier, router, teamIdForApi), + [isPremiumTier, router, teamIdForApi] + ); + + const onSelectSingleRow = useCallback( + ({ original: { os_version_id } }) => { + if (!os_version_id) { + return; + } + + router.push( + `${PATHS.MANAGE_HOSTS}?${buildQueryStringFromParams({ + os_version_id, + team_id: teamIdForApi, + })}` + ); + }, + [teamIdForApi, router] + ); + + const renderVulnerableOSTable = () => { + return ( + 1 ? "items" : "item"} + isLoading={false} // not rendered otherwise + emptyComponent={() => <>} + showMarkAllPages={false} + isAllPagesSelected={false} + disableMultiRowSelect + onSelectSingleRow={onSelectSingleRow} + /> + ); + }; + + return ( + +

Vulnerable OS

+ {renderVulnerableOSTable()} +
+ ); +}; + +export default SoftwareVulnOSVersions; diff --git a/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/SoftwareVulnOSVersions/SwVulnOSTableConfig.tsx b/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/SoftwareVulnOSVersions/SwVulnOSTableConfig.tsx new file mode 100644 index 000000000000..9dcbc20d9fbe --- /dev/null +++ b/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/SoftwareVulnOSVersions/SwVulnOSTableConfig.tsx @@ -0,0 +1,110 @@ +import React from "react"; + +import { Column } from "react-table"; + +import { IVulnerabilityOSVersion } from "interfaces/operating_system"; + +import LinkCell from "components/TableContainer/DataTable/LinkCell"; + +import PATHS from "router/paths"; +import SoftwareIcon from "pages/SoftwarePage/components/icons/SoftwareIcon"; +import TextCell from "components/TableContainer/DataTable/TextCell"; +import ViewAllHostsLink from "components/ViewAllHostsLink"; +import { InjectedRouter } from "react-router"; + +interface ICellProps { + row: { + original: IVulnerabilityOSVersion; + }; +} + +interface INumberCellProps extends ICellProps { + cell: { + value: number; + }; +} + +const generateColumnConfigs = ( + isPremiumTier: boolean, + router: InjectedRouter, + teamIdForApi?: number +): Column[] => { + const configs = [ + { + Header: "Name", + disableSortBy: true, + accessor: "name_only", + Cell: ({ row }: ICellProps) => { + const { name, os_version_id, platform } = row.original; + const endpoint = PATHS.SOFTWARE_OS_DETAILS(os_version_id); + // since No Teams not supported on this page, falsiness of 0 is okay + const path = teamIdForApi + ? `${endpoint}?team_id=${teamIdForApi}` + : endpoint; + const onClickVulnOs = (e: React.MouseEvent) => { + // Allows for button to be clickable in a clickable row + e.stopPropagation(); + + router?.push(path); + }; + return ( + + + {name} + + } + /> + ); + }, + }, + { + Header: "Version", + disableSortBy: true, + accessor: "version", + Cell: ({ cell }: INumberCellProps) => , + }, + { + Header: () => ( + <> + Resolved in
version
+ + ), + disableSortBy: true, + accessor: "resolved_in_version", + Cell: ({ cell }: INumberCellProps) => , + }, + { + Header: "Hosts", + disableSortBy: true, + accessor: "hosts_count", + Cell: ({ row }: ICellProps) => { + const { hosts_count, os_version_id } = row.original; + return ( + <> + + + + ); + }, + }, + ]; + + if (!isPremiumTier) { + return configs.filter( + (header) => header.accessor !== "resolved_in_version" + ); + } + + return configs; +}; + +export default generateColumnConfigs; diff --git a/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/SoftwareVulnOSVersions/index.ts b/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/SoftwareVulnOSVersions/index.ts new file mode 100644 index 000000000000..1e4fb1fecdb4 --- /dev/null +++ b/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/SoftwareVulnOSVersions/index.ts @@ -0,0 +1 @@ +export { default } from "./SoftwareVulnOSVersions"; diff --git a/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/SoftwareVulnSoftwareVersions/SoftwareVulnSoftwareVersions.tsx b/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/SoftwareVulnSoftwareVersions/SoftwareVulnSoftwareVersions.tsx new file mode 100644 index 000000000000..c19ba2da6471 --- /dev/null +++ b/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/SoftwareVulnSoftwareVersions/SoftwareVulnSoftwareVersions.tsx @@ -0,0 +1,78 @@ +/** software/vulnerabilities/:cve > Vulnerable software section */ + +import React, { useCallback, useMemo } from "react"; + +import { InjectedRouter } from "react-router"; + +import { IVulnerability } from "interfaces/vulnerability"; + +import PATHS from "router/paths"; +import { buildQueryStringFromParams } from "utilities/url"; + +import Card from "components/Card"; +import TableContainer from "components/TableContainer"; + +import generateColumnConfigs from "./SwVulnSwTableConfig"; + +const baseClass = "software-vuln-software-versions"; + +interface ISoftwareVulnSoftwareVersions { + vulnSoftware: IVulnerability["software"]; + isPremiumTier: boolean; + router: InjectedRouter; + teamIdForApi?: number; +} + +const SoftwareVulnSoftwareVersions = ({ + vulnSoftware, + isPremiumTier, + router, + teamIdForApi, +}: ISoftwareVulnSoftwareVersions) => { + const columnConfigs = useMemo( + () => generateColumnConfigs(isPremiumTier, router, teamIdForApi), + [isPremiumTier, router, teamIdForApi] + ); + + const onSelectSingleRow = useCallback( + ({ original: { id: software_title_id } }) => { + if (!software_title_id) { + return; + } + + router.push( + `${PATHS.MANAGE_HOSTS}?${buildQueryStringFromParams({ + software_title_id, + team_id: teamIdForApi, + })}` + ); + }, + [teamIdForApi, router] + ); + const renderVulnerableSoftwareTable = () => { + return ( + 1 ? "items" : "item"} + isLoading={false} // not rendered otherwise + emptyComponent={() => <>} + showMarkAllPages={false} + isAllPagesSelected={false} + disableMultiRowSelect + onSelectSingleRow={onSelectSingleRow} + /> + ); + }; + return ( + +

Vulnerable software

+ {renderVulnerableSoftwareTable()} +
+ ); +}; + +export default SoftwareVulnSoftwareVersions; diff --git a/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/SoftwareVulnSoftwareVersions/SwVulnSwTableConfig.tsx b/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/SoftwareVulnSoftwareVersions/SwVulnSwTableConfig.tsx new file mode 100644 index 000000000000..d9c3dcbdeda8 --- /dev/null +++ b/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/SoftwareVulnSoftwareVersions/SwVulnSwTableConfig.tsx @@ -0,0 +1,112 @@ +import React from "react"; +import { Column } from "react-table"; + +import { IVulnerabilitySoftware } from "interfaces/software"; + +import PATHS from "router/paths"; + +import LinkCell from "components/TableContainer/DataTable/LinkCell"; +import TextCell from "components/TableContainer/DataTable/TextCell"; +import ViewAllHostsLink from "components/ViewAllHostsLink"; +import SoftwareIcon from "pages/SoftwarePage/components/icons/SoftwareIcon"; +import { InjectedRouter } from "react-router"; + +interface ICellProps { + cell: { + value: number | string | IVulnerabilitySoftware; + }; + row: { + original: IVulnerabilitySoftware; + }; +} + +interface IStringCellProps extends ICellProps { + cell: { + value: string; + }; +} + +const generateColumnConfigs = ( + isPremiumTier: boolean, + router: InjectedRouter, + teamIdForApi?: number +): Column[] => { + const configs = [ + { + Header: "Name", + disableSortBy: true, + accessor: "name", + Cell: ({ row }: ICellProps) => { + const { name, id } = row.original; + const endpoint = PATHS.SOFTWARE_VERSION_DETAILS(id.toString()); + // since No Teams not supported on this page, falsiness of 0 is okay + const path = teamIdForApi + ? `${endpoint}?team_id=${teamIdForApi}` + : endpoint; + const onClickVulnSwVersion = (e: React.MouseEvent) => { + // Allows for button to be clickable in a clickable row + e.stopPropagation(); + + router?.push(path); + }; + return ( + + + {name} + + } + /> + ); + }, + }, + { + Header: "Version", + disableSortBy: true, + accessor: "version", + Cell: ({ cell }: IStringCellProps) => , + }, + { + Header: () => ( + <> + Resolved in
version
+ + ), + disableSortBy: true, + accessor: "resolved_in_version", + Cell: ({ cell }: IStringCellProps) => , + }, + { + Header: "Hosts", + disableSortBy: true, + accessor: "hosts_count", + Cell: ({ row }: ICellProps) => { + const { hosts_count, id } = row.original; + return ( + <> + + + + ); + }, + }, + ]; + + if (!isPremiumTier) { + return configs.filter( + (header) => header.accessor !== "resolved_in_version" + ); + } + + return configs; +}; + +export default generateColumnConfigs; diff --git a/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/SoftwareVulnSoftwareVersions/index.ts b/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/SoftwareVulnSoftwareVersions/index.ts new file mode 100644 index 000000000000..f4ff1dcedea1 --- /dev/null +++ b/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/SoftwareVulnSoftwareVersions/index.ts @@ -0,0 +1 @@ +export { default } from "./SoftwareVulnSoftwareVersions"; diff --git a/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/SoftwareVulnSummary/SoftwareVulnSummary.tsx b/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/SoftwareVulnSummary/SoftwareVulnSummary.tsx new file mode 100644 index 000000000000..853901d5aaaa --- /dev/null +++ b/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/SoftwareVulnSummary/SoftwareVulnSummary.tsx @@ -0,0 +1,105 @@ +/** software/vulnerabilities/:cve > Summary section */ + +import React from "react"; + +import { IVulnerability } from "interfaces/vulnerability"; + +import CustomLink from "components/CustomLink"; +import Card from "components/Card"; +import DataSet from "components/DataSet"; +import TooltipWrapper from "components/TooltipWrapper"; +import ViewAllHostsLink from "components/ViewAllHostsLink"; +import ProbabilityOfExploit from "components/ProbabilityOfExploit"; +import { HumanTimeDiffWithDateTip } from "components/HumanTimeDiffWithDateTip"; + +const baseClass = "software-vuln-summary"; + +interface ISoftwareVulnSummaryProps { + vuln: IVulnerability; + isPremiumTier: boolean; + teamIdForApi?: number; +} + +const SoftwareVulnSummary = ({ + vuln, + isPremiumTier, + teamIdForApi, +}: ISoftwareVulnSummaryProps) => { + const { + cve, + details_link, + cve_description, + cvss_score, + epss_probability, + cisa_known_exploit, + cve_published, + created_at, + hosts_count, + } = vuln; + + return ( + + +

{cve}

+ + + + +
+ {isPremiumTier && cve_description && ( +
{cve_description}
+ )} +
+ {isPremiumTier && ( + <> + {cvss_score && ( + + Severity + + } + value={cvss_score} + /> + )} + {epss_probability && ( + + Probability of exploit + + } + value={ + + } + /> + )} + {cve_published && ( + } + /> + )} + + )} + } + /> + +
+
+ ); +}; + +export default SoftwareVulnSummary; diff --git a/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/SoftwareVulnSummary/_styles.scss b/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/SoftwareVulnSummary/_styles.scss new file mode 100644 index 000000000000..0475f8b22c97 --- /dev/null +++ b/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/SoftwareVulnSummary/_styles.scss @@ -0,0 +1,46 @@ +.software-vuln-summary { + display: flex; + flex-direction: column; + gap: $large; + + &__header { + display: flex; + justify-content: space-between; + align-items: center; + + &__links { + display: flex; + gap: 1.2rem; + + .component__tooltip-wrapper__underline { + top: 0; + } + } + + h1 { + font-size: 1.2rem; + font-weight: $bold; + } + } + + &__description { + font-size: $x-small; + } + + &__description-list { + display: flex; + gap: 24px 40px; + flex-wrap: wrap; + + .data-set { + height: 40px; + display: flex; + flex-direction: column; + justify-content: space-between; + + .component__tooltip-wrapper__underline { + top: 0; + } + } + } +} diff --git a/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/SoftwareVulnerabilityDetailsPage.tsx b/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/SoftwareVulnerabilityDetailsPage.tsx new file mode 100644 index 000000000000..6aed06e47da9 --- /dev/null +++ b/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/SoftwareVulnerabilityDetailsPage.tsx @@ -0,0 +1,160 @@ +/** software/vulnerabilities/:cve */ + +import React, { useCallback, useContext } from "react"; +import { useQuery } from "react-query"; +import { useErrorHandler } from "react-error-boundary"; +import { RouteComponentProps } from "react-router"; +import { AxiosError } from "axios"; + +import useTeamIdParam from "hooks/useTeamIdParam"; + +import { AppContext } from "context/app"; + +import { IVulnerability } from "interfaces/vulnerability"; +import softwareVulnAPI, { + IGetVulnerabilityQueryKey, + IVulnerabilityResponse, +} from "services/entities/vulnerabilities"; +import { ignoreAxiosError } from "interfaces/errors"; + +import Spinner from "components/Spinner"; +import MainContent from "components/MainContent"; +import TeamsHeader from "components/TeamsHeader"; + +import SoftwareVulnSummary from "./SoftwareVulnSummary/SoftwareVulnSummary"; +import SoftwareVulnOSVersions from "./SoftwareVulnOSVersions"; +import SoftwareVulnSoftwareVersions from "./SoftwareVulnSoftwareVersions"; +import DetailsNoHosts from "../components/DetailsNoHosts"; + +const baseClass = "software-vulnerability-details-page"; + +interface ISoftwareVulnerabilityDetailsRouteParams { + cve: string; + team_id?: string; +} + +type ISoftwareVulnerabilityDetailsPageProps = RouteComponentProps< + undefined, + ISoftwareVulnerabilityDetailsRouteParams +>; + +const SoftwareVulnerabilityDetailsPage = ({ + router, + routeParams, + location, +}: ISoftwareVulnerabilityDetailsPageProps) => { + const { isPremiumTier, isOnGlobalTeam } = useContext(AppContext); + const handlePageError = useErrorHandler(); + + const { + currentTeamId, + teamIdForApi, + userTeams, + handleTeamChange, + } = useTeamIdParam({ + location, + router, + includeAllTeams: true, + includeNoTeam: false, + }); + + const { + data: vuln, + isLoading: isVulnLoading, + isError: isVulnError, + } = useQuery< + IVulnerabilityResponse, + AxiosError, + IVulnerability, + IGetVulnerabilityQueryKey[] + >( + [ + { + scope: "softwareVulnByCVE", + cve: routeParams.cve, + teamId: teamIdForApi, + }, + ], + ({ queryKey }) => { + return softwareVulnAPI.getVulnerability(queryKey[0]); + }, + { + select: (data) => data.vulnerability, + onError: (error) => { + if (!ignoreAxiosError(error, [403, 404])) { + handlePageError(error); + } + }, + } + ); + + const onTeamChange = useCallback( + (teamId: number) => { + handleTeamChange(teamId); + }, + [handleTeamChange] + ); + + const renderCards = (v: IVulnerability) => ( + <> + + {!!v.os_versions && v.os_versions.length > 0 && ( + + )} + + {!!v.software && v.software.length > 0 && ( + + )} + + ); + + const renderContent = () => { + if (isVulnLoading || !vuln) { + return ; + } + return ( + <> + {isPremiumTier && ( + + )} + {isVulnError ? ( + + ) : ( + renderCards(vuln) + )} + + ); + }; + + return ( + + <>{renderContent()} + + ); +}; + +export default SoftwareVulnerabilityDetailsPage; diff --git a/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/_styles.scss b/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/_styles.scss new file mode 100644 index 000000000000..62de267f92b9 --- /dev/null +++ b/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/_styles.scss @@ -0,0 +1,65 @@ +.software-vulnerability-details-page { + background-color: $ui-off-white; + @include page; + + .card { + display: flex; + flex-direction: column; + gap: $pad-large; + padding: $pad-xxlarge; + } + + * > h1, + h2 { + margin: 0; + font-weight: $bold; + } + + h1 { + font-size: 1.2rem; + } + h2 { + font-size: 1rem; + } + + .resolved-suffix { + display: inline; + @media (max-width: $break-md) { + display: none; + } + } + + .table-container { + &__header { + margin-top: 0; + margin-bottom: $pad-medium; + } + + &__results-count, + .results-count { + height: initial; + } + + .data-table-block .data-table tbody { + tr { + .hosts_count__cell { + display: flex; + justify-content: space-between; + align-items: center; + + .w250 { + min-width: initial; + height: min-content; + } + } + } + td { + .link-cell { + display: flex; + align-items: center; + gap: $pad-small; + } + } + } + } +} diff --git a/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/index.ts b/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/index.ts new file mode 100644 index 000000000000..65abf120eb1e --- /dev/null +++ b/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/index.ts @@ -0,0 +1 @@ +export { default } from "./SoftwareVulnerabilityDetailsPage"; diff --git a/frontend/pages/SoftwarePage/_styles.scss b/frontend/pages/SoftwarePage/_styles.scss index 1c4f5ba8fc06..cfdd0b9c51b9 100644 --- a/frontend/pages/SoftwarePage/_styles.scss +++ b/frontend/pages/SoftwarePage/_styles.scss @@ -58,6 +58,10 @@ } &__wrapper { + .component__tabs-wrapper { + margin-bottom: $pad-xxlarge; + } + .table-container { .software-icon { width: 24px; diff --git a/frontend/pages/SoftwarePage/components/DetailsNoHosts/DetailsNoHosts.tsx b/frontend/pages/SoftwarePage/components/DetailsNoHosts/DetailsNoHosts.tsx new file mode 100644 index 000000000000..6c1c62a60b3d --- /dev/null +++ b/frontend/pages/SoftwarePage/components/DetailsNoHosts/DetailsNoHosts.tsx @@ -0,0 +1,21 @@ +import React from "react"; + +import Card from "components/Card"; + +const baseClass = "details-no-hosts"; + +interface IDetailsNoHosts { + header: string; + details: string; +} + +const DetailsNoHosts = ({ header, details }: IDetailsNoHosts) => { + return ( + +

{header}

+

{details}

+
+ ); +}; + +export default DetailsNoHosts; diff --git a/frontend/pages/SoftwarePage/components/DetailsNoHosts/_styles.scss b/frontend/pages/SoftwarePage/components/DetailsNoHosts/_styles.scss new file mode 100644 index 000000000000..9d29403e5f43 --- /dev/null +++ b/frontend/pages/SoftwarePage/components/DetailsNoHosts/_styles.scss @@ -0,0 +1,10 @@ +.details-no-hosts { + padding: $pad-xxlarge; + gap: 0; + text-align: center; + * { + font-size: $x-small; + line-height: 21px; + margin: 0; + } +} diff --git a/frontend/pages/SoftwarePage/components/DetailsNoHosts/index.ts b/frontend/pages/SoftwarePage/components/DetailsNoHosts/index.ts new file mode 100644 index 000000000000..03d467892f5a --- /dev/null +++ b/frontend/pages/SoftwarePage/components/DetailsNoHosts/index.ts @@ -0,0 +1 @@ +export { default } from "./DetailsNoHosts"; diff --git a/frontend/pages/SoftwarePage/components/ManageSoftwareAutomationsModal/ManageSoftwareAutomationsModal.tsx b/frontend/pages/SoftwarePage/components/ManageSoftwareAutomationsModal/ManageSoftwareAutomationsModal.tsx index 415982a159f4..002bca8924ca 100644 --- a/frontend/pages/SoftwarePage/components/ManageSoftwareAutomationsModal/ManageSoftwareAutomationsModal.tsx +++ b/frontend/pages/SoftwarePage/components/ManageSoftwareAutomationsModal/ManageSoftwareAutomationsModal.tsx @@ -353,9 +353,9 @@ const ManageAutomationsModal = ({ searchable options={createIntegrationDropdownOptions()} onChange={onChangeSelectIntegration} - placeholder={"Select integration"} + placeholder="Select integration" value={selectedIntegration?.dropdownIndex} - label={"Integration"} + label="Integration" wrapperClassName={`${baseClass}__form-field ${baseClass}__form-field--frequency`} helpText="For each new vulnerability detected, Fleet will create a ticket with a list of the affected hosts." /> @@ -396,15 +396,15 @@ const ManageAutomationsModal = ({
diff --git a/frontend/pages/SoftwarePage/components/PreviewPayloadModal/PreviewPayloadModal.tsx b/frontend/pages/SoftwarePage/components/PreviewPayloadModal/PreviewPayloadModal.tsx index eeb5a965373d..b1e0b168a430 100644 --- a/frontend/pages/SoftwarePage/components/PreviewPayloadModal/PreviewPayloadModal.tsx +++ b/frontend/pages/SoftwarePage/components/PreviewPayloadModal/PreviewPayloadModal.tsx @@ -69,7 +69,7 @@ const PreviewPayloadModal = ({ return ( Top section +software/versions/:id > Top section +software/os/:id > Top section +*/ + import React from "react"; import { QueryParams } from "utilities/url"; import ViewAllHostsLink from "components/ViewAllHostsLink"; +import DataSet from "components/DataSet"; 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 { title: string; type?: string; diff --git a/frontend/pages/SoftwarePage/components/SoftwareDetailsSummary/_styles.scss b/frontend/pages/SoftwarePage/components/SoftwareDetailsSummary/_styles.scss index 2da1c3e5bcd8..3a273a704621 100644 --- a/frontend/pages/SoftwarePage/components/SoftwareDetailsSummary/_styles.scss +++ b/frontend/pages/SoftwarePage/components/SoftwareDetailsSummary/_styles.scss @@ -29,12 +29,4 @@ display: flex; gap: $pad-xxlarge; } - - &__data-set { - font-size: $x-small; - - dt { - font-weight: $bold; - } - } } diff --git a/frontend/pages/SoftwarePage/components/SoftwareVulnerabilitiesTable/SoftwareVulnerabilitiesTable.tsx b/frontend/pages/SoftwarePage/components/SoftwareVulnerabilitiesTable/SoftwareVulnerabilitiesTable.tsx index 9161807f5ca4..bf7ac3e620c0 100644 --- a/frontend/pages/SoftwarePage/components/SoftwareVulnerabilitiesTable/SoftwareVulnerabilitiesTable.tsx +++ b/frontend/pages/SoftwarePage/components/SoftwareVulnerabilitiesTable/SoftwareVulnerabilitiesTable.tsx @@ -1,9 +1,18 @@ +/** +software/versions/:id > Vulnerabilities table +software/os/:id > Vulnerabilities table +*/ + import React, { useContext, useMemo } from "react"; import classnames from "classnames"; +import { InjectedRouter } from "react-router"; +import { Row } from "react-table"; +import PATHS from "router/paths"; import { AppContext } from "context/app"; import { ISoftwareVulnerability } from "interfaces/software"; import { GITHUB_NEW_ISSUE_LINK } from "utilities/constants"; +import { buildQueryStringFromParams } from "utilities/url"; import TableContainer from "components/TableContainer"; import EmptyTable from "components/EmptyTable"; @@ -41,6 +50,14 @@ interface ISoftwareVulnerabilitiesTableProps { itemName: string; isLoading: boolean; className?: string; + router: InjectedRouter; + teamIdForApi?: number; +} + +interface IRowProps extends Row { + original: { + cve?: string; + }; } const SoftwareVulnerabilitiesTable = ({ @@ -48,13 +65,36 @@ const SoftwareVulnerabilitiesTable = ({ itemName, isLoading, className, + router, + teamIdForApi, }: ISoftwareVulnerabilitiesTableProps) => { const { isPremiumTier, isSandboxMode } = useContext(AppContext); const classNames = classnames(baseClass, className); + const handleRowSelect = (row: IRowProps) => { + const hostsBySoftwareParams = { + cve: row.original.cve, + team_id: teamIdForApi, + }; + + const path = hostsBySoftwareParams + ? `${PATHS.MANAGE_HOSTS}?${buildQueryStringFromParams( + hostsBySoftwareParams + )}` + : PATHS.MANAGE_HOSTS; + + router.push(path); + }; + const tableHeaders = useMemo( - () => generateTableConfig(Boolean(isPremiumTier), Boolean(isSandboxMode)), + () => + generateTableConfig( + Boolean(isPremiumTier), + Boolean(isSandboxMode), + router, + teamIdForApi + ), [isPremiumTier, isSandboxMode] ); return ( @@ -62,7 +102,7 @@ const SoftwareVulnerabilitiesTable = ({ } isAllPagesSelected={false} @@ -71,6 +111,8 @@ const SoftwareVulnerabilitiesTable = ({ pageSize={20} resultsTitle="vulnerabilities" showMarkAllPages={false} + disableMultiRowSelect + onSelectSingleRow={handleRowSelect} disableTableHeader={data.length === 0} />
diff --git a/frontend/pages/SoftwarePage/components/SoftwareVulnerabilitiesTable/SoftwareVulnerabilitiesTableConfig.tsx b/frontend/pages/SoftwarePage/components/SoftwareVulnerabilitiesTable/SoftwareVulnerabilitiesTableConfig.tsx index 2945f2ed189d..dc01ad8ceb5d 100644 --- a/frontend/pages/SoftwarePage/components/SoftwareVulnerabilitiesTable/SoftwareVulnerabilitiesTableConfig.tsx +++ b/frontend/pages/SoftwarePage/components/SoftwareVulnerabilitiesTable/SoftwareVulnerabilitiesTableConfig.tsx @@ -1,15 +1,19 @@ import React from "react"; +import { InjectedRouter } from "react-router"; -import { formatFloatAsPercentage } from "utilities/helpers"; -import { DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants"; +import { formatSeverity } from "utilities/helpers"; +import { buildQueryStringFromParams } from "utilities/url"; import { ISoftwareVulnerability } from "interfaces/software"; +import paths from "router/paths"; import HeaderCell from "components/TableContainer/DataTable/HeaderCell/HeaderCell"; import TextCell from "components/TableContainer/DataTable/TextCell"; import TooltipWrapper from "components/TooltipWrapper"; -import CustomLink from "components/CustomLink"; import { HumanTimeDiffWithDateTip } from "components/HumanTimeDiffWithDateTip"; import PremiumFeatureIconWithTooltip from "components/PremiumFeatureIconWithTooltip"; +import ProbabilityOfExploit from "components/ProbabilityOfExploit/ProbabilityOfExploit"; +import ViewAllHostsLink from "components/ViewAllHostsLink"; +import LinkCell from "components/TableContainer/DataTable/LinkCell"; interface IHeaderProps { column: { @@ -43,28 +47,11 @@ interface IDataColumn { sortType?: string; } -const formatSeverity = (float?: number | null) => { - if (float === null || float === undefined) { - return DEFAULT_EMPTY_CELL_VALUE; - } - - let severity = ""; - if (float < 4.0) { - severity = "Low"; - } else if (float < 7.0) { - severity = "Medium"; - } else if (float < 9.0) { - severity = "High"; - } else if (float <= 10.0) { - severity = "Critical"; - } - - return `${severity} (${float.toFixed(1)})`; -}; - const generateTableConfig = ( isPremiumTier: boolean, - isSandboxMode: boolean + isSandboxMode: boolean, + router: InjectedRouter, + teamId?: number ): IDataColumn[] => { const tableHeaders: IDataColumn[] = [ { @@ -72,51 +59,31 @@ const generateTableConfig = ( accessor: "cve", disableSortBy: true, Header: "Vulnerability", - Cell: ({ cell: { value }, row }: ITextCellProps) => { - return ( - - ); - }, - }, - ]; + Cell: ({ cell: { value } }: ITextCellProps) => { + const cveName = value.toString(); + const teamQueryParam = buildQueryStringFromParams({ + team_id: teamId, + }); + + const softwareVulnerabilityDetailsPath = `${paths.SOFTWARE_VULNERABILITY_DETAILS( + cveName + )}?${teamQueryParam}`; + + const onClickCVE = (e: React.MouseEvent) => { + // Allows for button to be clickable in a clickable row + e.stopPropagation(); + + router?.push(softwareVulnerabilityDetailsPath); + }; - const premiumHeaders: IDataColumn[] = [ - { - title: "Probability of exploit", - accessor: "epss_probability", - disableSortBy: false, - Header: (headerProps: IHeaderProps): JSX.Element => { - const titleWithToolTip = ( - - The probability that this vulnerability will be exploited in the - next 30 days (EPSS probability). -
- This data is reported by FIRST.org. - - } - > - Probability of exploit -
- ); return ( - <> - - {isSandboxMode && } - + ); }, - Cell: ({ cell: { value } }: ITextCellProps): JSX.Element => ( - - ), }, { title: "Severity", @@ -129,9 +96,6 @@ const generateTableConfig = ( <> The worst case impact across different environments (CVSS base score). -
- This data is reported by the National Vulnerability Database - (NVD). } > @@ -153,22 +117,22 @@ const generateTableConfig = ( ), }, { - title: "Known exploit", - accessor: "cisa_known_exploit", + title: "Probability of exploit", + accessor: "epss_probability", disableSortBy: false, - sortType: "boolean", Header: (headerProps: IHeaderProps): JSX.Element => { const titleWithToolTip = ( - The vulnerability has been actively exploited in the wild. This - data is reported by the Cybersecurity and Infrustructure - Security Agency (CISA). + The probability that this vulnerability will be exploited in the + next 30 days (EPSS probability).
+ This data is reported by FIRST.org. } > - Known exploit + Probability of exploit
); return ( @@ -181,8 +145,11 @@ const generateTableConfig = ( ); }, - Cell: ({ cell: { value } }: ITextCellProps): JSX.Element => ( - + Cell: (cellProps: ICellProps): JSX.Element => ( + ), }, { @@ -222,9 +189,65 @@ const generateTableConfig = ( ); }, }, + { + title: "Detected", + accessor: "created_at", + disableSortBy: false, + Header: (headerProps: IHeaderProps): JSX.Element => { + return ( + <> + + {isSandboxMode && } + + ); + }, + Cell: (cellProps: ICellProps): JSX.Element => { + const createdAt = cellProps.row.original.created_at || ""; + + return ( + + ); + }, + }, + { + title: "", + Header: "", + accessor: "linkToFilteredHosts", + disableSortBy: true, + Cell: (cellProps: ICellProps) => { + return ( + <> + {cellProps.row.original && ( + + )} + + ); + }, + }, ]; - return isPremiumTier ? tableHeaders.concat(premiumHeaders) : tableHeaders; + if (!isPremiumTier) { + return tableHeaders.filter( + (header) => + header.accessor !== "epss_probability" && + header.accessor !== "cve_published" + ); + } + + return tableHeaders; }; export default generateTableConfig; diff --git a/frontend/pages/SoftwarePage/components/SoftwareVulnerabilitiesTable/_styles.scss b/frontend/pages/SoftwarePage/components/SoftwareVulnerabilitiesTable/_styles.scss index c1340c305ba6..736bb51064a6 100644 --- a/frontend/pages/SoftwarePage/components/SoftwareVulnerabilitiesTable/_styles.scss +++ b/frontend/pages/SoftwarePage/components/SoftwareVulnerabilitiesTable/_styles.scss @@ -4,10 +4,10 @@ &__wrapper { overflow-x: auto; } - } - .empty-table__container { - margin: 1.5rem auto; + th:nth-last-child(2) { + border-right: 0; + } } // used to position header text with premium icon correctly @@ -15,4 +15,21 @@ display: flex; gap: $pad-small; } + + @media (max-width: $break-md) { + .linkToFilteredHosts__header { + width: 40px; + } + .view-all-hosts-link { + span { + display: none; + } + } + .epss_probability__cell, + .epss_probability__header, + .cve_published__cell, + .cve_published__header { + display: none; + } + } } diff --git a/frontend/pages/UserSettingsPage/UserSidePanel/index.ts b/frontend/pages/UserSettingsPage/UserSidePanel/index.ts deleted file mode 100644 index 9ed26379543d..000000000000 --- a/frontend/pages/UserSettingsPage/UserSidePanel/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./UserSidePanel"; diff --git a/frontend/pages/UserSettingsPage/index.ts b/frontend/pages/UserSettingsPage/index.ts deleted file mode 100644 index 2da89e3b600f..000000000000 --- a/frontend/pages/UserSettingsPage/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./UserSettingsPage"; diff --git a/frontend/pages/admin/IntegrationsPage/cards/AutomaticEnrollment/AutomaticEnrollment.tsx b/frontend/pages/admin/IntegrationsPage/cards/AutomaticEnrollment/AutomaticEnrollment.tsx index cf6151881471..3a6bc79db808 100644 --- a/frontend/pages/admin/IntegrationsPage/cards/AutomaticEnrollment/AutomaticEnrollment.tsx +++ b/frontend/pages/admin/IntegrationsPage/cards/AutomaticEnrollment/AutomaticEnrollment.tsx @@ -43,7 +43,11 @@ const AutomaticEnrollment = ({ router }: IAutomaticEnrollment) => { if (!isPremiumTier) return ; if (isLoadingMdmApple) { - return ; + return ( +
+ +
+ ); } if (errorMdmApple?.status === 404) { diff --git a/frontend/pages/admin/IntegrationsPage/cards/AutomaticEnrollment/components/EulaSection/components/DeleteEulaModal/DeleteEulaModal.tsx b/frontend/pages/admin/IntegrationsPage/cards/AutomaticEnrollment/components/EulaSection/components/DeleteEulaModal/DeleteEulaModal.tsx index f2e5fe5baba5..1c41df40d0eb 100644 --- a/frontend/pages/admin/IntegrationsPage/cards/AutomaticEnrollment/components/EulaSection/components/DeleteEulaModal/DeleteEulaModal.tsx +++ b/frontend/pages/admin/IntegrationsPage/cards/AutomaticEnrollment/components/EulaSection/components/DeleteEulaModal/DeleteEulaModal.tsx @@ -14,7 +14,7 @@ const DeleteEulaModal = ({ onDelete, onCancel }: IDeleteEulaModalProps) => { return ( onDelete()} > diff --git a/frontend/pages/admin/IntegrationsPage/cards/AutomaticEnrollment/components/EulaSection/components/EulaListItem/EulaListItem.tsx b/frontend/pages/admin/IntegrationsPage/cards/AutomaticEnrollment/components/EulaSection/components/EulaListItem/EulaListItem.tsx index d63a89a39419..b7ab2ab018f9 100644 --- a/frontend/pages/admin/IntegrationsPage/cards/AutomaticEnrollment/components/EulaSection/components/EulaListItem/EulaListItem.tsx +++ b/frontend/pages/admin/IntegrationsPage/cards/AutomaticEnrollment/components/EulaSection/components/EulaListItem/EulaListItem.tsx @@ -48,7 +48,7 @@ const EulaListItem = ({ eulaData, onDelete }: IEulaListItemProps) => { name="external-link" size="medium" className={`${baseClass}__external-icon`} - color={"ui-fleet-black-75"} + color="ui-fleet-black-75" /> + +
+ + + ); +}; + +export default RenameTeamModal; diff --git a/frontend/pages/admin/IntegrationsPage/cards/AutomaticEnrollment/components/RenameTeamModal/index.ts b/frontend/pages/admin/IntegrationsPage/cards/AutomaticEnrollment/components/RenameTeamModal/index.ts new file mode 100644 index 000000000000..89752243c84e --- /dev/null +++ b/frontend/pages/admin/IntegrationsPage/cards/AutomaticEnrollment/components/RenameTeamModal/index.ts @@ -0,0 +1 @@ +export { default } from "./RenameTeamModal"; diff --git a/frontend/pages/admin/IntegrationsPage/cards/Integrations/Integrations.tsx b/frontend/pages/admin/IntegrationsPage/cards/Integrations/Integrations.tsx index cf232034e9e0..75f95f836bdb 100644 --- a/frontend/pages/admin/IntegrationsPage/cards/Integrations/Integrations.tsx +++ b/frontend/pages/admin/IntegrationsPage/cards/Integrations/Integrations.tsx @@ -382,8 +382,8 @@ const Integrations = (): JSX.Element => { columnConfigs={tableHeaders} data={tableData} isLoading={isLoadingIntegrations} - defaultSortHeader={"name"} - defaultSortDirection={"asc"} + defaultSortHeader="name" + defaultSortDirection="asc" actionButton={{ name: "add integration", buttonText: "Add integration", @@ -391,7 +391,7 @@ const Integrations = (): JSX.Element => { onActionButtonClick: toggleAddIntegrationModal, hideButton: !tableData?.length, }} - resultsTitle={"integrations"} + resultsTitle="integrations" emptyComponent={() => ( { return ( -
+
integration-icon actionSelectHandler(value, cellProps.row.original) } - placeholder={"Actions"} + placeholder="Actions" /> ), }, diff --git a/frontend/pages/admin/IntegrationsPage/cards/Integrations/components/AddIntegrationModal/AddIntegrationModal.tsx b/frontend/pages/admin/IntegrationsPage/cards/Integrations/components/AddIntegrationModal/AddIntegrationModal.tsx index 4f98ba7da62b..0dc4dc630ad4 100644 --- a/frontend/pages/admin/IntegrationsPage/cards/Integrations/components/AddIntegrationModal/AddIntegrationModal.tsx +++ b/frontend/pages/admin/IntegrationsPage/cards/Integrations/components/AddIntegrationModal/AddIntegrationModal.tsx @@ -47,7 +47,7 @@ const AddIntegrationModal = ({ }, [backendValidators]); return ( - +
{!testingConnection && ( <> diff --git a/frontend/pages/admin/IntegrationsPage/cards/Integrations/components/DeleteIntegrationModal/DeleteIntegrationModal.tsx b/frontend/pages/admin/IntegrationsPage/cards/Integrations/components/DeleteIntegrationModal/DeleteIntegrationModal.tsx index 9d230e813874..7fce0cb4657b 100644 --- a/frontend/pages/admin/IntegrationsPage/cards/Integrations/components/DeleteIntegrationModal/DeleteIntegrationModal.tsx +++ b/frontend/pages/admin/IntegrationsPage/cards/Integrations/components/DeleteIntegrationModal/DeleteIntegrationModal.tsx @@ -22,7 +22,7 @@ const DeleteIntegrationModal = ({ }: IDeleteIntegrationModalProps): JSX.Element => { return ( + {testingConnection ? (
Testing connection diff --git a/frontend/pages/admin/IntegrationsPage/cards/Integrations/components/IntegrationForm/IntegrationForm.tsx b/frontend/pages/admin/IntegrationsPage/cards/Integrations/components/IntegrationForm/IntegrationForm.tsx index a62cd86c8125..0cce5bb5a19e 100644 --- a/frontend/pages/admin/IntegrationsPage/cards/Integrations/components/IntegrationForm/IntegrationForm.tsx +++ b/frontend/pages/admin/IntegrationsPage/cards/Integrations/components/IntegrationForm/IntegrationForm.tsx @@ -275,7 +275,7 @@ const IntegrationForm = ({ formData.apiToken === "" || formData.groupId === 0) } - className={"tooltip"} + className="tooltip" > -
diff --git a/frontend/pages/hosts/ManageHostsPage/components/FilterPill/FilterPill.tsx b/frontend/pages/hosts/ManageHostsPage/components/FilterPill/FilterPill.tsx index 65d304a809d8..f44e71b6ca68 100644 --- a/frontend/pages/hosts/ManageHostsPage/components/FilterPill/FilterPill.tsx +++ b/frontend/pages/hosts/ManageHostsPage/components/FilterPill/FilterPill.tsx @@ -62,7 +62,7 @@ const FilterPill = ({ diff --git a/frontend/pages/hosts/details/cards/Policies/Policies.tsx b/frontend/pages/hosts/details/cards/Policies/Policies.tsx index 8d28a21eb754..a750a6db68dc 100644 --- a/frontend/pages/hosts/details/cards/Policies/Policies.tsx +++ b/frontend/pages/hosts/details/cards/Policies/Policies.tsx @@ -68,7 +68,7 @@ const Policies = ({ data={generatePolicyDataSet(policies)} isLoading={isLoading} manualSortBy - resultsTitle={"policy items"} + resultsTitle="policy items" emptyComponent={() => <>} showMarkAllPages={false} isAllPagesSelected={false} diff --git a/frontend/pages/hosts/details/cards/Queries/HostQueries.tsx b/frontend/pages/hosts/details/cards/Queries/HostQueries.tsx index 6e1ebee7a950..dc9030215e86 100644 --- a/frontend/pages/hosts/details/cards/Queries/HostQueries.tsx +++ b/frontend/pages/hosts/details/cards/Queries/HostQueries.tsx @@ -107,7 +107,7 @@ const HostQueries = ({ disableCount disableMultiRowSelect isLoading={false} // loading state handled at parent level - {...{ onSelectSingleRow }} + onSelectSingleRow={onSelectSingleRow} />
)} diff --git a/frontend/pages/hosts/details/cards/Software/Software.tsx b/frontend/pages/hosts/details/cards/Software/Software.tsx index 00aae4e5ab62..55fd2ef2c5af 100644 --- a/frontend/pages/hosts/details/cards/Software/Software.tsx +++ b/frontend/pages/hosts/details/cards/Software/Software.tsx @@ -163,14 +163,14 @@ const SoftwareTable = ({ [deviceUser, router, pathname] ); - const handleVulnFilterDropdownChange = (isFilterVulnerable: string) => { + const handleVulnFilterDropdownChange = (isFilterVulnerable: boolean) => { const nextPath = getNextLocationPath({ pathPrefix, routeTemplate, queryParams: { ...queryParams, page: 0, - vulnerable: isFilterVulnerable, + vulnerable: isFilterVulnerable.toString(), }, }); router?.replace(nextPath); diff --git a/frontend/pages/hosts/details/cards/Users/Users.tsx b/frontend/pages/hosts/details/cards/Users/Users.tsx index ad54c1c4b588..a5d85de51edf 100644 --- a/frontend/pages/hosts/details/cards/Users/Users.tsx +++ b/frontend/pages/hosts/details/cards/Users/Users.tsx @@ -54,11 +54,11 @@ const Users = ({ columnConfigs={tableHeaders} data={usersState} isLoading={isLoading} - defaultSortHeader={"username"} - defaultSortDirection={"asc"} - inputPlaceHolder={"Search users by username"} + defaultSortHeader="username" + defaultSortDirection="asc" + inputPlaceHolder="Search users by username" onQueryChange={onUsersTableSearchChange} - resultsTitle={"users"} + resultsTitle="users" emptyComponent={() => ( { return ( ( [ { @@ -303,7 +304,11 @@ const ManagePolicyPage = ({ } ); - const { data: teamPoliciesCount, isFetching: isFetchingTeamCount } = useQuery< + const { + data: teamPoliciesCount, + isFetching: isFetchingTeamCount, + refetch: refetchTeamPoliciesCount, + } = useQuery< IPoliciesCountResponse, Error, number, @@ -364,8 +369,10 @@ const ManagePolicyPage = ({ const refetchPolicies = (teamId?: number) => { if (teamId) { refetchTeamPolicies(); + refetchTeamPoliciesCount(); } else { refetchGlobalPolicies(); // Only call on global policies as this is expensive + refetchGlobalPoliciesCount(); } }; @@ -567,8 +574,6 @@ const ManagePolicyPage = ({ }`; }; - const showTeamDescription = isPremiumTier && isAnyTeamSelected; - const showInheritedPoliciesButton = isAnyTeamSelected && !isFetchingTeamPolicies && @@ -737,17 +742,11 @@ const ManagePolicyPage = ({ )}
- {showTeamDescription ? ( -

- Add additional policies for all hosts assigned to this team - . -

- ) : ( -

- Add policies for all of your hosts to see which pass your - organization’s standards. -

- )} +

+ {isAnyTeamSelected + ? "Detect device health issues for all hosts assigned to this team." + : "Detect device health issues for all hosts."} +

{renderMainTable()} {showInheritedPoliciesButton && globalPoliciesCount && ( @@ -762,7 +761,7 @@ const ManagePolicyPage = ({ showInheritedTable, globalPoliciesCount )} - caretPosition={"before"} + caretPosition="before" tooltipContent={ <> "All teams" policies are checked diff --git a/frontend/pages/policies/ManagePoliciesPage/_styles.scss b/frontend/pages/policies/ManagePoliciesPage/_styles.scss index 6ce8cc0fed0b..62c29c1d5b65 100644 --- a/frontend/pages/policies/ManagePoliciesPage/_styles.scss +++ b/frontend/pages/policies/ManagePoliciesPage/_styles.scss @@ -51,13 +51,6 @@ margin: 0; margin-bottom: $pad-xxlarge; - h2 { - text-transform: uppercase; - color: $core-fleet-black; - font-weight: $regular; - font-size: $small; - } - p { color: $ui-fleet-black-75; margin: 0; diff --git a/frontend/pages/policies/ManagePoliciesPage/components/DeletePolicyModal/DeletePolicyModal.tsx b/frontend/pages/policies/ManagePoliciesPage/components/DeletePolicyModal/DeletePolicyModal.tsx index 1600519a8d93..37ac1081ed3e 100644 --- a/frontend/pages/policies/ManagePoliciesPage/components/DeletePolicyModal/DeletePolicyModal.tsx +++ b/frontend/pages/policies/ManagePoliciesPage/components/DeletePolicyModal/DeletePolicyModal.tsx @@ -18,7 +18,7 @@ const DeletePolicyModal = ({ }: IDeletePolicyModalProps): JSX.Element => { return (
diff --git a/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTable.tsx b/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTable.tsx index 9c089a936e03..9494e498054f 100644 --- a/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTable.tsx +++ b/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTable.tsx @@ -75,37 +75,13 @@ const PoliciesTable = ({ const emptyState = () => { const emptyPolicies: IEmptyTableProps = { graphicName: "empty-policies", - header: ( - <> - Ask yes or no questions about{" "} - all your hosts - - ), + header: <>You don't have any policies, info: ( <> - - Verify whether or not your hosts have security features turned on. -
- Track your efforts to keep installed software up to date on - your hosts. -
- Provide owners with a list of hosts that still need changes. + Add policies to detect device health issues and trigger automations. ), }; - - if (currentTeam) { - emptyPolicies.header = ( - <> - Ask yes or no questions about hosts assigned to{" "} - - {currentTeam.name} - - - ); - } if (canAddOrDeletePolicy) { emptyPolicies.primaryButton = ( ); } diff --git a/frontend/pages/policies/ManagePoliciesPage/components/PreviewPayloadModal/PreviewPayloadModal.tsx b/frontend/pages/policies/ManagePoliciesPage/components/PreviewPayloadModal/PreviewPayloadModal.tsx index f8c67d658b5a..b3cf3a8225ae 100644 --- a/frontend/pages/policies/ManagePoliciesPage/components/PreviewPayloadModal/PreviewPayloadModal.tsx +++ b/frontend/pages/policies/ManagePoliciesPage/components/PreviewPayloadModal/PreviewPayloadModal.tsx @@ -64,7 +64,7 @@ const PreviewPayloadModal = ({ return ( +
{canEditQuery && (
); diff --git a/frontend/pages/queries/details/QueryDetailsPage/_styles.scss b/frontend/pages/queries/details/QueryDetailsPage/_styles.scss index ec283c315175..0eae371af051 100644 --- a/frontend/pages/queries/details/QueryDetailsPage/_styles.scss +++ b/frontend/pages/queries/details/QueryDetailsPage/_styles.scss @@ -8,6 +8,7 @@ display: flex; justify-content: space-between; margin-top: $pad-small; + gap: $pad-medium; .name-description { display: flex; @@ -19,7 +20,7 @@ &__action-button-container { display: flex; justify-content: flex-end; - min-width: 266px; + min-width: max-content; gap: $pad-medium; } diff --git a/frontend/pages/queries/details/components/NoResults/NoResults.tsx b/frontend/pages/queries/details/components/NoResults/NoResults.tsx index 23b38e49388d..5a35a33b56a9 100644 --- a/frontend/pages/queries/details/components/NoResults/NoResults.tsx +++ b/frontend/pages/queries/details/components/NoResults/NoResults.tsx @@ -57,7 +57,7 @@ const NoResults = ({ return ( ); diff --git a/frontend/pages/queries/details/components/QueryReport/QueryReport.tsx b/frontend/pages/queries/details/components/QueryReport/QueryReport.tsx index e7e7c1807eed..2c78ef99b47d 100644 --- a/frontend/pages/queries/details/components/QueryReport/QueryReport.tsx +++ b/frontend/pages/queries/details/components/QueryReport/QueryReport.tsx @@ -13,7 +13,6 @@ import { IQueryReport, IQueryReportResultRow } from "interfaces/query_report"; import Button from "components/buttons/Button"; import Icon from "components/Icon/Icon"; import TableContainer from "components/TableContainer"; -import ShowQueryModal from "components/modals/ShowQueryModal"; import TooltipWrapper from "components/TooltipWrapper"; import EmptyTable from "components/EmptyTable"; @@ -46,7 +45,6 @@ const QueryReport = ({ }: IQueryReportProps): JSX.Element => { const { lastEditedQueryName, lastEditedQueryBody } = useContext(QueryContext); - const [showQueryModal, setShowQueryModal] = useState(false); const [filteredResults, setFilteredResults] = useState( flattenResults(queryReport?.results || []) ); @@ -77,22 +75,9 @@ const QueryReport = ({ ); }; - const onShowQueryModal = () => { - setShowQueryModal(!showQueryModal); - }; - const renderTableButtons = () => { return (
-