Merge branch 'main' into feat-labels-scoped-software

This commit is contained in:
Gabriel Hernandez 2024-12-16 10:40:19 -06:00
commit 8ecf75ae2b
189 changed files with 3400 additions and 1043 deletions

View file

@ -44,7 +44,7 @@ What else should contributors [keep in mind](https://fleetdm.com/handbook/compan
- [ ] YAML changes: TODO <!-- Specify changes in the YAML files doc page as a PR to the reference docs release branch. Put "No changes" if there are no changes necessary. -->
- [ ] REST API changes: TODO <!-- Specify changes in the the REST API doc page as a PR to reference docs release branch. Put "No changes" if there are no changes necessary. Move this item to the engineering list below if engineering will design the API changes. -->
- [ ] Fleet's agent (fleetd) changes: TODO <!-- Specify changes to fleetd. If the change requires a new Fleet (server) version, consider specifying to only enable this change in new Fleet versions. Put "No changes" if there are no changes necessary. -->
- [ ] Activity changes: TODO <!-- Specify changes to Fleet's activity feed as a draft PR to the Audit log page in the contributor docs: https://github.com/fleetdm/fleet/blob/main/docs/Contributing/Audit-logs.md This PR will be closed before release because the Audit log page is automatically generated: https://fleetdm.com/handbook/company/communications#audit-logs Put "No changes" if there are no changes necessary. -->
- [ ] Activity changes: TODO <!-- Specify changes to the Audit log page in the contributor docs. Put "No changes" if there are no changes necessary. -->
- [ ] Permissions changes: TODO <!-- Specify changes in the Manage access doc page as a PR to the reference docs release branch. If doc changes aren't necessary, explicitly mention no changes to the doc page. Put "No changes" if there are no permissions changes. -->
- [ ] Changes to paid features or tiers: TODO <!-- Specify changes in pricing-features-table.yml as a PR to reference docs release branch. Specify "Fleet Free" and/or "Fleet Premium" if there are no changes to the pricing page necessary. -->
- [ ] Other reference documentation changes: TODO <!-- Any other reference doc changes? Specify changes as a PR to reference docs release branch. Put "No changes" if there are no changes necessary. -->

View file

@ -0,0 +1,8 @@
{
"mysql_matrix": [
"mysql:8.0.36"
],
"pkg_to_test": "server/service",
"tests_to_run": "^TestIntegrationsMDM\\$$",
"num_tries": 20
}

View file

@ -19,7 +19,7 @@ defaults:
shell: bash
env:
FLEET_DESKTOP_VERSION: 1.36.0
FLEET_DESKTOP_VERSION: 1.37.0
permissions:
contents: read

View file

@ -62,11 +62,6 @@ jobs:
AC_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
CODESIGN_IDENTITY: 51049B247B25B3119FAE7E9C0CC4375A43E47237
- name: Attest binary
uses: actions/attest-build-provenance@619dbb2e03e0189af0c55118e7d3c5e129e99726 # v2.0
with:
subject-path: "dist/orbit-macos_darwin_all/orbit"
- name: Upload
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # 4.3.3
with:
@ -100,11 +95,6 @@ jobs:
- name: Run GoReleaser
run: go run github.com/goreleaser/goreleaser/v2@606c0e724fe9b980cd01090d08cbebff63cd0f72 release --verbose --clean --skip=publish -f orbit/goreleaser-linux.yml # v2.4.4
- name: Attest binary
uses: actions/attest-build-provenance@619dbb2e03e0189af0c55118e7d3c5e129e99726 # v2.0
with:
subject-path: "dist/orbit_linux_amd64_v1/orbit"
- name: Upload
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # 4.3.3
with:
@ -138,16 +128,11 @@ jobs:
- name: Run GoReleaser
run: go run github.com/goreleaser/goreleaser/v2@606c0e724fe9b980cd01090d08cbebff63cd0f72 release --verbose --clean --skip=publish -f orbit/goreleaser-linux-arm64.yml # v2.4.4
- name: Attest binary
uses: actions/attest-build-provenance@619dbb2e03e0189af0c55118e7d3c5e129e99726 # v2.0
with:
subject-path: "dist/orbit_linux_arm64/orbit"
- name: Upload
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # 4.3.3
with:
name: orbit-linux-arm64
path: dist/orbit_linux_arm64/orbit
path: dist/orbit_linux_arm64_v8.0/orbit
goreleaser-windows:
runs-on: windows-2022
@ -176,11 +161,6 @@ jobs:
- name: Run GoReleaser
run: go run github.com/goreleaser/goreleaser/v2@606c0e724fe9b980cd01090d08cbebff63cd0f72 release --verbose --clean --skip=publish -f orbit/goreleaser-windows.yml # v2.4.4
- name: Attest binary
uses: actions/attest-build-provenance@619dbb2e03e0189af0c55118e7d3c5e129e99726 # v2.0
with:
subject-path: "dist/orbit_windows_amd64_v1/orbit.exe"
- name: Upload
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # 4.3.3
with:

150
.github/workflows/randokiller-go.yml vendored Normal file
View file

@ -0,0 +1,150 @@
name: Stress Test Go Test
on:
push:
branches:
- "**-randokiller"
paths:
- "**.go"
- "go.mod"
- "go.sum"
- ".github/workflows/randokiller-go.yml"
- "docker-compose.yml"
- ".github/workflows/config/randokiller.json"
workflow_dispatch: # manual
# This allows a subsequently queued workflow run to interrupt previous runs
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id}}
cancel-in-progress: true
defaults:
run:
# fail-fast using bash -eo pipefail. See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#exit-codes-and-error-action-preference
shell: bash
permissions:
contents: read
jobs:
parse_config:
runs-on: ubuntu-latest
outputs:
json: ${{steps.get_config_json.outputs.json}}
steps:
- name: Checkout Code
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
- name: Parse Config
id: get_config_json
run: echo "json=$(jq -c . < .github/workflows/config/randokiller.json)" >> $GITHUB_OUTPUT
test-go:
needs: parse_config
strategy:
matrix:
os: [ubuntu-latest]
mysql: ${{fromJson(needs.parse_config.outputs.json).mysql_matrix}}
runs-on: ${{ matrix.os }}
env:
RACE_ENABLED: false
GO_TEST_TIMEOUT: 20m
steps:
- name: Harden Runner
uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0
with:
egress-policy: audit
- name: Checkout Code
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
- name: Install Go
uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2
with:
go-version-file: "go.mod"
# Pre-starting dependencies here means they are ready to go when we need them.
- name: Start Infra Dependencies
# Use & to background this
run: FLEET_MYSQL_IMAGE=${{ matrix.mysql }} docker compose -f docker-compose.yml -f docker-compose-redis-cluster.yml up -d mysql_test mysql_replica_test redis redis-cluster-1 redis-cluster-2 redis-cluster-3 redis-cluster-4 redis-cluster-5 redis-cluster-6 redis-cluster-setup minio saml_idp mailhog mailpit smtp4dev_test &
- name: Add TLS certificate for SMTP Tests
run: |
sudo cp tools/smtp4dev/fleet.crt /usr/local/share/ca-certificates/
sudo update-ca-certificates
# It seems faster not to cache Go dependencies
- name: Install Go Dependencies
run: make deps-go
- name: Generate static files
run: |
export PATH=$PATH:~/go/bin
make generate-go
- name: Install ZSH
run: sudo apt update && sudo apt install -y zsh
- name: Wait for mysql
run: |
echo "waiting for mysql..."
until docker compose exec -T mysql_test sh -c "mysql -uroot -p\"\${MYSQL_ROOT_PASSWORD}\" -e \"SELECT 1=1\" fleet" &> /dev/null; do
echo "."
sleep 1
done
echo "mysql is ready"
echo "waiting for mysql replica..."
until docker compose exec -T mysql_replica_test sh -c "mysql -uroot -p\"\${MYSQL_ROOT_PASSWORD}\" -e \"SELECT 1=1\" fleet" &> /dev/null; do
echo "."
sleep 1
done
echo "mysql replica is ready"
- name: Run Go Tests
run: |
set +e
counter=0
NUM_TRIES=${{ fromJSON(needs.parse_config.outputs.json).num_tries }}
while [ $counter -lt ${NUM_TRIES:-20} ]; do
((counter++))
echo
echo "----- TEST RUN #$counter -----"
echo
GO_TEST_EXTRA_FLAGS="-v -race=$RACE_ENABLED -timeout=$GO_TEST_TIMEOUT" \
TEST_LOCK_FILE_PATH=$(pwd)/lock \
TEST_CRON_NO_RECOVER=1 \
NETWORK_TEST_GITHUB_TOKEN=${{ secrets.FLEET_RELEASE_GITHUB_PAT }} \
make run-go-tests TESTS_TO_RUN=${{ fromJSON(needs.parse_config.outputs.json).tests_to_run }} PKG_TO_TEST=${{ fromJSON(needs.parse_config.outputs.json).pkg_to_test }} 2>&1 | tee /tmp/gotest.log
exit_code=$?
if [ $exit_code -ne 0 ]; then
echo "Test run #$counter failed with exit code $exit_code"
exit $exit_code
fi
done
- name: Create mysql identifier without colon
if: always()
run: |
echo "MATRIX_MYSQL_ID=$(echo ${{ matrix.mysql }} | tr -d ':')" >> $GITHUB_ENV
- name: Generate summary of errors
if: failure()
run: |
c1grep() { grep "$@" || test $? = 1; }
c1grep -oP 'FAIL: .*$' /tmp/gotest.log > /tmp/summary.txt
c1grep 'test timed out after' /tmp/gotest.log >> /tmp/summary.txt
c1grep 'fatal error:' /tmp/gotest.log >> /tmp/summary.txt
c1grep -A 10 'panic: runtime error: ' /tmp/gotest.log >> /tmp/summary.txt
c1grep ' FAIL\t' /tmp/gotest.log >> /tmp/summary.txt
GO_FAIL_SUMMARY=$(head -n 5 /tmp/summary.txt | sed ':a;N;$!ba;s/\n/\\n/g')
echo "GO_FAIL_SUMMARY=$GO_FAIL_SUMMARY"
if [[ -z "$GO_FAIL_SUMMARY" ]]; then
GO_FAIL_SUMMARY="unknown, please check the build URL"
fi
GO_FAIL_SUMMARY=$GO_FAIL_SUMMARY envsubst < .github/workflows/config/slack_payload_template.json > ./payload.json

View file

@ -41,8 +41,8 @@ jobs:
- name: Install terraform
uses: hashicorp/setup-terraform@633666f66e0061ca3b725c73b2ec20cd13a8fdd1 # v2.0.3
with:
terraform_version: 1.3.0
terraform_version: 1.9.0
# If we want to test more of these, consider using a matrix. With a matrix of directories, all terraform modules could be fully tested and potentially in parallel.
- name: Validate loadtesting
working-directory: ./infrastructure/loadtesting/terraform
run: |
@ -54,3 +54,15 @@ jobs:
run: |
terraform init -backend=false
terraform validate
- name: Validate dogfood
working-directory: ./infrastructure/dogfood/terraform/aws-tf-module
run: |
terraform init -backend=false
terraform validate
- name: Validate root terraform module
working-directory: ./terraform
run: |
terraform init -backend=false
terraform validate

View file

@ -65,9 +65,9 @@ go.mod @fleetdm/go
#
# (see website/config/custom.js for DRIs of other paths not listed here)
##############################################################################################
/docs @rachaelshaw @noahtalerman
/docs/REST\ API/rest-api.md @rachaelshaw @noahtalerman # « REST API reference documentation
/docs/Contributing/API-for-contributors.md @rachaelshaw @noahtalerman # « Advanced / contributors-only API reference documentation
/docs @rachaelshaw
/docs/REST\ API/rest-api.md @rachaelshaw # « REST API reference documentation
/docs/Contributing/API-for-contributors.md @rachaelshaw # « Advanced / contributors-only API reference documentation
/schema @eashaw # « Data tables (osquery/fleetd schema) documentation
/render.yaml @edwardsb

View file

