diff --git a/.github/workflows/deploy-fleet-website.yml b/.github/workflows/deploy-fleet-website.yml index 9dfe4c2f62..683c406eb0 100644 --- a/.github/workflows/deploy-fleet-website.yml +++ b/.github/workflows/deploy-fleet-website.yml @@ -7,7 +7,7 @@ on: jobs: build: if: ${{ github.repository == 'fleetdm/fleet' }} - + runs-on: ubuntu-latest strategy: @@ -49,14 +49,15 @@ jobs: # Run sanity checks - run: cd website/ && npm test - # Compile assets + # Compile browser assets & markdown content into generated collateral - run: cd website/ && npm run build-for-prod - # Commit newly-built assets locally so we can push them to Heroku below. + # Commit newly-generated collateral locally so we can push them to Heroku below. # (This commit will never be pushed to GitHub- only to Heroku.) # > The local config flags make this work in GitHub's environment. - run: git add website/.www - - run: git -c "user.name=Fleetwood" -c "user.email=github@example.com" commit -am 'AUTOMATED COMMIT - Deployed the latest, including modified HTML layouts and .sailsrc file that reference minified assets.' + - run: git add -f website/views/partials/built-from-markdown # « for new HTML files compiled from markdown content + - run: git -c "user.name=Fleetwood" -c "user.email=github@example.com" commit -am 'AUTOMATED COMMIT - Deployed the latest, including generated collateral such as compiled documentation, modified HTML layouts, and a .sailsrc file that references minified client-side code assets.' # Configure the Heroku app we'll be deploying to - run: heroku git:remote -a production-fleetdm-website diff --git a/.github/workflows/markdown-link-check-config.json b/.github/workflows/markdown-link-check-config.json index 470c4b521d..e7d33f6cbf 100644 --- a/.github/workflows/markdown-link-check-config.json +++ b/.github/workflows/markdown-link-check-config.json @@ -23,6 +23,9 @@ }, { "pattern": "github.com/fleetdm/confidential" + }, + { + "pattern": "osquery.io/schema" } ], "retryOn429": true, diff --git a/.gitignore b/.gitignore index fe0513dee2..20f75d9f03 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,7 @@ cypress/downloads # Fleet local development DB backups backup.sql.gz + +# Common mistake for new developers to run npm install and then end up +# committing a package-lock.json. Fleet app uses Yarn with yarn.lock. +package-lock.json diff --git a/CHANGELOG.md b/CHANGELOG.md index d4d34818ed..f0ecea52e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,22 @@ +## Fleet 3.12.0 (May 19, 2021) + +* Add scheduled queries to the _Host details_ page. Surface the "Name", "Description", "Frequency", and "Last run" information for each query in a pack that apply to a specific host. + +* Improve the freshness of host vitals by adding the ability to "refetch" the data on the _Host details_ page. + +* Add ability to copy log fields into Google Cloud Pub/Sub attributes. This allows users to use these values for subscription filters. + +* Add ability to duplicate live query results in Redis. When the `redis_duplicate_results` configuration option is set to `true`, all live query results will be copied to an additional Redis Pub/Sub channel named LQDuplicate. + +* Add ability to controls the server-side HTTP keepalive property. Turning off keepalives has helped reduce outstanding TCP connections in some deployments. + +* Fix an issue on the _Packs_ page in which Fleet would incorrectly handle the configured `server_url_prefix`. + ## Fleet 3.11.0 (Apr 28, 2021) * Improve Fleet performance by batch updating host seen time instead of updating synchronously. This improvement reduces MySQL CPU usage by ~33% with 4,000 simulated hosts and MySQL running in Docker. -* Add support for software inventory, introducing a list of installed software items on each host's respective _Host details_ page. This feature is flagged off by default (for now). Check out [the feature flag documentation for instructions on how to turn this feature on](./docs/2-Deployment/2-Configuration.md#software-inventory). +* Add support for software inventory, introducing a list of installed software items on each host's respective _Host details_ page. This feature is flagged off by default (for now). Check out [the feature flag documentation for instructions on how to turn this feature on](https://github.com/fleetdm/fleet/blob/master/docs/3-Deployment/2-Configuration.md#software-inventory). * Add Windows support for `fleetctl` agent autoupdates. The `fleetctl updates` command provides the ability to self-manage an agent update server. Available for Fleet Basic customers. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fe194fc8d6..64b8de7bde 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -33,3 +33,23 @@ To open your browser's **network requests**, press Control Shift J (Windows, Lin Sensitive security-related issues should be reported to [security@fleetdm.com](mailto:security@fleetdm.com) before a public issue is made. + +## Contributing to documentation + +Fleet currently uses GitHub as the central location for documentation. + +### Markdown links + +Due to the structure of the Fleet documentation and GitHub's unique markdown files, there are several practices we'd like to call out if you're documentation changes include links to other locations within the Fleet docs. + +#### Relative links + +When including a link to a different file in the Fleet documentation please use relative links when possible. + +For example, let's say you're working on changes in the Contribution docs and you'd like to add a link to the REST API docs. The relative link would look something like `../1-Using-Fleet/3-REST-API.md`. + +#### Special characters in anchor links + +There are certain characters GitHub doesn't support in the use of anchor links in markdown files. The general rule we've found is to only use a-z or A-Z characters in anchor links. All other characters should be removed. + +For example, consider the section title *How do I connect to the Mailhog simulated server?*. The valid GitHub anchor link for this section is #how-do-i-connect-to-the-mailhog-simulated-server. Notice the *?* character is removed. diff --git a/README.md b/README.md index 37e044d4d0..fc82ef88ca 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -

Fleet logo, landscape, dark text, transparent background

+

Fleet logo, landscape, dark text, transparent background

