Merge branch 'main' into feat-mdm-migration-updates

This commit is contained in:
Jahziel Villasana-Espinoza 2024-08-06 09:16:40 -04:00
commit 051ba6f780
249 changed files with 3372 additions and 1848 deletions

View file

@ -111,7 +111,7 @@ jobs:
done
- name: Start Infra Dependencies
run: FLEET_MYSQL_IMAGE=${{ matrix.mysql }} docker-compose up -d mysql redis &
run: FLEET_MYSQL_IMAGE=${{ matrix.mysql }} docker compose up -d mysql redis &
- name: Install JS Dependencies
run: make deps-js

View file

@ -24,7 +24,7 @@ defaults:
shell: bash
env:
FLEET_DESKTOP_VERSION: 1.29.0
FLEET_DESKTOP_VERSION: 1.30.0
permissions:
contents: read

View file

@ -24,7 +24,7 @@ defaults:
shell: bash
env:
OSQUERY_VERSION: 5.12.2
OSQUERY_VERSION: 5.13.0
permissions:
contents: read

View file

@ -46,7 +46,7 @@ jobs:
- name: Start Infra Dependencies
# Use & to background this
run: docker-compose up -d mysql_test &
run: docker compose up -d mysql_test &
- name: Verify test schema changes
run: |

View file

@ -70,7 +70,7 @@ jobs:
# 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 &
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: |
@ -98,13 +98,13 @@ jobs:
- 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
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
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

View file

@ -281,7 +281,7 @@ binary-arch: .pre-binary-arch .pre-binary-bundle .pre-fleet
# Drop, create, and migrate the e2e test database
e2e-reset-db:
docker-compose exec -T mysql_test bash -c 'echo "drop database if exists e2e; create database e2e;" | MYSQL_PWD=toor mysql -uroot'
docker compose exec -T mysql_test bash -c 'echo "drop database if exists e2e; create database e2e;" | MYSQL_PWD=toor mysql -uroot'
./build/fleet prepare db --mysql_address=localhost:3307 --mysql_username=root --mysql_password=toor --mysql_database=e2e
e2e-setup:
@ -312,7 +312,7 @@ e2e-serve-premium: e2e-reset-db
# Usage:
# make e2e-set-desktop-token host_id=1 token=foo
e2e-set-desktop-token:
docker-compose exec -T mysql_test bash -c 'echo "INSERT INTO e2e.host_device_auth (host_id, token) VALUES ($(host_id), \"$(token)\") ON DUPLICATE KEY UPDATE token=VALUES(token)" | MYSQL_PWD=toor mysql -uroot'
docker compose exec -T mysql_test bash -c 'echo "INSERT INTO e2e.host_device_auth (host_id, token) VALUES ($(host_id), \"$(token)\") ON DUPLICATE KEY UPDATE token=VALUES(token)" | MYSQL_PWD=toor mysql -uroot'
changelog:
sh -c "find changes -type f | grep -v .keep | xargs -I {} sh -c 'grep \"\S\" {}; echo' > new-CHANGELOG.md"
@ -347,7 +347,7 @@ fleetd-tuf:
# Reset the development DB
db-reset:
docker-compose exec -T mysql bash -c 'echo "drop database if exists fleet; create database fleet;" | MYSQL_PWD=toor mysql -uroot'
docker compose exec -T mysql bash -c 'echo "drop database if exists fleet; create database fleet;" | MYSQL_PWD=toor mysql -uroot'
./build/fleet prepare db --dev
# Back up the development DB to file
@ -424,6 +424,14 @@ endif
tar czf $(out-path)/swiftDialog.app.tar.gz -C $(TMP_DIR)/swiftDialog_pkg_payload_expanded/Library/Application\ Support/Dialog/ Dialog.app
rm -rf $(TMP_DIR)
# Generate escrowBuddy.pkg bundle from the Escrow Buddy repo.
#
# Usage:
# make escrow-buddy-pkg version=1.0.0 out-path=.
escrow-buddy-pkg:
curl -L https://github.com/macadmins/escrow-buddy/releases/download/v$(version)/Escrow.Buddy-$(version).pkg --output $(out-path)/escrowBuddy.pkg
# Build and generate desktop.app.tar.gz bundle.
#
# Usage:

View file