@ -1,31 +1,39 @@
# Leading financial company consolidates multiple tools with Fleet
<div purpose="attribution-quote">
We've been using Fleet for a few years and we couldn't be happier. The fact that it's also open-source made it easy for us to try it out, customize it to our needs, and seamlessly integrate it into our existing environment.
**- Head of Developer Infrastructure & Corporate Technology**
</div>
## Challenge
Scaling organizations face a common challenge: managing every device efficiently across varying teams and locations. Here, we take a deeper look at the impact that Fleet has made at a leading financial technology company.
## Fleets impact
The leading financial company looked to simplify how they manage devices and reduce tool overlap without sacrificing control over their infrastructure. The use of multiple proprietary device management tools was creating operational silos, and it required specialized expertise for different legacy systems, leading to inefficiencies.
* **Eliminate tool overlap:**
Fleet reduced tool overlap by consolidating multiple legacy solutions - improving efficiency and reducing SaaS spending without compromising functionality.
## Solution
* **Next-gen change management:**
GitOps capabilities reduce mistakes through peer reviews and keep track of changes for faster auditing.
The leading financial company migrated to Fleet without sacrificing a single point of their 99.99% uptime, replacing multiple device management suppliers with a single multi-platform system supporting macOS, desktop Linux, and Windows. They also implemented next-change management, reducing mistakes through peer reviews, and using the user interface and [Fleet API](https://fleetdm.com/docs/rest-api/rest-api) for reporting, automation, and to enable smarter end-user self-service.
* **Definitive data:**
Reliable, live access to their infrastructure to verify device data for better decisions surrounding end-user access and auditing context.
## Results
* **Seamless customization and integration:**
Electing to self-host Fleet, and as the company continued to scale, they did so without a single point to their impressive 99.99% uptime.
<div purpose="checklist">
"We've been using Fleet for a few years and we couldn't be happier. The fact that it's also open-source made it easy for us to try it out, customize it to our needs, and seamlessly integrate it into our existing environment." - Head of Developer Infrastructure & Corporate Technology
Reduced tool overlap by consolidating multiple legacy solutions, improving efficiency and reducing SaaS spending without compromising functionality.
**Challenge:** They looked to simplify how they manage devices and reduce tool overlap without sacrificing control over their infrastructure. The use of multiple proprietary device management tools was creating operational silos, and it required specialized expertise for different legacy systems, leading to inefficiencies.
Adopted Fleets GitOps capabilities to help reduce mistakes through peer reviews and keep track of changes for faster auditing.
**Solution:** The leading financial company migrated to Fleet without sacrificing a single point of their 99.99% uptime, replacing multiple device management suppliers with a single multi-platform system supporting macOS, desktop Linux, and Windows. They also implemented next-change management, reducing mistakes through peer reviews, and using the user interface and [Fleet API](https://fleetdm.com/docs/rest-api/rest-api) for reporting, automation, and to enable smarter end-user self-service.
Use Fleet to get reliable, live access to their infrastructure to verify device data for better decisions surrounding end-user access and auditing context.
**Impact:** They saw a reduction in wasted time by unblocking data collection for audits and overcame change inertia, allowing IT to move faster with less maintenance through convention over configuration and bare metal access to every supported platform, including Apple and desktop Linux. In this way, they were able to offer employees device choice without adding to their risk register.
Elected to self-host Fleet, and as the company continued to scale, they did so without a single point to their impressive 99.99% uptime.
</div>
By switching to Fleet, the leading financial company saw a reduction in wasted time by unblocking data collection for audits and overcame change inertia, allowing IT to move faster with less maintenance through convention over configuration and bare metal access to every supported platform, including Apple and desktop Linux. In this way, they were able to offer employees device choice without adding to their risk register.
## The challenge
## Their story
This company is a global technology company building economic infrastructure for the Internet. Businesses of every size, from new startups to public companies, use its software to accept payments and manage their businesses online and in person.
@ -33,39 +41,30 @@ As they expanded, it faced a growing complexity in managing a vast array of devi
To address these challenges, they set out to achieve four key goals:
- **Reduce tool overlap:** Replace multiple tools with a single solution that supports macOS, Windows, and Linux, ensuring consistency across all platforms.
- **Adopt next-generation change management:** Leverage GitOps workflows for tasks such as deploying configuration profiles, delivering MDM commands, updating custom settings, and reporting on application installations.
- **Streamline device health assessments:** Enable quick access to asset data to evaluate device health and make informed network access decisions efficiently.
- **Empower end-user self-service:** Provide users with clear instructions to resolve common issues independently, reducing dependence on IT teams.
## The solution
- Reduce tool overlap
- Adopt next-generation change management
- Streamline device health assessments
- Empower end-user self-service
The company was already using Fleet in early 2023 to manage osquery from a threat detection and compliance perspective with [scheduled queries](https://fleetdm.com/guides/queries). However, they mentioned the growing need to quickly reach out to users to educate them on enabling compliance checks.
Not soon after in April 2023, Fleet announced open-source, [cross-platform MDM capabilities](https://www.computerworld.com/article/1622574/fleet-announces-open-source-cross-platform-mdm-solution.html) building on top of osquery which they were already familiar with. Seeing this as an opportunity to leverage Fleet and reduce the amount of tools they had to manage. Fleet's combination of cross-platform support, open-source transparency, and scalability made it worthwhile to migrate MDMs.
Not long after, Fleet announced open-source, [cross-platform MDM capabilities](https://www.computerworld.com/article/1622574/fleet-announces-open-source-cross-platform-mdm-solution.html) building on top of osquery which they were already familiar with. Seeing this as an opportunity to leverage Fleet and reduce the amount of tools they had to manage. Fleet's combination of cross-platform support, open-source transparency, and scalability made it worthwhile to migrate MDMs.
### Eliminate tool overlap with easy deployment
"Mad props to how easy making a deploy pkg of the agent was. I wish everyone made stuff that easy."
— Staff Client Platform Engineer
<div purpose="attribution-quote">
Mad props to how easy making a deploy pkg of the agent was. I wish everyone made stuff that easy.
**— Staff Client Platform Engineer**
</div>
Fleet's straightforward deployment package allowed a quick install of the agent across all of its devices. By supporting macOS, Windows, and Linux, Fleet enabled them to not only continue managing osquery but also consolidate its legacy device management tools into a single self-hosted MDM without sacrificing existing control.
### Next-gen change management and open-source flexibility
"We've been using Fleet for a few years and we couldn't be happier. The fact that it's also open-source made it easy for us to try it out, customize it to our needs, and seamlessly integrate it into our existing environment." — Head of Developer Infrastructure & Corporate Technology
Being [open-source](http://fleetdm.com/handbook/company/why-this-way?utm_content=eo-security#why-open-source), Fleet provided the transparency and flexibility to tailor the platform to their specific requirements. This fostered trust among engineering teams and allows them to audit, customize, and extend the platform as needed.
**Fleet's next-gen change management capabilities:**
- Enforce custom settings updates and deployment: Manage custom settings across all devices using GitOps workflows.
- Implement change control: Reduce mistakes through peer reviews.
- Deploy configuration profiles to macOS devices: Update system settings and controls on macOS devices efficiently.
- Deliver MDM commands: Manage and execute MDM commands like lock, sleep, and wipe.
- Report on installed applications and versions: Generate comprehensive reports on installed applications, aiding in software management and compliance checks.
### Definitive data and end-user empowerment
Fleet can pull detailed information on assets across every operating system in seconds, allowing quick assessments of device health, installed applications, and verified configurations. Because Fleet is API-first, programmable, and built for automation, they configure all of their devices to access its network but only if they passed its predetermined policies.
@ -82,6 +81,9 @@ Fleet's cross-platform support and open-source transparency set it apart from co
To learn more about how Fleet can support your organization, visit [fleetdm.com/mdm](https://fleetdm.com/mdm).
<call-to-action></call-to-action>
<meta name="category" value="announcements">
<meta name="authorGitHubUsername" value="Drew-P-drawers">
<meta name="authorFullName" value="Andrew Baker">

View file

@ -0,0 +1,178 @@
## Introduction
Deploying Configuration Service Providers (CSPs) for Windows devices—the Windows equivalent of Apple's configuration profiles—can feel daunting, especially if you're new to the process or accustomed to ClickOps and other UI-driven approaches. The scarcity of straightforward documentation and guides can make it feel like you're venturing into a configuration rabbit hole.
![Rabbit Hole](../website/assets/images/articles/down-the-rabbit-hole.png)
This guide will help you understand the building blocks to crafting CSPs of varying complexity from simple payloads to more complex ones that involve modification of ADMX underpinnings.
> In Fleet, Windows CSPs are called "Custom OS settings." Learn more about Custom OS settings [here](https://fleetdm.com/guides/custom-os-settings).
## What is ADMX and why should you care?
From Microsoft:
>“Mobile Device Management (MDM) policy configuration support expanded to allow access of a selected set of Group Policy administrative templates (ADMX policies) for Windows PCs via the Policy configuration service provider (CSP). This expanded access ensures that enterprises can keep their devices compliant and prevent the risk of compromising security of their devices managed through the cloud.”
In an ADMX policy, an administrative template contains the metadata of a Windows Group Policy. Each setting in a Group Policy corresponds to a specific registry value. These Group Policy settings are defined in an XML file format known as an ADMX file. MDM doesnt use the same tools as Group Policy, the traditional way to control Windows settings. Instead, it uses the Policy CSP to read the ADMX instructions.
ADMX policies can be applied in two ways:
1. Shipped with Windows, located at ```SystemRoot\policydefinitions``` and processed into MDM policies at OS-build time
2. Ingested to a device through the Policy CSP URI, for example, ```./Vendor/MSFT/Policy/ConfigOperations/ADMXInstall``` which we will cover further on in this guide.
Windows maps the name and category path of a Group Policy to an MDM policy by parsing the associated ADMX file, finding the specified Group Policy, and storing the metadata in the MDM Policy CSP client store. When the MDM policy contains a SyncML command AND the Policy CSP URI ```.\[device|user]\vendor\msft\policy\[config|result]\<area>\<policy>```, this metadata is referenced and determines which registry keys are configured.
## Unpacking the “How”
Unfortunately, to capture handling of ADMX the admin building the policies must use a UI, such as the Group Policy Editor, to gather the necessary data. For this example, we will use the ```WindowsPowerShell``` which controls PowerShell settings and is an ADMX-backed policy. [This](https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-windowspowershell) is the official documentation that we will work from if you want to follow along. Notice this banner that indicates the ADMX requirement:
![ADMX Tool Tip](../website/assets/images/articles/admx-tool-tip.png)
In the Windows documentation, you will notice a section called ADMX Mapping:
![ADMX Mapping](../website/assets/images/articles/admx-mapping.png)
Pay attention to the line **ADMX File Name**, which will show you the name of the .admx file you need to open to help craft your CSP. All ADMX files are located at:
```C:\Windows\PolicyDefinitions\{ADMXFileName.admx}```
In this XML file are the keys, and their type listed which indicates the values the CSP can modify. In this example there are 5 parameters:
- ExecutionPolicy
- EnableModuleLogging
- EnableTranscripting
- EnableScriptBlockLogging
- EnableUpdateHelpDefaultSourcePath
Values can take one of the following types:
1. Text Element - The text element simply corresponds to a string and correspondingly to an edit box in a policy panel display by gpedit. The string is stored in the registry of type REG_SZ.
2. MultiText Element - The multiText element simply corresponds to a REG_MULTISZ registry string and correspondingly to a grid to enter multiple strings in a policy panel display by gpedit.
3. List Element - The list element corresponds to a hive of REG_SZ registry strings and correspondingly to a grid to enter multiple strings in a policy panel display by gpedit.msc.
- PRO TIP: Each pair is a REG_SZ name/value key. When applying policies through gpedit, visit the corresponding registry location to understand how list values are stored.
4. No Elements - Just an Enable/Disable of the policy, represented like `<Enabled/>`
5. Enum - Think of these as options from a dropdown in gpedit. This is the data type we are working with in the example
6. Decimal Element
7. Boolean Element
At this point in the build we know the ADMX keys for this specific policy, which values those keys accept, and now to translate that into a CSP that Fleet can interpret.
You can also see in the group policy editor the values that are being modified by the profile.
![GPEDIT](../website/assets/images/articles/gpedit-example.png)
In this example, we will modify the ExecutionPolicy value, which in group policy editor translates to “Turn on Script Execution”, the XML from the .admx looks like such:
```
<enum id="ExecutionPolicy" valueName="ExecutionPolicy" required="true">
<item displayName="$(string.AllScriptsSigned)">
<value>
<string>AllSigned</string>
</value>
</item>
<item displayName="$(string.RemoteSignedScripts)">
<value>
<string>RemoteSigned</string>
</value>
</item>
<item displayName="$(string.AllScripts)">
<value>
<string>Unrestricted</string>
</value>
```
To write this into the CSP, pull out the `id`, “ExecutionPolicy”, and the accepted values - an enum type with one of three possibilities, and use the `value` tag to create an entry in the CSP that looks like this:
```<data id="ExecutionPolicy" value="AllSigned"/>```
Going through all the keys in this policies, the payload will look like such:
```
<data id="ExecutionPolicy" value="AllSigned"/>
<data id="Listbox_ModuleNames" value="*"/>
<data id="OutputDirectory" value="false"/>
<data id="EnableScriptBlockInvocationLogging" value="true"/>
<data id="SourcePathForUpdateHelp" value="false"/>
```
>Data passed in these CSPs need to be represented in the CDATA format.
>CDATA (Character Data) is a section that allows including text data that should not be treated as XML markup. This is particularly useful for embedding content that might otherwise be misinterpreted as XML elements or attributes, such as raw text.
>A CDATA section starts with ```<![CDATA[ and ends with ]]>```. The text between these delimiters is treated as character data, not as XML.
Our block looks like:
```
<![CDATA[<enabled/><data id="ExecutionPolicy" value="AllSigned"/>
<data id="Listbox_ModuleNames" value="*"/>
<data id="OutputDirectory" value="false"/>
<data id="EnableScriptBlockInvocationLogging" value="true"/>
<data id="SourcePathForUpdateHelp" value="false"/>]]>
```
Note, I added the `<enabled/>` at the start of the block, this is required to enable the settings overall. All CSPs will at least need to have that `<enabled/>`. And its possible, depending on the setting, that this is the only key.
Now that we have the data block, we can finally put it all together to generate a CSP.
```
<Add>
<Item>
<Meta>
<Format xmlns="syncml:metinf">chr</Format>
</Meta>
<Target>
<LocURI>./Device/Vendor/MSFT/Policy/Config/WindowsPowerShell/TurnOnPowerShellScriptBlockLogging</LocURI>
</Target>
<Data>
<![CDATA[<enabled/><data id="ExecutionPolicy" value="AllSigned"/>
<data id="Listbox_ModuleNames" value="*"/>
<data id="OutputDirectory" value="false"/>
<data id="EnableScriptBlockInvocationLogging" value="true"/>
<data id="SourcePathForUpdateHelp" value="false"/>]]>
</Data>
</Item>
</Add>
```
Since were working with an ADMX-backed setting, the `Format` section needs to be
```<Format xmlns="syncml:metinf">chr</Format>```
The LocURL is listed in the CSP documentation and the `<Data>` block goe after in the CDATA format.
Pay attention to the verbs `<Add>` vs `<Replace>` when creating as these need to match the system configuration we are targeting or it oftentimes will fail.
## Debugging
There are a couple of ways to start troubleshooting issues with MDM.
In the Settings > Accounts > Access work or school > Click on Connected by Fleet and then Info > Create Report. This will generate a snapshot of the device, policies it has ingested and ADMX keys that have been modified.
Windows Event Logs can also be a helpful place to look.
Applications and Service Logs > Microsoft > Windows > DeviceManagement-Enterprise-Diagnostics-Provider.
The `Admin` logs will show you all profiles that have been pushed to the device and their status. It helps to use the `Find` function to look for keywords in your profile to narrow your search. Here is an example of the logs that show when the CSP we created here was deployed.
![Windows Event Logs](../website/assets/images/articles/windows-event-log.png)
[This](https://blog.mindcore.dk/2022/09/intune-error-codes-and-solutions/) blog post can also help you translate error codes that are present here.
## Conclusion
Deploying CSPs for your Windows fleet may seem complex at first, but with a structured approach and the right tools, it becomes a powerful way to standardize and secure your device configurations. Fleet provides the flexibility to deploy MDM profiles across platforms from a single, centralized management platform, enabling IT teams to maintain consistency, security, and efficiency.
Ready to take control of your Windows device configurations? Explore Fleets powerful API and GitOps workflows to start building and managing your CSPs today. Visit fleetdm.com to learn more or get in touch with our team to see how we can help streamline your device management strategy.
<meta name="articleTitle" value="Creating Windows CSPs">
<meta name="authorFullName" value="Harrison Ravazzolo">
<meta name="authorGitHubUsername" value="harrisonravazzolo">
<meta name="category" value="guides">
<meta name="publishedOn" value="2024-12-12">
<meta name="description" value="Learn how to create and deploy Windows CSPs with Fleet, leveraging ADMX-backed policies for streamlined and secure device management.">

View file

@ -0,0 +1,89 @@
# Foursquare quickly migrates to Fleet for device management
<div purpose="attribution-quote">
“After several fast-paced weeks of planning, testing, and collaboration, our corporate engineering team at Foursquare has officially completed the migration to Fleet Device Management as our new device management platform. This move represents a big step forward for us.”
**— Mike Meyer, Manager, Corporate Engineering at Foursquare**
</div>
## Challenge
[Foursquare](https://foursquare.com/), a leading location technology company, faced significant challenges with their existing device management solution. The platform's limited support and slow feature rollout hindered innovation and responsiveness, while its inflexible integrations with solutions like Okta Workflows and external logging destinations constrained infrastructure flexibility. Additionally, the lack of advanced DevOps capabilities and a complex, cluttered user interface resulted in operational inefficiencies and a poor user experience for their teams.
## Solution
Foursquare recently migrated to Fleet to unify their macOS and Windows devices into a single platform. Fleet also enabled infrastructure-as-code workflows and flexible [integrations](https://fleetdm.com/integrations) with their automation workflows and external logging systems. Additionally, Fleet offers advanced query capabilities, a user-friendly interface, and robust support, which led to an easy and quick migration.
## Results
<div purpose="checklist">
Foursquare unified its cross-platform management for macOS and Windows.
Introduced new infrastructure as code workflows.
Improved observability and vulnerability management through Fleets live query engine and open API.
Smooth and seamless migration.
</div>
The Foursquare engineering team valued Fleet's cleaner and more intuitive user interface, especially when managing multiple operating systems. They appreciate Fleets commitment to [transparency](https://fleetdm.com/better), enabling full visibility for both the team and their users, and dedication to providing industry-leading support. Fleet also provided ready-to-use GUI tools that streamlined the [migration](https://fleetdm.com/guides/seamless-mdm-migration) process without additional development.
## Their story
Foursquare is a global technology company specializing in combining the physical world with the digital through location intelligence and data science. By leveraging extensive location data, Foursquare provides businesses with actionable insights to drive engagement, improve customer experiences, and optimize operations.
They were looking to:
- Eliminate tool overlap.
- Centralize definitive data.
- Provide a better user experience.
- Integrate and surface data across teams.
- Enable next-gen change management and open-source flexibility.
### Eliminate tool overlap
Foursquare was able to replace multiple legacy tools with a single, [multi-platform](https://fleetdm.com/device-management) system, reducing tool overlap and operational silos.
### Definitive data
Fleet improves observability and [vulnerability management](https://fleetdm.com/software-management) through live query capabilities and more [user-friendly APIs](https://fleetdm.com/docs/rest-api/rest-api). These improvements in device monitoring strengthened Foursquares overall security posture with a unified reporting language.
### Better user experience
<div purpose="attribution-quote">
“The UI is cleaner, and much simpler to navigate, truly a breath of fresh air.”
**— Mike Meyer, Manager, Corporate Engineering at Foursquare**
</div>
An intuitive UI and self-service tools help empower end-users where they can resolve common policy issues without IT intervention, reducing support tickets and increasing efficiency. For the migration itself, Fleet provided native tools for end-users to understand and complete the migration with full transparency.
### Next-gen change management and flexibility
Fleet provides the flexibility for engineering teams to audit, customize, and extend the platform as needed with infrastructure-as-code workflows, making device management more agile and automated. While Fleets ability to support additional platforms, like Linux, positions Foursquare for future growth and diversification.
### Improved support and feature access
Fleet is open-source and has a three-week [release schedule](https://github.com/fleetdm/fleet/releases) that quickly rolls out new features like automated software updates, VPP app support, and [policy-based scripts](https://fleetdm.com/guides/policy-automation-run-script). Faster rollouts and best-in-class support from Fleet enable Foursquare to stay ahead of their device management needs.
## Conclusion
Foursquares migration to Fleet for device management highlights its commitment to leveraging advanced, flexible, and user-friendly tools to support its infrastructure. Fleets forward-thinking feature set, superior support, and emphasis on transparency not only resolved the limitations faced with their previous platform but also positioned Foursquare to better support their teams and build smarter, more efficient workflows.
<call-to-action></call-to-action>
<meta name="category" value="announcements">
<meta name="authorGitHubUsername" value="Drew-P-drawers">
<meta name="authorFullName" value="Andrew Baker">
<meta name="publishedOn" value="2024-12-13">
<meta name="articleTitle" value="Foursquare quickly migrates to Fleet for Device Management">
<meta name="description" value="Foursquare quickly migrates to Fleet for Device Management">

View file

@ -1,31 +1,39 @@
# Global edge cloud platform simplifies device management with Fleet
_“We had a different MDM for Mac, a different MDM for Windows, and a different MDM for Linux... its just too much.”_
<div purpose="attribution-quote">
## Fleet's impact:
We had a different MDM for Mac, a different MDM for Windows, and a different MDM for Linux... its just too much.
* **Eliminate tool overlap:**
Fleets unified platform eliminates the need to manage multiple MDM platforms for each operating system. By providing in-depth data insights in real-time, this global cloud platform was also able to replace other tools often deployed alongside other MDM solutions to maintain full control over their infrastructure.
**— Staff Client Platform Engineer**
</div>
* **Definitive data for instant audits:**
Fleet provides trustworthy data and portability to send their data to systems other teams can also work out of, simplifying compliance processes and making audits less burdensome.
## Challenge
* **Automated updates**
Policy automation eliminated manual intervention for software updates and patching, keeping their devices more secure and freeing IT resources.
The cloud platform was managing thousands of devices with multiple tools across multiple operating systems and global locations, resulting in fragmented processes and unreliable asset data. This complexity compromised security, compliance, and operational efficiency, making it difficult to maintain consistent device check-ins and accurate inventory.
* **Improved support efficiency**
Accurate, real-time data allowed support teams to resolve issues faster and assist employees more effectively.
## Solution
* **Zero-touch onboarding to self-service**
Fleet provides a great out-of-the-box user experience from day one, while also empowering employees to stay productive without needing to reach out to IT.
They implemented Fleet, a unified device management platform that centralizes operations for macOS, Windows, and Linux into a single system. It delivers real-time data insights alongside universal MDM capabilities and provides self-service tools for employees. Fleet also seamlessly integrates with existing tools and supports GitOps workflows to facilitate their rapid global growth.
**Challenge:** The cloud platform was managing thousands of devices with multiple tools across multiple operating systems and global locations, resulting in fragmented processes and unreliable asset data. This complexity compromised security, compliance, and operational efficiency, making it difficult to maintain consistent device check-ins and accurate inventory.
## Results
**Solution:** They implemented Fleet, a unified device management platform that centralizes operations for macOS, Windows, and Linux into a single system. It delivers real-time data insights alongside universal MDM capabilities and provides self-service tools for employees. Fleet also seamlessly integrates with existing tools and supports GitOps workflows to facilitate their rapid global growth.
<div purpose="checklist">
**Impact:** Fleet consolidated the previous device management strategy by eliminating multiple MDM tools, enhancing operational efficiency, and ensuring robust security and compliance. Automation and next-gen change management freed IT resources for strategic tasks, and self-service capabilities improved support efficiency and employee productivity.
Reduced tool overlap by eliminating the need to manage multiple MDM platforms for each operating system. Furthermore, Fleets in-depth data insights allowed them to replace other tools that were deployed alongside their previous MDM solution to maintain full control over their infrastructure.
## The challenge
Use Fleet to gather and share definitive data across their other teams and systems, simplifying compliance processes and making audits less of a burden.
Automated updates with Fleets policy automations to reduce manual intervention for software updates and patching, keeping their devices more secure and freeing IT resources.
Improved support efficiency with accurate, real-time data, allowing support teams to resolve issues faster and assist employees more effectively.
Use Fleets zero-touch onboarding and self-service capabilities to provide a great out-of-the-box user experience for onboarding, while also empowering employees to stay productive without needing to reach out to IT.
</div>
Fleet consolidated the previous device management strategy by eliminating multiple MDM tools, enhancing operational efficiency, and ensuring robust security and compliance. Automation and next-gen change management freed IT resources for strategic tasks, and self-service capabilities improved support efficiency and employee productivity.
## Their story
This company is a global edge cloud platform that empowers developers to create fast, secure, and scalable digital experiences. By providing a programmable edge cloud, they enable businesses to deliver content quickly and securely to users around the world.
@ -36,11 +44,6 @@ While continuing to expand, managing a diverse range of devices across multiple
- Simplify processes across multiple platforms to reduce operational complexity.
- Integrate existing tools for accurate, up-to-date inventory data.
## The solution
_“We had a different MDM for Mac, a different MDM for Windows, a different MDM for Linux, and a different MDM for Android build devices… its just too much.” — Staff Client Platform Engineer_
A unified solution was needed to replace multiple device management suppliers with a single platform. Fleet provided the tools and flexibility required to address these challenges through:
### Eliminating tool overlap
@ -74,9 +77,17 @@ In addition to automated patching, Fleet empowers employees with self-service to
## Conclusion
Fleet has become an essential part of this cloud platform's infrastructure, allowing the company to manage its devices more effectively. By consolidating tools across platforms, Fleet simplified device operations and ensured compliance with stringent requirements. The platforms automation and real-time data capabilities reduced manual efforts and provided accurate insights, while its self-service tools helped employees resolve unique issues independently. These improvements have supported the goals of maintaining security, reliability, and efficient operations as the company continues to grow.
By adopting Fleet, they've transformed its device management processes, achieving greater security, reliability, and scalability to support its global operations.
_“I love Fleet.” — Staff Client Platform Engineer_
<div purpose="attribution-quote">
I love Fleet.
**— Staff Client Platform Engineer**
</div>
<call-to-action></call-to-action>
<meta name="category" value="announcements">
<meta name="authorGitHubUsername" value="Drew-P-drawers">

View file

@ -0,0 +1,80 @@
# Large gaming company enhances server observability with Fleet
<div purpose="attribution-quote">
Fleet's extremely wide and diverse set of data allows us to answer questions that we didn't even know we had. On top of that, the experience is near instantaneous. Seconds to sort through billions of data points and return the exact handful that we need, with complete auditing and transparency. We're able to address reliability and compliance concerns without sacrificing a single point-of-a-percent of performance for our servers. All of this done consistently and continuously.
**— Principal Infrastructure Engineer**
</div>
## Challenge
The leading gaming company was looking for better visibility into an expansive server infrastructure without impacting the performance for millions of users. Existing tools would either leave gaps in visibility or require incredible amounts of manual intervention to make sure configurations were set to specification.
## Solution
Fleet is designed to scale seamlessly from tens of servers to hundreds of thousands of servers with negligible performance impact. This dramatically simplifies gathering data for compliance audits and makes it possible to build more advanced security paradigms.
## Results
<div purpose="checklist">
Fleet scaled out of the box, from managing tens to hundreds of thousands of servers.
They were able to get real-time [observability](https://fleetdm.com/observability) across every enrolled server, even within previous blindspots
They can now quickly answer complex questions, providing near-instantaneous access to precise data points with complete auditing and [transparency](https://fleetdm.com/better) across multiple teams.
They reduced the need for manual interventions and were able to integrate Fleet easily with their existing tools.
</div>
By switching to Fleet, they were able to save time utilizing Fleet's native automations, instead of writing logic manually and incorporating previous blind spots into their security program. With real-time data insights across hundreds of thousands of servers, they were able to answer questions before they had them, all without sacrificing performance or reliability.
## Their story
A leading online platform for user-generated games faced significant challenges in managing and observing its extensive server infrastructure. They were looking to:
- Manage a rapidly growing fleet of servers, with deployments scaling up to 100,000 servers, each with substantial memory and processing capabilities.
- Ensure server observability within edge data centers.
- Avoid even more fragmented processes and reduce the overhead of managing their servers.
- Facilitate easier data exports and integration with other systems, such as Splunk
To address these challenges, they adopted Fleet for server observability, leveraging its query engine and open API to enhance its infrastructure management alongside:
### Scalable deployment
Fleets architecture ensures minimal performance impact even as the server count grows exponentially.
### Comprehensive security and compliance
The gaming company now utilizes Fleets customizable [compliance checks](https://fleetdm.com/queries) and vulnerability assessments to maintain high-security standards across multiple teams.
### Robust API and integration
[Fleet API](https://fleetdm.com/docs/rest-api/rest-api) and webhook support enables automation and integration with their existing systems, eliminating the need for additional middleware and reducing reliance on manual configurations.
### Advanced data handling
Fleets ability to handle large data sets efficiently allows them to perform complex queries and generate accurate [inventories of software](https://fleetdm.com/software-management) components, everything from different Python versions to identified vulnerable software across varying server environments.
### User-friendly management
Fleet facilitates the deployment and maintenance of agents without the need for ongoing manual intervention, aligning with the goal of reducing operational overhead and enhancing reliability.
## Conclusion
By adopting Fleet for server observability, they've successfully addressed scalability, security, and operational challenges within their infrastructure. Fleets comprehensive and automated management capabilities have enabled them to maintain high-performance standards, ensure compliance, and support their expansive and dynamic server environment. As Fleet continues to integrate with their existing systems, it remains a critical component in the companys strategy to securely enable millions to create and exist in virtual worlds without any measurable performance hits.
<call-to-action></call-to-action>
<meta name="category" value="announcements">
<meta name="authorGitHubUsername" value="Drew-P-drawers">
<meta name="authorFullName" value="Andrew Baker">
<meta name="publishedOn" value="2024-12-11">
<meta name="articleTitle" value="Large gaming company enhances server observability with Fleet">
<meta name="description" value="Large gaming company enhances server observability with Fleet">

View file

@ -178,6 +178,8 @@ If you configure software and/or a script for setup experience, users will see a
This window shows the status of the software installations as well as the script exectution. Once all steps have completed, the window can be closed and Setup Assistant will proceed as usual.
> The setup experience script always runs after setup experience software is installed. Currently, software that [automatically installs](https://fleetdm.com/guides/automatic-software-install-in-fleet) and scripts that [automatically run](https://fleetdm.com/guides/policy-automation-run-script) are also installed and run during Setup Assistant but won't appear in the window. Automatic software and scripts may run before or after setup the experience software/script. They aren't installed/run in any particular order.
### Install software
To configure software to be installed during setup experience:

View file

@ -0,0 +1,84 @@
# Vehicle manufacturer transitions to Fleet for endpoint security
<div purpose="attribution-quote">
Fleet has become the central source for a lot of things. The visibility down into the assets covered by the agent is phenomenal.
**— Staff Cybersecurity Engineer**
</div>
## Challenge
A leading electric vehicle manufacturer was experiencing rapid growth that strained its existing IT and security infrastructure. Managing an expanding and unique fleet of endpoints within on-premise systems presented significant challenges. Their current solution was falling short in providing real-time data on their assets and enabling a more efficient vulnerability management program.
## Solution
They purchased Fleet as a replacement for their existing solution to reduce manual work and foster collaboration with their cybersecurity teams. Fleet offered visibility into their endpoints with real-time data, automated reporting, and robust vulnerability management capabilities.
## Results
<div purpose="checklist">
Fleet provided real-time visibility into all of their endpoints.
Automated patch compliance and vulnerability mitigation reduce their risk of security breaches and keep their systems up-to-date.
Automation of routine IT tasks and streamlined reporting processes.
Fleet verifies ongoing compliance with security policies, maintaining robust security configurations across all environments.
</div>
By switching to Fleet, they gained a centralized platform that significantly improved its ability to monitor and manage critical security processes. While new automations allowed them to proactively address and prioritize uncovered vulnerabilities that actually matter. The solution also facilitated better collaboration within their cybersecurity teams and ensured compliance across all devices, both within and outside the cloud environments.
## Their story
The leading electric vehicle manufacturer was experiencing rapid growth that strained its existing IT and security infrastructure. Managing a diverse and expanding fleet of endpoints across various environments presented significant challenges. Their current solution was failing to provide effective asset management and, at one point, even brought their production line to a halt.
To address these challenges, they set out to achieve four key goals:
- Gain real-time visibility into their endpoints.
- Institute proactive vulnerability management.
- Report on automated remediation efforts.
- Integrate and surface data across teams.
### Real-time visibility
<div purpose="attribution-quote">
Security is a data problem. We felt confident being able to know what we wanted to look for. We just needed the data and a platform to go and get it. With the hope of really pinpointing, these were the issues, these were not, and ignore the rest of the noise.
**— Senior Cybersecurity Manager**
</div>
Fleet enables comprehensive monitoring and management by communicating with online devices in real-time. It delivers chip-level data insights that surface critical information, ensuring they can maintain a secure and efficient operational environment. Additionally, Fleets query engine and open API enable them to create custom detections for zero-day threats, increasing their ability to identify and respond to emerging vulnerabilities swiftly.
### Proactive Vulnerability Management
By ensuring automated patch compliance and timely vulnerability mitigation, Fleet significantly reduces the risk of security breaches and keeps all systems consistently within pre-configured policies. This proactive approach not only compacts a typically lengthy vulnerability management process but also ensures continuous protection and risk reduction.
### Automating processes and day-to-day reporting
Fleet is built for automation, reducing the manual workload of routine tasks such as software installations, updates, and vulnerability mitigations from its IT and cybersecurity teams.
Integrating with their existing tools like Torq, made it easier to generate accurate and timely reports on day-to-day security, including the ongoing status of vulnerabilities and remediation efforts.
### Definitive data
By adhering to standard data shapes and formats, Fleet makes sure that data is easily interpretable and usable across various teams and applications. While serving as the central hub for security data, it provides a single source of truth that enhances the precision and efficiency of its asset inventory.
## Conclusion
The decision to purchase Fleet was driven by the need for a more reliable, comprehensive, visible, and collaborative solution that could effectively replace their existing platform. Fleet's real-time data access, vulnerability management, and automation helped achieve these objectives. By adopting Fleet, they were able to pinpoint their security issues, prioritize what actually mattered, and proactively manage and mitigate threats while saving time.
<call-to-action></call-to-action>
<meta name="category" value="announcements">
<meta name="authorGitHubUsername" value="Drew-P-drawers">
<meta name="authorFullName" value="Andrew Baker">
<meta name="publishedOn" value="2024-12-12">
<meta name="articleTitle" value="Vehicle manufacturer transitions to Fleet for endpoint security">
<meta name="description" value="Vehicle manufacturer transitions to Fleet for endpoint security">

View file

@ -1,6 +1,11 @@
# Worldwide security and authentication platform chooses Fleet for Linux management
“My biggest issue with Linux is that no one considers Linux. It's never the first thing. It's always the last thing. And as the advocate and evangelist for Linux in the IT org broadly, I have to make the security guys stop and go, hey, hang on a second. We're also here. You know, we matter…” — Technical Systems Engineer
<div purpose="attribution-quote">
My biggest issue with Linux is that no one considers Linux. It's never the first thing. It's always the last thing. And as the advocate and evangelist for Linux in the IT org broadly, I have to make the security guys stop and go, hey, hang on a second. We're also here. You know, we matter…
**— Technical Systems Engineer**
</div>
## Challenge
@ -15,13 +20,16 @@ By adopting Fleet, they were able to unify their [device management](https://fle
## Results
- Fleet enabled Linux device management and replaced legacy tools for telemetry and system configuration. This reduced their previously typical compliance woes and lack of standard MDM features.
<div purpose="checklist">
- Employees are free to work with their preferred operating systems of choice.
Fleet enabled Linux device management and replaced legacy tools for telemetry and system configuration. This reduced their previously typical compliance woes and lack of standard MDM features.
- [Fleets API](https://fleetdm.com/docs/rest-api/rest-api) and live query capabilities ensured up-to-date inventory data to meet stringent security and access control requirements while integrating with existing systems.
Employees are free to work with their preferred operating systems of choice.
- Fleets policy automation ensures that software and configurations can remain secure without requiring manual intervention, thereby freeing up IT resources.
[Fleets API](https://fleetdm.com/docs/rest-api/rest-api) and live query capabilities ensured up-to-date inventory data to meet stringent security and access control requirements while integrating with existing systems.
Fleets policy automation ensures that software and configurations can remain secure without requiring manual intervention, thereby freeing up IT resources.
</div>
Implementing Fleet significantly enhanced their device management strategy by centralizing Linux operations, improving security compliance, and increasing the operational efficiency of these previously overlooked devices. Fleets automation and integration capabilities ensured accurate and reliable asset inventory across teams, and empowered technical users with self-service tools, maintaining high security standards across all of their devices.
@ -69,7 +77,9 @@ Fleets ability to scale horizontally allowed the company to manage thousands
By providing tools typically overlooked for Linux, Fleet's open-source platform completes the circle of high-security standards, compliance, and trust with a technically proficient and privacy-conscious Linux user base. Fleets automation and integration capabilities meant that IT could still use the tools they wanted, without leaving Linux behind. This proved vital for the company which continues to scale globally.
To learn more about how Fleet can support your organization, visit [fleetdm.com/mdm](fleetdm.com/mdm)
To learn more about how Fleet can support your organization, visit [fleetdm.com/mdm](fleetdm.com/mdm).
<call-to-action></call-to-action>
<meta name="category" value="announcements">
<meta name="authorGitHubUsername" value="Drew-P-drawers">

View file

@ -0,0 +1,5 @@
- Fleshed out server response from `queries` endpoint to include `count` and `meta` pagination information.
- Updated UI queries page to filter, sort, paginate, etc. via query params in call to server.
- Updated platform filtering on queries page to refer to targeted platforms instead of compatible
platforms
- Updated queries API to support above targeted platform filtering

1
changes/22523-cve-500 Normal file
View file

@ -0,0 +1 @@
* Fixed a panic (and resulting failure to load CVE details) on new installs when OS versions have not been populated yet.

View file

@ -1,2 +1,3 @@
Added ability to use secrets ($FLEET_SECRET_NAME) in scripts and profiles.
Added ability to use secrets ($FLEET_SECRET_YOURNAME) in scripts and profiles.
- Added `/fleet/spec/secret_variables` API endpoint.
- fleetctl gitops identifies secrets in scripts and profiles and saves them on the Fleet server.

View file

@ -0,0 +1 @@
* Allowed team policy endpoint (`PATCH /api/latest/fleet/teams/{team_id}/policies/{policy_id}`) to receive explicit `null` as a value for `script_id` or `software_title_id` to unset a script or software installer respectively.

View file

@ -0,0 +1 @@
- Fix policy truncation UI bug

View file

@ -0,0 +1 @@
* Allowed calling `/api/v1/fleet/software/fleet_maintained_apps` with no team ID to retrieve the full global list of maintained apps

View file

@ -0,0 +1 @@
* Redirect when user provides an invalid URL param for fleet-maintained software id

View file

@ -0,0 +1 @@
Removed server error if no private IP was found by detail_query_network_interface.

View file

@ -1250,7 +1250,7 @@ func TestGetQueries(t *testing.T) {
}
return nil, &notFoundError{}
}
ds.ListQueriesFunc = func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, error) {
ds.ListQueriesFunc = func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, int, *fleet.PaginationMetadata, error) {
if opt.TeamID == nil { //nolint:gocritic // ignore ifElseChain
return []*fleet.Query{
{
@ -1282,7 +1282,7 @@ func TestGetQueries(t *testing.T) {
Saved: true, // ListQueries always returns the saved ones.
ObserverCanRun: true,
},
}, nil
}, 3, nil, nil
} else if *opt.TeamID == 1 {
return []*fleet.Query{
{
@ -1299,11 +1299,11 @@ func TestGetQueries(t *testing.T) {
TeamID: ptr.Uint(1),
ObserverCanRun: true,
},
}, nil
}, 1, nil, nil
} else if *opt.TeamID == 2 {
return []*fleet.Query{}, nil
return []*fleet.Query{}, 0, nil, nil
}
return nil, errors.New("invalid team ID")
return nil, 0, nil, errors.New("invalid team ID")
}
expectedGlobal := `+--------+-------------+-----------+-----------+--------------------------------+
@ -1563,7 +1563,7 @@ func TestGetQueriesAsObserver(t *testing.T) {
}
}
ds.ListQueriesFunc = func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, error) {
ds.ListQueriesFunc = func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, int, *fleet.PaginationMetadata, error) {
return []*fleet.Query{
{
ID: 42,
@ -1586,7 +1586,7 @@ func TestGetQueriesAsObserver(t *testing.T) {
Query: "select 3;",
ObserverCanRun: false,
},
}, nil
}, 3, nil, nil
}
for _, tc := range []struct {
@ -1794,7 +1794,7 @@ spec:
GlobalRole: nil,
Teams: []fleet.UserTeam{{Role: fleet.RoleObserver}},
})
ds.ListQueriesFunc = func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, error) {
ds.ListQueriesFunc = func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, int, *fleet.PaginationMetadata, error) {
return []*fleet.Query{
{
ID: 42,
@ -1810,12 +1810,12 @@ spec:
Query: "select 2;",
ObserverCanRun: false,
},
}, nil
}, 2, nil, nil
}
assert.Equal(t, "", runAppForTest(t, []string{"get", "queries"}))
// No filtering is performed if all are observer_can_run.
ds.ListQueriesFunc = func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, error) {
ds.ListQueriesFunc = func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, int, *fleet.PaginationMetadata, error) {
return []*fleet.Query{
{
ID: 42,
@ -1831,7 +1831,7 @@ spec:
Query: "select 2;",
ObserverCanRun: true,
},
}, nil
}, 2, nil, nil
}
expected = `+--------+-------------+-----------+-----------+----------------------------+
| NAME | DESCRIPTION | QUERY | TEAM | SCHEDULE |

View file

@ -121,6 +121,8 @@ func gitopsCommand() *cli.Command {
// We keep track of the secrets to check if duplicates exist during dry run
secrets := make(map[string]struct{})
// We keep track of the environment FLEET_SECRET_* variables
allFleetSecrets := make(map[string]string)
for _, flFilename := range flFilenames.Value() {
baseDir := filepath.Dir(flFilename)
config, err := spec.GitOpsFromFile(flFilename, baseDir, appConfig, logf)
@ -221,6 +223,10 @@ func gitopsCommand() *cli.Command {
}
}
err = fleetClient.SaveEnvSecrets(allFleetSecrets, config.FleetSecrets, flDryRun)
if err != nil {
return err
}
assumptions, err := fleetClient.DoGitOps(c.Context, config, flFilename, logf, flDryRun, teamDryRunAssumptions, appConfig, teamsSoftwareInstallers, teamsScripts)
if err != nil {
return err

View file

@ -15,6 +15,7 @@ import (
"github.com/fleetdm/fleet/v4/server/service"
"github.com/fleetdm/fleet/v4/server/test"
"github.com/go-git/go-git/v5"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
@ -38,6 +39,7 @@ func (s *integrationGitopsTestSuite) SetupSuite() {
require.NoError(s.T(), err)
appConf.MDM.EnabledAndConfigured = true
appConf.MDM.AppleBMEnabledAndConfigured = true
appConf.MDM.WindowsEnabledAndConfigured = true
err = s.ds.SaveAppConfig(context.Background(), appConf)
require.NoError(s.T(), err)
@ -100,6 +102,36 @@ func (s *integrationGitopsTestSuite) TestFleetGitops() {
t := s.T()
const fleetGitopsRepo = "https://github.com/fleetdm/fleet-gitops"
fleetctlConfig := s.createFleetctlConfig()
// Clone git repo
repoDir := t.TempDir()
_, err := git.PlainClone(
repoDir, false, &git.CloneOptions{
ReferenceName: "main",
SingleBranch: true,
Depth: 1,
URL: fleetGitopsRepo,
Progress: os.Stdout,
},
)
require.NoError(t, err)
// Set the required environment variables
t.Setenv("FLEET_URL", s.server.URL)
t.Setenv("FLEET_GLOBAL_ENROLL_SECRET", "global_enroll_secret")
globalFile := path.Join(repoDir, "default.yml")
// Dry run
_ = runAppForTest(t, []string{"gitops", "--config", fleetctlConfig.Name(), "-f", globalFile, "--dry-run"})
// Real run
_ = runAppForTest(t, []string{"gitops", "--config", fleetctlConfig.Name(), "-f", globalFile})
}
func (s *integrationGitopsTestSuite) createFleetctlConfig() *os.File {
t := s.T()
// Create a temporary fleetctl config file
fleetctlConfig, err := os.CreateTemp(t.TempDir(), "*.yml")
require.NoError(t, err)
@ -116,30 +148,75 @@ contexts:
)
_, err = fleetctlConfig.WriteString(configStr)
require.NoError(t, err)
return fleetctlConfig
}
// Clone git repo
repoDir := t.TempDir()
_, err = git.PlainClone(
repoDir, false, &git.CloneOptions{
ReferenceName: "main",
SingleBranch: true,
Depth: 1,
URL: fleetGitopsRepo,
Progress: os.Stdout,
},
func (s *integrationGitopsTestSuite) TestFleetGitopsWithFleetSecrets() {
t := s.T()
const (
secretName1 = "NAME"
secretName2 = "length"
)
require.NoError(t, err)
ctx := context.Background()
fleetctlConfig := s.createFleetctlConfig()
// Set the required environment variables
t.Setenv("FLEET_URL", s.server.URL)
t.Setenv("FLEET_GLOBAL_ENROLL_SECRET", "global_enroll_secret")
globalFile := path.Join(repoDir, "default.yml")
require.NoError(t, err)
t.Setenv("FLEET_SECRET_"+secretName1, "secret_value")
t.Setenv("FLEET_SECRET_"+secretName2, "2")
globalFile := path.Join("testdata", "gitops", "global_integration.yml")
// Dry run
_ = runAppForTest(t, []string{"gitops", "--config", fleetctlConfig.Name(), "-f", globalFile, "--dry-run"})
secrets, err := s.ds.GetSecretVariables(ctx, []string{secretName1})
require.NoError(t, err)
require.Empty(t, secrets)
// Real run
_ = runAppForTest(t, []string{"gitops", "--config", fleetctlConfig.Name(), "-f", globalFile})
// Check secrets
secrets, err = s.ds.GetSecretVariables(ctx, []string{secretName1, secretName2})
require.NoError(t, err)
require.Len(t, secrets, 2)
for _, secret := range secrets {
switch secret.Name {
case secretName1:
assert.Equal(t, "secret_value", secret.Value)
case secretName2:
assert.Equal(t, "2", secret.Value)
default:
t.Fatalf("unexpected secret %s", secret.Name)
}
}
// Check script(s)
scriptID, err := s.ds.GetScriptIDByName(ctx, "fleet-secret.sh", nil)
require.NoError(t, err)
expected, err := os.ReadFile("testdata/gitops/lib/fleet-secret.sh")
require.NoError(t, err)
script, err := s.ds.GetScriptContents(ctx, scriptID)
require.NoError(t, err)
assert.Equal(t, expected, script)
// Check Apple profiles
profiles, err := s.ds.ListMDMAppleConfigProfiles(ctx, nil)
require.NoError(t, err)
require.Len(t, profiles, 1)
assert.Contains(t, string(profiles[0].Mobileconfig), "$FLEET_SECRET_"+secretName1)
// Check Windows profiles
allProfiles, _, err := s.ds.ListMDMConfigProfiles(ctx, nil, fleet.ListOptions{})
require.NoError(t, err)
require.Len(t, allProfiles, 2)
var windowsProfileUUID string
for _, profile := range allProfiles {
if profile.Platform == "windows" {
windowsProfileUUID = profile.ProfileUUID
}
}
require.NotEmpty(t, windowsProfileUUID)
winProfile, err := s.ds.GetMDMWindowsConfigProfile(ctx, windowsProfileUUID)
require.NoError(t, err)
assert.Contains(t, string(winProfile.SyncML), "${FLEET_SECRET_"+secretName2+"}")
}

View file

@ -69,7 +69,9 @@ func TestGitOpsBasicGlobalFree(t *testing.T) {
return nil
}
ds.ListGlobalPoliciesFunc = func(ctx context.Context, opts fleet.ListOptions) ([]*fleet.Policy, error) { return nil, nil }
ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, error) { return nil, nil }
ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, int, *fleet.PaginationMetadata, error) {
return nil, 0, nil, nil
}
// Mock appConfig
savedAppConfig := &fleet.AppConfig{}
@ -224,7 +226,9 @@ func TestGitOpsBasicGlobalPremium(t *testing.T) {
return nil
}
ds.ListGlobalPoliciesFunc = func(ctx context.Context, opts fleet.ListOptions) ([]*fleet.Policy, error) { return nil, nil }
ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, error) { return nil, nil }
ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, int, *fleet.PaginationMetadata, error) {
return nil, 0, nil, nil
}
// Mock appConfig
savedAppConfig := &fleet.AppConfig{}
@ -364,7 +368,9 @@ func TestGitOpsBasicTeam(t *testing.T) {
) (teamPolicies []*fleet.Policy, inheritedPolicies []*fleet.Policy, err error) {
return nil, nil, nil
}
ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, error) { return nil, nil }
ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, int, *fleet.PaginationMetadata, error) {
return nil, 0, nil, nil
}
team := &fleet.Team{
ID: 1,
CreatedAt: time.Now(),
@ -600,8 +606,8 @@ func TestGitOpsFullGlobal(t *testing.T) {
query.ID = 1
query.Name = "Query to delete"
queryDeleted := false
ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, error) {
return []*fleet.Query{&query}, nil
ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, int, *fleet.PaginationMetadata, error) {
return []*fleet.Query{&query}, 1, nil, nil
}
ds.DeleteQueriesFunc = func(ctx context.Context, ids []uint) (uint, error) {
queryDeleted = true
@ -861,8 +867,8 @@ func TestGitOpsFullTeam(t *testing.T) {
query.TeamID = ptr.Uint(teamID)
query.Name = "Query to delete"
queryDeleted := false
ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, error) {
return []*fleet.Query{&query}, nil
ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, int, *fleet.PaginationMetadata, error) {
return []*fleet.Query{&query}, 1, nil, nil
}
ds.DeleteQueriesFunc = func(ctx context.Context, ids []uint) (uint, error) {
queryDeleted = true
@ -1154,7 +1160,9 @@ func TestGitOpsBasicGlobalAndTeam(t *testing.T) {
}
return nil, nil
}
ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, error) { return nil, nil }
ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, int, *fleet.PaginationMetadata, error) {
return nil, 0, nil, nil
}
ds.NewActivityFunc = func(
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
) error {
@ -1485,7 +1493,9 @@ func TestGitOpsBasicGlobalAndNoTeam(t *testing.T) {
ds.ListTeamsFunc = func(ctx context.Context, filter fleet.TeamFilter, opt fleet.ListOptions) ([]*fleet.Team, error) {
return nil, nil
}
ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, error) { return nil, nil }
ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, int, *fleet.PaginationMetadata, error) {
return nil, 0, nil, nil
}
ds.NewActivityFunc = func(
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
) error {
@ -2441,7 +2451,9 @@ func setupFullGitOpsPremiumServer(t *testing.T) (*mock.Store, **fleet.AppConfig,
}
return nil, nil
}
ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, error) { return nil, nil }
ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, int, *fleet.PaginationMetadata, error) {
return nil, 0, nil, nil
}
ds.NewActivityFunc = func(
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
) error {

View file

@ -60,11 +60,11 @@ func TestSavedLiveQuery(t *testing.T) {
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{}, nil
}
ds.ListQueriesFunc = func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, error) {
ds.ListQueriesFunc = func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, int, *fleet.PaginationMetadata, error) {
if opt.MatchQuery == queryName {
return []*fleet.Query{&query}, nil
return []*fleet.Query{&query}, 1, nil, nil
}
return []*fleet.Query{}, nil
return []*fleet.Query{}, 0, nil, nil
}
ds.NewDistributedQueryCampaignFunc = func(ctx context.Context, camp *fleet.DistributedQueryCampaign) (*fleet.DistributedQueryCampaign, error) {
camp.ID = 321

View file

@ -14,8 +14,8 @@ import (
func TestEarlySessionCheck(t *testing.T) {
_, ds := runServerWithMockedDS(t)
ds.ListQueriesFunc = func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, error) {
return nil, nil
ds.ListQueriesFunc = func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, int, *fleet.PaginationMetadata, error) {
return nil, 0, nil, nil
}
ds.SessionByKeyFunc = func(ctx context.Context, key string) (*fleet.Session, error) {
return nil, errors.New("invalid session")

View file

@ -0,0 +1,21 @@
policies:
queries:
agent_options:
path: ./lib/agent-options.yml
controls:
macos_settings:
custom_settings:
- path: ./lib/macos-password-secret.mobileconfig
windows_enabled_and_configured: true
windows_settings:
custom_settings:
- path: ./lib/windows-screenlock-secret.xml
scripts:
- path: ./lib/fleet-secret.sh
org_settings:
server_settings:
server_url: $FLEET_URL
org_info:
org_name: Fleet
secrets:
- secret: "$FLEET_GLOBAL_ENROLL_SECRET"

View file

@ -0,0 +1,13 @@
command_line_flags:
config:
decorators:
load:
- SELECT uuid AS host_uuid FROM system_info;
- SELECT hostname AS hostname FROM system_info;
options:
disable_distributed: false
distributed_interval: 10
distributed_plugin: tls
distributed_tls_max_attempts: 3
logger_tls_endpoint: /api/v1/osquery/log
pack_delimiter: /

View file

@ -0,0 +1 @@
echo "$FLEET_SECRET_NAME"

View file

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PayloadContent</key>
<array>
<dict>
<key>PayloadDescription</key>
<string>Configures Passcode settings</string>
<key>PayloadDisplayName</key>
<string>$FLEET_SECRET_NAME</string>
<key>PayloadIdentifier</key>
<string>com.github.erikberglund.ProfileCreator.F7CF282E-D91B-44E9-922F-A719634F9C8E.com.apple.mobiledevice.passwordpolicy.231DFC90-D5A7-41B8-9246-564056048AC5</string>
<key>PayloadOrganization</key>
<string></string>
<key>PayloadType</key>
<string>com.apple.mobiledevice.passwordpolicy</string>
<key>PayloadUUID</key>
<string>231DFC90-D5A7-41B8-9246-564056048AC5</string>
<key>PayloadVersion</key>
<integer>1</integer>
<key>allowSimple</key>
<true/>
<key>forcePIN</key>
<true/>
<key>maxFailedAttempts</key>
<integer>11</integer>
<key>maxGracePeriod</key>
<integer>1</integer>
<key>maxInactivity</key>
<integer>15</integer>
<key>minLength</key>
<integer>10</integer>
<key>requireAlphanumeric</key>
<true/>
</dict>
</array>
<key>PayloadDescription</key>
<string>Configures our Macs to require passwords that are 10 character long</string>
<key>PayloadDisplayName</key>
<string>Password policy - require 10 characters</string>
<key>PayloadIdentifier</key>
<string>com.github.erikberglund.ProfileCreator.F7CF282E-D91B-44E9-922F-A719634F9C8E</string>
<key>PayloadOrganization</key>
<string>FleetDM</string>
<key>PayloadScope</key>
<string>System</string>
<key>PayloadType</key>
<string>Configuration</string>
<key>PayloadUUID</key>
<string>F7CF282E-D91B-44E9-922F-A719634F9C8E</string>
<key>PayloadVersion</key>
<integer>1</integer>
</dict>
</plist>

View file

@ -0,0 +1,48 @@
<Replace>
<!-- Enforce screenlock -->
<Item>
<Meta>
<Format xmlns="syncml:metinf">int</Format>
</Meta>
<Target>
<LocURI>./Device/Vendor/MSFT/Policy/Config/DeviceLock/DevicePasswordEnabled</LocURI>
</Target>
<Data>0</Data>
</Item>
</Replace>
<Replace>
<!-- Enforce screenlock after 15 minutes -->
<Item>
<Meta>
<Format xmlns="syncml:metinf">int</Format>
</Meta>
<Target>
<LocURI>./Device/Vendor/MSFT/Policy/Config/DeviceLock/MaxInactivityTimeDeviceLock</LocURI>
</Target>
<Data>15</Data>
</Item>
</Replace>
<Replace>
<!-- Enforce PIN or password length (10 characters) -->
<Item>
<Meta>
<Format xmlns="syncml:metinf">int</Format>
</Meta>
<Target>
<LocURI>./Device/Vendor/MSFT/Policy/Config/DeviceLock/MinDevicePasswordLength</LocURI>
</Target>
<Data>${FLEET_SECRET_length}</Data>
</Item>
</Replace>
<Replace>
<!-- Enforce PIN or password has at least one lowercase letter and at least one number -->
<Item>
<Meta>
<Format xmlns="syncml:metinf">int</Format>
</Meta>
<Target>
<LocURI>./Device/Vendor/MSFT/Policy/Config/DeviceLock/MinDevicePasswordComplexCharacters</LocURI>
</Target>
<Data>2</Data>
</Item>
</Replace>

View file

@ -247,8 +247,8 @@ func TestFleetctlUpgradePacks_EmptyPacks(t *testing.T) {
return fleet.TargetMetrics{}, nil
}
ds.ListQueriesFunc = func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, error) {
return nil, nil
ds.ListQueriesFunc = func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, int, *fleet.PaginationMetadata, error) {
return nil, 0, nil, nil
}
tempDir := t.TempDir()
@ -314,12 +314,12 @@ func TestFleetctlUpgradePacks_NonEmpty(t *testing.T) {
return fleet.TargetMetrics{}, nil
}
ds.ListQueriesFunc = func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, error) {
ds.ListQueriesFunc = func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, int, *fleet.PaginationMetadata, error) {
return []*fleet.Query{
{Name: "q1", Query: "select 1"},
{Name: "q2", Query: "select 2"},
{Name: "q3", Query: "select 3"},
}, nil
}, 3, nil, nil
}
const expected = `

View file

@ -2410,7 +2410,7 @@ One of `query` and `query_id` must be specified.
#### Example with one host targeted by hostname
`POST /api/v1/fleet/queries/run_by_names`
`POST /api/v1/fleet/queries/run_by_identifiers`
##### Request body
@ -2449,7 +2449,7 @@ One of `query` and `query_id` must be specified.
#### Example with multiple hosts targeted by label name
`POST /api/v1/fleet/queries/run_by_names`
`POST /api/v1/fleet/queries/run_by_identifiers`
##### Request body
@ -2486,6 +2486,39 @@ One of `query` and `query_id` must be specified.
}
```
#### Example with invalid label
`POST /api/v1/fleet/queries/run_by_identifiers`
##### Request body
```json
{
"query": "SELECT instance_id FROM system_info",
"selected": {
"labels": ["Windows", "Banana", "Apple"]
}
}
```
##### Default response
`Status: 400`
```json
{
"message": "Bad request",
"errors": [
{
"name": "base",
"reason": "Invalid label name(s): Banana, Apple."
}
],
"uuid": "303649f4-5e45-4379-bae9-64ec0ef56287"
}
```
### Retrieve live query results (standard WebSocket API)
You can retrieve the results of a live query using the [standard WebSocket API](#https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_client_applications).

View file

@ -97,4 +97,11 @@ If you also have Fleetd running on hosts, it will need access to these API endpo
* `/api/fleet/orbit/device_mapping`
* `/api/osquery/log`
Hosts running Fleet Desktop will need access to these API endpoints:
* `/api/latest/fleet/device/.+/desktop`
* `/api/latest/fleet/device/.+/ping`
> Full list [here](https://github.com/fleetdm/fleet/blob/c080a3b0e1eed2184b4b7bb77a6abd8c2c39b9f4/server/service/handler.go#L791-L839)
<meta name="description" value="Find commonly asked questions and answers about contributing to Fleet as part of our community.">

View file

@ -461,8 +461,7 @@ This is the callback endpoint that the identity provider will use to send securi
### List activities
Returns a list of the activities that have been performed in Fleet as well as additional metadata.
for pagination. For a comprehensive list of activity types and detailed information, please see the [audit logs](https://fleetdm.com/docs/using-fleet/audit-activities) page.
Returns a list of the activities that have been performed in Fleet. For a comprehensive list of activity types and detailed information, please see the [audit logs](https://fleetdm.com/docs/using-fleet/audit-activities) page.
`GET /api/v1/fleet/activities`
@ -8732,7 +8731,7 @@ Gets the result of a script that was executed.
`GET /api/v1/fleet/scripts/results/:execution_id`
##### Default Response
##### Default response
`Status: 200`
@ -9582,12 +9581,13 @@ _Available in Fleet Premium._
Update a package to install on macOS, Windows, or Linux (Ubuntu) hosts.
`PATCH /api/v1/fleet/software/titles/:title_id/package`
`PATCH /api/v1/fleet/software/titles/:id/package`
#### Parameters
| Name | Type | In | Description |
| ---- | ------- | ---- | -------------------------------------------- |
| id | integer | path | ID of the software title being updated. |
| software | file | form | Installer package file. Supported packages are .pkg, .msi, .exe, .deb, and .rpm. |
| team_id | integer | form | **Required**. The team ID. Updates a software package in the specified team. |
| install_script | string | form | Command that Fleet runs to install software. If not specified Fleet runs the [default install command](https://github.com/fleetdm/fleet/tree/f71a1f183cc6736205510580c8366153ea083a8d/pkg/file/scripts) for each package type. |
@ -9877,13 +9877,13 @@ Add Fleet-maintained app so it's available for install.
_Available in Fleet Premium._
`GET /api/v1/fleet/software/titles/:software_title_id/package?alt=media`
`GET /api/v1/fleet/software/titles/:id/package?alt=media`
#### Parameters
| Name | Type | In | Description |
| ---- | ------- | ---- | -------------------------------------------- |
| software_title_id | integer | path | **Required**. The ID of the software title to download software package.|
| id | integer | path | **Required**. The ID of the software title to download software package.|
| team_id | integer | query | **Required**. The team ID. Downloads a software package added to the specified team. |
| alt | integer | query | **Required**. If specified and set to "media", downloads the specified software package. |

View file

@ -16,9 +16,17 @@ A dashboard to easily manage profiles and scripts across multiple teams on a Fle
This app has two required custom configuration values:
- `sails.config.custom.fleetBaseUrl`: The full URL of your Fleet instance. (e.g., https://fleet.example.com)
- `sails.config.custom.fleetApiToken`: An API token for an API-only user on your Fleet instance.
### Required configuration for software features
If you are using this app to manage software across multiple teams on a Fleet instance, five additional configuration values are required:
- `sails.config.uploads.bucket` The name of an AWS s3 bucket where unassigned software installers will be stored.
- `sails.config.uploads.secret` The secret for the S3 bucket where unassigned software installers will be stored.
- `sails.config.uploads.region` The region the AWS S3 bucket is located.
- `sails.config.uploads.bucketWithPostfix`: The name of the s3 bucket with the directory that the software installers are stored in on appended to it. If the files will be stored in the root directory of the bucket, this value should be identical to the `sails.config.uploads.bucket` value
- `sails.config.uploads.prefixForFileDeletion`: The directory path in the S3 bucket where the software installers will be stored. If the installers will be stored in the root directory, then this value can be set to ' '.
## Running the bulk operations dashboard with Docker.

View file

@ -0,0 +1,35 @@
/**
* File Upload Settings
* (sails.config.uploads)
*
* These options tell Sails where (and how) to store uploaded files.
*
* > This file is mainly useful for configuring how file uploads in your
* > work during development; for example, when lifting on your laptop.
* > For recommended production settings, see `config/env/production.js`
*
* For all available options, see:
* https://sailsjs.com/config/uploads
*/
module.exports.uploads = {
/***************************************************************************
* *
* Sails apps upload and download to the local disk filesystem by default, *
* using a built-in filesystem adapter called `skipper-disk`. This feature *
* is mainly intended for convenience during development since, in *
* production, many apps will opt to use a different approach for storing *
* uploaded files, such as Amazon S3, Azure, or GridFS. *
* *
* Most of the time, the following options should not be changed. *
* (Instead, you might want to have a look at `config/env/production.js`.) *
* *
***************************************************************************/
// bucket: '',// The name of the S3 bucket where software installers will be stored.
// region: '', // The region where the S3 bucket is located.
// secret: '', // The secret for the S3 bucket where unassigned software installers will be stored.
// bucketWithPostfix: '', // This value should be set to the same value as the bucket unless the files are stored in a folder in the S3 bucket. In that case, this value needs to be set to `{bucket name}{folder name}` e.g., unassigned-software-installers/staging
// prefixForFileDeletion: '', // Only required if the software installers are stored in a folder in the S3 bucket. The name of the folder where the software installers are stored in the S3 bucket with a trailing slash. e.g., staging/
};

View file

@ -14,6 +14,11 @@ services:
sails_models__migrate: safe
sails_custom__fleetBaseUrl: '' #Add the base url of your Fleet instance: ex: https://fleet.example.com
sails_custom__fleetApiToken: '' # Add the API token of an API-only user [?] Here's how you get one: https://fleetdm.com/docs/using-fleet/fleetctl-cli#get-the-api-token-of-an-api-only-user
sails_uploads__secret: '' # (Required to enable software features) The secret for the S3 bucket where unassigned software installers will be stored.
sails_uploads__bucket: '' # (Required to enable software features) The name of the S3 bucket where software installers will be stored.
sails_uploads__region: '' # (Required to enable software features) The region where the S3 bucket is located.
sails_uploads__bucketWithPostfix: '' # (Required to enable software features) This value should be set to the same value as the bucket unless the files are stored in a folder in the S3 bucket. In that case, this value needs to be set to `{bucket name}{folder name}` e.g., unassigned-software-installers/staging
sails_uploads__prefixForFileDeletion: '' # (Required to enable software features) Only required if the software installers are stored in a folder in the S3 bucket. The name of the folder where the software installers are stored in the S3 bucket with a trailing slash. e.g., staging/
postgres:
image: "postgres:alpine"

View file

@ -20,7 +20,9 @@ Missing items:
1. 2.6.3.1 Ensure Share Mac Analytics Is Disabled
2. 2.6.3.3 Ensure Improve Assistive Voice Features Is Disabled
3. 2.6.3.4 Ensure 'Share with app developers' Is Disabled
4. 5.11 Ensure Logging Is Enabled for Sudo
4. 2.6.3.5 Ensure Share iCloud Analytics Is Disabled
5. 2.7.2 Audit iPhone Mirroring
6. 5.11 Ensure Logging Is Enabled for Sudo
### Checks that require decision

View file

@ -152,11 +152,17 @@ func (svc *Service) AddFleetMaintainedApp(
return titleID, nil
}
func (svc *Service) ListFleetMaintainedApps(ctx context.Context, teamID uint, opts fleet.ListOptions) ([]fleet.MaintainedApp, *fleet.PaginationMetadata, error) {
if err := svc.authz.Authorize(ctx, &fleet.SoftwareInstaller{
TeamID: &teamID,
}, fleet.ActionRead); err != nil {
return nil, nil, err
func (svc *Service) ListFleetMaintainedApps(ctx context.Context, teamID *uint, opts fleet.ListOptions) ([]fleet.MaintainedApp, *fleet.PaginationMetadata, error) {
var authErr error
// viewing the maintained app list without showing team-specific info can be done by anyone who can view individual FMAs
if teamID == nil {
authErr = svc.authz.Authorize(ctx, &fleet.MaintainedApp{}, fleet.ActionRead)
} else { // viewing the maintained app list when showing team-specific info requires access to that team
authErr = svc.authz.Authorize(ctx, &fleet.SoftwareInstaller{TeamID: teamID}, fleet.ActionRead)
}
if authErr != nil {
return nil, nil, authErr
}
avail, meta, err := svc.ds.ListAvailableFleetMaintainedApps(ctx, teamID, opts)

View file

@ -12,6 +12,102 @@ import (
"github.com/stretchr/testify/require"
)
func TestListMaintainedAppsAuth(t *testing.T) {
t.Parallel()
ds := new(mock.Store)
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{}, nil
}
ds.ListAvailableFleetMaintainedAppsFunc = func(ctx context.Context, teamID *uint, opt fleet.ListOptions) ([]fleet.MaintainedApp, *fleet.PaginationMetadata, error) {
return []fleet.MaintainedApp{}, &fleet.PaginationMetadata{}, nil
}
authorizer, err := authz.NewAuthorizer()
require.NoError(t, err)
svc := &Service{authz: authorizer, ds: ds}
testCases := []struct {
name string
user *fleet.User
shouldFailWithNoTeam bool
shouldFailWithMatchingTeam bool
shouldFailWithDifferentTeam bool
}{
{
"global admin",
&fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
false,
false,
false,
},
{
"global maintainer",
&fleet.User{GlobalRole: ptr.String(fleet.RoleMaintainer)},
false,
false,
false,
},
{
"global observer",
&fleet.User{GlobalRole: ptr.String(fleet.RoleObserver)},
true,
true,
true,
},
{
"team admin",
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleAdmin}}},
false,
false,
true,
},
{
"team maintainer",
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleMaintainer}}},
false,
false,
true,
},
{
"team observer",
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleObserver}}},
true,
true,
true,
},
}
var forbiddenError *authz.Forbidden
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
ctx := viewer.NewContext(context.Background(), viewer.Viewer{User: tt.user})
_, _, err := svc.ListFleetMaintainedApps(ctx, nil, fleet.ListOptions{})
if tt.shouldFailWithNoTeam {
require.Error(t, err)
require.ErrorAs(t, err, &forbiddenError)
} else {
require.NoError(t, err)
}
_, _, err = svc.ListFleetMaintainedApps(ctx, ptr.Uint(1), fleet.ListOptions{})
if tt.shouldFailWithMatchingTeam {
require.Error(t, err)
require.ErrorAs(t, err, &forbiddenError)
} else {
require.NoError(t, err)
}
_, _, err = svc.ListFleetMaintainedApps(ctx, ptr.Uint(2), fleet.ListOptions{})
if tt.shouldFailWithDifferentTeam {
require.Error(t, err)
require.ErrorAs(t, err, &forbiddenError)
} else {
require.NoError(t, err)
}
})
}
}
func TestGetMaintainedAppAuth(t *testing.T) {
t.Parallel()
ds := new(mock.Store)

View file

@ -10,7 +10,7 @@ func (svc *Service) ListSoftware(ctx context.Context, opts fleet.SoftwareListOpt
// reuse ListSoftware, but include cve scores in premium version
// unless without_vulnerability_details is set to true
// including these details causes a lot of memory bloat
if !opts.WithoutVulnerabilityDetails {
if (opts.MaximumCVSS > 0 || opts.MinimumCVSS > 0 || opts.KnownExploit) || !opts.WithoutVulnerabilityDetails {
opts.IncludeCVEScores = true
}
return svc.Service.ListSoftware(ctx, opts)

View file

@ -88,8 +88,13 @@ const PlatformCompatibility = ({
tipContent={
<>
Estimated compatibility based on the <br />
tables used in the query. Querying <br />
iPhones & iPads is not supported.
tables used in the query.
<br />
<br />
Only live queries are supported on ChromeOS.
<br />
<br />
Querying iPhones & iPads is not supported.
</>
}
>

View file

@ -70,7 +70,6 @@ $base-class: "button";
border: 0;
position: relative;
cursor: pointer;
min-width: max-content;
&:focus {
outline: none;

View file

@ -54,7 +54,8 @@ export interface IDropdownWrapper {
isSearchable?: boolean;
isDisabled?: boolean;
placeholder?: string;
menuPortalTarget?: HTMLElement | null;
/** E.g. scroll to view dropdown menu in a scrollable parent container */
onMenuOpen?: () => void;
}
const baseClass = "dropdown-wrapper";
@ -72,7 +73,7 @@ const DropdownWrapper = ({
isSearchable,
isDisabled = false,
placeholder,
menuPortalTarget,
onMenuOpen,
}: IDropdownWrapper) => {
const wrapperClassNames = classnames(baseClass, className);
@ -326,12 +327,10 @@ const DropdownWrapper = ({
value={getCurrentValue()}
onChange={handleChange}
isDisabled={isDisabled}
menuPortalTarget={
menuPortalTarget === undefined ? document.body : menuPortalTarget
}
noOptionsMessage={() => "No results found"}
tabIndex={isDisabled ? -1 : 0} // Ensures disabled dropdown has no keyboard accessibility
placeholder={placeholder}
onMenuOpen={onMenuOpen}
/>
</FormField>
);

View file

@ -1,7 +1,7 @@
import React, { useCallback, useState } from "react";
import { useDebouncedCallback } from "use-debounce";
import { QueryablePlatform, SUPPORTED_PLATFORMS } from "interfaces/platform";
import { QueryablePlatform, QUERYABLE_PLATFORMS } from "interfaces/platform";
import { checkPlatformCompatibility } from "utilities/sql_tools";
import PlatformCompatibility from "components/PlatformCompatibility";
@ -37,7 +37,7 @@ const usePlatformCompatibility = (): IPlatformCompatibility => {
);
const getCompatiblePlatforms = useCallback(
() => SUPPORTED_PLATFORMS.filter((p) => compatiblePlatforms?.includes(p)),
() => QUERYABLE_PLATFORMS.filter((p) => compatiblePlatforms?.includes(p)),
[compatiblePlatforms]
);

View file

@ -3,7 +3,7 @@ import { forEach } from "lodash";
import {
SelectedPlatformString,
SUPPORTED_PLATFORMS,
QUERYABLE_PLATFORMS,
QueryablePlatform,
} from "interfaces/platform";
@ -48,7 +48,7 @@ const usePlatformSelector = (
};
const getSelectedPlatforms = useCallback(() => {
return SUPPORTED_PLATFORMS.filter((p) => checksByPlatform[p]);
return QUERYABLE_PLATFORMS.filter((p) => checksByPlatform[p]);
}, [checksByPlatform]);
const isAnyPlatformSelected = Object.values(checksByPlatform).includes(true);

View file

@ -22,13 +22,18 @@ export type QueryableDisplayPlatform = Exclude<
>;
export type QueryablePlatform = Exclude<Platform, "ios" | "ipados">;
export const SUPPORTED_PLATFORMS: QueryablePlatform[] = [
export const QUERYABLE_PLATFORMS: QueryablePlatform[] = [
"darwin",
"windows",
"linux",
"chrome",
];
export const isQueryablePlatform = (
platform: string | undefined
): platform is QueryablePlatform =>
QUERYABLE_PLATFORMS.includes(platform as QueryablePlatform);
// TODO - add "iOS" and "iPadOS" once we support them
export const VULN_SUPPORTED_PLATFORMS: Platform[] = ["darwin", "windows"];

View file

@ -14,10 +14,6 @@ export interface IStoredQueryResponse {
query: ISchedulableQuery;
}
export interface IFleetQueriesResponse {
queries: ISchedulableQuery[];
}
export interface IQuery {
created_at: string;
updated_at: string;

View file

@ -3,7 +3,11 @@ import PropTypes from "prop-types";
import { IFormField } from "./form_field";
import { IPack } from "./pack";
import { SelectedPlatformString, QueryablePlatform } from "./platform";
import {
SelectedPlatformString,
QueryablePlatform,
SelectedPlatform,
} from "./platform";
// Query itself
export interface ISchedulableQuery {
@ -15,7 +19,7 @@ export interface ISchedulableQuery {
query: string;
team_id: number | null;
interval: number;
platform: SelectedPlatformString; // Might more accurately be called `platforms_to_query` comma-sepparated string of platforms to query, default all platforms if ommitted
platform: SelectedPlatformString; // Might more accurately be called `platforms_to_query` or `targeted_platforms` comma-sepparated string of platforms to query, default all platforms if ommitted
min_osquery_version: string;
automations_enabled: boolean;
logging: QueryLoggingOption;
@ -32,7 +36,7 @@ export interface ISchedulableQuery {
export interface IEnhancedQuery extends ISchedulableQuery {
performance: string;
platforms: QueryablePlatform[];
targetedPlatforms: QueryablePlatform[];
}
export interface ISchedulableQueryStats {
user_time_p50?: number | null;
@ -67,7 +71,14 @@ export interface IListQueriesResponse {
export interface IQueryKeyQueriesLoadAll {
scope: "queries";
teamId: number | undefined;
teamId?: number;
page?: number;
perPage?: number;
query?: string;
orderDirection?: "asc" | "desc";
orderKey?: string;
mergeInherited?: boolean;
targetedPlatform?: SelectedPlatform;
}
// Create a new query
/** POST /api/v1/fleet/queries */

View file

@ -460,7 +460,7 @@ const DashboardPage = ({ router, location }: IDashboardProps): JSX.Element => {
setShowAddHostsModal(!showAddHostsModal);
};
// NOTE: this is called once on the initial rendering. The initial render of
// This is called once on the initial rendering. The initial render of
// the TableContainer child component will call this handler.
const onSoftwareQueryChange = async ({
pageIndex: newPageIndex,

View file

@ -98,6 +98,9 @@ const FleetMaintainedAppDetailsPage = ({
}: IFleetMaintainedAppDetailsPageProps) => {
const teamId = location.query.team_id;
const appId = parseInt(routeParams.id, 10);
if (isNaN(appId)) {
router.push(PATHS.SOFTWARE_ADD_FLEET_MAINTAINED);
}
const { renderFlash } = useContext(NotificationContext);
const { isPremiumTier } = useContext(AppContext);

View file

@ -413,7 +413,7 @@ const UsersPage = ({ location, router }: ITeamSubnavProps): JSX.Element => {
defaultSortDirection="asc"
actionButton={{
name: isGlobalAdmin ? "add user" : "create user",
buttonText: isGlobalAdmin ? "Add users" : "Create users",
buttonText: isGlobalAdmin ? "Add users" : "Create user",
variant: "brand",
onActionButtonClick: isGlobalAdmin
? toggleAddUserModal
@ -476,7 +476,7 @@ const UsersPage = ({ location, router }: ITeamSubnavProps): JSX.Element => {
onCancel={toggleCreateUserModal}
onSubmit={onCreateUserSubmit}
defaultGlobalRole={null}
defaultTeamRole="observer"
defaultTeamRole="Observer"
defaultTeams={[
{ id: currentTeamDetails.id, name: "", role: "observer" },
]}

View file

@ -59,7 +59,6 @@ const AddUserModal = ({
onCancel={onCancel}
onSubmit={onSubmit}
availableTeams={availableTeams || []}
submitText="Add"
isPremiumTier={isPremiumTier}
smtpConfigured={smtpConfigured}
sesConfigured={sesConfigured}

View file

@ -69,7 +69,6 @@ const EditUserModal = ({
onCancel={onCancel}
onSubmit={onSubmit}
availableTeams={availableTeams}
submitText="Save"
isPremiumTier={isPremiumTier}
smtpConfigured={smtpConfigured}
sesConfigured={sesConfigured}

View file

@ -13,6 +13,7 @@ interface ISelectRoleFormProps {
teams: ITeam[];
onFormChange: (teams: ITeam[]) => void;
isApiOnly?: boolean;
onMenuOpen?: () => void;
}
const generateSelectedTeamData = (
@ -33,6 +34,7 @@ const SelectRoleForm = ({
teams,
onFormChange,
isApiOnly,
onMenuOpen,
}: ISelectRoleFormProps): JSX.Element => {
const { isPremiumTier } = useContext(AppContext);
@ -55,10 +57,12 @@ const SelectRoleForm = ({
return (
<DropdownWrapper
name="Team role"
label="Role"
options={roleOptions({ isPremiumTier, isApiOnly })}
value={selectedRole}
onChange={updateSelectedRole}
isSearchable={false}
onMenuOpen={onMenuOpen}
/>
);
};

View file

@ -17,6 +17,7 @@ interface ISelectedTeamsFormProps {
usersCurrentTeams: ITeam[];
onFormChange: (teams: ITeam[]) => void;
isApiOnly?: boolean;
onMenuOpen?: () => void;
}
const baseClass = "selected-teams-form";
@ -106,6 +107,7 @@ const SelectedTeamsForm = ({
usersCurrentTeams,
onFormChange,
isApiOnly,
onMenuOpen,
}: ISelectedTeamsFormProps): JSX.Element => {
const [teamsFormList, updateSelectedTeams] = useSelectedTeamState(
availableTeams,
@ -137,6 +139,7 @@ const SelectedTeamsForm = ({
onChange={(newValue: SingleValue<CustomOptionType>) =>
updateSelectedTeams(teamItem.id, newValue as CustomOptionType)
}
onMenuOpen={onMenuOpen}
/>
</div>
);

View file

@ -11,7 +11,6 @@ describe("UserForm - component", () => {
availableTeams: [],
onCancel: noop,
onSubmit: noop,
submitText: "Add",
isModifiedByGlobalAdmin: true,
isPremiumTier: true,
smtpConfigured: true,
@ -42,7 +41,7 @@ describe("UserForm - component", () => {
it("renders SSO option when canUseSso is true", () => {
render(<UserForm {...defaultProps} canUseSso />);
expect(screen.getByLabelText("Enable single sign-on")).toBeInTheDocument();
expect(screen.getByLabelText("Single sign-on")).toBeInTheDocument();
});
it("disables invite user option when SMTP and SES are not configured", () => {
@ -72,20 +71,51 @@ describe("UserForm - component", () => {
// Verify that non-premium elements are still present
expect(screen.getByLabelText("Full name")).toBeInTheDocument();
expect(screen.getByLabelText("Email")).toBeInTheDocument();
expect(screen.getByLabelText("Password")).toBeInTheDocument();
expect(
screen.queryByRole("radio", { name: "Password" })
).toBeInTheDocument();
});
it("does not render password and 2FA sections when SSO is enabled", () => {
it("does not render password and 2FA sections when SSO is selected", () => {
render(<UserForm {...defaultProps} canUseSso />);
// Enable SSO
const ssoRadio = screen.getByLabelText("Enable single sign-on");
const ssoRadio = screen.getByLabelText("Single sign-on");
ssoRadio.click();
// Check that password and 2FA sections are not present
expect(screen.queryByText("Password")).not.toBeInTheDocument();
// Check that the password radio is present
const passwordRadio = screen.getByRole("radio", { name: "Password" });
expect(passwordRadio).not.toBeDisabled();
// Check that password input field and 2FA sections are not present
expect(
screen.queryByRole("input", { name: "Password" })
).not.toBeInTheDocument();
expect(
screen.queryByText("Enable two-factor authentication")
).not.toBeInTheDocument();
});
it("displays disabled SSO option when SSO is globally disabled but was previously enabled for the user", async () => {
const props = {
...defaultProps,
defaultName: "User 1",
defaultEmail: "user@example.com",
currentUserId: 1,
canUseSso: false,
isSsoEnabled: true,
isNewUser: false,
};
const { user } = renderWithSetup(<UserForm {...props} />);
// Check that the SSO radio is disabled
const ssoRadio = screen.getByLabelText("Single sign-on");
expect(ssoRadio).toBeDisabled();
await user.click(screen.getByText("Save"));
expect(
screen.getByText(/Password field must be completed/i)
).toBeInTheDocument();
});
});

View file

@ -32,7 +32,7 @@ import SelectedTeamsForm from "../SelectedTeamsForm/SelectedTeamsForm";
import SelectRoleForm from "../SelectRoleForm/SelectRoleForm";
import { roleOptions } from "../../helpers/userManagementHelpers";
const baseClass = "add-user-form";
const baseClass = "user-form";
export enum NewUserType {
AdminInvited = "ADMIN_INVITED",
@ -63,7 +63,6 @@ interface IUserFormProps {
availableTeams: ITeam[];
onCancel: () => void;
onSubmit: (formData: IUserFormData) => void;
submitText: string;
defaultName?: string;
defaultEmail?: string;
currentUserId?: number;
@ -90,7 +89,6 @@ const UserForm = ({
availableTeams,
onCancel,
onSubmit,
submitText,
defaultName,
defaultEmail,
currentUserId,
@ -140,6 +138,14 @@ const UserForm = ({
const [isGlobalUser, setIsGlobalUser] = useState(!!defaultGlobalRole);
useEffect(() => {
// If SSO is globally disabled but user previously signed in via SSO,
// require password is automatically selected on first render
if (!canUseSso && !isNewUser && isSsoEnabled) {
setFormData({ ...formData, sso_enabled: false });
}
}, []);
// For scrollable modal (re-rerun when formData changes)
useEffect(() => {
checkScroll();
@ -160,6 +166,19 @@ const UserForm = ({
};
};
// Used to show entire dropdown when a dropdown menu is open in scrollable component of a modal
// menuPortalTarget solution not used as scrolling is weird
const scrollToFitDropdownMenu = () => {
if (topDivRef?.current) {
setTimeout(() => {
if (topDivRef.current) {
topDivRef.current.scrollTop =
topDivRef.current.scrollHeight - topDivRef.current.clientHeight;
}
}, 50); // Delay needed for scrollHeight to update first
}
};
const onCheckboxChange = (formField: string): ((evt: string) => void) => {
return (evt: string) => {
return onInputChange(formField)(evt);
@ -248,12 +267,16 @@ const UserForm = ({
} else if (!validEmail(formData.email)) {
newErrors.email = `${formData.email} is not a valid email`;
}
// Only test password on new created user (not invited user) or if sso not enabled
if (
// Password auth required for new "create user" (not new "invite user") with SSO disabled
const isNewAdminCreatedUserWithoutSSO =
isNewUser &&
formData.newUserType === NewUserType.AdminCreated &&
!formData.sso_enabled
) {
!formData.sso_enabled;
// Force switch existing user to password auth if SSO is disabled globally but was enabled
const isExistingUserForcedToPasswordAuth = !canUseSso && isSsoEnabled;
if (isNewAdminCreatedUserWithoutSSO || isExistingUserForcedToPasswordAuth) {
if (formData.password !== null && !validPassword(formData.password)) {
newErrors.password = "Password must meet the criteria below";
}
@ -308,6 +331,7 @@ const UserForm = ({
}
}}
isSearchable={false}
onMenuOpen={scrollToFitDropdownMenu}
/>
</>
);
@ -356,15 +380,17 @@ const UserForm = ({
usersCurrentTeams={formData.teams}
onFormChange={onSelectedTeamChange}
isApiOnly={isApiOnly}
onMenuOpen={scrollToFitDropdownMenu}
/>
</>
) : (
<SelectRoleForm
currentTeam={currentTeam || formData.teams[0]}
teams={formData.teams}
defaultTeamRole={defaultTeamRole || "observer"}
defaultTeamRole={defaultTeamRole || "Observer"}
onFormChange={onTeamRoleChange}
isApiOnly={isApiOnly}
onMenuOpen={scrollToFitDropdownMenu}
/>
))}
{!availableTeams.length && renderNoTeamsMessage()}
@ -470,50 +496,39 @@ const UserForm = ({
<div className="form-field__label">Authentication</div>
<Radio
className={`${baseClass}__radio-input`}
label="Enable single sign-on"
id="enable-single-sign-on"
label={
canUseSso ? (
"Single sign-on"
) : (
<TooltipWrapper
tipContent={
<>
SSO is not enabled in organization settings.
<br />
User must sign in with a password.
</>
}
>
Single sign-on
</TooltipWrapper>
)
}
id="single-sign-on-authentication"
checked={!!formData.sso_enabled}
value="true"
name="authentication-type"
onChange={() => onSsoChange(true)}
disabled={!canUseSso}
/>
<Radio
className={`${baseClass}__radio-input`}
label="Require password"
id="require-password"
label="Password"
id="password-authentication"
disabled={!(smtpConfigured || sesConfigured)}
checked={!formData.sso_enabled}
value="false"
name="authentication-type"
onChange={() => onSsoChange(false)}
/** Note: This was a checkbox that is now a radio button, waiting on
* product to confirm if we're adding help text to radio buttons as
* it's not in Figma design system, the Figma here, or in the Radio
* component, but the helpText already existed on the checkbox version
*/
// helpText={
// canUseSso ? (
// <p className={`${baseClass}__sso-input sublabel`}>
// Password authentication will be disabled for this user.
// </p>
// ) : (
// <span className={`${baseClass}__sso-input sublabel-nosso`}>
// This user previously signed in via SSO, which has been
// globally disabled.{" "}
// <button
// className="button--text-link"
// onClick={onSsoDisable}
// >
// Add password instead
// <Icon
// name="chevron-right"
// color="core-fleet-blue"
// size="small"
// />
// </button>
// </span>
// )
// }
/>
</div>
);
@ -622,14 +637,14 @@ const UserForm = ({
<form autoComplete="off">
{isNewUser && renderAccountSection()}
{renderNameAndEmailSection()}
{((!isNewUser && formData.sso_enabled) || canUseSso) &&
renderAuthenticationSection()}
{renderAuthenticationSection()}
{((isNewUser && formData.newUserType !== NewUserType.AdminInvited) ||
(!isNewUser && !isInvitePending && isModifiedByGlobalAdmin)) &&
!formData.sso_enabled &&
renderPasswordSection()}
{(isPremiumTier || isMfaEnabled) &&
!formData.sso_enabled &&
isModifiedByGlobalAdmin &&
renderTwoFactorAuthenticationOption()}
{isPremiumTier ? renderPremiumRoleOptions() : renderGlobalRoleForm()}
</form>
@ -649,11 +664,11 @@ const UserForm = ({
type="submit"
variant="brand"
onClick={onFormSubmit}
className={`${submitText === "Add" ? "add" : "save"}-loading
className={`${isNewUser ? "add" : "save"}-loading
`}
isLoading={isUpdatingUsers}
>
{submitText}
{isNewUser ? "Add" : "Save"}
</Button>
</>
}

View file

@ -1,4 +1,4 @@
.add-user-form {
.user-form {
max-height: 581px;
overflow-y: auto;

View file

@ -214,19 +214,24 @@ const HostDetailsPage = ({
>("past");
const [activityPage, setActivityPage] = useState(0);
// Optimization TODO: move this call into the SelectQuery modal, since queries are only used if that modal is opened
const { data: fleetQueries, error: fleetQueriesError } = useQuery<
IListQueriesResponse,
Error,
ISchedulableQuery[],
IQueryKeyQueriesLoadAll[]
>([{ scope: "queries", teamId: undefined }], () => queryAPI.loadAll(), {
enabled: !!hostIdFromURL,
refetchOnMount: false,
refetchOnReconnect: false,
refetchOnWindowFocus: false,
retry: false,
select: (data: IListQueriesResponse) => data.queries,
});
>(
[{ scope: "queries", teamId: undefined }],
({ queryKey }) => queryAPI.loadAll(queryKey[0]),
{
enabled: !!hostIdFromURL,
refetchOnMount: false,
refetchOnReconnect: false,
refetchOnWindowFocus: false,
retry: false,
select: (data: IListQueriesResponse) => data.queries,
}
);
const { data: teams } = useQuery<ILoadTeamsResponse, Error, ITeam[]>(
"teams",

View file

@ -8,6 +8,7 @@ import Button from "components/buttons/Button";
import HeaderCell from "components/TableContainer/DataTable/HeaderCell";
import ViewAllHostsLink from "components/ViewAllHostsLink";
import { IndicatorStatus } from "components/StatusIndicatorWithIcon/StatusIndicatorWithIcon";
import TooltipTruncatedText from "components/TooltipTruncatedText";
interface IHeaderProps {
column: {
@ -82,7 +83,7 @@ const generatePolicyTableHeaders = (
onClick={onClickPolicyName}
variant="text-icon"
>
<span className={`policy-info-text`}>{name}</span>
<TooltipTruncatedText value={name} />
</Button>
);
},

View file

@ -23,6 +23,10 @@
tr {
.name__cell {
max-width: 0; // sets ellipsis
.truncated-tooltip {
font-weight: $regular;
}
}
.policy-link {
@ -39,12 +43,6 @@
width: 100%; // sets ellipsis
justify-content: left;
.policy-info-text {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
&::after {
content: url("../assets/images/icon-arrow-right-vibrant-blue-10x18@2x.png");
transform: scale(0.5);

View file

@ -250,14 +250,14 @@ const HostSoftware = ({
return isMyDevicePage
? generateDeviceSoftwareTableConfig()
: generateHostSoftwareTableConfig({
router,
softwareIdActionPending,
userHasSWWritePermission,
hostScriptsEnabled,
onSelectAction,
teamId: hostTeamId,
hostCanWriteSoftware,
hostMDMEnrolled,
softwareIdActionPending,
router,
teamId: hostTeamId,
onSelectAction,
});
}, [
isMyDevicePage,

View file

@ -2,20 +2,25 @@ import React, { useState, useCallback, useContext } from "react";
import { useQuery } from "react-query";
import { InjectedRouter, Params } from "react-router/lib/Router";
import { AppContext } from "context/app";
import { NotificationContext } from "context/notification";
import { IPack, IStoredPackResponse } from "interfaces/pack";
import { IQuery, IFleetQueriesResponse } from "interfaces/query";
import { IQuery } from "interfaces/query";
import {
IPackQueryFormData,
IScheduledQuery,
IStoredScheduledQueriesResponse,
} from "interfaces/scheduled_query";
import { ITarget, ITargetsAPIResponse } from "interfaces/target";
import { AppContext } from "context/app";
import { NotificationContext } from "context/notification";
import {
IQueryKeyQueriesLoadAll,
ISchedulableQuery,
} from "interfaces/schedulable_query";
import { getErrorReason } from "interfaces/errors";
import packsAPI from "services/entities/packs";
import queriesAPI from "services/entities/queries";
import queriesAPI, { IQueriesResponse } from "services/entities/queries";
import scheduledQueriesAPI from "services/entities/scheduled_queries";
import PATHS from "router/paths";
@ -50,13 +55,18 @@ const EditPacksPage = ({
const packId: number = parseInt(paramsPackId, 10);
const { data: fleetQueries } = useQuery<
IFleetQueriesResponse,
const { data: queries } = useQuery<
IQueriesResponse,
Error,
IQuery[]
>(["fleet queries"], () => queriesAPI.loadAll(), {
select: (data: IFleetQueriesResponse) => data.queries,
});
ISchedulableQuery[],
IQueryKeyQueriesLoadAll[]
>(
[{ scope: "queries", teamId: undefined }],
({ queryKey }) => queriesAPI.loadAll(queryKey[0]),
{
select: (data) => data.queries,
}
);
const { data: storedPack } = useQuery<IStoredPackResponse, Error, IPack>(
["stored pack"],
@ -244,17 +254,17 @@ const EditPacksPage = ({
isUpdatingPack={isUpdatingPack}
/>
)}
{showPackQueryEditorModal && fleetQueries && (
{showPackQueryEditorModal && queries && (
<PackQueryEditorModal
onCancel={togglePackQueryEditorModal}
onPackQueryFormSubmit={onPackQueryEditorSubmit}
allQueries={fleetQueries}
allQueries={queries}
editQuery={selectedPackQuery}
packId={packId}
isUpdatingPack={isUpdatingPack}
/>
)}
{showRemovePackQueryModal && fleetQueries && (
{showRemovePackQueryModal && queries && (
<RemovePackQueryModal
onCancel={toggleRemovePackQueryModal}
onSubmit={onRemovePackQuerySubmit}

View file

@ -169,7 +169,10 @@ const ManagePolicyPage = ({
// Needs update on location change or table state might not match URL
const [searchQuery, setSearchQuery] = useState(initialSearchQuery);
const [tableQueryData, setTableQueryData] = useState<ITableQueryData>();
const [
tableQueryDataForApi,
setTableQueryDataForApi,
] = useState<ITableQueryData>();
const [sortHeader, setSortHeader] = useState(initialSortHeader);
const [sortDirection, setSortDirection] = useState<
"asc" | "desc" | undefined
@ -225,7 +228,7 @@ const ManagePolicyPage = ({
[
{
scope: "globalPolicies",
page: tableQueryData?.pageIndex,
page: tableQueryDataForApi?.pageIndex,
perPage: DEFAULT_PAGE_SIZE,
query: searchQuery,
orderDirection: sortDirection,
@ -281,7 +284,7 @@ const ManagePolicyPage = ({
[
{
scope: "teamPolicies",
page: tableQueryData?.pageIndex,
page: tableQueryDataForApi?.pageIndex,
perPage: DEFAULT_PAGE_SIZE,
query: searchQuery,
orderDirection: sortDirection,
@ -390,7 +393,7 @@ const ManagePolicyPage = ({
// NOTE: used to reset page number to 0 when modifying filters
const handleResetPageIndex = () => {
setTableQueryData(
setTableQueryDataForApi(
(prevState) =>
({
...prevState,
@ -412,11 +415,11 @@ const ManagePolicyPage = ({
// TODO: Look into useDebounceCallback with dependencies
const onQueryChange = useCallback(
async (newTableQuery: ITableQueryData) => {
if (!isRouteOk || isEqual(newTableQuery, tableQueryData)) {
if (!isRouteOk || isEqual(newTableQuery, tableQueryDataForApi)) {
return;
}
setTableQueryData({ ...newTableQuery });
setTableQueryDataForApi({ ...newTableQuery });
const {
pageIndex: newPageIndex,
@ -451,11 +454,11 @@ const ManagePolicyPage = ({
queryParams: { ...queryParams, ...newQueryParams },
});
router?.replace(locationPath);
router?.push(locationPath);
},
[
isRouteOk,
tableQueryData,
tableQueryDataForApi,
sortDirection,
sortHeader,
searchQuery,
@ -557,9 +560,9 @@ const ManagePolicyPage = ({
responses.concat(
changedPolicies.map((changedPolicy) => {
return teamPoliciesAPI.update(changedPolicy.id, {
// "software_title_id": 0 will unset software install for the policy
// "software_title_id": null will unset software install for the policy
// "software_title_id": X will set the value to the given integer (except 0).
software_title_id: changedPolicy.swIdToInstall || 0,
software_title_id: changedPolicy.swIdToInstall || null,
team_id: teamIdForApi,
});
})
@ -610,9 +613,9 @@ const ManagePolicyPage = ({
responses.concat(
changedPolicies.map((changedPolicy) => {
return teamPoliciesAPI.update(changedPolicy.id, {
// "script_id": 0 will unset running a script for the policy (a script never has ID 0)
// "script_id": null will unset running a script for the policy
// "script_id": X will sets script X to run when the policy fails
script_id: changedPolicy.scriptIdToRun || 0,
script_id: changedPolicy.scriptIdToRun || null,
team_id: teamIdForApi,
});
})

View file

@ -55,12 +55,6 @@ const PoliciesTable = ({
}: IPoliciesTableProps): JSX.Element => {
const { config } = useContext(AppContext);
const onTableQueryChange = (newTableQuery: ITableQueryData) => {
onQueryChange({
...newTableQuery,
});
};
const emptyState = () => {
const emptyPolicies: IEmptyTableProps = {
graphicName: "empty-policies",
@ -135,7 +129,7 @@ const PoliciesTable = ({
})
}
renderCount={renderPoliciesCount}
onQueryChange={onTableQueryChange}
onQueryChange={onQueryChange}
inputPlaceHolder="Search by name"
searchable={searchable}
resetPageIndex={resetPageIndex}

View file

@ -14,7 +14,11 @@ import { QueryContext } from "context/query";
import { TableContext } from "context/table";
import { NotificationContext } from "context/notification";
import { getPerformanceImpactDescription } from "utilities/helpers";
import { QueryablePlatform, SelectedPlatform } from "interfaces/platform";
import {
isQueryablePlatform,
QueryablePlatform,
SelectedPlatform,
} from "interfaces/platform";
import {
IEnhancedQuery,
IQueryKeyQueriesLoadAll,
@ -22,10 +26,9 @@ import {
} from "interfaces/schedulable_query";
import { DEFAULT_TARGETS_BY_TYPE } from "interfaces/target";
import { API_ALL_TEAMS_ID } from "interfaces/team";
import queriesAPI from "services/entities/queries";
import queriesAPI, { IQueriesResponse } from "services/entities/queries";
import PATHS from "router/paths";
import { DEFAULT_QUERY } from "utilities/constants";
import { checkPlatformCompatibility } from "utilities/sql_tools";
import Button from "components/buttons/Button";
import Spinner from "components/Spinner";
import TableDataError from "components/DataError";
@ -37,13 +40,16 @@ import DeleteQueryModal from "./components/DeleteQueryModal";
import ManageQueryAutomationsModal from "./components/ManageQueryAutomationsModal/ManageQueryAutomationsModal";
import PreviewDataModal from "./components/PreviewDataModal/PreviewDataModal";
const DEFAULT_PAGE_SIZE = 20;
const baseClass = "manage-queries-page";
interface IManageQueriesPageProps {
router: InjectedRouter; // v3
location: {
pathname: string;
query: {
platform?: SelectedPlatform;
// note that the URL value "darwin" will correspond to the request query param "macos"
platform?: SelectedPlatform; // which targeted platform to filter queries by
page?: string;
query?: string;
order_key?: string;
@ -54,10 +60,9 @@ interface IManageQueriesPageProps {
};
}
const getPlatforms = (queryString: string): QueryablePlatform[] => {
const { platforms } = checkPlatformCompatibility(queryString);
return platforms ?? [];
const getTargetedPlatforms = (platformString: string): QueryablePlatform[] => {
const platforms = platformString.split(",");
return platforms.filter(isQueryablePlatform);
};
export const enhanceQuery = (q: ISchedulableQuery): IEnhancedQuery => {
@ -66,7 +71,7 @@ export const enhanceQuery = (q: ISchedulableQuery): IEnhancedQuery => {
performance: getPerformanceImpactDescription(
pick(q.stats, ["user_time_p50", "system_time_p50", "total_executions"])
),
platforms: getPlatforms(q.query),
targetedPlatforms: getTargetedPlatforms(q.platform),
};
};
@ -74,7 +79,6 @@ const ManageQueriesPage = ({
router,
location,
}: IManageQueriesPageProps): JSX.Element => {
const queryParams = location.query;
const {
isGlobalAdmin,
isGlobalMaintainer,
@ -118,46 +122,54 @@ const ManageQueriesPage = ({
const [showPreviewDataModal, setShowPreviewDataModal] = useState(false);
const [isUpdatingQueries, setIsUpdatingQueries] = useState(false);
const [isUpdatingAutomations, setIsUpdatingAutomations] = useState(false);
const [queriesAvailableToAutomate, setQueriesAvailableToAutomate] = useState<
IEnhancedQuery[] | []
>([]);
const curPageFromURL = location.query.page
? parseInt(location.query.page, 10)
: 0;
const {
data: enhancedQueries,
data: queriesResponse,
error: queriesError,
isFetching: isFetchingQueries,
isLoading: isLoadingQueries,
refetch: refetchQueries,
} = useQuery<
IEnhancedQuery[],
IQueriesResponse,
Error,
IEnhancedQuery[],
IQueriesResponse,
IQueryKeyQueriesLoadAll[]
>(
[{ scope: "queries", teamId: teamIdForApi }],
({ queryKey: [{ teamId }] }) =>
queriesAPI
.loadAll(teamId, teamId !== API_ALL_TEAMS_ID)
.then(({ queries }) => queries.map(enhanceQuery)),
[
{
scope: "queries",
teamId: teamIdForApi,
page: curPageFromURL,
perPage: DEFAULT_PAGE_SIZE,
// a search match query, not a Fleet Query
query: location.query.query,
orderDirection: location.query.order_direction,
orderKey: location.query.order_key,
mergeInherited: teamIdForApi !== API_ALL_TEAMS_ID,
targetedPlatform: location.query.platform,
},
],
({ queryKey }) => queriesAPI.loadAll(queryKey[0]),
{
refetchOnWindowFocus: false,
enabled: isRouteOk,
staleTime: 5000,
onSuccess: (data) => {
if (data) {
const enhancedAllQueries = data.map(enhanceQuery);
const allQueriesAvailableToAutomate = teamIdForApi
? enhancedAllQueries.filter(
(query: IEnhancedQuery) => query.team_id === currentTeamId
)
: enhancedAllQueries;
setQueriesAvailableToAutomate(allQueriesAvailableToAutomate);
}
},
}
);
const enhancedQueries = queriesResponse?.queries.map(enhanceQuery);
const queriesAvailableToAutomate =
(teamIdForApi
? enhancedQueries?.filter(
(query: IEnhancedQuery) => query.team_id === currentTeamId
)
: enhancedQueries) ?? [];
const onlyInheritedQueries = useMemo(() => {
if (teamIdForApi === API_ALL_TEAMS_ID) {
// global scope
@ -166,11 +178,9 @@ const ManageQueriesPage = ({
return !enhancedQueries?.some((query) => query.team_id === teamIdForApi);
}, [teamIdForApi, enhancedQueries]);
const automatedQueryIds = useMemo(() => {
return queriesAvailableToAutomate
.filter((query) => query.automations_enabled)
.map((query) => query.id);
}, [queriesAvailableToAutomate]);
const automatedQueryIds = queriesAvailableToAutomate
.filter((query) => query.automations_enabled)
.map((query) => query.id);
useEffect(() => {
const path = location.pathname + location.search;
@ -263,7 +273,7 @@ const ManageQueriesPage = ({
};
const renderQueriesTable = () => {
if (isFetchingQueries) {
if (isLoadingQueries) {
return <Spinner />;
}
if (queriesError) {
@ -271,7 +281,9 @@ const ManageQueriesPage = ({
}
return (
<QueriesTable
queriesList={enhancedQueries || []}
queries={enhancedQueries || []}
totalQueriesCount={queriesResponse?.count}
hasNextResults={!!queriesResponse?.meta.has_next_results}
onlyInheritedQueries={onlyInheritedQueries}
isLoading={isFetchingQueries}
onCreateQueryClick={onCreateQueryClick}
@ -279,8 +291,9 @@ const ManageQueriesPage = ({
isOnlyObserver={isOnlyObserver}
isObserverPlus={isObserverPlus}
isAnyTeamObserverPlus={isAnyTeamObserverPlus || false}
// changes in table state are propagated to the API call on this page via this router pushing to the URL
router={router}
queryParams={queryParams}
queryParams={location.query}
currentTeamId={teamIdForApi}
/>
);
@ -327,7 +340,12 @@ const ManageQueriesPage = ({
setIsUpdatingAutomations(false);
}
},
[refetchQueries, automatedQueryIds, toggleManageAutomationsModal]
[
automatedQueryIds,
renderFlash,
refetchQueries,
toggleManageAutomationsModal,
]
);
const renderModals = () => {
@ -367,6 +385,13 @@ const ManageQueriesPage = ({
isTeamMaintainer ||
isObserverPlus; // isObserverPlus checks global and selected team
const hideQueryActions =
// there are no filters and no returned queries, indicating there are no global/team queries at all
!(!!location.query.query || !!location.query.platform) &&
!queriesResponse?.count &&
// the user has permission
(!isOnlyObserver || isObserverPlus || isAnyTeamObserverPlus);
return (
<MainContent className={baseClass}>
<div className={`${baseClass}__wrapper`}>
@ -376,7 +401,8 @@ const ManageQueriesPage = ({
<div className={`${baseClass}__title`}>{renderHeader()}</div>
</div>
</div>
{!!enhancedQueries?.length && (
{!hideQueryActions && (
<div className={`${baseClass}__action-button-container`}>
{(isGlobalAdmin || isTeamAdmin) && !onlyInheritedQueries && (
<Button

View file

@ -146,7 +146,9 @@ describe("QueriesTable", () => {
it("Renders the page-wide empty state when no queries are present", () => {
const testData: IQueriesTableProps[] = [
{
queriesList: [],
queries: [],
totalQueriesCount: 0,
hasNextResults: false,
onlyInheritedQueries: false,
isLoading: false,
onDeleteQueryClick: jest.fn(),
@ -169,7 +171,9 @@ describe("QueriesTable", () => {
it("Renders inherited global queries and team queries when viewing a team, then renders the 'no-matching' empty state when a search string is entered that matches no queries", async () => {
const testData: IQueriesTableProps[] = [
{
queriesList: [...testGlobalQueries, ...testTeamQueries],
queries: [...testGlobalQueries, ...testTeamQueries],
totalQueriesCount: 4,
hasNextResults: false,
onlyInheritedQueries: false,
isLoading: false,
onDeleteQueryClick: jest.fn(),
@ -228,7 +232,9 @@ describe("QueriesTable", () => {
const { user } = render(
<QueriesTable
queriesList={testQueries}
queries={testQueries}
totalQueriesCount={1}
hasNextResults={false}
onlyInheritedQueries={false}
isLoading={false}
onDeleteQueryClick={jest.fn()}
@ -267,7 +273,9 @@ describe("QueriesTable", () => {
const { user } = render(
<QueriesTable
queriesList={testQueries}
queries={testQueries}
totalQueriesCount={1}
hasNextResults={false}
onlyInheritedQueries={false}
isLoading={false}
onDeleteQueryClick={jest.fn()}
@ -305,7 +313,9 @@ describe("QueriesTable", () => {
render(
<QueriesTable
queriesList={testQueries}
queries={testQueries}
totalQueriesCount={1}
hasNextResults={false}
onlyInheritedQueries={false}
isLoading={false}
onDeleteQueryClick={jest.fn()}
@ -332,7 +342,9 @@ describe("QueriesTable", () => {
const { container, user } = render(
<QueriesTable
queriesList={[...testTeamQueries, ...testGlobalQueries]}
queries={[...testTeamQueries, ...testGlobalQueries]}
totalQueriesCount={4}
hasNextResults={false}
onlyInheritedQueries={false}
isLoading={false}
onDeleteQueryClick={jest.fn()}

View file

@ -1,22 +1,14 @@
/* eslint-disable react/prop-types */
import React, {
useContext,
useCallback,
useMemo,
useState,
useEffect,
} from "react";
import React, { useContext, useCallback, useMemo } from "react";
import { InjectedRouter } from "react-router";
import { AppContext } from "context/app";
import { IEmptyTableProps } from "interfaces/empty_table";
import { SelectedPlatform } from "interfaces/platform";
import { isQueryablePlatform, SelectedPlatform } from "interfaces/platform";
import { IEnhancedQuery } from "interfaces/schedulable_query";
import { ITableQueryData } from "components/TableContainer/TableContainer";
import { IActionButtonProps } from "components/TableContainer/DataTable/ActionButton/ActionButton";
import PATHS from "router/paths";
import { getNextLocationPath } from "utilities/helpers";
import { checkPlatformCompatibility } from "utilities/sql_tools";
import Button from "components/buttons/Button";
import TableContainer from "components/TableContainer";
import TableCount from "components/TableContainer/TableCount";
@ -24,11 +16,14 @@ import CustomLink from "components/CustomLink";
import EmptyTable from "components/EmptyTable";
// @ts-ignore
import Dropdown from "components/forms/fields/Dropdown";
import generateColumnConfigs from "./QueriesTableConfig";
const baseClass = "queries-table";
export interface IQueriesTableProps {
queriesList: IEnhancedQuery[] | null;
queries: IEnhancedQuery[] | null;
totalQueriesCount: number | undefined;
hasNextResults: boolean;
onlyInheritedQueries: boolean;
isLoading: boolean;
onDeleteQueryClick: (selectedTableQueryIds: number[]) => void;
@ -38,7 +33,7 @@ export interface IQueriesTableProps {
isAnyTeamObserverPlus: boolean;
router?: InjectedRouter;
queryParams?: {
platform?: SelectedPlatform;
platform?: string; // which targeted platform to filter queries by
page?: string;
query?: string;
order_key?: string;
@ -50,44 +45,36 @@ export interface IQueriesTableProps {
const DEFAULT_SORT_DIRECTION = "asc";
const DEFAULT_SORT_HEADER = "name";
const DEFAULT_PAGE_SIZE = 20;
const DEFAULT_PLATFORM = "all";
// all platforms
const DEFAULT_PLATFORM: SelectedPlatform = "all";
const PLATFORM_FILTER_OPTIONS = [
{
disabled: false,
label: "All platforms",
value: "all",
helpText: "All queries.",
},
{
disabled: false,
label: "macOS",
value: "darwin",
helpText: "Queries that are compatible with macOS operating systems.",
},
{
disabled: false,
label: "Windows",
value: "windows",
helpText: "Queries that are compatible with Windows operating systems.",
},
{
disabled: false,
label: "Linux",
value: "linux",
helpText: "Queries that are compatible with Linux operating systems.",
},
{
disabled: false,
label: "ChromeOS",
value: "chrome",
helpText: "Queries that are compatible with Chromebooks.",
},
];
const QueriesTable = ({
queriesList,
queries,
totalQueriesCount,
hasNextResults,
onlyInheritedQueries,
isLoading,
onDeleteQueryClick,
@ -101,38 +88,6 @@ const QueriesTable = ({
}: IQueriesTableProps): JSX.Element | null => {
const { currentUser } = useContext(AppContext);
// Client side filtering bugs fixed with bypassing TableContainer filters
// queriesState tracks search filter and compatible platform filter
// to correctly show filtered queries and filtered count
// isQueryStateLoading prevents flashing of unfiltered count during clientside filtering
const [queriesState, setQueriesState] = useState<IEnhancedQuery[]>([]);
const [isQueriesStateLoading, setIsQueriesStateLoading] = useState(true);
useEffect(() => {
setIsQueriesStateLoading(true);
if (queriesList) {
setQueriesState(
queriesList.filter((query) => {
const filterSearchQuery = queryParams?.query
? query.name
.toLowerCase()
.includes(queryParams?.query.toLowerCase())
: true;
const compatiblePlatforms =
checkPlatformCompatibility(query.query).platforms || [];
const filterCompatiblePlatform =
queryParams?.platform && queryParams?.platform !== "all"
? compatiblePlatforms.includes(queryParams?.platform)
: true;
return filterSearchQuery && filterCompatiblePlatform;
}) || []
);
}
setIsQueriesStateLoading(false);
}, [queriesList, queryParams]);
// Functions to avoid race conditions
const initialSearchQuery = (() => queryParams?.query ?? "")();
const initialSortHeader = (() =>
@ -141,23 +96,27 @@ const QueriesTable = ({
const initialSortDirection = (() =>
(queryParams?.order_direction as "asc" | "desc") ??
DEFAULT_SORT_DIRECTION)();
const initialPlatform = (() =>
(queryParams?.platform as "all" | "windows" | "linux" | "darwin") ??
DEFAULT_PLATFORM)();
const initialPage = (() =>
queryParams && queryParams.page ? parseInt(queryParams?.page, 10) : 0)();
// Source of truth is state held within TableContainer. That state is initialized using URL
// params, then subsequent updates to that state are pushed to the URL.
// TODO - remove extraneous defintions around these values
const searchQuery = initialSearchQuery;
const platform = initialPlatform;
const page = initialPage;
const sortDirection = initialSortDirection;
const sortHeader = initialSortHeader;
const targetedPlatformParam = queryParams?.platform;
const curTargetedPlatformFilter: SelectedPlatform = isQueryablePlatform(
targetedPlatformParam
)
? targetedPlatformParam
: DEFAULT_PLATFORM;
// TODO: Look into useDebounceCallback with dependencies
const onQueryChange = useCallback(
async (newTableQuery: ITableQueryData) => {
(newTableQuery: ITableQueryData) => {
const {
pageIndex: newPageIndex,
searchQuery: newSearchQuery,
@ -165,13 +124,13 @@ const QueriesTable = ({
sortHeader: newSortHeader,
} = newTableQuery;
// Rebuild queryParams to dispatch new browser location to react-router
const newQueryParams: { [key: string]: string | number | undefined } = {};
// Updates URL params
const newQueryParams: Record<string, string | number | undefined> = {};
newQueryParams.order_key = newSortHeader;
newQueryParams.order_direction = newSortDirection;
newQueryParams.platform = platform; // must set from URL
newQueryParams.platform =
curTargetedPlatformFilter === "all"
? undefined
: curTargetedPlatformFilter;
newQueryParams.page = newPageIndex;
newQueryParams.query = newSearchQuery;
// Reset page number to 0 for new filters
@ -182,46 +141,36 @@ const QueriesTable = ({
) {
newQueryParams.page = "0";
}
newQueryParams.team_id = queryParams?.team_id;
const locationPath = getNextLocationPath({
pathPrefix: PATHS.MANAGE_QUERIES,
queryParams: { ...queryParams, ...newQueryParams },
});
router?.replace(locationPath);
router?.push(locationPath);
},
[sortHeader, sortDirection, searchQuery, platform, router, page]
);
const onClientSidePaginationChange = useCallback(
(pageIndex: number) => {
const newQueryParams = {
...queryParams,
page: pageIndex, // update main table index
query: searchQuery,
};
const locationPath = getNextLocationPath({
pathPrefix: PATHS.MANAGE_QUERIES,
queryParams: newQueryParams,
});
router?.replace(locationPath);
},
[platform, searchQuery, sortDirection, sortHeader] // Dependencies required for correct variable state
[
sortHeader,
sortDirection,
searchQuery,
curTargetedPlatformFilter,
router,
page,
]
);
const getEmptyStateParams = useCallback(() => {
const emptyQueries: IEmptyTableProps = {
const emptyParams: IEmptyTableProps = {
graphicName: "empty-queries",
header: "You don't have any queries",
};
if (searchQuery) {
delete emptyQueries.graphicName;
emptyQueries.header = "No matching queries";
emptyQueries.info = "No queries match the current filters.";
if (searchQuery || curTargetedPlatformFilter !== "all") {
delete emptyParams.graphicName;
emptyParams.header = "No matching queries";
emptyParams.info = "No queries match the current filters.";
} else if (!isOnlyObserver || isObserverPlus || isAnyTeamObserverPlus) {
emptyQueries.additionalInfo = (
emptyParams.additionalInfo = (
<>
Create a new query, or{" "}
<CustomLink
@ -231,7 +180,7 @@ const QueriesTable = ({
/>
</>
);
emptyQueries.primaryButton = (
emptyParams.primaryButton = (
<Button
variant="brand"
className={`${baseClass}__create-button`}
@ -242,7 +191,7 @@ const QueriesTable = ({
);
}
return emptyQueries;
return emptyParams;
}, [
isAnyTeamObserverPlus,
isObserverPlus,
@ -251,23 +200,31 @@ const QueriesTable = ({
searchQuery,
]);
const renderPlatformDropdown = useCallback(() => {
const handlePlatformFilterDropdownChange = (platformSelected: string) => {
router?.replace(
const handlePlatformFilterDropdownChange = useCallback(
(selectedTargetedPlatform: string) => {
router?.push(
getNextLocationPath({
pathPrefix: PATHS.MANAGE_QUERIES,
queryParams: {
...queryParams,
page: 0,
platform: platformSelected,
platform:
// separate URL & API 0-values of `platform` (undefined) from dropdown
// 0-value of "all"
selectedTargetedPlatform === "all"
? undefined
: selectedTargetedPlatform,
},
})
);
};
},
[queryParams, router]
);
const renderPlatformDropdown = useCallback(() => {
return (
<Dropdown
value={platform}
value={curTargetedPlatformFilter}
className={`${baseClass}__platform-dropdown`}
options={PLATFORM_FILTER_OPTIONS}
searchable={false}
@ -275,16 +232,7 @@ const QueriesTable = ({
iconName="filter"
/>
);
}, [platform, queryParams, router]);
const renderQueriesCount = useCallback(() => {
// Fixes flashing incorrect count before clientside filtering
if (isQueriesStateLoading) {
return null;
}
return <TableCount name="queries" count={queriesState?.length} />;
}, [queriesState, isQueriesStateLoading]);
}, [curTargetedPlatformFilter, queryParams, router]);
const columnConfigs = useMemo(
() =>
@ -297,7 +245,10 @@ const QueriesTable = ({
[currentUser, currentTeamId, onlyInheritedQueries]
);
const searchable = !(queriesList?.length === 0 && searchQuery === "");
const searchable =
(totalQueriesCount ?? 0) > 0 ||
!!curTargetedPlatformFilter ||
!!searchQuery;
const emptyComponent = useCallback(() => {
const {
@ -318,52 +269,41 @@ const QueriesTable = ({
const trimmedSearchQuery = searchQuery.trim();
const deleteQueryTableActionButtonProps = useMemo(
() =>
({
name: "delete query",
buttonText: "Delete",
iconSvg: "trash",
variant: "text-icon",
onActionButtonClick: onDeleteQueryClick,
// this maintains the existing typing, which is not actually correct
// TODO - update this object to actually implement IActionButtonProps
} as IActionButtonProps),
[onDeleteQueryClick]
);
return columnConfigs && !isLoading ? (
<div className={`${baseClass}`}>
<TableContainer
resultsTitle="queries"
columnConfigs={columnConfigs}
data={queriesState}
filters={{ name: trimmedSearchQuery }}
isLoading={isLoading || isQueriesStateLoading}
defaultSortHeader={sortHeader || DEFAULT_SORT_HEADER}
defaultSortDirection={sortDirection || DEFAULT_SORT_DIRECTION}
defaultSearchQuery={trimmedSearchQuery}
defaultPageIndex={page}
pageSize={DEFAULT_PAGE_SIZE}
inputPlaceHolder="Search by name"
onQueryChange={onQueryChange}
emptyComponent={emptyComponent}
showMarkAllPages={false}
isAllPagesSelected={false}
searchable={searchable}
searchQueryColumn="name"
customControl={searchable ? renderPlatformDropdown : undefined}
isClientSidePagination
onClientSidePaginationChange={onClientSidePaginationChange}
isClientSideFilter
primarySelectAction={deleteQueryTableActionButtonProps}
// TODO - consolidate this functionality within `filters`
selectedDropdownFilter={platform}
renderCount={renderQueriesCount}
/>
</div>
) : (
<></>
return (
columnConfigs && (
<div className={`${baseClass}`}>
<TableContainer
resultsTitle="queries"
columnConfigs={columnConfigs}
data={queries}
// won't ever actually be loading, see render condition above
isLoading={isLoading}
defaultSortHeader={sortHeader || DEFAULT_SORT_HEADER}
defaultSortDirection={sortDirection || DEFAULT_SORT_DIRECTION}
defaultSearchQuery={trimmedSearchQuery}
defaultPageIndex={page}
disableNextPage={!hasNextResults}
showMarkAllPages={false}
isAllPagesSelected={false}
primarySelectAction={{
name: "delete query",
buttonText: "Delete",
iconSvg: "trash",
variant: "text-icon",
onActionButtonClick: onDeleteQueryClick,
}}
emptyComponent={emptyComponent}
renderCount={() => (
<TableCount name="queries" count={totalQueriesCount} />
)}
inputPlaceHolder="Search by name"
onQueryChange={onQueryChange}
searchable={searchable}
customControl={searchable ? renderPlatformDropdown : undefined}
selectedDropdownFilter={curTargetedPlatformFilter}
/>
</div>
)
);
};

View file

@ -12,7 +12,11 @@ import {
IEnhancedQuery,
ISchedulableQuery,
} from "interfaces/schedulable_query";
import { QueryablePlatform } from "interfaces/platform";
import {
isQueryablePlatform,
QueryablePlatform,
SelectedPlatformString,
} from "interfaces/platform";
import { API_ALL_TEAMS_ID } from "interfaces/team";
import Icon from "components/Icon";
@ -81,7 +85,7 @@ interface IBoolCellProps extends IRowProps {
}
interface IPlatformCellProps extends IRowProps {
cell: {
value: QueryablePlatform[];
value: SelectedPlatformString;
};
}
@ -181,11 +185,17 @@ const generateTableHeaders = ({
},
{
title: "Platform",
Header: "Compatible with",
Header: "Targeted platforms",
disableSortBy: true,
accessor: "platforms",
accessor: "platform",
Cell: (cellProps: IPlatformCellProps): JSX.Element => {
return <PlatformCell platforms={cellProps.row.original.platforms} />;
const platforms = cellProps.cell.value
.split(",")
.map((s) => s.trim())
// this casting is necessary because make generate for some reason doesn't recognize the
// type guarding of `isQueryablePlatform` even though the language server in VSCode does
.filter((s) => isQueryablePlatform(s)) as QueryablePlatform[];
return <PlatformCell platforms={platforms} />;
},
},
{

View file

@ -1,5 +1,4 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { snakeCase, reduce } from "lodash";
import sendRequest from "services";
import endpoints from "utilities/endpoints";
@ -8,7 +7,10 @@ import {
ILoadAllPoliciesResponse,
IPoliciesCountResponse,
} from "interfaces/policy";
import { buildQueryStringFromParams, QueryParams } from "utilities/url";
import {
buildQueryStringFromParams,
convertParamsToSnakeCase,
} from "utilities/url";
interface IPoliciesApiParams {
page?: number;
@ -30,17 +32,6 @@ export interface IPoliciesCountQueryKey
const ORDER_KEY = "name";
const ORDER_DIRECTION = "asc";
const convertParamsToSnakeCase = (params: IPoliciesApiParams) => {
return reduce<typeof params, QueryParams>(
params,
(result, val, key) => {
result[snakeCase(key)] = val;
return result;
},
{}
);
};
export default {
// TODO: How does the frontend need to support legacy policies?
create: (data: IPolicyFormData) => {
@ -71,7 +62,7 @@ export default {
return sendRequest("GET", GLOBAL_POLICIES);
},
loadAllNew: async ({
loadAllNew: ({
page,
perPage,
orderKey = ORDER_KEY,
@ -94,7 +85,7 @@ export default {
return sendRequest("GET", path);
},
getCount: async ({
getCount: ({
query,
}: Pick<IPoliciesApiParams, "query">): Promise<IPoliciesCountResponse> => {
const { GLOBAL_POLICIES } = endpoints;

View file

@ -6,8 +6,37 @@ import { ISelectedTargetsForApi } from "interfaces/target";
import {
ICreateQueryRequestBody,
IModifyQueryRequestBody,
IQueryKeyQueriesLoadAll,
ISchedulableQuery,
} from "interfaces/schedulable_query";
import { buildQueryStringFromParams } from "utilities/url";
import {
buildQueryStringFromParams,
convertParamsToSnakeCase,
} from "utilities/url";
import { SelectedPlatform } from "interfaces/platform";
export interface ILoadQueriesParams {
teamId?: number;
page?: number;
perPage?: number;
query?: string;
orderDirection?: "asc" | "desc";
orderKey?: string;
mergeInherited?: boolean;
targetedPlatform?: SelectedPlatform;
}
export interface IQueryKeyLoadQueries extends ILoadQueriesParams {
scope: "queries";
}
export interface IQueriesResponse {
queries: ISchedulableQuery[];
count: number;
meta: {
has_next_results: boolean;
has_previous_results: boolean;
};
}
export default {
create: (createQueryRequestBody: ICreateQueryRequestBody) => {
@ -35,17 +64,41 @@ export default {
return sendRequest("GET", path);
},
loadAll: (teamId?: number, mergeInherited = false) => {
loadAll: ({
teamId,
page,
perPage,
query,
orderDirection,
orderKey,
mergeInherited,
// FE logic uses less ambiguous `targetedPlatform`, while API expects `platform` for alignment
// with other API conventions and database `queries.platform` column
targetedPlatform: platform,
}: IQueryKeyQueriesLoadAll): Promise<IQueriesResponse> => {
const { QUERIES } = endpoints;
const queryString = buildQueryStringFromParams({
team_id: teamId,
merge_inherited: mergeInherited || null,
const snakeCaseParams = convertParamsToSnakeCase({
teamId,
page,
perPage,
query,
orderDirection,
orderKey,
mergeInherited,
platform,
});
const path = `${QUERIES}`;
// API expects "macos" instead of "darwin"
if (snakeCaseParams.platform === "darwin") {
snakeCaseParams.platform = "macos";
}
const queryString = buildQueryStringFromParams(snakeCaseParams);
return sendRequest(
"GET",
queryString ? path.concat(`?${queryString}`) : path
queryString ? QUERIES.concat(`?${queryString}`) : QUERIES
);
},
run: async ({

View file

@ -4,7 +4,7 @@ import { intersection, isPlainObject } from "lodash";
import { osqueryTablesAvailable } from "utilities/osquery_tables";
import {
MACADMINS_EXTENSION_TABLES,
SUPPORTED_PLATFORMS,
QUERYABLE_PLATFORMS,
QueryablePlatform,
} from "interfaces/platform";
import { TableSchemaPlatform } from "interfaces/osquery_table";
@ -59,7 +59,7 @@ const filterCompatiblePlatforms = (
sqlTables: string[]
): QueryablePlatform[] => {
if (!sqlTables.length) {
return [...SUPPORTED_PLATFORMS]; // if a query has no tables but is still syntatically valid sql, it is treated as compatible with all platforms
return [...QUERYABLE_PLATFORMS]; // if a query has no tables but is still syntatically valid sql, it is treated as compatible with all platforms
}
const compatiblePlatforms = intersection(
@ -68,7 +68,7 @@ const filterCompatiblePlatforms = (
)
);
return SUPPORTED_PLATFORMS.filter((p) => compatiblePlatforms.includes(p));
return QUERYABLE_PLATFORMS.filter((p) => compatiblePlatforms.includes(p));
};
export const parseSqlTables = (

10
go.mod
View file

@ -123,16 +123,16 @@ require (
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.31.0
go.opentelemetry.io/otel/sdk v1.31.0
golang.org/x/crypto v0.28.0
golang.org/x/crypto v0.31.0
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842
golang.org/x/image v0.18.0
golang.org/x/mod v0.17.0
golang.org/x/net v0.30.0
golang.org/x/oauth2 v0.22.0
golang.org/x/sync v0.8.0
golang.org/x/sys v0.26.0
golang.org/x/term v0.25.0
golang.org/x/text v0.19.0
golang.org/x/sync v0.10.0
golang.org/x/sys v0.28.0
golang.org/x/term v0.27.0
golang.org/x/text v0.21.0
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d
google.golang.org/api v0.178.0
google.golang.org/grpc v1.67.1

20
go.sum
View file

@ -954,8 +954,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@ -1037,8 +1037,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -1098,8 +1098,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@ -1108,8 +1108,8 @@ golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
@ -1121,8 +1121,8 @@ golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=

View file

@ -796,40 +796,31 @@ You can learn more about how Fleet approaches security in the [security handbook
In responding to security questionnaires, Fleet endeavors to provide full transparency via our [security policies](https://fleetdm.com/handbook/digital-experience/security#security-policies), [trust](https://trust.fleetdm.com/), and [application security](https://fleetdm.com/handbook/digital-experience/security#application-security) documentation. In addition to this documentation, please refer to [the vendor questionnaires page](https://fleetdm.com/handbook/digital-experience//security#vendor-questionnaires). [Contact the Sales department](https://fleetdm.com/handbook/sales#contact-us) to address any pending questionnaires.
## Getting a contract reviewed
> If a document is ready for signature and does not need to be reviewed or negotiated, you can skip the review process and use the signature process documented above.
The [Digital Experience team](https://fleetdm.com/handbook/digital-experience#team) will review all contracts within **2 business days**.
To get a contract reviewed, upload the agreement to [Google Drive](https://drive.google.com/drive/folders/1G1JTpFxhKZZzmn2L2RppohCX5Bv_CQ9c).
> If a document is ready for signature and does not need to be reviewed or negotiated, you can skip the review process and [get the contract signed](https://fleetdm.com/handbook/company/communications#getting-a-contract-signed). Please submit other legal questions and requests to [Digital Experience](https://fleetdm.com/handbook/digital-experience#contact-us).
Complete the [contract review issue template in GitHub](https://github.com/fleetdm/confidential/issues/new?assignees=hollidayn&labels=%23g-digital-experience&projects=&template=contract-review.md&title=Review%3A++%F0%9F%96%8B%EF%B8%8F+__________________________), being sure to include the link to the document you uploaded and using the Calendly link in the issue template to schedule time to discuss the agreement with Nathan Holliday (allowing for sufficient time for him to have reviewed the contract prior to the call).
To get a contract reviewed, complete the [contract review issue template in GitHub](https://github.com/fleetdm/confidential/issues/new?assignees=hollidayn&labels=%23g-digital-experience&projects=&template=contract-review.md&title=Review%3A++%F0%9F%96%8B%EF%B8%8F+__________________________). Upload the docx version whenever possible and be sure to include the link to the document in the issue. Follow-up comments should be made in the GitHub issue and in the document itself to avoid losing context.
Follow up comments should be made in the GitHub issue and in the document itself so it is all in the same place.
If an agreement requires additional review during the negotiation process, the requestor will need to upload the new draft agreement and repeat the process. When no further review or action is required, the requestor is responsible for [routing the document](https://fleetdm.com/handbook/company/communications#getting-a-contract-signed) for signature.
The SLA for contract review is **2 business days**.
Once the review is complete, the issue will be closed.
If an agreement requires an additional review during the negotiation process, the requestor will need to follow these steps again. Uploading the new draft and creating a new issue in GitHub.
When no further review or action is required for an agreement and the document is ready to be signed, the requestor is then responsible for routing the document for signature.
> **Note:** Please submit other legal questions and requests to [Digital Experience](https://fleetdm.com/handbook/digital-experience#contact-us).
## Getting a contract signed
If a contract is ready for signature and requires no review or revision, log into DocuSign (credentials in 1Password) and route the agreement to the CEO for signature.
The SLA for contract signature is **2 business days**. Please do not follow up on signatures unless this time has elapsed. If a contract is ready for signature and **DOES NOT** require [review or revision](https://fleetdm.com/handbook/company/communications#getting-a-contract-reviewed) (i.e. no contract review issue necessary), follow the steps below:
When a contract is going to be routed for signature by someone outside of Fleet (i.e. the vendor or customer), the requestor is responsible for working with the other party to make sure the document gets routed to the CEO for signature.
First, log into DocuSign (credentials in 1Password) and route the agreement to the CEO for signature. When a contract is going to be routed for signature by someone outside of Fleet (i.e. the vendor or customer), the requestor is responsible for working with the other party to make sure the document gets routed to the CEO for signature.
The SLA for contract signature is **2 business days**. Please do not follow up on signature unless this time has elapsed.
Then, at-mention the [Head of Digital Experience](https://fleetdm.com/handbook/digital-experience#team) in the appropriate internal Slack channel (e.g. op channel, #g-digital-experience) with the following template:
_**Note:** Signature open time for the CEO is not currently measured, to avoid the overhead of creating separate signature issues to measure open and close time. This may change as signature volume increases._
```
@Sam Pfluger - :writing_hand Signature request
The following contract is ready to sign and has been routed to the CEO for signature: CONTRACT_DOC_URL_FROM_GOOGLE_DRIVE
```
> _**Note:** If a contract is ready for signature and requires no review or revision, log into DocuSign (credentials in 1Password) and route the agreement to the CEO for signature._
Please use [Fleet's billing email address](https://fleetdm.com/handbook/company/communications#email-relays) for all contracts, and never use individual emails except for signature. If the page to sign includes any individual emails in the docusign contract, please remove it before routing to the CEO for signature.
Please use [Fleet's billing email address](https://fleetdm.com/handbook/company/communications#email-relays) for all contracts and never include individual emails in any company agreement. If the agreement includes any individual emails, please remove them before routing the agreement to the CEO for signature.
## Trust

View file

@ -204,3 +204,4 @@
quoteAuthorProfileImageFilename: testimonial-author-arsenio-figueroa-48x48@2x.png
quoteAuthorJobTitle: Senior Systems Security Engineer
productCategories: [Observability, Software management]
# From social media company: “absolutely, will do! this amazing support has been one of the great things that helped make this decision easier and, trust me when I say this, you guys must be doing good if you can take some die-hards away from their ______ (competing product)”

View file

@ -450,6 +450,23 @@ Steps to renew the certificate:
Instructions for creating and maintaining a TUF repo are available on our [TUF handbook page](https://fleetdm.com/handbook/engineering/tuf).
### Fix flaky Go tests
Sometimes automated tests fail intermittently, causing PRs to become blocked and engineers to become sad and vengeful. Debugging a "flaky" or "rando" test failure typically involves:
* Adding extra logs to the test and/or related code to get more information about the failure.
* Running the test multiple times to reproduce the failure.
* Implementing an attempted fix to the test (or the related code, if there's an actual bug).
* Running the test multiple times to try and verify that the test no longer fails.
To aid in this process, we have the Stress Test Go Test action (aka the RandoKiller™). This is a Github Actions workflow that can be used to run one or more Go tests repeatedly until they fail (or until they pass a certain number of times). To use the RandoKiller:
* Create a branch whose name ends with `-randokiller` (for example `sgress454/enqueue-mdm-command-randokiller`).
* Modify the [.github/workflows/config/randokiller.json](https://github.com/fleetdm/fleet/blob/main/.github/workflows/config/randokiller.json) file to your specifications (choosing the packages and tests to run, the mysql matrix, and the number of runs to do).
* Push up the branch with whatever logs/changes you need to help diagnose or fix the flaky test.
* Monitor the [Stress Test Go Test](https://github.com/fleetdm/fleet/actions/workflows/randokiller-go.yml) workflow for your branch.
* Repeat until the stress test passes! Every push to your branch will trigger a new run of the workflow.
## Rituals
<rituals :rituals="rituals['handbook/engineering/engineering.rituals.yml']"></rituals>

View file

@ -53,6 +53,16 @@ Use the following steps to change a contact's organization in Salesforce:
- If the contact's organization in Salesforce is incorrect and we know where they're moving to, navigate to the contact in Salesforce, change the "Account name" to the contact's new organization, and save.
### Review Salesforce opportunities
Every week, the sales manager will review the necessary opportunities with internal stakeholders. The AE or CSM who owned the deal, their manager, the Head of Marketing, Head of Digital Experience, and the CEO will review all opportunities on the "[Ω Ops for review](https://fleetdm.lightning.force.com/lightning/r/Report/00OUG000001qyE12AI/view?queryScope=userFolders)" report in Salesforce. Opportunities will be reviewed for a number of reasons including:
- If the opportunity is older than 30 days but the prospect hasn't been sent an order form yet.
- Any closed lost new business or expansion opportunity from the previous week.
- Any opportunity with a closed date pushed into a different quarter.
If no opportunities meet these criteria, the meeting is used to discuss the oldest opportunities and close any that are stalled.
### Send an order form
In order to be transparent, Fleet sends order forms within 30 days of opportunity creation in most cases. All quotes and purchase orders must be approved by the CRO and 🌐 [Head of Digital Experience](https://fleetdm.com/handbook/digital-experience#team) before being sent to the prospect or customer. Often, the CRO will request legal review of any unique terms required. To prepare and send a subscription order form the Fleet owner of the opportunity (usually AE or CSM) will:
@ -62,7 +72,7 @@ In order to be transparent, Fleet sends order forms within 30 days of opportunit
3. Where possible, include a graphic of the customer's logo. Use good judgment and omit if a high-quality graphic is unavailable. If in doubt, ask Digital Experience for help.
> **Important**
> - All changes to the [subscription agreement template](https://docs.google.com/document/d/1X4fh2LsuFtAVyQDnU1ZGBggqg-Ec00vYHACyckEooqA/edit?tab=t.0), or [standard terms](http://fleetdm.com/terms) must be brought to ["🦢🗣 Design review (#g-digital-experience)"](https://app.zenhub.com/workspaces/-g-digital-experience-6451748b4eb15200131d4bab/board?sprints=none) for approval.
> - > Every week, any proposal not sent within 30 days of its creation in Salesforce should be reviewed and closed lost. The review of these opportunities and exceptions for them of one (1) week or less are the responsibility of the sales manager. For exceptions of more than one week, escalate to the CEO.
> - All non-standard (from another party) subscription agreements, NDAs, and similar contracts require legal review from Digital Experience before being signed. [Create an issue to request legal review](https://github.com/fleetdm/confidential/blob/main/.github/ISSUE_TEMPLATE/contract-review.md).
4. In the internal Slack channel for the deal, at-mention the CRO and the Head of Digital Experience with a link to the docx version of the order and ask them to approve the order form.

View file

@ -1,13 +1,4 @@
# https://github.com/fleetdm/fleet/pull/13084
-
task: "Close leads contacted ≥7 days ago"
startedOn: "2024-07-05"
frequency: "Daily"
description: "Close all of your leads in the 'Attempted to contact' stage and which have been there for 7 or more days. If follow-up is appropriate, and won't be bothersome, it can be done after closing the lead. (A new lead can always be opened for the contact later.)"
moreInfoUrl: ""
dri: "Every AE"
-
task: "Prioritize for next sprint" # Title that will actually show in rituals table
startedOn: "2023-09-04" # Needs to align with frequency e.g. if frequency is every thrid Thursday startedOn === any third thursday
@ -19,26 +10,12 @@
labels: [ "#g-sales" ] # label to be applied to issue
repo: "confidential"
-
task: "g-sales standup" # Title that will actually show in rituals table
startedOn: "2023-09-04" # Needs to align with frequency e.g. if frequency is every thrid Thursday startedOn === any third thursday
frequency: "Daily" # must be supported by
description: "Review progress on priorities for Sprint. Discuss previous day accomplishments, goals for today and any blockers." # example of a longer thing: description: "[Prioritizing next sprint](https://fleetdm.com/handbook/company/communication)"
moreInfoUrl: "https://fleetdm.com/handbook/company/why-this-way#why-make-work-visible" #URL used to highlight "description:" test in table
dri: "alexmitchelliii" # DRI for ritual (assignee if autoIssue) (TODO display GitHub proflie pic instead of name or title)
-
task: "Opportunity pipeline review" # Title that will actually show in rituals table
startedOn: "2023-09-04" # Needs to align with frequency e.g. if frequency is every thrid Thursday startedOn === any third thursday
frequency: "Weekly" # must be supported by
description: "Review status of sales opportunities and discuss next steps." # example of a longer thing: description: "[Prioritizing next sprint](https://fleetdm.com/handbook/company/communication)"
moreInfoUrl: "https://fleetdm.com/handbook/customers#review-rep-activity" #URL used to highlight "description:" test in table
dri: "alexmitchelliii" # DRI for ritual (assignee if autoIssue) (TODO display GitHub proflie pic instead of name or title)
task: "Review Salesforce opportunities"
startedOn: "2024-12-02"
frequency: "Weekly"
description: "https://fleetdm.com/handbook/sales#review-salesforce-opportunities"
moreInfoUrl: "https://fleetdm.com/handbook/sales#review-salesforce-opportunities"
dri: "alexmitchelliii"
autoIssue: # Enables automation of GitHub issues
labels: [ "#g-sales" ] # label to be applied to issue
repo: "confidential"
-
task: "Review rep activity"
startedOn: "2023-09-18"
frequency: "Monthly"
description: "https://fleetdm.com/handbook/customers#review-rep-activity"
moreInfoUrl: "https://fleetdm.com/handbook/customers#review-rep-activity"
dri: "alexmitchelliii"

View file

@ -1,4 +1,4 @@
url: https://zoom.us/client/6.2.11.5069/zoom_amd64.deb
self-service: true
pre_install_query:
path: ./queries/all-debian-hosts.yml
path: ../queries/all-debian-hosts.yml

View file

@ -0,0 +1,16 @@
{
"Type": "com.apple.configuration.softwareupdate.settings",
"Identifier": "com.fleetdm.config.softwareupdate.settings",
"Payload": {
"AllowStandardUserOSUpdates": true,
"AutomaticActions": {
"Download": "AlwaysOn",
"InstallOSUpdates": "Allowed",
"InstallSecurityUpdate": "AlwaysOn"
},
"Notifications": true,
"RapidSecurityResponse": {
"Enabled": true
}
}
}

View file

@ -31,3 +31,5 @@ controls:
policies:
queries:
software:
packages:
- path: ../lib/linux/software/zoom.yml # Zoom for Ubuntu

View file

@ -87,13 +87,14 @@ controls:
- path: ../lib/macos/configuration-profiles/disable-update-notifications.mobileconfig
- path: ../lib/macos/configuration-profiles/ensure-show-status-bar-is-enabled.mobileconfig
- path: ../lib/macos/declaration-profiles/passcode-settings.json
- path: ../lib/macos/declaration-profiles/software-update-settings.json
macos_setup:
bootstrap_package: ""
enable_end_user_authentication: false
macos_setup_assistant: null
macos_updates:
deadline: "2024-12-04"
minimum_version: "15.1.1"
deadline: "2025-01-03"
minimum_version: "15.2"
windows_settings:
custom_settings:
- path: ../lib/windows/configuration-profiles/windows-firewall.xml
@ -150,8 +151,7 @@ queries:
software:
packages:
- path: ../lib/macos/software/mozilla-firefox.yml # Mozilla Firefox for MacOS (universal)
- path: ../lib/linux/software/zoom.yml # Zoom for Ubuntu
app_store_apps:
- app_store_id: '803453959' # Slack Desktop
- app_store_id: '1333542190' # 1Password 7 Desktop
- app_store_id: '1477376905' # GitHub
- app_store_id: '1152747299' # Figma

View file

@ -66,8 +66,8 @@ controls:
- app_store_id: '803453959' # Slack Desktop
- app_store_id: '1333542190' # 1Password 7 Desktop
macos_updates:
deadline: "2024-12-04"
minimum_version: "15.1.1"
deadline: "2025-01-03"
minimum_version: "15.2"
windows_settings:
custom_settings: null
windows_updates:

View file

@ -1 +0,0 @@
* added support for linux key escrow on ubuntu 20.04

View file

@ -1,3 +1,17 @@
## Orbit 1.37.0 (Dec 13, 2024)
* Added support for key escrow on Ubuntu 20.04.
* Added support for kdialog Linux key escrow prompts for compatibility with Kubuntu systems. Currently supported browser on Kubuntu for Fleet desktop is Chrome.
* Fixed issue where the Linux encryption progress window in zenity was not displaying properly.
* Added support to migrate the MDM provider of Windows devices to Fleet.
* Added `nftables` table to show configuration for Linux `nftables` network filters.
* Updated Go version to 1.23.4.
## Orbit 1.36.0 (Nov 25, 2024)
* Upgraded macadmins osquery-extension to v1.2.3.

View file

@ -18,8 +18,8 @@ Following are the currently deployed versions of fleetd components on the `stabl
| Component\OS | macOS | Linux | Windows | Linux (arm64) |
|--------------|--------|--------|---------|---------------|
| orbit | 1.36.0 | 1.36.0 | 1.36.0 | 1.36.0 |
| desktop | 1.36.0 | 1.36.0 | 1.36.0 | 1.36.0 |
| orbit | 1.37.0 | 1.37.0 | 1.37.0 | 1.37.0 |
| desktop | 1.37.0 | 1.37.0 | 1.37.0 | 1.37.0 |
| osqueryd | 5.14.1 | 5.14.1 | 5.14.1 | 5.14.1 |
| nudge | - | - | - | - |
| swiftDialog | - | - | - | - |

View file

@ -1 +0,0 @@
* Added support to migrate the MDM provider of Windows devices to Fleet.

View file

@ -1 +0,0 @@
* added support for kdialog linux key escrow prompts for compatibility with kubuntu systems

View file

@ -1 +0,0 @@
* fixed issue where the linux encryption progress window in zenity was not displaying properly

View file

@ -1 +0,0 @@
- Add `nftables` table to show configuration for Linux `nftables` network filters.

View file

@ -1 +0,0 @@
* Updated Go version to 1.23.4

View file

@ -101,6 +101,8 @@ type GitOps struct {
Queries []*fleet.QuerySpec
// Software is only allowed on teams, not on global config.
Software GitOpsSoftware
// FleetSecrets is a map of secret names to their values, extracted from FLEET_SECRET_ environment variables used in profiles and scripts.
FleetSecrets map[string]string
}
type GitOpsSoftware struct {
@ -130,6 +132,7 @@ func GitOpsFromFile(filePath, baseDir string, appConfig *fleet.EnrichedAppConfig
var multiError *multierror.Error
result := &GitOps{}
result.FleetSecrets = make(map[string]string)
topKeys := []string{"name", "team_settings", "org_settings", "agent_options", "controls", "policies", "queries", "software"}
for k := range top {
@ -431,6 +434,7 @@ func parseControls(top map[string]json.RawMessage, result *GitOps, baseDir strin
return multierror.Append(multiError, fmt.Errorf("failed to unmarshal controls: %v", err))
}
controlsTop.Defined = true
controlsDir := baseDir
if controlsTop.Path == nil {
controlsTop.Scripts, err = resolveScriptPaths(controlsTop.Scripts, baseDir)
if err != nil {
@ -467,7 +471,69 @@ func parseControls(top map[string]json.RawMessage, result *GitOps, baseDir strin
}
result.Controls = pathControls
}
controlsDir = filepath.Dir(controlsFilePath)
}
// Find Fleet secrets in scripts.
for _, script := range result.Controls.Scripts {
if script.Path == nil {
// This should never happen because we checked for missing paths above (with code added in https://github.com/fleetdm/fleet/pull/24639).
return multierror.Append(multiError, errors.New("controls.scripts.path is missing"))
}
fileBytes, err := os.ReadFile(*script.Path)
if err != nil {
return multierror.Append(multiError, fmt.Errorf("failed to read scripts file %s: %v", *script.Path, err))
}
err = LookupEnvSecrets(string(fileBytes), result.FleetSecrets)
if err != nil {
return multierror.Append(multiError, err)
}
}
// Find Fleet secrets in profiles
var profiles []fleet.MDMProfileSpec
if result.Controls.MacOSSettings != nil {
// We are marshalling/unmarshalling to get the data into the fleet.MacOSSettings struct.
// This is inefficient, but it is more robust and less error-prone.
var macOSSettings fleet.MacOSSettings
data, err := json.Marshal(result.Controls.MacOSSettings)
if err != nil {
return multierror.Append(multiError, fmt.Errorf("failed to process controls.macos_settings: %v", err))
}
err = json.Unmarshal(data, &macOSSettings)
if err != nil {
return multierror.Append(multiError, fmt.Errorf("failed to process controls.macos_settings: %v", err))
}
profiles = append(profiles, macOSSettings.CustomSettings...)
}
if result.Controls.WindowsSettings != nil {
// We are marshalling/unmarshalling to get the data into the fleet.WindowsSettings struct.
// This is inefficient, but it is more robust and less error-prone.
var windowsSettings fleet.WindowsSettings
data, err := json.Marshal(result.Controls.WindowsSettings)
if err != nil {
return multierror.Append(multiError, fmt.Errorf("failed to process controls.windows_settings: %v", err))
}
err = json.Unmarshal(data, &windowsSettings)
if err != nil {
return multierror.Append(multiError, fmt.Errorf("failed to process controls.windows_settings: %v", err))
}
if windowsSettings.CustomSettings.Valid {
profiles = append(profiles, windowsSettings.CustomSettings.Value...)
}
}
for _, profile := range profiles {
resolvedPath := resolveApplyRelativePath(controlsDir, profile.Path)
fileBytes, err := os.ReadFile(resolvedPath)
if err != nil {
return multierror.Append(multiError, fmt.Errorf("failed to read profile file %s: %v", resolvedPath, err))
}
err = LookupEnvSecrets(string(fileBytes), result.FleetSecrets)
if err != nil {
return multierror.Append(multiError, err)
}
}
return multiError
}

View file

@ -79,6 +79,11 @@ func TestValidGitOpsYaml(t *testing.T) {
isTeam bool
}{
"global_config_no_paths": {
environment: map[string]string{
"FLEET_SECRET_FLEET_SECRET_": "fleet_secret",
"FLEET_SECRET_NAME": "secret_name",
"FLEET_SECRET_length": "10",
},
filePath: "testdata/global_config_no_paths.yml",
},
"global_config_with_paths": {
@ -86,10 +91,18 @@ func TestValidGitOpsYaml(t *testing.T) {
"LINUX_OS": "linux",
"DISTRIBUTED_DENYLIST_DURATION": "0",
"ORG_NAME": "Fleet Device Management",
"FLEET_SECRET_FLEET_SECRET_": "fleet_secret",
"FLEET_SECRET_NAME": "secret_name",
"FLEET_SECRET_length": "10",
},
filePath: "testdata/global_config.yml",
},
"team_config_no_paths": {
environment: map[string]string{
"FLEET_SECRET_FLEET_SECRET_": "fleet_secret",
"FLEET_SECRET_NAME": "secret_name",
"FLEET_SECRET_length": "10",
},
filePath: "testdata/team_config_no_paths.yml",
isTeam: true,
},
@ -99,6 +112,9 @@ func TestValidGitOpsYaml(t *testing.T) {
"LINUX_OS": "linux",
"DISTRIBUTED_DENYLIST_DURATION": "0",
"ENABLE_FAILING_POLICIES_WEBHOOK": "true",
"FLEET_SECRET_FLEET_SECRET_": "fleet_secret",
"FLEET_SECRET_NAME": "secret_name",
"FLEET_SECRET_length": "10",
},
filePath: "testdata/team_config.yml",
isTeam: true,
@ -220,6 +236,10 @@ func TestValidGitOpsYaml(t *testing.T) {
assert.True(t, ok, "windows_migration_enabled not found")
_, ok = gitops.Controls.WindowsUpdates.(map[string]interface{})
assert.True(t, ok, "windows_updates not found")
require.Len(t, gitops.FleetSecrets, 3)
assert.Equal(t, "fleet_secret", gitops.FleetSecrets["FLEET_SECRET_FLEET_SECRET_"])
assert.Equal(t, "secret_name", gitops.FleetSecrets["FLEET_SECRET_NAME"])
assert.Equal(t, "10", gitops.FleetSecrets["FLEET_SECRET_length"])
// Check agent options
assert.NotNil(t, gitops.AgentOptions)
@ -1051,3 +1071,19 @@ func getBaseConfig(options map[string]string, optsToExclude []string) string {
}
return config
}
func TestIllegalFleetSecret(t *testing.T) {
t.Parallel()
config := getGlobalConfig([]string{"policies"})
config += `
policies:
- name: $FLEET_SECRET_POLICY
platform: linux
query: SELECT 1 FROM osquery_info WHERE start_time < 0;
- name: My policy
platform: windows
query: SELECT 1;
`
_, err := gitOpsFromString(t, config)
assert.ErrorContains(t, err, "variables with \"FLEET_SECRET_\" prefix are only allowed")
}

View file

@ -157,6 +157,17 @@ func generateRandomString(sizeBytes int) string {
}
func ExpandEnv(s string) (string, error) {
out, err := expandEnv(s, true)
return out, err
}
// expandEnv expands environment variables for a gitops file.
// $ can be escaped with a backslash, e.g. \$VAR
// \$ can be escaped with another backslash, etc., e.g. \\\$VAR
// $FLEET_VAR_XXX will not be expanded. These variables are expanded on the server.
// If secretsMap is not nil, $FLEET_SECRET_XXX will be evaluated and put in the map
// If secretsMap is nil, $FLEET_SECRET_XXX will cause an error.
func expandEnv(s string, failOnSecret bool) (string, error) {
// Generate a random escaping prefix that doesn't exist in s.
var preventEscapingPrefix string
for {
@ -167,18 +178,27 @@ func ExpandEnv(s string) (string, error) {
}
s = escapeString(s, preventEscapingPrefix)
s = escapeFleetVar(s, preventEscapingPrefix)
var err *multierror.Error
s = os.Expand(s, func(env string) string {
if strings.HasPrefix(env, preventEscapingPrefix) {
return "$" + strings.TrimPrefix(env, preventEscapingPrefix)
s = fleet.MaybeExpand(s, func(env string) (string, bool) {
switch {
case strings.HasPrefix(env, preventEscapingPrefix):
return "$" + strings.TrimPrefix(env, preventEscapingPrefix), true
case strings.HasPrefix(env, fleet.ServerVarPrefix):
// Don't expand fleet vars -- they will be expanded on the server
return "", false
case strings.HasPrefix(env, fleet.FLEET_SECRET_PREFIX):
if failOnSecret {
err = multierror.Append(err, fmt.Errorf("environment variables with %q prefix are only allowed in profiles and scripts: %q",
fleet.FLEET_SECRET_PREFIX, env))
}
return "", false
}
v, ok := os.LookupEnv(env)
if !ok {
err = multierror.Append(err, fmt.Errorf("environment variable %q not set", env))
return ""
return "", false
}
return v
return v, true
})
if err != nil {
return "", err
@ -194,6 +214,40 @@ func ExpandEnvBytes(b []byte) ([]byte, error) {
return []byte(s), nil
}
func ExpandEnvBytesIgnoreSecrets(b []byte) ([]byte, error) {
s, err := expandEnv(string(b), false)
if err != nil {
return nil, err
}
return []byte(s), nil
}
// LookupEnvSecrets only looks up FLEET_SECRET_XXX environment variables. Escaping is not supported.
// This is used for finding secrets in scripts only. The original string is not modified.
// A map of secret names to values is updated.
func LookupEnvSecrets(s string, secretsMap map[string]string) error {
if secretsMap == nil {
return errors.New("secretsMap cannot be nil")
}
var err *multierror.Error
_ = fleet.MaybeExpand(s, func(env string) (string, bool) {
if strings.HasPrefix(env, fleet.FLEET_SECRET_PREFIX) {
// lookup the secret and save it, but don't replace
v, ok := os.LookupEnv(env)
if !ok {
err = multierror.Append(err, fmt.Errorf("environment variable %q not set", env))
return "", false
}
secretsMap[env] = v
}
return "", false
})
if err != nil {
return err
}
return nil
}
var escapePattern = regexp.MustCompile(`(\\+\$)`)
func escapeString(s string, preventEscapingPrefix string) string {
@ -204,11 +258,3 @@ func escapeString(s string, preventEscapingPrefix string) string {
return strings.Repeat("\\", (len(match)/2)-1) + "$" + preventEscapingPrefix
})
}
var escapeFleetVarPattern = regexp.MustCompile(`(\$FLEET_VAR_\w+)|(\${FLEET_VAR_\w+})`)
func escapeFleetVar(s string, preventEscapingPrefix string) string {
return escapeFleetVarPattern.ReplaceAllStringFunc(s, func(match string) string {
return strings.ReplaceAll(match, "$", "$"+preventEscapingPrefix)
})
}

Some files were not shown because too many files have changed in this diff Show more