diff --git a/.github/workflows/build-and-check-fleetctl-docker-and-deps.yml b/.github/workflows/build-and-check-fleetctl-docker-and-deps.yml index 50f4e58f13..ff20260409 100644 --- a/.github/workflows/build-and-check-fleetctl-docker-and-deps.yml +++ b/.github/workflows/build-and-check-fleetctl-docker-and-deps.yml @@ -47,7 +47,7 @@ jobs: - name: Set up Go uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: - go-version: ${{ vars.GO_VERSION }} + go-version-file: 'go.mod' - name: Install Go Dependencies run: make deps-go diff --git a/.github/workflows/build-binaries.yaml b/.github/workflows/build-binaries.yaml index ed18437c74..278f958b28 100644 --- a/.github/workflows/build-binaries.yaml +++ b/.github/workflows/build-binaries.yaml @@ -29,10 +29,13 @@ jobs: with: egress-policy: audit + - name: Checkout Code + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - name: Install Go uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: - go-version: ${{ vars.GO_VERSION }} + go-version-file: 'go.mod' # Set the Node.js version - name: Set up Node.js ${{ vars.NODE_VERSION }} @@ -40,9 +43,6 @@ jobs: with: node-version: ${{ vars.NODE_VERSION }} - - name: Checkout Code - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 - - name: JS Dependency Cache id: js-cache uses: actions/cache@69d9d449aced6a2ede0bc19182fadc3a0a42d2b0 # v2 diff --git a/.github/workflows/build-orbit.yaml b/.github/workflows/build-orbit.yaml index 09f296aece..002d2657f6 100644 --- a/.github/workflows/build-orbit.yaml +++ b/.github/workflows/build-orbit.yaml @@ -59,7 +59,7 @@ jobs: - name: Set up Go uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: - go-version: ${{ vars.GO_VERSION }} + go-version-file: 'go.mod' - name: Build, codesign and notarize orbit run: go run ./orbit/tools/build/build.go diff --git a/.github/workflows/check-automated-doc.yml b/.github/workflows/check-automated-doc.yml index c654e7ae4f..d289c55318 100644 --- a/.github/workflows/check-automated-doc.yml +++ b/.github/workflows/check-automated-doc.yml @@ -36,15 +36,16 @@ jobs: with: egress-policy: audit - - name: Install Go - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 - with: - go-version: ${{ vars.GO_VERSION }} - name: Checkout Code uses: actions/checkout@629c2de402a417ea7690ca6ce3f33229e27606a5 # v2 with: fetch-depth: 0 + - name: Install Go + uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 + with: + go-version-file: 'go.mod' + - name: Verify golang generated documentation is up-to-date run: | make generate-doc diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 246c6418a1..c69888f874 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -56,7 +56,7 @@ jobs: - name: Set up Go uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: - go-version: ${{ vars.GO_VERSION }} + go-version-file: 'go.mod' # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/deploy-fleet-website.yml b/.github/workflows/deploy-fleet-website.yml index 9fc044e13b..371a0014f0 100644 --- a/.github/workflows/deploy-fleet-website.yml +++ b/.github/workflows/deploy-fleet-website.yml @@ -64,7 +64,7 @@ jobs: - name: Set up Go uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: - go-version: ${{ vars.GO_VERSION }} + go-version-file: 'go.mod' # Download top-level dependencies and build Storybook in the website's assets/ folder - run: npm install --legacy-peer-deps && npm run build-storybook -- -o ./website/assets/storybook --loglevel verbose diff --git a/.github/workflows/dogfood-deploy.yml b/.github/workflows/dogfood-deploy.yml index f9d8cff071..f17768eec7 100644 --- a/.github/workflows/dogfood-deploy.yml +++ b/.github/workflows/dogfood-deploy.yml @@ -51,14 +51,17 @@ jobs: - id: fail-on-main run: "false" if: ${{ github.ref == 'main' }} + - uses: aws-actions/configure-aws-credentials@67fbcbb121271f7775d2e7715933280b06314838 # v1.7.0 with: role-to-assume: ${{env.AWS_IAM_ROLE}} aws-region: ${{ env.AWS_REGION }} + - name: Set up Go uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: - go-version: ${{ vars.GO_VERSION }} + go-version-file: 'go.mod' + - uses: hashicorp/setup-terraform@633666f66e0061ca3b725c73b2ec20cd13a8fdd1 # v2.0.3 with: terraform_version: 1.6.3 diff --git a/.github/workflows/fleet-and-orbit.yml b/.github/workflows/fleet-and-orbit.yml index 571d59d067..f4dfb2780e 100644 --- a/.github/workflows/fleet-and-orbit.yml +++ b/.github/workflows/fleet-and-orbit.yml @@ -62,7 +62,6 @@ jobs: timeout-minutes: 60 strategy: matrix: - go-version: ["${{ vars.GO_VERSION }}"] mysql: ["mysql:8.0.36"] runs-on: ubuntu-latest needs: gen @@ -72,10 +71,13 @@ jobs: with: egress-policy: audit + - name: Checkout Code + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - name: Install Go uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: - go-version: ${{ matrix.go-version }} + go-version-file: 'go.mod' # Set the Node.js version - name: Set up Node.js ${{ vars.NODE_VERSION }} @@ -83,9 +85,6 @@ jobs: with: node-version: ${{ vars.NODE_VERSION }} - - name: Checkout Code - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 - - name: Start tunnel env: CERT_PEM: ${{ secrets.CLOUDFLARE_TUNNEL_FLEETUEM_CERT_B64 }} @@ -175,9 +174,6 @@ jobs: # This job also makes sure the Fleet server is up and running. set-enroll-secret: timeout-minutes: 60 - strategy: - matrix: - go-version: ["${{ vars.GO_VERSION }}"] runs-on: ubuntu-latest needs: gen steps: @@ -186,13 +182,13 @@ jobs: with: egress-policy: audit + - name: Checkout Code + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - name: Install Go uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: - go-version: ${{ matrix.go-version }} - - - name: Checkout Code - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + go-version-file: 'go.mod' - name: Build Fleetctl run: make fleetctl @@ -218,9 +214,6 @@ jobs: # Here we generate the Fleet Desktop and osqueryd targets for # macOS which can only be generated from a macOS host. build-macos-targets: - strategy: - matrix: - go-version: ["${{ vars.GO_VERSION }}"] # Set macOS version to '12' (current equivalent to macos-latest) for # building the binary. This ensures compatibility with macOS version 13 and # later, avoiding runtime errors on systems using macOS 13 or newer. @@ -234,13 +227,13 @@ jobs: with: egress-policy: audit + - name: Checkout Code + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - name: Install Go uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: - go-version: ${{ matrix.go-version }} - - - name: Checkout Code - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + go-version-file: 'go.mod' - name: Build desktop.app.tar.gz and osqueryd.app.tar.gz run: | @@ -269,9 +262,6 @@ jobs: # installed, and installing it is time consuming and unreliable. run-tuf-and-gen-pkgs: timeout-minutes: 60 - strategy: - matrix: - go-version: ["${{ vars.GO_VERSION }}"] runs-on: ubuntu-latest needs: [gen, build-macos-targets] steps: @@ -280,13 +270,13 @@ jobs: with: egress-policy: audit + - name: Checkout Code + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - name: Install Go uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: - go-version: ${{ matrix.go-version }} - - - name: Checkout Code - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + go-version-file: 'go.mod' - name: Download macos pre-built apps id: download diff --git a/.github/workflows/fleetctl-preview-latest.yml b/.github/workflows/fleetctl-preview-latest.yml index dda4e0f73c..630cfd1dc3 100644 --- a/.github/workflows/fleetctl-preview-latest.yml +++ b/.github/workflows/fleetctl-preview-latest.yml @@ -53,7 +53,6 @@ jobs: # - Unattended installation of Docker on macOS fails. (see # https://github.com/docker/for-mac/issues/6450) os: [ubuntu-latest] - go-version: ['${{ vars.GO_VERSION }}'] runs-on: ${{ matrix.os }} steps: @@ -62,13 +61,13 @@ jobs: with: egress-policy: audit + - name: Checkout Code + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - name: Install Go uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: - go-version: ${{ matrix.go-version }} - - - name: Checkout Code - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + go-version-file: 'go.mod' - name: Build Fleetctl run: make fleetctl diff --git a/.github/workflows/fleetd-tuf.yml b/.github/workflows/fleetd-tuf.yml index ebeca889da..7641589f10 100644 --- a/.github/workflows/fleetd-tuf.yml +++ b/.github/workflows/fleetd-tuf.yml @@ -30,16 +30,16 @@ jobs: with: egress-policy: audit - - name: Install Go - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 - with: - go-version: ${{ vars.GO_VERSION }} - - name: Checkout Code uses: actions/checkout@629c2de402a417ea7690ca6ce3f33229e27606a5 # v2 with: fetch-depth: 0 + - name: Install Go + uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 + with: + go-version-file: 'go.mod' + - name: Update orbit/TUF.md run: | make fleetd-tuf diff --git a/.github/workflows/generate-desktop-targets.yml b/.github/workflows/generate-desktop-targets.yml index 67313ea762..d7324c9bf0 100644 --- a/.github/workflows/generate-desktop-targets.yml +++ b/.github/workflows/generate-desktop-targets.yml @@ -45,13 +45,13 @@ jobs: with: egress-policy: audit + - name: Checkout + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - name: Install Go uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: - go-version: ${{ vars.GO_VERSION }} - - - name: Checkout - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + go-version-file: 'go.mod' - name: Import signing keys env: @@ -98,13 +98,13 @@ jobs: with: egress-policy: audit + - name: Checkout + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - name: Install Go uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: - go-version: ${{ vars.GO_VERSION }} - - - name: Checkout - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + go-version-file: 'go.mod' - name: Generate fleet-desktop.exe run: | @@ -139,13 +139,13 @@ jobs: with: egress-policy: audit + - name: Checkout + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - name: Install Go uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: - go-version: ${{ vars.GO_VERSION }} - - - name: Checkout - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + go-version-file: 'go.mod' - name: Generate desktop.tar.gz run: | @@ -167,13 +167,13 @@ jobs: with: egress-policy: audit + - name: Checkout + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - name: Install Go uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: - go-version: ${{ vars.GO_VERSION }} - - - name: Checkout - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + go-version-file: 'go.mod' - name: Generate desktop.tar.gz run: | diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index df6b9792b7..3d3e95ed2c 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -38,7 +38,6 @@ jobs: matrix: # See #9943, we just need to add windows-latest here once all issues are fixed. os: [ubuntu-latest, macos-latest] - go-version: ['${{ vars.GO_VERSION }}'] runs-on: ${{ matrix.os }} steps: - name: Harden Runner @@ -52,7 +51,7 @@ jobs: - name: Install Go uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: - go-version: ${{ matrix.go-version }} + go-version-file: 'go.mod' - name: Install dependencies (Linux) if: matrix.os == 'ubuntu-latest' diff --git a/.github/workflows/goreleaser-fleet.yaml b/.github/workflows/goreleaser-fleet.yaml index f4224907e0..6ba9aff8f0 100644 --- a/.github/workflows/goreleaser-fleet.yaml +++ b/.github/workflows/goreleaser-fleet.yaml @@ -44,7 +44,7 @@ jobs: - name: Set up Go uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: - go-version: ${{ vars.GO_VERSION }} + go-version-file: 'go.mod' # Set the Node.js version - name: Set up Node.js ${{ vars.NODE_VERSION }} diff --git a/.github/workflows/goreleaser-orbit.yaml b/.github/workflows/goreleaser-orbit.yaml index 666f281120..54e16752b3 100644 --- a/.github/workflows/goreleaser-orbit.yaml +++ b/.github/workflows/goreleaser-orbit.yaml @@ -56,7 +56,7 @@ jobs: - name: Set up Go uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: - go-version: ${{ vars.GO_VERSION }} + go-version-file: 'go.mod' - name: Run GoReleaser run: go run github.com/goreleaser/goreleaser@56c9d09a1b925e2549631c6d180b0a1c2ebfac82 release --debug --rm-dist --skip-publish -f orbit/goreleaser-macos.yml # v1.20.0 @@ -95,7 +95,7 @@ jobs: - name: Set up Go uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: - go-version: ${{ vars.GO_VERSION }} + go-version-file: 'go.mod' - name: Run GoReleaser run: go run github.com/goreleaser/goreleaser@56c9d09a1b925e2549631c6d180b0a1c2ebfac82 release --debug --rm-dist --skip-publish -f orbit/goreleaser-linux.yml # v1.20.0 @@ -128,7 +128,7 @@ jobs: - name: Set up Go uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: - go-version: ${{ vars.GO_VERSION }} + go-version-file: 'go.mod' - name: Run GoReleaser run: go run github.com/goreleaser/goreleaser@56c9d09a1b925e2549631c6d180b0a1c2ebfac82 release --debug --rm-dist --skip-publish -f orbit/goreleaser-linux-arm64.yml # v1.20.0 @@ -161,7 +161,7 @@ jobs: - name: Set up Go uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: - go-version: ${{ vars.GO_VERSION }} + go-version-file: 'go.mod' - name: Run GoReleaser run: go run github.com/goreleaser/goreleaser@56c9d09a1b925e2549631c6d180b0a1c2ebfac82 release --debug --rm-dist --skip-publish -f orbit/goreleaser-windows.yml # v1.20.0 diff --git a/.github/workflows/goreleaser-snapshot-fleet.yaml b/.github/workflows/goreleaser-snapshot-fleet.yaml index 46c1da4193..927cf31be1 100644 --- a/.github/workflows/goreleaser-snapshot-fleet.yaml +++ b/.github/workflows/goreleaser-snapshot-fleet.yaml @@ -57,7 +57,7 @@ jobs: - name: Set up Go uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: - go-version: ${{ vars.GO_VERSION }} + go-version-file: 'go.mod' # Set the Node.js version - name: Set up Node.js ${{ vars.NODE_VERSION }} diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 015a464b4b..98c9cd3a59 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -264,13 +264,13 @@ jobs: npm install -g fleetctl fleetctl config set --address ${{ needs.gen.outputs.address }} --token ${{ needs.login.outputs.token }} + - name: Checkout Code + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - name: Install Go uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: - go-version: ${{ vars.GO_VERSION }} - - - name: Checkout Code - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + go-version-file: 'go.mod' - name: Build Fleetctl run: make fleetctl diff --git a/.github/workflows/release-fleetctl-docker-deps.yaml b/.github/workflows/release-fleetctl-docker-deps.yaml index 8fc698f6ac..c751655d93 100644 --- a/.github/workflows/release-fleetctl-docker-deps.yaml +++ b/.github/workflows/release-fleetctl-docker-deps.yaml @@ -36,13 +36,13 @@ jobs: with: egress-policy: audit + - name: Checkout Code + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - name: Install Go uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: - go-version: ${{ vars.GO_VERSION }} - - - name: Checkout Code - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + go-version-file: 'go.mod' - name: Login to Docker Hub uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a diff --git a/.github/workflows/release-fleetd-base.yml b/.github/workflows/release-fleetd-base.yml index d7b02cfcf7..9909901964 100644 --- a/.github/workflows/release-fleetd-base.yml +++ b/.github/workflows/release-fleetd-base.yml @@ -51,16 +51,16 @@ jobs: with: egress-policy: audit - - name: Install Go - uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 - with: - go-version: ${{ vars.GO_VERSION }} - - name: Checkout Code uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: fetch-depth: 0 + - name: Install Go + uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 + with: + go-version-file: 'go.mod' + - name: Check for fleetd component updates id: check-for-fleetd-component-updates run: | diff --git a/.github/workflows/test-db-changes.yml b/.github/workflows/test-db-changes.yml index ecfe464072..a5b7dd91e3 100644 --- a/.github/workflows/test-db-changes.yml +++ b/.github/workflows/test-db-changes.yml @@ -35,15 +35,16 @@ jobs: with: egress-policy: audit - - name: Install Go - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 - with: - go-version: ${{ vars.GO_VERSION }} - name: Checkout Code uses: actions/checkout@629c2de402a417ea7690ca6ce3f33229e27606a5 # v2 with: fetch-depth: 0 + - name: Install Go + uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 + with: + go-version-file: 'go.mod' + - name: Start Infra Dependencies # Use & to background this run: docker compose up -d mysql_test & diff --git a/.github/workflows/test-fleetd-chrome.yml b/.github/workflows/test-fleetd-chrome.yml index 47ba496ebb..8cbb0125f9 100644 --- a/.github/workflows/test-fleetd-chrome.yml +++ b/.github/workflows/test-fleetd-chrome.yml @@ -66,7 +66,8 @@ jobs: npm test - name: Upload to Codecov - uses: codecov/codecov-action@d9f34f8cd5cb3b3eb79b3e4b5dae3a16df499a70 # v3.1.1 + uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4.5.0 with: + token: ${{ secrets.CODECOV_TOKEN }} directory: ./ee/fleetd-chrome/coverage flags: fleetd-chrome diff --git a/.github/workflows/test-go.yaml b/.github/workflows/test-go.yaml index 9feba50d8e..b5f2b8fe94 100644 --- a/.github/workflows/test-go.yaml +++ b/.github/workflows/test-go.yaml @@ -44,7 +44,6 @@ jobs: matrix: suite: ["integration", "core"] os: [ubuntu-latest] - go-version: ['${{ vars.GO_VERSION }}'] mysql: ["mysql:8.0.36", "mysql:8.4.2"] continue-on-error: ${{ matrix.suite == 'integration' }} # Since integration tests have a higher chance of failing, often for unrelated reasons, we don't want to fail the whole job if they fail runs-on: ${{ matrix.os }} @@ -65,7 +64,7 @@ jobs: - name: Install Go uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 with: - go-version: ${{ matrix.go-version }} + go-version-file: 'go.mod' # Pre-starting dependencies here means they are ready to go when we need them. - name: Start Infra Dependencies @@ -131,13 +130,17 @@ jobs: NETWORK_TEST_GITHUB_TOKEN=${{ secrets.FLEET_RELEASE_GITHUB_PAT }} \ make test-go 2>&1 | tee /tmp/gotest.log - # note: it's fine to upload multiple reports (one per matrix combination) - # for the same run, see https://docs.codecov.com/docs/merging-reports - - name: Upload to Codecov - uses: codecov/codecov-action@d9f34f8cd5cb3b3eb79b3e4b5dae3a16df499a70 + - name: Create mysql identifier without colon + if: always() + run: | + echo "MATRIX_MYSQL_ID=$(echo ${{ matrix.mysql }} | tr -d ':')" >> $GITHUB_ENV + + - name: Save coverage + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: - files: coverage.txt - flags: backend + name: ${{ matrix.suite }}-${{ env.MATRIX_MYSQL_ID }}-coverage + path: ./coverage.txt + if-no-files-found: error - name: Generate summary of errors if: failure() @@ -167,14 +170,9 @@ jobs: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_G_HELP_ENGINEERING_WEBHOOK_URL }} SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK - - name: Create mysql identifier without colon - if: always() - run: | - echo "MATRIX_MYSQL_ID=$(echo ${{ matrix.mysql }} | tr -d ':')" >> $GITHUB_ENV - - name: Upload test log if: always() - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v2 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: ${{ matrix.suite }}-${{ env.MATRIX_MYSQL_ID }}-test-log path: /tmp/gotest.log @@ -182,7 +180,24 @@ jobs: - name: Upload summary test log if: always() - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v2 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: ${{ matrix.suite }}-${{ env.MATRIX_MYSQL_ID }}-summary-test-log path: /tmp/summary.txt + + # We upload all backend coverage in one step so that we're less like to end up in a situation with a partial coverage report. + upload-coverage: + needs: [test-go] + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - name: Download artifacts + uses: actions/download-artifact@9c19ed7fe5d278cd354c7dfd5d3b88589c7e2395 # v4.1.6 + with: + pattern: '*-coverage' + - name: Upload to Codecov + uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4.5.0 + with: + token: ${{ secrets.CODECOV_TOKEN }} + flags: backend diff --git a/.github/workflows/test-js.yml b/.github/workflows/test-js.yml index 9d63523737..15b4fd05ce 100644 --- a/.github/workflows/test-js.yml +++ b/.github/workflows/test-js.yml @@ -69,8 +69,9 @@ jobs: yarn test:ci - name: Upload to Codecov - uses: codecov/codecov-action@d9f34f8cd5cb3b3eb79b3e4b5dae3a16df499a70 + uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4.5.0 with: + token: ${{ secrets.CODECOV_TOKEN }} flags: frontend lint-js: diff --git a/.github/workflows/test-native-tooling-packaging.yml b/.github/workflows/test-native-tooling-packaging.yml index 7678e7eeaa..ff0dc4abad 100644 --- a/.github/workflows/test-native-tooling-packaging.yml +++ b/.github/workflows/test-native-tooling-packaging.yml @@ -41,7 +41,6 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest] - go-version: ['${{ vars.GO_VERSION }}'] runs-on: ${{ matrix.os }} steps: @@ -50,13 +49,13 @@ jobs: with: egress-policy: audit + - name: Checkout Code + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - name: Install Go uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: - go-version: ${{ matrix.go-version }} - - - name: Checkout Code - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + go-version-file: 'go.mod' - name: Install Go Dependencies run: make deps-go diff --git a/.github/workflows/test-packaging.yml b/.github/workflows/test-packaging.yml index f9643bd4e9..dbe5a96244 100644 --- a/.github/workflows/test-packaging.yml +++ b/.github/workflows/test-packaging.yml @@ -47,7 +47,6 @@ jobs: # `macos-latest` uses arm64 by default now, so please be careful when # updating this version. os: [ubuntu-latest, macos-13] - go-version: ['${{ vars.GO_VERSION }}'] runs-on: ${{ matrix.os }} steps: @@ -83,13 +82,13 @@ jobs: brew install colima colima start --mount $TMPDIR:w + - name: Checkout Code + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - name: Install Go uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: - go-version: ${{ matrix.go-version }} - - - name: Checkout Code - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + go-version-file: 'go.mod' - name: Install wine and wix if: startsWith(matrix.os, 'macos') diff --git a/.github/workflows/test-yml-specs.yml b/.github/workflows/test-yml-specs.yml index 75e46d6af0..fe8f3ecace 100644 --- a/.github/workflows/test-yml-specs.yml +++ b/.github/workflows/test-yml-specs.yml @@ -33,7 +33,6 @@ jobs: strategy: matrix: os: [ubuntu-latest] - go-version: ['${{ vars.GO_VERSION }}'] runs-on: ${{ matrix.os }} steps: @@ -42,13 +41,13 @@ jobs: with: egress-policy: audit + - name: Checkout Code + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - name: Install Go uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: - go-version: ${{ matrix.go-version }} - - - name: Checkout Code - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + go-version-file: 'go.mod' - name: Run apply spec tests run: | diff --git a/articles/mdm-migration.md b/articles/mdm-migration.md index 76c6254125..5eb9e8473d 100644 --- a/articles/mdm-migration.md +++ b/articles/mdm-migration.md @@ -1,11 +1,15 @@ # MDM migration -This section provides instructions for migrating your hosts away from your old MDM solution to Fleet. +This guide provides instructions for migrating devices from your current MDM solution to Fleet. + +> For seamless MDM migration, [view this guide](https://fleetdm.com/guides/seamless-mdm-migration). ## Requirements -1. A [deployed Fleet instance](https://fleetdm.com/docs/deploy/deploy-fleet) -2. [Fleet connected to Apple](https://fleetdm.com/guides/macos-mdm-setup) + +- A [deployed Fleet instance](https://fleetdm.com/docs/deploy/deploy-fleet) +- Fleet is connected to Apple Push Notification service (APNs) and Apple Business Manager (ABM). [See macOS MDM setup](https://fleetdm.com/guides/macos-mdm-setup) + ## Migrate manually enrolled hosts diff --git a/changes/19551-policy-software-automations b/changes/19551-policy-software-automations new file mode 100644 index 0000000000..4b88cb4c1f --- /dev/null +++ b/changes/19551-policy-software-automations @@ -0,0 +1 @@ +* Implement features allowing automatic installation of software on hosts that fail policies. diff --git a/changes/21315-vpp-premium-license b/changes/21315-vpp-premium-license new file mode 100644 index 0000000000..2fd081703e --- /dev/null +++ b/changes/21315-vpp-premium-license @@ -0,0 +1 @@ +- Verify user has premium license before uploading VPP tokens diff --git a/changes/21757-fix-scheduling-cron-jobs-at-startup b/changes/21757-fix-scheduling-cron-jobs-at-startup new file mode 100644 index 0000000000..b54ae2c84f --- /dev/null +++ b/changes/21757-fix-scheduling-cron-jobs-at-startup @@ -0,0 +1 @@ +* Fixed an issue with the scheduling of cron jobs at startup if the job has never run, which caused it to be delayed. diff --git a/changes/apns-errors b/changes/apns-errors new file mode 100644 index 0000000000..6de48617a1 --- /dev/null +++ b/changes/apns-errors @@ -0,0 +1 @@ +* Fixed logic to properly catch and log APNs errors. diff --git a/cmd/fleet/serve.go b/cmd/fleet/serve.go index 67ce51f7d0..6c363965a8 100644 --- a/cmd/fleet/serve.go +++ b/cmd/fleet/serve.go @@ -633,6 +633,12 @@ the way that the Fleet server works. appCfg.MDM.AppleBMEnabledAndConfigured = count > 0 } } + if appCfg.MDM.EnabledAndConfigured { + level.Info(logger).Log("msg", "Apple MDM enabled") + } + if appCfg.MDM.AppleBMEnabledAndConfigured { + level.Info(logger).Log("msg", "Apple Business Manager enabled") + } // register the Microsoft MDM services var ( diff --git a/docs/REST API/rest-api.md b/docs/REST API/rest-api.md index 607ac2f28d..c4ad4e3858 100644 --- a/docs/REST API/rest-api.md +++ b/docs/REST API/rest-api.md @@ -1090,7 +1090,7 @@ Modifies the Fleet's configuration with the supplied information. | integrations | object | body | Includes `jira`, `zendesk`, and `google_calendar` arrays. See [integrations](#integrations) for details. | | mdm | object | body | See [mdm](#mdm). | | features | object | body | See [features](#features). | -| scripts | list | body | A list of script files to add so they can be executed at a later time. | +| scripts | array | body | A list of script files to add so they can be executed at a later time. | | force | boolean | query | Whether to force-apply the agent options even if there are validation errors. | | dry_run | boolean | query | Whether to validate the configuration and return any validation errors **without** applying changes. | @@ -1518,10 +1518,10 @@ _Available in Fleet Premium._ | Name | Type | Description | | --------------------- | ----- | ---------------------------------------------------------------------------------------------- | -| host_status_webhook | list | See [`webhook_settings.host_status_webhook`](#webhook-settings-host-status-webhook). | -| failing_policies_webhook | list | See [`webhook_settings.failing_policies_webhook`](#webhook-settings-failing-policies-webhook). | -| vulnerabilities_webhook | list | See [`webhook_settings.vulnerabilities_webhook`](#webhook-settings-vulnerabilities-webhook). | -| activities_webhook | list | See [`webhook_settings.activities_webhook`](#webhook-settings-activities-webhook). | +| host_status_webhook | array | See [`webhook_settings.host_status_webhook`](#webhook-settings-host-status-webhook). | +| failing_policies_webhook | array | See [`webhook_settings.failing_policies_webhook`](#webhook-settings-failing-policies-webhook). | +| vulnerabilities_webhook | array | See [`webhook_settings.vulnerabilities_webhook`](#webhook-settings-vulnerabilities-webhook). | +| activities_webhook | array | See [`webhook_settings.activities_webhook`](#webhook-settings-activities-webhook). |
@@ -1614,9 +1614,9 @@ _Available in Fleet Premium._ | Name | Type | Description | | --------------------- | ----- | -------------------------------------------------------------------- | -| jira | list | See [`integrations.jira`](#integrations-jira). | -| zendesk | list | See [`integrations.zendesk`](#integrations-zendesk). | -| google_calendar | list | See [`integrations.google_calendar`](#integrations-google-calendar). | +| jira | array | See [`integrations.jira`](#integrations-jira). | +| zendesk | array | See [`integrations.zendesk`](#integrations-zendesk). | +| google_calendar | array | See [`integrations.google_calendar`](#integrations-google-calendar). | > Note that when making changes to the `integrations` object, all integrations must be provided (not just the one being modified). This is because the endpoint will consider missing integrations as deleted. @@ -1792,7 +1792,7 @@ _Available in Fleet Premium._ | Name | Type | Description | | --------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| custom_settings | list | macOS hosts that belong to no team will have custom profiles applied. | +| custom_settings | array | macOS hosts that belong to no team will have custom profiles applied. |
@@ -1802,7 +1802,7 @@ _Available in Fleet Premium._ | Name | Type | Description | | --------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| custom_settings | list | Windows hosts that belong to no team will have custom profiles applied. | +| custom_settings | array | Windows hosts that belong to no team will have custom profiles applied. |
@@ -2099,7 +2099,7 @@ Delete all of a team's existing enroll secrets | email | string | body | **Required.** The email of the invited user. This email will receive the invitation link. | | name | string | body | **Required.** The name of the invited user. | | sso_enabled | boolean | body | **Required.** Whether or not SSO will be enabled for the invited user. | -| teams | list | body | _Available in Fleet Premium_. A list of the teams the user is a member of. Each item includes the team's ID and the user's role in the specified team. | +| teams | array | body | _Available in Fleet Premium_. A list of the teams the user is a member of. Each item includes the team's ID and the user's role in the specified team. | #### Example @@ -2299,7 +2299,7 @@ Verify the specified invite. | email | string | body | The email of the invited user. Updates on the email won't resend the invitation. | | name | string | body | The name of the invited user. | | sso_enabled | boolean | body | Whether or not SSO will be enabled for the invited user. | -| teams | list | body | _Available in Fleet Premium_. A list of the teams the user is a member of. Each item includes the team's ID and the user's role in the specified team. | +| teams | array | body | _Available in Fleet Premium_. A list of the teams the user is a member of. Each item includes the team's ID and the user's role in the specified team. | #### Example @@ -2489,6 +2489,7 @@ the `software` table. | mdm_id | integer | query | The ID of the _mobile device management_ (MDM) solution to filter hosts by (that is, filter hosts that use a specific MDM provider and URL). | | mdm_name | string | query | The name of the _mobile device management_ (MDM) solution to filter hosts by (that is, filter hosts that use a specific MDM provider). | | mdm_enrollment_status | string | query | The _mobile device management_ (MDM) enrollment status to filter hosts by. Valid options are 'manual', 'automatic', 'enrolled', 'pending', or 'unenrolled'. | +| connected_to_fleet | boolean | query | Filter hosts that are talking to this Fleet server for MDM features. In rare cases, hosts can be enrolled to one Fleet server but talk to a different Fleet server for MDM features. In this case, the value would be `false`. Always `false` for Linux hosts. | | macos_settings | string | query | Filters the hosts by the status of the _mobile device management_ (MDM) profiles applied to hosts. Valid options are 'verified', 'verifying', 'pending', or 'failed'. **Note: If this filter is used in Fleet Premium without a team ID filter, the results include only hosts that are not assigned to any team.** | | munki_issue_id | integer | query | The ID of the _munki issue_ (a Munki-reported error or warning message) to filter hosts by (that is, filter hosts that are affected by that corresponding error or warning message). | | low_disk_space | integer | query | _Available in Fleet Premium_. Filters the hosts to only include hosts with less GB of disk space available than this value. Must be a number between 1-100. | @@ -3099,10 +3100,11 @@ Returns the information of the specified host. "timezone": "America/New_York" }, "mdm": { - "encryption_key_available": false, - "enrollment_status": null, - "name": "", - "server_url": null, + "encryption_key_available": true, + "enrollment_status": "On (manual)", + "name": "Fleet", + "connected_to_fleet": true, + "server_url": "https://acme.com/mdm/apple/mdm", "device_status": "unlocked", "pending_action": "", "macos_settings": { @@ -3518,10 +3520,11 @@ This is the API route used by the **My device** page in Fleet desktop to display } ], "mdm": { - "encryption_key_available": false, - "enrollment_status": null, - "name": "", - "server_url": null, + "encryption_key_available": true, + "enrollment_status": "On (manual)", + "name": "Fleet", + "connected_to_fleet": true, + "server_url": "https://acme.com/mdm/apple/mdm", "macos_settings": { "disk_encryption": null, "action_required": null @@ -3696,7 +3699,7 @@ _Available in Fleet Premium_ | Name | Type | In | Description | | ------- | ------- | ---- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| ids | list | body | A list of the host IDs you'd like to delete. If `ids` is specified, `filters` cannot be specified. | +| ids | array | body | A list of the host IDs you'd like to delete. If `ids` is specified, `filters` cannot be specified. | | filters | object | body | Contains any of the following four properties: `query` for search query keywords. Searchable fields include `hostname`, `hardware_serial`, `uuid`, and `ipv4`. `status` to indicate the status of the hosts to return. Can either be `new`, `online`, `offline`, `mia` or `missing`. `label_id` to indicate the selected label. `team_id` to indicate the selected team. If `filters` is specified, `id` cannot be specified. `label_id` and `status` cannot be used at the same time. | Either ids or filters are required. @@ -4663,9 +4666,9 @@ Adds manual labels to a host. #### Parameters -| Name | Type | In | Description | -| ---- | ------- | ---- | ---------------------------- | -| labels | list | body | The list of label names to add to the host. | +| Name | Type | In | Description | +| ------ | ------- | ---- | ---------------------------- | +| labels | array | body | The list of label names to add to the host. | #### Example @@ -4692,9 +4695,9 @@ Removes manual labels from a host. #### Parameters -| Name | Type | In | Description | -| ---- | ------- | ---- | ---------------------------- | -| labels | list | body | The list of label names to delete from the host. | +| Name | Type | In | Description | +| ------ | ------- | ---- | ---------------------------- | +| labels | array | body | The list of label names to delete from the host. | #### Example @@ -5424,7 +5427,6 @@ List all configuration profiles for macOS and Windows hosts enrolled to Fleet's { "name": "Label name 2", "broken": true, - "id": 2 }, { "name": "Label name 3", @@ -6559,7 +6561,7 @@ For example, a policy might ask “Is Gatekeeper enabled on macOS devices?“ Th | Name | Type | In | Description | | -------- | ------- | ---- | ------------------------------------------------- | -| ids | list | body | **Required.** The IDs of the policies to delete. | +| ids | array | body | **Required.** The IDs of the policies to delete. | #### Example @@ -6653,8 +6655,8 @@ Triggers [automations](https://fleetdm.com/docs/using-fleet/automations#policy-a | Name | Type | In | Description | | ---------- | -------- | ---- | -------------------------------------------------------- | -| policy_ids | list | body | Filters to only run policy automations for the specified policies. | -| team_ids | list | body | _Available in Fleet Premium_. Filters to only run policy automations for hosts in the specified teams. | +| policy_ids | array | body | Filters to only run policy automations for the specified policies. | +| team_ids | array | body | _Available in Fleet Premium_. Filters to only run policy automations for hosts in the specified teams. | #### Example @@ -6994,7 +6996,7 @@ Either `query` or `query_id` must be provided. | Name | Type | In | Description | | -------- | ------- | ---- | ------------------------------------------------- | | team_id | integer | path | **Required.** Defines what team ID to operate on | -| ids | list | body | **Required.** The IDs of the policies to delete. | +| ids | array | body | **Required.** The IDs of the policies to delete. | #### Example @@ -7431,14 +7433,14 @@ Creates a global query or team query. | name | string | body | **Required**. The name of the query. | | query | string | body | **Required**. The query in SQL syntax. | | description | string | body | The query's description. | -| observer_can_run | bool | body | Whether or not users with the `observer` role can run the query. In Fleet 4.0.0, 3 user roles were introduced (`admin`, `maintainer`, and `observer`). This field is only relevant for the `observer` role. The `observer_plus` role can run any query and is not limited by this flag (`observer_plus` role was added in Fleet 4.30.0). | +| observer_can_run | boolean | body | Whether or not users with the `observer` role can run the query. In Fleet 4.0.0, 3 user roles were introduced (`admin`, `maintainer`, and `observer`). This field is only relevant for the `observer` role. The `observer_plus` role can run any query and is not limited by this flag (`observer_plus` role was added in Fleet 4.30.0). | | team_id | integer | body | _Available in Fleet Premium_. The parent team to which the new query should be added. If omitted, the query will be global. | -| interval | integer | body | The amount of time, in seconds, the query waits before running. Can be set to `0` to never run. Default: 0. | +| interval | integer | body | The amount of time, in seconds, the query waits before running. Can be set to `0` to never run. Default: 0. | | platform | string | body | The OS platforms where this query will run (other platforms ignored). Comma-separated string. If omitted, runs on all compatible platforms. | | min_osquery_version | string | body | The minimum required osqueryd version installed on a host. If omitted, all osqueryd versions are acceptable. | | automations_enabled | boolean | body | Whether to send data to the configured log destination according to the query's `interval`. | -| logging | string | body | The type of log output for this query. Valid values: `"snapshot"`(default), `"differential"`, or `"differential_ignore_removals"`. | -| discard_data | bool | body | Whether to skip saving the latest query results for each host. Default: `false`. | +| logging | string | body | The type of log output for this query. Valid values: `"snapshot"`(default), `"differential"`, or `"differential_ignore_removals"`. | +| discard_data | boolean | body | Whether to skip saving the latest query results for each host. Default: `false`. | #### Example @@ -7505,13 +7507,13 @@ Modifies the query specified by ID. | name | string | body | The name of the query. | | query | string | body | The query in SQL syntax. | | description | string | body | The query's description. | -| observer_can_run | bool | body | Whether or not users with the `observer` role can run the query. In Fleet 4.0.0, 3 user roles were introduced (`admin`, `maintainer`, and `observer`). This field is only relevant for the `observer` role. The `observer_plus` role can run any query and is not limited by this flag (`observer_plus` role was added in Fleet 4.30.0). | +| observer_can_run | boolean | body | Whether or not users with the `observer` role can run the query. In Fleet 4.0.0, 3 user roles were introduced (`admin`, `maintainer`, and `observer`). This field is only relevant for the `observer` role. The `observer_plus` role can run any query and is not limited by this flag (`observer_plus` role was added in Fleet 4.30.0). | | interval | integer | body | The amount of time, in seconds, the query waits before running. Can be set to `0` to never run. Default: 0. | | platform | string | body | The OS platforms where this query will run (other platforms ignored). Comma-separated string. If set to "", runs on all compatible platforms. | | min_osquery_version | string | body | The minimum required osqueryd version installed on a host. If omitted, all osqueryd versions are acceptable. | | automations_enabled | boolean | body | Whether to send data to the configured log destination according to the query's `interval`. | | logging | string | body | The type of log output for this query. Valid values: `"snapshot"`(default), `"differential"`, or `"differential_ignore_removals"`. | -| discard_data | bool | body | Whether to skip saving the latest query results for each host. | +| discard_data | boolean | body | Whether to skip saving the latest query results for each host. | > Note that any of the following conditions will cause the existing query report to be deleted: > - Updating the `query` (SQL) field @@ -7615,9 +7617,9 @@ Deletes the queries specified by ID. Returns the count of queries successfully d #### Parameters -| Name | Type | In | Description | -| ---- | ---- | ---- | ------------------------------------- | -| ids | list | body | **Required.** The IDs of the queries. | +| Name | Type | In | Description | +| ---- | ----- | ---- | ------------------------------------- | +| ids | array | body | **Required.** The IDs of the queries. | #### Example @@ -8506,9 +8508,9 @@ Get a list of all software. | order_direction | string | query | **Requires `order_key`**. The direction of the order given the order key. Options include `asc` and `desc`. Default is `asc`. | | query | string | query | Search query keywords. Searchable fields include `title` and `cve`. | | team_id | integer | query | _Available in Fleet Premium_. Filters the software to only include the software installed on the hosts that are assigned to the specified team. Use `0` to filter by hosts assigned to "No team". | -| vulnerable | bool | query | If true or 1, only list software that has detected vulnerabilities. Default is `false`. | -| available_for_install | bool | query | If `true` or `1`, only list software that is available for install (added by the user). Default is `false`. | -| self_service | bool | query | If `true` or `1`, only lists self-service software. Default is `false`. | +| vulnerable | boolean | query | If true or 1, only list software that has detected vulnerabilities. Default is `false`. | +| available_for_install | boolean | query | If `true` or `1`, only list software that is available for install (added by the user). Default is `false`. | +| self_service | boolean | query | If `true` or `1`, only lists self-service software. Default is `false`. | #### Example @@ -8627,7 +8629,7 @@ Get a list of all software versions. | order_direction | string | query | **Requires `order_key`**. The direction of the order given the order key. Options include `asc` and `desc`. Default is `asc`. | | query | string | query | Search query keywords. Searchable fields include `name`, `version`, and `cve`. | | team_id | integer | query | _Available in Fleet Premium_. Filters the software to only include the software installed on the hosts that are assigned to the specified team. Use `0` to filter by hosts assigned to "No team". | -| vulnerable | bool | query | If true or 1, only list software that has detected vulnerabilities. Default is `false`. | +| vulnerable | boolean | query | If true or 1, only list software that has detected vulnerabilities. Default is `false`. | #### Example @@ -8990,6 +8992,8 @@ OS vulnerability data is currently available for Windows and macOS. For other pl ### Add package +> **Experimental feature**. This feature is undergoing rapid improvement, which may result in breaking changes to the API or configuration surface. It is not recommended for use in automated workflows. + _Available in Fleet Premium._ Add a package (.pkg, .msi, .exe, .deb) to install on macOS, Windows, or Linux (Ubuntu) hosts. @@ -9136,6 +9140,8 @@ Add App Store (VPP) app purchased in Apple Business Manager. ### Download package +> **Experimental feature**. This feature is undergoing rapid improvement, which may result in breaking changes to the API or configuration surface. It is not recommended for use in automated workflows. + _Available in Fleet Premium._ `GET /api/v1/fleet/software/titles/:software_title_id/package?alt=media` @@ -9191,6 +9197,8 @@ Install software (package or App Store app) on a macOS, iOS, iPadOS, Windows, or ### Get package install result +> **Experimental feature**. This feature is undergoing rapid improvement, which may result in breaking changes to the API or configuration surface. It is not recommended for use in automated workflows. + _Available in Fleet Premium._ `GET /api/v1/fleet/software/install/results/:install_uuid` @@ -9828,8 +9836,8 @@ _Available in Fleet Premium_ | ------------------------------------------------------- | ------- | ---- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | id | integer | path | **Required.** The desired team's ID. | | name | string | body | The team's name. | -| host_ids | list | body | A list of hosts that belong to the team. | -| user_ids | list | body | A list of users on the team. | +| host_ids | array | body | A list of hosts that belong to the team. | +| user_ids | array | body | A list of users on the team. | | webhook_settings | object | body | Webhook settings contains for the team. | |   failing_policies_webhook | object | body | Failing policies webhook settings. | |     enable_failing_policies_webhook | boolean | body | Whether or not the failing policies webhook is enabled. | @@ -9864,10 +9872,10 @@ _Available in Fleet Premium_ |     deadline_days | integer | body | Hosts that belong to this team and are enrolled into Fleet's MDM will have this number of days before updates are installed on Windows. | |     grace_period_days | integer | body | Hosts that belong to this team and are enrolled into Fleet's MDM will have this number of days before Windows restarts to install updates. | |   macos_settings | object | body | macOS-specific settings. | -|     custom_settings | list | body | The list of objects where each object includes .mobileconfig or JSON file (configuration profile) and label name to apply to macOS hosts that belong to this team and are members of the specified label. | +|     custom_settings | array | body | The list of objects where each object includes .mobileconfig or JSON file (configuration profile) and label name to apply to macOS hosts that belong to this team and are members of the specified label. | |     enable_disk_encryption | boolean | body | Hosts that belong to this team and are enrolled into Fleet's MDM will have disk encryption enabled if set to true. | |   windows_settings | object | body | Windows-specific settings. | -|     custom_settings | list | body | The list of objects where each object includes XML file (configuration profile) and label name to apply to Windows hosts that belong to this team and are members of the specified label. | +|     custom_settings | array | body | The list of objects where each object includes XML file (configuration profile) and label name to apply to Windows hosts that belong to this team and are members of the specified label. | |   macos_setup | object | body | Setup for automatic MDM enrollment of macOS hosts. | |     enable_end_user_authentication | boolean | body | If set to true, end user authentication will be required during automatic MDM enrollment of new macOS hosts. Settings for your IdP provider must also be [configured](https://fleetdm.com/docs/using-fleet/mdm-macos-setup-experience#end-user-authentication-and-eula). | | integrations | object | body | Integration settings for this team. | @@ -10087,8 +10095,8 @@ _Available in Fleet Premium_ | Name | Type | In | Description | | --- | --- | --- | --- | | id | integer | path | **Required.** The desired team's ID. | -| force | bool | query | Force apply the options even if there are validation errors. | -| dry_run | bool | query | Validate the options and return any validation errors, but do not apply the changes. | +| force | boolean | query | Force apply the options even if there are validation errors. | +| dry_run | boolean | query | Validate the options and return any validation errors, but do not apply the changes. | | _JSON data_ | object | body | The JSON to use as agent options for this team. See [Agent options](https://fleetdm.com/docs/using-fleet/configuration-files#agent-options) for details. | #### Example @@ -10199,9 +10207,9 @@ Transforms a host name into a host id. For example, the Fleet UI use this endpoi #### Parameters -| Name | Type | In | Description | -| ---- | ----- | ---- | ---------------------------------------- | -| list | array | body | **Required** list of items to translate. | +| Name | Type | In | Description | +| ----- | ----- | ---- | ---------------------------------------- | +| array | array | body | **Required** list of items to translate. | #### Example diff --git a/ee/server/service/vpp.go b/ee/server/service/vpp.go index 52f4c1f890..85c29652ca 100644 --- a/ee/server/service/vpp.go +++ b/ee/server/service/vpp.go @@ -5,6 +5,7 @@ import ( "database/sql" "errors" "fmt" + "io" "net/http" "sort" "strings" @@ -17,6 +18,9 @@ import ( "github.com/fleetdm/fleet/v4/server/mdm/apple/vpp" ) +// Used for overriding the env var value in testing +var testSetEmptyPrivateKey bool + // getVPPToken returns the base64 encoded VPP token, ready for use in requests to Apple's VPP API. // It returns an error if the token is expired. func (svc *Service) getVPPToken(ctx context.Context, teamID *uint) (string, error) { @@ -413,3 +417,143 @@ func getVPPAppsMetadata(ctx context.Context, ids []fleet.VPPAppTeam) ([]*fleet.V return apps, nil } + +func (svc *Service) UploadVPPToken(ctx context.Context, token io.ReadSeeker) (*fleet.VPPTokenDB, error) { + if err := svc.authz.Authorize(ctx, &fleet.AppleCSR{}, fleet.ActionWrite); err != nil { + return nil, err + } + + privateKey := svc.config.Server.PrivateKey + if testSetEmptyPrivateKey { + privateKey = "" + } + + if len(privateKey) == 0 { + return nil, ctxerr.New(ctx, "Couldn't upload content token. Missing required private key. Learn how to configure the private key here: https://fleetdm.com/learn-more-about/fleet-server-private-key") + } + + if token == nil { + return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("token", "Invalid token. Please provide a valid content token from Apple Business Manager.")) + } + + tokenBytes, err := io.ReadAll(token) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "reading VPP token") + } + + locName, err := vpp.GetConfig(string(tokenBytes)) + if err != nil { + var vppErr *vpp.ErrorResponse + if errors.As(err, &vppErr) { + // Per https://developer.apple.com/documentation/devicemanagement/app_and_book_management/app_and_book_management_legacy/interpreting_error_codes + if vppErr.ErrorNumber == 9622 { + return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("token", "Invalid token. Please provide a valid content token from Apple Business Manager.")) + } + } + return nil, ctxerr.Wrap(ctx, err, "validating VPP token with Apple") + } + + data := fleet.VPPTokenData{ + Token: string(tokenBytes), + Location: locName, + } + + tok, err := svc.ds.InsertVPPToken(ctx, &data) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "writing VPP token to db") + } + + if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), fleet.ActivityEnabledVPP{ + Location: locName, + }); err != nil { + return nil, ctxerr.Wrap(ctx, err, "create activity for upload VPP token") + } + + return tok, nil +} + +func (svc *Service) UpdateVPPToken(ctx context.Context, tokenID uint, token io.ReadSeeker) (*fleet.VPPTokenDB, error) { + if err := svc.authz.Authorize(ctx, &fleet.AppleCSR{}, fleet.ActionWrite); err != nil { + return nil, err + } + + privateKey := svc.config.Server.PrivateKey + if testSetEmptyPrivateKey { + privateKey = "" + } + + if len(privateKey) == 0 { + return nil, ctxerr.New(ctx, "Couldn't upload content token. Missing required private key. Learn how to configure the private key here: https://fleetdm.com/learn-more-about/fleet-server-private-key") + } + + if token == nil { + return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("token", "Invalid token. Please provide a valid content token from Apple Business Manager.")) + } + + tokenBytes, err := io.ReadAll(token) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "reading VPP token") + } + + locName, err := vpp.GetConfig(string(tokenBytes)) + if err != nil { + var vppErr *vpp.ErrorResponse + if errors.As(err, &vppErr) { + // Per https://developer.apple.com/documentation/devicemanagement/app_and_book_management/app_and_book_management_legacy/interpreting_error_codes + if vppErr.ErrorNumber == 9622 { + return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("token", "Invalid token. Please provide a valid content token from Apple Business Manager.")) + } + } + return nil, ctxerr.Wrap(ctx, err, "validating VPP token with Apple") + } + + data := fleet.VPPTokenData{ + Token: string(tokenBytes), + Location: locName, + } + + tok, err := svc.ds.UpdateVPPToken(ctx, tokenID, &data) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "updating vpp token") + } + + return tok, nil +} + +func (svc *Service) UpdateVPPTokenTeams(ctx context.Context, tokenID uint, teamIDs []uint) (*fleet.VPPTokenDB, error) { + if err := svc.authz.Authorize(ctx, &fleet.AppleCSR{}, fleet.ActionWrite); err != nil { + return nil, err + } + + tok, err := svc.ds.UpdateVPPTokenTeams(ctx, tokenID, teamIDs) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "updating vpp token team") + } + + return tok, nil +} + +func (svc *Service) GetVPPTokens(ctx context.Context) ([]*fleet.VPPTokenDB, error) { + if err := svc.authz.Authorize(ctx, &fleet.AppleCSR{}, fleet.ActionRead); err != nil { + return nil, err + } + + return svc.ds.ListVPPTokens(ctx) +} + +func (svc *Service) DeleteVPPToken(ctx context.Context, tokenID uint) error { + if err := svc.authz.Authorize(ctx, &fleet.AppleCSR{}, fleet.ActionWrite); err != nil { + return err + } + tok, err := svc.ds.GetVPPToken(ctx, tokenID) + if err != nil { + return ctxerr.Wrap(ctx, err, "getting vpp token") + } + if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), fleet.ActivityDisabledVPP{ + Location: tok.Location, + }); err != nil { + return ctxerr.Wrap(ctx, err, "create activity for delete VPP token") + } + + return svc.ds.DeleteVPPToken(ctx, tokenID) +} diff --git a/frontend/__mocks__/policyMock.ts b/frontend/__mocks__/policyMock.ts index b14095463e..ba60c55c74 100644 --- a/frontend/__mocks__/policyMock.ts +++ b/frontend/__mocks__/policyMock.ts @@ -23,6 +23,10 @@ const DEFAULT_POLICY_MOCK: IPolicyStats = { has_run: true, next_update_ms: 3600000, calendar_events_enabled: true, + install_software: { + name: "testSw0", + software_title_id: 1, + }, }; const createMockPolicy = (overrides?: Partial): IPolicyStats => { diff --git a/frontend/components/Editor/Editor.tsx b/frontend/components/Editor/Editor.tsx index e8f230e379..b724a296c3 100644 --- a/frontend/components/Editor/Editor.tsx +++ b/frontend/components/Editor/Editor.tsx @@ -29,6 +29,10 @@ interface IEditorProps { * @default "editor" */ name?: string; + /** Include correct styles as a form field. + * @default false + */ + isFormField?: boolean; maxLines?: number; className?: string; onChange?: (value: string, event?: any) => void; @@ -52,11 +56,13 @@ const Editor = ({ readOnly = false, wrapEnabled = false, name = "editor", + isFormField = false, maxLines = 20, className, onChange, }: IEditorProps) => { const classNames = classnames(baseClass, className, { + "form-field": isFormField, [`${baseClass}__error`]: !!error, }); diff --git a/frontend/components/Editor/_styles.scss b/frontend/components/Editor/_styles.scss index 676172697d..22fbacfd33 100644 --- a/frontend/components/Editor/_styles.scss +++ b/frontend/components/Editor/_styles.scss @@ -3,7 +3,6 @@ &__label { font-size: $x-small; font-weight: $bold; - margin-bottom: $pad-small; &--error { color: $core-vibrant-red; diff --git a/frontend/components/FleetAce/FleetAce.tsx b/frontend/components/FleetAce/FleetAce.tsx index c30232cb59..b0422d4cde 100644 --- a/frontend/components/FleetAce/FleetAce.tsx +++ b/frontend/components/FleetAce/FleetAce.tsx @@ -29,6 +29,7 @@ export interface IFleetAceProps { label?: string; name?: string; value?: string; + placeholder?: string; readOnly?: boolean; maxLines?: number; showGutter?: boolean; @@ -55,6 +56,7 @@ const FleetAce = ({ labelActionComponent, name = "query-editor", value, + placeholder, readOnly, maxLines = 20, showGutter = true, @@ -266,6 +268,7 @@ const FleetAce = ({ showPrintMargin={false} theme="fleet" value={value} + placeholder={placeholder} width="100%" wrapEnabled={wrapEnabled} style={style} diff --git a/frontend/components/FleetAce/_styles.scss b/frontend/components/FleetAce/_styles.scss index c12237a3a7..f9f0dccf89 100644 --- a/frontend/components/FleetAce/_styles.scss +++ b/frontend/components/FleetAce/_styles.scss @@ -25,6 +25,16 @@ } } + .ace_content { + padding-left: 4px; + } + + .ace_placeholder { + font-family: "SourceCodePro", $monospace; + margin: initial; + font-size: 15px; + } + &__help-text { @include help-text; diff --git a/frontend/components/forms/fields/Dropdown/Dropdown.jsx b/frontend/components/forms/fields/Dropdown/Dropdown.jsx index 1edde66fec..302233e5d6 100644 --- a/frontend/components/forms/fields/Dropdown/Dropdown.jsx +++ b/frontend/components/forms/fields/Dropdown/Dropdown.jsx @@ -27,6 +27,19 @@ class Dropdown extends Component { onClose: PropTypes.func, options: PropTypes.arrayOf(dropdownOptionInterface).isRequired, placeholder: PropTypes.oneOfType([PropTypes.array, PropTypes.string]), + /** + value must correspond to the value of a dropdown option to render + e.g. with options: + + [ + { + label: "Display name", + value: 1, <– the id of the thing + } + ] + + set value to 1, not "Display name" + */ value: PropTypes.oneOfType([ PropTypes.array, PropTypes.string, @@ -75,7 +88,7 @@ class Dropdown extends Component { const { multi, onChange, clearable, name, parseTarget } = this.props; if (parseTarget) { - // Returns both name and value + // Returns both name of the Dropdown and value of the selected option return onChange({ value: selected.value, name }); } diff --git a/frontend/interfaces/policy.ts b/frontend/interfaces/policy.ts index 41586ea22d..621c52f638 100644 --- a/frontend/interfaces/policy.ts +++ b/frontend/interfaces/policy.ts @@ -41,6 +41,12 @@ export interface IPolicy { updated_at: string; critical: boolean; calendar_events_enabled: boolean; + install_software?: IPolicySoftwareToInstall; +} + +export interface IPolicySoftwareToInstall { + name: string; + software_title_id: number; } // Used on the manage hosts page and other places where aggregate stats are displayed @@ -94,6 +100,8 @@ export interface IPolicyFormData { team_id?: number | null; id?: number; calendar_events_enabled?: boolean; + // undefined from GET/LIST when not set, null for PATCH to unset + software_title_id?: number | null; } export interface IPolicyNew { diff --git a/frontend/interfaces/software.ts b/frontend/interfaces/software.ts index bf6ba786a1..9d6d3617b2 100644 --- a/frontend/interfaces/software.ts +++ b/frontend/interfaces/software.ts @@ -109,6 +109,7 @@ export interface ISoftwareTitleDetails { source: string; // "apps" | "ios_apps" | "ipados_apps" | ? hosts_count: number; versions: ISoftwareTitleVersion[] | null; + versions_updated_at?: string; bundle_identifier?: string; browser?: string; versions_count?: number; diff --git a/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/ProfileUploader/components/AddProfileModal/AddProfileModal.tsx b/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/ProfileUploader/components/AddProfileModal/AddProfileModal.tsx index 12191a9641..694970c4ca 100644 --- a/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/ProfileUploader/components/AddProfileModal/AddProfileModal.tsx +++ b/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/components/ProfileUploader/components/AddProfileModal/AddProfileModal.tsx @@ -77,7 +77,7 @@ interface IFileDetailsProps { // TODO: if we reuse this one more time, we should consider moving this // into FileUploader as a default preview. Currently we have this in -// AddSoftwareForm.tsx and here. +// AddPackageForm.tsx and here. const FileDetails = ({ details: { name, platform } }: IFileDetailsProps) => (
diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/AdvancedOptionsModal/AdvancedOptionsModal.tsx b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/AdvancedOptionsModal/AdvancedOptionsModal.tsx index b3f7c31146..5cfc313e44 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/AdvancedOptionsModal/AdvancedOptionsModal.tsx +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/AdvancedOptionsModal/AdvancedOptionsModal.tsx @@ -38,6 +38,7 @@ const AdvancedOptionsModal = ({ helpText="Fleet will run this command on hosts to install software." label="Install script" labelTooltip="For security agents, add the script provided by the vendor." + isFormField /> {preInstallQuery && (
@@ -72,6 +73,7 @@ const AdvancedOptionsModal = ({ maxLines={10} value={postInstallScript} helpText="Shell (macOS and Linux) or PowerShell (Windows)." + isFormField />
)} diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/DeleteSoftwareModal/DeleteSoftwareModal.tsx b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/DeleteSoftwareModal/DeleteSoftwareModal.tsx index 23d03c5852..fa75f1d554 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/DeleteSoftwareModal/DeleteSoftwareModal.tsx +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/DeleteSoftwareModal/DeleteSoftwareModal.tsx @@ -3,11 +3,15 @@ import React, { useCallback, useContext } from "react"; import softwareAPI from "services/entities/software"; import { NotificationContext } from "context/notification"; +import { getErrorReason } from "interfaces/errors"; + import Modal from "components/Modal"; import Button from "components/buttons/Button"; const baseClass = "delete-software-modal"; +const DELETE_SW_USED_BY_POLICY_ERROR_MSG = + "Couldn't delete. Policy automation uses this software. Please disable policy automation for this software and try again."; interface IDeleteSoftwareModalProps { softwareId: number; teamId: number; @@ -28,8 +32,13 @@ const DeleteSoftwareModal = ({ await softwareAPI.deleteSoftwarePackage(softwareId, teamId); renderFlash("success", "Software deleted successfully!"); onSuccess(); - } catch { - renderFlash("error", "Couldn't delete. Please try again."); + } catch (error) { + const reason = getErrorReason(error); + if (reason.includes("Policy automation uses this software")) { + renderFlash("error", DELETE_SW_USED_BY_POLICY_ERROR_MSG); + } else { + renderFlash("error", "Couldn't delete. Please try again."); + } } onExit(); }, [softwareId, teamId, renderFlash, onSuccess, onExit]); diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwarePackageCard/SoftwarePackageCard.tsx b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwarePackageCard/SoftwarePackageCard.tsx index 8453abfbb6..1c6a31e9d7 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwarePackageCard/SoftwarePackageCard.tsx +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwarePackageCard/SoftwarePackageCard.tsx @@ -4,8 +4,6 @@ import React, { useLayoutEffect, useState, } from "react"; -import FileSaver from "file-saver"; -import { parse } from "content-disposition"; import PATHS from "router/paths"; import { AppContext } from "context/app"; @@ -45,10 +43,15 @@ function useTruncatedElement(ref: React.RefObject) { useLayoutEffect(() => { const element = ref.current; - if (element) { - const { scrollWidth, clientWidth } = element; - setIsTruncated(scrollWidth > clientWidth); + function updateIsTruncated() { + if (element) { + const { scrollWidth, clientWidth } = element; + setIsTruncated(scrollWidth > clientWidth); + } } + window.addEventListener("resize", updateIsTruncated); + updateIsTruncated(); + return () => window.removeEventListener("resize", updateIsTruncated); }, [ref]); return isTruncated; @@ -92,20 +95,29 @@ const STATUS_DISPLAY_OPTIONS: Record< iconName: "success", tooltip: ( <> - Fleet installed software on these hosts. Currently, if the software is - uninstalled, the "Installed" status won't be updated. + Software is installed on these hosts (install script finished +
+ with exit code 0). Currently, if the software is uninstalled, the +
+ "installed" status won't be updated. ), }, pending: { displayName: "Pending", iconName: "pending-outline", - tooltip: "Fleet will install software when these hosts come online.", + tooltip: "Fleet is installing or will install when the host comes online.", }, failed: { displayName: "Failed", iconName: "error", - tooltip: "Fleet failed to install software on these hosts.", + tooltip: ( + <> + These hosts failed to install software. Click on a host to view +
+ error(s). + + ), }, }; @@ -130,16 +142,18 @@ const PackageStatusCount = ({ })}`; return (
- {displayData.displayName} +
{displayData.displayName}
} @@ -305,7 +319,7 @@ const SoftwarePackageCard = ({ return ( -
+
{/* TODO: main-info could be a seperate component as its reused on a couple pages already. Come back and pull this into a component */}
@@ -315,46 +329,46 @@ const SoftwarePackageCard = ({ {renderDetails()}
-
- - - +
+ {isSelfService && ( +
+ + Self-service +
+ )} + {showActions && ( + + )}
-
- {isSelfService && ( -
- - Self-service -
- )} - {showActions && ( - - )} +
+ + +
{showAdvancedOptionsModal && ( diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsTable/SoftwareTitleDetailsTable.tsx b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsTable/SoftwareTitleDetailsTable.tsx index 4eb9660e62..6eaafa2a28 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsTable/SoftwareTitleDetailsTable.tsx +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsTable/SoftwareTitleDetailsTable.tsx @@ -13,6 +13,7 @@ import TableContainer from "components/TableContainer"; import TableCount from "components/TableContainer/TableCount"; import EmptyTable from "components/EmptyTable"; import CustomLink from "components/CustomLink"; +import LastUpdatedText from "components/LastUpdatedText"; import generateSoftwareTitleDetailsTableConfig from "./SoftwareTitleDetailsTableConfig"; @@ -21,6 +22,21 @@ const DEFAULT_SORT_DIRECTION = "desc"; const baseClass = "software-title-details-table"; +const SoftwareLastUpdatedInfo = (lastUpdatedAt: string) => { + return ( + + The last time software data was
+ updated, including vulnerabilities
+ and host counts. + + } + /> + ); +}; + const NoVersionsDetected = (isAvailableForInstall = false): JSX.Element => { return ( { const handleRowSelect = (row: IRowProps) => { const hostsBySoftwareParams = { @@ -95,7 +113,10 @@ const SoftwareTitleDetailsTable = ({ ); const renderVersionsCount = () => ( - + <> + + {countsUpdatedAt && SoftwareLastUpdatedInfo(countsUpdatedAt)} + ); return ( diff --git a/frontend/pages/SoftwarePage/components/AddPackage/AddPackage.tsx b/frontend/pages/SoftwarePage/components/AddPackage/AddPackage.tsx index 2e7e4b17a5..52cb805f7f 100644 --- a/frontend/pages/SoftwarePage/components/AddPackage/AddPackage.tsx +++ b/frontend/pages/SoftwarePage/components/AddPackage/AddPackage.tsx @@ -1,13 +1,19 @@ import React, { useContext, useEffect, useState } from "react"; import { InjectedRouter } from "react-router"; +import { getErrorReason } from "interfaces/errors"; + import PATHS from "router/paths"; import { NotificationContext } from "context/notification"; import softwareAPI from "services/entities/software"; import { QueryParams, buildQueryStringFromParams } from "utilities/url"; +import { LEARN_MORE_ABOUT_BASE_LINK } from "utilities/constants"; + +import CustomLink from "components/CustomLink"; + import AddPackageForm from "../AddPackageForm"; -import { IAddSoftwareFormData } from "../AddPackageForm/AddSoftwareForm"; +import { IAddPackageFormData } from "../AddPackageForm/AddPackageForm"; import { getErrorMessage } from "../AddSoftwareModal/helpers"; const baseClass = "add-package"; @@ -60,7 +66,7 @@ const AddPackage = ({ }; }, [isUploading]); - const onAddPackage = async (formData: IAddSoftwareFormData) => { + const onAddPackage = async (formData: IAddPackageFormData) => { setIsUploading(true); if (formData.software && formData.software.size > MAX_FILE_SIZE_BYTES) { @@ -98,6 +104,21 @@ const AddPackage = ({ `${PATHS.SOFTWARE_TITLES}?${buildQueryStringFromParams(newQueryParams)}` ); } catch (e) { + const reason = getErrorReason(e); + if ( + reason.includes("Couldn't add. Fleet couldn't read the version from") + ) { + renderFlash( + "error", + `${reason}. ${( + + )} ` + ); + } renderFlash("error", getErrorMessage(e)); } diff --git a/frontend/pages/SoftwarePage/components/AddPackageAdvancedOptions/AddPackageAdvancedOptions.tsx b/frontend/pages/SoftwarePage/components/AddPackageAdvancedOptions/AddPackageAdvancedOptions.tsx new file mode 100644 index 0000000000..5d6524e8dc --- /dev/null +++ b/frontend/pages/SoftwarePage/components/AddPackageAdvancedOptions/AddPackageAdvancedOptions.tsx @@ -0,0 +1,101 @@ +import React, { useState } from "react"; + +import Editor from "components/Editor"; +import CustomLink from "components/CustomLink"; +import FleetAce from "components/FleetAce"; +import RevealButton from "components/buttons/RevealButton"; + +const baseClass = "add-package-advanced-options"; + +interface IAddPackageAdvancedOptionsProps { + errors: { preInstallQuery?: string; postInstallScript?: string }; + preInstallQuery?: string; + installScript: string; + postInstallScript?: string; + onChangePreInstallQuery: (value?: string) => void; + onChangeInstallScript: (value: string) => void; + onChangePostInstallScript: (value?: string) => void; +} + +const AddPackageAdvancedOptions = ({ + errors, + preInstallQuery, + installScript, + postInstallScript, + onChangePreInstallQuery, + onChangeInstallScript, + onChangePostInstallScript, +}: IAddPackageAdvancedOptionsProps) => { + const [showAdvancedOptions, setShowAdvancedOptions] = useState(false); + + return ( +
+ setShowAdvancedOptions(!showAdvancedOptions)} + /> + {showAdvancedOptions && ( +
+ + Software will be installed only if the{" "} + + + } + /> + + Fleet will run this script on hosts to install software. Use the +
+ $INSTALLER_PATH variable to point to the installer. + + } + isFormField + /> + +
+ )} +
+ ); +}; + +export default AddPackageAdvancedOptions; diff --git a/frontend/pages/SoftwarePage/components/AddSoftwareAdvancedOptions/_styles.scss b/frontend/pages/SoftwarePage/components/AddPackageAdvancedOptions/_styles.scss similarity index 88% rename from frontend/pages/SoftwarePage/components/AddSoftwareAdvancedOptions/_styles.scss rename to frontend/pages/SoftwarePage/components/AddPackageAdvancedOptions/_styles.scss index 58f1f85892..0728e32415 100644 --- a/frontend/pages/SoftwarePage/components/AddSoftwareAdvancedOptions/_styles.scss +++ b/frontend/pages/SoftwarePage/components/AddPackageAdvancedOptions/_styles.scss @@ -1,4 +1,4 @@ -.add-software-advanced-options { +.add-package-advanced-options { display: flex; flex-direction: column; align-items: flex-start; diff --git a/frontend/pages/SoftwarePage/components/AddPackageAdvancedOptions/index.ts b/frontend/pages/SoftwarePage/components/AddPackageAdvancedOptions/index.ts new file mode 100644 index 0000000000..004c96332d --- /dev/null +++ b/frontend/pages/SoftwarePage/components/AddPackageAdvancedOptions/index.ts @@ -0,0 +1 @@ +export { default } from "./AddPackageAdvancedOptions"; diff --git a/frontend/pages/SoftwarePage/components/AddPackageForm/AddSoftwareForm.tsx b/frontend/pages/SoftwarePage/components/AddPackageForm/AddPackageForm.tsx similarity index 55% rename from frontend/pages/SoftwarePage/components/AddPackageForm/AddSoftwareForm.tsx rename to frontend/pages/SoftwarePage/components/AddPackageForm/AddPackageForm.tsx index aab97985d0..cf3802b3f6 100644 --- a/frontend/pages/SoftwarePage/components/AddPackageForm/AddSoftwareForm.tsx +++ b/frontend/pages/SoftwarePage/components/AddPackageForm/AddPackageForm.tsx @@ -6,7 +6,6 @@ import getInstallScript from "utilities/software_install_scripts"; import Button from "components/buttons/Button"; import Checkbox from "components/forms/fields/Checkbox"; -import Editor from "components/Editor"; import { FileUploader, FileDetails, @@ -14,25 +13,25 @@ import { import Spinner from "components/Spinner"; import TooltipWrapper from "components/TooltipWrapper"; -import AddSoftwareAdvancedOptions from "../AddSoftwareAdvancedOptions"; +import AddPackageAdvancedOptions from "../AddPackageAdvancedOptions"; import { generateFormValidation } from "./helpers"; -export const baseClass = "add-software-form"; +export const baseClass = "add-package-form"; const UploadingSoftware = () => { return (
-

Uploading. It may take a few minutes to finish.

+

Adding software. This may take a few minutes to finish.

); }; -export interface IAddSoftwareFormData { +export interface IAddPackageFormData { software: File | null; installScript: string; - preInstallCondition?: string; + preInstallQuery?: string; postInstallScript?: string; selfService: boolean; } @@ -40,30 +39,28 @@ export interface IAddSoftwareFormData { export interface IFormValidation { isValid: boolean; software: { isValid: boolean }; - preInstallCondition?: { isValid: boolean; message?: string }; + preInstallQuery?: { isValid: boolean; message?: string }; postInstallScript?: { isValid: boolean; message?: string }; selfService?: { isValid: boolean }; } -interface IAddSoftwareFormProps { +interface IAddPackageFormProps { isUploading: boolean; onCancel: () => void; - onSubmit: (formData: IAddSoftwareFormData) => void; + onSubmit: (formData: IAddPackageFormData) => void; } -const AddSoftwareForm = ({ +const AddPackageForm = ({ isUploading, onCancel, onSubmit, -}: IAddSoftwareFormProps) => { +}: IAddPackageFormProps) => { const { renderFlash } = useContext(NotificationContext); - const [showPreInstallCondition, setShowPreInstallCondition] = useState(false); - const [showPostInstallScript, setShowPostInstallScript] = useState(false); - const [formData, setFormData] = useState({ + const [formData, setFormData] = useState({ software: null, installScript: "", - preInstallCondition: undefined, + preInstallQuery: undefined, postInstallScript: undefined, selfService: false, }); @@ -90,13 +87,7 @@ const AddSoftwareForm = ({ installScript, }; setFormData(newData); - setFormValidation( - generateFormValidation( - newData, - showPreInstallCondition, - showPostInstallScript - ) - ); + setFormValidation(generateFormValidation(newData)); } }; @@ -105,62 +96,26 @@ const AddSoftwareForm = ({ onSubmit(formData); }; - const onTogglePreInstallConditionCheckbox = (value: boolean) => { - const newData = { ...formData, preInstallCondition: undefined }; - setShowPreInstallCondition(value); - setFormData(newData); - setFormValidation( - generateFormValidation(newData, value, showPostInstallScript) - ); - }; - - const onTogglePostInstallScriptCheckbox = (value: boolean) => { - const newData = { ...formData, postInstallScript: undefined }; - setShowPostInstallScript(value); - setFormData(newData); - setFormValidation( - generateFormValidation(newData, showPreInstallCondition, value) - ); - }; - const onChangeInstallScript = (value: string) => { setFormData({ ...formData, installScript: value }); }; - const onChangePreInstallCondition = (value?: string) => { - const newData = { ...formData, preInstallCondition: value }; + const onChangePreInstallQuery = (value?: string) => { + const newData = { ...formData, preInstallQuery: value }; setFormData(newData); - setFormValidation( - generateFormValidation( - newData, - showPreInstallCondition, - showPostInstallScript - ) - ); + setFormValidation(generateFormValidation(newData)); }; const onChangePostInstallScript = (value?: string) => { const newData = { ...formData, postInstallScript: value }; setFormData(newData); - setFormValidation( - generateFormValidation( - newData, - showPreInstallCondition, - showPostInstallScript - ) - ); + setFormValidation(generateFormValidation(newData)); }; const onToggleSelfServiceCheckbox = (value: boolean) => { const newData = { ...formData, selfService: value }; setFormData(newData); - setFormValidation( - generateFormValidation( - newData, - showPreInstallCondition, - showPostInstallScript - ) - ); + setFormValidation(generateFormValidation(newData)); }; const isSubmitDisabled = !formValidation.isValid; @@ -185,25 +140,6 @@ const AddSoftwareForm = ({ ) } /> - {formData.software && ( - - For security agents, add the script provided by the vendor. -
- In custom scripts, you can use the $INSTALLER_PATH variable to - point to the installer. - - } - /> - )} -
diff --git a/frontend/pages/SoftwarePage/components/AppStoreVpp/_styles.scss b/frontend/pages/SoftwarePage/components/AppStoreVpp/_styles.scss index e530588b7a..604750a258 100644 --- a/frontend/pages/SoftwarePage/components/AppStoreVpp/_styles.scss +++ b/frontend/pages/SoftwarePage/components/AppStoreVpp/_styles.scss @@ -53,6 +53,7 @@ &__no-software-description { margin: 0; color: $ui-fleet-black-75; + text-align: center; } &__error { diff --git a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/MdmSettings.tsx b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/MdmSettings.tsx index ff3cf5b539..b6e0f02795 100644 --- a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/MdmSettings.tsx +++ b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/MdmSettings.tsx @@ -27,6 +27,8 @@ interface IMdmSettingsProps { const MdmSettings = ({ router }: IMdmSettingsProps) => { const { isPremiumTier, config } = useContext(AppContext); + const isMdmEnabled = !!config?.mdm.enabled_and_configured; + // Currently the status of this API call is what determines various UI states on // this page. Because of this we will not render any of this components UI until this API // call has completed. @@ -48,7 +50,7 @@ const MdmSettings = ({ router }: IMdmSettingsProps) => { // we're fetching and setting the config, but for now we'll just assume that any 400 response // means that MDM is not enabled and we'll show the "Turn on MDM" button. staleTime: 5000, - enabled: !!config?.mdm.enabled_and_configured, + enabled: isMdmEnabled, } ); @@ -63,7 +65,7 @@ const MdmSettings = ({ router }: IMdmSettingsProps) => { { ...DEFAULT_USE_QUERY_OPTIONS, retry: false, - enabled: isPremiumTier && !!config?.mdm.enabled_and_configured, + enabled: isPremiumTier && isMdmEnabled, } ); @@ -80,7 +82,7 @@ const MdmSettings = ({ router }: IMdmSettingsProps) => { { ...DEFAULT_USE_QUERY_OPTIONS, retry: false, - enabled: isPremiumTier && !!config?.mdm.enabled_and_configured, + enabled: isPremiumTier && isMdmEnabled, } ); @@ -104,7 +106,7 @@ const MdmSettings = ({ router }: IMdmSettingsProps) => { // we use this to determine if we have all the data we need to render the UI. // Notice that we do not need VPP or EULA data to render this page. - const hasAllData = !!APNSInfo; + const hasAllData = !isMdmEnabled || !!APNSInfo; return (
diff --git a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/VppPage/components/AddVppModal/helpers.tsx b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/VppPage/components/AddVppModal/helpers.tsx index 77dc842782..7716c6de6c 100644 --- a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/VppPage/components/AddVppModal/helpers.tsx +++ b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/VppPage/components/AddVppModal/helpers.tsx @@ -20,7 +20,7 @@ export const getErrorMessage = (err: unknown) => { reasonIncludes: "Duplicate entry", }); const invalidTokenReason = getErrorReason(err, { - reasonIncludes: "invalid", + reasonIncludes: "Invalid token", }); if (duplicateEntryReason) { @@ -28,7 +28,7 @@ export const getErrorMessage = (err: unknown) => { } if (invalidTokenReason) { - return "Invalid token. Please provide a valid token from Apple Business Manager."; + return invalidTokenReason; } return DEFAULT_ERROR_MESSAGE; diff --git a/frontend/pages/hosts/details/cards/Software/InstallStatusCell/InstallStatusCell.tsx b/frontend/pages/hosts/details/cards/Software/InstallStatusCell/InstallStatusCell.tsx index b99503903d..ef5ac1b737 100644 --- a/frontend/pages/hosts/details/cards/Software/InstallStatusCell/InstallStatusCell.tsx +++ b/frontend/pages/hosts/details/cards/Software/InstallStatusCell/InstallStatusCell.tsx @@ -4,7 +4,6 @@ import ReactTooltip from "react-tooltip"; import { uniqueId } from "lodash"; import { IHostSoftware, SoftwareInstallStatus } from "interfaces/software"; -import { dateAgo } from "utilities/date_format"; import Icon from "components/Icon"; import TextCell from "components/TableContainer/DataTable/TextCell"; @@ -14,6 +13,7 @@ const baseClass = "install-status-cell"; type IStatusValue = SoftwareInstallStatus | "avaiableForInstall"; interface TootipArgs { softwareName?: string | null; + // this field is used in My device > Self-service lastInstalledAt?: string; isAppStoreApp?: boolean; } @@ -36,26 +36,23 @@ export const INSTALL_STATUS_DISPLAY_OPTIONS: Record< installed: { iconName: "success", displayText: "Installed", - tooltip: ({ lastInstalledAt: lastInstall }) => ( - <> - Fleet installed software on this host ({dateAgo(lastInstall as string)} - ). Currently, if the software is uninstalled, the "Installed" - status won't be updated. - - ), + tooltip: () => + "Software is installed (install script finished with exit code 0).", }, pending: { iconName: "pending-outline", displayText: "Pending", - tooltip: () => "Fleet will install software when the host comes online.", + tooltip: () => + "Fleet is installing or will install when the host comes online.", }, failed: { iconName: "error", displayText: "Failed", - tooltip: ({ lastInstalledAt: lastInstall }) => ( + tooltip: () => ( <> - Fleet failed to install software ({dateAgo(lastInstall as string)} ago). - Select Actions > Software details to see more. + The host failed to install software. To view errors, select +
+ Actions > Show details. ), }, @@ -96,10 +93,6 @@ const InstallStatusCell = ({ app_store_app, }: IInstallStatusCellProps) => { // FIXME: Improve the way we handle polymophism of software_package and app_store_app - const lastInstalledAt = - software_package?.last_install?.installed_at || - app_store_app?.last_install?.installed_at || - ""; const hasPackage = !!software_package; const hasAppStoreApp = !!app_store_app; @@ -140,7 +133,6 @@ const InstallStatusCell = ({ {displayConfig.tooltip({ softwareName, - lastInstalledAt, isAppStoreApp: hasAppStoreApp, })} diff --git a/frontend/pages/hosts/details/cards/Software/SelfService/SelfService.tests.tsx b/frontend/pages/hosts/details/cards/Software/SelfService/SelfService.tests.tsx index 2c995aa2b2..a7ca5e5cdd 100644 --- a/frontend/pages/hosts/details/cards/Software/SelfService/SelfService.tests.tsx +++ b/frontend/pages/hosts/details/cards/Software/SelfService/SelfService.tests.tsx @@ -166,7 +166,7 @@ describe("SelfService", () => { ).toHaveTextContent("Install"); }); - it("renders no action button with 'Install in progress...' status", async () => { + it("renders no action button with 'Pending' status", async () => { mockServer.use( customDeviceSoftwareHandler({ software: [ @@ -186,7 +186,7 @@ describe("SelfService", () => { expect( screen.getByTestId("self-service-item__status--test") - ).toHaveTextContent("Install in progress..."); + ).toHaveTextContent("Pending"); expect( screen.queryByTestId("self-service-item__item-action-button--test") diff --git a/frontend/pages/hosts/details/cards/Software/SelfService/SelfServiceItem/SelfServiceItem.tsx b/frontend/pages/hosts/details/cards/Software/SelfService/SelfServiceItem/SelfServiceItem.tsx index d0fce6aca3..3d817c9d16 100644 --- a/frontend/pages/hosts/details/cards/Software/SelfService/SelfServiceItem/SelfServiceItem.tsx +++ b/frontend/pages/hosts/details/cards/Software/SelfService/SelfServiceItem/SelfServiceItem.tsx @@ -25,25 +25,20 @@ const STATUS_CONFIG: Record = { installed: { iconName: "success", displayText: "Installed", - tooltip: ({ lastInstalledAt }) => ( - <> - Software installed successfully ({dateAgo(lastInstalledAt as string)}). - Currently, if the software is uninstalled, the "Installed" - status won't be updated. - - ), + tooltip: ({ lastInstalledAt }) => + `Software is installed (${dateAgo(lastInstalledAt as string)}).`, }, pending: { iconName: "pending-outline", - displayText: "Install in progress...", - tooltip: () => "Software installation in progress...", + displayText: "Pending", + tooltip: () => "Fleet is installing software.", }, failed: { iconName: "error", displayText: "Failed", tooltip: ({ lastInstalledAt = "" }) => ( <> - Software failed to install + Software failed to install{" "} {lastInstalledAt ? ` (${dateAgo(lastInstalledAt)})` : ""}. Select{" "} Retry to install again, or contact your IT department. @@ -144,7 +139,6 @@ const getInstallButtonText = (status: SoftwareInstallStatus | null) => { case "installed": return "Reinstall"; default: - // we don't show a button for pending installs return ""; } }; @@ -165,10 +159,7 @@ const InstallerStatusAction = ({ SoftwareInstallStatus | undefined >(undefined); - // displayStatus allows us to display the localStatus (if any) or the status from the list - // software reponse - const displayStatus = localStatus || status; - const installButtonText = getInstallButtonText(displayStatus); + const installButtonText = getInstallButtonText(status); // if the localStatus is "failed", we don't want our tooltip to include the old installed_at date so we // set this to null, which tells the tooltip to omit the parenthetical date @@ -200,21 +191,16 @@ const InstallerStatusAction = ({ return (
- +
{!!installButtonText && ( -
+ ); })} -
+ A calendar event will be created for end users if one of their hosts fail any of these policies.{" "} diff --git a/frontend/pages/policies/ManagePoliciesPage/components/InstallSoftwareModal/InstallSoftwareModal.tsx b/frontend/pages/policies/ManagePoliciesPage/components/InstallSoftwareModal/InstallSoftwareModal.tsx new file mode 100644 index 0000000000..7c29b4979f --- /dev/null +++ b/frontend/pages/policies/ManagePoliciesPage/components/InstallSoftwareModal/InstallSoftwareModal.tsx @@ -0,0 +1,276 @@ +import React, { useCallback, useState } from "react"; + +import { useQuery } from "react-query"; +import { omit } from "lodash"; + +import { IPolicyStats } from "interfaces/policy"; +import softwareAPI, { + ISoftwareTitlesQueryKey, + ISoftwareTitlesResponse, +} from "services/entities/software"; +import { DEFAULT_USE_QUERY_OPTIONS } from "utilities/constants"; + +// @ts-ignore +import Dropdown from "components/forms/fields/Dropdown"; +import Modal from "components/Modal"; +import DataError from "components/DataError"; +import Spinner from "components/Spinner"; +import Checkbox from "components/forms/fields/Checkbox"; +import TooltipTruncatedText from "components/TooltipTruncatedText"; +import CustomLink from "components/CustomLink"; +import Button from "components/buttons/Button"; +import { ISoftwareTitle } from "interfaces/software"; + +const getPlatformDisplayFromPackageSuffix = (packageName: string) => { + const split = packageName.split("."); + const suff = split[split.length - 1]; + switch (suff) { + case "pkg": + return "macOS"; + case "deb": + return "Linux"; + case "exe": + return "Windows"; + case "msi": + return "Windows"; + default: + return null; + } +}; + +const AFI_SOFTWARE_BATCH_SIZE = 1000; + +const baseClass = "install-software-modal"; + +interface ISwDropdownField { + name: string; + value: number; +} +interface IFormPolicy { + name: string; + id: number; + installSoftwareEnabled: boolean; + swIdToInstall?: number; +} + +export type IInstallSoftwareFormData = IFormPolicy[]; + +interface IInstallSoftwareModal { + onExit: () => void; + onSubmit: (formData: IInstallSoftwareFormData) => void; + isUpdating: boolean; + policies: IPolicyStats[]; + teamId: number; +} +const InstallSoftwareModal = ({ + onExit, + onSubmit, + isUpdating, + policies, + teamId, +}: IInstallSoftwareModal) => { + const [formData, setFormData] = useState( + policies.map((policy) => ({ + name: policy.name, + id: policy.id, + installSoftwareEnabled: !!policy.install_software, + swIdToInstall: policy.install_software?.software_title_id, + })) + ); + + const anyPolicyEnabledWithoutSelectedSoftware = formData.some( + (policy) => policy.installSoftwareEnabled && !policy.swIdToInstall + ); + const { + data: titlesAFI, + isLoading: isTitlesAFILoading, + isError: isTitlesAFIError, + } = useQuery< + ISoftwareTitlesResponse, + Error, + ISoftwareTitle[], + [ISoftwareTitlesQueryKey] + >( + [ + { + scope: "software-titles", + page: 0, + perPage: AFI_SOFTWARE_BATCH_SIZE, + query: "", + orderDirection: "desc", + orderKey: "hosts_count", + teamId, + availableForInstall: true, + packagesOnly: true, + }, + ], + ({ queryKey: [queryKey] }) => + softwareAPI.getSoftwareTitles(omit(queryKey, "scope")), + { + select: (data) => data.software_titles, + ...DEFAULT_USE_QUERY_OPTIONS, + } + ); + + const onUpdateInstallSoftware = useCallback(() => { + onSubmit(formData); + }, [formData, onSubmit]); + + const onChangeEnableInstallSoftware = useCallback( + (newVal: { policyName: string; value: boolean }) => { + const { policyName, value } = newVal; + setFormData( + formData.map((policy) => { + if (policy.name === policyName) { + return { + ...policy, + installSoftwareEnabled: value, + swIdToInstall: value ? policy.swIdToInstall : undefined, + }; + } + return policy; + }) + ); + }, + [formData] + ); + + const onSelectPolicySoftware = useCallback( + ({ name, value }: ISwDropdownField) => { + const [policyName, softwareId] = [name, value]; + setFormData( + formData.map((policy) => { + if (policy.name === policyName) { + return { ...policy, swIdToInstall: softwareId }; + } + return policy; + }) + ); + }, + [formData] + ); + + const availableSoftwareOptions = titlesAFI?.map((title) => { + const platformDisplay = getPlatformDisplayFromPackageSuffix( + title.software_package?.name ?? "" + ); + const platformString = platformDisplay ? `${platformDisplay} • ` : ""; + return { + label: title.name, + value: title.id, + helpText: `${platformString}${title.software_package?.version ?? ""}`, + }; + }); + + const renderPolicySwInstallOption = (policy: IFormPolicy) => { + const { + name: policyName, + id: policyId, + installSoftwareEnabled: enabled, + swIdToInstall, + } = policy; + + return ( +
  • + { + onChangeEnableInstallSoftware({ + policyName, + value: !enabled, + }); + }} + > + + + {enabled && ( + + )} +
  • + ); + }; + + const renderContent = () => { + if (isTitlesAFIError) { + return ; + } + if (isTitlesAFILoading) { + return ; + } + if (!titlesAFI?.length) { + return ( +
    + No software available for install + + Go to Software to add software to this team. + +
    + ); + } + + return ( +
    +
    +
    Policies:
    +
      + {formData.map((policyData) => + renderPolicySwInstallOption(policyData) + )} +
    + + Selected software will be installed when hosts fail the chosen + policy.{" "} + + +
    +
    + + +
    +
    + ); + }; + + return ( + + {renderContent()} + + ); +}; + +export default InstallSoftwareModal; diff --git a/frontend/pages/policies/ManagePoliciesPage/components/InstallSoftwareModal/_styles.scss b/frontend/pages/policies/ManagePoliciesPage/components/InstallSoftwareModal/_styles.scss new file mode 100644 index 0000000000..de9cfc05be --- /dev/null +++ b/frontend/pages/policies/ManagePoliciesPage/components/InstallSoftwareModal/_styles.scss @@ -0,0 +1,41 @@ +.manage-policies-page { + .install-software-modal { + .form-field--dropdown { + width: 276px; + .Select-placeholder { + color: $ui-fleet-black-50; + } + .Select-menu { + max-height: none; + overflow: visible; + } + .Select-menu-outer { + max-height: 240px; + overflow-y: auto; + } + } + .policy-row { + height: 40px; + padding-top: 4px; + padding-bottom: 4px; + } + + &__no-software { + display: flex; + height: 178px; + flex-direction: column; + align-items: center; + gap: $pad-small; + justify-content: center; + font-size: $small; + + span { + color: $ui-fleet-black-75; + font-size: $xx-small; + } + } + .data-error { + padding: 78px; + } + } +} diff --git a/frontend/pages/policies/ManagePoliciesPage/components/InstallSoftwareModal/index.ts b/frontend/pages/policies/ManagePoliciesPage/components/InstallSoftwareModal/index.ts new file mode 100644 index 0000000000..a9f46a726a --- /dev/null +++ b/frontend/pages/policies/ManagePoliciesPage/components/InstallSoftwareModal/index.ts @@ -0,0 +1 @@ +export { default } from "./InstallSoftwareModal"; diff --git a/frontend/pages/policies/ManagePoliciesPage/components/OtherWorkflowsModal/OtherWorkflowsModal.tsx b/frontend/pages/policies/ManagePoliciesPage/components/OtherWorkflowsModal/OtherWorkflowsModal.tsx index d34ae56ff6..7a9668e825 100644 --- a/frontend/pages/policies/ManagePoliciesPage/components/OtherWorkflowsModal/OtherWorkflowsModal.tsx +++ b/frontend/pages/policies/ManagePoliciesPage/components/OtherWorkflowsModal/OtherWorkflowsModal.tsx @@ -416,8 +416,8 @@ const OtherWorkflowsModal = ({ const { isChecked, name, id } = policyItem; return (
    { @@ -220,8 +219,8 @@ export default { formData.append("software", data.software); formData.append("self_service", data.selfService.toString()); data.installScript && formData.append("install_script", data.installScript); - data.preInstallCondition && - formData.append("pre_install_query", data.preInstallCondition); + data.preInstallQuery && + formData.append("pre_install_query", data.preInstallQuery); data.postInstallScript && formData.append("post_install_script", data.postInstallScript); teamId && formData.append("team_id", teamId.toString()); diff --git a/frontend/services/entities/team_policies.ts b/frontend/services/entities/team_policies.ts index a10d954e8b..d2e1386372 100644 --- a/frontend/services/entities/team_policies.ts +++ b/frontend/services/entities/team_policies.ts @@ -86,6 +86,7 @@ export default { platform, critical, calendar_events_enabled, + software_title_id, } = data; const { TEAMS } = endpoints; const path = `${TEAMS}/${team_id}/policies/${id}`; @@ -98,6 +99,7 @@ export default { platform, critical, calendar_events_enabled, + software_title_id, }); }, destroy: (teamId: number | undefined, ids: number[]) => { diff --git a/frontend/utilities/constants.tsx b/frontend/utilities/constants.tsx index c399b5e9c1..4b780aebc9 100644 --- a/frontend/utilities/constants.tsx +++ b/frontend/utilities/constants.tsx @@ -60,9 +60,13 @@ export const HOST_STATUS_WEBHOOK_WINDOW_DROPDOWN_OPTIONS: IDropdownOption[] = [ export const GITHUB_NEW_ISSUE_LINK = "https://github.com/fleetdm/fleet/issues/new?assignees=&labels=bug%2C%3Areproduce&template=bug-report.md"; -export const SUPPORT_LINK = "https://fleetdm.com/support"; +export const FLEET_WEBSITE_URL = "https://fleetdm.com"; -export const CONTACT_FLEET_LINK = "https://fleetdm.com/contact"; +export const SUPPORT_LINK = `${FLEET_WEBSITE_URL}/support`; + +export const CONTACT_FLEET_LINK = `${FLEET_WEBSITE_URL}/contact`; + +export const LEARN_MORE_ABOUT_BASE_LINK = `${FLEET_WEBSITE_URL}/learn-more-about`; /** July 28, 2016 is the date of the initial commit to fleet/fleet. */ export const INITIAL_FLEET_DATE = "2016-07-28T00:00:00Z"; diff --git a/handbook/business-operations/business-operations.rituals.yml b/handbook/business-operations/business-operations.rituals.yml index e6df744a0a..fec5055898 100644 --- a/handbook/business-operations/business-operations.rituals.yml +++ b/handbook/business-operations/business-operations.rituals.yml @@ -52,7 +52,7 @@ task: "Prioritize for next sprint" # Title that will actually show in rituals table startedOn: "2023-08-09" # Needs to align with frequency e.g. if frequency is every thrid Thursday startedOn === any third thursday frequency: "Triweekly" # must be supported by https://github.com/fleetdm/fleet/blob/dbbb501358e226fa3fdf48865175efe3334c826c/website/scripts/build-static-content.js - description: "Drag next sprint's priorities to the top of the 'Not yet' column." # example of a longer thing: description: "[Prioritizing next sprint](https://fleetdm.com/handbook/digital-experiencemunication)" + description: "Using your departmental kanban board, prioritize and finalize next sprint's goals for your team by draging the appropriate issues to the top of the 'Not yet' column." # example of a longer thing: description: "[Prioritizing next sprint](https://fleetdm.com/handbook/digital-experiencemunication)" moreInfoUrl: "https://fleetdm.com/handbook/company/why-this-way#why-make-work-visible" #URL used to highlight "description:" test in table dri: "jostableford" # DRI for ritual (assignee if autoIssue) (TODO display GitHub proflie pic instead of name or title) autoIssue: # Enables automation of GitHub issues diff --git a/handbook/company/product-groups.md b/handbook/company/product-groups.md index fa8bf1bf31..3190df3fc8 100644 --- a/handbook/company/product-groups.md +++ b/handbook/company/product-groups.md @@ -584,6 +584,10 @@ Use the 🧩 ["Design System (current)"](https://www.figma.com/file/8oXlYXpgCV1S Use `---`, with color `$ui-fleet-black-50` as the default UI for empty columns. +**Images** + +Simple icons (aka any images used in the icon [design system component](https://www.figma.com/design/8oXlYXpgCV1Sn4ek7OworP/%F0%9F%A7%A9-Design-system-(current)?node-id=12-2&t=iO2vXbQ9Sc1kFVEJ-1)) are exported as SVGs. All other images are exported as PNGs, following the [Fleet website image](https://github.com/fleetdm/fleet/tree/main/website/assets/images) naming conventions. + **Form behavior** Pressing the return or enter key with an open form will cause the form to be submitted. diff --git a/handbook/customer-success/customer-success.rituals.yml b/handbook/customer-success/customer-success.rituals.yml index 017063ae96..2c493ccf74 100644 --- a/handbook/customer-success/customer-success.rituals.yml +++ b/handbook/customer-success/customer-success.rituals.yml @@ -2,7 +2,7 @@ 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 frequency: "Triweekly" # must be supported by - description: "Drag next sprint's priorities to the top of the 'Not yet' column." # example of a longer thing: description: "[Prioritizing next sprint](https://fleetdm.com/handbook/company/communication)" + description: "Using your departmental kanban board, prioritize and finalize next sprint's goals for your team by draging the appropriate issues to the top of the 'Not yet' column." # 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: "zayhanlon" # DRI for ritual (assignee if autoIssue) (TODO display GitHub proflie pic instead of name or title) autoIssue: # Enables automation of GitHub issues diff --git a/handbook/demand/demand.rituals.yml b/handbook/demand/demand.rituals.yml index f64b087da0..1594c4d510 100644 --- a/handbook/demand/demand.rituals.yml +++ b/handbook/demand/demand.rituals.yml @@ -9,7 +9,7 @@ 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 frequency: "Triweekly" - description: "Drag next sprint's priorities to the top of the 'Not yet' column." # example of a longer thing: description: "[Prioritizing next sprint](https://fleetdm.com/handbook/company/communication)" + description: "Using your departmental kanban board, prioritize and finalize next sprint's goals for your team by draging the appropriate issues to the top of the 'Not yet' column." # 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: "mikermcneil" # DRI for ritual (assignee if autoIssue) (TODO display GitHub proflie pic instead of name or title) autoIssue: # « Enable automation of GitHub issues @@ -22,6 +22,13 @@ description: "https://fleetdm.com/handbook/demand#settle-event-strategy" moreInfoUrl: "https://fleetdm.com/handbook/demand#settle-event-strategy" dri: "Drew-P-drawers" +- + task: "🫧 Pipeline sync" + startedOn: "2024-08-29" + frequency: "Weekly" + description: "Allign with CRO and AEs on pipeline processes and incoming leads" + moreInfoUrl: "" + dri: "Drew-P-drawers" - task: "Optimize ads" startedOn: "2024-02-26" diff --git a/handbook/digital-experience/digital-experience.rituals.yml b/handbook/digital-experience/digital-experience.rituals.yml index 5e2e58a922..9e1999d304 100644 --- a/handbook/digital-experience/digital-experience.rituals.yml +++ b/handbook/digital-experience/digital-experience.rituals.yml @@ -74,7 +74,7 @@ task: "Prioritize for next sprint" # Title that will actually show in rituals table startedOn: "2023-08-09" # Needs to align with frequency e.g. if frequency is every thrid Thursday startedOn === any third thursday frequency: "Triweekly" # must be supported by https://github.com/fleetdm/fleet/blob/dbbb501358e226fa3fdf48865175efe3334c826c/website/scripts/build-static-content.js - description: "Drag next sprint's priorities to the top of the 'Not yet' column." # example of a longer thing: description: "[Prioritizing next sprint](https://fleetdm.com/handbook/digital-experiencemunication)" + description: "Using your departmental kanban board, prioritize and finalize next sprint's goals for your team by draging the appropriate issues to the top of the 'Not yet' column." # example of a longer thing: description: "[Prioritizing next sprint](https://fleetdm.com/handbook/digital-experiencemunication)" moreInfoUrl: "https://fleetdm.com/handbook/company/why-this-way#why-make-work-visible" #URL used to highlight "description:" test in table dri: "sampfluger88" # DRI for ritual (assignee if autoIssue) (TODO display GitHub proflie pic instead of name or title) autoIssue: # Enables automation of GitHub issues @@ -123,8 +123,8 @@ repo: "confidential" - task: "Process and backup Sid agenda" - startedOn: "2023-09-15" - frequency: "Weekly" + startedOn: "2023-09-25" + frequency: "Monthly" description: "Process and backup Sid agenda" moreInfoUrl: "https://fleetdm.com/handbook/digital-experience#process-and-backup-e-group-agenda" dri: "SFriendLee" diff --git a/handbook/engineering/README.md b/handbook/engineering/README.md index d6f18bde2c..70d9cd75f4 100644 --- a/handbook/engineering/README.md +++ b/handbook/engineering/README.md @@ -111,11 +111,10 @@ If there is partially merged feature work when the release candidate is created, Before kicking off release QA, confirm that we are using the latest versions of dependencies we want to keep up-to-date with each release. Currently, those dependencies are: 1. **Go**: Latest minor release -- Check the [version included in Fleet](https://github.com/fleetdm/fleet/settings/variables/actions). +- Check the [Go version specified in Fleet's go.mod file](https://github.com/fleetdm/fleet/blob/main/go.mod) (`go 1.XX.YY`). - Check the [latest minor version of Go](https://go.dev/dl/). For example, if we are using `go1.19.8`, and there is a new minor version `go1.19.9`, we will upgrade. - If the latest minor version is greater than the version included in Fleet, [file a bug](https://github.com/fleetdm/fleet/issues/new?assignees=&labels=bug%2C%3Areproduce&projects=&template=bug-report.md&title=) and assign it to the [release ritual DRI](https://fleetdm.com/handbook/engineering#rituals) and the current oncall engineer. Add the `~release blocker` label. We must upgrade to the latest minor version before publishing the next release. - If the latest major version is greater than the version included in Fleet, [create a story](https://github.com/fleetdm/fleet/issues/new?assignees=&labels=story%2C%3Aproduct&projects=&template=story.md&title=) and assign it to the [release ritual DRI](https://fleetdm.com/handbook/engineering#rituals) and the current oncall engineer. This will be considered for an upcoming sprint. The release can proceed without upgrading the major version. -- Note that major version upgrades also require an [update to go.mod](https://github.com/fleetdm/fleet/blob/7b3134498873a31ba748ca27fabb0059cef70db9/go.mod#L3). > In Go versioning, the number after the first dot is the "major" version, while the number after the second dot is the "minor" version. For example, in Go 1.19.9, "19" is the major version and "9" is the minor version. Major version upgrades are assessed separately by engineering. diff --git a/handbook/sales/sales.rituals.yml b/handbook/sales/sales.rituals.yml index 6b2fd8fd0d..601d659e3d 100644 --- a/handbook/sales/sales.rituals.yml +++ b/handbook/sales/sales.rituals.yml @@ -6,7 +6,7 @@ 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 frequency: "Triweekly" # must be supported by - description: "Drag next sprint's priorities to the top of the 'Not yet' column." # example of a longer thing: description: "[Prioritizing next sprint](https://fleetdm.com/handbook/company/communication)" + description: "Using your departmental kanban board, prioritize and finalize next sprint's goals for your team by draging the appropriate issues to the top of the 'Not yet' column." # 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) autoIssue: # Enables automation of GitHub issues diff --git a/schema/osquery_fleet_schema.json b/schema/osquery_fleet_schema.json index ed0f676276..097534dbd1 100644 --- a/schema/osquery_fleet_schema.json +++ b/schema/osquery_fleet_schema.json @@ -5349,7 +5349,7 @@ }, { "name": "cryptoinfo", - "description": "Get info about the a certificate on the host.", + "description": "Get info about a certificate on the host.", "evented": false, "notes": "This table is not a core osquery table. It is included as part of fleetd, the osquery manager from Fleet. Code based on work by [Kolide](https://github.com/kolide/launcher).", "platforms": [ diff --git a/schema/tables/cryptoinfo.yml b/schema/tables/cryptoinfo.yml index 4fd8d5da37..38055381b7 100644 --- a/schema/tables/cryptoinfo.yml +++ b/schema/tables/cryptoinfo.yml @@ -1,5 +1,5 @@ name: cryptoinfo -description: Get info about the a certificate on the host. +description: Get info about a certificate on the host. evented: false notes: This table is not a core osquery table. It is included as part of fleetd, the osquery manager from Fleet. Code based on work by [Kolide](https://github.com/kolide/launcher). platforms: diff --git a/server/docs/patterns.md b/server/docs/patterns.md new file mode 100644 index 0000000000..49b97c6ac0 --- /dev/null +++ b/server/docs/patterns.md @@ -0,0 +1,26 @@ +# Backend patterns + +The backend software patterns that we follow in Fleet. + +> NOTE: There are always exceptions to the rules, but we try to follow these patterns as much as possible unless a specific use case calls +> for something else. These should be discussed within the team and documented before merging. + +## MySQL + +Use high precision for all time fields. Precise timestamps make sure that we can accurately track when records were created and updated, +keep records in order with a reliable sort, and speed up testing by not having to wait for the time to +update. [MySQL reference](https://dev.mysql.com/doc/refman/8.4/en/date-and-time-type-syntax.html). [Backend sync where discussed](https://us-65885.app.gong.io/call?id=8041045095900447703). +Example: + +```sql +CREATE TABLE `sample` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `created_at` TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + `updated_at` TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), + PRIMARY KEY (`id`) +); +``` + +Do not use [goqu](https://github.com/doug-martin/goqu); use MySQL queries directly. Searching for, understanding, and debugging direct MySQL +queries is easier. If needing to modify an existing `goqu` query, try to rewrite it in +MySQL. [Backend sync where discussed](https://us-65885.app.gong.io/call?id=8041045095900447703). diff --git a/server/fleet/mdm.go b/server/fleet/mdm.go index 44b422f913..69df8e0323 100644 --- a/server/fleet/mdm.go +++ b/server/fleet/mdm.go @@ -3,7 +3,6 @@ package fleet import ( "bytes" "context" - "encoding/base64" "encoding/json" "fmt" "net/url" @@ -791,48 +790,6 @@ type TeamTuple struct { Name string `json:"name"` } -// ExtractToken extracts the metadata from the token as stored in the database, -// and returns the raw token that can be used directly with Apple's VPP API. If -// while extracting the token it notices that the metadata has changed, it will -// update t and return true as second return value, indicating that it changed -// and should be saved. -func (t *VPPTokenDB) ExtractToken() (rawAppleToken string, didUpdateMetadata bool, err error) { - var vppTokenData VPPTokenData - if err := json.Unmarshal([]byte(t.Token), &vppTokenData); err != nil { - return "", false, fmt.Errorf("unmarshaling VPP token data: %w", err) - } - - vppTokenRawBytes, err := base64.StdEncoding.DecodeString(vppTokenData.Token) - if err != nil { - return "", false, fmt.Errorf("decoding raw vpp token data: %w", err) - } - - var vppTokenRaw VPPTokenRaw - if err := json.Unmarshal(vppTokenRawBytes, &vppTokenRaw); err != nil { - return "", false, fmt.Errorf("unmarshaling raw vpp token data: %w", err) - } - - exp, err := time.Parse("2006-01-02T15:04:05Z0700", vppTokenRaw.ExpDate) - if err != nil { - return "", false, fmt.Errorf("parsing vpp token expiration date: %w", err) - } - - if vppTokenData.Location != t.Location { - t.Location = vppTokenData.Location - didUpdateMetadata = true - } - if vppTokenRaw.OrgName != t.OrgName { - t.OrgName = vppTokenRaw.OrgName - didUpdateMetadata = true - } - if !exp.Equal(t.RenewDate) { - t.RenewDate = exp.UTC() - didUpdateMetadata = true - } - - return vppTokenRaw.Token, didUpdateMetadata, nil -} - type NullTeamType string const ( diff --git a/server/mdm/apple/commander.go b/server/mdm/apple/commander.go index 68bd90c82e..6a098a5284 100644 --- a/server/mdm/apple/commander.go +++ b/server/mdm/apple/commander.go @@ -5,6 +5,8 @@ import ( "encoding/base64" "fmt" "net/http" + "sort" + "strings" "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" "github.com/fleetdm/fleet/v4/server/fleet" @@ -383,14 +385,15 @@ func (svc *MDMAppleCommander) SendNotifications(ctx context.Context, hostUUIDs [ // Even if we didn't get an error, some of the APNs // responses might have failed, signal that to the caller. - var failed []string + failed := map[string]error{} for uuid, response := range apnsResponses { if response.Err != nil { - failed = append(failed, uuid) + failed[uuid] = response.Err } } + if len(failed) > 0 { - return &APNSDeliveryError{FailedUUIDs: failed, Err: err} + return &APNSDeliveryError{errorsByUUID: failed} } return nil @@ -399,14 +402,38 @@ func (svc *MDMAppleCommander) SendNotifications(ctx context.Context, hostUUIDs [ // APNSDeliveryError records an error and the associated host UUIDs in which it // occurred. type APNSDeliveryError struct { - FailedUUIDs []string - Err error + errorsByUUID map[string]error } func (e *APNSDeliveryError) Error() string { - return fmt.Sprintf("APNS delivery failed with: %s, for UUIDs: %v", e.Err, e.FailedUUIDs) + var uuids []string + for uuid := range e.errorsByUUID { + uuids = append(uuids, uuid) + } + + // sort UUIDs alphabetically for deterministic output + sort.Strings(uuids) + + var errStrings []string + for _, uuid := range uuids { + errStrings = append(errStrings, fmt.Sprintf("UUID: %s, Error: %v", uuid, e.errorsByUUID[uuid])) + } + + return fmt.Sprintf( + "APNS delivery failed with the following errors:\n%s", + strings.Join(errStrings, "\n"), + ) } -func (e *APNSDeliveryError) Unwrap() error { return e.Err } +func (e *APNSDeliveryError) FailedUUIDs() []string { + var uuids []string + for uuid := range e.errorsByUUID { + uuids = append(uuids, uuid) + } + + // sort UUIDs alphabetically for deterministic output + sort.Strings(uuids) + return uuids +} func (e *APNSDeliveryError) StatusCode() int { return http.StatusBadGateway } diff --git a/server/mdm/apple/commander_test.go b/server/mdm/apple/commander_test.go index 5978944b52..0d21c66ab5 100644 --- a/server/mdm/apple/commander_test.go +++ b/server/mdm/apple/commander_test.go @@ -3,7 +3,9 @@ package apple_mdm import ( "context" "crypto/tls" + "errors" "fmt" + "net/http" "os" "testing" @@ -200,3 +202,50 @@ func mobileconfigForTest(name, identifier string) []byte { `, name, identifier, uuid.New().String())) } + +func TestAPNSDeliveryError(t *testing.T) { + tests := []struct { + name string + errorsByUUID map[string]error + expectedError string + expectedFailedUUIDs []string + expectedStatusCode int + }{ + { + name: "single error", + errorsByUUID: map[string]error{ + "uuid1": errors.New("network error"), + }, + expectedError: `APNS delivery failed with the following errors: +UUID: uuid1, Error: network error`, + expectedFailedUUIDs: []string{"uuid1"}, + expectedStatusCode: http.StatusBadGateway, + }, + { + name: "multiple errors, sorted", + errorsByUUID: map[string]error{ + "uuid3": errors.New("timeout error"), + "uuid1": errors.New("network error"), + "uuid2": errors.New("certificate error"), + }, + expectedError: `APNS delivery failed with the following errors: +UUID: uuid1, Error: network error +UUID: uuid2, Error: certificate error +UUID: uuid3, Error: timeout error`, + expectedFailedUUIDs: []string{"uuid1", "uuid2", "uuid3"}, + expectedStatusCode: http.StatusBadGateway, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + apnsErr := &APNSDeliveryError{ + errorsByUUID: tt.errorsByUUID, + } + + require.Equal(t, tt.expectedError, apnsErr.Error()) + require.Equal(t, tt.expectedFailedUUIDs, apnsErr.FailedUUIDs()) + require.Equal(t, tt.expectedStatusCode, apnsErr.StatusCode()) + }) + } +} diff --git a/server/service/apple_mdm.go b/server/service/apple_mdm.go index 7d2e59ddce..3ca7d580ad 100644 --- a/server/service/apple_mdm.go +++ b/server/service/apple_mdm.go @@ -3149,7 +3149,7 @@ func SendPushesToPendingDevices( if err := commander.SendNotifications(ctx, uuids); err != nil { var apnsErr *apple_mdm.APNSDeliveryError if errors.As(err, &apnsErr) { - level.Info(logger).Log("msg", "failed to send APNs notification to some hosts", "host_uuids", apnsErr.FailedUUIDs) + level.Info(logger).Log("msg", "failed to send APNs notification to some hosts", "error", apnsErr.Error()) return nil } diff --git a/server/service/mdm.go b/server/service/mdm.go index db6ce00944..294d503d81 100644 --- a/server/service/mdm.go +++ b/server/service/mdm.go @@ -30,7 +30,6 @@ import ( "github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/mdm" apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple" - "github.com/fleetdm/fleet/v4/server/mdm/apple/vpp" "github.com/fleetdm/fleet/v4/server/mdm/assets" nanomdm "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm" "github.com/fleetdm/fleet/v4/server/ptr" @@ -567,13 +566,14 @@ func (svc *Service) enqueueAppleMDMCommand(ctx context.Context, rawXMLCmd []byte var apnsErr *apple_mdm.APNSDeliveryError var mysqlErr *mysql.MySQLError if errors.As(err, &apnsErr) { - if len(apnsErr.FailedUUIDs) < len(deviceIDs) { + failedUUIDs := apnsErr.FailedUUIDs() + if len(failedUUIDs) < len(deviceIDs) { // some hosts properly received the command, so return success, with the list // of failed uuids. return &fleet.CommandEnqueueResult{ CommandUUID: cmd.CommandUUID, RequestType: cmd.Command.RequestType, - FailedUUIDs: apnsErr.FailedUUIDs, + FailedUUIDs: failedUUIDs, }, nil } // push failed for all hosts @@ -2542,335 +2542,3 @@ func (svc *Service) DeleteMDMAppleAPNSCert(ctx context.Context) error { return svc.ds.SaveAppConfig(ctx, appCfg) } - -//////////////////////////////////////////////////////////////////////////////// -// POST /api/_version_/vpp_tokens -//////////////////////////////////////////////////////////////////////////////// - -type uploadVPPTokenRequest struct { - File *multipart.FileHeader -} - -func (uploadVPPTokenRequest) DecodeRequest(ctx context.Context, r *http.Request) (interface{}, error) { - decoded := uploadVPPTokenRequest{} - - err := r.ParseMultipartForm(512 * units.MiB) - if err != nil { - return nil, &fleet.BadRequestError{ - Message: "failed to parse multipart form", - InternalErr: err, - } - } - - if r.MultipartForm.File["token"] == nil || len(r.MultipartForm.File["token"]) == 0 { - return nil, &fleet.BadRequestError{ - Message: "token multipart field is required", - InternalErr: err, - } - } - - decoded.File = r.MultipartForm.File["token"][0] - - return &decoded, nil -} - -type uploadVPPTokenResponse struct { - Err error `json:"error,omitempty"` - Token *fleet.VPPTokenDB `json:"token,omitempty"` -} - -func (r uploadVPPTokenResponse) Status() int { return http.StatusAccepted } - -func (r uploadVPPTokenResponse) error() error { - return r.Err -} - -func uploadVPPTokenEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { - req := request.(*uploadVPPTokenRequest) - file, err := req.File.Open() - if err != nil { - return uploadVPPTokenResponse{Err: err}, nil - } - defer file.Close() - - tok, err := svc.UploadVPPToken(ctx, file) - if err != nil { - return uploadVPPTokenResponse{Err: err}, nil - } - - return uploadVPPTokenResponse{Token: tok}, nil -} - -func (svc *Service) UploadVPPToken(ctx context.Context, token io.ReadSeeker) (*fleet.VPPTokenDB, error) { - if err := svc.authz.Authorize(ctx, &fleet.AppleCSR{}, fleet.ActionWrite); err != nil { - return nil, err - } - - privateKey := svc.config.Server.PrivateKey - if testSetEmptyPrivateKey { - privateKey = "" - } - - if len(privateKey) == 0 { - return nil, ctxerr.New(ctx, "Couldn't upload content token. Missing required private key. Learn how to configure the private key here: https://fleetdm.com/learn-more-about/fleet-server-private-key") - } - - if token == nil { - return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("token", "Invalid token. Please provide a valid content token from Apple Business Manager.")) - } - - tokenBytes, err := io.ReadAll(token) - if err != nil { - return nil, ctxerr.Wrap(ctx, err, "reading VPP token") - } - - locName, err := vpp.GetConfig(string(tokenBytes)) - if err != nil { - var vppErr *vpp.ErrorResponse - if errors.As(err, &vppErr) { - // Per https://developer.apple.com/documentation/devicemanagement/app_and_book_management/app_and_book_management_legacy/interpreting_error_codes - if vppErr.ErrorNumber == 9622 { - return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("token", "Invalid token. Please provide a valid content token from Apple Business Manager.")) - } - } - return nil, ctxerr.Wrap(ctx, err, "validating VPP token with Apple") - } - - data := fleet.VPPTokenData{ - Token: string(tokenBytes), - Location: locName, - } - - tok, err := svc.ds.InsertVPPToken(ctx, &data) - if err != nil { - return nil, ctxerr.Wrap(ctx, err, "writing VPP token to db") - } - - if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), fleet.ActivityEnabledVPP{ - Location: locName, - }); err != nil { - return nil, ctxerr.Wrap(ctx, err, "create activity for upload VPP token") - } - - return tok, nil -} - -//////////////////////////////////////////////////// -// PATCH /api/_version_/fleet/vpp_tokens/%d/renew // -//////////////////////////////////////////////////// - -type patchVPPTokenRenewRequest struct { - ID uint `url:"id"` - File *multipart.FileHeader -} - -func (patchVPPTokenRenewRequest) DecodeRequest(ctx context.Context, r *http.Request) (interface{}, error) { - decoded := patchVPPTokenRenewRequest{} - - err := r.ParseMultipartForm(512 * units.MiB) - if err != nil { - return nil, &fleet.BadRequestError{ - Message: "failed to parse multipart form", - InternalErr: err, - } - } - - if r.MultipartForm.File["token"] == nil || len(r.MultipartForm.File["token"]) == 0 { - return nil, &fleet.BadRequestError{ - Message: "token multipart field is required", - InternalErr: err, - } - } - - decoded.File = r.MultipartForm.File["token"][0] - - return &decoded, nil -} - -type patchVPPTokenRenewResponse struct { - Err error `json:"error,omitempty"` - Token *fleet.VPPTokenDB `json:"token,omitempty"` -} - -func (r patchVPPTokenRenewResponse) Status() int { return http.StatusAccepted } - -func (r patchVPPTokenRenewResponse) error() error { - return r.Err -} - -func patchVPPTokenRenewEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { - req := request.(*patchVPPTokenRenewRequest) - file, err := req.File.Open() - if err != nil { - return patchVPPTokenRenewResponse{Err: err}, nil - } - defer file.Close() - - tok, err := svc.UpdateVPPToken(ctx, req.ID, file) - if err != nil { - return patchVPPTokenRenewResponse{Err: err}, nil - } - - return patchVPPTokenRenewResponse{Token: tok}, nil -} - -func (svc *Service) UpdateVPPToken(ctx context.Context, tokenID uint, token io.ReadSeeker) (*fleet.VPPTokenDB, error) { - if err := svc.authz.Authorize(ctx, &fleet.AppleCSR{}, fleet.ActionWrite); err != nil { - return nil, err - } - - privateKey := svc.config.Server.PrivateKey - if testSetEmptyPrivateKey { - privateKey = "" - } - - if len(privateKey) == 0 { - return nil, ctxerr.New(ctx, "Couldn't upload content token. Missing required private key. Learn how to configure the private key here: https://fleetdm.com/learn-more-about/fleet-server-private-key") - } - - if token == nil { - return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("token", "Invalid token. Please provide a valid content token from Apple Business Manager.")) - } - - tokenBytes, err := io.ReadAll(token) - if err != nil { - return nil, ctxerr.Wrap(ctx, err, "reading VPP token") - } - - locName, err := vpp.GetConfig(string(tokenBytes)) - if err != nil { - var vppErr *vpp.ErrorResponse - if errors.As(err, &vppErr) { - // Per https://developer.apple.com/documentation/devicemanagement/app_and_book_management/app_and_book_management_legacy/interpreting_error_codes - if vppErr.ErrorNumber == 9622 { - return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("token", "Invalid token. Please provide a valid content token from Apple Business Manager.")) - } - } - return nil, ctxerr.Wrap(ctx, err, "validating VPP token with Apple") - } - - data := fleet.VPPTokenData{ - Token: string(tokenBytes), - Location: locName, - } - - tok, err := svc.ds.UpdateVPPToken(ctx, tokenID, &data) - if err != nil { - return nil, ctxerr.Wrap(ctx, err, "updating vpp token") - } - - return tok, nil -} - -//////////////////////////////////////////////////// -// PATCH /api/_version_/fleet/vpp_tokens/%d/teams // -//////////////////////////////////////////////////// - -type patchVPPTokensTeamsRequest struct { - ID uint `url:"id"` - TeamIDs []uint `json:"teams"` -} - -type patchVPPTokensTeamsResponse struct { - Token *fleet.VPPTokenDB `json:"token,omitempty"` - Err error `json:"error,omitempty"` -} - -func (r patchVPPTokensTeamsResponse) error() error { return r.Err } - -func patchVPPTokensTeams(ctx context.Context, request any, svc fleet.Service) (errorer, error) { - req := request.(*patchVPPTokensTeamsRequest) - - tok, err := svc.UpdateVPPTokenTeams(ctx, req.ID, req.TeamIDs) - if err != nil { - return patchVPPTokensTeamsResponse{Err: err}, nil - } - return patchVPPTokensTeamsResponse{Token: tok}, nil -} - -func (svc *Service) UpdateVPPTokenTeams(ctx context.Context, tokenID uint, teamIDs []uint) (*fleet.VPPTokenDB, error) { - if err := svc.authz.Authorize(ctx, &fleet.AppleCSR{}, fleet.ActionWrite); err != nil { - return nil, err - } - - tok, err := svc.ds.UpdateVPPTokenTeams(ctx, tokenID, teamIDs) - if err != nil { - return nil, ctxerr.Wrap(ctx, err, "updating vpp token team") - } - - return tok, nil -} - -/////////////////////////////////////////////// -// DELETE /api/_version_/fleet/vpp_tokens/%d // -/////////////////////////////////////////////// - -type getVPPTokensRequest struct{} - -type getVPPTokensResponse struct { - Tokens []*fleet.VPPTokenDB `json:"vpp_tokens"` - Err error `json:"error,omitempty"` -} - -func (r getVPPTokensResponse) error() error { return r.Err } - -func getVPPTokens(ctx context.Context, request any, svc fleet.Service) (errorer, error) { - tokens, err := svc.GetVPPTokens(ctx) - if err != nil { - return getVPPTokensResponse{Err: err}, nil - } - - if tokens == nil { - tokens = []*fleet.VPPTokenDB{} - } - - return getVPPTokensResponse{Tokens: tokens}, nil -} - -func (svc *Service) GetVPPTokens(ctx context.Context) ([]*fleet.VPPTokenDB, error) { - if err := svc.authz.Authorize(ctx, &fleet.AppleCSR{}, fleet.ActionRead); err != nil { - return nil, err - } - - return svc.ds.ListVPPTokens(ctx) -} - -type deleteVPPTokenRequest struct { - ID uint `url:"id"` -} - -type deleteVPPTokenResponse struct { - Err error `json:"error,omitempty"` -} - -func (r deleteVPPTokenResponse) error() error { return r.Err } - -func (r deleteVPPTokenResponse) Status() int { return http.StatusNoContent } - -func deleteVPPToken(ctx context.Context, request any, svc fleet.Service) (errorer, error) { - req := request.(*deleteVPPTokenRequest) - - err := svc.DeleteVPPToken(ctx, req.ID) - if err != nil { - return deleteVPPTokenResponse{Err: err}, nil - } - - return deleteVPPTokenResponse{}, nil -} - -func (svc *Service) DeleteVPPToken(ctx context.Context, tokenID uint) error { - if err := svc.authz.Authorize(ctx, &fleet.AppleCSR{}, fleet.ActionWrite); err != nil { - return err - } - tok, err := svc.ds.GetVPPToken(ctx, tokenID) - if err != nil { - return ctxerr.Wrap(ctx, err, "getting vpp token") - } - if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), fleet.ActivityDisabledVPP{ - Location: tok.Location, - }); err != nil { - return ctxerr.Wrap(ctx, err, "create activity for delete VPP token") - } - - return svc.ds.DeleteVPPToken(ctx, tokenID) -} diff --git a/server/service/schedule/schedule.go b/server/service/schedule/schedule.go index 8965416a64..7ca865416a 100644 --- a/server/service/schedule/schedule.go +++ b/server/service/schedule/schedule.go @@ -167,7 +167,13 @@ func (s *Schedule) Start() { level.Error(s.logger).Log("err", "start schedule", "details", err) ctxerr.Handle(s.ctx, err) } - s.setIntervalStartedAt(prevScheduledRun.CreatedAt) + + // if there is no previous run, set the start time to the current time. + startedAt := prevScheduledRun.CreatedAt + if startedAt.IsZero() { + startedAt = time.Now() + } + s.setIntervalStartedAt(startedAt) initialWait := 10 * time.Second if schedInterval := s.getSchedInterval(); schedInterval < initialWait { diff --git a/server/service/vpp.go b/server/service/vpp.go index 04b1ac57a3..c2e25eddc0 100644 --- a/server/service/vpp.go +++ b/server/service/vpp.go @@ -2,7 +2,11 @@ package service import ( "context" + "io" + "mime/multipart" + "net/http" + "github.com/docker/go-units" "github.com/fleetdm/fleet/v4/server/fleet" ) @@ -73,3 +77,239 @@ func (svc *Service) AddAppStoreApp(ctx context.Context, _ *uint, _ fleet.VPPAppT return fleet.ErrMissingLicense } + +//////////////////////////////////////////////////////////////////////////////// +// POST /api/_version_/vpp_tokens +//////////////////////////////////////////////////////////////////////////////// + +type uploadVPPTokenRequest struct { + File *multipart.FileHeader +} + +func (uploadVPPTokenRequest) DecodeRequest(ctx context.Context, r *http.Request) (interface{}, error) { + decoded := uploadVPPTokenRequest{} + + err := r.ParseMultipartForm(512 * units.MiB) + if err != nil { + return nil, &fleet.BadRequestError{ + Message: "failed to parse multipart form", + InternalErr: err, + } + } + + if r.MultipartForm.File["token"] == nil || len(r.MultipartForm.File["token"]) == 0 { + return nil, &fleet.BadRequestError{ + Message: "token multipart field is required", + InternalErr: err, + } + } + + decoded.File = r.MultipartForm.File["token"][0] + + return &decoded, nil +} + +type uploadVPPTokenResponse struct { + Err error `json:"error,omitempty"` + Token *fleet.VPPTokenDB `json:"token,omitempty"` +} + +func (r uploadVPPTokenResponse) Status() int { return http.StatusAccepted } + +func (r uploadVPPTokenResponse) error() error { + return r.Err +} + +func uploadVPPTokenEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { + req := request.(*uploadVPPTokenRequest) + file, err := req.File.Open() + if err != nil { + return uploadVPPTokenResponse{Err: err}, nil + } + defer file.Close() + + tok, err := svc.UploadVPPToken(ctx, file) + if err != nil { + return uploadVPPTokenResponse{Err: err}, nil + } + + return uploadVPPTokenResponse{Token: tok}, nil +} + +func (svc *Service) UploadVPPToken(ctx context.Context, file io.ReadSeeker) (*fleet.VPPTokenDB, error) { + // skipauth: No authorization check needed due to implementation returning + // only license error. + svc.authz.SkipAuthorization(ctx) + + return nil, fleet.ErrMissingLicense +} + +//////////////////////////////////////////////////// +// PATCH /api/_version_/fleet/vpp_tokens/%d/renew // +//////////////////////////////////////////////////// + +type patchVPPTokenRenewRequest struct { + ID uint `url:"id"` + File *multipart.FileHeader +} + +func (patchVPPTokenRenewRequest) DecodeRequest(ctx context.Context, r *http.Request) (interface{}, error) { + decoded := patchVPPTokenRenewRequest{} + + err := r.ParseMultipartForm(512 * units.MiB) + if err != nil { + return nil, &fleet.BadRequestError{ + Message: "failed to parse multipart form", + InternalErr: err, + } + } + + if r.MultipartForm.File["token"] == nil || len(r.MultipartForm.File["token"]) == 0 { + return nil, &fleet.BadRequestError{ + Message: "token multipart field is required", + InternalErr: err, + } + } + + decoded.File = r.MultipartForm.File["token"][0] + + return &decoded, nil +} + +type patchVPPTokenRenewResponse struct { + Err error `json:"error,omitempty"` + Token *fleet.VPPTokenDB `json:"token,omitempty"` +} + +func (r patchVPPTokenRenewResponse) Status() int { return http.StatusAccepted } + +func (r patchVPPTokenRenewResponse) error() error { + return r.Err +} + +func patchVPPTokenRenewEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { + req := request.(*patchVPPTokenRenewRequest) + file, err := req.File.Open() + if err != nil { + return patchVPPTokenRenewResponse{Err: err}, nil + } + defer file.Close() + + tok, err := svc.UpdateVPPToken(ctx, req.ID, file) + if err != nil { + return patchVPPTokenRenewResponse{Err: err}, nil + } + + return patchVPPTokenRenewResponse{Token: tok}, nil +} + +func (svc *Service) UpdateVPPToken(ctx context.Context, tokenID uint, token io.ReadSeeker) (*fleet.VPPTokenDB, error) { + // skipauth: No authorization check needed due to implementation returning + // only license error. + svc.authz.SkipAuthorization(ctx) + + return nil, fleet.ErrMissingLicense +} + +//////////////////////////////////////////////////// +// PATCH /api/_version_/fleet/vpp_tokens/%d/teams // +//////////////////////////////////////////////////// + +type patchVPPTokensTeamsRequest struct { + ID uint `url:"id"` + TeamIDs []uint `json:"teams"` +} + +type patchVPPTokensTeamsResponse struct { + Token *fleet.VPPTokenDB `json:"token,omitempty"` + Err error `json:"error,omitempty"` +} + +func (r patchVPPTokensTeamsResponse) error() error { return r.Err } + +func patchVPPTokensTeams(ctx context.Context, request any, svc fleet.Service) (errorer, error) { + req := request.(*patchVPPTokensTeamsRequest) + + tok, err := svc.UpdateVPPTokenTeams(ctx, req.ID, req.TeamIDs) + if err != nil { + return patchVPPTokensTeamsResponse{Err: err}, nil + } + return patchVPPTokensTeamsResponse{Token: tok}, nil +} + +func (svc *Service) UpdateVPPTokenTeams(ctx context.Context, tokenID uint, teamIDs []uint) (*fleet.VPPTokenDB, error) { + // skipauth: No authorization check needed due to implementation returning + // only license error. + svc.authz.SkipAuthorization(ctx) + + return nil, fleet.ErrMissingLicense +} + +///////////////////////////////////////// +// GET /api/_version_/fleet/vpp_tokens // +///////////////////////////////////////// + +type getVPPTokensRequest struct{} + +type getVPPTokensResponse struct { + Tokens []*fleet.VPPTokenDB `json:"vpp_tokens"` + Err error `json:"error,omitempty"` +} + +func (r getVPPTokensResponse) error() error { return r.Err } + +func getVPPTokens(ctx context.Context, request any, svc fleet.Service) (errorer, error) { + tokens, err := svc.GetVPPTokens(ctx) + if err != nil { + return getVPPTokensResponse{Err: err}, nil + } + + if tokens == nil { + tokens = []*fleet.VPPTokenDB{} + } + + return getVPPTokensResponse{Tokens: tokens}, nil +} + +func (svc *Service) GetVPPTokens(ctx context.Context) ([]*fleet.VPPTokenDB, error) { + // skipauth: No authorization check needed due to implementation returning + // only license error. + svc.authz.SkipAuthorization(ctx) + + return nil, fleet.ErrMissingLicense +} + +/////////////////////////////////////////////// +// DELETE /api/_version_/fleet/vpp_tokens/%d // +/////////////////////////////////////////////// + +type deleteVPPTokenRequest struct { + ID uint `url:"id"` +} + +type deleteVPPTokenResponse struct { + Err error `json:"error,omitempty"` +} + +func (r deleteVPPTokenResponse) error() error { return r.Err } + +func (r deleteVPPTokenResponse) Status() int { return http.StatusNoContent } + +func deleteVPPToken(ctx context.Context, request any, svc fleet.Service) (errorer, error) { + req := request.(*deleteVPPTokenRequest) + + err := svc.DeleteVPPToken(ctx, req.ID) + if err != nil { + return deleteVPPTokenResponse{Err: err}, nil + } + + return deleteVPPTokenResponse{}, nil +} + +func (svc *Service) DeleteVPPToken(ctx context.Context, tokenID uint) error { + // skipauth: No authorization check needed due to implementation returning + // only license error. + svc.authz.SkipAuthorization(ctx) + + return fleet.ErrMissingLicense +} diff --git a/server/test/mdm.go b/server/test/mdm.go index 0bcd17d258..df59cb2e6c 100644 --- a/server/test/mdm.go +++ b/server/test/mdm.go @@ -36,6 +36,14 @@ func CreateVPPTokenEncoded(expiration time.Time, orgName, location string) ([]by if err != nil { return nil, err } + return []byte(dataToken.Token), nil +} + +func CreateVPPTokenEncodedAfterMigration(expiration time.Time, orgName, location string) ([]byte, error) { + dataToken, err := CreateVPPTokenData(expiration, orgName, location) + if err != nil { + return nil, err + } dataTokenJson, err := json.Marshal(dataToken) if err != nil { diff --git a/server/worker/db_migrations.go b/server/worker/db_migrations.go index be5bc31762..88d0839d19 100644 --- a/server/worker/db_migrations.go +++ b/server/worker/db_migrations.go @@ -2,7 +2,10 @@ package worker import ( "context" + "encoding/base64" "encoding/json" + "fmt" + "time" "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" "github.com/fleetdm/fleet/v4/server/fleet" @@ -70,7 +73,7 @@ func (m *DBMigration) migrateVPPToken(ctx context.Context) error { return ctxerr.Wrap(ctx, err, "get VPP token to migrate") } - rawToken, didUpdate, err := tok.ExtractToken() + tokenData, didUpdate, err := extractVPPTokenFromMigration(tok) if err != nil { return ctxerr.Wrap(ctx, err, "extract VPP token metadata") } @@ -81,7 +84,47 @@ func (m *DBMigration) migrateVPPToken(ctx context.Context) error { m.Log.Log("info", "VPP token metadata was not updated") } - tokenData := fleet.VPPTokenData{Token: rawToken, Location: tok.Location} - _, err = m.Datastore.UpdateVPPToken(ctx, tok.ID, &tokenData) - return ctxerr.Wrap(ctx, err, "update VPP token") + if _, err := m.Datastore.UpdateVPPToken(ctx, tok.ID, tokenData); err != nil { + return ctxerr.Wrap(ctx, err, "update VPP token") + } + // the migated token should target "All teams" + _, err = m.Datastore.UpdateVPPTokenTeams(ctx, tok.ID, []uint{}) + return ctxerr.Wrap(ctx, err, "update VPP token teams") +} + +func extractVPPTokenFromMigration(migratedToken *fleet.VPPTokenDB) (tokData *fleet.VPPTokenData, didUpdateMetadata bool, err error) { + var vppTokenData fleet.VPPTokenData + if err := json.Unmarshal([]byte(migratedToken.Token), &vppTokenData); err != nil { + return nil, false, fmt.Errorf("unmarshaling VPP token data: %w", err) + } + + vppTokenRawBytes, err := base64.StdEncoding.DecodeString(vppTokenData.Token) + if err != nil { + return nil, false, fmt.Errorf("decoding raw vpp token data: %w", err) + } + + var vppTokenRaw fleet.VPPTokenRaw + if err := json.Unmarshal(vppTokenRawBytes, &vppTokenRaw); err != nil { + return nil, false, fmt.Errorf("unmarshaling raw vpp token data: %w", err) + } + + exp, err := time.Parse("2006-01-02T15:04:05Z0700", vppTokenRaw.ExpDate) + if err != nil { + return nil, false, fmt.Errorf("parsing vpp token expiration date: %w", err) + } + + if vppTokenData.Location != migratedToken.Location { + migratedToken.Location = vppTokenData.Location + didUpdateMetadata = true + } + if vppTokenRaw.OrgName != migratedToken.OrgName { + migratedToken.OrgName = vppTokenRaw.OrgName + didUpdateMetadata = true + } + if !exp.Equal(migratedToken.RenewDate) { + migratedToken.RenewDate = exp.UTC() + didUpdateMetadata = true + } + + return &vppTokenData, didUpdateMetadata, nil } diff --git a/server/worker/db_migrations_test.go b/server/worker/db_migrations_test.go index 8faca12acd..1aeeb697e9 100644 --- a/server/worker/db_migrations_test.go +++ b/server/worker/db_migrations_test.go @@ -12,12 +12,11 @@ import ( "github.com/fleetdm/fleet/v4/server/test" kitlog "github.com/go-kit/log" "github.com/jmoiron/sqlx" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestDBMigrationsVPPToken(t *testing.T) { - // FIXME - t.Skip() ctx := context.Background() ds := mysql.CreateMySQLDS(t) @@ -38,7 +37,7 @@ func TestDBMigrationsVPPToken(t *testing.T) { // create the migrated token and enqueue the job expDate := time.Date(2024, 8, 27, 0, 0, 0, 0, time.UTC) - tok, err := test.CreateVPPTokenEncoded(expDate, "test-org", "test-loc") + tok, err := test.CreateVPPTokenEncodedAfterMigration(expDate, "test-org", "test-loc") require.NoError(t, err) encTok, err := mysql.EncryptWithPrivateKey(t, ds, tok) require.NoError(t, err) @@ -49,12 +48,10 @@ INSERT INTO vpp_tokens organization_name, location, renew_at, - token, - team_id, - null_team_type + token ) VALUES - ('', '', DATE('2000-01-01'), ?, NULL, 'allteams') + ('', '', DATE('2000-01-01'), ?) ` const insJob = ` @@ -93,7 +90,9 @@ VALUES (?, ?, ?, '', ?, ?, ?) // nothing more to run jobs, err := ds.GetQueuedJobs(ctx, 1, time.Now().UTC().Add(time.Minute)) // look in the future to catch any delayed job require.NoError(t, err) - require.Empty(t, jobs) + if !assert.Empty(t, jobs) { + t.Logf(">>> %#+v", jobs[0]) + } // token should've been updated vppTok, err := ds.GetVPPTokenByLocation(ctx, "test-loc") @@ -101,7 +100,7 @@ VALUES (?, ?, ?, '', ?, ?, ?) require.Equal(t, "test-org", vppTok.OrgName) require.Equal(t, "test-loc", vppTok.Location) require.Equal(t, expDate, vppTok.RenewDate) - require.Equal(t, string(tok), vppTok.Token) + require.Contains(t, string(tok), `"token":"`+vppTok.Token+`"`) // the DB-stored token is the "token" JSON field in the raw tok require.NotNil(t, vppTok.Teams) require.Len(t, vppTok.Teams, 0) diff --git a/tools/mdm/migration/mdmproxy/Dockerfile b/tools/mdm/migration/mdmproxy/Dockerfile index 5d3369304b..bfe5fb62f1 100644 --- a/tools/mdm/migration/mdmproxy/Dockerfile +++ b/tools/mdm/migration/mdmproxy/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.22.5-alpine3.20@sha256:8c9183f715b0b4eca05b8b3dbf59766aaedb41ec07477b132ee2891ac0110a07 +FROM golang:1.22.6-alpine3.20@sha256:1a478681b671001b7f029f94b5016aed984a23ad99c707f6a0ab6563860ae2f3 ARG TAG RUN apk update && apk add --no-cache git RUN git clone -b $TAG --depth=1 --no-tags --progress --no-recurse-submodules https://github.com/fleetdm/fleet.git && cd /go/fleet/tools/mdm/migration/mdmproxy && go build . diff --git a/tools/mdm/migration/mdmproxy/mdmproxy.go b/tools/mdm/migration/mdmproxy/mdmproxy.go index 723db4f19f..c59bf4b425 100644 --- a/tools/mdm/migration/mdmproxy/mdmproxy.go +++ b/tools/mdm/migration/mdmproxy/mdmproxy.go @@ -84,14 +84,6 @@ func (m *mdmProxy) handleProxy(w http.ResponseWriter, r *http.Request) { return } - if !strings.HasPrefix(r.URL.Path, "/mdm") { - if m.logSkipped { - log.Printf("Forbidden non-mdm request: %s %s", r.Method, r.URL.String()) - } - http.Error(w, "Forbidden", http.StatusForbidden) - return - } - // Send all micromdm repo requests to the existing server if strings.HasPrefix(r.URL.Path, "/repo") { log.Printf("%s %s -> Existing (Repo)", r.Method, r.URL.String()) @@ -100,6 +92,14 @@ func (m *mdmProxy) handleProxy(w http.ResponseWriter, r *http.Request) { } + if !strings.HasPrefix(r.URL.Path, "/mdm") { + if m.logSkipped { + log.Printf("Forbidden non-mdm request: %s %s", r.Method, r.URL.String()) + } + http.Error(w, "Forbidden", http.StatusForbidden) + return + } + // Read the body of the request body, err := io.ReadAll(r.Body) _ = r.Body.Close() diff --git a/website/api/controllers/webhooks/receive-from-github.js b/website/api/controllers/webhooks/receive-from-github.js index 3923a50a06..4dcbad029b 100644 --- a/website/api/controllers/webhooks/receive-from-github.js +++ b/website/api/controllers/webhooks/receive-from-github.js @@ -89,6 +89,8 @@ module.exports = { 'SFriendLee', 'ddribeiro', 'rebeccaui', + 'allenhouchins', + 'harrisonravazzolo', ]; let GREEN_LABEL_COLOR = 'C2E0C6';// « Used in multiple places below. (FUTURE: Use the "+" prefix for this instead of color. 2022-05-05) diff --git a/website/assets/styles/layout.less b/website/assets/styles/layout.less index 94e56945a8..6eefec24a9 100644 --- a/website/assets/styles/layout.less +++ b/website/assets/styles/layout.less @@ -444,8 +444,9 @@ html, body { } } [purpose='gh-button'] { - margin-left: 20px; - margin-right: 20px; + padding: 0px 20px; + min-width: 140px; + width: 140px; } [purpose='header-dropdown'] { box-shadow: 0px 4px 40px rgba(0, 0, 0, 0.4); diff --git a/website/scripts/get-bug-and-pr-report.js b/website/scripts/get-bug-and-pr-report.js index 5894be6ecb..06791bf10a 100644 --- a/website/scripts/get-bug-and-pr-report.js +++ b/website/scripts/get-bug-and-pr-report.js @@ -33,8 +33,10 @@ module.exports = { let daysSinceReleasedBugsWereOpened = []; let allBugsWithUnreleasedLabel = []; let allBugsWithReleasedLabel = []; + let allBugs32DaysOrOlder = []; let allBugsCreatedInPastWeek = []; let allBugsClosedInPastWeek = []; + let allBugsReportedByCustomersInPastWeek = []; let daysSincePullRequestsWereOpened = []; let daysSinceContributorPullRequestsWereOpened = []; let commitToMergeTimesInDays = []; @@ -44,7 +46,7 @@ module.exports = { let allNonPublicOpenPrs = []; let nonPublicPrsClosedInThePastThreeWeeks = []; - // Product group KPIS + // Endpoint operations let allBugsCreatedInPastWeekEndpointOps = []; @@ -103,8 +105,16 @@ module.exports = { let timeOpenInMS = Math.abs(todaysDate - issueOpenedOn); // Convert the miliseconds to days and add the value to the daysSinceBugsWereOpened array let timeOpenInDays = timeOpenInMS / ONE_DAY_IN_MILLISECONDS; + if (timeOpenInDays >= 32) { + allBugs32DaysOrOlder.push(issue); + } if (timeOpenInDays <= 7) { + // All bugs in past week allBugsCreatedInPastWeek.push(issue); + // Customer-reported bugs + if (issue.labels.some(label => label.name.indexOf('customer-') >= 0)) { + allBugsReportedByCustomersInPastWeek.push(issue); + } // Get Endpoint Ops KPIs if (issue.labels.some(label => label.name === '#g-endpoint-ops')) { allBugsCreatedInPastWeekEndpointOps.push(issue); @@ -132,6 +142,7 @@ module.exports = { } } } + daysSinceBugsWereOpened.push(timeOpenInDays); // Send to released or unreleased bugs array if (issue.labels.some(label => label.name === '~unreleased bug')) { @@ -316,8 +327,8 @@ module.exports = { // async()=>{ - // Fetch confidential and classified PRs (current open, and recent closed) - for (let repoName of ['classified', 'confidential']) { + // Fetch confidential PRs (current open, and recent closed) + for (let repoName of ['confidential']) { // [?] https://docs.github.com/en/free-pro-team@latest/rest/pulls/pulls#list-pull-requests let openPrs = await sails.helpers.http.get(`https://api.github.com/repos/fleetdm/${encodeURIComponent(repoName)}/pulls`, { state: 'open', @@ -380,25 +391,12 @@ module.exports = { // NOTE: If order of the KPI sheets columns changes, the order values are pushed into this array needs to change, as well. kpiResults.push( averageDaysContributorPullRequestsAreOpenFor, - daysSinceContributorPullRequestsWereOpened.length, - averageDaysPullRequestsAreOpenFor, - daysSincePullRequestsWereOpened.length, + allBugs32DaysOrOlder.length, + allBugsReportedByCustomersInPastWeek.length, averageNumberOfDaysReleasedBugsAreOpenFor, averageNumberOfDaysUnreleasedBugsAreOpenFor, - allBugsClosedInPastWeek.length, - averageNumberOfDaysBugsAreOpenFor, allBugsCreatedInPastWeek.length, - allBugsCreatedInPastWeekEndpointOps.length, - allBugsCreatedInPastWeekEndpointOpsCustomerImpacting.length, - allBugsCreatedInPastWeekEndpointOpsReleased.length, - allBugsCreatedInPastWeekEndpointOpsUnreleased.length, - allBugsCreatedInPastWeekMobileDeviceManagement.length, - allBugsCreatedInPastWeekMobileDeviceManagementCustomerImpacting.length, - allBugsCreatedInPastWeekMobileDeviceManagementReleased.length, - allBugsCreatedInPastWeekMobileDeviceManagementUnreleased.length, - daysSinceBugsWereOpened.length, - allBugsWithReleasedLabel.length, - allBugsWithUnreleasedLabel.length); + allBugsClosedInPastWeek.length,); // Log the results sails.log(` @@ -407,17 +405,19 @@ module.exports = { --------------------------- ${kpiResults.join(',')} - Note: Copy the values above, then in Google sheets paste them into a cell and select "Split text to columns" to paste the values into separate cells. + Note: Copy the values above, then paste into Google KPI sheet and select "Split text to columns" to split the values into separate columns. Pull requests: --------------------------- Average open time (no bots, no handbook, no ceo): ${averageDaysContributorPullRequestsAreOpenFor} days. + Number of open pull requests in the fleetdm/fleet Github repo (no bots, no handbook, no ceo): ${daysSinceContributorPullRequestsWereOpened.length} Average open time (all PRs): ${averageDaysPullRequestsAreOpenFor} days. + Number of open pull requests in the fleetdm/fleet Github repo: ${daysSincePullRequestsWereOpened.length} - Bugs (part 1): + Bugs: --------------------------- Average open time (released bugs): ${averageNumberOfDaysReleasedBugsAreOpenFor} days. @@ -429,6 +429,12 @@ module.exports = { Number of issues with the "bug" label opened in the past week: ${allBugsCreatedInPastWeek.length} + Number of open issues with the "bug" label in fleetdm/fleet: ${daysSinceBugsWereOpened.length} + + Number of open issues with the "~released bug" label in fleetdm/fleet: ${allBugsWithReleasedLabel.length} + + Number of open issues with the "~unreleased bug" label in fleetdm/fleet: ${allBugsWithUnreleasedLabel.length} + Endpoint Operations: --------------------------- Number of issues with the "#g-endpoint-ops" and "bug" labels opened in the past week: ${allBugsCreatedInPastWeekEndpointOps.length} @@ -449,17 +455,10 @@ module.exports = { Number of issues with the "#g-mdm", "bug", and "~unreleased bug" labels opened in the past week: ${allBugsCreatedInPastWeekMobileDeviceManagementUnreleased.length} - Bugs (part 2): - --------------------------- - Number of open issues with the "bug" label in fleetdm/fleet: ${daysSinceBugsWereOpened.length} - - Number of open issues with the "~released bug" label in fleetdm/fleet: ${allBugsWithReleasedLabel.length} - - Number of open issues with the "~unreleased bug" label in fleetdm/fleet: ${allBugsWithUnreleasedLabel.length} - Pull requests requiring CEO review --------------------------------------- Number of open ~ceo pull requests in the fleetdm Github org: ${ceoDependentOpenPrs.length} + Average open time (~ceo PRs): ${Math.round(ceoDependentPrOpenTime*100)/100} days. `);