@ -43,8 +43,6 @@ Fleet has no ambition to replace all of your other tools. (Though it might repl
Fleet plays well with Munki, Chef, Puppet, and Ansible, as well as with security tools like Crowdstrike and SentinelOne. For example, you can use the free version of Fleet to quickly report on what hosts are _actually_ running your EDR agent.
While most folks prefer to use one or the other, Fleet can also coexist peacefully with Rapid7 and other agent-based vulnerability scanners. This can be useful during migrations.
#### Free as in free
The free version of Fleet will [always be free](https://fleetdm.com/pricing). Fleet is [independently backed](https://linkedin.com/company/fleetdm) and actively maintained with the help of many amazing [contributors](https://github.com/fleetdm/fleet/graphs/contributors).

View file

@ -1,6 +1,6 @@
# Deploy Fleet on AWS ECS
> **This article was archived on May 16, 2024,** and may be outdated. Check out [Deploy Fleet](https://fleetdm.com/docs/deploy/deploy-fleet) for our recommended deployment method.
> **This article was archived on May 16, 2024.** Check out [Deploy Fleet](https://fleetdm.com/docs/deploy/deploy-fleet) for the most up to date deployment method.
![Deploy Fleet on AWS ECS](../website/assets/images/articles/deploy-fleet-on-aws-ecs-800x450@2x.png)

View file

@ -1,6 +1,6 @@
# Deploy Fleet on AWS with Terraform
> **This article was archived on May 16, 2024,** and may be outdated. Check out [Deploy Fleet](https://fleetdm.com/docs/deploy/deploy-fleet) for our recommended deployment method.
> **This article was archived on May 16, 2024.** Check out [Deploy Fleet](https://fleetdm.com/docs/deploy/deploy-fleet) for the most up to date deployment method.
![Deploy Fleet on AWS ECS](../website/assets/images/articles/deploy-fleet-on-aws-with-terraform-800x450@2x.png)

View file

@ -1,6 +1,6 @@
# Deploy Fleet on CentOS
> **This article was archived on May 16, 2024,** and may be outdated. Check out [Deploy Fleet](https://fleetdm.com/docs/deploy/deploy-fleet) for our recommended deployment method.
> **This article was archived on May 16, 2024.** Check out [Deploy Fleet](https://fleetdm.com/docs/deploy/deploy-fleet) for the most up to date deployment method.
![Deploy Fleet on CentOS](../website/assets/images/articles/deploy-fleet-on-centos-800x450@2x.png)

View file

@ -1,6 +1,6 @@
# Deploy Fleet on Cloud.gov (Cloud Foundry)
> **This article was archived on May 16, 2024,** and may be outdated. Check out [Deploy Fleet](https://fleetdm.com/docs/deploy/deploy-fleet) for our recommended deployment method.
> **This article was archived on May 16, 2024.** Check out [Deploy Fleet](https://fleetdm.com/docs/deploy/deploy-fleet) for the most up to date deployment method.
![Deploy Fleet on Cloud.gov](../website/assets/images/articles/deploy-fleet-on-cloudgov-800x450@2x.png)

View file

@ -1,6 +1,6 @@
# Deploy Fleet on Hetzner Cloud
> **This article was archived on May 16, 2024,** and may be outdated. Check out [Deploy Fleet](https://fleetdm.com/docs/deploy/deploy-fleet) for our recommended deployment method.
> **This article was archived on May 16, 2024.** Check out [Deploy Fleet](https://fleetdm.com/docs/deploy/deploy-fleet) for the most up to date deployment method.
![Deploy Fleet on Hetzner Cloud](../website/assets/images/articles/deploy-fleet-on-hetzner-cloud-800x450@2x.png)

View file

@ -1,6 +1,6 @@
# Deploy Fleet on Kubernetes
> **This article was archived on May 16, 2024,** and may be outdated. Check out [Deploy Fleet](https://fleetdm.com/docs/deploy/deploy-fleet) for our recommended deployment method.
> **This article was archived on May 16, 2024.** Check out [Deploy Fleet](https://fleetdm.com/docs/deploy/deploy-fleet) for the most up to date deployment method.
![Deploy Fleet on Kubernetes](../website/assets/images/articles/deploy-fleet-on-kubernetes-800x450@2x.png)

View file

@ -1,6 +1,6 @@
# Deploy Fleet on Render
> **This article was archived on May 16, 2024,** and may be outdated. Check out [Deploy Fleet](https://fleetdm.com/docs/deploy/deploy-fleet) for our recommended deployment method.
> **This article was archived on May 16, 2024.** Check out [Deploy Fleet](https://fleetdm.com/docs/deploy/deploy-fleet) for the most up to date deployment method.
![Deploy Fleet on Render](../website/assets/images/articles/deploy-fleet-on-render-800x450@2x.png)

View file

@ -1,5 +1,7 @@
# Fleet in your calendar: introducing maintenance windows
> Unlike other Fleet features which take advantage of declarative device management (DDM), the approach described in this article still uses traditional MDM commands. (More to come.)
![Fleet in your calendar: introducing maintenance windows](../website/assets/images/articles/fleet-in-your-calendar-introducing-maintenance-windows-cover-900x450@2x.png)
Fleet is excited to announce the release of "maintenance windows", a new feature in Fleet v4.48 that helps make sure OS updates occur during times that disrupt your users the least. Now, just like any good colleague, when Fleet needs some of your time, it puts it on your calendar. This approach avoids interrupting your key activities or important meetings, whether in the office, on the road, or working remotely.

View file

@ -121,7 +121,7 @@ On macOS, there are two utilities that enable osquery process auditing: [OpenBSM
To use the `es_process_events` tables, use the flag `--disable_endpointsecurity=false`. See the [EndpointSecurity instructions](https://osquery.readthedocs.io/en/latest/deployment/process-auditing/#auditing-processes-with-endpointsecurity) for more information. To use `process_events` and `socket_events` with OpenBSM, see the [OpenBSM instructions](https://osquery.readthedocs.io/en/latest/deployment/process-auditing/#auditing-processes-with-openbsm).
#### Windows
Currently, osquery does not support process auditing for Windows. To learn more about process auditing on Windows, visit [Microsoft's security auditing overview](https://docs.microsoft.com/en-us/windows/security/threat-protection/auditing/security-auditing-overview). Fleet is tracking work to build process auditing for Windows in osquery. [Stay up to date on GitHub](https://github.com/fleetdm/fleet/issues/7732).
Fleet supports auditing process events on Windows via the `process_etw_events` table. To learn more about process auditing on Windows, visit [Microsoft's security auditing overview](https://docs.microsoft.com/en-us/windows/security/threat-protection/auditing/security-auditing-overview). Fleet is tracking work to add file auditing for Windows in osquery. [Stay up to date on GitHub](https://github.com/fleetdm/fleet/issues/20946).
### YARA scanning
[YARA](https://virustotal.github.io/yara/) is a malware research and detection tool available on Linux and macOS that allows users to create descriptions of malware families based on patterns of text or binary code. Each potential piece of malware is matched against a YARA rule and triggers if the specified conditions are met.

1
changes/13157-fv-escrow Normal file
View file

@ -0,0 +1 @@
* `fleetd` now uses Escrow Buddy to rotate FileVault keys. Internal API endpoints documented in the API for contributors have been modified and/or removed.

View file

@ -1 +0,0 @@
- Updated MDM features to enforce minimum OS version settings during Apple Automated Device Enrollment (ADE).

View file

@ -0,0 +1 @@
- display the label names case-insensitive alphabetical order in the fleet UI

View file

@ -0,0 +1 @@
- Fig bug where software install results could not be retrieved for deleted hosts in the activity feed

View file

@ -0,0 +1 @@
* Fix a styling issue in the Controls > OS Settings > disk encryption table

View file

@ -0,0 +1 @@
* Added a special-case to properly name the Notion .exe Windows installer the same as how it will be reported by osquery post-install.

View file

@ -0,0 +1 @@
* Fix a bug where hosts page would sometimes allow excess pagination

View file

@ -0,0 +1 @@
- Use new gitops format for software pre install query

View file

@ -0,0 +1 @@
Linux .deb packages 'on hold' are now included in the installed software list.

View file

@ -0,0 +1 @@
- Updated text for "Turn on MDM" banners in UI.

View file

@ -0,0 +1 @@
- add a disabled overlay to the Other Workflows modal on the policy page.

View file

@ -0,0 +1 @@
* Fixed a bug in `fleetctl preview` that was causing it to fail if Docker was installed without support for the deprecated `docker-compose` CLI

View file

@ -0,0 +1,2 @@
- Adds a migration to migrate older team configurations to the new version that includes both
installers and App Store apps.

View file

@ -90,7 +90,7 @@ func applyCommand() *cli.Command {
opts.TeamForPolicies = policiesTeamName
}
baseDir := filepath.Dir(flFilename)
_, err = fleetClient.ApplyGroup(c.Context, specs, baseDir, logf, opts)
_, err = fleetClient.ApplyGroup(c.Context, specs, baseDir, logf, nil, opts)
if err != nil {
return err
}

View file

@ -90,7 +90,7 @@ func gitopsCommand() *cli.Command {
secrets := make(map[string]struct{})
for _, flFilename := range flFilenames.Value() {
baseDir := filepath.Dir(flFilename)
config, err := spec.GitOpsFromFile(flFilename, baseDir)
config, err := spec.GitOpsFromFile(flFilename, baseDir, appConfig)
if err != nil {
return err
}

View file

@ -176,6 +176,7 @@ contexts:
fmt.Sprintf(
`
controls:
software:
queries:
policies:
agent_options:
@ -230,5 +231,4 @@ team_settings:
for _, fileName := range teamFileNames {
_ = runAppForTest(t, []string{"gitops", "--config", fleetctlConfig.Name(), "-f", fileName})
}
}

View file

@ -36,7 +36,7 @@ const (
orgName = "GitOps Test"
)
func TestFilenameValidation(t *testing.T) {
func TestFilenameGitOpsValidation(t *testing.T) {
filename := strings.Repeat("a", filenameMaxLength+1)
_, err := runAppNoChecks([]string{"gitops", "-f", filename})
assert.ErrorContains(t, err, "file name must be less than")
@ -207,6 +207,9 @@ func TestBasicGlobalPremiumGitOps(t *testing.T) {
ds.NewJobFunc = func(ctx context.Context, job *fleet.Job) (*fleet.Job, error) {
return &fleet.Job{}, nil
}
ds.BatchSetSoftwareInstallersFunc = func(ctx context.Context, teamID *uint, installers []*fleet.UploadSoftwareInstallerPayload) error {
return nil
}
tmpFile, err := os.CreateTemp(t.TempDir(), "*.yml")
require.NoError(t, err)
@ -238,6 +241,7 @@ org_settings:
org_logo_url_light_background: ""
org_name: ${ORG_NAME}
secrets:
software:
`,
)
require.NoError(t, err)
@ -381,6 +385,7 @@ agent_options:
name: ${TEST_TEAM_NAME}
team_settings:
secrets: ${TEST_SECRET}
software:
`,
)
require.NoError(t, err)
@ -538,6 +543,7 @@ func TestFullGlobalGitOps(t *testing.T) {
t.Setenv("FLEET_SERVER_URL", fleetServerURL)
t.Setenv("ORG_NAME", orgName)
t.Setenv("APPLE_BM_DEFAULT_TEAM", teamName)
t.Setenv("SOFTWARE_INSTALLER_URL", fleetServerURL)
file := "./testdata/gitops/global_config_no_paths.yml"
// Dry run should fail because Apple BM Default Team does not exist and premium license is not set
@ -834,6 +840,7 @@ agent_options:
name: ${TEST_TEAM_NAME}
team_settings:
secrets: [{"secret":"${TEST_SECRET}"}]
software:
`,
)
require.NoError(t, err)
@ -1011,6 +1018,7 @@ org_settings:
org_logo_url_light_background: ""
org_name: ${ORG_NAME}
secrets: [{"secret":"globalSecret"}]
software:
`,
)
require.NoError(t, err)
@ -1030,6 +1038,7 @@ agent_options:
name: ${TEST_TEAM_NAME}
team_settings:
secrets: [{"secret":"${TEST_SECRET}"}]
software:
`,
)
require.NoError(t, err)
@ -1045,6 +1054,7 @@ agent_options:
name: ${TEST_TEAM_NAME}
team_settings:
secrets: [{"secret":"${TEST_SECRET}"},{"secret":"globalSecret"}]
software:
`,
)
require.NoError(t, err)
@ -1215,24 +1225,69 @@ func TestTeamSofwareInstallersGitOps(t *testing.T) {
{"testdata/gitops/team_software_installer_unsupported.yml", "The file should be .pkg, .msi, .exe or .deb."},
{"testdata/gitops/team_software_installer_too_large.yml", "The maximum file size is 500 MB"},
{"testdata/gitops/team_software_installer_valid.yml", ""},
{"testdata/gitops/team_software_installer_valid_apply.yml", ""},
{"testdata/gitops/team_software_installer_pre_condition_multiple_queries.yml", "should have only one query."},
{"testdata/gitops/team_software_installer_pre_condition_multiple_queries_apply.yml", "should have only one query."},
{"testdata/gitops/team_software_installer_pre_condition_not_found.yml", "no such file or directory"},
{"testdata/gitops/team_software_installer_install_not_found.yml", "no such file or directory"},
{"testdata/gitops/team_software_installer_post_install_not_found.yml", "no such file or directory"},
{"testdata/gitops/team_software_installer_no_url.yml", "software URL is required"},
{"testdata/gitops/team_software_installer_invalid_self_service_value.yml", "cannot unmarshal string into Go struct field TeamSpecSoftware.packages of type bool"},
{"testdata/gitops/team_software_installer_invalid_self_service_value.yml", "cannot unmarshal string into Go struct field SoftwareSpec.packages of type bool"},
}
for _, c := range cases {
t.Run(filepath.Base(c.file), func(t *testing.T) {
ds, _, _ := setupFullGitOpsPremiumServer(t)
setupFullGitOpsPremiumServer(t)
ds.SetTeamVPPAppsFunc = func(ctx context.Context, teamID *uint, adamIDs []fleet.VPPAppID) error {
return nil
}
ds.BatchInsertVPPAppsFunc = func(ctx context.Context, apps []*fleet.VPPApp) error {
return nil
_, err := runAppNoChecks([]string{"gitops", "-f", c.file})
if c.wantErr == "" {
require.NoError(t, err)
} else {
require.ErrorContains(t, err, c.wantErr)
}
})
}
}
func TestTeamSoftwareInstallersGitopsQueryEnv(t *testing.T) {
startSoftwareInstallerServer(t)
ds, _, _ := setupFullGitOpsPremiumServer(t)
t.Setenv("QUERY_VAR", "IT_WORKS")
ds.BatchSetSoftwareInstallersFunc = func(ctx context.Context, tmID *uint, installers []*fleet.UploadSoftwareInstallerPayload) error {
if installers[0].PreInstallQuery != "select IT_WORKS" {
return fmt.Errorf("Missing env var, got %s", installers[0].PreInstallQuery)
}
return nil
}
_, err := runAppNoChecks([]string{"gitops", "-f", "testdata/gitops/team_software_installer_valid_env_query.yml"})
require.NoError(t, err)
}
func TestNoTeamSoftwareInstallersGitOps(t *testing.T) {
startSoftwareInstallerServer(t)
cases := []struct {
file string
wantErr string
}{
{"testdata/gitops/no_team_software_installer_not_found.yml", "Please make sure that URLs are publicy accessible to the internet."},
{"testdata/gitops/no_team_software_installer_unsupported.yml", "The file should be .pkg, .msi, .exe or .deb."},
{"testdata/gitops/no_team_software_installer_too_large.yml", "The maximum file size is 500 MB"},
{"testdata/gitops/no_team_software_installer_valid.yml", ""},
{"testdata/gitops/no_team_software_installer_pre_condition_multiple_queries.yml", "should have only one query."},
{"testdata/gitops/no_team_software_installer_pre_condition_not_found.yml", "no such file or directory"},
{"testdata/gitops/no_team_software_installer_install_not_found.yml", "no such file or directory"},
{"testdata/gitops/no_team_software_installer_post_install_not_found.yml", "no such file or directory"},
{"testdata/gitops/no_team_software_installer_no_url.yml", "software URL is required"},
{"testdata/gitops/no_team_software_installer_invalid_self_service_value.yml", "cannot unmarshal string into Go struct field SoftwareSpec.packages of type bool"},
}
for _, c := range cases {
t.Run(filepath.Base(c.file), func(t *testing.T) {
setupFullGitOpsPremiumServer(t)
t.Setenv("APPLE_BM_DEFAULT_TEAM", "")
_, err := runAppNoChecks([]string{"gitops", "-f", c.file})
if c.wantErr == "" {
require.NoError(t, err)

View file

@ -74,7 +74,7 @@ func (d dockerCompose) Command(arg ...string) *exec.Cmd {
func newDockerCompose() (dockerCompose, error) {
// first, check if `docker compose` is available
if err := exec.Command("docker compose").Run(); err == nil {
if err := exec.Command("docker", "compose").Run(); err == nil {
return dockerCompose{dockerComposeV2}, nil
}
@ -387,7 +387,7 @@ Use the stop and reset subcommands to manage the server and dependencies once st
}
// this only applies standard queries, the base directory is not used,
// so pass in the current working directory.
_, err = client.ApplyGroup(c.Context, specs, ".", logf, fleet.ApplyClientSpecOptions{})
_, err = client.ApplyGroup(c.Context, specs, ".", logf, nil, fleet.ApplyClientSpecOptions{})
if err != nil {
return err
}

View file

@ -187,3 +187,4 @@ org_settings:
secrets: # These secrets are used to enroll hosts to the "All teams" team
- secret: SampleSecret123
- secret: ABC
software:

View file

@ -91,3 +91,4 @@ org_settings:
databases_path: ""
secrets:
- secret: ABC
software:

View file

@ -97,3 +97,4 @@ org_settings:
databases_path: ""
secrets:
- secret: ABC
software:

View file

@ -93,3 +93,4 @@ org_settings:
databases_path: ""
secrets:
- secret: ABC
software:

View file

@ -91,3 +91,4 @@ org_settings:
databases_path: ""
secrets:
- secret: ABC
software:

View file

@ -1,11 +1,4 @@
apiVersion: v1
kind: query
spec:
name: query_ruby
- name: query_ruby
query: select 1
---
apiVersion: v1
kind: query
spec:
name: query_ruby2
- name: query_ruby2
query: select 2

View file

@ -0,0 +1,11 @@
apiVersion: v1
kind: query
spec:
name: query_ruby
query: select 1
---
apiVersion: v1
kind: query
spec:
name: query_ruby2
query: select 2

View file

@ -1,5 +1,2 @@
apiVersion: v1
kind: query
spec:
name: query_ruby
- name: query_ruby
query: select 1

View file

@ -0,0 +1,5 @@
apiVersion: v1
kind: query
spec:
name: query_ruby
query: select 1

View file

@ -0,0 +1,2 @@
- name: query_ruby
query: select ${QUERY_VAR}

View file

@ -0,0 +1,19 @@
# Test config
controls:
queries:
policies:
agent_options:
org_settings:
server_settings:
server_url: $FLEET_SERVER_URL
org_info:
contact_url: https://example.com/contact
org_logo_url: ""
org_logo_url_light_background: ""
org_name: ${ORG_NAME}
secrets: [{"secret":"globalSecret"}]
software:
packages:
- url: ${SOFTWARE_INSTALLER_URL}/ruby.deb
install_script:
path: lib/notfound.sh

View file

@ -0,0 +1,18 @@
# Test config
controls:
queries:
policies:
agent_options:
org_settings:
server_settings:
server_url: $FLEET_SERVER_URL
org_info:
contact_url: https://example.com/contact
org_logo_url: ""
org_logo_url_light_background: ""
org_name: ${ORG_NAME}
secrets: [{"secret":"globalSecret"}]
software:
packages:
- url: ${SOFTWARE_INSTALLER_URL}/invalidtype.txt
self_service: "not a boolean"

View file

@ -0,0 +1,22 @@
# Test config
controls:
queries:
policies:
agent_options:
org_settings:
server_settings:
server_url: $FLEET_SERVER_URL
org_info:
contact_url: https://example.com/contact
org_logo_url: ""
org_logo_url_light_background: ""
org_name: ${ORG_NAME}
secrets: [{"secret":"globalSecret"}]
software:
packages:
- install_script:
path: lib/install_ruby.sh
pre_install_query:
path: lib/query_ruby.yml
post_install_script:
path: lib/post_install_ruby.sh

View file

@ -0,0 +1,17 @@
# Test config
controls:
queries:
policies:
agent_options:
org_settings:
server_settings:
server_url: $FLEET_SERVER_URL
org_info:
contact_url: https://example.com/contact
org_logo_url: ""
org_logo_url_light_background: ""
org_name: ${ORG_NAME}
secrets: [{"secret":"globalSecret"}]
software:
packages:
- url: ${SOFTWARE_INSTALLER_URL}/notfound.deb

View file

@ -0,0 +1,21 @@
# Test config
controls:
queries:
policies:
agent_options:
org_settings:
server_settings:
server_url: $FLEET_SERVER_URL
org_info:
contact_url: https://example.com/contact
org_logo_url: ""
org_logo_url_light_background: ""
org_name: ${ORG_NAME}
secrets: [{"secret":"globalSecret"}]
software:
packages:
- url: ${SOFTWARE_INSTALLER_URL}/ruby.deb
install_script:
path: lib/install_ruby.sh
post_install_script:
path: lib/notfound.sh

View file

@ -0,0 +1,23 @@
# Test config
controls:
queries:
policies:
agent_options:
org_settings:
server_settings:
server_url: $FLEET_SERVER_URL
org_info:
contact_url: https://example.com/contact
org_logo_url: ""
org_logo_url_light_background: ""
org_name: ${ORG_NAME}
secrets: [{"secret":"globalSecret"}]
software:
packages:
- url: ${SOFTWARE_INSTALLER_URL}/ruby.deb
install_script:
path: lib/install_ruby.sh
pre_install_query:
path: lib/query_multiple.yml
post_install_script:
path: lib/post_install_ruby.sh

View file

@ -0,0 +1,21 @@
# Test config
controls:
queries:
policies:
agent_options:
org_settings:
server_settings:
server_url: $FLEET_SERVER_URL
org_info:
contact_url: https://example.com/contact
org_logo_url: ""
org_logo_url_light_background: ""
org_name: ${ORG_NAME}
secrets: [{"secret":"globalSecret"}]
software:
packages:
- url: ${SOFTWARE_INSTALLER_URL}/ruby.deb
install_script:
path: lib/install_ruby.sh
pre_install_query:
path: lib/notfound.yml

View file

@ -0,0 +1,17 @@
# Test config
controls:
queries:
policies:
agent_options:
org_settings:
server_settings:
server_url: $FLEET_SERVER_URL
org_info:
contact_url: https://example.com/contact
org_logo_url: ""
org_logo_url_light_background: ""
org_name: ${ORG_NAME}
secrets: [{"secret":"globalSecret"}]
software:
packages:
- url: ${SOFTWARE_INSTALLER_URL}/toolarge.deb

View file

@ -0,0 +1,17 @@
# Test config
controls:
queries:
policies:
agent_options:
org_settings:
server_settings:
server_url: $FLEET_SERVER_URL
org_info:
contact_url: https://example.com/contact
org_logo_url: ""
org_logo_url_light_background: ""
org_name: ${ORG_NAME}
secrets: [{"secret":"globalSecret"}]
software:
packages:
- url: ${SOFTWARE_INSTALLER_URL}/invalidtype.txt

View file

@ -0,0 +1,25 @@
# Test config
controls:
queries:
policies:
agent_options:
org_settings:
server_settings:
server_url: $FLEET_SERVER_URL
org_info:
contact_url: https://example.com/contact
org_logo_url: ""
org_logo_url_light_background: ""
org_name: ${ORG_NAME}
secrets: [{"secret":"globalSecret"}]
software:
packages:
- url: ${SOFTWARE_INSTALLER_URL}/ruby.deb
install_script:
path: lib/install_ruby.sh
pre_install_query:
path: lib/query_ruby.yml
post_install_script:
path: lib/post_install_ruby.sh
- url: ${SOFTWARE_INSTALLER_URL}/other.deb
self_service: true

View file

@ -0,0 +1,23 @@
name: "${TEST_TEAM_NAME}"
team_settings:
secrets:
- secret: "ABC"
features:
enable_host_users: true
enable_software_inventory: true
host_expiry_settings:
host_expiry_enabled: true
host_expiry_window: 30
agent_options:
controls:
policies:
queries:
software:
packages:
- url: ${SOFTWARE_INSTALLER_URL}/ruby.deb
install_script:
path: lib/install_ruby.sh
pre_install_query:
path: lib/query_multiple_apply.yml
post_install_script:
path: lib/post_install_ruby.sh

View file

@ -0,0 +1,25 @@
name: "${TEST_TEAM_NAME}"
team_settings:
secrets:
- secret: "ABC"
features:
enable_host_users: true
enable_software_inventory: true
host_expiry_settings:
host_expiry_enabled: true
host_expiry_window: 30
agent_options:
controls:
policies:
queries:
software:
packages:
- url: ${SOFTWARE_INSTALLER_URL}/ruby.deb
install_script:
path: lib/install_ruby.sh
pre_install_query:
path: lib/query_ruby_apply.yml
post_install_script:
path: lib/post_install_ruby.sh
- url: ${SOFTWARE_INSTALLER_URL}/other.deb
self_service: true

View file

@ -0,0 +1,25 @@
name: "${TEST_TEAM_NAME}"
team_settings:
secrets:
- secret: "ABC"
features:
enable_host_users: true
enable_software_inventory: true
host_expiry_settings:
host_expiry_enabled: true
host_expiry_window: 30
agent_options:
controls:
policies:
queries:
software:
packages:
- url: ${SOFTWARE_INSTALLER_URL}/ruby.deb
install_script:
path: lib/install_ruby.sh
pre_install_query:
path: lib/query_ruby_env.yml
post_install_script:
path: lib/post_install_ruby.sh
- url: ${SOFTWARE_INSTALLER_URL}/other.deb
self_service: true

View file

@ -2481,6 +2481,7 @@ Gets all information required by Fleet Desktop, this includes things like the nu
```json
{
"failing_policies_count": 3,
"self_service": true,
"notifications": {
"needs_mdm_migration": true,
"renew_enrollment_profile": false,
@ -2755,27 +2756,6 @@ Signals the Fleet server to send a webbook request with the device UUID and seri
---
#### Trigger FileVault key escrow
Sends a signal to Fleet Desktop to initiate a FileVault key escrow. This is useful for setting the escrow key initially as well as in scenarios where a token rotation is required. **Requires Fleet Premium license**
`POST /api/v1/fleet/device/{token}/rotate_encryption_key`
##### Parameters
| Name | Type | In | Description |
| ----- | ------ | ---- | ---------------------------------- |
| token | string | path | The device's authentication token. |
##### Example
`POST /api/v1/fleet/device/abcdef012456789/rotate_encryption_key`
##### Default response
`Status: 204`
### Report an agent error
Notifies the server about an agent error, resulting in two outcomes:
@ -2977,8 +2957,8 @@ _Available in Fleet Premium._
| Name | Type | In | Description |
| --------- | ------ | ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| team_id | number | query | The ID of the team to add the software package to. Only one team identifier (`team_id` or `team_name`) can be included in the request, omit this parameter if using `team_name`. |
| team_name | string | query | The name of the team to add the software package to. Only one team identifier (`team_id` or `team_name`) can be included in the request, omit this parameter if using `team_id`. |
| team_id | number | query | The ID of the team to add the software package to. Only one team identifier (`team_id` or `team_name`) can be included in the request, omit this parameter if using `team_name`. Ommitting these parameters will add software to 'No Team'. |
| team_name | string | query | The name of the team to add the software package to. Only one team identifier (`team_id` or `team_name`) can be included in the request, omit this parameter if using `team_id`. Ommitting these parameters will add software to 'No Team'. |
| dry_run | bool | query | If `true`, will validate the provided software packages and return any validation errors, but will not apply the changes. |
| software | list | body | An array of software objects. Each object consists of:`url`- URL to the software package (PKG, MSI, EXE or DEB),`install_script` - command that Fleet runs to install software, `pre_install_query` - condition query that determines if the install will proceed, and `post_install_script` - script that runs after software install. |

View file

@ -150,7 +150,7 @@ In some cases adding a read replica can increase database performance for specif
#### Traffic load balancing
Load balancing enables distributing request traffic over many instances of the backend application. Using AWS Application
Load Balancer can also [offload SSL termination](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/create-https-listener.html), freeing Fleet to spend the majority of it's allocated compute dedicated
Load Balancer can also [offload SSL termination](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/create-https-listener.html), freeing Fleet to spend the majority of its allocated compute dedicated
to its core functionality. More details about ALB can be found [here](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/introduction.html).
_**Note if using [terraform reference architecture](https://github.com/fleetdm/fleet/tree/main/infrastructure/dogfood/terraform/aws#terraform) all configurations can dynamically scale based on load(cpu/memory) and all configurations

View file

@ -67,5 +67,7 @@ A collection of guides to help you get up and running with Fleet.
- [Generate process trees with osquery](https://fleetdm.com/guides/generate-process-trees-with-osquery)
<a style="text-decoration: none;" href="https://fleetdm.com/guides"><animated-arrow-button>See all guides</animated-arrow-button></a>
<meta name="description" value="Links to deployment tutorials and guides for using Fleet.">
<meta name="pageOrderInSection" value="300">

View file

@ -3102,18 +3102,17 @@ Returns the information of the specified host.
### Get host by identifier
Returns the information of the host specified using the `uuid`, `hardware_serial`, `osquery_host_id`, `hostname`, or
`node_key` as an identifier.
Returns the information of the host specified using the `hostname`, `uuid`, or `hardware_serial` as an identifier.
If `hostname` is specified when there is more than one host with the same hostname, the endpoint returns the first matching host. In Fleet, hostnames are fully qualified domain names (FQDNs).
If `hostname` is specified when there is more than one host with the same hostname, the endpoint returns the first matching host. In Fleet, hostnames are fully qualified domain names (FQDNs). `hostname` (e.g. johns-macbook-air.local) is not the same as `display_name` (e.g. John's MacBook Air).
`GET /api/v1/fleet/hosts/identifier/:identifier`
#### Parameters
| Name | Type | In | Description |
| ---------- | ----------------- | ---- | ----------------------------------------------------------------------------- |
| identifier | integer or string | path | **Required**. The host's `hardware_serial`, `uuid`, `osquery_host_id`, `hostname`, or `node_key` |
| Name | Type | In | Description |
| ---------- | ----------------- | ---- | ------------------------------------------------------------------ |
| identifier | string | path | **Required**. The host's `hostname`, `uuid`, or `hardware_serial`. |
| exclude_software | boolean | query | If `true`, the response will not include a list of installed software for the host. |
#### Example
@ -3505,6 +3504,7 @@ This is the API route used by the **My device** page in Fleet desktop to display
]
}
},
"self_service": true,
"org_logo_url": "https://example.com/logo.jpg",
"license": {
"tier": "free",

View file

@ -28,7 +28,7 @@ Fleet's agent (fleetd) generated for MacOS by `fleetctl package` does not includ
> Ubuntu 24.04 comes with Wayland enabled by default. To use X11 instead of Wayland you can set
> `WaylandEnable=false` in `/etc/gdm3/custom.conf` and reboot.
> Fedora, CentOS 8 and 9 require a [gnome extension](https://extensions.gnome.org/extension/615/appindicator-support/) and Google Chrome set to the default browser for Fleet Desktop.
> Fedora, CentOS 8 and 9 require a [gnome extension](https://extensions.gnome.org/extension/615/appindicator-support/) and Google Chrome for Fleet Desktop.
> The `fleetctl package` command is not supported on DISA-STIG distribution.

View file

@ -504,7 +504,7 @@ SELECT
'' AS arch,
'' AS installed_path
FROM deb_packages
WHERE status = 'install ok installed'
WHERE status LIKE '% ok installed'
UNION
SELECT
package AS name,

View file

@ -12,7 +12,7 @@ Fleet supports the [latest version of osquery](https://github.com/osquery/osquer
## CLI
> You must have `fleetctl` installed. [Learn how to install `fleetctl`](https://fleetdm.com/fleetctl-preview).
> You must have `fleetctl` installed. [Learn how to install `fleetctl`](https://fleetdm.com/docs/using-fleet/fleetctl-cli#installing-fleetctl).
The `fleetctl package` command is used to generate Fleet's agent (fleetd).

View file

@ -270,6 +270,8 @@ func updatesAddFunc(c *cli.Context) error {
dstPath += ".exe"
case strings.HasSuffix(target, ".app.tar.gz"):
dstPath += ".app.tar.gz"
case strings.HasSuffix(target, ".pkg"):
dstPath += ".pkg"
// osquery extensions require the .ext suffix
case strings.HasSuffix(target, ".ext"):
dstPath += ".ext"

View file

@ -144,6 +144,9 @@ func adjustEmail(email string) string {
func (lowLevelAPI *GoogleCalendarLowLevelAPI) GetSetting(name string) (*calendar.Setting, error) {
result, err := lowLevelAPI.withRetry(
func() (any, error) {
if lowLevelAPI.service == nil || lowLevelAPI.service.Settings == nil {
return nil, errors.New("calendar service or settings not initialized")
}
return lowLevelAPI.service.Settings.Get(name).Do()
},
)
@ -153,6 +156,9 @@ func (lowLevelAPI *GoogleCalendarLowLevelAPI) GetSetting(name string) (*calendar
func (lowLevelAPI *GoogleCalendarLowLevelAPI) CreateEvent(event *calendar.Event) (*calendar.Event, error) {
result, err := lowLevelAPI.withRetry(
func() (any, error) {
if lowLevelAPI.service == nil || lowLevelAPI.service.Events == nil {
return nil, errors.New("calendar service or events not initialized (CreateEvent)")
}
return lowLevelAPI.service.Events.Insert(calendarID, event).Do()
},
)
@ -162,6 +168,9 @@ func (lowLevelAPI *GoogleCalendarLowLevelAPI) CreateEvent(event *calendar.Event)
func (lowLevelAPI *GoogleCalendarLowLevelAPI) UpdateEvent(event *calendar.Event) (*calendar.Event, error) {
result, err := lowLevelAPI.withRetry(
func() (any, error) {
if lowLevelAPI.service == nil || lowLevelAPI.service.Events == nil {
return nil, errors.New("calendar service or events not initialized (UpdateEvent)")
}
return lowLevelAPI.service.Events.Update(calendarID, event.Id, event).Do()
},
)
@ -171,6 +180,9 @@ func (lowLevelAPI *GoogleCalendarLowLevelAPI) UpdateEvent(event *calendar.Event)
func (lowLevelAPI *GoogleCalendarLowLevelAPI) GetEvent(id, eTag string) (*calendar.Event, error) {
result, err := lowLevelAPI.withRetry(
func() (any, error) {
if lowLevelAPI.service == nil || lowLevelAPI.service.Events == nil {
return nil, errors.New("calendar service or events not initialized (GetEvent)")
}
return lowLevelAPI.service.Events.Get(calendarID, id).IfNoneMatch(eTag).Do()
},
)
@ -180,6 +192,9 @@ func (lowLevelAPI *GoogleCalendarLowLevelAPI) GetEvent(id, eTag string) (*calend
func (lowLevelAPI *GoogleCalendarLowLevelAPI) ListEvents(timeMin, timeMax string) (*calendar.Events, error) {
result, err := lowLevelAPI.withRetry(
func() (any, error) {
if lowLevelAPI.service == nil || lowLevelAPI.service.Events == nil {
return nil, errors.New("calendar service or events not initialized (ListEvents)")
}
// Default maximum number of events returned is 250, which should be sufficient for most calendars.
return lowLevelAPI.service.Events.List(calendarID).
EventTypes("default").
@ -197,6 +212,9 @@ func (lowLevelAPI *GoogleCalendarLowLevelAPI) ListEvents(timeMin, timeMax string
func (lowLevelAPI *GoogleCalendarLowLevelAPI) DeleteEvent(id string) error {
_, err := lowLevelAPI.withRetry(
func() (any, error) {
if lowLevelAPI.service == nil || lowLevelAPI.service.Events == nil {
return nil, errors.New("calendar service or events not initialized (DeleteEvent)")
}
return nil, lowLevelAPI.service.Events.Delete(calendarID, id).Do()
},
)
@ -206,6 +224,9 @@ func (lowLevelAPI *GoogleCalendarLowLevelAPI) DeleteEvent(id string) error {
func (lowLevelAPI *GoogleCalendarLowLevelAPI) Watch(eventUUID string, channelID string, ttl uint64) (resourceID string, err error) {
resp, err := lowLevelAPI.withRetry(
func() (any, error) {
if lowLevelAPI.service == nil || lowLevelAPI.service.Events == nil {
return nil, errors.New("calendar service or events not initialized (Watch)")
}
return lowLevelAPI.service.Events.Watch(calendarID, &calendar.Channel{
Id: channelID, // channelID is also used for authentication -- it should be a random value
Type: "web_hook",
@ -226,6 +247,9 @@ func (lowLevelAPI *GoogleCalendarLowLevelAPI) Watch(eventUUID string, channelID
func (lowLevelAPI *GoogleCalendarLowLevelAPI) Stop(channelID string, resourceID string) error {
_, err := lowLevelAPI.withRetry(
func() (any, error) {
if lowLevelAPI.service == nil || lowLevelAPI.service.Channels == nil {
return nil, errors.New("calendar service or channels not initialized (Stop)")
}
return nil, lowLevelAPI.service.Channels.Stop(&calendar.Channel{
Id: channelID,
ResourceId: resourceID,
@ -274,30 +298,30 @@ func (c *GoogleCalendar) Configure(userEmail string) error {
}
func (c *GoogleCalendar) UpdateEventBody(event *fleet.CalendarEvent,
genBodyFn fleet.CalendarGenBodyFn) error {
genBodyFn fleet.CalendarGenBodyFn) (string, error) {
details, err := c.unmarshalDetails(event)
if err != nil {
return err
return "", err
}
gEvent, err := c.config.API.GetEvent(details.ID, "")
if err != nil {
return ctxerr.Wrap(c.config.Context, err, "retrieving Google calendar event")
return "", ctxerr.Wrap(c.config.Context, err, "retrieving Google calendar event")
}
// Check if the current description contains the conflict text
conflict := strings.Contains(gEvent.Description, fleet.CalendarEventConflictText)
var ok bool
gEvent.Description, ok, err = genBodyFn(conflict)
if err != nil {
return ctxerr.Wrap(c.config.Context, err, "generating calendar event body")
return "", ctxerr.Wrap(c.config.Context, err, "generating calendar event body")
}
if !ok {
return nil
return "", nil
}
_, err = c.config.API.UpdateEvent(gEvent)
updatedEvent, err := c.config.API.UpdateEvent(gEvent)
if err != nil {
return ctxerr.Wrap(c.config.Context, err, "updating Google calendar event")
return "", ctxerr.Wrap(c.config.Context, err, "updating Google calendar event")
}
return nil
return updatedEvent.Etag, nil
}
func (c *GoogleCalendar) GetAndUpdateEvent(event *fleet.CalendarEvent, genBodyFn fleet.CalendarGenBodyFn,
@ -417,7 +441,8 @@ func (c *GoogleCalendar) GetAndUpdateEvent(event *fleet.CalendarEvent, genBodyFn
if err != nil {
return nil, false, err
}
fleetEvent, err := c.googleEventToFleetEvent(*startTime, *endTime, gEvent, event.UUID, details.ChannelID, details.ResourceID)
fleetEvent, err := c.googleEventToFleetEvent(*startTime, *endTime, gEvent, event.UUID, details.ChannelID, details.ResourceID,
details.BodyTag)
if err != nil {
return nil, false, err
}
@ -650,8 +675,8 @@ func (c *GoogleCalendar) createEvent(
resourceID = opts.ResourceID
}
// Convert Google event to Fleet event
fleetEvent, err := c.googleEventToFleetEvent(eventStart, eventEnd, event, eventUUID, channelID, resourceID)
// Convert Google event to Fleet event. Body tag will be updated by the calling function.
fleetEvent, err := c.googleEventToFleetEvent(eventStart, eventEnd, event, eventUUID, channelID, resourceID, "body_tag")
if err != nil {
return nil, err
}
@ -702,8 +727,7 @@ func getLocation(tz string, config *GoogleCalendarConfig) *time.Location {
}
func (c *GoogleCalendar) googleEventToFleetEvent(startTime time.Time, endTime time.Time, event *calendar.Event, eventUUID string,
channelID string,
resourceID string) (
channelID string, resourceID string, bodyTag string) (
*fleet.CalendarEvent, error,
) {
tzName := c.location.String()
@ -718,6 +742,7 @@ func (c *GoogleCalendar) googleEventToFleetEvent(startTime time.Time, endTime ti
ETag: event.Etag,
ChannelID: channelID,
ResourceID: resourceID,
BodyTag: bodyTag,
}
detailsJson, err := json.Marshal(details)
if err != nil {

View file

@ -235,7 +235,9 @@ func (svc *Service) processCalendarEvent(ctx context.Context, eventDetails *flee
return ctxerr.Wrap(ctx, err, "set recent update flag")
}
// Event was updated, so we need to save it
err = event.SaveBodyTag(generatedTag)
if generatedTag != "" {
err = event.SaveDataItems("body_tag", generatedTag)
}
if err != nil {
return ctxerr.Wrap(ctx, err, "save calendar event body tag")
}

View file

@ -17,10 +17,6 @@ func (svc *Service) ListDevicePolicies(ctx context.Context, host *fleet.Host) ([
return svc.ds.ListPoliciesForHost(ctx, host)
}
func (svc *Service) RequestEncryptionKeyRotation(ctx context.Context, hostID uint) error {
return svc.ds.SetDiskEncryptionResetStatus(ctx, hostID, true)
}
const refetchMDMUnenrollCriticalQueryDuration = 3 * time.Minute
// TriggerMigrateMDMDevice triggers the webhook associated with the MDM

View file

@ -38,6 +38,12 @@ for user in $logged_in; do
done
# Create the pam_nologin file
touch /etc/nologin
echo "Locked by administrator" > /etc/nologin
# Disable systemd-user-sessions, a service that deletes /etc/nologin
if [ -f /usr/lib/systemd/system/systemd-user-sessions.service ]; then
systemctl mask systemd-user-sessions
systemctl daemon-reload
fi
echo "All non-root users have been logged out and their accounts locked."

View file

@ -21,4 +21,11 @@ do
done
# Remove the pam_nologin file
rm /etc/nologin
[ -f /etc/nologin ] && rm /etc/nologin
# Enable systemd-user-sessions, a service that deletes /etc/nologin
if [ -f /usr/lib/systemd/system/systemd-user-sessions.service ]; then
systemctl unmask systemd-user-sessions
systemctl daemon-reload
/usr/lib/systemd/systemd-user-sessions start
fi

View file

@ -20,6 +20,7 @@ import (
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mdm/apple/vpp"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/go-kit/log/level"
"github.com/google/uuid"
"golang.org/x/sync/errgroup"
@ -121,7 +122,7 @@ func (svc *Service) deleteVPPApp(ctx context.Context, teamID *uint, meta *fleet.
}
var teamName *string
if teamID != nil {
if teamID != nil && *teamID != 0 {
t, err := svc.ds.Team(ctx, *teamID)
if err != nil {
return ctxerr.Wrap(ctx, err, "getting team name for deleted VPP app")
@ -161,11 +162,19 @@ func (svc *Service) deleteSoftwareInstaller(ctx context.Context, meta *fleet.Sof
teamName = &t.Name
}
var teamID *uint
switch {
case meta.TeamID == nil:
teamID = ptr.Uint(0)
case meta.TeamID != nil:
teamID = meta.TeamID
}
if err := svc.NewActivity(ctx, vc.User, fleet.ActivityTypeDeletedSoftware{
SoftwareTitle: meta.SoftwareTitle,
SoftwarePackage: meta.Name,
TeamName: teamName,
TeamID: meta.TeamID,
TeamID: teamID,
SelfService: meta.SelfService,
}); err != nil {
return ctxerr.Wrap(ctx, err, "creating activity for deleted software")
@ -425,7 +434,7 @@ func (svc *Service) installSoftwareFromVPP(ctx context.Context, host *fleet.Host
return ctxerr.Wrapf(ctx, err, "sending command to install VPP %s application to host with serial %s", vppApp.AdamID, host.HardwareSerial)
}
err = svc.ds.InsertHostVPPSoftwareInstall(ctx, host.ID, user.ID, vppApp.AdamID, cmdUUID, eventID)
err = svc.ds.InsertHostVPPSoftwareInstall(ctx, host.ID, user.ID, vppApp.VPPAppID, cmdUUID, eventID)
if err != nil {
return ctxerr.Wrapf(ctx, err, "inserting host vpp software install for host with serial %s and app with adamID %s", host.HardwareSerial, vppApp.AdamID)
}
@ -463,12 +472,38 @@ func (svc *Service) GetSoftwareInstallResults(ctx context.Context, resultUUID st
res, err := svc.ds.GetSoftwareInstallResults(ctx, resultUUID)
if err != nil {
return nil, err
if fleet.IsNotFound(err) {
if err := svc.authz.Authorize(ctx, &fleet.HostSoftwareInstallerResultAuthz{}, fleet.ActionRead); err != nil {
return nil, err
}
}
svc.authz.SkipAuthorization(ctx)
return nil, ctxerr.Wrap(ctx, err, "get software install result")
}
// Team specific auth check
if err := svc.authz.Authorize(ctx, &fleet.HostSoftwareInstallerResultAuthz{HostTeamID: res.HostTeamID}, fleet.ActionRead); err != nil {
return nil, err
if res.HostDeletedAt == nil {
// host is not deleted, get it and authorize for the host's team
host, err := svc.ds.HostLite(ctx, res.HostID)
// if error is because the host does not exist, check first if the user
// had access to run a script (to prevent leaking valid host ids).
if err != nil {
if fleet.IsNotFound(err) {
if err := svc.authz.Authorize(ctx, &fleet.HostSoftwareInstallerResultAuthz{}, fleet.ActionRead); err != nil {
return nil, err
}
}
svc.authz.SkipAuthorization(ctx)
return nil, ctxerr.Wrap(ctx, err, "get host lite")
}
// Team specific auth check
if err := svc.authz.Authorize(ctx, &fleet.HostSoftwareInstallerResultAuthz{HostTeamID: host.TeamID}, fleet.ActionRead); err != nil {
return nil, err
}
} else {
// host was deleted, authorize for no-team as a fallback
if err := svc.authz.Authorize(ctx, &fleet.HostSoftwareInstallerResultAuthz{}, fleet.ActionRead); err != nil {
return nil, err
}
}
res.EnhanceOutputDetails()
@ -545,25 +580,24 @@ func (svc *Service) addMetadataToSoftwarePayload(ctx context.Context, payload *f
const maxInstallerSizeBytes int64 = 1024 * 1024 * 500
func (svc *Service) BatchSetSoftwareInstallers(ctx context.Context, tmName string, payloads []fleet.SoftwareInstallerPayload, dryRun bool) error {
if tmName == "" {
svc.authz.SkipAuthorization(ctx) // so that the error message is not replaced by "forbidden"
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("team_name", "must not be empty"))
}
if err := svc.authz.Authorize(ctx, &fleet.Team{}, fleet.ActionRead); err != nil {
return err
}
tm, err := svc.ds.TeamByName(ctx, tmName)
if err != nil {
// If this is a dry run, the team may not have been created yet
if dryRun && fleet.IsNotFound(err) {
return nil
var teamID *uint
if tmName != "" {
tm, err := svc.ds.TeamByName(ctx, tmName)
if err != nil {
// If this is a dry run, the team may not have been created yet
if dryRun && fleet.IsNotFound(err) {
return nil
}
return err
}
return err
teamID = &tm.ID
}
if err := svc.authz.Authorize(ctx, &fleet.SoftwareInstaller{TeamID: &tm.ID}, fleet.ActionWrite); err != nil {
if err := svc.authz.Authorize(ctx, &fleet.SoftwareInstaller{TeamID: teamID}, fleet.ActionWrite); err != nil {
return ctxerr.Wrap(ctx, err, "validating authorization")
}
@ -637,7 +671,7 @@ func (svc *Service) BatchSetSoftwareInstallers(ctx context.Context, tmName strin
}
installer := &fleet.UploadSoftwareInstallerPayload{
TeamID: &tm.ID,
TeamID: teamID,
InstallScript: p.InstallScript,
PreInstallQuery: p.PreInstallQuery,
PostInstallScript: p.PostInstallScript,
@ -697,7 +731,7 @@ func (svc *Service) BatchSetSoftwareInstallers(ctx context.Context, tmName strin
}
}
if err := svc.ds.BatchSetSoftwareInstallers(ctx, &tm.ID, installers); err != nil {
if err := svc.ds.BatchSetSoftwareInstallers(ctx, teamID, installers); err != nil {
return ctxerr.Wrap(ctx, err, "batch set software installers")
}

View file

@ -1246,7 +1246,7 @@ func (svc *Service) editTeamFromSpec(
if spec.Software != nil {
if team.Config.Software == nil {
team.Config.Software = &fleet.TeamSpecSoftware{}
team.Config.Software = &fleet.SoftwareSpec{}
}
if spec.Software.Packages.Set {

View file

@ -13,7 +13,6 @@ import Spinner from "components/Spinner/Spinner";
import { IMdmCommandResult } from "interfaces/mdm";
import { IActivityDetails } from "interfaces/activity";
import { IconNames } from "components/icons";
import {
getInstallDetailsStatusPredicate,
INSTALL_DETAILS_STATUS_ICONS,
@ -46,7 +45,10 @@ export const AppInstallDetails = ({
return mdmApi.getCommandResults(command_uuid).then((response) => {
const results = response.results?.[0];
if (!results) {
return Promise.reject(new Error("No data returned"));
// FIXME: It's currently possible that the command results API response is empty for pending
// commands. As a temporary workaround to handle this case, we'll ignore the empty response and
// display some minimal pending UI. This should be removed once the API response is fixed.
return {} as IMdmCommandResult;
}
return {
...results,
@ -66,31 +68,40 @@ export const AppInstallDetails = ({
} else if (isError) {
return <DataError description="Close this modal and try again." />;
} else if (!result) {
// FIXME: Find a better solution for this.
return <DataError description="No data returned." />;
// FIXME: It's currently possible that the command results API response is empty for pending
// commands. As a temporary workaround to handle this case, we'll ignore the empty response and
// display some minimal pending UI. This should be updated once the API response is fixed.
}
const displayStatus = (status as SoftwareInstallStatus) || "pending";
const iconName = INSTALL_DETAILS_STATUS_ICONS[displayStatus];
// Note: We need to reconcile status values from two different sources. From props, we
// get the status from the activity item details (which can be "failed", "pending", or
// "installed"). From the command results API response, we also receive the raw status
// from the MDM protocol, e.g., "NotNow" or "Acknowledged". We need to display some special
// messaging for the "NotNow" status, which otherwise would be treated as "pending".
const isStatusNotNow = result.status === "NotNow";
let iconName: IconNames;
const isStatusNotNow = result?.status === "NotNow";
let predicate: string;
let subordinate: string;
if (isStatusNotNow) {
iconName = INSTALL_DETAILS_STATUS_ICONS.pending;
predicate = "tried to install";
subordinate =
" but couldnt because the host was locked or was running on battery power while in Power Nap. Fleet will try again";
} else {
iconName = INSTALL_DETAILS_STATUS_ICONS[status as SoftwareInstallStatus];
predicate = getInstallDetailsStatusPredicate(status);
predicate = getInstallDetailsStatusPredicate(displayStatus);
subordinate = status === "pending" ? " when it comes online" : "";
}
const showCommandResponse = isStatusNotNow || status !== "pending";
const formattedHost = host_display_name ? (
<b>{host_display_name}</b>
) : (
"the host"
);
const showCommandPayload = !!result?.payload;
const showCommandResponse =
!!result?.result && (isStatusNotNow || status !== "pending");
return (
<>
@ -98,20 +109,21 @@ export const AppInstallDetails = ({
<div className={`${baseClass}__status-message`}>
{!!iconName && <Icon name={iconName} />}
<span>
Fleet {predicate} <b>{software_title}</b> on{" "}
<b>{host_display_name}</b>
Fleet {predicate} <b>{software_title}</b> on {formattedHost}
{subordinate}.
</span>
</div>
<div className={`${baseClass}__script-output`}>
Request payload:
<Textarea className={`${baseClass}__output-textarea`}>
{result.payload}
</Textarea>
</div>
{showCommandPayload && (
<div className={`${baseClass}__script-output`}>
Request payload:
<Textarea className={`${baseClass}__output-textarea`}>
{result.payload}
</Textarea>
</div>
)}
{showCommandResponse && (
<div className={`${baseClass}__script-output`}>
The response from <b>{host_display_name}</b>:
The response from {formattedHost}:
<Textarea className={`${baseClass}__output-textarea`}>
{result.result}
</Textarea>

View file

@ -1,6 +1,7 @@
import React from "react";
import { useQuery } from "react-query";
import { IActivityDetails } from "interfaces/activity";
import {
ISoftwareInstallResult,
ISoftwareInstallResults,
@ -21,17 +22,28 @@ import {
const baseClass = "software-install-details";
// TODO: Expand to include more details as needed
export type IPackageInstallDetails = Pick<
IActivityDetails,
"install_uuid" | "host_display_name"
>;
const StatusMessage = ({
result: { host_display_name, software_package, software_title, status },
}: {
result: ISoftwareInstallResult;
}) => {
const formattedHost = host_display_name ? (
<b>{host_display_name}</b>
) : (
"the host"
);
return (
<div className={`${baseClass}__status-message`}>
<Icon name={INSTALL_DETAILS_STATUS_ICONS[status]} />
<span>
Fleet {getInstallDetailsStatusPredicate(status)} <b>{software_title}</b>{" "}
({software_package}) on <b>{host_display_name}</b>
({software_package}) on {formattedHost}
{status === "pending" ? " when it comes online" : ""}.
</span>
</div>
@ -56,18 +68,17 @@ const Output = ({
};
export const SoftwareInstallDetails = ({
installUuid,
}: {
installUuid: string;
}) => {
host_display_name = "",
install_uuid = "",
}: IPackageInstallDetails) => {
const { data: result, isLoading, isError } = useQuery<
ISoftwareInstallResults,
Error,
ISoftwareInstallResult
>(
["softwareInstallResults", installUuid],
["softwareInstallResults", install_uuid],
() => {
return softwareAPI.getSoftwareInstallResult(installUuid);
return softwareAPI.getSoftwareInstallResult(install_uuid);
},
{
refetchOnWindowFocus: false,
@ -88,7 +99,11 @@ export const SoftwareInstallDetails = ({
return (
<>
<div className={`${baseClass}__software-install-details`}>
<StatusMessage result={result} />
<StatusMessage
result={
result.host_display_name ? result : { ...result, host_display_name } // prefer result.host_display_name (it may be empty if the host was deleted) otherwise default to whatever we received via props
}
/>
{result.status !== "pending" && (
<>
{result.pre_install_query_output && (
@ -106,10 +121,10 @@ export const SoftwareInstallDetails = ({
};
export const SoftwareInstallDetailsModal = ({
installUuid,
details,
onCancel,
}: {
installUuid: string;
details: IPackageInstallDetails;
onCancel: () => void;
}) => {
return (
@ -121,7 +136,7 @@ export const SoftwareInstallDetailsModal = ({
>
<>
<div className={`${baseClass}__modal-content`}>
<SoftwareInstallDetails installUuid={installUuid} />
<SoftwareInstallDetails {...details} />
</div>
<div className="modal-cta-wrap">
<Button onClick={onCancel} variant="brand">

View file

@ -14,7 +14,7 @@ const INSTALL_DETAILS_STATUS_PREDICATES: Record<
SoftwareInstallStatus,
string
> = {
pending: "will install",
pending: "is installing or will install",
installed: "installed",
failed: "failed to install",
} as const;

View file

@ -15,7 +15,7 @@ interface IDataErrorProps {
children?: React.ReactNode;
card?: boolean;
className?: string;
// flag to use the updated DataError design
/** Flag to use the updated DataError design */
useNew?: boolean;
}

View file

@ -75,7 +75,7 @@ const Editor = ({
<TooltipWrapper
className={labelClassName}
tipContent={labelTooltip}
position="top"
position="top-start"
>
{labelText}
</TooltipWrapper>

View file

@ -37,9 +37,12 @@ const MainContent = ({
config,
isPremiumTier,
noSandboxHosts,
apnsExpiry = "",
abmExpiry = "",
vppExpiry = "",
isApplePnsExpired,
isAppleBmExpired,
isVppExpired,
willAppleBmExpire,
willApplePnsExpire,
willVppExpire,
} = useContext(AppContext);
const sandboxExpiryTime =
@ -49,29 +52,22 @@ const MainContent = ({
const renderAppWideBanner = () => {
const isAppleBmTermsExpired = config?.mdm?.apple_bm_terms_expired;
const isApplePnsExpired = hasLicenseExpired(apnsExpiry);
const willApplePnsExpireIn30Days = willExpireWithinXDays(apnsExpiry, 30);
const isAppleBmExpired = hasLicenseExpired(abmExpiry); // NOTE: See Rachel's related FIXME added to App.tsx in https://github.com/fleetdm/fleet/pull/19571
const willAppleBmExpireIn30Days = willExpireWithinXDays(abmExpiry, 30);
const isFleetLicenseExpired = hasLicenseExpired(
config?.license.expiration || ""
);
const isVppExpired = hasLicenseExpired(vppExpiry);
const willVppExpireIn30Days = willExpireWithinXDays(vppExpiry, 30);
let banner: JSX.Element | null = null;
if (isPremiumTier) {
if (isApplePnsExpired || willApplePnsExpireIn30Days) {
if (isApplePnsExpired || willApplePnsExpire) {
banner = <ApplePNCertRenewalMessage expired={isApplePnsExpired} />;
} else if (isAppleBmExpired || willAppleBmExpireIn30Days) {
} else if (isAppleBmExpired || willAppleBmExpire) {
banner = <AppleBMRenewalMessage expired={isAppleBmExpired} />;
} else if (isAppleBmTermsExpired) {
banner = <AppleBMTermsMessage />;
} else if (isFleetLicenseExpired) {
banner = <LicenseExpirationBanner />;
} else if (isVppExpired || willVppExpireIn30Days) {
} else if (isVppExpired || willVppExpire) {
banner = <VppRenewalMessage expired={isVppExpired} />;
}
}

View file

@ -18,6 +18,11 @@ export interface IModalProps {
isHidden?: boolean;
/** isLoading can be set true to enable targeting elements by loading state */
isLoading?: boolean;
/** isContentDisabled can be set to true to display the modal content as disabled.
* At the moment this will place an overlay over the modal content and make it
* unclickable.
*/
isContentDisabled?: boolean;
className?: string;
}
@ -29,6 +34,7 @@ const Modal = ({
width = "medium",
isHidden = false,
isLoading = false,
isContentDisabled = false,
className,
}: IModalProps): JSX.Element => {
useEffect(() => {
@ -61,26 +67,30 @@ const Modal = ({
}
}, [onEnter]);
const modalContainerClassName = classnames(
`${baseClass}__modal_container`,
const backgroundClasses = classnames(`${baseClass}__background`, {
[`${baseClass}__hidden`]: isHidden,
});
const modalContainerClasses = classnames(
className,
{ [`${baseClass}__modal_container__medium`]: width === "medium" },
{ [`${baseClass}__modal_container__large`]: width === "large" },
{ [`${baseClass}__modal_container__xlarge`]: width === "xlarge" },
{ [`${baseClass}__modal_container__auto`]: width === "auto" }
`${baseClass}__modal_container`,
`${baseClass}__modal_container__${width}`,
{
[`${className}__loading`]: isLoading,
}
);
const contentWrapperClasses = classnames(`${baseClass}__content-wrapper`, {
[`${baseClass}__content-wrapper-disabled`]: isContentDisabled,
});
const contentClasses = classnames(`${baseClass}__content`, {
[`${baseClass}__content-disabled`]: isContentDisabled,
});
return (
<div
className={`${baseClass}__background ${
isHidden ? `${baseClass}__hidden` : ""
}`}
>
<div
className={`${modalContainerClassName} ${
isLoading ? `${className}__loading` : ""
}`}
>
<div className={backgroundClasses}>
<div className={modalContainerClasses}>
<div className={`${baseClass}__header`}>
<span>{title}</span>
<div className={`${baseClass}__ex`}>
@ -89,7 +99,13 @@ const Modal = ({
</Button>
</div>
</div>
<div className={`${baseClass}__content`}>{children}</div>
<div className={contentWrapperClasses}>
{isContentDisabled && (
<div className={`${baseClass}__disabled-overlay`} />
)}
<div className={contentClasses}>{children}</div>
</div>
</div>
</div>
);

View file

@ -16,7 +16,7 @@
}
}
&__content {
&__content-wrapper {
margin-top: $pad-large;
font-size: $x-small;
@ -100,4 +100,21 @@
width: auto;
}
}
// these styles are for the modal content when it is disabled
&__content-wrapper-disabled {
position: relative;
}
&__content-disabled {
transition: opacity 150ms ease-in-out;
opacity: 0.5; // this adds a disabled effect to the modal content
}
&__disabled-overlay {
position: absolute;
height: 100%;
width: 100%;
z-index: 1000;
}
}

View file

@ -56,6 +56,7 @@ const REGEX_GLOBAL_PAGES = {
const REGEX_EXCLUDE_NO_TEAM_PAGES = {
MANAGE_POLICIES: /\/policies\/manage/i,
MANAGE_QUERIES: /\/queries\/manage/i,
};
const testDetailPage = (path: string, re: RegExp) => {
@ -96,7 +97,6 @@ const SiteTopNav = ({
isGlobalMaintainer,
isAnyTeamMaintainer,
isNoAccess,
isSandboxMode,
} = useContext(AppContext);
const isActiveDetailPage = isDetailPage(currentPath);
@ -187,7 +187,7 @@ const SiteTopNav = ({
<LinkWithContext
className={`${navItemBaseClass}__link`}
withParams={withParams}
currentQueryParams={currentQueryParams}
currentQueryParams={{ team_id: currentQueryParams.team_id }}
to={navItem.location.pathname}
>
<span
@ -220,8 +220,7 @@ const SiteTopNav = ({
isAnyTeamAdmin,
isAnyTeamMaintainer,
isGlobalMaintainer,
isNoAccess,
isSandboxMode
isNoAccess
);
const renderNavItems = () => {
@ -238,7 +237,6 @@ const SiteTopNav = ({
currentUser={currentUser}
isAnyTeamAdmin={isAnyTeamAdmin}
isGlobalAdmin={isGlobalAdmin}
isSandboxMode={isSandboxMode}
/>
</div>
);

View file

@ -27,8 +27,7 @@ export default (
isAnyTeamAdmin = false,
isAnyTeamMaintainer = false,
isGlobalMaintainer = false,
isNoAccess = false,
isSandboxMode = false
isNoAccess = false
): INavItem[] => {
if (!user) {
return [];
@ -67,7 +66,7 @@ export default (
regex: new RegExp(`^${URL_PREFIX}/controls/`),
pathname: PATHS.CONTROLS,
},
exclude: isSandboxMode || !isMaintainerOrAdmin,
exclude: !isMaintainerOrAdmin,
withParams: { type: "query", names: ["team_id"] },
},
{

View file

@ -17,7 +17,6 @@ interface IUserMenuProps {
isAnyTeamAdmin: boolean | undefined;
isGlobalAdmin: boolean | undefined;
currentUser: IUser;
isSandboxMode?: boolean;
}
const UserMenu = ({
@ -26,7 +25,6 @@ const UserMenu = ({
isAnyTeamAdmin,
isGlobalAdmin,
currentUser,
isSandboxMode = false,
}: IUserMenuProps): JSX.Element => {
const accountNavigate = onNavItemClick(PATHS.ACCOUNT);
const dropdownItems = [
@ -44,7 +42,7 @@ const UserMenu = ({
},
];
if (isGlobalAdmin && !isSandboxMode) {
if (isGlobalAdmin) {
const manageUsersNavigate = onNavItemClick(PATHS.ADMIN_USERS);
const manageUserNavItem = {

View file

@ -11,6 +11,7 @@ import {
import { IUser } from "interfaces/user";
import permissions from "utilities/permissions";
import sort from "utilities/sort";
import { hasLicenseExpired, willExpireWithinXDays } from "utilities/helpers";
enum ACTIONS {
SET_AVAILABLE_TEAMS = "SET_AVAILABLE_TEAMS",
@ -144,6 +145,12 @@ type InitialStateType = {
isOnlyObserver?: boolean;
isObserverPlus?: boolean;
isNoAccess?: boolean;
isAppleBmExpired: boolean;
isApplePnsExpired: boolean;
isVppExpired: boolean;
willAppleBmExpire: boolean;
willApplePnsExpire: boolean;
willVppExpire: boolean;
abmExpiry?: string;
apnsExpiry?: string;
vppExpiry?: string;
@ -206,6 +213,12 @@ export const initialState = {
filteredSoftwarePath: undefined,
filteredQueriesPath: undefined,
filteredPoliciesPath: undefined,
isAppleBmExpired: false,
isApplePnsExpired: false,
isVppExpired: false,
willAppleBmExpire: false,
willApplePnsExpire: false,
willVppExpire: false,
setAvailableTeams: () => null,
setCurrentUser: () => null,
setCurrentTeam: () => null,
@ -339,6 +352,8 @@ const reducer = (state: InitialStateType, action: IAction) => {
return {
...state,
abmExpiry,
isAppleBmExpired: hasLicenseExpired(abmExpiry),
willAppleBmExpire: willExpireWithinXDays(abmExpiry, 30),
};
}
case ACTIONS.SET_APNS_EXPIRY: {
@ -346,6 +361,8 @@ const reducer = (state: InitialStateType, action: IAction) => {
return {
...state,
apnsExpiry,
isApplePnsExpired: hasLicenseExpired(apnsExpiry),
willApplePnsExpire: willExpireWithinXDays(apnsExpiry, 30),
};
}
case ACTIONS.SET_VPP_EXPIRY: {
@ -353,6 +370,8 @@ const reducer = (state: InitialStateType, action: IAction) => {
return {
...state,
vppExpiry,
isVppExpired: hasLicenseExpired(vppExpiry),
willVppExpire: willExpireWithinXDays(vppExpiry, 30),
};
}
case ACTIONS.SET_SANDBOX_EXPIRY: {
@ -418,6 +437,12 @@ const AppProvider = ({ children }: Props): JSX.Element => {
abmExpiry: state.abmExpiry,
apnsExpiry: state.apnsExpiry,
vppExpiry: state.vppExpiry,
isAppleBmExpired: state.isAppleBmExpired,
isApplePnsExpired: state.isApplePnsExpired,
isVppExpired: state.isVppExpired,
willAppleBmExpire: state.willAppleBmExpire,
willApplePnsExpire: state.willApplePnsExpire,
willVppExpire: state.willVppExpire,
noSandboxHosts: state.noSandboxHosts,
filteredHostsPath: state.filteredHostsPath,
filteredSoftwarePath: state.filteredSoftwarePath,

View file

@ -395,6 +395,8 @@ export const useTeamIdParam = ({
currentTeamName: currentTeam?.name,
currentTeamSummary: currentTeam,
isAnyTeamSelected: isAnyTeamSelected(currentTeam?.id),
isAllTeamsSelected:
!isAnyTeamSelected(currentTeam?.id) && currentTeam?.id !== 0,
isRouteOk,
isTeamAdmin:
!!currentTeam?.id && permissions.isTeamAdmin(currentUser, currentTeam.id),

View file

@ -37,7 +37,10 @@ const ActivityFeed = ({
const [pageIndex, setPageIndex] = useState(0);
const [showShowQueryModal, setShowShowQueryModal] = useState(false);
const [showScriptDetailsModal, setShowScriptDetailsModal] = useState(false);
const [installedSoftwareUuid, setInstalledSoftwareUuid] = useState("");
const [
packageInstallDetails,
setPackageInstallDetails,
] = useState<IActivityDetails | null>(null);
const [
appInstallDetails,
setAppInstallDetails,
@ -88,7 +91,6 @@ const ActivityFeed = ({
activityType: ActivityType,
details: IActivityDetails
) => {
console.log("activityType", activityType);
switch (activityType) {
case ActivityType.LiveQuery:
queryShown.current = details.query_sql ?? "";
@ -102,10 +104,10 @@ const ActivityFeed = ({
setShowScriptDetailsModal(true);
break;
case ActivityType.InstalledSoftware:
setInstalledSoftwareUuid(details.install_uuid ?? "");
setPackageInstallDetails({ ...details });
break;
case ActivityType.InstalledAppStoreApp:
setAppInstallDetails(details);
setAppInstallDetails({ ...details });
break;
default:
break;
@ -197,10 +199,10 @@ const ActivityFeed = ({
onCancel={() => setShowScriptDetailsModal(false)}
/>
)}
{installedSoftwareUuid && (
{packageInstallDetails && (
<SoftwareInstallDetailsModal
installUuid={installedSoftwareUuid}
onCancel={() => setInstalledSoftwareUuid("")}
details={packageInstallDetails}
onCancel={() => setPackageInstallDetails(null)}
/>
)}
{appInstallDetails && (

View file

@ -18,4 +18,9 @@
.side-nav__card-container > .custom-settings {
max-width: none;
}
@media (max-width: 1120px) {
.side-nav__nav-list {
padding-right: 0;
}
}
}

View file

@ -99,6 +99,10 @@
flex-direction: column;
align-items: center;
gap: $pad-small;
&--message {
text-align: center;
}
}
&__button-wrap {

View file

@ -7,7 +7,7 @@ import { NotificationContext } from "context/notification";
import { IApiError } from "interfaces/errors";
import { ILabelSummary } from "interfaces/label";
import labelsAPI from "services/entities/labels";
import labelsAPI, { getCustomLabels } from "services/entities/labels";
import mdmAPI from "services/entities/mdm";
// @ts-ignore
@ -250,10 +250,7 @@ const AddProfileModal = ({
isError: isErrorLabels,
} = useQuery<ILabelSummary[], Error>(
["custom_labels"],
() =>
labelsAPI
.summary()
.then((res) => res.labels.filter((l) => l.label_type !== "builtin")),
() => labelsAPI.summary().then((res) => getCustomLabels(res.labels)),
{
enabled: isPremiumTier,

View file

@ -57,10 +57,10 @@
flex-direction: column;
align-items: center;
gap: $pad-small;
}
&__profile-graphic--message {
text-align: center;
&--message {
text-align: center;
}
}
&__button-wrap {

View file

@ -9,11 +9,29 @@
border-right: none;
}
@media (max-width: 1120px) {
.view-hosts-link {
span {
display: none;
}
.linkToFilteredHosts__header {
width: auto;
max-width: 120px;
}
}
@media (max-width: 1120px) {
.view-hosts-link {
span {
display: none;
}
}
}
.linkToFilteredHosts {
&__header {
width: 0;
.column-header {
width: 0;
}
}
&__cell {
width: min-content;
}
}
}

View file

@ -151,10 +151,13 @@ const SoftwarePage = ({ children, router, location }: ISoftwarePageProps) => {
const [showPreviewTicketModal, setShowPreviewTicketModal] = useState(false);
const [showAddSoftwareModal, setShowAddSoftwareModal] = useState(false);
const [resetPageIndex, setResetPageIndex] = useState<boolean>(false);
const [addedSoftwareToken, setAddedSoftwareToken] = useState<string | null>(
null
);
const {
currentTeamId,
isAnyTeamSelected,
isAllTeamsSelected,
isRouteOk,
teamIdForApi,
userTeams,
@ -315,7 +318,7 @@ const SoftwarePage = ({ children, router, location }: ISoftwarePageProps) => {
const renderPageActions = () => {
const canManageAutomations =
isGlobalAdmin && (!isPremiumTier || !isAnyTeamSelected);
isGlobalAdmin && (!isPremiumTier || isAllTeamsSelected);
const canAddSoftware =
isGlobalAdmin || isGlobalMaintainer || isTeamAdmin || isTeamMaintainer;
@ -345,8 +348,8 @@ const SoftwarePage = ({ children, router, location }: ISoftwarePageProps) => {
const renderHeaderDescription = () => {
return (
<p>
Manage software and search for installed software, OS and
vulnerabilities {isAnyTeamSelected ? "on this team" : "for all hosts"}.
Manage software and search for installed software, OS, and
vulnerabilities {isAllTeamsSelected ? "for all hosts" : "on this team"}.
</p>
);
};
@ -385,6 +388,7 @@ const SoftwarePage = ({ children, router, location }: ISoftwarePageProps) => {
showExploitedVulnerabilitiesOnly,
softwareFilter,
resetPageIndex,
addedSoftwareToken,
})}
</div>
);
@ -424,6 +428,7 @@ const SoftwarePage = ({ children, router, location }: ISoftwarePageProps) => {
teamId={currentTeamId ?? 0}
router={router}
onExit={toggleAddSoftwareModal}
setAddedSoftwareToken={setAddedSoftwareToken}
/>
)}
</div>

View file

@ -76,11 +76,13 @@ const getSoftwareNameCellData = (
iconUrl = app_store_app.icon_url;
}
const isAllTeams = teamId === undefined;
return {
name: softwareTitle.name,
source: softwareTitle.source,
path: softwareTitleDetailsPath,
hasPackage: hasPackage && !!teamId,
hasPackage: hasPackage && !isAllTeams,
isSelfService,
iconUrl,
};

View file

@ -43,6 +43,7 @@ interface ISoftwareTitlesProps {
currentPage: number;
teamId?: number;
resetPageIndex: boolean;
addedSoftwareToken: string | null;
}
const SoftwareTitles = ({
@ -56,6 +57,7 @@ const SoftwareTitles = ({
currentPage,
teamId,
resetPageIndex,
addedSoftwareToken,
}: ISoftwareTitlesProps) => {
const showVersions = location.pathname === PATHS.SOFTWARE_VERSIONS;
@ -80,6 +82,7 @@ const SoftwareTitles = ({
orderDirection,
orderKey,
teamId,
addedSoftwareToken,
...getSoftwareFilterForQueryKey(softwareFilter),
},
],
@ -113,6 +116,7 @@ const SoftwareTitles = ({
orderKey,
teamId,
vulnerable: softwareFilter === "vulnerableSoftware",
addedSoftwareToken,
},
],
({ queryKey: [queryKey] }) =>

View file

@ -21,9 +21,15 @@ interface IAddPackageProps {
teamId: number;
router: InjectedRouter;
onExit: () => void;
setAddedSoftwareToken: (token: string) => void;
}
const AddPackage = ({ teamId, router, onExit }: IAddPackageProps) => {
const AddPackage = ({
teamId,
router,
onExit,
setAddedSoftwareToken,
}: IAddPackageProps) => {
const { renderFlash } = useContext(NotificationContext);
const [isUploading, setIsUploading] = useState(false);
@ -86,7 +92,8 @@ const AddPackage = ({ teamId, router, onExit }: IAddPackageProps) => {
} else {
newQueryParams.available_for_install = true;
}
// any unique string - triggers SW refetch
setAddedSoftwareToken(`${Date.now()}`);
router.push(
`${PATHS.SOFTWARE_TITLES}?${buildQueryStringFromParams(newQueryParams)}`
);

View file

@ -37,12 +37,14 @@ interface IAddSoftwareModalProps {
teamId: number;
router: InjectedRouter;
onExit: () => void;
setAddedSoftwareToken: (token: string) => void;
}
const AddSoftwareModal = ({
teamId,
router,
onExit,
setAddedSoftwareToken,
}: IAddSoftwareModalProps) => {
return (
<Modal
@ -62,10 +64,20 @@ const AddSoftwareModal = ({
<Tab>App Store (VPP)</Tab>
</TabList>
<TabPanel>
<AddPackage teamId={teamId} router={router} onExit={onExit} />
<AddPackage
teamId={teamId}
router={router}
onExit={onExit}
setAddedSoftwareToken={setAddedSoftwareToken}
/>
</TabPanel>
<TabPanel>
<AppStoreVpp teamId={teamId} router={router} onExit={onExit} />
<AppStoreVpp
teamId={teamId}
router={router}
onExit={onExit}
setAddedSoftwareToken={setAddedSoftwareToken}
/>
</TabPanel>
</Tabs>
</TabsWrapper>

View file

@ -22,7 +22,7 @@ import { NotificationContext } from "context/notification";
import { getErrorReason } from "interfaces/errors";
import { buildQueryStringFromParams } from "utilities/url";
import SoftwareIcon from "../icons/SoftwareIcon";
import { getErrorMessage } from "./helpers";
import { getErrorMessage, getUniqueAppId } from "./helpers";
const baseClass = "app-store-vpp";
@ -64,10 +64,16 @@ const NoVppAppsCard = () => (
interface IVppAppListItemProps {
app: IVppApp;
selected: boolean;
uniqueAppId: string;
onSelect: (software: IVppApp) => void;
}
const VppAppListItem = ({ app, selected, onSelect }: IVppAppListItemProps) => {
const VppAppListItem = ({
app,
selected,
uniqueAppId,
onSelect,
}: IVppAppListItemProps) => {
return (
<li className={`${baseClass}__list-item`}>
<Radio
@ -77,9 +83,9 @@ const VppAppListItem = ({ app, selected, onSelect }: IVppAppListItemProps) => {
<span>{app.name}</span>
</div>
}
id={`vppApp-${app.app_store_id}`}
id={`vppApp-${uniqueAppId}`}
checked={selected}
value={app.app_store_id.toString()}
value={uniqueAppId}
name="vppApp"
onChange={() => onSelect(app)}
/>
@ -98,28 +104,41 @@ interface IVppAppListProps {
onSelect: (app: IVppApp) => void;
}
const VppAppList = ({ apps, selectedApp, onSelect }: IVppAppListProps) => (
<div className={`${baseClass}__list-container`}>
<ul className={`${baseClass}__list`}>
{apps.map((app) => (
<VppAppListItem
key={app.app_store_id}
app={app}
selected={selectedApp?.app_store_id === app.app_store_id}
onSelect={onSelect}
/>
))}
</ul>
</div>
);
const VppAppList = ({ apps, selectedApp, onSelect }: IVppAppListProps) => {
const uniqueSelectedAppId = selectedApp ? getUniqueAppId(selectedApp) : null;
return (
<div className={`${baseClass}__list-container`}>
<ul className={`${baseClass}__list`}>
{apps.map((app) => {
const uniqueAppId = getUniqueAppId(app);
return (
<VppAppListItem
key={uniqueAppId}
app={app}
selected={uniqueSelectedAppId === uniqueAppId}
uniqueAppId={uniqueAppId}
onSelect={onSelect}
/>
);
})}
</ul>
</div>
);
};
interface IAppStoreVppProps {
teamId: number;
router: InjectedRouter;
onExit: () => void;
setAddedSoftwareToken: (token: string) => void;
}
const AppStoreVpp = ({ teamId, router, onExit }: IAppStoreVppProps) => {
const AppStoreVpp = ({
teamId,
router,
onExit,
setAddedSoftwareToken,
}: IAppStoreVppProps) => {
const { renderFlash } = useContext(NotificationContext);
const [isSubmitDisabled, setIsSubmitDisabled] = useState(true);
const [selectedApp, setSelectedApp] = useState<IVppApp | null>(null);
@ -160,7 +179,11 @@ const AppStoreVpp = ({ teamId, router, onExit }: IAppStoreVppProps) => {
}
try {
await mdmAppleAPI.addVppApp(teamId, selectedApp.app_store_id);
await mdmAppleAPI.addVppApp(
teamId,
selectedApp.app_store_id,
selectedApp.platform
);
renderFlash(
"success",
<>
@ -172,6 +195,8 @@ const AppStoreVpp = ({ teamId, router, onExit }: IAppStoreVppProps) => {
team_id: teamId,
available_for_install: true,
});
// any unique string - triggers SW refetch
setAddedSoftwareToken(`${Date.now()}`);
router.push(`${PATHS.SOFTWARE}?${queryParams}`);
} catch (e) {
renderFlash("error", getErrorMessage(e));

View file

@ -1,5 +1,6 @@
import React from "react";
import { getErrorReason } from "interfaces/errors";
import { IVppApp } from "services/entities/mdm_apple";
const ADD_SOFTWARE_ERROR_PREFIX = "Couldnt add software.";
const DEFAULT_ERROR_MESSAGE = `${ADD_SOFTWARE_ERROR_PREFIX} Please try again.`;
@ -40,3 +41,6 @@ export const getErrorMessage = (e: unknown) => {
}
return DEFAULT_ERROR_MESSAGE;
};
export const getUniqueAppId = (app: IVppApp) =>
`${app.app_store_id}_${app.platform}`;

View file

@ -4,6 +4,7 @@ import { useQuery } from "react-query";
import { AxiosError } from "axios";
import PATHS from "router/paths";
import { AppContext } from "context/app";
import { NotificationContext } from "context/notification";
import { getErrorReason } from "interfaces/errors";
import mdmAppleAPI, { IGetVppInfoResponse } from "services/entities/mdm_apple";
@ -121,6 +122,7 @@ interface IVppSetupPageProps {
const VppSetupPage = ({ router }: IVppSetupPageProps) => {
const [showDisableModal, setShowDisableModal] = useState(false);
const [showRenewModal, setShowRenewModal] = useState(false);
const { setVppExpiry } = useContext(AppContext);
const {
data: vppData,
@ -134,6 +136,9 @@ const VppSetupPage = ({ router }: IVppSetupPageProps) => {
{
...DEFAULT_USE_QUERY_OPTIONS,
retry: false,
onSuccess: (data) => {
setVppExpiry(data.renew_date);
},
}
);

View file

@ -55,7 +55,7 @@ export const LABEL_SLUG_PREFIX = "labels/";
export const DEFAULT_SORT_HEADER = "display_name";
export const DEFAULT_SORT_DIRECTION = "asc";
export const DEFAULT_PAGE_SIZE = 20;
export const DEFAULT_PAGE_SIZE = 50;
export const DEFAULT_PAGE_INDEX = 0;
export const getHostSelectStatuses = (isSandboxMode = false) => {

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