#### [Website](https://fleetdm.com/)   [News](http://twitter.com/fleetctl)   [Report a bug](https://github.com/fleetdm/fleet/issues/new) @@ -13,7 +13,7 @@ Fleet is the most widely used open source osquery manager. Deploying osquery wi ```bash # Install the Fleet command-line tool -npm install -g fleetctl +sudo npm install -g fleetctl # Run a local demo of the Fleet server sudo fleetctl preview ``` @@ -47,6 +47,10 @@ Documentation for Fleet can be found [here on GitHub](./docs/README.md). Please join us in the #fleet channel on [osquery Slack](https://osquery.slack.com/join/shared_invite/zt-h29zm0gk-s2DBtGUTW4CFel0f0IjTEw#/). +#### Contributing + +Contributions are welcome, whether you answer questions on Slack/GitHub/StackOverflow/Twitter, improve the documentation or website, write a tutorial, give a talk, start a local osquery meetup, troubleshoot reported issues, or [submit a patch](https://github.com/fleetdm/fleet/blob/master/CONTRIBUTING.md). The Fleet code of conduct is [on GitHub](https://github.com/fleetdm/fleet/blob/master/CODE_OF_CONDUCT.md). + #### Community projects Below are some projects created by Fleet community members. Please submit a pull request if you'd like your project featured. diff --git a/assets/images/icon-accordion-collapse-16x16@2x.png b/assets/images/icon-accordion-collapse-16x16@2x.png new file mode 100644 index 0000000000..1234f114dd Binary files /dev/null and b/assets/images/icon-accordion-collapse-16x16@2x.png differ diff --git a/assets/images/icon-accordion-collapse-black-16x16@2x.png b/assets/images/icon-accordion-collapse-black-16x16@2x.png new file mode 100644 index 0000000000..d89ec4e652 Binary files /dev/null and b/assets/images/icon-accordion-collapse-black-16x16@2x.png differ diff --git a/assets/images/icon-accordion-collapse-blue-16x16@2x.png b/assets/images/icon-accordion-collapse-blue-16x16@2x.png new file mode 100644 index 0000000000..d89ec4e652 Binary files /dev/null and b/assets/images/icon-accordion-collapse-blue-16x16@2x.png differ diff --git a/assets/images/icon-chevron-black-16x16@2x.png b/assets/images/icon-chevron-black-16x16@2x.png new file mode 100644 index 0000000000..bc37f8848b Binary files /dev/null and b/assets/images/icon-chevron-black-16x16@2x.png differ diff --git a/assets/images/icon-chevron-black-9x6@2x.png b/assets/images/icon-chevron-black-9x6@2x.png new file mode 100644 index 0000000000..58e9f47f74 Binary files /dev/null and b/assets/images/icon-chevron-black-9x6@2x.png differ diff --git a/assets/images/icon-chevron-blue-16x16@2x.png b/assets/images/icon-chevron-blue-16x16@2x.png new file mode 100644 index 0000000000..121e004fcf Binary files /dev/null and b/assets/images/icon-chevron-blue-16x16@2x.png differ diff --git a/assets/images/icon-chevron-blue-9x6@2x.png b/assets/images/icon-chevron-blue-9x6@2x.png new file mode 100644 index 0000000000..a633a5ec4b Binary files /dev/null and b/assets/images/icon-chevron-blue-9x6@2x.png differ diff --git a/assets/images/icon-chevron-down-9x6@2x.png b/assets/images/icon-chevron-down-9x6@2x.png new file mode 100644 index 0000000000..46c0151dfe Binary files /dev/null and b/assets/images/icon-chevron-down-9x6@2x.png differ diff --git a/assets/images/icon-chevron-purple-9x6@2x.png b/assets/images/icon-chevron-purple-9x6@2x.png new file mode 100644 index 0000000000..32f1662b7e Binary files /dev/null and b/assets/images/icon-chevron-purple-9x6@2x.png differ diff --git a/assets/images/icon-chevron-white-16x16@2x.png b/assets/images/icon-chevron-white-16x16@2x.png new file mode 100644 index 0000000000..1f6b4cfe43 Binary files /dev/null and b/assets/images/icon-chevron-white-16x16@2x.png differ diff --git a/assets/images/icon-chevron-white-9x6@2x.png b/assets/images/icon-chevron-white-9x6@2x.png new file mode 100644 index 0000000000..71f774b722 Binary files /dev/null and b/assets/images/icon-chevron-white-9x6@2x.png differ diff --git a/assets/images/icon-close-8x8@2x.png b/assets/images/icon-close-8x8@2x.png new file mode 100644 index 0000000000..22fc241309 Binary files /dev/null and b/assets/images/icon-close-8x8@2x.png differ diff --git a/assets/images/icon-close-fleet-blue-16x16@2x.png b/assets/images/icon-close-fleet-blue-16x16@2x.png new file mode 100644 index 0000000000..2e5c63aef2 Binary files /dev/null and b/assets/images/icon-close-fleet-blue-16x16@2x.png differ diff --git a/assets/images/icon-close-vibrant-blue-16x16@2x.png b/assets/images/icon-close-vibrant-blue-16x16@2x.png new file mode 100644 index 0000000000..fdaaae88d2 Binary files /dev/null and b/assets/images/icon-close-vibrant-blue-16x16@2x.png differ diff --git a/assets/images/icon-collapse-black-16x16@2x.png b/assets/images/icon-collapse-black-16x16@2x.png new file mode 100644 index 0000000000..d89ec4e652 Binary files /dev/null and b/assets/images/icon-collapse-black-16x16@2x.png differ diff --git a/assets/images/icon-collapse-blue-16x16@2x.png b/assets/images/icon-collapse-blue-16x16@2x.png new file mode 100644 index 0000000000..d89ec4e652 Binary files /dev/null and b/assets/images/icon-collapse-blue-16x16@2x.png differ diff --git a/assets/images/icon-plus-circle-16x16@2x.png b/assets/images/icon-plus-circle-16x16@2x.png new file mode 100644 index 0000000000..8f1b4475f6 Binary files /dev/null and b/assets/images/icon-plus-circle-16x16@2x.png differ diff --git a/assets/images/icon-refetch-12x12@2x.png b/assets/images/icon-refetch-12x12@2x.png new file mode 100644 index 0000000000..960ea80732 Binary files /dev/null and b/assets/images/icon-refetch-12x12@2x.png differ diff --git a/assets/images/icon-refetch-36x36@2x.png b/assets/images/icon-refetch-36x36@2x.png new file mode 100644 index 0000000000..185a15aa21 Binary files /dev/null and b/assets/images/icon-refetch-36x36@2x.png differ diff --git a/charts/fleet/Chart.yaml b/charts/fleet/Chart.yaml index ca00e5a206..f7b228edfc 100644 --- a/charts/fleet/Chart.yaml +++ b/charts/fleet/Chart.yaml @@ -4,8 +4,8 @@ name: fleet keywords: - fleet - osquery -version: 3.11.0 +version: 3.12.0 home: https://github.com/fleetdm/fleet sources: - https://github.com/fleetdm/fleet.git -appVersion: 3.11.0 +appVersion: 3.12.0 diff --git a/charts/fleet/values.yaml b/charts/fleet/values.yaml index 2ff8623990..8ee9bf995c 100644 --- a/charts/fleet/values.yaml +++ b/charts/fleet/values.yaml @@ -2,7 +2,7 @@ # All settings related to how Fleet is deployed in Kubernetes hostName: fleet.localhost replicas: 3 # The number of Fleet instances to deploy -imageTag: 3.11.0 # Version of Fleet to deploy +imageTag: 3.12.0 # Version of Fleet to deploy createIngress: true # Whether or not to automatically create an Ingress ingressAnnotations: {} # Additional annotation to add to the Ingress podAnnotations: {} # Additional annotations to add to the Fleet pod diff --git a/cmd/fleet/serve.go b/cmd/fleet/serve.go index 3c931cf5d6..bfd72636ca 100644 --- a/cmd/fleet/serve.go +++ b/cmd/fleet/serve.go @@ -216,7 +216,7 @@ the way that the Fleet server works. } redisPool := pubsub.NewRedisPool(config.Redis.Address, config.Redis.Password, config.Redis.Database, config.Redis.UseTLS) - resultStore := pubsub.NewRedisQueryResults(redisPool) + resultStore := pubsub.NewRedisQueryResults(redisPool, config.Redis.DuplicateResults) liveQueryStore := live_query.NewRedisLiveQuery(redisPool) ssoSessionStore := sso.NewSessionStore(redisPool) @@ -368,6 +368,7 @@ the way that the Fleet server works. IdleTimeout: 5 * time.Minute, MaxHeaderBytes: 1 << 18, // 0.25 MB (262144 bytes) } + srv.SetKeepAlivesEnabled(config.Server.Keepalive) errs := make(chan error, 2) go func() { if !config.Server.TLS { diff --git a/docs/1-Using-Fleet/2-fleetctl-CLI.md b/docs/1-Using-Fleet/2-fleetctl-CLI.md index 79c79773da..2ddba8aeaf 100644 --- a/docs/1-Using-Fleet/2-fleetctl-CLI.md +++ b/docs/1-Using-Fleet/2-fleetctl-CLI.md @@ -34,7 +34,7 @@ This guide illustrates: ### Running Fleet -For the sake of this tutorial, I will be using the local development Docker Compose infrastructure to run Fleet locally. This is documented in some detail in the [developer documentation](../3-Contribution/1-Building-Fleet.md#development-infrastructure), but the following are the minimal set of commands that you can run from the root of the repository (assuming that you have a working Go/JavaScript toolchain installed along with Docker Compose): +For the sake of this tutorial, I will be using the local development Docker Compose infrastructure to run Fleet locally. This is documented in some detail in the [developer documentation](../4-Contribution/1-Building-Fleet.md#development-infrastructure), but the following are the minimal set of commands that you can run from the root of the repository (assuming that you have a working Go/JavaScript toolchain installed along with Docker Compose): ``` docker-compose up -d @@ -80,7 +80,7 @@ It's possible to specify the password via the `--password` flag or the `$PASSWOR ### Connecting a host -For the sake of this tutorial, I'm going to be using Kolide's osquery launcher to start osquery locally and connect it to Fleet. To learn more about connecting osquery to Fleet, see the [Adding Hosts to Fleet](../2-Deployment/3-Adding-hosts.md) documentation. +For the sake of this tutorial, I'm going to be using Kolide's osquery launcher to start osquery locally and connect it to Fleet. To learn more about connecting osquery to Fleet, see the [Adding Hosts to Fleet](../3-Deployment/3-Adding-hosts.md) documentation. To get your osquery enroll secret, run the following: @@ -211,30 +211,7 @@ Note the token can also be set with `fleetctl config set --token`, but this may ## Using fleetctl for configuration -A Fleet configuration is defined using one or more declarative "messages" in yaml syntax. Each message can live in it's own file or multiple in one file, each separated by `---`. Each file/message contains a few required top-level keys: - -- `apiVersion` - the API version of the file/request -- `spec` - the "data" of the request -- `kind ` - the type of file/object (i.e.: pack, query, config) - -The file may optionally also include some `metadata` for more complex data types (i.e.: packs). - -When you reason about how to manage these config files, consider following the [General Config Tips](https://kubernetes.io/docs/concepts/configuration/overview/#general-config-tips) published by the Kubernetes project. Some of the especially relevant tips are included here as well: - -- When defining configurations, specify the latest stable API version. -- Configuration files should be stored in version control before being pushed to the cluster. This allows quick roll-back of a configuration if needed. It also aids with cluster re-creation and restoration if necessary. -- Group related objects into a single file whenever it makes sense. One file is often easier to manage than several. See the [config-single-file.yml](../../examples/config-single-file.yml) file as an example of this syntax. -- Don’t specify default values unnecessarily – simple and minimal configs will reduce errors. - -All of these files can be concatenated together into [one file](../../examples/config-single-file.yml) (separated by `---`), or they can be in [individual files with a directory structure](../../examples/config-many-files) like the following: - -``` -|-- config.yml -|-- labels.yml -|-- packs -| `-- osquery-monitoring.yml -`-- queries.yml -``` +A Fleet configuration is defined using one or more declarative "messages" in yaml syntax. Check out the [configuration files](./configuration-files/README.md) section of the documentation for example yaml files. ### Convert osquery JSON @@ -577,7 +554,7 @@ spec: Fleet supports osquery's file carving functionality as of Fleet 3.3.0. This allows the Fleet server to request files (and sets of files) from osquery agents, returning the full contents to Fleet. -File carving data can be either stored in Fleet's database or to an external S3 bucket. For information on how to configure the latter, consult the [configuration docs](../2-Deployment/2-Configuration.md#s3-file-carving-backend). +File carving data can be either stored in Fleet's database or to an external S3 bucket. For information on how to configure the latter, consult the [configuration docs](https://github.com/fleetdm/fleet/blob/master/docs/3-Deployment/2-Configuration.md#s3-file-carving-backend). ### Configuration diff --git a/docs/1-Using-Fleet/3-REST-API.md b/docs/1-Using-Fleet/3-REST-API.md index af8001efca..25e9e68018 100644 --- a/docs/1-Using-Fleet/3-REST-API.md +++ b/docs/1-Using-Fleet/3-REST-API.md @@ -1,4 +1,5 @@ # REST API + - [Overview](#overview) - [fleetctl](#fleetctl) - [Current API](#current-api) @@ -23,8 +24,8 @@ Fleet is powered by a Go API server which serves three types of endpoints: - All other endpoints are served by the React single page application bundle. The React app uses React Router to determine whether or not the URI is a valid route and what to do. - -Note: We have deprecated `/api/v1/kolide/` routes and will remove them in the Fleet 4.0 release. Please migrate all routes to `/api/v1/fleet/`. + +Note: We have deprecated `/api/v1/kolide/` routes and will remove them in the Fleet 4.0 release. Please migrate all routes to `/api/v1/fleet/`. ### fleetctl @@ -51,8 +52,6 @@ Queries, packs, scheduled queries, labels, invites, users, sessions all behave t All of these objects are put together and distributed to the appropriate osquery agents at the appropriate time. At this time, the best source of truth for the API is the [HTTP handler file](https://github.com/fleetdm/fleet/blob/master/server/service/handler.go) in the Go application. The REST API is exposed via a transport layer on top of an RPC service which is implemented using a micro-service library called [Go Kit](https://github.com/go-kit/kit). If using the Fleet API is important to you right now, being familiar with Go Kit would definitely be helpful. - - ## Authentication - [Log in](#log-in) @@ -94,7 +93,7 @@ Then, use that API token to authenticate all subsequent API requests by sending Authorization: Bearer ``` -> For SSO users, username/password login is disabled. The API token can instead be retrieved from the "Settings" page in the UI. +> For SSO users, username/password login is disabled. The API token can instead be retrieved from the "Settings" page in the UI. ### Log in @@ -263,11 +262,11 @@ Resets a user's password. Which user is determined by the password reset token u #### Parameters -| Name | Type | In | Description | -| ---- | ------- | ----- | ---------------------------- | -| new_password | string | body | **Required**. The new password. | -| new_password_confirmation | string | body | **Required**. Confirmation for the new password. | -| password_reset_token | string | body | **Required**. The token provided to the user in the password reset email. | +| Name | Type | In | Description | +| ------------------------- | ------ | ---- | ------------------------------------------------------------------------- | +| new_password | string | body | **Required**. The new password. | +| new_password_confirmation | string | body | **Required**. Confirmation for the new password. | +| password_reset_token | string | body | **Required**. The token provided to the user in the password reset email. | #### Example @@ -401,8 +400,8 @@ Gets the current SSO configuration. #### Parameters -| Name | Type | In | Description | -| --------- | ------ | ---- | -------------------------------------------------------------------------- | +| Name | Type | In | Description | +| --------- | ------ | ---- | --------------------------------------------------------------------------- | | relay_url | string | body | **Required**. The relative url to be navigated to after successful sign in. | #### Example @@ -445,8 +444,8 @@ This is the callback endpoint that the identity provider will use to send securi #### Parameters -| Name | Type | In | Description | -| --------- | ------ | ---- | -------------------------------------------------------------------------- | +| Name | Type | In | Description | +| ------------ | ------ | ---- | ----------------------------------------------------------- | | SAMLResponse | string | body | **Required**. The SAML response from the identity provider. | #### Example @@ -478,6 +477,7 @@ This is the callback endpoint that the identity provider will use to send securi - [Get host](#get-host) - [Get host by identifier](#get-host-by-identifier) - [Delete host](#delete-host) +- [Refetch host](#refetch-host) ### List hosts @@ -485,14 +485,14 @@ This is the callback endpoint that the identity provider will use to send securi #### Parameters -| Name | Type | In | Description | -| ----------------------- | ------- | ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| page | integer | query | Page number of the results to fetch. | -| per_page | integer | query | Results per page. | -| order_key | string | query | What to order results by. Can be any column in the hosts table. | -| order_direction | string | query | **Requires `order_key`**. The direction of the order given the order key. Options include `asc` and `desc`. Default is `asc`. | -| status | string | query | Indicates the status of the hosts to return. Can either be `new`, `online`, `offline`, or `mia`. | -| query | string | query | Search query keywords. Searchable fields include `hostname`, `machine_serial`, `uuid`, and `ipv4`. | +| Name | Type | In | Description | +| ----------------------- | ------- | ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| page | integer | query | Page number of the results to fetch. | +| per_page | integer | query | Results per page. | +| order_key | string | query | What to order results by. Can be any column in the hosts table. | +| order_direction | string | query | **Requires `order_key`**. The direction of the order given the order key. Options include `asc` and `desc`. Default is `asc`. | +| status | string | query | Indicates the status of the hosts to return. Can either be `new`, `online`, `offline`, or `mia`. | +| query | string | query | Search query keywords. Searchable fields include `hostname`, `machine_serial`, `uuid`, and `ipv4`. | | additional_info_filters | string | query | A comma-delimited list of fields to include in each host's additional information object. See [Fleet Configuration Options](https://github.com/fleetdm/fleet/blob/master/docs/1-Using-Fleet/2-fleetctl-CLI.md#fleet-configuration-options) for an example configuration with hosts' additional information. | #### Example @@ -571,7 +571,6 @@ None. `GET /api/v1/fleet/host_summary` - ##### Default response `Status: 200` @@ -587,23 +586,22 @@ None. ### Get host -Returns the information of the specified host. +Returns the information of the specified host. -The endpoint returns the host's installed `software` if the software inventory feature flag is turned on. This feature flag is turned off by default. [Check out the feature flag documentation](../2-Deployment/2-Configuration.md#feature-flags) for instructions on how to turn on the software inventory feature. +The endpoint returns the host's installed `software` if the software inventory feature flag is turned on. This feature flag is turned off by default. [Check out the feature flag documentation](../3-Deployment/2-Configuration.md#feature-flags) for instructions on how to turn on the software inventory feature. `GET /api/v1/fleet/hosts/{id}` #### Parameters -| Name | Type | In | Description | -| --------------------- | ------ | ---- | --------------------------------------------------------------- | -| id | integer | path | **Required**. The host's id. | +| Name | Type | In | Description | +| ---- | ------- | ---- | ---------------------------- | +| id | integer | path | **Required**. The host's id. | #### Example `GET /api/v1/fleet/hosts/121` - ##### Default response `Status: 200` @@ -704,15 +702,14 @@ Returns the information of the host specified using the `uuid`, `osquery_host_id #### Parameters -| Name | Type | In | Description | -| --------------------- | ------ | ---- | --------------------------------------------------------------- | -| identifier | integer or string | path | **Required**. The host's `uuid`, `osquery_host_id`, `hostname`, or `node_key`| +| Name | Type | In | Description | +| ---------- | ----------------- | ---- | ----------------------------------------------------------------------------- | +| identifier | integer or string | path | **Required**. The host's `uuid`, `osquery_host_id`, `hostname`, or `node_key` | #### Example `GET /api/v1/fleet/hosts/identifier/f01c4390-0000-0000-a1e5-14346a5724dc` - ##### Default response `Status: 200` @@ -768,14 +765,37 @@ Deletes the specified host from Fleet. Note that a deleted host will fail authen #### Parameters -| Name | Type | In | Description | -| --------------------- | ------ | ---- | --------------------------------------------------------------- | -| id | integer | path | **Required**. The host's id. | +| Name | Type | In | Description | +| ---- | ------- | ---- | ---------------------------- | +| id | integer | path | **Required**. The host's id. | #### Example `DELETE /api/v1/fleet/hosts/121` +##### Default response + +`Status: 200` + +``` +{} +``` + +### Refetch host + +Flags the host details to be refetched the next time the host checks in for live queries. Note that we cannot be certain when the host will actually check in and update these details. Further requests to the host APIs will indicate that the refetch has been requested through the `refetch_requested` field on the host object. + +`POST /api/v1/fleet/hosts/{id}/refetch` + +#### Parameters + +| Name | Type | In | Description | +| ---- | ------- | ---- | ---------------------------- | +| id | integer | path | **Required**. The host's id. | + +#### Example + +`POST /api/v1/fleet/hosts/121/refetch` ##### Default response @@ -808,12 +828,12 @@ Creates a dynamic label. #### Parameters -| Name | Type | In | Description | -| --------------------- | ------ | ---- | --------------------------------------------------------------- | -| name | string | body | **Required**. The label's name. | -| description | string | body | The label's description. | -| query | string | body | **Required**. The query in SQL syntax used to filter the hosts. | -| platform | string | body | The specific platform for the label to target. Provides an additional filter. Choices for platform are `darwin`, `windows`, `ubuntu`, and `centos`. All platforms are included by default and this option is represented by an empty string.| +| Name | Type | In | Description | +| ----------- | ------ | ---- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| name | string | body | **Required**. The label's name. | +| description | string | body | The label's description. | +| query | string | body | **Required**. The query in SQL syntax used to filter the hosts. | +| platform | string | body | The specific platform for the label to target. Provides an additional filter. Choices for platform are `darwin`, `windows`, `ubuntu`, and `centos`. All platforms are included by default and this option is represented by an empty string. | #### Example @@ -860,12 +880,12 @@ Modifies the specified label. Note: Label queries are immutable. To change the q #### Parameters -| Name | Type | In | Description | -| --------------------- | ------ | ---- | --------------------------------------------------------------- | -| id | integer | path | **Required**. The label's id. | -| name | string | body | The label's name. | -| description | string | body | The label's description. | -| platform | string | body | The specific platform for the label to target. Provides an additional filter. Choices for platform are `darwin`, `windows`, `ubuntu`, and `centos`. All platforms are included by default and this option is represented by an empty string.| +| Name | Type | In | Description | +| ----------- | ------- | ---- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| id | integer | path | **Required**. The label's id. | +| name | string | body | The label's name. | +| description | string | body | The label's description. | +| platform | string | body | The specific platform for the label to target. Provides an additional filter. Choices for platform are `darwin`, `windows`, `ubuntu`, and `centos`. All platforms are included by default and this option is represented by an empty string. | #### Example @@ -912,9 +932,9 @@ Returns the specified label. #### Parameters -| Name | Type | In | Description | -| --------------------- | ------ | ---- | --------------------------------------------------------------- | -| id | integer | path | **Required**. The label's id. | +| Name | Type | In | Description | +| ---- | ------- | ---- | ----------------------------- | +| id | integer | path | **Required**. The label's id. | #### Example @@ -950,11 +970,11 @@ Returns a list of all the labels in Fleet. #### Parameters -| Name | Type | In | Description | -| --------------------- | ------ | ---- | --------------------------------------------------------------- | -| id | integer | path | **Required**. The label's id. | -| order_key | string | query | What to order results by. Can be any column in the labels table. | -| order_direction | string | query | **Requires `order_key`**. The direction of the order given the order key. Options include `asc` and `desc`. Default is `asc`. | +| Name | Type | In | Description | +| --------------- | ------- | ----- | ----------------------------------------------------------------------------------------------------------------------------- | +| id | integer | path | **Required**. The label's id. | +| order_key | string | query | What to order results by. Can be any column in the labels table. | +| order_direction | string | query | **Requires `order_key`**. The direction of the order given the order key. Options include `asc` and `desc`. Default is `asc`. | #### Example @@ -1051,12 +1071,12 @@ Returns a list of the hosts that belong to the specified label. #### Parameters -| Name | Type | In | Description | -| --------------------- | ------ | ---- | --------------------------------------------------------------- | -| id | integer | path | **Required**. The label's id. | -| order_key | string | query | What to order results by. Can be any column in the hosts table. | -| 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 `hostname`, `machine_serial`, `uuid`, and `ipv4`. | +| Name | Type | In | Description | +| --------------- | ------- | ----- | ----------------------------------------------------------------------------------------------------------------------------- | +| id | integer | path | **Required**. The label's id. | +| order_key | string | query | What to order results by. Can be any column in the hosts table. | +| 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 `hostname`, `machine_serial`, `uuid`, and `ipv4`. | #### Example @@ -1119,9 +1139,9 @@ Deletes the label specified by name. #### Parameters -| Name | Type | In | Description | -| --------------------- | ------ | ---- | --------------------------------------------------------------- | -| name | string | path | **Required**. The label's name. | +| Name | Type | In | Description | +| ---- | ------ | ---- | ------------------------------- | +| name | string | path | **Required**. The label's name. | #### Example @@ -1143,9 +1163,9 @@ Deletes the label specified by ID. #### Parameters -| Name | Type | In | Description | -| --------------------- | ------ | ---- | --------------------------------------------------------------- | -| id | integer | path | **Required**. The label's id. | +| Name | Type | In | Description | +| ---- | ------- | ---- | ----------------------------- | +| id | integer | path | **Required**. The label's id. | #### Example @@ -1171,9 +1191,9 @@ If the `label_membership_type` is set to `manual`, the `hosts` property must als #### Parameters -| Name | Type | In | Description | -| --------------------- | ------ | ---- | --------------------------------------------------------------- | -| specs | list | path | A list of the label to apply. Each label requires the `name`, `query`, and `label_membership_type` properties| +| Name | Type | In | Description | +| ----- | ---- | ---- | ------------------------------------------------------------------------------------------------------------- | +| specs | list | path | A list of the label to apply. Each label requires the `name`, `query`, and `label_membership_type` properties | #### Example @@ -1403,14 +1423,14 @@ Creates a user account after an invited user provides registration information a #### Parameters -| Name | Type | In | Description | -| --------------------- | ------ | ---- | --------------------------------------------------------------- | -| email | string | body | **Required**. The email address of the user. | +| Name | Type | In | Description | +| --------------------- | ------ | ---- | ----------------------------------------------------------------- | +| email | string | body | **Required**. The email address of the user. | | invite_token | string | body | **Required**. Token provided to the user in the invitation email. | -| name | string | body | The name of the user. | -| username | string | body | **Required**. The username chosen by the user | -| password | string | body | **Required**. The password chosen by the user. | -| password_confirmation | string | body | **Required**. Confirmation of the password chosen by the user. | +| name | string | body | The name of the user. | +| username | string | body | **Required**. The username chosen by the user | +| password | string | body | **Required**. The password chosen by the user. | +| password_confirmation | string | body | **Required**. Confirmation of the password chosen by the user. | #### Example @@ -1509,11 +1529,12 @@ Creates a user account without requiring an invitation, the user is enabled imme #### Parameters -| Name | Type | In | Description | -| ---------- | ------- | ---- | ------------------------------------------------ | -| username | string | body | **Required**. The user's username. | -| email | string | body | **Required**. The user's email address. | -| password | string | body | **Required**. The user's password. | +| Name | Type | In | Description | +| ---------- | ------- | ---- | ---------------------------------------------------- | +| username | string | body | **Required**. The user's username. | +| email | string | body | **Required**. The user's email address. | +| password | string | body | **Required**. The user's password. | +| invited_by | integer | body | **Required**. ID of the admin creating the user. | | admin | boolean | body | **Required**. Whether the user has admin privileges. | #### Example @@ -1593,8 +1614,8 @@ Returns all information about a specific user. #### Parameters -| Name | Type | In | Description | -| ---- | ------- | ----- | ---------------------------- | +| Name | Type | In | Description | +| ---- | ------- | ---- | ---------------------------- | | id | integer | path | **Required**. The user's id. | #### Example @@ -1669,14 +1690,14 @@ Returns all information about a specific user. #### Parameters -| Name | Type | In | Description | -| ---- | ------- | ----- | ---------------------------- | -| id | integer | path | **Required**. The user's id. | -| name | string | body | The user's name. | -| username | string | body | The user's username. | -| position | string | body | The user's position. | -| email | string | body | The user's email. | -| sso_enabled | boolean | body | Whether or not SSO is enabled for the user. | +| Name | Type | In | Description | +| ----------- | ------- | ---- | ------------------------------------------- | +| id | integer | path | **Required**. The user's id. | +| name | string | body | The user's name. | +| username | string | body | The user's username. | +| position | string | body | The user's position. | +| email | string | body | The user's email. | +| sso_enabled | boolean | body | Whether or not SSO is enabled for the user. | #### Example @@ -1747,9 +1768,9 @@ Promotes or demotes the selected user's level of access as an admin in Fleet. Ad #### Parameters -| Name | Type | In | Description | -| ----- | ------- | ----- | ---------------------------- | -| id | integer | path | **Required**. The user's id. | +| Name | Type | In | Description | +| ----- | ------- | ---- | -------------------------------------------------- | +| id | integer | path | **Required**. The user's id. | | admin | boolean | body | **Required**. Whether or not the user is an admin. | #### Example @@ -1794,10 +1815,10 @@ The selected user is logged out of Fleet and required to reset their password du #### Parameters -| Name | Type | In | Description | -| ---- | ------- | ----- | ---------------------------- | -| id | integer | path | **Required**. The user's id. | -| reset | boolean | body | Whether or not the user is required to reset their password during the next attempt to log in. | +| Name | Type | In | Description | +| ----- | ------- | ---- | ---------------------------------------------------------------------------------------------- | +| id | integer | path | **Required**. The user's id. | +| reset | boolean | body | Whether or not the user is required to reset their password during the next attempt to log in. | #### Example @@ -1881,9 +1902,9 @@ Deletes the selected user's sessions in Fleet. Also deletes the user's API token #### Parameters -| Name | Type | In | Description | -| ---------- | ------- | ---- | ------------------------------------------------ | -| id | integer | path | **Required**. The ID of the desired user. | +| Name | Type | In | Description | +| ---- | ------- | ---- | ----------------------------------------- | +| id | integer | path | **Required**. The ID of the desired user. | #### Example @@ -1900,6 +1921,7 @@ Deletes the selected user's sessions in Fleet. Also deletes the user's API token --- ## Sessions + - [Get session info](#get-session-info) - [Delete session](#delete-session) @@ -1911,9 +1933,9 @@ Returns the session information for the session specified by ID. #### Parameters -| Name | Type | In | Description | -| ---------- | ------- | ---- | ------------------------------------------------ | -| id | integer | path | **Required**. The ID of the desired session. | +| Name | Type | In | Description | +| ---- | ------- | ---- | -------------------------------------------- | +| id | integer | path | **Required**. The ID of the desired session. | #### Example @@ -1939,15 +1961,14 @@ Deletes the session specified by ID. When the user associated with the session n #### Parameters -| Name | Type | In | Description | -| ---------- | ------- | ---- | ------------------------------------------------ | -| id | integer | path | **Required**. The id of the desired session. | +| Name | Type | In | Description | +| ---- | ------- | ---- | -------------------------------------------- | +| id | integer | path | **Required**. The id of the desired session. | #### Example `DELETE /api/v1/fleet/sessions/1` - ##### Default response `Status: 200` @@ -1985,15 +2006,14 @@ Returns the query specified by ID. #### Parameters -| Name | Type | In | Description | -| ---------- | ------- | ---- | ------------------------------------------------ | -| id | integer | path | **Required**. The id of the desired query. | +| Name | Type | In | Description | +| ---- | ------- | ---- | ------------------------------------------ | +| id | integer | path | **Required**. The id of the desired query. | #### Example `GET /api/v1/fleet/queries/31` - ##### Default response `Status: 200` @@ -2033,16 +2053,15 @@ Returns a list of all queries in the Fleet instance. #### Parameters -| Name | Type | In | Description | -| --------------------- | ------ | ---- | --------------------------------------------------------------- | -| order_key | string | query | What to order results by. Can be any column in the queries table. | -| order_direction | string | query | **Requires `order_key`**. The direction of the order given the order key. Options include `asc` and `desc`. Default is `asc`. | +| Name | Type | In | Description | +| --------------- | ------ | ----- | ----------------------------------------------------------------------------------------------------------------------------- | +| order_key | string | query | What to order results by. Can be any column in the queries table. | +| order_direction | string | query | **Requires `order_key`**. The direction of the order given the order key. Options include `asc` and `desc`. Default is `asc`. | #### Example `GET /api/v1/fleet/queries` - ##### Default response `Status: 200` @@ -2134,11 +2153,11 @@ Returns a list of all queries in the Fleet instance. #### Parameters -| Name | Type | In | Description | -| ---------- | ------- | ---- | ------------------------------------------------ | -| 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. | +| Name | Type | In | Description | +| ----------- | ------ | ---- | -------------------------------------- | +| 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. | #### Example @@ -2183,12 +2202,12 @@ Returns the query specified by ID. #### Parameters -| Name | Type | In | Description | -| ---------- | ------- | ---- | ------------------------------------------------ | -| id | integer | path | **Required.** The ID of the query. | -| name | string | body | The name of the query. | -| query | string | body | The query in SQL syntax. | -| description | string | body | The query's description. | +| Name | Type | In | Description | +| ----------- | ------- | ---- | ---------------------------------- | +| id | integer | path | **Required.** The ID of the query. | +| name | string | body | The name of the query. | +| query | string | body | The query in SQL syntax. | +| description | string | body | The query's description. | #### Example @@ -2231,9 +2250,9 @@ Deletes the query specified by name. #### Parameters -| Name | Type | In | Description | -| ---------- | ------- | ---- | ------------------------------------------------ | -| name | string | path | **Required.** The name of the query. | +| Name | Type | In | Description | +| ---- | ------ | ---- | ------------------------------------ | +| name | string | path | **Required.** The name of the query. | #### Example @@ -2255,9 +2274,9 @@ Deletes the query specified by ID. #### Parameters -| Name | Type | In | Description | -| ---------- | ------- | ---- | ------------------------------------------------ | -| id | integer | path | **Required.** The ID of the query. | +| Name | Type | In | Description | +| ---- | ------- | ---- | ---------------------------------- | +| id | integer | path | **Required.** The ID of the query. | #### Example @@ -2279,9 +2298,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 | list | body | **Required.** The IDs of the queries. | #### Example @@ -2355,9 +2374,9 @@ Returns the name, description, and SQL of the query specified by name. #### Parameters -| Name | Type | In | Description | -| ---------- | ------- | ---- | ------------------------------------------------ | -| name | string | path | **Required.** The name of the query. | +| Name | Type | In | Description | +| ---- | ------ | ---- | ------------------------------------ | +| name | string | path | **Required.** The name of the query. | #### Example @@ -2385,9 +2404,9 @@ Creates and/or modifies the queries included in the specs list. To modify an exi #### Parameters -| Name | Type | In | Description | -| ---------- | ------- | ---- | ------------------------------------------------ | -| specs | list | body | **Required.** The list of the queries to be created or modified. | +| Name | Type | In | Description | +| ----- | ---- | ---- | ---------------------------------------------------------------- | +| specs | list | body | **Required.** The list of the queries to be created or modified. | #### Example @@ -2477,10 +2496,10 @@ Runs the specified query as a live query on the specified hosts or group of host #### Parameters -| Name | Type | In | Description | -| ---------- | ------- | ---- | ------------------------------------------------ | -| query | string | body | **Required.** The SQL of the query. | -| selected | object | body | **Required.** The desired targets for the query specified by ID. This object can contain `hosts` and/or `labels` properties. See examples below. | +| Name | Type | In | Description | +| -------- | ------ | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------ | +| query | string | body | **Required.** The SQL of the query. | +| selected | object | body | **Required.** The desired targets for the query specified by ID. This object can contain `hosts` and/or `labels` properties. See examples below. | #### Example with one host targeted by ID @@ -2491,7 +2510,7 @@ Runs the specified query as a live query on the specified hosts or group of host ``` { "query": "select instance_id from system_info", - "selected": { + "selected": { "hosts": [171] } } @@ -2530,7 +2549,7 @@ Runs the specified query as a live query on the specified hosts or group of host ``` { "query": "select instance_id from system_info;", - "selected": { + "selected": { "labels": [7] } } @@ -2568,10 +2587,10 @@ Runs the specified query as a live query on the specified hosts or group of host #### Parameters -| Name | Type | In | Description | -| ---------- | ------- | ---- | ------------------------------------------------ | -| query | string | body | **Required.** The SQL of the query. | -| selected | object | body | **Required.** The desired targets for the query specified by name. This object can contain `hosts` and/or `labels` properties. See examples below. | +| Name | Type | In | Description | +| -------- | ------ | ---- | -------------------------------------------------------------------------------------------------------------------------------------------------- | +| query | string | body | **Required.** The SQL of the query. | +| selected | object | body | **Required.** The desired targets for the query specified by name. This object can contain `hosts` and/or `labels` properties. See examples below. | #### Example with one host targeted by hostname @@ -2582,9 +2601,9 @@ Runs the specified query as a live query on the specified hosts or group of host ``` { "query": "select instance_id from system_info", - "selected": { + "selected": { "hosts": [ - "macbook-pro.local", + "macbook-pro.local", ] } } @@ -2623,7 +2642,7 @@ Runs the specified query as a live query on the specified hosts or group of host ``` { "query": "select instance_id from system_info", - "selected": { + "selected": { "labels": [ "All Hosts" ] @@ -2657,7 +2676,7 @@ Runs the specified query as a live query on the specified hosts or group of host ### Retrieve live query results (standard WebSocket API) -You can retrieve the results of a live query using the [standard WebSocket API](#https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_client_applications). +You can retrieve the results of a live query using the [standard WebSocket API](#https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_client_applications). Before you retrieve the live query results, you must create a live query campaign by running the live query. See the documentation for the [Run live query](#run-live-query) endpoint to create a live query campaign. @@ -2665,10 +2684,10 @@ Before you retrieve the live query results, you must create a live query campaig #### Parameters -| Name | Type | In | Description | -| ---------- | ------- | ---- | ------------------------------------------------ | -| token | string | | **Required.** The token used to authenticate with the Fleet API. | -| campaignID | integer | | **Required.** The ID of the live query campaign. | +| Name | Type | In | Description | +| ---------- | ------- | --- | ---------------------------------------------------------------- | +| token | string | | **Required.** The token used to authenticate with the Fleet API. | +| campaignID | integer | | **Required.** The ID of the live query campaign. | #### Example @@ -2707,9 +2726,9 @@ o ``` [ - { - "type": "auth", - "data": { "token": } + { + "type": "auth", + "data": { "token": } } ] ``` @@ -2717,7 +2736,7 @@ o ``` [ { - "type": "select_campaign", + "type": "select_campaign", "data": { "campaign_id": 12 } } ] @@ -2804,10 +2823,10 @@ You can also retrieve live query results with a [SockJS client](https://github.c #### Parameters -| Name | Type | In | Description | -| ---------- | ------- | ---- | ------------------------------------------------ | -| token | string | | **Required.** The token used to authenticate with the Fleet API. | -| campaignID | integer | | **Required.** The ID of the live query campaign. | +| Name | Type | In | Description | +| ---------- | ------- | --- | ---------------------------------------------------------------- | +| token | string | | **Required.** The token used to authenticate with the Fleet API. | +| campaignID | integer | | **Required.** The ID of the live query campaign. | #### Example @@ -2824,7 +2843,7 @@ socket.onopen = () => { socket.onmessage = ({ data }) => { console.log(data); const message = JSON.parse(data); - + if (message.type === 'status' && message.data.status === 'finished') { socket.close(); } @@ -2847,9 +2866,9 @@ o ``` [ - { - "type": "auth", - "data": { "token": } + { + "type": "auth", + "data": { "token": } } ] ``` @@ -2857,7 +2876,7 @@ o ``` [ { - "type": "select_campaign", + "type": "select_campaign", "data": { "campaign_id": 12 } } ] @@ -2961,12 +2980,12 @@ o #### Parameters -| Name | Type | In | Description | -| ---- | ------- | ----- | ---------------------------- | -| name | string | body | **Required**. The pack's name. | -| description | string | body | The pack's description. | -| host_ids | list | body | A list containing the targeted host IDs. | -| label_ids | list | body | A list containing the targeted label's IDs. | +| Name | Type | In | Description | +| ----------- | ------ | ---- | ------------------------------------------- | +| name | string | body | **Required**. The pack's name. | +| description | string | body | The pack's description. | +| host_ids | list | body | A list containing the targeted host IDs. | +| label_ids | list | body | A list containing the targeted label's IDs. | #### Example @@ -3011,13 +3030,13 @@ o #### Parameters -| Name | Type | In | Description | -| ---- | ------- | ----- | ---------------------------- | -| id | integer | path | **Required.** The pack's id. | -| name | string | body | The pack's name. | -| description | string | body | The pack's description. | -| host_ids | list | body | A list containing the targeted host IDs. | -| label_ids | list | body | A list containing the targeted label's IDs. | +| Name | Type | In | Description | +| ----------- | ------- | ---- | ------------------------------------------- | +| id | integer | path | **Required.** The pack's id. | +| name | string | body | The pack's name. | +| description | string | body | The pack's description. | +| host_ids | list | body | A list containing the targeted host IDs. | +| label_ids | list | body | A list containing the targeted label's IDs. | #### Example @@ -3061,8 +3080,8 @@ o #### Parameters -| Name | Type | In | Description | -| ---- | ------- | ----- | ---------------------------- | +| Name | Type | In | Description | +| ---- | ------- | ---- | ---------------------------- | | id | integer | path | **Required.** The pack's id. | #### Example @@ -3097,10 +3116,10 @@ o #### Parameters -| Name | Type | In | Description | -| --------------------- | ------ | ---- | --------------------------------------------------------------- | -| order_key | string | query | What to order results by. Can be any column in the packs table. | -| order_direction | string | query | **Requires `order_key`**. The direction of the order given the order key. Options include `asc` and `desc`. Default is `asc`. | +| Name | Type | In | Description | +| --------------- | ------ | ----- | ----------------------------------------------------------------------------------------------------------------------------- | +| order_key | string | query | What to order results by. Can be any column in the packs table. | +| order_direction | string | query | **Requires `order_key`**. The direction of the order given the order key. Options include `asc` and `desc`. Default is `asc`. | #### Example @@ -3149,9 +3168,9 @@ o #### Parameters -| Name | Type | In | Description | -| ---- | ------- | ----- | ---------------------------- | -| name | string | path | **Required.** The pack's name. | +| Name | Type | In | Description | +| ---- | ------ | ---- | ------------------------------ | +| name | string | path | **Required.** The pack's name. | #### Example @@ -3171,8 +3190,8 @@ o #### Parameters -| Name | Type | In | Description | -| ---- | ------- | ----- | ---------------------------- | +| Name | Type | In | Description | +| ---- | ------- | ---- | ---------------------------- | | id | integer | path | **Required.** The pack's ID. | #### Example @@ -3193,8 +3212,8 @@ o #### Parameters -| Name | Type | In | Description | -| ---- | ------- | ----- | ---------------------------- | +| Name | Type | In | Description | +| ---- | ------- | ---- | ---------------------------- | | id | integer | path | **Required.** The pack's ID. | #### Example @@ -3260,22 +3279,22 @@ o #### Parameters -| Name | Type | In | Description | -| ---- | ------- | ----- | ---------------------------- | -| pack_id | integer | body | **Required.** The pack's ID. | -| query_id | integer | body | **Required.** The query's ID. | -| interval | integer | body | **Required.** The amount of time, in seconds, the query waits before running. | -| snapshot | boolean | body | **Required.** Whether the queries logs show everything in its current state. | -| removed | boolean | body | **Required.** Whether "removed" actions should be logged. | -| platform | string | body | The computer platform where this query will run (other platforms ignored). Empty value runs on all platforms. | -| shard | integer | body | Restrict this query to a percentage (1-100) of target hosts. | -| version | string | body | The minimum required osqueryd version installed on a host. | +| Name | Type | In | Description | +| -------- | ------- | ---- | ------------------------------------------------------------------------------------------------------------- | +| pack_id | integer | body | **Required.** The pack's ID. | +| query_id | integer | body | **Required.** The query's ID. | +| interval | integer | body | **Required.** The amount of time, in seconds, the query waits before running. | +| snapshot | boolean | body | **Required.** Whether the queries logs show everything in its current state. | +| removed | boolean | body | **Required.** Whether "removed" actions should be logged. | +| platform | string | body | The computer platform where this query will run (other platforms ignored). Empty value runs on all platforms. | +| shard | integer | body | Restrict this query to a percentage (1-100) of target hosts. | +| version | string | body | The minimum required osqueryd version installed on a host. | #### Example `POST /api/v1/fleet/schedule` -#### Request body +#### Request body ``` { @@ -3321,8 +3340,8 @@ o #### Parameters -| Name | Type | In | Description | -| ---- | ------- | ----- | ---------------------------- | +| Name | Type | In | Description | +| ---- | ------- | ---- | --------------------------------------- | | id | integer | path | **Required.** The scheduled query's ID. | #### Example @@ -3360,21 +3379,21 @@ o #### Parameters -| Name | Type | In | Description | -| ---- | ------- | ----- | ---------------------------- | -| id | integer | path | **Required.** The scheduled query's ID. | -| interval | integer | body | The amount of time, in seconds, the query waits before running. | -| snapshot | boolean | body | Whether the queries logs show everything in its current state. | -| removed | boolean | body | Whether "removed" actions should be logged. | -| platform | string | body | The computer platform where this query will run (other platforms ignored). Empty value runs on all platforms. | -| shard | integer | body | Restrict this query to a percentage (1-100) of target hosts. | -| version | string | body | The minimum required osqueryd version installed on a host. | +| Name | Type | In | Description | +| -------- | ------- | ---- | ------------------------------------------------------------------------------------------------------------- | +| id | integer | path | **Required.** The scheduled query's ID. | +| interval | integer | body | The amount of time, in seconds, the query waits before running. | +| snapshot | boolean | body | Whether the queries logs show everything in its current state. | +| removed | boolean | body | Whether "removed" actions should be logged. | +| platform | string | body | The computer platform where this query will run (other platforms ignored). Empty value runs on all platforms. | +| shard | integer | body | Restrict this query to a percentage (1-100) of target hosts. | +| version | string | body | The minimum required osqueryd version installed on a host. | #### Example `PATCH /api/v1/fleet/schedule/56` -#### Request body +#### Request body ``` { @@ -3413,8 +3432,8 @@ o #### Parameters -| Name | Type | In | Description | -| ---- | ------- | ----- | ---------------------------- | +| Name | Type | In | Description | +| ---- | ------- | ---- | --------------------------------------- | | id | integer | path | **Required.** The scheduled query's ID. | #### Example @@ -3538,9 +3557,9 @@ Returns the specs for all packs in the Fleet instance. #### Parameters -| Name | Type | In | Description | -| ---- | ------- | ----- | ---------------------------- | -| specs | list | body | **Required.** A list that includes the specs for each pack to be added to the Fleet instance. | +| Name | Type | In | Description | +| ----- | ---- | ---- | --------------------------------------------------------------------------------------------- | +| specs | list | body | **Required.** A list that includes the specs for each pack to be added to the Fleet instance. | #### Example @@ -3651,9 +3670,9 @@ Returns the spec for the specified pack by pack name. #### Parameters -| Name | Type | In | Description | -| ---- | ------- | ----- | ---------------------------- | -| name | string | path | **Required.** The pack's name. | +| Name | Type | In | Description | +| ---- | ------ | ---- | ------------------------------ | +| name | string | path | **Required.** The pack's name. | #### Example @@ -3735,10 +3754,10 @@ The search targets endpoint returns two lists. The first list includes the possi #### Parameters -| Name | Type | In | Description | -| ---- | ------- | ----- | ---------------------------- | -| query | string | body | The search query. Searchable items include a host's hostname or IPv4 address and labels. | -| selected | object | body | The targets already selected. The object includes a `hosts` property which contains a list of host IDs and a `labels` property which contains a list of label IDs.| +| Name | Type | In | Description | +| -------- | ------ | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| query | string | body | The search query. Searchable items include a host's hostname or IPv4 address and labels. | +| selected | object | body | The targets already selected. The object includes a `hosts` property which contains a list of host IDs and a `labels` property which contains a list of label IDs. | #### Example @@ -3750,7 +3769,7 @@ The search targets endpoint returns two lists. The first list includes the possi { "query": "172", "selected": { - "hosts": [], + "hosts": [], "labels": [7] } } @@ -3893,7 +3912,6 @@ None. `GET /api/v1/fleet/config/certificate` - ##### Default response `Status: 200` @@ -3918,7 +3936,6 @@ None. `GET /api/v1/fleet/config` - ##### Default response `Status: 200` @@ -3975,33 +3992,33 @@ Modifies the Fleet's configuration with the supplied information. #### Parameters -| Name | Type | In | Description | -| ---------- | ------- | ---- | ------------------------------------------------ | -| org_name | string | body | *Organization information*. The organization name. | -| org_logo_url | string | body | *Organization information*. The URL for the organization logo. | -| kolide_server_url | string | body | *Server settings*. The Fleet server URL. | -| live_query_disabled | boolean | body | *Server settings*. Whether the live query capabilities are disabled. | -| enable_smtp | boolean | body | *SMTP settings*. Whether SMTP is enabled for the Fleet app. | -| sender_address | string | body | *SMTP settings*. The sender email address for the Fleet app. An invitation email is an example of the emails that may use this sender address | -| server | string | body | *SMTP settings*. The SMTP server for the Fleet app. | -| port | integer | body | *SMTP settings*. The SMTP port for the Fleet app. | -| authentication_type | string | body | *SMTP settings*. The authentication type used by the SMTP server. Options include `"authtype_username_and_password"` or `"none"`| -| username_name | string | body | *SMTP settings*. The username used to authenticate requests made to the SMTP server.| -| password | string | body | *SMTP settings*. The password used to authenticate requests made to the SMTP server.| -| enable_ssl_tls | boolean | body | *SMTP settings*. Whether or not SSL and TLS are enabled for the SMTP server.| -| authentication_method | string | body | *SMTP settings*. The authentication method used to make authenticate requests to SMTP server. Options include `"authmethod_plain"`, `"authmethod_cram_md5"`, and `"authmethod_login"`.| -| domain | string | body | *SMTP settings*. The domain for the SMTP server.| -| verify_ssl_certs | boolean | body | *SMTP settings*. Whether or not SSL certificates are verified by the SMTP server. Turn this off (not recommended) if you use a self-signed certificate. | -| enabled_start_tls | boolean | body | *SMTP settings*. Detects if STARTTLS is enabled in your SMTP server and starts to use it.| -| enabled_sso | boolean | body | *SSO settings*. Whether or not SSO is enabled for the Fleet application. If this value is true, you must also include most of the SSO settings parameters below.| -| entity_id | string | body | *SSO settings*. The required entity ID is a URI that you use to identify Fleet when configuring the identity provider. | -| issuer_uri | string | body | *SSO settings*. The URI you provide here must exactly match the Entity ID field used in the identity provider configuration. | -| idp_image_url | string | body | *SSO settings*. An optional link to an image such as a logo for the identity provider. | -| metadata | string | body | *SSO settings*. Metadata provided by the identity provider. Either metadata or a metadata URL must be provided. | -| metadata_url | string | body | *SSO settings*. A URL that references the identity provider metadata. If available from the identity provider, this is the preferred means of providing metadata. | -| host_expiry_enabled | boolean | body | *Host expiry settings*. When enabled, allows automatic cleanup of hosts that have not communicated with Fleet in some number of days. | -| host_expiry_window | integer | body | *Host expiry settings*. If a host has not communicated with Fleet in the specified number of days, it will be removed. | -| additional_queries | boolean | body | Whether or not additional queries are enabled on hosts. | +| Name | Type | In | Description | +| --------------------- | ------- | ---- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| org_name | string | body | _Organization information_. The organization name. | +| org_logo_url | string | body | _Organization information_. The URL for the organization logo. | +| kolide_server_url | string | body | _Server settings_. The Fleet server URL. | +| live_query_disabled | boolean | body | _Server settings_. Whether the live query capabilities are disabled. | +| enable_smtp | boolean | body | _SMTP settings_. Whether SMTP is enabled for the Fleet app. | +| sender_address | string | body | _SMTP settings_. The sender email address for the Fleet app. An invitation email is an example of the emails that may use this sender address | +| server | string | body | _SMTP settings_. The SMTP server for the Fleet app. | +| port | integer | body | _SMTP settings_. The SMTP port for the Fleet app. | +| authentication_type | string | body | _SMTP settings_. The authentication type used by the SMTP server. Options include `"authtype_username_and_password"` or `"none"` | +| username_name | string | body | _SMTP settings_. The username used to authenticate requests made to the SMTP server. | +| password | string | body | _SMTP settings_. The password used to authenticate requests made to the SMTP server. | +| enable_ssl_tls | boolean | body | _SMTP settings_. Whether or not SSL and TLS are enabled for the SMTP server. | +| authentication_method | string | body | _SMTP settings_. The authentication method used to make authenticate requests to SMTP server. Options include `"authmethod_plain"`, `"authmethod_cram_md5"`, and `"authmethod_login"`. | +| domain | string | body | _SMTP settings_. The domain for the SMTP server. | +| verify_ssl_certs | boolean | body | _SMTP settings_. Whether or not SSL certificates are verified by the SMTP server. Turn this off (not recommended) if you use a self-signed certificate. | +| enabled_start_tls | boolean | body | _SMTP settings_. Detects if STARTTLS is enabled in your SMTP server and starts to use it. | +| enabled_sso | boolean | body | _SSO settings_. Whether or not SSO is enabled for the Fleet application. If this value is true, you must also include most of the SSO settings parameters below. | +| entity_id | string | body | _SSO settings_. The required entity ID is a URI that you use to identify Fleet when configuring the identity provider. | +| issuer_uri | string | body | _SSO settings_. The URI you provide here must exactly match the Entity ID field used in the identity provider configuration. | +| idp_image_url | string | body | _SSO settings_. An optional link to an image such as a logo for the identity provider. | +| metadata | string | body | _SSO settings_. Metadata provided by the identity provider. Either metadata or a metadata URL must be provided. | +| metadata_url | string | body | _SSO settings_. A URL that references the identity provider metadata. If available from the identity provider, this is the preferred means of providing metadata. | +| host_expiry_enabled | boolean | body | _Host expiry settings_. When enabled, allows automatic cleanup of hosts that have not communicated with Fleet in some number of days. | +| host_expiry_window | integer | body | _Host expiry settings_. If a host has not communicated with Fleet in the specified number of days, it will be removed. | +| additional_queries | boolean | body | Whether or not additional queries are enabled on hosts. | #### Example @@ -4023,7 +4040,6 @@ Modifies the Fleet's configuration with the supplied information. } ``` - ##### Default response `Status: 200` @@ -4086,7 +4102,6 @@ None. `GET /api/v1/fleet/spec/enroll_secret` - ##### Default response `Status: 200` @@ -4126,11 +4141,11 @@ Modifies and/or creates the specified enroll secret(s). #### Parameters -| Name | Type | In | Description | -| ---------- | ------- | ---- | ------------------------------------------------ | -| name | string | body | **Required.** The name of the enroll secret | -| secret | string | body | **Required.** The plain text string used as the enroll secret. | -| active | boolean | body | Whether or not the enroll secret is active. Must be set to true for hosts to enroll using the enroll secret. | +| Name | Type | In | Description | +| ------ | ------- | ---- | ------------------------------------------------------------------------------------------------------------ | +| name | string | body | **Required.** The name of the enroll secret | +| secret | string | body | **Required.** The plain text string used as the enroll secret. | +| active | boolean | body | Whether or not the enroll secret is active. Must be set to true for hosts to enroll using the enroll secret. | #### Example @@ -4152,7 +4167,6 @@ Modifies and/or creates the specified enroll secret(s). `POST /api/v1/fleet/spec/enroll_secret` - ##### Default response `Status: 200` @@ -4163,7 +4177,6 @@ Modifies and/or creates the specified enroll secret(s). ### Create invite - `POST /api/v1/fleet/invites` #### Parameters @@ -4190,7 +4203,6 @@ Modifies and/or creates the specified enroll secret(s). `POST /api/v1/fleet/invites` - ##### Default response `Status: 200` @@ -4228,7 +4240,6 @@ Returns a list of the active invitations in Fleet. `GET /api/v1/fleet/invites` - ##### Default response `Status: 200` @@ -4268,15 +4279,14 @@ Delete the specified invite from Fleet. #### Parameters -| Name | Type | In | Description | -| ---------- | ------- | ---- | ------------------------------------------------ | -| id | integer | path | **Required.** The user's id. | +| Name | Type | In | Description | +| ---- | ------- | ---- | ---------------------------- | +| id | integer | path | **Required.** The user's id. | #### Example `DELETE /api/v1/fleet/invites/{id}` - ##### Default response `Status: 200` @@ -4293,15 +4303,14 @@ Verify the specified invite. #### Parameters -| Name | Type | In | Description | -| ---------- | ------- | ---- | ------------------------------------------------ | -| token | integer | path | **Required.** Token provided to the user in the invitation email.| +| Name | Type | In | Description | +| ----- | ------- | ---- | ----------------------------------------------------------------- | +| token | integer | path | **Required.** Token provided to the user in the invitation email. | #### Example `GET /api/v1/fleet/invites/{token}` - ##### Default response `Status: 200` @@ -4345,15 +4354,14 @@ Changes the email specified by token. #### Parameters -| Name | Type | In | Description | -| ---------- | ------- | ---- | ------------------------------------------------ | -| token | integer | path | **Required.** The token provided to the user in the email change confirmation email.| +| Name | Type | In | Description | +| ----- | ------- | ---- | ------------------------------------------------------------------------------------ | +| token | integer | path | **Required.** The token provided to the user in the email change confirmation email. | #### Example `GET /api/v1/fleet/invites/{token}` - ##### Default response `Status: 200` @@ -4363,6 +4371,7 @@ Changes the email specified by token. "new_email": janedoe@example.com } ``` + --- ### Version @@ -4393,6 +4402,7 @@ None. "build_user": "zwass" } ``` + --- ## Osquery options @@ -4414,7 +4424,6 @@ None. `GET /api/v1/fleet/spec/osquery_options` - ##### Default response `Status: 200` @@ -4453,9 +4462,9 @@ Modifies the osquery options configuration set in Fleet. #### Parameters -| Name | Type | In | Description | -| ---------- | ------- | ---- | ------------------------------------------------ | -| spec | JSON | body | **Required.** The modified osquery spec. | +| Name | Type | In | Description | +| ---- | ---- | ---- | ---------------------------------------- | +| spec | JSON | body | **Required.** The modified osquery spec. | #### Example @@ -4489,7 +4498,6 @@ Modifies the osquery options configuration set in Fleet. } ``` - ##### Default response `Status: 200` @@ -4508,7 +4516,7 @@ Modifies the osquery options configuration set in Fleet. Fleet supports osquery's file carving functionality as of Fleet 3.3.0. This allows the Fleet server to request files (and sets of files) from osquery agents, returning the full contents to Fleet. -To initiate a file carve using the Fleet API, you can use the [live query](#run-live-query) or [scheduled query](#add-scheduled-query-to-a-pack) endpoints to run a query against the `carves` table. +To initiate a file carve using the Fleet API, you can use the [live query](#run-live-query) or [scheduled query](#add-scheduled-query-to-a-pack) endpoints to run a query against the `carves` table. For more information on executing a file carve in Fleet, go to the [File carving with Fleet docs](../1-Using-Fleet/2-fleetctl-CLI.md#file-carving-with-fleet). @@ -4573,9 +4581,9 @@ Retrieves the specified carve. #### Parameters -| Name | Type | In | Description | -| ---------- | ------- | ---- | ------------------------------------------------ | -| id | integer | path | **Required.** The desired carve's ID. | +| Name | Type | In | Description | +| ---- | ------- | ---- | ------------------------------------- | +| id | integer | path | **Required.** The desired carve's ID. | #### Example @@ -4612,10 +4620,10 @@ Retrieves the specified carve block. This endpoint retrieves the data that was c #### Parameters -| Name | Type | In | Description | -| ---------- | ------- | ---- | ------------------------------------------------ | -| id | integer | path | **Required.** The desired carve's ID. | -| block_id | integer | path | **Required.** The desired carve block's ID. | +| Name | Type | In | Description | +| -------- | ------- | ---- | ------------------------------------------- | +| id | integer | path | **Required.** The desired carve's ID. | +| block_id | integer | path | **Required.** The desired carve block's ID. | #### Example diff --git a/docs/1-Using-Fleet/4-Osquery-logs.md b/docs/1-Using-Fleet/4-Osquery-logs.md index d51b300fef..2550b1a4ee 100644 --- a/docs/1-Using-Fleet/4-Osquery-logs.md +++ b/docs/1-Using-Fleet/4-Osquery-logs.md @@ -22,21 +22,21 @@ Fleet supports the following logging plugins for osquery logs: - [PubSub](#pubsub) - Logs are written to Google Cloud PubSub topics. - [Stdout](#stdout) - Logs are written to stdout. -To set the osquery logging plugins, use the `--osquery_result_log_plugin` and `--osquery_status_log_plugin` flags (or [equivalents for environment variables or configuration files](../2-Deployment/2-Configuration.md#options)). +To set the osquery logging plugins, use the `--osquery_result_log_plugin` and `--osquery_status_log_plugin` flags (or [equivalents for environment variables or configuration files](../3-Deployment/2-Configuration.md#options)). ### Filesystem The default logging plugin. - Plugin name: `filesystem` -- Flag namespace: [filesystem](../2-Deployment/2-Configuration.md#filesystem) +- Flag namespace: [filesystem](../3-Deployment/2-Configuration.md#filesystem) With the filesystem plugin, osquery result and/or status logs are written to the local filesystem on the Fleet server. This is typically used with a log forwarding agent on the Fleet server that will push the logs into a logging pipeline. Note that if multiple load-balanced Fleet servers are used, the logs will be load-balanced across those servers (not duplicated). ### Firehose - Plugin name: `firehose` -- Flag namespace: [firehose](../2-Deployment/2-Configuration.md#firehose) +- Flag namespace: [firehose](../3-Deployment/2-Configuration.md#firehose) With the Firehose plugin, osquery result and/or status logs are written to [AWS Firehose](https://aws.amazon.com/kinesis/data-firehose/) streams. This is a very good method for aggregating osquery logs into AWS S3 storage. @@ -45,7 +45,7 @@ Note that Firehose logging has limits [discussed in the documentation](https://d ### Kinesis - Plugin name: `kinesis` -- Flag namespace: [kinesis](../2-Deployment/2-Configuration.md#kinesis) +- Flag namespace: [kinesis](../3-Deployment/2-Configuration.md#kinesis) With the Kinesis plugin, osquery result and/or status logs are written to [AWS Kinesis](https://aws.amazon.com/kinesis/data-streams) streams. @@ -58,7 +58,7 @@ output in the Fleet logs and those logs _will not_ be sent to Kinesis. ### Lambda - Plugin name: `lambda` -- Flag namespace: [lambda](../2-Deployment/2-Configuration.md#lambda) +- Flag namespace: [lambda](../3-Deployment/2-Configuration.md#lambda) With the Lambda plugin, osquery result and/or status logs are written to [AWS Lambda](https://aws.amazon.com/lambda/) functions. @@ -73,7 +73,7 @@ output in the Fleet logs and those logs _will not_ be sent to Lambda. ### PubSub - Plugin name: `pubsub` -- Flag namespace: [pubsub](../2-Deployment/2-Configuration.md#pubsub) +- Flag namespace: [pubsub](../3-Deployment/2-Configuration.md#pubsub) With the PubSub plugin, osquery result and/or status logs are written to [PubSub](https://cloud.google.com/pubsub/) topics. @@ -82,7 +82,7 @@ Note that messages over 10MB will be dropped, with a notification sent to the fl ### Stdout - Plugin name: `stdout` -- Flag namespace: [stdout](../2-Deployment/2-Configuration.md#stdout) +- Flag namespace: [stdout](../3-Deployment/2-Configuration.md#stdout) With the stdout plugin, osquery result and/or status logs are written to stdout on the Fleet server. This is typically used for debugging or with a log diff --git a/docs/1-Using-Fleet/6-Security-best-practices.md b/docs/1-Using-Fleet/6-Security-best-practices.md index f3a54d0345..f3ff1acf19 100644 --- a/docs/1-Using-Fleet/6-Security-best-practices.md +++ b/docs/1-Using-Fleet/6-Security-best-practices.md @@ -27,7 +27,7 @@ Fleet supports SAML auth which means that it can be configured such that it neve Passwords are never stored in plaintext in the database. We store a `bcrypt`ed hash of the password along with a randomly generated salt. The `bcrypt` iteration count and salt key size are admin-configurable. ### Authentication tokens -The size and expiration time of session tokens is admin-configurable. See [https://github.com/fleetdm/fleet/blob/master/docs/2-Deployment/2-Configuration.md#session_duration](../2-Deployment/2-Configuration.md#session_duration). +The size and expiration time of session tokens is admin-configurable. See [https://github.com/fleetdm/fleet/blob/master/docs/3-Deployment/2-Configuration.md#session_duration](../3-Deployment/2-Configuration.md#session_duration). It is possible to revoke all session tokens for a user by forcing a password reset. diff --git a/docs/1-Using-Fleet/7-Updating-Fleet.md b/docs/1-Using-Fleet/7-Updating-Fleet.md index 842693b552..1bdee324e5 100644 --- a/docs/1-Using-Fleet/7-Updating-Fleet.md +++ b/docs/1-Using-Fleet/7-Updating-Fleet.md @@ -7,7 +7,7 @@ ## Overview -This guide explains how to update and run new versions of Fleet. For initial installation instructions, see [Installing Fleet](../2-Deployment/1-Installation.md). +This guide explains how to update and run new versions of Fleet. For initial installation instructions, see [Installing Fleet](../3-Deployment/1-Installation.md). There are two steps to perform a typical Fleet update. If any other steps are required, they will be noted in the release notes. diff --git a/docs/1-Using-Fleet/FAQ.md b/docs/1-Using-Fleet/FAQ.md index 5054bd9392..7aeb59eaae 100644 --- a/docs/1-Using-Fleet/FAQ.md +++ b/docs/1-Using-Fleet/FAQ.md @@ -1,22 +1,35 @@ # Using Fleet FAQ + - [Has anyone stress tested Fleet? How many clients can the Fleet server handle?](#has-anyone-stress-tested-fleet-how-many-clients-can-the-fleet-server-handle) +- [Can I target my hosts using their enroll secrets?](#can-I-target-my-hosts-using-their-enroll-secrets) - [How often do labels refresh? Is the refresh frequency configurable?](#how-often-do-labels-refresh-is-the-refresh-frequency-configurable) - [How do I revoke the authorization tokens for a user?](#how-do-i-revoke-the-authorization-tokens-for-a-user) - [How do I monitor the performance of my queries?](#how-do-i-monitor-the-performance-of-my-queries) - [How do I monitor a Fleet server?](#how-do-i-monitor-a-fleet-server) - [Why is the “Add User” button disabled?](#why-is-the-add-user-button-disabled) +- [Can I disable password-based authentication in the Fleet UI?](#can-i-disable-password-based-authentication-in-the-fleet-ui) - [Where are my query results?](#where-are-my-query-results) - [Why aren’t my live queries being logged?](#why-arent-my-live-queries-being-logged) +- [Can I use the Fleet API to fetch results from a scheduled query pack?](#can-i-use-the-fleet-api-to-fetch-results-from-a-scheduled-query-pack) +- [How do I automatically add hosts to packs when the hosts enroll to Fleet?](#how-do-i-automatically-add-hosts-to-packs-when-the-hosts-enroll-to-Fleet) ## Has anyone stress tested Fleet? How many clients can the Fleet server handle? -Fleet has been stress tested to 150,000 online hosts and 400,000 total enrolled hosts. There are numerous production deployments in the thousands, in the tens of thousands of hosts range, and there are production deployments in the high tens of thousands of hosts range. +Fleet has been stress tested to 150,000 online hosts and 400,000 total enrolled hosts. Production deployments exist with over 100,000 hosts and numerous production deployments manage tens of thousands of hosts. -It’s standard deployment practice to have multiple Fleet servers behind a load balancer. However, typically the MySQL database is the bottleneck and an individual Fleet server can handle tens of thousands of hosts. +It’s standard deployment practice to have multiple Fleet servers behind a load balancer. However, typically the MySQL database is the performance bottleneck and a single Fleet server can handle tens of thousands of hosts. + +## Can I target my hosts using their enroll secrets? + +No, currently, there’s no way to retrieve the name of the enroll secret with a query. This means that there's no way to create a label using your hosts' enroll secrets and then use this label as a target for queries or query packs. + +Typically folks will use some other unique identifier to create labels that distinguish each type of device. As a workaround, [Fleet's manual labels](./2-fleetctl-CLI.md#host-labels) provide a way to create groups of hosts without a query. These manual labels can then be used as targets for queries or query packs. + +In the coming months, Fleet will introduce the [Teams feature](https://github.com/fleetdm/fleet/issues/288) to Fleet Basic customers. Teams will allow users to group hosts into teams using their enroll secret. These teams can then be targeted with specific queries, query packs, and configuration. ## How often do labels refresh? Is the refresh frequency configurable? -The update frequency for labels is configurable with the [—osquery_label_update_interval](../2-Deployment/2-Configuration.md#osquery_label_update_interval) flag (default 1 hour). +The update frequency for labels is configurable with the [—osquery_label_update_interval](https://github.com/fleetdm/fleet/blob/master/docs/3-Deployment/2-Configuration.md#osquery_label_update_interval) flag (default 1 hour). ## How do I revoke the authorization tokens for a user? @@ -30,13 +43,21 @@ Fleet can live query the `osquery_schedule` table. Performing this live query al Fleet provides standard interfaces for monitoring and alerting. See the [Monitoring Fleet](./5-Monitoring-Fleet.md) documentation for details. - ## Why is the “Add User” button disabled? The “Add User” button is disabled if SMTP (email) has not been configured for the Fleet server. Currently, there is no way to add new users without email capabilities. One way to hack around this is to use a simulated mailserver like [Mailhog](https://github.com/mailhog/MailHog). You can retrieve the email that was “sent” in the Mailhog UI, and provide users with the invite URL manually. +## Can I disable password-based authentication in the Fleet UI? + +Some folks like to enforce users with SAML SSO enabled to login only via the SSO and not via password. + +There is no option in the Fleet UI for disabling password-based authentication. +However, users that have SSO enabled in Fleet will not be able to log in via password-based authentication. + +If a user has SSO enabled, the Login page in the Fleet UI does displays the “Username” and “Password” fields but on attempted password-based login, this user will receive an “Authentication failed” message. + ## Where are my query results? ### Live Queries @@ -45,15 +66,15 @@ Live query results (executed in the web UI or `fleetctl query`) are pushed direc ### Scheduled Queries -Scheduled query results (queries that are scheduled to run in Packs) are typically sent to the Fleet server, and will be available on the filesystem of the server at the path configurable by [`--osquery_result_log_file`](../2-Deployment/2-Configuration.md#osquery_result_log_file). This defaults to `/tmp/osquery_result`. +Scheduled query results (queries that are scheduled to run in Packs) are typically sent to the Fleet server, and will be available on the filesystem of the server at the path configurable by [`--osquery_result_log_file`](../3-Deployment/2-Configuration.md#osquery_result_log_file). This defaults to `/tmp/osquery_result`. It is possible to configure osqueryd to log query results outside of Fleet. For results to go to Fleet, the `--logger_plugin` flag must be set to `tls`. ### What are my options for storing the osquery logs? -Folks typically use Fleet to ship logs to data aggregation systems like Splunk, the ELK stack, and Graylog. +Folks typically use Fleet to ship logs to data aggregation systems like Splunk, the ELK stack, and Graylog. -The [logger configuration options](../2-Deployment/2-Configuration.md#osquery_status_log_plugin) allow you to select the log output plugin. Using the log outputs you can route the logs to your chosen aggregation system. +The [logger configuration options](https://github.com/fleetdm/fleet/blob/master/docs/3-Deployment/2-Configuration.md#osquery_status_log_plugin) allow you to select the log output plugin. Using the log outputs you can route the logs to your chosen aggregation system. ### Troubleshooting @@ -63,8 +84,30 @@ Expecting results, but not seeing anything in the logs? - Check whether the query is scheduled in differential mode. If so, new results will only be logged when the result set changes. - Ensure that the query is scheduled to run on the intended platforms, and that the tables queried are supported by those platforms. - Use live query to `SELECT * FROM osquery_schedule` to check whether the query has been scheduled on the host. -- Look at the status logs provided by osquery. In a standard configuration these are available on the filesystem of the Fleet server at the path configurable by [`--filesystem_status_log_file`](../2-Deployment/2-Configuration.md#filesystem_status_log_file). This defaults to `/tmp/osquery_status`. The host will output a status log each time it executes the query. +- Look at the status logs provided by osquery. In a standard configuration these are available on the filesystem of the Fleet server at the path configurable by [`--filesystem_status_log_file`](../3-Deployment/2-Configuration.md#filesystem_status_log_file). This defaults to `/tmp/osquery_status`. The host will output a status log each time it executes the query. ## Why aren’t my live queries being logged? -Live query results are never logged to the filesystem of the Fleet server. See [Where are my query results?](#where-are-my-query-results). \ No newline at end of file +Live query results are never logged to the filesystem of the Fleet server. See [Where are my query results?](#where-are-my-query-results). + +## Can I use the Fleet API to fetch results from a scheduled query pack? + +You cannot. Scheduled query results are logged to whatever logging plugin you have configured and are not stored in the Fleet DB. + +However, the Fleet API exposes a significant amount of host information via the [`api/v1/fleet/hosts`](./3-REST-API.md#list-hosts) and the [`api/v1/fleet/hosts/{id}`](./3-REST-API.md#get-host) API endpoints. The `api/v1/fleet/hosts` [can even be configured to return additional host information](https://github.com/fleetdm/fleet/blob/9fb9da31f5462fa7dda4819a114bbdbc0252c347/docs/1-Using-Fleet/2-fleetctl-CLI.md#fleet-configuration-options). + +As an example, let's say you want to retrieve a host's OS version, installed software, and kernel version: + +Each host’s OS version is available using the `api/v1/fleet/hosts` API endpoint. [Check out the API documentation for this endpoint](./3-REST-API.md#list-hosts). + +The ability to view each host’s installed software was released behind a feature flag in Fleet 3.11.0 and called Software inventory. [Check out the feature flag documentation for instructions on turning on Software inventory in Fleet](../3-Deployment/2-Configuration.md#feature-flags). + +Once the Software inventory feature is turned on, a list of a specific host’s installed software is available using the `api/v1/fleet/hosts/{id}` endpoint. [Check out the documentation for this endpoint](./3-REST-API.md#get-host). + +It’s possible in Fleet to retrieve each host’s kernel version, using the Fleet API, through `additional_queries`. The Fleet configuration options yaml file includes an `additional_queries` property that allows you to append custom query results to the host details returned by the `api/v1/fleet/hosts` endpoint. [Check out an example configuration file with the additional_queries field](./2-fleetctl-CLI.md#fleet-configuration-options). + +## How do I automatically add hosts to packs when the hosts enroll to Fleet? + +You can do this by setting the `targets` field in the [YAML configuration file](./2-fleetctl-CLI.md#query-packs) that manages the packs that are added to your Fleet instance. + +The `targets` field allows you to specify the `labels` field. With the `labels` field, the hosts that become members of the specified labels, upon enrolling to Fleet, will automatically become targets of the given pack. diff --git a/docs/1-Using-Fleet/configuration-files/README.md b/docs/1-Using-Fleet/configuration-files/README.md new file mode 100644 index 0000000000..83d8ca7de0 --- /dev/null +++ b/docs/1-Using-Fleet/configuration-files/README.md @@ -0,0 +1,34 @@ +# Configuration files + +Entities in Fleet, such as queries, packs, labels, agent options, and enroll secrets, can be managed with configuration files in yaml syntax. + +This page contains links to examples that can help you understand the configuration options for your Fleet yaml file(s). + +Examples in this directory are presented in two forms: +- [`single-file-configuration.yml`](./single-file-configuration.yml) presents multiple yaml documents in one file. One file is often easier to manage than several. Group related objects into a single file whenever it makes sense. +- The `multi-file-configuration` directory presents multiple yaml documents in separate files. They are in the following structure: + +``` +├─ packs +├ └─ osquery-monitoring.yml +├─ agent-options.yml +├─ enroll-secrets.yml +├─ labels.yml +├─ queries.yml +``` + +## Using yaml files in Fleet + +A Fleet configuration is defined using one or more declarative "messages" in yaml syntax. Each message can live in it's own file or multiple in one file, each separated by `---`. Each file/message contains a few required top-level keys: + +- `apiVersion` - the API version of the file/request +- `spec` - the "data" of the request +- `kind ` - the type of file/object (i.e.: pack, query, config) + +The file may optionally also include some `metadata` for more complex data types (i.e.: packs). + +When you reason about how to manage these config files, consider following the [General Config Tips](https://kubernetes.io/docs/concepts/configuration/overview/#general-config-tips) published by the Kubernetes project. Some of the especially relevant tips are included here as well: + +- When defining configurations, specify the latest stable API version. +- Configuration files should be stored in version control before being pushed to the cluster. This allows quick roll-back of a configuration if needed. It also aids with cluster re-creation and restoration if necessary. +- Don’t specify default values unnecessarily – simple and minimal configs will reduce errors. diff --git a/examples/kubernetes/fleet-deployment.yml b/docs/1-Using-Fleet/configuration-files/kubernetes/fleet-deployment.yml similarity index 100% rename from examples/kubernetes/fleet-deployment.yml rename to docs/1-Using-Fleet/configuration-files/kubernetes/fleet-deployment.yml diff --git a/examples/kubernetes/fleet-migrations.yml b/docs/1-Using-Fleet/configuration-files/kubernetes/fleet-migrations.yml similarity index 100% rename from examples/kubernetes/fleet-migrations.yml rename to docs/1-Using-Fleet/configuration-files/kubernetes/fleet-migrations.yml diff --git a/examples/kubernetes/fleet-service.yml b/docs/1-Using-Fleet/configuration-files/kubernetes/fleet-service.yml similarity index 100% rename from examples/kubernetes/fleet-service.yml rename to docs/1-Using-Fleet/configuration-files/kubernetes/fleet-service.yml diff --git a/examples/config-many-files/config.yml b/docs/1-Using-Fleet/configuration-files/multi-file-configuration/agent-options.yml similarity index 100% rename from examples/config-many-files/config.yml rename to docs/1-Using-Fleet/configuration-files/multi-file-configuration/agent-options.yml diff --git a/examples/config-many-files/enroll-secret.yml b/docs/1-Using-Fleet/configuration-files/multi-file-configuration/enroll-secrets.yml similarity index 100% rename from examples/config-many-files/enroll-secret.yml rename to docs/1-Using-Fleet/configuration-files/multi-file-configuration/enroll-secrets.yml diff --git a/examples/config-many-files/labels.yml b/docs/1-Using-Fleet/configuration-files/multi-file-configuration/labels.yml similarity index 100% rename from examples/config-many-files/labels.yml rename to docs/1-Using-Fleet/configuration-files/multi-file-configuration/labels.yml diff --git a/examples/config-many-files/packs/osquery-monitoring.yml b/docs/1-Using-Fleet/configuration-files/multi-file-configuration/packs/osquery-monitoring.yml similarity index 100% rename from examples/config-many-files/packs/osquery-monitoring.yml rename to docs/1-Using-Fleet/configuration-files/multi-file-configuration/packs/osquery-monitoring.yml diff --git a/examples/config-many-files/queries.yml b/docs/1-Using-Fleet/configuration-files/multi-file-configuration/queries.yml similarity index 100% rename from examples/config-many-files/queries.yml rename to docs/1-Using-Fleet/configuration-files/multi-file-configuration/queries.yml diff --git a/examples/config-single-file.yml b/docs/1-Using-Fleet/configuration-files/single-file-configuration.yml similarity index 100% rename from examples/config-single-file.yml rename to docs/1-Using-Fleet/configuration-files/single-file-configuration.yml diff --git a/docs/1-Using-Fleet/standard-query-library/README.md b/docs/1-Using-Fleet/standard-query-library/README.md new file mode 100644 index 0000000000..4ead185ed7 --- /dev/null +++ b/docs/1-Using-Fleet/standard-query-library/README.md @@ -0,0 +1,39 @@ +# Standard query library + +Fleet's standard query library includes a growing collection of useful queries for organizations deploying Fleet and osquery. + +## Importing the queries in Fleet + +#### After cloning the fleetdm/fleet repo, import the queries using fleetctl: +``` +fleetctl apply -f fleet/docs/1-Using-Fleet/standard-query-library/standard-query-library.yml +``` + +## Contributors + +Want to add your own query? + +1. Please copy the following yaml section and paste it at the bottom of the [`standard-query-library.yml`](./standard-query-library.yml) file. +```yaml +--- +apiVersion: v1 +kind: query +spec: + name: What is your query called? Please use a human readable query name. + platforms: What operating systems support your query? This can usually be determined by the osquery tables included in your query. Heading to the https://osquery.io/schema webpage to see which operating systems are supported by the tables you include. + description: Describe your query. What does information does your query reveal? + query: Insert query here + purpose: What is the goal of running your query? Ex. Detection + remediation: Are there any remediation steps to resolve the detection triggered by your query? If not, insert "N/A." + contributors: zwass,mike-j-thomas +``` +2. Replace each field and submit a pull request to the fleetdm/fleet GitHub repository. + +For instructions on submitting pull requests to Fleet check out [the Committing Changes section](https://github.com/fleetdm/fleet/blob/58445ede82550cb574775a83ae4cf5433f325a7e/docs/4-Contribution/4-Committing-Changes.md#committing-changes) in the Contributors documentation. + +## Additional resources + +Listed below are great resources that contain additional queries. + +- Osquery (https://github.com/osquery/osquery/tree/master/packs) +- Palantir osquery configuration (https://github.com/palantir/osquery-configuration/tree/master/Fleet) diff --git a/docs/1-Using-Fleet/standard-query-library/standard-query-library.yml b/docs/1-Using-Fleet/standard-query-library/standard-query-library.yml new file mode 100644 index 0000000000..c822af202b --- /dev/null +++ b/docs/1-Using-Fleet/standard-query-library/standard-query-library.yml @@ -0,0 +1,250 @@ +--- +apiVersion: v1 +kind: query +spec: + name: Count Apple applications installed + platforms: macOS + description: Count the number of Apple applications installed on the machine. + query: SELECT COUNT(*) FROM apps WHERE bundle_identifier LIKE 'com.apple.%'; + purpose: Informational + remediation: N/A +--- +apiVersion: v1 +kind: query +spec: + name: Detect Linux hosts with high severity vulnerable versions of OpenSSL + platforms: Linux + description: Retrieves the OpenSSL version. + query: SELECT name AS name, version AS version, 'deb_packages' AS source FROM deb_packages WHERE name LIKE 'openssl%' UNION SELECT name AS name, version AS version, 'apt_sources' AS source FROM apt_sources WHERE name LIKE 'openssl%' UNION SELECT name AS name, version AS version, 'rpm_packages' AS source FROM rpm_packages WHERE name LIKE 'openssl%'; + purpose: Detection + remediation: N/A +--- +apiVersion: v1 +kind: query +spec: + name: Detect machines with Gatekeeper disabled + platforms: macOS + description: Gatekeeper tries to ensure only trusted software is run on a mac machine. + query: SELECT * FROM gatekeeper WHERE assessments_enabled = 0; + purpose: Detection + remediation: N/A +--- +apiVersion: v1 +kind: query +spec: + name: Detect presence of authorized SSH keys + platforms: macOS, Linux + description: Presence of authorized SSH keys may be unusual on laptops. Could be completely normal on servers, but may be worth auditing for unusual keys and/or changes. + query: SELECT username, authorized_keys. * FROM users CROSS JOIN authorized_keys USING (uid); + purpose: Detection + remediation: Check out the linked table (https://github.com/fleetdm/fleet/blob/32b4d53e7f1428ce43b0f9fa52838cbe7b413eed/handbook/queries/detect-hosts-with-high-severity-vulnerable-versions-of-openssl.md#table-of-vulnerable-openssl-versions) to determine if the installed version is a high severity vulnerability and view the corresponding CVE(s) +--- +apiVersion: v1 +kind: query +spec: + name: Get authorized keys + platforms: macOS, Linux + description: List authorized_keys for each user on the system. + query: SELECT * FROM users CROSS JOIN authorized_keys USING (uid); + purpose: Informational + remediation: N/A +--- +apiVersion: v1 +kind: query +spec: + name: Get crashes + platforms: macOS + description: Retrieve application, system, and mobile app crash logs. + query: SELECT uid, datetime, responsible, exception_type, identifier, version, crash_path FROM users CROSS JOIN crashes USING (uid); + purpose: Informational + remediation: N/A +--- +apiVersion: v1 +kind: query +spec: + name: Get installed Chrome Extensions + platforms: macOS, Linux, Windows, FreeBSD + description: List installed Chrome Extensions for all users. + query: SELECT uid, datetime, responsible, exception_type, identifier, version, crash_path FROM users CROSS JOIN crashes USING (uid); + purpose: Informational + remediation: N/A +--- +apiVersion: v1 +kind: query +spec: + name: Get installed FreeBSD software + platforms: FreeBSD + description: Get all software installed on a FreeBSD computer, including browser plugins and installed packages. Note, this does not included other running processes in the processes table. + query: SELECT name AS name, version AS version, 'Browser plugin (Chrome)' AS type, 'chrome_extensions' AS source FROM chrome_extensions UNION SELECT name AS name, version AS version, 'Browser plugin (Firefox)' AS type, 'firefox_addons' AS source FROM firefox_addons UNION SELECT name AS name, version AS version, 'Package (Atom)' AS type, 'atom_packages' AS source FROM atom_packages UNION SELECT name AS name, version AS version, 'Package (Python)' AS type, 'python_packages' AS source FROM python_packages UNION SELECT name AS name, version AS version, 'Package (pkg)' AS type, 'pkg_packages' AS source FROM pkg_packages; + purpose: Informational + remediation: N/A +--- +apiVersion: v1 +kind: query +spec: + name: Get Homebrew Packages + platforms: macOS + description: Get the installed homebrew package database. + query: SELECT * FROM homebrew_packages; + purpose: Informational + remediation: N/A +--- +apiVersion: v1 +kind: query +spec: + name: Get installed Linux software + platforms: Linux + description: Get all software installed on a Linux computer, including browser plugins and installed packages. Note, this does not included other running processes in the processes table. + query: SELECT name AS name, version AS version, 'Package (APT)' AS type, 'apt_sources' AS source FROM apt_sources UNION SELECT name AS name, version AS version, 'Package (deb)' AS type, 'deb_packages' AS source FROM deb_packages UNION SELECT package AS name, version AS version, 'Package (Portage)' AS type, 'portage_packages' AS source FROM portage_packages UNION SELECT name AS name, version AS version, 'Package (RPM)' AS type, 'rpm_packages' AS source FROM rpm_packages UNION SELECT name AS name, '' AS version, 'Package (YUM)' AS type, 'yum_sources' AS source FROM yum_sources UNION SELECT name AS name, version AS version, 'Package (NPM)' AS type, 'npm_packages' AS source FROM npm_packages UNION SELECT name AS name, version AS version, 'Package (Atom)' AS type, 'atom_packages' AS source FROM atom_packages UNION SELECT name AS name, version AS version, 'Package (Python)' AS type, 'python_packages' AS source FROM python_packages; + purpose: Informational + remediation: N/A +--- +apiVersion: v1 +kind: query +spec: + name: Get installed macOS software + platforms: macOS + description: Get all software installed on a macOS computer, including apps, browser plugins, and installed packages. Note, this does not included other running processes in the processes table. + query: SELECT name AS name, bundle_short_version AS version, 'Application (macOS)' AS type, 'apps' AS source FROM apps UNION SELECT name AS name, version AS version, 'Package (Python)' AS type, 'python_packages' AS source FROM python_packages UNION SELECT name AS name, version AS version, 'Browser plugin (Chrome)' AS type, 'chrome_extensions' AS source FROM chrome_extensions UNION SELECT name AS name, version AS version, 'Browser plugin (Firefox)' AS type, 'firefox_addons' AS source FROM firefox_addons UNION SELECT name As name, version AS version, 'Browser plugin (Safari)' AS type, 'safari_extensions' AS source FROM safari_extensions UNION SELECT name AS name, version AS version, 'Package (Homebrew)' AS type, 'homebrew_packages' AS source FROM homebrew_packages; + purpose: Informational + remediation: N/A +--- +apiVersion: v1 +kind: query +spec: + name: Get installed Safari extensions + platforms: macOS + description: Retrieves the list of installed Safari Extensions for all users in the target system. + query: SELECT safari_extensions.* FROM users join safari_extensions USING (uid); + purpose: Informational + remediation: N/A +--- +apiVersion: v1 +kind: query +spec: + name: Get installed Windows software + platforms: Windows + description: Get all software installed on a Windows computer, including programs, browser plugins, and installed packages. Note, this does not included other running processes in the processes table. + query: SELECT name AS name, version AS version, 'Program (Windows)' AS type, 'programs' AS source FROM programs UNION SELECT name AS name, version AS version, 'Package (Python)' AS type, 'python_packages' AS source FROM python_packages UNION SELECT name AS name, version AS version, 'Browser plugin (IE)' AS type, 'ie_extensions' AS source FROM ie_extensions UNION SELECT name AS name, version AS version, 'Browser plugin (Chrome)' AS type, 'chrome_extensions' AS source FROM chrome_extensions UNION SELECT name AS name, version AS version, 'Browser plugin (Firefox)' AS type, 'firefox_addons' AS source FROM firefox_addons UNION SELECT name AS name, version AS version, 'Package (Chocolatey)' AS type, 'chocolatey_packages' AS source FROM chocolatey_packages UNION SELECT name AS name, version AS version, 'Package (Atom)' AS type, 'atom_packages' AS source FROM atom_packages UNION SELECT name AS name, version AS version, 'Package (Python)' AS type, 'python_packages' AS source FROM python_packages; + purpose: Informational + remediation: N/A +--- +apiVersion: v1 +kind: query +spec: + name: Get laptops with failing batteries + platforms: macOS + description: + query: SELECT * FROM battery WHERE health != 'Good' AND condition NOT IN ('', 'Normal'); + purpose: Informational + remediation: N/A +--- +apiVersion: v1 +kind: query +spec: + name: Get macOS disk free space percentage + platforms: macOS + description: Displays the percentage of free space available on the primary disk partition. + query: SELECT (blocks_available * 100 / blocks) AS pct, * FROM mounts WHERE path = '/'; + purpose: Informational + remediation: N/A +--- +apiVersion: v1 +kind: query +spec: + name: Get macOS disk free space percentage + platforms: macOS + description: Displays the percentage of free space available on the primary disk partition. + query: SELECT (blocks_available * 100 / blocks) AS pct, * FROM mounts WHERE path = '/'; + purpose: Informational + remediation: N/A +--- +apiVersion: v1 +kind: query +spec: + name: Get mounts + platforms: macOS, Linux + description: Shows system mounted devices and filesystems (not process specific). + query: SELECT device, device_alias, path, type, blocks_size FROM mounts; + purpose: Informational + remediation: N/A +--- +apiVersion: v1 +kind: query +spec: + name: Get the version of the resident operating system + platforms: macOS, Linux, Windows, FreeBSD + description: Shows system mounted devices and filesystems (not process specific). + query: SELECT * FROM os_version; + purpose: Informational + remediation: N/A +--- +apiVersion: v1 +kind: query +spec: + name: Get platform info + platforms: macOS + description: Shows information about the host platform + query: SELECT vendor, version, date, revision from platform_info; + purpose: Informational + remediation: N/A +--- +apiVersion: v1 +kind: query +spec: + name: Get startup items + platforms: macOS, Linux, Windows, FreeBSD + description: Shows applications and binaries set as user/login startup items. + query: SELECT * FROM startup_items; + purpose: Informational + remediation: N/A +--- +apiVersion: v1 +kind: query +spec: + name: Get system logins and logouts + platforms: macOS + description: Get a list of system logins and logouts. + query: SELECT * FROM last; + purpose: Informational + remediation: N/A +--- +apiVersion: v1 +kind: query +spec: + name: Get system uptime + platforms: macOS, Linux, Windows, FreeBSD + description: Shows the system uptime. + query: SELECT * FROM uptime; + purpose: Informational + remediation: N/A +--- +apiVersion: v1 +kind: query +spec: + name: Get USB devices + platforms: macOS, Linux + description: Shows all USB devices that are actively plugged into the host system. + query: SELECT * FROM usb_devices; + purpose: Informational + remediation: N/A +--- +apiVersion: v1 +kind: query +spec: + name: Get wifi status + platforms: macOS + description: Shows information about the wifi network that a host is currently connected to. + query: SELECT * FROM wifi_status; + purpose: Informational + remediation: N/A +--- +apiVersion: v1 +kind: query +spec: + name: Get Windows machines with unencrypted hard disks + platforms: Windows + description: + query: SELECT * FROM bitlocker_info WHERE protection_status = 0; + purpose: Informational + remediation: N/A diff --git a/docs/2-Orbit-osquery/README.md b/docs/2-Orbit-osquery/README.md new file mode 100644 index 0000000000..bc094ae52c --- /dev/null +++ b/docs/2-Orbit-osquery/README.md @@ -0,0 +1,152 @@ +# Orbit osquery + +Orbit is an [osquery](https://github.com/osquery/osquery) runtime and autoupdater. With Orbit, it's easy to deploy osquery, manage configurations, and stay up to date. Orbit eases the deployment of osquery connected with a [Fleet server](https://github.com/fleetdm/fleet), and is a (near) drop-in replacement for osquery in a variety of deployment scenarios. + +Orbit is the recommended agent for Fleet. But Orbit can be used with or without Fleet, and Fleet can be used with or without Orbit. + +## Usage + +General information and flag documentation can be accessed by running `orbit --help`. + +### Permissions + +Orbit generally expects root permissions to be able to create and access it's working files. + +To get root level permissions: + +#### macOS/Linux + +Prefix `orbit` commands with `sudo` (`sudo orbit ...`) or run in a root shell. + +#### Windows + +Run Powershell or cmd.exe with "Run as administrator" and start `orbit` commands from that shell. + +### Osquery shell + +Run an `osqueryi` shell with `orbit osqueryi` or `orbit shell`. + +### Connect to a Fleet server + +Use the `--fleet-url` and `--enroll-secret` flags to connect to a Fleet server. + +For example: + +``` sh +orbit --fleet-url=https://localhost:8080 --enroll-secret=the_secret_value +``` + +Use `--fleet_certificate` to provide a path to a certificate bundle when necessary for osquery to verify the authenticity of the Fleet server (typically when using a Windows client or self-signed certificates): + +``` sh +orbit --fleet-url=https://localhost:8080 --enroll-secret=the_secret_value --fleet-certificate=cert.pem +``` + +Add the `--insecure` flag for connections using otherwise invalid certificates: + +``` sh +orbit --fleet-url=https://localhost:8080 --enroll-secret=the_secret_value --insecure +``` + +### Osquery flags + +Orbit can be used as near drop-in replacement for `osqueryd`, enhancing standard osquery with autoupdate capabilities. Orbit passes through any options after `--` directly to the `osqueryd` instance. + +For example, the following would be a typical drop-in usage of Orbit: + +``` sh +orbit -- --flagfile=flags.txt +``` + +## Packaging + +Orbit, like standalone osquery, is typically deployed via OS-specific packages. Tooling is provided with this repository to generate installation packages. + +### Packaging support + +- **macOS** - `.pkg` package generation with (optional) [Notarization](https://developer.apple.com/documentation/xcode/notarizing_macos_software_before_distribution) and codesigning - Persistence via `launchd`. + +- **Linux** - `.deb` (Debian, Ubuntu, etc.) & `.rpm` (RHEL, CentOS, etc.) package generation - Persistence via `systemd`. + +- **Windows** (coming soon) - `.msi` package generation - Persistence via Services. + +### Building packages + +Before building packages, clone or download this repository and [install Go](https://golang.org/doc/install). + +Use `go run ./cmd/package` from this directory to run the packaging tools. + +The only required parameter is `--type`, use one of `deb`, `rpm`, or `pkg` (`msi` coming soon). + +Configure osquery to connect to a Fleet (or other TLS) server with the `--fleet-url` and `--enroll-secret` flags. + +A minimal invocation for communicating with Fleet: + +``` sh +go run ./cmd/package --type deb --fleet-url=fleet.example.com --enroll-secret=notsosecret +``` + +This will build a `.deb` package configured to communicate with a Fleet server at `fleet.example.com` using the enroll secret `notsosecret`. + +When the Fleet server uses a self-signed (or otherwise invalid) TLS certificate, package with the `--insecure` or `--fleet-certificate` options. + +See `go run ./cmd/package` for the full range of packaging options. + +#### Update channels + +Orbit uses the concept of "update channels" to determine the version of Orbit, osquery, and any extensions (extension support coming soon) to run. This concept is modeled from the common versioning convention for Docker containers. + +Configure update channels for Orbit and osqueryd with the `--orbit-channel` and `--osqueryd-channel` flags when packaging. + +| Channel | Versions | +| ------------------------------------ | ------ | +| `4` | 4.x.x | +| `4.6` | 4.6.x | +| `4.6.0` | 4.6.0 | + +Additionally `stable` and `edge` are special channel names. `stable` will always return the version Fleet deems to be stable, while `edge` will provide newer releases for beta testing. + +#### macOS signing & Notarization + +Orbit's packager can automate the codesigning and Notarization steps to allow the resulting package to generate packages that appear "trusted" when install on macOS hosts. Signing & notarization are supported only on macOS hosts. + +For signing, a "Developer ID Installer" certificate must be available on the build machine ([generation instructions](https://help.apple.com/xcode/mac/current/#/dev154b28f09)). Use `security find-identity -v` to verify the existence of this certificate and make note of the identifier provided in the left column. + +For Notarization, valid App Store Connect credentials must be available on the build machine. Set these in the environment variables `AC_USERNAME` and `AC_PASSWORD`. It is common to configure this via [app-specific passwords](https://support.apple.com/en-ca/HT204397). + +Build a signed and notarized macOS package with an invocation like the following: + +``` sh +AC_USERNAME=zach@fleetdm.com AC_PASSWORD=llpk-sije-kjlz-jdzw go run ./cmd/package --type=pkg --fleet-url=fleet.example.com --enroll-secret=63SBzTT+2UyW --sign-identity 3D7260BF99539C6E80A94835A8921A988F4E6498 --notarize +``` + +This process may take several minutes to complete as the Notarization process completes on Apple's servers. + +After successful notarization, the generated "ticket" is automatically stapled to the package. + +## FAQs + +### How does Orbit compare with Kolide Launcher? + +Orbit is inspired by the success of [Kolide Launcher](https://github.com/kolide/launcher), and approaches a similar problem domain with new strategies informed by the challenges encountered in real world deployments. Orbit does not share any code with Launcher. + +- Both Orbit and Launcher use [The Update Framework](https://theupdateframework.com/) specification for managing updates. Orbit utilizes the official [go-tuf](https://github.com/theupdateframework/go-tuf) library, while Launcher has it's own implementation of the specification. +- Orbit can be deployed as a (near) drop-in replacement for osquery, supporting full customization of the osquery flags. Launcher heavily manages the osquery flags making deployment outside of Fleet or Kolide's SaaS difficult. +- Orbit prefers the battle-tested plugins of osquery. Orbit uses the built-in logging, configuration, and live query plugins, while Launcher uses custom implementations. +- Orbit prefers the built-in osquery remote APIs. Launcher utilizes a custom gRPC API that has led to issues with character encoding, load balancers/proxies, and request size limits. +- Orbit encourages use of the osquery performance Watchdog, while Launcher disables the Watchdog. + +Additionally, Orbit aims to tackle problems out of scope for Launcher: + +- Configure updates via release channels, providing more granular control over agent versioning. +- Support for deploying and updating osquery extensions (🔜). +- Manage osquery versions and startup flags from a remote (Fleet) server (🔜). +- Further control of osquery performance via cgroups (🔜). + +### Is Orbit Free? + +Yes! Orbit is licensed under an MIT license and all uses are encouraged. + +### How does orbit update osquery? And how do the stable and edge channels get triggered to update osquery on a self hosted Fleet instance? + +Orbit uses a configurable update server. We expect that many folks will just use the update server we manage (similar to what Kolide does with Launcher's update server). We are also offering [tooling for self-managing an update server](https://github.com/fleetdm/fleet/blob/master/docs/3-Deployment/4-fleetctl-agent-updates.md) as part of Fleet Basic (the subscription offering). diff --git a/docs/2-Deployment/1-Installation.md b/docs/3-Deployment/1-Installation.md similarity index 100% rename from docs/2-Deployment/1-Installation.md rename to docs/3-Deployment/1-Installation.md diff --git a/docs/2-Deployment/2-Configuration.md b/docs/3-Deployment/2-Configuration.md similarity index 96% rename from docs/2-Deployment/2-Configuration.md rename to docs/3-Deployment/2-Configuration.md index 8b31cff321..edabb998a8 100644 --- a/docs/2-Deployment/2-Configuration.md +++ b/docs/3-Deployment/2-Configuration.md @@ -333,6 +333,19 @@ The database to use when connecting to the Redis instance. redis: database: 14 ``` + +###### `redis_duplicate_results` + +Whether or not to duplicate Live Query results to another Redis channel named `LQDuplicate`. This is useful in a scenario that would involve shipping the Live Query results outside of Fleet, near-realtime. + +- Default value: `false` +- Environment variable: `FLEET_REDIS_DUPLICATE_RESULTS` +- Config file format: + + ``` + redis: + duplicate_results: true + ``` ##### Server @@ -420,6 +433,21 @@ Note that some other configurations may need to be changed when modifying the UR url_prefix: /apps/fleet ``` +###### `server_keepalive` + +Controls the server side http keep alive property. + +Turning off keepalives has helped reduce outstanding TCP connections in some deployments. + +- Default value: true +- Environment variable: `FLEET_SERVER_KEEPALIVE` +- Config file format: + + ``` + server: + keepalive: true + ``` + ##### Auth ###### `auth_jwt_key` @@ -1168,6 +1196,28 @@ The identifier of the pubsub topic that osquery status logs will be published to status_topic: osquery_status ``` +###### `pubsub_add_attributes` + +This flag only has effect if `osquery_status_log_plugin` is set to `pubsub`. + +Add Pub/Sub attributes to messages. When enabled, the plugin parses the osquery result +messages, and adds the following Pub/Sub message attributes: + +- `name` - the `name` attribute from the message body +- `timestamp` - the `unixTime` attribute from the message body, converted to rfc3339 format +- Each decoration from the message + +This feature is useful when combined with [subscription filters](https://cloud.google.com/pubsub/docs/filtering). + +- Default value: false +- Environment variable: `FLEET_PUBSUB_ADD_ATTRIBUTES` +- Config file format: + + ``` + pubsub: + status_topic: osquery_status + ``` + ##### S3 file carving backend ###### `s3_bucket` diff --git a/docs/2-Deployment/3-Adding-hosts.md b/docs/3-Deployment/3-Adding-hosts.md similarity index 100% rename from docs/2-Deployment/3-Adding-hosts.md rename to docs/3-Deployment/3-Adding-hosts.md diff --git a/docs/2-Deployment/4-Example-deployment-scenarios.md b/docs/3-Deployment/4-Example-deployment-scenarios.md similarity index 97% rename from docs/2-Deployment/4-Example-deployment-scenarios.md rename to docs/3-Deployment/4-Example-deployment-scenarios.md index f9ba2e7e2a..a99fac5a6d 100644 --- a/docs/2-Deployment/4-Example-deployment-scenarios.md +++ b/docs/3-Deployment/4-Example-deployment-scenarios.md @@ -447,14 +447,14 @@ We will use this address when we configure the Kubernetes deployment and databas The last step is to run the Fleet database migrations on your new MySQL server. To do this, run the following: ``` -kubectl create -f ./examples/kubernetes/fleet-migrations.yml +kubectl create -f ./docs/1-Using-Fleet/configuration-files/kubernetes/fleet-migrations.yml ``` In Kubernetes, you can only run a job once. If you'd like to run it again (i.e.: you'd like to run the migrations again using the same file), you must delete the job before re-creating it. To delete the job and re-run it, you can run the following commands: ``` -kubectl delete -f ./examples/kubernetes/fleet-migrations.yml -kubectl create -f ./examples/kubernetes/fleet-migrations.yml +kubectl delete -f ./docs/1-Using-Fleet/configuration-files/kubernetes/fleet-migrations.yml +kubectl create -f ./docs/1-Using-Fleet/configuration-files/kubernetes/fleet-migrations.yml ``` #### Redis @@ -523,7 +523,7 @@ kubectl create secret generic fleet-server-auth-key --from-file=./build/fleet-se First we must deploy the instances of the Fleet webserver. The Fleet webserver is described using a Kubernetes deployment object. To create this deployment, run the following: ``` -kubectl apply -f ./examples/kubernetes/fleet-deployment.yml +kubectl apply -f ./docs/1-Using-Fleet/configuration-files/kubernetes/fleet-deployment.yml ``` You should be able to get an instance of the webserver running via `kubectl get pods` and you should see the following logs: @@ -539,7 +539,7 @@ ts=2017-11-16T02:48:38.441148166Z transport=https address=0.0.0.0:443 msg=listen Now that the Fleet server is running on our cluster, we have to expose the Fleet webservers to the internet via a load balancer. To create a Kubernetes `Service` of type `LoadBalancer`, run the following: ``` -kubectl apply -f ./examples/kubernetes/fleet-service.yml +kubectl apply -f ./docs/1-Using-Fleet/configuration-files/kubernetes/fleet-service.yml ``` #### Configure DNS diff --git a/docs/2-Deployment/4-fleetctl-agent-updates.md b/docs/3-Deployment/4-fleetctl-agent-updates.md similarity index 100% rename from docs/2-Deployment/4-fleetctl-agent-updates.md rename to docs/3-Deployment/4-fleetctl-agent-updates.md diff --git a/docs/2-Deployment/FAQ.md b/docs/3-Deployment/FAQ.md similarity index 98% rename from docs/2-Deployment/FAQ.md rename to docs/3-Deployment/FAQ.md index a8184ca7b7..0de639071f 100644 --- a/docs/2-Deployment/FAQ.md +++ b/docs/3-Deployment/FAQ.md @@ -47,6 +47,8 @@ Osquery requires that all communication between the agent and Fleet are over a s This error usually indicates that the Fleet server has run out of file descriptors. Fix this by increasing the `ulimit` on the Fleet process. See the `LimitNOFILE` setting in the [example systemd unit file](./2-Configuration.md#runing-with-systemd) for an example of how to do this with systemd. +Some deployments may benefit by setting the [`--server_keepalive`](./2-Configuration.md#server_keepalive) flag to false. + ## I upgraded my database, but Fleet is still running slowly. What could be going on? This could be caused by a mismatched connection limit between the Fleet server and the MySQL server that prevents Fleet from fully utilizing the database. First [determine how many open connections your MySQL server supports](https://dev.mysql.com/doc/refman/8.0/en/too-many-connections.html). Now set the [`--mysql_max_open_conns`](./2-Configuration.md#mysql_max_open_conns) and [`--mysql_max_idle_conns`](./2-Configuration.md#mysql_max_idle_conns) flags appropriately. diff --git a/docs/2-Deployment/README.md b/docs/3-Deployment/README.md similarity index 100% rename from docs/2-Deployment/README.md rename to docs/3-Deployment/README.md diff --git a/docs/3-Contribution/1-Building-Fleet.md b/docs/4-Contribution/1-Building-Fleet.md similarity index 100% rename from docs/3-Contribution/1-Building-Fleet.md rename to docs/4-Contribution/1-Building-Fleet.md diff --git a/docs/3-Contribution/2-Testing.md b/docs/4-Contribution/2-Testing.md similarity index 100% rename from docs/3-Contribution/2-Testing.md rename to docs/4-Contribution/2-Testing.md diff --git a/docs/3-Contribution/3-Migrations.md b/docs/4-Contribution/3-Migrations.md similarity index 100% rename from docs/3-Contribution/3-Migrations.md rename to docs/4-Contribution/3-Migrations.md diff --git a/docs/3-Contribution/4-Committing-Changes.md b/docs/4-Contribution/4-Committing-Changes.md similarity index 100% rename from docs/3-Contribution/4-Committing-Changes.md rename to docs/4-Contribution/4-Committing-Changes.md diff --git a/docs/3-Contribution/5-Releasing-Fleet.md b/docs/4-Contribution/5-Releasing-Fleet.md similarity index 100% rename from docs/3-Contribution/5-Releasing-Fleet.md rename to docs/4-Contribution/5-Releasing-Fleet.md diff --git a/docs/3-Contribution/FAQ.md b/docs/4-Contribution/FAQ.md similarity index 100% rename from docs/3-Contribution/FAQ.md rename to docs/4-Contribution/FAQ.md diff --git a/docs/3-Contribution/README.md b/docs/4-Contribution/README.md similarity index 100% rename from docs/3-Contribution/README.md rename to docs/4-Contribution/README.md diff --git a/docs/README.md b/docs/README.md index 68b906da59..6e152ad0d5 100644 --- a/docs/README.md +++ b/docs/README.md @@ -3,12 +3,15 @@ Welcome to the documentation for the Fleet osquery fleet manager. ### [Using Fleet](./1-Using-Fleet/README.md) -Resources for using the Fleet UI, fleetctl CLI, and Fleet REST API +Resources for using the Fleet UI, fleetctl CLI, and Fleet REST API. -### [Deployment](./2-Deployment/README.md) -Resources for installing Fleet's infrastructure dependencies, configuring Fleet, deploying osquery to hosts, and viewing example deployment scenarios +### [Orbit osquery](./2-Orbit-osquery/README.md) +Resources for using Orbit for osquery: the recommended agent for Fleet. -### [Contribution](./3-Contribution/README.md) +### [Deployment](./3-Deployment/README.md) +Resources for installing Fleet's infrastructure dependencies, configuring Fleet, deploying osquery to hosts, and viewing example deployment scenarios. + +### [Contribution](./4-Contribution/README.md) If you're interested in interacting with the Fleet source code, you'll find information on modifying and building the code here. --- diff --git a/frontend/README.md b/frontend/README.md index a826dd477f..88d42027da 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -5,7 +5,7 @@ The Fleet front-end is a Single Page Application using React and Redux. ## Running the Fleet web app For details instruction on building and serving the Fleet web application -consult the [Contribution documentation](../docs/3-Contribution/README.md) +consult the [Contribution documentation](../docs/4-Contribution/README.md) ## Directory Structure diff --git a/frontend/components/LoginRoutes/_styles.scss b/frontend/components/LoginRoutes/_styles.scss index 90b9e1c35c..3db8a8804a 100644 --- a/frontend/components/LoginRoutes/_styles.scss +++ b/frontend/components/LoginRoutes/_styles.scss @@ -5,5 +5,5 @@ justify-content: center; position: relative; min-height: 100vh; - background-color: $gradients-dark-gradient; + background-color: $gradients-dark-gradient-vertical; } diff --git a/frontend/components/buttons/DropdownButton/DropdownButton.jsx b/frontend/components/buttons/DropdownButton/DropdownButton.jsx index 82b06dcf43..db81ffe59b 100644 --- a/frontend/components/buttons/DropdownButton/DropdownButton.jsx +++ b/frontend/components/buttons/DropdownButton/DropdownButton.jsx @@ -93,7 +93,7 @@ export class DropdownButton extends Component { return (
    diff --git a/frontend/components/buttons/DropdownButton/_styles.scss b/frontend/components/buttons/DropdownButton/_styles.scss index 8221835232..0b448932bb 100644 --- a/frontend/components/buttons/DropdownButton/_styles.scss +++ b/frontend/components/buttons/DropdownButton/_styles.scss @@ -52,3 +52,15 @@ } } } + +.downcarat { + &::after { + content: url("../assets/images/icon-chevron-white-16x16@2x.png"); + transform: scale(0.5); + width: 16px; + border-radius: 0px; + padding: 0px; + padding-left: 2px; + margin-top: 2px; + } +} diff --git a/frontend/components/forms/admin/AppConfigForm/AppConfigForm.jsx b/frontend/components/forms/admin/AppConfigForm/AppConfigForm.jsx index 62f1b3565d..7b007e9739 100644 --- a/frontend/components/forms/admin/AppConfigForm/AppConfigForm.jsx +++ b/frontend/components/forms/admin/AppConfigForm/AppConfigForm.jsx @@ -56,7 +56,11 @@ const formFields = [ ]; const Header = ({ showAdvancedOptions }) => { const CaratIcon = ( - + + />
); } diff --git a/frontend/components/forms/fields/SelectTargetsDropdown/TargetOption/_styles.scss b/frontend/components/forms/fields/SelectTargetsDropdown/TargetOption/_styles.scss index 4ef2629c21..18f308deff 100644 --- a/frontend/components/forms/fields/SelectTargetsDropdown/TargetOption/_styles.scss +++ b/frontend/components/forms/fields/SelectTargetsDropdown/TargetOption/_styles.scss @@ -26,10 +26,11 @@ } &__add-btn { - transition: color 0ms; - color: $core-vibrant-blue; - - font-size: $medium; + content: url("../assets/images/icon-plus-circle-16x16@2x.png"); + height: 16px; + width: 16px; + margin-top: 8px; + border: none; } &__target-content { diff --git a/frontend/components/forms/fields/SelectTargetsDropdown/_styles.scss b/frontend/components/forms/fields/SelectTargetsDropdown/_styles.scss index ffbe37c68b..2b6f9a3066 100644 --- a/frontend/components/forms/fields/SelectTargetsDropdown/_styles.scss +++ b/frontend/components/forms/fields/SelectTargetsDropdown/_styles.scss @@ -31,12 +31,30 @@ } .Select-arrow { - border-width: 8px 8px 2.5px; + content: url("../assets/images/icon-chevron-black-16x16@2x.png"); + height: 16px; + width: 16px; + margin-top: 6px; + margin-left: 6px; + border: none; + + &:hover { + content: url("../assets/images/icon-chevron-blue-16x16@2x.png"); + } } &.is-open { .Select-arrow { - border-width: 2.5px 8px 8px; + content: url("../assets/images/icon-collapse-black-16x16@2x.png"); + height: 16px; + width: 16px; + margin-top: 11px; + margin-left: 6px; + border: none; + + &:hover { + content: url("../assets/images/icon-collapse-blue-16x16@2x.png"); + } } .Select-arrow-zone { @@ -67,7 +85,7 @@ } .Select-arrow-zone { - padding-right: 19px; + padding-right: 30px; } &.Select--multi { @@ -139,32 +157,20 @@ border: solid 1px $ui-fleet-blue-15; .Select-value-icon { - border: 0; - float: right; - position: relative; - line-height: 34px; - width: 25px; - padding: 0; - margin: 0 5px; - text-indent: -999em; + height: 100%; &::after { - @extend %kolidecon; - transition: color 150ms ease-in-out; - transform: translate(-50%, -50%); - content: "\2715"; // hex code for multiplication cross - position: absolute; - top: 50%; - left: 50%; - visibility: visible; - color: $core-fleet-black; - display: block; - text-indent: 0; + content: url("../assets/images/icon-close-fleet-blue-16x16@2x.png"); + transform: scale(0.5); + border-radius: 0px; + margin-top: -15px; + margin-left: -15px; } &:hover { &::after { - color: $ui-error; + content: url("../assets/images/icon-close-vibrant-blue-16x16@2x.png"); + transform: scale(0.5); } } } @@ -179,17 +185,32 @@ } .Select-clear { - font-size: $x-large; - margin-right: 10px; - color: $core-fleet-black; + content: url("../assets/images/icon-close-fleet-blue-16x16@2x.png"); + width: 16px; + height: 16px; + border-radius: 0px; + margin-left: -4px; + margin-top: 6px; &:hover { - color: $ui-error; + content: url("../assets/images/icon-close-vibrant-blue-16x16@2x.png"); + width: 16px; + height: 16px; + border-radius: 0px; } } + .Select-clear-zone { + width: 30px; + } + + .Select-arrow-zone { + width: 30px; + margin-right: 5px; + } + .Select-loading-zone { - padding-right: 10px; + margin-right: 10px; } &.is-empty { diff --git a/frontend/components/modals/Modal/Modal.tsx b/frontend/components/modals/Modal/Modal.tsx index a057eef1e7..eab0843309 100644 --- a/frontend/components/modals/Modal/Modal.tsx +++ b/frontend/components/modals/Modal/Modal.tsx @@ -1,10 +1,6 @@ import React, { Component } from "react"; import classnames from "classnames"; -// ignore TS error for now until these are rewritten in ts. -// @ts-ignore -import KolideIcon from "components/icons/KolideIcon"; - const baseClass = "modal"; interface IModalProps { @@ -28,9 +24,7 @@ class Modal extends Component {
{title}
- +
{children}
diff --git a/frontend/components/modals/Modal/_styles.scss b/frontend/components/modals/Modal/_styles.scss index 37a98063ad..15604fe3f5 100644 --- a/frontend/components/modals/Modal/_styles.scss +++ b/frontend/components/modals/Modal/_styles.scss @@ -21,14 +21,17 @@ text-decoration: none; .button { - @include size(30px); - line-height: 30px; - color: $core-fleet-black; - font-size: $x-large; + content: url("../assets/images/icon-close-fleet-blue-16x16@2x.png"); + width: 17px; + height: 17px; + border-radius: 0px; + } - &:hover { - color: $ui-error; - } + .button:hover { + content: url("../assets/images/icon-close-vibrant-blue-16x16@2x.png"); + width: 17px; + height: 17px; + border-radius: 0px; } } diff --git a/frontend/components/packs/PacksList/Row/_styles.scss b/frontend/components/packs/PacksList/Row/_styles.scss index d4b5d13aeb..ab17fd65a2 100644 --- a/frontend/components/packs/PacksList/Row/_styles.scss +++ b/frontend/components/packs/PacksList/Row/_styles.scss @@ -3,7 +3,7 @@ cursor: pointer; &--selected { - background-color: $ui-vibrant-blue-10; + background-color: $ui-vibrant-blue-25; } &:focus { diff --git a/frontend/components/queries/QueriesList/QueriesListRow/_styles.scss b/frontend/components/queries/QueriesList/QueriesListRow/_styles.scss index 08a83d0bb8..9b1ad30c13 100644 --- a/frontend/components/queries/QueriesList/QueriesListRow/_styles.scss +++ b/frontend/components/queries/QueriesList/QueriesListRow/_styles.scss @@ -11,7 +11,7 @@ } &--selected { - background-color: $ui-vibrant-blue-10; + background-color: $ui-vibrant-blue-25; } &:hover { diff --git a/frontend/components/queries/ScheduledQueriesList/ScheduledQueriesListItem/_styles.scss b/frontend/components/queries/ScheduledQueriesList/ScheduledQueriesListItem/_styles.scss index 4d365643bc..9e6468be80 100644 --- a/frontend/components/queries/ScheduledQueriesList/ScheduledQueriesListItem/_styles.scss +++ b/frontend/components/queries/ScheduledQueriesList/ScheduledQueriesListItem/_styles.scss @@ -8,6 +8,6 @@ } &--selected { - background-color: $ui-vibrant-blue-10; + background-color: $ui-vibrant-blue-25; } } diff --git a/frontend/components/side_panels/HostSidePanel/PanelGroupItem/_styles.scss b/frontend/components/side_panels/HostSidePanel/PanelGroupItem/_styles.scss index 035a709492..3d398e58cd 100644 --- a/frontend/components/side_panels/HostSidePanel/PanelGroupItem/_styles.scss +++ b/frontend/components/side_panels/HostSidePanel/PanelGroupItem/_styles.scss @@ -13,7 +13,7 @@ $base-class: "panel-group-item"; &.#{$base-class}--selected { font-weight: $bold; - background-color: $ui-vibrant-blue-10; + background-color: $ui-vibrant-blue-25; .#{$base-class}__count { font-weight: $bold; diff --git a/frontend/components/side_panels/PackDetailsSidePanel/ScheduledQueriesSection.jsx b/frontend/components/side_panels/PackDetailsSidePanel/ScheduledQueriesSection.jsx index 0a75f3a68c..460b7613e4 100644 --- a/frontend/components/side_panels/PackDetailsSidePanel/ScheduledQueriesSection.jsx +++ b/frontend/components/side_panels/PackDetailsSidePanel/ScheduledQueriesSection.jsx @@ -2,6 +2,8 @@ import React, { Component } from "react"; import PropTypes from "prop-types"; import { Link } from "react-router"; +import PATHS from "router/paths"; + import scheduledQueryInterface from "interfaces/scheduled_query"; const baseClass = "pack-details-side-panel"; @@ -23,7 +25,7 @@ class ScheduledQueriesSection extends Component { return (
  • {scheduledQuery.name} diff --git a/frontend/components/side_panels/PackDetailsSidePanel/ScheduledQueriesSection.tests.jsx b/frontend/components/side_panels/PackDetailsSidePanel/ScheduledQueriesSection.tests.jsx index 21d61a630b..8e69206f18 100644 --- a/frontend/components/side_panels/PackDetailsSidePanel/ScheduledQueriesSection.tests.jsx +++ b/frontend/components/side_panels/PackDetailsSidePanel/ScheduledQueriesSection.tests.jsx @@ -1,5 +1,6 @@ import React from "react"; import { mount } from "enzyme"; +import PATHS from "router/paths"; import ScheduledQueriesSection from "components/side_panels/PackDetailsSidePanel/ScheduledQueriesSection"; import { scheduledQueryStub } from "test/stubs"; @@ -11,7 +12,8 @@ describe("ScheduledQueriesSection - component", () => { ); const Link = Component.find("Link"); + const path = `${PATHS.EDIT_QUERY({ id: scheduledQuery.query_id })}`; - expect(Link.prop("to")).toEqual(`/queries/${scheduledQuery.query_id}`); + expect(Link.prop("to")).toEqual(path); }); }); diff --git a/frontend/interfaces/query.ts b/frontend/interfaces/query.ts index b4776cfd62..2ac7806c29 100644 --- a/frontend/interfaces/query.ts +++ b/frontend/interfaces/query.ts @@ -5,6 +5,8 @@ export default PropTypes.shape({ name: PropTypes.string, query: PropTypes.string, id: PropTypes.number, + interval: PropTypes.number, + last_excuted: PropTypes.string, }); export interface IQuery { @@ -12,4 +14,6 @@ export interface IQuery { name: string; query: string; id: number; + interval: number; + last_excuted: string; } diff --git a/frontend/kolide/entities/hosts.js b/frontend/kolide/entities/hosts.js index eb1e501192..26c1c933a9 100644 --- a/frontend/kolide/entities/hosts.js +++ b/frontend/kolide/entities/hosts.js @@ -8,6 +8,16 @@ export default (client) => { return client.authenticatedDelete(endpoint); }, + + refetch: (host) => { + const { HOSTS } = endpoints; + const endpoint = client._endpoint(`${HOSTS}/${host.id}/refetch`); + + return client + .authenticatedPost(endpoint) + .then((response) => response.host); + }, + load: (hostID) => { const { HOSTS } = endpoints; const endpoint = client._endpoint(`${HOSTS}/${hostID}`); diff --git a/frontend/kolide/helpers.ts b/frontend/kolide/helpers.ts index 64d1e22b2b..0853083f39 100644 --- a/frontend/kolide/helpers.ts +++ b/frontend/kolide/helpers.ts @@ -301,6 +301,26 @@ export const humanHostDetailUpdated = (detailUpdated: string): string => { return moment(detailUpdated).fromNow(); }; +export const humanQueryLastRun = (lastRun: string): string => { + // Handles the case when a query has never been ran. + // July 28, 2016 is the date of the initial commit to kolide/fleet. + if (lastRun < "2016-07-28T00:00:00Z") { + return "Never"; + } + + return moment(lastRun).fromNow(); +}; + +export const secondsToHms = (d: number): string => { + const h = Math.floor(d / 3600); + const m = Math.floor((d % 3600) / 60); + const s = Math.floor((d % 3600) % 60); + + const hDisplay = h > 0 ? h + (h === 1 ? " hr " : " hrs ") : ""; + const mDisplay = m > 0 ? m + (m === 1 ? " min " : " mins ") : ""; + const sDisplay = s > 0 ? s + (s === 1 ? " sec " : " secs ") : ""; + return hDisplay + mDisplay + sDisplay; +}; export default { addGravatarUrlToResource, formatConfigDataForServer, @@ -313,6 +333,8 @@ export default { humanHostEnrolled, humanHostMemory, humanHostDetailUpdated, + humanQueryLastRun, + secondsToHms, labelSlug, setupData, }; diff --git a/frontend/pages/ForgotPasswordPage/ForgotPasswordPage.jsx b/frontend/pages/ForgotPasswordPage/ForgotPasswordPage.jsx index 98c6e9ab49..8ecef067a0 100644 --- a/frontend/pages/ForgotPasswordPage/ForgotPasswordPage.jsx +++ b/frontend/pages/ForgotPasswordPage/ForgotPasswordPage.jsx @@ -4,6 +4,8 @@ import { connect } from "react-redux"; import { push } from "react-router-redux"; import { noop } from "lodash"; +import PATHS from "router/paths"; + import { clearForgotPasswordErrors, forgotPasswordAction, @@ -84,7 +86,7 @@ export class ForgotPasswordPage extends Component { return ( diff --git a/frontend/pages/hosts/HostDetailsPage/HostDetailsPage.jsx b/frontend/pages/hosts/HostDetailsPage/HostDetailsPage.jsx index 948b20846e..e968f4239f 100644 --- a/frontend/pages/hosts/HostDetailsPage/HostDetailsPage.jsx +++ b/frontend/pages/hosts/HostDetailsPage/HostDetailsPage.jsx @@ -3,6 +3,7 @@ import PropTypes from "prop-types"; import { connect } from "react-redux"; import classnames from "classnames"; +import { Link } from "react-router"; import ReactTooltip from "react-tooltip"; import { noop, pick } from "lodash"; @@ -10,6 +11,7 @@ import Spinner from "components/loaders/Spinner"; import Button from "components/buttons/Button"; import Modal from "components/modals/Modal"; import SoftwareListRow from "pages/hosts/HostDetailsPage/SoftwareListRow"; +import PackQueriesListRow from "pages/hosts/HostDetailsPage/PackQueriesListRow"; import entityGetter from "redux/utilities/entityGetter"; import queryActions from "redux/nodes/entities/queries/actions"; @@ -17,6 +19,13 @@ import queryInterface from "interfaces/query"; import { renderFlash } from "redux/nodes/notifications/actions"; import { push } from "react-router-redux"; import PATHS from "router/paths"; +import { + Accordion, + AccordionItem, + AccordionItemHeading, + AccordionItemButton, + AccordionItemPanel, +} from "react-accessible-accordion"; import hostInterface from "interfaces/host"; import { @@ -25,10 +34,13 @@ import { humanHostEnrolled, humanHostMemory, humanHostDetailUpdated, + secondsToHms, } from "kolide/helpers"; import helpers from "./helpers"; import SelectQueryModal from "./SelectQueryModal"; +import BackChevron from "../../../../assets/images/icon-chevron-down-9x6@2x.png"; + const baseClass = "host-details"; export class HostDetailsPage extends Component { @@ -52,6 +64,7 @@ export class HostDetailsPage extends Component { this.state = { showDeleteHostModal: false, showQueryHostModal: false, + showRefetchLoadingSpinner: false, }; } @@ -67,8 +80,9 @@ export class HostDetailsPage extends Component { const { dispatch, hostID } = this.props; const { fetchHost } = helpers; - fetchHost(dispatch, hostID); - + fetchHost(dispatch, hostID).then((host) => + this.setState({ showRefetchLoadingSpinner: host.refetch_requested }) + ); return false; } @@ -88,6 +102,21 @@ export class HostDetailsPage extends Component { return false; }; + onRefetchHost = () => { + const { dispatch, host } = this.props; + const { refetchHost } = helpers; + + this.setState({ showRefetchLoadingSpinner: true }); + + refetchHost(dispatch, host).catch((error) => { + this.setState({ showRefetchLoadingSpinner: false }); + console.log(error); + dispatch(renderFlash("error", `Host "${host.hostname}" refetch error`)); + }); + + return false; + }; + onPackClick = (pack) => { const { dispatch } = this.props; @@ -151,7 +180,7 @@ export class HostDetailsPage extends Component { revoke the host's enroll secret.

    -
    @@ -219,7 +248,7 @@ export class HostDetailsPage extends Component { }); return ( -
    +

    Labels

    {labels.length === 0 ? (

    No labels are associated with this host.

    @@ -231,31 +260,63 @@ export class HostDetailsPage extends Component { }; renderPacks = () => { - const { onPackClick } = this; const { host } = this.props; - const { packs = [] } = host; + const { pack_stats } = host; + const wrapperClassName = `${baseClass}__table`; - const packItems = packs.map((pack) => { - return ( -
  • - -
  • - ); - }); + let packsAccordion; + if (pack_stats) { + packsAccordion = pack_stats.map((pack) => { + return ( + + + {pack.pack_name} + + + {pack.query_stats.length === 0 ? ( +
    There are no schedule queries for this pack.
    + ) : ( +
    + + + + + + + + + + + {!!pack.query_stats.length && + pack.query_stats.map((query) => { + return ( + + ); + })} + +
    Query nameDescriptionFrequencyLast run
    +
    + )} +
    +
    + ); + }); + } return (

    Packs

    - {packs.length === 0 ? ( -

    No packs have this host as a target.

    + {!pack_stats ? ( +

    + No packs with scheduled queries have this host as a target. +

    ) : ( -
      {packItems}
    + + {packsAccordion} + )}
    ); @@ -306,6 +367,51 @@ export class HostDetailsPage extends Component { ); }; + renderRefetch = () => { + const { onRefetchHost } = this; + const { host } = this.props; + const { showRefetchLoadingSpinner } = this.state; + + const isOnline = host.status === "online"; + const isOffline = host.status === "offline"; + return ( + <> +
    + +
    + + + You can’t fetch data from
    an offline host. +
    +
    + + ); + }; + render() { const { host, isLoadingHost, dispatch, queries, queryErrors } = this.props; const { showQueryHostModal } = this.state; @@ -316,6 +422,7 @@ export class HostDetailsPage extends Component { renderLabels, renderSoftware, renderPacks, + renderRefetch, } = this; const titleData = pick(host, [ @@ -344,6 +451,12 @@ export class HostDetailsPage extends Component { Object.keys(object).forEach((key) => { if (object[key] === "") { object[key] = "--"; + } else if ( + key === "logger_tls_period" || + key === "config_tls_refresh" || + key === "distributed_interval" + ) { + object[key] = secondsToHms(object[key]); } }); }); @@ -356,13 +469,22 @@ export class HostDetailsPage extends Component { return (
    +
    + + back chevron + Back to all hosts + +

    {host.hostname}

    -

    {`Last fetched ${humanHostDetailUpdated( - titleData.detail_updated_at - )}`}

    +

    + {`Last fetched ${humanHostDetailUpdated( + titleData.detail_updated_at + )}`}{" "} +

    + {renderRefetch()}
    @@ -395,22 +517,20 @@ export class HostDetailsPage extends Component {
    {renderActionButtons()}
    -
    +

    About this host

    - Last seen - Enrolled - Uptime -
    -
    - - {humanHostLastSeen(aboutData.seen_time)} - + Created at {humanHostEnrolled(aboutData.last_enrolled_at)} + Updated at + + {humanHostLastSeen(titleData.detail_updated_at)} + + Uptime {humanHostUptime(aboutData.uptime)} @@ -419,33 +539,27 @@ export class HostDetailsPage extends Component {
    Hardware model - Serial number - IP address -
    -
    {aboutData.hardware_model} + Serial number {aboutData.hardware_serial} + IPv4 {aboutData.primary_ip}
    -
    -

    Osquery configuration

    -
    -
    - Config refresh +
    +

    Agent options

    +
    +
    + Config TLS refresh {osqueryData.config_tls_refresh} -
    -
    Logger TLS period {osqueryData.logger_tls_period} -
    -
    Distributed interval {osqueryData.distributed_interval} @@ -455,6 +569,8 @@ export class HostDetailsPage extends Component {
    {renderLabels()} {renderPacks()} + {/* The Software inventory feature is behind a feature flag + so we only render the sofware section if the feature is enabled */} {host.software && renderSoftware()} {renderDeleteHostModal()} {showQueryHostModal && ( diff --git a/frontend/pages/hosts/HostDetailsPage/HostDetailsPage.tests.jsx b/frontend/pages/hosts/HostDetailsPage/HostDetailsPage.tests.jsx index b278d743cf..d4de26283f 100644 --- a/frontend/pages/hosts/HostDetailsPage/HostDetailsPage.tests.jsx +++ b/frontend/pages/hosts/HostDetailsPage/HostDetailsPage.tests.jsx @@ -40,7 +40,7 @@ describe("HostDetailsPage - component", () => { const dispatch = () => Promise.resolve(); const props = { ...propsWithOfflineHost, dispatch }; const page = mount(); - const deleteBtn = page.find("Button").at(1); + const deleteBtn = page.find("Button").at(2); expect(deleteBtn.text()).toBe("Delete"); jest.spyOn(hostActions, "destroy").mockImplementation(() => () => { @@ -66,7 +66,7 @@ describe("HostDetailsPage - component", () => { const dispatch = () => Promise.resolve(); const props = { ...propsWithOnlineHost, dispatch }; const page = mount(); - const deleteBtn = page.find("Button").at(1); + const deleteBtn = page.find("Button").at(2); expect(deleteBtn.text()).toBe("Delete"); jest.spyOn(hostActions, "destroy").mockImplementation(() => () => { @@ -93,7 +93,7 @@ describe("HostDetailsPage - component", () => { const dispatch = () => Promise.resolve(); const props = { ...propsWithOnlineHost, dispatch }; const page = mount(); - const queryBtn = page.find("Button").at(0); + const queryBtn = page.find("Button").at(1); expect(queryBtn.text()).toBe("Query"); expect(page.find("Modal").length).toEqual(0); diff --git a/frontend/pages/hosts/HostDetailsPage/PackQueriesListRow/PackQueriesListRow.jsx b/frontend/pages/hosts/HostDetailsPage/PackQueriesListRow/PackQueriesListRow.jsx new file mode 100644 index 0000000000..7826d5402f --- /dev/null +++ b/frontend/pages/hosts/HostDetailsPage/PackQueriesListRow/PackQueriesListRow.jsx @@ -0,0 +1,37 @@ +import React, { Component } from "react"; +import { humanQueryLastRun, secondsToHms } from "kolide/helpers"; + +import queryInterface from "interfaces/query"; + +const baseClass = "pack-queries-list-row"; + +class PackQueriesListRow extends Component { + static propTypes = { + query: queryInterface.isRequired, + }; + + render() { + const { query } = this.props; + const { + scheduled_query_name, + description, + interval, + last_executed, + } = query; + + const frequency = secondsToHms(interval); + + return ( + + {scheduled_query_name} + {description} + {frequency} + + {humanQueryLastRun(last_executed)} + + + ); + } +} + +export default PackQueriesListRow; diff --git a/frontend/pages/hosts/HostDetailsPage/PackQueriesListRow/_styles.scss b/frontend/pages/hosts/HostDetailsPage/PackQueriesListRow/_styles.scss new file mode 100644 index 0000000000..a6665e0828 --- /dev/null +++ b/frontend/pages/hosts/HostDetailsPage/PackQueriesListRow/_styles.scss @@ -0,0 +1,29 @@ +.pack-queries-list-row { + line-height: 38px; + border-bottom: 1px solid $ui-fleet-blue-15; + + &__type { + max-width: 280px; + } + + &__installed-version { + max-width: 280px; + } + + &:last-child { + border-bottom: 0; + } + + td { + font-size: $x-small; + + .form-field { + margin: 0; + } + } + + &__name { + @include ellipsis(120px); + display: table-cell; + } +} diff --git a/frontend/pages/hosts/HostDetailsPage/PackQueriesListRow/index.js b/frontend/pages/hosts/HostDetailsPage/PackQueriesListRow/index.js new file mode 100644 index 0000000000..926ed24fa8 --- /dev/null +++ b/frontend/pages/hosts/HostDetailsPage/PackQueriesListRow/index.js @@ -0,0 +1 @@ +export { default } from "./PackQueriesListRow"; diff --git a/frontend/pages/hosts/HostDetailsPage/SelectQueryModal/SelectQueryModal.jsx b/frontend/pages/hosts/HostDetailsPage/SelectQueryModal/SelectQueryModal.jsx index 0769fd0d57..15bef27773 100644 --- a/frontend/pages/hosts/HostDetailsPage/SelectQueryModal/SelectQueryModal.jsx +++ b/frontend/pages/hosts/HostDetailsPage/SelectQueryModal/SelectQueryModal.jsx @@ -57,6 +57,18 @@ const SelectQueryModal = (props) => { }); }; + const customQueryButton = () => { + return ( + + ); + }; + const onFilterQueries = (event) => { setQueriesFilter(event); return false; @@ -69,7 +81,7 @@ const SelectQueryModal = (props) => { const results = () => { if (queryErrors) { return ( -
    +
    error icon Something's gone wrong. @@ -86,6 +98,20 @@ const SelectQueryModal = (props) => { open new tab + {customQueryButton()} +
    + ); + } + + if (!queriesFilter && queriesCount === 0) { + return ( +
    + You have no saved queries. + + Expecting to see queries? Try again in a few seconds as the system + catches up. + + {customQueryButton()}
    ); } @@ -106,43 +132,43 @@ const SelectQueryModal = (props) => { }); return (
    -
    - - +
    +
    + + +
    +
    + OR + {customQueryButton()} +
    {queryList}
    ); } - if (!queriesFilter && queriesCount === 0) { - return ( -
    - You have no saved queries. - - Expecting to see queries? Try again in a few seconds as the system - catches up. - -
    - ); - } - if (queriesFilter && queriesCount === 0) { return (
    -
    - - +
    +
    + + +
    +
    + OR + {customQueryButton()} +
    @@ -165,15 +191,6 @@ const SelectQueryModal = (props) => { className={`${baseClass}__modal`} > {results()} -

    - -

    ); }; diff --git a/frontend/pages/hosts/HostDetailsPage/SelectQueryModal/_styles.scss b/frontend/pages/hosts/HostDetailsPage/SelectQueryModal/_styles.scss index 2a93e306eb..71b1356f97 100644 --- a/frontend/pages/hosts/HostDetailsPage/SelectQueryModal/_styles.scss +++ b/frontend/pages/hosts/HostDetailsPage/SelectQueryModal/_styles.scss @@ -46,8 +46,23 @@ } } + &__no-queries { + .info { + &__header { + margin: $pad-medium 0 $pad-medium 0; + } + &__data { + margin: 0 0 $pad-large 0; + } + } + } + + &__query-modal { + display: flex; + } + &__filter-queries { - flex-grow: 1; + flex-grow: 3; position: relative; .input-field { @@ -63,9 +78,14 @@ } } + &__create-query { + span { + margin: 15px; + font-weight: bold; + } + } + &__custom-query-button { font-size: $x-small; - color: $core-vibrant-blue; - font-weight: $bold; } } diff --git a/frontend/pages/hosts/HostDetailsPage/_styles.scss b/frontend/pages/hosts/HostDetailsPage/_styles.scss index 126722c77e..576e55e69f 100644 --- a/frontend/pages/hosts/HostDetailsPage/_styles.scss +++ b/frontend/pages/hosts/HostDetailsPage/_styles.scss @@ -1,17 +1,27 @@ .host-details { display: flex; - flex-direction: column; + flex-wrap: wrap; + flex-grow: 1; + align-content: flex-start; padding-bottom: 50px; min-width: 0; + background-color: $ui-off-white; + gap: $pad-large; .section { + flex: 100%; display: flex; flex-direction: column; - margin: $pad-xxlarge 0 0; + background-color: $core-white; + border-radius: 16px; + border: 1px solid $ui-fleet-blue-15; + padding: $pad-xxlarge; + box-shadow: 0px 3px 0px rgba(226, 228, 234, 0.4); &__header { - margin: 0 0 $pad-medium; font-size: $medium; + font-weight: bold; + margin: 0 0 $pad-large 0; } .info { @@ -50,16 +60,18 @@ } .results { - margin: auto; + margin: 0; width: 350px; &__header { + margin: 0 0 $pad-medium 0; font-size: $small; color: $core-fleet-black; font-weight: $bold; } &__data { + margin: 0; font-size: $x-small; } } @@ -92,9 +104,75 @@ margin: 0; padding-left: $pad-small; } + + .refetch { + display: flex; + + .refetch-btn { + color: $core-vibrant-blue; + cursor: default; + font-size: $x-small; + display: inline; + margin: 9px 0 0 0; // aligns with spinner + + &::before { + display: inline-block; + position: relative; + padding: 5px 0 0 0; // centers spin + content: url(../assets/images/icon-refetch-12x12@2x.png); + transform: scale(0.5); + vertical-align: bottom; + height: 20px; + } + + &:hover { + cursor: pointer; + } + } + + .refetch-offline { + opacity: 25%; + + &:hover { + cursor: default; + } + } + + .refetch-spinner { + color: $core-vibrant-blue; + cursor: default; + font-size: $x-small; + display: inline; + opacity: 50%; + filter: saturate(100%); + + &::before { + display: inline-block; + position: relative; + top: 7px; // aligns with text + padding: 5px 0 0 0; // centers spin + display: inline-block; + content: url(../assets/images/icon-refetch-12x12@2x.png); + transform: scale(0.5); + animation: spin 2s linear infinite; + } + + @keyframes spin { + 0% { + transform: scale(0.5) rotate(0deg); + transform-origin: center center; + } + 100% { + transform: scale(0.5) rotate(360deg); + transform-origin: center center; + } + } + } + } } - .about { + .about, + .osquery { .info { &__item { &--about { @@ -107,18 +185,25 @@ display: flex; flex-direction: column; margin-right: $pad-xxlarge; + } + &__data { + margin-bottom: $pad-medium; - span { - margin-bottom: $pad-medium; - - &:last-child { - margin-bottom: 0; - } + &:last-child { + margin-bottom: 0; } } } } + .col-50 { + flex: 2; + } + + .col-25 { + flex: 1; + } + .status { color: $core-fleet-black; text-transform: capitalize; @@ -224,4 +309,83 @@ white-space: nowrap; } } + + &__back-link { + display: flex; + align-items: center; + height: 16px; + font-size: $x-small; + color: $core-vibrant-blue; + font-weight: $bold; + text-decoration: none; + } + + #back-chevron { + width: 16px; + margin-right: $pad-small; + } + + .accordion { + border-radius: 2px; + } + + .accordion__item + .accordion__item { + border-top: 1px solid rgba(0, 0, 0, 0.1); + } + + .accordion__button { + background-color: #fff; + color: $core-fleet-black; + cursor: pointer; + text-align: left; + font-size: $x-small; + font-weight: bold; + border: none; + padding: 17px 12px; + } + + .accordion__button:hover { + background-color: $ui-vibrant-blue-10; + } + + .accordion__button:after { + display: block; + content: url("../assets/images/icon-chevron-purple-9x6@2x.png"); + text-align: center; + top: 50%; + float: right; + width: 32px; + height: 32px; + border-radius: 4px; + transform: scale(0.5) translate(-50%, -60%); + } + + .accordion__button[aria-expanded="true"]::after, + .accordion__button[aria-selected="true"]::after { + background-color: $core-vibrant-blue; + content: url("../assets/images/icon-accordion-collapse-16x16@2x.png"); + } + + [hidden] { + display: none; + } + + .accordion__panel { + padding: 20px; + animation: fadein 0.35s ease-in; + } + + /* -------------------------------------------------- */ + /* ---------------- Animation part ------------------ */ + /* -------------------------------------------------- */ + + @keyframes fadein { + 0% { + opacity: 0; + } + + 100% { + opacity: 1; + } + } } diff --git a/frontend/pages/hosts/HostDetailsPage/helpers.js b/frontend/pages/hosts/HostDetailsPage/helpers.js index bf9a54dc2c..b16e8fb1d1 100644 --- a/frontend/pages/hosts/HostDetailsPage/helpers.js +++ b/frontend/pages/hosts/HostDetailsPage/helpers.js @@ -16,6 +16,10 @@ export const destroyHost = (dispatch, host) => { }); }; +export const refetchHost = (dispatch, host) => { + return dispatch(hostActions.refetchHostStart(host)); +}; + export const queryHostCustom = (dispatch, host) => { return dispatch( push({ @@ -34,4 +38,10 @@ export const queryHostSaved = (dispatch, host, selectedQuery) => { ); }; -export default { fetchHost, destroyHost, queryHostCustom, queryHostSaved }; +export default { + fetchHost, + destroyHost, + queryHostCustom, + queryHostSaved, + refetchHost, +}; diff --git a/frontend/pages/hosts/ManageHostsPage/components/AddHostModal/AddHostModal.jsx b/frontend/pages/hosts/ManageHostsPage/components/AddHostModal/AddHostModal.jsx index 725608751b..7354fe54cd 100644 --- a/frontend/pages/hosts/ManageHostsPage/components/AddHostModal/AddHostModal.jsx +++ b/frontend/pages/hosts/ManageHostsPage/components/AddHostModal/AddHostModal.jsx @@ -152,7 +152,7 @@ class AddHostModal extends Component {

    diff --git a/frontend/redux/nodes/entities/hosts/actions.js b/frontend/redux/nodes/entities/hosts/actions.js index 88fc6d5131..b78c2e142c 100644 --- a/frontend/redux/nodes/entities/hosts/actions.js +++ b/frontend/redux/nodes/entities/hosts/actions.js @@ -1,7 +1,49 @@ +import Kolide from "kolide"; import config from "./config"; const { actions } = config; +export const LOAD_PAGINATED = "LOAD_PAGINATED"; +export const REFETCH_HOST_START = "REFETCH_HOST_START"; +export const REFETCH_HOST_SUCCESS = "REFETCH_HOST_SUCCESS"; +export const REFETCH_HOST_FAILURE = "REFETCH_HOST"; + +export const loadPaginated = () => { + const { loadRequest } = actions; + return (dispatch) => { + dispatch(loadRequest()); + + return Kolide.hosts; + }; +}; + +export const refetchHostSuccess = (data) => { + return { type: REFETCH_HOST_SUCCESS, payload: { data } }; +}; + +export const refetchHostFailure = (errors) => { + return { type: REFETCH_HOST_FAILURE, payload: { errors } }; +}; + +export const refetchHostStart = (host) => { + return (dispatch) => { + return Kolide.hosts + .refetch(host) + .then((data) => { + dispatch(refetchHostSuccess(data)); + return data; + }) + .catch((errors) => { + dispatch(refetchHostFailure(errors)); + + throw errors; + }); + }; +}; + export default { ...actions, + refetchHostSuccess, + refetchHostFailure, + refetchHostStart, }; diff --git a/frontend/redux/nodes/entities/hosts/reducer.js b/frontend/redux/nodes/entities/hosts/reducer.js index 3dd9f98a31..d69a3606cb 100644 --- a/frontend/redux/nodes/entities/hosts/reducer.js +++ b/frontend/redux/nodes/entities/hosts/reducer.js @@ -1,3 +1,20 @@ -import config from "./config"; +import { REFETCH_HOST_SUCCESS, REFETCH_HOST_FAILURE } from "./actions"; +import config, { initialState } from "./config"; -export default config.reducer; +export default (state = initialState, { type, payload }) => { + switch (type) { + case REFETCH_HOST_SUCCESS: + return { + ...state, + loading: false, + }; + case REFETCH_HOST_FAILURE: + return { + ...state, + errors: payload.errors, + loading: false, + }; + default: + return config.reducer(state, { type, payload }); + } +}; diff --git a/frontend/styles/global/_global.scss b/frontend/styles/global/_global.scss index 1036a56537..3aef2cf874 100644 --- a/frontend/styles/global/_global.scss +++ b/frontend/styles/global/_global.scss @@ -46,7 +46,7 @@ a { } .wrapper { - background: $gradients-dark-gradient; + background: $gradients-dark-gradient-vertical; margin: 0; } diff --git a/go.mod b/go.mod index 0b6b078541..957a5fe480 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/briandowns/spinner v0.0.0-20170614154858-48dbb65d7bd5 github.com/cenkalti/backoff/v4 v4.0.0 github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect + github.com/davecgh/go-spew v1.1.1 github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/e-dard/netbug v0.0.0-20151029172837-e64d308a0b20 github.com/elazarl/go-bindata-assetfs v1.0.0 @@ -23,8 +23,8 @@ require ( github.com/ghodss/yaml v1.0.0 github.com/go-kit/kit v0.9.0 github.com/go-sql-driver/mysql v1.5.0 - github.com/gomodule/redigo v2.0.0+incompatible - github.com/google/go-cmp v0.5.5 // indirect + github.com/gomodule/redigo v1.8.4 + github.com/google/go-cmp v0.5.5 github.com/google/uuid v0.0.0-20161128191214-064e2069ce9c github.com/gorilla/mux v1.6.2 github.com/gorilla/websocket v1.4.2 @@ -59,7 +59,7 @@ require ( github.com/stretchr/testify v1.6.1 github.com/theupdateframework/go-tuf v0.0.0-20201230183259-aee6270feb55 github.com/throttled/throttled v2.2.5+incompatible - github.com/throttled/throttled/v2 v2.7.1 + github.com/throttled/throttled/v2 v2.7.2 github.com/urfave/cli/v2 v2.3.0 go.opencensus.io v0.20.2 // indirect golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 diff --git a/go.sum b/go.sum index cf41a90436..9b57d40fd4 100644 --- a/go.sum +++ b/go.sum @@ -8,8 +8,11 @@ github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= +github.com/Microsoft/go-winio v0.4.9 h1:3RbgqgGVqmcpbOiwrjbVtDHLlJBGF6aE+yHmNtBNsFQ= github.com/Microsoft/go-winio v0.4.9/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= +github.com/Shopify/sarama v1.19.0 h1:9oksLxC6uxVPHPVYUmq6xhr1BOF/hHobWH2UzO67z1s= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= +github.com/Shopify/toxiproxy v2.1.4+incompatible h1:TKdv8HiTLgE5wdJuEML90aBgNWsokNbMijUGhmcoBJc= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrdtl/UvroE= github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= @@ -17,7 +20,9 @@ github.com/VividCortex/mysqlerr v0.0.0-20170204212430-6c6b55f8796f h1:HR5nRmUQgX github.com/VividCortex/mysqlerr v0.0.0-20170204212430-6c6b55f8796f/go.mod h1:f3HiCrHjHBdcm6E83vGaXh1KomZMA2P6aeo3hKx/wg0= github.com/WatchBeam/clock v0.0.0-20170901150240-b08e6b4da7ea h1:C9Xwp9fZf9BFJMsTqs8P+4PETXwJPUOuJZwBfVci+4A= github.com/WatchBeam/clock v0.0.0-20170901150240-b08e6b4da7ea/go.mod h1:N5eJIl14rhNCrE5I3O10HIyhZ1HpjaRHT9WDg1eXxtI= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/apache/thrift v0.12.0 h1:pODnxUFNcjP9UTLZGTdeh+j16A8lJbRvD3rOtrk/7bs= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= @@ -33,30 +38,35 @@ github.com/c-bata/go-prompt v0.2.3 h1:jjCS+QhG/sULBhAaBdjb2PlMRVaKXQgn+4yzaauvs2 github.com/c-bata/go-prompt v0.2.3/go.mod h1:VzqtzE2ksDBcdln8G7mk2RX9QyGjH+OVqOCSiVIqS34= github.com/cenkalti/backoff/v4 v4.0.0 h1:6VeaLF9aI+MAUQ95106HwWzYZgJJpZ4stumjj6RFYAU= github.com/cenkalti/backoff/v4 v4.0.0/go.mod h1:eEew/i+1Q6OrCDZh3WiXYv3+nJwBASZ8Bog/87DQnVg= +github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/crewjam/saml v0.0.0-20190521120225-344d075952c9 h1:+cz/lCIhz+eg8+jC8cWk5LBLbbpH39IKyHliN6GZyUE= github.com/crewjam/saml v0.0.0-20190521120225-344d075952c9/go.mod h1:w5eu+HNtubx+kRpQL6QFT2F3yIFfYVe6+EzOFVU7Hko= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/e-dard/netbug v0.0.0-20151029172837-e64d308a0b20 h1:eDPsdileewX4H5a2Jph4gS8mFf749gzIrzpbnPy1oRs= github.com/e-dard/netbug v0.0.0-20151029172837-e64d308a0b20/go.mod h1:WXFUXJ0Y/SzNqXmhUU7VkE7a2Pag0zZnE2b6I87YWIs= +github.com/eapache/go-resiliency v1.1.0 h1:1NtRmCAqadE2FN4ZcN6g90TP3uk8cg9rn9eNK2197aU= github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= +github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21 h1:YEetp8/yCZMuEPMUDHG0CW/brkkEp8mzqk2+ODEitlw= github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= +github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= github.com/elazarl/go-bindata-assetfs v1.0.0 h1:G/bYguwHIzWq9ZoyUQqrjTmJbbYn3j3CKKpKinvZLFk= github.com/elazarl/go-bindata-assetfs v1.0.0/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4= github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fleetdm/goose v0.0.0-20210205022138-27718647ba5b h1:Ybt9lIr16BghU3nK0jpeRC4B6JguVLtdubUEcRBPdzQ= -github.com/fleetdm/goose v0.0.0-20210205022138-27718647ba5b/go.mod h1:d7Q+0eCENnKQUhkfAUVLfGnD4QcgJMF/uB9WRTN9TDI= github.com/fleetdm/goose v0.0.0-20210209032905-c3c01484bacb h1:p02npmJlTo+Px1s0VptKOJOJqH/rGlGBEVvLJRtzY3A= github.com/fleetdm/goose v0.0.0-20210209032905-c3c01484bacb/go.mod h1:d7Q+0eCENnKQUhkfAUVLfGnD4QcgJMF/uB9WRTN9TDI= +github.com/flynn/go-docopt v0.0.0-20140912013429-f6dd2ebbb31e h1:Ss/B3/5wWRh8+emnK0++g5zQzwDTi30W10pKxKc4JXI= github.com/flynn/go-docopt v0.0.0-20140912013429-f6dd2ebbb31e/go.mod h1:HyVoz1Mz5Co8TFO8EupIdlcpwShBmY98dkT2xeHkvEI= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= @@ -67,16 +77,19 @@ github.com/go-kit/kit v0.9.0 h1:wDJmvq38kDhkVxi50ni9ykkdUr1PKgqKOoi01fa0Mdk= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0 h1:8HUsc87TaSWLKwrnumgC8/YconD2fJQsRJAsWaPg2ic= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-redis/redis v6.15.8+incompatible h1:BKZuG6mCnRj5AOaWJXoCgf6rqTYnYJLe4en2hxT7r9o= github.com/go-redis/redis v6.15.8+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.0 h1:xU6/SpYbvkNYiptHJYEDRseDLvYE7wSqhYYNy0QSUzI= github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= @@ -86,19 +99,22 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0= -github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= +github.com/gomodule/redigo v1.8.4 h1:Z5JUg94HMTR1XpwBaSH4vq3+PNSIykBLxMdglbw10gg= +github.com/gomodule/redigo v1.8.4/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c h1:964Od4U6p2jUkFxvCydnIczKteheJEzHRToSGK3Bnlw= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57 h1:eqyIo2HjKhKe/mJzTG8n4VqvLXIOEG+SLdDqX7xGtkY= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/uuid v0.0.0-20161128191214-064e2069ce9c h1:jWtZjFEUE/Bz0IeIhqCnyZ3HG6KRXSntXe4SjtuTH7c= github.com/google/uuid v0.0.0-20161128191214-064e2069ce9c/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -117,6 +133,7 @@ github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+l github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce h1:xdsDDbiBDQTKASoGEZ+pEmF1OnWuu8AQ9I8iNbHNeno= github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/igm/sockjs-go/v3 v3.0.0 h1:4wLoB9WCnQ8RI87cmqUH778ACDFVmRpkKRCWBeuc+Ww= github.com/igm/sockjs-go/v3 v3.0.0/go.mod h1:UqchsOjeagIBFHvd+RZpLaVRbCwGilEC08EDHsD1jYE= @@ -130,10 +147,13 @@ github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22 github.com/jonboulle/clockwork v0.2.0/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ= github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024 h1:rBMNdlhTLzJjJSDIjNEXX1Pz3Hmwmz91v+zycvx9PJc= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/julienschmidt/httprouter v1.2.0 h1:TDTW5Yz1mjftljbcKqRcrYhd4XeOoI98t+9HbQbYf7g= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kevinburke/go-bindata v3.22.0+incompatible h1:/JmqEhIWQ7GRScV0WjX/0tqBrC5D21ALg0H0U/KZ/ts= github.com/kevinburke/go-bindata v3.22.0+incompatible/go.mod h1:/pEEZ72flUW2p0yi30bslSp9YqD9pysLxunQDdb2CPM= +github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kolide/kit v0.0.0-20180421083548-36eb8dc43916 h1:voNqT94Ob/YSzma6rQDv1QYob86bTC5xOv1pCKwglLw= github.com/kolide/kit v0.0.0-20180421083548-36eb8dc43916/go.mod h1:N3Yv8okDVC/5qZhPA9uxVYRfkp4mD2vrlQiSCWlNCpg= @@ -141,11 +161,14 @@ github.com/kolide/launcher v0.0.0-20180427153757-cb412b945cf7 h1:6E3WfyhTsIOTL7a github.com/kolide/launcher v0.0.0-20180427153757-cb412b945cf7/go.mod h1:EGgVr4L+g8f041t5OrmY/KV431GlIWhBxsHsT+qc2Mg= github.com/kolide/osquery-go v0.0.0-20190904034940-a74aa860032d h1:alVW+rIOMejarlhjLl4AYaQaGAyfvIUDg9NyyKRhSek= github.com/kolide/osquery-go v0.0.0-20190904034940-a74aa860032d/go.mod h1:QVQKz6+eKB0syu5u8BS1E4S/nTMuy44d9WbRFQ4nLnQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= @@ -172,6 +195,7 @@ github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0j github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238 h1:+MZW2uvHgN8kYvksEN3f7eFL2wpzk0GxmlFsMybWc7E= github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223 h1:F9x/1yl3T2AeKLr2AMdilSD8+f9bvMnNN8VS5iDtovc= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= @@ -179,14 +203,18 @@ github.com/olekukonko/tablewriter v0.0.0-20180506121414-d4647c9c7a84 h1:fiKJgB4J github.com/olekukonko/tablewriter v0.0.0-20180506121414-d4647c9c7a84/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo= github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/openzipkin/zipkin-go v0.1.6 h1:yXiysv1CSK7Q5yjGy1710zZGnsbMUIjluWBxtLXHPBo= github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= github.com/patrickmn/sortutil v0.0.0-20120526081524-abeda66eb583 h1:+gFSK6FP5Ky3BPFrxQjHz92uRsj0DsrBL+xoIbiWRco= github.com/patrickmn/sortutil v0.0.0-20120526081524-abeda66eb583/go.mod h1:DyFPU22sg+Or/eRPmpwVVp0fUw+aQSYddY0DzzNjSN4= github.com/pelletier/go-toml v1.1.0 h1:cmiOvKzEunMsAxyhXSzpL5Q1CRKpVv0KQsnAIcSEVYM= github.com/pelletier/go-toml v1.1.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -206,6 +234,7 @@ github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8 github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1 h1:/K3IL0Z1quvmJ7X0A1AwNEK7CRkVK3YwfOU/QAL4WGg= github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a h1:9ZKAASQSHhDYGoxY8uLVpewe1GDZ2vu2Tr/vTdVAkFQ= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/russellhaering/goxmldsig v0.0.0-20180430223755-7acd5e4a6ef7/go.mod h1:Oz4y6ImuOQZxynhbSXk7btjEfNBtGlj2dcaOvXl2FSM= github.com/russellhaering/goxmldsig v1.1.0 h1:lK/zeJie2sqG52ZAlPNn1oBBqsIsEKypUUBGpYYF6lk= @@ -213,7 +242,9 @@ github.com/russellhaering/goxmldsig v1.1.0/go.mod h1:QK8GhXPB3+AfuCrfo0oRISa9Nfz github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/spf13/afero v1.1.0 h1:bopulORc2JeYaxfHLvJa5NzxviA9PoWhpiiJkru7Ji4= github.com/spf13/afero v1.1.0/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= @@ -235,6 +266,7 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= github.com/tent/canonical-json-go v0.0.0-20130607151641-96e4ba3a7613 h1:iGnD/q9160NWqKZZ5vY4p0dMiYMRknzctfSkqA4nBDw= github.com/tent/canonical-json-go v0.0.0-20130607151641-96e4ba3a7613/go.mod h1:g6AnIpDSYMcphz193otpSIzN+11Rs+AAIIC6rm1enug= @@ -242,12 +274,13 @@ github.com/theupdateframework/go-tuf v0.0.0-20201230183259-aee6270feb55 h1:Zn+mA github.com/theupdateframework/go-tuf v0.0.0-20201230183259-aee6270feb55/go.mod h1:L+uU/NRFK/7h0NYAnsmvsX9EghDB5QVCcHCIrK2h5nw= github.com/throttled/throttled v2.2.5+incompatible h1:65UB52X0qNTYiT0Sohp8qLYVFwZQPDw85uSa65OljjQ= github.com/throttled/throttled v2.2.5+incompatible/go.mod h1:0BjlrEGQmvxps+HuXLsyRdqpSRvJpq0PNIsOtqP9Nos= -github.com/throttled/throttled/v2 v2.7.1 h1:FnBysDX4Sok55bvfDMI0l2Y71V1vM2wi7O79OW7fNtw= -github.com/throttled/throttled/v2 v2.7.1/go.mod h1:fuOeyK9fmnA+LQnsBbfT/mmPHjmkdogRBQxaD8YsgZ8= +github.com/throttled/throttled/v2 v2.7.2 h1:UHk0GmVH5Zk1YiY2Di/C8WdtJ9E0wjjfkEiuS/e0pns= +github.com/throttled/throttled/v2 v2.7.2/go.mod h1:q1QyZVQXxb2NUfJ+Hjucmlrsrz9s/jt2ilMwSMo7a2I= github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= +github.com/zenazn/goji v0.9.0 h1:RSQQAbXGArQ0dIDEq+PI6WqN6if+5KHu6x2Cx/GXLTQ= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs= github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= @@ -260,9 +293,11 @@ golang.org/x/crypto v0.0.0-20190424203555-c05e17bb3b2d/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4 h1:c2HOrn5iMezYjSlGPncknSEr/8x5LELb/ilJbXi9DEA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f h1:hX65Cu3JDlGH3uEdK7I99Ii+9kjD6mvnnpfLdEAH0x4= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -304,14 +339,16 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c h1:fqgJT0MGcGpPgpWU7VRdRjuArfcOvC4AoJmILihzhDg= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138 h1:H3uGjxCR/6Ds0Mjgyp7LMK81+LvmbvWWEnJhzk1Pi9E= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -335,16 +372,19 @@ google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miE google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/guregu/null.v3 v3.4.0 h1:AOpMtZ85uElRhQjEDsFx21BkXqFPwA7uoJukd4KErIs= gopkg.in/guregu/null.v3 v3.4.0/go.mod h1:E4tX2Qe3h7QdL+uZ3a0vqvYwKQsRSQKM5V4YltdgH9Y= gopkg.in/natefinch/lumberjack.v2 v2.0.0-20170531160350-a96e63847dc3 h1:AFxeG48hTWHhDTQDk/m2gorfVHUEa9vo3tp3D7TzwjI= gopkg.in/natefinch/lumberjack.v2 v2.0.0-20170531160350-a96e63847dc3/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -355,4 +395,5 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a h1:/8zB6iBfHCl1qAnEAWwGPNrUvapuy6CPla1VM0k8hQw= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/handbook/README.md b/handbook/README.md index 057147279d..c7a2de284f 100644 --- a/handbook/README.md +++ b/handbook/README.md @@ -44,6 +44,18 @@ In brief, this means that as a Fleet team member, you may: For more detailed information about __spending guidelines and limits__, please read [GitLab's expense policy](https://about.gitlab.com/handbook/spending-company-money/) +### Fleet website + +### When can I merge a change to the website? +When merging a PR to master, bear in mind that whatever you merge to master gets deployed live immediately. So if the PR's changes contain anything that you don't think is appropriate to be seen publicly by all guests of [fleetdm.com](https://fleetdm.com/), then please do not merge. + +Merge a PR (aka deploy the website) when you think it is appropriately clean to represent our brand. When in doubt, use the standards and level of quality seen on existing pages, ensure correct functionality, and check responsive behavior - starting widescreen and resizing down to ≈320px width. + +### The Deploy Fleet Website GitHub action failed +If the action fails, please complete the following steps: +1. Head to the fleetdm-website app in the [Heroku dashboard](https://heroku.com) and select the "Activity" tab. +2. Select "Roll back to here" on the second to most recent deploy. +3. Head to the fleetdm/fleet GitHub repository and re-run the Deploy Fleet Website action. ### About the handbook #### Why bother? diff --git a/handbook/manual-qa.md b/handbook/manual-qa.md new file mode 100644 index 0000000000..a1e55ce3c2 --- /dev/null +++ b/handbook/manual-qa.md @@ -0,0 +1,105 @@ +# Manual QA + +This living document outlines the manual quality assurance process conducted to ensure each release of Fleet meets organization standards. + +All steps should be conducted during each QA pass. + +As new features are added to Fleet, new steps and flows will be added. + +> Note: Currently, the testing focuses on the Fleet UI. As this document grows, more testing procedures for the fleetctl CLI tool will be added. + + +## Collecting bugs + +The goal of manual QA is to catch unexpected behavior prior to release. + +Please start the manual QA process by creating a blank GitHub issue. As you complete each of the flows, record a list of the bugs you encounter in this new issue. Each item in this list should contain one sentence describing the bug and a screenshot if the item is a frontend bug. + +### Clear your local MySQL database + +Before you fire up your local Fleet server, wipe your local MySQL database by running the following command: + +``` +docker volume rm fleet_mysql-persistent-volume +``` + +If you receive an error that says "No such volume," double check that the MySQL volume doesn't have a different name by running this command: + +``` +docker volume ls +``` + +### Start your development server + +Next, fire up your local Fleet server. Check out [this Loom video](https://www.loom.com/share/e7439f058eb44c45af872abe8f8de4a1) for instructions on starting up your local development environment. + +### Set up flow + +Successfully set up Fleet. + +### Login and logout flow + +Successfully logout and then login to your local Fleet. + +### Enroll host flow + +Enroll your local machine to Fleet. Check out the [Orbit for osquery documentation](../docs/2-Orbit-osquery/README.md#packaging) for instructions on generating and installing an Orbit package. + +### Host page + +To populate the Fleet UI with more than just one host you'll need to use the [fleetdm/osquery-perf tool](https://github.com/fleetdm/osquery-perf/tree/629a7efb6097f9108f706ccd45828793ff73cf9c). + +First, clone the fleetdm/osquery perf repo and then run the following commands from the top level of the cloned directory: + +``` +go run agent.go --host_count 200 --enroll_secret +``` + +After about 10 seconds, the Fleet UI should be populated with 200 simulated hosts. + +### Label flow +`Flow is covered by e2e testing` + +Create a new label by selecting "Add a new label" on the Hosts page. Make sure it correctly filters the host on the hosts page. + +Edit this label and then delete this label. + +### Query flow +`Flow is covered by e2e testing` + +Create a new saved query. + +Run this query as a live query against your local machine. + +Edit this query and then delete this query. + +### Pack flow +`Flow is covered by e2e testing` + +Create a new pack. + +Add a query as a saved query to the pack. Remove this query. Delete the pack. + +### Organization settings flow + +As an admin user, select the "Settings" tab in the top navigation and then select "Organization settings". + +Follow [the instructions outlined in the Testing documentation](../docs/4-Contribution/2-Testing.md#email) to set up a local SMTP server. + +Successfully edit your organization's name in Fleet. + +### Manage users flow + +Invite a new user. To be able to invite users, you must have your local SMTP server configured. Instructions for setting up a local SMTP server are outlined in [the Testing documentation](../docs/4-Contribution/2-Testing.md#email) + +Logout of your current admin user and accept the invitation for the newly invited user. With your local SMTP server configured, head to https://localhost:8025 to view and select the invitation link. + +### Agent options flow + +Head to the global agent options page and set the `distributed_iterval` field to `5`. + +Refresh the page to confirm that the agent options have been updated. + +### My account flow + +Head to the My account page by selecting the dropdown icon next to your avatar in the top navigation. Select "My account" and successfully update your password. \ No newline at end of file diff --git a/handbook/queries/README.md b/handbook/queries/README.md index 5ee5c5b46d..1b56c69ed7 100644 --- a/handbook/queries/README.md +++ b/handbook/queries/README.md @@ -27,12 +27,8 @@ Fleet's standard query library includes a growing collection of useful queries f - [Get OS version](./get-os-version.md) (macOS, Linux, Windows, FreeBSD) - [Get mounts](./get-mounts.md) (macOS, Linux) - [Get startup items](./get-startup-items.md) (macOS, Linux, Windows, FreeBSD) - -### Contributors - -Want to add your own query? - -Please submit a pull request using the [Fleet standard query library template](./standard-query-library-template.md). +- [Get system uptime](./get-system-uptime.md) (macOS, Linux, Windows, FreeBSD) +- [Get crashes](./get-crashes.md) (macOS) ### Additional resources diff --git a/handbook/queries/get-crashes.md b/handbook/queries/get-crashes.md new file mode 100644 index 0000000000..cf41ddbd7a --- /dev/null +++ b/handbook/queries/get-crashes.md @@ -0,0 +1,16 @@ +# Get crashes + +Retrieve application, system, and mobile app crash logs. + +### Support +macOS + +### Query +```sql +SELECT uid, datetime, responsible, exception_type, identifier, version, crash_path FROM users CROSS JOIN crashes USING (uid); +``` +### Purpose +Informational + +### Remediation +N/A \ No newline at end of file diff --git a/handbook/queries/get-installed-safari-extensions.md b/handbook/queries/get-installed-safari-extensions.md index d26b050cf4..4e80c0703f 100644 --- a/handbook/queries/get-installed-safari-extensions.md +++ b/handbook/queries/get-installed-safari-extensions.md @@ -1,6 +1,6 @@ # Get installed Safari Extensions -Retreives the list of installed Safari Extensions for all users in the target system. +Retrieves the list of installed Safari Extensions for all users in the target system. ### Support macOS diff --git a/handbook/queries/get-system-uptime.md b/handbook/queries/get-system-uptime.md new file mode 100644 index 0000000000..ccc42b24c0 --- /dev/null +++ b/handbook/queries/get-system-uptime.md @@ -0,0 +1,16 @@ +# Get system uptime + +Shows the system uptime. + +### Support +macOS, Linux, Windows, FreeBSD + +### Query +```sql +SELECT * FROM uptime; +``` +### Purpose +Informational + +### Remediation +N/A \ No newline at end of file diff --git a/package.json b/package.json index c73244c132..c4b5125fd9 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "proxy-middleware": "0.15.0", "rc-pagination": "1.16.3", "react": "^16.8.6", + "react-accessible-accordion": "^3.3.4", "react-ace": "^6.3.2", "react-dom": "^16", "react-entity-getter": "0.0.8", diff --git a/server/config/config.go b/server/config/config.go index 62e6d7a18a..2ec2c535dd 100644 --- a/server/config/config.go +++ b/server/config/config.go @@ -35,10 +35,11 @@ type MysqlConfig struct { // RedisConfig defines configs related to Redis type RedisConfig struct { - Address string - Password string - Database int - UseTLS bool `yaml:"use_tls"` + Address string + Password string + Database int + UseTLS bool `yaml:"use_tls"` + DuplicateResults bool `yaml:"duplicate_results"` } const ( @@ -55,6 +56,7 @@ type ServerConfig struct { TLS bool TLSProfile string // TODO #271 set `yaml:"tls_compatibility"` URLPrefix string `yaml:"url_prefix"` + Keepalive bool `yaml:"keepalive"` } // AuthConfig defines configs related to user authorization @@ -139,9 +141,10 @@ type S3Config struct { // PubSubConfig defines configs the for Google PubSub logging plugin type PubSubConfig struct { - Project string - StatusTopic string `yaml:"status_topic"` - ResultTopic string `yaml:"result_topic"` + Project string + StatusTopic string `yaml:"status_topic"` + ResultTopic string `yaml:"result_topic"` + AddAttributes bool `yaml:"add_attributes"` } // FilesystemConfig defines configs for the Filesystem logging plugin @@ -211,6 +214,7 @@ func (man Manager) addConfigs() { man.addConfigInt("redis.database", 0, "Redis server database number") man.addConfigBool("redis.use_tls", false, "Redis server enable TLS") + man.addConfigBool("redis.duplicate_results", false, "Duplicate Live Query results to another Redis channel") // Server man.addConfigString("server.address", "0.0.0.0:8080", @@ -226,6 +230,8 @@ func (man Manager) addConfigs() { TLSProfileModern, TLSProfileIntermediate)) man.addConfigString("server.url_prefix", "", "URL prefix used on server and frontend endpoints") + man.addConfigBool("server.keepalive", true, + "Controls wether HTTP keep-alives are enabled.") // Auth man.addConfigString("auth.jwt_key", "", @@ -325,6 +331,7 @@ func (man Manager) addConfigs() { man.addConfigString("pubsub.project", "", "Google Cloud Project to use") man.addConfigString("pubsub.status_topic", "", "PubSub topic for status logs") man.addConfigString("pubsub.result_topic", "", "PubSub topic for result logs") + man.addConfigBool("pubsub.add_attributes", false, "Add PubSub attributes in addition to the message body") // Filesystem man.addConfigString("filesystem.status_log_file", "/tmp/osquery_status", @@ -383,10 +390,11 @@ func (man Manager) LoadConfig() KolideConfig { ConnMaxLifetime: man.getConfigInt("mysql.conn_max_lifetime"), }, Redis: RedisConfig{ - Address: man.getConfigString("redis.address"), - Password: man.getConfigString("redis.password"), - Database: man.getConfigInt("redis.database"), - UseTLS: man.getConfigBool("redis.use_tls"), + Address: man.getConfigString("redis.address"), + Password: man.getConfigString("redis.password"), + Database: man.getConfigInt("redis.database"), + UseTLS: man.getConfigBool("redis.use_tls"), + DuplicateResults: man.getConfigBool("redis.duplicate_results"), }, Server: ServerConfig{ Address: man.getConfigString("server.address"), @@ -395,6 +403,7 @@ func (man Manager) LoadConfig() KolideConfig { TLS: man.getConfigBool("server.tls"), TLSProfile: man.getConfigTLSProfile(), URLPrefix: man.getConfigString("server.url_prefix"), + Keepalive: man.getConfigBool("server.keepalive"), }, Auth: AuthConfig{ JwtKey: man.getConfigString("auth.jwt_key"), @@ -459,9 +468,10 @@ func (man Manager) LoadConfig() KolideConfig { StsAssumeRoleArn: man.getConfigString("s3.sts_assume_role_arn"), }, PubSub: PubSubConfig{ - Project: man.getConfigString("pubsub.project"), - StatusTopic: man.getConfigString("pubsub.status_topic"), - ResultTopic: man.getConfigString("pubsub.result_topic"), + Project: man.getConfigString("pubsub.project"), + StatusTopic: man.getConfigString("pubsub.status_topic"), + ResultTopic: man.getConfigString("pubsub.result_topic"), + AddAttributes: man.getConfigBool("pubsub.add_attributes"), }, Filesystem: FilesystemConfig{ StatusLogFile: man.getConfigString("filesystem.status_log_file"), diff --git a/server/datastore/datastore.go b/server/datastore/datastore.go index 7abb75eb30..1ea8eeebdc 100644 --- a/server/datastore/datastore.go +++ b/server/datastore/datastore.go @@ -45,6 +45,7 @@ var TestFunctions = [...]func(*testing.T, kolide.Datastore){ testListHostsInLabel, testListUniqueHostsInLabels, testSaveHosts, + testSaveHostPackStats, testDeleteHost, testListHosts, testListHostsFilterAdditional, diff --git a/server/datastore/datastore_hosts.go b/server/datastore/datastore_hosts.go index 2dd36c9069..a58657c846 100644 --- a/server/datastore/datastore_hosts.go +++ b/server/datastore/datastore_hosts.go @@ -93,6 +93,125 @@ func testSaveHosts(t *testing.T, ds kolide.Datastore) { assert.Nil(t, host) } +func testSaveHostPackStats(t *testing.T, ds kolide.Datastore) { + host, err := ds.NewHost(&kolide.Host{ + DetailUpdateTime: time.Now(), + LabelUpdateTime: time.Now(), + SeenTime: time.Now(), + NodeKey: "1", + UUID: "1", + HostName: "foo.local", + PrimaryIP: "192.168.1.1", + PrimaryMac: "30-65-EC-6F-C4-58", + }) + require.NoError(t, err) + require.NotNil(t, host) + + // Pack and query must exist for stats to save successfully + pack1 := test.NewPack(t, ds, "test1") + query1 := test.NewQuery(t, ds, "time", "select * from time", 0, true) + squery1 := test.NewScheduledQuery(t, ds, pack1.ID, query1.ID, 30, true, true, "time-scheduled") + stats1 := []kolide.ScheduledQueryStats{ + { + ScheduledQueryName: squery1.Name, + ScheduledQueryID: squery1.ID, + QueryName: query1.Name, + PackName: pack1.Name, + PackID: pack1.ID, + AverageMemory: 8000, + Denylisted: false, + Executions: 164, + Interval: 30, + LastExecuted: time.Unix(1620325191, 0).UTC(), + OutputSize: 1337, + SystemTime: 150, + UserTime: 180, + WallTime: 0, + }, + } + + pack2 := test.NewPack(t, ds, "test2") + squery2 := test.NewScheduledQuery(t, ds, pack2.ID, query1.ID, 30, true, true, "time-scheduled") + query2 := test.NewQuery(t, ds, "processes", "select * from processes", 0, true) + squery3 := test.NewScheduledQuery(t, ds, pack2.ID, query2.ID, 30, true, true, "processes") + stats2 := []kolide.ScheduledQueryStats{ + { + ScheduledQueryName: squery2.Name, + ScheduledQueryID: squery2.ID, + QueryName: query1.Name, + PackName: pack2.Name, + PackID: pack2.ID, + AverageMemory: 431, + Denylisted: true, + Executions: 1, + Interval: 30, + LastExecuted: time.Unix(980943843, 0).UTC(), + OutputSize: 134, + SystemTime: 1656, + UserTime: 18453, + WallTime: 10, + }, + { + ScheduledQueryName: squery3.Name, + ScheduledQueryID: squery3.ID, + QueryName: query2.Name, + PackName: pack2.Name, + PackID: pack2.ID, + AverageMemory: 8000, + Denylisted: false, + Executions: 164, + Interval: 30, + LastExecuted: time.Unix(1620325191, 0).UTC(), + OutputSize: 1337, + SystemTime: 150, + UserTime: 180, + WallTime: 0, + }, + } + + host.PackStats = []kolide.PackStats{ + { + PackName: "test1", + // Append an additional entry to be sure that receiving stats for a + // now-deleted query doesn't break saving. This extra entry should + // not be returned on loading the host. + QueryStats: append(stats1, kolide.ScheduledQueryStats{PackName: "foo", ScheduledQueryName: "bar"}), + }, + { + PackName: "test2", + QueryStats: stats2, + }, + } + + require.NoError(t, ds.SaveHost(host)) + + host, err = ds.Host(host.ID) + require.NoError(t, err) + + require.Len(t, host.PackStats, 2) + sort.Slice(host.PackStats, func(i, j int) bool { + return host.PackStats[i].PackName < host.PackStats[j].PackName + }) + assert.Equal(t, host.PackStats[0].PackName, "test1") + assert.ElementsMatch(t, host.PackStats[0].QueryStats, stats1) + assert.Equal(t, host.PackStats[1].PackName, "test2") + assert.ElementsMatch(t, host.PackStats[1].QueryStats, stats2) + + // Set to nil should not overwrite + host.PackStats = nil + require.NoError(t, ds.SaveHost(host)) + host, err = ds.Host(host.ID) + require.NoError(t, err) + require.Len(t, host.PackStats, 2) + + // Set to empty should make it empty + host.PackStats = []kolide.PackStats{} + require.NoError(t, ds.SaveHost(host)) + host, err = ds.Host(host.ID) + require.NoError(t, err) + require.Len(t, host.PackStats, 0) +} + func testDeleteHost(t *testing.T, ds kolide.Datastore) { host, err := ds.NewHost(&kolide.Host{ DetailUpdateTime: time.Now(), diff --git a/server/datastore/datastore_scheduled_queries.go b/server/datastore/datastore_scheduled_queries.go index caf586c2e0..3525930f4c 100644 --- a/server/datastore/datastore_scheduled_queries.go +++ b/server/datastore/datastore_scheduled_queries.go @@ -84,9 +84,11 @@ func testNewScheduledQuery(t *testing.T, ds kolide.Datastore) { query, err := ds.NewScheduledQuery(&kolide.ScheduledQuery{ PackID: p1.ID, QueryID: q1.ID, + Name: "foo-scheduled", }) require.Nil(t, err) - assert.Equal(t, "foo", query.Name) + assert.Equal(t, "foo", query.QueryName) + assert.Equal(t, "foo-scheduled", query.Name) assert.Equal(t, "select * from time;", query.Query) } @@ -94,7 +96,7 @@ func testScheduledQuery(t *testing.T, ds kolide.Datastore) { u1 := test.NewUser(t, ds, "Admin", "admin", "admin@kolide.co", true) q1 := test.NewQuery(t, ds, "foo", "select * from time;", u1.ID, true) p1 := test.NewPack(t, ds, "baz") - sq1 := test.NewScheduledQuery(t, ds, p1.ID, q1.ID, 60, false, false) + sq1 := test.NewScheduledQuery(t, ds, p1.ID, q1.ID, 60, false, false, "") query, err := ds.ScheduledQuery(sq1.ID) require.Nil(t, err) @@ -118,7 +120,7 @@ func testDeleteScheduledQuery(t *testing.T, ds kolide.Datastore) { u1 := test.NewUser(t, ds, "Admin", "admin", "admin@kolide.co", true) q1 := test.NewQuery(t, ds, "foo", "select * from time;", u1.ID, true) p1 := test.NewPack(t, ds, "baz") - sq1 := test.NewScheduledQuery(t, ds, p1.ID, q1.ID, 60, false, false) + sq1 := test.NewScheduledQuery(t, ds, p1.ID, q1.ID, 60, false, false, "") query, err := ds.ScheduledQuery(sq1.ID) require.Nil(t, err) diff --git a/server/datastore/inmem/hosts.go b/server/datastore/inmem/hosts.go index 5f23cbdb47..10a24e3e15 100644 --- a/server/datastore/inmem/hosts.go +++ b/server/datastore/inmem/hosts.go @@ -43,9 +43,7 @@ func (d *Datastore) DeleteHost(hid uint) error { d.mtx.Lock() defer d.mtx.Unlock() - if _, ok := d.hosts[hid]; ok { - delete(d.hosts, hid) - } + delete(d.hosts, hid) return nil } @@ -68,7 +66,7 @@ func (d *Datastore) ListHosts(opt kolide.HostListOptions) ([]*kolide.Host, error // We need to sort by keys to provide reliable ordering keys := []int{} - for k, _ := range d.hosts { + for k := range d.hosts { keys = append(keys, int(k)) } sort.Ints(keys) diff --git a/server/datastore/inmem/invites.go b/server/datastore/inmem/invites.go index 358e949b24..f6d5d804e4 100644 --- a/server/datastore/inmem/invites.go +++ b/server/datastore/inmem/invites.go @@ -36,7 +36,7 @@ func (d *Datastore) ListInvites(opt kolide.ListOptions) ([]*kolide.Invite, error // We need to sort by keys to provide reliable ordering keys := []int{} - for k, _ := range d.invites { + for k := range d.invites { keys = append(keys, int(k)) } sort.Ints(keys) diff --git a/server/datastore/inmem/packs.go b/server/datastore/inmem/packs.go index f7b2af2ec1..c6e3e8f3fc 100644 --- a/server/datastore/inmem/packs.go +++ b/server/datastore/inmem/packs.go @@ -67,7 +67,7 @@ func (d *Datastore) ListPacks(opt kolide.ListOptions) ([]*kolide.Pack, error) { // We need to sort by keys to provide reliable ordering keys := []int{} - for k, _ := range d.packs { + for k := range d.packs { keys = append(keys, int(k)) } sort.Ints(keys) diff --git a/server/datastore/inmem/queries.go b/server/datastore/inmem/queries.go index a4ce751446..5762eed89c 100644 --- a/server/datastore/inmem/queries.go +++ b/server/datastore/inmem/queries.go @@ -86,7 +86,7 @@ func (d *Datastore) ListQueries(opt kolide.ListOptions) ([]*kolide.Query, error) // We need to sort by keys to provide reliable ordering keys := []int{} - for k, _ := range d.queries { + for k := range d.queries { keys = append(keys, int(k)) } sort.Ints(keys) diff --git a/server/datastore/inmem/users.go b/server/datastore/inmem/users.go index 7cd768d37c..9cb2b3bfe9 100644 --- a/server/datastore/inmem/users.go +++ b/server/datastore/inmem/users.go @@ -43,7 +43,7 @@ func (d *Datastore) ListUsers(opt kolide.UserListOptions) ([]*kolide.User, error // We need to sort by keys to provide reliable ordering keys := []int{} - for k, _ := range d.users { + for k := range d.users { keys = append(keys, int(k)) } sort.Ints(keys) diff --git a/server/datastore/mysql/hosts.go b/server/datastore/mysql/hosts.go index 5a48606fef..e3b89bdc37 100644 --- a/server/datastore/mysql/hosts.go +++ b/server/datastore/mysql/hosts.go @@ -3,6 +3,7 @@ package mysql import ( "database/sql" "fmt" + "strings" "time" "github.com/cenkalti/backoff/v4" @@ -88,7 +89,8 @@ func (d *Datastore) SaveHost(host *kolide.Host) error { additional = COALESCE(?, additional), enroll_secret_name = ?, primary_ip = ?, - primary_mac = ? + primary_mac = ?, + refetch_requested = ? WHERE id = ? ` _, err := d.db.Exec(sqlStatement, @@ -123,12 +125,133 @@ func (d *Datastore) SaveHost(host *kolide.Host) error { host.EnrollSecretName, host.PrimaryIP, host.PrimaryMac, + host.RefetchRequested, host.ID, ) if err != nil { return errors.Wrapf(err, "save host with id %d", host.ID) } + // Save host pack stats only if it is non-nil. Empty stats should be + // represented by an empty slice. + if host.PackStats != nil { + if err := d.saveHostPackStats(host); err != nil { + return err + } + } + + return nil +} + +func (d *Datastore) saveHostPackStats(host *kolide.Host) error { + if err := d.withRetryTxx(func(tx *sqlx.Tx) error { + sql := ` + DELETE FROM scheduled_query_stats + WHERE host_id = ? + ` + if _, err := tx.Exec(sql, host.ID); err != nil { + return errors.Wrap(err, "delete old stats") + } + + // Bulk insert software entries + var args []interface{} + queryCount := 0 + for _, pack := range host.PackStats { + for _, query := range pack.QueryStats { + queryCount++ + + args = append(args, + query.PackName, + query.ScheduledQueryName, + host.ID, + query.AverageMemory, + query.Denylisted, + query.Executions, + query.Interval, + query.LastExecuted, + query.OutputSize, + query.SystemTime, + query.UserTime, + query.WallTime, + ) + } + } + + if queryCount == 0 { + return nil + } + + values := strings.TrimSuffix(strings.Repeat("((SELECT sq.id FROM scheduled_queries sq JOIN packs p ON (sq.pack_id = p.id) WHERE p.name = ? AND sq.name = ?),?,?,?,?,?,?,?,?,?,?),", queryCount), ",") + sql = fmt.Sprintf(` + INSERT IGNORE INTO scheduled_query_stats ( + scheduled_query_id, + host_id, + average_memory, + denylisted, + executions, + schedule_interval, + last_executed, + output_size, + system_time, + user_time, + wall_time + ) + VALUES %s + `, values) + if _, err := tx.Exec(sql, args...); err != nil { + return errors.Wrap(err, "insert pack stats") + } + + return nil + }); err != nil { + return errors.Wrap(err, "save pack stats") + } + return nil +} + +func (d *Datastore) loadHostPackStats(host *kolide.Host) error { + sql := ` +SELECT + sqs.scheduled_query_id, + sqs.average_memory, + sqs.denylisted, + sqs.executions, + sqs.schedule_interval, + sqs.last_executed, + sqs.output_size, + sqs.system_time, + sqs.user_time, + sqs.wall_time, + sq.name AS scheduled_query_name, + sq.id AS scheduled_query_id, + sq.query_name AS query_name, + p.name AS pack_name, + p.id as pack_id, + q.description +FROM scheduled_query_stats sqs + JOIN scheduled_queries sq ON (sqs.scheduled_query_id = sq.id) + JOIN packs p ON (sq.pack_id = p.id) + JOIN queries q ON (sq.query_name = q.name) +WHERE host_id = ? +` + var stats []kolide.ScheduledQueryStats + if err := d.db.Select(&stats, sql, host.ID); err != nil { + return errors.Wrap(err, "load pack stats") + } + + packs := map[uint]kolide.PackStats{} + for _, query := range stats { + pack := packs[query.PackID] + pack.PackName = query.PackName + pack.PackID = query.PackID + pack.QueryStats = append(pack.QueryStats, query) + packs[pack.PackID] = pack + } + + for _, pack := range packs { + host.PackStats = append(host.PackStats, pack) + } + return nil } @@ -149,7 +272,10 @@ func (d *Datastore) Host(id uint) (*kolide.Host, error) { host := &kolide.Host{} err := d.db.Get(host, sqlStatement, id) if err != nil { - return nil, errors.Wrap(err, "getting host by id") + return nil, errors.Wrap(err, "get host by id") + } + if err := d.loadHostPackStats(host); err != nil { + return nil, err } return host, nil @@ -193,6 +319,7 @@ func (d *Datastore) ListHosts(opt kolide.HostListOptions) ([]*kolide.Host, error h.label_update_time, h.enroll_secret_name, h.team_id, + h.refetch_requested, t.name AS team_name, ` @@ -203,7 +330,7 @@ func (d *Datastore) ListHosts(opt kolide.HostListOptions) ([]*kolide.Host, error sql += `JSON_OBJECT( ` for _, field := range opt.AdditionalFilters { - sql += fmt.Sprintf(`?, JSON_EXTRACT(additional, ?), `) + sql += `?, JSON_EXTRACT(additional, ?), ` params = append(params, field, fmt.Sprintf(`$."%s"`, field)) } sql = sql[:len(sql)-2] @@ -411,7 +538,8 @@ func (d *Datastore) AuthenticateHost(nodeKey string) (*kolide.Host, error) { config_tls_refresh, primary_ip, primary_mac, - enroll_secret_name + enroll_secret_name, + refetch_requested FROM hosts WHERE node_key = ? LIMIT 1 @@ -609,6 +737,10 @@ func (d *Datastore) HostByIdentifier(identifier string) (*kolide.Host, error) { return nil, errors.Wrap(err, "get host by identifier") } + if err := d.loadHostPackStats(host); err != nil { + return nil, err + } + return host, nil } diff --git a/server/datastore/mysql/migrations/tables/20201102112520_ModifyEnrollSecretNodeKeyCollation.go b/server/datastore/mysql/migrations/tables/20201102112520_ModifyEnrollSecretNodeKeyCollation.go index b9e6b3285a..59269fe629 100644 --- a/server/datastore/mysql/migrations/tables/20201102112520_ModifyEnrollSecretNodeKeyCollation.go +++ b/server/datastore/mysql/migrations/tables/20201102112520_ModifyEnrollSecretNodeKeyCollation.go @@ -1,35 +1,35 @@ package tables import ( - "database/sql" + "database/sql" - "github.com/pkg/errors" + "github.com/pkg/errors" ) func init() { - MigrationClient.AddMigration(Up_20201102112520, Down_20201102112520) + MigrationClient.AddMigration(Up_20201102112520, Down_20201102112520) } func Up_20201102112520(tx *sql.Tx) error { - query := ` + query := ` ALTER TABLE enroll_secrets MODIFY secret VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin ` - if _, err := tx.Exec(query); err != nil { - return errors.Wrap(err, "alter enroll secret collation") - } + if _, err := tx.Exec(query); err != nil { + return errors.Wrap(err, "alter enroll secret collation") + } - query = ` + query = ` ALTER TABLE hosts MODIFY node_key VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin ` - if _, err := tx.Exec(query); err != nil { - return errors.Wrap(err, "alter node key collation") - } + if _, err := tx.Exec(query); err != nil { + return errors.Wrap(err, "alter node key collation") + } - return nil + return nil } func Down_20201102112520(tx *sql.Tx) error { - return nil + return nil } diff --git a/server/datastore/mysql/migrations/tables/20210506095025_AddScheduledQueryStats.go b/server/datastore/mysql/migrations/tables/20210506095025_AddScheduledQueryStats.go new file mode 100644 index 0000000000..d6be87535e --- /dev/null +++ b/server/datastore/mysql/migrations/tables/20210506095025_AddScheduledQueryStats.go @@ -0,0 +1,40 @@ +package tables + +import ( + "database/sql" + + "github.com/pkg/errors" +) + +func init() { + MigrationClient.AddMigration(Up_20210506095025, Down_20210506095025) +} + +func Up_20210506095025(tx *sql.Tx) error { + sql := ` + CREATE TABLE scheduled_query_stats ( + host_id int unsigned NOT NULL, + scheduled_query_id int unsigned NOT NULL, + average_memory int, + denylisted tinyint(1), + executions int, + schedule_interval int, + last_executed timestamp, + output_size int, + system_time int, + user_time int, + wall_time int, + PRIMARY KEY (host_id, scheduled_query_id), + FOREIGN KEY (host_id) REFERENCES hosts (id) ON DELETE CASCADE ON UPDATE CASCADE, + FOREIGN KEY (scheduled_query_id) REFERENCES scheduled_queries (id) ON DELETE CASCADE ON UPDATE CASCADE + ) + ` + if _, err := tx.Exec(sql); err != nil { + return errors.Wrap(err, "create scheduled_query_stats") + } + return nil +} + +func Down_20210506095025(tx *sql.Tx) error { + return nil +} diff --git a/server/datastore/mysql/migrations/tables/20210513115729_RefetchHostDetail.go b/server/datastore/mysql/migrations/tables/20210513115729_RefetchHostDetail.go new file mode 100644 index 0000000000..9f3ee04fb3 --- /dev/null +++ b/server/datastore/mysql/migrations/tables/20210513115729_RefetchHostDetail.go @@ -0,0 +1,26 @@ +package tables + +import ( + "database/sql" + + "github.com/pkg/errors" +) + +func init() { + MigrationClient.AddMigration(Up_20210513115729, Down_20210513115729) +} + +func Up_20210513115729(tx *sql.Tx) error { + sql := ` + ALTER TABLE hosts + ADD COLUMN refetch_requested TINYINT(1) NOT NULL DEFAULT 0 + ` + if _, err := tx.Exec(sql); err != nil { + return errors.Wrap(err, "add column refetch_requested") + } + return nil +} + +func Down_20210513115729(tx *sql.Tx) error { + return nil +} diff --git a/server/datastore/mysql/scheduled_queries.go b/server/datastore/mysql/scheduled_queries.go index 151c862ecd..0bdbaee3fb 100644 --- a/server/datastore/mysql/scheduled_queries.go +++ b/server/datastore/mysql/scheduled_queries.go @@ -87,7 +87,7 @@ func (d *Datastore) NewScheduledQuery(sq *kolide.ScheduledQuery, opts ...kolide. } sq.Query = metadata[0].Query - sq.Name = metadata[0].Name + sq.QueryName = metadata[0].Name return sq, nil } diff --git a/server/datastore/mysql/targets.go b/server/datastore/mysql/targets.go index c2397f3ae4..5e5a4dda57 100644 --- a/server/datastore/mysql/targets.go +++ b/server/datastore/mysql/targets.go @@ -4,8 +4,8 @@ import ( "fmt" "time" - "github.com/jmoiron/sqlx" "github.com/fleetdm/fleet/server/kolide" + "github.com/jmoiron/sqlx" "github.com/pkg/errors" ) diff --git a/server/kolide/hosts.go b/server/kolide/hosts.go index 8e5afdb636..3b78f5ebc7 100644 --- a/server/kolide/hosts.go +++ b/server/kolide/hosts.go @@ -89,6 +89,9 @@ type HostService interface { // Possible matches can be on osquery_host_identifier, node_key, UUID, or // hostname. HostByIdentifier(ctx context.Context, identifier string) (*HostDetail, error) + // RefetchHost requests a refetch of host details for the provided host. + RefetchHost(ctx context.Context, id uint) (err error) + FlushSeenHosts(ctx context.Context) error // AddHostsToTeam adds hosts to an existing team, clearing their team // settings if teamID is nil. @@ -117,6 +120,7 @@ type Host struct { LabelUpdateTime time.Time `json:"label_updated_at" db:"label_update_time"` // Time that the host details were last updated LastEnrollTime time.Time `json:"last_enrolled_at" db:"last_enroll_time"` // Time that the host last enrolled SeenTime time.Time `json:"seen_time" db:"seen_time"` // Time that the host was last "seen" + RefetchRequested bool `json:"refetch_requested" db:"refetch_requested"` NodeKey string `json:"-" db:"node_key"` HostName string `json:"hostname" db:"host_name"` // there is a fulltext index on this field UUID string `json:"uuid" db:"uuid"` // there is a fulltext index on this field @@ -154,6 +158,8 @@ type Host struct { TeamID null.Int `json:"team_id" db:"team_id"` // TeamName is the name of the team, loaded by JOIN to the teams table. TeamName *string `json:"team_name,omitempty" db:"team_name"` + // Loaded via JOIN in DB + PackStats []PackStats `json:"pack_stats"` } // HostDetail provides the full host metadata along with associated labels and diff --git a/server/kolide/packs.go b/server/kolide/packs.go index 88475eee7d..c614fb4928 100644 --- a/server/kolide/packs.go +++ b/server/kolide/packs.go @@ -176,3 +176,9 @@ type PackTarget struct { PackID uint Target } + +type PackStats struct { + PackID uint `json:"pack_id,omitempty"` + PackName string `json:"pack_name,omitempty"` + QueryStats []ScheduledQueryStats `json:"query_stats"` +} diff --git a/server/kolide/scheduled_queries.go b/server/kolide/scheduled_queries.go index e20dd9af0b..15829095e6 100644 --- a/server/kolide/scheduled_queries.go +++ b/server/kolide/scheduled_queries.go @@ -2,6 +2,7 @@ package kolide import ( "context" + "time" "gopkg.in/guregu/null.v3" ) @@ -51,3 +52,26 @@ type ScheduledQueryPayload struct { Shard *null.Int `json:"shard"` Denylist *bool `json:"denylist"` } + +type ScheduledQueryStats struct { + ScheduledQueryName string `json:"scheduled_query_name,omitempty" db:"scheduled_query_name"` + ScheduledQueryID uint `json:"scheduled_query_id,omitempty" db:"scheduled_query_id"` + + QueryName string `json:"query_name,omitempty" db:"query_name"` + Description string `json:"description,omitempty" db:"description"` + + PackName string `json:"pack_name,omitempty" db:"pack_name"` + PackID uint `json:"pack_id,omitempty" db:"pack_id"` + + // From osquery directly + AverageMemory int `json:"average_memory" db:"average_memory"` + Denylisted bool `json:"denylisted" db:"denylisted"` + Executions int `json:"executions" db:"executions"` + // Note schedule_interval is used for DB since "interval" is a reserved word in MySQL + Interval int `json:"interval" db:"schedule_interval"` + LastExecuted time.Time `json:"last_executed" db:"last_executed"` + OutputSize int `json:"output_size" db:"output_size"` + SystemTime int `json:"system_time" db:"system_time"` + UserTime int `json:"user_time" db:"user_time"` + WallTime int `json:"wall_time" db:"wall_time"` +} diff --git a/server/logging/firehose_test.go b/server/logging/firehose_test.go index 1f5c393c93..97a47d23f0 100644 --- a/server/logging/firehose_test.go +++ b/server/logging/firehose_test.go @@ -10,8 +10,8 @@ import ( "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/service/firehose" "github.com/aws/aws-sdk-go/service/firehose/firehoseiface" - "github.com/go-kit/kit/log" "github.com/fleetdm/fleet/server/logging/mock" + "github.com/go-kit/kit/log" "github.com/stretchr/testify/assert" ) diff --git a/server/logging/kinesis_test.go b/server/logging/kinesis_test.go index 87ba95c698..e087825d39 100644 --- a/server/logging/kinesis_test.go +++ b/server/logging/kinesis_test.go @@ -12,8 +12,8 @@ import ( "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/service/kinesis" "github.com/aws/aws-sdk-go/service/kinesis/kinesisiface" - "github.com/go-kit/kit/log" "github.com/fleetdm/fleet/server/logging/mock" + "github.com/go-kit/kit/log" "github.com/stretchr/testify/assert" ) diff --git a/server/logging/logging.go b/server/logging/logging.go index 33755208da..b870698bd3 100644 --- a/server/logging/logging.go +++ b/server/logging/logging.go @@ -74,6 +74,7 @@ func New(config config.KolideConfig, logger log.Logger) (*OsqueryLogger, error) status, err = NewPubSubLogWriter( config.PubSub.Project, config.PubSub.StatusTopic, + false, logger, ) if err != nil { @@ -145,6 +146,7 @@ func New(config config.KolideConfig, logger log.Logger) (*OsqueryLogger, error) result, err = NewPubSubLogWriter( config.PubSub.Project, config.PubSub.ResultTopic, + config.PubSub.AddAttributes, logger, ) if err != nil { diff --git a/server/logging/pubsub.go b/server/logging/pubsub.go index 9ea8d0e431..981c236d8a 100644 --- a/server/logging/pubsub.go +++ b/server/logging/pubsub.go @@ -3,6 +3,7 @@ package logging import ( "context" "encoding/json" + "time" "cloud.google.com/go/pubsub" "github.com/go-kit/kit/log" @@ -11,11 +12,18 @@ import ( ) type pubSubLogWriter struct { - topic *pubsub.Topic - logger log.Logger + topic *pubsub.Topic + logger log.Logger + addAttributes bool } -func NewPubSubLogWriter(projectId string, topicName string, logger log.Logger) (*pubSubLogWriter, error) { +type PubSubAttributes struct { + Name string `json:"name"` + UnixTime int64 `json:"unixTime"` + Decorations map[string]string `json:"decorations"` +} + +func NewPubSubLogWriter(projectId string, topicName string, addAttributes bool, logger log.Logger) (*pubSubLogWriter, error) { ctx := context.Background() client, err := pubsub.NewClient(ctx, projectId) @@ -29,14 +37,24 @@ func NewPubSubLogWriter(projectId string, topicName string, logger log.Logger) ( "msg", "GCP PubSub writer configured", "project", projectId, "topic", topicName, + "add_attributes", addAttributes, ) return &pubSubLogWriter{ - topic: topic, - logger: logger, + topic: topic, + logger: logger, + addAttributes: addAttributes, }, nil } +func estimateAttributeSize(attributes map[string]string) int { + var sz int + for k, v := range attributes { + sz += len(k) + len(v) + 2 + } + return sz +} + func (w *pubSubLogWriter) Write(ctx context.Context, logs []json.RawMessage) error { results := make([]*pubsub.PublishResult, len(logs)) @@ -47,7 +65,22 @@ func (w *pubSubLogWriter) Write(ctx context.Context, logs []json.RawMessage) err return errors.Wrap(err, "marshal message into JSON") } - if len(data) > pubsub.MaxPublishRequestBytes { + attributes := make(map[string]string) + + if w.addAttributes { + var unmarshaled PubSubAttributes + + if err := json.Unmarshal(log, &unmarshaled); err != nil { + return errors.Wrap(err, "unmarshalling log message JSON") + } + attributes["name"] = unmarshaled.Name + attributes["timestamp"] = time.Unix(unmarshaled.UnixTime, 0).Format(time.RFC3339) + for k, v := range unmarshaled.Decorations { + attributes[k] = v + } + } + + if len(data)+estimateAttributeSize(attributes) > pubsub.MaxPublishRequestBytes { level.Info(w.logger).Log( "msg", "dropping log over 10MB PubSub limit", "size", len(data), @@ -57,7 +90,8 @@ func (w *pubSubLogWriter) Write(ctx context.Context, logs []json.RawMessage) err } message := &pubsub.Message{ - Data: data, + Data: data, + Attributes: attributes, } results[i] = w.topic.Publish(ctx, message) diff --git a/server/pubsub/query_results_test.go b/server/pubsub/query_results_test.go index 5f5b8315de..86ec22fc95 100644 --- a/server/pubsub/query_results_test.go +++ b/server/pubsub/query_results_test.go @@ -61,13 +61,14 @@ func TestInmem(t *testing.T) { func setupRedis(t *testing.T) (store *redisQueryResults, teardown func()) { var ( - addr = "127.0.0.1:6379" - password = "" - database = 0 - useTLS = false + addr = "127.0.0.1:6379" + password = "" + database = 0 + useTLS = false + dupResults = false ) - store = NewRedisQueryResults(NewRedisPool(addr, password, database, useTLS)) + store = NewRedisQueryResults(NewRedisPool(addr, password, database, useTLS), dupResults) _, err := store.pool.Get().Do("PING") require.Nil(t, err) diff --git a/server/pubsub/redis_query_results.go b/server/pubsub/redis_query_results.go index 647be5766e..672eed4786 100644 --- a/server/pubsub/redis_query_results.go +++ b/server/pubsub/redis_query_results.go @@ -6,14 +6,15 @@ import ( "fmt" "time" - "github.com/gomodule/redigo/redis" "github.com/fleetdm/fleet/server/kolide" + "github.com/gomodule/redigo/redis" "github.com/pkg/errors" ) type redisQueryResults struct { // connection pool - pool *redis.Pool + pool *redis.Pool + duplicateResults bool } var _ kolide.QueryResultStore = &redisQueryResults{} @@ -25,7 +26,17 @@ func NewRedisPool(server, password string, database int, useTLS bool) *redis.Poo MaxIdle: 3, IdleTimeout: 240 * time.Second, Dial: func() (redis.Conn, error) { - c, err := redis.Dial("tcp", server, redis.DialDatabase(database), redis.DialUseTLS(useTLS)) + c, err := redis.Dial( + "tcp", + server, + redis.DialDatabase(database), + redis.DialUseTLS(useTLS), + redis.DialConnectTimeout(5*time.Second), + redis.DialReadTimeout(5*time.Second), + redis.DialWriteTimeout(5*time.Second), + redis.DialKeepAlive(5*time.Second), + ) + if err != nil { return nil, err } @@ -49,8 +60,8 @@ func NewRedisPool(server, password string, database int, useTLS bool) *redis.Poo // NewRedisQueryResults creats a new Redis implementation of the // QueryResultStore interface using the provided Redis connection pool. -func NewRedisQueryResults(pool *redis.Pool) *redisQueryResults { - return &redisQueryResults{pool: pool} +func NewRedisQueryResults(pool *redis.Pool, duplicateResults bool) *redisQueryResults { + return &redisQueryResults{pool: pool, duplicateResults: duplicateResults} } func pubSubForID(id uint) string { @@ -69,6 +80,11 @@ func (r *redisQueryResults) WriteResult(result kolide.DistributedQueryResult) er } n, err := redis.Int(conn.Do("PUBLISH", channelName, string(jsonVal))) + + if n != 0 && r.duplicateResults { + redis.Int(conn.Do("PUBLISH", "LQDuplicate", string(jsonVal))) + } + if err != nil { return errors.Wrap(err, "PUBLISH failed to channel "+channelName) } diff --git a/server/service/client.go b/server/service/client.go index 587d146374..42332f7616 100644 --- a/server/service/client.go +++ b/server/service/client.go @@ -196,7 +196,7 @@ func (l *logRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { } // Log response - took := time.Now().Sub(start).Truncate(time.Millisecond) + took := time.Since(start).Truncate(time.Millisecond) fmt.Fprintf(os.Stderr, "%s %s %s (%s)\n", res.Request.Method, res.Request.URL, res.Status, took) resBody := &bytes.Buffer{} diff --git a/server/service/endpoint_carves.go b/server/service/endpoint_carves.go index 5825f27008..db3c04e3d4 100644 --- a/server/service/endpoint_carves.go +++ b/server/service/endpoint_carves.go @@ -3,8 +3,8 @@ package service import ( "context" - "github.com/go-kit/kit/endpoint" "github.com/fleetdm/fleet/server/kolide" + "github.com/go-kit/kit/endpoint" ) //////////////////////////////////////////////////////////////////////////////// @@ -84,7 +84,6 @@ func makeCarveBlockEndpoint(svc kolide.Service) endpoint.Endpoint { return carveBlockResponse{Err: err}, nil } - return carveBlockResponse{Success: true}, nil } } @@ -99,7 +98,7 @@ type getCarveRequest struct { type getCarveResponse struct { Carve kolide.CarveMetadata `json:"carve"` - Err error `json:"error,omitempty"` + Err error `json:"error,omitempty"` } func (r getCarveResponse) error() error { return r.Err } @@ -112,7 +111,7 @@ func makeGetCarveEndpoint(svc kolide.Service) endpoint.Endpoint { return getCarveResponse{Err: err}, nil } - return getCarveResponse{Carve:*carve}, nil + return getCarveResponse{Carve: *carve}, nil } } @@ -127,7 +126,7 @@ type listCarvesRequest struct { type listCarvesResponse struct { Carves []kolide.CarveMetadata `json:"carves"` - Err error `json:"error,omitempty"` + Err error `json:"error,omitempty"` } func (r listCarvesResponse) error() error { return r.Err } @@ -153,13 +152,13 @@ func makeListCarvesEndpoint(svc kolide.Service) endpoint.Endpoint { //////////////////////////////////////////////////////////////////////////////// type getCarveBlockRequest struct { - ID int64 + ID int64 BlockId int64 } type getCarveBlockResponse struct { Data []byte `json:"data"` - Err error `json:"error,omitempty"` + Err error `json:"error,omitempty"` } func (r getCarveBlockResponse) error() error { return r.Err } diff --git a/server/service/endpoint_certificate.go b/server/service/endpoint_certificate.go index 6814e988cc..46f1870882 100644 --- a/server/service/endpoint_certificate.go +++ b/server/service/endpoint_certificate.go @@ -3,8 +3,8 @@ package service import ( "context" - "github.com/go-kit/kit/endpoint" "github.com/fleetdm/fleet/server/kolide" + "github.com/go-kit/kit/endpoint" ) type certificateResponse struct { diff --git a/server/service/endpoint_change_email.go b/server/service/endpoint_change_email.go index a707b82be6..128fc43b90 100644 --- a/server/service/endpoint_change_email.go +++ b/server/service/endpoint_change_email.go @@ -3,8 +3,8 @@ package service import ( "context" - "github.com/go-kit/kit/endpoint" "github.com/fleetdm/fleet/server/kolide" + "github.com/go-kit/kit/endpoint" ) type changeEmailRequest struct { diff --git a/server/service/endpoint_hosts.go b/server/service/endpoint_hosts.go index e102561944..391cf93afb 100644 --- a/server/service/endpoint_hosts.go +++ b/server/service/endpoint_hosts.go @@ -213,3 +213,28 @@ func makeAddHostsToTeamEndpoint(svc kolide.Service) endpoint.Endpoint { return addHostsToTeamResponse{}, err } } + +//////////////////////////////////////////////////////////////////////////////// +// Refetch Host +//////////////////////////////////////////////////////////////////////////////// + +type refetchHostRequest struct { + ID uint `json:"id"` +} + +type refetchHostResponse struct { + Err error `json:"error,omitempty"` +} + +func (r refetchHostResponse) error() error { return r.Err } + +func makeRefetchHostEndpoint(svc kolide.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(refetchHostRequest) + err := svc.RefetchHost(ctx, req.ID) + if err != nil { + return refetchHostResponse{Err: err}, nil + } + return refetchHostResponse{}, nil + } +} diff --git a/server/service/endpoint_invites.go b/server/service/endpoint_invites.go index abaf3ddfdc..88bc0f3d8e 100644 --- a/server/service/endpoint_invites.go +++ b/server/service/endpoint_invites.go @@ -3,8 +3,8 @@ package service import ( "context" - "github.com/go-kit/kit/endpoint" "github.com/fleetdm/fleet/server/kolide" + "github.com/go-kit/kit/endpoint" ) type createInviteRequest struct { diff --git a/server/service/endpoint_queries.go b/server/service/endpoint_queries.go index ac0d095f85..5e95cb70d8 100644 --- a/server/service/endpoint_queries.go +++ b/server/service/endpoint_queries.go @@ -3,8 +3,8 @@ package service import ( "context" - "github.com/go-kit/kit/endpoint" "github.com/fleetdm/fleet/server/kolide" + "github.com/go-kit/kit/endpoint" ) //////////////////////////////////////////////////////////////////////////////// diff --git a/server/service/endpoint_scheduled_queries.go b/server/service/endpoint_scheduled_queries.go index 8d3165c265..b95f191529 100644 --- a/server/service/endpoint_scheduled_queries.go +++ b/server/service/endpoint_scheduled_queries.go @@ -3,8 +3,8 @@ package service import ( "context" - "github.com/go-kit/kit/endpoint" "github.com/fleetdm/fleet/server/kolide" + "github.com/go-kit/kit/endpoint" ) //////////////////////////////////////////////////////////////////////////////// diff --git a/server/service/endpoint_status.go b/server/service/endpoint_status.go index 3ffaec315e..4f837b3fec 100644 --- a/server/service/endpoint_status.go +++ b/server/service/endpoint_status.go @@ -3,8 +3,8 @@ package service import ( "context" - "github.com/go-kit/kit/endpoint" "github.com/fleetdm/fleet/server/kolide" + "github.com/go-kit/kit/endpoint" ) type statusResponse struct { diff --git a/server/service/endpoint_targets.go b/server/service/endpoint_targets.go index 44e3792f6f..f7399c9813 100644 --- a/server/service/endpoint_targets.go +++ b/server/service/endpoint_targets.go @@ -4,8 +4,8 @@ import ( "context" "time" - "github.com/go-kit/kit/endpoint" "github.com/fleetdm/fleet/server/kolide" + "github.com/go-kit/kit/endpoint" ) //////////////////////////////////////////////////////////////////////////////// diff --git a/server/service/frontend.go b/server/service/frontend.go index 5b02b0182d..c4544e4f4f 100644 --- a/server/service/frontend.go +++ b/server/service/frontend.go @@ -6,8 +6,8 @@ import ( "net/http" assetfs "github.com/elazarl/go-bindata-assetfs" - "github.com/go-kit/kit/log" "github.com/fleetdm/fleet/server/bindata" + "github.com/go-kit/kit/log" ) func newBinaryFileSystem(root string) *assetfs.AssetFS { diff --git a/server/service/handler.go b/server/service/handler.go index f316f5946c..ba0a498b1f 100644 --- a/server/service/handler.go +++ b/server/service/handler.go @@ -94,6 +94,7 @@ type KolideEndpoints struct { GetHost endpoint.Endpoint HostByIdentifier endpoint.Endpoint DeleteHost endpoint.Endpoint + RefetchHost endpoint.Endpoint ListHosts endpoint.Endpoint GetHostSummary endpoint.Endpoint AddHostsToTeam endpoint.Endpoint @@ -201,6 +202,7 @@ func MakeKolideServerEndpoints(svc kolide.Service, jwtKey, urlPrefix string, lim GetHostSummary: authenticatedUser(jwtKey, svc, makeGetHostSummaryEndpoint(svc)), DeleteHost: authenticatedUser(jwtKey, svc, makeDeleteHostEndpoint(svc)), AddHostsToTeam: authenticatedUser(jwtKey, svc, makeAddHostsToTeamEndpoint(svc)), + RefetchHost: authenticatedUser(jwtKey, svc, makeRefetchHostEndpoint(svc)), CreateLabel: authenticatedUser(jwtKey, svc, makeCreateLabelEndpoint(svc)), ModifyLabel: authenticatedUser(jwtKey, svc, makeModifyLabelEndpoint(svc)), GetLabel: authenticatedUser(jwtKey, svc, makeGetLabelEndpoint(svc)), @@ -320,6 +322,7 @@ type kolideHandlers struct { GetHost http.Handler HostByIdentifier http.Handler DeleteHost http.Handler + RefetchHost http.Handler ListHosts http.Handler GetHostSummary http.Handler AddHostsToTeam http.Handler @@ -423,6 +426,7 @@ func makeKolideKitHandlers(e KolideEndpoints, opts []kithttp.ServerOption) *koli GetHost: newServer(e.GetHost, decodeGetHostRequest), HostByIdentifier: newServer(e.HostByIdentifier, decodeHostByIdentifierRequest), DeleteHost: newServer(e.DeleteHost, decodeDeleteHostRequest), + RefetchHost: newServer(e.RefetchHost, decodeRefetchHostRequest), ListHosts: newServer(e.ListHosts, decodeListHostsRequest), GetHostSummary: newServer(e.GetHostSummary, decodeNoParamsRequest), AddHostsToTeam: newServer(e.AddHostsToTeam, decodeAddHostsToTeamRequest), @@ -644,6 +648,7 @@ func attachKolideAPIRoutes(r *mux.Router, h *kolideHandlers) { r.Handle("/api/v1/fleet/hosts/identifier/{identifier}", h.HostByIdentifier).Methods("GET").Name("host_by_identifier") r.Handle("/api/v1/fleet/hosts/{id}", h.DeleteHost).Methods("DELETE").Name("delete_host") r.Handle("/api/v1/fleet/hosts/transfer", h.AddHostsToTeam).Methods("POST").Name("add_hosts_to_team") + r.Handle("/api/v1/fleet/hosts/{id}/refetch", h.RefetchHost).Methods("POST").Name("refetch_host") r.Handle("/api/v1/fleet/targets", h.SearchTargets).Methods("POST").Name("search_targets") diff --git a/server/service/http_auth.go b/server/service/http_auth.go index 6897f687ca..d4ee49e447 100644 --- a/server/service/http_auth.go +++ b/server/service/http_auth.go @@ -5,10 +5,10 @@ import ( "net/http" "strings" - kithttp "github.com/go-kit/kit/transport/http" "github.com/fleetdm/fleet/server/contexts/token" "github.com/fleetdm/fleet/server/contexts/viewer" "github.com/fleetdm/fleet/server/kolide" + kithttp "github.com/go-kit/kit/transport/http" ) // setRequestsContexts updates the request with necessary context values for a request diff --git a/server/service/metrics.go b/server/service/metrics.go index 1211302393..97937432e8 100644 --- a/server/service/metrics.go +++ b/server/service/metrics.go @@ -1,8 +1,8 @@ package service import ( - "github.com/go-kit/kit/metrics" "github.com/fleetdm/fleet/server/kolide" + "github.com/go-kit/kit/metrics" ) type metricsMiddleware struct { diff --git a/server/service/service.go b/server/service/service.go index c6fcf01fea..e3a0cfd6f2 100644 --- a/server/service/service.go +++ b/server/service/service.go @@ -121,7 +121,7 @@ func (m *seenHostSet) getAndClearHostIDs() []uint { m.mutex.Lock() defer m.mutex.Unlock() var ids []uint - for id, _ := range m.hostIDs { + for id := range m.hostIDs { ids = append(ids, id) } m.hostIDs = make(map[uint]bool) diff --git a/server/service/service_hosts.go b/server/service/service_hosts.go index c53da7311e..e235f7ff9f 100644 --- a/server/service/service_hosts.go +++ b/server/service/service_hosts.go @@ -79,3 +79,17 @@ func (svc *service) FlushSeenHosts(ctx context.Context) error { func (svc service) AddHostsToTeam(ctx context.Context, teamID *uint, hostIDs []uint) error { return svc.ds.AddHostsToTeam(teamID, hostIDs) } + +func (svc *service) RefetchHost(ctx context.Context, id uint) error { + host, err := svc.ds.Host(id) + if err != nil { + return errors.Wrap(err, "find host for refetch") + } + + host.RefetchRequested = true + if err := svc.ds.SaveHost(host); err != nil { + return errors.Wrap(err, "save host") + } + + return nil +} diff --git a/server/service/service_hosts_test.go b/server/service/service_hosts_test.go index 14b141dc55..bdb801ab40 100644 --- a/server/service/service_hosts_test.go +++ b/server/service/service_hosts_test.go @@ -100,3 +100,21 @@ func TestHostDetails(t *testing.T) { assert.Equal(t, expectedLabels, hostDetail.Labels) assert.Equal(t, expectedPacks, hostDetail.Packs) } + +func TestRefetchHost(t *testing.T) { + ds := new(mock.Store) + svc := service{ds: ds} + + host := &kolide.Host{ID: 3} + ctx := context.Background() + + ds.HostFunc = func(hid uint) (*kolide.Host, error) { + return host, nil + } + ds.SaveHostFunc = func(host *kolide.Host) error { + assert.True(t, host.RefetchRequested) + return nil + } + + require.NoError(t, svc.RefetchHost(ctx, host.ID)) +} diff --git a/server/service/service_osquery.go b/server/service/service_osquery.go index 6089220836..2a677a3d37 100644 --- a/server/service/service_osquery.go +++ b/server/service/service_osquery.go @@ -747,6 +747,80 @@ FROM python_packages; Platforms: []string{"windows"}, IngestFunc: ingestSoftware, }, + "scheduled_query_stats": { + Query: ` + SELECT *, + (SELECT value from osquery_flags where name = 'pack_delimiter') AS delimiter + FROM osquery_schedule +`, + IngestFunc: func(logger log.Logger, host *kolide.Host, rows []map[string]string) error { + packs := map[string][]kolide.ScheduledQueryStats{} + + for _, row := range rows { + providedName := row["name"] + if providedName == "" { + level.Debug(logger).Log( + "msg", "host reported scheduled query with empty name", + "host", host.HostName, + ) + continue + } + delimiter := row["delimiter"] + if delimiter == "" { + level.Debug(logger).Log( + "msg", "host reported scheduled query with empty delimiter", + "host", host.HostName, + ) + continue + } + + // Split with a limit of 2 in case query name includes the + // delimiter. Not much we can do if pack name includes the + // delimiter. + trimmedName := strings.TrimPrefix(providedName, "pack"+delimiter) + parts := strings.SplitN(trimmedName, delimiter, 2) + if len(parts) != 2 { + level.Debug(logger).Log( + "msg", "could not split pack and query names", + "host", host.HostName, + "name", providedName, + "delimiter", delimiter, + ) + continue + } + packName, scheduledName := parts[0], parts[1] + + stats := kolide.ScheduledQueryStats{ + ScheduledQueryName: scheduledName, + PackName: packName, + AverageMemory: cast.ToInt(row["average_memory"]), + Denylisted: cast.ToBool(row["denylisted"]), + Executions: cast.ToInt(row["executions"]), + Interval: cast.ToInt(row["interval"]), + // Cast to int first to allow cast.ToTime to interpret the unix timestamp. + LastExecuted: time.Unix(cast.ToInt64(row["last_executed"]), 0).UTC(), + OutputSize: cast.ToInt(row["output_size"]), + SystemTime: cast.ToInt(row["system_time"]), + UserTime: cast.ToInt(row["user_time"]), + WallTime: cast.ToInt(row["wall_time"]), + } + packs[packName] = append(packs[packName], stats) + } + + host.PackStats = []kolide.PackStats{} + for packName, stats := range packs { + host.PackStats = append( + host.PackStats, + kolide.PackStats{ + PackName: packName, + QueryStats: stats, + }, + ) + } + + return nil + }, + }, } func ingestSoftware(logger log.Logger, host *kolide.Host, rows []map[string]string) error { @@ -787,7 +861,7 @@ func ingestSoftware(logger log.Logger, host *kolide.Host, rows []map[string]stri // osqueryd to fill in the host details func (svc service) hostDetailQueries(host kolide.Host) (map[string]string, error) { queries := make(map[string]string) - if host.DetailUpdateTime.After(svc.clock.Now().Add(-svc.config.Osquery.DetailUpdateInterval)) { + if host.DetailUpdateTime.After(svc.clock.Now().Add(-svc.config.Osquery.DetailUpdateInterval)) && !host.RefetchRequested { // No need to update already fresh details return queries, nil } @@ -885,6 +959,9 @@ func (svc service) ingestDetailQuery(host *kolide.Host, name string, rows []map[ } } + // Refetch is no longer needed after ingesting details. + host.RefetchRequested = false + return nil } @@ -975,7 +1052,7 @@ func (svc service) SubmitDistributedQueryResults(ctx context.Context, results ko // Check for label queries and if so, load host additional. If we don't do // this, we will end up unintentionally dropping any existing host // additional info. - for query, _ := range results { + for query := range results { if strings.HasPrefix(query, hostLabelQueryPrefix) { fullHost, err := svc.ds.Host(host.ID) if err != nil { diff --git a/server/service/service_osquery_test.go b/server/service/service_osquery_test.go index af1df9a4eb..0b5fac6ecc 100644 --- a/server/service/service_osquery_test.go +++ b/server/service/service_osquery_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "sort" "strconv" "strings" "sync" @@ -273,7 +274,14 @@ func TestHostDetailQueries(t *testing.T) { queries, err := svc.hostDetailQueries(host) assert.Nil(t, err) - assert.Empty(t, queries, 0) + assert.Empty(t, queries) + + // With refetch requested queries should be returned + host.RefetchRequested = true + queries, err = svc.hostDetailQueries(host) + assert.Nil(t, err) + assert.NotEmpty(t, queries) + host.RefetchRequested = false // Advance the time mockClock.AddTime(1*time.Hour + 1*time.Minute) @@ -281,7 +289,7 @@ func TestHostDetailQueries(t *testing.T) { queries, err = svc.hostDetailQueries(host) assert.Nil(t, err) assert.Len(t, queries, expectedDetailQueries+2) - for name, _ := range queries { + for name := range queries { assert.True(t, strings.HasPrefix(name, hostDetailQueryPrefix) || strings.HasPrefix(name, hostAdditionalQueryPrefix), ) @@ -933,6 +941,177 @@ func TestDetailQueryNetworkInterfaces(t *testing.T) { assert.Equal(t, "00:00:00:00:00:00", host.PrimaryMac) } +func TestDetailQueryScheduledQueryStats(t *testing.T) { + host := kolide.Host{} + + ingest := detailQueries["scheduled_query_stats"].IngestFunc + + assert.NoError(t, ingest(log.NewNopLogger(), &host, nil)) + assert.Len(t, host.PackStats, 0) + + resJSON := ` +[ + { + "average_memory":"33", + "delimiter":"/", + "denylisted":"0", + "executions":"1", + "interval":"33", + "last_executed":"1620325191", + "name":"pack/pack-2/time", + "output_size":"", + "query":"SELECT * FROM time", + "system_time":"100", + "user_time":"60", + "wall_time":"180" + }, + { + "average_memory":"8000", + "delimiter":"/", + "denylisted":"0", + "executions":"164", + "interval":"30", + "last_executed":"1620325191", + "name":"pack/test/osquery info", + "output_size":"1337", + "query":"SELECT * FROM osquery_info", + "system_time":"150", + "user_time":"180", + "wall_time":"0" + }, + { + "average_memory":"50400", + "delimiter":"/", + "denylisted":"1", + "executions":"188", + "interval":"30", + "last_executed":"1620325203", + "name":"pack/test/processes?", + "output_size":"", + "query":"SELECT * FROM processes", + "system_time":"140", + "user_time":"190", + "wall_time":"1" + }, + { + "average_memory":"0", + "delimiter":"/", + "denylisted":"0", + "executions":"1", + "interval":"3600", + "last_executed":"1620323381", + "name":"pack/test/processes?-1", + "output_size":"", + "query":"SELECT * FROM processes", + "system_time":"0", + "user_time":"0", + "wall_time":"0" + }, + { + "average_memory":"0", + "delimiter":"/", + "denylisted":"0", + "executions":"105", + "interval":"47", + "last_executed":"1620325190", + "name":"pack/test/time", + "output_size":"", + "query":"SELECT * FROM time", + "system_time":"70", + "user_time":"50", + "wall_time":"1" + } +] +` + + var rows []map[string]string + require.NoError(t, json.Unmarshal([]byte(resJSON), &rows)) + + assert.NoError(t, ingest(log.NewNopLogger(), &host, rows)) + assert.Len(t, host.PackStats, 2) + sort.Slice(host.PackStats, func(i, j int) bool { + return host.PackStats[i].PackName < host.PackStats[j].PackName + }) + assert.Equal(t, host.PackStats[0].PackName, "pack-2") + assert.ElementsMatch(t, host.PackStats[0].QueryStats, + []kolide.ScheduledQueryStats{ + { + ScheduledQueryName: "time", + PackName: "pack-2", + AverageMemory: 33, + Denylisted: false, + Executions: 1, + Interval: 33, + LastExecuted: time.Unix(1620325191, 0).UTC(), + OutputSize: 0, + SystemTime: 100, + UserTime: 60, + WallTime: 180, + }, + }, + ) + assert.Equal(t, host.PackStats[1].PackName, "test") + assert.ElementsMatch(t, host.PackStats[1].QueryStats, + []kolide.ScheduledQueryStats{ + { + ScheduledQueryName: "osquery info", + PackName: "test", + AverageMemory: 8000, + Denylisted: false, + Executions: 164, + Interval: 30, + LastExecuted: time.Unix(1620325191, 0).UTC(), + OutputSize: 1337, + SystemTime: 150, + UserTime: 180, + WallTime: 0, + }, + { + ScheduledQueryName: "processes?", + PackName: "test", + AverageMemory: 50400, + Denylisted: true, + Executions: 188, + Interval: 30, + LastExecuted: time.Unix(1620325203, 0).UTC(), + OutputSize: 0, + SystemTime: 140, + UserTime: 190, + WallTime: 1, + }, + { + ScheduledQueryName: "processes?-1", + PackName: "test", + AverageMemory: 0, + Denylisted: false, + Executions: 1, + Interval: 3600, + LastExecuted: time.Unix(1620323381, 0).UTC(), + OutputSize: 0, + SystemTime: 0, + UserTime: 0, + WallTime: 0, + }, + { + ScheduledQueryName: "time", + PackName: "test", + AverageMemory: 0, + Denylisted: false, + Executions: 105, + Interval: 47, + LastExecuted: time.Unix(1620325190, 0).UTC(), + OutputSize: 0, + SystemTime: 70, + UserTime: 50, + WallTime: 1, + }, + }, + ) + + assert.NoError(t, ingest(log.NewNopLogger(), &host, nil)) + assert.Len(t, host.PackStats, 0) +} + func TestNewDistributedQueryCampaign(t *testing.T) { ds := &mock.Store{ AppConfigStore: mock.AppConfigStore{ diff --git a/server/service/service_users.go b/server/service/service_users.go index cbd7dcded2..567bfb6dcf 100644 --- a/server/service/service_users.go +++ b/server/service/service_users.go @@ -348,7 +348,7 @@ func (svc service) RequestPasswordReset(ctx context.Context, email string) error UserID: user.ID, Token: token, } - request, err = svc.ds.NewPasswordResetRequest(request) + _, err = svc.ds.NewPasswordResetRequest(request) if err != nil { return err } diff --git a/server/service/transport_carves.go b/server/service/transport_carves.go index ad8839014d..960f4cf2bf 100644 --- a/server/service/transport_carves.go +++ b/server/service/transport_carves.go @@ -5,8 +5,8 @@ import ( "encoding/json" "net/http" - "github.com/pkg/errors" "github.com/fleetdm/fleet/server/kolide" + "github.com/pkg/errors" ) func decodeCarveBeginRequest(ctx context.Context, r *http.Request) (interface{}, error) { @@ -68,4 +68,3 @@ func decodeGetCarveBlockRequest(ctx context.Context, r *http.Request) (interface } return getCarveBlockRequest{ID: int64(id), BlockId: int64(blockId)}, nil } - diff --git a/server/service/transport_hosts.go b/server/service/transport_hosts.go index 862a8df5fe..57c2352a01 100644 --- a/server/service/transport_hosts.go +++ b/server/service/transport_hosts.go @@ -30,6 +30,14 @@ func decodeDeleteHostRequest(ctx context.Context, r *http.Request) (interface{}, return deleteHostRequest{ID: id}, nil } +func decodeRefetchHostRequest(ctx context.Context, r *http.Request) (interface{}, error) { + id, err := idFromRequest(r, "id") + if err != nil { + return nil, err + } + return refetchHostRequest{ID: id}, nil +} + func decodeListHostsRequest(ctx context.Context, r *http.Request) (interface{}, error) { hopt, err := hostListOptionsFromRequest(r) if err != nil { diff --git a/server/service/transport_osquery_test.go b/server/service/transport_osquery_test.go index c0c23f01fe..03facd43d0 100644 --- a/server/service/transport_osquery_test.go +++ b/server/service/transport_osquery_test.go @@ -8,8 +8,8 @@ import ( "net/http/httptest" "testing" - "github.com/gorilla/mux" "github.com/fleetdm/fleet/server/kolide" + "github.com/gorilla/mux" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/server/test/new_objects.go b/server/test/new_objects.go index 7c4713dc26..07a3db4e15 100644 --- a/server/test/new_objects.go +++ b/server/test/new_objects.go @@ -9,10 +9,14 @@ import ( ) func NewQuery(t *testing.T, ds kolide.Datastore, name, q string, authorID uint, saved bool) *kolide.Query { + authorPtr := &authorID + if authorID == 0 { + authorPtr = nil + } query, err := ds.NewQuery(&kolide.Query{ Name: name, Query: q, - AuthorID: &authorID, + AuthorID: authorPtr, Saved: saved, }) require.Nil(t, err) @@ -121,8 +125,9 @@ func NewUser(t *testing.T, ds kolide.Datastore, name, username, email string, ad return u } -func NewScheduledQuery(t *testing.T, ds kolide.Datastore, pid, qid, interval uint, snapshot, removed bool) *kolide.ScheduledQuery { +func NewScheduledQuery(t *testing.T, ds kolide.Datastore, pid, qid, interval uint, snapshot, removed bool, name string) *kolide.ScheduledQuery { sq, err := ds.NewScheduledQuery(&kolide.ScheduledQuery{ + Name: name, PackID: pid, QueryID: qid, Interval: interval, diff --git a/server/test/testingt.go b/server/test/testingt.go index f80332a367..06cc52d46a 100644 --- a/server/test/testingt.go +++ b/server/test/testingt.go @@ -13,5 +13,4 @@ func Quiet(t *testing.T) *quiet { func (q *quiet) Logf(format string, args ...interface{}) { // No logging - return } diff --git a/tools/backup_db/README.md b/tools/backup_db/README.md index 928c706e72..3a6dc69d93 100644 --- a/tools/backup_db/README.md +++ b/tools/backup_db/README.md @@ -1,3 +1,3 @@ These scripts are for backing up and restore the Docker development MySQL database. -Usage is documented [here](../../docs/3-Contribution/2-Testing.md#database-backuprestore). +Usage is documented [here](../../docs/4-Contribution/2-Testing.md#database-backuprestore). diff --git a/tools/fleetctl-npm/package.json b/tools/fleetctl-npm/package.json index 173aaff891..55b03abe85 100644 --- a/tools/fleetctl-npm/package.json +++ b/tools/fleetctl-npm/package.json @@ -1,6 +1,6 @@ { "name": "fleetctl", - "version": "3.11.0", + "version": "3.12.0", "description": "Installer for the fleetctl CLI tool", "bin": { "fleetctl": "./run.js" diff --git a/website/.gitignore b/website/.gitignore index a1ddb80e79..4177735bbc 100644 --- a/website/.gitignore +++ b/website/.gitignore @@ -132,3 +132,9 @@ lib-cov *.out *.pid + +################################################ +# HTML files compiled from markdown in build. +################################################ + +views/partials/built-from-markdown/ diff --git a/website/api/controllers/deliver-contact-form-message.js b/website/api/controllers/deliver-contact-form-message.js index 7668439757..a129cc4c79 100644 --- a/website/api/controllers/deliver-contact-form-message.js +++ b/website/api/controllers/deliver-contact-form-message.js @@ -23,15 +23,22 @@ module.exports = { example: 'I want to buy stuff.' }, - fullName: { + firstName: { required: true, type: 'string', - description: 'The full name of the human sending this message.', - example: 'Hermione Granger' + description: 'The first name of the human sending this message.', + example: 'Emma' + }, + + lastName: { + required: true, + type: 'string', + description: 'The last name of the human sending this message.', + example: 'Watson' }, message: { - required: true, + required: false, type: 'string', description: 'The custom message, in plain text.' } @@ -48,32 +55,19 @@ module.exports = { }, - fn: async function({emailAddress, topic, fullName, message}) { + fn: async function({emailAddress, topic, firstName, lastName, message}) { - if (!sails.config.custom.internalEmailAddress) { + if (!sails.config.custom.slackWebhookUrlForContactForm) { throw new Error( -`Cannot deliver incoming message from contact form because there is no internal -email address (\`sails.config.custom.internalEmailAddress\`) configured for this -app. To enable contact form emails, you'll need to add this missing setting to -your custom config -- usually in \`config/custom.js\`, \`config/staging.js\`, -\`config/production.js\`, or via system environment variables.` + 'Message not delivered: slackWebhookUrlForContactForm needs to be configured in sails.config.custom. Here\'s the undelivered message: ' + + `Name: ${firstName + ' ' + lastName}, Email: ${emailAddress}, Topic: ${topic}, Message: ${message ? message : 'No message.'}` ); + } else { + await sails.helpers.http.post(sails.config.custom.slackWebhookUrlForContactForm, { + text: `New contact form message: (Remember: we have to email back; can't just reply to this thread.) cc @sales `+ + `Name: ${firstName + ' ' + lastName}, Email: ${emailAddress}, Topic: ${topic}, Message: ${message ? message : 'No message.'}` + }); } - - await sails.helpers.sendTemplateEmail.with({ - to: sails.config.custom.internalEmailAddress, - subject: 'New contact form message', - template: 'internal/email-contact-form', - layout: false, - templateData: { - contactName: fullName, - contactEmail: emailAddress, - topic, - message, - } - }); - } - }; diff --git a/website/api/controllers/view-query-detail.js b/website/api/controllers/view-query-detail.js new file mode 100644 index 0000000000..01ba96b1ec --- /dev/null +++ b/website/api/controllers/view-query-detail.js @@ -0,0 +1,27 @@ +module.exports = { + + + friendlyName: 'View query detail', + + + description: 'Display "Query detail" page.', + + + exits: { + + success: { + viewTemplatePath: 'pages/query-detail' + } + + }, + + + fn: async function () { + + // Respond with view. + return {}; + + } + + +}; diff --git a/website/api/helpers/compile-markdown-content.js b/website/api/helpers/compile-markdown-content.js new file mode 100644 index 0000000000..937bb63900 --- /dev/null +++ b/website/api/helpers/compile-markdown-content.js @@ -0,0 +1,193 @@ +module.exports = { + + + friendlyName: 'Compile markdown content', + + + description: 'Compile documentation templates from markdown.', + + + inputs: { + + repoPath: { + description: 'The path of the subdirectory in the Git repo to compile from.', + type: 'string', + example: 'docs/', + required: true + }, + + repoBranch: { + description: 'The name of the branch to compile from.', + type: 'string', + defaultsTo: 'master' + }, + + repoUrl: { + description: 'The git:// URL of the remote Git repo to compile from.', + type: 'string', + defaultsTo: 'git://github.com/fleetdm/fleet.git', + } + + }, + + + exits: { + success: { outputDescription: 'A list of metadata about all generated HTML files.', outputType: [{}] } + }, + + + fn: async function ({repoPath, repoBranch, repoUrl}) { + + let path = require('path'); + let cheerio = require('cheerio'); + let DocTemplater = require('doc-templater'); + + sails.log.info('Compiling `%s` docs from the `%s` branch of `%s`...', repoPath, repoBranch, repoUrl); + + // Relative path within this app where output will be written. + let htmlOutputPath = path.join('views/partials/built-from-markdown/', _.kebabCase(repoPath)); + + // Relative path within this app where temporary menu data will be written. + // (This is consumed later in this file.) + let jsMenuOutputPath = path.join('.tmp/doc-templater/menus/', `${_.kebabCase(repoPath)}.tmp-menu.json`);// fka "the .jsmenu file" + + // Delete existing output from previous runs, if any. + await sails.helpers.fs.rmrf(path.resolve(sails.config.appPath, htmlOutputPath)); + await sails.helpers.fs.rmrf(path.resolve(sails.config.appPath, jsMenuOutputPath)); + + // Compile the markdown into HTML files and a JSON file (aka "jsmenu") representing + // the manifest of all compiled HTML files and their hierarchy. + await new Promise((resolve, reject)=>{ + DocTemplater().build([{ + remote: repoUrl, + branch: repoBranch, + remoteSubPath: repoPath, + outputExtension: 'ejs',//« the file extension for resulting HTML files + htmlDirPath: htmlOutputPath, + jsMenuPath: jsMenuOutputPath, + beforeConvert: (mdString, proceed)=>{// This function is applied to each template before the markdown is converted to markup + // Based on the github-flavored markdown's language annotation, (e.g. ```js```) add a temporary marker to code blocks that can be parsed post-md-compilation by the `afterConvert()` lifecycle hook + // Note: This is an HTML comment because it is easy to over-match and "accidentally" add it underneath each code block as well (being an HTML comment ensures it doesn't show up or break anything) + let LANG_MARKER_PREFIX = ''; + let modifiedMd = mdString.replace(/(```)([a-zA-Z0-9\-]*)(\s*\n)/g, '$1\n' + LANG_MARKER_PREFIX + '$2' + LANG_MARKER_SUFFIX + '$3'); + + // FUTURE: implement a way of skipping non-markdown files, such as pngs (maybe here, but prbly just make it built-in) + + return proceed(undefined, modifiedMd); + }, + afterConvert: (html, proceed)=>{// This function is applied to each template after the markdown is converted to markup + + let modifiedHtml = html; + + // Replace github emoji with unicode emojis + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + // TODO: actually sub unicode, instead of the following (there's probably an open source lib out there to do it) + // modifiedHtml = html.replace(/\:white_check_mark\:/g, ''); + // modifiedHtml = modifiedHtml.replace(/\:white_large_square\:/g, ''); + // modifiedHtml = modifiedHtml.replace(/\:heavy_multiplication_x\:/g, ''); + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + // TODO: As equivalent concepts are identified in the Fleet docs (e.g. in the API reference), maybe bring this back: + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + // // Replace ((bubble))s with HTML + // modifiedHtml = modifiedHtml.replace(/\(\(([^())]*)\)\)/g, ''); + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + // Flag

    ,

    ,

    , and

    tags with the `permalinkable` directive, so they can be clicked + // e.g. ?q=transport-compatibility + let $ = cheerio.load(modifiedHtml); + $('h2, h3, h4, h5').each(function() { + let content = $(this).text() || ''; + + // build the URL slug suffix + let slug = content + .replace(/[\?\!\.\-\_\:\;\'\"]/g, '') // punctuation => gone + .replace(/\s/g, '-') // spaces => dashes + .toLowerCase(); + + // set the "permalink" HTML attr to the slug + $(this).attr('permalink', slug); + + if ($(this) && typeof $(this).wrap === 'function') {// this was throwing ".wrap is undefined" + $(this).wrap(''); + } + + }); + modifiedHtml = $.html(); + + // Modify links + modifiedHtml = modifiedHtml.replace(/(href="https?:\/\/([^"]+)")/g, (hrefString)=>{ + // Check if this is an external link (like https://google.com) but that is ALSO not a link + // to some page on the destination site where this will be hosted, like `(*.)?sailsjs.com`. + // If external, add target="_blank" so the link will open in a new tab. + let isExternal = ! hrefString.match(/^href=\"https?:\/\/([^\.]+\.)*fleetdm\.com/g); + if (isExternal) { + return hrefString.replace(/(href="https?:\/\/([^"]+)")/g, '$1 target="_blank"'); + } else { + // Otherwise, change the link to be web root relative. + // (e.g. 'href="http://sailsjs.com/documentation/concepts"'' becomes simply 'href="/documentation/concepts"'') + // Note: See the Git version history of this file for examples of ways this can work across versioned subdomains. + return hrefString.replace(/href="https?:\/\//, '').replace(/^fleetdm\.com/, 'href="'); + } + });//∞ + + // Add the appropriate class to the `` based on the temporary marker that was added in the `beforeConvert` function above + // console.log('RAN AFTER HOOK, found: ',modifiedHtml.match(/(]*)(>\s*)(\<!--\s*__LANG=\%[^\%]*\%__\s*--\>)/g)); + modifiedHtml = modifiedHtml.replace(// Interpret `js` as `javascript` + // $1 $2 $3 $4 + /(]*)(>\s*)(\<!-- __LANG=\%js\%__ --\>)\s*/gm, + '$1 class="javascript"$2$3' + ); + modifiedHtml = modifiedHtml.replace(// Interpret `sh` and `bash` as `bash` + // $1 $2 $3 $4 + /(]*)(>\s*)(\<!-- __LANG=\%(bash|sh)\%__ --\>)\s*/gm, + '$1 class="bash"$2$3' + ); + modifiedHtml = modifiedHtml.replace(// When unspecified, default to `text` + // $1 $2 $3 $4 + /(]*)(>\s*)(\<!-- __LANG=\%\%__ --\>)\s*/gm, + '$1 class="nohighlight"$2$3' + ); + modifiedHtml = modifiedHtml.replace(// Finally, nab the rest, leaving the code language as-is. + // $1 $2 $3 $4 $5 $6 + /(]*)(>\s*)(\<!-- __LANG=\%)([^%]+)(\%__ --\>)\s*/gm, + '$1 class="$5"$2$3' + ); + + return proceed(undefined, modifiedHtml); + }, + }], (err)=>{ + if (err) { + reject(err); + } else { + resolve(); + } + });//_∏_ + }); + + // Now return the menu data, to provide a nicer way of programmatically working with it. + // But first, we'll clean it up a bit. + let filesGenerated = await sails.helpers.fs.readJson(jsMenuOutputPath); + + // TODO: bring in the other cleanup in marshal-doc-page-metadata from sailsjs.com repo + + // Since the format from the doc-templater package can be a little bit misleading, + // we'll also munge the resulting data a little bit. + for (let fileInfo of filesGenerated) { + + fileInfo.path = fileInfo.fullPathAndFileName;// « for clarity (it's not technically the full path) + delete fileInfo.fullPathAndFileName; + + fileInfo.fallbackTitle = sails.helpers.toSentenceCase(path.basename(fileInfo.templateTitle, '.ejs'));// « for clarity (the page isn't a template, necessarily, and this title is just a guess. Display title will, more likely than not, come from a tag -- see the bottom of the original, raw unformatted markdown of any page in the sailsjs docs for an example of how to use docmeta tags) + delete fileInfo.templateTitle; + + delete fileInfo.data.lastModified;// « for clarity (this isn't the timestamp you're expecting, so we delete it) + }//∞ + + return filesGenerated; + + } + + +}; diff --git a/website/api/helpers/to-sentence-case.js b/website/api/helpers/to-sentence-case.js new file mode 100644 index 0000000000..3454aaaa8f --- /dev/null +++ b/website/api/helpers/to-sentence-case.js @@ -0,0 +1,33 @@ +module.exports = { + + + friendlyName: 'To sentence case', + + + description: 'Make a best-effort conversion of the specified text into sentence case.', + + + sync: true, + + + inputs: { + text: { type: 'string', example: 'the-Weird Catfood', required: true } + }, + + + exits: { + success: { outputType: 'string', outputExample: 'The weird catfood' }, + }, + + + fn: function ({ text }) { + return text + .split(/[\s-_]+/) + .filter((word, idx) => !(idx === 0 && word.match(/[0-9]+/))) // « strip off any leading numbers so first word is actually capitalized + .map((word, idx) => (idx === 0? word[0].toUpperCase() : word[0].toLowerCase())+word.slice(1)) + .join(' '); + } + + +}; + diff --git a/website/assets/images/chevron-12x8@2x.png b/website/assets/images/chevron-12x8@2x.png new file mode 100644 index 0000000000..c58c19b7ee Binary files /dev/null and b/website/assets/images/chevron-12x8@2x.png differ diff --git a/website/assets/js/pages/contact.page.js b/website/assets/js/pages/contact.page.js index 035e53d7b5..5f6b65b6f7 100644 --- a/website/assets/js/pages/contact.page.js +++ b/website/assets/js/pages/contact.page.js @@ -16,9 +16,10 @@ parasails.registerPage('contact', { // Form rules formRules: { emailAddress: {isEmail: true, required: true}, - fullName: {required: true}, + firstName: {required: true}, + lastName: {required: true}, topic: {required: true}, - message: {required: true}, + message: {required: false}, }, // Server error state for the form diff --git a/website/assets/js/pages/query-detail.page.js b/website/assets/js/pages/query-detail.page.js new file mode 100644 index 0000000000..752921e501 --- /dev/null +++ b/website/assets/js/pages/query-detail.page.js @@ -0,0 +1,25 @@ +parasails.registerPage('query-detail', { + // ╦╔╗╔╦╔╦╗╦╔═╗╦ ╔═╗╔╦╗╔═╗╔╦╗╔═╗ + // ║║║║║ ║ ║╠═╣║ ╚═╗ ║ ╠═╣ ║ ║╣ + // ╩╝╚╝╩ ╩ ╩╩ ╩╩═╝ ╚═╝ ╩ ╩ ╩ ╩ ╚═╝ + data: { + //… + }, + + // ╦ ╦╔═╗╔═╗╔═╗╦ ╦╔═╗╦ ╔═╗ + // ║ ║╠╣ ║╣ ║ ╚╦╝║ ║ ║╣ + // ╩═╝╩╚ ╚═╝╚═╝ ╩ ╚═╝╩═╝╚═╝ + beforeMount: function() { + //… + }, + mounted: async function() { + //… + }, + + // ╦╔╗╔╔╦╗╔═╗╦═╗╔═╗╔═╗╔╦╗╦╔═╗╔╗╔╔═╗ + // ║║║║ ║ ║╣ ╠╦╝╠═╣║ ║ ║║ ║║║║╚═╗ + // ╩╝╚╝ ╩ ╚═╝╩╚═╩ ╩╚═╝ ╩ ╩╚═╝╝╚╝╚═╝ + methods: { + //… + } +}); diff --git a/website/assets/styles/bootstrap-overrides.less b/website/assets/styles/bootstrap-overrides.less index 5ff72e033f..c03b5974b3 100644 --- a/website/assets/styles/bootstrap-overrides.less +++ b/website/assets/styles/bootstrap-overrides.less @@ -12,6 +12,7 @@ body { font-family: @main-font; + color: @core-fleet-black; } img { @@ -51,6 +52,13 @@ footer { line-height: 24px; } +.text-danger { + color: @core-vibrant-red !important; //lesshint-disable-line importantRule +} + +a.text-danger:hover, a.text-danger:focus { + color: darken(@core-vibrant-red, 20%) !important; //lesshint-disable-line importantRule +} .btn { font-family: @header-font; @@ -79,25 +87,42 @@ footer { } &.btn-primary { - background-color: #EF7182; - border-color: #EF7182; + background-color: #ff5c83; + border-color: #ff5c83; &:hover { - background-color: darken(#EF7182, 10%); - border-color: darken(#EF7182, 10%); + background-color: darken(#ff5c83, 10%); + border-color: darken(#ff5c83, 10%); + } + &:focus, &.focus { + box-shadow: 0 0 0.25rem 0.2rem #ff5c83; + } + &.disabled, &:disabled { + color: #fff; + background-color: darken(#ff5c83, 10%), 0.25; + border-color: darken(#ff5c83, 10%), 0.25; + } + &:not(:disabled):not(.disabled):active, &:not(:disabled):not(.disabled).active, .show > &.dropdown-toggle { + color: #fff; + background-color: darken(#ff5c83, 10%), 0.25; + border-color: darken(#ff5c83, 10%), 0.25; + } + &:not(:disabled):not(.disabled):active:focus, &:not(:disabled):not(.disabled).active:focus, .show > &.dropdown-toggle:focus { + box-shadow: 0 0 0.25rem 0.2rem #ff5c83; } } + &.btn-info { - background-color: #6B6CFA; - border-color: #6B6CFA; + background-color: #6b6cfa; + border-color: #6b6cfa; &:hover { - background-color: darken(#6B6CFA, 10%); - border-color: darken(#6B6CFA, 10%); + background-color: darken(#6b6cfa, 10%); + border-color: darken(#6b6cfa, 10%); } } } .card { - box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.1); + box-shadow: 0px 6px 20px rgba(0, 0, 0, 0.05); } .dropdown:hover > .dropdown-menu { @@ -108,7 +133,7 @@ footer { } .dropdown-menu { - left: 16px !important;//lesshint-disable-line importantRule + left: 16px !important; //lesshint-disable-line importantRule min-width: 0; margin: 0; } @@ -133,3 +158,32 @@ footer { pointer-events: none; font-weight: @bold; } + +.form-control { + height: 54px; + padding: 8px 16px; + font-size: 18px; + color: #192147; + border: 1px solid #C5C7D1; +} +.form-control:focus { + border: 2px solid #6a67fe; + box-shadow: unset; +} +.form-control.is-invalid:focus { + box-shadow: unset; +} +.form-control.is-invalid { + border: 2px solid #FF5C83; +} +textarea.form-control { + height: 108px; +} +.form-group { + margin-bottom: 24px; +} + +.invalid-feedback { + color: @core-vibrant-red; +} + diff --git a/website/assets/styles/importer.less b/website/assets/styles/importer.less index 4b287d8fc1..00cd70bcc7 100644 --- a/website/assets/styles/importer.less +++ b/website/assets/styles/importer.less @@ -42,3 +42,5 @@ @import 'pages/404.less'; @import 'pages/500.less'; @import 'pages/498.less'; + +@import 'pages/query-detail.less'; diff --git a/website/assets/styles/layout.less b/website/assets/styles/layout.less index d013930be7..c51b2f7cb3 100644 --- a/website/assets/styles/layout.less +++ b/website/assets/styles/layout.less @@ -22,7 +22,7 @@ html, body { right: 0; .header-btn { color: #ffffff; - cursor: default !important;//lesshint-disable-line importantRule + cursor: pointer !important;//lesshint-disable-line importantRule font-family: @navigation-font; font-weight: @bold; } @@ -80,6 +80,24 @@ html, body { background-color: #f1f0ff; font-weight: @bold; } + .menu-link { + color: #192147; + font-weight: @bold; + svg { + path { + fill: #192147; + } + } + } + .menu-link:hover { + color: #6a67fe; + + svg { + path { + fill: #6a67fe; + } + } + } } [purpose='page-header'].header { @@ -88,7 +106,7 @@ html, body { border-bottom: 1px solid #e2e4ea; .header-btn { color: #192147; - cursor: default !important;//lesshint-disable-line importantRule + cursor: pointer !important;//lesshint-disable-line importantRule font-family: @navigation-font; font-weight: @bold; } @@ -141,6 +159,24 @@ html, body { background-color: #f1f0ff; font-weight: @bold; } + .menu-link { + color: #192147; + font-weight: @bold; + svg { + path { + fill: #192147; + } + } + } + .menu-link:hover { + color: #6a67fe; + + svg { + path { + fill: #6a67fe; + } + } + } } [purpose='page-footer'] { @@ -177,21 +213,25 @@ body.detected-mobile { // … } +.dropdown:hover > .btn.btn-link { + color: #6a67fe; +} + @media (max-width: 1199px) { [purpose='page-wrap'] { - padding-bottom: 230px; + padding-bottom: 260px; [purpose='page-footer'] { - height: 230px; + height: 260px; } } } @media (max-width: 767px) { [purpose='page-wrap'] { - padding-bottom: 360px; + padding-bottom: 380px; [purpose='page-footer'] { padding-top: 44px; - height: 360px; + height: 380px; } } } diff --git a/website/assets/styles/mixins-and-variables/colors.less b/website/assets/styles/mixins-and-variables/colors.less index 42f35bd939..817041b2cc 100644 --- a/website/assets/styles/mixins-and-variables/colors.less +++ b/website/assets/styles/mixins-and-variables/colors.less @@ -2,12 +2,14 @@ * Color Variables */ +@core-fleet-black: #192147; +@core-vibrant-red: #FF5C83; + @brand: #14acc2; -@error: #B53A03; +@error: @core-vibrant-red; - -@text-normal: #000; +@text-normal: @core-fleet-black; @text-muted: lighten(@text-normal, 60%); @bg-lt-gray: #f1f1f1; diff --git a/website/assets/styles/pages/contact.less b/website/assets/styles/pages/contact.less index 488cc71e78..c240d894f0 100644 --- a/website/assets/styles/pages/contact.less +++ b/website/assets/styles/pages/contact.less @@ -1,5 +1,43 @@ #contact { - //… + label { + font-weight: 700; + font-size: 18px; + } + .btn-primary { + background-color: #ff5c83; + border-color: #ff5c83; + &:hover { + background-color: darken(#ff5c83, 10%); + border-color: darken(#ff5c83, 10%); + } + } + + .selectbox { + position: relative; + } + .selectbox::after { + content: url('/images/chevron-12x8@2x.png'); + right: 16px; + transform: scale(0.5); + top: 16px; + position: absolute; + pointer-events: none; + } + .selectbox select { + appearance: none; + } + + @media (max-width: 575px) { + h2, p, .btn-container { + text-align: center; + } + + button { + display: inline; + width: 100%; + max-width: 325px; + } + } } diff --git a/website/assets/styles/pages/get-started.less b/website/assets/styles/pages/get-started.less index d52f62c6dd..73bb9fcdea 100644 --- a/website/assets/styles/pages/get-started.less +++ b/website/assets/styles/pages/get-started.less @@ -9,7 +9,7 @@ color: #6a67fe; } #primary-button { - color: #fff; + color: #fff; } #learn-more { background: #f9fafc; @@ -33,6 +33,7 @@ border-color: #f9fafc; } } + #arrow { padding-right: 45px; cursor: pointer; @@ -41,11 +42,11 @@ color: #192147; font-weight: bold; user-select: none; - transition: 0.2s linear; - -o-transition: 0.2s linear; - -ms-transition: 0.2s linear; - -moz-transition: 0.2s linear; - -webkit-transition: 0.2s linear; + transition: 0.2s ease-in-out; + -o-transition: 0.2s ease-in-out; + -ms-transition: 0.2s ease-in-out; + -moz-transition: 0.2s ease-in-out; + -webkit-transition: 0.2s ease-in-out; } #arrow:before { @@ -54,16 +55,21 @@ position: absolute; top: 12px; left: 80%; // <--- here + transition: 0.2s ease-in-out; + -o-transition: 0.2s ease-in-out; + -ms-transition: 0.2s ease-in-out; + -moz-transition: 0.2s ease-in-out; + -webkit-transition: 0.2s ease-in-out; /* opacity: 0; */ } #arrow:hover:before { left: 82%; // <--- here - transition: 0.2s linear; - -o-transition: 0.2s linear; - -ms-transition: 0.2s linear; - -moz-transition: 0.2s linear; - -webkit-transition: 0.2s linear; + transition: 0.2s ease-in-out; + -o-transition: 0.2s ease-in-out; + -ms-transition: 0.2s ease-in-out; + -moz-transition: 0.2s ease-in-out; + -webkit-transition: 0.2s ease-in-out; /* opacity:1; */ } } diff --git a/website/assets/styles/pages/homepage.less b/website/assets/styles/pages/homepage.less index e7bad929d3..942632c780 100644 --- a/website/assets/styles/pages/homepage.less +++ b/website/assets/styles/pages/homepage.less @@ -1,4 +1,12 @@ #homepage { + .btn-primary { + background-color: #ff5c83; + border-color: #ff5c83; + &:hover { + background-color: darken(#ff5c83, 10%); + border-color: darken(#ff5c83, 10%); + } + } .hero-banner { background: rgb(31, 29, 66); background: linear-gradient(170.26deg, #1F1D42 7.33%, #00FFF0 130.16%);//lesshint-disable-line duplicateProperty @@ -27,9 +35,6 @@ .quote-mark { font-family: @header-font; } - .footer-fade { - background: linear-gradient(360deg, #201E43 0%, #353D62 100%); - } .log-destinations-container { display: flex; flex-wrap: wrap; @@ -53,6 +58,48 @@ font-weight: bold; font-size: 0.9rem; } + + // TODO: dedupe this into a mixin and use from here and in pricing.less, or use existing transition mixin + [purpose='animated-arrow-button'] { + padding-right: 45px; + cursor: pointer; + position: relative; + width: auto; + color: #192147; + font-weight: bold; + user-select: none; + transition: 0.2s ease-in-out; + -o-transition: 0.2s ease-in-out; + -ms-transition: 0.2s ease-in-out; + -moz-transition: 0.2s ease-in-out; + -webkit-transition: 0.2s ease-in-out; + } + + [purpose='animated-arrow-button']:before { + content: url('/images/arrow-right-16x16@2x.png'); + transform: scale(0.5); + position: absolute; + top: 12px; + left: 80%; // <--- here + transition: 0.2s ease-in-out; + -o-transition: 0.2s ease-in-out; + -ms-transition: 0.2s ease-in-out; + -moz-transition: 0.2s ease-in-out; + -webkit-transition: 0.2s ease-in-out; + /* opacity: 0; */ + } + + [purpose='animated-arrow-button']:hover:before { + left: 82%; // <--- here + transition: 0.2s ease-in-out; + -o-transition: 0.2s ease-in-out; + -ms-transition: 0.2s ease-in-out; + -moz-transition: 0.2s ease-in-out; + -webkit-transition: 0.2s ease-in-out; + /* opacity:1; */ + } + + @media (max-width: 1175px) { .hero-bg { background-position: -1100px bottom; diff --git a/website/assets/styles/pages/pricing.less b/website/assets/styles/pages/pricing.less index d58de5e90e..4c5f486c4e 100644 --- a/website/assets/styles/pages/pricing.less +++ b/website/assets/styles/pages/pricing.less @@ -2,32 +2,16 @@ h1, p { color: #192147; } - section { + section {// TODO remove this in favor of making HTML match homepage.ejs w/ bootstrap row classes and such max-width: 800px; } - .btn { - font-family: @header-font; - font-weight: 700; - padding-top: 18px; - padding-bottom: 18px; - font-size: 20px; - line-height: 23px; - border-radius: 10px; - &.btn-white { - position: relative; - padding-right: 20px; - color: #192147; - background-color: #fff; - border-color: #fff; - } - } - .card-body { - box-shadow: 0px 6px 20px rgba(0, 0, 0, 0.05); - border: 1px solid #e2e4ea; - border-radius: 0.75rem; + + @media (max-width: 767px) { + p {font-size: 16px;} } - #arrow { + // TODO: dedupe animated-arrow-button (see homepage.less) + [purpose='animated-arrow-button'] { padding-right: 45px; cursor: pointer; position: relative; @@ -35,29 +19,34 @@ color: #192147; font-weight: bold; user-select: none; - transition: 0.2s linear; - -o-transition: 0.2s linear; - -ms-transition: 0.2s linear; - -moz-transition: 0.2s linear; - -webkit-transition: 0.2s linear; + transition: 0.2s ease-in-out; + -o-transition: 0.2s ease-in-out; + -ms-transition: 0.2s ease-in-out; + -moz-transition: 0.2s ease-in-out; + -webkit-transition: 0.2s ease-in-out; } - #arrow:before { + [purpose='animated-arrow-button']:before { content: url('/images/arrow-right-16x16@2x.png'); transform: scale(0.5); position: absolute; top: 12px; left: 80%; // <--- here + transition: 0.2s ease-in-out; + -o-transition: 0.2s ease-in-out; + -ms-transition: 0.2s ease-in-out; + -moz-transition: 0.2s ease-in-out; + -webkit-transition: 0.2s ease-in-out; /* opacity: 0; */ } - #arrow:hover:before { + [purpose='animated-arrow-button']:hover:before { left: 82%; // <--- here - transition: 0.2s linear; - -o-transition: 0.2s linear; - -ms-transition: 0.2s linear; - -moz-transition: 0.2s linear; - -webkit-transition: 0.2s linear; + transition: 0.2s ease-in-out; + -o-transition: 0.2s ease-in-out; + -ms-transition: 0.2s ease-in-out; + -moz-transition: 0.2s ease-in-out; + -webkit-transition: 0.2s ease-in-out; /* opacity:1; */ } } diff --git a/website/assets/styles/pages/query-detail.less b/website/assets/styles/pages/query-detail.less new file mode 100644 index 0000000000..f3963d5be7 --- /dev/null +++ b/website/assets/styles/pages/query-detail.less @@ -0,0 +1,5 @@ +#query-detail { + + //… + +} diff --git a/website/config/custom.js b/website/config/custom.js index 44de0294c5..fa8de14f08 100644 --- a/website/config/custom.js +++ b/website/config/custom.js @@ -98,8 +98,9 @@ module.exports.custom = { /*************************************************************************** * * * Any other custom config this Sails app should use during development. * + * (and possibly in ALL environments, if not overridden in config/env/) * * * ***************************************************************************/ - // … + //… }; diff --git a/website/config/env/development.js b/website/config/env/development.js index fdd45c303e..19ac24135e 100644 --- a/website/config/env/development.js +++ b/website/config/env/development.js @@ -14,4 +14,10 @@ module.exports = { // be running on port 1337 on your computer.) port: 2024, + // Add any dev-only routes for local development of not-yet-released pages. + // e.g. http://localhost:2024/sandbox/example-query + routes: { + 'GET /sandbox/example-query': { action: 'view-query-detail' }, + }, + }; diff --git a/website/config/policies.js b/website/config/policies.js index 97d83384b1..662f79a854 100644 --- a/website/config/policies.js +++ b/website/config/policies.js @@ -23,5 +23,6 @@ module.exports.policies = { 'legal/view-terms': true, 'legal/view-privacy': true, 'deliver-contact-form-message': true, + 'view-query-detail': true, }; diff --git a/website/config/routes.js b/website/config/routes.js index be89883e3d..0d3458635e 100644 --- a/website/config/routes.js +++ b/website/config/routes.js @@ -17,26 +17,19 @@ module.exports.routes = { '/blog': 'https://medium.com/fleetdm', - '/pricing': (req, res)=>{ - // FUTURE: pricing page explaining commercial support and EE, w/ our subscription terms - res.redirect('mailto:todo@example.com?subject=Pricing question&body=Please do not send this email!\n\nWe are a very young company and still working on our processes. For now, if you have a pricing question or would like to know Fleet\'s latest pricing and support tiers, please create an issue at https://github.com/fleetdm/fleet/issues. Thank you!'); - }, - '/legal/terms': 'https://docs.google.com/document/d/1OM6YDVIs7bP8wg6iA3VG13X086r64tWDqBSRudG4a0Y/edit', - '/security': (req, res)=>{ - // FUTURE: make a page- check out how Sails does it, and also https://about.gitlab.com/security/ - res.redirect('mailto:todo@example.com?subject=Security vulnerability&body=Please do not send this email!\n\nWe are a very young company and still working on our processes. For now, if you have a security vulnerability to report, please send a DM to mikermcneil or Zach Wasserman in the "osquery" Slack workspace. Thank you for letting us know!'); - }, + '/security': 'https://github.com/fleetdm/fleet/security/policy', '/company/about': '/blog', // FUTURE: brief "about" page explaining the origins of the company '/company/stewardship': 'https://github.com/fleetdm/fleet', // FUTURE: page about how we approach open source and our commitments to the community 'GET /company/contact': { action: 'view-contact' }, - 'GET /get-started': { action: 'view-get-started' }, - 'GET /pricing': { action: 'view-pricing' }, - '/try-fleet': '/get-started', + 'GET /apply': 'https://fleet-device-management.breezy.hr', + + 'GET /get-started': { action: 'view-pricing' }, + 'GET /install': 'https://github.com/fleetdm/fleet/blob/master/README.md', // « FUTURE: When ready, bring back { action: 'view-get-started' } '/documentation': 'https://github.com/fleetdm/fleet/tree/master/docs', - '/contribute': 'https://github.com/fleetdm/fleet/tree/master/docs/3-Contribution', + '/contribute': 'https://github.com/fleetdm/fleet/tree/master/docs/4-Contribution', '/hall-of-fame': 'https://github.com/fleetdm/fleet/pulse', @@ -62,13 +55,18 @@ module.exports.routes = { // ╔╦╗╦╔═╗╔═╗ ╦═╗╔═╗╔╦╗╦╦═╗╔═╗╔═╗╔╦╗╔═╗ ┬ ╔╦╗╔═╗╦ ╦╔╗╔╦ ╔═╗╔═╗╔╦╗╔═╗ // ║║║║╚═╗║ ╠╦╝║╣ ║║║╠╦╝║╣ ║ ║ ╚═╗ ┌┼─ ║║║ ║║║║║║║║ ║ ║╠═╣ ║║╚═╗ // ╩ ╩╩╚═╝╚═╝ ╩╚═╚═╝═╩╝╩╩╚═╚═╝╚═╝ ╩ ╚═╝ └┘ ═╩╝╚═╝╚╩╝╝╚╝╩═╝╚═╝╩ ╩═╩╝╚═╝ - // '/logout': '/api/v1/account/logout', + + // Convenience + '/pricing': '/get-started', '/company': '/company/about', '/support': '/company/contact', '/contact': '/company/contact', '/legal': '/legal/terms', '/terms': '/legal/terms', + // '/logout': '/api/v1/account/logout', + // Legacy (to avoid breaking links) + '/try-fleet': '/get-started', // ╦ ╦╔═╗╔╗ ╦ ╦╔═╗╔═╗╦╔═╔═╗ // ║║║║╣ ╠╩╗╠═╣║ ║║ ║╠╩╗╚═╗ @@ -89,6 +87,6 @@ module.exports.routes = { // 'POST /api/v1/entrance/signup': { action: 'entrance/signup' }, // 'POST /api/v1/entrance/send-password-recovery-email': { action: 'entrance/send-password-recovery-email' }, // 'POST /api/v1/entrance/update-password-and-login': { action: 'entrance/update-password-and-login' }, - // 'POST /api/v1/deliver-contact-form-message': { action: 'deliver-contact-form-message' }, + 'POST /api/v1/deliver-contact-form-message': { action: 'deliver-contact-form-message' }, }; diff --git a/website/package-lock.json b/website/package-lock.json index 8ad0b5f8bf..9f6a492a8f 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -72,6 +72,30 @@ "uid2": "0.0.3" } }, + "CSSselect": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/CSSselect/-/CSSselect-0.4.1.tgz", + "integrity": "sha1-+Kt+H4QYzmPNput713ioXX7EkrI=", + "requires": { + "CSSwhat": "0.4", + "domutils": "1.4" + }, + "dependencies": { + "domutils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.4.3.tgz", + "integrity": "sha1-CGVRN5bGswYDGFDhdVFrr4C3Km8=", + "requires": { + "domelementtype": "1" + } + } + } + }, + "CSSwhat": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/CSSwhat/-/CSSwhat-0.4.7.tgz", + "integrity": "sha1-hn2g/zn3eGEyQsRM/qg/CqTr35s=" + }, "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -646,6 +670,44 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, + "cheerio": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-0.18.0.tgz", + "integrity": "sha1-ThwGN35yW3QOmW4N/sNThj3md/o=", + "requires": { + "CSSselect": "~0.4.0", + "dom-serializer": "~0.0.0", + "entities": "~1.1.1", + "htmlparser2": "~3.8.1", + "lodash": "~2.4.1" + }, + "dependencies": { + "dom-serializer": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.0.1.tgz", + "integrity": "sha1-lYmCfx4y0iw3yCmtq9WbMkevjq8=", + "requires": { + "domelementtype": "~1.1.1", + "entities": "~1.1.1" + } + }, + "domelementtype": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz", + "integrity": "sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs=" + }, + "entities": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", + "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==" + }, + "lodash": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=" + } + } + }, "class-utils": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", @@ -843,6 +905,11 @@ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" }, + "convert-to-ecmascript-compatible-varname": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/convert-to-ecmascript-compatible-varname/-/convert-to-ecmascript-compatible-varname-0.1.5.tgz", + "integrity": "sha1-9npJOMUjNENWQlBHnGcBS6yHhJk=" + }, "cookie": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", @@ -1166,6 +1233,305 @@ } } }, + "doc-templater": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/doc-templater/-/doc-templater-0.4.0.tgz", + "integrity": "sha1-/3UagCSWN/ePUlYkjIvw1g2BWb0=", + "requires": { + "async": "~0.9.0", + "fs-extra": "~0.8.1", + "lodash": "~2.4.1", + "machine": "^2.0.4", + "machinepack-fs": "~1.3.4", + "machinepack-git": "^2.2.0", + "machinepack-markdown": "^1.0.1", + "machinepack-process": "^3.0.0-4" + }, + "dependencies": { + "async": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz", + "integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=" + }, + "fs-extra": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.8.1.tgz", + "integrity": "sha1-Dld5/7/t9RG8dVWVx/A8BtS0Po0=", + "requires": { + "jsonfile": "~1.1.0", + "mkdirp": "0.3.x", + "ncp": "~0.4.2", + "rimraf": "~2.2.0" + } + }, + "include-all": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/include-all/-/include-all-1.0.8.tgz", + "integrity": "sha1-6LuEsFcniiLPlEMZA32XAMGKQ3k=", + "requires": { + "lodash": "3.10.1" + }, + "dependencies": { + "lodash": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", + "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=" + } + } + }, + "jsonfile": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-1.1.1.tgz", + "integrity": "sha1-2k/WrXfxolUgPqY8e8Mtwx72RDM=" + }, + "lodash": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=" + }, + "machine": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/machine/-/machine-2.0.6.tgz", + "integrity": "sha1-pO3cp6zoy6I8KYdI0oov70z3myM=", + "requires": { + "convert-to-ecmascript-compatible-varname": "^0.1.0", + "debug": "^2.1.1", + "lodash": "~2.4.1", + "object-hash": "~0.3.0", + "rttc": "^1.0.0", + "switchback": "~1.1.0" + } + }, + "machinepack-fs": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/machinepack-fs/-/machinepack-fs-1.3.4.tgz", + "integrity": "sha1-5MoHeslnhelPFLp8L29KI8+OYl8=", + "requires": { + "async": "^0.9.0", + "fs-extra": "~0.10.0", + "list-directory-contents": "0.0.2", + "lodash": "^2.4.1", + "machine": "^1.0.0" + }, + "dependencies": { + "fs-extra": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.10.0.tgz", + "integrity": "sha1-mcDsL9XqqtnWRiReSwFLVnKZgq8=", + "requires": { + "jsonfile": "^1.2.0", + "mkdirp": "^0.5.0", + "ncp": "^0.5.1", + "rimraf": "^2.2.8" + } + }, + "jsonfile": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-1.2.0.tgz", + "integrity": "sha1-XzXWzZ+Ubsl7Wxg1P6nljfG4b2o=" + }, + "machine": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/machine/-/machine-1.3.2.tgz", + "integrity": "sha1-m/Z4xdpXV2p+HyZRWw6squzMj0g=", + "requires": { + "convert-to-ecmascript-compatible-varname": "^0.1.0", + "lodash": "~2.4.1", + "object-hash": "~0.3.0", + "rttc": "^0.2.2", + "switchback": "~1.1.0" + } + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "requires": { + "minimist": "^1.2.5" + } + }, + "ncp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/ncp/-/ncp-0.5.1.tgz", + "integrity": "sha1-dDmFMW49tFkoG1hxaehFc1oFQ58=" + }, + "rttc": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/rttc/-/rttc-0.2.4.tgz", + "integrity": "sha1-XlctrnBlhZSqC2cAgKIxZCGfqVE=", + "requires": { + "lodash": "~2.4.1" + } + } + } + }, + "machinepack-json": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/machinepack-json/-/machinepack-json-2.0.1.tgz", + "integrity": "sha1-uZGrXIjYxR6dgafe87U4gpVmzEA=", + "requires": { + "lodash.iserror": "3.1.1", + "lodash.isfunction": "3.0.8", + "lodash.isregexp": "3.0.5", + "machine": "~12.1.0" + }, + "dependencies": { + "convert-to-ecmascript-compatible-varname": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/convert-to-ecmascript-compatible-varname/-/convert-to-ecmascript-compatible-varname-0.1.4.tgz", + "integrity": "sha1-Sf9G6WwdNWqR1Lg+X/4BM8PIrBQ=" + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "lodash": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", + "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=" + }, + "machine": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/machine/-/machine-12.1.1.tgz", + "integrity": "sha512-fohf/zxGNvZL69JfbZI/rf660cLnC2tU3tSz8BHGrl+5c7C/82Zypy2fpT2FKPh1q56zfAaCqyI5hSROqBF90g==", + "requires": { + "convert-to-ecmascript-compatible-varname": "0.1.4", + "debug": "3.1.0", + "lodash": "3.10.1", + "object-hash": "0.3.0", + "rttc": "~9.3.0", + "switchback": "2.0.0" + } + }, + "rttc": { + "version": "9.3.4", + "resolved": "https://registry.npmjs.org/rttc/-/rttc-9.3.4.tgz", + "integrity": "sha1-vABXU7c80WrFANkURta5kyBhctc=", + "requires": { + "lodash": "3.8.0" + }, + "dependencies": { + "lodash": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.8.0.tgz", + "integrity": "sha1-N265i9zZOCqTZcM8TLglDeEyW5E=" + } + } + }, + "switchback": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/switchback/-/switchback-2.0.0.tgz", + "integrity": "sha1-KifZAzPe8wWnUh3MHjL2qOOtcgU=", + "requires": { + "lodash": "~2.4.1" + }, + "dependencies": { + "lodash": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=" + } + } + } + } + }, + "machinepack-process": { + "version": "3.0.0-4", + "resolved": "https://registry.npmjs.org/machinepack-process/-/machinepack-process-3.0.0-4.tgz", + "integrity": "sha1-t6q3yCdAnfxCC+2gkBZYMd5wuIA=", + "requires": { + "lodash": "3.10.1", + "machine": "^13.0.0-7", + "machinepack-json": "~2.0.0", + "open": "0.0.5" + }, + "dependencies": { + "convert-to-ecmascript-compatible-varname": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/convert-to-ecmascript-compatible-varname/-/convert-to-ecmascript-compatible-varname-0.1.4.tgz", + "integrity": "sha1-Sf9G6WwdNWqR1Lg+X/4BM8PIrBQ=" + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "lodash": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", + "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=" + }, + "machine": { + "version": "13.0.0-24", + "resolved": "https://registry.npmjs.org/machine/-/machine-13.0.0-24.tgz", + "integrity": "sha512-M4jMQbHlAgPklsGdCxP6udDgeOEABlYxwSV0oybcgt4bZ5hz0CLIIpJUtBNtpDNe29K9u6qFHQrGAAIkEiNa7w==", + "requires": { + "@sailshq/lodash": "^3.10.2", + "convert-to-ecmascript-compatible-varname": "0.1.4", + "debug": "3.1.0", + "include-all": "^1.0.5", + "rttc": "^9.8.1", + "switchback": "^2.0.1" + } + }, + "rttc": { + "version": "9.8.2", + "resolved": "https://registry.npmjs.org/rttc/-/rttc-9.8.2.tgz", + "integrity": "sha1-IzfSHUE/SjT/+IF3+V6uft+9Jr8=", + "requires": { + "lodash": "3.10.1" + } + }, + "switchback": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/switchback/-/switchback-2.0.5.tgz", + "integrity": "sha512-w9gnsTxR5geOKt45QUryhDP9KTLcOAqje9usR2VQ2ng8DfhaF+mkIcArxioMP/p6Z/ecKE58i2/B0DDlMJK1jw==", + "requires": { + "@sailshq/lodash": "^3.10.3" + } + } + } + }, + "mkdirp": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz", + "integrity": "sha1-3j5fiWHIjHh+4TaN+EmsRBPsqNc=" + }, + "ncp": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/ncp/-/ncp-0.4.2.tgz", + "integrity": "sha1-q8xsvT7C7Spyn/bnwfqPAXhKhXQ=" + }, + "rimraf": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz", + "integrity": "sha1-5Dm+Kq7jJzIZUnMPmaiSnk/FBYI=" + }, + "rttc": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/rttc/-/rttc-1.0.2.tgz", + "integrity": "sha1-TTZCjpUoQrJ0P6cC5PVhoi9kje8=", + "requires": { + "lodash": "~2.4.1" + } + }, + "switchback": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/switchback/-/switchback-1.1.3.tgz", + "integrity": "sha1-EscBCTSNailvc5upEO64U/i25jE=", + "requires": { + "lodash": "~2.4.1" + } + } + } + }, "doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -1179,8 +1545,6 @@ "version": "0.2.2", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", - "dev": true, - "optional": true, "requires": { "domelementtype": "^2.0.1", "entities": "^2.0.0" @@ -1189,32 +1553,24 @@ "domelementtype": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz", - "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==", - "dev": true, - "optional": true + "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==" }, "entities": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", - "dev": true, - "optional": true + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==" } } }, "domelementtype": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", - "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", - "dev": true, - "optional": true + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==" }, "domhandler": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.3.0.tgz", "integrity": "sha1-LeWaCCLVAn+r/28DLCsloqir5zg=", - "dev": true, - "optional": true, "requires": { "domelementtype": "1" } @@ -1223,8 +1579,6 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", - "dev": true, - "optional": true, "requires": { "dom-serializer": "0", "domelementtype": "1" @@ -1349,9 +1703,7 @@ "entities": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/entities/-/entities-1.0.0.tgz", - "integrity": "sha1-sph6o4ITR/zeZCsk/fyeT7cSvyY=", - "dev": true, - "optional": true + "integrity": "sha1-sph6o4ITR/zeZCsk/fyeT7cSvyY=" }, "error-ex": { "version": "1.3.2", @@ -2910,8 +3262,6 @@ "version": "3.8.3", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.8.3.tgz", "integrity": "sha1-mWwosZFRaovoZQGn15dX5ccMEGg=", - "dev": true, - "optional": true, "requires": { "domelementtype": "1", "domhandler": "2.3", @@ -2924,8 +3274,6 @@ "version": "1.1.14", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "dev": true, - "optional": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.1", @@ -3588,6 +3936,14 @@ "resolve": "^1.1.7" } }, + "list-directory-contents": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/list-directory-contents/-/list-directory-contents-0.0.2.tgz", + "integrity": "sha1-D3Kqpy9xJum6LvJkUnt8E2ihnJY=", + "requires": { + "walker": "~1.0.6" + } + }, "load-json-file": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", @@ -3614,11 +3970,26 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "lodash.iserror": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lodash.iserror/-/lodash.iserror-3.1.1.tgz", + "integrity": "sha1-KXuaBfq2cUvCRE18wZ0dfES17Ow=" + }, + "lodash.isfunction": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.8.tgz", + "integrity": "sha1-TbcJ/IG8So/XEnpFilNGxc3OLGs=" + }, "lodash.isplainobject": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" }, + "lodash.isregexp": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/lodash.isregexp/-/lodash.isregexp-3.0.5.tgz", + "integrity": "sha1-4PWWJC8voiioQAhrbIrYLktx/S0=" + }, "lodash.issafeinteger": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/lodash.issafeinteger/-/lodash.issafeinteger-4.0.4.tgz", @@ -3689,6 +4060,54 @@ "walker": "1.0.7" } }, + "machinepack-git": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/machinepack-git/-/machinepack-git-2.2.0.tgz", + "integrity": "sha1-ahYf8Z3+npQgujDovkpP4wEBods=", + "requires": { + "machine": "^10.3.1", + "machinepack-process": "^1.2.0" + }, + "dependencies": { + "lodash": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", + "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=" + }, + "machine": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/machine/-/machine-10.4.0.tgz", + "integrity": "sha1-m1Ys5GeCEzKCijd9GQ65NrTkB7I=", + "requires": { + "convert-to-ecmascript-compatible-varname": "^0.1.0", + "debug": "^2.1.1", + "lodash": "^3.8.0", + "object-hash": "~0.3.0", + "rttc": "^7.2.1", + "switchback": "^2.0.0" + } + }, + "machinepack-process": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/machinepack-process/-/machinepack-process-1.4.0.tgz", + "integrity": "sha1-MDE++2I+pO5Hc4a1gKhcpGI2Ymg=", + "requires": { + "lodash": "^3.9.2", + "machine": "^10.3.1", + "machinepack-json": "^1.1.0", + "open": "0.0.5" + } + }, + "rttc": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/rttc/-/rttc-7.4.0.tgz", + "integrity": "sha1-vJys1Grdkj3rYklaAZNOt+9hn7Q=", + "requires": { + "lodash": "^3.8.0" + } + } + } + }, "machinepack-http": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/machinepack-http/-/machinepack-http-8.0.0.tgz", @@ -3701,6 +4120,124 @@ "rttc": "^10.0.1" } }, + "machinepack-json": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/machinepack-json/-/machinepack-json-1.2.1.tgz", + "integrity": "sha1-lRj+7w6LYD01e4ExLBhM3Yv33gU=", + "requires": { + "lodash": "^3.9.2", + "machine": "^11.0.1", + "rttc": "^8.1.5" + }, + "dependencies": { + "lodash": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", + "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=" + }, + "machine": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/machine/-/machine-11.0.3.tgz", + "integrity": "sha1-a0fMYVgGr+D+0CquYf9K72oREOk=", + "requires": { + "convert-to-ecmascript-compatible-varname": "^0.1.0", + "debug": "^2.1.1", + "lodash": "^3.8.0", + "object-hash": "~0.3.0", + "rttc": "^8.1.5", + "switchback": "^2.0.0" + } + }, + "rttc": { + "version": "8.1.7", + "resolved": "https://registry.npmjs.org/rttc/-/rttc-8.1.7.tgz", + "integrity": "sha1-ODksjpA5beVdgbnxjR/2D3RDAgw=", + "requires": { + "lodash": "^3.8.0" + } + } + } + }, + "machinepack-markdown": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/machinepack-markdown/-/machinepack-markdown-1.1.0.tgz", + "integrity": "sha1-lACobKM1wS9gGGU25Dfhgy2oZEg=", + "requires": { + "async": "~0.9.0", + "fs-extra": "~0.10.0", + "lodash": "~2.4.1", + "machine": "^10.3.1", + "marked": "~0.3.2" + }, + "dependencies": { + "async": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz", + "integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=" + }, + "fs-extra": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.10.0.tgz", + "integrity": "sha1-mcDsL9XqqtnWRiReSwFLVnKZgq8=", + "requires": { + "jsonfile": "^1.2.0", + "mkdirp": "^0.5.0", + "ncp": "^0.5.1", + "rimraf": "^2.2.8" + } + }, + "jsonfile": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-1.2.0.tgz", + "integrity": "sha1-XzXWzZ+Ubsl7Wxg1P6nljfG4b2o=" + }, + "lodash": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=" + }, + "machine": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/machine/-/machine-10.4.0.tgz", + "integrity": "sha1-m1Ys5GeCEzKCijd9GQ65NrTkB7I=", + "requires": { + "convert-to-ecmascript-compatible-varname": "^0.1.0", + "debug": "^2.1.1", + "lodash": "^3.8.0", + "object-hash": "~0.3.0", + "rttc": "^7.2.1", + "switchback": "^2.0.0" + }, + "dependencies": { + "lodash": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", + "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=" + } + } + }, + "ncp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/ncp/-/ncp-0.5.1.tgz", + "integrity": "sha1-dDmFMW49tFkoG1hxaehFc1oFQ58=" + }, + "rttc": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/rttc/-/rttc-7.4.0.tgz", + "integrity": "sha1-vJys1Grdkj3rYklaAZNOt+9hn7Q=", + "requires": { + "lodash": "^3.8.0" + }, + "dependencies": { + "lodash": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", + "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=" + } + } + } + } + }, "machinepack-postgresql": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/machinepack-postgresql/-/machinepack-postgresql-1.0.2.tgz", @@ -3816,6 +4353,11 @@ "object-visit": "^1.0.0" } }, + "marked": { + "version": "0.3.19", + "resolved": "https://registry.npmjs.org/marked/-/marked-0.3.19.tgz", + "integrity": "sha512-ea2eGWOqNxPcXv8dyERdSr/6FmzvWwzjMxpfGB/sbMccXoct+xY+YukPD+QTUZwyvK7BZwcr4m21WBOW41pAkg==" + }, "math-random": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.4.tgz", @@ -4132,6 +4674,11 @@ } } }, + "object-hash": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-0.3.0.tgz", + "integrity": "sha1-VIII5Ds2pE5NowutbFasU7iF50Q=" + }, "object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", @@ -4210,6 +4757,11 @@ "mimic-fn": "^1.0.0" } }, + "open": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/open/-/open-0.0.5.tgz", + "integrity": "sha1-QsPhjslUZra/DcQvOilFw/DK2Pw=" + }, "opn": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/opn/-/opn-5.3.0.tgz", diff --git a/website/package.json b/website/package.json index 0792d77840..d4422f9f90 100644 --- a/website/package.json +++ b/website/package.json @@ -16,6 +16,8 @@ "sails-postgresql": "^1.0.2" }, "devDependencies": { + "cheerio": "0.18.0", + "doc-templater": "^0.4.0", "eslint": "5.16.0", "grunt": "1.0.4", "htmlhint": "0.11.0", @@ -24,9 +26,9 @@ }, "scripts": { "custom-tests": "echo \"(No other custom tests yet.)\" && echo", - "build-for-prod": "echo 'Now building+minifying assets for production...' && echo '(Hang tight, this could take a while.)' && echo && node node_modules/grunt/bin/grunt buildProd || (echo && echo '------------------------------------------' && echo 'IMPORTANT! IMPORTANT! IMPORTANT!' && echo 'ERROR: Could not compile assets for production!' && echo && echo 'Please fix the issues logged above' && echo 'and push that up. Then, try deploying again.' && echo '------------------------------------------' && echo) && mv www .www && node -e 'sailsrc = JSON.parse(require(\"fs\").readFileSync(\"./.sailsrc\", \"utf8\")); if (sailsrc.paths&&sailsrc.paths.public !== undefined || sailsrc.hooks&&sailsrc.hooks.grunt !== undefined) { throw new Error(\"Cannot complete deployment script: .sailsrc file has conflicting contents! Please remove the conflicting stuff from .sailsrc, then commit and push that up.\"); } sailsrc.paths = sailsrc.paths || {}; sailsrc.paths.public = \"./.www\"; sailsrc.hooks = sailsrc.hooks || {}; sailsrc.hooks.grunt = false; require(\"fs\").writeFileSync(\"./.sailsrc\", JSON.stringify(sailsrc))' && echo 'Build is complete. Ready to deploy.'", + "build-for-prod": "echo 'Now compiling markdown content and building+minifying assets for production...' && echo '(Hang tight, this could take a while.)' && echo && ./node_modules/sails/bin/sails.js run build-from-markdown && echo && node node_modules/grunt/bin/grunt buildProd || (echo && echo '------------------------------------------' && echo 'IMPORTANT! IMPORTANT! IMPORTANT!' && echo 'ERROR: Could not compile assets for production!' && echo && echo 'Please fix the issues logged above' && echo 'and push that up. Then, try deploying again.' && echo '------------------------------------------' && echo) && mv www .www && node -e 'sailsrc = JSON.parse(require(\"fs\").readFileSync(\"./.sailsrc\", \"utf8\")); if (sailsrc.paths&&sailsrc.paths.public !== undefined || sailsrc.hooks&&sailsrc.hooks.grunt !== undefined) { throw new Error(\"Cannot complete deployment script: .sailsrc file has conflicting contents! Please remove the conflicting stuff from .sailsrc, then commit and push that up.\"); } sailsrc.paths = sailsrc.paths || {}; sailsrc.paths.public = \"./.www\"; sailsrc.hooks = sailsrc.hooks || {}; sailsrc.hooks.grunt = false; require(\"fs\").writeFileSync(\"./.sailsrc\", JSON.stringify(sailsrc))' && echo 'Build is complete. Ready to deploy.'", "build": "echo '\"npm run build\" deliberately left unimplemented to prevent its use, since different platforms like Heroku and GitHub Actions all like to try and run it by default, which can lead to inadvertent duplication and unnecessary lock-in, since one has to find the config to turn that off.'", - "lint": "./node_modules/eslint/bin/eslint.js . --max-warnings=0 --report-unused-disable-directives && echo '✔ Your .js files look so good.' && ./node_modules/htmlhint/bin/htmlhint -c ./.htmlhintrc views/*.ejs && ./node_modules/htmlhint/bin/htmlhint -c ./.htmlhintrc views/**/*.ejs && ./node_modules/htmlhint/bin/htmlhint -c ./.htmlhintrc views/**/**/*.ejs && ./node_modules/htmlhint/bin/htmlhint -c ./.htmlhintrc views/**/**/**/*.ejs && ./node_modules/htmlhint/bin/htmlhint -c ./.htmlhintrc views/**/**/**/**/*.ejs && ./node_modules/htmlhint/bin/htmlhint -c ./.htmlhintrc views/**/**/**/**/**/*.ejs && ./node_modules/htmlhint/bin/htmlhint -c ./.htmlhintrc views/**/**/**/**/**/**/*.ejs && echo '✔ So do your .ejs files.' && ./node_modules/lesshint/bin/lesshint assets/styles/ --max-warnings=0 && echo '✔ Your .less files look good, too.'", + "lint": "./node_modules/eslint/bin/eslint.js . --max-warnings=0 --report-unused-disable-directives && echo '✔ Your .js files look so good.' && ./node_modules/htmlhint/bin/htmlhint -c ./.htmlhintrc views/*.ejs && ./node_modules/htmlhint/bin/htmlhint -c ./.htmlhintrc views/**/*.ejs && ./node_modules/htmlhint/bin/htmlhint -c ./.htmlhintrc views/**/**/*.ejs && ./node_modules/htmlhint/bin/htmlhint -c ./.htmlhintrc views/pages/**/**/*.ejs && ./node_modules/htmlhint/bin/htmlhint -c ./.htmlhintrc views/pages/**/**/**/*.ejs && ./node_modules/htmlhint/bin/htmlhint -c ./.htmlhintrc views/pages/**/**/**/**/*.ejs && ./node_modules/htmlhint/bin/htmlhint -c ./.htmlhintrc views/pages/**/**/**/**/**/*.ejs && echo '✔ So do your .ejs files.' && ./node_modules/lesshint/bin/lesshint assets/styles/ --max-warnings=0 && echo '✔ Your .less files look good, too.'", "start": "NODE_ENV=production node app.js", "test": "npm run lint && npm run custom-tests && echo 'Done.'", "wipe-staging": "sails_datastores__default__adapter=sails-postgresql sails_datastores__default__ssl=true sails_datastores__default__url='REAL_DB_URI_HERE' sails lift --drop" diff --git a/website/scripts/build-from-markdown.js b/website/scripts/build-from-markdown.js new file mode 100644 index 0000000000..bafcc76135 --- /dev/null +++ b/website/scripts/build-from-markdown.js @@ -0,0 +1,44 @@ +module.exports = { + + + friendlyName: 'Build from markdown', + + + description: 'Generate HTML partials from markdown content in fleetdm/fleet repo, and configure metadata about the generate files so it is available in `sails.config.builtFromMarkdown`.', + + + fn: async function () { + + let path = require('path'); + + let filesGeneratedBySection = { + documentation: await sails.helpers.compileMarkdownContent('docs/'), + queryLibrary: await sails.helpers.compileMarkdownContent('handbook/queries/') + }; + // FUTURE: Make this work in parallel as shown here by improving doctemplater to avoid the alreadyExists error + // (this actually only fails the very first time, but still, thinking is that it's not worth leaving a hack in + // here for a trivial build perf boost right now, especially since it only affects website deploys) + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + // let filesGeneratedBySection = await sails.helpers.flow.simultaneously({ + // documentation: async() => await sails.helpers.compileMarkdownContent('docs/'), + // queryLibrary: async() => await sails.helpers.compileMarkdownContent('handbook/queries/') + // }); + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + // console.log(filesGeneratedBySection); + + // Compile and generate XML sitemap + // (see "refresh" action in sailsjs.com repo for example) + // TODO + + // Now take the compiled menu file and inject it into the .sailsrc file so it + // can be accessed for the purposes of config using `sails.config.builtFromMarkdown`. + let sailsrcPath = path.resolve(sails.config.appPath, '.sailsrc'); + let sailsrcContent = await sails.helpers.fs.readJson(sailsrcPath); + sailsrcContent.builtFromMarkdown = filesGeneratedBySection; + await sails.helpers.fs.writeJson(sailsrcPath, sailsrcContent, true); + + } + + +}; + diff --git a/website/views/layouts/layout.ejs b/website/views/layouts/layout.ejs index dbe7c70c92..20248f1104 100644 --- a/website/views/layouts/layout.ejs +++ b/website/views/layouts/layout.ejs @@ -73,7 +73,9 @@