Merge branch 'main' into feat-setup-experience
This commit is contained in:
@@ -19,8 +19,15 @@ It is [planned and ready](https://fleetdm.com/handbook/company/development-group
|
||||
| I want to _________________________________________
|
||||
| so that I can _________________________________________.
|
||||
|
||||
## Objective
|
||||
|
||||
<!-- What quarterly objective does this story contribute to, if any? If it doesn't contribute to an objective, explain why it's being prioritized. -->
|
||||
|
||||
## Original requests
|
||||
|
||||
<!-- Insert the link to the feature request(s) that this story contributes to. Put "None" if it doesn't contribute to a request. For customer requests, add the `customer-xyz` label(s). -->
|
||||
|
||||
## Context
|
||||
- Requestor(s): _________________________ <!-- Who are the non-customer requestor(s) for this story, if any? Put their GitHub usernames here. They should be notified if the story gets de-prioritized. For customer requestors, use the `customer-xyz` label instead. -->
|
||||
- Product designer: _________________________ <!-- Who is the product designer to contact if folks have questions about the UI, CLI, or API changes? -->
|
||||
|
||||
<!--
|
||||
|
||||
@@ -97,6 +97,8 @@ The next step to ensure Okta detects the device as managed is to issue a SCEP ce
|
||||
|
||||
```
|
||||
|
||||
> Make sure to use `.mobileconfig` as the file extension
|
||||
|
||||
* Enforce the configuration profile on your hosts. You can follow [this guide on enforcing custom OS settings in Fleet](https://fleetdm.com/guides/custom-os-settings).
|
||||
* You can optionally verify the issued certificate by opening Keychain Access on the device or by running a [live query](https://fleetdm.com/guides/get-current-telemetry-from-your-devices-with-live-queries):
|
||||
|
||||
|
||||
@@ -33,11 +33,11 @@ Requests sent by Fleet Desktop and the web page that opens when clicking on the
|
||||
|
||||
The server uses this token to authenticate requests that give host information. Fleet uses the following methods to secure access to this information.
|
||||
|
||||
**Rate Limiting**
|
||||
**Rate limiting**
|
||||
|
||||
To prevent brute-forcing, Fleet rate-limits the endpoints used by Fleet Desktop on a per-IP basis. If an IP requests more than 720 invalid UUIDs in a one-hour interval, Fleet will return HTTP error code 429.
|
||||
|
||||
**Token Rotation**
|
||||
**Token rotation**
|
||||
|
||||
```
|
||||
ℹ️ In Fleet v4.22.0, token rotation for Fleet Desktop was introduced.
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
* Fixed a bug where policy failures of a host were not being cleared in the host details page after configuring the host to not run any policies.
|
||||
@@ -0,0 +1 @@
|
||||
* Record which policy automation triggered a script run in the activity feed
|
||||
+15
-15
@@ -76,8 +76,7 @@ import (
|
||||
var allowedURLPrefixRegexp = regexp.MustCompile("^(?:/[a-zA-Z0-9_.~-]+)+$")
|
||||
|
||||
const (
|
||||
softwareInstallerUploadTimeout = 4 * time.Minute
|
||||
liveQueryMemCacheDuration = 1 * time.Second
|
||||
liveQueryMemCacheDuration = 1 * time.Second
|
||||
)
|
||||
|
||||
type initializer interface {
|
||||
@@ -1153,22 +1152,23 @@ the way that the Fleet server works.
|
||||
|
||||
if (req.Method == http.MethodPost && strings.HasSuffix(req.URL.Path, "/fleet/software/package")) ||
|
||||
(req.Method == http.MethodPatch && strings.HasSuffix(req.URL.Path, "/package") && strings.Contains(req.URL.Path, "/fleet/software/titles/")) {
|
||||
// when uploading a software installer, the file might be large so
|
||||
// the read timeout (to read the full request body) must be extended.
|
||||
var zeroTime time.Time
|
||||
rc := http.NewResponseController(rw)
|
||||
// the frontend times out waiting for the upload after 4 minutes,
|
||||
// use that same timeout:
|
||||
// https://www.figma.com/design/oQl2oQUG0iRkUy0YOxc307/%2314921-Deploy-security-agents-to-macOS%2C-Windows%2C-and-Linux-hosts?node-id=773-18032&t=QjEU6tc73tddNSqn-0
|
||||
if err := rc.SetReadDeadline(time.Now().Add(softwareInstallerUploadTimeout)); err != nil {
|
||||
// For large software installers, the server time needs time to read the full
|
||||
// request body so we use the zero value to remove the deadline and override the
|
||||
// default read timeout.
|
||||
// TODO: Is this really how we want to handle this? Or would an arbitrarily long
|
||||
// timeout be better?
|
||||
if err := rc.SetReadDeadline(zeroTime); err != nil {
|
||||
level.Error(logger).Log("msg", "http middleware failed to override endpoint read timeout", "err", err)
|
||||
}
|
||||
// the write timeout should be extended to give the server time to
|
||||
// store the installer to S3 (or the configured storage location) and
|
||||
// write a response body, otherwise the connection is terminated
|
||||
// abruptly. Give it twice the read timeout, so that if it takes
|
||||
// 3m59s to upload an installer, we don't fail because of a lack of
|
||||
// time to store to S3.
|
||||
if err := rc.SetWriteDeadline(time.Now().Add(2 * softwareInstallerUploadTimeout)); err != nil {
|
||||
// For large software installers, the server time needs time to store the
|
||||
// installer to S3 (or the configured storage location) and write the response
|
||||
// body so we use the zero value to remove the deadline and override the
|
||||
// default write timeout.
|
||||
// TODO: Is this really how we want to handle this? Or would an arbitrarily long
|
||||
// timeout be better?
|
||||
if err := rc.SetWriteDeadline(zeroTime); err != nil {
|
||||
level.Error(logger).Log("msg", "http middleware failed to override endpoint write timeout", "err", err)
|
||||
}
|
||||
req.Body = http.MaxBytesReader(rw, req.Body, service.MaxSoftwareInstallerSize)
|
||||
|
||||
@@ -251,7 +251,7 @@ controls:
|
||||
labels_include_all:
|
||||
- Label 2
|
||||
windows_settings:
|
||||
custom_settings
|
||||
custom_settings:
|
||||
- path: ../lib/windows-profile.xml
|
||||
macos_setup: # Available in Fleet Premium
|
||||
bootstrap_package: https://example.org/bootstrap_package.pkg
|
||||
|
||||
@@ -887,6 +887,8 @@ This activity contains the following fields:
|
||||
- "script_execution_id": Execution ID of the script run.
|
||||
- "script_name": Name of the script (empty if it was an anonymous script).
|
||||
- "async": Whether the script was executed asynchronously.
|
||||
- "policy_id": ID of the policy whose failure triggered the script run. Null if no associated policy.
|
||||
- "policy_name": Name of the policy whose failure triggered the script run. Null if no associated policy.
|
||||
|
||||
#### Example
|
||||
|
||||
@@ -896,7 +898,9 @@ This activity contains the following fields:
|
||||
"host_display_name": "Anna's MacBook Pro",
|
||||
"script_name": "set-timezones.sh",
|
||||
"script_execution_id": "d6cffa75-b5b5-41ef-9230-15073c8a88cf",
|
||||
"async": false
|
||||
"async": false,
|
||||
"policy_id": 123,
|
||||
"policy_name": "Ensure photon torpedoes are primed"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -5,10 +5,8 @@ import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/pkg/file"
|
||||
@@ -19,6 +17,9 @@ import (
|
||||
"github.com/fleetdm/fleet/v4/server/mdm/maintainedapps"
|
||||
)
|
||||
|
||||
// noCheckHash is used by homebrew to signal that a hash shouldn't be checked.
|
||||
const noCheckHash = "no_check"
|
||||
|
||||
func (svc *Service) AddFleetMaintainedApp(
|
||||
ctx context.Context,
|
||||
teamID *uint,
|
||||
@@ -52,16 +53,25 @@ func (svc *Service) AddFleetMaintainedApp(
|
||||
return ctxerr.Wrap(ctx, err, "downloading app installer")
|
||||
}
|
||||
|
||||
// Validate the bytes we got are what we expected
|
||||
h := sha256.New()
|
||||
_, err = h.Write(installerBytes)
|
||||
extension, err := maintainedapps.ExtensionForBundleIdentifier(app.BundleIdentifier)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "generating SHA256 of maintained app installer")
|
||||
return ctxerr.Errorf(ctx, "getting extension from bundle identifier %q", app.BundleIdentifier)
|
||||
}
|
||||
gotHash := hex.EncodeToString(h.Sum(nil))
|
||||
|
||||
if gotHash != app.SHA256 {
|
||||
return ctxerr.New(ctx, "mismatch in maintained app SHA256 hash")
|
||||
// Validate the bytes we got are what we expected, if homebrew supports
|
||||
// it, the string "no_check" is a special token used to signal that the
|
||||
// hash shouldn't be checked.
|
||||
if app.SHA256 != noCheckHash {
|
||||
h := sha256.New()
|
||||
_, err = h.Write(installerBytes)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "generating SHA256 of maintained app installer")
|
||||
}
|
||||
gotHash := hex.EncodeToString(h.Sum(nil))
|
||||
|
||||
if gotHash != app.SHA256 {
|
||||
return ctxerr.New(ctx, "mismatch in maintained app SHA256 hash")
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to the filename if we weren't able to extract a filename from the installer response
|
||||
@@ -69,6 +79,12 @@ func (svc *Service) AddFleetMaintainedApp(
|
||||
filename = app.Name
|
||||
}
|
||||
|
||||
// The UI requires all filenames to have extensions. If we couldn't get
|
||||
// one, use the extension we extracted prior
|
||||
if filepath.Ext(filename) == "" {
|
||||
filename = filename + "." + extension
|
||||
}
|
||||
|
||||
installScript = file.Dos2UnixNewlines(installScript)
|
||||
if installScript == "" {
|
||||
installScript = app.InstallScript
|
||||
@@ -79,11 +95,6 @@ func (svc *Service) AddFleetMaintainedApp(
|
||||
uninstallScript = app.UninstallScript
|
||||
}
|
||||
|
||||
installerURL, err := url.Parse(app.InstallerURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
installerReader := bytes.NewReader(installerBytes)
|
||||
payload := &fleet.UploadSoftwareInstallerPayload{
|
||||
InstallerFile: installerReader,
|
||||
@@ -94,7 +105,7 @@ func (svc *Service) AddFleetMaintainedApp(
|
||||
Filename: filename,
|
||||
Platform: string(app.Platform),
|
||||
Source: "apps",
|
||||
Extension: strings.TrimPrefix(filepath.Ext(installerURL.Path), "."),
|
||||
Extension: extension,
|
||||
BundleIdentifier: app.BundleIdentifier,
|
||||
StorageID: app.SHA256,
|
||||
FleetLibraryAppID: &app.ID,
|
||||
|
||||
@@ -1286,7 +1286,7 @@ func (svc *Service) softwareBatchUpload(
|
||||
}
|
||||
|
||||
var g errgroup.Group
|
||||
g.SetLimit(3)
|
||||
g.SetLimit(3) // TODO: consider lowering this limit, see https://github.com/fleetdm/fleet/issues/22704#issuecomment-2397407837
|
||||
// critical to avoid data race, the slice is pre-allocated and each
|
||||
// goroutine only writes to its index.
|
||||
installers := make([]*fleet.UploadSoftwareInstallerPayload, len(payloads))
|
||||
|
||||
@@ -71,8 +71,8 @@ const DEFAULT_SOFTWARE_VERSION_MOCK: ISoftwareVersion = {
|
||||
name: "test.app",
|
||||
version: "1.2.3",
|
||||
bundle_identifier: "com.test.Desktop",
|
||||
source: "test_package",
|
||||
browser: "",
|
||||
source: "apps",
|
||||
browser: "chrome",
|
||||
release: "1",
|
||||
vendor: "test_vendor",
|
||||
arch: "x86_64",
|
||||
@@ -161,7 +161,7 @@ const DEFAULT_SOFTWARE_TITLE_DETAILS_MOCK: ISoftwareTitleDetails = {
|
||||
name: "test.app",
|
||||
software_package: null,
|
||||
app_store_app: null,
|
||||
source: "test_package",
|
||||
source: "apps",
|
||||
hosts_count: 1,
|
||||
versions: [createMockSoftwareTitleVersion()],
|
||||
bundle_identifier: "com.test.Desktop",
|
||||
|
||||
@@ -95,12 +95,12 @@ export interface ISoftwareTitle {
|
||||
id: number;
|
||||
name: string;
|
||||
versions_count: number;
|
||||
source: string; // "apps" | "ios_apps" | "ipados_apps" | ?
|
||||
source: SoftwareSource;
|
||||
hosts_count: number;
|
||||
versions: ISoftwareTitleVersion[] | null;
|
||||
software_package: ISoftwarePackage | null;
|
||||
app_store_app: IAppStoreApp | null;
|
||||
browser?: string;
|
||||
browser?: BrowserType;
|
||||
}
|
||||
|
||||
export interface ISoftwareTitleDetails {
|
||||
@@ -108,12 +108,12 @@ export interface ISoftwareTitleDetails {
|
||||
name: string;
|
||||
software_package: ISoftwarePackage | null;
|
||||
app_store_app: IAppStoreApp | null;
|
||||
source: string; // "apps" | "ios_apps" | "ipados_apps" | ?
|
||||
source: SoftwareSource;
|
||||
hosts_count: number;
|
||||
versions: ISoftwareTitleVersion[] | null;
|
||||
versions_updated_at?: string;
|
||||
bundle_identifier?: string;
|
||||
browser?: string;
|
||||
browser?: BrowserType;
|
||||
versions_count?: number;
|
||||
}
|
||||
|
||||
@@ -134,8 +134,8 @@ export interface ISoftwareVersion {
|
||||
name: string; // e.g., "Figma.app"
|
||||
version: string; // e.g., "2.1.11"
|
||||
bundle_identifier?: string; // e.g., "com.figma.Desktop"
|
||||
source: string; // "apps" | "ipados_apps" | "ios_apps" | ?
|
||||
browser: string; // e.g., "chrome"
|
||||
source: SoftwareSource;
|
||||
browser: BrowserType;
|
||||
release: string; // TODO: on software/verions/:id?
|
||||
vendor: string;
|
||||
arch: string; // e.g., "x86_64" // TODO: on software/verions/:id?
|
||||
@@ -144,7 +144,7 @@ export interface ISoftwareVersion {
|
||||
hosts_count?: number;
|
||||
}
|
||||
|
||||
export const SOURCE_TYPE_CONVERSION: Record<string, string> = {
|
||||
export const SOURCE_TYPE_CONVERSION = {
|
||||
apt_sources: "Package (APT)",
|
||||
deb_packages: "Package (deb)",
|
||||
portage_packages: "Package (Portage)",
|
||||
@@ -167,7 +167,9 @@ export const SOURCE_TYPE_CONVERSION: Record<string, string> = {
|
||||
vscode_extensions: "IDE extension (VS Code)",
|
||||
} as const;
|
||||
|
||||
const BROWSER_TYPE_CONVERSION: Record<string, string> = {
|
||||
export type SoftwareSource = keyof typeof SOURCE_TYPE_CONVERSION;
|
||||
|
||||
const BROWSER_TYPE_CONVERSION = {
|
||||
chrome: "Chrome",
|
||||
chromium: "Chromium",
|
||||
opera: "Opera",
|
||||
@@ -177,14 +179,16 @@ const BROWSER_TYPE_CONVERSION: Record<string, string> = {
|
||||
edge_beta: "Edge Beta",
|
||||
} as const;
|
||||
|
||||
export type BrowserType = keyof typeof BROWSER_TYPE_CONVERSION;
|
||||
|
||||
export const formatSoftwareType = ({
|
||||
source,
|
||||
browser,
|
||||
}: {
|
||||
source: string;
|
||||
browser?: string;
|
||||
source: SoftwareSource;
|
||||
browser?: BrowserType;
|
||||
}) => {
|
||||
let type = SOURCE_TYPE_CONVERSION[source] || "Unknown";
|
||||
let type: string = SOURCE_TYPE_CONVERSION[source] || "Unknown";
|
||||
if (browser) {
|
||||
type = `Browser plugin (${
|
||||
BROWSER_TYPE_CONVERSION[browser] || startCase(browser)
|
||||
@@ -310,7 +314,7 @@ export interface IHostSoftware {
|
||||
name: string;
|
||||
software_package: IHostSoftwarePackage | null;
|
||||
app_store_app: IHostAppStoreApp | null;
|
||||
source: string;
|
||||
source: SoftwareSource;
|
||||
bundle_identifier?: string;
|
||||
status: Exclude<SoftwareInstallStatus, "uninstalled"> | null;
|
||||
installed_versions: ISoftwareInstallVersion[] | null;
|
||||
|
||||
+15
-12
@@ -1,5 +1,6 @@
|
||||
import React, { useContext, useEffect } from "react";
|
||||
import { InjectedRouter } from "react-router";
|
||||
import { isAxiosError } from "axios";
|
||||
|
||||
import PATHS from "router/paths";
|
||||
import { LEARN_MORE_ABOUT_BASE_LINK } from "utilities/constants";
|
||||
@@ -8,7 +9,6 @@ import { buildQueryStringFromParams, QueryParams } from "utilities/url";
|
||||
import softwareAPI, {
|
||||
MAX_FILE_SIZE_BYTES,
|
||||
MAX_FILE_SIZE_MB,
|
||||
UPLOAD_TIMEOUT,
|
||||
} from "services/entities/software";
|
||||
|
||||
import { NotificationContext } from "context/notification";
|
||||
@@ -43,8 +43,6 @@ const SoftwareCustomPackage = ({
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
let timeout: NodeJS.Timeout;
|
||||
|
||||
const beforeUnloadHandler = (e: BeforeUnloadEvent) => {
|
||||
e.preventDefault();
|
||||
// Next line with e.returnValue is included for legacy support
|
||||
@@ -55,9 +53,6 @@ const SoftwareCustomPackage = ({
|
||||
// set up event listener to prevent user from leaving page while uploading
|
||||
if (uploadDetails) {
|
||||
addEventListener("beforeunload", beforeUnloadHandler);
|
||||
timeout = setTimeout(() => {
|
||||
removeEventListener("beforeunload", beforeUnloadHandler);
|
||||
}, UPLOAD_TIMEOUT);
|
||||
} else {
|
||||
removeEventListener("beforeunload", beforeUnloadHandler);
|
||||
}
|
||||
@@ -65,7 +60,6 @@ const SoftwareCustomPackage = ({
|
||||
// clean up event listener and timeout on component unmount
|
||||
return () => {
|
||||
removeEventListener("beforeunload", beforeUnloadHandler);
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
}, [uploadDetails]);
|
||||
|
||||
@@ -103,9 +97,11 @@ const SoftwareCustomPackage = ({
|
||||
await softwareAPI.addSoftwarePackage({
|
||||
data: formData,
|
||||
teamId: currentTeamId,
|
||||
timeout: UPLOAD_TIMEOUT,
|
||||
onUploadProgress: (progressEvent) => {
|
||||
setUploadProgress(progressEvent.progress || 0);
|
||||
const progress = progressEvent.progress || 0;
|
||||
// for large uploads it seems to take a bit for the server to finalize its response so we'll keep the
|
||||
// progress bar at 97% until the server response is received
|
||||
setUploadProgress(Math.max(progress - 0.03, 0.01));
|
||||
},
|
||||
});
|
||||
renderFlash(
|
||||
@@ -128,10 +124,17 @@ const SoftwareCustomPackage = ({
|
||||
`${PATHS.SOFTWARE_TITLES}?${buildQueryStringFromParams(newQueryParams)}`
|
||||
);
|
||||
} catch (e) {
|
||||
const isTimeout =
|
||||
isAxiosError(e) &&
|
||||
(e.response?.status === 504 || e.response?.status === 408);
|
||||
const reason = getErrorReason(e);
|
||||
if (
|
||||
reason.includes("Couldn't add. Fleet couldn't read the version from")
|
||||
) {
|
||||
|
||||
if (isTimeout) {
|
||||
renderFlash(
|
||||
"error",
|
||||
`Couldn’t upload. Request timeout. Please make sure your server and load balancer timeout is long enough.`
|
||||
);
|
||||
} else if (reason.includes("Fleet couldn't read the version from")) {
|
||||
renderFlash(
|
||||
"error",
|
||||
<>
|
||||
|
||||
+5
-1
@@ -25,7 +25,11 @@ const EmptyFleetAppsTable = () => (
|
||||
info={
|
||||
<>
|
||||
Can' find app?{" "}
|
||||
<CustomLink newTab url="" text="File and issue on GitHub" />
|
||||
<CustomLink
|
||||
newTab
|
||||
url="https://github.com/fleetdm/fleet/issues/new/choose"
|
||||
text="File an issue on GitHub"
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
+14
-10
@@ -1,6 +1,7 @@
|
||||
import React, { useContext, useState, useEffect } from "react";
|
||||
import { InjectedRouter } from "react-router";
|
||||
import classnames from "classnames";
|
||||
import { isAxiosError } from "axios";
|
||||
|
||||
import { getErrorReason } from "interfaces/errors";
|
||||
|
||||
@@ -8,7 +9,6 @@ import { NotificationContext } from "context/notification";
|
||||
import softwareAPI, {
|
||||
MAX_FILE_SIZE_BYTES,
|
||||
MAX_FILE_SIZE_MB,
|
||||
UPLOAD_TIMEOUT,
|
||||
} from "services/entities/software";
|
||||
|
||||
import { LEARN_MORE_ABOUT_BASE_LINK } from "utilities/constants";
|
||||
@@ -76,8 +76,6 @@ const EditSoftwareModal = ({
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
let timeout: NodeJS.Timeout;
|
||||
|
||||
const beforeUnloadHandler = (e: BeforeUnloadEvent) => {
|
||||
e.preventDefault();
|
||||
// Next line with e.returnValue is included for legacy support
|
||||
@@ -88,9 +86,6 @@ const EditSoftwareModal = ({
|
||||
// set up event listener to prevent user from leaving page while uploading
|
||||
if (isUpdatingSoftware) {
|
||||
addEventListener("beforeunload", beforeUnloadHandler);
|
||||
timeout = setTimeout(() => {
|
||||
removeEventListener("beforeunload", beforeUnloadHandler);
|
||||
}, UPLOAD_TIMEOUT);
|
||||
} else {
|
||||
removeEventListener("beforeunload", beforeUnloadHandler);
|
||||
}
|
||||
@@ -98,7 +93,6 @@ const EditSoftwareModal = ({
|
||||
// clean up event listener and timeout on component unmount
|
||||
return () => {
|
||||
removeEventListener("beforeunload", beforeUnloadHandler);
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
}, [isUpdatingSoftware]);
|
||||
|
||||
@@ -126,9 +120,11 @@ const EditSoftwareModal = ({
|
||||
data: formData,
|
||||
softwareId,
|
||||
teamId,
|
||||
timeout: UPLOAD_TIMEOUT,
|
||||
onUploadProgress: (progressEvent) => {
|
||||
setUploadProgress(progressEvent.progress || 0);
|
||||
const progress = progressEvent.progress || 0;
|
||||
// for large uploads it seems to take a bit for the server to finalize its response so we'll keep the
|
||||
// progress bar at 97% until the server response is received
|
||||
setUploadProgress(Math.max(progress - 0.03, 0.01));
|
||||
},
|
||||
});
|
||||
|
||||
@@ -144,8 +140,16 @@ const EditSoftwareModal = ({
|
||||
onExit();
|
||||
refetchSoftwareTitle();
|
||||
} catch (e) {
|
||||
const isTimeout =
|
||||
isAxiosError(e) &&
|
||||
(e.response?.status === 504 || e.response?.status === 408);
|
||||
const reason = getErrorReason(e);
|
||||
if (reason.includes("Fleet couldn't read the version from")) {
|
||||
if (isTimeout) {
|
||||
renderFlash(
|
||||
"error",
|
||||
`Couldn’t upload. Request timeout. Please make sure your server and load balancer timeout is long enough.`
|
||||
);
|
||||
} else if (reason.includes("Fleet couldn't read the version from")) {
|
||||
renderFlash(
|
||||
"error",
|
||||
<>
|
||||
|
||||
+3
-3
@@ -98,7 +98,7 @@ const SelectQueryModal = ({
|
||||
);
|
||||
};
|
||||
|
||||
const renderResults = (): JSX.Element => {
|
||||
const renderQueries = (): JSX.Element => {
|
||||
if (queryErrors) {
|
||||
return <DataError />;
|
||||
}
|
||||
@@ -145,7 +145,7 @@ const SelectQueryModal = ({
|
||||
iconSvg="search"
|
||||
iconPosition="start"
|
||||
/>
|
||||
<div>{queryList}</div>
|
||||
<div className={`${baseClass}__query-selection`}>{queryList}</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -188,7 +188,7 @@ const SelectQueryModal = ({
|
||||
>
|
||||
<>
|
||||
{renderDescription()}
|
||||
{renderResults()}
|
||||
{renderQueries()}
|
||||
</>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -31,4 +31,10 @@
|
||||
font-weight: $bold;
|
||||
}
|
||||
}
|
||||
|
||||
&__query-selection {
|
||||
.children-wrapper {
|
||||
width: 680px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import React from "react";
|
||||
import { CellProps, Column } from "react-table";
|
||||
|
||||
import { IHostSoftware, SOURCE_TYPE_CONVERSION } from "interfaces/software";
|
||||
import {
|
||||
IHostSoftware,
|
||||
SoftwareSource,
|
||||
SOURCE_TYPE_CONVERSION,
|
||||
} from "interfaces/software";
|
||||
import { IHeaderProps, IStringCellProps } from "interfaces/datatable_config";
|
||||
|
||||
import HeaderCell from "components/TableContainer/DataTable/HeaderCell/HeaderCell";
|
||||
@@ -21,7 +25,7 @@ type IInstalledVersionsCellProps = CellProps<
|
||||
>;
|
||||
type IVulnerabilitiesCellProps = IInstalledVersionsCellProps;
|
||||
|
||||
const formatSoftwareType = (source: string) => {
|
||||
const formatSoftwareType = (source: SoftwareSource) => {
|
||||
const DICT = SOURCE_TYPE_CONVERSION;
|
||||
return DICT[source] || "Unknown";
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
IHostSoftware,
|
||||
IHostSoftwarePackage,
|
||||
SoftwareInstallStatus,
|
||||
SoftwareSource,
|
||||
formatSoftwareType,
|
||||
isIpadOrIphoneSoftwareSource,
|
||||
} from "interfaces/software";
|
||||
@@ -201,7 +202,11 @@ export const generateSoftwareTableHeaders = ({
|
||||
Cell: (cellProps: ITableStringCellProps) => (
|
||||
<TextCell
|
||||
value={cellProps.cell.value}
|
||||
formatter={() => formatSoftwareType({ source: cellProps.cell.value })}
|
||||
formatter={() =>
|
||||
formatSoftwareType({
|
||||
source: cellProps.cell.value as SoftwareSource,
|
||||
})
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
|
||||
+2
-1
@@ -4,6 +4,7 @@ import { Tab, TabList, TabPanel, Tabs } from "react-tabs";
|
||||
import {
|
||||
IHostSoftware,
|
||||
ISoftwareInstallVersion,
|
||||
SoftwareSource,
|
||||
formatSoftwareType,
|
||||
hasHostSoftwareAppLastInstall,
|
||||
hasHostSoftwarePackageLastInstall,
|
||||
@@ -37,7 +38,7 @@ const generateVulnerabilitiesValue = (vulnerabilities: string[]) => {
|
||||
|
||||
interface ISoftwareDetailsInfoProps {
|
||||
installedVersion: ISoftwareInstallVersion;
|
||||
source: string;
|
||||
source: SoftwareSource;
|
||||
bundleIdentifier?: string;
|
||||
}
|
||||
|
||||
|
||||
+8
-9
@@ -21,17 +21,14 @@ import CustomLink from "components/CustomLink";
|
||||
import Button from "components/buttons/Button";
|
||||
import { ISoftwareTitle } from "interfaces/software";
|
||||
|
||||
const getPlatformDisplayFromPackageSuffix = (packageName: string) => {
|
||||
const split = packageName.split(".");
|
||||
const suff = split[split.length - 1];
|
||||
switch (suff) {
|
||||
const getPlatformDisplayFromPackageExtension = (ext: string | undefined) => {
|
||||
switch (ext) {
|
||||
case "pkg":
|
||||
return "macOS";
|
||||
case "deb":
|
||||
case "rpm":
|
||||
return "Linux";
|
||||
case "exe":
|
||||
return "Windows";
|
||||
case "msi":
|
||||
return "Windows";
|
||||
default:
|
||||
@@ -153,10 +150,12 @@ const InstallSoftwareModal = ({
|
||||
);
|
||||
|
||||
const availableSoftwareOptions = titlesAFI?.map((title) => {
|
||||
const platformDisplay = getPlatformDisplayFromPackageSuffix(
|
||||
title.software_package?.name ?? ""
|
||||
);
|
||||
const platformString = platformDisplay ? `${platformDisplay} • ` : "";
|
||||
const splitName = title.software_package?.name.split(".") ?? "";
|
||||
const ext =
|
||||
splitName.length > 1 ? splitName[splitName.length - 1] : undefined;
|
||||
const platformString = ext
|
||||
? `${getPlatformDisplayFromPackageExtension(ext)} (.${ext}) • `
|
||||
: "";
|
||||
return {
|
||||
label: title.name,
|
||||
value: title.id,
|
||||
|
||||
@@ -143,8 +143,7 @@ interface IAddFleetMaintainedAppPostBody {
|
||||
const ORDER_KEY = "name";
|
||||
const ORDER_DIRECTION = "asc";
|
||||
|
||||
export const UPLOAD_TIMEOUT = (8 * 60 + 15) * 1000;
|
||||
export const MAX_FILE_SIZE_MB = 500;
|
||||
export const MAX_FILE_SIZE_MB = 3000;
|
||||
export const MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024;
|
||||
|
||||
export default {
|
||||
|
||||
@@ -19,6 +19,7 @@ require (
|
||||
github.com/beevik/etree v1.3.0
|
||||
github.com/beevik/ntp v0.3.0
|
||||
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb
|
||||
github.com/boltdb/bolt v1.3.1
|
||||
github.com/briandowns/spinner v1.23.1
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible
|
||||
github.com/cenkalti/backoff/v4 v4.3.0
|
||||
@@ -223,6 +224,7 @@ require (
|
||||
github.com/fatih/structs v1.1.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/garyburd/go-oauth v0.0.0-20180319155456-bca2e7f09a17 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.5.0 // indirect
|
||||
github.com/go-logfmt/logfmt v0.5.1 // indirect
|
||||
|
||||
@@ -301,6 +301,8 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB
|
||||
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
|
||||
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb h1:m935MPodAbYS46DG4pJSv7WO+VECIWUQ7OJYSoTrMh4=
|
||||
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI=
|
||||
github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4=
|
||||
github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
|
||||
github.com/briandowns/spinner v1.23.1 h1:t5fDPmScwUjozhDj4FA46p5acZWIPXYE30qW2Ptu650=
|
||||
github.com/briandowns/spinner v1.23.1/go.mod h1:LaZeM4wm2Ywy6vO571mvhQNRcWfRUnXOs0RcKV0wYKM=
|
||||
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||
@@ -463,6 +465,8 @@ github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4
|
||||
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/garyburd/go-oauth v0.0.0-20180319155456-bca2e7f09a17 h1:GOfMz6cRgTJ9jWV0qAezv642OhPnKEG7gtUjJSdStHE=
|
||||
github.com/garyburd/go-oauth v0.0.0-20180319155456-bca2e7f09a17/go.mod h1:HfkOCN6fkKKaPSAeNq/er3xObxTW4VLeY6UUK895gLQ=
|
||||
github.com/getsentry/sentry-go v0.18.0 h1:MtBW5H9QgdcJabtZcuJG80BMOwaBpkRDZkxRkNC1sN0=
|
||||
github.com/getsentry/sentry-go v0.18.0/go.mod h1:Kgon4Mby+FJ7ZWHFUAZgVaIa8sxHtnRJRLTXZr51aKQ=
|
||||
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
|
||||
|
||||
@@ -320,7 +320,7 @@ Here are the steps hiring managers follow to get an offer out to a candidate:
|
||||
|
||||
3. **Compile feedback into a single doc:** In the "interview packet", include feedback from interviews, reference checks, and challenge submissions. Include any other notes you can think of offhand, and embed links to any supporting documents that were impactful in your final decision-making, such as portfolios or challenge submissions.
|
||||
- Name the doc with a short, formulaic name that's easy to understand in an instant from just an email subject line (e.g. "_Why hire Jane Doe ("Train Conductor") - 2023-03-21_").
|
||||
- _Share_ this single document with the CEO.
|
||||
- _Share_ this single document with the [CEO and Head of Digital Experience](https://fleetdm.com/handbook/digital-experience#team).
|
||||
4. **Request a CEO interview:** Copy the template below, paste it in the hiring Slack channel for the position, and complete all "TODOs" before sending.
|
||||
|
||||
```
|
||||
|
||||
@@ -16,7 +16,7 @@ This handbook page details processes specific to working [with](#contact-us) and
|
||||
- Please use **issue comments and GitHub mentions** to communicate follow-ups or answer questions related to your request.
|
||||
- Any Fleet team member can [view the kanban board](https://app.zenhub.com/workspaces/g-demand-64e6c8e2d35c7f001a457b7f/board?sprints=none) for this department, including pending tasks and the status of new requests.
|
||||
|
||||
> To **make a request** related to **product marketing**, **press**, **brandfronts**, **pitchfronts**, **featurefronts**, **ideal customer profiles (ICPs)**, **personas**, or **targeting** [create an issue](https://github.com/fleetdm/confidential/issues/new?assignees=&labels=%23g-digital-experience&projects=&template=custom-request.md&title=Product%20marketing%20request%3A+_______________________) (If urgent, at-mention the [Head of Product Marketing](#team) in the [#help-leadership](https://fleetdm.slack.com/archives/C0600L1TTPY) Slack channel).
|
||||
> To **make a request** related to **product marketing**, **brand**, **press**, **brandfronts**, **pitchfronts**, **featurefronts**, **ideal customer profiles (ICPs)**, **personas**, or **targeting** [create an issue](https://github.com/fleetdm/confidential/issues/new?assignees=&labels=%23g-digital-experience&projects=&template=custom-request.md&title=Product%20marketing%20request%3A+_______________________) (If urgent, at-mention the [CEO](https://fleetdm.com/handbook/digital-experience#team) in the [#help-leadership](https://fleetdm.slack.com/archives/C0600L1TTPY) Slack channel).
|
||||
|
||||
|
||||
## Responsibilities
|
||||
|
||||
@@ -57,7 +57,7 @@ Once notified, Digital Experience takes the following steps:
|
||||
|
||||
### Inform managers about hours worked
|
||||
|
||||
Every Friday at 2:00 PM CT, we collect hours worked for all hourly employees at Fleet, including core team members and consultants, regardless of their location.
|
||||
Every Friday, we collect hours worked for all hourly employees at Fleet, including core team members and consultants, regardless of their location.
|
||||
|
||||
Here's how:
|
||||
|
||||
@@ -130,12 +130,13 @@ When Digital Experience receives notification of a Fleetie's manager changing, f
|
||||
|
||||
> **Note:** The Fleeties spreadsheet is the source of truth for who everyone's manager is and their job titles.
|
||||
|
||||
|
||||
### Recognize employee workiversaries
|
||||
|
||||
At Fleet, everyone is recognized on their [workiversary](https://fleetdm.com/handbook/company/communications#workiversaries). To ensure this happens, take the following steps:
|
||||
|
||||
1. Bimonthly, use [Fleeties (private google doc)](https://docs.google.com/spreadsheets/d/1OSLn-ZCbGSjPusHPiR5dwQhheH1K8-xqyZdsOe9y7qc/edit#gid=0) to determine who is celebrating their workiversary in the following two months.
|
||||
2. Post in the #help-classifed Slack channel and cc the Head of Digital Experience. Use the following template:
|
||||
1. On the 15th of every month, use [Fleeties (private google doc)](https://docs.google.com/spreadsheets/d/1OSLn-ZCbGSjPusHPiR5dwQhheH1K8-xqyZdsOe9y7qc/edit#gid=0) to determine who is celebrating their workiversary in the next month.
|
||||
2. At-mention the Head of Digital Experience. Use the following template:
|
||||
|
||||
|
||||
```
|
||||
@@ -143,7 +144,6 @@ At Fleet, everyone is recognized on their [workiversary](https://fleetdm.com/han
|
||||
[workiversary date (DD-MMM)] - [teammate name] - [number of years at Fleet]
|
||||
```
|
||||
|
||||
|
||||
The Head of Digital Experience will also use this post to update the [All hands](https://fleetdm.com/handbook/company/communications#all-hands) deck.
|
||||
3. On the day prior to a workiversary, send the teammate’s manager a DM on Slack:
|
||||
|
||||
@@ -191,6 +191,7 @@ Annually, around mid-year, Fleet will be prompted by Gusto to review company ben
|
||||
4. Approximately 2-3 months after survery completion, Gusto will suggest plans based on Fleet's responses. Choose plans with minimal changes.
|
||||
5. Gusto will offer these plans to employees during open enrollment, with new coverage starting 3-4 weeks afterward.
|
||||
|
||||
|
||||
### Grant equity
|
||||
|
||||
Equity grants for new hires are queued up as part of the [hiring process](https://fleetdm.com/handbook/digital-experience#hiring), then grants and consents are [batched and processed quarterly](https://github.com/fleetdm/confidential/issues/new/choose).
|
||||
@@ -520,7 +521,7 @@ Use the following steps to schedule an interview between a candidate and the CEO
|
||||
1. Once you receive a [CEO interview request](https://fleetdm.com/handbook/company/leadership#hiring-a-new-team-member), apply the "eyes" (👀) emoji to the Slack post to acknowledge you've seen the request.
|
||||
2. Reach out to the candidate via email to find a time when the CEO and candidate are both available.
|
||||
> This entire process takes an hour for the CEO: a 30-minute interview followed by a 30-minute "¶¶ Postgame" Be sure to offer times that accommodate this.
|
||||
3. [Make a copy of the "¶¶ CEO interview template"](https://docs.google.com/document/d/1yARlH6iZY-cP9cQbmL3z6TbMy-Ii7lO64RbuolpWQzI/copy) (private Google doc) and move it to the "[🕵️ ¶±¶ Reference checks & hiring data](https://drive.google.com/drive/folders/1VgKT6_VrQ9zYMnDOwJGE1mT1WrrMFqJw?usp=drive_link)" folder in Google Drive.
|
||||
3. [Make a copy of the "¶¶ CEO interview template"](https://docs.google.com/document/d/1yARlH6iZY-cP9cQbmL3z6TbMy-Ii7lO64RbuolpWQzI/copy) (private Google doc) and move it to the "[¶¶ Interview feedback](https://drive.google.com/drive/folders/1v5Z1WB9S855hLZMUWgOiXA_ei2EpEGlA?usp=drive_link)" folder in Google Drive.
|
||||
4. Prep the CEO interview doc:
|
||||
- Change file name and heading of doc to `¶¶ CANDIDATE_NAME (CANDIDATE_TITLE) <> Mike McNeil, CEO final interview (YYYY-MM-DD)`.
|
||||
- Add candidate's personal email in the "👥" (attendees) section at the top of the doc.
|
||||
|
||||
@@ -210,10 +210,13 @@
|
||||
-
|
||||
task: "Recognize and benchmark workiversaries"
|
||||
startedOn: "2024-07-15"
|
||||
frequency: "Bimonthly"
|
||||
frequency: "Monthly"
|
||||
description: "Identify workiversaries coming up in the next two months and follow the steps to ensure they're recognized and benchmarked"
|
||||
moreInfoUrl: "https://fleetdm.com/handbook/digital-experience#recognize-employee-workiversaries"
|
||||
dri: "sampfluger88"
|
||||
dri: "SFriendLee"
|
||||
autoIssue:
|
||||
labels: [ "#g-digital-experience" ]
|
||||
repo: "confidential"
|
||||
-
|
||||
task: "Quarterly grants"
|
||||
startedOn: "2024-02-01"
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
task: "Sprint kickoff review" # 2024-03-06 TODO: Link to responsibility or corresponding "how to" info e.g. https://fleetdm.com/handbook/company/product-groups#making-changes
|
||||
startedOn: "2024-03-07"
|
||||
frequency: "Triweekly"
|
||||
description: "Identify stories that did not make it into this sprint and remove them from the board. Notify relevant requesters/stakeholders. Ensure bugs have been effectively prioritized across teams. Recommend highlights for next release notes. Record the number of drops for KPI reporting. Consider product group staffing. Are we scheduling what we prioritized? Did we finish what we scheduled in the sprint? (Look at org chart.)"
|
||||
description: "Review stories that made it into this sprint and recommend highlights for next release notes. Then, review stories that did not make it into this sprint. Ensure stories/bugs have been effectively prioritized across teams."
|
||||
moreInfoUrl:
|
||||
dri: "noahtalerman"
|
||||
-
|
||||
|
||||
+2
-2
@@ -7,8 +7,8 @@ Following are the currently deployed versions of fleetd components on the `stabl
|
||||
|
||||
| Component\OS | macOS | Linux | Windows | Linux (arm64) |
|
||||
|--------------|--------------|--------|---------|---------------|
|
||||
| orbit | 1.33.0 | 1.33.0 | 1.33.0 | 1.33.0 |
|
||||
| desktop | 1.33.0 | 1.33.0 | 1.33.0 | 1.33.0 |
|
||||
| orbit | 1.34.0 | 1.34.0 | 1.34.0 | 1.34.0 |
|
||||
| desktop | 1.34.0 | 1.34.0 | 1.34.0 | 1.34.0 |
|
||||
| osqueryd | 5.13.1 | 5.13.1 | 5.13.1 | 5.13.1 |
|
||||
| nudge | 1.1.10.81462 | - | - | - |
|
||||
| swiftDialog | 2.1.0 | - | - | - |
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"evented": false,
|
||||
"cacheable": false,
|
||||
"notes": "- The values in this OpenDirectory table are related to account creation. In the past, it was fairly common to use OpenDirectory to have a home folder (`~`) on a server, and then log in and get that folder wherever they are. (These days, this use case is more uncommon.)\n- To determine who is logged in to the Mac, or for example, to check the record name versus the computer's \"short name\", consider using the data in [the DSCL table](https://fleetdm.com/tables/dscl).\n- Many installers incorporate scripts due to actions that are handled by pre or post-scripts vs installer package payloads. These script actions aren't tracked in the \"bill of materials\" (.bom) file. So, don't blindly trust the \"bill of materials\" (.bom) file as the source of truth on what has or hasn't been installed.",
|
||||
"examples": "Query the creation date of user accounts. You could also query the date of the last failed login attempt or password change.\n\n```\nSELECT strftime('%Y-%m-%d %H:%M:%S',creation_time,'unixepoch') AS creationdate FROM account_policy_data;\n```\n\nSee each user's last password set date and number of failed logins since last successful login to detect any intrusion attempts.\n\n```\nSELECT u.username u.uid, strftime('%Y-%m-%dT%H:%M:%S', a.password_last_set_time, 'unixepoch') AS password_last_set_time, a.failed_login_count, strftime('%Y-%m-%dT%H:%M:%S', a.failed_login_timestamp, 'unixepoch') AS failed_login_timestamp FROM account_policy_data AS a CROSS JOIN users AS u USING (uid) ORDER BY password_last_set_time ASC;\n```",
|
||||
"examples": "Query the creation date of user accounts. You could also query the date of the last failed login attempt or password change.\n\n```\nSELECT strftime('%Y-%m-%d %H:%M:%S',creation_time,'unixepoch') AS creationdate FROM account_policy_data;\n```\n\nSee each user's last password set date and number of failed logins since last successful login to detect any intrusion attempts.\n\n```\nSELECT u.username, u.uid, strftime('%Y-%m-%dT%H:%M:%S', a.password_last_set_time, 'unixepoch') AS password_last_set_time, a.failed_login_count, strftime('%Y-%m-%dT%H:%M:%S', a.failed_login_timestamp, 'unixepoch') AS failed_login_timestamp FROM account_policy_data AS a CROSS JOIN users AS u USING (uid) ORDER BY password_last_set_time ASC;\n```",
|
||||
"columns": [
|
||||
{
|
||||
"name": "uid",
|
||||
|
||||
@@ -19,5 +19,5 @@ examples: |-
|
||||
See each user's last password set date and number of failed logins since last successful login to detect any intrusion attempts.
|
||||
|
||||
```
|
||||
SELECT u.username u.uid, strftime('%Y-%m-%dT%H:%M:%S', a.password_last_set_time, 'unixepoch') AS password_last_set_time, a.failed_login_count, strftime('%Y-%m-%dT%H:%M:%S', a.failed_login_timestamp, 'unixepoch') AS failed_login_timestamp FROM account_policy_data AS a CROSS JOIN users AS u USING (uid) ORDER BY password_last_set_time ASC;
|
||||
SELECT u.username, u.uid, strftime('%Y-%m-%dT%H:%M:%S', a.password_last_set_time, 'unixepoch') AS password_last_set_time, a.failed_login_count, strftime('%Y-%m-%dT%H:%M:%S', a.failed_login_timestamp, 'unixepoch') AS failed_login_timestamp FROM account_policy_data AS a CROSS JOIN users AS u USING (uid) ORDER BY password_last_set_time ASC;
|
||||
```
|
||||
|
||||
@@ -15,6 +15,8 @@ import (
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
var automationActivityAuthor = "Fleet"
|
||||
|
||||
// NewActivity stores an activity item that the user performed
|
||||
func (ds *Datastore) NewActivity(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
@@ -39,6 +41,10 @@ func (ds *Datastore) NewActivity(
|
||||
}
|
||||
userName = &user.Name
|
||||
userEmail = &user.Email
|
||||
} else if ranScriptActivity, ok := activity.(fleet.ActivityTypeRanScript); ok {
|
||||
if ranScriptActivity.PolicyID != nil {
|
||||
userName = &automationActivityAuthor
|
||||
}
|
||||
}
|
||||
|
||||
cols := []string{"user_id", "user_name", "activity_type", "details", "created_at"}
|
||||
@@ -293,7 +299,7 @@ func (ds *Datastore) ListHostUpcomingActivities(ctx context.Context, hostID uint
|
||||
// list pending scripts
|
||||
`SELECT
|
||||
hsr.execution_id as uuid,
|
||||
u.name as name,
|
||||
IF(hsr.policy_id IS NOT NULL, 'Fleet', u.name) as name,
|
||||
u.id as user_id,
|
||||
u.gravatar_url as gravatar_url,
|
||||
u.email as user_email,
|
||||
@@ -304,12 +310,16 @@ func (ds *Datastore) ListHostUpcomingActivities(ctx context.Context, hostID uint
|
||||
'host_display_name', COALESCE(hdn.display_name, ''),
|
||||
'script_name', COALESCE(scr.name, ''),
|
||||
'script_execution_id', hsr.execution_id,
|
||||
'async', NOT hsr.sync_request
|
||||
'async', NOT hsr.sync_request,
|
||||
'policy_id', hsr.policy_id,
|
||||
'policy_name', p.name
|
||||
) as details
|
||||
FROM
|
||||
host_script_results hsr
|
||||
LEFT OUTER JOIN
|
||||
users u ON u.id = hsr.user_id
|
||||
LEFT OUTER JOIN
|
||||
policies p ON p.id = hsr.policy_id
|
||||
LEFT OUTER JOIN
|
||||
host_display_names hdn ON hdn.host_id = hsr.host_id
|
||||
LEFT OUTER JOIN
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package tables
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func init() {
|
||||
MigrationClient.AddMigration(Up_20241004005000, Down_20241004005000)
|
||||
}
|
||||
|
||||
func Up_20241004005000(tx *sql.Tx) error {
|
||||
if _, err := tx.Exec(`
|
||||
ALTER TABLE host_script_results
|
||||
ADD COLUMN policy_id INT UNSIGNED DEFAULT NULL,
|
||||
ADD FOREIGN KEY fk_script_result_policy_id (policy_id) REFERENCES policies (id) ON DELETE SET NULL
|
||||
`); err != nil {
|
||||
return fmt.Errorf("failed to add policy_id to host script results: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Down_20241004005000(tx *sql.Tx) error {
|
||||
return nil
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package tables
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestUp_20241004005000(t *testing.T) {
|
||||
db := applyUpToPrev(t)
|
||||
|
||||
// insert a team
|
||||
teamID := execNoErrLastID(t, db, `INSERT INTO teams (name) VALUES ("Foo")`)
|
||||
|
||||
// insert a policy
|
||||
policyID := execNoErrLastID(t, db, `INSERT INTO policies (name, query, description, team_id, checksum)
|
||||
VALUES ('test_policy', "SELECT 1", "", ?, "a123b123")`, teamID)
|
||||
|
||||
// insert a script
|
||||
scriptContentID := execNoErrLastID(t, db, `INSERT INTO script_contents (md5_checksum, contents) VALUES ("md5", "echo 'Hello World'")`)
|
||||
scriptID := execNoErrLastID(t, db, `INSERT INTO scripts (
|
||||
team_id, global_or_team_id, name, script_content_id
|
||||
) VALUES (?, ?, "hello-world.sh", ?)`, teamID, teamID, scriptContentID)
|
||||
|
||||
// Apply current migration.
|
||||
applyNext(t, db)
|
||||
|
||||
// insert a script result
|
||||
hostScriptResultID := execNoErrLastID(t, db, `INSERT INTO host_script_results
|
||||
(host_id, execution_id, script_content_id, output, script_id, policy_id, user_id, sync_request)
|
||||
VALUES (1, 'a123b123', ?, '', ?, ?, NULL, FALSE)`, scriptContentID, scriptID, policyID)
|
||||
|
||||
// delete the associated policy
|
||||
execNoErr(t, db, `DELETE FROM policies WHERE id = ?`, policyID)
|
||||
|
||||
// policy ID should be null but script result should still exist
|
||||
var count int
|
||||
err := db.Get(&count, "SELECT COUNT(*) FROM host_script_results WHERE policy_id IS NULL AND id = ?", hostScriptResultID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, count)
|
||||
}
|
||||
@@ -26,8 +26,10 @@ const policyCols = `
|
||||
p.calendar_events_enabled, p.software_installer_id, p.script_id
|
||||
`
|
||||
|
||||
var errSoftwareTitleIDOnGlobalPolicy = errors.New("install software title id can be only be set on team policies")
|
||||
var errScriptIDOnGlobalPolicy = errors.New("run script id can only be set on team or \"no team\" policies")
|
||||
var (
|
||||
errSoftwareTitleIDOnGlobalPolicy = errors.New("install software title id can be only be set on team policies")
|
||||
errScriptIDOnGlobalPolicy = errors.New("run script id can only be set on team or \"no team\" policies")
|
||||
)
|
||||
|
||||
var policySearchColumns = []string{"p.name"}
|
||||
|
||||
@@ -123,7 +125,7 @@ func (ds *Datastore) PolicyLite(ctx context.Context, id uint) (*fleet.PolicyLite
|
||||
var policy fleet.PolicyLite
|
||||
err := sqlx.GetContext(
|
||||
ctx, ds.reader(ctx), &policy,
|
||||
`SELECT id, description, resolution FROM policies WHERE id=?`, id,
|
||||
`SELECT id, name, description, resolution FROM policies WHERE id=?`, id,
|
||||
)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
@@ -177,8 +179,10 @@ func (ds *Datastore) SavePolicy(ctx context.Context, p *fleet.Policy, shouldRemo
|
||||
)
|
||||
}
|
||||
|
||||
var errMismatchedInstallerTeam = &fleet.BadRequestError{Message: "software installer is associated with a different team"}
|
||||
var errMismatchedScriptTeam = &fleet.BadRequestError{Message: "script is associated with a different team"}
|
||||
var (
|
||||
errMismatchedInstallerTeam = &fleet.BadRequestError{Message: "software installer is associated with a different team"}
|
||||
errMismatchedScriptTeam = &fleet.BadRequestError{Message: "script is associated with a different team"}
|
||||
)
|
||||
|
||||
func (ds *Datastore) assertTeamMatches(ctx context.Context, teamID uint, softwareInstallerID *uint, scriptID *uint) error {
|
||||
if softwareInstallerID != nil {
|
||||
@@ -382,10 +386,12 @@ func (ds *Datastore) RecordPolicyQueryExecutions(ctx context.Context, host *flee
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(results) > 0 {
|
||||
if err := ds.UpdateHostIssuesFailingPolicies(ctx, []uint{host.ID}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// ds.UpdateHostIssuesFailingPolicies should be executed even if len(results) == 0
|
||||
// because this means the host is configured to run no policies and we would like
|
||||
// to cleanup the counts (if any).
|
||||
if err := ds.UpdateHostIssuesFailingPolicies(ctx, []uint{host.ID}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if deferredSaveHost {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -39,8 +39,8 @@ func (ds *Datastore) NewHostScriptExecutionRequest(ctx context.Context, request
|
||||
|
||||
func newHostScriptExecutionRequest(ctx context.Context, tx sqlx.ExtContext, request *fleet.HostScriptRequestPayload) (*fleet.HostScriptResult, error) {
|
||||
const (
|
||||
insStmt = `INSERT INTO host_script_results (host_id, execution_id, script_content_id, output, script_id, user_id, sync_request) VALUES (?, ?, ?, '', ?, ?, ?)`
|
||||
getStmt = `SELECT hsr.id, hsr.host_id, hsr.execution_id, hsr.created_at, hsr.script_id, hsr.user_id, hsr.sync_request, sc.contents as script_contents FROM host_script_results hsr JOIN script_contents sc WHERE sc.id = hsr.script_content_id AND hsr.id = ?`
|
||||
insStmt = `INSERT INTO host_script_results (host_id, execution_id, script_content_id, output, script_id, policy_id, user_id, sync_request) VALUES (?, ?, ?, '', ?, ?, ?, ?)`
|
||||
getStmt = `SELECT hsr.id, hsr.host_id, hsr.execution_id, hsr.created_at, hsr.script_id, hsr.policy_id, hsr.user_id, hsr.sync_request, sc.contents as script_contents FROM host_script_results hsr JOIN script_contents sc WHERE sc.id = hsr.script_content_id AND hsr.id = ?`
|
||||
)
|
||||
|
||||
execID := uuid.New().String()
|
||||
@@ -49,6 +49,7 @@ func newHostScriptExecutionRequest(ctx context.Context, tx sqlx.ExtContext, requ
|
||||
execID,
|
||||
request.ScriptContentID,
|
||||
request.ScriptID,
|
||||
request.PolicyID,
|
||||
request.UserID,
|
||||
request.SyncRequest,
|
||||
)
|
||||
@@ -260,6 +261,7 @@ func (ds *Datastore) getHostScriptExecutionResultDB(ctx context.Context, q sqlx.
|
||||
hsr.execution_id,
|
||||
sc.contents as script_contents,
|
||||
hsr.script_id,
|
||||
hsr.policy_id,
|
||||
hsr.output,
|
||||
hsr.runtime,
|
||||
hsr.exit_code,
|
||||
|
||||
@@ -1166,11 +1166,13 @@ func (a ActivityTypeDisabledWindowsMDM) Documentation() (activity, details, deta
|
||||
}
|
||||
|
||||
type ActivityTypeRanScript struct {
|
||||
HostID uint `json:"host_id"`
|
||||
HostDisplayName string `json:"host_display_name"`
|
||||
ScriptExecutionID string `json:"script_execution_id"`
|
||||
ScriptName string `json:"script_name"`
|
||||
Async bool `json:"async"`
|
||||
HostID uint `json:"host_id"`
|
||||
HostDisplayName string `json:"host_display_name"`
|
||||
ScriptExecutionID string `json:"script_execution_id"`
|
||||
ScriptName string `json:"script_name"`
|
||||
Async bool `json:"async"`
|
||||
PolicyID *uint `json:"policy_id"`
|
||||
PolicyName *string `json:"policy_name"`
|
||||
}
|
||||
|
||||
func (a ActivityTypeRanScript) ActivityName() string {
|
||||
@@ -1188,12 +1190,16 @@ func (a ActivityTypeRanScript) Documentation() (activity, details, detailsExampl
|
||||
- "host_display_name": Display name of the host.
|
||||
- "script_execution_id": Execution ID of the script run.
|
||||
- "script_name": Name of the script (empty if it was an anonymous script).
|
||||
- "async": Whether the script was executed asynchronously.`, `{
|
||||
- "async": Whether the script was executed asynchronously.
|
||||
- "policy_id": ID of the policy whose failure triggered the script run. Null if no associated policy.
|
||||
- "policy_name": Name of the policy whose failure triggered the script run. Null if no associated policy.`, `{
|
||||
"host_id": 1,
|
||||
"host_display_name": "Anna's MacBook Pro",
|
||||
"script_name": "set-timezones.sh",
|
||||
"script_execution_id": "d6cffa75-b5b5-41ef-9230-15073c8a88cf",
|
||||
"async": false
|
||||
"async": false,
|
||||
"policy_id": 123,
|
||||
"policy_name": "Ensure photon torpedoes are primed"
|
||||
}`
|
||||
}
|
||||
|
||||
|
||||
@@ -271,6 +271,8 @@ type PolicyScriptData struct {
|
||||
// PolicyLite is a stripped down version of the policy.
|
||||
type PolicyLite struct {
|
||||
ID uint `db:"id"`
|
||||
// Name is the name of the policy.
|
||||
Name string `db:"name"`
|
||||
// Description describes the policy.
|
||||
Description string `db:"description"`
|
||||
// Resolution describes how to solve a failing policy.
|
||||
|
||||
@@ -138,6 +138,7 @@ func (hs *HostScriptDetail) setLastExecution(executionID *string, executedAt *ti
|
||||
type HostScriptRequestPayload struct {
|
||||
HostID uint `json:"host_id"`
|
||||
ScriptID *uint `json:"script_id"`
|
||||
PolicyID *uint `json:"policy_id"`
|
||||
ScriptContents string `json:"script_contents"`
|
||||
ScriptContentID uint `json:"-"`
|
||||
ScriptName string `json:"script_name"`
|
||||
@@ -217,6 +218,9 @@ type HostScriptResult struct {
|
||||
// ScriptID is the id of the saved script to execute, or nil if this was an
|
||||
// anonymous script execution.
|
||||
ScriptID *uint `json:"script_id" db:"script_id"`
|
||||
// PolicyID is the id of the policy that triggered the script execution, or
|
||||
// nil if the execution was not triggered by a policy failure
|
||||
PolicyID *uint `json:"policy_id" db:"policy_id"`
|
||||
// UserID is the id of the user that requested execution. It is not part of
|
||||
// the rendered JSON as it is only returned by the
|
||||
// /hosts/:id/activities/upcoming endpoint which doesn't use this struct as
|
||||
|
||||
@@ -54,6 +54,29 @@ func Refresh(ctx context.Context, ds fleet.Datastore, logger kitlog.Logger) erro
|
||||
return i.ingest(ctx, apps)
|
||||
}
|
||||
|
||||
// ExtensionForBundleIdentifier returns an extension for the given FMA
|
||||
// identifier. If one can't be found it returns an empty string.
|
||||
//
|
||||
// This function is used because we can't always extract the extension based on
|
||||
// the installer URL.
|
||||
func ExtensionForBundleIdentifier(identifier string) (string, error) {
|
||||
var apps []maintainedApp
|
||||
if err := json.Unmarshal(appsJSON, &apps); err != nil {
|
||||
return "", fmt.Errorf("unmarshal embedded apps.json: %w", err)
|
||||
}
|
||||
|
||||
for _, app := range apps {
|
||||
if app.BundleIdentifier == identifier {
|
||||
formats := strings.Split(app.InstallerFormat, ":")
|
||||
if len(formats) > 0 {
|
||||
return formats[0], nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
type ingester struct {
|
||||
baseURL string
|
||||
ds fleet.Datastore
|
||||
|
||||
@@ -166,3 +166,57 @@ func TestIngestValidations(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtensionForBundleIdentifier(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
identifier string
|
||||
expected string
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
name: "Valid identifier with zip format",
|
||||
identifier: "com.1password.1password",
|
||||
expected: "zip",
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "Valid identifier with dmg format",
|
||||
identifier: "com.adobe.Reader",
|
||||
expected: "dmg",
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "Valid identifier with pkg format",
|
||||
identifier: "com.box.desktop",
|
||||
expected: "pkg",
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "Non-existent identifier",
|
||||
identifier: "com.nonexistent.app",
|
||||
expected: "",
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "Empty identifier",
|
||||
identifier: "",
|
||||
expected: "",
|
||||
expectErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
extension, err := ExtensionForBundleIdentifier(tc.identifier)
|
||||
|
||||
if tc.expectErr {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
require.Equal(t, tc.expected, extension)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +67,8 @@ func (svc *Service) NewActivity(ctx context.Context, user *fleet.User, activity
|
||||
return newActivity(ctx, user, activity, svc.ds, svc.logger)
|
||||
}
|
||||
|
||||
var automationActivityAuthor = "Fleet"
|
||||
|
||||
func newActivity(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, ds fleet.Datastore, logger kitlog.Logger) error {
|
||||
appConfig, err := ds.AppConfig(ctx)
|
||||
if err != nil {
|
||||
@@ -84,6 +86,8 @@ func newActivity(ctx context.Context, user *fleet.User, activity fleet.ActivityD
|
||||
var userID *uint
|
||||
var userName *string
|
||||
var userEmail *string
|
||||
activityType := activity.ActivityName()
|
||||
|
||||
if user != nil {
|
||||
// To support creating activities with users that were deleted. This can happen
|
||||
// for automatically installed software which uses the author of the upload as the author of
|
||||
@@ -93,8 +97,12 @@ func newActivity(ctx context.Context, user *fleet.User, activity fleet.ActivityD
|
||||
}
|
||||
userName = &user.Name
|
||||
userEmail = &user.Email
|
||||
} else if ranScriptActivity, ok := activity.(fleet.ActivityTypeRanScript); ok {
|
||||
if ranScriptActivity.PolicyID != nil {
|
||||
userName = &automationActivityAuthor
|
||||
}
|
||||
}
|
||||
activityType := activity.ActivityName()
|
||||
|
||||
go func() {
|
||||
retryStrategy := backoff.NewExponentialBackOff()
|
||||
retryStrategy.MaxElapsedTime = 30 * time.Minute
|
||||
|
||||
@@ -12149,3 +12149,63 @@ func (s *integrationTestSuite) TestAutofillPolicies() {
|
||||
resp = s.Do("POST", "/api/latest/fleet/autofill/policy", req, http.StatusBadRequest)
|
||||
assertBodyContains(t, resp, "AI features are disabled")
|
||||
}
|
||||
|
||||
func (s *integrationTestSuite) TestHostWithNoPoliciesClearsPolicyCounts() {
|
||||
t := s.T()
|
||||
ctx := context.Background()
|
||||
|
||||
team, err := s.ds.NewTeam(ctx, &fleet.Team{Name: "Zoobar"})
|
||||
require.NoError(t, err)
|
||||
|
||||
host, err := s.ds.NewHost(ctx, &fleet.Host{
|
||||
DetailUpdatedAt: time.Now(),
|
||||
LabelUpdatedAt: time.Now(),
|
||||
PolicyUpdatedAt: time.Now(),
|
||||
SeenTime: time.Now(),
|
||||
NodeKey: ptr.String("foobar"),
|
||||
UUID: "foobar",
|
||||
Hostname: "com.foobar.local",
|
||||
Platform: "linux",
|
||||
TeamID: &team.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
policy, err := s.ds.NewTeamPolicy(ctx, team.ID, nil, fleet.PolicyPayload{
|
||||
Name: "Barfoo",
|
||||
Query: "SELECT 1;",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
distributedWriteResp := submitDistributedQueryResultsResponse{}
|
||||
s.DoJSON("POST", "/api/osquery/distributed/write", genDistributedReqWithPolicyResults(
|
||||
host,
|
||||
map[uint]*bool{
|
||||
policy.ID: ptr.Bool(false),
|
||||
},
|
||||
), http.StatusOK, &distributedWriteResp)
|
||||
|
||||
listHostsResp := listHostsResponse{}
|
||||
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listHostsResp)
|
||||
require.Len(t, listHostsResp.Hosts, 1)
|
||||
require.Equal(t, uint64(1), listHostsResp.Hosts[0].FailingPoliciesCount)
|
||||
|
||||
_, err = s.ds.DeleteTeamPolicies(ctx, team.ID, []uint{policy.ID})
|
||||
require.NoError(t, err)
|
||||
|
||||
distributedWriteResp = submitDistributedQueryResultsResponse{}
|
||||
results := make(map[string]json.RawMessage)
|
||||
results[hostNoPoliciesWildcard] = json.RawMessage("{\"1\": \"1\"}")
|
||||
statuses := make(map[string]interface{})
|
||||
statuses[hostNoPoliciesWildcard] = 0
|
||||
s.DoJSON("POST", "/api/osquery/distributed/write", submitDistributedQueryResultsRequestShim{
|
||||
NodeKey: *host.NodeKey,
|
||||
Results: results,
|
||||
Statuses: statuses,
|
||||
Stats: map[string]*fleet.Stats{},
|
||||
}, http.StatusOK, &distributedWriteResp)
|
||||
|
||||
listHostsResp = listHostsResponse{}
|
||||
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listHostsResp)
|
||||
require.Len(t, listHostsResp.Hosts, 1)
|
||||
require.Equal(t, uint64(0), listHostsResp.Hosts[0].FailingPoliciesCount)
|
||||
}
|
||||
|
||||
@@ -6284,7 +6284,7 @@ func (s *integrationEnterpriseTestSuite) TestRunHostSavedScript() {
|
||||
s.lastActivityMatches(
|
||||
fleet.ActivityTypeRanScript{}.ActivityName(),
|
||||
fmt.Sprintf(
|
||||
`{"host_id": %d, "host_display_name": %q, "script_name": %q, "script_execution_id": %q, "async": true}`,
|
||||
`{"host_id": %d, "host_display_name": %q, "script_name": %q, "script_execution_id": %q, "async": true, "policy_id": null, "policy_name": null}`,
|
||||
host.ID, host.DisplayName(), savedNoTmScript.Name, scriptResultResp.ExecutionID,
|
||||
),
|
||||
0,
|
||||
@@ -12196,7 +12196,7 @@ func (s *integrationEnterpriseTestSuite) TestHostScriptSoftDelete() {
|
||||
s.lastActivityOfTypeMatches(
|
||||
fleet.ActivityTypeRanScript{}.ActivityName(),
|
||||
fmt.Sprintf(
|
||||
`{"host_id": %d, "host_display_name": %q, "script_name": "", "script_execution_id": %q, "async": true}`,
|
||||
`{"host_id": %d, "host_display_name": %q, "script_name": "", "script_execution_id": %q, "async": true, "policy_id": null, "policy_name": null}`,
|
||||
host.ID, host.DisplayName(), scriptExecID), 0)
|
||||
|
||||
// create a saved script execution request
|
||||
@@ -12218,7 +12218,7 @@ func (s *integrationEnterpriseTestSuite) TestHostScriptSoftDelete() {
|
||||
http.StatusOK)
|
||||
s.lastActivityOfTypeMatches(
|
||||
fleet.ActivityTypeRanScript{}.ActivityName(),
|
||||
fmt.Sprintf(`{"host_id": %d, "host_display_name": %q, "script_name": "script1.sh", "script_execution_id": %q, "async": true}`,
|
||||
fmt.Sprintf(`{"host_id": %d, "host_display_name": %q, "script_name": "script1.sh", "script_execution_id": %q, "async": true, "policy_id": null, "policy_name": null}`,
|
||||
host.ID, host.DisplayName(), savedScriptExecID), 0)
|
||||
|
||||
// get the anoymous script result details
|
||||
@@ -14717,11 +14717,12 @@ func (s *integrationEnterpriseTestSuite) TestPolicyAutomationsScripts() {
|
||||
},
|
||||
), http.StatusOK, &distributedResp)
|
||||
|
||||
hostPendingScript, err = s.ds.IsExecutionPendingForHost(ctx, host3Team2.ID, psScript.ID)
|
||||
host3PendingScripts, err := s.ds.ListPendingHostScriptExecutions(ctx, host3Team2.ID)
|
||||
require.NoError(t, err)
|
||||
require.True(t, hostPendingScript)
|
||||
require.Len(t, host3PendingScripts, 1)
|
||||
host3executionID := host3PendingScripts[0].ExecutionID
|
||||
|
||||
// Unassociate policy4Team2 from script.
|
||||
// Dissociate policy4Team2 from script.
|
||||
mtplr = modifyTeamPolicyResponse{}
|
||||
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d/policies/%d", team2.ID, policy4Team2.ID), modifyTeamPolicyRequest{
|
||||
ModifyPolicyPayload: fleet.ModifyPolicyPayload{
|
||||
@@ -14750,6 +14751,38 @@ func (s *integrationEnterpriseTestSuite) TestPolicyAutomationsScripts() {
|
||||
hostPendingScripts, err := s.ds.ListPendingHostScriptExecutions(ctx, hostVanillaOsquery5Team1.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, hostPendingScripts, 0)
|
||||
|
||||
// activity feed should show script run as pending, with "Fleet" as author, policy ID and name set in body
|
||||
var listResp listActivitiesResponse
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/activities/upcoming", host3Team2.ID), nil, http.StatusOK, &listResp)
|
||||
require.Len(t, listResp.Activities, 1)
|
||||
require.Nil(t, listResp.Activities[0].ActorEmail)
|
||||
require.Equal(t, "Fleet", *listResp.Activities[0].ActorFullName)
|
||||
require.Nil(t, listResp.Activities[0].ActorGravatar)
|
||||
require.Equal(t, "ran_script", listResp.Activities[0].Type)
|
||||
var activityJson map[string]interface{}
|
||||
err = json.Unmarshal(*listResp.Activities[0].Details, &activityJson)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, float64(policy4Team2.ID), activityJson["policy_id"])
|
||||
require.Equal(t, "policy4Team2", activityJson["policy_name"])
|
||||
|
||||
// post script result response
|
||||
var orbitPostScriptResp orbitPostScriptResultResponse
|
||||
s.DoJSON("POST", "/api/fleet/orbit/scripts/result",
|
||||
json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q, "execution_id": %q, "exit_code": 0, "output": "ok"}`, *host3Team2.OrbitNodeKey, host3executionID)),
|
||||
http.StatusOK, &orbitPostScriptResp)
|
||||
|
||||
// activity feed should show script run as completed
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/activities", host3Team2.ID), nil, http.StatusOK, &listResp)
|
||||
require.Len(t, listResp.Activities, 1)
|
||||
require.Equal(t, "", *listResp.Activities[0].ActorEmail) // actor email is blank rather than nil here 👀
|
||||
require.Equal(t, "Fleet", *listResp.Activities[0].ActorFullName)
|
||||
require.Nil(t, listResp.Activities[0].ActorGravatar)
|
||||
require.Equal(t, "ran_script", listResp.Activities[0].Type)
|
||||
err = json.Unmarshal(*listResp.Activities[0].Details, &activityJson)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, float64(policy4Team2.ID), activityJson["policy_id"])
|
||||
require.Equal(t, "policy4Team2", activityJson["policy_name"])
|
||||
}
|
||||
|
||||
func (s *integrationEnterpriseTestSuite) TestSoftwareInstallersWithoutBundleIdentifier() {
|
||||
|
||||
@@ -733,6 +733,13 @@ func (svc *Service) SaveHostScriptResult(ctx context.Context, result *fleet.Host
|
||||
}
|
||||
default:
|
||||
// TODO(sarah): We may need to special case lock/unlock script results here?
|
||||
var policyName *string
|
||||
if hsr.PolicyID != nil {
|
||||
if policy, err := svc.ds.PolicyLite(ctx, *hsr.PolicyID); err == nil {
|
||||
policyName = &policy.Name // fall back to blank policy name if we can't retrieve the policy
|
||||
}
|
||||
}
|
||||
|
||||
if err := svc.NewActivity(
|
||||
ctx,
|
||||
user,
|
||||
@@ -742,6 +749,8 @@ func (svc *Service) SaveHostScriptResult(ctx context.Context, result *fleet.Host
|
||||
ScriptExecutionID: hsr.ExecutionID,
|
||||
ScriptName: scriptName,
|
||||
Async: !hsr.SyncRequest,
|
||||
PolicyID: hsr.PolicyID,
|
||||
PolicyName: policyName,
|
||||
},
|
||||
); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "create activity for script execution request")
|
||||
|
||||
@@ -1931,6 +1931,8 @@ func (svc *Service) processScriptsForNewlyFailingPolicies(
|
||||
}
|
||||
|
||||
for _, failingPolicyWithScript := range failingPoliciesWithScript {
|
||||
policyID := failingPolicyWithScript.ID
|
||||
|
||||
scriptMetadata, err := svc.ds.Script(ctx, failingPolicyWithScript.ScriptID)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "get script metadata by id")
|
||||
@@ -1938,7 +1940,7 @@ func (svc *Service) processScriptsForNewlyFailingPolicies(
|
||||
logger := log.With(svc.logger,
|
||||
"host_id", hostID,
|
||||
"host_platform", hostPlatform,
|
||||
"policy_id", failingPolicyWithScript.ID,
|
||||
"policy_id", policyID,
|
||||
"script_id", failingPolicyWithScript.ScriptID,
|
||||
"script_name", scriptMetadata.Name,
|
||||
)
|
||||
@@ -1989,6 +1991,7 @@ func (svc *Service) processScriptsForNewlyFailingPolicies(
|
||||
ScriptContentID: scriptMetadata.ScriptContentID,
|
||||
ScriptID: &scriptMetadata.ID,
|
||||
TeamID: policyTeamID,
|
||||
PolicyID: &policyID,
|
||||
// no user ID as scripts are executed by Fleet
|
||||
}
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ type uploadSoftwareInstallerResponse struct {
|
||||
|
||||
// MaxSoftwareInstallerSize is the maximum size allowed for software
|
||||
// installers. This is enforced by the endpoints that upload installers.
|
||||
const MaxSoftwareInstallerSize = 500 * units.MiB
|
||||
const MaxSoftwareInstallerSize = 3000 * units.MiB
|
||||
|
||||
// TODO: We parse the whole body before running svc.authz.Authorize.
|
||||
// An authenticated but unauthorized user could abuse this.
|
||||
@@ -68,7 +68,7 @@ func (updateSoftwareInstallerRequest) DecodeRequest(ctx context.Context, r *http
|
||||
var mbe *http.MaxBytesError
|
||||
if errors.As(err, &mbe) {
|
||||
return nil, &fleet.BadRequestError{
|
||||
Message: "The maximum file size is 500 MB.",
|
||||
Message: "The maximum file size is 3 GB.",
|
||||
InternalErr: err,
|
||||
}
|
||||
}
|
||||
@@ -91,7 +91,7 @@ func (updateSoftwareInstallerRequest) DecodeRequest(ctx context.Context, r *http
|
||||
if decoded.File.Size > MaxSoftwareInstallerSize {
|
||||
// Should never happen here since the request's body is limited to the maximum size.
|
||||
return nil, &fleet.BadRequestError{
|
||||
Message: "The maximum file size is 500 MB.",
|
||||
Message: "The maximum file size is 3 GB.",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# MicroMDM touchless migration
|
||||
|
||||
This Go script reads information from a MicroMDM database and outputs:
|
||||
|
||||
- SCEP, APNs and ADE certificates.
|
||||
- A file with MySQL statements to import the records into a Fleet database.
|
||||
|
||||
### Usage
|
||||
|
||||
The only requirement is to have a compatible version of Go installed, and a MicroMDM database.
|
||||
|
||||
Here's an example of how a successful run looks like:
|
||||
|
||||
```sh
|
||||
$ go run tools/mdm/migration/micromdm/touchless/main.go --db ~/projects/micromdm/micromdm.db
|
||||
|
||||
2024/09/11 10:05:53 Open DB for devices
|
||||
2024/09/11 10:05:53 Found 1 devices
|
||||
2024/09/11 10:05:53 Wrote device/enrollment records to dump.sql
|
||||
2024/09/11 10:05:53 Open DB for SCEP cert and key
|
||||
2024/09/11 10:05:53 Wrote SCEP cert/key to scep.cert/scep.key
|
||||
```
|
||||
|
||||
@@ -0,0 +1,393 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/pem"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
scepdepot "github.com/fleetdm/fleet/v4/server/mdm/scep/depot/bolt"
|
||||
"github.com/groob/plist"
|
||||
apnsbuiltin "github.com/micromdm/micromdm/platform/apns/builtin"
|
||||
"github.com/micromdm/micromdm/platform/device"
|
||||
devicebuiltin "github.com/micromdm/micromdm/platform/device/builtin"
|
||||
"github.com/micromdm/micromdm/platform/pubsub/inmem"
|
||||
"go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
type Authenticate struct {
|
||||
MessageType string
|
||||
UDID string
|
||||
Topic string
|
||||
BuildVersion string `plist:",omitempty"`
|
||||
DeviceName string `plist:",omitempty"`
|
||||
Model string `plist:",omitempty"`
|
||||
ModelName string `plist:",omitempty"`
|
||||
OSVersion string `plist:",omitempty"`
|
||||
ProductName string `plist:",omitempty"`
|
||||
SerialNumber string `plist:",omitempty"`
|
||||
IMEI string `plist:",omitempty"`
|
||||
MEID string `plist:",omitempty"`
|
||||
}
|
||||
|
||||
type TokenUpdate struct {
|
||||
MessageType string
|
||||
UDID string
|
||||
PushMagic string
|
||||
Topic string
|
||||
Token []byte
|
||||
UnlockToken []byte `plist:",omitempty"`
|
||||
UserID string `plist:",omitempty"`
|
||||
UserShortName string `plist:",omitempty"`
|
||||
UserLongName string `plist:",omitempty"`
|
||||
}
|
||||
|
||||
// referenceTime is used as a canary to insert/update records. As long as a
|
||||
// record has an `updated_at` timestamp, the script will update it, but if the
|
||||
// timestamp has changed, the record will be completely ignored.
|
||||
const referenceTime = "2000-01-01 00:00:00"
|
||||
|
||||
func main() {
|
||||
flDB := flag.String("db", "/var/db/micromdm/micromdm.db", "path to micromdm DB")
|
||||
flag.Parse()
|
||||
|
||||
// Device records
|
||||
func() {
|
||||
log.Println("Open DB for devices")
|
||||
boltDB, err := bolt.Open(*flDB, 0o600, nil)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer boltDB.Close()
|
||||
|
||||
ps := inmem.NewPubSub()
|
||||
apnsDB, err := apnsbuiltin.NewDB(boltDB, ps)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
deviceDB, err := devicebuiltin.NewDB(boltDB)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
devices, err := deviceDB.List(context.Background(), device.ListDevicesOption{})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if len(devices) == 0 {
|
||||
log.Printf("No devices found. Are you sure %s is a MicroMDM DB?", *flDB)
|
||||
} else {
|
||||
log.Printf("Found %d devices", len(devices))
|
||||
}
|
||||
|
||||
// SCEP certificates are stored using the certificate CN as the
|
||||
// key. I couldn't find any CN <-> device association in the
|
||||
// db, as it's not needed: micro extracts the certificate CN
|
||||
// from the request and uses it to authenticate devices.
|
||||
//
|
||||
// To avoid loading all certs in memory (rough estimation is
|
||||
// ~2KB per cert) we store a map of the cert hash (which is
|
||||
// stored along the device record) to the CN for later
|
||||
// retrieval.
|
||||
certHashToCertKey := make(map[string][]byte, len(devices))
|
||||
// NOTE: the depot doesn't expose methods to list certs so we
|
||||
// need to use bolt directly.
|
||||
err = boltDB.View(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte("scep_certificates"))
|
||||
if bucket == nil {
|
||||
log.Fatalf("No identity certificates found. Are you sure %s is a MicroMDM DB?", *flDB)
|
||||
}
|
||||
return bucket.ForEach(func(k, v []byte) error {
|
||||
hash := sha256.Sum256(v)
|
||||
certHashToCertKey[string(hash[:])] = k
|
||||
return nil
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
for _, device := range devices {
|
||||
if len(device.UDID) == 0 {
|
||||
log.Println("Skipping device with empty UDID. Serial: ", device.SerialNumber, " UUID: ", device.UUID, " Last seen: ", device.LastSeen)
|
||||
continue
|
||||
}
|
||||
pushInfo, err := apnsDB.PushInfo(context.Background(), device.UDID)
|
||||
if err != nil {
|
||||
log.Println(device.UDID, " FAILED: ", err)
|
||||
continue
|
||||
}
|
||||
|
||||
authenticate := &Authenticate{
|
||||
MessageType: "Authenticate",
|
||||
UDID: device.UDID,
|
||||
Topic: pushInfo.MDMTopic,
|
||||
BuildVersion: device.BuildVersion,
|
||||
DeviceName: device.DeviceName,
|
||||
Model: device.Model,
|
||||
ModelName: device.ModelName,
|
||||
OSVersion: device.OSVersion,
|
||||
ProductName: device.ProductName,
|
||||
SerialNumber: device.SerialNumber,
|
||||
IMEI: device.IMEI,
|
||||
MEID: device.MEID,
|
||||
}
|
||||
|
||||
authenticatePlist, err := plist.Marshal(authenticate)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
continue
|
||||
}
|
||||
|
||||
token, err := hex.DecodeString(pushInfo.Token)
|
||||
if err != nil {
|
||||
log.Println(device.UDID, " FAILED: ", err)
|
||||
continue
|
||||
}
|
||||
unlockToken, err := hex.DecodeString(device.UnlockToken)
|
||||
if err != nil {
|
||||
log.Println(device.UDID, " FAILED: ", err)
|
||||
continue
|
||||
}
|
||||
|
||||
tokenUpdate := &TokenUpdate{
|
||||
MessageType: "TokenUpdate",
|
||||
UDID: device.UDID,
|
||||
|
||||
PushMagic: pushInfo.PushMagic,
|
||||
Token: token,
|
||||
Topic: pushInfo.MDMTopic,
|
||||
|
||||
UnlockToken: unlockToken,
|
||||
}
|
||||
|
||||
tokenPlist, err := plist.Marshal(tokenUpdate)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
continue
|
||||
}
|
||||
|
||||
certHash, err := deviceDB.GetUDIDCertHash([]byte(device.UDID))
|
||||
if err != nil {
|
||||
log.Println(device.UDID, " FAILED: ", err)
|
||||
continue
|
||||
}
|
||||
|
||||
var certDer []byte
|
||||
certKey := certHashToCertKey[string(certHash)]
|
||||
err = boltDB.View(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte("scep_certificates"))
|
||||
certDer = bucket.Get(certKey)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
log.Println(device.UDID, " FAILED: ", err)
|
||||
continue
|
||||
}
|
||||
|
||||
var certExpiration string
|
||||
var certPEM []byte
|
||||
if certDer != nil {
|
||||
// parse the cert to extract the expiration date
|
||||
cert, err := x509.ParseCertificate(certDer)
|
||||
if err != nil {
|
||||
log.Printf("WARN: unable to parse SCEP identity certificate for %s: %s\n", device.UDID, err)
|
||||
}
|
||||
certExpiration = cert.NotAfter.Format("2006-01-02 15:04:05")
|
||||
|
||||
// encode it to PEM to store it in the DB in
|
||||
// the format that nano expects. At the moment
|
||||
// we don't really need this value as we can
|
||||
// make do with the hash and the expiration,
|
||||
// but I figured it would be good to have it.
|
||||
pemBlock := &pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: cert.Raw,
|
||||
}
|
||||
certPEM = pem.EncodeToMemory(pemBlock)
|
||||
}
|
||||
|
||||
if len(device.BootstrapToken) == 0 {
|
||||
log.Println("Device with empty bootstrap token: ", device.UDID, " Last seen: ", device.LastSeen.String())
|
||||
}
|
||||
|
||||
base64BootstrapToken := base64.StdEncoding.EncodeToString(device.BootstrapToken)
|
||||
|
||||
sb.WriteString(fmt.Sprintf(`
|
||||
INSERT INTO nano_devices
|
||||
(
|
||||
id,
|
||||
serial_number,
|
||||
authenticate,
|
||||
authenticate_at,
|
||||
token_update,
|
||||
token_update_at,
|
||||
bootstrap_token_b64,
|
||||
bootstrap_token_at,
|
||||
identity_cert,
|
||||
updated_at
|
||||
)
|
||||
SELECT
|
||||
'%s',
|
||||
'%s',
|
||||
'%s',
|
||||
CURRENT_TIMESTAMP,
|
||||
'%s',
|
||||
CURRENT_TIMESTAMP,
|
||||
NULLIF('%s', ''),
|
||||
CURRENT_TIMESTAMP,
|
||||
NULLIF('%s', ''),
|
||||
'%s'
|
||||
WHERE
|
||||
NOT EXISTS (
|
||||
SELECT 1 FROM nano_devices
|
||||
WHERE id = '%s' AND updated_at != '%s'
|
||||
)
|
||||
ON DUPLICATE KEY
|
||||
UPDATE
|
||||
updated_at = updated_at, -- preserve updated_at
|
||||
serial_number = VALUES(serial_number),
|
||||
authenticate = VALUES(authenticate),
|
||||
authenticate_at = CURRENT_TIMESTAMP,
|
||||
token_update = VALUES(token_update),
|
||||
token_update_at = CURRENT_TIMESTAMP,
|
||||
bootstrap_token_b64 = VALUES(bootstrap_token_b64),
|
||||
bootstrap_token_at = CURRENT_TIMESTAMP,
|
||||
identity_cert = VALUES(identity_cert);
|
||||
`, device.UDID, device.SerialNumber, authenticatePlist, tokenPlist, base64BootstrapToken, certPEM, referenceTime, device.UDID, referenceTime))
|
||||
|
||||
sb.WriteString(fmt.Sprintf(`
|
||||
INSERT INTO nano_enrollments (
|
||||
id,
|
||||
device_id,
|
||||
user_id, type,
|
||||
topic,
|
||||
push_magic,
|
||||
token_hex,
|
||||
enabled,
|
||||
last_seen_at,
|
||||
enrolled_from_migration,
|
||||
token_update_tally,
|
||||
updated_at
|
||||
)
|
||||
SELECT
|
||||
'%s',
|
||||
'%s',
|
||||
NULL,
|
||||
'Device',
|
||||
'%s',
|
||||
'%s',
|
||||
'%s',
|
||||
%t,
|
||||
CURRENT_TIMESTAMP,
|
||||
1,
|
||||
1,
|
||||
'%s'
|
||||
WHERE
|
||||
NOT EXISTS (
|
||||
SELECT 1 FROM nano_enrollments
|
||||
WHERE id = '%s' AND updated_at != '%s'
|
||||
)
|
||||
ON DUPLICATE KEY
|
||||
UPDATE
|
||||
updated_at = updated_at, -- preserve updated_at
|
||||
device_id = VALUES(device_id),
|
||||
user_id = VALUES(user_id),
|
||||
type = VALUES(type),
|
||||
topic = VALUES(topic),
|
||||
push_magic = VALUES(push_magic),
|
||||
token_hex = VALUES(token_hex),
|
||||
enabled = VALUES(enabled),
|
||||
last_seen_at = CURRENT_TIMESTAMP,
|
||||
token_update_tally = nano_enrollments.token_update_tally + 1;`,
|
||||
device.UDID,
|
||||
device.UDID,
|
||||
tokenUpdate.Topic,
|
||||
tokenUpdate.PushMagic,
|
||||
hex.EncodeToString(tokenUpdate.Token),
|
||||
device.Enrolled,
|
||||
referenceTime,
|
||||
device.UDID,
|
||||
referenceTime,
|
||||
))
|
||||
|
||||
sb.WriteString(fmt.Sprintf(`
|
||||
INSERT INTO nano_cert_auth_associations
|
||||
(id, sha256, cert_not_valid_after, updated_at)
|
||||
SELECT
|
||||
'%s', '%s', NULLIF('%s', ''), '%s'
|
||||
WHERE
|
||||
NOT EXISTS (
|
||||
SELECT 1 FROM nano_cert_auth_associations
|
||||
WHERE id = '%s' AND updated_at != '%s'
|
||||
)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
id = VALUES(id),
|
||||
updated_at = updated_at, -- preserve updated_at
|
||||
sha256 = VALUES(sha256),
|
||||
cert_not_valid_after = VALUES(cert_not_valid_after);
|
||||
`, device.UDID, hex.EncodeToString(certHash[:]), certExpiration, referenceTime, device.UDID, referenceTime))
|
||||
}
|
||||
|
||||
sb.WriteString("\n")
|
||||
if err := os.WriteFile("dump.sql", []byte(sb.String()), 0o600); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Println("Wrote device/enrollment records to dump.sql")
|
||||
}()
|
||||
|
||||
// SCEP cert/key
|
||||
func() {
|
||||
log.Println("Open DB for SCEP cert and key")
|
||||
bboltDB, err := bbolt.Open(*flDB, 0o600, nil)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer bboltDB.Close()
|
||||
|
||||
scepBoltDepot, err := scepdepot.NewBoltDepot(bboltDB)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
key, err := scepBoltDepot.CreateOrLoadKey(2048)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
crt, err := scepBoltDepot.CreateOrLoadCA(key, 5, "MicroMDM", "US")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
privateKeyBytes := x509.MarshalPKCS1PrivateKey(key)
|
||||
privateKeyPEM := pem.EncodeToMemory(&pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Bytes: privateKeyBytes,
|
||||
})
|
||||
|
||||
if err := os.WriteFile("scep.key", privateKeyPEM, 0o600); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
certPEM := pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: crt.Raw,
|
||||
})
|
||||
|
||||
if err := os.WriteFile("scep.cert", certPEM, 0o600); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
log.Println("Wrote SCEP cert/key to scep.cert/scep.key")
|
||||
}()
|
||||
}
|
||||
+10
-3
@@ -55,6 +55,7 @@ setup () {
|
||||
|
||||
mkdir -p "$REPOSITORY_DIRECTORY"
|
||||
mkdir -p "$STAGED_DIRECTORY"
|
||||
|
||||
cp -r "$KEYS_SOURCE_DIRECTORY" "$KEYS_DIRECTORY"
|
||||
|
||||
if ! aws sts get-caller-identity &> /dev/null; then
|
||||
@@ -86,9 +87,12 @@ setup () {
|
||||
export FLEET_TARGETS_PASSPHRASE
|
||||
FLEET_SNAPSHOT_PASSPHRASE=$(op read "op://$SNAPSHOT_PASSPHRASE_1PASSWORD_PATH")
|
||||
export FLEET_SNAPSHOT_PASSPHRASE
|
||||
FLEET_TIMESTAMP_PASSPHRASE=$(op read "op://$TIMESTAMP_PASSPHRASE_1PASSWORD_PATH")
|
||||
export FLEET_TIMESTAMP_PASSPHRASE
|
||||
elif [[ $ACTION == "update-timestamp" ]]; then
|
||||
FLEET_TIMESTAMP_PASSPHRASE=$(op read "op://$TIMESTAMP_PASSPHRASE_1PASSWORD_PATH")
|
||||
export FLEET_TIMESTAMP_PASSPHRASE
|
||||
fi
|
||||
FLEET_TIMESTAMP_PASSPHRASE=$(op read "op://$TIMESTAMP_PASSPHRASE_1PASSWORD_PATH")
|
||||
export FLEET_TIMESTAMP_PASSPHRASE
|
||||
|
||||
go build -o "$GO_TOOLS_DIRECTORY/replace" "$SCRIPT_DIR/../../tools/tuf/replace"
|
||||
go build -o "$GO_TOOLS_DIRECTORY/download-artifacts" "$SCRIPT_DIR/../../tools/tuf/download-artifacts"
|
||||
@@ -321,6 +325,8 @@ print_reminder () {
|
||||
elif [[ $COMPONENT == "osqueryd" ]]; then
|
||||
prompt "Make sure to install fleetd with '--osqueryd-channel=stable' on a Linux, Windows and macOS VM. (To smoke test the release.)"
|
||||
fi
|
||||
elif [[ $ACTION == "update-timestamp" ]]; then
|
||||
:
|
||||
elif [[ $ACTION != "update-timestamp" ]]; then
|
||||
echo "Unsupported action: $ACTION"
|
||||
exit 1
|
||||
@@ -330,6 +336,7 @@ print_reminder () {
|
||||
trap clean_up EXIT
|
||||
print_reminder
|
||||
setup
|
||||
|
||||
pull_from_remote
|
||||
|
||||
if [[ $ACTION == "release-to-edge" ]]; then
|
||||
@@ -343,4 +350,4 @@ else
|
||||
exit 1
|
||||
fi
|
||||
|
||||
push_to_remote
|
||||
push_to_remote
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 280 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 182 KiB After Width: | Height: | Size: 272 KiB |
+1
-1
@@ -28,7 +28,7 @@ img {
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-family: @header-font;
|
||||
}
|
||||
h1, h2, h3, h4, h5 {
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-weight: 800;
|
||||
color: @core-fleet-black;
|
||||
}
|
||||
|
||||
Vendored
+1
-1
@@ -160,7 +160,7 @@ html, body {
|
||||
background-image: url('/images/cta-thumbnail-psystage-4-has-use-case-128x128@2x.png');
|
||||
}
|
||||
&.stage-five {
|
||||
background-image: url('/images/cta-thumbnail-psystage-5-100x100@2x.png');
|
||||
background-image: url('/images/cta-thumbnail-psystage-5-feeling-confident-128x128@2x.png');
|
||||
}
|
||||
&.stage-six {
|
||||
background-image: url('/images/psystage-6-has-team-buy-in-504x784@2x.png');
|
||||
|
||||
+36
-15
@@ -1,26 +1,34 @@
|
||||
#basic-documentation {
|
||||
|
||||
h1 {
|
||||
font-size: 28px;
|
||||
line-height: 34px;
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 24px;
|
||||
line-height: 28px;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 20px;
|
||||
line-height: 24px;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin-top: 24px;
|
||||
font-size: 18px;
|
||||
line-height: 24px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
|
||||
|
||||
a:not(.btn) {
|
||||
color: @core-vibrant-blue;
|
||||
@@ -532,25 +540,33 @@
|
||||
|
||||
h3 {
|
||||
padding-bottom: 16px;
|
||||
margin-top: 24px;
|
||||
margin-bottom: 40px;
|
||||
border-bottom: 1px dashed @core-fleet-black-25;
|
||||
margin-top: 40px;
|
||||
scroll-margin-top: 104px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
padding-bottom: 24px;
|
||||
margin-top: 24px;
|
||||
margin-bottom: 0px;
|
||||
scroll-margin-top: 104px;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
font-family: @main-font;
|
||||
margin-bottom: 16px;
|
||||
margin-top: 16px;
|
||||
margin-bottom: 24px;
|
||||
scroll-margin-top: 104px;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-weight: 700;
|
||||
font-family: @main-font;
|
||||
margin-top: 16px;
|
||||
margin-bottom: 24px;
|
||||
scroll-margin-top: 104px;
|
||||
}
|
||||
|
||||
pre + hr + h3 {
|
||||
padding-top: 0px;
|
||||
}
|
||||
@@ -750,7 +766,7 @@
|
||||
border-collapse: collapse;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: 0;
|
||||
word-wrap: break-word;
|
||||
|
||||
th {
|
||||
@@ -767,6 +783,11 @@
|
||||
padding: 8px 7px 7px 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.table-responsive {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.note {
|
||||
background-color: @core-vibrant-blue-10;
|
||||
border-radius: 12px;
|
||||
|
||||
+5
-12
@@ -85,7 +85,7 @@
|
||||
padding-right: 0px;
|
||||
}
|
||||
[purpose='page-section'] {
|
||||
margin-bottom: 180px;
|
||||
margin-bottom: 120px;
|
||||
max-width: 960px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
@@ -282,8 +282,8 @@
|
||||
}
|
||||
|
||||
[purpose='homepage-text-block'] {
|
||||
margin-top: 160px;
|
||||
margin-bottom: 120px;
|
||||
margin-top: 80px;
|
||||
margin-bottom: 80px;
|
||||
max-width: 642px;
|
||||
h2 {
|
||||
font-weight: 800;
|
||||
@@ -296,7 +296,7 @@
|
||||
}
|
||||
}
|
||||
[purpose='platform-block'] {
|
||||
margin-bottom: 64px;
|
||||
margin-bottom: 120px;
|
||||
max-width: 1080px;
|
||||
}
|
||||
[purpose='platform-block'].visible {
|
||||
@@ -514,10 +514,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[purpose='integrations-section'] {
|
||||
margin-top: 180px;
|
||||
}
|
||||
[purpose='integration-cards'] {
|
||||
margin-top: 40px;
|
||||
margin-bottom: 40px;
|
||||
@@ -716,10 +712,7 @@
|
||||
padding-left: 60px;
|
||||
}
|
||||
[purpose='page-section'] {
|
||||
margin-bottom: 160px;
|
||||
}
|
||||
[purpose='integrations-section'] {
|
||||
margin-top: 160px;
|
||||
margin-bottom: 120px;
|
||||
}
|
||||
[purpose='video-modal'] {
|
||||
[purpose='modal-dialog'] {
|
||||
|
||||
+6
-2
@@ -82,7 +82,7 @@
|
||||
background-image: url('/images/psystage-4-has-use-case-508x784@2x.png');
|
||||
}
|
||||
&.stage-five {
|
||||
background-image: url('/images/cropped-fleet-cloud-city-504x784@2x.png');
|
||||
background-image: url('/images/psystage-5-feeling-confident-504x784@2x.png');
|
||||
}
|
||||
&.stage-six {
|
||||
background-image: url('/images/psystage-6-has-team-buy-in-504x784@2x.png');
|
||||
@@ -325,7 +325,7 @@
|
||||
}
|
||||
[purpose='form-container'] {
|
||||
padding: 0;
|
||||
min-width: unset;
|
||||
// min-width: unset;
|
||||
max-width: 504px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
@@ -341,6 +341,10 @@
|
||||
padding-left: 24px;
|
||||
padding-right: 24px;
|
||||
}
|
||||
[purpose='form-container'] {
|
||||
min-width: unset;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@media (max-width: 575px) {
|
||||
[purpose='logo-container'] {
|
||||
|
||||
Vendored
+115
-114
@@ -37,12 +37,12 @@
|
||||
<img alt="Operating systems entering a glass device management dome" src="/images/device-management-hero-380x293@2x.png">
|
||||
</div>
|
||||
<div purpose="category-text-block" class="d-flex flex-column">
|
||||
<strong>Everything in one place</strong>
|
||||
<p>Manage devices consistently in a single, open platform for Apple, Windows, and Linux. Going only Mac or 50/50 Mac and Windows? What about Linux? It’s up to you. </p>
|
||||
<strong>Everything in one place - even Linux</strong>
|
||||
<p>Enroll all your computing devices from a single system. Going only Apple or 50/50 Mac and Windows? What about Linux? It’s up to you. </p>
|
||||
<strong>Debunk the cross-platform myth</strong>
|
||||
<p>Fleet exposes familiar concepts like custom attributes and dynamic grouping, but in a way that lets you work directly with data and events from each native operating system.</p>
|
||||
<p>Fleet exposes familiar concepts from traditional MDMs like custom attributes and dynamic grouping, but in a way that lets you work directly with data and events from each native operating system.</p>
|
||||
<strong>Less friction</strong>
|
||||
<p>Fork the CIS benchmarks or easily build your own compliance framework. 100% source available.</p>
|
||||
<p>Automatically manage updates and patches for apps on macOS, Windows, and Linux computers.</p>
|
||||
<div>
|
||||
<a purpose="category-button" class="text-nowrap btn btn-primary" href="/device-management">Start with device management</a>
|
||||
</div>
|
||||
@@ -180,8 +180,113 @@
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
<%/* Integration cards */%>
|
||||
<div purpose="integrations-section">
|
||||
<div purpose="page-section">
|
||||
<div purpose="endpoints-banner">
|
||||
<div purpose="endpoint-banner-text">
|
||||
<h2>An open interface for every OS</h2>
|
||||
<p>Normalize how you manage clouds and computers without losing low-level access to OS-specific features.</p>
|
||||
<div class="d-flex flex-row align-items-center justify-content-md-start justify-content-center">
|
||||
<animated-arrow-button href="/docs/get-started/why-fleet">Read the docs</animated-arrow-button>
|
||||
</div>
|
||||
</div>
|
||||
<div purpose="endpoints-images">
|
||||
<div purpose="endpoint-images-row" class="d-md-flex d-none">
|
||||
<div purpose="endpoint-empty-image-box"></div>
|
||||
<div purpose="endpoint-empty-image-box"></div>
|
||||
<div purpose="endpoint-empty-image-box"></div>
|
||||
<div purpose="endpoint-empty-image-box"></div>
|
||||
</div>
|
||||
<div purpose="endpoint-images-row">
|
||||
<div purpose="endpoint-empty-image-box" class="d-md-none d-flex"></div>
|
||||
<div purpose="endpoint-empty-image-box" class="d-md-none d-flex"></div>
|
||||
<div purpose="endpoint-image-box">
|
||||
<img alt="AWS" src="/images/logo-aws-36x36@2x.png">
|
||||
<p>AWS</p>
|
||||
</div>
|
||||
<div purpose="endpoint-image-box">
|
||||
<img alt="Google Cloud" src="/images/icon-gcp-36x36@2x.png">
|
||||
<p>GCP</p>
|
||||
</div>
|
||||
<div purpose="endpoint-image-box">
|
||||
<img alt="Azure" src="/images/logo-azure-37x36@2x.png">
|
||||
<p>Azure</p>
|
||||
</div>
|
||||
<div purpose="endpoint-image-box" class="d-md-none d-flex">
|
||||
<img alt="Datacenters" src="/images/homepage-platform-greyscale-datacenters-48x48@2x.png">
|
||||
<p>Data centers</p>
|
||||
</div>
|
||||
<div purpose="endpoint-image-box" class="d-md-none d-flex">
|
||||
<img alt="iOS" src="/images/icon-ios-36x36@2x.png">
|
||||
<p>iOS/iPadOS</p>
|
||||
</div>
|
||||
<div purpose="endpoint-empty-image-box"></div>
|
||||
<div purpose="endpoint-empty-image-box"></div>
|
||||
</div>
|
||||
<div purpose="endpoint-images-row">
|
||||
<div purpose="endpoint-empty-image-box" class="d-md-none d-flex"></div>
|
||||
<div purpose="endpoint-empty-image-box" class="d-md-none d-flex"></div>
|
||||
<div purpose="endpoint-image-box">
|
||||
<img alt="macOS" src="/images/logo-apple-36x36@2x.png">
|
||||
<p>macOS</p>
|
||||
</div>
|
||||
<div purpose="endpoint-image-box">
|
||||
<img alt="Windows" src="/images/logo-windows-36x36@2x.png">
|
||||
<p>Windows</p>
|
||||
</div>
|
||||
<div purpose="endpoint-image-box">
|
||||
<img alt="Linux" src="/images/logo-linux-36x36@2x.png">
|
||||
<p>Linux</p>
|
||||
</div>
|
||||
<div purpose="endpoint-image-box" class="d-md-flex d-none">
|
||||
<img alt="Datacenters" src="/images/homepage-platform-greyscale-datacenters-48x48@2x.png">
|
||||
<p>Data centers</p>
|
||||
</div>
|
||||
<div purpose="endpoint-image-box" class="d-md-none d-flex">
|
||||
<img alt="ChromeOS" src="/images/logo-chrome-36x37@2x.png">
|
||||
<p>Chromebook</p>
|
||||
</div>
|
||||
<div purpose="endpoint-image-box" class="d-md-none d-flex">
|
||||
<img alt="Containers" src="/images/logo-docker-40x40@2x.png">
|
||||
<p>Containers</p>
|
||||
</div>
|
||||
<div purpose="endpoint-empty-image-box"></div>
|
||||
<div purpose="endpoint-empty-image-box"></div>
|
||||
</div>
|
||||
<div purpose="endpoint-images-row">
|
||||
<div purpose="endpoint-empty-image-box" class="d-md-flex d-none"></div>
|
||||
<div purpose="endpoint-image-box" class="d-md-flex d-none">
|
||||
<img alt="ChromeOS" src="/images/logo-chrome-36x37@2x.png">
|
||||
<p>Chromebook</p>
|
||||
</div>
|
||||
<div purpose="endpoint-image-box" class="d-md-flex d-none">
|
||||
<img alt="Containers" src="/images/logo-docker-40x40@2x.png">
|
||||
<p>Containers</p>
|
||||
</div>
|
||||
<div purpose="endpoint-image-box" class="d-md-flex d-none">
|
||||
<img alt="iOS" src="/images/icon-ios-36x36@2x.png">
|
||||
<p>iOS/iPadOS</p>
|
||||
</div>
|
||||
<div purpose="endpoint-empty-image-box"></div>
|
||||
<div purpose="endpoint-empty-image-box"></div>
|
||||
<div purpose="endpoint-empty-image-box"></div>
|
||||
<div purpose="endpoint-empty-image-box"></div>
|
||||
<div purpose="endpoint-empty-image-box"></div>
|
||||
<div purpose="endpoint-empty-image-box"></div>
|
||||
<div purpose="endpoint-empty-image-box"></div>
|
||||
</div>
|
||||
<div purpose="endpoint-images-row">
|
||||
<div purpose="endpoint-empty-image-box"></div>
|
||||
<div purpose="endpoint-empty-image-box"></div>
|
||||
<div purpose="endpoint-empty-image-box"></div>
|
||||
<div purpose="endpoint-empty-image-box"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<%/* Integration cards */%>
|
||||
<!-- <div purpose="integrations-section">
|
||||
<h2 class="text-center">Connect your favorite tools</h2>
|
||||
<div purpose="integration-cards" class="d-flex flex-sm-row flex-column justify-content-center">
|
||||
<div purpose="integration-card-column" class="d-flex flex-lg-row flex-column">
|
||||
@@ -231,116 +336,12 @@
|
||||
<div class="d-flex flex-column align-items-center">
|
||||
<animated-arrow-button href="/integrations">View all integrations</animated-arrow-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div purpose="page-section">
|
||||
<div purpose="endpoints-banner">
|
||||
<div purpose="endpoint-banner-text">
|
||||
<h2>An open interface for every OS</h2>
|
||||
<p>Normalize how you manage clouds and computers without losing low-level access to OS-specific features.</p>
|
||||
<div class="d-flex flex-row align-items-center justify-content-md-start justify-content-center">
|
||||
<animated-arrow-button href="/docs/get-started/why-fleet">Read the docs</animated-arrow-button>
|
||||
</div>
|
||||
</div>
|
||||
<div purpose="endpoints-images">
|
||||
<div purpose="endpoint-images-row" class="d-md-flex d-none">
|
||||
<div purpose="endpoint-empty-image-box"></div>
|
||||
<div purpose="endpoint-empty-image-box"></div>
|
||||
<div purpose="endpoint-empty-image-box"></div>
|
||||
<div purpose="endpoint-empty-image-box"></div>
|
||||
</div>
|
||||
<div purpose="endpoint-images-row">
|
||||
<div purpose="endpoint-empty-image-box" class="d-md-none d-flex"></div>
|
||||
<div purpose="endpoint-empty-image-box" class="d-md-none d-flex"></div>
|
||||
<div purpose="endpoint-image-box">
|
||||
<img alt="AWS" src="/images/logo-aws-36x36@2x.png">
|
||||
<p>AWS</p>
|
||||
</div>
|
||||
<div purpose="endpoint-image-box">
|
||||
<img alt="Google Cloud" src="/images/icon-gcp-36x36@2x.png">
|
||||
<p>GCP</p>
|
||||
</div>
|
||||
<div purpose="endpoint-image-box">
|
||||
<img alt="Azure" src="/images/logo-azure-37x36@2x.png">
|
||||
<p>Azure</p>
|
||||
</div>
|
||||
<div purpose="endpoint-image-box" class="d-md-none d-flex">
|
||||
<img alt="Datacenters" src="/images/homepage-platform-greyscale-datacenters-48x48@2x.png">
|
||||
<p>Data centers</p>
|
||||
</div>
|
||||
<div purpose="endpoint-image-box" class="d-md-none d-flex">
|
||||
<img alt="IOT" src="/images/icon-iot-36x36@2x.png">
|
||||
<p>IoT (Linux)</p>
|
||||
</div>
|
||||
<div purpose="endpoint-empty-image-box"></div>
|
||||
<div purpose="endpoint-empty-image-box"></div>
|
||||
</div>
|
||||
<div purpose="endpoint-images-row">
|
||||
<div purpose="endpoint-empty-image-box" class="d-md-none d-flex"></div>
|
||||
<div purpose="endpoint-empty-image-box" class="d-md-none d-flex"></div>
|
||||
<div purpose="endpoint-image-box">
|
||||
<img alt="macOS" src="/images/logo-apple-36x36@2x.png">
|
||||
<p>macOS</p>
|
||||
</div>
|
||||
<div purpose="endpoint-image-box">
|
||||
<img alt="Windows" src="/images/logo-windows-36x36@2x.png">
|
||||
<p>Windows</p>
|
||||
</div>
|
||||
<div purpose="endpoint-image-box">
|
||||
<img alt="Linux" src="/images/logo-linux-36x36@2x.png">
|
||||
<p>Linux</p>
|
||||
</div>
|
||||
<div purpose="endpoint-image-box" class="d-md-flex d-none">
|
||||
<img alt="Datacenters" src="/images/homepage-platform-greyscale-datacenters-48x48@2x.png">
|
||||
<p>Data centers</p>
|
||||
</div>
|
||||
<div purpose="endpoint-image-box" class="d-md-none d-flex">
|
||||
<img alt="ChromeOS" src="/images/logo-chrome-36x37@2x.png">
|
||||
<p>Chromebook</p>
|
||||
</div>
|
||||
<div purpose="endpoint-image-box" class="d-md-none d-flex">
|
||||
<img alt="Containers" src="/images/logo-docker-40x40@2x.png">
|
||||
<p>Containers</p>
|
||||
</div>
|
||||
<div purpose="endpoint-empty-image-box"></div>
|
||||
<div purpose="endpoint-empty-image-box"></div>
|
||||
</div>
|
||||
<div purpose="endpoint-images-row">
|
||||
<div purpose="endpoint-empty-image-box" class="d-md-flex d-none"></div>
|
||||
<div purpose="endpoint-image-box" class="d-md-flex d-none">
|
||||
<img alt="ChromeOS" src="/images/logo-chrome-36x37@2x.png">
|
||||
<p>Chromebook</p>
|
||||
</div>
|
||||
<div purpose="endpoint-image-box" class="d-md-flex d-none">
|
||||
<img alt="Containers" src="/images/logo-docker-40x40@2x.png">
|
||||
<p>Containers</p>
|
||||
</div>
|
||||
<div purpose="endpoint-image-box" class="d-md-flex d-none">
|
||||
<img alt="IOT" src="/images/icon-iot-36x36@2x.png">
|
||||
<p>IoT (Linux)</p>
|
||||
</div>
|
||||
<div purpose="endpoint-empty-image-box"></div>
|
||||
<div purpose="endpoint-empty-image-box"></div>
|
||||
<div purpose="endpoint-empty-image-box"></div>
|
||||
<div purpose="endpoint-empty-image-box"></div>
|
||||
<div purpose="endpoint-empty-image-box"></div>
|
||||
<div purpose="endpoint-empty-image-box"></div>
|
||||
<div purpose="endpoint-empty-image-box"></div>
|
||||
</div>
|
||||
<div purpose="endpoint-images-row">
|
||||
<div purpose="endpoint-empty-image-box"></div>
|
||||
<div purpose="endpoint-empty-image-box"></div>
|
||||
<div purpose="endpoint-empty-image-box"></div>
|
||||
<div purpose="endpoint-empty-image-box"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
<div purpose="homepage-text-block" class="text-center mx-auto">
|
||||
<h2>Open by design</h2>
|
||||
<p>Fleet is dedicated to flexibility, accessibility, and clarity. We think everyone can contribute and that tools should be as easy as possible for everyone to understand.</p>
|
||||
<p>At Fleet, everyone can contribute. We are dedicated to making tools that are easy for everyone to understand.</p>
|
||||
</div>
|
||||
|
||||
<div purpose="three-column-features" class="mx-auto">
|
||||
@@ -349,13 +350,13 @@
|
||||
<div purpose="feature" class="ml-sm-0">
|
||||
<img alt="transparency" src="/images/homepage-icon-transparency-54x64@2x.png" class="mx-auto mx-md-0">
|
||||
<h5>Scope transparency</h5>
|
||||
<p>Let end users see the source code for exactly <a href="/docs/using-fleet/fleet-desktop">how they are being monitored</a>, and set clear expectations about what is and isn’t acceptable use of work computers.</p>
|
||||
<p>Let end users see the source code for exactly <a href="/better">how they are being monitored</a>, and set clear expectations about what is and isn’t acceptable use of work computers.</p>
|
||||
</div>
|
||||
|
||||
<div purpose="feature">
|
||||
<img alt="Free as in free" src="/images/homepage-icon-free-48x64@2x.png">
|
||||
<h5>Free as in free</h5>
|
||||
<p>The <a href="/pricing">free version of Fleet</a> will always be free. Fleet is independently backed and actively maintained with the help of many amazing <a href="https://github.com/fleetdm/fleet/graphs/contributors">contributors</a>.</p>
|
||||
<p>The <a href="https://fleetdm.com/docs/get-started/faq#what-is-your-commitment-to-open-source-stewardship">free version of Fleet</a> will always be free. Fleet is independently backed and actively maintained with the help of many amazing <a href="https://github.com/fleetdm/fleet/graphs/contributors">contributors</a>.</p>
|
||||
</div>
|
||||
|
||||
<div purpose="feature" class="mr-sm-0 mb-0">
|
||||
|
||||
Vendored
+10
-17
@@ -424,29 +424,16 @@
|
||||
<div purpose="form-progress-bar"><div purpose="current-progress" style="width: 50%"></div></div>
|
||||
<img purpose="success-icon" alt="🚀" src="/images/icon-form-success-16x16@2x.png">
|
||||
</div>
|
||||
<h2>Want to try it?</h2>
|
||||
<div purpose="form-header" class="mx-auto">
|
||||
<p class="">We don’t have a public sample environment, but you can try Fleet out locally on your computer.</p>
|
||||
</div>
|
||||
<h2>Is it any good?</h2>
|
||||
<div purpose="start-cards">
|
||||
<a purpose="card" href="/try-fleet?start">
|
||||
<img alt="Run a local demo of Fleet" src="/images/start-try-fleet-64x64@2x.png">
|
||||
<h2>Try Fleet yourself</h2>
|
||||
<p>Try Fleet locally on your device</p>
|
||||
</a>
|
||||
<a purpose="card" href="https://youtu.be/QpdRADHWP_o?feature=shared&t=1333" target="_blank">
|
||||
<a purpose="card" class="w-100" href="https://youtu.be/QpdRADHWP_o?feature=shared&t=1333" target="_blank">
|
||||
<img alt="Watch a demo" src="/images/play-button-64x64@2x.png">
|
||||
<h2>Watch a demo</h2>
|
||||
<p>See what Fleet can do</p>
|
||||
</a>
|
||||
</div>
|
||||
<p>Got questions? <a purpose="contact-link" href="/contact">Ask us anything</a>.</p>
|
||||
<div purpose="form-tip" class="d-flex flex-row align-items-center justify-content-between">
|
||||
<div><img alt="A lightbulb" src="/images/icon-suggestion-64x64@2x.png"></div>
|
||||
<div>
|
||||
<p>You can come back here any time to continue with your deployment.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div purpose="form-buttons">
|
||||
<a purpose="back-button" @click="clickGoToPreviousStep()">Back</a>
|
||||
<ajax-button type="submit" purpose="submit-button" class="btn btn-primary" :syncing="syncing">Continue</ajax-button>
|
||||
@@ -483,6 +470,12 @@
|
||||
<div class="invalid-feedback" v-if="formErrors.whatDidYouThink">Please select an option</div>
|
||||
</div>
|
||||
<cloud-error v-if="cloudError"></cloud-error>
|
||||
<div purpose="form-tip" class="d-flex flex-row align-items-center justify-content-start">
|
||||
<div><img alt="A lightbulb" src="/images/icon-suggestion-64x64@2x.png"></div>
|
||||
<div>
|
||||
<p>Not ready to talk? You can also <a href="/try-fleet">run your own trial</a> with Docker.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div purpose="form-buttons">
|
||||
<a purpose="back-button" @click="clickGoToPreviousStep()">Back</a>
|
||||
<ajax-button type="submit" purpose="submit-button" class="btn btn-primary" :syncing="syncing">Continue</ajax-button>
|
||||
@@ -753,7 +746,7 @@
|
||||
</ajax-form>
|
||||
</div>
|
||||
</div>
|
||||
<div purpose="form-image" :class="currentStep === 'start' ? 'stage-one' : ['thanks-for-checking-out-fleet', 'what-did-you-think'].includes(currentStep) ? 'cloud-city' : ['2 - Aware'].includes(psychologicalStage) ? 'stage-two' : ['3 - Intrigued'].includes(psychologicalStage) ? 'stage-three' : ['4 - Has use case'].includes(psychologicalStage) ? 'stage-four' : ['5 - Personally confident'].includes(psychologicalStage) ? 'stage-five' : ['6 - Has team buy-in'].includes(psychologicalStage) ? 'stage-six' : 'd-none w-0'">
|
||||
<div purpose="form-image" :class="currentStep === 'start' ? 'stage-one' : ['thanks-for-checking-out-fleet'].includes(currentStep) ? 'cloud-city' : ['2 - Aware'].includes(psychologicalStage) ? 'stage-two' : ['3 - Intrigued'].includes(psychologicalStage) ? 'stage-three' : ['4 - Has use case'].includes(psychologicalStage) ? 'stage-four' : ['5 - Personally confident'].includes(psychologicalStage) ? 'stage-five' : ['6 - Has team buy-in'].includes(psychologicalStage) ? 'stage-six' : 'd-none w-0'">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user