diff --git a/.github/workflows/check-automated-doc.yml b/.github/workflows/check-automated-doc.yml index d289c55318de..ae07734cbc26 100644 --- a/.github/workflows/check-automated-doc.yml +++ b/.github/workflows/check-automated-doc.yml @@ -38,8 +38,6 @@ jobs: - name: Checkout Code uses: actions/checkout@629c2de402a417ea7690ca6ce3f33229e27606a5 # v2 - with: - fetch-depth: 0 - name: Install Go uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 @@ -51,7 +49,8 @@ jobs: make generate-doc if [[ $(git diff) ]]; then echo "❌ fail: uncommited changes" - echo "please run `make generate-doc` and commit the changes" + echo "please run 'make generate-doc' and commit the changes" + git --no-pager diff exit 1 fi @@ -62,6 +61,7 @@ jobs: ./node_modules/sails/bin/sails.js run generate-merged-schema if [[ $(git diff) ]]; then echo "❌ fail: uncommited changes" - echo "please run `cd website && npm install && ./node_modules/sails/bin/sails.js run generate-merged-schema` and commit the changes" + echo "please run 'cd website && npm install && ./node_modules/sails/bin/sails.js run generate-merged-schema' and commit the changes" + git --no-pager diff exit 1 fi diff --git a/.github/workflows/goreleaser-fleet.yaml b/.github/workflows/goreleaser-fleet.yaml index 4bb67abbb293..09f138d70850 100644 --- a/.github/workflows/goreleaser-fleet.yaml +++ b/.github/workflows/goreleaser-fleet.yaml @@ -106,8 +106,8 @@ jobs: continue-on-error: true id: image_digests run: | - echo "digest_fleet=$(cat ./dist/artifact.json | jq -r '.[]|select(.type == "Published Docker Image" and (.name | contains("fleetdm/fleet:${{ steps.commit.outputs.short_commit }}"))) | select(. != null)|.extra.Digest')" >> "$GITHUB_OUTPUT" - echo "digest_fleetctl=$(cat ./dist/artifact.json | jq -r '.[]|select(.type == "Published Docker Image" and (.name | contains("fleetdm/fleetctl:${{ steps.commit.outputs.short_commit }}"))) | select(. != null)|.extra.Digest')" >> "$GITHUB_OUTPUT" + echo "digest_fleet=$(cat ./dist/artifacts.json | jq -r '.[]|select(.type == "Published Docker Image" and (.name | contains("fleetdm/fleet:${{ steps.commit.outputs.short_commit }}"))) | select(. != null)|.extra.Digest')" >> "$GITHUB_OUTPUT" + echo "digest_fleetctl=$(cat ./dist/artifacts.json | jq -r '.[]|select(.type == "Published Docker Image" and (.name | contains("fleetdm/fleetctl:${{ steps.commit.outputs.short_commit }}"))) | select(. != null)|.extra.Digest')" >> "$GITHUB_OUTPUT" - name: Attest Fleet image uses: actions/attest-build-provenance@619dbb2e03e0189af0c55118e7d3c5e129e99726 # v2.0 diff --git a/.github/workflows/test-go.yaml b/.github/workflows/test-go.yaml index e347f7278217..1a65e08d65ad 100644 --- a/.github/workflows/test-go.yaml +++ b/.github/workflows/test-go.yaml @@ -42,7 +42,7 @@ jobs: test-go: strategy: matrix: - suite: ["integration", "core", "mysql", "fleetctl", "vuln"] + suite: ["integration-core", "integration-enterprise", "integration-mdm", "core", "mysql", "fleetctl", "vuln"] os: [ubuntu-latest] mysql: ["mysql:8.0.36", "mysql:8.4.3", "mysql:9.1.0"] # make sure to update supported versions docs when this changes isCron: @@ -120,9 +120,15 @@ jobs: if [[ "${{ matrix.suite }}" == "core" ]]; then CI_TEST_PKG=main RUN_TESTS_ARG='-skip=^TestIntegrations' - elif [[ "${{ matrix.suite }}" == "integration" ]]; then - CI_TEST_PKG=main - RUN_TESTS_ARG='-run=^TestIntegrations' + elif [[ "${{ matrix.suite }}" == "integration-core" ]]; then + CI_TEST_PKG=integration + RUN_TESTS_ARG='-run=^TestIntegrations -skip "^(TestIntegrationsMDM|TestIntegrationsEnterprise)"' + elif [[ "${{ matrix.suite }}" == "integration-mdm" ]]; then + CI_TEST_PKG=integration + RUN_TESTS_ARG='-run=^TestIntegrationsMDM' + elif [[ "${{ matrix.suite }}" == "integration-enterprise" ]]; then + CI_TEST_PKG=integration + RUN_TESTS_ARG='-run=^TestIntegrationsEnterprise' else CI_TEST_PKG="${{ matrix.suite }}" RUN_TESTS_ARG='' diff --git a/Makefile b/Makefile index 6ffdca38de79..23c7fc5c178c 100644 --- a/Makefile +++ b/Makefile @@ -158,6 +158,8 @@ dlv_test_pkg_to_test := $(addprefix github.com/fleetdm/fleet/v4/,$(PKG_TO_TEST)) DEFAULT_PKG_TO_TEST := ./cmd/... ./ee/... ./orbit/pkg/... ./orbit/cmd/orbit ./pkg/... ./server/... ./tools/... ifeq ($(CI_TEST_PKG), main) CI_PKG_TO_TEST=$(shell go list ${DEFAULT_PKG_TO_TEST} | grep -v "server/datastore/mysql" | grep -v "cmd/fleetctl" | grep -v "server/vulnerabilities" | sed -e 's|github.com/fleetdm/fleet/v4/||g') +else ifeq ($(CI_TEST_PKG), integration) + CI_PKG_TO_TEST="server/service" else ifeq ($(CI_TEST_PKG), mysql) CI_PKG_TO_TEST="server/datastore/mysql/..." else ifeq ($(CI_TEST_PKG), fleetctl) diff --git a/articles/deploy-software-packages.md b/articles/deploy-software-packages.md index f5483bb5197a..7d6a9ed9c007 100644 --- a/articles/deploy-software-packages.md +++ b/articles/deploy-software-packages.md @@ -30,8 +30,6 @@ Learn more about automatically installing software in a separate guide [here](ht * Select the hosts that you want to target with this software, under "Target". Select "All hosts" if you want the software to be available to all your hosts. Select "Custom" to scope the software to specific groups of hosts based on label membership. You can select "Include any", which will scope the software to hosts that have any of the labels you select, or "Exclude any", which will scope the software to hosts that do _not_ have the selected labels. -* Select the hosts that you want to target with this software, under "Target". Select "All hosts" if you want the software to be available to all your hosts. Select "Custom" to scope the software to specific groups of hosts based on label membership. You can select "Include any", which will scope the software to hosts that have any of the labels you select, or "Exclude any", which will scope the software to hosts that do _not_ have the selected labels. - * To allow users to install the software from Fleet Desktop, check the “Self-service” checkbox. * To customize installer behavior, click on “Advanced options.” diff --git a/articles/fleet-4.62.0.md b/articles/fleet-4.62.0.md index b82d33db17b2..24926d2d927f 100644 --- a/articles/fleet-4.62.0.md +++ b/articles/fleet-4.62.0.md @@ -22,7 +22,7 @@ Fleet now creates policies automatically when you add a custom package. This eli ### Hide secrets in configuration profiles and scripts -Fleet ensures that GitHub or GitLab secrets, like API tokens and license keys used in scripts (Shell & PowerShell) and configuration profiles (macOS & Windows), are hidden when viewed or downloaded in Fleet. This protects sensitive information, keeping it secure until it’s deployed to the hosts. Learn more about secrets [here](https://fleetdm.com/secret-variables). +Fleet ensures that GitHub or GitLab secrets, like API tokens and license keys used in scripts (Shell & PowerShell) and configuration profiles (macOS & Windows), are hidden when viewed or downloaded in Fleet. This protects sensitive information, keeping it secure until it’s deployed to the hosts. Learn more about secrets [here](https://fleetdm.com/guides/secret-variables). ## Changes diff --git a/changes/24038-agent-options-key-error b/changes/24038-agent-options-key-error new file mode 100644 index 000000000000..dd8235c59a9e --- /dev/null +++ b/changes/24038-agent-options-key-error @@ -0,0 +1 @@ +- Display the correct path for agent options when a key is placed in the wrong object diff --git a/changes/24618-make-email-logo-dark-mode-compatible b/changes/24618-make-email-logo-dark-mode-compatible new file mode 100644 index 000000000000..421e91d9b787 --- /dev/null +++ b/changes/24618-make-email-logo-dark-mode-compatible @@ -0,0 +1 @@ +* Use an email logo compatible with dark modes diff --git a/cmd/fleetctl/package.go b/cmd/fleetctl/package.go index 281cec25e9a5..271203390693 100644 --- a/cmd/fleetctl/package.go +++ b/cmd/fleetctl/package.go @@ -13,6 +13,7 @@ import ( eefleetctl "github.com/fleetdm/fleet/v4/ee/fleetctl" "github.com/fleetdm/fleet/v4/orbit/pkg/packaging" + "github.com/fleetdm/fleet/v4/orbit/pkg/update" "github.com/fleetdm/fleet/v4/pkg/filepath_windows" "github.com/fleetdm/fleet/v4/server/fleet" "github.com/rs/zerolog" @@ -127,7 +128,7 @@ func packageCommand() *cli.Command { &cli.StringFlag{ Name: "update-url", Usage: "URL for update server", - Value: "https://tuf.fleetctl.com", + Value: update.DefaultURL, Destination: &opt.UpdateURL, }, &cli.StringFlag{ diff --git a/cmd/fleetctl/preview.go b/cmd/fleetctl/preview.go index 4cbc5a5d6c4c..e4286d940dc0 100644 --- a/cmd/fleetctl/preview.go +++ b/cmd/fleetctl/preview.go @@ -762,7 +762,7 @@ func previewResetCommand() *cli.Command { return fmt.Errorf("Failed to stop orbit: %w", err) } - if err := os.RemoveAll(filepath.Join(orbitDir, "tuf-metadata.json")); err != nil { + if err := os.RemoveAll(filepath.Join(orbitDir, update.MetadataFileName)); err != nil { return fmt.Errorf("failed to remove preview update metadata file: %w", err) } if err := os.RemoveAll(filepath.Join(orbitDir, "bin")); err != nil { diff --git a/docs/Contributing/API-for-contributors.md b/docs/Contributing/API-for-contributors.md index 32a08b9190aa..8bfa61d60752 100644 --- a/docs/Contributing/API-for-contributors.md +++ b/docs/Contributing/API-for-contributors.md @@ -4179,7 +4179,7 @@ _Available in Fleet Premium._ ```json { "team_name": "Foobar", - "app_store_apps": { + "app_store_apps": [ { "app_store_id": "597799333", "self_service": false @@ -4188,7 +4188,7 @@ _Available in Fleet Premium._ "app_store_id": "497799835", "self_service": true, } - } + ] } ``` diff --git a/ee/server/service/teams.go b/ee/server/service/teams.go index eded9b4788ac..121351b2559a 100644 --- a/ee/server/service/teams.go +++ b/ee/server/service/teams.go @@ -414,6 +414,23 @@ func (svc *Service) ModifyTeamAgentOptions(ctx context.Context, teamID uint, tea if teamOptions != nil { if err := fleet.ValidateJSONAgentOptions(ctx, svc.ds, teamOptions, true); err != nil { + if field := fleet.GetJSONUnknownField(err); field != nil { + correctKeyPath, keyErr := fleet.FindAgentOptionsKeyPath(*field) + if keyErr != nil { + level.Error(svc.logger).Log("err", err, "msg", "error parsing generated agent options structs") + } + var keyPathJoined string + switch pathLen := len(correctKeyPath); { + case pathLen > 1: + keyPathJoined = fmt.Sprintf("%q", strings.Join(correctKeyPath[:len(correctKeyPath)-1], ".")) + case pathLen == 1: + keyPathJoined = "top level" + } + if keyPathJoined != "" { + err = fmt.Errorf("%q should be part of the %s object", *field, keyPathJoined) + } + } + err = fleet.NewUserMessageError(err, http.StatusBadRequest) if applyOptions.Force && !applyOptions.DryRun { level.Info(svc.logger).Log("err", err, "msg", "force-apply team agent options with validation errors") diff --git a/frontend/components/TableContainer/TableContainer.tsx b/frontend/components/TableContainer/TableContainer.tsx index db1a2196276f..648c6d7d47c6 100644 --- a/frontend/components/TableContainer/TableContainer.tsx +++ b/frontend/components/TableContainer/TableContainer.tsx @@ -83,7 +83,7 @@ interface ITableContainerProps { onQueryChange?: | ((queryData: ITableQueryData) => void) | ((queryData: ITableQueryData) => number); - customControl?: () => JSX.Element; + customControl?: () => JSX.Element | null; /** Filter button right of the search rendering alternative responsive design where search bar moves to new line but filter button remains inline with other table headers */ customFiltersButton?: () => JSX.Element; stackControls?: boolean; @@ -288,11 +288,11 @@ const TableContainer = ({ const opacity = isLoading ? { opacity: 0.4 } : { opacity: 1 }; // New preferred pattern uses grid container/box to allow for more dynamic responsiveness - // At low widths, search bar (3rd div of 4) moves above other 3 divs - if (customFiltersButton) { + // At low widths, right header stacks on top of left header + if (stackControls) { return (
-
+
{renderCount && !disableCount && (
({
)}
-
- {actionButton && !actionButton.hideButton && ( + + {actionButton && !actionButton.hideButton && ( +
- )} +
+ )} +
{customControl && customControl()} -
-
{searchable && !wideSearch && (
({
)} + {customFiltersButton && customFiltersButton()}
-
{customFiltersButton()}
); } diff --git a/frontend/components/TableContainer/_styles.scss b/frontend/components/TableContainer/_styles.scss index f15fdf5824eb..13805e126385 100644 --- a/frontend/components/TableContainer/_styles.scss +++ b/frontend/components/TableContainer/_styles.scss @@ -6,14 +6,14 @@ // Container is responsive design used when customFilters is rendered .container { display: grid; - grid-template-columns: 1fr auto auto; /* First column takes all remaining space */ - grid-template-rows: auto auto; /* Two rows */ + grid-template-columns: 1fr auto; /* First column takes all remaining space */ + grid-template-rows: auto auto; /* Two rows for smaller screens*/ width: 100%; height: max-content; gap: $pad-small $pad-medium; } - .box { + .stackable-header { min-width: max-content; align-content: center; display: flex; @@ -24,56 +24,46 @@ display: flex; flex-direction: row; } + + // only if in stackable header + .table-container__search { + width: 100%; + } } - .search { + .top-shift-header { grid-column: 1 / -1; /* Span across all columns */ grid-row: 1; /* Place in the first row */ - } - .box:nth-child(1) { - grid-column: 1 / span 2; /* Make Box 1 expand across two columns */ - grid-row: 2; + .Select-multi-value-wrapper { + height: 36px; // Fixes height issues + width: 236px; + } } - .box:nth-child(2) { - grid-column: 2; /* Place Box 2 in the second row, second column */ + .stackable-header:nth-child(1) { + grid-column: 1 / span 2; /* Make Header 1 expand across two columns */ grid-row: 2; - } - .box:nth-child(4) { - grid-column: 3; /* Place Box 4 in the second row, third column */ - grid-row: 2; - max-width: min-content; + .form-field--dropdown { + width: 235px; + } } /* Media query for larger screens */ - @media (min-width: $table-controls-break) { + @media (min-width: $break-md) { .container { - grid-template-columns: 1fr auto auto auto; /* First column takes all remaining space */ + grid-template-columns: 1fr auto; /* First column takes all remaining space */ grid-template-rows: auto; /* Single row */ } - .search { - grid-column: 1 / -1; /* Keep spanning across all columns if needed */ - grid-row: auto; + .top-shift-header { + grid-column: 2; /* Single row */ } - .box:nth-child(1) { - grid-column: 1; /* Ensure Box 1 stays in the first column */ - } - - .box:nth-child(2) { - grid-column: 2; /* Place Box 2 in the second column */ - } - - .box:nth-child(3) { - grid-column: 3; /* Place Box 3 in the third column */ - grid-row: 2; - } - - .box:nth-child(4) { - grid-column: 4; /* Place Box 4 in the fourth column */ + .stackable-header:nth-child(1) { + grid-column: 1; /* Ensure Header 1 stays in the first column */ + grid-row: 1; /* Single row */ } } @@ -138,10 +128,6 @@ justify-content: space-between; } } - - .Select-multi-value-wrapper { - height: 38px; // Fixes overlap with .Select outline - } } &__results-count { diff --git a/frontend/components/forms/fields/DropdownWrapper/DropdownWrapper.stories.tsx b/frontend/components/forms/fields/DropdownWrapper/DropdownWrapper.stories.tsx index ee0d223c58ca..4a869b7ca521 100644 --- a/frontend/components/forms/fields/DropdownWrapper/DropdownWrapper.stories.tsx +++ b/frontend/components/forms/fields/DropdownWrapper/DropdownWrapper.stories.tsx @@ -1,62 +1,81 @@ // stories/DropdownWrapper.stories.tsx import React from "react"; -import { Meta, Story } from "@storybook/react"; -import DropdownWrapper, { - IDropdownWrapper, - CustomOptionType, -} from "./DropdownWrapper"; +import type { Meta, StoryObj } from "@storybook/react"; +import DropdownWrapper, { CustomOptionType } from "./DropdownWrapper"; // Define metadata for the story -export default { +const meta: Meta = { title: "Components/DropdownWrapper", component: DropdownWrapper, argTypes: { onChange: { action: "changed" }, }, -} as Meta; + // Padding added to view tooltips + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; -// Define a template for the stories -const Template: Story = (args) => ( - -); +type Story = StoryObj; // Sample options to be used in the dropdown const sampleOptions: CustomOptionType[] = [ - { label: "Option 1", value: "option1", helpText: "Help text for option 1" }, { - label: "Option 2", + label: "Option 1 - just help text", + value: "option1", + helpText: "Help text for option 1", + }, + { + label: "Option 2 - just tooltip", value: "option2", tooltipContent: "Tooltip for option 2", }, - { label: "Option 3", value: "option3", isDisabled: true }, + { label: "Option 3 - just disabled", value: "option3", isDisabled: true }, + { + label: "Option 4 - help text, disabled, and tooltip", + value: "option4", + helpText: "Help text for option 4", + isDisabled: true, + tooltipContent: "Tooltip for option 4", + }, ]; // Default story -export const Default = Template.bind({}); -Default.args = { - options: sampleOptions, - name: "dropdown-example", - label: "Select an option", +export const Default: Story = { + args: { + options: sampleOptions, + name: "dropdown-example", + label: "Select an option", + }, }; // Disabled story -export const Disabled = Template.bind({}); -Disabled.args = { - ...Default.args, - isDisabled: true, +export const Disabled: Story = { + args: { + ...Default.args, + isDisabled: true, + }, }; // With Help Text story -export const WithHelpText = Template.bind({}); -WithHelpText.args = { - ...Default.args, - helpText: "This is some help text for the dropdown", +export const WithHelpText: Story = { + args: { + ...Default.args, + helpText: "This is some help text for the dropdown", + }, }; // With Error story -export const WithError = Template.bind({}); -WithError.args = { - ...Default.args, - error: "This is an error message", +export const WithError: Story = { + args: { + ...Default.args, + error: "This is an error message", + }, }; diff --git a/frontend/components/forms/fields/DropdownWrapper/DropdownWrapper.tsx b/frontend/components/forms/fields/DropdownWrapper/DropdownWrapper.tsx index 789d17679c3b..1b8e51f50941 100644 --- a/frontend/components/forms/fields/DropdownWrapper/DropdownWrapper.tsx +++ b/frontend/components/forms/fields/DropdownWrapper/DropdownWrapper.tsx @@ -26,6 +26,7 @@ import { PADDING } from "styles/var/padding"; import FormField from "components/forms/FormField"; import DropdownOptionTooltipWrapper from "components/forms/fields/Dropdown/DropdownOptionTooltipWrapper"; import Icon from "components/Icon"; +import { IconNames } from "components/icons"; const getOptionBackgroundColor = (state: any) => { return state.isSelected || state.isFocused @@ -39,6 +40,7 @@ export interface CustomOptionType { tooltipContent?: string; helpText?: string; isDisabled?: boolean; + iconName?: IconNames; } export interface IDropdownWrapper { @@ -53,6 +55,7 @@ export interface IDropdownWrapper { helpText?: JSX.Element | string; isSearchable?: boolean; isDisabled?: boolean; + iconName?: IconNames; placeholder?: string; /** E.g. scroll to view dropdown menu in a scrollable parent container */ onMenuOpen?: () => void; @@ -70,8 +73,9 @@ const DropdownWrapper = ({ error, label, helpText, - isSearchable, + isSearchable = false, isDisabled = false, + iconName, placeholder, onMenuOpen, }: IDropdownWrapper) => { @@ -120,7 +124,7 @@ const DropdownWrapper = ({ }; const CustomDropdownIndicator = ( - props: DropdownIndicatorProps + props: DropdownIndicatorProps ) => { const { isFocused, selectProps } = props; const color = @@ -142,6 +146,19 @@ const DropdownWrapper = ({ ); }; + const ValueContainer = ({ children, ...props }: any) => { + return ( + components.ValueContainer && ( + + {!!children && iconName && ( + + )} + {children} + + ) + ); + }; + const customStyles: StylesConfig = { container: (provided) => ({ ...provided, @@ -235,10 +252,13 @@ const DropdownWrapper = ({ menuList: (provided) => ({ ...provided, padding: PADDING["pad-small"], + maxHeight: "none", }), valueContainer: (provided) => ({ ...provided, padding: 0, + display: "flex", + gap: PADDING["pad-small"], }), option: (provided, state) => ({ ...provided, @@ -260,7 +280,6 @@ const DropdownWrapper = ({ color: COLORS["ui-fleet-black-50"], fontStyle: "italic", cursor: "not-allowed", - pointerEvents: "none", }), // Styles for custom option ".dropdown-wrapper__option": { @@ -323,6 +342,7 @@ const DropdownWrapper = ({ Option: CustomOption, DropdownIndicator: CustomDropdownIndicator, IndicatorSeparator: () => null, + ValueContainer, }} value={getCurrentValue()} onChange={handleChange} diff --git a/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTable/SoftwareTable.tsx b/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTable/SoftwareTable.tsx index 5af6fcee5f26..5bcb4cb75ed1 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTable/SoftwareTable.tsx +++ b/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTable/SoftwareTable.tsx @@ -21,8 +21,6 @@ import { } from "services/entities/software"; import { ISoftwareTitle, ISoftwareVersion } from "interfaces/software"; -// @ts-ignore -import Dropdown from "components/forms/fields/Dropdown"; import TableContainer from "components/TableContainer"; import Slider from "components/forms/fields/Slider"; import CustomLink from "components/CustomLink"; @@ -32,6 +30,9 @@ import TableCount from "components/TableContainer/TableCount"; import Button from "components/buttons/Button"; import Icon from "components/Icon"; import TooltipWrapper from "components/TooltipWrapper"; +import { SingleValue } from "react-select-5"; +import DropdownWrapper from "components/forms/fields/DropdownWrapper"; +import { CustomOptionType } from "components/forms/fields/DropdownWrapper/DropdownWrapper"; import EmptySoftwareTable from "pages/SoftwarePage/components/EmptySoftwareTable"; @@ -284,28 +285,36 @@ const SoftwareTable = ({ } /> )} + ); }; const renderCustomControls = () => { + // Hidden when viewing versions table + if (showVersions) { + return null; + } + return (
- {!showVersions && ( // Hidden when viewing versions table - - )} - ) => + newValue && + handleCustomFilterDropdownChange( + newValue.value as ISoftwareDropdownFilterVal + ) + } + iconName="filter" />
); diff --git a/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTable/_styles.scss b/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTable/_styles.scss index d6ae84038c3c..8b51fd4d4341 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTable/_styles.scss +++ b/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTable/_styles.scss @@ -1,22 +1,6 @@ .software-table { - &__vuln_dropdown { - .Select-menu-outer { - width: 250px; - max-height: 310px; - - .Select-menu { - max-height: none; - } - } - - .Select-value { - padding-left: $pad-medium; - padding-right: $pad-medium; - } - - .dropdown__custom-value-label { - width: 155px; // Override 105px for longer text options - } + &__filter-dropdown { + min-width: 213px; } &__filter-controls { @@ -63,7 +47,7 @@ width: 100%; // Search bar across entire table .input-icon-field__input { - width: 100%; + min-width: 213px; } @media (min-width: $table-controls-break) { diff --git a/frontend/pages/SoftwarePage/SoftwareVulnerabilities/SoftwareVulnerabilitiesTable/SoftwareVulnerabilitiesTable.tests.tsx b/frontend/pages/SoftwarePage/SoftwareVulnerabilities/SoftwareVulnerabilitiesTable/SoftwareVulnerabilitiesTable.tests.tsx index 2e869892d182..5c115465934c 100644 --- a/frontend/pages/SoftwarePage/SoftwareVulnerabilities/SoftwareVulnerabilitiesTable/SoftwareVulnerabilitiesTable.tests.tsx +++ b/frontend/pages/SoftwarePage/SoftwareVulnerabilities/SoftwareVulnerabilitiesTable/SoftwareVulnerabilitiesTable.tests.tsx @@ -2,10 +2,7 @@ import React from "react"; import { screen, waitFor } from "@testing-library/react"; import { createCustomRenderer } from "test/test-utils"; -import { - createMockVulnerabilitiesResponse, - createMockVulnerability, -} from "__mocks__/vulnerabilitiesMock"; +import { createMockVulnerabilitiesResponse } from "__mocks__/vulnerabilitiesMock"; import createMockUser from "__mocks__/userMock"; import SoftwareVulnerabilitiesTable from "./SoftwareVulnerabilitiesTable"; @@ -353,7 +350,7 @@ describe("Software Vulnerabilities table", () => { expect( screen.getByText("Exploited vulnerabilities").parentElement?.parentElement ?.parentElement - ).toHaveClass("is-disabled"); + ).toHaveClass("react-select__option--is-disabled"); await waitFor(() => { waitFor(() => { diff --git a/frontend/pages/SoftwarePage/SoftwareVulnerabilities/SoftwareVulnerabilitiesTable/SoftwareVulnerabilitiesTable.tsx b/frontend/pages/SoftwarePage/SoftwareVulnerabilities/SoftwareVulnerabilitiesTable/SoftwareVulnerabilitiesTable.tsx index b7c49b4b47ef..4783a5e25ebb 100644 --- a/frontend/pages/SoftwarePage/SoftwareVulnerabilities/SoftwareVulnerabilitiesTable/SoftwareVulnerabilitiesTable.tsx +++ b/frontend/pages/SoftwarePage/SoftwareVulnerabilities/SoftwareVulnerabilitiesTable/SoftwareVulnerabilitiesTable.tsx @@ -13,13 +13,14 @@ import { } from "utilities/constants"; import { isIncompleteQuoteQuery } from "utilities/strings/stringUtils"; -// @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 TableCount from "components/TableContainer/TableCount"; +import { SingleValue } from "react-select-5"; +import DropdownWrapper from "components/forms/fields/DropdownWrapper"; +import { CustomOptionType } from "components/forms/fields/DropdownWrapper/DropdownWrapper"; import EmptyVulnerabilitiesTable from "pages/SoftwarePage/components/EmptyVulnerabilitiesTable"; @@ -165,7 +166,7 @@ const SoftwareVulnerabilitiesTable = ({ }, [data, router, teamId]); const handleExploitedVulnFilterDropdownChange = ( - isFilterExploited: boolean + isFilterExploited: string ) => { router.replace( getNextLocationPath({ @@ -176,7 +177,7 @@ const SoftwareVulnerabilitiesTable = ({ team_id: teamId, order_direction: orderDirection, order_key: orderKey, - exploit: isFilterExploited.toString(), + exploit: isFilterExploited, page: 0, // resets page index }, }) @@ -236,12 +237,14 @@ const SoftwareVulnerabilitiesTable = ({ // Exploited vulnerabilities is a premium feature const renderExploitedVulnerabilitiesDropdown = () => { return ( - ) => + newValue && handleExploitedVulnFilterDropdownChange(newValue.value) + } iconName="filter" /> ); @@ -280,6 +283,7 @@ const SoftwareVulnerabilitiesTable = ({ customControl={ searchable ? renderExploitedVulnerabilitiesDropdown : undefined } + stackControls renderCount={renderVulnerabilityCount} renderTableHelpText={renderTableHelpText} disableMultiRowSelect diff --git a/frontend/pages/SoftwarePage/SoftwareVulnerabilities/SoftwareVulnerabilitiesTable/_styles.scss b/frontend/pages/SoftwarePage/SoftwareVulnerabilities/SoftwareVulnerabilitiesTable/_styles.scss index 5ab1e1a438bd..7cb91b208d01 100644 --- a/frontend/pages/SoftwarePage/SoftwareVulnerabilities/SoftwareVulnerabilitiesTable/_styles.scss +++ b/frontend/pages/SoftwarePage/SoftwareVulnerabilities/SoftwareVulnerabilitiesTable/_styles.scss @@ -1,4 +1,8 @@ .software-vulnerabilities-table { + &__exploited-vulnerabilities-dropdown { + min-width: 250px; + } + &__count { display: flex; gap: 12px; diff --git a/frontend/pages/SoftwarePage/SoftwareVulnerabilities/SoftwareVulnerabilitiesTable/helpers.ts b/frontend/pages/SoftwarePage/SoftwareVulnerabilities/SoftwareVulnerabilitiesTable/helpers.ts index 39fba03fa415..08f06bed6e0a 100644 --- a/frontend/pages/SoftwarePage/SoftwareVulnerabilities/SoftwareVulnerabilitiesTable/helpers.ts +++ b/frontend/pages/SoftwarePage/SoftwareVulnerabilities/SoftwareVulnerabilitiesTable/helpers.ts @@ -5,18 +5,18 @@ export const getExploitedVulnerabilitiesDropdownOptions = ( return [ { - disabled: false, + isDisabled: false, label: "All vulnerabilities", - value: false, + value: "false", helpText: "All vulnerabilities detected on your hosts.", }, { - disabled: !isPremiumTier, + isDisabled: !isPremiumTier, label: "Exploited vulnerabilities", - value: true, + value: "true", helpText: "Vulnerabilities that have been actively exploited in the wild.", - tooltipContent: !isPremiumTier && disabledTooltipContent, + tooltipContent: !isPremiumTier ? disabledTooltipContent : undefined, }, ]; }; diff --git a/frontend/pages/SoftwarePage/SoftwareVulnerabilities/_styles.scss b/frontend/pages/SoftwarePage/SoftwareVulnerabilities/_styles.scss index fecd08d5b955..0257fb327d51 100644 --- a/frontend/pages/SoftwarePage/SoftwareVulnerabilities/_styles.scss +++ b/frontend/pages/SoftwarePage/SoftwareVulnerabilities/_styles.scss @@ -1,5 +1,2 @@ .software-vulnerabilities { - .dropdown__custom-value-label { - width: 260px; // Override 105px for longer text options - } } diff --git a/orbit/changes/8488-new-tuf-repository b/orbit/changes/8488-new-tuf-repository new file mode 100644 index 000000000000..6b054b047f27 --- /dev/null +++ b/orbit/changes/8488-new-tuf-repository @@ -0,0 +1 @@ +* Added changes to migrate to new TUF repository from https://tuf.fleetctl.com to https://updates.fleetdm.com. diff --git a/orbit/cmd/orbit/orbit.go b/orbit/cmd/orbit/orbit.go index 2cf8138045fd..c8a561ad2c1e 100644 --- a/orbit/cmd/orbit/orbit.go +++ b/orbit/cmd/orbit/orbit.go @@ -471,7 +471,26 @@ func main() { } } - localStore, err := filestore.New(filepath.Join(c.String("root-dir"), "tuf-metadata.json")) + if updateURL := c.String("update-url"); updateURL != update.OldFleetTUFURL && updateURL != update.DefaultURL { + // Migrate agents running with a custom TUF to use the new metadata file. + // We'll keep the old metadata file to support downgrades. + newMetadataFilePath := filepath.Join(c.String("root-dir"), update.MetadataFileName) + ok, err := file.Exists(newMetadataFilePath) + if err != nil { + // If we cannot stat this file then we cannot do other operations on it thus we fail with fatal error. + log.Fatal().Err(err).Msg("failed to check for new metadata file path") + } + if !ok { + oldMetadataFilePath := filepath.Join(c.String("root-dir"), update.OldMetadataFileName) + err := file.Copy(oldMetadataFilePath, newMetadataFilePath, constant.DefaultFileMode) + if err != nil { + // If we cannot write to this file then we cannot do other operations on it thus we fail with fatal error. + log.Fatal().Err(err).Msg("failed to copy new metadata file path") + } + } + } + + localStore, err := filestore.New(filepath.Join(c.String("root-dir"), update.MetadataFileName)) if err != nil { log.Fatal().Err(err).Msg("create local metadata store") } @@ -503,6 +522,29 @@ func main() { opt.RootDirectory = c.String("root-dir") opt.ServerURL = c.String("update-url") + checkAccessToNewTUF := false + if opt.ServerURL == update.OldFleetTUFURL { + // + // This only gets executed on orbit 1.38.0+ + // when it is configured to connect to the old TUF server + // (fleetd instances packaged before the migration, + // built by fleetctl previous to v4.63.0). + // + + if ok := update.HasAccessToNewTUFServer(opt); ok { + // orbit 1.38.0+ will use the new TUF server if it has access to the new TUF repository. + opt.ServerURL = update.DefaultURL + } else { + // orbit 1.38.0+ will use the old TUF server and old metadata path if it does not have access + // to the new TUF repository. During its execution (update.Runner) it will exit once it finds + // out it can access the new TUF server. + localStore, err = filestore.New(filepath.Join(c.String("root-dir"), update.OldMetadataFileName)) + if err != nil { + log.Fatal().Err(err).Msg("create local old metadata store") + } + checkAccessToNewTUF = true + } + } opt.LocalStore = localStore opt.InsecureTransport = c.Bool("insecure") opt.ServerCertificatePath = c.String("update-tls-certificate") @@ -545,13 +587,12 @@ func main() { var updater *update.Updater var updateRunner *update.Runner if !c.Bool("disable-updates") || c.Bool("dev-mode") { - updater, err = update.NewUpdater(opt) + updater, err := update.NewUpdater(opt) if err != nil { return fmt.Errorf("create updater: %w", err) } - if err := updater.UpdateMetadata(); err != nil { - log.Info().Err(err).Msg("update metadata, using saved metadata") + log.Info().Err(err).Msg("update metadata") } signaturesExpiredAtStartup := updater.SignaturesExpired() @@ -571,6 +612,7 @@ func main() { CheckInterval: c.Duration("update-interval"), Targets: targets, SignaturesExpiredAtStartup: signaturesExpiredAtStartup, + CheckAccessToNewTUF: checkAccessToNewTUF, }) if err != nil { return err @@ -1394,10 +1436,17 @@ func getFleetdComponentPaths( log.Error().Err(err).Msg("update metadata before getting components") } - // "root", "targets", or "snapshot" signatures have expired, thus - // we attempt to get local paths for the targets (updater.Get will fail - // because of the expired signatures). - if updater.SignaturesExpired() { + // + // updater.SignaturesExpired(): + // "root", "targets", or "snapshot" signatures have expired, thus + // we attempt to get local paths for the targets (updater.Get will fail + // because of the expired signatures). + // + // updater.LookupsFail(): + // Any of the targets fails to load thus we resort to the local executables we have. + // This could happen if the new TUF server is down during the first run of the TUF migration. + // + if updater.SignaturesExpired() || updater.LookupsFail() { log.Error().Err(err).Msg("expired metadata, using local targets") // Attempt to get local path of osqueryd. diff --git a/orbit/cmd/orbit/shell.go b/orbit/cmd/orbit/shell.go index 5acf521122ad..7a7d0900f71d 100644 --- a/orbit/cmd/orbit/shell.go +++ b/orbit/cmd/orbit/shell.go @@ -47,7 +47,7 @@ var shellCommand = &cli.Command{ return fmt.Errorf("initialize root dir: %w", err) } - localStore, err := filestore.New(filepath.Join(c.String("root-dir"), "tuf-metadata.json")) + localStore, err := filestore.New(filepath.Join(c.String("root-dir"), update.MetadataFileName)) if err != nil { log.Fatal().Err(err).Msg("failed to create local metadata store") } diff --git a/orbit/pkg/packaging/packaging.go b/orbit/pkg/packaging/packaging.go index 39ba97e4d880..279da1dc5e71 100644 --- a/orbit/pkg/packaging/packaging.go +++ b/orbit/pkg/packaging/packaging.go @@ -172,7 +172,7 @@ func (u UpdatesData) String() string { } func InitializeUpdates(updateOpt update.Options) (*UpdatesData, error) { - localStore, err := filestore.New(filepath.Join(updateOpt.RootDirectory, "tuf-metadata.json")) + localStore, err := filestore.New(filepath.Join(updateOpt.RootDirectory, update.MetadataFileName)) if err != nil { return nil, fmt.Errorf("failed to create local metadata store: %w", err) } @@ -236,6 +236,17 @@ func InitializeUpdates(updateOpt update.Options) (*UpdatesData, error) { } } + // Copy the new metadata file to the old location (pre-migration) to + // support orbit downgrades to 1.37.0 or lower. + // + // Once https://tuf.fleetctl.com is brought down (which means downgrades to 1.37.0 or + // lower won't be possible), we can remove this copy. + oldMetadataPath := filepath.Join(updateOpt.RootDirectory, update.OldMetadataFileName) + newMetadataPath := filepath.Join(updateOpt.RootDirectory, update.MetadataFileName) + if err := file.Copy(newMetadataPath, oldMetadataPath, constant.DefaultFileMode); err != nil { + return nil, fmt.Errorf("failed to create %s copy: %w", oldMetadataPath, err) + } + return &UpdatesData{ OrbitPath: orbitPath, OrbitVersion: orbitCustom.Version, diff --git a/orbit/pkg/update/options_darwin.go b/orbit/pkg/update/options_darwin.go index 54f243b64685..1d0c1365cab9 100644 --- a/orbit/pkg/update/options_darwin.go +++ b/orbit/pkg/update/options_darwin.go @@ -6,8 +6,8 @@ import ( var defaultOptions = Options{ RootDirectory: "/opt/orbit", - ServerURL: defaultURL, - RootKeys: defaultRootKeys, + ServerURL: DefaultURL, + RootKeys: defaultRootMetadata, LocalStore: client.MemoryLocalStore(), InsecureTransport: false, Targets: DarwinTargets, diff --git a/orbit/pkg/update/options_linux_amd64.go b/orbit/pkg/update/options_linux_amd64.go index 69e002e7c103..c3ab3b3089a6 100644 --- a/orbit/pkg/update/options_linux_amd64.go +++ b/orbit/pkg/update/options_linux_amd64.go @@ -6,8 +6,8 @@ import ( var defaultOptions = Options{ RootDirectory: "/opt/orbit", - ServerURL: defaultURL, - RootKeys: defaultRootKeys, + ServerURL: DefaultURL, + RootKeys: defaultRootMetadata, LocalStore: client.MemoryLocalStore(), InsecureTransport: false, Targets: LinuxTargets, diff --git a/orbit/pkg/update/options_linux_arm64.go b/orbit/pkg/update/options_linux_arm64.go index 5ed37667ddcc..aa431f2662b1 100644 --- a/orbit/pkg/update/options_linux_arm64.go +++ b/orbit/pkg/update/options_linux_arm64.go @@ -6,8 +6,8 @@ import ( var defaultOptions = Options{ RootDirectory: "/opt/orbit", - ServerURL: defaultURL, - RootKeys: defaultRootKeys, + ServerURL: DefaultURL, + RootKeys: defaultRootMetadata, LocalStore: client.MemoryLocalStore(), InsecureTransport: false, Targets: LinuxArm64Targets, diff --git a/orbit/pkg/update/options_windows.go b/orbit/pkg/update/options_windows.go index efee81416ebd..98a63b71ab67 100644 --- a/orbit/pkg/update/options_windows.go +++ b/orbit/pkg/update/options_windows.go @@ -9,8 +9,8 @@ import ( var defaultOptions = Options{ RootDirectory: `C:\Program Files\Orbit`, - ServerURL: defaultURL, - RootKeys: defaultRootKeys, + ServerURL: DefaultURL, + RootKeys: defaultRootMetadata, LocalStore: client.MemoryLocalStore(), InsecureTransport: false, Targets: WindowsTargets, diff --git a/orbit/pkg/update/runner.go b/orbit/pkg/update/runner.go index ae30f9c81cf2..498451e1d16a 100644 --- a/orbit/pkg/update/runner.go +++ b/orbit/pkg/update/runner.go @@ -39,6 +39,11 @@ type RunnerOptions struct { // An expired signature for the "timestamp" role does not cause issues // at start up (the go-tuf libary allows loading the targets). SignaturesExpiredAtStartup bool + + // CheckAccessToNewTUF, if set to true, will perform a check of access to the new Fleet TUF + // server on every update interval (once the access is confirmed it will store the confirmation + // of access to disk and will exit to restart). + CheckAccessToNewTUF bool } // Runner is a specialized runner for an Updater. It is designed with Execute and @@ -121,6 +126,14 @@ func NewRunner(updater *Updater, opt RunnerOptions) (*Runner, error) { return runner, nil } + if _, err := updater.Lookup(constant.OrbitTUFTargetName); errors.Is(err, client.ErrNoLocalSnapshot) { + // Return early and skip optimization, this will cause an unnecessary auto-update of orbit + // but allows orbit to start up if there's no local metadata AND if the TUF server is down + // (which may be the case during the migration from https://tuf.fleetctl.com to + // https://updates.fleetdm.com). + return runner, nil + } + // Initialize the hashes of the local files for all tracked targets. // // This is an optimization to not compute the hash of the local files every opt.CheckInterval @@ -204,6 +217,13 @@ func (r *Runner) Execute() error { case <-ticker.C: ticker.Reset(r.opt.CheckInterval) + if r.opt.CheckAccessToNewTUF { + if HasAccessToNewTUFServer(r.updater.opt) { + log.Info().Msg("detected access to new TUF repository, exiting") + return nil + } + } + if r.opt.SignaturesExpiredAtStartup { if r.updater.SignaturesExpired() { log.Debug().Msg("signatures still expired") diff --git a/orbit/pkg/update/update.go b/orbit/pkg/update/update.go index 51277c9d429d..79ce594f0792 100644 --- a/orbit/pkg/update/update.go +++ b/orbit/pkg/update/update.go @@ -21,7 +21,9 @@ import ( "github.com/fleetdm/fleet/v4/orbit/pkg/build" "github.com/fleetdm/fleet/v4/orbit/pkg/constant" "github.com/fleetdm/fleet/v4/orbit/pkg/platform" + "github.com/fleetdm/fleet/v4/orbit/pkg/update/filestore" "github.com/fleetdm/fleet/v4/pkg/certificate" + "github.com/fleetdm/fleet/v4/pkg/file" "github.com/fleetdm/fleet/v4/pkg/fleethttp" "github.com/fleetdm/fleet/v4/pkg/retry" "github.com/fleetdm/fleet/v4/pkg/secure" @@ -34,9 +36,32 @@ import ( const ( binDir = "bin" stagingDir = "staging" +) + +const ( + // + // For users using Fleet's TUF: + // - orbit 1.38.0+ we migrate TUF from https://tuf.fleetctl.com to https://updates.fleetdm.com. + // - orbit 1.38.0+ will start using `updates-metadata.json` instead of `tuf-metadata.json`. If it is missing + // (which will be the case for the first run after the auto-update) then it will generate it from the new pinned roots. + // + // For users using a custom TUF: + // - orbit 1.38.0+ will start using `updates-metadata.json` instead of `tuf-metadata.json` (if it is missing then + // it will perform a copy). + // + // For both Fleet's TUF and custom TUF, fleetd packages built with fleetctl 4.63.0+ will contain both files + // `updates-metadata.json` and `tuf-metadata.json` (same content) to support downgrades to orbit 1.37.0 or lower. - defaultURL = "https://tuf.fleetctl.com" - defaultRootKeys = `{"signed":{"_type":"root","spec_version":"1.0","version":4,"expires":"2024-10-06T17:47:49Z","keys":{"0cd79ade57d278957069e03a0fca6b975b95c2895fb20bdc3075f71fc19a4474":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"4627d9071a4b4a78c5ee867ea70439583b08dbe4ff23514e3bcb0a292de9406f"}},"1a4d9beb826d1ff4e036d757cfcd6e36d0f041e58d25f99ef3a20ae3f8dd71e3":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"1083b5fedbcaf8f98163f2f7083bbb2761a334b2ba8de44df7be3feb846725f6"}},"3c1fbd1f3b3429d8ccadfb1abfbae5826d0cf74b0a6bcd384c3045d2fe27613c":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"b07555d05d4260410bdf12de7f76be905e288e801071877c7ca3d7f0459bee0f"}},"5003eae9f614f7e2a6c94167d20803eabffc6f65b8731e828e56d068f1b1d834":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"5d91bdfddc381e03d109e3e2f08413ed4ba181d98766eb97802967fb6cf2b87d"}},"5b99d0520321d0177d66b84136f3fc800dde1d36a501c12e28aa12d59a239315":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"8113955a28517752982ed6521e2162cf207605cfd316b8cba637c0a9b7a72856"}},"5f42172605e1a317c6bdae3891b4312a952759185941490624e052889821c929":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"86e26b13b9a64f7de4ad24b47e2bb9779a8628cae0e1afa61e56f2003c2ab586"}},"6c0e404295d4bf8915b46754b5f4546ab0d11ff7d83804d4aa2d178cfc38eafc":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"3782135dcec329bcd0e1eefc1acead987dc6a7d629db62c9fdde8bc8ff3fa781"}},"7cbbc9772d4d6acea33b7edf5a4bc52c85ff283475d428ffee73f9dbd0f62c89":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"f79d0d357aaa534a251abc7b0604725ba7b035eb53d1bdf5cc3173d73e3d9678"}},"7ea5cd46d58ac97ec1424007b7a6b0b3403308bb8aa8de885a75841f6f1d50dd":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"978cdddce95311d56b7fed39419a31019a38a1dab179cddb541ffaf99f442f1b"}},"94ca5921eb097bb871272c1cc3ea2cad833cb8d4c2dea4a826646be656059640":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"6512498c7596f55a23405889539fadbcefecd0909e4af0b54e29f45d49f9b9f7"}},"ae943cb8be8a849b37c66ed46bdd7e905ba3118c0c051a6ee3cd30625855a076":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"e7ffa6355dedd0cd34defc903dfac05a7a8c1855d63be24cecb5577cfde1f990"}},"d940df08b59b12c30f95622a05cc40164b78a11dd7d408395ee4f79773331b30":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"64d15cc3cbaac7eccfd9e0de5a56a0789aadfec3d02e77bf9180b8090a2c48d6"}},"efb4e9bd7a7d9e045edf6f5140c9835dbcbb7770850da44bf15a800b248c810e":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"0b8b28b30b44ddb733c7457a7c0f75fcbac563208ea1fe7179b5888a4f1d2237"}}},"roles":{"root":{"keyids":["5f42172605e1a317c6bdae3891b4312a952759185941490624e052889821c929"],"threshold":1},"snapshot":{"keyids":["94ca5921eb097bb871272c1cc3ea2cad833cb8d4c2dea4a826646be656059640","1a4d9beb826d1ff4e036d757cfcd6e36d0f041e58d25f99ef3a20ae3f8dd71e3","7ea5cd46d58ac97ec1424007b7a6b0b3403308bb8aa8de885a75841f6f1d50dd","5003eae9f614f7e2a6c94167d20803eabffc6f65b8731e828e56d068f1b1d834"],"threshold":1},"targets":{"keyids":["0cd79ade57d278957069e03a0fca6b975b95c2895fb20bdc3075f71fc19a4474","ae943cb8be8a849b37c66ed46bdd7e905ba3118c0c051a6ee3cd30625855a076","6c0e404295d4bf8915b46754b5f4546ab0d11ff7d83804d4aa2d178cfc38eafc","3c1fbd1f3b3429d8ccadfb1abfbae5826d0cf74b0a6bcd384c3045d2fe27613c"],"threshold":1},"timestamp":{"keyids":["efb4e9bd7a7d9e045edf6f5140c9835dbcbb7770850da44bf15a800b248c810e","d940df08b59b12c30f95622a05cc40164b78a11dd7d408395ee4f79773331b30","7cbbc9772d4d6acea33b7edf5a4bc52c85ff283475d428ffee73f9dbd0f62c89","5b99d0520321d0177d66b84136f3fc800dde1d36a501c12e28aa12d59a239315"],"threshold":1}},"consistent_snapshot":false},"signatures":[{"keyid":"39a1db745ca254d8f8eb27493df8867264d9fb394572ecee76876f4d7e9cb753","sig":"841a44dcd98bbd78727f0b4b2a6e7dbb6d54e8469ca14965c9c5f9f7bb792dfe792f05e90a2724c75e966c007928ff7e7809de4608aab0bd27771f7b049c230f"},{"keyid":"5f42172605e1a317c6bdae3891b4312a952759185941490624e052889821c929","sig":"f6a16446edbbb632521649d21c2188b11eafeacb826caf2b8f3e2b8e9a343b573bca0a786c16ed2aeade25471c6d5103aac810ee05c50b044acd98d4b31d190c"},{"keyid":"63b4cb9241c93bca9218c67334da3651394de4cf36c44bb1320bad7111df7bba","sig":"62b5effddc00f7c9c06f4227cc1bfd4c09c47326a6c388451df28af798386d0e8d93412850bcc55f89147f439b5511bb63581ad09cd9ca215f72086348f9260b"},{"keyid":"656c44011cf8b80a4da765bec1516ee9598ffca5fa7ccb51f0a9feb04e6e6cbd","sig":"7b786c3825b206ed0c43fdfc852ebc5d363f7547a2f4940965c4c3eb89a8be069a5eddc942f8e796e645eea76b321dbbafc7f4c8d153181070da84d7a39bbe03"},{"keyid":"950097b911794bb554d7e83aa20c8aad11efcdc98f54b775fda76ac39eafa8fb","sig":"14e281d44c3384928e80a458214e4773f6c6c068a8d53e7458e8615fa5d1fe8f3daff11f736bec614cdba9e62d6f43850c6746cf2af7615445703af3ddeddb03"},{"keyid":"d6e90309d70431729bf722b089a8049efaf449230d94dc90bafa1cfc12d2b36f","sig":"bb7278ba1affc0c2bcbd952b7678ffa95268722121011df9ac18c19c1901e9c17ee3a1048a8471ca7c833ce86ecb054dc446c1ae473f118c1dc81a6e9b1dfb04"},{"keyid":"e5d1873c4d5268f650a26ea3c6ffb4bec1e523875888ebb6303fac2bfd578cd0","sig":"82019b8aba472b25f90899944db0ce94fd4ae1314f6336e2828bb30d592a9e3e34e6a66a75b1d310e0e85119a826bff345b99fe8647515057315da32e9847b04"}]}` + // + // The following variables are used by `fleetctl package` and by orbit in the migration to the new TUF repository. + // + + DefaultURL = `https://updates.fleetdm.com` + MetadataFileName = "updates-metadata.json" + defaultRootMetadata = `{"signed":{"_type":"root","spec_version":"1.0","version":6,"expires":"2026-01-08T22:23:47Z","keys":{"13f738181c9c50798d66316afaccf117658481b4db7b38715c358f522dc3bc40":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"34684874493cce2ac307c0dca88c241a287180c3eec9225c5f3e29bc4aeae080"}},"1ab0b9598e8b6ea653a24112f69b3eb3a84152c6a73d8dfdf43de4f63a93d3af":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"47a38623303bbe7b4ce47948011042b9387d7ec184642c156e06459d8fb6411b"}},"216e2dae905e73df943e003c428c7d340f21280750edb9d9ce2b15beeada667a":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"5c73ff497dc14380861892706c23bba0e3272c77c7f6f9524238b3a6b808b844"}},"2a83f45d24101ba91f669dca42981f97fc8bcde7cdf29c1887bc55c485103c49":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"e0bae1fae56d87e7f30f6359fd0a8cbfed231b19f58882e155c091c3bdb13c40"}},"3bc9408c1bcd999e69ba56b39e747656c6ebdafbd1e2c3e378c53e96e4477a64":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"8899adaa7ccd5bceb6a8c5f552fe4a9e59eb67e2a364db6638e06bbcf6f6eaeb"}},"4c0a5f49dc9665f13329d8157a2184985bd183503990469d06e32ad1bd6e70ee":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"eceeea79c6a353f5c7ed3be552a6144458ecf5fe78972eba88a77a45a230c58b"}},"57227c64d19605636d0afbab41d0887455de4287c6f328c5f69650005f793de0":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"d962cdf1d3e974f6c2b3d602260c87e0647fd54372afe7c31238f26a56a75443"}},"61e70c06858064c5e33e5009734222000610013e26fb6846ee17f55ddfb22da3":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"7e42b715cd9eedd8252a6b715fcfb8ef789531782ed19027a3c2ae11a2b0243b"}},"79a257e77793cb26d5d0cc0af6b2d2c94e7e8ca8b875dc504eb10fb343753f94":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"93409c7be4e3942ecff111d36cd097cda5778cb4f53305a07f20855b08f26071"}},"868858a9723ce357e8e251617ae11f7d3ae8a348588872cb2ce4149ee70ba155":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"f91c5b1fcb4ed3a1a65b956fa9025a89f458cea9036259b8cdfa276bc04faf45"}},"91629787db6e18b226027587733b2f667a7982eed9509c2e39dfeaf4cfb1a17a":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"3e2ac750e2e0eb22f87f35ad5309932b7b081c40891d249493fa9e2cef28066b"}},"97b3353aa23d09f88323e63cdc587a368df0a8818da67b91720b2cab00e68297":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"6b92f54f51eb617069a963a41aed75b4a23fca45e4c9ca8fc6d748d9b58b0451"}},"bc711f19576de2d71d1ca41eeccf7412f12c3ee4971185cd69066f8dc51d1ce6":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"7e39afe9a0310e7645ed389f243dc7156069d6972505cfbb460f8147949343cd"}},"c1ce9675f7302d2f09514f78ec7b3bdc00758d69b659e00c1c6731a4d0836bb9":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"a97c44dc10ee979ead46beb3be22f3407238e72b68240a41f92e65051eb27cb1"}}},"roles":{"root":{"keyids":["bc711f19576de2d71d1ca41eeccf7412f12c3ee4971185cd69066f8dc51d1ce6","4c0a5f49dc9665f13329d8157a2184985bd183503990469d06e32ad1bd6e70ee","57227c64d19605636d0afbab41d0887455de4287c6f328c5f69650005f793de0"],"threshold":1},"snapshot":{"keyids":["1ab0b9598e8b6ea653a24112f69b3eb3a84152c6a73d8dfdf43de4f63a93d3af","868858a9723ce357e8e251617ae11f7d3ae8a348588872cb2ce4149ee70ba155","97b3353aa23d09f88323e63cdc587a368df0a8818da67b91720b2cab00e68297"],"threshold":1},"targets":{"keyids":["3bc9408c1bcd999e69ba56b39e747656c6ebdafbd1e2c3e378c53e96e4477a64","c1ce9675f7302d2f09514f78ec7b3bdc00758d69b659e00c1c6731a4d0836bb9","2a83f45d24101ba91f669dca42981f97fc8bcde7cdf29c1887bc55c485103c49"],"threshold":1},"timestamp":{"keyids":["13f738181c9c50798d66316afaccf117658481b4db7b38715c358f522dc3bc40","79a257e77793cb26d5d0cc0af6b2d2c94e7e8ca8b875dc504eb10fb343753f94","91629787db6e18b226027587733b2f667a7982eed9509c2e39dfeaf4cfb1a17a","61e70c06858064c5e33e5009734222000610013e26fb6846ee17f55ddfb22da3","216e2dae905e73df943e003c428c7d340f21280750edb9d9ce2b15beeada667a"],"threshold":1}},"consistent_snapshot":false},"signatures":[{"keyid":"bc711f19576de2d71d1ca41eeccf7412f12c3ee4971185cd69066f8dc51d1ce6","sig":"7a0d8eda3e6058bf10f21bdda4b876c499b4182335ab943737c2121603d0e2ec707222e92eace3d10051264988705d9c51e2159d13e234d57441e60ba1a3c10a"}]}` + + OldFleetTUFURL = "https://tuf.fleetctl.com" + OldMetadataFileName = "tuf-metadata.json" ) // Updater is responsible for managing update state. @@ -122,32 +147,10 @@ func NewUpdater(opt Options) (*Updater, error) { return nil, errors.New("opt.LocalStore must be non-nil") } - tlsConfig := &tls.Config{ - InsecureSkipVerify: opt.InsecureTransport, - } - - if opt.ServerCertificatePath != "" { - rootCAs, err := certificate.LoadPEM(opt.ServerCertificatePath) - if err != nil { - return nil, fmt.Errorf("loading server root CA: %w", err) - } - tlsConfig.RootCAs = rootCAs - } - - if opt.ClientCertificate != nil { - tlsConfig.Certificates = []tls.Certificate{*opt.ClientCertificate} - } - - httpClient := fleethttp.NewClient(fleethttp.WithTLSClientConfig(tlsConfig)) - - remoteOpt := &client.HTTPRemoteOptions{ - UserAgent: fmt.Sprintf("orbit/%s (%s %s)", build.Version, runtime.GOOS, runtime.GOARCH), - } - remoteStore, err := client.HTTPRemoteStore(opt.ServerURL, remoteOpt, httpClient) + remoteStore, err := createTUFRemoteStore(opt, opt.ServerURL) if err != nil { - return nil, fmt.Errorf("init remote store: %w", err) + return nil, fmt.Errorf("get tls config: %w", err) } - tufClient := client.NewClient(opt.LocalStore, remoteStore) // TODO(lucas): Related to the NOTE below. @@ -164,10 +167,13 @@ func NewUpdater(opt Options) (*Updater, error) { return nil, fmt.Errorf("read metadata: %w", err) } if meta["root.json"] == nil { - // NOTE: This path is currently only used when (1) packaging Orbit (`fleetctl package`) and - // (2) in the edge-case when Orbit's metadata JSON local file is removed for some reason. - // When edge-case (2) happens, Orbit will attempt to use Fleet DM's root JSON + // NOTE: This path is currently only used when (1) packaging Orbit (`fleetctl package`), or + // (2) in the edge-case when orbit's metadata JSON local file is removed for some reason, or + // (3) first run on TUF migration from https://tuf.fleetctl.com to https://updates.fleetdm.com. + // + // When edge-case (2) happens, orbit will attempt to use Fleet DM's root JSON // (which may be unexpected on custom TUF Orbit deployments). + log.Info().Msg("initialize TUF from embedded root keys") if err := tufClient.Init([]byte(opt.RootKeys)); err != nil { return nil, fmt.Errorf("client init with configuration metadata: %w", err) } @@ -203,7 +209,7 @@ func NewDisabled(opt Options) *Updater { // UpdateMetadata downloads and verifies remote repository metadata. func (u *Updater) UpdateMetadata() error { if _, err := u.client.Update(); err != nil { - return fmt.Errorf("update metadata: %w", err) + return fmt.Errorf("client update: %w", err) } return nil } @@ -225,6 +231,16 @@ func (u *Updater) SignaturesExpired() bool { return IsExpiredErr(err) } +// LookupsFail returns true if lookups are failing for any of the targets. +func (u *Updater) LookupsFail() bool { + for target := range u.opt.Targets { + if _, err := u.Lookup(target); err != nil { + return true + } + } + return false +} + // repoPath returns the path of the target in the remote repository. func (u *Updater) repoPath(target string) (string, error) { u.mu.Lock() @@ -736,3 +752,110 @@ func CanRun(rootDirPath, targetName string, targetInfo TargetInfo) bool { return true } + +// HasAccessToNewTUFServer will verify if the agent has access to Fleet's new TUF +// by downloading the metadata and the orbit stable target. +// +// The metadata and the test target files will be downloaded to a temporary directory +// that will be removed before this method returns. +func HasAccessToNewTUFServer(opt Options) bool { + fp := filepath.Join(opt.RootDirectory, "new-tuf-checked") + ok, err := file.Exists(fp) + if err != nil { + log.Error().Err(err).Msg("failed to check new-tuf-checked file exists") + return false + } + if ok { + return true + } + tmpDir, err := os.MkdirTemp(opt.RootDirectory, "tuf-tmp*") + if err != nil { + log.Error().Err(err).Msg("failed to create tuf-tmp directory") + return false + } + defer os.RemoveAll(tmpDir) + localStore, err := filestore.New(filepath.Join(tmpDir, "tuf-tmp.json")) + if err != nil { + log.Error().Err(err).Msg("failed to create tuf-tmp local store") + return false + } + remoteStore, err := createTUFRemoteStore(opt, DefaultURL) + if err != nil { + log.Error().Err(err).Msg("failed to create TUF remote store") + return false + } + tufClient := client.NewClient(localStore, remoteStore) + if err := tufClient.Init([]byte(opt.RootKeys)); err != nil { + log.Error().Err(err).Msg("failed to pin root keys") + return false + } + if _, err := tufClient.Update(); err != nil { + // Logging as debug to not fill logs until users allow connections to new TUF server. + log.Debug().Err(err).Msg("failed to update metadata from new TUF") + return false + } + tmpFile, err := secure.OpenFile( + filepath.Join(tmpDir, "orbit"), + os.O_CREATE|os.O_WRONLY, + constant.DefaultFileMode, + ) + if err != nil { + log.Error().Err(err).Msg("failed open temp file for download") + return false + } + defer tmpFile.Close() + // We are using the orbit stable target as the test target. + var ( + platform string + executable string + ) + switch runtime.GOOS { + case "darwin": + platform = "macos" + executable = "orbit" + case "windows": + platform = "windows" + executable = "orbit.exe" + case "linux": + platform = "linux" + executable = "orbit" + } + if err := tufClient.Download(fmt.Sprintf("orbit/%s/stable/%s", platform, executable), &fileDestination{tmpFile}); err != nil { + // Logging as debug to not fill logs until users allow connections to new TUF server. + log.Debug().Err(err).Msg("failed to download orbit from TUF") + return false + } + + if err := os.WriteFile(fp, []byte("new-tuf-checked"), constant.DefaultFileMode); err != nil { + // We log the error and return success below anyway because the access check was successful. + log.Error().Err(err).Msg("failed to write new-tuf-checked file") + } + // We assume access to the whole repository + // if the orbit macOS stable target is downloaded successfully. + return true +} + +func createTUFRemoteStore(opt Options, serverURL string) (client.RemoteStore, error) { + tlsConfig := &tls.Config{ + InsecureSkipVerify: opt.InsecureTransport, + } + if opt.ServerCertificatePath != "" { + rootCAs, err := certificate.LoadPEM(opt.ServerCertificatePath) + if err != nil { + return nil, fmt.Errorf("loading server root CA: %w", err) + } + tlsConfig.RootCAs = rootCAs + } + if opt.ClientCertificate != nil { + tlsConfig.Certificates = []tls.Certificate{*opt.ClientCertificate} + } + remoteOpt := &client.HTTPRemoteOptions{ + UserAgent: fmt.Sprintf("orbit/%s (%s %s)", build.Version, runtime.GOOS, runtime.GOARCH), + } + httpClient := fleethttp.NewClient(fleethttp.WithTLSClientConfig(tlsConfig)) + remoteStore, err := client.HTTPRemoteStore(serverURL, remoteOpt, httpClient) + if err != nil { + return nil, fmt.Errorf("init remote store: %w", err) + } + return remoteStore, nil +} diff --git a/server/fleet/agent_options.go b/server/fleet/agent_options.go index 0e185a3fee2e..51f9d94dcfdc 100644 --- a/server/fleet/agent_options.go +++ b/server/fleet/agent_options.go @@ -325,3 +325,88 @@ func validateJSONAgentOptionsSet(rawJSON json.RawMessage) error { } return nil } + +func FindAgentOptionsKeyPath(key string) ([]string, error) { + if key == "script_execution_timeout" { + return []string{"script_execution_timeout"}, nil + } + + configPath, err := locateStructJSONKeyPath(key, "config", osqueryAgentOptions{}) + if err != nil { + return nil, fmt.Errorf("locating key path in agent options: %w", err) + } + if configPath != nil { + return configPath, nil + } + + if key == "overrides" { + return []string{"overrides"}, nil + } + if key == "platforms" { + return []string{"overrides", "platforms"}, nil + } + + commandLinePath, err := locateStructJSONKeyPath(key, "command_line_flags", osqueryCommandLineFlags{}) + if err != nil { + return nil, fmt.Errorf("locating key path in agent command line options: %w", err) + } + if commandLinePath != nil { + return commandLinePath, nil + } + + extensionsPath, err := locateStructJSONKeyPath(key, "extensions", ExtensionInfo{}) + if err != nil { + return nil, fmt.Errorf("locating key path in agent extensions options: %w", err) + } + if extensionsPath != nil { + return extensionsPath, nil + } + + channelsPath, err := locateStructJSONKeyPath(key, "update_channels", OrbitUpdateChannels{}) + if err != nil { + return nil, fmt.Errorf("locating key path in agent update channels: %w", err) + } + if channelsPath != nil { + return channelsPath, nil + } + + return nil, nil +} + +// Only searches two layers deep +func locateStructJSONKeyPath(key, startKey string, target any) ([]string, error) { + if key == startKey { + return []string{startKey}, nil + } + + optionsBytes, err := json.Marshal(target) + if err != nil { + return nil, fmt.Errorf("unable to marshall target: %w", err) + } + + var opts map[string]any + + if err := json.Unmarshal(optionsBytes, &opts); err != nil { + return nil, fmt.Errorf("unable to unmarshall target: %w", err) + } + + var path [3]string + path[0] = startKey + for k, v := range opts { + path[1] = k + if k == key { + return path[:2], nil + } + + if inner, ok := v.(map[string]any); ok { + for k2 := range inner { + path[2] = k2 + if key == k2 { + return path[:3], nil + } + } + } + } + + return nil, nil +} diff --git a/server/fleet/errors.go b/server/fleet/errors.go index fb5e302c11ca..77098617bc73 100644 --- a/server/fleet/errors.go +++ b/server/fleet/errors.go @@ -477,6 +477,15 @@ func IsJSONUnknownFieldError(err error) bool { return rxJSONUnknownField.MatchString(err.Error()) } +func GetJSONUnknownField(err error) *string { + errCause := Cause(err) + if IsJSONUnknownFieldError(errCause) { + substr := rxJSONUnknownField.FindStringSubmatch(errCause.Error()) + return &substr[1] + } + return nil +} + // UserMessage implements the user-friendly translation of the error if its // root cause is one of the supported types, otherwise it returns the error // message. diff --git a/server/mail/templates/change_email_confirmation.html b/server/mail/templates/change_email_confirmation.html index 917e45a8a2a3..b804150ec444 100644 --- a/server/mail/templates/change_email_confirmation.html +++ b/server/mail/templates/change_email_confirmation.html @@ -2,7 +2,10 @@ - +