` command. "
}
],
- "notes": "This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).",
+ "notes": "This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).",
"evented": false,
"url": "https://fleetdm.com/tables/authdb",
"fleetRepoUrl": "https://github.com/fleetdm/fleet/blob/main/schema/tables/authdb.yml"
@@ -4143,7 +4143,7 @@
"description": "Contains the value for the queried CIS item."
}
],
- "notes": "This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).",
+ "notes": "This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).",
"evented": false,
"url": "https://fleetdm.com/tables/cis_audit",
"fleetRepoUrl": "https://github.com/fleetdm/fleet/blob/main/schema/tables/cis_audit.yml"
@@ -4520,7 +4520,7 @@
"description": "Whether a password is currently required to unlock the volume"
}
],
- "notes": "This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).",
+ "notes": "This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).",
"evented": false,
"url": "https://fleetdm.com/tables/corestorage_logical_volume_families",
"fleetRepoUrl": "https://github.com/fleetdm/fleet/blob/main/schema/tables/corestorage_logical_volume_families.yml"
@@ -4719,7 +4719,7 @@
"description": "Name of the filesystem in the logical volume"
}
],
- "notes": "This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).",
+ "notes": "This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).",
"evented": false,
"url": "https://fleetdm.com/tables/corestorage_logical_volumes",
"fleetRepoUrl": "https://github.com/fleetdm/fleet/blob/main/schema/tables/corestorage_logical_volumes.yml"
@@ -5434,7 +5434,7 @@
"description": "Sealed System Volume is a security feature introduced in macOS 11.0 Big Sur. During system installation, a SHA-256 cryptographic hash is calculated for all immutable system files and stored in a Merkle tree which itself is hashed as the Seal. Both are stored in the metadata of the snapshot created of the System volume. The seal is verified by the boot loader at startup. macOS will not boot if system files have been tampered with. If validation fails, the user will be instructed to reinstall the operating system. During read operations for files located in the Sealed System Volume, a hash is calculated and compared to the value stored in the Merkle tree."
}
],
- "notes": "This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).",
+ "notes": "This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).",
"evented": false,
"url": "https://fleetdm.com/tables/csrutil_info",
"fleetRepoUrl": "https://github.com/fleetdm/fleet/blob/main/schema/tables/csrutil_info.yml"
@@ -9129,7 +9129,7 @@
"description": "The value of the read path and key. The value is the empty string if the key doesn't exist."
}
],
- "notes": "This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).",
+ "notes": "This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).",
"evented": false,
"url": "https://fleetdm.com/tables/dscl",
"fleetRepoUrl": "https://github.com/fleetdm/fleet/blob/main/schema/tables/dscl.yml"
@@ -10020,7 +10020,7 @@
"columns": [
{
"name": "options",
- "description": "The falconctol options to run. Supported values are listed here: `--aid`, `--apd`,`--aph`, `--app`, `--cid`, `--feature`, `--metadata-query`, `--rfm-reason`,`--rfm-state`, `--tags`, `--version`",
+ "description": "The falconctl options to run. Supported values are listed here: `--aid`, `--apd`,`--aph`, `--app`, `--cid`, `--feature`, `--metadata-query`, `--rfm-reason`,`--rfm-state`, `--tags`, `--version`",
"type": "text",
"required": true
}
@@ -10639,7 +10639,7 @@
},
{
"name": "file_lines",
- "notes": "This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).",
+ "notes": "This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).",
"description": "Allows reading an arbitrary file.",
"platforms": [
"darwin",
@@ -10679,7 +10679,7 @@
"description": "The base64-encoded contents of the encrypted FileVault personal recovery key stored at `/var/db/FileVaultPRK.dat` (see also https://developer.apple.com/documentation/devicemanagement/fderecoverykeyescrow)"
}
],
- "notes": "This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).",
+ "notes": "This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).",
"evented": false,
"url": "https://fleetdm.com/tables/filevault_prk",
"fleetRepoUrl": "https://github.com/fleetdm/fleet/blob/main/schema/tables/filevault_prk.yml"
@@ -10705,7 +10705,7 @@
},
{
"name": "filevault_users",
- "notes": "This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).",
+ "notes": "This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).",
"description": "Information on the users able to unlock the current boot volume if protected with FileVault.",
"platforms": [
"darwin"
@@ -10761,7 +10761,7 @@
"description": "Contains the found paths."
}
],
- "notes": "This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd installers can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).",
+ "notes": "This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).",
"evented": false,
"url": "https://fleetdm.com/tables/find_cmd",
"fleetRepoUrl": "https://github.com/fleetdm/fleet/blob/main/schema/tables/find_cmd.yml"
@@ -10978,7 +10978,7 @@
"description": "Output of the `/usr/libexec/firmwarecheckers/eficheck/eficheck --integrity-check` command. This value is only valid when chip is \"intel-t1\"."
}
],
- "notes": "This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).",
+ "notes": "This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).",
"evented": false,
"url": "https://fleetdm.com/tables/firmware_eficheck_integrity_check",
"fleetRepoUrl": "https://github.com/fleetdm/fleet/blob/main/schema/tables/firmware_eficheck_integrity_check.yml"
@@ -11155,7 +11155,7 @@
},
{
"name": "google_chrome_profiles",
- "notes": "This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).",
+ "notes": "This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).",
"description": "Profiles configured in Google Chrome.",
"platforms": [
"darwin",
@@ -11671,7 +11671,7 @@
"description": "whether iCloud Private Relay is on or off. 1 is on. 0 is off."
}
],
- "notes": "This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).",
+ "notes": "This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).",
"evented": false,
"url": "https://fleetdm.com/tables/icloud_private_relay",
"fleetRepoUrl": "https://github.com/fleetdm/fleet/blob/main/schema/tables/icloud_private_relay.yml"
@@ -15097,7 +15097,7 @@
},
{
"name": "macadmins_unified_log",
- "notes": "This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).",
+ "notes": "This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).",
"description": "Allows querying macOS [unified logs](https://developer.apple.com/documentation/os/logging).",
"platforms": [
"darwin"
@@ -15213,7 +15213,7 @@
},
{
"name": "macos_profiles",
- "notes": "This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).",
+ "notes": "This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).",
"description": "High level information on installed profiles enrollment.",
"platforms": [
"darwin"
@@ -15275,7 +15275,7 @@
},
{
"name": "macos_rsr",
- "notes": "This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).",
+ "notes": "This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).",
"description": "Returns information about installed Rapid Security Responses (RSRs).",
"platforms": [
"darwin"
@@ -15896,7 +15896,7 @@
},
{
"name": "mdm",
- "notes": "This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer). Code based on work by [Kolide](https://github.com/kolide/launcher).
Due to changes in macOS 12.3, the output of `profiles show -type enrollment` can only be generated once a day. If you are running this command with another tool, you should set the `PROFILES_SHOW_ENROLLMENT_CACHE_PATH` environment variable to the path you are caching this. The cache file should be `json` with the keys `dep_capable` and `rate_limited present`, both booleans representing whether the device is capable of DEP enrollment and whether the response from `profiles show -type enrollment` is being rate limited or not.",
+ "notes": "- This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).\n- Code based on work by [Kolide](https://github.com/kolide/launcher).\n- Due to changes in macOS 12.3, the output of `profiles show -type enrollment` can only be generated once a day. If you are running this command with another tool, you should set the `PROFILES_SHOW_ENROLLMENT_CACHE_PATH` environment variable to the path you are caching this. The cache file should be `json` with the keys `dep_capable` and `rate_limited present`, both booleans representing whether the device is capable of DEP enrollment and whether the response from `profiles show -type enrollment` is being rate limited or not.",
"description": "Information on the device's MDM enrollment.",
"platforms": [
"darwin"
@@ -16024,7 +16024,7 @@
"description": "The full raw output of the MDM command execution."
}
],
- "notes": "This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).",
+ "notes": "This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).",
"evented": false,
"url": "https://fleetdm.com/tables/mdm_bridge",
"fleetRepoUrl": "https://github.com/fleetdm/fleet/blob/main/schema/tables/mdm_bridge.yml"
@@ -16911,7 +16911,7 @@
},
{
"name": "munki_info",
- "notes": "This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).
Code based on work by [macadmins/osquery-extension](https://github.com/macadmins/osquery-extension) and [Kolide](https://github.com/kolide/launcher).",
+ "notes": "- This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).\n- Code based on work by [macadmins/osquery-extension](https://github.com/macadmins/osquery-extension) and [Kolide](https://github.com/kolide/launcher).",
"description": "Information from the last [Munki](https://github.com/munki/munki) run.",
"platforms": [
"darwin"
@@ -16979,7 +16979,7 @@
},
{
"name": "munki_installs",
- "notes": "This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).
Code based on work by [macadmins/osquery-extension](https://github.com/macadmins/osquery-extension) and [Kolide](https://github.com/kolide/launcher).",
+ "notes": "- This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).\n- Code based on work by [macadmins/osquery-extension](https://github.com/macadmins/osquery-extension) and [Kolide](https://github.com/kolide/launcher).",
"description": "Software packages and other items [Munki](https://github.com/munki/munki) is managing.",
"platforms": [
"darwin"
@@ -17536,7 +17536,7 @@
"description": "Apple Mobile File Integrity (AMFI) was first released in macOS 10.12. The daemon and service block attempts to run unsigned code. AMFI uses lanchd, code signatures, certificates, entitlements, and provisioning profiles to create a filtered entitlement dictionary for an app. AMFI is the macOS kernel module that enforces code-signing and library validation. Note: AMFI cannot be disabled with SIP enabled, but a change attempt can be made that will appear successful, and report incorrectly as successful. If the AMFI audit fails, and the SIP audit passes, this is still an issue the admin should research."
}
],
- "notes": "This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).",
+ "notes": "This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).",
"evented": false,
"url": "https://fleetdm.com/tables/nvram_info",
"fleetRepoUrl": "https://github.com/fleetdm/fleet/blob/main/schema/tables/nvram_info.yml"
@@ -17715,7 +17715,7 @@
"description": "1 if running scripts is enabled, 0 if disabled."
}
],
- "notes": "This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).",
+ "notes": "This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).",
"evented": false,
"url": "https://fleetdm.com/tables/orbit_info",
"fleetRepoUrl": "https://github.com/fleetdm/fleet/blob/main/schema/tables/orbit_info.yml"
@@ -18794,7 +18794,7 @@
},
{
"name": "parse_ini",
- "notes": "This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).",
+ "notes": "This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).",
"description": "Parse a file as INI configuration.",
"platforms": [
"darwin",
@@ -18839,7 +18839,7 @@
},
{
"name": "parse_json",
- "notes": "This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).",
+ "notes": "This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).",
"description": "Parses an entire file as JSON. See `parse_jsonl` where multiple JSON documents are supported.",
"platforms": [
"darwin",
@@ -18884,7 +18884,7 @@
},
{
"name": "parse_jsonl",
- "notes": "This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).",
+ "notes": "This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).",
"description": "Parses each line of a file as a separate JSON document. See `parse_json` to treat an entire file as a single JSON document.",
"platforms": [
"darwin",
@@ -18929,7 +18929,7 @@
},
{
"name": "parse_xml",
- "notes": "This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).",
+ "notes": "This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).",
"description": "Parses a file as an XML document.",
"platforms": [
"darwin",
@@ -19639,7 +19639,7 @@
"description": "Result of the command in JSON format."
}
],
- "notes": "This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd installers can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).",
+ "notes": "This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).",
"evented": false,
"url": "https://fleetdm.com/tables/pmset",
"fleetRepoUrl": "https://github.com/fleetdm/fleet/blob/main/schema/tables/pmset.yml"
@@ -22060,7 +22060,7 @@
},
{
"name": "puppet_info",
- "notes": "This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).",
+ "notes": "This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).",
"description": "Information on the last [Puppet](https://puppet.com/) run. This table uses data from the `last_run_report` that Puppet creates.",
"platforms": [
"darwin",
@@ -22131,7 +22131,7 @@
"type": "text"
},
{
- "name": "noop_prending",
+ "name": "noop_pending",
"description": "Items pending from a [noop](https://puppet.com/docs/puppet/latest/metaparameter.html#noop) run.",
"required": false,
"type": "text"
@@ -22178,7 +22178,7 @@
},
{
"name": "puppet_logs",
- "notes": "This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).",
+ "notes": "This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).",
"description": "Outputs [Puppet](https://puppet.com/) logs from the last run.",
"platforms": [
"darwin",
@@ -22230,7 +22230,7 @@
},
{
"name": "puppet_state",
- "notes": "This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).",
+ "notes": "This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).",
"description": "State of every resource [Puppet](https://puppet.com/) is managing. This table uses data from the `last_run_report` that Puppet creates.",
"platforms": [
"darwin",
@@ -22327,7 +22327,7 @@
"platforms": [
"darwin"
],
- "description": "Password Policiy (e.g max failed password attempts).",
+ "description": "Password Policy (e.g., max failed password attempts).",
"columns": [
{
"name": "max_failed_attempts",
@@ -22360,7 +22360,7 @@
"description": "This parameter indicates the minimum number of mixed characters in a password."
}
],
- "notes": "This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd installers can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).",
+ "notes": "This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).",
"evented": false,
"url": "https://fleetdm.com/tables/pwd_policy",
"fleetRepoUrl": "https://github.com/fleetdm/fleet/blob/main/schema/tables/pwd_policy.yml"
@@ -25003,7 +25003,7 @@
"description": "Offset between the host's time and the SNTP time in milliseconds."
}
],
- "notes": "This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd installers can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).",
+ "notes": "This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).",
"evented": false,
"url": "https://fleetdm.com/tables/sntp_request",
"fleetRepoUrl": "https://github.com/fleetdm/fleet/blob/main/schema/tables/sntp_request.yml"
@@ -25191,7 +25191,7 @@
"description": "If true, means one of the Apple softwares installed on this machine has a new available upgrade."
}
],
- "notes": "This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).",
+ "notes": "This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).",
"evented": false,
"url": "https://fleetdm.com/tables/software_update",
"fleetRepoUrl": "https://github.com/fleetdm/fleet/blob/main/schema/tables/software_update.yml"
@@ -25343,7 +25343,7 @@
"description": "A JSON document with the key value pairs parsed from `sudo -V` output."
}
],
- "notes": "This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).",
+ "notes": "This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).",
"evented": false,
"url": "https://fleetdm.com/tables/sudo_info",
"fleetRepoUrl": "https://github.com/fleetdm/fleet/blob/main/schema/tables/sudo_info.yml"
@@ -27084,7 +27084,7 @@
"description": "whether password hint is enabled for any user. 1 means one or more users has a password hint set, 0 means no user has a password hint set"
}
],
- "notes": "This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).",
+ "notes": "This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).",
"evented": false,
"url": "https://fleetdm.com/tables/user_login_settings",
"fleetRepoUrl": "https://github.com/fleetdm/fleet/blob/main/schema/tables/user_login_settings.yml"
diff --git a/schema/tables/puppet_info.yml b/schema/tables/puppet_info.yml
index 81a22390b8..87794420f0 100644
--- a/schema/tables/puppet_info.yml
+++ b/schema/tables/puppet_info.yml
@@ -55,7 +55,7 @@ columns:
description: Indicates if Puppet was run in [noop](https://puppet.com/docs/puppet/latest/metaparameter.html#noop) mode.
required: false
type: text
- - name: noop_prending
+ - name: noop_pending
description: Items pending from a [noop](https://puppet.com/docs/puppet/latest/metaparameter.html#noop) run.
required: false
type: text
@@ -82,4 +82,4 @@ columns:
- name: transaction_uuid
description: The [UUID](https://en.wikipedia.org/wiki/Universally_unique_identifier) of the transaction.
required: false
- type: text
\ No newline at end of file
+ type: text
diff --git a/server/cron/calendar_cron.go b/server/cron/calendar_cron.go
index adfd76509e..becabff31c 100644
--- a/server/cron/calendar_cron.go
+++ b/server/cron/calendar_cron.go
@@ -5,6 +5,8 @@ import (
"errors"
"fmt"
"slices"
+ "strconv"
+ "strings"
"sync"
"time"
@@ -18,6 +20,8 @@ import (
)
const calendarConsumers = 18
+const defaultDescription = "needs to make sure your device meets the organization's requirements."
+const defaultResolution = "During this maintenance window, you can expect updates to be applied automatically. Your device may be unavailable during this time."
func NewCalendarSchedule(
ctx context.Context,
@@ -63,11 +67,13 @@ func cronCalendarEvents(ctx context.Context, ds fleet.Datastore, logger kitlog.L
googleCalendarIntegrationConfig := appConfig.Integrations.GoogleCalendar[0]
domain := googleCalendarIntegrationConfig.Domain
- teams, err := ds.ListTeams(ctx, fleet.TeamFilter{
- User: &fleet.User{
- GlobalRole: ptr.String(fleet.RoleAdmin),
- },
- }, fleet.ListOptions{})
+ teams, err := ds.ListTeams(
+ ctx, fleet.TeamFilter{
+ User: &fleet.User{
+ GlobalRole: ptr.String(fleet.RoleAdmin),
+ },
+ }, fleet.ListOptions{},
+ )
if err != nil {
return fmt.Errorf("list teams: %w", err)
}
@@ -182,7 +188,7 @@ func cronCalendarEventsForTeam(
"msg", "failing_hosts", "took", time.Since(start),
)
- // At last we want to log the hosts that are failing and don't have an associated email.
+ // At last, we want to log the hosts that are failing and don't have an associated email.
logHostsWithoutAssociatedEmail(
domain,
failingHostsWithoutAssociatedEmail,
@@ -204,6 +210,9 @@ func processCalendarFailingHosts(
hostsCh := make(chan fleet.HostPolicyMembershipData)
var wg sync.WaitGroup
+ // policyIDtoPolicy maps policy IDs to policies.
+ // It is a cache to avoid querying the database for each host.
+ policyIDtoPolicy := sync.Map{}
for i := 0; i < calendarConsumers; i++ {
wg.Add(+1)
@@ -248,14 +257,14 @@ func processCalendarFailingHosts(
switch {
case err == nil && !expiredEvent:
if err := processFailingHostExistingCalendarEvent(
- ctx, ds, userCalendar, orgName, hostCalendarEvent, calendarEvent, host,
+ ctx, ds, userCalendar, orgName, hostCalendarEvent, calendarEvent, host, &policyIDtoPolicy, logger,
); err != nil {
level.Info(logger).Log("msg", "process failing host existing calendar event", "err", err)
continue // continue with next host
}
case fleet.IsNotFound(err) || expiredEvent:
if err := processFailingHostCreateCalendarEvent(
- ctx, ds, userCalendar, orgName, host,
+ ctx, ds, userCalendar, orgName, host, &policyIDtoPolicy, logger,
); err != nil {
level.Info(logger).Log("msg", "process failing host create calendar event", "err", err)
continue // continue with next host
@@ -303,6 +312,8 @@ func processFailingHostExistingCalendarEvent(
hostCalendarEvent *fleet.HostCalendarEvent,
calendarEvent *fleet.CalendarEvent,
host fleet.HostPolicyMembershipData,
+ policyIDtoPolicy *sync.Map,
+ logger kitlog.Logger,
) error {
updatedEvent := calendarEvent
updated := false
@@ -310,9 +321,11 @@ func processFailingHostExistingCalendarEvent(
if shouldReloadCalendarEvent(now, calendarEvent, hostCalendarEvent) {
var err error
- updatedEvent, _, err = calendar.GetAndUpdateEvent(calendarEvent, func(conflict bool) string {
- return generateCalendarEventBody(orgName, host.HostDisplayName, conflict)
- })
+ updatedEvent, _, err = calendar.GetAndUpdateEvent(
+ calendarEvent, func(conflict bool) string {
+ return generateCalendarEventBody(ctx, ds, orgName, host, policyIDtoPolicy, conflict, logger)
+ },
+ )
if err != nil {
return fmt.Errorf("get event calendar on db: %w", err)
}
@@ -321,7 +334,8 @@ func processFailingHostExistingCalendarEvent(
}
if updated {
- if err := ds.UpdateCalendarEvent(ctx,
+ if err := ds.UpdateCalendarEvent(
+ ctx,
calendarEvent.ID,
updatedEvent.StartTime,
updatedEvent.EndTime,
@@ -400,28 +414,36 @@ func processFailingHostCreateCalendarEvent(
userCalendar fleet.UserCalendar,
orgName string,
host fleet.HostPolicyMembershipData,
+ policyIDtoPolicy *sync.Map,
+ logger kitlog.Logger,
) error {
- calendarEvent, err := attemptCreatingEventOnUserCalendar(orgName, host, userCalendar)
+ calendarEvent, err := attemptCreatingEventOnUserCalendar(ctx, ds, orgName, host, userCalendar, policyIDtoPolicy, logger)
if err != nil {
return fmt.Errorf("create event on user calendar: %w", err)
}
- if _, err := ds.CreateOrUpdateCalendarEvent(ctx, host.Email, calendarEvent.StartTime, calendarEvent.EndTime, calendarEvent.Data, host.HostID, fleet.CalendarWebhookStatusNone); err != nil {
+ if _, err := ds.CreateOrUpdateCalendarEvent(
+ ctx, host.Email, calendarEvent.StartTime, calendarEvent.EndTime, calendarEvent.Data, host.HostID, fleet.CalendarWebhookStatusNone,
+ ); err != nil {
return fmt.Errorf("create calendar event on db: %w", err)
}
return nil
}
func attemptCreatingEventOnUserCalendar(
+ ctx context.Context,
+ ds fleet.Datastore,
orgName string,
host fleet.HostPolicyMembershipData,
userCalendar fleet.UserCalendar,
+ policyIDtoPolicy *sync.Map,
+ logger kitlog.Logger,
) (*fleet.CalendarEvent, error) {
year, month, today := time.Now().Date()
preferredDate := getPreferredCalendarEventDate(year, month, today)
for {
calendarEvent, err := userCalendar.CreateEvent(
preferredDate, func(conflict bool) string {
- return generateCalendarEventBody(orgName, host.HostDisplayName, conflict)
+ return generateCalendarEventBody(ctx, ds, orgName, host, policyIDtoPolicy, conflict, logger)
},
)
var dee fleet.DayEndedError
@@ -469,6 +491,8 @@ func addBusinessDay(date time.Time) time.Time {
nextBusinessDay += 2
case time.Saturday:
nextBusinessDay += 1
+ default:
+ // nextBusinessDay is 1
}
return date.AddDate(0, 0, nextBusinessDay)
}
@@ -490,10 +514,12 @@ func removeCalendarEventsFromPassingHosts(
}
emails := make([]emailWithHosts, 0, len(hostIDsByEmail))
for email, hostIDs := range hostIDsByEmail {
- emails = append(emails, emailWithHosts{
- email: email,
- hostIDs: hostIDs,
- })
+ emails = append(
+ emails, emailWithHosts{
+ email: email,
+ hostIDs: hostIDs,
+ },
+ )
}
emailsCh := make(chan emailWithHosts)
@@ -555,19 +581,74 @@ func logHostsWithoutAssociatedEmail(
)
}
-func generateCalendarEventBody(orgName, hostDisplayName string, conflict bool) string {
+func generateCalendarEventBody(
+ ctx context.Context, ds fleet.Datastore, orgName string, host fleet.HostPolicyMembershipData, policyIDtoPolicy *sync.Map, conflict bool,
+ logger kitlog.Logger,
+) string {
+ description, resolution := getCalendarEventDescriptionAndResolution(ctx, ds, orgName, host, policyIDtoPolicy, logger)
+
conflictStr := ""
if conflict {
conflictStr = " because there was no remaining availability"
}
- return fmt.Sprintf(`Please leave your computer on and connected to power.
+ return fmt.Sprintf(
+ `%s reserved this time to make some changes to your work computer%s.
-Expect an automated restart.
+Please leave your device on and connected to power.
-%s reserved this time to fix %s%s.`, orgName, hostDisplayName, conflictStr,
+Why it matters
+%s
+
+What we'll do
+%s
+`,
+ orgName, conflictStr, description, resolution,
)
}
+func getCalendarEventDescriptionAndResolution(
+ ctx context.Context, ds fleet.Datastore, orgName string, host fleet.HostPolicyMembershipData, policyIDtoPolicy *sync.Map,
+ logger kitlog.Logger,
+) (string, string) {
+ getDefaultDescription := func() string {
+ return fmt.Sprintf(`%s %s`, orgName, defaultDescription)
+ }
+
+ var description, resolution string
+ policyIDs := strings.Split(host.FailingPolicyIDs, ",")
+ if len(policyIDs) == 1 && policyIDs[0] != "" {
+ var policy *fleet.PolicyLite
+ policyAny, ok := policyIDtoPolicy.Load(policyIDs[0])
+ if !ok {
+ id, err := strconv.ParseUint(policyIDs[0], 10, 64)
+ if err != nil {
+ level.Error(logger).Log("msg", "parse policy id", "err", err)
+ return getDefaultDescription(), defaultResolution
+ }
+ policy, err = ds.PolicyLite(ctx, uint(id))
+ if err != nil {
+ level.Error(logger).Log("msg", "get policy", "err", err)
+ return getDefaultDescription(), defaultResolution
+ }
+ policyIDtoPolicy.Store(policyIDs[0], policy)
+ } else {
+ policy = policyAny.(*fleet.PolicyLite)
+ }
+ policyDescription := strings.TrimSpace(policy.Description)
+ if policyDescription == "" || policy.Resolution == nil || strings.TrimSpace(*policy.Resolution) == "" {
+ description = getDefaultDescription()
+ resolution = defaultResolution
+ } else {
+ description = policyDescription
+ resolution = strings.TrimSpace(*policy.Resolution)
+ }
+ } else {
+ description = getDefaultDescription()
+ resolution = defaultResolution
+ }
+ return description, resolution
+}
+
func isHostOnline(ctx context.Context, ds fleet.Datastore, hostID uint) (bool, error) {
hostLite, err := ds.HostLiteByID(ctx, hostID)
if err != nil {
@@ -616,11 +697,13 @@ func cronCalendarEventsCleanup(ctx context.Context, ds fleet.Datastore, logger k
// Feature is configured globally, but now we have to check team by team.
//
- teams, err := ds.ListTeams(ctx, fleet.TeamFilter{
- User: &fleet.User{
- GlobalRole: ptr.String(fleet.RoleAdmin),
- },
- }, fleet.ListOptions{})
+ teams, err := ds.ListTeams(
+ ctx, fleet.TeamFilter{
+ User: &fleet.User{
+ GlobalRole: ptr.String(fleet.RoleAdmin),
+ },
+ }, fleet.ListOptions{},
+ )
if err != nil {
return fmt.Errorf("list teams: %w", err)
}
@@ -709,13 +792,15 @@ func cleanupTeamCalendarEvents(
return nil
}
// Feature is enabled but there are no calendar policies,
- // so we want to cleanup all calendar events for the team.
+ // so we want to clean up all calendar events for the team.
}
return deleteAllCalendarEvents(ctx, ds, calendarConfig, &team.ID, logger)
}
-func deleteCalendarEvent(ctx context.Context, ds fleet.Datastore, userCalendar fleet.UserCalendar, calendarEvent *fleet.CalendarEvent) error {
+func deleteCalendarEvent(
+ ctx context.Context, ds fleet.Datastore, userCalendar fleet.UserCalendar, calendarEvent *fleet.CalendarEvent,
+) error {
if userCalendar != nil {
// Only delete events from the user's calendar if the event is in the future.
if eventInFuture := time.Now().Before(calendarEvent.StartTime); eventInFuture {
diff --git a/server/cron/calendar_cron_test.go b/server/cron/calendar_cron_test.go
index 21e2a8fe1d..94c2576cd1 100644
--- a/server/cron/calendar_cron_test.go
+++ b/server/cron/calendar_cron_test.go
@@ -2,7 +2,10 @@ package cron
import (
"context"
+ "encoding/json"
"fmt"
+ "github.com/fleetdm/fleet/v4/server/ptr"
+ "github.com/stretchr/testify/assert"
"os"
"strconv"
"strings"
@@ -166,9 +169,10 @@ func TestEventForDifferentHost(t *testing.T) {
require.Equal(t, []uint{policyID1}, policyIDs)
return []fleet.HostPolicyMembershipData{
{
- HostID: hostID1,
- Email: userEmail1,
- Passing: false,
+ HostID: hostID1,
+ Email: userEmail1,
+ Passing: false,
+ FailingPolicyIDs: fmt.Sprintf("%d", policyID1),
},
}, nil
}
@@ -278,9 +282,10 @@ func TestCalendarEventsMultipleHosts(t *testing.T) {
require.Equal(t, []uint{policyID1, policyID2}, policyIDs)
return []fleet.HostPolicyMembershipData{
{
- HostID: hostID1,
- Email: userEmail1,
- Passing: false,
+ HostID: hostID1,
+ Email: userEmail1,
+ Passing: false,
+ FailingPolicyIDs: fmt.Sprintf("%d,%d", policyID1, policyID2),
},
{
HostID: hostID2,
@@ -288,9 +293,10 @@ func TestCalendarEventsMultipleHosts(t *testing.T) {
Passing: true,
},
{
- HostID: hostID3,
- Email: "", // because it does not belong to example.com
- Passing: false,
+ HostID: hostID3,
+ Email: "", // because it does not belong to example.com
+ Passing: false,
+ FailingPolicyIDs: fmt.Sprintf("%d,%d", policyID1, policyID2),
},
{
HostID: hostID4,
@@ -299,6 +305,23 @@ func TestCalendarEventsMultipleHosts(t *testing.T) {
},
}, nil
}
+ ds.PolicyLiteFunc = func(ctx context.Context, policyID uint) (*fleet.PolicyLite, error) {
+ switch policyID {
+ case policyID1:
+ return &fleet.PolicyLite{
+ ID: policyID1,
+ Description: "Policy 1",
+ }, nil
+ case policyID2:
+ return &fleet.PolicyLite{
+ ID: policyID2,
+ Description: "Policy 2",
+ }, nil
+ default:
+ t.Errorf("unexpected policy ID: %d", policyID)
+ return nil, nil
+ }
+ }
ds.GetHostCalendarEventByEmailFunc = func(ctx context.Context, email string) (*fleet.HostCalendarEvent, *fleet.CalendarEvent, error) {
return nil, nil, notFoundErr{}
@@ -352,6 +375,8 @@ func TestCalendarEventsMultipleHosts(t *testing.T) {
createdCalendarEvents := calendar.ListGoogleMockEvents()
require.Len(t, createdCalendarEvents, 1)
+ strings.Contains(createdCalendarEvents["1"].Description, defaultDescription)
+ strings.Contains(createdCalendarEvents["1"].Description, defaultResolution)
}
type notFoundErr struct{}
@@ -531,13 +556,36 @@ func TestCalendarEvents1KHosts(t *testing.T) {
hosts := make([]fleet.HostPolicyMembershipData, 0, 1000)
for i := 0; i < 1000; i++ {
- hosts = append(hosts, fleet.HostPolicyMembershipData{
+ newHost := fleet.HostPolicyMembershipData{
Email: fmt.Sprintf("user%d@example.com", i),
Passing: i%2 == 0,
HostID: uint(i),
HostDisplayName: fmt.Sprintf("display_name%d", i),
HostHardwareSerial: fmt.Sprintf("serial%d", i),
- })
+ }
+ if !newHost.Passing {
+ switch {
+ case i >= 0 && i < 200:
+ newHost.FailingPolicyIDs = fmt.Sprintf("%d,%d", policyID1, policyID2)
+ case i >= 200 && i < 400:
+ newHost.FailingPolicyIDs = fmt.Sprintf("%d", policyID4)
+ case i >= 400 && i < 600:
+ newHost.FailingPolicyIDs = fmt.Sprintf("%d", policyID5)
+ case i >= 600 && i < 800:
+ newHost.FailingPolicyIDs = fmt.Sprintf("%d,%d", policyID7, policyID8)
+ default:
+ newHost.FailingPolicyIDs = fmt.Sprintf("%d,%d", policyID9, policyID10)
+ }
+ }
+ hosts = append(hosts, newHost)
+ }
+ ds.PolicyLiteFunc = func(ctx context.Context, policyID uint) (*fleet.PolicyLite, error) {
+ resolution := fmt.Sprintf("Resolution for policy %d", policyID)
+ return &fleet.PolicyLite{
+ ID: policyID,
+ Description: fmt.Sprintf("Policy %d", policyID),
+ Resolution: &resolution,
+ }, nil
}
ds.GetTeamHostsPolicyMembershipsFunc = func(
@@ -645,3 +693,262 @@ func TestCalendarEvents1KHosts(t *testing.T) {
createdCalendarEvents = calendar.ListGoogleMockEvents()
require.Len(t, createdCalendarEvents, 0)
}
+
+// TestEventDescription tests generation of the event description.
+func TestEventDescription(t *testing.T) {
+ ds := new(mock.Store)
+ ctx := context.Background()
+ logger := kitlog.With(kitlog.NewLogfmtLogger(os.Stdout))
+ t.Cleanup(
+ func() {
+ calendar.ClearMockEvents()
+ },
+ )
+
+ //
+ // Test setup
+ //
+ // team1:
+ //
+ // policyID1 (calendar) -- has description and resolution
+ // policyID2 (calendar) -- has description, but blank resolution
+ // policyID3 (calendar) -- has description, but nil resolution
+ // policyID4 (calendar) -- has no description, but has resolution
+ // policyID5 (calendar) -- returns error on lookup
+ //
+ // hostID1 not passing policyID1
+ // hostID2 not passing policyID2
+ // hostID3 not passing policyID3
+ // hostID4 not passing policyID4
+ // hostID5 not passing policies 1,2,3,4
+ // hostID6 also not passing policyID1
+ // hostID7 not passing policyID5
+ //
+
+ const orgName = "Test Organization"
+ ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
+ return &fleet.AppConfig{
+ OrgInfo: fleet.OrgInfo{
+ OrgName: orgName,
+ },
+ Integrations: fleet.Integrations{
+ GoogleCalendar: []*fleet.GoogleCalendarIntegration{
+ {
+ Domain: "example.com",
+ ApiKey: map[string]string{
+ fleet.GoogleCalendarEmail: "calendar-mock@example.com",
+ },
+ },
+ },
+ },
+ }, nil
+ }
+
+ teamID1 := uint(1)
+ ds.ListTeamsFunc = func(ctx context.Context, filter fleet.TeamFilter, opt fleet.ListOptions) ([]*fleet.Team, error) {
+ return []*fleet.Team{
+ {
+ ID: teamID1,
+ Config: fleet.TeamConfig{
+ Integrations: fleet.TeamIntegrations{
+ GoogleCalendar: &fleet.TeamGoogleCalendarIntegration{
+ Enable: true,
+ WebhookURL: "https://foo.example.com",
+ },
+ },
+ },
+ },
+ }, nil
+ }
+
+ policyID1 := uint(10)
+ policyID2 := uint(11)
+ policyID3 := uint(12)
+ policyID4 := uint(13)
+ policyID5 := uint(14)
+ ds.GetCalendarPoliciesFunc = func(ctx context.Context, teamID uint) ([]fleet.PolicyCalendarData, error) {
+ require.Equal(t, teamID1, teamID)
+ return []fleet.PolicyCalendarData{
+ {
+ ID: policyID1,
+ Name: "Policy 1",
+ },
+ {
+ ID: policyID2,
+ Name: "Policy 2",
+ },
+ {
+ ID: policyID3,
+ Name: "Policy 3",
+ },
+ {
+ ID: policyID4,
+ Name: "Policy 4",
+ },
+ {
+ ID: policyID5,
+ Name: "Policy 5",
+ },
+ }, nil
+ }
+
+ hostID1, userEmail1 := uint(100), "user1@example.com"
+ hostID2, userEmail2 := uint(101), "user2@example.com"
+ hostID3, userEmail3 := uint(102), "user3@example.com"
+ hostID4, userEmail4 := uint(103), "user4@example.com"
+ hostID5, userEmail5 := uint(104), "user5@example.com"
+ hostID6, userEmail6 := uint(105), "user6@example.com"
+ hostID7, userEmail7 := uint(106), "user7@example.com"
+
+ ds.GetTeamHostsPolicyMembershipsFunc = func(
+ ctx context.Context, domain string, teamID uint, policyIDs []uint,
+ ) ([]fleet.HostPolicyMembershipData, error) {
+ require.Equal(t, "example.com", domain)
+ require.Equal(t, teamID1, teamID)
+ require.Equal(t, []uint{policyID1, policyID2, policyID3, policyID4, policyID5}, policyIDs)
+ return []fleet.HostPolicyMembershipData{
+ {
+ HostID: hostID1,
+ Email: userEmail1,
+ Passing: false,
+ FailingPolicyIDs: fmt.Sprintf("%d", policyID1),
+ },
+ {
+ HostID: hostID2,
+ Email: userEmail2,
+ Passing: false,
+ FailingPolicyIDs: fmt.Sprintf("%d", policyID2),
+ },
+ {
+ HostID: hostID3,
+ Email: userEmail3,
+ Passing: false,
+ FailingPolicyIDs: fmt.Sprintf("%d", policyID3),
+ },
+ {
+ HostID: hostID4,
+ Email: userEmail4,
+ Passing: false,
+ FailingPolicyIDs: fmt.Sprintf("%d", policyID4),
+ },
+ {
+ HostID: hostID5,
+ Email: userEmail5,
+ Passing: false,
+ FailingPolicyIDs: fmt.Sprintf("%d,%d,%d,%d", policyID1, policyID2, policyID3, policyID4),
+ },
+ {
+ HostID: hostID6,
+ Email: userEmail6,
+ Passing: false,
+ FailingPolicyIDs: fmt.Sprintf("%d", policyID1),
+ },
+ {
+ HostID: hostID7,
+ Email: userEmail7,
+ Passing: false,
+ FailingPolicyIDs: fmt.Sprintf("%d", policyID5),
+ },
+ }, nil
+ }
+ ds.PolicyLiteFunc = func(ctx context.Context, policyID uint) (*fleet.PolicyLite, error) {
+ switch policyID {
+ case policyID1:
+ return &fleet.PolicyLite{
+ ID: policyID1,
+ Description: "Description for policy 1",
+ Resolution: ptr.String("Resolution for policy 1"),
+ }, nil
+ case policyID2:
+ return &fleet.PolicyLite{
+ ID: policyID2,
+ Description: "Description for policy 2",
+ Resolution: ptr.String(""),
+ }, nil
+ case policyID3:
+ return &fleet.PolicyLite{
+ ID: policyID2,
+ Description: "Description for policy 3",
+ Resolution: nil,
+ }, nil
+ case policyID4:
+ return &fleet.PolicyLite{
+ ID: policyID4,
+ Resolution: ptr.String("Resolution for policy 4"),
+ }, nil
+ case policyID5:
+ return nil, notFoundErr{}
+ default:
+ t.Errorf("unexpected policy ID: %d", policyID)
+ return nil, nil
+ }
+ }
+
+ ds.GetHostCalendarEventByEmailFunc = func(ctx context.Context, email string) (*fleet.HostCalendarEvent, *fleet.CalendarEvent, error) {
+ return nil, nil, notFoundErr{}
+ }
+
+ var eventsMu sync.Mutex
+ calendarEvents := make(map[uint]*fleet.CalendarEvent)
+ hostCalendarEvents := make(map[uint]*fleet.HostCalendarEvent)
+
+ ds.CreateOrUpdateCalendarEventFunc = func(
+ ctx context.Context,
+ email string,
+ startTime, endTime time.Time,
+ data []byte,
+ hostID uint,
+ webhookStatus fleet.CalendarWebhookStatus,
+ ) (*fleet.CalendarEvent, error) {
+ require.Equal(t, fleet.CalendarWebhookStatusNone, webhookStatus)
+ require.NotEmpty(t, data)
+ require.NotZero(t, startTime)
+ require.NotZero(t, endTime)
+
+ eventsMu.Lock()
+ calendarEventID := uint(len(calendarEvents) + 1)
+ calendarEvents[hostID] = &fleet.CalendarEvent{
+ ID: calendarEventID,
+ Email: email,
+ StartTime: startTime,
+ EndTime: endTime,
+ Data: data,
+ }
+ hostCalendarEventID := uint(len(hostCalendarEvents) + 1)
+ hostCalendarEvents[hostID] = &fleet.HostCalendarEvent{
+ ID: hostCalendarEventID,
+ HostID: hostID,
+ CalendarEventID: calendarEventID,
+ WebhookStatus: webhookStatus,
+ }
+ eventsMu.Unlock()
+ return nil, nil
+ }
+
+ err := cronCalendarEvents(ctx, ds, logger)
+ require.NoError(t, err)
+
+ numberOfEvents := 7
+ eventsMu.Lock()
+ require.Len(t, calendarEvents, numberOfEvents)
+ require.Len(t, hostCalendarEvents, numberOfEvents)
+ eventsMu.Unlock()
+
+ createdCalendarEvents := calendar.ListGoogleMockEvents()
+ require.Len(t, createdCalendarEvents, numberOfEvents)
+ for _, hostCalEvent := range hostCalendarEvents {
+ var details map[string]string
+ err = json.Unmarshal(calendarEvents[hostCalEvent.HostID].Data, &details)
+ require.NoError(t, err)
+ description := createdCalendarEvents[details["id"]].Description
+ defaultDescriptionWithOrg := fmt.Sprintf("%s %s", orgName, defaultDescription)
+ switch hostCalEvent.HostID {
+ case hostID1, hostID6:
+ assert.Contains(t, description, "Description for policy 1")
+ assert.Contains(t, description, "Resolution for policy 1")
+ default:
+ assert.Contains(t, description, defaultDescriptionWithOrg)
+ assert.Contains(t, description, defaultResolution)
+ }
+ }
+}
diff --git a/server/datastore/mysql/activities.go b/server/datastore/mysql/activities.go
index 83bf7790b7..8005f995f0 100644
--- a/server/datastore/mysql/activities.go
+++ b/server/datastore/mysql/activities.go
@@ -10,6 +10,7 @@ import (
"github.com/fleetdm/fleet/v4/pkg/scripts"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/fleet"
+ "github.com/go-kit/log/level"
"github.com/jmoiron/sqlx"
)
@@ -83,7 +84,6 @@ func (ds *Datastore) ListActivities(ctx context.Context, opt fleet.ListActivitie
a.user_id,
a.created_at,
a.activity_type,
- a.details,
a.user_name as name,
a.streamed,
a.user_email
@@ -100,12 +100,44 @@ func (ds *Datastore) ListActivities(ctx context.Context, opt fleet.ListActivitie
activitiesQ, args = appendListOptionsWithCursorToSQL(activitiesQ, args, &opt.ListOptions)
err := sqlx.SelectContext(ctx, ds.reader(ctx), &activities, activitiesQ, args...)
- if err == sql.ErrNoRows {
- return nil, nil, ctxerr.Wrap(ctx, notFound("Activity"))
- } else if err != nil {
+ if err != nil {
return nil, nil, ctxerr.Wrap(ctx, err, "select activities")
}
+ if len(activities) > 0 {
+ // Fetch details as a separate query due to sort buffer issue triggered by large JSON details entries. Issue last reproduced on MySQL 8.0.36
+ // https://stackoverflow.com/questions/29575835/error-1038-out-of-sort-memory-consider-increasing-sort-buffer-size/67266529
+ IDs := make([]uint, 0, len(activities))
+ for _, a := range activities {
+ IDs = append(IDs, a.ID)
+ }
+ detailsStmt, detailsArgs, err := sqlx.In("SELECT id, details FROM activities WHERE id IN (?)", IDs)
+ if err != nil {
+ return nil, nil, ctxerr.Wrap(ctx, err, "Error binding activity IDs")
+ }
+ type activityDetails struct {
+ ID uint `db:"id"`
+ Details *json.RawMessage `db:"details"`
+ }
+ var details []activityDetails
+ err = sqlx.SelectContext(ctx, ds.reader(ctx), &details, detailsStmt, detailsArgs...)
+ if err != nil {
+ return nil, nil, ctxerr.Wrap(ctx, err, "select activities details")
+ }
+ detailsLookup := make(map[uint]*json.RawMessage, len(details))
+ for _, d := range details {
+ detailsLookup[d.ID] = d.Details
+ }
+ for _, a := range activities {
+ det, ok := detailsLookup[a.ID]
+ if !ok {
+ level.Warn(ds.logger).Log("msg", "Activity details not found", "activity_id", a.ID)
+ continue
+ }
+ a.Details = det
+ }
+ }
+
// Fetch users as a stand-alone query (because of performance reasons)
lookup := make(map[uint][]int)
diff --git a/server/datastore/mysql/hosts.go b/server/datastore/mysql/hosts.go
index 56474197b4..70cd850289 100644
--- a/server/datastore/mysql/hosts.go
+++ b/server/datastore/mysql/hosts.go
@@ -2717,6 +2717,9 @@ func (ds *Datastore) AddHostsToTeam(ctx context.Context, teamID *uint, hostIDs [
if err := cleanupPolicyMembershipOnTeamChange(ctx, tx, hostIDs); err != nil {
return ctxerr.Wrap(ctx, err, "AddHostsToTeam delete policy membership")
}
+ if err := cleanupQueryResultsOnTeamChange(ctx, tx, hostIDs); err != nil {
+ return ctxerr.Wrap(ctx, err, "AddHostsToTeam delete query results")
+ }
query, args, err := sqlx.In(`UPDATE hosts SET team_id = ? WHERE id IN (?)`, teamID, hostIDs)
if err != nil {
diff --git a/server/datastore/mysql/hosts_test.go b/server/datastore/mysql/hosts_test.go
index 453a69e1d6..f6e5613b86 100644
--- a/server/datastore/mysql/hosts_test.go
+++ b/server/datastore/mysql/hosts_test.go
@@ -165,6 +165,7 @@ func TestHosts(t *testing.T) {
{"HostHealth", testHostHealth},
{"GetHostOrbitInfo", testGetHostOrbitInfo},
{"HostnamesByIdentifiers", testHostnamesByIdentifiers},
+ {"HostsAddToTeamCleansUpTeamQueryResults", testHostsAddToTeamCleansUpTeamQueryResults},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
@@ -8860,3 +8861,200 @@ func testHostnamesByIdentifiers(t *testing.T, ds *Datastore) {
})
}
}
+
+func testHostsAddToTeamCleansUpTeamQueryResults(t *testing.T, ds *Datastore) {
+ ctx := context.Background()
+
+ team1, err := ds.NewTeam(ctx, &fleet.Team{Name: "team1"})
+ require.NoError(t, err)
+ team2, err := ds.NewTeam(ctx, &fleet.Team{Name: "team2"})
+ require.NoError(t, err)
+
+ hostCount := 1
+ newHost := func(teamID *uint) *fleet.Host {
+ h, err := ds.NewHost(ctx, &fleet.Host{
+ OsqueryHostID: ptr.String(fmt.Sprintf("foobar%d", hostCount)),
+ NodeKey: ptr.String(fmt.Sprintf("nodekey%d", hostCount)),
+ TeamID: teamID,
+ })
+ require.NoError(t, err)
+ hostCount++
+ return h
+ }
+ newQuery := func(name string, teamID *uint) *fleet.Query {
+ q, err := ds.NewQuery(ctx, &fleet.Query{
+ Name: name,
+ Query: "SELECT 1:",
+ TeamID: teamID,
+ Logging: fleet.LoggingSnapshot,
+ })
+ require.NoError(t, err)
+ return q
+ }
+
+ h0 := newHost(nil)
+ h1 := newHost(&team1.ID)
+ h2 := newHost(&team2.ID)
+ h3 := newHost(&team2.ID)
+
+ hostStaticOnTeam1 := newHost(&team1.ID) // host that we won't move
+
+ query0Global := newQuery("query0Global", nil)
+ query1Team1 := newQuery("query1Team1", &team1.ID)
+ query2Team2 := newQuery("query2Team2", &team2.ID)
+
+ // Transfer h2 from team2 to team1 and back without any query results yet.
+ err = ds.AddHostsToTeam(ctx, &team1.ID, []uint{h2.ID})
+ require.NoError(t, err)
+ err = ds.AddHostsToTeam(ctx, &team2.ID, []uint{h2.ID})
+ require.NoError(t, err)
+
+ data := ptr.RawMessage(json.RawMessage(`{"foo": "bar"}`))
+ h0Results := []*fleet.ScheduledQueryResultRow{
+ {
+ HostID: h0.ID,
+ QueryID: query0Global.ID,
+ Data: data,
+ },
+ }
+ h1Global0Results := []*fleet.ScheduledQueryResultRow{
+ {
+ HostID: h1.ID,
+ QueryID: query0Global.ID,
+ Data: data,
+ },
+ }
+ h1Query1Results := []*fleet.ScheduledQueryResultRow{
+ {
+ HostID: h1.ID,
+ QueryID: query1Team1.ID,
+ Data: data,
+ },
+ }
+ h2Global0Results := []*fleet.ScheduledQueryResultRow{
+ {
+ HostID: h2.ID,
+ QueryID: query0Global.ID,
+ Data: data,
+ },
+ }
+ h2Query2Results := []*fleet.ScheduledQueryResultRow{
+ {
+ HostID: h2.ID,
+ QueryID: query2Team2.ID,
+ Data: data,
+ },
+ }
+ h3Global0Results := []*fleet.ScheduledQueryResultRow{
+ {
+ HostID: h3.ID,
+ QueryID: query0Global.ID,
+ Data: data,
+ },
+ }
+ h3Query2Results := []*fleet.ScheduledQueryResultRow{
+ {
+ HostID: h3.ID,
+ QueryID: query2Team2.ID,
+ Data: data,
+ },
+ }
+ h4Global0Results := []*fleet.ScheduledQueryResultRow{
+ {
+ HostID: hostStaticOnTeam1.ID,
+ QueryID: query0Global.ID,
+ Data: data,
+ },
+ }
+ h4Query1Results := []*fleet.ScheduledQueryResultRow{
+ {
+ HostID: hostStaticOnTeam1.ID,
+ QueryID: query1Team1.ID,
+ Data: data,
+ },
+ }
+ for _, results := range [][]*fleet.ScheduledQueryResultRow{
+ h0Results,
+ h1Global0Results,
+ h1Query1Results,
+ h2Global0Results,
+ h2Query2Results,
+ h3Global0Results,
+ h3Query2Results,
+ h4Global0Results,
+ h4Query1Results,
+ } {
+ err = ds.OverwriteQueryResultRows(ctx, results)
+ require.NoError(t, err)
+ }
+
+ tf := fleet.TeamFilter{
+ User: &fleet.User{
+ GlobalRole: ptr.String(fleet.RoleAdmin),
+ },
+ }
+
+ rows, err := ds.QueryResultRows(ctx, query0Global.ID, tf)
+ require.NoError(t, err)
+ require.Len(t, rows, 5)
+ rows, err = ds.QueryResultRows(ctx, query1Team1.ID, tf)
+ require.NoError(t, err)
+ require.Len(t, rows, 2)
+ rows, err = ds.QueryResultRows(ctx, query2Team2.ID, tf)
+ require.NoError(t, err)
+ require.Len(t, rows, 2)
+
+ // Transfer h2 from team2 to team1.
+ err = ds.AddHostsToTeam(ctx, &team1.ID, []uint{h2.ID})
+ require.NoError(t, err)
+ // Transfer h1 from team1 to team2.
+ err = ds.AddHostsToTeam(ctx, &team2.ID, []uint{h1.ID})
+ require.NoError(t, err)
+ // Transfer h3 from team2 to global.
+ err = ds.AddHostsToTeam(ctx, nil, []uint{h3.ID})
+ require.NoError(t, err)
+
+ // No global query results should be deleted
+ rows, err = ds.QueryResultRows(ctx, query0Global.ID, tf)
+ require.NoError(t, err)
+ require.Len(t, rows, 5)
+ // Results for h1 should be gone, and results for hostStaticOnTeam1 should be here.
+ rows, err = ds.QueryResultRows(ctx, query1Team1.ID, tf)
+ require.NoError(t, err)
+ require.Len(t, rows, 1)
+ require.Equal(t, hostStaticOnTeam1.ID, rows[0].HostID)
+ // Results for h2 and h3 should be gone.
+ rows, err = ds.QueryResultRows(ctx, query2Team2.ID, tf)
+ require.NoError(t, err)
+ require.Empty(t, rows)
+
+ // h1 should have only the global result.
+ h1, err = ds.Host(ctx, h1.ID)
+ require.NoError(t, err)
+ require.Len(t, h1.PackStats, 1)
+ require.Len(t, h1.PackStats[0].QueryStats, 1)
+ require.Equal(t, query0Global.ID, h1.PackStats[0].QueryStats[0].ScheduledQueryID)
+
+ // h2 should have only the global result.
+ h2, err = ds.Host(ctx, h2.ID)
+ require.NoError(t, err)
+ require.Len(t, h2.PackStats, 1)
+ require.Len(t, h2.PackStats[0].QueryStats, 1)
+ require.Equal(t, query0Global.ID, h2.PackStats[0].QueryStats[0].ScheduledQueryID)
+
+ // h3 should have only the global result.
+ h3, err = ds.Host(ctx, h3.ID)
+ require.NoError(t, err)
+ require.Len(t, h3.PackStats, 1)
+ require.Len(t, h3.PackStats[0].QueryStats, 1)
+ require.Equal(t, query0Global.ID, h3.PackStats[0].QueryStats[0].ScheduledQueryID)
+
+ // hostStaticOnTeam1 should have the global result and the team1 result.
+ hostStaticOnTeam1, err = ds.Host(ctx, hostStaticOnTeam1.ID)
+ require.NoError(t, err)
+ require.Len(t, hostStaticOnTeam1.PackStats, 2)
+ require.Len(t, hostStaticOnTeam1.PackStats[0].QueryStats, 1)
+ require.Equal(t, query0Global.ID, hostStaticOnTeam1.PackStats[0].QueryStats[0].ScheduledQueryID)
+ require.Len(t, hostStaticOnTeam1.PackStats[1].QueryStats, 1)
+ require.Equal(t, query1Team1.ID, hostStaticOnTeam1.PackStats[1].QueryStats[0].ScheduledQueryID)
+}
diff --git a/server/datastore/mysql/migrations/tables/20240430111727_CleanupQueryResults.go b/server/datastore/mysql/migrations/tables/20240430111727_CleanupQueryResults.go
new file mode 100644
index 0000000000..063a150617
--- /dev/null
+++ b/server/datastore/mysql/migrations/tables/20240430111727_CleanupQueryResults.go
@@ -0,0 +1,30 @@
+package tables
+
+import (
+ "database/sql"
+ "fmt"
+)
+
+func init() {
+ MigrationClient.AddMigration(Up_20240430111727, Down_20240430111727)
+}
+
+func Up_20240430111727(tx *sql.Tx) error {
+ // This cleanup correspond to the following bug: https://github.com/fleetdm/fleet/issues/18079.
+ // The following deletes "team query results" that do not match the host's team.
+ _, err := tx.Exec(`
+ DELETE qr
+ FROM query_results qr
+ JOIN queries q ON (q.id=qr.query_id)
+ JOIN hosts h ON (h.id=qr.host_id)
+ WHERE q.team_id IS NOT NULL AND q.team_id != COALESCE(h.team_id, 0);
+ `)
+ if err != nil {
+ return fmt.Errorf("failed to delete query_results %w", err)
+ }
+ return nil
+}
+
+func Down_20240430111727(tx *sql.Tx) error {
+ return nil
+}
diff --git a/server/datastore/mysql/migrations/tables/20240430111727_CleanupQueryResults_test.go b/server/datastore/mysql/migrations/tables/20240430111727_CleanupQueryResults_test.go
new file mode 100644
index 0000000000..a2f7df2bf8
--- /dev/null
+++ b/server/datastore/mysql/migrations/tables/20240430111727_CleanupQueryResults_test.go
@@ -0,0 +1,98 @@
+package tables
+
+import (
+ "fmt"
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestUp_20240430111727(t *testing.T) {
+ db := applyUpToPrev(t)
+
+ hostID := 1
+ newTeam := func(name string) uint {
+ return uint(execNoErrLastID(t, db,
+ `INSERT INTO teams (name) VALUES (?);`,
+ name,
+ ))
+ }
+ newHost := func(teamID *uint) uint {
+ id := fmt.Sprintf("%d", hostID)
+ hostID++
+ return uint(execNoErrLastID(t, db,
+ `INSERT INTO hosts (osquery_host_id, node_key, team_id) VALUES (?, ?, ?);`,
+ id, id, teamID,
+ ))
+ }
+ newQuery := func(name string, teamID *uint) uint {
+ return uint(execNoErrLastID(t, db,
+ `INSERT INTO queries (name, description, logging_type, team_id, query, saved) VALUES (?, '', 'snapshot', ?, 'SELECT 1;', 1);`,
+ name, teamID,
+ ))
+ }
+ newQueryResults := func(queryID, hostID uint, resultCount int) {
+ var args []interface{}
+ for i := 0; i < resultCount; i++ {
+ args = append(args, queryID, hostID, fmt.Sprintf(`{"foo": "bar%d"}`, i))
+ }
+ values := strings.TrimSuffix(strings.Repeat("(?, ?, ?, NOW()),", resultCount), ",")
+ _, err := db.Exec(fmt.Sprintf(`INSERT INTO query_results (query_id, host_id, data, last_fetched) VALUES %s;`, values),
+ args...,
+ )
+ require.NoError(t, err)
+ }
+
+ team1ID := newTeam("team1")
+ team2ID := newTeam("team2")
+ host1GlobalID := newHost(nil)
+ host2Team1ID := newHost(&team1ID)
+ host3Team2ID := newHost(&team2ID)
+ query1GlobalID := newQuery("query1Global", nil)
+ query2Team1ID := newQuery("query2Team1", &team1ID)
+ query3Team2ID := newQuery("query3Team2", &team2ID)
+
+ newQueryResults(query1GlobalID, host1GlobalID, 1)
+ newQueryResults(query1GlobalID, host2Team1ID, 2)
+ newQueryResults(query1GlobalID, host3Team2ID, 3)
+
+ newQueryResults(query2Team1ID, host1GlobalID, 4)
+ newQueryResults(query2Team1ID, host2Team1ID, 5)
+ newQueryResults(query2Team1ID, host3Team2ID, 6)
+
+ newQueryResults(query3Team2ID, host1GlobalID, 7)
+ newQueryResults(query3Team2ID, host2Team1ID, 8)
+ newQueryResults(query3Team2ID, host3Team2ID, 9)
+
+ // Apply current migration.
+ applyNext(t, db)
+
+ getQueryResultsCount := func(queryID, hostID uint) int {
+ var count int
+ err := db.Get(&count, `SELECT COUNT(*) FROM query_results WHERE query_id = ? AND host_id = ?`, queryID, hostID)
+ require.NoError(t, err)
+ return count
+ }
+
+ count := getQueryResultsCount(query1GlobalID, host1GlobalID)
+ require.Equal(t, 1, count) // result for global queries are not deleted.
+ count = getQueryResultsCount(query1GlobalID, host2Team1ID)
+ require.Equal(t, 2, count) // result for global queries are not deleted.
+ count = getQueryResultsCount(query1GlobalID, host3Team2ID)
+ require.Equal(t, 3, count) // result for global queries are not deleted.
+
+ count = getQueryResultsCount(query2Team1ID, host1GlobalID)
+ require.Equal(t, 0, count) // query results of a team query different than the host's team are deleted.
+ count = getQueryResultsCount(query2Team1ID, host2Team1ID)
+ require.Equal(t, 5, count) // team query results of the host's team are not deleted.
+ count = getQueryResultsCount(query2Team1ID, host3Team2ID)
+ require.Equal(t, 0, count) // query results of a team query different than the host's team are deleted.
+
+ count = getQueryResultsCount(query3Team2ID, host1GlobalID)
+ require.Equal(t, 0, count) // query results of a team query different than the host's team are deleted.
+ count = getQueryResultsCount(query3Team2ID, host2Team1ID)
+ require.Equal(t, 0, count) // query results of a team query different than the host's team are deleted.
+ count = getQueryResultsCount(query3Team2ID, host3Team2ID)
+ require.Equal(t, 9, count) // team query results of the host's team are not deleted.
+}
diff --git a/server/datastore/mysql/mysql.go b/server/datastore/mysql/mysql.go
index 2b5238f1d8..8b0ca18ea0 100644
--- a/server/datastore/mysql/mysql.go
+++ b/server/datastore/mysql/mysql.go
@@ -978,7 +978,7 @@ func (ds *Datastore) whereFilterTeams(filter fleet.TeamFilter, teamKey string) s
if filter.User.GlobalRole != nil {
switch *filter.User.GlobalRole {
- case fleet.RoleAdmin, fleet.RoleMaintainer, fleet.RoleObserverPlus:
+ case fleet.RoleAdmin, fleet.RoleMaintainer, fleet.RoleGitOps, fleet.RoleObserverPlus:
return "TRUE"
case fleet.RoleObserver:
if filter.IncludeObserver {
@@ -995,6 +995,7 @@ func (ds *Datastore) whereFilterTeams(filter fleet.TeamFilter, teamKey string) s
for _, team := range filter.User.Teams {
if team.Role == fleet.RoleAdmin ||
team.Role == fleet.RoleMaintainer ||
+ team.Role == fleet.RoleGitOps ||
team.Role == fleet.RoleObserverPlus ||
(team.Role == fleet.RoleObserver && filter.IncludeObserver) {
idStrs = append(idStrs, strconv.Itoa(int(team.ID)))
diff --git a/server/datastore/mysql/policies.go b/server/datastore/mysql/policies.go
index 1ad7ef8f47..bff19b5b0d 100644
--- a/server/datastore/mysql/policies.go
+++ b/server/datastore/mysql/policies.go
@@ -110,6 +110,21 @@ func policyDB(ctx context.Context, q sqlx.QueryerContext, id uint, teamID *uint)
return &policy, nil
}
+func (ds *Datastore) PolicyLite(ctx context.Context, id uint) (*fleet.PolicyLite, error) {
+ var policy fleet.PolicyLite
+ err := sqlx.GetContext(
+ ctx, ds.reader(ctx), &policy,
+ `SELECT id, description, resolution FROM policies WHERE id=?`, id,
+ )
+ if err != nil {
+ if errors.Is(err, sql.ErrNoRows) {
+ return nil, ctxerr.Wrap(ctx, notFound("Policy").WithID(id))
+ }
+ return nil, ctxerr.Wrap(ctx, err, "getting policy")
+ }
+ return &policy, nil
+}
+
// SavePolicy updates some fields of the given policy on the datastore.
//
// Currently, SavePolicy does not allow updating the team of an existing policy.
@@ -436,6 +451,25 @@ func (ds *Datastore) CountPolicies(ctx context.Context, teamID *uint, matchQuery
return count, nil
}
+func (ds *Datastore) CountMergedTeamPolicies(ctx context.Context, teamID uint, matchQuery string) (int, error) {
+ var args []interface{}
+
+ query := `SELECT count(*) FROM policies p WHERE p.team_id = ? OR p.team_id IS NULL`
+ args = append(args, teamID)
+
+ // We must normalize the name for full Unicode support (Unicode equivalence).
+ match := norm.NFC.String(matchQuery)
+ query, args = searchLike(query, args, match, policySearchColumns...)
+
+ var count int
+ err := sqlx.GetContext(ctx, ds.reader(ctx), &count, query, args...)
+ if err != nil {
+ return 0, ctxerr.Wrap(ctx, err, "counting merged team policies")
+ }
+
+ return count, nil
+}
+
func (ds *Datastore) PoliciesByID(ctx context.Context, ids []uint) (map[uint]*fleet.Policy, error) {
sql := `SELECT ` + policyCols + `,
COALESCE(u.name, '') AS author_name,
@@ -593,6 +627,40 @@ func (ds *Datastore) ListTeamPolicies(ctx context.Context, teamID uint, opts fle
return teamPolicies, inheritedPolicies, err
}
+func (ds *Datastore) ListMergedTeamPolicies(ctx context.Context, teamID uint, opts fleet.ListOptions) ([]*fleet.Policy, error) {
+ var args []interface{}
+
+ query := `
+ SELECT
+ ` + policyCols + `,
+ COALESCE(u.name, '') AS author_name,
+ COALESCE(u.email, '') AS author_email,
+ ps.updated_at as host_count_updated_at,
+ COALESCE(ps.passing_host_count, 0) as passing_host_count,
+ COALESCE(ps.failing_host_count, 0) as failing_host_count
+ FROM policies p
+ LEFT JOIN users u ON p.author_id = u.id
+ LEFT JOIN policy_stats ps ON p.id = ps.policy_id
+ AND ps.inherited_team_id = COALESCE(p.team_id, 0)
+ WHERE (p.team_id = ? OR p.team_id IS NULL)
+ `
+
+ args = append(args, teamID)
+
+ // We must normalize the name for full Unicode support (Unicode equivalence).
+ match := norm.NFC.String(opts.MatchQuery)
+ query, args = searchLike(query, args, match, policySearchColumns...)
+ query, _ = appendListOptionsToSQL(query, &opts)
+
+ var policies []*fleet.Policy
+ err := sqlx.SelectContext(ctx, ds.reader(ctx), &policies, query, args...)
+ if err != nil {
+ return nil, ctxerr.Wrap(ctx, err, "listing merged team policies")
+ }
+
+ return policies, nil
+}
+
func (ds *Datastore) DeleteTeamPolicies(ctx context.Context, teamID uint, ids []uint) ([]uint, error) {
return deletePolicyDB(ctx, ds.writer(ctx), ids, &teamID)
}
@@ -826,6 +894,22 @@ func cleanupPolicyMembershipOnTeamChange(ctx context.Context, tx sqlx.ExtContext
return nil
}
+func cleanupQueryResultsOnTeamChange(ctx context.Context, tx sqlx.ExtContext, hostIDs []uint) error {
+ // Similar to cleanupPolicyMembershipOnTeamChange, hosts can belong to one team only, so we just delete all
+ // the query results of the hosts that belong to queries that are not global.
+ const cleanupQuery = `
+ DELETE FROM query_results
+ WHERE query_id IN (SELECT id FROM queries WHERE team_id IS NOT NULL) AND host_id IN (?)`
+ query, args, err := sqlx.In(cleanupQuery, hostIDs)
+ if err != nil {
+ return ctxerr.Wrap(ctx, err, "build cleanup query results query")
+ }
+ if _, err := tx.ExecContext(ctx, query, args...); err != nil {
+ return ctxerr.Wrap(ctx, err, "exec cleanup query results query")
+ }
+ return nil
+}
+
func cleanupPolicyMembershipOnPolicyUpdate(ctx context.Context, db sqlx.ExecerContext, policyID uint, platforms string) error {
if platforms == "" {
// all platforms allowed, nothing to clean up
@@ -1214,40 +1298,95 @@ func (ds *Datastore) UpdateHostPolicyCounts(ctx context.Context) error {
// NOTE these queries are duplicated in the below migration. Updates
// to these queries should be reflected there as well.
// https://github.com/fleetdm/fleet/blob/main/server/datastore/mysql/migrations/tables/20231215122713_InsertPolicyStatsData.go#L12
+ // This implementation should be functionally equivalent to the migration.
// Update Counts for Inherited Global Policies for each Team
- _, err := ds.writer(ctx).ExecContext(ctx, `
- INSERT INTO policy_stats (policy_id, inherited_team_id, passing_host_count, failing_host_count)
- SELECT
- p.id,
- t.id AS inherited_team_id,
- (
- SELECT COUNT(*)
- FROM policy_membership pm
- INNER JOIN hosts h ON pm.host_id = h.id
- WHERE pm.policy_id = p.id AND pm.passes = true AND h.team_id = t.id
- ) AS passing_host_count,
- (
- SELECT COUNT(*)
- FROM policy_membership pm
- INNER JOIN hosts h ON pm.host_id = h.id
- WHERE pm.policy_id = p.id AND pm.passes = false AND h.team_id = t.id
- ) AS failing_host_count
- FROM policies p
- CROSS JOIN teams t
- WHERE p.team_id IS NULL
- GROUP BY p.id, t.id
- ON DUPLICATE KEY UPDATE
- updated_at = NOW(),
- passing_host_count = VALUES(passing_host_count),
- failing_host_count = VALUES(failing_host_count);
- `)
+ // The original implementation that used INSERT ... SELECT (SELECT COUNT(*)) ... caused performance issues.
+ // Given 50 global policies, 10 teams, and 10,000 hosts per team, the INSERT query took 30-60 seconds to complete.
+ // Since it was an INSERT query, it blocked other hosts from updating their policy results in policy_membership.
+
+ // Now, we separate the INSERT from the SELECT, since SELECT by itself does not block other hosts from updating their policy results.
+ // In addition, we process one global policy at a time, which reduces the time to complete the SELECT query to <2 seconds, and limits the memory usage.
+ // We are not using a transaction to reduce locks. This means that INSERT may fail if the policy was deleted by a parallel process.
+ // Also, the INSERT may overwrite a clearing of the stats. This is acceptable, since these are very rare cases. We log and proceed in that case.
+
+ db := ds.writer(ctx)
+
+ // Inherited policies are only relevant for teams, so we check whether we have teams
+ var hasTeams bool
+ err := sqlx.GetContext(ctx, db, &hasTeams, `SELECT 1 FROM teams`)
if err != nil {
- return ctxerr.Wrap(ctx, err, "update host policy counts for inherited global policies")
+ if errors.Is(err, sql.ErrNoRows) {
+ // No teams, so no inherited policies
+ hasTeams = false
+ } else {
+ return ctxerr.Wrap(ctx, err, "count teams")
+ }
+ }
+
+ if hasTeams {
+ globalPolicies, err := ds.ListGlobalPolicies(ctx, fleet.ListOptions{})
+ if err != nil {
+ return ctxerr.Wrap(ctx, err, "list global policies")
+ }
+ type policyStat struct {
+ PolicyID uint `db:"policy_id"`
+ InheritedTeamID uint `db:"inherited_team_id"`
+ PassingHostCount uint `db:"passing_host_count"`
+ FailingHostCount uint `db:"failing_host_count"`
+ }
+ var policyStats []policyStat
+ for _, policy := range globalPolicies {
+ selectStmt := `SELECT
+ p.id as policy_id,
+ t.id AS inherited_team_id,
+ (
+ SELECT COUNT(*)
+ FROM policy_membership pm
+ INNER JOIN hosts h ON pm.host_id = h.id
+ WHERE pm.policy_id = p.id AND pm.passes = true AND h.team_id = t.id
+ ) AS passing_host_count,
+ (
+ SELECT COUNT(*)
+ FROM policy_membership pm
+ INNER JOIN hosts h ON pm.host_id = h.id
+ WHERE pm.policy_id = p.id AND pm.passes = false AND h.team_id = t.id
+ ) AS failing_host_count
+ FROM policies p
+ CROSS JOIN teams t
+ WHERE p.team_id IS NULL AND p.id = ?
+ GROUP BY t.id, p.id`
+ err = sqlx.SelectContext(ctx, db, &policyStats, selectStmt, policy.ID)
+ if err != nil && !errors.Is(err, sql.ErrNoRows) {
+ if errors.Is(err, sql.ErrNoRows) {
+ // Policy or team was deleted by a parallel process. We proceed.
+ level.Error(ds.logger).Log(
+ "msg", "policy not found for inherited global policies. Was policy or team(s) deleted?", "policy_id", policy.ID,
+ )
+ continue
+ }
+ return ctxerr.Wrap(ctx, err, "select policy counts for inherited global policies")
+ }
+ insertStmt := `INSERT INTO policy_stats (policy_id, inherited_team_id, passing_host_count, failing_host_count)
+ VALUES (:policy_id, :inherited_team_id, :passing_host_count, :failing_host_count)
+ ON DUPLICATE KEY UPDATE
+ updated_at = NOW(),
+ passing_host_count = VALUES(passing_host_count),
+ failing_host_count = VALUES(failing_host_count)`
+ _, err = sqlx.NamedExecContext(ctx, db, insertStmt, policyStats)
+ if err != nil {
+ // INSERT may fail due to rare race conditions. We log and proceed.
+ level.Error(ds.logger).Log(
+ "msg", "insert policy stats for inherited global policies. Was policy deleted?", "policy_id", policy.ID, "err", err,
+ )
+ }
+ }
}
// Update Counts for Global and Team Policies
- _, err = ds.writer(ctx).ExecContext(ctx, `
+ // The performance of this query is linear with the number of policies.
+ _, err = db.ExecContext(
+ ctx, `
INSERT INTO policy_stats (policy_id, inherited_team_id, passing_host_count, failing_host_count)
SELECT
p.id,
@@ -1289,14 +1428,15 @@ func (ds *Datastore) GetTeamHostsPolicyMemberships(
SELECT
COALESCE(sh.email, '') AS email,
COALESCE(pm.passing, 1) AS passing,
+ COALESCE(pm.failing_policy_ids, '') AS failing_policy_ids,
h.id AS host_id,
COALESCE(hdn.display_name, '') AS host_display_name,
h.hardware_serial AS host_hardware_serial
FROM hosts h
LEFT JOIN (
- SELECT host_id, BIT_AND(passes) AS passing
+ SELECT host_id, 0 AS passing, GROUP_CONCAT(policy_id) AS failing_policy_ids
FROM policy_membership
- WHERE policy_id IN (?) AND passes IS NOT NULL
+ WHERE policy_id IN (?) AND passes = 0
GROUP BY host_id
) pm ON h.id = pm.host_id
LEFT JOIN (
diff --git a/server/datastore/mysql/policies_test.go b/server/datastore/mysql/policies_test.go
index 90d9015489..b5be1af776 100644
--- a/server/datastore/mysql/policies_test.go
+++ b/server/datastore/mysql/policies_test.go
@@ -35,6 +35,7 @@ func TestPolicies(t *testing.T) {
{"MembershipViewNotDeferred", func(t *testing.T, ds *Datastore) { testPoliciesMembershipView(false, t, ds) }},
{"TeamPolicyLegacy", testTeamPolicyLegacy},
{"TeamPolicyProprietary", testTeamPolicyProprietary},
+ {"ListMergedTeamPolicies", testListMergedTeamPolicies},
{"PolicyQueriesForHost", testPolicyQueriesForHost},
{"PolicyQueriesForHostPlatforms", testPolicyQueriesForHostPlatforms},
{"PoliciesByID", testPoliciesByID},
@@ -709,6 +710,75 @@ func testTeamPolicyProprietary(t *testing.T, ds *Datastore) {
require.Equal(t, user1.ID, *team2Policies[0].AuthorID)
}
+func testListMergedTeamPolicies(t *testing.T, ds *Datastore) {
+ ctx := context.Background()
+ gpol, err := ds.NewGlobalPolicy(ctx, nil, fleet.PolicyPayload{
+ Name: "query1 global",
+ Query: "select 1;",
+ Description: "query1 desc",
+ Resolution: "query1 resolution",
+ })
+ require.NoError(t, err)
+
+ team1, err := ds.NewTeam(ctx, &fleet.Team{Name: "team1"})
+ require.NoError(t, err)
+
+ p, err := ds.NewTeamPolicy(ctx, team1.ID, nil, fleet.PolicyPayload{
+ Name: "query2 team1",
+ Query: "select 1;",
+ Description: "query1 desc",
+ Resolution: "query1 resolution",
+ })
+ require.NoError(t, err)
+
+ team2, err := ds.NewTeam(ctx, &fleet.Team{Name: "team2"})
+ require.NoError(t, err)
+
+ _, err = ds.NewTeamPolicy(ctx, team2.ID, nil, fleet.PolicyPayload{
+ Name: "query3 team2",
+ Query: "select 2;",
+ Description: "query2 desc",
+ Resolution: "query2 resolution",
+ })
+ require.NoError(t, err)
+
+ merged, err := ds.ListMergedTeamPolicies(ctx, team1.ID, fleet.ListOptions{
+ OrderKey: "name",
+ OrderDirection: fleet.OrderAscending,
+ })
+ require.NoError(t, err)
+
+ require.Len(t, merged, 2)
+ assert.Equal(t, gpol.ID, merged[0].ID)
+ assert.Equal(t, p.ID, merged[1].ID)
+
+ // Test list options affect both global and team policies
+ merged, err = ds.ListMergedTeamPolicies(ctx, team1.ID, fleet.ListOptions{
+ OrderKey: "name",
+ OrderDirection: fleet.OrderDescending,
+ })
+ require.NoError(t, err)
+
+ require.Len(t, merged, 2)
+ assert.Equal(t, p.ID, merged[0].ID)
+ assert.Equal(t, gpol.ID, merged[1].ID)
+
+ // Test filter
+ merged, err = ds.ListMergedTeamPolicies(ctx, team1.ID, fleet.ListOptions{
+ MatchQuery: "query1",
+ })
+ require.NoError(t, err)
+ require.Len(t, merged, 1)
+ assert.Equal(t, gpol.ID, merged[0].ID)
+
+ merged, err = ds.ListMergedTeamPolicies(ctx, team1.ID, fleet.ListOptions{
+ MatchQuery: "query2",
+ })
+ require.NoError(t, err)
+ require.Len(t, merged, 1)
+ assert.Equal(t, p.ID, merged[0].ID)
+}
+
func newTestHostWithPlatform(t *testing.T, ds *Datastore, hostname, platform string, teamID *uint) *fleet.Host {
nodeKey, err := server.GenerateRandomText(32)
require.NoError(t, err)
@@ -2899,6 +2969,10 @@ func testCountPolicies(t *testing.T, ds *Datastore) {
require.NoError(t, err)
assert.Equal(t, 0, teamCount)
+ mergedCount, err := ds.CountMergedTeamPolicies(ctx, tm.ID, "")
+ require.NoError(t, err)
+ assert.Equal(t, 0, mergedCount)
+
// 10 global policies
for i := 0; i < 10; i++ {
_, err := ds.NewGlobalPolicy(ctx, nil, fleet.PolicyPayload{Name: fmt.Sprintf("global policy %d", i)})
@@ -2913,6 +2987,10 @@ func testCountPolicies(t *testing.T, ds *Datastore) {
require.NoError(t, err)
assert.Equal(t, 0, teamCount)
+ mergedCount, err = ds.CountMergedTeamPolicies(ctx, tm.ID, "")
+ require.NoError(t, err)
+ assert.Equal(t, 10, mergedCount)
+
// add 5 team policies
for i := 0; i < 5; i++ {
_, err := ds.NewTeamPolicy(ctx, tm.ID, nil, fleet.PolicyPayload{Name: fmt.Sprintf("team policy %d", i)})
@@ -2926,6 +3004,10 @@ func testCountPolicies(t *testing.T, ds *Datastore) {
globalCount, err = ds.CountPolicies(ctx, nil, "")
require.NoError(t, err)
assert.Equal(t, 10, globalCount)
+
+ mergedCount, err = ds.CountMergedTeamPolicies(ctx, tm.ID, "")
+ require.NoError(t, err)
+ assert.Equal(t, 15, mergedCount)
}
func testUpdatePolicyHostCounts(t *testing.T, ds *Datastore) {
@@ -2933,34 +3015,18 @@ func testUpdatePolicyHostCounts(t *testing.T, ds *Datastore) {
policy, err := ds.NewGlobalPolicy(context.Background(), nil, fleet.PolicyPayload{Name: "global policy 1"})
require.NoError(t, err)
- team, err := ds.NewTeam(context.Background(), &fleet.Team{Name: "team1"})
- require.NoError(t, err)
-
- // create 4 team hosts
- var teamHosts []*fleet.Host
- for i := 0; i < 4; i++ {
- h, err := ds.NewHost(context.Background(), &fleet.Host{OsqueryHostID: ptr.String(fmt.Sprintf("host%d", i)), NodeKey: ptr.String(fmt.Sprintf("host%d", i)), TeamID: &team.ID})
- require.NoError(t, err)
- teamHosts = append(teamHosts, h)
- }
-
// create 4 global hosts
var globalHosts []*fleet.Host
- for i := 4; i < 8; i++ {
- h, err := ds.NewHost(context.Background(), &fleet.Host{OsqueryHostID: ptr.String(fmt.Sprintf("host%d", i)), NodeKey: ptr.String(fmt.Sprintf("host%d", i)), TeamID: nil})
+ for i := 100; i < 104; i++ {
+ h, err := ds.NewHost(
+ context.Background(),
+ &fleet.Host{OsqueryHostID: ptr.String(fmt.Sprintf("host%d", i)), NodeKey: ptr.String(fmt.Sprintf("host%d", i)), TeamID: nil},
+ )
require.NoError(t, err)
globalHosts = append(globalHosts, h)
}
- // add policy responses
- for _, h := range teamHosts {
- res := map[uint]*bool{
- policy.ID: ptr.Bool(true),
- }
- err = ds.RecordPolicyQueryExecutions(context.Background(), h, res, time.Now(), false)
- require.NoError(t, err)
- }
-
+ // add policy responses to global hosts
for _, h := range globalHosts {
res := map[uint]*bool{
policy.ID: ptr.Bool(true),
@@ -2986,7 +3052,7 @@ func testUpdatePolicyHostCounts(t *testing.T, ds *Datastore) {
policy, err = ds.Policy(context.Background(), policy.ID)
require.NoError(t, err)
require.Equal(t, uint(0), policy.FailingHostCount)
- require.Equal(t, uint(8), policy.PassingHostCount)
+ require.Equal(t, uint(4), policy.PassingHostCount)
require.NotNil(t, policy.HostCountUpdatedAt)
assert.True(
t, policy.HostCountUpdatedAt.Compare(now) >= 0, fmt.Sprintf("reference:%v HostCountUpdatedAt:%v", now, *policy.HostCountUpdatedAt),
@@ -2994,6 +3060,127 @@ func testUpdatePolicyHostCounts(t *testing.T, ds *Datastore) {
assert.True(
t, policy.HostCountUpdatedAt.Compare(later) < 0, fmt.Sprintf("later:%v HostCountUpdatedAt:%v", later, *policy.HostCountUpdatedAt),
)
+
+ team, err := ds.NewTeam(context.Background(), &fleet.Team{Name: "team1"})
+ require.NoError(t, err)
+
+ // create 4 team hosts
+ var teamHosts []*fleet.Host
+ for i := 0; i < 4; i++ {
+ h, err := ds.NewHost(context.Background(), &fleet.Host{OsqueryHostID: ptr.String(fmt.Sprintf("host%d", i)), NodeKey: ptr.String(fmt.Sprintf("host%d", i)), TeamID: &team.ID})
+ require.NoError(t, err)
+ teamHosts = append(teamHosts, h)
+ }
+
+ // add policy responses to team hosts
+ for _, h := range teamHosts {
+ var result *bool
+ switch h.ID % 5 {
+ case 0, 1: // 2 fails
+ result = ptr.Bool(false)
+ case 2: // 1 pass
+ result = ptr.Bool(true)
+ default:
+ // remain null
+ }
+
+ res := map[uint]*bool{
+ policy.ID: result,
+ }
+ err = ds.RecordPolicyQueryExecutions(context.Background(), h, res, time.Now(), false)
+ require.NoError(t, err)
+ }
+
+ // update policy host counts
+ now = time.Now().Truncate(time.Second)
+ later = now.Add(10 * time.Second)
+ err = ds.UpdateHostPolicyCounts(context.Background())
+ require.NoError(t, err)
+
+ // check policy host counts
+ policy, err = ds.Policy(context.Background(), policy.ID)
+ require.NoError(t, err)
+ require.Equal(t, uint(2), policy.FailingHostCount)
+ require.Equal(t, uint(5), policy.PassingHostCount)
+ require.NotNil(t, policy.HostCountUpdatedAt)
+ assert.True(
+ t, policy.HostCountUpdatedAt.Compare(now) >= 0, fmt.Sprintf("reference:%v HostCountUpdatedAt:%v", now, *policy.HostCountUpdatedAt),
+ )
+ assert.True(
+ t, policy.HostCountUpdatedAt.Compare(later) < 0, fmt.Sprintf("later:%v HostCountUpdatedAt:%v", later, *policy.HostCountUpdatedAt),
+ )
+
+ // new global policy
+ policy2, err := ds.NewGlobalPolicy(context.Background(), nil, fleet.PolicyPayload{Name: "global policy 2"})
+ require.NoError(t, err)
+
+ // new team
+ team2, err := ds.NewTeam(context.Background(), &fleet.Team{Name: "team2"})
+ require.NoError(t, err)
+
+ // create 4 team2 hosts
+ for i := 4; i < 8; i++ {
+ h, err := ds.NewHost(
+ context.Background(), &fleet.Host{
+ OsqueryHostID: ptr.String(fmt.Sprintf("host%d", i)), NodeKey: ptr.String(fmt.Sprintf("host%d", i)), TeamID: &team2.ID,
+ },
+ )
+ require.NoError(t, err)
+ teamHosts = append(teamHosts, h)
+ }
+
+ // Update policy results for all hosts.
+ // All fail policy 1, all pass policy 2
+ for _, h := range globalHosts {
+ res := map[uint]*bool{
+ policy.ID: ptr.Bool(false),
+ policy2.ID: ptr.Bool(true),
+ }
+ err = ds.RecordPolicyQueryExecutions(context.Background(), h, res, time.Now(), false)
+ require.NoError(t, err)
+ }
+ for _, h := range teamHosts {
+ res := map[uint]*bool{
+ policy.ID: ptr.Bool(false),
+ policy2.ID: ptr.Bool(true),
+ }
+ err = ds.RecordPolicyQueryExecutions(context.Background(), h, res, time.Now(), false)
+ require.NoError(t, err)
+ }
+
+ // update policy host counts
+ now = time.Now().Truncate(time.Second)
+ later = now.Add(10 * time.Second)
+ err = ds.UpdateHostPolicyCounts(context.Background())
+ require.NoError(t, err)
+
+ // check policy 1 host counts
+ policy, err = ds.Policy(context.Background(), policy.ID)
+ require.NoError(t, err)
+ require.Equal(t, uint(12), policy.FailingHostCount)
+ require.Equal(t, uint(0), policy.PassingHostCount)
+ require.NotNil(t, policy.HostCountUpdatedAt)
+ assert.True(
+ t, policy.HostCountUpdatedAt.Compare(now) >= 0, fmt.Sprintf("reference:%v HostCountUpdatedAt:%v", now, *policy.HostCountUpdatedAt),
+ )
+ assert.True(
+ t, policy.HostCountUpdatedAt.Compare(later) < 0, fmt.Sprintf("later:%v HostCountUpdatedAt:%v", later, *policy.HostCountUpdatedAt),
+ )
+
+ // check policy 2 host counts
+ policy2, err = ds.Policy(context.Background(), policy2.ID)
+ require.NoError(t, err)
+ require.Equal(t, uint(0), policy2.FailingHostCount)
+ require.Equal(t, uint(12), policy2.PassingHostCount)
+ require.NotNil(t, policy2.HostCountUpdatedAt)
+ assert.True(
+ t, policy2.HostCountUpdatedAt.Compare(now) >= 0,
+ fmt.Sprintf("reference:%v HostCountUpdatedAt:%v", now, *policy2.HostCountUpdatedAt),
+ )
+ assert.True(
+ t, policy2.HostCountUpdatedAt.Compare(later) < 0, fmt.Sprintf("later:%v HostCountUpdatedAt:%v", later, *policy2.HostCountUpdatedAt),
+ )
+
}
func testPoliciesNameUnicode(t *testing.T, ds *Datastore) {
diff --git a/server/datastore/mysql/queries.go b/server/datastore/mysql/queries.go
index 2a26884985..2ad343922c 100644
--- a/server/datastore/mysql/queries.go
+++ b/server/datastore/mysql/queries.go
@@ -4,11 +4,12 @@ import (
"context"
"database/sql"
"fmt"
+ "strings"
+
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/go-kit/log/level"
"github.com/jmoiron/sqlx"
- "strings"
)
const (
@@ -414,7 +415,6 @@ func (ds *Datastore) deleteQueryStats(ctx context.Context, queryIDs []uint) {
level.Error(ds.logger).Log("msg", "error deleting aggregated stats", "err", err)
}
}
-
}
// Query returns a single Query identified by id, if such exists.
@@ -504,10 +504,14 @@ func (ds *Datastore) ListQueries(ctx context.Context, opt fleet.ListQueryOptions
args := []interface{}{false, fleet.AggregatedStatsTypeScheduledQuery}
whereClauses := "WHERE saved = true"
- if opt.TeamID != nil {
+ switch {
+ case opt.TeamID != nil && opt.MergeInherited:
+ args = append(args, *opt.TeamID)
+ whereClauses += " AND (team_id = ? OR team_id IS NULL)"
+ case opt.TeamID != nil:
args = append(args, *opt.TeamID)
whereClauses += " AND team_id = ?"
- } else {
+ default:
whereClauses += " AND team_id IS NULL"
}
diff --git a/server/datastore/mysql/queries_test.go b/server/datastore/mysql/queries_test.go
index 2096683477..f43c07fe7e 100644
--- a/server/datastore/mysql/queries_test.go
+++ b/server/datastore/mysql/queries_test.go
@@ -219,7 +219,6 @@ func testQueriesDelete(t *testing.T, ds *Datastore) {
case <-time.After(10 * time.Second):
t.Error("Timeout: stats not deleted for testQueriesDelete")
}
-
}
func testQueriesGetByName(t *testing.T, ds *Datastore) {
@@ -765,6 +764,27 @@ func testListQueriesFiltersByTeamID(t *testing.T, ds *Datastore) {
)
require.NoError(t, err)
test.QueryElementsMatch(t, queries, []*fleet.Query{teamQ1, teamQ2, teamQ3})
+
+ // test merge inherited
+ queries, err = ds.ListQueries(
+ context.Background(),
+ fleet.ListQueryOptions{
+ TeamID: &team.ID,
+ MergeInherited: true,
+ },
+ )
+ require.NoError(t, err)
+ test.QueryElementsMatch(t, queries, []*fleet.Query{globalQ1, globalQ2, globalQ3, teamQ1, teamQ2, teamQ3})
+
+ // merge inherited ignored for global queries
+ queries, err = ds.ListQueries(
+ context.Background(),
+ fleet.ListQueryOptions{
+ MergeInherited: true,
+ },
+ )
+ require.NoError(t, err)
+ test.QueryElementsMatch(t, queries, []*fleet.Query{globalQ1, globalQ2, globalQ3})
}
func testListQueriesFiltersByIsScheduled(t *testing.T, ds *Datastore) {
diff --git a/server/datastore/mysql/schema.sql b/server/datastore/mysql/schema.sql
index be08aec070..2cd4865f2a 100644
--- a/server/datastore/mysql/schema.sql
+++ b/server/datastore/mysql/schema.sql
@@ -41,7 +41,7 @@ CREATE TABLE `app_config_json` (
UNIQUE KEY `id` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
-INSERT INTO `app_config_json` VALUES (1,'{\"mdm\": {\"macos_setup\": {\"bootstrap_package\": null, \"macos_setup_assistant\": null, \"enable_end_user_authentication\": false, \"enable_release_device_manually\": false}, \"macos_updates\": {\"deadline\": null, \"minimum_version\": null}, \"macos_settings\": {\"custom_settings\": null}, \"macos_migration\": {\"mode\": \"\", \"enable\": false, \"webhook_url\": \"\"}, \"windows_updates\": {\"deadline_days\": null, \"grace_period_days\": null}, \"windows_settings\": {\"custom_settings\": null}, \"apple_bm_default_team\": \"\", \"apple_bm_terms_expired\": false, \"enable_disk_encryption\": false, \"enabled_and_configured\": false, \"end_user_authentication\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"issuer_uri\": \"\", \"metadata_url\": \"\"}, \"windows_enabled_and_configured\": false, \"apple_bm_enabled_and_configured\": false}, \"scripts\": null, \"features\": {\"enable_host_users\": true, \"enable_software_inventory\": false}, \"org_info\": {\"org_name\": \"\", \"contact_url\": \"\", \"org_logo_url\": \"\", \"org_logo_url_light_background\": \"\"}, \"integrations\": {\"jira\": null, \"zendesk\": null, \"google_calendar\": null}, \"sso_settings\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"enable_sso\": false, \"issuer_uri\": \"\", \"metadata_url\": \"\", \"idp_image_url\": \"\", \"enable_jit_role_sync\": false, \"enable_sso_idp_login\": false, \"enable_jit_provisioning\": false}, \"agent_options\": {\"config\": {\"options\": {\"logger_plugin\": \"tls\", \"pack_delimiter\": \"/\", \"logger_tls_period\": 10, \"distributed_plugin\": \"tls\", \"disable_distributed\": false, \"logger_tls_endpoint\": \"/api/osquery/log\", \"distributed_interval\": 10, \"distributed_tls_max_attempts\": 3}, \"decorators\": {\"load\": [\"SELECT uuid AS host_uuid FROM system_info;\", \"SELECT hostname AS hostname FROM system_info;\"]}}, \"overrides\": {}}, \"fleet_desktop\": {\"transparency_url\": \"\"}, \"smtp_settings\": {\"port\": 587, \"domain\": \"\", \"server\": \"\", \"password\": \"\", \"user_name\": \"\", \"configured\": false, \"enable_smtp\": false, \"enable_ssl_tls\": true, \"sender_address\": \"\", \"enable_start_tls\": true, \"verify_ssl_certs\": true, \"authentication_type\": \"0\", \"authentication_method\": \"0\"}, \"server_settings\": {\"server_url\": \"\", \"enable_analytics\": false, \"scripts_disabled\": false, \"deferred_save_host\": false, \"live_query_disabled\": false, \"query_reports_disabled\": false}, \"webhook_settings\": {\"interval\": \"0s\", \"host_status_webhook\": {\"days_count\": 0, \"destination_url\": \"\", \"host_percentage\": 0, \"enable_host_status_webhook\": false}, \"vulnerabilities_webhook\": {\"destination_url\": \"\", \"host_batch_size\": 0, \"enable_vulnerabilities_webhook\": false}, \"failing_policies_webhook\": {\"policy_ids\": null, \"destination_url\": \"\", \"host_batch_size\": 0, \"enable_failing_policies_webhook\": false}}, \"host_expiry_settings\": {\"host_expiry_window\": 0, \"host_expiry_enabled\": false}, \"vulnerability_settings\": {\"databases_path\": \"\"}, \"activity_expiry_settings\": {\"activity_expiry_window\": 0, \"activity_expiry_enabled\": false}}','2020-01-01 01:01:01','2020-01-01 01:01:01');
+INSERT INTO `app_config_json` VALUES (1,'{\"mdm\": {\"macos_setup\": {\"bootstrap_package\": null, \"macos_setup_assistant\": null, \"enable_end_user_authentication\": false, \"enable_release_device_manually\": false}, \"macos_updates\": {\"deadline\": null, \"minimum_version\": null}, \"macos_settings\": {\"custom_settings\": null}, \"macos_migration\": {\"mode\": \"\", \"enable\": false, \"webhook_url\": \"\"}, \"windows_updates\": {\"deadline_days\": null, \"grace_period_days\": null}, \"windows_settings\": {\"custom_settings\": null}, \"apple_bm_default_team\": \"\", \"apple_bm_terms_expired\": false, \"enable_disk_encryption\": false, \"enabled_and_configured\": false, \"end_user_authentication\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"issuer_uri\": \"\", \"metadata_url\": \"\"}, \"windows_enabled_and_configured\": false, \"apple_bm_enabled_and_configured\": false}, \"scripts\": null, \"features\": {\"enable_host_users\": true, \"enable_software_inventory\": false}, \"org_info\": {\"org_name\": \"\", \"contact_url\": \"\", \"org_logo_url\": \"\", \"org_logo_url_light_background\": \"\"}, \"integrations\": {\"jira\": null, \"zendesk\": null, \"google_calendar\": null}, \"sso_settings\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"enable_sso\": false, \"issuer_uri\": \"\", \"metadata_url\": \"\", \"idp_image_url\": \"\", \"enable_jit_role_sync\": false, \"enable_sso_idp_login\": false, \"enable_jit_provisioning\": false}, \"agent_options\": {\"config\": {\"options\": {\"logger_plugin\": \"tls\", \"pack_delimiter\": \"/\", \"logger_tls_period\": 10, \"distributed_plugin\": \"tls\", \"disable_distributed\": false, \"logger_tls_endpoint\": \"/api/osquery/log\", \"distributed_interval\": 10, \"distributed_tls_max_attempts\": 3}, \"decorators\": {\"load\": [\"SELECT uuid AS host_uuid FROM system_info;\", \"SELECT hostname AS hostname FROM system_info;\"]}}, \"overrides\": {}}, \"fleet_desktop\": {\"transparency_url\": \"\"}, \"smtp_settings\": {\"port\": 587, \"domain\": \"\", \"server\": \"\", \"password\": \"\", \"user_name\": \"\", \"configured\": false, \"enable_smtp\": false, \"enable_ssl_tls\": true, \"sender_address\": \"\", \"enable_start_tls\": true, \"verify_ssl_certs\": true, \"authentication_type\": \"0\", \"authentication_method\": \"0\"}, \"server_settings\": {\"server_url\": \"\", \"enable_analytics\": false, \"scripts_disabled\": false, \"deferred_save_host\": false, \"live_query_disabled\": false, \"ai_features_disabled\": false, \"query_reports_disabled\": false}, \"webhook_settings\": {\"interval\": \"0s\", \"host_status_webhook\": {\"days_count\": 0, \"destination_url\": \"\", \"host_percentage\": 0, \"enable_host_status_webhook\": false}, \"vulnerabilities_webhook\": {\"destination_url\": \"\", \"host_batch_size\": 0, \"enable_vulnerabilities_webhook\": false}, \"failing_policies_webhook\": {\"policy_ids\": null, \"destination_url\": \"\", \"host_batch_size\": 0, \"enable_failing_policies_webhook\": false}}, \"host_expiry_settings\": {\"host_expiry_window\": 0, \"host_expiry_enabled\": false}, \"vulnerability_settings\": {\"databases_path\": \"\"}, \"activity_expiry_settings\": {\"activity_expiry_window\": 0, \"activity_expiry_enabled\": false}}','2020-01-01 01:01:01','2020-01-01 01:01:01');
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `calendar_events` (
@@ -910,9 +910,9 @@ CREATE TABLE `migration_status_tables` (
`tstamp` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `id` (`id`)
-) ENGINE=InnoDB AUTO_INCREMENT=265 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+) ENGINE=InnoDB AUTO_INCREMENT=266 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
-INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'),(142,20220713091130,1,'2020-01-01 01:01:01'),(143,20220802135510,1,'2020-01-01 01:01:01'),(144,20220818101352,1,'2020-01-01 01:01:01'),(145,20220822161445,1,'2020-01-01 01:01:01'),(146,20220831100036,1,'2020-01-01 01:01:01'),(147,20220831100151,1,'2020-01-01 01:01:01'),(148,20220908181826,1,'2020-01-01 01:01:01'),(149,20220914154915,1,'2020-01-01 01:01:01'),(150,20220915165115,1,'2020-01-01 01:01:01'),(151,20220915165116,1,'2020-01-01 01:01:01'),(152,20220928100158,1,'2020-01-01 01:01:01'),(153,20221014084130,1,'2020-01-01 01:01:01'),(154,20221027085019,1,'2020-01-01 01:01:01'),(155,20221101103952,1,'2020-01-01 01:01:01'),(156,20221104144401,1,'2020-01-01 01:01:01'),(157,20221109100749,1,'2020-01-01 01:01:01'),(158,20221115104546,1,'2020-01-01 01:01:01'),(159,20221130114928,1,'2020-01-01 01:01:01'),(160,20221205112142,1,'2020-01-01 01:01:01'),(161,20221216115820,1,'2020-01-01 01:01:01'),(162,20221220195934,1,'2020-01-01 01:01:01'),(163,20221220195935,1,'2020-01-01 01:01:01'),(164,20221223174807,1,'2020-01-01 01:01:01'),(165,20221227163855,1,'2020-01-01 01:01:01'),(166,20221227163856,1,'2020-01-01 01:01:01'),(167,20230202224725,1,'2020-01-01 01:01:01'),(168,20230206163608,1,'2020-01-01 01:01:01'),(169,20230214131519,1,'2020-01-01 01:01:01'),(170,20230303135738,1,'2020-01-01 01:01:01'),(171,20230313135301,1,'2020-01-01 01:01:01'),(172,20230313141819,1,'2020-01-01 01:01:01'),(173,20230315104937,1,'2020-01-01 01:01:01'),(174,20230317173844,1,'2020-01-01 01:01:01'),(175,20230320133602,1,'2020-01-01 01:01:01'),(176,20230330100011,1,'2020-01-01 01:01:01'),(177,20230330134823,1,'2020-01-01 01:01:01'),(178,20230405232025,1,'2020-01-01 01:01:01'),(179,20230408084104,1,'2020-01-01 01:01:01'),(180,20230411102858,1,'2020-01-01 01:01:01'),(181,20230421155932,1,'2020-01-01 01:01:01'),(182,20230425082126,1,'2020-01-01 01:01:01'),(183,20230425105727,1,'2020-01-01 01:01:01'),(184,20230501154913,1,'2020-01-01 01:01:01'),(185,20230503101418,1,'2020-01-01 01:01:01'),(186,20230515144206,1,'2020-01-01 01:01:01'),(187,20230517140952,1,'2020-01-01 01:01:01'),(188,20230517152807,1,'2020-01-01 01:01:01'),(189,20230518114155,1,'2020-01-01 01:01:01'),(190,20230520153236,1,'2020-01-01 01:01:01'),(191,20230525151159,1,'2020-01-01 01:01:01'),(192,20230530122103,1,'2020-01-01 01:01:01'),(193,20230602111827,1,'2020-01-01 01:01:01'),(194,20230608103123,1,'2020-01-01 01:01:01'),(195,20230629140529,1,'2020-01-01 01:01:01'),(196,20230629140530,1,'2020-01-01 01:01:01'),(197,20230711144622,1,'2020-01-01 01:01:01'),(198,20230721135421,1,'2020-01-01 01:01:01'),(199,20230721161508,1,'2020-01-01 01:01:01'),(200,20230726115701,1,'2020-01-01 01:01:01'),(201,20230807100822,1,'2020-01-01 01:01:01'),(202,20230814150442,1,'2020-01-01 01:01:01'),(203,20230823122728,1,'2020-01-01 01:01:01'),(204,20230906152143,1,'2020-01-01 01:01:01'),(205,20230911163618,1,'2020-01-01 01:01:01'),(206,20230912101759,1,'2020-01-01 01:01:01'),(207,20230915101341,1,'2020-01-01 01:01:01'),(208,20230918132351,1,'2020-01-01 01:01:01'),(209,20231004144339,1,'2020-01-01 01:01:01'),(210,20231009094541,1,'2020-01-01 01:01:01'),(211,20231009094542,1,'2020-01-01 01:01:01'),(212,20231009094543,1,'2020-01-01 01:01:01'),(213,20231009094544,1,'2020-01-01 01:01:01'),(214,20231016091915,1,'2020-01-01 01:01:01'),(215,20231024174135,1,'2020-01-01 01:01:01'),(216,20231025120016,1,'2020-01-01 01:01:01'),(217,20231025160156,1,'2020-01-01 01:01:01'),(218,20231031165350,1,'2020-01-01 01:01:01'),(219,20231106144110,1,'2020-01-01 01:01:01'),(220,20231107130934,1,'2020-01-01 01:01:01'),(221,20231109115838,1,'2020-01-01 01:01:01'),(222,20231121054530,1,'2020-01-01 01:01:01'),(223,20231122101320,1,'2020-01-01 01:01:01'),(224,20231130132828,1,'2020-01-01 01:01:01'),(225,20231130132931,1,'2020-01-01 01:01:01'),(226,20231204155427,1,'2020-01-01 01:01:01'),(227,20231206142340,1,'2020-01-01 01:01:01'),(228,20231207102320,1,'2020-01-01 01:01:01'),(229,20231207102321,1,'2020-01-01 01:01:01'),(230,20231207133731,1,'2020-01-01 01:01:01'),(231,20231212094238,1,'2020-01-01 01:01:01'),(232,20231212095734,1,'2020-01-01 01:01:01'),(233,20231212161121,1,'2020-01-01 01:01:01'),(234,20231215122713,1,'2020-01-01 01:01:01'),(235,20231219143041,1,'2020-01-01 01:01:01'),(236,20231224070653,1,'2020-01-01 01:01:01'),(237,20240110134315,1,'2020-01-01 01:01:01'),(238,20240119091637,1,'2020-01-01 01:01:01'),(239,20240126020642,1,'2020-01-01 01:01:01'),(240,20240126020643,1,'2020-01-01 01:01:01'),(241,20240129162819,1,'2020-01-01 01:01:01'),(242,20240130115133,1,'2020-01-01 01:01:01'),(243,20240131083822,1,'2020-01-01 01:01:01'),(244,20240205095928,1,'2020-01-01 01:01:01'),(245,20240205121956,1,'2020-01-01 01:01:01'),(246,20240209110212,1,'2020-01-01 01:01:01'),(247,20240212111533,1,'2020-01-01 01:01:01'),(248,20240221112844,1,'2020-01-01 01:01:01'),(249,20240222073518,1,'2020-01-01 01:01:01'),(250,20240222135115,1,'2020-01-01 01:01:01'),(251,20240226082255,1,'2020-01-01 01:01:01'),(252,20240228082706,1,'2020-01-01 01:01:01'),(253,20240301173035,1,'2020-01-01 01:01:01'),(254,20240302111134,1,'2020-01-01 01:01:01'),(255,20240312103753,1,'2020-01-01 01:01:01'),(256,20240313143416,1,'2020-01-01 01:01:01'),(257,20240314085226,1,'2020-01-01 01:01:01'),(258,20240314151747,1,'2020-01-01 01:01:01'),(259,20240320145650,1,'2020-01-01 01:01:01'),(260,20240327115530,1,'2020-01-01 01:01:01'),(261,20240327115617,1,'2020-01-01 01:01:01'),(262,20240408085837,1,'2020-01-01 01:01:01'),(263,20240415104633,1,'2020-01-01 01:01:01'),(264,20240424124712,1,'2020-01-01 01:01:01');
+INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'),(142,20220713091130,1,'2020-01-01 01:01:01'),(143,20220802135510,1,'2020-01-01 01:01:01'),(144,20220818101352,1,'2020-01-01 01:01:01'),(145,20220822161445,1,'2020-01-01 01:01:01'),(146,20220831100036,1,'2020-01-01 01:01:01'),(147,20220831100151,1,'2020-01-01 01:01:01'),(148,20220908181826,1,'2020-01-01 01:01:01'),(149,20220914154915,1,'2020-01-01 01:01:01'),(150,20220915165115,1,'2020-01-01 01:01:01'),(151,20220915165116,1,'2020-01-01 01:01:01'),(152,20220928100158,1,'2020-01-01 01:01:01'),(153,20221014084130,1,'2020-01-01 01:01:01'),(154,20221027085019,1,'2020-01-01 01:01:01'),(155,20221101103952,1,'2020-01-01 01:01:01'),(156,20221104144401,1,'2020-01-01 01:01:01'),(157,20221109100749,1,'2020-01-01 01:01:01'),(158,20221115104546,1,'2020-01-01 01:01:01'),(159,20221130114928,1,'2020-01-01 01:01:01'),(160,20221205112142,1,'2020-01-01 01:01:01'),(161,20221216115820,1,'2020-01-01 01:01:01'),(162,20221220195934,1,'2020-01-01 01:01:01'),(163,20221220195935,1,'2020-01-01 01:01:01'),(164,20221223174807,1,'2020-01-01 01:01:01'),(165,20221227163855,1,'2020-01-01 01:01:01'),(166,20221227163856,1,'2020-01-01 01:01:01'),(167,20230202224725,1,'2020-01-01 01:01:01'),(168,20230206163608,1,'2020-01-01 01:01:01'),(169,20230214131519,1,'2020-01-01 01:01:01'),(170,20230303135738,1,'2020-01-01 01:01:01'),(171,20230313135301,1,'2020-01-01 01:01:01'),(172,20230313141819,1,'2020-01-01 01:01:01'),(173,20230315104937,1,'2020-01-01 01:01:01'),(174,20230317173844,1,'2020-01-01 01:01:01'),(175,20230320133602,1,'2020-01-01 01:01:01'),(176,20230330100011,1,'2020-01-01 01:01:01'),(177,20230330134823,1,'2020-01-01 01:01:01'),(178,20230405232025,1,'2020-01-01 01:01:01'),(179,20230408084104,1,'2020-01-01 01:01:01'),(180,20230411102858,1,'2020-01-01 01:01:01'),(181,20230421155932,1,'2020-01-01 01:01:01'),(182,20230425082126,1,'2020-01-01 01:01:01'),(183,20230425105727,1,'2020-01-01 01:01:01'),(184,20230501154913,1,'2020-01-01 01:01:01'),(185,20230503101418,1,'2020-01-01 01:01:01'),(186,20230515144206,1,'2020-01-01 01:01:01'),(187,20230517140952,1,'2020-01-01 01:01:01'),(188,20230517152807,1,'2020-01-01 01:01:01'),(189,20230518114155,1,'2020-01-01 01:01:01'),(190,20230520153236,1,'2020-01-01 01:01:01'),(191,20230525151159,1,'2020-01-01 01:01:01'),(192,20230530122103,1,'2020-01-01 01:01:01'),(193,20230602111827,1,'2020-01-01 01:01:01'),(194,20230608103123,1,'2020-01-01 01:01:01'),(195,20230629140529,1,'2020-01-01 01:01:01'),(196,20230629140530,1,'2020-01-01 01:01:01'),(197,20230711144622,1,'2020-01-01 01:01:01'),(198,20230721135421,1,'2020-01-01 01:01:01'),(199,20230721161508,1,'2020-01-01 01:01:01'),(200,20230726115701,1,'2020-01-01 01:01:01'),(201,20230807100822,1,'2020-01-01 01:01:01'),(202,20230814150442,1,'2020-01-01 01:01:01'),(203,20230823122728,1,'2020-01-01 01:01:01'),(204,20230906152143,1,'2020-01-01 01:01:01'),(205,20230911163618,1,'2020-01-01 01:01:01'),(206,20230912101759,1,'2020-01-01 01:01:01'),(207,20230915101341,1,'2020-01-01 01:01:01'),(208,20230918132351,1,'2020-01-01 01:01:01'),(209,20231004144339,1,'2020-01-01 01:01:01'),(210,20231009094541,1,'2020-01-01 01:01:01'),(211,20231009094542,1,'2020-01-01 01:01:01'),(212,20231009094543,1,'2020-01-01 01:01:01'),(213,20231009094544,1,'2020-01-01 01:01:01'),(214,20231016091915,1,'2020-01-01 01:01:01'),(215,20231024174135,1,'2020-01-01 01:01:01'),(216,20231025120016,1,'2020-01-01 01:01:01'),(217,20231025160156,1,'2020-01-01 01:01:01'),(218,20231031165350,1,'2020-01-01 01:01:01'),(219,20231106144110,1,'2020-01-01 01:01:01'),(220,20231107130934,1,'2020-01-01 01:01:01'),(221,20231109115838,1,'2020-01-01 01:01:01'),(222,20231121054530,1,'2020-01-01 01:01:01'),(223,20231122101320,1,'2020-01-01 01:01:01'),(224,20231130132828,1,'2020-01-01 01:01:01'),(225,20231130132931,1,'2020-01-01 01:01:01'),(226,20231204155427,1,'2020-01-01 01:01:01'),(227,20231206142340,1,'2020-01-01 01:01:01'),(228,20231207102320,1,'2020-01-01 01:01:01'),(229,20231207102321,1,'2020-01-01 01:01:01'),(230,20231207133731,1,'2020-01-01 01:01:01'),(231,20231212094238,1,'2020-01-01 01:01:01'),(232,20231212095734,1,'2020-01-01 01:01:01'),(233,20231212161121,1,'2020-01-01 01:01:01'),(234,20231215122713,1,'2020-01-01 01:01:01'),(235,20231219143041,1,'2020-01-01 01:01:01'),(236,20231224070653,1,'2020-01-01 01:01:01'),(237,20240110134315,1,'2020-01-01 01:01:01'),(238,20240119091637,1,'2020-01-01 01:01:01'),(239,20240126020642,1,'2020-01-01 01:01:01'),(240,20240126020643,1,'2020-01-01 01:01:01'),(241,20240129162819,1,'2020-01-01 01:01:01'),(242,20240130115133,1,'2020-01-01 01:01:01'),(243,20240131083822,1,'2020-01-01 01:01:01'),(244,20240205095928,1,'2020-01-01 01:01:01'),(245,20240205121956,1,'2020-01-01 01:01:01'),(246,20240209110212,1,'2020-01-01 01:01:01'),(247,20240212111533,1,'2020-01-01 01:01:01'),(248,20240221112844,1,'2020-01-01 01:01:01'),(249,20240222073518,1,'2020-01-01 01:01:01'),(250,20240222135115,1,'2020-01-01 01:01:01'),(251,20240226082255,1,'2020-01-01 01:01:01'),(252,20240228082706,1,'2020-01-01 01:01:01'),(253,20240301173035,1,'2020-01-01 01:01:01'),(254,20240302111134,1,'2020-01-01 01:01:01'),(255,20240312103753,1,'2020-01-01 01:01:01'),(256,20240313143416,1,'2020-01-01 01:01:01'),(257,20240314085226,1,'2020-01-01 01:01:01'),(258,20240314151747,1,'2020-01-01 01:01:01'),(259,20240320145650,1,'2020-01-01 01:01:01'),(260,20240327115530,1,'2020-01-01 01:01:01'),(261,20240327115617,1,'2020-01-01 01:01:01'),(262,20240408085837,1,'2020-01-01 01:01:01'),(263,20240415104633,1,'2020-01-01 01:01:01'),(264,20240424124712,1,'2020-01-01 01:01:01'),(265,20240430111727,1,'2020-01-01 01:01:01');
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `mobile_device_management_solutions` (
diff --git a/server/fleet/app.go b/server/fleet/app.go
index 957a20befb..6ea543252e 100644
--- a/server/fleet/app.go
+++ b/server/fleet/app.go
@@ -881,6 +881,7 @@ type ServerSettings struct {
DeferredSaveHost bool `json:"deferred_save_host"`
QueryReportsDisabled bool `json:"query_reports_disabled"`
ScriptsDisabled bool `json:"scripts_disabled"`
+ AIFeaturesDisabled bool `json:"ai_features_disabled"`
}
// HostExpirySettings contains settings pertaining to automatic host expiry.
@@ -1020,6 +1021,9 @@ type ListQueryOptions struct {
TeamID *uint
// IsScheduled filters queries that are meant to run at a set interval.
IsScheduled *bool
+ // MergeInherited merges inherited global queries into the team list. Is only valid when TeamID
+ // is set.
+ MergeInherited bool
}
type ListActivitiesOptions struct {
@@ -1040,6 +1044,11 @@ type ApplySpecOptions struct {
TeamForPolicies string
}
+type ApplyTeamSpecOptions struct {
+ ApplySpecOptions
+ DryRunAssumptions *TeamSpecsDryRunAssumptions
+}
+
// RawQuery returns the ApplySpecOptions url-encoded for use in an URL's
// query string parameters. It only sets the parameters that are not the
// default values.
diff --git a/server/fleet/calendar_events.go b/server/fleet/calendar_events.go
index d7b22d478e..90cb8934b2 100644
--- a/server/fleet/calendar_events.go
+++ b/server/fleet/calendar_events.go
@@ -38,4 +38,5 @@ type HostPolicyMembershipData struct {
HostID uint `db:"host_id"`
HostDisplayName string `db:"host_display_name"`
HostHardwareSerial string `db:"host_hardware_serial"`
+ FailingPolicyIDs string `db:"failing_policy_ids"`
}
diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go
index 33d3198097..7b65a6c35b 100644
--- a/server/fleet/datastore.go
+++ b/server/fleet/datastore.go
@@ -615,6 +615,7 @@ type Datastore interface {
NewGlobalPolicy(ctx context.Context, authorID *uint, args PolicyPayload) (*Policy, error)
Policy(ctx context.Context, id uint) (*Policy, error)
+ PolicyLite(ctx context.Context, id uint) (*PolicyLite, error)
// SavePolicy updates some fields of the given policy on the datastore.
//
@@ -625,6 +626,7 @@ type Datastore interface {
PoliciesByID(ctx context.Context, ids []uint) (map[uint]*Policy, error)
DeleteGlobalPolicies(ctx context.Context, ids []uint) ([]uint, error)
CountPolicies(ctx context.Context, teamID *uint, matchQuery string) (int, error)
+ CountMergedTeamPolicies(ctx context.Context, teamID uint, matchQuery string) (int, error)
UpdateHostPolicyCounts(ctx context.Context) error
PolicyQueriesForHost(ctx context.Context, host *Host) (map[string]string, error)
@@ -674,6 +676,8 @@ type Datastore interface {
NewTeamPolicy(ctx context.Context, teamID uint, authorID *uint, args PolicyPayload) (*Policy, error)
ListTeamPolicies(ctx context.Context, teamID uint, opts ListOptions, iopts ListOptions) (teamPolicies, inheritedPolicies []*Policy, err error)
+ ListMergedTeamPolicies(ctx context.Context, teamID uint, opts ListOptions) ([]*Policy, error)
+
DeleteTeamPolicies(ctx context.Context, teamID uint, ids []uint) ([]uint, error)
TeamPolicy(ctx context.Context, teamID uint, policyID uint) (*Policy, error)
diff --git a/server/fleet/policies.go b/server/fleet/policies.go
index dda2ec047d..6ce5e38097 100644
--- a/server/fleet/policies.go
+++ b/server/fleet/policies.go
@@ -184,6 +184,15 @@ type PolicyCalendarData struct {
Name string `db:"name" json:"name"`
}
+// PolicyLite is a stripped down version of the policy.
+type PolicyLite struct {
+ ID uint `db:"id"`
+ // Description describes the policy.
+ Description string `db:"description"`
+ // Resolution describes how to solve a failing policy.
+ Resolution *string `db:"resolution"`
+}
+
func (p Policy) AuthzType() string {
return "policy"
}
diff --git a/server/fleet/scripts.go b/server/fleet/scripts.go
index 0bfd620fbb..3bffbcef32 100644
--- a/server/fleet/scripts.go
+++ b/server/fleet/scripts.go
@@ -297,7 +297,23 @@ const (
)
// anchored, so that it matches to the end of the line
-var scriptHashbangValidation = regexp.MustCompile(`^#!\s*/bin/sh\s*$`)
+var scriptHashbangValidation = regexp.MustCompile(`^#!\s*(:?/usr)?/bin/z?sh(?:\s*|\s+.*)$`)
+var ErrUnsupportedInterpreter = errors.New(`Interpreter not supported. Shell scripts must run in "#!/bin/sh" or "#!/bin/zsh."`)
+
+// ValidateShebang validates if we support a script, and whether we
+// can execute it directly, or need to pass it to a shell interpreter.
+func ValidateShebang(s string) (directExecute bool, err error) {
+ if strings.HasPrefix(s, "#!") {
+ // read the first line in a portable way
+ s := bufio.NewScanner(strings.NewReader(s))
+ // if a hashbang is present, it can only be `/bin/sh` or `(/usr)/bin/zsh` for now
+ if s.Scan() && !scriptHashbangValidation.MatchString(s.Text()) {
+ return false, ErrUnsupportedInterpreter
+ }
+ return true, nil
+ }
+ return false, nil
+}
func ValidateHostScriptContents(s string, isSavedScript bool) error {
if s == "" {
@@ -330,13 +346,8 @@ func ValidateHostScriptContents(s string, isSavedScript bool) error {
return errors.New("Wrong data format. Only plain text allowed.")
}
- if strings.HasPrefix(s, "#!") {
- // read the first line in a portable way
- s := bufio.NewScanner(strings.NewReader(s))
- // if a hashbang is present, it can only be `/bin/sh` for now
- if s.Scan() && !scriptHashbangValidation.MatchString(s.Text()) {
- return errors.New(`Interpreter not supported. Bash scripts must run in "#!/bin/shβ.`)
- }
+ if _, err := ValidateShebang(s); err != nil {
+ return err
}
return nil
diff --git a/server/fleet/scripts_test.go b/server/fleet/scripts_test.go
index 1587c86d01..e872eb1748 100644
--- a/server/fleet/scripts_test.go
+++ b/server/fleet/scripts_test.go
@@ -58,6 +58,50 @@ func TestScriptValidate(t *testing.T) {
}
}
+func TestValidateShebang(t *testing.T) {
+ tests := []struct {
+ name string
+ contents string
+ directExecute bool
+ err error
+ }{
+ {
+ name: "no shebang",
+ contents: "echo hi",
+ directExecute: false,
+ },
+ {
+ name: "posix shebang",
+ contents: "#!/bin/sh\necho hi",
+ directExecute: true,
+ },
+ {
+ name: "zsh shebang",
+ contents: "#!/bin/zsh\necho hi",
+ directExecute: true,
+ },
+ {
+ name: "zsh shebang with args",
+ contents: "#!/bin/zsh -x\necho hi",
+ directExecute: true,
+ },
+ {
+ name: "shebang with unsupported interpreter",
+ contents: "#!/usr/bin/python\nprint('hi')",
+ directExecute: false,
+ err: ErrUnsupportedInterpreter,
+ },
+ }
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ directExecute, err := ValidateShebang(tc.contents)
+ require.Equal(t, tc.directExecute, directExecute)
+ require.ErrorIs(t, tc.err, err)
+
+ })
+ }
+}
+
func TestValidateHostScriptContents(t *testing.T) {
tests := []struct {
name string
@@ -100,13 +144,23 @@ func TestValidateHostScriptContents(t *testing.T) {
{
name: "unsupported interpreter",
script: "#!/bin/bash\necho 'hello'",
- wantErr: errors.New(`Interpreter not supported. Bash scripts must run in "#!/bin/shβ.`),
+ wantErr: ErrUnsupportedInterpreter,
},
{
name: "valid script",
script: "#!/bin/sh\necho 'hello'",
wantErr: nil,
},
+ {
+ name: "valid zsh script",
+ script: "#!/bin/zsh\necho 'hello'",
+ wantErr: nil,
+ },
+ {
+ name: "valid zsh script",
+ script: "#!/usr/bin/zsh\necho 'hello'",
+ wantErr: nil,
+ },
}
for _, tt := range tests {
diff --git a/server/fleet/service.go b/server/fleet/service.go
index f4ef1f5fca..ae77d8a176 100644
--- a/server/fleet/service.go
+++ b/server/fleet/service.go
@@ -270,7 +270,9 @@ type Service interface {
// for distributed queries but not saved should not be returned).
// When is set to scheduled != nil, then only scheduled queries will be returned if `*scheduled == true`
// and only non-scheduled queries will be returned if `*scheduled == false`.
- ListQueries(ctx context.Context, opt ListOptions, teamID *uint, scheduled *bool) ([]*Query, error)
+ // If mergeInherited is true and a teamID is provided, then queries from the global team will be
+ // included in the results.
+ ListQueries(ctx context.Context, opt ListOptions, teamID *uint, scheduled *bool, mergeInherited bool) ([]*Query, error)
GetQuery(ctx context.Context, id uint) (*Query, error)
// GetQueryReportResults returns all the stored results of a query for hosts the requestor has access to
GetQueryReportResults(ctx context.Context, id uint) ([]HostQueryResultRow, error)
@@ -546,7 +548,7 @@ type Service interface {
ModifyTeamEnrollSecrets(ctx context.Context, teamID uint, secrets []EnrollSecret) ([]*EnrollSecret, error)
// ApplyTeamSpecs applies the changes for each team as defined in the specs.
// On success, it returns the mapping of team names to team ids.
- ApplyTeamSpecs(ctx context.Context, specs []*TeamSpec, applyOpts ApplySpecOptions) (map[string]uint, error)
+ ApplyTeamSpecs(ctx context.Context, specs []*TeamSpec, applyOpts ApplyTeamSpecOptions) (map[string]uint, error)
// /////////////////////////////////////////////////////////////////////////////
// ActivitiesService
@@ -611,6 +613,7 @@ type Service interface {
GetPolicyByIDQueries(ctx context.Context, policyID uint) (*Policy, error)
ApplyPolicySpecs(ctx context.Context, policies []*PolicySpec) error
CountGlobalPolicies(ctx context.Context, matchQuery string) (int, error)
+ AutofillPolicySql(ctx context.Context, sql string) (description string, resolution string, err error)
// /////////////////////////////////////////////////////////////////////////////
// Software
@@ -653,11 +656,11 @@ type Service interface {
// Team Policies
NewTeamPolicy(ctx context.Context, teamID uint, p PolicyPayload) (*Policy, error)
- ListTeamPolicies(ctx context.Context, teamID uint, opts ListOptions, iopts ListOptions) (teamPolicies, inheritedPolicies []*Policy, err error)
+ ListTeamPolicies(ctx context.Context, teamID uint, opts ListOptions, iopts ListOptions, mergeInherited bool) (teamPolicies, inheritedPolicies []*Policy, err error)
DeleteTeamPolicies(ctx context.Context, teamID uint, ids []uint) ([]uint, error)
ModifyTeamPolicy(ctx context.Context, teamID uint, id uint, p ModifyPolicyPayload) (*Policy, error)
GetTeamPolicyByIDQueries(ctx context.Context, teamID uint, policyID uint) (*Policy, error)
- CountTeamPolicies(ctx context.Context, teamID uint, matchQuery string) (int, error)
+ CountTeamPolicies(ctx context.Context, teamID uint, matchQuery string, mergeInherited bool) (int, error)
// /////////////////////////////////////////////////////////////////////////////
// Geolocation
@@ -950,7 +953,7 @@ type Service interface {
// team or for hosts with no team.
BatchSetMDMProfiles(
ctx context.Context, teamID *uint, teamName *string, profiles []MDMProfileBatchPayload, dryRun bool, skipBulkPending bool,
- assumeEnabled bool,
+ assumeEnabled *bool,
) error
///////////////////////////////////////////////////////////////////////////////
diff --git a/server/fleet/teams.go b/server/fleet/teams.go
index 55016fbb4b..db83210230 100644
--- a/server/fleet/teams.go
+++ b/server/fleet/teams.go
@@ -425,6 +425,11 @@ type TeamSpecIntegrations struct {
GoogleCalendar *TeamGoogleCalendarIntegration `json:"google_calendar"`
}
+// TeamSpecsDryRunAssumptions holds the assumptions that are made when applying team specs in dry-run mode.
+type TeamSpecsDryRunAssumptions struct {
+ WindowsEnabledAndConfigured optjson.Bool `json:"windows_enabled_and_configured,omitempty"`
+}
+
// TeamSpecFromTeam returns a TeamSpec constructed from the given Team.
func TeamSpecFromTeam(t *Team) (*TeamSpec, error) {
features, err := json.Marshal(t.Config.Features)
diff --git a/server/mock/datastore_mock.go b/server/mock/datastore_mock.go
index 3abc80fb7a..f7142ba225 100644
--- a/server/mock/datastore_mock.go
+++ b/server/mock/datastore_mock.go
@@ -443,6 +443,8 @@ type NewGlobalPolicyFunc func(ctx context.Context, authorID *uint, args fleet.Po
type PolicyFunc func(ctx context.Context, id uint) (*fleet.Policy, error)
+type PolicyLiteFunc func(ctx context.Context, id uint) (*fleet.PolicyLite, error)
+
type SavePolicyFunc func(ctx context.Context, p *fleet.Policy, shouldRemoveAllPolicyMemberships bool, removePolicyStats bool) error
type ListGlobalPoliciesFunc func(ctx context.Context, opts fleet.ListOptions) ([]*fleet.Policy, error)
@@ -453,6 +455,8 @@ type DeleteGlobalPoliciesFunc func(ctx context.Context, ids []uint) ([]uint, err
type CountPoliciesFunc func(ctx context.Context, teamID *uint, matchQuery string) (int, error)
+type CountMergedTeamPoliciesFunc func(ctx context.Context, teamID uint, matchQuery string) (int, error)
+
type UpdateHostPolicyCountsFunc func(ctx context.Context) error
type PolicyQueriesForHostFunc func(ctx context.Context, host *fleet.Host) (map[string]string, error)
@@ -501,6 +505,8 @@ type NewTeamPolicyFunc func(ctx context.Context, teamID uint, authorID *uint, ar
type ListTeamPoliciesFunc func(ctx context.Context, teamID uint, opts fleet.ListOptions, iopts fleet.ListOptions) (teamPolicies []*fleet.Policy, inheritedPolicies []*fleet.Policy, err error)
+type ListMergedTeamPoliciesFunc func(ctx context.Context, teamID uint, opts fleet.ListOptions) ([]*fleet.Policy, error)
+
type DeleteTeamPoliciesFunc func(ctx context.Context, teamID uint, ids []uint) ([]uint, error)
type TeamPolicyFunc func(ctx context.Context, teamID uint, policyID uint) (*fleet.Policy, error)
@@ -1576,6 +1582,9 @@ type DataStore struct {
PolicyFunc PolicyFunc
PolicyFuncInvoked bool
+ PolicyLiteFunc PolicyLiteFunc
+ PolicyLiteFuncInvoked bool
+
SavePolicyFunc SavePolicyFunc
SavePolicyFuncInvoked bool
@@ -1591,6 +1600,9 @@ type DataStore struct {
CountPoliciesFunc CountPoliciesFunc
CountPoliciesFuncInvoked bool
+ CountMergedTeamPoliciesFunc CountMergedTeamPoliciesFunc
+ CountMergedTeamPoliciesFuncInvoked bool
+
UpdateHostPolicyCountsFunc UpdateHostPolicyCountsFunc
UpdateHostPolicyCountsFuncInvoked bool
@@ -1663,6 +1675,9 @@ type DataStore struct {
ListTeamPoliciesFunc ListTeamPoliciesFunc
ListTeamPoliciesFuncInvoked bool
+ ListMergedTeamPoliciesFunc ListMergedTeamPoliciesFunc
+ ListMergedTeamPoliciesFuncInvoked bool
+
DeleteTeamPoliciesFunc DeleteTeamPoliciesFunc
DeleteTeamPoliciesFuncInvoked bool
@@ -3807,6 +3822,13 @@ func (s *DataStore) Policy(ctx context.Context, id uint) (*fleet.Policy, error)
return s.PolicyFunc(ctx, id)
}
+func (s *DataStore) PolicyLite(ctx context.Context, id uint) (*fleet.PolicyLite, error) {
+ s.mu.Lock()
+ s.PolicyLiteFuncInvoked = true
+ s.mu.Unlock()
+ return s.PolicyLiteFunc(ctx, id)
+}
+
func (s *DataStore) SavePolicy(ctx context.Context, p *fleet.Policy, shouldRemoveAllPolicyMemberships bool, removePolicyStats bool) error {
s.mu.Lock()
s.SavePolicyFuncInvoked = true
@@ -3842,6 +3864,13 @@ func (s *DataStore) CountPolicies(ctx context.Context, teamID *uint, matchQuery
return s.CountPoliciesFunc(ctx, teamID, matchQuery)
}
+func (s *DataStore) CountMergedTeamPolicies(ctx context.Context, teamID uint, matchQuery string) (int, error) {
+ s.mu.Lock()
+ s.CountMergedTeamPoliciesFuncInvoked = true
+ s.mu.Unlock()
+ return s.CountMergedTeamPoliciesFunc(ctx, teamID, matchQuery)
+}
+
func (s *DataStore) UpdateHostPolicyCounts(ctx context.Context) error {
s.mu.Lock()
s.UpdateHostPolicyCountsFuncInvoked = true
@@ -4010,6 +4039,13 @@ func (s *DataStore) ListTeamPolicies(ctx context.Context, teamID uint, opts flee
return s.ListTeamPoliciesFunc(ctx, teamID, opts, iopts)
}
+func (s *DataStore) ListMergedTeamPolicies(ctx context.Context, teamID uint, opts fleet.ListOptions) ([]*fleet.Policy, error) {
+ s.mu.Lock()
+ s.ListMergedTeamPoliciesFuncInvoked = true
+ s.mu.Unlock()
+ return s.ListMergedTeamPoliciesFunc(ctx, teamID, opts)
+}
+
func (s *DataStore) DeleteTeamPolicies(ctx context.Context, teamID uint, ids []uint) ([]uint, error) {
s.mu.Lock()
s.DeleteTeamPoliciesFuncInvoked = true
diff --git a/server/service/client.go b/server/service/client.go
index 0492f6486d..53d8cc2fae 100644
--- a/server/service/client.go
+++ b/server/service/client.go
@@ -172,6 +172,24 @@ func (c *Client) AuthenticatedDo(verb, path, rawQuery string, params interface{}
return c.doContextWithHeaders(context.Background(), verb, path, rawQuery, params, headers)
}
+func (c *Client) AuthenticatedDoCustomHeaders(verb, path, rawQuery string, params interface{}, customHeaders map[string]string) (*http.Response, error) {
+ if c.token == "" {
+ return nil, errors.New("authentication token is empty")
+ }
+
+ headers := map[string]string{
+ "Content-Type": "application/json",
+ "Accept": "application/json",
+ "Authorization": fmt.Sprintf("Bearer %s", c.token),
+ }
+
+ for key, value := range customHeaders {
+ headers[key] = value
+ }
+
+ return c.doContextWithHeaders(context.Background(), verb, path, rawQuery, params, headers)
+}
+
func (c *Client) SetToken(t string) {
c.token = t
}
@@ -544,7 +562,11 @@ func (c *Client) ApplyGroup(
// Next, apply the teams specs before saving the profiles, so that any
// non-existing team gets created.
var err error
- teamIDsByName, err = c.ApplyTeams(specs.Teams, opts)
+ teamOpts := fleet.ApplyTeamSpecOptions{
+ ApplySpecOptions: opts,
+ DryRunAssumptions: specs.TeamsDryRunAssumptions,
+ }
+ teamIDsByName, err = c.ApplyTeams(specs.Teams, teamOpts)
if err != nil {
return nil, fmt.Errorf("applying teams: %w", err)
}
@@ -556,7 +578,7 @@ func (c *Client) ApplyGroup(
logfn("[+] would've applied MDM profiles for new team %s\n", tmName)
} else {
logfn("[+] applying MDM profiles for team %s\n", tmName)
- if err := c.ApplyTeamProfiles(tmName, profs, opts); err != nil {
+ if err := c.ApplyTeamProfiles(tmName, profs, teamOpts); err != nil {
return nil, fmt.Errorf("applying custom settings for team %q: %w", tmName, err)
}
}
@@ -870,8 +892,10 @@ func (c *Client) DoGitOps(
baseDir string,
logf func(format string, args ...interface{}),
dryRun bool,
+ teamDryRunAssumptions *fleet.TeamSpecsDryRunAssumptions,
appConfig *fleet.EnrichedAppConfig,
-) error {
+) (*fleet.TeamSpecsDryRunAssumptions, error) {
+ var teamAssumptions *fleet.TeamSpecsDryRunAssumptions
var err error
logFn := func(format string, args ...interface{}) {
if logf != nil {
@@ -916,7 +940,7 @@ func (c *Client) DoGitOps(
}
mdmAppConfig, ok = mdmConfig.(map[string]interface{})
if !ok {
- return errors.New("org_settings.mdm config is not a map")
+ return nil, errors.New("org_settings.mdm config is not a map")
}
// Put in default values for macos_migration
@@ -936,6 +960,11 @@ func (c *Client) DoGitOps(
} else {
mdmAppConfig["windows_enabled_and_configured"] = false
}
+ if windowsEnabledAndConfiguredAssumption, ok := mdmAppConfig["windows_enabled_and_configured"].(bool); ok {
+ teamAssumptions = &fleet.TeamSpecsDryRunAssumptions{
+ WindowsEnabledAndConfigured: optjson.SetBool(windowsEnabledAndConfiguredAssumption),
+ }
+ }
group.AppConfig.(map[string]interface{})["scripts"] = scripts
} else {
team = make(map[string]interface{})
@@ -970,14 +999,14 @@ func (c *Client) DoGitOps(
team["integrations"] = integrations
_, ok = integrations.(map[string]interface{})
if !ok {
- return errors.New("team_settings.integrations config is not a map")
+ return nil, errors.New("team_settings.integrations config is not a map")
}
if googleCal, ok := integrations.(map[string]interface{})["google_calendar"]; !ok || googleCal == nil {
integrations.(map[string]interface{})["google_calendar"] = map[string]interface{}{}
} else {
_, ok = googleCal.(map[string]interface{})
if !ok {
- return errors.New("team_settings.integrations.google_calendar config is not a map")
+ return nil, errors.New("team_settings.integrations.google_calendar config is not a map")
}
}
@@ -1043,10 +1072,10 @@ func (c *Client) DoGitOps(
if appConfig.License.IsPremium() {
windowsUpdates := mdmAppConfig["windows_updates"].(map[string]interface{})
if deadlineDays, ok := windowsUpdates["deadline_days"]; !ok || deadlineDays == nil {
- windowsUpdates["deadline_days"] = 0
+ windowsUpdates["deadline_days"] = nil
}
if gracePeriodDays, ok := windowsUpdates["grace_period_days"]; !ok || gracePeriodDays == nil {
- windowsUpdates["grace_period_days"] = 0
+ windowsUpdates["grace_period_days"] = nil
}
}
// Put in default value for enable_disk_encryption
@@ -1058,39 +1087,40 @@ func (c *Client) DoGitOps(
if config.TeamName != nil {
rawTeam, err := json.Marshal(team)
if err != nil {
- return fmt.Errorf("error marshalling team spec: %w", err)
+ return nil, fmt.Errorf("error marshalling team spec: %w", err)
}
group.Teams = []json.RawMessage{rawTeam}
+ group.TeamsDryRunAssumptions = teamDryRunAssumptions
}
// Apply org settings, scripts, enroll secrets, and controls
teamIDsByName, err := c.ApplyGroup(ctx, &group, baseDir, logf, fleet.ApplySpecOptions{DryRun: dryRun})
if err != nil {
- return err
+ return nil, err
}
if config.TeamName != nil {
teamID, ok := teamIDsByName[*config.TeamName]
if !ok || teamID == 0 {
if dryRun {
logFn("[+] would've added any policies/queries to new team %s\n", *config.TeamName)
- return nil
+ return nil, nil
}
- return fmt.Errorf("team %s not created", *config.TeamName)
+ return nil, fmt.Errorf("team %s not created", *config.TeamName)
}
config.TeamID = &teamID
}
err = c.doGitOpsPolicies(config, logFn, dryRun)
if err != nil {
- return err
+ return nil, err
}
err = c.doGitOpsQueries(config, logFn, dryRun)
if err != nil {
- return err
+ return nil, err
}
- return nil
+ return teamAssumptions, nil
}
func (c *Client) doGitOpsPolicies(config *spec.GitOps, logFn func(format string, args ...interface{}), dryRun bool) error {
diff --git a/server/service/client_scripts.go b/server/service/client_scripts.go
index cca580050c..18285013fd 100644
--- a/server/service/client_scripts.go
+++ b/server/service/client_scripts.go
@@ -13,7 +13,15 @@ import (
func (c *Client) RunHostScriptSync(hostID uint, scriptContents []byte, scriptName string, teamID uint) (*fleet.HostScriptResult, error) {
verb, path := "POST", "/api/latest/fleet/scripts/run/sync"
+ return c.runHostScript(verb, path, hostID, scriptContents, scriptName, teamID, http.StatusOK)
+}
+func (c *Client) RunHostScriptAsync(hostID uint, scriptContents []byte, scriptName string, teamID uint) (*fleet.HostScriptResult, error) {
+ verb, path := "POST", "/api/latest/fleet/scripts/run"
+ return c.runHostScript(verb, path, hostID, scriptContents, scriptName, teamID, http.StatusAccepted)
+}
+
+func (c *Client) runHostScript(verb, path string, hostID uint, scriptContents []byte, scriptName string, teamID uint, successStatusCode int) (*fleet.HostScriptResult, error) {
req := fleet.HostScriptRequestPayload{
HostID: hostID,
ScriptName: scriptName,
@@ -32,7 +40,7 @@ func (c *Client) RunHostScriptSync(hostID uint, scriptContents []byte, scriptNam
defer res.Body.Close()
switch res.StatusCode {
- case http.StatusOK:
+ case successStatusCode:
b, err := io.ReadAll(res.Body)
if err != nil {
return nil, fmt.Errorf("reading %s %s response: %w", verb, path, err)
@@ -45,13 +53,10 @@ func (c *Client) RunHostScriptSync(hostID uint, scriptContents []byte, scriptNam
if err != nil {
return nil, err
}
-
if strings.Contains(errMsg, fleet.RunScriptScriptsDisabledGloballyErrMsg) {
return nil, errors.New(fleet.RunScriptScriptsDisabledGloballyErrMsg)
}
-
return nil, errors.New(fleet.RunScriptForbiddenErrMsg)
-
case http.StatusPaymentRequired:
if teamID > 0 {
return nil, errors.New("Team id parameter requires Fleet Premium license.")
diff --git a/server/service/client_teams.go b/server/service/client_teams.go
index fa59b4178b..bfa8f4b5d4 100644
--- a/server/service/client_teams.go
+++ b/server/service/client_teams.go
@@ -52,10 +52,14 @@ func (c *Client) DeleteTeam(teamID uint) error {
// ApplyTeams sends the list of Teams to be applied to the
// Fleet instance.
-func (c *Client) ApplyTeams(specs []json.RawMessage, opts fleet.ApplySpecOptions) (map[string]uint, error) {
+func (c *Client) ApplyTeams(specs []json.RawMessage, opts fleet.ApplyTeamSpecOptions) (map[string]uint, error) {
verb, path := "POST", "/api/latest/fleet/spec/teams"
var responseBody applyTeamSpecsResponse
- err := c.authenticatedRequestWithQuery(map[string]interface{}{"specs": specs}, verb, path, &responseBody, opts.RawQuery())
+ params := map[string]interface{}{"specs": specs}
+ if opts.DryRun && opts.DryRunAssumptions != nil {
+ params["dry_run_assumptions"] = opts.DryRunAssumptions
+ }
+ err := c.authenticatedRequestWithQuery(params, verb, path, &responseBody, opts.RawQuery())
if err != nil {
return nil, err
}
@@ -64,13 +68,16 @@ func (c *Client) ApplyTeams(specs []json.RawMessage, opts fleet.ApplySpecOptions
// ApplyTeamProfiles sends the list of profiles to be applied for the specified
// team.
-func (c *Client) ApplyTeamProfiles(tmName string, profiles []fleet.MDMProfileBatchPayload, opts fleet.ApplySpecOptions) error {
+func (c *Client) ApplyTeamProfiles(tmName string, profiles []fleet.MDMProfileBatchPayload, opts fleet.ApplyTeamSpecOptions) error {
verb, path := "POST", "/api/latest/fleet/mdm/profiles/batch"
query, err := url.ParseQuery(opts.RawQuery())
if err != nil {
return err
}
query.Add("team_name", tmName)
+ if opts.DryRunAssumptions != nil && opts.DryRunAssumptions.WindowsEnabledAndConfigured.Valid {
+ query.Add("assume_enabled", strconv.FormatBool(opts.DryRunAssumptions.WindowsEnabledAndConfigured.Value))
+ }
return c.authenticatedRequestWithQuery(map[string]interface{}{"profiles": profiles}, verb, path, nil, query.Encode())
}
diff --git a/server/service/global_policies.go b/server/service/global_policies.go
index 2cecd274e9..28a9c3789a 100644
--- a/server/service/global_policies.go
+++ b/server/service/global_policies.go
@@ -1,9 +1,16 @@
package service
import (
+ "bytes"
"context"
+ "encoding/json"
"errors"
"fmt"
+ "github.com/fleetdm/fleet/v4/pkg/fleethttp"
+ "io"
+ "net/http"
+ "strings"
+ "time"
"github.com/fleetdm/fleet/v4/server/authz"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
@@ -550,3 +557,141 @@ func (svc *Service) ApplyPolicySpecs(ctx context.Context, policies []*fleet.Poli
}
return nil
}
+
+/////////////////////////////////////////////////////////////////////////////////
+// Autofill
+/////////////////////////////////////////////////////////////////////////////////
+
+type autofillPoliciesRequest struct {
+ SQL string `json:"sql"`
+}
+
+type autofillPoliciesResponse struct {
+ Description string `json:"description"`
+ Resolution string `json:"resolution"`
+ Err error `json:"error,omitempty"`
+}
+
+func (a autofillPoliciesResponse) error() error {
+ return a.Err
+}
+
+func autofillPoliciesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
+ req := request.(*autofillPoliciesRequest)
+ description, resolution, err := svc.AutofillPolicySql(ctx, req.SQL)
+ return autofillPoliciesResponse{Description: description, Resolution: resolution, Err: err}, nil
+}
+
+// Exposing external URL and timeout for testing purposes
+var getHumanInterpretationFromOsquerySqlUrl = "https://fleetdm.com/api/v1/get-human-interpretation-from-osquery-sql"
+var getHumanInterpretationFromOsquerySqlTimeout = 30 * time.Second
+
+type AutofillError struct {
+ Message string
+ InternalErr error
+}
+
+// Error implements the error interface.
+func (e AutofillError) Error() string {
+ return e.Message
+}
+
+// StatusCode implements the kithttp.StatusCoder interface.
+func (e AutofillError) StatusCode() int {
+ return http.StatusUnprocessableEntity
+}
+
+func (e AutofillError) Internal() string {
+ if e.InternalErr == nil {
+ return ""
+ }
+ return e.InternalErr.Error()
+}
+
+func (svc *Service) AutofillPolicySql(ctx context.Context, sql string) (description string, resolution string, err error) {
+ // We expect that only users with policy write permissions will autofill policies.
+ if err = svc.authz.Authorize(ctx, &fleet.Policy{}, fleet.ActionWrite); err != nil {
+ return "", "", err
+ }
+
+ appConfig, err := svc.ds.AppConfig(ctx)
+ if err != nil {
+ return "", "", err
+ }
+ if appConfig.ServerSettings.AIFeaturesDisabled {
+ return "", "", ctxerr.Wrap(
+ ctx, &fleet.BadRequestError{
+ Message: "AI features are disabled (server_settings.ai_features_disabled)",
+ },
+ )
+ }
+
+ sql = strings.TrimSpace(sql)
+ if sql == "" {
+ return "", "", ctxerr.Wrap(ctx, &fleet.BadRequestError{Message: "'sql' cannot be empty"})
+ }
+
+ // Using a timeout smaller than the Fleet server's WriteTimeout
+ client := fleethttp.NewClient(fleethttp.WithTimeout(getHumanInterpretationFromOsquerySqlTimeout))
+ reqBodyValues := map[string]string{"sql": sql}
+ reqBody, err := json.Marshal(reqBodyValues)
+ if err != nil {
+ return "", "", ctxerr.Wrap(
+ ctx, &fleet.BadRequestError{
+ Message: fmt.Sprintf("Could not process sql: %s", sql),
+ },
+ )
+ }
+ resp, err := client.Post(
+ getHumanInterpretationFromOsquerySqlUrl, "application/json", bytes.NewBuffer(reqBody),
+ )
+ if err != nil {
+ return "", "", ctxerr.Wrap(
+ ctx, AutofillError{
+ Message: "error sending request to get human interpretation from osquery sql",
+ InternalErr: err,
+ },
+ )
+ }
+ defer resp.Body.Close()
+ if (resp.StatusCode / 100) != 2 {
+ return "", "", ctxerr.Wrap(
+ ctx, AutofillError{
+ Message: "error from human interpretation of osquery sql",
+ InternalErr: fmt.Errorf(
+ "%s returned %d status code", getHumanInterpretationFromOsquerySqlUrl, resp.StatusCode,
+ ),
+ },
+ )
+ }
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return "", "", ctxerr.Wrap(
+ ctx, AutofillError{
+ Message: "error reading response body from human interpretation of osquery sql",
+ InternalErr: err,
+ },
+ )
+ }
+
+ var result map[string]string
+ err = json.Unmarshal(body, &result)
+ if err != nil {
+ return "", "", ctxerr.Wrap(
+ ctx, AutofillError{
+ Message: "error unmarshaling response body from human interpretation of osquery sql",
+ InternalErr: err,
+ },
+ )
+ }
+ const maxLength = 1<<16 - 1
+ descriptionTrimmed := result["risks"]
+ if len(descriptionTrimmed) > maxLength {
+ descriptionTrimmed = descriptionTrimmed[:maxLength]
+ }
+ resolutionTrimmed := result["whatWillProbablyHappenDuringMaintenance"]
+ if len(resolutionTrimmed) > maxLength {
+ resolutionTrimmed = resolutionTrimmed[:maxLength]
+ }
+ return descriptionTrimmed, resolutionTrimmed, nil
+}
diff --git a/server/service/global_schedule.go b/server/service/global_schedule.go
index a8efa4c87d..c75d860486 100644
--- a/server/service/global_schedule.go
+++ b/server/service/global_schedule.go
@@ -37,7 +37,7 @@ func getGlobalScheduleEndpoint(ctx context.Context, request interface{}, svc fle
}
func (svc *Service) GetGlobalScheduledQueries(ctx context.Context, opts fleet.ListOptions) ([]*fleet.ScheduledQuery, error) {
- queries, err := svc.ListQueries(ctx, opts, nil, ptr.Bool(true)) // teamID == nil means global
+ queries, err := svc.ListQueries(ctx, opts, nil, ptr.Bool(true), false) // teamID == nil means global
if err != nil {
return nil, err
}
diff --git a/server/service/handler.go b/server/service/handler.go
index e37828e1b8..01969c4b8a 100644
--- a/server/service/handler.go
+++ b/server/service/handler.go
@@ -491,6 +491,9 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC
ue.POST("/api/_version_/fleet/hosts/{id:[0-9]+}/unlock", unlockHostEndpoint, unlockHostRequest{})
ue.POST("/api/_version_/fleet/hosts/{id:[0-9]+}/wipe", wipeHostEndpoint, wipeHostRequest{})
+ // Generative AI
+ ue.POST("/api/_version_/fleet/autofill/policy", autofillPoliciesEndpoint, autofillPoliciesRequest{})
+
// Only Fleet MDM specific endpoints should be within the root /mdm/ path.
// NOTE: remember to update
// `service.mdmConfigurationRequiredEndpoints` when you add an
diff --git a/server/service/integration_core_test.go b/server/service/integration_core_test.go
index edf03091c1..c3f064d88f 100644
--- a/server/service/integration_core_test.go
+++ b/server/service/integration_core_test.go
@@ -263,7 +263,7 @@ func (s *integrationTestSuite) TestQueryCreationLogsActivity() {
}
var createQueryResp createQueryResponse
s.DoJSON("POST", "/api/latest/fleet/queries", ¶ms, http.StatusOK, &createQueryResp)
- defer cleanupQuery(s, createQueryResp.Query.ID)
+ defer s.cleanupQuery(createQueryResp.Query.ID)
activities := listActivitiesResponse{}
s.DoJSON("GET", "/api/latest/fleet/activities", nil, http.StatusOK, &activities)
@@ -1579,7 +1579,7 @@ func (s *integrationTestSuite) TestListHosts() {
user1 := test.NewUser(t, s.ds, "Alice", "alice@example.com", true)
q := test.NewQuery(t, s.ds, nil, "query1", "select 1", 0, true)
- defer cleanupQuery(s, q.ID)
+ defer s.cleanupQuery(q.ID)
globalPolicy0, err := s.ds.NewGlobalPolicy(
context.Background(), &user1.ID, fleet.PolicyPayload{
QueryID: &q.ID,
@@ -5791,7 +5791,7 @@ func (s *integrationTestSuite) TestQueriesBadRequests() {
s.DoJSON("POST", "/api/latest/fleet/queries", reqQuery, http.StatusOK, &createQueryResp)
require.NotNil(t, createQueryResp.Query)
existingQueryID := createQueryResp.Query.ID
- defer cleanupQuery(s, existingQueryID)
+ defer s.cleanupQuery(existingQueryID)
for _, tc := range []struct {
tname string
@@ -6176,6 +6176,7 @@ func (s *integrationTestSuite) TestAppConfig() {
assert.False(t, acResp.MDM.AppleBMTermsExpired)
assert.False(t, acResp.ActivityExpirySettings.ActivityExpiryEnabled)
assert.Zero(t, acResp.ActivityExpirySettings.ActivityExpiryWindow)
+ assert.False(t, acResp.ServerSettings.AIFeaturesDisabled)
// set the apple BM terms expired flag, and the enabled and configured flags,
// we'll check again at the end of this test to make sure they weren't
@@ -6242,6 +6243,20 @@ func (s *integrationTestSuite) TestAppConfig() {
require.True(t, acResp.ActivityExpirySettings.ActivityExpiryEnabled)
require.Equal(t, 42, acResp.ActivityExpirySettings.ActivityExpiryWindow)
+ // Disable AI features.
+ acResp = appConfigResponse{}
+ s.DoJSON(
+ "PATCH", "/api/latest/fleet/config", json.RawMessage(
+ `{
+ "server_settings": {
+ "ai_features_disabled": true
+ }
+ }`,
+ ), http.StatusOK, &acResp,
+ )
+ s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &acResp)
+ assert.True(t, acResp.ServerSettings.AIFeaturesDisabled)
+
// test a change that does clear the agent options (the field is provided but empty).
s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
"agent_options": {}
@@ -8996,7 +9011,7 @@ func createSession(t *testing.T, uid uint, ds fleet.Datastore) *fleet.Session {
return ssn
}
-func cleanupQuery(s *integrationTestSuite, queryID uint) {
+func (s *integrationTestSuite) cleanupQuery(queryID uint) {
var delResp deleteQueryByIDResponse
s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/queries/id/%d", queryID), nil, http.StatusOK, &delResp)
}
@@ -11569,3 +11584,96 @@ func (s *integrationTestSuite) TestDebugDB() {
s.DoJSON("GET", "/debug/db/innodb-status", nil, http.StatusOK, &responseString)
assert.Contains(t, responseString, "INNODB MONITOR OUTPUT")
}
+
+func (s *integrationTestSuite) TestAutofillPolicies() {
+ t := s.T()
+ startMockServer := func(t *testing.T) string {
+ // create a test http server
+ srv := httptest.NewServer(
+ http.HandlerFunc(
+ func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != "POST" {
+ w.WriteHeader(http.StatusMethodNotAllowed)
+ return
+ }
+ switch r.URL.Path {
+ case "/ok":
+ var body map[string]interface{}
+ err := json.NewDecoder(r.Body).Decode(&body)
+ if err != nil {
+ t.Log(err)
+ w.WriteHeader(http.StatusBadRequest)
+ return
+ }
+ _, _ = w.Write([]byte(`{"risks":"description", "whatWillProbablyHappenDuringMaintenance":"resolution"}`))
+ case "/error":
+ w.WriteHeader(http.StatusTeapot)
+ _, _ = w.Write([]byte(`{}`))
+ case "/badBody":
+ _, _ = w.Write([]byte(`{bad json}`))
+ case "/timeout":
+ time.Sleep(2 * time.Second)
+ _, _ = w.Write([]byte(`{"risks":"description", "whatWillProbablyHappenDuringMaintenance":"resolution"}`))
+ default:
+ w.WriteHeader(http.StatusNotFound)
+ }
+ },
+ ),
+ )
+ t.Cleanup(srv.Close)
+ return srv.URL
+ }
+ mockUrl := startMockServer(t)
+ originalUrl := getHumanInterpretationFromOsquerySqlUrl
+ originalTimeout := getHumanInterpretationFromOsquerySqlTimeout
+ t.Cleanup(
+ func() {
+ getHumanInterpretationFromOsquerySqlUrl = originalUrl
+ getHumanInterpretationFromOsquerySqlTimeout = originalTimeout
+ },
+ )
+
+ req := autofillPoliciesRequest{
+ SQL: " ", // empty
+ }
+ getHumanInterpretationFromOsquerySqlUrl = mockUrl + "/ok"
+ // empty sql
+ resp := s.Do("POST", "/api/latest/fleet/autofill/policy", req, http.StatusBadRequest)
+ assertBodyContains(t, resp, "cannot be empty")
+
+ // good request
+ req.SQL = "select 1"
+ var res autofillPoliciesResponse
+ s.DoJSON("POST", "/api/latest/fleet/autofill/policy", req, http.StatusOK, &res)
+ assert.Equal(t, "description", res.Description)
+ assert.Equal(t, "resolution", res.Resolution)
+
+ // good request with weird characters
+ req.SQL = `select * from " with ' and "" \"`
+ res = autofillPoliciesResponse{}
+ s.DoJSON("POST", "/api/latest/fleet/autofill/policy", req, http.StatusOK, &res)
+ assert.Equal(t, "description", res.Description)
+ assert.Equal(t, "resolution", res.Resolution)
+
+ getHumanInterpretationFromOsquerySqlUrl = mockUrl + "/error"
+ resp = s.Do("POST", "/api/latest/fleet/autofill/policy", req, http.StatusUnprocessableEntity)
+ assertBodyContains(t, resp, "error from human interpretation of osquery sql")
+
+ getHumanInterpretationFromOsquerySqlUrl = mockUrl + "/badBody"
+ resp = s.Do("POST", "/api/latest/fleet/autofill/policy", req, http.StatusUnprocessableEntity)
+ assertBodyContains(t, resp, "error unmarshaling response body from human interpretation of osquery sql")
+
+ getHumanInterpretationFromOsquerySqlUrl = mockUrl + "/timeout"
+ getHumanInterpretationFromOsquerySqlTimeout = 1 * time.Millisecond
+ resp = s.Do("POST", "/api/latest/fleet/autofill/policy", req, http.StatusUnprocessableEntity)
+ assertBodyContains(t, resp, "error sending request to get human interpretation from osquery sql")
+
+ // disable AI features
+ appConfigSpec := map[string]map[string]bool{
+ "server_settings": {"ai_features_disabled": true},
+ }
+ s.Do("PATCH", "/api/latest/fleet/config", appConfigSpec, http.StatusOK)
+ resp = s.Do("POST", "/api/latest/fleet/autofill/policy", req, http.StatusBadRequest)
+ assertBodyContains(t, resp, "AI features are disabled")
+
+}
diff --git a/server/service/integration_enterprise_test.go b/server/service/integration_enterprise_test.go
index 90c875296a..bd4cdb2050 100644
--- a/server/service/integration_enterprise_test.go
+++ b/server/service/integration_enterprise_test.go
@@ -844,6 +844,35 @@ func (s *integrationEnterpriseTestSuite) TestTeamPolicies() {
assert.Equal(t, gpol.Name, ts.InheritedPolicies[0].Name)
assert.Equal(t, gpol.ID, ts.InheritedPolicies[0].ID)
+ tc := countTeamPoliciesResponse{}
+ s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d/policies/count", team1.ID), nil, http.StatusOK, &tc)
+ require.Nil(t, tc.Err)
+ require.Equal(t, 1, tc.Count)
+
+ gc := countGlobalPoliciesResponse{}
+ s.DoJSON("GET", "/api/latest/fleet/policies/count", nil, http.StatusOK, &gc)
+ require.Nil(t, gc.Err)
+ require.Equal(t, 1, gc.Count)
+
+ // Test merge inherited
+ ts = listTeamPoliciesResponse{}
+ s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d/policies", team1.ID), nil, http.StatusOK, &ts, "merge_inherited", "true", "order_key", "team_id", "order_direction", "desc")
+ require.Len(t, ts.Policies, 2)
+ require.Nil(t, ts.InheritedPolicies)
+ assert.Equal(t, "TestQuery2", ts.Policies[0].Name)
+ assert.Equal(t, "select * from osquery;", ts.Policies[0].Query)
+ assert.Equal(t, "Some description", ts.Policies[0].Description)
+ require.NotNil(t, ts.Policies[0].Resolution)
+ assert.Equal(t, "some team resolution", *ts.Policies[0].Resolution)
+ assert.Equal(t, gpol.Name, ts.Policies[1].Name)
+ assert.Equal(t, gpol.ID, ts.Policies[1].ID)
+
+ countResp := countTeamPoliciesResponse{}
+ s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d/policies/count", team1.ID), nil, http.StatusOK, &countResp, "merge_inherited", "true")
+ require.Nil(t, countResp.Err)
+ require.Equal(t, 2, countResp.Count)
+
+ // Test delete
deletePolicyParams := deleteTeamPoliciesRequest{IDs: []uint{ts.Policies[0].ID}}
deletePolicyResp := deleteTeamPoliciesResponse{}
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/teams/%d/policies/delete", team1.ID), deletePolicyParams, http.StatusOK, &deletePolicyResp)
@@ -853,6 +882,53 @@ func (s *integrationEnterpriseTestSuite) TestTeamPolicies() {
require.Len(t, ts.Policies, 0)
}
+func (s *integrationEnterpriseTestSuite) TestTeamQueries() {
+ t := s.T()
+
+ team1, err := s.ds.NewTeam(context.Background(), &fleet.Team{
+ ID: 42,
+ Name: "team1" + t.Name(),
+ Description: "desc team1",
+ })
+ require.NoError(t, err)
+
+ oldToken := s.token
+ t.Cleanup(func() {
+ s.token = oldToken
+ })
+
+ // create global query
+ params := fleet.QueryPayload{
+ Name: ptr.String("global1"),
+ Query: ptr.String("select * from time;"),
+ }
+ var createQueryResp createQueryResponse
+ s.DoJSON("POST", "/api/latest/fleet/queries", ¶ms, http.StatusOK, &createQueryResp)
+ defer s.cleanupQuery(createQueryResp.Query.ID)
+
+ // create team query
+ params = fleet.QueryPayload{
+ Name: ptr.String("team1"),
+ Query: ptr.String("select * from time;"),
+ TeamID: ptr.Uint(team1.ID),
+ }
+ createQueryResp = createQueryResponse{}
+ s.DoJSON("POST", "/api/latest/fleet/queries", ¶ms, http.StatusOK, &createQueryResp)
+ defer s.cleanupQuery(createQueryResp.Query.ID)
+
+ // list team queries
+ var listQueriesResp listQueriesResponse
+ s.DoJSON("GET", "/api/latest/fleet/queries", nil, http.StatusOK, &listQueriesResp, "team_id", fmt.Sprint(team1.ID))
+ require.Len(t, listQueriesResp.Queries, 1)
+ assert.Equal(t, "team1", listQueriesResp.Queries[0].Name)
+
+ // list merged team queries
+ s.DoJSON("GET", "/api/latest/fleet/queries", nil, http.StatusOK, &listQueriesResp, "team_id", fmt.Sprint(team1.ID), "merge_inherited", "true", "order_key", "team_id", "order_direction", "desc")
+ require.Len(t, listQueriesResp.Queries, 2)
+ assert.Equal(t, "team1", listQueriesResp.Queries[0].Name)
+ assert.Equal(t, "global1", listQueriesResp.Queries[1].Name)
+}
+
func (s *integrationEnterpriseTestSuite) TestModifyTeamEnrollSecrets() {
t := s.T()
@@ -2840,7 +2916,8 @@ func (s *integrationEnterpriseTestSuite) TestMDMMacOSUpdates() {
// edited macos min version activity got created
s.lastActivityMatches(fleet.ActivityTypeEditedMacOSMinVersion{}.ActivityName(), `{"deadline":"2022-01-01", "minimum_version":"12.3.1", "team_id": null, "team_name": null}`, 0)
s.assertMacOSUpdatesDeclaration(nil, &fleet.MacOSUpdates{
- MinimumVersion: optjson.SetString("12.3.1"), Deadline: optjson.SetString("2022-01-01")})
+ MinimumVersion: optjson.SetString("12.3.1"), Deadline: optjson.SetString("2022-01-01"),
+ })
// get the appconfig
acResp = appConfigResponse{}
@@ -2864,7 +2941,8 @@ func (s *integrationEnterpriseTestSuite) TestMDMMacOSUpdates() {
// another edited macos min version activity got created
lastActivity = s.lastActivityMatches(fleet.ActivityTypeEditedMacOSMinVersion{}.ActivityName(), `{"deadline":"2024-01-01", "minimum_version":"12.3.1", "team_id": null, "team_name": null}`, 0)
s.assertMacOSUpdatesDeclaration(nil, &fleet.MacOSUpdates{
- MinimumVersion: optjson.SetString("12.3.1"), Deadline: optjson.SetString("2024-01-01")})
+ MinimumVersion: optjson.SetString("12.3.1"), Deadline: optjson.SetString("2024-01-01"),
+ })
// update something unrelated - the transparency url
acResp = appConfigResponse{}
@@ -2875,7 +2953,8 @@ func (s *integrationEnterpriseTestSuite) TestMDMMacOSUpdates() {
// no activity got created
s.lastActivityMatches("", ``, lastActivity)
s.assertMacOSUpdatesDeclaration(nil, &fleet.MacOSUpdates{
- MinimumVersion: optjson.SetString("12.3.1"), Deadline: optjson.SetString("2024-01-01")})
+ MinimumVersion: optjson.SetString("12.3.1"), Deadline: optjson.SetString("2024-01-01"),
+ })
// clear the macos requirement
acResp = appConfigResponse{}
@@ -8708,3 +8787,8 @@ func triggerAndWait(ctx context.Context, t *testing.T, ds fleet.Datastore, s *sc
}
}
}
+
+func (s *integrationEnterpriseTestSuite) cleanupQuery(queryID uint) {
+ var delResp deleteQueryByIDResponse
+ s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/queries/id/%d", queryID), nil, http.StatusOK, &delResp)
+}
diff --git a/server/service/mdm.go b/server/service/mdm.go
index 322298d02b..ef47f609ca 100644
--- a/server/service/mdm.go
+++ b/server/service/mdm.go
@@ -1437,7 +1437,7 @@ type batchSetMDMProfilesRequest struct {
TeamID *uint `json:"-" query:"team_id,optional"`
TeamName *string `json:"-" query:"team_name,optional"`
DryRun bool `json:"-" query:"dry_run,optional"` // if true, apply validation but do not save changes
- AssumeEnabled bool `json:"-" query:"assume_enabled,optional"` // if true, assume MDM is enabled
+ AssumeEnabled *bool `json:"-" query:"assume_enabled,optional"` // if true, assume MDM is enabled
Profiles backwardsCompatProfilesParam `json:"profiles"`
}
@@ -1481,7 +1481,9 @@ func (r batchSetMDMProfilesResponse) Status() int { return http.StatusNoContent
func batchSetMDMProfilesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
req := request.(*batchSetMDMProfilesRequest)
- if err := svc.BatchSetMDMProfiles(ctx, req.TeamID, req.TeamName, req.Profiles, req.DryRun, false, req.AssumeEnabled); err != nil {
+ if err := svc.BatchSetMDMProfiles(
+ ctx, req.TeamID, req.TeamName, req.Profiles, req.DryRun, false, req.AssumeEnabled,
+ ); err != nil {
return batchSetMDMProfilesResponse{Err: err}, nil
}
return batchSetMDMProfilesResponse{}, nil
@@ -1489,7 +1491,7 @@ func batchSetMDMProfilesEndpoint(ctx context.Context, request interface{}, svc f
func (svc *Service) BatchSetMDMProfiles(
ctx context.Context, tmID *uint, tmName *string, profiles []fleet.MDMProfileBatchPayload, dryRun, skipBulkPending bool,
- assumeEnabled bool,
+ assumeEnabled *bool,
) error {
var err error
if tmID, tmName, err = svc.authorizeBatchProfiles(ctx, tmID, tmName); err != nil {
@@ -1500,8 +1502,8 @@ func (svc *Service) BatchSetMDMProfiles(
if err != nil {
return ctxerr.Wrap(ctx, err, "getting app config")
}
- if assumeEnabled {
- appCfg.MDM.WindowsEnabledAndConfigured = true
+ if assumeEnabled != nil {
+ appCfg.MDM.WindowsEnabledAndConfigured = *assumeEnabled
}
if err := validateProfiles(profiles); err != nil {
diff --git a/server/service/mdm_test.go b/server/service/mdm_test.go
index 013571ff31..a5c9635b18 100644
--- a/server/service/mdm_test.go
+++ b/server/service/mdm_test.go
@@ -1394,7 +1394,7 @@ func TestMDMBatchSetProfiles(t *testing.T) {
}
ctx = license.NewContext(ctx, &fleet.LicenseInfo{Tier: tier})
- err := svc.BatchSetMDMProfiles(ctx, tt.teamID, tt.teamName, tt.profiles, false, false, false)
+ err := svc.BatchSetMDMProfiles(ctx, tt.teamID, tt.teamName, tt.profiles, false, false, nil)
if tt.wantErr == "" {
require.NoError(t, err)
require.True(t, ds.BatchSetMDMProfilesFuncInvoked)
diff --git a/server/service/osquery.go b/server/service/osquery.go
index b007c279f7..bf72824aae 100644
--- a/server/service/osquery.go
+++ b/server/service/osquery.go
@@ -1865,6 +1865,16 @@ func (svc *Service) saveResultLogsToQueryReports(ctx context.Context, unmarshale
continue
}
+ hostTeamID := uint(0)
+ if host.TeamID != nil {
+ hostTeamID = *host.TeamID
+ }
+ if dbQuery.TeamID != nil && *dbQuery.TeamID != hostTeamID {
+ // The host was transferred to another team/global so we ignore the incoming results
+ // of this query that belong to a different team.
+ continue
+ }
+
// We first check the current query results count using the DB reader (also cached)
// to reduce the DB writer load of osquery/log requests when the host count is high.
count, err := svc.ds.ResultCountForQuery(ctx, dbQuery.ID)
@@ -1958,18 +1968,45 @@ func getMostRecentResults(results []*fleet.ScheduledQueryResult) []*fleet.Schedu
// The expected format for s is "pack{Global|team-}"
//
// Returns "" if it failed to parse the pack_delimiter.
+
+var (
+ dcounter = regexp.MustCompile(`(Global)|(team-\d+)`)
+ pattern = regexp.MustCompile(`^(.*)(?:(Global)|(team-\d+))`)
+)
+
func findPackDelimiterString(scheduledQueryName string) string {
- // Go's regexp doesn't support backreferences so we have to perform some manual work.
scheduledQueryName = scheduledQueryName[4:] // always starts with "pack"
- for l := 1; l < len(scheduledQueryName); l++ {
- sep := scheduledQueryName[:l]
- rest := scheduledQueryName[l:]
- pattern := fmt.Sprintf(`^(?:(Global)|(team-\d+))%s.+`, regexp.QuoteMeta(sep))
- matched, _ := regexp.MatchString(pattern, rest)
- if matched {
- return sep
+
+ count := dcounter.FindAllString(scheduledQueryName, -1)
+
+ // If Global or team- does not appear, then the
+ // pack_delimiter is invalid.
+ if len(count) == 0 {
+ return ""
+ }
+
+ if len(count) == 1 {
+ matches := pattern.FindStringSubmatch(scheduledQueryName)
+ if len(matches) > 1 {
+ return matches[1]
}
}
+
+ // Handle edge cases where "Global" or "team-"" appears multiple times in the query
+ // name. Regex is not pre-compiled, so it is a less performant operation.
+ // Go's regexp doesn't support backreferences so we have to perform some manual work.
+ if len(count) > 1 {
+ for l := 1; l < len(scheduledQueryName); l++ {
+ sep := scheduledQueryName[:l]
+ rest := scheduledQueryName[l:]
+ pattern := fmt.Sprintf(`^(?:(Global)|(team-\d+))%s.+`, regexp.QuoteMeta(sep))
+ matched, _ := regexp.MatchString(pattern, rest)
+ if matched {
+ return sep
+ }
+ }
+ }
+
return ""
}
@@ -1995,6 +2032,10 @@ func getQueryNameAndTeamIDFromResult(path string) (*uint, string, error) {
// For pattern: pack/Global/Name
globalPattern := "pack" + sep + "Global" + sep
if strings.HasPrefix(path, globalPattern) {
+ name := strings.TrimPrefix(path, globalPattern)
+ if name == "" {
+ return nil, "", fmt.Errorf("parsing query name: %s", path)
+ }
return nil, strings.TrimPrefix(path, globalPattern), nil
}
@@ -2006,6 +2047,9 @@ func getQueryNameAndTeamIDFromResult(path string) (*uint, string, error) {
if len(teamIDAndQueryNameParts) != 2 {
return nil, "", fmt.Errorf("parsing team number part: %s", path)
}
+ if teamIDAndQueryNameParts[1] == "" {
+ return nil, "", fmt.Errorf("parsing query name: %s", path)
+ }
teamNumberUint, err := strconv.ParseUint(teamIDAndQueryNameParts[0], 10, 32)
if err != nil {
return nil, "", fmt.Errorf("parsing team number: %w", err)
diff --git a/server/service/osquery_test.go b/server/service/osquery_test.go
index 10387de164..35d2d59627 100644
--- a/server/service/osquery_test.go
+++ b/server/service/osquery_test.go
@@ -546,6 +546,22 @@ func TestSubmitResultLogsToLogDestination(t *testing.T) {
}
ds.QueryByNameFunc = func(ctx context.Context, teamID *uint, name string) (*fleet.Query, error) {
switch {
+ case teamID != nil && *teamID == 1:
+ return &fleet.Query{
+ ID: 4242,
+ Name: name,
+ AutomationsEnabled: true,
+ TeamID: ptr.Uint(1),
+ Logging: fleet.LoggingSnapshot,
+ }, nil
+ case teamID != nil && *teamID == 2:
+ return &fleet.Query{
+ ID: 4343,
+ Name: name,
+ AutomationsEnabled: true,
+ TeamID: ptr.Uint(2),
+ Logging: fleet.LoggingSnapshot,
+ }, nil
case teamID == nil && (name == "time" || name == "system_info" || name == "encrypted" || name == "hosts"):
return &fleet.Query{
Name: name,
@@ -597,11 +613,16 @@ func TestSubmitResultLogsToLogDestination(t *testing.T) {
ds.ResultCountForQueryFunc = func(ctx context.Context, queryID uint) (int, error) {
return 0, nil
}
+ teamQueryResultsStored := false
ds.OverwriteQueryResultRowsFunc = func(ctx context.Context, rows []*fleet.ScheduledQueryResultRow) error {
if len(rows) == 0 {
return nil
}
switch {
+ case rows[0].QueryID == 4242:
+ t.Fatal("should not happen, as query 4242 is a team query and host is global")
+ case rows[0].QueryID == 4343:
+ teamQueryResultsStored = true
case rows[0].QueryID == 123:
require.Len(t, rows, 1)
require.Equal(t, uint(999), rows[0].HostID)
@@ -654,6 +675,9 @@ func TestSubmitResultLogsToLogDestination(t *testing.T) {
`{"snapshot":[{"hour":"20","minutes":"8"}],"action":"snapshot","name":"pack/team-foo/bar","hostIdentifier":"1379f59d98f4","calendarTime":"Tue Jan 10 20:08:51 2017 UTC","unixTime":1484078931,"decorations":{"host_uuid":"EB714C9D-C1F8-A436-B6DA-3F853C5502EA"}}`,
`{"snapshot":[{"hour":"20","minutes":"8"}],"action":"snapshot","name":"pack/team-","hostIdentifier":"1379f59d98f4","calendarTime":"Tue Jan 10 20:08:51 2017 UTC","unixTime":1484078931,"decorations":{"host_uuid":"EB714C9D-C1F8-A436-B6DA-3F853C5502EA"}}`,
`{"snapshot":[{"hour":"20","minutes":"8"}],"action":"snapshot","name":"pack/PackName","hostIdentifier":"1379f59d98f4","calendarTime":"Tue Jan 10 20:08:51 2017 UTC","unixTime":1484078931,"decorations":{"host_uuid":"EB714C9D-C1F8-A436-B6DA-3F853C5502EA"}}`,
+
+ // Query results of a query that belongs to a different team than the host's team (can happen when host is transferred from one team to another or no team).
+ `{"snapshot":[{"hour":"20","minutes":"8"}],"action":"snapshot","name":"pack/team-1/Foobar","hostIdentifier":"1379f59d98f4","calendarTime":"Tue Jan 10 20:08:51 2017 UTC","unixTime":1484078931,"decorations":{"host_uuid":"EB714C9D-C1F8-A436-B6DA-3F853C5502EA"}}`,
}
logJSON := fmt.Sprintf("[%s]", strings.Join(validLogResults, ","))
@@ -671,7 +695,8 @@ func TestSubmitResultLogsToLogDestination(t *testing.T) {
require.NoError(t, err)
host := fleet.Host{
- ID: 999,
+ ID: 999,
+ TeamID: nil, // Global host.
}
ctx = hostctx.NewContext(ctx, &host)
@@ -691,6 +716,25 @@ func TestSubmitResultLogsToLogDestination(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, validResults, testLogger.logs)
+
+ //
+ // Run a similar test but now with a team host.
+ //
+ host = fleet.Host{
+ ID: 999,
+ TeamID: ptr.Uint(2),
+ }
+ ctx = hostctx.NewContext(ctx, &host)
+ results := []json.RawMessage{
+ // This query should be ignored.
+ json.RawMessage(`{"snapshot":[{"hour":"20","minutes":"8"}],"action":"snapshot","name":"pack/team-1/Foobar","hostIdentifier":"1379f59d98f4","calendarTime":"Tue Jan 10 20:08:51 2017 UTC","unixTime":1484078931,"decorations":{"host_uuid":"EB714C9D-C1F8-A436-B6DA-3F853C5502EA"}}`),
+ // This query should be stored.
+ json.RawMessage(`{"snapshot":[{"hour":"20","minutes":"8"}],"action":"snapshot","name":"pack/team-2/Zoobar","hostIdentifier":"1379f59d98f4","calendarTime":"Tue Jan 10 20:08:51 2017 UTC","unixTime":1484078931,"decorations":{"host_uuid":"EB714C9D-C1F8-A436-B6DA-3F853C5502EA"}}`),
+ }
+ err = serv.SubmitResultLogs(ctx, results)
+ require.NoError(t, err)
+
+ require.True(t, teamQueryResultsStored)
}
func TestSaveResultLogsToQueryReports(t *testing.T) {
@@ -906,11 +950,11 @@ func TestGetQueryNameAndTeamIDFromResult(t *testing.T) {
expectedName string
hasErr bool
}{
- {"pack/Global/Query Name", nil, "Query Name", false},
- {"pack/team-1/Query Name", ptr.Uint(1), "Query Name", false},
- {"pack/team-12345/Another Query", ptr.Uint(12345), "Another Query", false},
- {"pack/team-foo/Query", nil, "", true},
- {"pack/Global/QueryWith/Slash", nil, "QueryWith/Slash", false},
+ {"pack/Global/Query Name", nil, "Query Name", false}, // valid global query
+ {"pack/team-1/Query Name", ptr.Uint(1), "Query Name", false}, // valid team query
+ {"pack/team-12345/Another Query", ptr.Uint(12345), "Another Query", false}, // valid team query
+ {"pack/team-foo/Query", nil, "", true}, // missing team ID
+ {"pack/Global/QueryWith/Slash", nil, "QueryWith/Slash", false}, // query name contains forward slash
{"packGlobalGlobalGlobalGlobal", nil, "Global", false}, // pack_delimiter=Global
{"packXGlobalGlobalXGlobalQueryWith/Slash", nil, "QueryWith/Slash", false}, // pack_delimiter=XGlobal
{"pack//Global//QueryWith/Slash", nil, "QueryWith/Slash", false}, // pack_delimiter=//
@@ -920,6 +964,8 @@ func TestGetQueryNameAndTeamIDFromResult(t *testing.T) {
{"pack123π123team-1123π123QueryWith/Slash", ptr.Uint(1), "QueryWith/Slash", false}, // pack_delimiter=123π123
{"pack(foo)team-1(foo)fo(o)bar", ptr.Uint(1), "fo(o)bar", false}, // pack_delimiter=(foo)
{"packteam-1team-1team-1team-1", ptr.Uint(1), "team-1", false}, // pack_delimiter=team-1
+ {"pack/Global/GlobalInQueryName", nil, "GlobalInQueryName", false}, // query name contains Global
+ {"pack/team-1/team-1InQueryName", ptr.Uint(1), "team-1InQueryName", false}, // query name contains team-1
{"InvalidString", nil, "", true},
{"Invalid/Query", nil, "", true},
@@ -3803,3 +3849,26 @@ func TestDetailQueriesLinuxDistros(t *testing.T) {
require.Contains(t, m, "software_linux")
}
}
+
+// Benchmark function
+func BenchmarkFindPackDelimiterStringCommon(b *testing.B) {
+ // Input data for benchmarking
+ input := "pack/Global/Foo"
+
+ // Run the benchmark
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ findPackDelimiterString(input)
+ }
+}
+
+func BenchmarkFindPackDelimiterStringTeamPack(b *testing.B) {
+ // Input data for benchmarking
+ input := "packGlobalGlobalGlobalGlobal" // global pack delimiter, global team, query name global
+
+ // Run the benchmark
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ findPackDelimiterString(input)
+ }
+}
diff --git a/server/service/osquery_utils/queries.go b/server/service/osquery_utils/queries.go
index ecbbf264ca..26b4bef53e 100644
--- a/server/service/osquery_utils/queries.go
+++ b/server/service/osquery_utils/queries.go
@@ -85,8 +85,8 @@ FROM
-- whereas on Windows ia.interface is the IP of the interface.
JOIN routes r ON %s
WHERE
- -- Destination 0.0.0.0/0 is the default route on route tables.
- r.destination = '0.0.0.0' AND r.netmask = 0
+ -- Destination 0.0.0.0/0 or ::/0 (IPv6) is the default route on route tables.
+ (r.destination = '0.0.0.0' OR r.destination = '::') AND r.netmask = 0
-- Type of route is "gateway" for Unix, "remote" for Windows.
AND r.type = '%s'
-- We are only interested on private IPs (some devices have their Public IP as Primary IP too).
@@ -173,6 +173,12 @@ var hostDetailQueries = map[string]DetailQuery{
},
},
"os_version_windows": {
+ // Fleet requires the DisplayVersion as well as the UBR (4th part of the version number) to
+ // correctly map OS vulnerabilities to hosts. The UBR is not available in the os_version table.
+ // The full version number is available in the `kernel_info` table, but there is a Win10 bug
+ // which is reporting an incorrect build number (3rd part), so we query the Windows registry for the UBR
+ // here instead. To note, osquery 5.12.0 will have the UBR in the os_version table.
+
// display_version is not available in some versions of
// Windows (Server 2019). By including it using a JOIN it can
// return no rows and the query will still succeed
@@ -181,16 +187,23 @@ var hostDetailQueries = map[string]DetailQuery{
SELECT data as display_version
FROM registry
WHERE path = 'HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\DisplayVersion'
+ ),
+ ubr_table AS (
+ SELECT data AS ubr
+ FROM registry
+ WHERE path ='HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\UBR'
)
SELECT
os.name,
COALESCE(d.display_version, '') AS display_version,
- k.version
+ COALESCE(CONCAT((SELECT version FROM os_version), '.', u.ubr), k.version) AS version
FROM
os_version os,
kernel_info k
LEFT JOIN
- display_version_table d`,
+ display_version_table d
+ LEFT JOIN
+ ubr_table u`,
Platforms: []string{"windows"},
IngestFunc: func(ctx context.Context, logger log.Logger, host *fleet.Host, rows []map[string]string) error {
if len(rows) != 1 {
@@ -542,6 +555,7 @@ var extraDetailQueries = map[string]DetailQuery{
// This query is used to populate the `operating_systems` and `host_operating_system`
// tables. Separately, the `hosts` table is populated via the `os_version` and
// `os_version_windows` detail queries above.
+ // See above description for the `os_version_windows` detail query.
//
// DisplayVersion doesn't exist on all versions of Windows (Server 2019).
// To prevent the query from failing in those cases, we join
@@ -552,19 +566,26 @@ var extraDetailQueries = map[string]DetailQuery{
SELECT data as display_version
FROM registry
WHERE path = 'HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\DisplayVersion'
+ ),
+ ubr_table AS (
+ SELECT data AS ubr
+ FROM registry
+ WHERE path ='HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\UBR'
)
SELECT
os.name,
os.platform,
os.arch,
k.version as kernel_version,
- os.version,
+ COALESCE(CONCAT((SELECT version FROM os_version), '.', u.ubr), k.version) AS version,
COALESCE(d.display_version, '') AS display_version
FROM
os_version os,
kernel_info k
LEFT JOIN
- display_version_table d`,
+ display_version_table d
+ LEFT JOIN
+ ubr_table u`,
Platforms: []string{"windows"},
DirectIngestFunc: directIngestOSWindows,
},
@@ -1095,9 +1116,9 @@ func directIngestOSWindows(ctx context.Context, logger log.Logger, host *fleet.H
hostOS := fleet.OperatingSystem{
Name: rows[0]["name"],
Arch: rows[0]["arch"],
- KernelVersion: rows[0]["kernel_version"],
+ KernelVersion: rows[0]["version"],
Platform: rows[0]["platform"],
- Version: rows[0]["kernel_version"],
+ Version: rows[0]["version"],
}
displayVersion := rows[0]["display_version"]
diff --git a/server/service/osquery_utils/queries_test.go b/server/service/osquery_utils/queries_test.go
index 4a6da7a2ad..1715b71430 100644
--- a/server/service/osquery_utils/queries_test.go
+++ b/server/service/osquery_utils/queries_test.go
@@ -52,6 +52,21 @@ func TestDetailQueryNetworkInterfaces(t *testing.T) {
assert.NoError(t, ingest(context.Background(), log.NewNopLogger(), &host, rows))
assert.Equal(t, "10.0.1.2", host.PrimaryIP)
assert.Equal(t, "bc:d0:74:4b:10:6d", host.PrimaryMac)
+
+ rows = make([]map[string]string, 1)
+ require.NoError(
+ t, json.Unmarshal(
+ []byte(`
+[
+ {"address":"fd7a:115c:a1e0::d401:6637","mac":"b2:a2:e4:62:0f:1e"}
+]`),
+ &rows,
+ ),
+ )
+ assert.NoError(t, ingest(context.Background(), log.NewNopLogger(), &host, rows))
+ assert.Equal(t, "fd7a:115c:a1e0::d401:6637", host.PrimaryIP)
+ assert.Equal(t, "b2:a2:e4:62:0f:1e", host.PrimaryMac)
+
}
func TestDetailQueryScheduledQueryStats(t *testing.T) {
@@ -971,7 +986,7 @@ func TestDirectIngestOSWindows(t *testing.T) {
DisplayVersion: "21H2",
},
data: []map[string]string{
- {"name": "Microsoft Windows 11 Enterprise", "display_version": "21H2", "version": "10.0.22000.795", "release_id": "", "arch": "64-bit", "kernel_version": "10.0.22000.795"},
+ {"name": "Microsoft Windows 11 Enterprise", "display_version": "21H2", "version": "10.0.22000.795", "arch": "64-bit"},
},
},
{
@@ -983,7 +998,7 @@ func TestDirectIngestOSWindows(t *testing.T) {
DisplayVersion: "",
},
data: []map[string]string{
- {"name": "Microsoft Windows 10 Enterprise", "display_version": "", "version": "10.0.17763", "release_id": "1809", "arch": "64-bit", "kernel_version": "10.0.17763.2183"},
+ {"name": "Microsoft Windows 10 Enterprise", "display_version": "", "version": "10.0.17763.2183", "arch": "64-bit"},
},
},
}
diff --git a/server/service/queries.go b/server/service/queries.go
index a4d39fef83..988f3d232b 100644
--- a/server/service/queries.go
+++ b/server/service/queries.go
@@ -58,7 +58,8 @@ func (svc *Service) GetQuery(ctx context.Context, id uint) (*fleet.Query, error)
type listQueriesRequest struct {
ListOptions fleet.ListOptions `url:"list_options"`
// TeamID url argument set to 0 means global.
- TeamID uint `query:"team_id,optional"`
+ TeamID uint `query:"team_id,optional"`
+ MergeInherited bool `query:"merge_inherited,optional"`
}
type listQueriesResponse struct {
@@ -76,7 +77,7 @@ func listQueriesEndpoint(ctx context.Context, request interface{}, svc fleet.Ser
teamID = &req.TeamID
}
- queries, err := svc.ListQueries(ctx, req.ListOptions, teamID, nil)
+ queries, err := svc.ListQueries(ctx, req.ListOptions, teamID, nil, req.MergeInherited)
if err != nil {
return listQueriesResponse{Err: err}, nil
}
@@ -90,7 +91,7 @@ func listQueriesEndpoint(ctx context.Context, request interface{}, svc fleet.Ser
}, nil
}
-func (svc *Service) ListQueries(ctx context.Context, opt fleet.ListOptions, teamID *uint, scheduled *bool) ([]*fleet.Query, error) {
+func (svc *Service) ListQueries(ctx context.Context, opt fleet.ListOptions, teamID *uint, scheduled *bool, mergeInherited bool) ([]*fleet.Query, error) {
// Check the user is allowed to list queries on the given team.
if err := svc.authz.Authorize(ctx, &fleet.Query{
TeamID: teamID,
@@ -99,9 +100,10 @@ func (svc *Service) ListQueries(ctx context.Context, opt fleet.ListOptions, team
}
queries, err := svc.ds.ListQueries(ctx, fleet.ListQueryOptions{
- ListOptions: opt,
- TeamID: teamID,
- IsScheduled: scheduled,
+ ListOptions: opt,
+ TeamID: teamID,
+ IsScheduled: scheduled,
+ MergeInherited: mergeInherited,
})
if err != nil {
return nil, err
@@ -733,7 +735,7 @@ func getQuerySpecsEndpoint(ctx context.Context, request interface{}, svc fleet.S
}
func (svc *Service) GetQuerySpecs(ctx context.Context, teamID *uint) ([]*fleet.QuerySpec, error) {
- queries, err := svc.ListQueries(ctx, fleet.ListOptions{}, teamID, nil)
+ queries, err := svc.ListQueries(ctx, fleet.ListOptions{}, teamID, nil, false)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "getting queries")
}
diff --git a/server/service/queries_test.go b/server/service/queries_test.go
index 0fc1a44aec..9b9cdfb1c9 100644
--- a/server/service/queries_test.go
+++ b/server/service/queries_test.go
@@ -632,7 +632,7 @@ func TestQueryAuth(t *testing.T) {
_, err = svc.QueryReportIsClipped(ctx, tt.qid)
checkAuthErr(t, tt.shouldFailRead, err)
- _, err = svc.ListQueries(ctx, fleet.ListOptions{}, query.TeamID, nil)
+ _, err = svc.ListQueries(ctx, fleet.ListOptions{}, query.TeamID, nil, false)
checkAuthErr(t, tt.shouldFailRead, err)
teamName := ""
diff --git a/server/service/scripts_test.go b/server/service/scripts_test.go
index 60cb72a11e..98d7e2dfb8 100644
--- a/server/service/scripts_test.go
+++ b/server/service/scripts_test.go
@@ -309,11 +309,13 @@ func TestHostRunScript(t *testing.T) {
{"large script", strings.Repeat("a", fleet.UnsavedScriptMaxRuneLen), ""},
{"invalid utf8", "\xff\xfa", "Wrong data format."},
{"valid without hashbang", "echo 'a'", ""},
- {"valid with hashbang", "#!/bin/sh\necho 'a'", ""},
+ {"valid with posix hashbang", "#!/bin/sh\necho 'a'", ""},
+ {"valid with usr zsh hashbang", "#!/usr/bin/zsh\necho 'a'", ""},
+ {"valid with zsh hashbang", "#!/bin/zsh\necho 'a'", ""},
+ {"valid with zsh hashbang and arguments", "#!/bin/zsh -x\necho 'a'", ""},
{"valid with hashbang and spacing", "#! /bin/sh \necho 'a'", ""},
{"valid with hashbang and Windows newline", "#! /bin/sh \r\necho 'a'", ""},
{"invalid hashbang", "#!/bin/bash\necho 'a'", "Interpreter not supported."},
- {"invalid hashbang suffix", "#!/bin/sh -n\necho 'a'", "Interpreter not supported."},
}
ctx = viewer.NewContext(ctx, viewer.Viewer{User: test.UserAdmin})
diff --git a/server/service/team_policies.go b/server/service/team_policies.go
index 75cbe3ae96..a2698c145e 100644
--- a/server/service/team_policies.go
+++ b/server/service/team_policies.go
@@ -106,6 +106,7 @@ type listTeamPoliciesRequest struct {
InheritedPerPage uint `query:"inherited_per_page,optional"`
InheritedOrderDirection fleet.OrderDirection `query:"inherited_order_direction,optional"`
InheritedOrderKey string `query:"inherited_order_key,optional"`
+ MergeInherited bool `query:"merge_inherited,optional"`
}
type listTeamPoliciesResponse struct {
@@ -126,14 +127,14 @@ func listTeamPoliciesEndpoint(ctx context.Context, request interface{}, svc flee
OrderKey: req.InheritedOrderKey,
}
- tmPols, inheritedPols, err := svc.ListTeamPolicies(ctx, req.TeamID, req.Opts, inheritedListOptions)
+ tmPols, inheritedPols, err := svc.ListTeamPolicies(ctx, req.TeamID, req.Opts, inheritedListOptions, req.MergeInherited)
if err != nil {
return listTeamPoliciesResponse{Err: err}, nil
}
return listTeamPoliciesResponse{Policies: tmPols, InheritedPolicies: inheritedPols}, nil
}
-func (svc *Service) ListTeamPolicies(ctx context.Context, teamID uint, opts fleet.ListOptions, iopts fleet.ListOptions) (teamPolicies, inheritedPolicies []*fleet.Policy, err error) {
+func (svc *Service) ListTeamPolicies(ctx context.Context, teamID uint, opts fleet.ListOptions, iopts fleet.ListOptions, mergeInherited bool) (teamPolicies, inheritedPolicies []*fleet.Policy, err error) {
if err := svc.authz.Authorize(ctx, &fleet.Policy{
PolicyData: fleet.PolicyData{
TeamID: ptr.Uint(teamID),
@@ -146,6 +147,11 @@ func (svc *Service) ListTeamPolicies(ctx context.Context, teamID uint, opts flee
return nil, nil, ctxerr.Wrapf(ctx, err, "loading team %d", teamID)
}
+ if mergeInherited {
+ p, err := svc.ds.ListMergedTeamPolicies(ctx, teamID, opts)
+ return p, nil, err
+ }
+
return svc.ds.ListTeamPolicies(ctx, teamID, opts, iopts)
}
@@ -154,8 +160,9 @@ func (svc *Service) ListTeamPolicies(ctx context.Context, teamID uint, opts flee
/////////////////////////////////////////////////////////////////////////////////
type countTeamPoliciesRequest struct {
- ListOptions fleet.ListOptions `url:"list_options"`
- TeamID uint `url:"team_id"`
+ ListOptions fleet.ListOptions `url:"list_options"`
+ TeamID uint `url:"team_id"`
+ MergeInherited bool `query:"merge_inherited,optional"`
}
type countTeamPoliciesResponse struct {
@@ -167,14 +174,14 @@ func (r countTeamPoliciesResponse) error() error { return r.Err }
func countTeamPoliciesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
req := request.(*countTeamPoliciesRequest)
- resp, err := svc.CountTeamPolicies(ctx, req.TeamID, req.ListOptions.MatchQuery)
+ resp, err := svc.CountTeamPolicies(ctx, req.TeamID, req.ListOptions.MatchQuery, req.MergeInherited)
if err != nil {
return countTeamPoliciesResponse{Err: err}, nil
}
return countTeamPoliciesResponse{Count: resp}, nil
}
-func (svc *Service) CountTeamPolicies(ctx context.Context, teamID uint, matchQuery string) (int, error) {
+func (svc *Service) CountTeamPolicies(ctx context.Context, teamID uint, matchQuery string, mergeInherited bool) (int, error) {
if err := svc.authz.Authorize(ctx, &fleet.Policy{
PolicyData: fleet.PolicyData{
TeamID: ptr.Uint(teamID),
@@ -187,6 +194,10 @@ func (svc *Service) CountTeamPolicies(ctx context.Context, teamID uint, matchQue
return 0, ctxerr.Wrapf(ctx, err, "loading team %d", teamID)
}
+ if mergeInherited {
+ return svc.ds.CountMergedTeamPolicies(ctx, teamID, matchQuery)
+ }
+
return svc.ds.CountPolicies(ctx, &teamID, matchQuery)
}
diff --git a/server/service/team_policies_test.go b/server/service/team_policies_test.go
index 9e1a502f67..6a2d35d4ff 100644
--- a/server/service/team_policies_test.go
+++ b/server/service/team_policies_test.go
@@ -149,7 +149,7 @@ func TestTeamPoliciesAuth(t *testing.T) {
})
checkAuthErr(t, tt.shouldFailWrite, err)
- _, _, err = svc.ListTeamPolicies(ctx, 1, fleet.ListOptions{}, fleet.ListOptions{})
+ _, _, err = svc.ListTeamPolicies(ctx, 1, fleet.ListOptions{}, fleet.ListOptions{}, false)
checkAuthErr(t, tt.shouldFailRead, err)
_, err = svc.GetTeamPolicyByIDQueries(ctx, 1, 1)
diff --git a/server/service/team_schedule.go b/server/service/team_schedule.go
index 24ad3cde3b..da31740a77 100644
--- a/server/service/team_schedule.go
+++ b/server/service/team_schedule.go
@@ -47,7 +47,7 @@ func (svc Service) GetTeamScheduledQueries(ctx context.Context, teamID uint, opt
if teamID != 0 {
teamID_ = &teamID
}
- queries, err := svc.ListQueries(ctx, opts, teamID_, ptr.Bool(true))
+ queries, err := svc.ListQueries(ctx, opts, teamID_, ptr.Bool(true), false)
if err != nil {
return nil, err
}
diff --git a/server/service/teams.go b/server/service/teams.go
index 992234a315..1d8a30032d 100644
--- a/server/service/teams.go
+++ b/server/service/teams.go
@@ -178,9 +178,10 @@ func (svc *Service) DeleteTeam(ctx context.Context, tid uint) error {
////////////////////////////////////////////////////////////////////////////////
type applyTeamSpecsRequest struct {
- Force bool `json:"-" query:"force,optional"` // if true, bypass strict incoming json validation
- DryRun bool `json:"-" query:"dry_run,optional"` // if true, apply validation but do not save changes
- Specs []*fleet.TeamSpec `json:"specs"`
+ Force bool `json:"-" query:"force,optional"` // if true, bypass strict incoming json validation
+ DryRun bool `json:"-" query:"dry_run,optional"` // if true, apply validation but do not save changes
+ DryRunAssumptions *fleet.TeamSpecsDryRunAssumptions `json:"dry_run_assumptions,omitempty"`
+ Specs []*fleet.TeamSpec `json:"specs"`
}
func (req *applyTeamSpecsRequest) DecodeBody(ctx context.Context, r io.Reader, u url.Values, c []*x509.Certificate) error {
@@ -224,6 +225,9 @@ func (r applyTeamSpecsResponse) error() error { return r.Err }
func applyTeamSpecsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
req := request.(*applyTeamSpecsRequest)
+ if !req.DryRun {
+ req.DryRunAssumptions = nil
+ }
// remove any nil spec (may happen in conversion from YAML to JSON with fleetctl, but also
// with the API should someone send such JSON)
@@ -236,17 +240,21 @@ func applyTeamSpecsEndpoint(ctx context.Context, request interface{}, svc fleet.
}
}
- idsByName, err := svc.ApplyTeamSpecs(ctx, actualSpecs, fleet.ApplySpecOptions{
- Force: req.Force,
- DryRun: req.DryRun,
- })
+ idsByName, err := svc.ApplyTeamSpecs(
+ ctx, actualSpecs, fleet.ApplyTeamSpecOptions{
+ ApplySpecOptions: fleet.ApplySpecOptions{
+ Force: req.Force,
+ DryRun: req.DryRun,
+ },
+ DryRunAssumptions: req.DryRunAssumptions,
+ })
if err != nil {
return applyTeamSpecsResponse{Err: err}, nil
}
return applyTeamSpecsResponse{TeamIDsByName: idsByName}, nil
}
-func (svc Service) ApplyTeamSpecs(ctx context.Context, specs []*fleet.TeamSpec, applyOpts fleet.ApplySpecOptions) (map[string]uint, error) {
+func (svc Service) ApplyTeamSpecs(ctx context.Context, _ []*fleet.TeamSpec, _ fleet.ApplyTeamSpecOptions) (map[string]uint, error) {
// skipauth: No authorization check needed due to implementation returning
// only license error.
svc.authz.SkipAuthorization(ctx)
diff --git a/server/service/teams_test.go b/server/service/teams_test.go
index 8177890ff0..136e396697 100644
--- a/server/service/teams_test.go
+++ b/server/service/teams_test.go
@@ -187,7 +187,7 @@ func TestTeamAuth(t *testing.T) {
_, err = svc.ModifyTeamEnrollSecrets(ctx, 1, []fleet.EnrollSecret{{Secret: "newteamsecret", CreatedAt: time.Now()}})
checkAuthErr(t, tt.shouldFailTeamSecretsWrite, err)
- _, err = svc.ApplyTeamSpecs(ctx, []*fleet.TeamSpec{{Name: "team1"}}, fleet.ApplySpecOptions{})
+ _, err = svc.ApplyTeamSpecs(ctx, []*fleet.TeamSpec{{Name: "team1"}}, fleet.ApplyTeamSpecOptions{})
checkAuthErr(t, tt.shouldFailTeamWrite, err)
})
}
@@ -281,7 +281,7 @@ func TestApplyTeamSpecs(t *testing.T) {
return nil
}
- _, err := svc.ApplyTeamSpecs(ctx, []*fleet.TeamSpec{{Name: "team1", Features: tt.spec}}, fleet.ApplySpecOptions{})
+ _, err := svc.ApplyTeamSpecs(ctx, []*fleet.TeamSpec{{Name: "team1", Features: tt.spec}}, fleet.ApplyTeamSpecOptions{})
require.NoError(t, err)
})
}
@@ -362,7 +362,9 @@ func TestApplyTeamSpecs(t *testing.T) {
return nil
}
- idsByTeam, err := svc.ApplyTeamSpecs(ctx, []*fleet.TeamSpec{{Name: "team1", Features: tt.spec}}, fleet.ApplySpecOptions{})
+ idsByTeam, err := svc.ApplyTeamSpecs(
+ ctx, []*fleet.TeamSpec{{Name: "team1", Features: tt.spec}}, fleet.ApplyTeamSpecOptions{},
+ )
require.NoError(t, err)
require.Len(t, idsByTeam, 1)
require.Equal(t, uint(123), idsByTeam["team1"])
@@ -398,7 +400,7 @@ func TestApplyTeamSpecEnrollSecretForNewTeams(t *testing.T) {
return &fleet.Team{ID: 1}, nil
}
- _, err := svc.ApplyTeamSpecs(ctx, []*fleet.TeamSpec{{Name: "Foo"}}, fleet.ApplySpecOptions{})
+ _, err := svc.ApplyTeamSpecs(ctx, []*fleet.TeamSpec{{Name: "Foo"}}, fleet.ApplyTeamSpecOptions{})
require.NoError(t, err)
require.True(t, ds.TeamByNameFuncInvoked)
require.True(t, ds.NewTeamFuncInvoked)
@@ -413,7 +415,9 @@ func TestApplyTeamSpecEnrollSecretForNewTeams(t *testing.T) {
return &fleet.Team{ID: 1}, nil
}
- _, err := svc.ApplyTeamSpecs(ctx, []*fleet.TeamSpec{{Name: "Foo", Secrets: []fleet.EnrollSecret{enrollSecret}}}, fleet.ApplySpecOptions{})
+ _, err := svc.ApplyTeamSpecs(
+ ctx, []*fleet.TeamSpec{{Name: "Foo", Secrets: []fleet.EnrollSecret{enrollSecret}}}, fleet.ApplyTeamSpecOptions{},
+ )
require.NoError(t, err)
require.True(t, ds.TeamByNameFuncInvoked)
require.True(t, ds.NewTeamFuncInvoked)
@@ -436,7 +440,7 @@ func TestApplyTeamSpecsErrorInTeamByName(t *testing.T) {
}
authzctx := &authz_ctx.AuthorizationContext{}
ctx = authz_ctx.NewContext(ctx, authzctx)
- _, err := svc.ApplyTeamSpecs(ctx, []*fleet.TeamSpec{{Name: "Foo"}}, fleet.ApplySpecOptions{})
+ _, err := svc.ApplyTeamSpecs(ctx, []*fleet.TeamSpec{{Name: "Foo"}}, fleet.ApplyTeamSpecOptions{})
require.Error(t, err)
az, ok := authz_ctx.FromContext(ctx)
require.True(t, ok)
diff --git a/server/vulnerabilities/msrc/parsed/product.go b/server/vulnerabilities/msrc/parsed/product.go
index af73081c1c..b6317abfca 100644
--- a/server/vulnerabilities/msrc/parsed/product.go
+++ b/server/vulnerabilities/msrc/parsed/product.go
@@ -38,12 +38,19 @@ func (p Products) GetMatchForOS(ctx context.Context, os fleet.OperatingSystem) (
break
}
- // Ensure a match against an unknown or blank os.DisplayVersion to
- // a MSRC product that does not have a display version (eg. The initial release
- // of Windows 11 is 21H2, which does not appear in the MSRC data)
+ // If os.DisplayVersion is empty, we need to confirm that the product
+ // matches the correct build number. This is necessary to avoid false
+ // positives when vulnerability scans have run before the host has been
+ // updated after an upgrade to fleet v4.44.0 or later
if !product.HasDisplayVersion() {
- noDvMatch = pID
- continue
+ var build string
+ parts := strings.Split(os.KernelVersion, ".")
+ if len(parts) > 3 {
+ build = parts[2]
+ }
+ if build == "22000" || build == "10240" {
+ noDvMatch = pID
+ }
}
}
diff --git a/server/vulnerabilities/msrc/parsed/product_test.go b/server/vulnerabilities/msrc/parsed/product_test.go
index 7986926378..13f7a4db49 100644
--- a/server/vulnerabilities/msrc/parsed/product_test.go
+++ b/server/vulnerabilities/msrc/parsed/product_test.go
@@ -470,6 +470,10 @@ func TestProductHasDisplayVersion(t *testing.T) {
}
var msrcWinProducts = Products{
+ "10729": "Windows 10 for 32-bit Systems",
+ "10735": "Windows 10 for x64-based Systems",
+ "10852": "Windows 10 Version 1607 for 32-bit Systems",
+ "10853": "Windows 10 Version 1607 for x64-based Systems",
"11926": "Windows 11 for x64-based Systems",
"11927": "Windows 11 for ARM64-based Systems",
"12085": "Windows 11 Version 22H2 for ARM64-based Systems",
@@ -492,7 +496,7 @@ func TestMatchesOperatingSystem(t *testing.T) {
{
name: "OS with known Display Version Match x64",
os: fleet.OperatingSystem{
- Name: "Windows 11 Pro 22H2",
+ Name: "Windows 11 Pro Version 22H2",
Arch: "x86_64",
DisplayVersion: "22H2",
},
@@ -502,7 +506,7 @@ func TestMatchesOperatingSystem(t *testing.T) {
{
name: "OS with known Display Version Match ARM64",
os: fleet.OperatingSystem{
- Name: "Windows 11 Pro 22H2",
+ Name: "Windows 11 Pro Version 22H2",
Arch: "ARM 64-bit Processor",
DisplayVersion: "22H2",
},
@@ -510,17 +514,48 @@ func TestMatchesOperatingSystem(t *testing.T) {
err: nil,
},
{
- name: "OS with no Display Version",
+ name: "Win 11 with no Display Version and matching build number",
os: fleet.OperatingSystem{
- Name: "Windows 11 Pro",
- Arch: "64-bit",
+ Name: "Windows 11 Pro",
+ Arch: "64-bit",
+ KernelVersion: "10.0.22000.795", // matches on build number for 22000 only
},
want: "11926",
+ err: nil,
+ },
+ {
+ name: "Win 11 with no Display Version with wrong build number",
+ os: fleet.OperatingSystem{
+ Name: "Windows 11 Pro",
+ Arch: "64-bit",
+ KernelVersion: "10.0.22631.795", // matches on build number for 22000 only
+ },
+ err: ErrNoMatch,
+ },
+ {
+ name: "Win 10 with no Display Version and matching build number",
+ os: fleet.OperatingSystem{
+ Name: "Windows 10 Pro",
+ Arch: "64-bit",
+ KernelVersion: "10.0.10240.795", // matches on build number for 10240 only
+ },
+ want: "10735",
+ err: nil,
+ },
+ {
+ name: "Win10 with no Display Version with wrong build number",
+ os: fleet.OperatingSystem{
+ Name: "Windows 10 Pro",
+ Arch: "64-bit",
+ KernelVersion: "10.0.19045.795", // matches on build number for 10240 only
+ },
+ want: "",
+ err: ErrNoMatch,
},
{
name: "Product contains 'Edition' keyword",
os: fleet.OperatingSystem{
- Name: "Windows Server 2022 23H2",
+ Name: "Windows Server 2022 Edition 23H2",
Arch: "64-bit",
DisplayVersion: "23H2",
},
diff --git a/server/vulnerabilities/oval/oval_platform.go b/server/vulnerabilities/oval/oval_platform.go
index 9238429f54..4b7c4a4923 100644
--- a/server/vulnerabilities/oval/oval_platform.go
+++ b/server/vulnerabilities/oval/oval_platform.go
@@ -80,6 +80,10 @@ func (op Platform) IsSupported() bool {
"ubuntu_2104",
"ubuntu_2110",
"ubuntu_2204",
+ "ubuntu_2210",
+ "ubuntu_2304",
+ "ubuntu_2310",
+ "ubuntu_2404",
"rhel_05",
"rhel_06",
"rhel_07",
diff --git a/terraform/README.md b/terraform/README.md
index cb1fd829a1..4ec9a70a0d 100644
--- a/terraform/README.md
+++ b/terraform/README.md
@@ -75,7 +75,7 @@ No resources.
| [alb\_config](#input\_alb\_config) | n/a | object({
name = optional(string, "fleet")
security_groups = optional(list(string), [])
access_logs = optional(map(string), {})
allowed_cidrs = optional(list(string), ["0.0.0.0/0"])
allowed_ipv6_cidrs = optional(list(string), ["::/0"])
egress_cidrs = optional(list(string), ["0.0.0.0/0"])
egress_ipv6_cidrs = optional(list(string), ["::/0"])
extra_target_groups = optional(any, [])
https_listener_rules = optional(any, [])
tls_policy = optional(string, "ELBSecurityPolicy-TLS-1-2-2017-01")
idle_timeout = optional(number, 60)
}) | `{}` | no |
| [certificate\_arn](#input\_certificate\_arn) | n/a | `string` | n/a | yes |
| [ecs\_cluster](#input\_ecs\_cluster) | The config for the terraform-aws-modules/ecs/aws module | object({
autoscaling_capacity_providers = optional(any, {})
cluster_configuration = optional(any, {
execute_command_configuration = {
logging = "OVERRIDE"
log_configuration = {
cloud_watch_log_group_name = "/aws/ecs/aws-ec2"
}
}
})
cluster_name = optional(string, "fleet")
cluster_settings = optional(map(string), {
"name" : "containerInsights",
"value" : "enabled",
})
create = optional(bool, true)
default_capacity_provider_use_fargate = optional(bool, true)
fargate_capacity_providers = optional(any, {
FARGATE = {
default_capacity_provider_strategy = {
weight = 100
}
}
FARGATE_SPOT = {
default_capacity_provider_strategy = {
weight = 0
}
}
})
tags = optional(map(string))
}) | {
"autoscaling_capacity_providers": {},
"cluster_configuration": {
"execute_command_configuration": {
"log_configuration": {
"cloud_watch_log_group_name": "/aws/ecs/aws-ec2"
},
"logging": "OVERRIDE"
}
},
"cluster_name": "fleet",
"cluster_settings": {
"name": "containerInsights",
"value": "enabled"
},
"create": true,
"default_capacity_provider_use_fargate": true,
"fargate_capacity_providers": {
"FARGATE": {
"default_capacity_provider_strategy": {
"weight": 100
}
},
"FARGATE_SPOT": {
"default_capacity_provider_strategy": {
"weight": 0
}
}
},
"tags": {}
} | no |
-| [fleet\_config](#input\_fleet\_config) | The configuration object for Fleet itself. Fields that default to null will have their respective resources created if not specified. | object({
mem = optional(number, 4096)
cpu = optional(number, 512)
image = optional(string, "fleetdm/fleet:v4.49.1")
family = optional(string, "fleet")
sidecars = optional(list(any), [])
depends_on = optional(list(any), [])
mount_points = optional(list(any), [])
volumes = optional(list(any), [])
extra_environment_variables = optional(map(string), {})
extra_iam_policies = optional(list(string), [])
extra_execution_iam_policies = optional(list(string), [])
extra_secrets = optional(map(string), {})
security_groups = optional(list(string), null)
security_group_name = optional(string, "fleet")
iam_role_arn = optional(string, null)
repository_credentials = optional(string, "")
service = optional(object({
name = optional(string, "fleet")
}), {
name = "fleet"
})
database = optional(object({
password_secret_arn = string
user = string
database = string
address = string
rr_address = optional(string, null)
}), {
password_secret_arn = null
user = null
database = null
address = null
rr_address = null
})
redis = optional(object({
address = string
use_tls = optional(bool, true)
}), {
address = null
use_tls = true
})
awslogs = optional(object({
name = optional(string, null)
region = optional(string, null)
create = optional(bool, true)
prefix = optional(string, "fleet")
retention = optional(number, 5)
}), {
name = null
region = null
prefix = "fleet"
retention = 5
})
loadbalancer = optional(object({
arn = string
}), {
arn = null
})
extra_load_balancers = optional(list(any), [])
networking = optional(object({
subnets = list(string)
security_groups = optional(list(string), null)
}), {
subnets = null
security_groups = null
})
autoscaling = optional(object({
max_capacity = optional(number, 5)
min_capacity = optional(number, 1)
memory_tracking_target_value = optional(number, 80)
cpu_tracking_target_value = optional(number, 80)
}), {
max_capacity = 5
min_capacity = 1
memory_tracking_target_value = 80
cpu_tracking_target_value = 80
})
iam = optional(object({
role = optional(object({
name = optional(string, "fleet-role")
policy_name = optional(string, "fleet-iam-policy")
}), {
name = "fleet-role"
policy_name = "fleet-iam-policy"
})
execution = optional(object({
name = optional(string, "fleet-execution-role")
policy_name = optional(string, "fleet-execution-role")
}), {
name = "fleet-execution-role"
policy_name = "fleet-iam-policy-execution"
})
}), {
name = "fleetdm-execution-role"
})
}) | {
"autoscaling": {
"cpu_tracking_target_value": 80,
"max_capacity": 5,
"memory_tracking_target_value": 80,
"min_capacity": 1
},
"awslogs": {
"create": true,
"name": null,
"prefix": "fleet",
"region": null,
"retention": 5
},
"cpu": 256,
"database": {
"address": null,
"database": null,
"password_secret_arn": null,
"rr_address": null,
"user": null
},
"depends_on": [],
"extra_environment_variables": {},
"extra_execution_iam_policies": [],
"extra_iam_policies": [],
"extra_load_balancers": [],
"extra_secrets": {},
"family": "fleet",
"iam": {
"execution": {
"name": "fleet-execution-role",
"policy_name": "fleet-iam-policy-execution"
},
"role": {
"name": "fleet-role",
"policy_name": "fleet-iam-policy"
}
},
"iam_role_arn": null,
"image": "fleetdm/fleet:v4.31.1",
"loadbalancer": {
"arn": null
},
"mem": 512,
"mount_points": [],
"networking": {
"security_groups": null,
"subnets": null
},
"redis": {
"address": null,
"use_tls": true
},
"repository_credentials": "",
"security_group_name": "fleet",
"security_groups": null,
"service": {
"name": "fleet"
},
"sidecars": [],
"volumes": []
} | no |
+| [fleet\_config](#input\_fleet\_config) | The configuration object for Fleet itself. Fields that default to null will have their respective resources created if not specified. | object({
mem = optional(number, 4096)
cpu = optional(number, 512)
image = optional(string, "fleetdm/fleet:v4.49.2")
family = optional(string, "fleet")
sidecars = optional(list(any), [])
depends_on = optional(list(any), [])
mount_points = optional(list(any), [])
volumes = optional(list(any), [])
extra_environment_variables = optional(map(string), {})
extra_iam_policies = optional(list(string), [])
extra_execution_iam_policies = optional(list(string), [])
extra_secrets = optional(map(string), {})
security_groups = optional(list(string), null)
security_group_name = optional(string, "fleet")
iam_role_arn = optional(string, null)
repository_credentials = optional(string, "")
service = optional(object({
name = optional(string, "fleet")
}), {
name = "fleet"
})
database = optional(object({
password_secret_arn = string
user = string
database = string
address = string
rr_address = optional(string, null)
}), {
password_secret_arn = null
user = null
database = null
address = null
rr_address = null
})
redis = optional(object({
address = string
use_tls = optional(bool, true)
}), {
address = null
use_tls = true
})
awslogs = optional(object({
name = optional(string, null)
region = optional(string, null)
create = optional(bool, true)
prefix = optional(string, "fleet")
retention = optional(number, 5)
}), {
name = null
region = null
prefix = "fleet"
retention = 5
})
loadbalancer = optional(object({
arn = string
}), {
arn = null
})
extra_load_balancers = optional(list(any), [])
networking = optional(object({
subnets = list(string)
security_groups = optional(list(string), null)
}), {
subnets = null
security_groups = null
})
autoscaling = optional(object({
max_capacity = optional(number, 5)
min_capacity = optional(number, 1)
memory_tracking_target_value = optional(number, 80)
cpu_tracking_target_value = optional(number, 80)
}), {
max_capacity = 5
min_capacity = 1
memory_tracking_target_value = 80
cpu_tracking_target_value = 80
})
iam = optional(object({
role = optional(object({
name = optional(string, "fleet-role")
policy_name = optional(string, "fleet-iam-policy")
}), {
name = "fleet-role"
policy_name = "fleet-iam-policy"
})
execution = optional(object({
name = optional(string, "fleet-execution-role")
policy_name = optional(string, "fleet-execution-role")
}), {
name = "fleet-execution-role"
policy_name = "fleet-iam-policy-execution"
})
}), {
name = "fleetdm-execution-role"
})
}) | {
"autoscaling": {
"cpu_tracking_target_value": 80,
"max_capacity": 5,
"memory_tracking_target_value": 80,
"min_capacity": 1
},
"awslogs": {
"create": true,
"name": null,
"prefix": "fleet",
"region": null,
"retention": 5
},
"cpu": 256,
"database": {
"address": null,
"database": null,
"password_secret_arn": null,
"rr_address": null,
"user": null
},
"depends_on": [],
"extra_environment_variables": {},
"extra_execution_iam_policies": [],
"extra_iam_policies": [],
"extra_load_balancers": [],
"extra_secrets": {},
"family": "fleet",
"iam": {
"execution": {
"name": "fleet-execution-role",
"policy_name": "fleet-iam-policy-execution"
},
"role": {
"name": "fleet-role",
"policy_name": "fleet-iam-policy"
}
},
"iam_role_arn": null,
"image": "fleetdm/fleet:v4.31.1",
"loadbalancer": {
"arn": null
},
"mem": 512,
"mount_points": [],
"networking": {
"security_groups": null,
"subnets": null
},
"redis": {
"address": null,
"use_tls": true
},
"repository_credentials": "",
"security_group_name": "fleet",
"security_groups": null,
"service": {
"name": "fleet"
},
"sidecars": [],
"volumes": []
} | no |
| [migration\_config](#input\_migration\_config) | The configuration object for Fleet's migration task. | object({
mem = number
cpu = number
}) | {
"cpu": 1024,
"mem": 2048
} | no |
| [rds\_config](#input\_rds\_config) | The config for the terraform-aws-modules/rds-aurora/aws module | object({
name = optional(string, "fleet")
engine_version = optional(string, "8.0.mysql_aurora.3.04.2")
instance_class = optional(string, "db.t4g.large")
subnets = optional(list(string), [])
allowed_security_groups = optional(list(string), [])
allowed_cidr_blocks = optional(list(string), [])
apply_immediately = optional(bool, true)
monitoring_interval = optional(number, 10)
db_parameter_group_name = optional(string)
db_parameters = optional(map(string), {})
db_cluster_parameter_group_name = optional(string)
db_cluster_parameters = optional(map(string), {})
enabled_cloudwatch_logs_exports = optional(list(string), [])
master_username = optional(string, "fleet")
snapshot_identifier = optional(string)
cluster_tags = optional(map(string), {})
}) | {
"allowed_cidr_blocks": [],
"allowed_security_groups": [],
"apply_immediately": true,
"cluster_tags": {},
"db_cluster_parameter_group_name": null,
"db_cluster_parameters": {},
"db_parameter_group_name": null,
"db_parameters": {},
"enabled_cloudwatch_logs_exports": [],
"engine_version": "8.0.mysql_aurora.3.04.2",
"instance_class": "db.t4g.large",
"master_username": "fleet",
"monitoring_interval": 10,
"name": "fleet",
"snapshot_identifier": null,
"subnets": []
} | no |
| [redis\_config](#input\_redis\_config) | n/a | object({
name = optional(string, "fleet")
replication_group_id = optional(string)
elasticache_subnet_group_name = optional(string)
allowed_security_group_ids = optional(list(string), [])
subnets = optional(list(string))
availability_zones = optional(list(string))
cluster_size = optional(number, 3)
instance_type = optional(string, "cache.m5.large")
apply_immediately = optional(bool, true)
automatic_failover_enabled = optional(bool, false)
engine_version = optional(string, "6.x")
family = optional(string, "redis6.x")
at_rest_encryption_enabled = optional(bool, true)
transit_encryption_enabled = optional(bool, true)
parameter = optional(list(object({
name = string
value = string
})), [])
log_delivery_configuration = optional(list(map(any)), [])
tags = optional(map(string), {})
}) | {
"allowed_security_group_ids": [],
"apply_immediately": true,
"at_rest_encryption_enabled": true,
"automatic_failover_enabled": false,
"availability_zones": null,
"cluster_size": 3,
"elasticache_subnet_group_name": null,
"engine_version": "6.x",
"family": "redis6.x",
"instance_type": "cache.m5.large",
"log_delivery_configuration": [],
"name": "fleet",
"parameter": [],
"replication_group_id": null,
"subnets": null,
"tags": {},
"transit_encryption_enabled": true
} | no |
diff --git a/terraform/addons/monitoring/README.md b/terraform/addons/monitoring/README.md
index b35a415e1d..5ee696f9ca 100644
--- a/terraform/addons/monitoring/README.md
+++ b/terraform/addons/monitoring/README.md
@@ -138,7 +138,7 @@ No modules.
| Name | Description | Type | Default | Required |
|------|-------------|------|---------|:--------:|
| [acm\_certificate\_arn](#input\_acm\_certificate\_arn) | n/a | `string` | `null` | no |
-| [albs](#input\_albs) | n/a | list(object({
name = string
arn_suffix = string
target_group_name = string
target_group_arn_suffix = string
min_containers = optional(string, 1)
ecs_service_name = string
})) | `[]` | no |
+| [albs](#input\_albs) | n/a | list(object({
name = string
arn_suffix = string
target_group_name = string
target_group_arn_suffix = string
min_containers = optional(string, 1)
ecs_service_name = string
alert_thresholds = optional(
object({
HTTPCode_ELB_5XX_Count = object({
period = number
threshold = number
})
HTTPCode_Target_5XX_Count = object({
period = number
threshold = number
})
}),
{
HTTPCode_ELB_5XX_Count = {
period = 120
threshold = 0
},
HTTPCode_Target_5XX_Count = {
period = 120
threshold = 0
}
}
)
})) | `[]` | no |
| [cron\_monitoring](#input\_cron\_monitoring) | n/a | object({
mysql_host = string
mysql_database = string
mysql_user = string
mysql_password_secret_name = string
vpc_id = string
subnet_ids = list(string)
rds_security_group_id = string
delay_tolerance = string
run_interval = string
log_retention_in_days = optional(number, 7)
}) | `null` | no |
| [customer\_prefix](#input\_customer\_prefix) | n/a | `string` | `"fleet"` | no |
| [default\_sns\_topic\_arns](#input\_default\_sns\_topic\_arns) | n/a | `list(string)` | `[]` | no |
diff --git a/terraform/addons/monitoring/main.tf b/terraform/addons/monitoring/main.tf
index 4af90e5ca2..09dff06488 100644
--- a/terraform/addons/monitoring/main.tf
+++ b/terraform/addons/monitoring/main.tf
@@ -37,7 +37,7 @@ resource "aws_db_event_subscription" "default" {
}
locals {
- alb_map = {for k, v in var.albs: k => v}
+ alb_map = { for k, v in var.albs : k => v }
}
@@ -102,7 +102,7 @@ resource "aws_cloudwatch_metric_alarm" "target_response_time" {
locals {
http_5xx_alert_names = ["HTTPCode_ELB_5XX_Count", "HTTPCode_Target_5XX_Count"]
http_5xx_alerts_list = flatten([for alert in local.http_5xx_alert_names : [for alb in var.albs : merge(alb, { "alert" : alert })]])
- http_5xx_alerts = {for k, v in local.http_5xx_alerts_list : k => v}
+ http_5xx_alerts = { for k, v in local.http_5xx_alerts_list : k => v }
}
@@ -113,9 +113,9 @@ resource "aws_cloudwatch_metric_alarm" "lb" {
evaluation_periods = "1"
metric_name = each.value.alert
namespace = "AWS/ApplicationELB"
- period = "120"
+ period = each.value.alert_thresholds[each.value.alert].period
statistic = "Sum"
- threshold = "0"
+ threshold = each.value.alert_thresholds[each.value.alert].threshold
alarm_description = "This alarm indicates there are an abnormal amount of 5XX responses. Either the lb cannot talk with the Fleet backend target or Fleet is returning an error."
alarm_actions = lookup(var.sns_topic_arns_map, "alb_httpcode_5xx", var.default_sns_topic_arns)
ok_actions = lookup(var.sns_topic_arns_map, "alb_httpcode_5xx", var.default_sns_topic_arns)
diff --git a/terraform/addons/monitoring/variables.tf b/terraform/addons/monitoring/variables.tf
index 365455beec..4bd494e870 100644
--- a/terraform/addons/monitoring/variables.tf
+++ b/terraform/addons/monitoring/variables.tf
@@ -16,10 +16,33 @@ variable "albs" {
target_group_arn_suffix = string
min_containers = optional(string, 1)
ecs_service_name = string
+ alert_thresholds = optional(
+ object({
+ HTTPCode_ELB_5XX_Count = object({
+ period = number
+ threshold = number
+ })
+ HTTPCode_Target_5XX_Count = object({
+ period = number
+ threshold = number
+ })
+ }),
+ {
+ HTTPCode_ELB_5XX_Count = {
+ period = 120
+ threshold = 0
+ },
+ HTTPCode_Target_5XX_Count = {
+ period = 120
+ threshold = 0
+ }
+ }
+ )
}))
default = []
}
+
variable "default_sns_topic_arns" {
type = list(string)
default = []
diff --git a/terraform/byo-vpc/README.md b/terraform/byo-vpc/README.md
index 89aa013d1f..7019d0720b 100644
--- a/terraform/byo-vpc/README.md
+++ b/terraform/byo-vpc/README.md
@@ -34,7 +34,7 @@ No requirements.
| [alb\_config](#input\_alb\_config) | n/a | object({
name = optional(string, "fleet")
subnets = list(string)
security_groups = optional(list(string), [])
access_logs = optional(map(string), {})
certificate_arn = string
allowed_cidrs = optional(list(string), ["0.0.0.0/0"])
allowed_ipv6_cidrs = optional(list(string), ["::/0"])
egress_cidrs = optional(list(string), ["0.0.0.0/0"])
egress_ipv6_cidrs = optional(list(string), ["::/0"])
extra_target_groups = optional(any, [])
https_listener_rules = optional(any, [])
tls_policy = optional(string, "ELBSecurityPolicy-TLS-1-2-2017-01")
idle_timeout = optional(number, 60)
}) | n/a | yes |
| [ecs\_cluster](#input\_ecs\_cluster) | The config for the terraform-aws-modules/ecs/aws module | object({
autoscaling_capacity_providers = optional(any, {})
cluster_configuration = optional(any, {
execute_command_configuration = {
logging = "OVERRIDE"
log_configuration = {
cloud_watch_log_group_name = "/aws/ecs/aws-ec2"
}
}
})
cluster_name = optional(string, "fleet")
cluster_settings = optional(map(string), {
"name" : "containerInsights",
"value" : "enabled",
})
create = optional(bool, true)
default_capacity_provider_use_fargate = optional(bool, true)
fargate_capacity_providers = optional(any, {
FARGATE = {
default_capacity_provider_strategy = {
weight = 100
}
}
FARGATE_SPOT = {
default_capacity_provider_strategy = {
weight = 0
}
}
})
tags = optional(map(string))
}) | {
"autoscaling_capacity_providers": {},
"cluster_configuration": {
"execute_command_configuration": {
"log_configuration": {
"cloud_watch_log_group_name": "/aws/ecs/aws-ec2"
},
"logging": "OVERRIDE"
}
},
"cluster_name": "fleet",
"cluster_settings": {
"name": "containerInsights",
"value": "enabled"
},
"create": true,
"default_capacity_provider_use_fargate": true,
"fargate_capacity_providers": {
"FARGATE": {
"default_capacity_provider_strategy": {
"weight": 100
}
},
"FARGATE_SPOT": {
"default_capacity_provider_strategy": {
"weight": 0
}
}
},
"tags": {}
} | no |
<<<<<<< HEAD
-| [fleet\_config](#input\_fleet\_config) | The configuration object for Fleet itself. Fields that default to null will have their respective resources created if not specified. | object({
mem = optional(number, 4096)
cpu = optional(number, 512)
image = optional(string, "fleetdm/fleet:v4.49.1")
family = optional(string, "fleet")
sidecars = optional(list(any), [])
depends_on = optional(list(any), [])
mount_points = optional(list(any), [])
volumes = optional(list(any), [])
extra_environment_variables = optional(map(string), {})
extra_iam_policies = optional(list(string), [])
extra_execution_iam_policies = optional(list(string), [])
extra_secrets = optional(map(string), {})
security_groups = optional(list(string), null)
security_group_name = optional(string, "fleet")
iam_role_arn = optional(string, null)
service = optional(object({
name = optional(string, "fleet")
}), {
name = "fleet"
})
database = optional(object({
password_secret_arn = string
user = string
database = string
address = string
rr_address = optional(string, null)
}), {
password_secret_arn = null
user = null
database = null
address = null
rr_address = null
})
redis = optional(object({
address = string
use_tls = optional(bool, true)
}), {
address = null
use_tls = true
})
awslogs = optional(object({
name = optional(string, null)
region = optional(string, null)
create = optional(bool, true)
prefix = optional(string, "fleet")
retention = optional(number, 5)
}), {
name = null
region = null
prefix = "fleet"
retention = 5
})
loadbalancer = optional(object({
arn = string
}), {
arn = null
})
extra_load_balancers = optional(list(any), [])
networking = optional(object({
subnets = list(string)
security_groups = optional(list(string), null)
}), {
subnets = null
security_groups = null
})
autoscaling = optional(object({
max_capacity = optional(number, 5)
min_capacity = optional(number, 1)
memory_tracking_target_value = optional(number, 80)
cpu_tracking_target_value = optional(number, 80)
}), {
max_capacity = 5
min_capacity = 1
memory_tracking_target_value = 80
cpu_tracking_target_value = 80
})
iam = optional(object({
role = optional(object({
name = optional(string, "fleet-role")
policy_name = optional(string, "fleet-iam-policy")
}), {
name = "fleet-role"
policy_name = "fleet-iam-policy"
})
execution = optional(object({
name = optional(string, "fleet-execution-role")
policy_name = optional(string, "fleet-execution-role")
}), {
name = "fleet-execution-role"
policy_name = "fleet-iam-policy-execution"
})
}), {
name = "fleetdm-execution-role"
})
}) | {
"autoscaling": {
"cpu_tracking_target_value": 80,
"max_capacity": 5,
"memory_tracking_target_value": 80,
"min_capacity": 1
},
"awslogs": {
"create": true,
"name": null,
"prefix": "fleet",
"region": null,
"retention": 5
},
"cpu": 256,
"database": {
"address": null,
"database": null,
"password_secret_arn": null,
"rr_address": null,
"user": null
},
"depends_on": [],
"extra_environment_variables": {},
"extra_execution_iam_policies": [],
"extra_iam_policies": [],
"extra_load_balancers": [],
"extra_secrets": {},
"family": "fleet",
"iam": {
"execution": {
"name": "fleet-execution-role",
"policy_name": "fleet-iam-policy-execution"
},
"role": {
"name": "fleet-role",
"policy_name": "fleet-iam-policy"
}
},
"iam_role_arn": null,
"image": "fleetdm/fleet:v4.31.1",
"loadbalancer": {
"arn": null
},
"mem": 512,
"mount_points": [],
"networking": {
"security_groups": null,
"subnets": null
},
"redis": {
"address": null,
"use_tls": true
},
"security_group_name": "fleet",
"security_groups": null,
"service": {
"name": "fleet"
},
"sidecars": [],
"volumes": []
} | no |
+| [fleet\_config](#input\_fleet\_config) | The configuration object for Fleet itself. Fields that default to null will have their respective resources created if not specified. | object({
mem = optional(number, 4096)
cpu = optional(number, 512)
image = optional(string, "fleetdm/fleet:v4.49.2")
family = optional(string, "fleet")
sidecars = optional(list(any), [])
depends_on = optional(list(any), [])
mount_points = optional(list(any), [])
volumes = optional(list(any), [])
extra_environment_variables = optional(map(string), {})
extra_iam_policies = optional(list(string), [])
extra_execution_iam_policies = optional(list(string), [])
extra_secrets = optional(map(string), {})
security_groups = optional(list(string), null)
security_group_name = optional(string, "fleet")
iam_role_arn = optional(string, null)
service = optional(object({
name = optional(string, "fleet")
}), {
name = "fleet"
})
database = optional(object({
password_secret_arn = string
user = string
database = string
address = string
rr_address = optional(string, null)
}), {
password_secret_arn = null
user = null
database = null
address = null
rr_address = null
})
redis = optional(object({
address = string
use_tls = optional(bool, true)
}), {
address = null
use_tls = true
})
awslogs = optional(object({
name = optional(string, null)
region = optional(string, null)
create = optional(bool, true)
prefix = optional(string, "fleet")
retention = optional(number, 5)
}), {
name = null
region = null
prefix = "fleet"
retention = 5
})
loadbalancer = optional(object({
arn = string
}), {
arn = null
})
extra_load_balancers = optional(list(any), [])
networking = optional(object({
subnets = list(string)
security_groups = optional(list(string), null)
}), {
subnets = null
security_groups = null
})
autoscaling = optional(object({
max_capacity = optional(number, 5)
min_capacity = optional(number, 1)
memory_tracking_target_value = optional(number, 80)
cpu_tracking_target_value = optional(number, 80)
}), {
max_capacity = 5
min_capacity = 1
memory_tracking_target_value = 80
cpu_tracking_target_value = 80
})
iam = optional(object({
role = optional(object({
name = optional(string, "fleet-role")
policy_name = optional(string, "fleet-iam-policy")
}), {
name = "fleet-role"
policy_name = "fleet-iam-policy"
})
execution = optional(object({
name = optional(string, "fleet-execution-role")
policy_name = optional(string, "fleet-execution-role")
}), {
name = "fleet-execution-role"
policy_name = "fleet-iam-policy-execution"
})
}), {
name = "fleetdm-execution-role"
})
}) | {
"autoscaling": {
"cpu_tracking_target_value": 80,
"max_capacity": 5,
"memory_tracking_target_value": 80,
"min_capacity": 1
},
"awslogs": {
"create": true,
"name": null,
"prefix": "fleet",
"region": null,
"retention": 5
},
"cpu": 256,
"database": {
"address": null,
"database": null,
"password_secret_arn": null,
"rr_address": null,
"user": null
},
"depends_on": [],
"extra_environment_variables": {},
"extra_execution_iam_policies": [],
"extra_iam_policies": [],
"extra_load_balancers": [],
"extra_secrets": {},
"family": "fleet",
"iam": {
"execution": {
"name": "fleet-execution-role",
"policy_name": "fleet-iam-policy-execution"
},
"role": {
"name": "fleet-role",
"policy_name": "fleet-iam-policy"
}
},
"iam_role_arn": null,
"image": "fleetdm/fleet:v4.31.1",
"loadbalancer": {
"arn": null
},
"mem": 512,
"mount_points": [],
"networking": {
"security_groups": null,
"subnets": null
},
"redis": {
"address": null,
"use_tls": true
},
"security_group_name": "fleet",
"security_groups": null,
"service": {
"name": "fleet"
},
"sidecars": [],
"volumes": []
} | no |
=======
| [fleet\_config](#input\_fleet\_config) | The configuration object for Fleet itself. Fields that default to null will have their respective resources created if not specified. | object({
mem = optional(number, 4096)
cpu = optional(number, 512)
image = optional(string, "fleetdm/fleet:v4.48.0")
family = optional(string, "fleet")
sidecars = optional(list(any), [])
depends_on = optional(list(any), [])
mount_points = optional(list(any), [])
volumes = optional(list(any), [])
extra_environment_variables = optional(map(string), {})
extra_iam_policies = optional(list(string), [])
extra_execution_iam_policies = optional(list(string), [])
extra_secrets = optional(map(string), {})
security_groups = optional(list(string), null)
security_group_name = optional(string, "fleet")
iam_role_arn = optional(string, null)
repository_credentials = optional(string, "")
service = optional(object({
name = optional(string, "fleet")
}), {
name = "fleet"
})
database = optional(object({
password_secret_arn = string
user = string
database = string
address = string
rr_address = optional(string, null)
}), {
password_secret_arn = null
user = null
database = null
address = null
rr_address = null
})
redis = optional(object({
address = string
use_tls = optional(bool, true)
}), {
address = null
use_tls = true
})
awslogs = optional(object({
name = optional(string, null)
region = optional(string, null)
create = optional(bool, true)
prefix = optional(string, "fleet")
retention = optional(number, 5)
}), {
name = null
region = null
prefix = "fleet"
retention = 5
})
loadbalancer = optional(object({
arn = string
}), {
arn = null
})
extra_load_balancers = optional(list(any), [])
networking = optional(object({
subnets = list(string)
security_groups = optional(list(string), null)
}), {
subnets = null
security_groups = null
})
autoscaling = optional(object({
max_capacity = optional(number, 5)
min_capacity = optional(number, 1)
memory_tracking_target_value = optional(number, 80)
cpu_tracking_target_value = optional(number, 80)
}), {
max_capacity = 5
min_capacity = 1
memory_tracking_target_value = 80
cpu_tracking_target_value = 80
})
iam = optional(object({
role = optional(object({
name = optional(string, "fleet-role")
policy_name = optional(string, "fleet-iam-policy")
}), {
name = "fleet-role"
policy_name = "fleet-iam-policy"
})
execution = optional(object({
name = optional(string, "fleet-execution-role")
policy_name = optional(string, "fleet-execution-role")
}), {
name = "fleet-execution-role"
policy_name = "fleet-iam-policy-execution"
})
}), {
name = "fleetdm-execution-role"
})
}) | {
"autoscaling": {
"cpu_tracking_target_value": 80,
"max_capacity": 5,
"memory_tracking_target_value": 80,
"min_capacity": 1
},
"awslogs": {
"create": true,
"name": null,
"prefix": "fleet",
"region": null,
"retention": 5
},
"cpu": 256,
"database": {
"address": null,
"database": null,
"password_secret_arn": null,
"rr_address": null,
"user": null
},
"depends_on": [],
"extra_environment_variables": {},
"extra_execution_iam_policies": [],
"extra_iam_policies": [],
"extra_load_balancers": [],
"extra_secrets": {},
"family": "fleet",
"iam": {
"execution": {
"name": "fleet-execution-role",
"policy_name": "fleet-iam-policy-execution"
},
"role": {
"name": "fleet-role",
"policy_name": "fleet-iam-policy"
}
},
"iam_role_arn": null,
"image": "fleetdm/fleet:v4.31.1",
"loadbalancer": {
"arn": null
},
"mem": 512,
"mount_points": [],
"networking": {
"security_groups": null,
"subnets": null
},
"redis": {
"address": null,
"use_tls": true
},
"repository_credentials": "",
"security_group_name": "fleet",
"security_groups": null,
"service": {
"name": "fleet"
},
"sidecars": [],
"volumes": []
} | no |
>>>>>>> 025004bcf (support private registry in the ecs task definition)
diff --git a/terraform/byo-vpc/byo-db/byo-ecs/variables.tf b/terraform/byo-vpc/byo-db/byo-ecs/variables.tf
index 3e9bd4507a..e7550b8e6f 100644
--- a/terraform/byo-vpc/byo-db/byo-ecs/variables.tf
+++ b/terraform/byo-vpc/byo-db/byo-ecs/variables.tf
@@ -13,7 +13,7 @@ variable "fleet_config" {
type = object({
mem = optional(number, 4096)
cpu = optional(number, 512)
- image = optional(string, "fleetdm/fleet:v4.49.1")
+ image = optional(string, "fleetdm/fleet:v4.49.2")
family = optional(string, "fleet")
sidecars = optional(list(any), [])
depends_on = optional(list(any), [])
diff --git a/terraform/byo-vpc/byo-db/variables.tf b/terraform/byo-vpc/byo-db/variables.tf
index 194a11aef7..c2a6f83d8b 100644
--- a/terraform/byo-vpc/byo-db/variables.tf
+++ b/terraform/byo-vpc/byo-db/variables.tf
@@ -74,7 +74,7 @@ variable "fleet_config" {
type = object({
mem = optional(number, 4096)
cpu = optional(number, 512)
- image = optional(string, "fleetdm/fleet:v4.49.1")
+ image = optional(string, "fleetdm/fleet:v4.49.2")
family = optional(string, "fleet")
sidecars = optional(list(any), [])
depends_on = optional(list(any), [])
diff --git a/terraform/byo-vpc/example/main.tf b/terraform/byo-vpc/example/main.tf
index aae37cc107..545b5e6bad 100644
--- a/terraform/byo-vpc/example/main.tf
+++ b/terraform/byo-vpc/example/main.tf
@@ -17,7 +17,7 @@ provider "aws" {
}
locals {
- fleet_image = "fleetdm/fleet:v4.49.1"
+ fleet_image = "fleetdm/fleet:v4.49.2"
domain_name = "example.com"
}
diff --git a/terraform/byo-vpc/variables.tf b/terraform/byo-vpc/variables.tf
index e9463dda11..09f71bcdb6 100644
--- a/terraform/byo-vpc/variables.tf
+++ b/terraform/byo-vpc/variables.tf
@@ -167,7 +167,7 @@ variable "fleet_config" {
type = object({
mem = optional(number, 4096)
cpu = optional(number, 512)
- image = optional(string, "fleetdm/fleet:v4.49.1")
+ image = optional(string, "fleetdm/fleet:v4.49.2")
family = optional(string, "fleet")
sidecars = optional(list(any), [])
depends_on = optional(list(any), [])
diff --git a/terraform/example/main.tf b/terraform/example/main.tf
index 4a245a7969..5bba54de5c 100644
--- a/terraform/example/main.tf
+++ b/terraform/example/main.tf
@@ -59,8 +59,8 @@ module "fleet" {
fleet_config = {
# To avoid pull-rate limiting from dockerhub, consider using our quay.io mirror
- # for the Fleet image. e.g. "quay.io/fleetdm/fleet:v4.49.1"
- image = "fleetdm/fleet:v4.49.1" # override default to deploy the image you desire
+ # for the Fleet image. e.g. "quay.io/fleetdm/fleet:v4.49.2"
+ image = "fleetdm/fleet:v4.49.2" # override default to deploy the image you desire
# See https://fleetdm.com/docs/deploy/reference-architectures#aws for appropriate scaling
# memory and cpu.
autoscaling = {
diff --git a/terraform/variables.tf b/terraform/variables.tf
index d12ca9e1c7..65a182758e 100644
--- a/terraform/variables.tf
+++ b/terraform/variables.tf
@@ -215,7 +215,7 @@ variable "fleet_config" {
type = object({
mem = optional(number, 4096)
cpu = optional(number, 512)
- image = optional(string, "fleetdm/fleet:v4.49.1")
+ image = optional(string, "fleetdm/fleet:v4.49.2")
family = optional(string, "fleet")
sidecars = optional(list(any), [])
depends_on = optional(list(any), [])
diff --git a/tools/cloner-check/generated_files/appconfig.txt b/tools/cloner-check/generated_files/appconfig.txt
index f3b6c8df0f..f07c2831dd 100644
--- a/tools/cloner-check/generated_files/appconfig.txt
+++ b/tools/cloner-check/generated_files/appconfig.txt
@@ -11,6 +11,7 @@ github.com/fleetdm/fleet/v4/server/fleet/ServerSettings DebugHostIDs []uint
github.com/fleetdm/fleet/v4/server/fleet/ServerSettings DeferredSaveHost bool
github.com/fleetdm/fleet/v4/server/fleet/ServerSettings QueryReportsDisabled bool
github.com/fleetdm/fleet/v4/server/fleet/ServerSettings ScriptsDisabled bool
+github.com/fleetdm/fleet/v4/server/fleet/ServerSettings AIFeaturesDisabled bool
github.com/fleetdm/fleet/v4/server/fleet/AppConfig SMTPSettings *fleet.SMTPSettings
github.com/fleetdm/fleet/v4/server/fleet/SMTPSettings SMTPEnabled bool
github.com/fleetdm/fleet/v4/server/fleet/SMTPSettings SMTPConfigured bool
diff --git a/tools/fleetctl-npm/package.json b/tools/fleetctl-npm/package.json
index e14b40f2c6..874a46f567 100644
--- a/tools/fleetctl-npm/package.json
+++ b/tools/fleetctl-npm/package.json
@@ -1,6 +1,6 @@
{
"name": "fleetctl",
- "version": "v4.49.1",
+ "version": "v4.49.2",
"description": "Installer for the fleetctl CLI tool",
"bin": {
"fleetctl": "./run.js"
diff --git a/tools/mdm/apple/loadtest/loadtest.go b/tools/mdm/apple/loadtest/loadtest.go
index 2c4cb67676..566a077775 100644
--- a/tools/mdm/apple/loadtest/loadtest.go
+++ b/tools/mdm/apple/loadtest/loadtest.go
@@ -112,7 +112,7 @@ func main() {
for _, team := range teams {
printf("Applying profiles to team %s...\n", team.Name)
- if err := apiClient.ApplyTeamProfiles(team.Name, profiles, fleet.ApplySpecOptions{}); err != nil {
+ if err := apiClient.ApplyTeamProfiles(team.Name, profiles, fleet.ApplyTeamSpecOptions{}); err != nil {
log.Fatalf("apply profiles to team %s: %s", team.Name, err)
}
}
@@ -217,7 +217,7 @@ func main() {
start = time.Now()
for _, team := range extraTeams {
- if err := apiClient.ApplyTeamProfiles(team.Name, profiles, fleet.ApplySpecOptions{}); err != nil {
+ if err := apiClient.ApplyTeamProfiles(team.Name, profiles, fleet.ApplyTeamSpecOptions{}); err != nil {
log.Fatalf("apply profiles to extra team %s: %s", team.Name, err)
}
}
diff --git a/website/api/controllers/account/logout.js b/website/api/controllers/account/logout.js
index 8119faa6ef..cba2775be9 100644
--- a/website/api/controllers/account/logout.js
+++ b/website/api/controllers/account/logout.js
@@ -37,6 +37,9 @@ actually logged in. (If they weren't, then this action is just a no-op.)`,
// Clear the `userId` property from this session.
delete this.req.session.userId;
+ // Clear personalization from liu're session.
+ delete this.req.session.primaryBuyingSituation;
+
// Then finish up, sending an appropriate response.
// > Under the covers, this persists the now-logged-out session back
// > to the underlying session store.
diff --git a/website/api/controllers/customers/save-billing-info-and-subscribe.js b/website/api/controllers/customers/save-billing-info-and-subscribe.js
index 2794131294..31f6e7df7e 100644
--- a/website/api/controllers/customers/save-billing-info-and-subscribe.js
+++ b/website/api/controllers/customers/save-billing-info-and-subscribe.js
@@ -149,24 +149,6 @@ module.exports = {
fleetLicenseKey: licenseKey,
});
- // Send a POST request to Zapier
- await sails.helpers.http.post(
- 'https://hooks.zapier.com/hooks/catch/3627242/blhrvf1/',
- {
- 'emailAddress': this.req.me.emailAddress,
- 'numberOfHosts': quoteRecord.numberOfHosts,
- 'subscriptionPrice': quoteRecord.quotedPrice,
- 'nextBillingTimestamp': new Date(subscription.current_period_end * 1000).toISOString(),
- 'webhookSecret': sails.config.custom.zapierSandboxWebhookSecret
- }
- )
- .timeout(5000)
- .tolerate(['non200Response', 'requestFailed'], (err)=>{
- // Note that Zapier responds with a 2xx status code even if something goes wrong, so just because this message is not logged doesn't mean everything is hunky dory. More info: https://github.com/fleetdm/fleet/pull/6380#issuecomment-1204395762
- sails.log.warn(`When a user purchased a Fleet Premium license, a lead/contact could not be updated in the CRM for this email address: ${this.req.me.emailAddress}. Raw error: ${err}`);
- return;
- });
-
// Send the order confirmation template email
await sails.helpers.sendTemplateEmail.with({
to: this.req.me.emailAddress,
diff --git a/website/api/controllers/download-sitemap.js b/website/api/controllers/download-sitemap.js
index 1488d4e626..7e10078554 100644
--- a/website/api/controllers/download-sitemap.js
+++ b/website/api/controllers/download-sitemap.js
@@ -43,25 +43,32 @@ module.exports = {
// β ββ£β ββ£βββ ββββββ β β ββββ£ ββ β βββ ββ£β β¦ββ£ βββ
// β© β©β© β©βββββ©β ββββββββ©ββββββ©β β© β© β©βββββββββ
let HAND_CODED_HTML_PAGES = [
- '/',
- '/company/contact',
- '/queries',
+ '/',//Β« home page
'/pricing',
- '/transparency',
- '/docs',
- '/logos',
- '/reports/state-of-device-management',
- '/releases',
- '/success-stories',
- '/securing',
- '/engineering',
- '/guides',
- '/announcements',
- '/report',
- '/deploy',
- '/podcasts',
- '/device-management',
+ '/contact',
'/support',
+ '/integrations',
+ '/logos',// « brand usage guidelines
+ '/articles',// Β« overview page (individual article pages are dynamic)
+ '/releases',// Β« article category page
+ '/success-stories',// Β« article category page
+ '/securing',// Β« article category page
+ '/engineering',// Β« article category page
+ '/guides',// Β« article category page
+ '/announcements',// Β« article category page
+ '/deploy',// Β« article category page
+ '/podcasts',// Β« article category page
+ // Product category pages:
+ '/endpoint-ops',
+ '/device-management',
+ '/vulnerability-management',
+ // Other stuff:
+ // > Note: /handbook overview page is already included amongst the markdown pages
+ // > Note: Same for /docs
+ '/transparency',// Β« default transparency link, pointed at by Fleet Desktop
+ '/queries',// Β« overview page (all subpages are dynamic)
+ '/tables',// Β« overview page (all subpages are dynamic)
+ '/reports/state-of-device-management',// Β« 2021 research
// FUTURE: Do something smarter to get hand-coded HTML pages from routes.js, like how rebuild-cloud-sdk works, to avoid this manual duplication.
// See also https://github.com/sailshq/sailsjs.com/blob/b53c6e6a90c9afdf89e5cae00b9c9dd3f391b0e7/api/helpers/get-pages-for-sitemap.js#L27
];
@@ -78,6 +85,7 @@ module.exports = {
// ββ¦ββ¦ β¦ββββββββ¦ββ¦βββ βββββββββββββββ ββββ¦βββββββ¦β ββ¦βββββ¦βββ¦ββββ¦βββββ¦ β¦βββ
// ββββ¦βββββ ββ£βββββ β βββ ββ£β β¦ββ£ βββ β β£ β β¦ββ ββββ ββββ ββ£β β¦ββ β©β βββ βββββββ
// ββ©β β© ββββ© β©β© β©β©βββ β© β© β©βββββββββ β β©ββββββ© β© β© β©β© β©β©βββ© β©ββ©ββββββ©ββββ
+ // (includes data table documentation pages; i.e. `/tables/*`)
for (let pageInfo of sails.config.builtStaticContent.markdownPages) {
sitemapXml +=`${_.escape(sails.config.custom.baseUrl+pageInfo.url)}${_.escape(new Date(pageInfo.lastModifiedAt).toJSON())}`;
}//β
diff --git a/website/api/controllers/entrance/signup.js b/website/api/controllers/entrance/signup.js
index e20c3252e1..387cd8f8ae 100644
--- a/website/api/controllers/entrance/signup.js
+++ b/website/api/controllers/entrance/signup.js
@@ -138,32 +138,24 @@ the account verification message.)`,
.intercept({name: 'UsageError'}, 'invalid')
.fetch();
+ if(sails.config.environment === 'production') {
+ let recordIds = await sails.helpers.salesforce.updateOrCreateContactAndAccount.with({
+ emailAddress: newEmailAddress,
+ firstName: firstName,
+ lastName: lastName,
+ organization: organization,
+ });
- await sails.helpers.salesforce.updateOrCreateContactAndAccount.with({
- emailAddress: newEmailAddress,
- firstName: firstName,
- lastName: lastName,
- organization: organization,
- });
-
- // Send a POST request to Zapier
- await sails.helpers.http.post.with({
- url: 'https://hooks.zapier.com/hooks/catch/3627242/30bq2ib/',
- data: {
- newEmailAddress,
- firstName,
- lastName,
- organization,
- signupReason,
- webhookSecret: sails.config.custom.zapierSandboxWebhookSecret,
- }
- })
- .timeout(5000)
- .tolerate(['non200Response', 'requestFailed'], (err)=>{
- // Note that Zapier responds with a 2xx status code even if something goes wrong, so just because this message is not logged doesn't mean everything is hunky dory. More info: https://github.com/fleetdm/fleet/pull/6380#issuecomment-1204395762
- sails.log.warn(`When a user submitted a contact form message, a lead/contact could not be updated in the CRM for this email address: ${newEmailAddress}. Raw error: ${err}`);
- return;
- });
+ await sails.helpers.salesforce.createLead.with({
+ salesforceContactId: recordIds.salesforceContactId,
+ salesforceAccountId: recordIds.salesforceAccountId,
+ leadSource: 'Website - Sign up',
+ })
+ .tolerate((err)=>{
+ sails.log.warn(`When a user signed up, a lead could not be created in the CRM for this email address: ${newEmailAddress}. Error from create-lead helper: ${err}`);
+ return;
+ });
+ }
// Store the user's new id in their session.
this.req.session.userId = newUserRecord.id;
diff --git a/website/api/controllers/get-human-interpretation-from-osquery-sql.js b/website/api/controllers/get-human-interpretation-from-osquery-sql.js
index a7d2e18b4a..19c48eb7aa 100644
--- a/website/api/controllers/get-human-interpretation-from-osquery-sql.js
+++ b/website/api/controllers/get-human-interpretation-from-osquery-sql.js
@@ -51,10 +51,13 @@ Please give me all of the above in JSON, with this data shape:
{
risks: 'TODO',
- whatWillProbablyHappenDuringMaintenance: 'TODO'
+ whatWillHappenDuringMaintenance: 'TODO'
}
Please do not add any text outside of the JSON report or wrap it in a code fence.`;
+ // > Note that this returns `whatWillHappenDuringMaintenance` instead of `whatWillProbablyHappenDuringMaintenance`.
+ // > This naming gets a better (more decisive-sounding) result from Open AI. We'll rename it for our final response.
+
// Fallback message in case LLM API request fails.
let failureMessage = 'Failed to generate human interpretation using generative AI.';
@@ -90,6 +93,9 @@ Please do not add any text outside of the JSON report or wrap it in a code fence
let report;
try {
report = JSON.parse(openAiResponse.choices[0].message.content);
+ // Change `whatWillHappenDuringMaintenance` to `whatWillProbablyHappenDuringMaintenance` (the naming we want to use in our API response)
+ report.whatWillProbablyHappenDuringMaintenance = report.whatWillHappenDuringMaintenance;
+ delete report.whatWillHappenDuringMaintenance;
} catch (err) {
sails.log.warn('When trying to parse a JSON report returned from the Open AI API, an error occurred. Error details from JSON.parse: '+err.stack+'\n Report returned from Open AI:'+openAiResponse.choices[0].message.content);
report = {
diff --git a/website/api/controllers/save-questionnaire-progress.js b/website/api/controllers/save-questionnaire-progress.js
index 36794f96f6..ef53e88ac5 100644
--- a/website/api/controllers/save-questionnaire-progress.js
+++ b/website/api/controllers/save-questionnaire-progress.js
@@ -26,6 +26,8 @@ module.exports = {
'deploy-fleet-in-your-environment',
'managed-cloud-for-growing-deployments',
'self-hosted-deploy',
+ 'whats-left-to-get-you-set-up',
+ 'how-was-your-deployment',
]
},
formData: {
@@ -76,12 +78,12 @@ module.exports = {
// - (any option) = stage 2
// 'have-you-ever-used-fleet':
// - yes-deployed: Β» Stage 6
- // - yes-recently-deployed: Β» Stage 6
+ // - yes-recently-deployed: Β» Stage 5
// - yes-deployed-local: Β» Stage 3 (Tried Fleet but might not have a use case)
// - yes-deployed-long-time: Stage 2 (Tried Fleet long ago but might not fully grasp)
// - no: Stage 2 (Never tried Fleet and might not fully grasp)
- // 'how-many-hosts': Stage 6
- // 'will-you-be-self-hosting': Stage 6
+ // 'how-many-hosts': Stage 4/5/6
+ // 'will-you-be-self-hosting': Stage 5/6
// 'what-are-you-working-on-eo-security'
// - no-use-case-yet: Β» Stage 2/3 (depends on answer from 'have-you-ever-used-fleet' step)
// - All other options Β» Stage 4
@@ -96,9 +98,22 @@ module.exports = {
// - All other options Β» Stage 4
// 'is-it-any-good': Stage 2/3/4 (depends on answer from 'have-you-ever-used-fleet' & the buying situation specific step)
// 'what-did-you-think'
- // - deploy-fleet-in-environment Β» Stage 5
+ // - host-fleet-for-me Β» Stage 4
+ // - deploy-fleet-in-environment Β» Stage 4
// - let-me-think-about-it Β» Stage 2
- // - host-fleet-for-me Β» N/A (currently not selectable, but should set the user's psychologicalStage to stage 5)
+ // FUTURE: Should the step about deploying fleet in your env be here? (For same reason is-it-any-good is here: when navigating back then forwards?)
+ // 'how-was-your-deployment'
+ // - up-and-running Β» Stage 5
+ // - kinda-stuck Β» Stage 4 (...at best! Still got the use case.)
+ // - havent-gotten-to-it Β» Stage 4 (same as above)
+ // - changed-mind-want-managed-deployment Β» Stage 4 (same as above)
+ // - decided-to-not-use-fleet Β» Stage 2
+ // 'whats-left-to-get-you-set-up'
+ // - need-premium-license-key Β» No change (Stage ??)
+ // - help-show-fleet-to-my-team Β» No change (Stage ??)
+ // - procurement-wants-some-stuff Β» No change (Stage ??)
+ // - nothing Β» No change (Stage ??)
+
let psychologicalStage = userRecord.psychologicalStage;
// Get the value of the submitted formData, we do this so we only need to check one variable, instead of (formData.attribute === 'foo');
@@ -108,9 +123,11 @@ module.exports = {
} else if(currentStep === 'what-are-you-using-fleet-for') {
psychologicalStage = '2 - Aware';
} else if(currentStep === 'have-you-ever-used-fleet') {
- if(['yes-deployed', 'yes-recently-deployed'].includes(valueFromFormData)) {
+ if(['yes-deployed'].includes(valueFromFormData)) {
// If the user has Fleet deployed, set their stage to 6.
psychologicalStage = '6 - Has team buy-in';
+ } else if(valueFromFormData === 'yes-recently-deployed'){
+ psychologicalStage = '5 - Personally confident';
} else if(valueFromFormData === 'yes-deployed-local'){
// If they've tried Fleet locally, set their stage to 3.
psychologicalStage = '3 - Intrigued';
@@ -138,8 +155,8 @@ module.exports = {
}
} else if(currentStep === 'is-it-any-good') {
if(currentSelectedBuyingSituation === 'mdm') {
- // Since the mdm use case question is the only buying situation-sepcific question where a use case can't
- // be selected, we'll check the user's previous answers befroe changing their psyStage
+ // Since the mdm use case question is the only buying situation-specific question where a use case can't
+ // be selected, we'll check the user's previous answers before changing their psyStage
if(questionnaireProgress['what-do-you-manage-mdm'].mdmUseCase === 'no-use-case-yet'){
// Check the user's answer to the have-you-ever-used-fleet question.
if(hasUsedFleetAnswer === 'yes-deployed-local') {
@@ -155,36 +172,58 @@ module.exports = {
psychologicalStage = '4 - Has use case';
// FUTURE: check previous answers for other selected buying situations.
}
- } else if(currentStep === 'what-did-you-think') {
- // If the user is ready to deploy Fleet in their work environemnt, then they're ready to get buy-in from their team, so set their psyStage to 5.
- if(valueFromFormData === 'deploy-fleet-in-environment') {
- psychologicalStage = '5 - Personally confident';
- } else if(valueFromFormData === 'let-me-think-about-it') {
- // If the user selects "Let me think about it", their stage change to 2
+ } else if(currentStep === 'what-did-you-think') {// (what did you think about [presumably after you actually did...] trying it locally)
+ // If the user selects "Let me think about it", set their psyStage to 2.
+ if(valueFromFormData === 'let-me-think-about-it') {
psychologicalStage = '2 - Aware';
- }
- // If the user selects "Iβd like you to host Fleet for me", the form is not submitted, and they are taken to the /contact page instead. FUTURE: set stage to stage 5.
+ } else if (['deploy-fleet-in-environment','host-fleet-for-me'].includes(valueFromFormData)) {
+ psychologicalStage = '4 - Has use case';
+ } else { require('assert')(false,'This should never happen.'); }
+ } else if(currentStep === 'how-was-your-deployment') {
+ if(valueFromFormData === 'decided-to-not-use-fleet') {
+ psychologicalStage = '2 - Aware';
+ } else if(valueFromFormData === 'up-and-running'){
+ psychologicalStage = '5 - Personally confident';
+ } else if(['kinda-stuck', 'havent-gotten-to-it', 'changed-mind-want-managed-deployment'].includes(valueFromFormData)){
+ psychologicalStage = '4 - Has use case';
+ } else { require('assert')(false,'This should never happen.'); }
+ } else if (currentStep === 'whats-left-to-get-you-set-up') {
+ // FUTURE: do more stuff (for now this always acts like 'no change')
} else if(currentStep === 'how-many-hosts') {
- // If they have Fleet deployed, they have team buy-in
- psychologicalStage = '6 - Has team buy-in';
+ if(['yes-deployed'].includes(hasUsedFleetAnswer)) {
+ psychologicalStage = '6 - Has team buy-in';
+ } else if(valueFromFormData === 'yes-recently-deployed'){
+ psychologicalStage = '5 - Personally confident';
+ } else {
+ // IWMIH then we want Fleet to host for us (either because we wanted that from the get-go, or we backtracked because deploying looked too time-consuming)
+ psychologicalStage = '4 - Has use case';
+ }
} else if(currentStep === 'will-you-be-self-hosting') {
- // If they have Fleet deployed, they have team buy-in
- psychologicalStage = '6 - Has team buy-in';
+ if(['yes-deployed'].includes(hasUsedFleetAnswer)) {
+ psychologicalStage = '6 - Has team buy-in';
+ } else if(valueFromFormData === 'yes-recently-deployed'){
+ psychologicalStage = '5 - Personally confident';
+ } else { require('assert')(false, 'This should never happen.'); }
}//ο¬
-
}//ο¬
// Only update CRM records if the user's psychological stage changes.
- if(currentStep !== userRecord.currentStep){
- await sails.helpers.salesforce.updateOrCreateContactAndAccount.with({
- emailAddress: this.req.me.emailAddress,
- firstName: this.req.me.firstName,
- lastName: this.req.me.lastName,
- primaryBuyingSituation: primaryBuyingSituation === 'eo-security' ? 'Endpoint operations - Security' : primaryBuyingSituation === 'eo-it' ? 'Endpoint operations - IT' : primaryBuyingSituation === 'mdm' ? 'Device management (MDM)' : primaryBuyingSituation === 'vm' ? 'Vulnerability management' : undefined,
- organization: this.req.me.organization,
- psychologicalStage,
- });
- }
+ if(psychologicalStage !== userRecord.psychologicalStage) {
+ // Use setImmediate to queue CRM updates.
+ // [?]: https://nodejs.org/api/timers.html#setimmediatecallback-args
+ require('timers').setImmediate(async ()=>{
+ await sails.helpers.salesforce.updateOrCreateContactAndAccount.with({
+ emailAddress: this.req.me.emailAddress,
+ firstName: this.req.me.firstName,
+ lastName: this.req.me.lastName,
+ primaryBuyingSituation: primaryBuyingSituation === 'eo-security' ? 'Endpoint operations - Security' : primaryBuyingSituation === 'eo-it' ? 'Endpoint operations - IT' : primaryBuyingSituation === 'mdm' ? 'Device management (MDM)' : primaryBuyingSituation === 'vm' ? 'Vulnerability management' : undefined,
+ organization: this.req.me.organization,
+ psychologicalStage,
+ }).tolerate((err)=>{
+ sails.log.warn(`Background task failed: When a user (email: ${this.req.me.emailAddress} submitted a step of the get started questionnaire, a Contact and Account record could not be created/updated in the CRM. Full error:`, err);
+ });
+ });//_β_ (Meanwhile...)
+ }//ο¬
// TODO: send all other answers to Salesforce (when there are fields for them)
// await sails.helpers.http.post.with({
diff --git a/website/api/controllers/view-device-management.js b/website/api/controllers/view-device-management.js
index 18dbe2afdd..44b86bd210 100644
--- a/website/api/controllers/view-device-management.js
+++ b/website/api/controllers/view-device-management.js
@@ -29,7 +29,7 @@ module.exports = {
});
// Specify an order for the testimonials on this page using the last names of quote authors
- let testimonialOrderForThisPage = ['Erik Gomez', 'Nick Fohs', 'Dan Grzelak', 'Kenny Botelho', 'Wes Whetstone', 'Matt Carr'];
+ let testimonialOrderForThisPage = ['Erik Gomez', 'Kenny Botelho', 'Wes Whetstone', 'Matt Carr', 'Dan Grzelak', 'Nick Fohs'];
testimonialsForScrollableTweets.sort((a, b)=>{
if(testimonialOrderForThisPage.indexOf(a.quoteAuthorName) === -1){
return 1;
diff --git a/website/api/controllers/webhooks/receive-from-github.js b/website/api/controllers/webhooks/receive-from-github.js
index 0f801981d8..b74e0494f0 100644
--- a/website/api/controllers/webhooks/receive-from-github.js
+++ b/website/api/controllers/webhooks/receive-from-github.js
@@ -88,6 +88,7 @@ module.exports = {
'pintomi1989',
'nonpunctual',
'dantecatalfamo',
+ 'PezHub',
];
let GREEN_LABEL_COLOR = 'C2E0C6';// Β« Used in multiple places below. (FUTURE: Use the "+" prefix for this instead of color. 2022-05-05)
diff --git a/website/api/helpers/iq/get-enriched.js b/website/api/helpers/iq/get-enriched.js
index c347069380..f4b56ca88a 100644
--- a/website/api/helpers/iq/get-enriched.js
+++ b/website/api/helpers/iq/get-enriched.js
@@ -54,8 +54,7 @@ module.exports = {
fn: async function ({emailAddress,linkedinUrl,firstName,lastName,organization}) {
require('assert')(sails.config.custom.iqSecret);// FUTURE: Rename this config
-
- let RX_PROTOCOL_AND_COMMON_SUBDOMAINS = /^https?\:\/\/(www\.|about\.)*/;
+ require('assert')(sails.config.custom.RX_PROTOCOL_AND_COMMON_SUBDOMAINS);
sails.log.verbose('Enriching fromβ¦', emailAddress,linkedinUrl,firstName,lastName,organization);
@@ -108,7 +107,7 @@ module.exports = {
Authorization: `Bearer ${sails.config.custom.iqSecret}`,
'content-type': 'application/json'
}).tolerate((err)=>{
- sails.log.warn(`Failed to enrich (${emailAddress},${linkedinUrl},${firstName},${lastName},${organization}):`,err);
+ sails.log.info(`Failed to enrich (${emailAddress},${linkedinUrl},${firstName},${lastName},${organization}):`,err);
return [];
});
linkedinPersonIdOrUrlSlug = matchingLinkedinPersonIds[0];
@@ -124,21 +123,33 @@ module.exports = {
Authorization: `Bearer ${sails.config.custom.iqSecret}`,
'content-type': 'application/json'
}).tolerate((err)=>{
- sails.log.warn(`Failed to enrich (${emailAddress},${linkedinUrl},${firstName},${lastName},${organization}):`,err);
+ sails.log.info(`Failed to enrich (${emailAddress},${linkedinUrl},${firstName},${lastName},${organization}):`,err);
return undefined;
});
if (matchingPersonInfo) {
require('assert')(Array.isArray(matchingPersonInfo.member_experience_collection));
- let matchingWorkExperience = (
- matchingPersonInfo.member_experience_collection.filter((workExperience) =>
- !workExperience.deleted &&
- workExperience.order_in_profile === 1 &&
- !workExperience.date_to
- // FUTURE: Be smarter by also trying to match the stated organization, if one is provided, for the edge case where someone has multiple current positions.
- )
- )[0];
+ let matchingWorkExperience;
+ if(organization){
+ // If organization was provided, we know it is listed in this person's work experience so we'll use it to filter the results.
+ matchingWorkExperience = (
+ matchingPersonInfo.member_experience_collection.filter((workExperience) =>
+ !workExperience.deleted &&
+ !workExperience.date_to &&
+ workExperience.company_name === organization
+ )
+ )[0];
+ } else {
+ // Otherwise, we'll use the top experience on this user's profile.
+ matchingWorkExperience = (
+ matchingPersonInfo.member_experience_collection.filter((workExperience) =>
+ !workExperience.deleted &&
+ workExperience.order_in_profile === 1 &&
+ !workExperience.date_to
+ )
+ )[0];
+ }//ο¬
let matchedOrganizationName;
let matchedTitle;
@@ -149,7 +160,7 @@ module.exports = {
}
person = {
- linkedinUrl: matchingPersonInfo.canonical_url.replace(RX_PROTOCOL_AND_COMMON_SUBDOMAINS,''),
+ linkedinUrl: matchingPersonInfo.canonical_url.replace(sails.config.custom.RX_PROTOCOL_AND_COMMON_SUBDOMAINS,''),
firstName: matchingPersonInfo.first_name,
lastName: matchingPersonInfo.last_name,
organization: matchedOrganizationName || '',
@@ -157,16 +168,16 @@ module.exports = {
};
if (linkedinUrl && person.linkedinUrl && person.linkedinUrl !== linkedinUrl) {
- sails.log.warn(`Unexpected result when enriching: Matched linkedin URL for person (${person.linkedinUrl}) does not equal the provided linkedin URL (${linkedinUrl})`);
+ sails.log.info(`Unexpected result when enriching: Matched linkedin URL for person (${person.linkedinUrl}) does not equal the provided linkedin URL (${linkedinUrl})`);
}//ο¬
if (firstName && person.firstName && person.firstName !== firstName) {
- sails.log.warn(`Unexpected result when enriching: Matched current firstName for person (${person.firstName}) does not equal the provided "firstName" (${firstName})`);
+ sails.log.info(`Unexpected result when enriching: Matched current firstName for person (${person.firstName}) does not equal the provided "firstName" (${firstName})`);
}//ο¬
if (lastName && person.lastName && person.lastName !== lastName) {
- sails.log.warn(`Unexpected result when enriching: Matched current lastName for person (${person.lastName}) does not equal the provided "lastName" (${lastName})`);
+ sails.log.info(`Unexpected result when enriching: Matched current lastName for person (${person.lastName}) does not equal the provided "lastName" (${lastName})`);
}//ο¬
if (organization && person.organization && person.organization !== organization) {
- sails.log.warn(`Unexpected result when enriching: Matched current TOP organization for person (${person.organization}) does not equal the provided "organization" (${organization})`);
+ sails.log.info(`Unexpected result when enriching: Matched current TOP organization for person (${person.organization}) does not equal the provided "organization" (${organization})`);
}//ο¬
}//ο¬
}//ο¬
@@ -193,7 +204,7 @@ module.exports = {
Authorization: `Bearer ${sails.config.custom.iqSecret}`,
'content-type': 'application/json'
}).tolerate((err)=>{
- sails.log.warn(`Failed to enrich (${emailAddress},${linkedinUrl},${firstName},${lastName},${organization}):`,err);
+ sails.log.info(`Failed to enrich (${emailAddress},${linkedinUrl},${firstName},${lastName},${organization}):`,err);
return [];
});
@@ -206,7 +217,7 @@ module.exports = {
Authorization: `Bearer ${sails.config.custom.iqSecret}`,
'content-type': 'application/json'
}).tolerate((err)=>{
- sails.log.warn(`Failed to enrich (${emailAddress},${linkedinUrl},${firstName},${lastName},${organization}):`,err);
+ sails.log.info(`Failed to enrich (${emailAddress},${linkedinUrl},${firstName},${lastName},${organization}):`,err);
return [];
});
}//ο¬
@@ -222,21 +233,24 @@ module.exports = {
Authorization: `Bearer ${sails.config.custom.iqSecret}`,
'content-type': 'application/json'
}).tolerate((err)=>{
- sails.log.warn(`Failed to enrich (${emailAddress},${linkedinUrl},${firstName},${lastName},${organization}):`,err);
+ sails.log.info(`Failed to enrich (${emailAddress},${linkedinUrl},${firstName},${lastName},${organization}):`,err);
return undefined;
});
if (matchingCompanyPageInfo) {
+ let parsedCompanyEmailDomain = require('url').parse(matchingCompanyPageInfo.website);
+ // If a company's website does not include the protocol (https://), url.parse will return null as the hostname, if this happens, we'll use the href value returned instead.
+ let emailDomain = parsedCompanyEmailDomain.hostname ? parsedCompanyEmailDomain.hostname.replace(sails.config.custom.RX_PROTOCOL_AND_COMMON_SUBDOMAINS,'') : parsedCompanyEmailDomain.href.replace(sails.config.custom.RX_PROTOCOL_AND_COMMON_SUBDOMAINS,'');
employer = {
organization: matchingCompanyPageInfo.name,
numberOfEmployees: matchingCompanyPageInfo.employees_count,
- emailDomain: require('url').parse(matchingCompanyPageInfo.website).hostname.replace(RX_PROTOCOL_AND_COMMON_SUBDOMAINS,''),
- linkedinCompanyPageUrl: matchingCompanyPageInfo.canonical_url.replace(RX_PROTOCOL_AND_COMMON_SUBDOMAINS,''),
+ emailDomain: emailDomain,
+ linkedinCompanyPageUrl: matchingCompanyPageInfo.canonical_url.replace(sails.config.custom.RX_PROTOCOL_AND_COMMON_SUBDOMAINS,''),
};
if (organization && employer.organization && employer.organization !== organization) {
- sails.log.warn(`Unexpected result when enriching: Matched organization name (${employer.organization}) does not equal the provided "organization" (${organization})`);
+ sails.log.info(`Unexpected result when enriching: Matched organization name (${employer.organization}) does not equal the provided "organization" (${organization})`);
}//ο¬
if (emailDomain && employer.emailDomain && employer.emailDomain !== emailDomain) {
- sails.log.warn(`Unexpected result when enriching: Email domain inferred from matched organization website (${employer.emailDomain}) does not equal the parsed email domain (${emailDomain}) that was derived from the provided "emailAddress" (${emailAddress})`);
+ sails.log.info(`Unexpected result when enriching: Email domain inferred from matched organization website (${employer.emailDomain}) does not equal the parsed email domain (${emailDomain}) that was derived from the provided "emailAddress" (${emailAddress})`);
}//ο¬
}//ο¬
}//ο¬
diff --git a/website/api/helpers/salesforce/create-lead.js b/website/api/helpers/salesforce/create-lead.js
index 8ae92c399a..cd55ffb76e 100644
--- a/website/api/helpers/salesforce/create-lead.js
+++ b/website/api/helpers/salesforce/create-lead.js
@@ -9,19 +9,31 @@ module.exports = {
inputs: {
- salesforceAccountId: { type: 'string', required: true },
- salesforceContactId: { type: 'string', required: true },
- leadDescription: { type: 'string', description: 'A description of what this lead is about; e.g. a contact form message, or the size of t-shirt being requested.' },
- leadSource: { type: 'string', required: true, isIn: ['Website - Contact forms', 'Website - Sign up', 'Website - Waitlist', 'Website - swag request'], },// TODO verify and complete enum
-
-
- // FUTURE: Move these off eventually:
- firstName: { type: 'string', required: true, description: 'The first name of the referenced contact.' },
- lastName: { type: 'string', required: true, description: 'The last name of the referenced contact.' },
- emailAddress: { type: 'string', description: 'The email address of the referenced contact.', extendedDescription: 'Included here so that the little Salesforce thingie that shows email and calendar activity shows maximum contact in both the Contact and Lead views.' },
- primaryBuyingSituation: { type: 'string' },
+ salesforceAccountId: {
+ type: 'string',
+ required: true,
+ description: 'The ID of the Account record that was found or updated by the updateOrCreateContactAndAccount helper.'
+ },
+ salesforceContactId: {
+ type: 'string',
+ required: true
+ },
+ leadDescription: {
+ type: 'string',
+ description: 'A description of what this lead is about; e.g. a contact form message, or the size of t-shirt being requested.'
+ },
+ leadSource: {
+ type: 'string',
+ required: true,
+ isIn: [
+ 'Website - Contact forms',
+ 'Website - Sign up',
+ 'Website - Waitlist',
+ 'Website - swag request',
+ ],
+ },
+ primaryBuyingSituation: { type: 'string', isin: ['eo-it', 'eo-security', 'mdm', 'vm'] },
numberOfHosts: { type: 'number' },
-
},
@@ -34,55 +46,68 @@ module.exports = {
},
- fn: async function ({salesforceAccountId, salesforceContactId, leadDescription, leadSource, firstName, lastName, emailAddress, primaryBuyingSituation, numberOfHosts}) {
+ fn: async function ({salesforceAccountId, salesforceContactId, leadDescription, leadSource, primaryBuyingSituation, numberOfHosts}) {
require('assert')(sails.config.custom.salesforceIntegrationUsername);
require('assert')(sails.config.custom.salesforceIntegrationPasskey);
let jsforce = require('jsforce');
- console.log(firstName, lastName, emailAddress, primaryBuyingSituation, numberOfHosts);
+
+ // login to Salesforce
let salesforceConnection = new jsforce.Connection({
loginUrl : 'https://fleetdm.my.salesforce.com'
});
await salesforceConnection.login(sails.config.custom.salesforceIntegrationUsername, sails.config.custom.salesforceIntegrationPasskey);
- // Get the contact record
- let contactRecord = await salesforceConnection.sobject('Contact')
- .retrieve(salesforceContactId);
- // Verify that the account ID provided is valid.
- let accountRecord = await salesforceConnection.sobject('Account')
- .retrieve(salesforceAccountId);
- // TODO better error messages
- if(contactRecord === null) {
- throw new Error(`When attempting to create a Salesforce lead using the ID of a Contact record, no Contact matching the id provided (${salesforceContactId} was found.`);
- }
- if(accountRecord === null) {
- throw new Error(`When attempting to create a Salesforce lead, no account matching the id provided (${salesforceContactId} could be found`);
- }
-
- // TODO: wrap this in a try-catch block to handle errors from Salesforce.
- // Create the new Lead record.
- let lead = await salesforceConnection.sobject('Lead')
- .create({
- FirstName: contactRecord.FirstName,
- LastName: contactRecord.LastName,
- Email: contactRecord.Email,
- Website: contactRecord.Website,
- // eslint-disable-next-line camelcase
- of_hosts__c: contactRecord.of_hosts__c,
- // eslint-disable-next-line camelcase
- Primary_buying_scenario__c: contactRecord.Primary_buying_situation__c,
- // eslint-disable-next-line camelcase
- LinkedIn_profile__c: contactRecord.LinkedIn_profile__c,
- Description: leadDescription,
- LeadSource: leadSource,
- // eslint-disable-next-line camelcase
- Contact_associated_by_website__c: salesforceContactId,
- // eslint-disable-next-line camelcase
- Account__c: salesforceAccountId,
- OwnerId: accountRecord.OwnerId
+ // Get the Contact record.
+ let contactRecord = await sails.helpers.flow.build(async ()=>{
+ return await salesforceConnection.sobject('Contact')
+ .retrieve(salesforceContactId);
+ }).intercept((err)=>{
+ return new Error(`When attempting to create a new Lead record using an existing Contact record (ID: ${salesforceContactId}), an error occurred when retreiving the specified record. Full error: ${err}`);
});
- console.log(`Created lead! ${lead}`);
- // TODO handle duplicate leads:
+ // Get the Account record.
+ let accountRecord = await sails.helpers.flow.build(async ()=>{
+ return await salesforceConnection.sobject('Account')
+ .retrieve(salesforceAccountId);
+ }).intercept((err)=>{
+ return new Error(`When attempting to create a Lead record using an exisitng Account record (ID: ${salesforceAccountId}), An error occured when retreiving the specified record. Full error: ${err}`);
+ });
+
+ let primaryBuyingSituationValuesByCodename = {
+ 'vm': 'Vulnerability management',
+ 'mdm': 'Device management (MDM)',
+ 'eo-it': 'Endpoint operations - IT',
+ 'eo-security': 'Endpoint operations - Security',
+ };
+
+ // If numberOfHosts or primaryBuyingSituationToSet was provided, set that value on the new Lead, otherwise fallback to the value on the contact record. (If it has one)
+ // Note: If these were not provided and a retreived contact record does not have this information, these values will be set to 'null' and are safe to pass into the sobject('Lead').create method below.
+ let numberOfHostsToSet = numberOfHosts ? numberOfHosts : contactRecord.of_hosts__c;
+ let primaryBuyingSituationToSet = primaryBuyingSituation ? primaryBuyingSituationValuesByCodename[primaryBuyingSituation] : contactRecord.Primary_buying_situation__c;
+
+ // Create the new Lead record.
+ await sails.helpers.flow.build(async ()=>{
+ return await salesforceConnection.sobject('Lead')
+ .create({
+ // Information from inputs:
+ Description: leadDescription,
+ LeadSource: leadSource,
+ Account__c: salesforceAccountId,// eslint-disable-line camelcase
+ Contact_associated_by_website__c: salesforceContactId,// eslint-disable-line camelcase
+ // Information from contact record:
+ FirstName: contactRecord.FirstName,
+ LastName: contactRecord.LastName,
+ Email: contactRecord.Email,
+ Website: contactRecord.Website,
+ of_hosts__c: numberOfHostsToSet,// eslint-disable-line camelcase
+ Primary_buying_scenario__c: primaryBuyingSituationToSet,// eslint-disable-line camelcase
+ LinkedIn_profile__c: contactRecord.LinkedIn_profile__c,// eslint-disable-line camelcase
+ // Information from the account record:
+ OwnerId: accountRecord.OwnerId
+ });
+ }).intercept((err)=>{
+ return new Error(`Could not create new Lead record. Full error: ${err}`);
+ });
}
diff --git a/website/api/helpers/salesforce/update-or-create-contact-and-account.js b/website/api/helpers/salesforce/update-or-create-contact-and-account.js
index 8ad95413ea..922d649399 100644
--- a/website/api/helpers/salesforce/update-or-create-contact-and-account.js
+++ b/website/api/helpers/salesforce/update-or-create-contact-and-account.js
@@ -47,17 +47,26 @@ module.exports = {
fn: async function ({emailAddress, linkedinUrl, firstName, lastName, organization, primaryBuyingSituation, psychologicalStage}) {
if(sails.config.environment !== 'production') {
sails.log.verbose('Skipping Salesforce integration...');
- return;
+ return {
+ salesforceAccountId: undefined,
+ salesforceContactId: undefined
+ };
}
require('assert')(sails.config.custom.salesforceIntegrationUsername);
require('assert')(sails.config.custom.salesforceIntegrationPasskey);
require('assert')(sails.config.custom.iqSecret);
+ require('assert')(sails.config.custom.RX_PROTOCOL_AND_COMMON_SUBDOMAINS);
if(!emailAddress && !linkedinUrl){
throw new Error('UsageError: when updating or creating a contact and account in salesforce, either an email or linkedInUrl is required.');
}
+
+ if(linkedinUrl){
+ // If linkedinUrl was provided, strip the protocol and subdomain from the URL.
+ linkedinUrl = linkedinUrl.replace(sails.config.custom.RX_PROTOCOL_AND_COMMON_SUBDOMAINS, '');
+ }
// Send the information we have to the enrichment helper.
let enrichmentData = await sails.helpers.iq.getEnriched(emailAddress, linkedinUrl, firstName, lastName, organization);
// console.log(enrichmentData);
@@ -77,7 +86,7 @@ module.exports = {
// Special sacraficial meat cave where the contacts with no organization go.
// https://fleetdm.lightning.force.com/lightning/r/Account/0014x000025JC8DAAW/view
salesforceAccountId = '0014x000025JC8DAAW';
- salesforceAccountOwnerId = '0054x00000735wDAAQ';
+ salesforceAccountOwnerId = '0054x00000735wDAAQ';// Β« "Integrations admin" user.
} else {
let existingAccountRecord = await salesforceConnection.sobject('Account')
.findOne({
@@ -85,14 +94,13 @@ module.exports = {
// 'LinkedIn_company_URL__c': enrichmentData.employer.linkedinCompanyPageUrl // TODO: if this information is not present on an existing account, nothing will be returned.
});
// console.log(existingAccountRecord);
- if(existingAccountRecord) {
+ if(existingAccountRecord && existingAccountRecord.OwnerId !== '0054x00000735wDAAQ') {
// Store the ID of the Account record we found.
salesforceAccountId = existingAccountRecord.Id;
salesforceAccountOwnerId = existingAccountRecord.OwnerId;
// console.log('exising account found!', salesforceAccountId);
} else {
-
-
+ // If we didn't find an existing record, or found one onwned by the integrations admin, we'll round robin it between the AE's Salesforce users.
let roundRobinUsers = await salesforceConnection.sobject('User')
.find({
AE_Round_robin__c: true,// eslint-disable-line camelcase
@@ -101,34 +109,45 @@ module.exports = {
let today = new Date();
let nowOn = today.toISOString().replace('Z', '+0000');
-
+ // Update the accountOwnerId value to be the ID of the next user in the round robin.
salesforceAccountOwnerId = userWithEarliestAssignTimeStamp.Id;
-
- // Update this user to putthem atthe bottom of the round robin list.
+ // Update this user to put them at the bottom of the round robin list.
await salesforceConnection.sobject('User')
.update({
Id: salesforceAccountOwnerId,
// eslint-disable-next-line camelcase
AE_Account_Assignment_round_robin__c: nowOn
});
- // If no existing account record was found, create a new one.
- let newAccountRecord = await salesforceConnection.sobject('Account')
- .create({
- OwnerId: salesforceAccountOwnerId,
- Account_Assigned_date__c: nowOn,// eslint-disable-line camelcase
- // eslint-disable-next-line camelcase
- Current_Assignment_Reason__c: 'Inbound Lead',// TODO verify that this matters. if not, do not set it.
- Prospect_Status__c: 'Assigned',// eslint-disable-line camelcase
- Name: enrichmentData.employer.organization,// IFWMIH: We know organization exists
- Website: enrichmentData.employer.emailDomain,
- LinkedIn_company_URL__c: enrichmentData.employer.linkedinCompanyPageUrl,// eslint-disable-line camelcase
- NumberOfEmployees: enrichmentData.employer.numberOfEmployees,
- });
- salesforceAccountId = newAccountRecord.id;
+
+ if(existingAccountRecord){
+ // If we found an existing Account record owned by the integrations admin user account, reassign it to the new owner.
+ salesforceAccountId = existingAccountRecord.Id;
+ await salesforceConnection.sobject('Account')
+ .update({
+ Id: salesforceAccountId,
+ OwnerId: salesforceAccountOwnerId
+ });
+ } else {
+ // If no existing account record was found, create a new one.
+ let newAccountRecord = await salesforceConnection.sobject('Account')
+ .create({
+ OwnerId: salesforceAccountOwnerId,
+ Account_Assigned_date__c: nowOn,// eslint-disable-line camelcase
+ // eslint-disable-next-line camelcase
+ Current_Assignment_Reason__c: 'Inbound Lead',// TODO verify that this matters. if not, do not set it.
+ Prospect_Status__c: 'Assigned',// eslint-disable-line camelcase
+
+ Name: enrichmentData.employer.organization,// IFWMIH: We know organization exists
+ Website: enrichmentData.employer.emailDomain,
+ LinkedIn_company_URL__c: enrichmentData.employer.linkedinCompanyPageUrl,// eslint-disable-line camelcase
+ NumberOfEmployees: enrichmentData.employer.numberOfEmployees,
+ });
+ salesforceAccountId = newAccountRecord.id;
+ }//ο¬
// console.log('New account created!', salesforceAccountId);
- }
- }
+ }//ο¬
+ }//ο¬
@@ -151,7 +170,7 @@ module.exports = {
});
} else {
existingContactRecord = undefined;
- }
+ }//ο¬
let salesforceContactId;
let valuesToSet = {};
@@ -195,7 +214,7 @@ module.exports = {
// console.log(newContactRecord);
salesforceContactId = newContactRecord.id;
// console.log(`New contact record created! ${salesforceContactId}`);
- }
+ }//ο¬
return {
diff --git a/website/assets/images/articles/fleet-in-your-calendar-introducing-maintenance-windows-1-900x450@2x.png b/website/assets/images/articles/fleet-in-your-calendar-introducing-maintenance-windows-1-900x450@2x.png
new file mode 100644
index 0000000000..4e92edf7de
Binary files /dev/null and b/website/assets/images/articles/fleet-in-your-calendar-introducing-maintenance-windows-1-900x450@2x.png differ
diff --git a/website/assets/images/articles/fleet-in-your-calendar-introducing-maintenance-windows-2-900x450@2x.png b/website/assets/images/articles/fleet-in-your-calendar-introducing-maintenance-windows-2-900x450@2x.png
new file mode 100644
index 0000000000..8405577e42
Binary files /dev/null and b/website/assets/images/articles/fleet-in-your-calendar-introducing-maintenance-windows-2-900x450@2x.png differ
diff --git a/website/assets/images/articles/fleet-in-your-calendar-introducing-maintenance-windows-cover-900x450@2x.png b/website/assets/images/articles/fleet-in-your-calendar-introducing-maintenance-windows-cover-900x450@2x.png
new file mode 100644
index 0000000000..cb61dba9bb
Binary files /dev/null and b/website/assets/images/articles/fleet-in-your-calendar-introducing-maintenance-windows-cover-900x450@2x.png differ
diff --git a/website/assets/images/homepage-hero-3227x920@2x.png b/website/assets/images/homepage-hero-3227x920@2x.png
new file mode 100644
index 0000000000..faad97331f
Binary files /dev/null and b/website/assets/images/homepage-hero-3227x920@2x.png differ
diff --git a/website/assets/images/homepage-hero-background-3840x500@2x.png b/website/assets/images/homepage-hero-background-3840x500@2x.png
deleted file mode 100644
index 1ca26bed15..0000000000
Binary files a/website/assets/images/homepage-hero-background-3840x500@2x.png and /dev/null differ
diff --git a/website/assets/images/icon-play-video-32x32@2x.png b/website/assets/images/icon-play-video-32x32@2x.png
new file mode 100644
index 0000000000..98d0f34a04
Binary files /dev/null and b/website/assets/images/icon-play-video-32x32@2x.png differ
diff --git a/website/assets/images/vuln-management-hero-image-345x380@2x.png b/website/assets/images/vuln-management-hero-image-345x380@2x.png
deleted file mode 100644
index 3e6ce9aab5..0000000000
Binary files a/website/assets/images/vuln-management-hero-image-345x380@2x.png and /dev/null differ
diff --git a/website/assets/images/vulnerability-management-hero-image-345x380@2x.png b/website/assets/images/vulnerability-management-hero-image-345x380@2x.png
new file mode 100644
index 0000000000..93b4deb503
Binary files /dev/null and b/website/assets/images/vulnerability-management-hero-image-345x380@2x.png differ
diff --git a/website/assets/js/pages/device-management.page.js b/website/assets/js/pages/device-management.page.js
index be249f1a8f..4ac94adabc 100644
--- a/website/assets/js/pages/device-management.page.js
+++ b/website/assets/js/pages/device-management.page.js
@@ -20,6 +20,11 @@ parasails.registerPage('device-management-page', {
// ββββ β ββ£ β β¦ββ ββ£β β ββ βββββββ
// β©βββ β© ββββ©βββ© β©βββ β© β©βββββββββ
methods: {
- //β¦
+ clickOpenVideoModal: function(modalName) {
+ this.modal = modalName;
+ },
+ closeModal: function() {
+ this.modal = undefined;
+ }
}
});
diff --git a/website/assets/js/pages/start.page.js b/website/assets/js/pages/start.page.js
index 47dfe0bc98..7be20fa6ba 100644
--- a/website/assets/js/pages/start.page.js
+++ b/website/assets/js/pages/start.page.js
@@ -20,6 +20,9 @@ parasails.registerPage('start', {
'what-do-you-manage-mdm': {},
'is-it-any-good': {stepCompleted: true},
'what-did-you-think': {},
+ 'deploy-fleet-in-your-environment': {stepCompleted: true},
+ 'how-was-your-deployment': {},
+ 'whats-left-to-get-you-set-up': {},
},
// For tracking client-side validation errors in our form.
// > Has property set to `true` for each invalid property in `formData`.
@@ -56,6 +59,12 @@ parasails.registerPage('start', {
endpointOpsSecurityWhatDidYouThinkFormRules: {
whatDidYouThink: {required: true}
},
+ howWasYourDeploymentFormRules: {
+ howWasYourDeployment: {required: true}
+ },
+ whatsLeftToGetYouSetUpFormRules: {
+ whatsLeftToGetSetUp: {required: true}
+ },
previouslyAnsweredQuestions: {},
// Server error state for the forms
@@ -94,8 +103,12 @@ parasails.registerPage('start', {
formData: formDataForThisStep,
});
this.previouslyAnsweredQuestions[this.currentStep] = getStartedProgress[this.currentStep];
- this.syncing = false;
- this.currentStep = nextStep;
+ if(_.startsWith(nextStep, '/')){
+ window.location = nextStep;
+ } else {
+ this.syncing = false;
+ this.currentStep = nextStep;
+ }
},
clickGoToPreviousStep: async function() {
switch(this.currentStep) {
@@ -103,7 +116,11 @@ parasails.registerPage('start', {
this.currentStep = 'what-are-you-using-fleet-for';
break;
case 'how-many-hosts':
- this.currentStep = 'have-you-ever-used-fleet';
+ if(this.formData['have-you-ever-used-fleet'].fleetUseStatus === 'yes-recently-deployed' || this.formData['have-you-ever-used-fleet'].fleetUseStatus === 'yes-deployed') {
+ this.currentStep = 'have-you-ever-used-fleet';
+ } else {
+ this.currentStep = 'what-did-you-think';
+ }
break;
case 'will-you-be-self-hosting':
this.currentStep = 'how-many-hosts';
@@ -112,7 +129,11 @@ parasails.registerPage('start', {
this.currentStep = 'will-you-be-self-hosting';
break;
case 'managed-cloud-for-growing-deployments':
- this.currentStep = 'will-you-be-self-hosting';
+ if(this.formData['have-you-ever-used-fleet'].fleetUseStatus === 'yes-recently-deployed' || this.formData['have-you-ever-used-fleet'].fleetUseStatus === 'yes-deployed') {
+ this.currentStep = 'will-you-be-self-hosting';
+ } else {
+ this.currentStep = 'how-many-hosts';
+ }
break;
case 'what-are-you-working-on-eo-security':
this.currentStep = 'have-you-ever-used-fleet';
@@ -150,6 +171,12 @@ parasails.registerPage('start', {
case 'what-do-you-manage-mdm':
this.currentStep = 'have-you-ever-used-fleet';
break;
+ case 'how-was-your-deployment':
+ this.currentStep = 'deploy-fleet-in-your-environment';
+ break;
+ case 'whats-left-to-get-you-set-up':
+ this.currentStep = 'how-was-your-deployment';
+ break;
}
},
getNextStep: function() {
@@ -179,11 +206,20 @@ parasails.registerPage('start', {
}
break;
case 'how-many-hosts':
- if(this.formData['how-many-hosts'].numberOfHosts === '1-100' ||
- this.formData['how-many-hosts'].numberOfHosts === '100-700') {
- nextStepInForm = 'will-you-be-self-hosting';
+ if(this.formData['have-you-ever-used-fleet'].fleetUseStatus === 'yes-recently-deployed' || this.formData['have-you-ever-used-fleet'].fleetUseStatus === 'yes-deployed') {
+ if(this.formData['how-many-hosts'].numberOfHosts === '1-100' ||
+ this.formData['how-many-hosts'].numberOfHosts === '100-700') {
+ nextStepInForm = 'will-you-be-self-hosting';
+ } else {
+ nextStepInForm = 'lets-talk-to-your-team';
+ }
} else {
- nextStepInForm = 'lets-talk-to-your-team';
+ if(this.formData['how-many-hosts'].numberOfHosts === '1-100' ||
+ this.formData['how-many-hosts'].numberOfHosts === '100-700') {
+ nextStepInForm = 'managed-cloud-for-growing-deployments';
+ } else {
+ nextStepInForm = 'lets-talk-to-your-team';
+ }
}
break;
case 'will-you-be-self-hosting':
@@ -210,11 +246,38 @@ parasails.registerPage('start', {
break;
case 'what-did-you-think':
if(this.formData['what-did-you-think'].whatDidYouThink === 'let-me-think-about-it'){
- nextStepInForm = 'is-it-any-good';
+ nextStepInForm = '/announcements';
+ } else if(this.formData['what-did-you-think'].whatDidYouThink === 'host-fleet-for-me') {
+ nextStepInForm = 'how-many-hosts';
} else {
nextStepInForm = 'deploy-fleet-in-your-environment';
}
break;
+ case 'deploy-fleet-in-your-environment':
+ nextStepInForm = 'how-was-your-deployment';
+ break;
+ case 'how-was-your-deployment':
+ if(this.formData['how-was-your-deployment'].howWasYourDeployment === 'up-and-running') {
+ nextStepInForm = 'whats-left-to-get-you-set-up';
+ } else if(this.formData['how-was-your-deployment'].howWasYourDeployment === 'kinda-stuck'){
+ nextStepInForm = '/contact';
+ } else if(this.formData['how-was-your-deployment'].howWasYourDeployment === 'havent-gotten-to-it') {
+ nextStepInForm = 'deploy-fleet-in-your-environment';
+ } else if(this.formData['how-was-your-deployment'].howWasYourDeployment === 'changed-mind-want-managed-deployment'){
+ nextStepInForm = 'how-many-hosts';
+ } else if(this.formData['how-was-your-deployment'].howWasYourDeployment === 'decided-to-not-use-fleet'){
+ nextStepInForm = '/';
+ }
+ break;
+ case 'whats-left-to-get-you-set-up':
+ if(this.formData['whats-left-to-get-you-set-up'].whatsLeftToGetSetUp === 'need-premium-license-key') {
+ nextStepInForm = '/new-license';
+ } else if(this.formData['whats-left-to-get-you-set-up'].whatsLeftToGetSetUp === 'nothing'){
+ nextStepInForm = '/swag';
+ } else {
+ nextStepInForm = '/contact';
+ }
+ break;
}
return nextStepInForm;
},
@@ -235,6 +298,9 @@ parasails.registerPage('start', {
this.formData[step] = this.previouslyAnsweredQuestions[step];
}
this.currentStep = this.getNextStep();
+ if(_.startsWith(this.currentStep, '/')){
+ this.currentStep = this.me.lastSubmittedGetStartedQuestionnaireStep;
+ }
}
},
}
diff --git a/website/assets/styles/components/ajax-button.component.less b/website/assets/styles/components/ajax-button.component.less
index 27ec3d605e..859851a421 100644
--- a/website/assets/styles/components/ajax-button.component.less
+++ b/website/assets/styles/components/ajax-button.component.less
@@ -44,6 +44,7 @@
&.syncing {
.button-loader, .button-loading {
display: inline-block;
+ height: 16px;
}
.button-text {
display: none;
diff --git a/website/assets/styles/pages/device-management.less b/website/assets/styles/pages/device-management.less
index 24817141ac..21d927f058 100644
--- a/website/assets/styles/pages/device-management.less
+++ b/website/assets/styles/pages/device-management.less
@@ -28,10 +28,11 @@
font-family: 'Roboto Mono';
font-style: normal;
font-weight: 400;
- font-size: 18px;
- line-height: 24px;
+ font-size: 14px;
+ line-height: 150%;
+ text-transform: uppercase;
color: @core-fleet-black-75;
- margin-bottom: 4px;
+ margin-bottom: 0;
}
p {
font-size: 16px;
@@ -301,78 +302,77 @@
[parasails-component='logo-carousel'] {
margin-bottom: 80px;
}
- [purpose='calendar-section'] {
- padding: 0px 64px 40px 64px;
- margin-bottom: 40px;
- max-width: 1200px;
- margin-left: auto;
- margin-right: auto;
- }
- [purpose='calendar-card-body'] {
- width: 50%;
- padding-left: 64px;
- padding-bottom: 24px;
- margin-right: 24px;
- text-align: left;
- h3 {
- font-size: 32px;
- line-height: 120%;
- margin-bottom: 24px;
- }
- p {
- max-width: 720px;
- margin-bottom: 24px;
- margin-top: 24px;
- }
- a {
- margin-bottom: 6px;
- padding-right: 24px;
- }
- }
- [purpose='calendar-card'] {
- border-radius: 24px;
- border: 1px solid #E2E4EA;
- background: linear-gradient(165deg, #F9FDFE 10.56%, #FFF 33.43%);
- overflow: hidden;
- height: 420px;
- }
- [purpose='coming-soon-badge'] {
- background-color: #0587FF;
- padding: 4px 8px 3px 8px;
- display: flex;
- align-items: center;
- border-radius: 14px;
- color: #FFF;
- font-size: 12px;
- font-weight: 500;
- line-height: 18px;
- text-transform: uppercase;
- margin-bottom: 8px;
- }
- [purpose='calendar-image'] {
- padding-top: 32px;
- padding-bottom: 24px;
- height: 420px;
- width: 50%;
- display: flex;
- flex-direction: row;
- overflow-x: hidden;
- justify-content: center;
- position: relative;
- &:before {
- position: absolute;
- content: ' ';
- width: 55px;
- left: 0px;
- top: 0px;
- height: 420px;
- background: linear-gradient(270deg, rgba(255, 255, 255, 0.00) 0%, #FFF 100%);
- }
- img {
- height: 100%;
- width: auto;
- }
- }
+ [purpose='calendar-feature'] {
+ margin-bottom: 140px;
+ h3 {
+ font-size: 32px;
+ margin-bottom: 24px;
+ }
+ [purpose='calendar-feature-text'] {
+ max-width: 480px;
+ }
+ [purpose='new-badge'] {
+ background-color: #0587FF;
+ padding: 4px 8px 3px 8px;
+ display: flex;
+ align-items: center;
+ border-radius: 14px;
+ color: #FFF;
+ font-size: 12px;
+ font-weight: 500;
+ line-height: 18px;
+ text-transform: uppercase;
+ margin-bottom: 12px;
+ width: min-content;
+ }
+ [purpose='calendar-checklist'] {
+ margin-top: 8px;
+ margin-bottom: 24px;
+ p {
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 21px;
+ padding-left: 37px;
+ text-indent: -37px;
+ margin-bottom: 1.5rem;
+ &:last-of-type {
+ margin-bottom: 0px;
+ }
+ }
+ p::before {
+ content: ' ';
+ background-image: url('/images/icon-checkmark-green-20x20@2x.png');
+ background-size: 20px 20px;
+ display: inline-block;
+ position: relative;
+ top: 5px;
+ margin-right: 16px;
+ width: 20px;
+ height: 20px;
+ }
+ }
+ [purpose='feature-video'] {
+ margin-left: 80px;
+ max-width: 468px;
+ video {
+ max-width: 100%;
+ max-height: 100%;
+ border-radius: 16px;
+ }
+ }
+ [purpose='video-button'] {
+ margin-top: 12px;
+ cursor: pointer;
+ img {
+ height: 32px;
+ margin-right: 8px;
+ }
+ font-size: 14px;
+ font-weight: 700;
+ line-height: 21px;
+ }
+ }
@@ -534,8 +534,12 @@
padding-left: 40px;
padding-right: 40px;
}
- [purpose='calendar-section'] {
- padding: 0px 40px 40px 40px;
+ [purpose='calendar-feature'] {
+ [purpose='feature-video'] {
+ margin-left: auto;
+ margin-right: auto;
+ margin-bottom: 40px;
+ }
}
[purpose='page-section'] {
margin-bottom: 140px;
diff --git a/website/assets/styles/pages/endpoint-ops.less b/website/assets/styles/pages/endpoint-ops.less
index 410f45c88b..7f69ef95e6 100644
--- a/website/assets/styles/pages/endpoint-ops.less
+++ b/website/assets/styles/pages/endpoint-ops.less
@@ -22,10 +22,11 @@
h4 {
font-family: 'Roboto Mono', monospace;
font-weight: 400;
- font-size: 18px;
- line-height: 24px;
+ font-size: 14px;
+ line-height: 150%;
+ text-transform: uppercase;
color: @core-fleet-black-75;
- margin-bottom: 8px;
+ margin-bottom: 0;
}
p {
font-size: 16px;
@@ -256,6 +257,13 @@
margin-right: 0px;
margin-left: 12px;
}
+ &.charles-zaffery {
+ background: url('/images/homepage-hero-3227x920@2x.png');
+ background-position: center;
+ background-size: cover;
+ margin-right: 0px;
+ margin-left: 12px;
+ }
}
[purpose='video-modal'] {
[purpose='modal-dialog'] {
@@ -292,76 +300,74 @@
[parasails-component='logo-carousel'] {
margin-bottom: 80px;
}
- [purpose='calendar-section'] {
- padding: 0px 64px 40px 64px;
- margin-bottom: 40px;
- max-width: 1200px;
- margin-left: auto;
- margin-right: auto;
- }
- [purpose='calendar-card-body'] {
- width: 50%;
- padding-left: 64px;
- padding-bottom: 24px;
- margin-right: 24px;
- text-align: left;
+ [purpose='calendar-feature'] {
+ margin-bottom: 140px;
h3 {
- font-size: 32px;
- line-height: 120%;
margin-bottom: 24px;
}
- p {
- max-width: 720px;
+ [purpose='calendar-feature-text'] {
+ max-width: 480px;
+ }
+ [purpose='new-badge'] {
+ background-color: #0587FF;
+ padding: 4px 8px 3px 8px;
+ display: flex;
+ align-items: center;
+ border-radius: 14px;
+ color: #FFF;
+ font-size: 12px;
+ font-weight: 500;
+ line-height: 18px;
+ text-transform: uppercase;
+ margin-bottom: 12px;
+ width: min-content;
+ }
+ [purpose='calendar-checklist'] {
+ margin-top: 8px;
margin-bottom: 24px;
- margin-top: 24px;
+ p {
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 21px;
+ padding-left: 37px;
+ text-indent: -37px;
+ margin-bottom: 1.5rem;
+ &:last-of-type {
+ margin-bottom: 0px;
+ }
+ }
+ p::before {
+ content: ' ';
+ background-image: url('/images/icon-checkmark-green-20x20@2x.png');
+ background-size: 20px 20px;
+ display: inline-block;
+ position: relative;
+ top: 5px;
+ margin-right: 16px;
+ width: 20px;
+ height: 20px;
+ }
}
- a {
- margin-bottom: 6px;
- padding-right: 24px;
+ [purpose='feature-video'] {
+ margin-left: 80px;
+ max-width: 468px;
+ video {
+ max-width: 100%;
+ max-height: 100%;
+ border-radius: 16px;
+ }
}
- }
- [purpose='calendar-card'] {
- border-radius: 24px;
- border: 1px solid #E2E4EA;
- background: linear-gradient(165deg, #F9FDFE 10.56%, #FFF 33.43%);
- overflow: hidden;
- height: 420px;
- }
- [purpose='coming-soon-badge'] {
- background-color: #0587FF;
- padding: 4px 8px 3px 8px;
- display: flex;
- align-items: center;
- border-radius: 14px;
- color: #FFF;
- font-size: 12px;
- font-weight: 500;
- line-height: 18px;
- text-transform: uppercase;
- margin-bottom: 8px;
- }
- [purpose='calendar-image'] {
- padding-top: 32px;
- padding-bottom: 24px;
- height: 420px;
- width: 50%;
- display: flex;
- flex-direction: row;
- overflow-x: hidden;
- justify-content: center;
- position: relative;
- &:before {
- position: absolute;
- content: ' ';
- width: 55px;
- left: 0px;
- top: 0px;
- height: 420px;
- background: linear-gradient(270deg, rgba(255, 255, 255, 0.00) 0%, #FFF 100%);
- }
- img {
- height: 100%;
- width: auto;
+ [purpose='video-button'] {
+ margin-top: 12px;
+ cursor: pointer;
+ img {
+ height: 32px;
+ margin-right: 8px;
+ }
+ font-size: 14px;
+ font-weight: 700;
+ line-height: 21px;
}
}
@@ -533,6 +539,13 @@
[purpose='calendar-section'] {
padding: 0px 40px 40px 40px;
}
+ [purpose='calendar-feature'] {
+ [purpose='feature-video'] {
+ margin-left: auto;
+ margin-right: auto;
+ margin-bottom: 40px;
+ }
+ }
[purpose='page-content'] {
max-width: 840px;
}
diff --git a/website/assets/styles/pages/homepage.less b/website/assets/styles/pages/homepage.less
index 455e28d27d..5ea90a8bfc 100644
--- a/website/assets/styles/pages/homepage.less
+++ b/website/assets/styles/pages/homepage.less
@@ -1,7 +1,7 @@
#homepage {
h1 {
font-weight: 800;
- font-size: 64px;
+ font-size: 48px;
line-height: 120%;
}
h3 {
@@ -18,13 +18,14 @@
}
h4 {
+ text-transform: uppercase;
font-family: 'Roboto Mono';
font-style: normal;
font-weight: 400;
- font-size: 18px;
+ font-size: 14px;
line-height: 120%;
color: @core-fleet-black-75;
- margin-bottom: 4px;
+ margin-bottom: 8px;
}
p {
color: @core-fleet-black-75;
@@ -36,22 +37,22 @@
}
[purpose='hero-container'] {
- background: #E4F4F4;
+ background: linear-gradient(#E4F3F4, #FFFFFF);
overflow: hidden;
}
[purpose='hero-background-image'] {
- background: url('/images/homepage-hero-background-3840x500@2x.png');
- background-size: 3840px auto;
+ background: url('/images/homepage-hero-3227x920@2x.png');
+ background-size: auto 400px;
background-position: center bottom;
background-repeat: repeat-x;
}
[purpose='homepage-hero'] {
- padding-top: 100px;
+ padding-top: 64px;
padding-left: 40px;
padding-right: 40px;
- padding-bottom: 480px;
+ padding-bottom: 380px;
max-width: 1200px;
}
@@ -62,8 +63,8 @@
margin-bottom: 16px;
}
p {
- margin-bottom: 40px;
- font-size: 18px;
+ margin-bottom: 32px;
+ font-size: 16px;
}
}
@@ -86,6 +87,80 @@
margin-right: auto;
}
+ [purpose='calendar-feature'] {
+ margin-bottom: 120px; /*increased while testimonial videos are temporarily hidden.*/
+ h3 {
+ margin-bottom: 24px;
+ }
+ [purpose='calendar-feature-text'] {
+ max-width: 480px;
+ }
+ [purpose='new-badge'] {
+ background-color: #0587FF;
+ padding: 4px 8px 3px 8px;
+ display: flex;
+ align-items: center;
+ border-radius: 14px;
+ color: #FFF;
+ font-size: 12px;
+ font-weight: 500;
+ line-height: 18px;
+ text-transform: uppercase;
+ margin-bottom: 12px;
+ width: min-content;
+ }
+ [purpose='calendar-checklist'] {
+ margin-top: 8px;
+ margin-bottom: 24px;
+ p {
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 21px;
+ padding-left: 37px;
+ text-indent: -37px;
+ margin-bottom: 1.5rem;
+ &:last-of-type {
+ margin-bottom: 0px;
+ }
+ }
+ p::before {
+ content: ' ';
+ background-image: url('/images/icon-checkmark-green-20x20@2x.png');
+ background-size: 20px 20px;
+ display: inline-block;
+ position: relative;
+ top: 5px;
+ margin-right: 16px;
+ width: 20px;
+ height: 20px;
+ }
+ }
+ [purpose='feature-video'] {
+ margin-left: 80px;
+ max-width: 468px;
+ video {
+ max-width: 100%;
+ max-height: 100%;
+ border-radius: 16px;
+ }
+ }
+ [purpose='video-button'] {
+ margin-top: 16px;
+ cursor: pointer;
+ img {
+ height: 32px;
+ margin-right: 8px;
+ }
+ font-size: 14px;
+ font-weight: 700;
+ line-height: 21px;
+ }
+ }
+
+
+
+
[purpose='testimonials'] {
margin-bottom: 80px;
}
@@ -228,9 +303,6 @@
[purpose='category-text-block'] {
max-width: 468px;
- h4 {
- font-size: 16px;
- }
h3 {
font-size: 32px;
margin-bottom: 32px;
@@ -594,16 +666,13 @@
@media (max-width: 1199px) {
[purpose='hero-background-image'] {
- background-size: 3400px auto;
+ background-size: auto 400px;
background-position: center bottom;
background-repeat: no-repeat;
}
- [purpose='calendar-section'] {
- padding: 40px;
-
- }
[purpose='homepage-hero'] {
max-width: 1080px;
+ padding-bottom: 380px;
}
[purpose='hero-logos'] {
@@ -656,26 +725,21 @@
font-size: 48px;
}
-
- [purpose='calendar-section'] {
- padding: 40px 32px;
+ [purpose='calendar-feature'] {
+ [purpose='feature-video'] {
+ margin-left: auto;
+ margin-right: auto;
+ margin-bottom: 40px;
+ }
}
-
- [purpose='calendar-card-body'] {
- padding: 60px 24px 24px 24px;
- }
-
- [purpose='hero-background-image'] {
- background-size: 2800px auto;
- background-position: center bottom;
- background-repeat: no-repeat;
- }
-
[purpose='homepage-hero'] {
max-width: 100%;
- padding-top: 80px;
- padding-bottom: 400px;
+ padding-bottom: 380px;
}
+ [purpose='hero-background-image'] {
+ background-size: auto 400px;
+ }
+
[purpose='homepage-content'] {
padding-right: 60px;
padding-left: 60px;
@@ -865,20 +929,13 @@
font-size: 48px;
}
- [purpose='calendar-section'] {
- padding: 40px 24px;
+ [purpose='homepage-hero'] {
+ padding-top: 64px;
+ padding-bottom: 360px;
}
[purpose='hero-background-image'] {
- background-size: auto 278px;
- background-position: center bottom;
- background-repeat: no-repeat;
+ background-size: auto 360px;
}
-
- [purpose='homepage-hero'] {
- padding-top: 80px;
- padding-bottom: 308px;
- }
-
[purpose='homepage-content'] {
padding-right: 40px;
padding-left: 40px;
@@ -1137,17 +1194,23 @@
height: 287px;
}
[purpose='homepage-hero'] {
- padding-top: 60px;
- padding-bottom: 270px;
+ padding-bottom: 320px;
padding-left: 24px;
padding-right: 24px;
}
-
+ [purpose='hero-background-image'] {
+ background-size: auto 320px;
+ }
[purpose='hero-text'] {
p {
font-size: 16px;
}
}
+ [purpose='button-row'] {
+ max-width: 224px;
+ margin-left: auto;
+ margin-right: auto;
+ }
[purpose='platform-block'] {
margin-bottom: 100px;
}
@@ -1350,6 +1413,14 @@
display: none;
}
+ [purpose='homepage-hero'] {
+ padding-top: 40px;
+ padding-bottom: 220px;
+ }
+
+ [purpose='hero-background-image'] {
+ background-size: auto 220px;
+ }
[purpose='hero-text'] {
p {
diff --git a/website/assets/styles/pages/start.less b/website/assets/styles/pages/start.less
index 89fe061bc2..9a43754c76 100644
--- a/website/assets/styles/pages/start.less
+++ b/website/assets/styles/pages/start.less
@@ -148,6 +148,10 @@
line-height: 150%;
margin-right: 24px;
width: 89px;
+ height: 45px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
}
[purpose='back-button'] {
cursor: pointer;
diff --git a/website/assets/styles/pages/vulnerability-management.less b/website/assets/styles/pages/vulnerability-management.less
index 0289fa5e29..93bdc23532 100644
--- a/website/assets/styles/pages/vulnerability-management.less
+++ b/website/assets/styles/pages/vulnerability-management.less
@@ -12,15 +12,16 @@
}
h3 {
font-weight: 800;
- font-size: 24px;
- line-height: 32px;
+ font-size: 32px;
+ line-height: 120%;
}
h4 {
font-family: 'Roboto Mono';
font-style: normal;
font-weight: 400;
- font-size: 18px;
- line-height: 24px;
+ font-size: 14px;
+ line-height: 150%;
+ text-transform: uppercase;
color: @core-fleet-black-75;
margin-bottom: 4px;
}
@@ -292,78 +293,76 @@
[parasails-component='logo-carousel'] {
margin-bottom: 80px;
}
- [purpose='calendar-section'] {
- padding: 0px 64px 40px 64px;
- margin-bottom: 80px;
- max-width: 1200px;
- margin-left: auto;
- margin-right: auto;
- }
- [purpose='calendar-card-body'] {
- width: 50%;
- padding-left: 64px;
- padding-bottom: 24px;
- margin-right: 24px;
- text-align: left;
- h3 {
- font-size: 32px;
- line-height: 120%;
- margin-bottom: 24px;
- }
- p {
- max-width: 720px;
- margin-bottom: 24px;
- margin-top: 24px;
- }
- a {
- margin-bottom: 6px;
- padding-right: 24px;
- }
- }
- [purpose='calendar-card'] {
- border-radius: 24px;
- border: 1px solid #E2E4EA;
- background: linear-gradient(165deg, #F9FDFE 10.56%, #FFF 33.43%);
- overflow: hidden;
- height: 420px;
- }
- [purpose='coming-soon-badge'] {
- background-color: #0587FF;
- padding: 4px 8px 3px 8px;
- display: flex;
- align-items: center;
- border-radius: 14px;
- color: #FFF;
- font-size: 12px;
- font-weight: 500;
- line-height: 18px;
- text-transform: uppercase;
- margin-bottom: 8px;
- }
- [purpose='calendar-image'] {
- padding-top: 32px;
- padding-bottom: 24px;
- height: 420px;
- width: 50%;
- display: flex;
- flex-direction: row;
- overflow-x: hidden;
- justify-content: center;
- position: relative;
- &:before {
- position: absolute;
- content: ' ';
- width: 55px;
- left: 0px;
- top: 0px;
- height: 420px;
- background: linear-gradient(270deg, rgba(255, 255, 255, 0.00) 0%, #FFF 100%);
- }
- img {
- height: 100%;
- width: auto;
- }
- }
+ [purpose='calendar-feature'] {
+ margin-bottom: 140px;
+ h3 {
+ margin-bottom: 24px;
+ }
+ [purpose='calendar-feature-text'] {
+ max-width: 480px;
+ }
+ [purpose='new-badge'] {
+ background-color: #0587FF;
+ padding: 4px 8px 3px 8px;
+ display: flex;
+ align-items: center;
+ border-radius: 14px;
+ color: #FFF;
+ font-size: 12px;
+ font-weight: 500;
+ line-height: 18px;
+ text-transform: uppercase;
+ margin-bottom: 12px;
+ width: min-content;
+ }
+ [purpose='calendar-checklist'] {
+ margin-top: 8px;
+ margin-bottom: 24px;
+ p {
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 21px;
+ padding-left: 37px;
+ text-indent: -37px;
+ margin-bottom: 1.5rem;
+ &:last-of-type {
+ margin-bottom: 0px;
+ }
+ }
+ p::before {
+ content: ' ';
+ background-image: url('/images/icon-checkmark-green-20x20@2x.png');
+ background-size: 20px 20px;
+ display: inline-block;
+ position: relative;
+ top: 5px;
+ margin-right: 16px;
+ width: 20px;
+ height: 20px;
+ }
+ }
+ [purpose='feature-video'] {
+ margin-left: 80px;
+ max-width: 468px;
+ video {
+ max-width: 100%;
+ max-height: 100%;
+ border-radius: 16px;
+ }
+ }
+ [purpose='video-button'] {
+ margin-top: 12px;
+ cursor: pointer;
+ img {
+ height: 32px;
+ margin-right: 8px;
+ }
+ font-size: 14px;
+ font-weight: 700;
+ line-height: 21px;
+ }
+ }
[purpose='feature'] {
margin-bottom: 180px;
@@ -469,6 +468,13 @@
[purpose='calendar-section'] {
padding: 0px 40px 40px 40px;
}
+ [purpose='calendar-feature'] {
+ [purpose='feature-video'] {
+ margin-left: auto;
+ margin-right: auto;
+ margin-bottom: 40px;
+ }
+ }
[purpose='page-content'] {
max-width: 840px;
}
diff --git a/website/assets/videos/calendar-feature-video.mp4 b/website/assets/videos/calendar-feature-video.mp4
new file mode 100755
index 0000000000..3a9934df22
Binary files /dev/null and b/website/assets/videos/calendar-feature-video.mp4 differ
diff --git a/website/config/custom.js b/website/config/custom.js
index c46e9c634b..f5c634cca3 100644
--- a/website/config/custom.js
+++ b/website/config/custom.js
@@ -91,6 +91,7 @@ module.exports.custom = {
// iqSecret: undefined, // You gotta use the base64-encoded API secret. (Get it in your account settings in LeadIQ.)
// salesforceIntegrationUsername: undefined,
// salesforceIntegrationPasskey: undefined,
+ RX_PROTOCOL_AND_COMMON_SUBDOMAINS: /^(https?\:\/\/)?(www\.|about\.|ch\.|uk\.|pl\.|ca\.|jp\.|im\.|fr\.|pt\.|vn\.)*/,// For cleaning up LinkedIn URLs before creating CRM records.
// βββββββ βββββββ βββββββββββ
// βββββββββββββββββββββββββββ
@@ -170,7 +171,7 @@ module.exports.custom = {
// π GitHub issue templates
- //'.github/ISSUE_TEMPLATE': 'mikermcneil',// Β« Covered in CODEOWNERS (2023-08-10)
+ '.github/ISSUE_TEMPLATE': 'sampfluger88',
},
@@ -262,9 +263,9 @@ module.exports.custom = {
// GitHub issue templates
'.github/ISSUE_TEMPLATE': ['mikermcneil', 'lukeheath', 'sampfluger88'],
- '.github/ISSUE_TEMPLATE/bug-report.md': ['xpkoala','noahtalerman', 'lukeheath'],
- '.github/ISSUE_TEMPLATE/feature-request.md': ['xpkoala','noahtalerman', 'lukeheath'],
- '.github/ISSUE_TEMPLATE/release-qa.md': ['xpkoala','lukeheath','noahtalerman', 'lukeheath'],
+ '.github/ISSUE_TEMPLATE/bug-report.md': ['xpkoala','noahtalerman'],
+ '.github/ISSUE_TEMPLATE/feature-request.md': ['xpkoala','noahtalerman'],
+ '.github/ISSUE_TEMPLATE/release-qa.md': ['xpkoala','noahtalerman'],
},
confidentialGithubRepoMaintainersByPath: {// fleetdm/confidential
diff --git a/website/config/routes.js b/website/config/routes.js
index 7ad2a790a0..2e84470a31 100644
--- a/website/config/routes.js
+++ b/website/config/routes.js
@@ -439,6 +439,7 @@ module.exports.routes = {
//
// For example, a clever user might try to visit fleetdm.com/documentation, not knowing that Fleet's website
// puts this kind of thing under /docs, NOT /documentation. These "convenience" redirects are to help them out.
+ 'GET /admin': '/admin/email-preview',
'GET /renew': 'https://calendly.com/zayhanlon/fleet-renewal-discussion',
'GET /documentation': '/docs',
'GET /contribute': '/docs/contributing',
@@ -493,6 +494,7 @@ module.exports.routes = {
'GET /learn-more-about/downgrading': '/docs/using-fleet/downgrading-fleet',
'GET /learn-more-about/fleetd': '/docs/get-started/anatomy#fleetd',
'GET /learn-more-about/rotating-enroll-secrets': '/docs/configuration/configuration-files#rotating-enroll-secrets',
+ 'GET /learn-more-about/calendar-events': '/announcements/fleet-in-your-calendar-introducing-maintenance-windows',
// Sitemap
// =============================================================================================================
@@ -523,6 +525,12 @@ module.exports.routes = {
'GET /swag': 'https://kqphpqst851.typeform.com/to/Y6NYxM5A',
'GET /community': 'https://join.slack.com/t/osquery/shared_invite/zt-1wkw5fzba-lWEyke60sjV6C4cdinFA1w',
+ // Temporary redirects
+ // =============================================================================================================
+ // For events, etc. that can be removed after a certain date. Please leave a comment with a valid until date.
+ 'GET /rsaparty': 'https://www.eventbrite.com/e/fleet-launch-party-at-rsac-tickets-877549332677?aff=fleetdm', // Valid until 2024-05-09
+ 'GET /rsavip': 'https://www.eventbrite.com/e/fleet-launch-party-at-rsac-tickets-877549332677?aff=fleetdm&discount=Fleet2024', // Valid until 2024-05-09
+
// β¦ β¦βββββ β¦ β¦βββββββ¦βββββ
// βββββ£ β β©ββ ββ£β ββ ββ β©ββββ
diff --git a/website/scripts/send-data-to-vanta.js b/website/scripts/send-data-to-vanta.js
index 7c3565e962..929cdd3251 100644
--- a/website/scripts/send-data-to-vanta.js
+++ b/website/scripts/send-data-to-vanta.js
@@ -63,7 +63,7 @@ module.exports = {
{'Authorization': 'Bearer '+updatedRecord.fleetApiKey }
)
.tolerate((err)=>{// If an error occurs while sending a request to the Fleet instance, we'll add the error to the errorReportById object, with this connections ID set as the key.
- errorReportById[connectionIdAsString] = new Error(`When sending a request to the /users endpoint of a Fleet instance for a VantaConnection (id: ${connectionIdAsString}), the Fleet instance returned an Error: ${err}`);
+ errorReportById[connectionIdAsString] = new Error(`When sending a request to the /users endpoint of a Fleet instance for a VantaConnection (id: ${connectionIdAsString}), the Fleet instance returned an Error: ${util.inspect(err.raw)}`);
});
if(errorReportById[connectionIdAsString]){// If there was an error with the previous request, bail early for this Vanta connection.
@@ -199,7 +199,7 @@ module.exports = {
)
.retry()
.intercept((err)=>{// If an error occurs while sending a request to the Fleet instance, we'll throw an error.
- return new Error(`When sending a request to the Fleet instance's /hosts/${host.id} endpoint for a Vanta connection (id: ${connectionIdAsString}), an error occurred: ${err}`);
+ return new Error(`When sending a request to the Fleet instance's /hosts/${host.id} endpoint for a Vanta connection (id: ${connectionIdAsString}), an error occurred: ${util.inspect(err.raw)}`);
});
if(!detailedInformationAboutThisHost.host) {
@@ -287,7 +287,7 @@ module.exports = {
)
.retry()
.intercept((err)=>{// If an error occurs while sending a request to the Fleet instance, we'll throw an error.
- return new Error(`When sending a request to the Fleet instance's /hosts/${host.id} endpoint for a Vanta connection (id: ${connectionIdAsString}), an error occurred: ${err}`);
+ return new Error(`When sending a request to the Fleet instance's /hosts/${host.id} endpoint for a Vanta connection (id: ${connectionIdAsString}), an error occurred: ${util.inspect(err.raw)}`);
});
if(!detailedInformationAboutThisHost.host){
@@ -353,7 +353,7 @@ module.exports = {
})
.retry();
} catch(error) {
- errorReportById[connectionIdAsString] = new Error(`vantaError: When sending a PUT request to the Vanta's '/user_account/sync_all' endpoint for a Vanta connection (id: ${connectionIdAsString}), an error occurred: ${error.stack}`);
+ errorReportById[connectionIdAsString] = new Error(`vantaError: When sending a PUT request to the Vanta's '/user_account/sync_all' endpoint for a Vanta connection (id: ${connectionIdAsString}), an error occurred: ${util.inspect(error.raw)}`);
}
if(errorReportById[connectionIdAsString]){// If an error occured in the previous request, we'll bail early for this connection.
@@ -381,7 +381,7 @@ module.exports = {
})
.retry();
} catch (error) {
- errorReportById[connectionIdAsString] = new Error(`vantaError: When sending a PUT request to the Vanta's '/macos_user_computer/sync_all' endpoint for a Vanta connection (id: ${connectionIdAsString}), an error occurred: ${error.stack}`);
+ errorReportById[connectionIdAsString] = new Error(`vantaError: When sending a PUT request to the Vanta's '/macos_user_computer/sync_all' endpoint for a Vanta connection (id: ${connectionIdAsString}), an error occurred: ${util.inspect(error.raw)}`);
}
if(errorReportById[connectionIdAsString]){// If an error occured in the previous request, we'll bail early for this connection.
@@ -409,7 +409,7 @@ module.exports = {
})
.retry();
} catch (error) {
- errorReportById[connectionIdAsString] = new Error(`vantaError: When sending a PUT request to the Vanta's '/macos_user_computer/sync_all' endpoint for a Vanta connection (id: ${connectionIdAsString}), an error occurred: ${error.stack}`);
+ errorReportById[connectionIdAsString] = new Error(`vantaError: When sending a PUT request to the Vanta's '/macos_user_computer/sync_all' endpoint for a Vanta connection (id: ${connectionIdAsString}), an error occurred: ${util.inspect(error.raw)}`);
}
if(errorReportById[connectionIdAsString]){// If an error occured in the previous request, we'll bail early for this connection.
@@ -428,7 +428,7 @@ module.exports = {
} else {
// If an error was logged for a VantaConnection, log the error, and increment the numberOfLoggedErrors
numberOfLoggedErrors++;
- sails.log.warn('An error occurred while syncing the vanta connection for VantaCustomer with id '+connectionIdAsString+'. Logged error:\n'+errorReportById[connectionIdAsString]);
+ sails.log.warn('p1: An error occurred while syncing the vanta connection for VantaCustomer with id '+connectionIdAsString+'. Logged error:\n'+errorReportById[connectionIdAsString]);
}
}//β
diff --git a/website/views/pages/device-management.ejs b/website/views/pages/device-management.ejs
index 63c80fde9e..11fbfd83f0 100644
--- a/website/views/pages/device-management.ejs
+++ b/website/views/pages/device-management.ejs
@@ -25,28 +25,27 @@