Amend policy creation and spec (for proprietary query), and add update APIs (#2890)

* Amend policy creation (proprietary query), add update APIs

* Fix Datastore.SavePolicy bug (and add tests)

* Add integration tests for new policy APIs

* Add author email

* Add activities

* Push breaking changes for return policy fields

* WIP

* Add integration test for host policies

* Make more improvements to policy representation

* Improve upgrade code (from PR review comments)

* PR changes

* Revert activities for policies

* Use *uint instead of uint for queryID, use fleet.PolicyPayload

* Filter out other schemas

* New policy flow (#2922)

* created new policy flow -- no API connection

* added api props

* fixed prop name

* lint fixes

* removed unused modal; fixed style

* name, desc icons; created global components

* lint fixes

* ignoring certain files and lines for prettier

* Update frontend/pages/policies/PolicyPage/PolicyPage.tsx

* Make policy names unique across deployment

* Amend upgrade script

* Fix migration for unique names

* Do not deduplicate but instead rename policies

Co-authored-by: Martavis Parker <47053705+martavis@users.noreply.github.com>
This commit is contained in:
Lucas Manuel Rodriguez 2021-11-24 14:16:42 -03:00 committed by GitHub
parent d10741bddb
commit 964f85b174
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
72 changed files with 4405 additions and 813 deletions

View file

@ -34,4 +34,8 @@ cypress/videos
cypress/downloads
# fleetdm.com website (uses its own formatting conventions)
website/
website/
# certain JS files that are not meant to be formatted
frontend/components/FleetAce/mode.ts
frontend/components/FleetAce/theme.ts

12
.vscode/launch.json vendored
View file

@ -14,7 +14,7 @@
"MYSQL_TEST": 1,
"REDIS_TEST": 1
},
"buildFlags": "--tags='full,fts5'",
"buildFlags": "-tags='full,fts5'",
"program": "${fileDirname}"
},
{
@ -27,7 +27,7 @@
"MYSQL_TEST": 1,
"REDIS_TEST": 1
},
"buildFlags": "--tags='full,fts5'",
"buildFlags": "-tags='full,fts5'",
"program": "${file}"
},
{
@ -35,12 +35,13 @@
"type": "go",
"request": "launch",
"mode": "auto",
"buildFlags": "--tags='full,fts5'",
"buildFlags": "-tags='full,fts5'",
"cwd": "${workspaceFolder}",
"program": "${workspaceFolder}/cmd/fleet",
"args": [
"serve",
"--dev"
"--dev",
"--logging_debug"
]
},
{
@ -48,12 +49,13 @@
"type": "go",
"request": "launch",
"mode": "auto",
"buildFlags": "--tags='full,fts5'",
"buildFlags": "-tags='full,fts5'",
"cwd": "${workspaceFolder}",
"program": "${workspaceFolder}/cmd/fleet",
"args": [
"serve",
"--dev",
"--logging_debug",
"--dev_license"
]
},

View file

@ -110,7 +110,7 @@ lint-js:
yarn lint
lint-go:
golangci-lint run
golangci-lint run --skip-dirs ./node_modules
lint: lint-go lint-js

View file

@ -0,0 +1,4 @@
* Policies now have proprietary queries and are managed independently from the saved queries.
* Amended policy creation API to support proprietary queries.
* New update/edit APIs for policies.
* Amended policy spec APIs to support the new proprietary queries.

View file

@ -91,7 +91,10 @@ spec:
}
func TestGetTeams(t *testing.T) {
expiredBanner := "Your license for Fleet Premium is about to expire. If youd like to renew or have questions about downgrading, please navigate to https://github.com/fleetdm/fleet/blob/main/docs/01-Using-Fleet/10-Teams.md#expired_license and contact us for help."
var expiredBanner strings.Builder
fleet.WriteExpiredLicenseBanner(&expiredBanner)
require.Contains(t, expiredBanner.String(), "Your license for Fleet Premium is about to expire")
testCases := []struct {
name string
license *fleet.LicenseInfo
@ -180,9 +183,9 @@ spec:
{"kind":"team","apiVersion":"v1","spec":{"team":{"id":43,"created_at":"1999-03-10T02:45:06.371Z","name":"team2","description":"team2 description","agent_options":{"config":{"foo":"bar"},"overrides":{"platforms":{"darwin":{"foo":"override"}}}},"user_count":87,"host_count":0}}}
`
if tt.shouldHaveExpiredBanner {
expectedJson = expiredBanner + "\n" + expectedJson
expectedYaml = expiredBanner + "\n" + expectedYaml
expectedText = expiredBanner + "\n" + expectedText
expectedJson = expiredBanner.String() + expectedJson
expectedYaml = expiredBanner.String() + expectedYaml
expectedText = expiredBanner.String() + expectedText
}
assert.Equal(t, expectedText, runAppForTest(t, []string{"get", "teams"}))
@ -244,7 +247,8 @@ func TestGetHosts(t *testing.T) {
LastEnrolledAt: time.Time{},
SeenTime: time.Time{},
ComputerName: "test_host",
Hostname: "test_host"}, nil
Hostname: "test_host",
}, nil
}
ds.LoadHostSoftwareFunc = func(ctx context.Context, host *fleet.Host) error {
@ -256,19 +260,36 @@ func TestGetHosts(t *testing.T) {
ds.ListPacksForHostFunc = func(ctx context.Context, hid uint) (packs []*fleet.Pack, err error) {
return make([]*fleet.Pack, 0), nil
}
defaultPolicyQuery := "select 1 from osquery_info where start_time > 1;"
ds.ListPoliciesForHostFunc = func(ctx context.Context, hid uint) ([]*fleet.HostPolicy, error) {
return []*fleet.HostPolicy{
{
ID: 1,
QueryID: 2,
QueryName: "query1",
Response: "passes",
PolicyData: fleet.PolicyData{
ID: 1,
Name: "query1",
Query: defaultPolicyQuery,
Description: "Some description",
AuthorID: ptr.Uint(1),
AuthorName: "Alice",
AuthorEmail: "alice@example.com",
Resolution: ptr.String("Some resolution"),
TeamID: ptr.Uint(1),
},
Response: "passes",
},
{
ID: 2,
QueryID: 43,
QueryName: "query2",
Response: "fails",
PolicyData: fleet.PolicyData{
ID: 2,
Name: "query2",
Query: defaultPolicyQuery,
Description: "",
AuthorID: ptr.Uint(1),
AuthorName: "Alice",
AuthorEmail: "alice@example.com",
Resolution: nil,
TeamID: nil,
},
Response: "fails",
},
}, nil
}

View file

@ -720,7 +720,7 @@ func loadPolicies(client *service.Client) error {
if err != nil {
return fmt.Errorf("creating query: %w", err)
}
err = client.CreatePolicy(q.ID, policy.resolution)
err = client.CreatePolicy(&q.ID, policy.resolution)
if err != nil {
return fmt.Errorf("creating policy: %w", err)
}

View file

@ -54,19 +54,30 @@
"policies":[
{
"id":1,
"query_id":2,
"query_name":"query1",
"query_description":"",
"query":"select 1 from osquery_info where start_time > 1;",
"name":"query1",
"description":"Some description",
"author_email":"alice@example.com",
"author_id":1,
"author_name":"Alice",
"response":"passes",
"resolution":""
"resolution":"Some resolution",
"team_id": 1,
"updated_at":"0001-01-01T00:00:00Z",
"created_at":"0001-01-01T00:00:00Z"
},
{
"id":2,
"query_id":43,
"query_name":"query2",
"query_description":"",
"query":"select 1 from osquery_info where start_time > 1;",
"name":"query2",
"description":"",
"author_email":"alice@example.com",
"author_id":1,
"author_name":"Alice",
"response":"fails",
"resolution":""
"team_id":null,
"updated_at":"0001-01-01T00:00:00Z",
"created_at":"0001-01-01T00:00:00Z"
}
],
"status":"mia",

View file

@ -37,18 +37,29 @@ spec:
platform: ""
platform_like: ""
policies:
- id: 1
query_description: ""
query_id: 2
query_name: query1
resolution: ""
- author_email: "alice@example.com"
author_id: 1
author_name: Alice
id: 1
description: "Some description"
name: query1
query: select 1 from osquery_info where start_time > 1;
resolution: "Some resolution"
response: passes
- id: 2
query_description: ""
query_id: 43
query_name: query2
resolution: ""
team_id: 1
created_at: "0001-01-01T00:00:00Z"
updated_at: "0001-01-01T00:00:00Z"
- author_email: "alice@example.com"
author_id: 1
author_name: Alice
id: 2
description: ""
name: query2
query: select 1 from osquery_info where start_time > 1;
response: fails
team_id: null
created_at: "0001-01-01T00:00:00Z"
updated_at: "0001-01-01T00:00:00Z"
policy_updated_at: "0001-01-01T00:00:00Z"
primary_ip: ""
primary_mac: ""
@ -59,4 +70,4 @@ spec:
team_name: null
updated_at: "0001-01-01T00:00:00Z"
uptime: 0
uuid: ""
uuid: ""

View file

@ -796,25 +796,25 @@ If the scheduled queries haven't run on the host yet, the stats have zero values
"policies": [
{
"id": 1,
"query_id": 2,
"query_name": "SomeQuery",
"query_description": "this is a query",
"name": "SomeQuery",
"query": "select * from foo;",
"description": "this is a query",
"resolution": "fix with these steps...",
"response": "pass"
},
{
"id": 2,
"query_id": 4,
"query_name": "SomeQuery2",
"query_description": "this is another query",
"name": "SomeQuery2",
"query": "select * from bar;",
"description": "this is another query",
"resolution": "fix with these other steps...",
"response": "fail"
},
{
"id": 3,
"query_id": 255,
"query_name": "SomeQuery3",
"query_description": "",
"name": "SomeQuery3",
"query": "select * from baz;",
"description": "",
"resolution": "",
"response": ""
}
@ -3402,6 +3402,7 @@ This allows you to easily configure scheduled queries that will impact a whole t
- [Get policy by ID](#get-policy-by-id)
- [Add policy](#add-policy)
- [Remove policies](#remove-policies)
- [Edit policy](#edit-policy)
`In Fleet 4.3.0, the Policies feature was introduced.`
@ -3434,16 +3435,24 @@ For example, a policy might ask “Is Gatekeeper enabled on macOS devices?“ Th
"policies": [
{
"id": 1,
"query_id": 2,
"query_name": "Gatekeeper enabled",
"name": "Gatekeeper enabled",
"query": "SELECT 1 FROM gatekeeper WHERE assessments_enabled = 1;",
"description": "Checks if gatekeeper is enabled on macOS devices",
"author_id": 42,
"author_name": "John",
"author_email": "john@example.com",
"resolution": "Resolution steps",
"passing_host_count": 2000,
"failing_host_count": 300
},
{
"id": 2,
"query_id": 3,
"query_name": "Primary disk encrypted",
"name": "Windows machines with encrypted hard disks",
"query": "SELECT 1 FROM bitlocker_info WHERE protection_status = 1;",
"description": "Checks if the hard disk is encrypted on Windows devices",
"author_id": 43,
"author_name": "Alice",
"author_email": "alice@example.com",
"passing_host_count": 2300,
"failing_host_count": 0
}
@ -3473,8 +3482,12 @@ For example, a policy might ask “Is Gatekeeper enabled on macOS devices?“ Th
{
"policy": {
"id": 1,
"query_id": 2,
"query_name": "Gatekeeper enabled",
"name": "Gatekeeper enabled",
"query": "SELECT 1 FROM gatekeeper WHERE assessments_enabled = 1;",
"description": "Checks if gatekeeper is enabled on macOS devices",
"author_id": 42,
"author_name": "John",
"author_email": "john@example.com",
"resolution": "Resolution steps",
"passing_host_count": 2000,
"failing_host_count": 300
@ -3484,16 +3497,64 @@ For example, a policy might ask “Is Gatekeeper enabled on macOS devices?“ Th
### Add policy
There are two ways of adding a policy:
1. by setting "name", "query", "description". This is the preferred way.
2. (Legacy) re-using the data of an existing query, by setting "query_id". If "query_id" is set,
then "query" must not be set, and "name" and "description" are ignored.
An error is returned if both "query" and "query_id" are set on the request.
`POST /api/v1/fleet/global/policies`
#### Parameters
| Name | Type | In | Description |
| ---------- | ------- | ---- | ------------------------------------- |
| query_id | integer | body | **Required.** The query's ID. |
| resolution | string | body | The resolution steps for the policy. |
| Name | Type | In | Description |
| ---------- | ------- | ---- | ------------------------------------ |
| name | string | body | The query's name. |
| query | string | body | The query in SQL. |
| description | string | body | The query's description. |
| resolution | string | body | The resolution steps for the policy. |
| query_id | integer | body | An existing query's ID (legacy). |
#### Example
Either `query` or `query_id` must be provided.
#### Example Add Policy
`POST /api/v1/fleet/global/policies`
#### Request body
```json
{
"name": "Gatekeeper enabled",
"query": "SELECT 1 FROM gatekeeper WHERE assessments_enabled = 1;",
"description": "Checks if gatekeeper is enabled on macOS devices",
"resolution": "Resolution steps"
}
```
##### Default response
`Status: 200`
```json
{
"policy": {
"id": 43,
"name": "Gatekeeper enabled",
"query": "SELECT 1 FROM gatekeeper WHERE assessments_enabled = 1;",
"description": "Checks if gatekeeper is enabled on macOS devices",
"author_id": 42,
"author_name": "John",
"author_email": "john@example.com",
"resolution": "Resolution steps",
"passing_host_count": 0,
"failing_host_count": 0
}
}
```
#### Example Legacy Add Policy
`POST /api/v1/fleet/global/policies`
@ -3505,6 +3566,8 @@ For example, a policy might ask “Is Gatekeeper enabled on macOS devices?“ Th
}
```
Where `query_id` references an existing `query`.
##### Default response
`Status: 200`
@ -3512,13 +3575,17 @@ For example, a policy might ask “Is Gatekeeper enabled on macOS devices?“ Th
```json
{
"policy": {
"id": 2,
"query_id": 2,
"query_name": "Primary disk encrypted",
"resolution": "Some resolution steps",
"passing_host_count": 0,
"failing_host_count": 0
}
"id": 43,
"name": "Gatekeeper enabled",
"query": "SELECT 1 FROM gatekeeper WHERE assessments_enabled = 1;",
"description": "Checks if gatekeeper is enabled on macOS devices",
"author_id": 42,
"author_name": "John",
"author_email": "john@example.com",
"resolution": "Resolution steps",
"passing_host_count": 0,
"failing_host_count": 0
}
}
```
@ -3554,6 +3621,56 @@ For example, a policy might ask “Is Gatekeeper enabled on macOS devices?“ Th
}
```
### Edit policy
`PATCH /api/v1/fleet/global/policies/{policy_id}`
#### Parameters
| Name | Type | In | Description |
| ---------- | ------- | ---- | ------------------------------------ |
| id | integer | path | The policy's ID. |
| name | string | body | The query's name. |
| query | string | body | The query in SQL. |
| description | string | body | The query's description. |
| resolution | string | body | The resolution steps for the policy. |
#### Example Edit Policy
`PATCH /api/v1/fleet/global/policies/42`
##### Request body
```json
{
"name": "Gatekeeper enabled",
"query": "SELECT 1 FROM gatekeeper WHERE assessments_enabled = 1;",
"description": "Checks if gatekeeper is enabled on macOS devices",
"resolution": "Resolution steps"
}
```
##### Default response
`Status: 200`
```json
{
"policy": {
"id": 42,
"name": "Gatekeeper enabled",
"query": "SELECT 1 FROM gatekeeper WHERE assessments_enabled = 1;",
"description": "Checks if gatekeeper is enabled on macOS devices",
"author_id": 43,
"author_name": "John",
"author_email": "john@example.com",
"resolution": "Resolution steps",
"passing_host_count": 0,
"failing_host_count": 0
}
}
```
---
## Team Policies
@ -3562,6 +3679,7 @@ For example, a policy might ask “Is Gatekeeper enabled on macOS devices?“ Th
- [Get team policy by ID](#get-team-policy-by-id)
- [Add team policy](#add-team-policy)
- [Remove team policies](#remove-team-policies)
- [Edit team policy](#edit-team-policy)
_Available in Fleet Premium_
@ -3585,22 +3703,33 @@ Team policies work the same as policies, but at the team level.
`Status: 200`
```
```json
{
"policies": [
{
"id": 1,
"query_id": 2,
"query_name": "Gatekeeper enabled",
"name": "Gatekeeper enabled",
"query": "SELECT 1 FROM gatekeeper WHERE assessments_enabled = 1;",
"description": "Checks if gatekeeper is enabled on macOS devices",
"author_id": 42,
"author_name": "John",
"author_email": "john@example.com",
"team_id": 1,
"resolution": "Resolution steps",
"passing_host_count": 2000,
"failing_host_count": 300,
"failing_host_count": 300
},
{
"id": 2,
"query_id": 3,
"query_name": "Primary disk encrypted",
"name": "Windows machines with encrypted hard disks",
"query": "SELECT 1 FROM bitlocker_info WHERE protection_status = 1;",
"description": "Checks if the hard disk is encrypted on Windows devices",
"author_id": 43,
"author_name": "Alice",
"author_email": "alice@example.com",
"team_id": 1,
"passing_host_count": 2300,
"failing_host_count": 0,
"failing_host_count": 0
}
]
}
@ -3619,44 +3748,61 @@ Team policies work the same as policies, but at the team level.
#### Example
`GET /api/v1/fleet/teams/1/policies/1`
`GET /api/v1/fleet/teams/1/policies/43`
##### Default response
`Status: 200`
```
```json
{
"policy": {
"id": 1,
"query_id": 2,
"query_name": "Gatekeeper enabled",
"passing_host_count": 2000,
"failing_host_count": 300,
"id": 43,
"name": "Gatekeeper enabled",
"query": "SELECT 1 FROM gatekeeper WHERE assessments_enabled = 1;",
"description": "Checks if gatekeeper is enabled on macOS devices",
"author_id": 42,
"author_name": "John",
"author_email": "john@example.com",
"team_id": 1,
"resolution": "Resolution steps",
"passing_host_count": 0,
"failing_host_count": 0
}
}
```
### Add team policy
The semantics for creating a team policy are the same as for global policies, see [Add policy](#add-policy).
`POST /api/v1/fleet/teams/{team_id}/policies`
#### Parameters
| Name | Type | In | Description |
| -------- | ------- | ---- | ----------------------------------- |
| team_id | integer | url | Defines what team id to operate on |
| query_id | integer | body | **Required.** The query's ID. |
| Name | Type | In | Description |
| ---------- | ------- | ---- | ------------------------------------ |
| team_id | integer | url | Defines what team id to operate on. |
| name | string | body | The query's name. |
| query | string | body | The query in SQL. |
| description | string | body | The query's description. |
| resolution | string | body | The resolution steps for the policy. |
| query_id | integer | body | An existing query's ID (legacy). |
Either `query` or `query_id` must be provided.
#### Example
`POST /api/v1/fleet/teams/1/policies`
#### Request body
##### Request body
```
```json
{
"query_id": 12
"name": "Gatekeeper enabled",
"query": "SELECT 1 FROM gatekeeper WHERE assessments_enabled = 1;",
"description": "Checks if gatekeeper is enabled on macOS devices",
"resolution": "Resolution steps"
}
```
@ -3664,15 +3810,21 @@ Team policies work the same as policies, but at the team level.
`Status: 200`
```
```json
{
"policy": {
"id": 2,
"query_id": 2,
"query_name": "Primary disk encrypted",
"passing_host_count": 0,
"failing_host_count": 0,
},
"id": 43,
"name": "Gatekeeper enabled",
"query": "SELECT 1 FROM gatekeeper WHERE assessments_enabled = 1;",
"description": "Checks if gatekeeper is enabled on macOS devices",
"author_id": 42,
"author_name": "John",
"author_email": "john@example.com",
"team_id": 1,
"resolution": "Resolution steps",
"passing_host_count": 0,
"failing_host_count": 0
}
}
```
@ -3691,9 +3843,9 @@ Team policies work the same as policies, but at the team level.
`POST /api/v1/fleet/teams/1/policies/delete`
#### Request body
##### Request body
```
```json
{
"ids": [ 1 ]
}
@ -3703,12 +3855,64 @@ Team policies work the same as policies, but at the team level.
`Status: 200`
```
```json
{
"deleted": 1
}
```
### Edit team policy
`PATCH /api/v1/fleet/teams/{team_id}/policies/{policy_id}`
#### Parameters
| Name | Type | In | Description |
| ---------- | ------- | ---- | ------------------------------------ |
| team_id | integer | path | The team's ID. |
| policy_id | integer | path | The policy's ID. |
| name | string | body | The query's name. |
| query | string | body | The query in SQL. |
| description | string | body | The query's description. |
| resolution | string | body | The resolution steps for the policy. |
#### Example Edit Policy
`PATCH /api/v1/fleet/teams/2/policies/42`
##### Request body
```json
{
"name": "Gatekeeper enabled",
"query": "SELECT 1 FROM gatekeeper WHERE assessments_enabled = 1;",
"description": "Checks if gatekeeper is enabled on macOS devices",
"resolution": "Resolution steps"
}
```
##### Default response
`Status: 200`
```json
{
"policy": {
"id": 42,
"name": "Gatekeeper enabled",
"query": "SELECT 1 FROM gatekeeper WHERE assessments_enabled = 1;",
"description": "Checks if gatekeeper is enabled on macOS devices",
"author_id": 43,
"author_name": "John",
"author_email": "john@example.com",
"resolution": "Resolution steps",
"team_id": 2,
"passing_host_count": 0,
"failing_host_count": 0
}
}
```
---
## Activities

View file

@ -12,6 +12,7 @@ import { IUser } from "interfaces/user";
import { IConfig } from "interfaces/config";
import TableProvider from "context/table";
import QueryProvider from "context/query";
import PolicyProvider from "context/policy";
import { AppContext } from "context/app";
import { IEnrollSecret } from "interfaces/enroll_secret";
import FleetErrorBoundary from "pages/errors/FleetErrorBoundary";
@ -85,9 +86,11 @@ const App = ({ children }: IAppProps) => {
<QueryClientProvider client={queryClient}>
<TableProvider>
<QueryProvider>
<FleetErrorBoundary>
<div className={wrapperStyles}>{children}</div>
</FleetErrorBoundary>
<PolicyProvider>
<FleetErrorBoundary>
<div className={wrapperStyles}>{children}</div>
</FleetErrorBoundary>
</PolicyProvider>
</QueryProvider>
</TableProvider>
</QueryClientProvider>

View file

@ -17,7 +17,7 @@ import InputField from "components/forms/fields/InputField";
import QueryResultsRow from "components/queries/QueryResultsRow";
import Spinner from "components/Spinner";
import TabsWrapper from "components/TabsWrapper";
import DownloadIcon from "../../../../../../assets/images/icon-download-12x12@2x.png";
import DownloadIcon from "../../../assets/images/icon-download-12x12@2x.png";
interface IQueryResultsProps {
campaign: ICampaign;

View file

@ -9,7 +9,7 @@ import { filter, isEmpty, pullAllBy } from "lodash";
import Input from "components/forms/fields/InputFieldWithIcon";
import TableContainer from "components/TableContainer";
import { generateTableHeaders } from "./TargetsInputHostsTableConfig";
import ExternalURLIcon from "../../../../../../assets/images/icon-external-url-12x12@2x.png";
import ExternalURLIcon from "../../../assets/images/icon-external-url-12x12@2x.png";
interface ITargetsInputProps {
tabIndex: number;

View file

@ -1,24 +1,14 @@
/* eslint-disable react/prop-types */
import React from "react";
import { Cell, UseRowSelectInstanceProps } from "react-table";
import { Cell } from "react-table";
import { IDataColumn } from "interfaces/datatable_config";
// @ts-ignore
import Checkbox from "components/forms/fields/Checkbox";
import TextCell from "components/TableContainer/DataTable/TextCell";
import StatusCell from "components/TableContainer/DataTable/StatusCell/StatusCell";
import RemoveIcon from "../../../../../../assets/images/icon-action-remove-20x20@2x.png";
interface ITargetHostsTableData {
hostname: string;
status: string;
primary_ip: string;
primary_mac: string;
os_version: string;
osquery_version: string;
}
import RemoveIcon from "../../../assets/images/icon-action-remove-20x20@2x.png";
// NOTE: cellProps come from react-table
// more info here https://react-table.tanstack.com/docs/api/useTable#cell-properties

106
frontend/context/policy.tsx Normal file
View file

@ -0,0 +1,106 @@
import React, { createContext, useReducer, ReactNode } from "react";
import { find } from "lodash";
// @ts-ignore
import { osqueryTables } from "utilities/osquery_tables";
import { DEFAULT_POLICY } from "utilities/constants";
import { IOsqueryTable } from "interfaces/osquery_table";
type Props = {
children: ReactNode;
};
type InitialStateType = {
selectedOsqueryTable: IOsqueryTable;
lastEditedQueryName: string;
lastEditedQueryDescription: string;
lastEditedQueryBody: string;
setLastEditedQueryName: (value: string) => void;
setLastEditedQueryDescription: (value: string) => void;
setLastEditedQueryBody: (value: string) => void;
setSelectedOsqueryTable: (tableName: string) => void;
};
const initialState = {
selectedOsqueryTable: find(osqueryTables, { name: "users" }),
lastEditedQueryName: DEFAULT_POLICY.name,
lastEditedQueryDescription: DEFAULT_POLICY.description,
lastEditedQueryBody: DEFAULT_POLICY.query,
setLastEditedQueryName: () => null,
setLastEditedQueryDescription: () => null,
setLastEditedQueryBody: () => null,
setSelectedOsqueryTable: () => null,
};
const actions = {
SET_SELECTED_OSQUERY_TABLE: "SET_SELECTED_OSQUERY_TABLE",
SET_LAST_EDITED_QUERY_INFO: "SET_LAST_EDITED_QUERY_INFO",
};
const reducer = (state: any, action: any) => {
switch (action.type) {
case actions.SET_SELECTED_OSQUERY_TABLE:
return {
...state,
selectedOsqueryTable: find(osqueryTables, { name: action.tableName }),
};
case actions.SET_LAST_EDITED_QUERY_INFO:
return {
...state,
lastEditedQueryName:
typeof action.lastEditedQueryName === "undefined"
? state.lastEditedQueryName
: action.lastEditedQueryName,
lastEditedQueryDescription:
typeof action.lastEditedQueryDescription === "undefined"
? state.lastEditedQueryDescription
: action.lastEditedQueryDescription,
lastEditedQueryBody:
typeof action.lastEditedQueryBody === "undefined"
? state.lastEditedQueryBody
: action.lastEditedQueryBody,
};
default:
return state;
}
};
export const PolicyContext = createContext<InitialStateType>(initialState);
const PolicyProvider = ({ children }: Props) => {
const [state, dispatch] = useReducer(reducer, initialState);
const value = {
selectedOsqueryTable: state.selectedOsqueryTable,
lastEditedQueryName: state.lastEditedQueryName,
lastEditedQueryDescription: state.lastEditedQueryDescription,
lastEditedQueryBody: state.lastEditedQueryBody,
setLastEditedQueryName: (lastEditedQueryName: string) => {
dispatch({
type: actions.SET_LAST_EDITED_QUERY_INFO,
lastEditedQueryName,
});
},
setLastEditedQueryDescription: (lastEditedQueryDescription: string) => {
dispatch({
type: actions.SET_LAST_EDITED_QUERY_INFO,
lastEditedQueryDescription,
});
},
setLastEditedQueryBody: (lastEditedQueryBody: string) => {
dispatch({
type: actions.SET_LAST_EDITED_QUERY_INFO,
lastEditedQueryBody,
});
},
setSelectedOsqueryTable: (tableName: string) => {
dispatch({ type: actions.SET_SELECTED_OSQUERY_TABLE, tableName });
},
};
return (
<PolicyContext.Provider value={value}>{children}</PolicyContext.Provider>
);
};
export default PolicyProvider;

View file

@ -1,9 +1,19 @@
export interface IPolicy {
id: number;
query_id: number;
query_name: string;
team_id: number;
name: string;
query: string;
description: string;
author_id: number;
author_name: string;
author_email: string;
resolution: string;
passing_host_count: number;
failing_host_count: number;
last_run_time?: string;
team_id?: number;
}
export interface IPolicyFormData {
description?: string | number | boolean | any[] | undefined;
name?: string | number | boolean | any[] | undefined;
query?: string | number | boolean | any[] | undefined;
}

View file

@ -1086,12 +1086,12 @@ const ManageHostsPage = ({
/>
<div className={`${baseClass}__policies-filter-name-card`}>
<img src={PolicyIcon} alt="Policy" />
{policy?.query_name}
{policy?.name}
<Button
className={`${baseClass}__clear-policies-filter`}
onClick={handleClearPoliciesFilter}
variant={"small-text-icon"}
title={policy?.query_name}
title={policy?.name}
>
<img src={CloseIcon} alt="Remove policy filter" />
</Button>

View file

@ -1,4 +1,5 @@
import React, { useCallback, useContext, useEffect, useState } from "react";
import { Link } from "react-router";
import { useQuery } from "react-query";
import { useDispatch } from "react-redux";
import { noop } from "lodash";
@ -28,7 +29,6 @@ import Button from "components/buttons/Button";
import InfoBanner from "components/InfoBanner/InfoBanner";
import IconToolTip from "components/IconToolTip";
import PoliciesListWrapper from "./components/PoliciesListWrapper";
import AddPolicyModal from "./components/AddPolicyModal";
import RemovePoliciesModal from "./components/RemovePoliciesModal";
import TeamsDropdown from "./components/TeamsDropdown";
@ -105,7 +105,6 @@ const ManagePolicyPage = (managePoliciesPageProps: {
const [selectedPolicyIds, setSelectedPolicyIds] = useState<
number[] | never[]
>([]);
const [showAddPolicyModal, setShowAddPolicyModal] = useState(false);
const [showRemovePoliciesModal, setShowRemovePoliciesModal] = useState(false);
const [showInheritedPolicies, setShowInheritedPolicies] = useState(false);
const [updateInterval, setUpdateInterval] = useState<string>(
@ -164,8 +163,6 @@ const ManagePolicyPage = (managePoliciesPageProps: {
setSelectedPolicyIds([]);
};
const toggleAddPolicyModal = () => setShowAddPolicyModal(!showAddPolicyModal);
const toggleRemovePoliciesModal = () =>
setShowRemovePoliciesModal(!showRemovePoliciesModal);
@ -208,31 +205,6 @@ const ManagePolicyPage = (managePoliciesPageProps: {
}
};
const onAddPolicySubmit = async (query_id: number | undefined) => {
if (!query_id) {
dispatch(renderFlash("error", "Could not add policy. Please try again."));
return false;
}
try {
const request = selectedTeamId
? teamPoliciesAPI.create(selectedTeamId, query_id)
: globalPoliciesAPI.create(query_id);
await request.then(() => {
dispatch(renderFlash("success", `Successfully added policy.`));
});
} catch {
dispatch(renderFlash("error", "Could not add policy. Please try again."));
} finally {
toggleAddPolicyModal();
getPolicies(selectedTeamId);
}
return false;
};
// Sort list of teams the current user has permission to access and set as userTeams.
useEffect(() => {
if (isPremiumTier) {
@ -358,13 +330,12 @@ const ManagePolicyPage = (managePoliciesPageProps: {
</div>
{canAddOrRemovePolicy(currentUser, selectedTeamId) && (
<div className={`${baseClass}__action-button-container`}>
<Button
variant="brand"
className={`${baseClass}__add-policy-button`}
onClick={toggleAddPolicyModal}
<Link
to={PATHS.NEW_POLICY}
className={`${baseClass}__add-policy-link`}
>
Add a policy
</Button>
</Link>
</div>
)}
</div>
@ -407,7 +378,6 @@ const ManagePolicyPage = (managePoliciesPageProps: {
policiesList={teamPolicies}
isLoading={isLoadingTeamPolicies}
onRemovePoliciesClick={onRemovePoliciesClick}
toggleAddPolicyModal={toggleAddPolicyModal}
canAddOrRemovePolicy={canAddOrRemovePolicy(
currentUser,
selectedTeamId
@ -423,7 +393,6 @@ const ManagePolicyPage = (managePoliciesPageProps: {
policiesList={globalPolicies}
isLoading={isLoadingGlobalPolicies}
onRemovePoliciesClick={onRemovePoliciesClick}
toggleAddPolicyModal={toggleAddPolicyModal}
canAddOrRemovePolicy={canAddOrRemovePolicy(
currentUser,
selectedTeamId
@ -463,7 +432,6 @@ const ManagePolicyPage = (managePoliciesPageProps: {
isLoading={isLoadingGlobalPolicies}
policiesList={globalPolicies}
onRemovePoliciesClick={noop}
toggleAddPolicyModal={noop}
resultsTitle="policies"
canAddOrRemovePolicy={canAddOrRemovePolicy(
currentUser,
@ -474,13 +442,6 @@ const ManagePolicyPage = (managePoliciesPageProps: {
/>
</div>
)}
{showAddPolicyModal && (
<AddPolicyModal
onCancel={toggleAddPolicyModal}
onSubmit={onAddPolicySubmit}
allQueries={fleetQueries}
/>
)}
{showRemovePoliciesModal && (
<RemovePoliciesModal
onCancel={toggleRemovePoliciesModal}

View file

@ -110,6 +110,34 @@
}
}
&__add-policy-link {
transition: color 150ms ease-in-out, background 150ms ease-in-out,
top 50ms ease-in-out, box-shadow 50ms ease-in-out, border 50ms ease-in-out;
position: relative;
color: $core-white;
text-decoration: none;
flex-direction: row;
justify-content: center;
align-items: center;
padding-left: $pad-medium;
padding-right: $pad-medium;
border-radius: 4px;
font-size: $x-small;
font-family: "Nunito Sans", sans-serif;
font-weight: $bold;
display: inline-flex;
height: 38px;
top: 0;
border: 0;
cursor: pointer;
@include button-variant(
$core-vibrant-blue,
$core-vibrant-blue-over,
$core-vibrant-blue-down
);
}
&__inherited-policies-button {
padding-top: $pad-small;
margin: $pad-large 0 0 0;

View file

@ -1,92 +0,0 @@
/* This component is used for creating policies */
import React, { useState, useCallback } from "react";
// @ts-ignore
import Modal from "components/Modal";
import Button from "components/buttons/Button";
import InfoBanner from "components/InfoBanner/InfoBanner";
// @ts-ignore
import Dropdown from "components/forms/fields/Dropdown";
import { IQuery } from "interfaces/query";
const baseClass = "add-policy-modal";
interface IAddPolicyModalProps {
allQueries: IQuery[];
onCancel: () => void;
onSubmit: (query_id: number | undefined) => void;
}
const AddPolicyModal = ({
onCancel,
onSubmit,
allQueries,
}: IAddPolicyModalProps): JSX.Element => {
const [selectedQuery, setSelectedQuery] = useState<number>();
const createQueryDropdownOptions = () => {
return allQueries.map(({ id, name }) => ({
value: id,
label: name,
}));
};
const onChangeSelectQuery = useCallback(
(queryId: number) => {
setSelectedQuery(queryId);
},
[setSelectedQuery]
);
return (
<Modal title={"Add a policy"} onExit={onCancel} className={baseClass}>
<form className={`${baseClass}__form`}>
<Dropdown
searchable
options={createQueryDropdownOptions()}
onChange={onChangeSelectQuery}
placeholder={"Select query"}
value={selectedQuery}
wrapperClassName={`${baseClass}__select-query-dropdown-wrapper`}
/>
<InfoBanner className={`${baseClass}__sandbox-info`}>
<p>
Host that return results for the selected query are <b>Passing</b>.
</p>
<p>
Hosts that do not return results for the selected query are{" "}
<b>Failing</b>.
</p>
<p>
To test which hosts return results, it is recommended to first run
your query as a live query by heading to <b>Queries</b> and then
selecting a query.
</p>
</InfoBanner>
<div className={`${baseClass}__btn-wrap`}>
<Button
className={`${baseClass}__btn`}
type="button"
variant="brand"
onClick={() => onSubmit(selectedQuery)}
disabled={!selectedQuery}
>
Add
</Button>
<Button
className={`${baseClass}__btn`}
onClick={onCancel}
variant="inverse"
>
Cancel
</Button>
</div>
</form>
</Modal>
);
};
export default AddPolicyModal;

View file

@ -1,71 +0,0 @@
.add-policy-modal {
&__sandbox-info {
margin-top: $pad-medium;
margin-bottom: $pad-large;
p {
margin: 0;
margin-bottom: $pad-medium;
}
p:last-child {
margin-bottom: 0;
}
}
a {
color: $core-vibrant-blue;
font-weight: $bold;
font-size: $x-small;
text-decoration: none;
}
&__info-header {
font-weight: $bold;
}
&__btn-wrap {
display: flex;
flex-direction: row-reverse;
}
&__btn {
margin-left: 12px;
}
&__advanced-options-button {
margin: $pad-medium 0;
color: $core-vibrant-blue;
font-weight: $bold;
font-size: $x-small;
}
.downcarat {
&::after {
content: url("../assets/images/icon-chevron-blue-16x16@2x.png");
transform: scale(0.5);
width: 16px;
border-radius: 0px;
padding: 0px;
padding-left: 2px;
margin-bottom: 2px;
}
}
.upcarat {
&::after {
content: url("../assets/images/icon-chevron-blue-16x16@2x.png");
transform: scale(0.5) rotate(180deg);
width: 16px;
border-radius: 0px;
padding: 0px;
padding-left: 2px;
margin-bottom: 4px;
margin-left: 14px;
}
}
.Select-value-label {
font-size: $small;
}
}

View file

@ -1 +0,0 @@
export { default } from "./AddPolicyModal";

View file

@ -23,7 +23,6 @@ interface IPoliciesListWrapperProps {
policiesList: IPolicy[];
isLoading: boolean;
onRemovePoliciesClick: (selectedTableIds: number[]) => void;
toggleAddPolicyModal: () => void;
resultsTitle?: string;
canAddOrRemovePolicy?: boolean;
tableType?: string;
@ -34,7 +33,6 @@ const PoliciesListWrapper = ({
policiesList,
isLoading,
onRemovePoliciesClick,
toggleAddPolicyModal,
resultsTitle,
canAddOrRemovePolicy,
tableType,
@ -85,17 +83,6 @@ const PoliciesListWrapper = ({
changes.
</p>
</div>
{canAddOrRemovePolicy && (
<div className={`${noPoliciesClass}__-cta-buttons`}>
<Button
variant="brand"
className={`${noPoliciesClass}__add-policy-button`}
onClick={toggleAddPolicyModal}
>
Add a policy
</Button>
</div>
)}
</div>
</div>
</div>
@ -117,7 +104,7 @@ const PoliciesListWrapper = ({
})}
data={generateDataSet(policiesList)}
isLoading={isLoading}
defaultSortHeader={"query_name"}
defaultSortHeader={"name"}
defaultSortDirection={"asc"}
manualSortBy
showMarkAllPages={false}
@ -125,8 +112,8 @@ const PoliciesListWrapper = ({
disablePagination
onPrimarySelectActionClick={onRemovePoliciesClick}
primarySelectActionButtonVariant="text-icon"
primarySelectActionButtonIcon="close"
primarySelectActionButtonText={"Remove"}
primarySelectActionButtonIcon="delete"
primarySelectActionButtonText={"Delete"}
emptyComponent={NoPolicies}
onQueryChange={noop}
disableCount={tableType === "inheritedPolicies"}

View file

@ -75,9 +75,12 @@ const generateTableHeaders = (options: {
title: "Query",
Header: "Query",
disableSortBy: true,
accessor: "query_name",
accessor: "name",
Cell: (cellProps: ICellProps): JSX.Element => (
<TextCell value={cellProps.cell.value} />
<LinkCell
value={cellProps.cell.value}
path={PATHS.EDIT_POLICY(cellProps.row.original)}
/>
),
},
];
@ -87,9 +90,12 @@ const generateTableHeaders = (options: {
title: "Query",
Header: "Query",
disableSortBy: true,
accessor: "query_name",
accessor: "name",
Cell: (cellProps: ICellProps): JSX.Element => (
<TextCell value={cellProps.cell.value} />
<LinkCell
value={cellProps.cell.value}
path={PATHS.EDIT_POLICY(cellProps.row.original)}
/>
),
},
{
@ -171,7 +177,7 @@ const generateTableHeaders = (options: {
const generateDataSet = memoize((policiesList: IPolicy[] = []): IPolicy[] => {
policiesList = policiesList.sort((a, b) =>
sortUtils.caseInsensitiveAsc(a.query_name, b.query_name)
sortUtils.caseInsensitiveAsc(a.name, b.name)
);
return policiesList;
});

View file

@ -15,9 +15,9 @@ const RemovePoliciesModal = ({
onSubmit,
}: IRemovePoliciesModalProps): JSX.Element => {
return (
<Modal title={"Remove policies"} onExit={onCancel} className={baseClass}>
<Modal title={"Delete policies"} onExit={onCancel} className={baseClass}>
<div className={baseClass}>
Are you sure you want to remove the selected policies?
Are you sure you want to delete the selected policies?
<div className={`${baseClass}__btn-wrap`}>
<Button
className={`${baseClass}__btn`}
@ -25,7 +25,7 @@ const RemovePoliciesModal = ({
variant="alert"
onClick={onSubmit}
>
Remove
Delete
</Button>
<Button
className={`${baseClass}__btn`}

View file

@ -0,0 +1,232 @@
import React, { useState, useEffect, useContext } from "react";
import { useQuery, useMutation } from "react-query";
import { InjectedRouter, Params } from "react-router/lib/Router";
// @ts-ignore
import Fleet from "fleet"; // @ts-ignore
import { AppContext } from "context/app";
import { PolicyContext } from "context/policy";
import { QUERIES_PAGE_STEPS, DEFAULT_POLICY } from "utilities/constants";
import globalPoliciesAPI from "services/entities/global_policies"; // @ts-ignore
import hostAPI from "services/entities/hosts"; // @ts-ignore
import { IPolicyFormData, IPolicy } from "interfaces/policy";
import { ITarget } from "interfaces/target";
import { IHost } from "interfaces/host";
import QuerySidePanel from "components/side_panels/QuerySidePanel";
import QueryEditor from "pages/policies/PolicyPage/screens/QueryEditor";
import SelectTargets from "pages/policies/PolicyPage/screens/SelectTargets";
import RunQuery from "pages/policies/PolicyPage/screens/RunQuery";
import ExternalURLIcon from "../../../../assets/images/icon-external-url-12x12@2x.png";
interface IPolicyPageProps {
router: InjectedRouter;
params: Params;
location: any; // no type in react-router v3
}
interface IStoredPolicyResponse {
policy: IPolicy;
}
interface IHostResponse {
host: IHost;
}
const baseClass = "policy-page";
const PolicyPage = ({
router,
params: { id: paramsPolicyId },
location: { query: URLQuerySearch },
}: IPolicyPageProps): JSX.Element => {
const policyIdForEdit = paramsPolicyId ? parseInt(paramsPolicyId, 10) : null;
const {
isGlobalAdmin,
isGlobalMaintainer,
isAnyTeamMaintainerOrTeamAdmin,
} = useContext(AppContext);
const {
selectedOsqueryTable,
setSelectedOsqueryTable,
setLastEditedQueryName,
setLastEditedQueryDescription,
setLastEditedQueryBody,
} = useContext(PolicyContext);
const [step, setStep] = useState<string>(QUERIES_PAGE_STEPS[1]);
const [selectedTargets, setSelectedTargets] = useState<ITarget[]>([]);
const [isLiveQueryRunnable, setIsLiveQueryRunnable] = useState<boolean>(true);
const [isSidebarOpen, setIsSidebarOpen] = useState<boolean>(true);
const [
showOpenSchemaActionText,
setShowOpenSchemaActionText,
] = useState<boolean>(false);
// disabled on page load so we can control the number of renders
// else it will re-populate the context on occasion
const {
isLoading: isStoredPolicyLoading,
data: storedPolicy,
error: storedPolicyError,
refetch: refetchStoredPolicy,
} = useQuery<IStoredPolicyResponse, Error, IPolicy>(
["query", policyIdForEdit],
() => globalPoliciesAPI.load(policyIdForEdit as number),
{
enabled: false,
refetchOnWindowFocus: false,
select: (data: IStoredPolicyResponse) => data.policy,
onSuccess: (returnedQuery) => {
setLastEditedQueryName(returnedQuery.name);
setLastEditedQueryDescription(returnedQuery.description);
setLastEditedQueryBody(returnedQuery.query);
},
}
);
// if URL is like `/policies/1?host_ids=22`, add the host
// to the selected targets automatically
useQuery<IHostResponse, Error, IHost>(
"hostFromURL",
() => hostAPI.load(parseInt(URLQuerySearch.host_ids as string, 10)),
{
enabled: !!URLQuerySearch.host_ids,
select: (data: IHostResponse) => data.host,
onSuccess: (data) => {
const targets = selectedTargets;
const hostTarget = data as any; // intentional so we can add to the object
hostTarget.target_type = "hosts";
targets.push(hostTarget as IHost);
setSelectedTargets([...targets]);
},
}
);
const {
mutateAsync: createPolicy,
} = useMutation((formData: IPolicyFormData) =>
globalPoliciesAPI.create(formData)
);
useEffect(() => {
const detectIsFleetQueryRunnable = () => {
Fleet.status.live_query().catch(() => {
setIsLiveQueryRunnable(false);
});
};
detectIsFleetQueryRunnable();
!!policyIdForEdit && refetchStoredPolicy();
setLastEditedQueryName(DEFAULT_POLICY.name);
setLastEditedQueryDescription(DEFAULT_POLICY.description);
setLastEditedQueryBody(DEFAULT_POLICY.query);
}, []);
useEffect(() => {
setShowOpenSchemaActionText(!isSidebarOpen);
}, [isSidebarOpen]);
const onOsqueryTableSelect = (tableName: string) => {
setSelectedOsqueryTable(tableName);
};
const onCloseSchemaSidebar = () => {
setIsSidebarOpen(false);
};
const onOpenSchemaSidebar = () => {
setIsSidebarOpen(true);
};
const renderLiveQueryWarning = (): JSX.Element | null => {
if (isLiveQueryRunnable) {
return null;
}
return (
<div className={`${baseClass}__warning`}>
<div className={`${baseClass}__message`}>
<p>
Fleet is unable to run a live query. Refresh the page or log in
again. If this keeps happening please{" "}
<a
target="_blank"
rel="noopener noreferrer"
href="https://github.com/fleetdm/fleet/issues/new/choose"
>
file an issue <img alt="" src={ExternalURLIcon} />
</a>
</p>
</div>
</div>
);
};
const renderScreen = () => {
const step1Opts = {
router,
baseClass,
policyIdForEdit,
showOpenSchemaActionText,
storedPolicy,
isStoredPolicyLoading,
storedPolicyError,
createPolicy,
onOsqueryTableSelect,
goToSelectTargets: () => setStep(QUERIES_PAGE_STEPS[2]),
onOpenSchemaSidebar,
renderLiveQueryWarning,
};
const step2Opts = {
baseClass,
selectedTargets: [...selectedTargets],
policyIdForEdit,
goToQueryEditor: () => setStep(QUERIES_PAGE_STEPS[1]),
goToRunQuery: () => setStep(QUERIES_PAGE_STEPS[3]),
setSelectedTargets,
};
const step3Opts = {
selectedTargets,
storedPolicy,
policyIdForEdit,
setSelectedTargets,
goToQueryEditor: () => setStep(QUERIES_PAGE_STEPS[1]),
};
switch (step) {
case QUERIES_PAGE_STEPS[2]:
return <SelectTargets {...step2Opts} />;
case QUERIES_PAGE_STEPS[3]:
return <RunQuery {...step3Opts} />;
default:
return <QueryEditor {...step1Opts} />;
}
};
const isFirstStep = step === QUERIES_PAGE_STEPS[1];
const sidebarClass = isFirstStep && isSidebarOpen && "has-sidebar";
const showSidebar =
isFirstStep &&
isSidebarOpen &&
(isGlobalAdmin || isGlobalMaintainer || isAnyTeamMaintainerOrTeamAdmin);
return (
<div className={`${baseClass} ${sidebarClass}`}>
<div className={`${baseClass}__content`}>{renderScreen()}</div>
{showSidebar && (
<QuerySidePanel
onOsqueryTableSelect={onOsqueryTableSelect}
selectedOsqueryTable={selectedOsqueryTable}
onClose={onCloseSchemaSidebar}
/>
)}
</div>
);
};
export default PolicyPage;

View file

@ -0,0 +1,199 @@
.policy-page {
&__results {
display: flex;
flex-grow: 1;
position: relative;
min-height: 400px;
}
&__warning {
padding: $pad-medium;
font-size: $x-small;
color: $core-fleet-black;
background-color: #fff0b9;
border: 1px solid #f2c94c;
border-radius: $border-radius;
p {
margin: 0;
line-height: 20px;
a {
color: $core-vibrant-blue;
font-weight: 700;
text-decoration: none;
img {
vertical-align: text-bottom;
position: relative;
top: 1px;
left: -3px;
transform: scale(0.5);
}
}
}
}
&__observer-query-view {
width: 90%;
max-width: 1060px;
margin: 0 auto;
color: $core-fleet-black;
h1 {
font-size: $medium;
}
p {
font-size: $x-small;
}
}
&__observer-query-details {
padding: 0 2rem;
h1 {
margin: $pad-large 0;
font-size: $large;
}
p {
margin-bottom: $pad-small;
}
.sql-button {
color: $core-vibrant-blue;
font-weight: $bold;
font-size: $x-small;
}
}
&__query-preview {
margin-top: 15px;
.fleet-ace__label {
display: none;
}
}
&__back-link {
font-size: $x-small;
color: $core-vibrant-blue;
font-weight: $bold;
text-decoration: none;
#back-chevron {
width: 16px;
margin-right: $pad-small;
vertical-align: text-top;
}
}
.ace_content {
min-height: 500px !important;
}
&__target-selectors {
margin-top: 24px;
margin-bottom: 24px;
max-width: 980px;
h3 {
margin-top: 24px;
margin-bottom: 8px;
font-size: $x-small;
}
.selector-block {
display: flex;
align-items: center;
flex-wrap: wrap;
.target-pill-selector {
padding: 8px;
background-color: $core-white;
border: none;
box-shadow: inset 0 0 0 1px $ui-fleet-black-25;
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
&:not(:last-of-type) {
margin-right: 8px;
}
img {
max-width: 12px;
}
.selector-name {
margin-left: 8px;
font-size: $x-small;
flex: 1;
}
.selector-count {
margin-left: 8px;
font-size: $xxx-small;
font-weight: 700;
}
&[data-selected="true"] {
background-color: $ui-vibrant-blue-10;
box-shadow: inset 0 0 0 1px $core-vibrant-blue;
}
}
}
}
&__targets-button-wrap {
margin-top: 30px;
display: flex;
align-items: center;
button:not(:first-of-type) {
margin-left: 16px;
}
}
&__targets-total-count {
margin-left: 16px;
font-size: $x-small;
span {
font-weight: 700;
}
}
&__page-loading {
.loading-spinner {
margin: $pad-large 0 0;
}
}
&__page-error {
h4 {
margin: 0;
margin-top: 28px;
margin-left: -7px;
font-size: $small;
img {
transform: scale(0.5);
vertical-align: middle;
position: relative;
top: -2px;
}
}
p {
margin: 0;
margin-top: $pad-medium;
font-size: $x-small;
a {
color: $core-vibrant-blue;
font-weight: 700;
text-decoration: none;
img {
vertical-align: text-bottom;
position: relative;
top: 1px;
left: -3px;
transform: scale(0.5);
}
}
}
}
}

View file

@ -0,0 +1,111 @@
import React, { useState } from "react";
import { size } from "lodash";
import { IPolicyFormData } from "interfaces/policy";
import { useDeepEffect } from "utilities/hooks";
// @ts-ignore
import InputField from "components/forms/fields/InputField";
import Button from "components/buttons/Button";
import Modal from "components/Modal";
export interface INewPolicyModalProps {
baseClass: string;
queryValue: string;
onCreatePolicy: (formData: IPolicyFormData) => void;
setIsSaveModalOpen: (isOpen: boolean) => void;
}
const validatePolicyName = (name: string) => {
const errors: { [key: string]: any } = {};
if (!name) {
errors.name = "Policy name must be present";
}
const valid = !size(errors);
return { valid, errors };
};
const NewPolicyModal = ({
baseClass,
queryValue,
onCreatePolicy,
setIsSaveModalOpen,
}: INewPolicyModalProps) => {
const [name, setName] = useState<string>("");
const [description, setDescription] = useState<string>("");
const [errors, setErrors] = useState<{ [key: string]: any }>({});
useDeepEffect(() => {
if (name) {
setErrors({});
}
}, [name]);
const handleUpdate = (evt: React.MouseEvent<HTMLButtonElement>) => {
evt.preventDefault();
const { valid, errors: newErrors } = validatePolicyName(name);
setErrors({
...errors,
...newErrors,
});
if (valid) {
onCreatePolicy({
description,
name,
query: queryValue,
});
setIsSaveModalOpen(false);
}
};
return (
<Modal title={"Save policy"} onExit={() => setIsSaveModalOpen(false)}>
<form className={`${baseClass}__save-modal-form`} autoComplete="off">
<InputField
name="name"
onChange={(value: string) => setName(value)}
value={name}
error={errors.name}
inputClassName={`${baseClass}__policy-save-modal-name`}
label="Name"
placeholder="What is your policy called?"
/>
<InputField
name="description"
onChange={(value: string) => setDescription(value)}
value={description}
inputClassName={`${baseClass}__policy-save-modal-description`}
label="Description"
type="textarea"
placeholder="What information does your policy reveal?"
/>
<div
className={`${baseClass}__button-wrap ${baseClass}__button-wrap--modal`}
>
<Button
className={`${baseClass}__btn`}
onClick={() => setIsSaveModalOpen(false)}
variant="text-link"
>
Cancel
</Button>
<Button
className={`${baseClass}__btn`}
type="button"
variant="brand"
onClick={handleUpdate}
>
Save policy
</Button>
</div>
</form>
</Modal>
);
};
export default NewPolicyModal;

View file

@ -0,0 +1 @@
export { default } from "./NewPolicyModal";

View file

@ -0,0 +1,505 @@
import React, { useState, useContext, useEffect } from "react";
import { IAceEditor } from "react-ace/lib/types";
import ReactTooltip from "react-tooltip";
import { size } from "lodash";
import { useDebouncedCallback } from "use-debounce/lib";
import { addGravatarUrlToResource } from "fleet/helpers";
// @ts-ignore
import { listCompatiblePlatforms, parseSqlTables } from "utilities/sql_tools";
import { AppContext } from "context/app";
import { PolicyContext } from "context/policy";
import { IPolicy, IPolicyFormData } from "interfaces/policy";
import Avatar from "components/Avatar";
import FleetAce from "components/FleetAce"; // @ts-ignore
import validateQuery from "components/forms/validators/validate_query";
import Button from "components/buttons/Button";
import Checkbox from "components/forms/fields/Checkbox";
import Spinner from "components/Spinner"; // @ts-ignore
import InputField from "components/forms/fields/InputField";
import NewPolicyModal from "../NewPolicyModal";
import CompatibleIcon from "../../../../../../assets/images/icon-compatible-green-16x16@2x.png";
import IncompatibleIcon from "../../../../../../assets/images/icon-incompatible-red-16x16@2x.png";
import InfoIcon from "../../../../../../assets/images/icon-info-purple-14x14@2x.png";
import QuestionIcon from "../../../../../../assets/images/icon-question-16x16@2x.png";
import PencilIcon from "../../../../../../assets/images/icon-pencil-14x14@2x.png";
const baseClass = "policy-form";
interface IPolicyFormProps {
policyIdForEdit: number | null;
showOpenSchemaActionText: boolean;
storedPolicy: IPolicy | undefined;
isStoredPolicyLoading: boolean;
onCreatePolicy: (formData: IPolicyFormData) => void;
onOsqueryTableSelect: (tableName: string) => void;
goToSelectTargets: () => void;
onUpdate: (formData: IPolicyFormData) => void;
onOpenSchemaSidebar: () => void;
renderLiveQueryWarning: () => JSX.Element | null;
}
const validateQuerySQL = (query: string) => {
const errors: { [key: string]: any } = {};
const { error: queryError, valid: queryValid } = validateQuery(query);
if (!queryValid) {
errors.query = queryError;
}
const valid = !size(errors);
return { valid, errors };
};
const PolicyForm = ({
policyIdForEdit,
showOpenSchemaActionText,
storedPolicy,
isStoredPolicyLoading,
onCreatePolicy,
onOsqueryTableSelect,
goToSelectTargets,
onUpdate,
onOpenSchemaSidebar,
renderLiveQueryWarning,
}: IPolicyFormProps): JSX.Element => {
const isEditMode = !!policyIdForEdit;
const [errors, setErrors] = useState<{ [key: string]: any }>({});
const [isSaveModalOpen, setIsSaveModalOpen] = useState<boolean>(false);
const [showQueryEditor, setShowQueryEditor] = useState<boolean>(false);
const [compatiblePlatforms, setCompatiblePlatforms] = useState<string[]>([]);
const [isEditingName, setIsEditingName] = useState<boolean>(false);
const [isEditingDescription, setIsEditingDescription] = useState<boolean>(
false
);
// Note: The PolicyContext values should always be used for any mutable policy data such as query name
// The storedPolicy prop should only be used to access immutable metadata such as author id
const {
lastEditedQueryName,
lastEditedQueryDescription,
lastEditedQueryBody,
setLastEditedQueryName,
setLastEditedQueryDescription,
setLastEditedQueryBody,
} = useContext(PolicyContext);
const {
currentUser,
isOnlyObserver,
isGlobalObserver,
isAnyTeamMaintainerOrTeamAdmin,
isGlobalAdmin,
isGlobalMaintainer,
} = useContext(AppContext);
const debounceCompatiblePlatforms = useDebouncedCallback(
(queryString: string) => {
setCompatiblePlatforms(
listCompatiblePlatforms(parseSqlTables(queryString))
);
},
300
);
useEffect(() => {
debounceCompatiblePlatforms(lastEditedQueryBody);
}, [lastEditedQueryBody]);
const hasTeamMaintainerPermissions = isEditMode
? isAnyTeamMaintainerOrTeamAdmin &&
storedPolicy &&
currentUser &&
storedPolicy.author_id === currentUser.id
: isAnyTeamMaintainerOrTeamAdmin;
const hasSavePermissions = isGlobalAdmin || isGlobalMaintainer;
const onLoad = (editor: IAceEditor) => {
editor.setOptions({
enableLinking: true,
});
// @ts-expect-error
// the string "linkClick" is not officially in the lib but we need it
editor.on("linkClick", (data: EditorSession) => {
const { type, value } = data.token;
if (type === "osquery-token") {
return onOsqueryTableSelect(value);
}
return false;
});
};
const promptSaveQuery = (forceNew = false) => (
evt: React.MouseEvent<HTMLButtonElement>
) => {
evt.preventDefault();
if (isEditMode && !lastEditedQueryName) {
return setErrors({
...errors,
name: "Policy name must be present",
});
}
let valid = true;
const { valid: isValidated, errors: newErrors } = validateQuerySQL(
lastEditedQueryBody
);
valid = isValidated;
setErrors({
...errors,
...newErrors,
});
if (valid) {
if (!isEditMode || forceNew) {
setIsSaveModalOpen(true);
} else {
onUpdate({
name: lastEditedQueryName,
description: lastEditedQueryDescription,
query: lastEditedQueryBody,
});
setErrors({});
}
setIsEditingName(false);
setIsEditingDescription(false);
}
};
const renderAuthor = (): JSX.Element | null => {
return storedPolicy ? (
<>
<b>Author</b>
<div>
<Avatar
user={addGravatarUrlToResource({
email: storedPolicy.author_email,
})}
size="xsmall"
/>
<span>
{storedPolicy.author_name === currentUser?.name
? "You"
: storedPolicy.author_name}
</span>
</div>
</>
) : null;
};
const renderLabelComponent = (): JSX.Element | null => {
if (!showOpenSchemaActionText) {
return null;
}
return (
<Button variant="text-icon" onClick={onOpenSchemaSidebar}>
<>
<img alt="" src={InfoIcon} />
Show schema
</>
</Button>
);
};
const renderPlatformCompatibility = () => {
const displayOrder = ["macOS", "Windows", "Linux"];
const displayIncompatibilityText = () => {
if (compatiblePlatforms[0] === "Invalid query") {
return "No platforms (check your query for a possible syntax error)";
} else if (compatiblePlatforms[0] === "None") {
return "No platforms (check your query for invalid tables or tables that are supported on different platforms)";
}
};
const displayFormattedPlatforms = compatiblePlatforms.map((string) => {
switch (string) {
case "darwin":
return "macOS";
case "windows":
return "Windows";
case "linux":
return "Linux";
default:
return string;
}
});
return (
<span className={`${baseClass}__platform-compatibility`}>
<b>Compatible with:</b>
<span className={`tooltip`}>
<span
className={`tooltip__tooltip-icon`}
data-tip
data-for="query-compatibility-tooltip"
data-tip-disable={false}
>
<img alt="question icon" src={QuestionIcon} />
</span>
<ReactTooltip
place="bottom"
type="dark"
effect="solid"
backgroundColor="#3e4771"
id="query-compatibility-tooltip"
data-html
>
<span className={`tooltip__tooltip-text`}>
Estimated compatiblity
<br />
based on the tables used
<br />
in the query
</span>
</ReactTooltip>
</span>
{displayIncompatibilityText() ||
displayOrder.map((platform) => {
const isCompatible =
displayFormattedPlatforms.includes(platform) ||
displayFormattedPlatforms[0] === "No tables in query AST"; // If query has no tables but is still syntatically valid sql, we treat it as compatible with all platforms
return (
<span
key={`platform-compatibility__${platform}`}
className="platform"
>
{platform}{" "}
<img
alt={isCompatible ? "compatible" : "incompatible"}
src={isCompatible ? CompatibleIcon : IncompatibleIcon}
/>
</span>
);
})}
</span>
);
};
const renderName = () => {
if (isEditMode) {
if (isEditingName) {
return (
<InputField
id="policy-name"
type="textarea"
name="policy-name"
error={errors.name}
value={lastEditedQueryName}
placeholder="Add name here"
inputClassName={`${baseClass}__policy-name`}
onChange={setLastEditedQueryName}
inputOptions={{
autoFocus: true,
}}
/>
);
}
/* eslint-disable */
// eslint complains about the button role
// applied to H1 - this is needed to avoid
// using a real button
// prettier-ignore
return (
<h1
role="button"
className={`${baseClass}__policy-name`}
onClick={() => setIsEditingName(true)}
>
{lastEditedQueryName}
<img alt="Edit name" src={PencilIcon} />
</h1>
);
/* eslint-enable */
}
return <h1 className={`${baseClass}__policy-name no-hover`}>New policy</h1>;
};
const renderDescription = () => {
if (isEditMode) {
if (isEditingDescription) {
return (
<InputField
id="policy-description"
type="textarea"
name="policy-description"
value={lastEditedQueryDescription}
placeholder="Add description here."
inputClassName={`${baseClass}__policy-description`}
onChange={setLastEditedQueryDescription}
inputOptions={{
autoFocus: true,
}}
/>
);
}
/* eslint-disable */
// eslint complains about the button role
// applied to span - this is needed to avoid
// using a real button
// prettier-ignore
return (
<span
role="button"
className={`${baseClass}__policy-description`}
onClick={() => setIsEditingDescription(true)}
>
{lastEditedQueryDescription}
<img alt="Edit description" src={PencilIcon} />
</span>
);
/* eslint-enable */
}
return null;
};
const renderRunForObserver = (
<form className={`${baseClass}__wrapper`}>
<div className={`${baseClass}__title-bar`}>
<div className="name-description">
<h1 className={`${baseClass}__policy-name no-hover`}>
{lastEditedQueryName}
</h1>
<p className={`${baseClass}__policy-description no-hover`}>
{lastEditedQueryDescription}
</p>
</div>
<div className="author">{renderAuthor()}</div>
</div>
<Button
className={`${baseClass}__toggle-sql`}
variant="text-link"
onClick={() => setShowQueryEditor(!showQueryEditor)}
disabled={false}
>
{showQueryEditor ? "Hide SQL" : "Show SQL"}
</Button>
{showQueryEditor && (
<FleetAce
value={lastEditedQueryBody}
name="query editor"
wrapperClassName={`${baseClass}__text-editor-wrapper`}
readOnly
/>
)}
{renderLiveQueryWarning()}
</form>
);
const renderForGlobalAdminOrAnyMaintainer = (
<>
<form className={`${baseClass}__wrapper`} autoComplete="off">
<div className={`${baseClass}__title-bar`}>
<div className="name-description">
{renderName()}
{renderDescription()}
</div>
<div className="author">{isEditMode && renderAuthor()}</div>
</div>
<FleetAce
value={lastEditedQueryBody}
error={errors.query}
label="Query:"
labelActionComponent={renderLabelComponent()}
name="query editor"
onLoad={onLoad}
wrapperClassName={`${baseClass}__text-editor-wrapper`}
onChange={(sqlString: string) => {
setLastEditedQueryBody(sqlString);
}}
handleSubmit={promptSaveQuery}
/>
{renderPlatformCompatibility()}
{renderLiveQueryWarning()}
<div
className={`${baseClass}__button-wrap ${baseClass}__button-wrap--new-policy`}
>
{(hasSavePermissions || isAnyTeamMaintainerOrTeamAdmin) && (
<div className="query-form__button-wrap--save-policy-button">
<div
data-tip
data-for="save-query-button"
data-tip-disable={
!(
isAnyTeamMaintainerOrTeamAdmin &&
!hasTeamMaintainerPermissions
)
}
>
<Button
className={`${baseClass}__save`}
variant="brand"
onClick={promptSaveQuery()}
disabled={
isAnyTeamMaintainerOrTeamAdmin &&
!hasTeamMaintainerPermissions
}
>
Save
</Button>
</div>{" "}
<ReactTooltip
className={`save-policy-button-tooltip`}
place="bottom"
type="dark"
effect="solid"
backgroundColor="#3e4771"
id="save-query-button"
data-html
>
<div
className={`tooltip`}
style={{ width: "152px", textAlign: "center" }}
>
You can only save changes to a query if you are the author.
</div>
</ReactTooltip>
</div>
)}
<Button
className={`${baseClass}__run`}
variant="blue-green"
onClick={goToSelectTargets}
>
Run query
</Button>
</div>
</form>
{isSaveModalOpen && (
<NewPolicyModal
baseClass={baseClass}
queryValue={lastEditedQueryBody}
onCreatePolicy={onCreatePolicy}
setIsSaveModalOpen={setIsSaveModalOpen}
/>
)}
</>
);
if (isStoredPolicyLoading) {
return <Spinner />;
}
if (isOnlyObserver || isGlobalObserver) {
return renderRunForObserver;
}
return renderForGlobalAdminOrAnyMaintainer;
};
export default PolicyForm;

View file

@ -0,0 +1,205 @@
.policy-form {
&__wrapper {
position: relative;
font-size: $x-small;
.policy-page__warning {
margin: 0;
margin-top: $pad-large;
}
.form-field--input {
margin: 0;
}
}
&__title-bar {
display: flex;
justify-content: space-between;
.form-field {
margin-bottom: 0px;
}
.input-field,
.input-field__text-area {
min-height: auto;
line-height: normal;
white-space: normal;
}
/* Hide scrollbar for Chrome, Safari and Opera */
.input-field::-webkit-scrollbar {
display: none;
}
/* Hide scrollbar for IE, Edge and Firefox */
.input-field {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
.name-description {
flex-grow: 1;
margin-right: 24px;
.policy-form__policy-name {
margin-top: $pad-large;
line-height: 2rem;
height: 2rem;
cursor: pointer;
}
.policy-form__policy-description:not(textarea) {
margin: 0.25rem 0 1rem;
cursor: pointer;
}
textarea.policy-form__policy-description {
margin-top: 0.8125rem;
}
img {
transform: scale(0.5);
position: relative;
top: 6px;
}
}
.author {
flex-shrink: 1;
margin-top: 24px;
text-align: right;
justify-content: right;
b {
text-align: right;
}
img,
div {
display: flex;
align-items: center;
}
span {
padding-left: $pad-small;
}
}
}
&__policy-name,
&__policy-description {
width: 100%;
margin: 0;
padding: 0;
border: 0;
resize: none;
white-space: normal;
background-color: transparent;
&:hover:not(.focus-visible):not(.no-hover) {
color: $core-vibrant-blue;
cursor: pointer;
}
&.focus-visible {
outline: 0;
cursor: text;
}
}
&__policy-name {
margin-top: $pad-large;
font-size: $large;
&.input-field--error {
border: 1px solid $core-vibrant-red;
}
}
&__policy-description {
margin-top: 0;
font-size: $x-small;
}
&__toggle-sql {
margin-top: $pad-large;
}
&__text-editor-wrapper {
margin: 0;
margin-top: $pad-large;
}
&__button-wrap {
margin: 0;
margin-top: $pad-large;
&--new-policy {
display: flex;
align-items: center;
}
&--modal {
display: flex;
justify-content: flex-end;
align-items: center;
}
button:not(:first-of-type) {
margin-left: $pad-medium;
}
}
&__button-wrap--save-policy-button:first-child {
margin-right: $pad-medium;
}
&__button-wrap--save-policy-button:not(:first-child),
&__run {
margin-left: $pad-medium;
}
&__save-policy-btn,
&__cancel-btn {
margin-right: $pad-xsmall;
}
&__title {
color: $core-fleet-black;
display: inline-block;
font-size: $large;
}
&__platform-compatibility {
display: flex;
align-items: center;
padding-top: $pad-medium;
b,
img,
span {
display: flex;
align-items: center;
}
b {
padding-right: $pad-xsmall;
}
img {
height: 16px;
width: auto;
padding-left: $pad-xsmall;
}
.tooltip {
padding-right: 12px;
img {
padding: 0px;
}
&__tooltip-text {
text-align: center;
}
}
.platform {
padding-right: 12px;
}
}
}

View file

@ -0,0 +1 @@
export { default } from "./PolicyForm";

View file

@ -0,0 +1 @@
export { default } from "./PolicyPage";

View file

@ -0,0 +1,139 @@
import React, { useContext, useEffect } from "react";
import { Link } from "react-router";
import { useDispatch } from "react-redux";
import { InjectedRouter } from "react-router/lib/Router";
import { UseMutateAsyncFunction } from "react-query";
import globalPoliciesAPI from "services/entities/global_policies";
import { AppContext } from "context/app";
import { PolicyContext } from "context/policy"; // @ts-ignore
import { renderFlash } from "redux/nodes/notifications/actions";
import PATHS from "router/paths"; // @ts-ignore
import debounce from "utilities/debounce"; // @ts-ignore
import deepDifference from "utilities/deep_difference";
import { IPolicyFormData, IPolicy } from "interfaces/policy";
import PolicyForm from "pages/policies/PolicyPage/components/PolicyForm";
import BackChevron from "../../../../../assets/images/icon-chevron-down-9x6@2x.png";
interface IQueryEditorProps {
router: InjectedRouter;
baseClass: string;
policyIdForEdit: number | null;
storedPolicy: IPolicy | undefined;
storedPolicyError: any;
showOpenSchemaActionText: boolean;
isStoredPolicyLoading: boolean;
createPolicy: UseMutateAsyncFunction<any, unknown, IPolicyFormData, unknown>;
onOsqueryTableSelect: (tableName: string) => void;
goToSelectTargets: () => void;
onOpenSchemaSidebar: () => void;
renderLiveQueryWarning: () => JSX.Element | null;
}
const QueryEditor = ({
router,
baseClass,
policyIdForEdit,
storedPolicy,
storedPolicyError,
showOpenSchemaActionText,
isStoredPolicyLoading,
createPolicy,
onOsqueryTableSelect,
goToSelectTargets,
onOpenSchemaSidebar,
renderLiveQueryWarning,
}: IQueryEditorProps) => {
const dispatch = useDispatch();
const { currentUser } = useContext(AppContext);
// Note: The PolicyContext values should always be used for any mutable policy data such as query name
// The storedPolicy prop should only be used to access immutable metadata such as author id
const {
lastEditedQueryName,
lastEditedQueryDescription,
lastEditedQueryBody,
} = useContext(PolicyContext);
useEffect(() => {
if (storedPolicyError) {
dispatch(
renderFlash(
"error",
"Something went wrong retrieving your policy. Please try again."
)
);
}
}, []);
const onSavePolicyFormSubmit = debounce(async (formData: IPolicyFormData) => {
try {
const { policy }: { policy: IPolicy } = await createPolicy(formData);
router.push(PATHS.EDIT_POLICY(policy));
dispatch(renderFlash("success", "Policy created!"));
} catch (createError) {
console.error(createError);
dispatch(
renderFlash(
"error",
"Something went wrong creating your policy. Please try again."
)
);
}
});
const onUpdatePolicy = async (formData: IPolicyFormData) => {
if (!policyIdForEdit) {
return false;
}
const updatedPolicy = deepDifference(formData, {
lastEditedQueryName,
lastEditedQueryDescription,
lastEditedQueryBody,
});
try {
await globalPoliciesAPI.update(policyIdForEdit, updatedPolicy);
dispatch(renderFlash("success", "Policy updated!"));
} catch (updateError) {
console.error(updateError);
dispatch(
renderFlash(
"error",
"Something went wrong updating your policy. Please try again."
)
);
}
return false;
};
if (!currentUser) {
return null;
}
return (
<div className={`${baseClass}__form body-wrap`}>
<Link to={PATHS.MANAGE_POLICIES} className={`${baseClass}__back-link`}>
<img src={BackChevron} alt="back chevron" id="back-chevron" />
<span>Back to policies</span>
</Link>
<PolicyForm
onCreatePolicy={onSavePolicyFormSubmit}
goToSelectTargets={goToSelectTargets}
onOsqueryTableSelect={onOsqueryTableSelect}
onUpdate={onUpdatePolicy}
storedPolicy={storedPolicy}
policyIdForEdit={policyIdForEdit}
isStoredPolicyLoading={isStoredPolicyLoading}
showOpenSchemaActionText={showOpenSchemaActionText}
onOpenSchemaSidebar={onOpenSchemaSidebar}
renderLiveQueryWarning={renderLiveQueryWarning}
/>
</div>
);
};
export default QueryEditor;

View file

@ -0,0 +1,220 @@
import React, { useState, useEffect, useRef, useContext } from "react";
import { useDispatch } from "react-redux";
import SockJS from "sockjs-client";
// @ts-ignore
import { PolicyContext } from "context/policy";
import { formatSelectedTargetsForApi } from "fleet/helpers"; // @ts-ignore
import { renderFlash } from "redux/nodes/notifications/actions"; // @ts-ignore
import campaignHelpers from "redux/nodes/entities/campaigns/helpers";
import queryAPI from "services/entities/queries"; // @ts-ignore
import debounce from "utilities/debounce"; // @ts-ignore
import { BASE_URL, DEFAULT_CAMPAIGN_STATE } from "utilities/constants"; // @ts-ignore
import local from "utilities/local"; // @ts-ignore
import { ICampaign, ICampaignState } from "interfaces/campaign";
import { IPolicy } from "interfaces/policy";
import { ITarget } from "interfaces/target";
import QueryResults from "components/QueryResults";
interface IRunQueryProps {
storedPolicy: IPolicy | undefined;
selectedTargets: ITarget[];
policyIdForEdit: number | null;
setSelectedTargets: (value: ITarget[]) => void;
goToQueryEditor: () => void;
}
const RunQuery = ({
storedPolicy,
selectedTargets,
policyIdForEdit,
setSelectedTargets,
goToQueryEditor,
}: IRunQueryProps) => {
const dispatch = useDispatch();
const [isQueryFinished, setIsQueryFinished] = useState<boolean>(false);
const [campaignState, setCampaignState] = useState<ICampaignState>(
DEFAULT_CAMPAIGN_STATE
);
const { lastEditedQueryBody } = useContext(PolicyContext);
const ws = useRef(null);
const runQueryInterval = useRef<any>(null);
const globalSocket = useRef<any>(null);
const previousSocketData = useRef<any>(null);
const removeSocket = () => {
if (globalSocket.current) {
globalSocket.current.close();
globalSocket.current = null;
previousSocketData.current = null;
}
};
const setupDistributedQuery = (socket: WebSocket | null) => {
globalSocket.current = socket;
const update = () => {
setCampaignState((prevCampaignState) => ({
...prevCampaignState,
runQueryMilliseconds: prevCampaignState.runQueryMilliseconds + 1000,
}));
};
if (!runQueryInterval.current) {
runQueryInterval.current = setInterval(update, 1000);
}
};
const teardownDistributedQuery = () => {
if (runQueryInterval.current) {
clearInterval(runQueryInterval.current);
runQueryInterval.current = null;
}
setCampaignState((prevCampaignState) => ({
...prevCampaignState,
queryIsRunning: false,
runQueryMilliseconds: 0,
}));
setIsQueryFinished(true);
removeSocket();
};
const destroyCampaign = () => {
setCampaignState(DEFAULT_CAMPAIGN_STATE);
};
const connectAndRunLiveQuery = (returnedCampaign: ICampaign) => {
let { current: websocket }: { current: WebSocket | null } = ws;
websocket = new SockJS(`${BASE_URL}/v1/fleet/results`, undefined, {});
websocket.onopen = () => {
setupDistributedQuery(websocket);
setCampaignState((prevCampaignState) => ({
...prevCampaignState,
campaign: returnedCampaign,
queryIsRunning: true,
}));
websocket?.send(
JSON.stringify({
type: "auth",
data: { token: local.getItem("auth_token") },
})
);
websocket?.send(
JSON.stringify({
type: "select_campaign",
data: { campaign_id: returnedCampaign.id },
})
);
};
websocket.onmessage = ({ data }: { data: string }) => {
// string is easy to compare before converting to object
if (data === previousSocketData.current) {
return false;
}
previousSocketData.current = data;
const socketData = JSON.parse(data);
setCampaignState((prevCampaignState) => {
return {
...prevCampaignState,
...campaignHelpers.updateCampaignState(socketData)(prevCampaignState),
};
});
if (
socketData.type === "status" &&
socketData.data.status === "finished"
) {
return teardownDistributedQuery();
}
};
};
const onRunQuery = debounce(async () => {
if (!lastEditedQueryBody) {
dispatch(
renderFlash(
"error",
"Something went wrong running your query. Please try again."
)
);
return false;
}
const selected = formatSelectedTargetsForApi(selectedTargets);
setIsQueryFinished(false);
removeSocket();
destroyCampaign();
try {
const isStoredQueryEdited = storedPolicy?.query !== lastEditedQueryBody;
// because we are not using the saved query id if user edits the SQL
const queryId = isStoredQueryEdited ? null : policyIdForEdit;
const returnedCampaign = await queryAPI.run({
query: lastEditedQueryBody,
queryId,
selected,
});
connectAndRunLiveQuery(returnedCampaign);
} catch (campaignError: any) {
if (campaignError === "resource already created") {
dispatch(
renderFlash(
"error",
"A campaign with the provided query text has already been created"
)
);
}
if ("message" in campaignError) {
const { message } = campaignError;
if (message === "forbidden") {
dispatch(
renderFlash(
"error",
"It seems you do not have the rights to run this query. If you believe this is in error, please contact your administrator."
)
);
} else {
dispatch(
renderFlash("error", "Something has gone wrong. Please try again.")
);
}
}
return teardownDistributedQuery();
}
});
const onStopQuery = (evt: React.MouseEvent<HTMLButtonElement>) => {
evt.preventDefault();
return teardownDistributedQuery();
};
useEffect(() => {
onRunQuery();
}, []);
const { campaign } = campaignState;
return (
<QueryResults
campaign={campaign}
onRunQuery={onRunQuery}
onStopQuery={onStopQuery}
isQueryFinished={isQueryFinished}
setSelectedTargets={setSelectedTargets}
goToQueryEditor={goToQueryEditor}
/>
);
};
export default RunQuery;

View file

@ -0,0 +1,333 @@
import React, { useState } from "react";
import { useQuery } from "react-query";
import { Row } from "react-table";
import { filter, forEach, isEmpty, remove, unionBy } from "lodash";
// @ts-ignore
import { formatSelectedTargetsForApi } from "fleet/helpers";
import targetsAPI from "services/entities/targets";
import { ITarget, ITargets, ITargetsAPIResponse } from "interfaces/target";
import { ILabel } from "interfaces/label";
import { ITeam } from "interfaces/team";
import { IHost } from "interfaces/host";
// @ts-ignore
import TargetsInput from "components/TargetsInput";
import Button from "components/buttons/Button";
import Spinner from "components/Spinner";
import PlusIcon from "../../../../../assets/images/icon-plus-purple-32x32@2x.png";
import CheckIcon from "../../../../../assets/images/icon-check-purple-32x32@2x.png";
import ExternalURLIcon from "../../../../../assets/images/icon-external-url-12x12@2x.png";
import ErrorIcon from "../../../../../assets/images/icon-error-16x16@2x.png";
interface ITargetPillSelectorProps {
entity: ILabel | ITeam;
isSelected: boolean;
onClick: (
value: ILabel | ITeam
) => React.MouseEventHandler<HTMLButtonElement>;
}
interface ISelectTargetsProps {
baseClass: string;
selectedTargets: ITarget[];
policyIdForEdit: number | null;
goToQueryEditor: () => void;
goToRunQuery: () => void;
setSelectedTargets: React.Dispatch<React.SetStateAction<ITarget[]>>;
}
interface IModifiedUseQueryTargetsResponse {
results: IHost[] | ITargets;
targetsCount: number;
onlineCount: number;
}
const TargetPillSelector = ({
entity,
isSelected,
onClick,
}: ITargetPillSelectorProps): JSX.Element => {
const displayText = () => {
switch (entity.display_text) {
case "All Hosts":
return "All hosts";
case "All Linux":
return "Linux";
default:
return entity.display_text;
}
};
return (
<button
className="target-pill-selector"
data-selected={isSelected}
onClick={(e) => onClick(entity)(e)}
>
<img alt="" src={isSelected ? CheckIcon : PlusIcon} />
<span className="selector-name">{displayText()}</span>
<span className="selector-count">{entity.count}</span>
</button>
);
};
const SelectTargets = ({
baseClass,
selectedTargets,
policyIdForEdit,
goToQueryEditor,
goToRunQuery,
setSelectedTargets,
}: ISelectTargetsProps) => {
const [targetsTotalCount, setTargetsTotalCount] = useState<number | null>(
null
);
const [targetsOnlinePercent, setTargetsOnlinePercent] = useState<number>(0);
const [allHostsLabels, setAllHostsLabels] = useState<ILabel[] | null>(null);
const [platformLabels, setPlatformLabels] = useState<ILabel[] | null>(null);
const [teams, setTeams] = useState<ITeam[] | null>(null);
const [otherLabels, setOtherLabels] = useState<ILabel[] | null>(null);
const [selectedLabels, setSelectedLabels] = useState<any>([]);
const [inputTabIndex, setInputTabIndex] = useState<number>(0);
const [searchText, setSearchText] = useState<string>("");
const [relatedHosts, setRelatedHosts] = useState<IHost[]>([]);
const { isLoading: isTargetsLoading, isError: isTargetsError } = useQuery(
// triggers query on change
["targetsFromSearch", searchText, [...selectedTargets]],
() =>
targetsAPI.loadAll({
query: searchText,
queryId: policyIdForEdit,
selected: formatSelectedTargetsForApi(selectedTargets) as any,
}),
{
refetchOnWindowFocus: false,
// only retrieve the whole targets object once
// we will only update related hosts when a search query fires
select: (data: ITargetsAPIResponse) =>
allHostsLabels
? {
results: data.targets.hosts,
targetsCount: data.targets_count,
onlineCount: data.targets_online,
}
: {
results: data.targets,
targetsCount: data.targets_count,
onlineCount: data.targets_online,
},
onSuccess: ({
results,
targetsCount,
onlineCount,
}: IModifiedUseQueryTargetsResponse) => {
if ("labels" in results) {
// this will only run once
const { labels, teams: targetTeams } = results as ITargets;
const allHosts = filter(
labels,
({ display_text: text }) => text === "All Hosts"
);
const platforms = filter(
labels,
({ display_text: text }) =>
text === "macOS" || text === "MS Windows" || text === "All Linux"
);
const other = filter(
labels,
({ label_type: type }) => type === "regular"
);
setAllHostsLabels(allHosts);
setPlatformLabels(platforms);
setTeams(targetTeams);
setOtherLabels(other);
const labelCount =
allHosts.length +
platforms.length +
targetTeams.length +
other.length;
setInputTabIndex(labelCount || 0);
} else if (searchText === "") {
setRelatedHosts([]);
} else {
// this will always update as the user types
setRelatedHosts([...results] as IHost[]);
}
setTargetsTotalCount(targetsCount);
if (targetsCount > 0) {
setTargetsOnlinePercent(
Math.round((onlineCount / targetsCount) * 100)
);
}
},
}
);
const handleSelectedLabels = (entity: ILabel | ITeam) => (
e: React.MouseEvent<HTMLButtonElement>
): void => {
e.preventDefault();
const labels = selectedLabels;
let newTargets = null;
const targets = selectedTargets;
const removed = remove(labels, ({ id }) => id === entity.id);
// visually show selection
const isRemoval = removed.length > 0;
if (isRemoval) {
newTargets = labels;
} else {
labels.push(entity);
// prepare the labels data
forEach(labels, (label) => {
label.target_type = "label_type" in label ? "labels" : "teams";
});
newTargets = unionBy(targets, labels, "id");
}
setSelectedLabels([...labels]);
setSelectedTargets([...newTargets]);
};
const handleRowSelect = (row: Row) => {
const targets = selectedTargets;
const hostTarget = row.original as any; // intentional so we can add to the object
hostTarget.target_type = "hosts";
targets.push(hostTarget as IHost);
setSelectedTargets([...targets]);
setSearchText("");
};
const handleRowRemove = (row: Row) => {
const targets = selectedTargets;
const hostTarget = row.original as ITarget;
remove(targets, (t) => t.id === hostTarget.id);
setSelectedTargets([...targets]);
};
const renderTargetEntityList = (
header: string,
entityList: ILabel[] | ITeam[]
): JSX.Element => (
<>
{header && <h3>{header}</h3>}
<div className="selector-block">
{entityList?.map((entity: ILabel | ITeam) => (
<TargetPillSelector
key={entity.id}
entity={entity}
isSelected={selectedLabels.some(
({ id }: ILabel | ITeam) => id === entity.id
)}
onClick={handleSelectedLabels}
/>
))}
</div>
</>
);
if (isEmpty(searchText) && isTargetsLoading) {
return (
<div className={`${baseClass}__wrapper body-wrap`}>
<h1>Select targets</h1>
<div className={`${baseClass}__page-loading`}>
<Spinner />
</div>
</div>
);
}
if (isEmpty(searchText) && isTargetsError) {
return (
<div className={`${baseClass}__wrapper body-wrap`}>
<h1>Select targets</h1>
<div className={`${baseClass}__page-error`}>
<h4>
<img alt="" src={ErrorIcon} />
Something&apos;s gone wrong.
</h4>
<p>Refresh the page or log in again.</p>
<p>
If this keeps happening please{" "}
<a
className="file-issue-link"
target="_blank"
rel="noopener noreferrer"
href="https://github.com/fleetdm/fleet/issues/new/choose"
>
file an issue <img alt="" src={ExternalURLIcon} />
</a>
</p>
</div>
</div>
);
}
return (
<div className={`${baseClass}__wrapper body-wrap`}>
<h1>Select targets</h1>
<div className={`${baseClass}__target-selectors`}>
{allHostsLabels &&
allHostsLabels.length > 0 &&
renderTargetEntityList("", allHostsLabels)}
{platformLabels &&
platformLabels.length > 0 &&
renderTargetEntityList("Platforms", platformLabels)}
{teams && teams.length > 0 && renderTargetEntityList("Teams", teams)}
{otherLabels &&
otherLabels.length > 0 &&
renderTargetEntityList("Labels", otherLabels)}
</div>
<TargetsInput
tabIndex={inputTabIndex}
searchText={searchText}
relatedHosts={[...relatedHosts]}
isTargetsLoading={isTargetsLoading}
selectedTargets={[...selectedTargets]}
hasFetchError={isTargetsError}
setSearchText={setSearchText}
handleRowSelect={handleRowSelect}
handleRowRemove={handleRowRemove}
/>
<div className={`${baseClass}__targets-button-wrap`}>
<Button
className={`${baseClass}__btn`}
onClick={goToQueryEditor}
variant="text-link"
>
Cancel
</Button>
<Button
className={`${baseClass}__btn`}
type="button"
variant="blue-green"
disabled={!targetsTotalCount}
onClick={goToRunQuery}
>
Run
</Button>
<div className={`${baseClass}__targets-total-count`}>
{!!targetsTotalCount && (
<>
<span>{targetsTotalCount}</span> targets selected&nbsp; (
{targetsOnlinePercent}% online)
</>
)}
</div>
</div>
</div>
);
};
export default SelectTargets;

View file

@ -24,6 +24,7 @@ import CompatibleIcon from "../../../../../../assets/images/icon-compatible-gree
import IncompatibleIcon from "../../../../../../assets/images/icon-incompatible-red-16x16@2x.png";
import InfoIcon from "../../../../../../assets/images/icon-info-purple-14x14@2x.png";
import QuestionIcon from "../../../../../../assets/images/icon-question-16x16@2x.png";
import PencilIcon from "../../../../../../assets/images/icon-pencil-14x14@2x.png";
const baseClass = "query-form";
@ -69,6 +70,10 @@ const QueryForm = ({
const [isSaveModalOpen, setIsSaveModalOpen] = useState<boolean>(false);
const [showQueryEditor, setShowQueryEditor] = useState<boolean>(false);
const [compatiblePlatforms, setCompatiblePlatforms] = useState<string[]>([]);
const [isEditingName, setIsEditingName] = useState<boolean>(false);
const [isEditingDescription, setIsEditingDescription] = useState<boolean>(
false
);
// Note: The QueryContext values should always be used for any mutable query data such as query name
// The storedQuery prop should only be used to access immutable metadata such as author id
@ -282,6 +287,87 @@ const QueryForm = ({
);
};
const renderName = () => {
if (isEditMode) {
if (isEditingName) {
return (
<InputField
id="query-name"
type="textarea"
name="query-name"
error={errors.name}
value={lastEditedQueryName}
placeholder="Add name here"
inputClassName={`${baseClass}__query-name`}
onChange={setLastEditedQueryName}
inputOptions={{
autoFocus: true,
}}
/>
);
}
/* eslint-disable */
// eslint complains about the button role
// applied to H1 - this is needed to avoid
// using a real button
// prettier-ignore
return (
<h1
role="button"
className={`${baseClass}__query-name`}
onClick={() => setIsEditingName(true)}
>
{lastEditedQueryName}
<img alt="Edit name" src={PencilIcon} />
</h1>
);
/* eslint-enable */
}
return <h1 className={`${baseClass}__query-name no-hover`}>New query</h1>;
};
const renderDescription = () => {
if (isEditMode) {
if (isEditingDescription) {
return (
<InputField
id="query-description"
type="textarea"
name="query-description"
value={lastEditedQueryDescription}
placeholder="Add description here."
inputClassName={`${baseClass}__query-description`}
onChange={setLastEditedQueryDescription}
inputOptions={{
autoFocus: true,
}}
/>
);
}
/* eslint-disable */
// eslint complains about the button role
// applied to span - this is needed to avoid
// using a real button
// prettier-ignore
return (
<span
role="button"
className={`${baseClass}__query-description`}
onClick={() => setIsEditingDescription(true)}
>
{lastEditedQueryDescription}
<img alt="Edit description" src={PencilIcon} />
</span>
);
/* eslint-enable */
}
return null;
};
const renderRunForObserver = (
<form className={`${baseClass}__wrapper`}>
<div className={`${baseClass}__title-bar`}>
@ -333,31 +419,8 @@ const QueryForm = ({
<form className={`${baseClass}__wrapper`} autoComplete="off">
<div className={`${baseClass}__title-bar`}>
<div className="name-description">
{isEditMode ? (
<InputField
id="query-name"
type="textarea"
name="query-name"
error={errors.name}
value={lastEditedQueryName}
placeholder="Add name here"
inputClassName={`${baseClass}__query-name`}
onChange={setLastEditedQueryName}
/>
) : (
<h1 className={`${baseClass}__query-name no-hover`}>New query</h1>
)}
{isEditMode && (
<InputField
id="query-description"
type="textarea"
name="query-description"
value={lastEditedQueryDescription}
placeholder="Add description here."
inputClassName={`${baseClass}__query-description`}
onChange={setLastEditedQueryDescription}
/>
)}
{renderName()}
{renderDescription()}
</div>
<div className="author">{isEditMode && renderAuthor()}</div>
</div>

View file

@ -43,11 +43,22 @@
flex-grow: 1;
margin-right: 24px;
.query-form__query-name {
margin-top: $pad-large;
line-height: 2rem;
height: 2rem;
cursor: pointer;
}
.query-form__query-description {
.query-form__query-description:not(textarea) {
margin: 0.25rem 0 1rem;
cursor: pointer;
}
textarea.query-form__query-description {
margin-top: 0.8125rem;
}
img {
transform: scale(0.5);
position: relative;
top: 6px;
}
}

View file

@ -16,7 +16,7 @@ import { IQuery } from "interfaces/query";
import { ITarget } from "interfaces/target";
// import { useLastEditedQueryInfo } from "../helpers";
import QueryResults from "../components/QueryResults";
import QueryResults from "components/QueryResults";
interface IRunQueryProps {
storedQuery: IQuery | undefined;

View file

@ -12,7 +12,7 @@ import { ITeam } from "interfaces/team";
import { IHost } from "interfaces/host";
// @ts-ignore
import TargetsInput from "pages/queries/QueryPage/components/TargetsInput";
import TargetsInput from "components/TargetsInput";
import Button from "components/buttons/Button";
import Spinner from "components/Spinner";
import PlusIcon from "../../../../../assets/images/icon-plus-purple-32x32@2x.png";

View file

@ -40,6 +40,7 @@ import ManageSchedulePage from "pages/schedule/ManageSchedulePage";
import PackPageWrapper from "components/packs/PackPageWrapper";
import PackComposerPage from "pages/packs/PackComposerPage";
import PoliciesPageWrapper from "components/policies/PoliciesPageWrapper";
import PolicyPage from "pages/policies/PolicyPage";
import QueryPage from "pages/queries/QueryPage";
import QueryPageWrapper from "components/queries/QueryPageWrapper";
import RegistrationPage from "pages/RegistrationPage";
@ -156,6 +157,10 @@ const routes = (
</Route>
<Route path="policies" component={PoliciesPageWrapper}>
<Route path="manage" component={ManagePoliciesPage} />
<Route component={AuthAnyMaintainerAnyAdminRoutes}>
<Route path="new" component={PolicyPage} />
</Route>
<Route path=":id" component={PolicyPage} />
</Route>
<Route path="profile" component={UserSettingsPage} />
</Route>

View file

@ -1,5 +1,6 @@
import { IHost } from "../interfaces/host";
import { IQuery } from "../interfaces/query";
import { IPolicy } from "../interfaces/policy";
import URL_PREFIX from "./url_prefix";
export default {
@ -18,6 +19,9 @@ export default {
EDIT_QUERY: (query: IQuery): string => {
return `${URL_PREFIX}/queries/${query.id}`;
},
EDIT_POLICY: (policy: IPolicy): string => {
return `${URL_PREFIX}/policies/${policy.id}`;
},
FORGOT_PASSWORD: `${URL_PREFIX}/login/forgot`,
API_ONLY_USER: `${URL_PREFIX}/apionlyuser`,
FLEET_403: `${URL_PREFIX}/403`,
@ -42,6 +46,7 @@ export default {
return `${URL_PREFIX}/schedule/manage/teams/${teamId}`;
},
MANAGE_POLICIES: `${URL_PREFIX}/policies/manage`,
NEW_POLICY: `${URL_PREFIX}/policies/new`,
NEW_QUERY: `${URL_PREFIX}/queries/new`,
RESET_PASSWORD: `${URL_PREFIX}/login/reset`,
SETUP: `${URL_PREFIX}/setup`,

View file

@ -1,13 +1,17 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import sendRequest from "services";
import endpoints from "fleet/endpoints";
// import { IPolicyFormData, IPolicy } from "interfaces/policy";
import { IPolicyFormData } from "interfaces/policy";
export default {
create: (query_id: number) => {
create: (data: number | IPolicyFormData) => {
const { GLOBAL_POLICIES } = endpoints;
return sendRequest("POST", GLOBAL_POLICIES, { query_id });
if (typeof data === "number") {
return sendRequest("POST", GLOBAL_POLICIES, { query_id: data });
}
return sendRequest("POST", GLOBAL_POLICIES, data);
},
destroy: (ids: number[]) => {
const { GLOBAL_POLICIES } = endpoints;
@ -15,6 +19,12 @@ export default {
return sendRequest("POST", path, { ids });
},
update: (id: number, data: IPolicyFormData) => {
const { GLOBAL_POLICIES } = endpoints;
const path = `${GLOBAL_POLICIES}/${id}`;
return sendRequest("PATCH", path, data);
},
load: (id: number) => {
const { GLOBAL_POLICIES } = endpoints;
const path = `${GLOBAL_POLICIES}/${id}`;

View file

@ -66,7 +66,7 @@ export const QUERIES_PAGE_STEPS = {
export const DEFAULT_QUERY = {
description: "",
name: "",
query: "SELECT * FROM osquery_info",
query: "SELECT * FROM osquery_info;",
id: 0,
interval: 0,
last_excuted: "",
@ -79,6 +79,19 @@ export const DEFAULT_QUERY = {
packs: [],
};
export const DEFAULT_POLICY = {
id: 1,
name: "Gatekeeper enabled",
query: "SELECT 1 FROM gatekeeper WHERE assessments_enabled = 1;",
description: "Checks if gatekeeper is enabled on macOS devices",
author_id: 42,
author_name: "John",
author_email: "john@example.com",
resolution: "Resolution steps",
passing_host_count: 2000,
failing_host_count: 300,
};
export const DEFAULT_CAMPAIGN = {
created_at: "",
errors: [],

159
go.sum
View file

@ -27,12 +27,9 @@ cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNF
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0 h1:PQcPefKFdaIzjQFbiyOgAqyx8q5djaE7x9Sqe712DPA=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0 h1:/May9ojXjRkPBNVrq+oWLqmWCkr4OU5uRY29bu0mRyQ=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/firestore v1.1.0 h1:9x7Bx0A9R5/M9jibeJeZWqjeVEIxYW9fZYqB9a70/bY=
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
@ -44,15 +41,12 @@ cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiy
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0 h1:STgFzyU5/8miMl0//zKh2aQeTyeaUH3WN9bSUiJ09bA=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9 h1:VpgP7xuJadIUuKccphEpTJnWhS2jkQyMt6Y7pJCD7fY=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/AbGuthrie/goquery/v2 v2.0.1 h1:h0tIhmeRroyqYjT9zxXPXOrheNp1xqNTV+XFWuDI+eA=
github.com/AbGuthrie/goquery/v2 v2.0.1/go.mod h1:xpDLF4kUr+TRFXogclRa7Zzc8bMAB/fYm1zG/XX1WOA=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOCMdNH+CiXJVYJh5UQNCOBG7jbELc=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
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=
@ -82,7 +76,6 @@ github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8
github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
github.com/OpenPeeDeeP/depguard v1.0.1 h1:VlW4R6jmBIv3/u1JNlawEvJMM4J+dPORPaZasQee8Us=
github.com/OpenPeeDeeP/depguard v1.0.1/go.mod h1:xsIw86fROiiwelg+jB2uM9PiKihMMmUx/1V+TNhjQvM=
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6 h1:fLjPD/aNc3UIOA6tDi6QXUemppXK3P9BI7mr2hd6gx8=
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
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=
@ -90,41 +83,30 @@ 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/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8=
github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs=
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
github.com/alecthomas/kingpin v2.2.6+incompatible h1:5svnBTFgJjZvGKyYBtMB0+m5wvrbUHiqye8wRJMlnYI=
github.com/alecthomas/kingpin v2.2.6+incompatible/go.mod h1:59OFYbFVLKQKq+mqrL6Rw5bR0c3ACQaawgXx0QYndlE=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
github.com/andybalholm/brotli v1.0.2 h1:JKnhI/XQ75uFBTiuzXpzFrUriDPiZjlOSzh6wXogP0E=
github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/apache/thrift v0.13.1-0.20200603211036-eac4d0c79a5f h1:33BV5v3u8I6dA2dEoPuXWCsAaHHOJfPtdxZhAMQV4uo=
github.com/apache/thrift v0.13.1-0.20200603211036-eac4d0c79a5f/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3 h1:ZSTrOEhiM5J5RFxEaFvMZVEAM1KvT1YzbEOwB2EAGjA=
github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM=
github.com/apparentlymart/go-textseg v1.0.0 h1:rRmlIsPEEhUTIKQb7T++Nz/A5Q6C9IuX2wFoYVvnCs0=
github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e h1:QEF07wC0T1rKkctt1RINW/+RMTVmiwxETico2l3gxJA=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6 h1:G1bPvciwNyF7IUmKXNt9Ak3m6u9DE1rF+RmtIkBpVdA=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da h1:8GUt8eRujhVEGZFFEjBj46YV4rDjvGrNxb0KMWYkL2I=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310 h1:BUAU3CGlLvorLI26FmByPp2eC2qla6E1Tw+scpcg/to=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
@ -140,10 +122,8 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
github.com/bketelsen/crypt v0.0.4 h1:w/jqZtC9YD4DS/Vp9GhWfWcCpuAL58oTnLoI8vE9YHU=
github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb h1:m935MPodAbYS46DG4pJSv7WO+VECIWUQ7OJYSoTrMh4=
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI=
@ -157,47 +137,32 @@ github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e h1:hHg27A0RS
github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e/go.mod h1:oDpT4efm8tSYHXV5tHSdRvBet/b/QzxZ+XyyPehvm3A=
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/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403 h1:cqQfy1jclcSy/FwLjemeg3SR1yaINm74aQyupQ0Bl8M=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/coreos/bbolt v1.3.2 h1:wZwiHHUieZCquLkDL0B8UhzreNWsPHooDAG3q34zk0s=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/etcd v3.3.13+incompatible h1:8F3hqu9fGYLBifCmRCJsicFqDx/D68Rt3q1JMazcgBQ=
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible h1:bXhRBIXoTm9BYHS3gE0TtQuyNZyeEMux2sDi4oo5YOo=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e h1:Wf6HqHfScWJN9/ZjdUKyjop4mf3Qdd+1TvvltAvM3m8=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f h1:lBNOc5arjvs8E5mO2tbpBpLoyyu8B6e44T7hJy6potg=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
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/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
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/daixiang0/gci v0.2.7 h1:bosLNficubzJZICsVzxuyNc6oAbdz0zcqLG2G/RxtY4=
github.com/daixiang0/gci v0.2.7/go.mod h1:+4dZ7TISfSmqfAGv59ePaHfNzgGtIkHAhhdKggP1JAc=
@ -206,18 +171,15 @@ 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/denis-tingajkin/go-header v0.4.2 h1:jEeSF4sdv8/3cT/WY8AgDHUoItNSoEZ7qg9dX7pc218=
github.com/denis-tingajkin/go-header v0.4.2/go.mod h1:eLRHAVXzE5atsKAnNRDB90WHCFFnBUn4RN0nRcs1LJA=
github.com/denisenkom/go-mssqldb v0.10.0 h1:QykgLZBorFE95+gO3u9esLd0BmbvpWp0/waNNZfHBM8=
github.com/denisenkom/go-mssqldb v0.10.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/dgraph-io/badger/v2 v2.2007.2 h1:EjjK0KqwaFMlPin1ajhP943VPENHJdEz1KLIegjaI3k=
github.com/dgraph-io/badger/v2 v2.2007.2/go.mod h1:26P/7fbL4kUZVEVKLAKXkBXKOydDmM2p1e+NhhnBCAE=
github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de h1:t0UHb5vdojIDUqktM6+xJAfScFBsVpXZmqC9dsgJmeA=
github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E=
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/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954 h1:RMLoZVzv4GliuWafOuPuQDKSm1SJph7uCRnnS61JAn4=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/dnaeon/go-vcr/v2 v2.0.1 h1:KQnAAR6r4GbcJ71KfVxM7qhX85oVj3A0zWbuoxpbYcA=
github.com/dnaeon/go-vcr/v2 v2.0.1/go.mod h1:bklL092gNVdADdsX/u2vDs4wGZ52NSgh7YNcZRSiArs=
@ -239,9 +201,7 @@ github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.m
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d h1:QyzYnTnPE15SQyUeqU6qLbWxMkwyAyu+vGksa0b7j00=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0 h1:EQciDnbrYxy13PgWoY8AqoxGiPrpgBZ1R8UNe3ddc+A=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/facebookincubator/flog v0.0.0-20190930132826-d2511d0ce33c h1:KqlxcP2nuOcMjudCvK0qME2K/aFBDH+xcvYv7HYQaYc=
github.com/facebookincubator/flog v0.0.0-20190930132826-d2511d0ce33c/go.mod h1:QGzNH9ujQ2ZUr/CjDGZGWeDAVStrWNjHeEcjJL96Nuk=
@ -254,9 +214,7 @@ github.com/fatih/color v1.12.0 h1:mRhaKNwANqRgUBGKmnI5ZxEk7QXmjQeCcuYFMX2bfcc=
github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
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/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
@ -276,10 +234,8 @@ github.com/go-git/go-git-fixtures/v4 v4.0.2-0.20200613231340-f56387b50c12 h1:PbK
github.com/go-git/go-git-fixtures/v4 v4.0.2-0.20200613231340-f56387b50c12/go.mod h1:m+ICp2rF3jDhFgEZ/8yziagdT1C+ZpZcrJjappBCDSw=
github.com/go-git/go-git/v5 v5.2.0 h1:YPBLG/3UK1we1ohRkncLjaXWLW+HKp5QNM/jTli2JgI=
github.com/go-git/go-git/v5 v5.2.0/go.mod h1:kh02eMX+wdqqxgNMEyq8YgwlIOsDOa9homkUq1PoTMs=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1 h1:QbL/5oDUmRBzO9/Z7Seo6zf912W/a6Sr4Eu0G/3Jho0=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4 h1:WtGNWLvXpe6ZudgnXrq0barxBImvnnJoMEhXAzcbM0I=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-kit/kit v0.7.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
@ -289,9 +245,7 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0 h1:TrB8swr/68K7m9CcGut2g3UOihhbcbiMAYiuTXdEih4=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-ole/go-ole v1.2.1 h1:2lOsA72HgjxAuMlKpFiCbHTvu44PIVkZ5hqm3RSdI/E=
github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8=
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.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
@ -301,9 +255,7 @@ github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LB
github.com/go-stack/stack v1.7.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
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/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/go-toolsmith/astcast v1.0.0 h1:JojxlmI6STnFVG9yOImLeGREv8W2ocNUM+iOhR6jE7g=
github.com/go-toolsmith/astcast v1.0.0/go.mod h1:mt2OdQTeAQcY4DQgPSArJjHCcOwlX+Wl/kwN+LbLGQ4=
@ -313,7 +265,6 @@ github.com/go-toolsmith/astequal v1.0.0 h1:4zxD8j3JRFNyLN46lodQuqz3xdKSrur7U/sr0
github.com/go-toolsmith/astequal v1.0.0/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY=
github.com/go-toolsmith/astfmt v1.0.0 h1:A0vDDXt+vsvLEdbMFJAUBI/uTbRw1ffOPnxsILnFL6k=
github.com/go-toolsmith/astfmt v1.0.0/go.mod h1:cnWmsOAuq4jJY6Ct5YWlVLmcmLMn1JUPuQIHCY7CJDw=
github.com/go-toolsmith/astinfo v0.0.0-20180906194353-9809ff7efb21 h1:wP6mXeB2V/d1P1K7bZ5vDUO3YqEzcvOREOxZPEu3gVI=
github.com/go-toolsmith/astinfo v0.0.0-20180906194353-9809ff7efb21/go.mod h1:dDStQCHtmZpYOmjRP/8gHHnCCch3Zz3oEgCdZVdtweU=
github.com/go-toolsmith/astp v1.0.0 h1:alXE75TXgcmupDsMK1fRAy0YUzLzqPVvBKoyWV+KPXg=
github.com/go-toolsmith/astp v1.0.0/go.mod h1:RSyrtpVlfTFGDYRbrjyWP1pYu//tSFcvdYrA8meBmLI=
@ -328,20 +279,16 @@ github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b h1:khEcpUM4yFcxg4
github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofrs/flock v0.8.0 h1:MSdYClljsF3PbENUUEx85nkWfJSGfzYI9yEBZOJz6CY=
github.com/gofrs/flock v0.8.0/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v4 v4.0.0 h1:RAqyYixv1p7uEnocuy8P1nru5wprCh/MH2BIlW5z5/o=
github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
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/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@ -355,7 +302,6 @@ github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/mock v1.5.0 h1:jlYHihg//f7RRwuPfptm04yp4s7O6Kw8EZiVYIGcH0g=
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
github.com/golang/protobuf v0.0.0-20181025225059-d3de96c4c28e/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0=
github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@ -415,7 +361,6 @@ github.com/gomodule/redigo v1.8.4/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUz
github.com/gomodule/redigo v1.8.5 h1:nRAxCa+SVsyjSBrtZmG/cqb6VbTmuRzpg/PoTFlpumc=
github.com/gomodule/redigo v1.8.5/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo=
github.com/google/btree v1.0.0/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=
@ -434,12 +379,9 @@ github.com/google/go-github/v37 v37.0.0 h1:rCspN8/6kB1BAJWZfuafvHhyfIo5fkAulaP/3
github.com/google/go-github/v37 v37.0.0/go.mod h1:LM7in3NmXDrX58GbEHy7FtNLbI2JijX93RnMKvWG3m4=
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
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/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0 h1:wCKgOCHuUEVfsaQLpPSJb7VdYCdTVZQAuOdYm1yc/60=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
@ -452,9 +394,7 @@ github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hf
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5 h1:zIaiqGYDQwa4HVx5wGRTXbx38Pqxjemn4BP98wpzpXo=
github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/rpmpack v0.0.0-20201225075926-0a97c2c4b688 h1:jyGRCFMyDK/gKe6QRZQWci5+wEUBFElvxLHs3iwO3hY=
github.com/google/rpmpack v0.0.0-20201225075926-0a97c2c4b688/go.mod h1:+y9lKiqDhR4zkLl+V9h4q0rdyrYVsWWm6LLCQP33DIk=
@ -466,7 +406,6 @@ github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gookit/color v1.3.1 h1:PPD/C7sf8u2L8XQPdPgsWRoAiLQGZEZOzU3cf5IYYUk=
github.com/gookit/color v1.3.1/go.mod h1:R3ogXq2B9rTbXoSHJ1HyUVAZ3poOJHpd9nQmyGZsfvQ=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
@ -494,16 +433,11 @@ github.com/gosuri/uilive v0.0.4 h1:hUEBpQDj8D8jXgtCdBu7sWsy5sbW/5GhuO8KBwJ2jyY=
github.com/gosuri/uilive v0.0.4/go.mod h1:V/epo5LjjlDE5RJUcqx8dbw+zc93y5Ya3yg8tfZ74VI=
github.com/groob/plist v0.0.0-20200425180238-0f631f258c01 h1:0T3XGXebqLj7zSVLng9wX9axQzTEnvj/h6eT7iLfUas=
github.com/groob/plist v0.0.0-20200425180238-0f631f258c01/go.mod h1:itkABA+w2cw7x5nYUS/pLRef6ludkZKOigbROmCTaFw=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0 h1:Iju5GlWwrvL6UBg4zJJt3btmonfrMlCDdsejg4CZE7c=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hashicorp/consul/api v1.1.0 h1:BNQPM9ytxj6jbjjdRPioQ94T6YXriSopn0i8COv6SRA=
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
github.com/hashicorp/consul/sdk v0.1.1 h1:LnuDWGNsoajlhGyHJvuWW6FVqRl8JOTPqS6CPTsYjhY=
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@ -512,24 +446,17 @@ github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtng
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
github.com/hashicorp/go-hclog v0.9.3-0.20191025211905-234833755cb2 h1:STV8OvzphW1vlhPFxcG8d6OIilzBSKRAoWFJt+Onu10=
github.com/hashicorp/go-hclog v0.9.3-0.20191025211905-234833755cb2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3 h1:zKjpN5BK/P5lMYrLmBHdBULWbJ0XpYR+7NGzqkZzoD4=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-retryablehttp v0.6.3 h1:tuulM+WnToeqa05z83YLmKabZxrySOmJAd4mJ+s2Nfg=
github.com/hashicorp/go-retryablehttp v0.6.3/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
github.com/hashicorp/go-rootcerts v1.0.0 h1:Rqb66Oo1X/eSV1x66xbDccZjhJigjg0+e82kpwzSwCI=
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
github.com/hashicorp/go-sockaddr v1.0.0 h1:GeH6tui99pF4NJgfnhp+L6+FfobzVW3Ah46sLo0ICXs=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-syslog v1.0.0 h1:KaodqZuhUoZereWVIYmpUgZysurB1kBLX2j0MwMrUAE=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go.net v0.0.1 h1:sNCoNyDEvN1xa+X0baata4RdcpKwcMS6DH+xwfqPgjw=
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
@ -537,24 +464,17 @@ 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 v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/hcl/v2 v2.0.0 h1:efQznTz+ydmQXq3BOnRa3AXzvCeTq1P4dKj/z5GLlY8=
github.com/hashicorp/hcl/v2 v2.0.0/go.mod h1:oVVDG71tEinNGYCxinCYadcmKU9bglqW9pV3txagJ90=
github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.0 h1:WhIgCr5a7AaVH6jPUwjtRuuE7/RDufnUvzIr48smyxs=
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
github.com/hashicorp/memberlist v0.1.3 h1:EmmoJme1matNzb+hMpDuR/0sbJSUisxyqBGG676r31M=
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/serf v0.8.2 h1:YZ7UKsJv+hKjqGVUUbtE3HNj79Eln2oQ75tniF6iPt0=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/hectane/go-acl v0.0.0-20190604041725-da78bae5fc95 h1:S4qyfL2sEm5Budr4KVMyEniCy+PbS55651I/a+Kn/NQ=
github.com/hectane/go-acl v0.0.0-20190604041725-da78bae5fc95/go.mod h1:QiyDdbZLaJ/mZP4Zwc9g2QsfaEA4o7XvvgZegSci5/E=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw=
github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639 h1:mV02weKRL81bEnm8A0HT1/CAelMQDBuQIfLw8n+d6xI=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
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=
@ -565,7 +485,6 @@ github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NH
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jgautheron/goconst v0.0.0-20201117150253-ccae5bf973f3 h1:7nkB9fLPMwtn/R6qfPcHileL/x9ydlhw8XyDrLI1ZXg=
github.com/jgautheron/goconst v0.0.0-20201117150253-ccae5bf973f3/go.mod h1:aAosetZ5zaeC/2EfMeRswtxUFBpe2Hr7HzkgX4fanO4=
@ -587,14 +506,12 @@ github.com/jonboulle/clockwork v0.2.0/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUB
github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ=
github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
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=
@ -603,7 +520,6 @@ github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 h1:DowS9hvgy
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
github.com/kisielk/errcheck v1.5.0 h1:e8esj/e4R+SAOwFwN+n3zr0nYeCyeweozKfO23MvHzY=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
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=
@ -617,16 +533,11 @@ github.com/kolide/osquery-go v0.0.0-20200604192029-b019be7063ac h1:TI5z/itepBADx
github.com/kolide/osquery-go v0.0.0-20200604192029-b019be7063ac/go.mod h1:rp36fokOKgd/5mOgbvv4fkpdaucQ43mnvb+8BR62Xo8=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
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/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@ -635,7 +546,6 @@ github.com/kulti/thelper v0.1.0 h1:ig1EW6yhDiRNN3dplbhdsW2gTvbxTz9i4q5Rr/tRfpk=
github.com/kulti/thelper v0.1.0/go.mod h1:vMu2Cizjy/grP+jmsvOFDx1kYP6+PD1lqg4Yu5exl2U=
github.com/kunwardeep/paralleltest v1.0.2 h1:/jJRv0TiqPoEy/Y8dQxCFJhD56uS/pnvtatgTZBHokU=
github.com/kunwardeep/paralleltest v1.0.2/go.mod h1:ZPqNm1fVHPllh5LPVujzbVz1JN2GhLxSfY+oqUsvG30=
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4=
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
github.com/kyoh86/exportloopref v0.1.8 h1:5Ry/at+eFdkX9Vsdw3qU4YkvGtzuVfzT4X7S77LoN/M=
github.com/kyoh86/exportloopref v0.1.8/go.mod h1:1tUcJeiioIs7VWe5gcOObrux3lb66+sBqGZrRkMwPgg=
@ -643,7 +553,6 @@ github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.10.1 h1:6VXZrLU0jHBYyAqrSPa+MgPfnSvTPuMgK+k0o5kVFWo=
github.com/lib/pq v1.10.1/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e h1:9MlwzLdW7QSDrhDjFlsEYmxpFyIoXmYRon3dt0io31k=
github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
github.com/macadmins/osquery-extension v0.0.5 h1:bF0xgFRU5oYU4BF5xwOCnQIi70RxeoJN8vzrDWv5CCU=
github.com/macadmins/osquery-extension v0.0.5/go.mod h1:GJLCEZWld1jM8fwLQJ4xixl8XkvWjWgQugRXFvlc/FA=
@ -682,34 +591,25 @@ github.com/mattn/go-sqlite3 v1.14.8 h1:gDp86IdQsN/xWjIEmr9MF6o9mpksUgh0fu+9ByFxz
github.com/mattn/go-sqlite3 v1.14.8/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-tty v0.0.3 h1:5OfyWorkyO7xP52Mq7tB36ajHDG5OHrmBGIS/DtakQI=
github.com/mattn/go-tty v0.0.3/go.mod h1:ihxohKRERHTVzN+aSVRwACLCeqIoZAWpoICkkvrWyR0=
github.com/mattn/goveralls v0.0.2 h1:7eJB6EqsPhRVxvwEXGnqdO2sJI0PTsrWoTMXEk9/OQc=
github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mbilski/exhaustivestruct v1.1.0 h1:4ykwscnAFeHJruT+EY3M3vdeP8uXMh0VV2E61iR7XD8=
github.com/mbilski/exhaustivestruct v1.1.0/go.mod h1:OeTBVxQWoEmB2J2JCHmXWPJ0aksxSUOUy+nvtVEfzXc=
github.com/miekg/dns v1.0.14 h1:9jZdLNd/P4+SfEJ0TNyxYpsK8N4GtfylBLqtbYN1sbA=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/cli v1.0.0 h1:iGBIsUe3+HZ/AD/Vd7DErOt5sU9fa8Uj7A2s1aggv1Y=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ=
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM=
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
github.com/mitchellh/gon v0.2.3 h1:fObN7hD14VacGG++t27GzTW6opP0lwI7TsgTPL55wBo=
github.com/mitchellh/gon v0.2.3/go.mod h1:Ua18ZhqjZHg8VyqZo8kNHAY331ntV6nNJ9mT3s2mIo8=
github.com/mitchellh/gox v0.4.0 h1:lfGJxY7ToLJQjHHwi0EX6uYBdK78egf954SQl13PQJc=
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/iochan v1.0.0 h1:C+X3KsSTLFVBr/tK1eYN/vs4rJcvsiLU338UhYPJWeY=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
@ -723,18 +623,13 @@ github.com/mitchellh/reflectwalk v1.0.1/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx
github.com/mna/redisc v1.3.2 h1:sc9C+nj6qmrTFnsXb70xkjAHpXKtjjBuE6v2UcQV0ZE=
github.com/mna/redisc v1.3.2/go.mod h1:CplIoaSTDi5h9icnj4FLbRgHoNKCHDNJDVRztWDGeSQ=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5 h1:8Q0qkMVC/MmWkpIdlvZgcv2o2jrlF6zqVOh7W5YHdMA=
github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
github.com/moricho/tparallel v0.2.1 h1:95FytivzT6rYzdJLdtfn6m1bfFJylOJK41+lgv/EHf4=
github.com/moricho/tparallel v0.2.1/go.mod h1:fXEIZxG2vdfl0ZF8b42f5a78EhjjD5mX8qUplsoSU4k=
github.com/mozilla/tls-observatory v0.0.0-20200317151703-4fa42e1c2dee h1:1xJ+Xi9lYWLaaP4yB67ah0+548CD3110mCPWhVVjFkI=
github.com/mozilla/tls-observatory v0.0.0-20200317151703-4fa42e1c2dee/go.mod h1:SrKMQvPiws7F7iqYp8/TX+IhxCYhzr6N/1yb8cwHsGk=
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/nakabonne/nestif v0.3.0 h1:+yOViDGhg8ygGrmII72nV9B/zGxY188TYpfolntsaPw=
github.com/nakabonne/nestif v0.3.0/go.mod h1:dI314BppzXjJ4HsCnbo7XzrJHPszZsjnk5wEBSYHI2c=
@ -750,7 +645,6 @@ github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+
github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA=
github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU=
github.com/oklog/ulid v0.3.0/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
@ -774,17 +668,13 @@ github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0C
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/open-policy-agent/opa v0.24.0 h1:fnGOIux+TTGZsC0du1bRBtV8F+KPN55Hks12uE3Fq3E=
github.com/open-policy-agent/opa v0.24.0/go.mod h1:qEyD/i8j+RQettHGp4f86yjrjvv+ZYia+JHCMv2G7wA=
github.com/opencensus-integrations/ocsql v0.1.1 h1:+J5BmLX1kNWCH9/5wJdleej2oRyJrhVEt+FAjq1VqaI=
github.com/opencensus-integrations/ocsql v0.1.1/go.mod h1:ozPYpNVBHZsX33jfoQPO5TlI5lqh0/3R36kirEqJKAM=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c h1:Lgl0gzECD8GnQ5QCWA8o6BtfL6mDH5rQgM4/fX3avOs=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g=
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5dSQ=
github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/peterh/liner v0.0.0-20170211195444-bf27d3ba8e1d h1:zapSxdmZYY6vJWXFKLQ+MkI+agc+HQyfrCGowDSHiKs=
github.com/peterh/liner v0.0.0-20170211195444-bf27d3ba8e1d/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc=
github.com/phayes/checkstyle v0.0.0-20170904204023-bfd46e6a821d h1:CdDQnGF8Nq9ocOS/xlSptM1N3BbrA6/kmaep5ggwaIA=
github.com/phayes/checkstyle v0.0.0-20170904204023-bfd46e6a821d/go.mod h1:3OzsM7FXDQlpCiw2j81fOmAwQLnZnLGXVKUzeKQXIAw=
@ -793,7 +683,6 @@ github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.10.1 h1:VasscCm72135zRysgrJDKsntdmPN+OuU3+nnHYA9wyc=
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
github.com/pkg/term v0.0.0-20190109203006-aa71e9d9e942 h1:A7GG7zcGjl3jqAqGPmcNjd/D9hzL95SuoOQAaFNdLU0=
github.com/pkg/term v0.0.0-20190109203006-aa71e9d9e942/go.mod h1:eCbImbZ95eXtAUIbLAuAVnBnwf83mjf6QIVH8SHYwqQ=
@ -801,7 +690,6 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/polyfloyd/go-errorlint v0.0.0-20201127212506-19bd8db6546f h1:xAw10KgJqG5NJDfmRqJ05Z0IFblKumjtMeyiOLxj3+4=
github.com/polyfloyd/go-errorlint v0.0.0-20201127212506-19bd8db6546f/go.mod h1:wi9BfjxjF/bwiZ701TzmfKu6UKC357IOAtNr0Td0Lvw=
github.com/posener/complete v1.1.1 h1:ccV59UEOTzVDnDUEFdT95ZzHVZ+5+158q8+SJb2QV5w=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/prometheus/client_golang v0.0.0-20181025174421-f30f42803563/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
@ -821,9 +709,7 @@ github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.2.0 h1:wH4vA7pcjKuZzjF7lM8awk4fnuJO6idemZXoKnULUx4=
github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/tsdb v0.7.1 h1:YZcsG11NqnK4czYLrWd9mpEuAJIHVQLwdrleYfszMAA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/quasilyte/go-consistent v0.0.0-20190521200055-c6f3937de18c h1:JoUA0uz9U0FVFq5p4LjEq4C0VgQ0El320s3Ms0V4eww=
github.com/quasilyte/go-consistent v0.0.0-20190521200055-c6f3937de18c/go.mod h1:5STLWrekHfjyYwxBRVRXNOSewLJ3PWfDJd1VyTS21fI=
github.com/quasilyte/go-ruleguard v0.2.0/go.mod h1:2RT/tf0Ce0UDj5y243iWKosQogJd8+1G3Rs2fxmlYnw=
github.com/quasilyte/go-ruleguard v0.2.1 h1:56eRm0daAyny9UhJnmtJW/UyLZQusukBAB8oT8AHKHo=
@ -835,15 +721,12 @@ github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqn
github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 h1:MkV+77GLUNo5oJ0jf870itWm3D0Sjh7+Za9gazKc5LQ=
github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.5.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.6.2 h1:aIihoIOHCiLZHxyoNQ+ABL4NKhFTgKLBdMLyEAh98m0=
github.com/rogpeppe/go-internal v1.6.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rotisserie/eris v0.5.1 h1:SbzZloAUjoKX0eiQW187wop45Q5740Pz212NlIz5mLs=
github.com/rotisserie/eris v0.5.1/go.mod h1:JmkIDhvuvDk1kDFGe5RZ3LXIrkEGEN0E6HskH5BCehE=
github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.20.0 h1:38k9hgtUBdxFwE34yS8rTHmHBa4eN16E4DJlv177LNs=
github.com/rs/zerolog v1.20.0/go.mod h1:IzD0RJ65iWH0w97OQQebJEvTZYvsCUm9WVLWBQrJRjo=
@ -859,13 +742,9 @@ github.com/ryancurrah/gomodguard v1.2.0 h1:YWfhGOrXwLGiqcC/u5EqG6YeS8nh+1fw0HEc8
github.com/ryancurrah/gomodguard v1.2.0/go.mod h1:rNqbC4TOIdUDcVMSIpNNAzTbzXAZa6W5lnUepvuMMgQ=
github.com/ryanrolds/sqlclosecheck v0.3.0 h1:AZx+Bixh8zdUBxUA1NxbxVAS78vTPq4rCb8OUZI9xFw=
github.com/ryanrolds/sqlclosecheck v0.3.0/go.mod h1:1gREqxyTGR3lVtpngyFo3hZAgk0KCtEdgEkHwDbigdA=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f h1:UFr9zpz4xgTnIE5yIMtWAMngCdZ9p/+q6lTbgelo80M=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sassoftware/go-rpmutils v0.0.0-20190420191620-a8f1baeba37b h1:+gCnWOZV8Z/8jehJ2CdqB47Z3S+SREmQcuXkRFLNsiI=
github.com/sassoftware/go-rpmutils v0.0.0-20190420191620-a8f1baeba37b/go.mod h1:am+Fp8Bt506lA3Rk3QCmSqmYmLMnPDhdDUcosQCAx+I=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/sebdah/goldie v1.0.0 h1:9GNhIat69MSlz/ndaBg48vl9dF5fI+NBB6kfOxgfkMc=
github.com/sebdah/goldie v1.0.0/go.mod h1:jXP4hmWywNEwZzhMuv2ccnqTSFpuq8iyQhtQdkkZBH4=
@ -876,15 +755,10 @@ github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c h1:W65qqJCIOVP4jpqPQ0YvHYKwcMEMVWIzWC5iNQQfBTU=
github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c/go.mod h1:/PevMnwAxekIXwN8qQyfc5gl2NlkB3CQlkizAbOkeBs=
github.com/shirou/gopsutil v0.0.0-20190901111213-e4ec7b275ada h1:WokF3GuxBeL+n4Lk4Fa8v9mbdjlrl7bHuneF4N1bk2I=
github.com/shirou/gopsutil v0.0.0-20190901111213-e4ec7b275ada/go.mod h1:WWnYX4lzhCH5h/3YBfyVA3VbLYjlMZZAQcW9ojMexNc=
github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4 h1:udFKJ0aHUL60LboW/A+DfgoHVedieIzIXE8uylPue0U=
github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc=
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e h1:MZM7FHLqUHYI0Y/mQAt3d2aYa0SiNms/hFqC9qJYolM=
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041 h1:llrF3Fs4018ePo4+G/HV/uQUqEI1HMDjCeOf2V6puPc=
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
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/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
@ -898,7 +772,6 @@ github.com/smartystreets/assertions v1.0.0 h1:UVQPSSmc3qtTi+zPPkCXvZX9VvW/xT/NsR
github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4 h1:0HKaf1o97UwFjHH9o5XsHUOF+tqmdA7KEzXLpiyaw0E=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/sonatard/noctx v0.0.1 h1:VC1Qhl6Oxx9vvWo3UDgrGXYCeKCe3Wbw7qAWL6FrmTY=
github.com/sonatard/noctx v0.0.1/go.mod h1:9D2D/EoULe8Yy2joDHJj7bv3sZoq9AaSb8B4lqBjiZI=
@ -948,7 +821,6 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
github.com/tdakkota/asciicheck v0.0.0-20200416190851-d7f85be797a2/go.mod h1:yHp0ai0Z9gUljN3o0xMhYJnH/IcvkdTBOX2fmJ93JEM=
github.com/tdakkota/asciicheck v0.0.0-20200416200610-e657995f937b h1:HxLVTlqcHhFAz3nWUcuvpH7WuOMv8LQoCWmruLfFH2U=
@ -964,14 +836,12 @@ github.com/throttled/throttled/v2 v2.8.0/go.mod h1:q1QyZVQXxb2NUfJ+Hjucmlrsrz9s/
github.com/timakin/bodyclose v0.0.0-20190930140734-f7f2e9bca95e/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk=
github.com/timakin/bodyclose v0.0.0-20200424151742-cb6215831a94 h1:ig99OeTyDwQWhPe2iw9lwfQVF1KB3Q4fpP3X7/2VBG8=
github.com/timakin/bodyclose v0.0.0-20200424151742-cb6215831a94/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5 h1:LnC5Kc/wtumK+WB441p7ynQJzVuNRJiqddSIE3IlSEQ=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/tomarrell/wrapcheck v0.0.0-20200807122107-df9e8bcb914d/go.mod h1:yiFB6fFoV7saXirUGfuK+cPtUh4NX/Hf5y2WC2lehu0=
github.com/tomarrell/wrapcheck v0.0.0-20201130113247-1683564d9756 h1:zV5mu0ESwb+WnzqVaW2z1DdbAP0S46UtjY8DHQupQP4=
github.com/tomarrell/wrapcheck v0.0.0-20201130113247-1683564d9756/go.mod h1:yiFB6fFoV7saXirUGfuK+cPtUh4NX/Hf5y2WC2lehu0=
github.com/tommy-muehle/go-mnd v1.3.1-0.20201008215730-16041ac3fe65 h1:Y0bLA422kvb32uZI4fy/Plop/Tbld0l9pSzl+j1FWok=
github.com/tommy-muehle/go-mnd v1.3.1-0.20201008215730-16041ac3fe65/go.mod h1:T22e7iRN4LsFPZGyRLRXeF+DWVXFuV9thsyO7NjbbTI=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8 h1:3SVOIvH7Ae1KRYyQWRjXWJEA9sS/c/pjvH++55Gr648=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/ulikunitz/xz v0.5.7/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.9 h1:RsKRIA2MO8x56wkkcd3LbtcE/uMszhb6DpRf+3uwa3I=
@ -989,21 +859,16 @@ github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyC
github.com/valyala/fasthttp v1.16.0/go.mod h1:YOKImeEosDdBPnxc0gy7INqi3m1zK6A+xl6TwOBhHCA=
github.com/valyala/fasthttp v1.31.0 h1:lrauRLII19afgCs2fnWRJ4M5IkV0lo2FqA61uGkNBfE=
github.com/valyala/fasthttp v1.31.0/go.mod h1:2rsYD01CKFrjjsvFxx75KlEUNpWNBY9JWD3K/7o2Cus=
github.com/valyala/quicktemplate v1.6.3 h1:O7EuMwuH7Q94U2CXD6sOX8AYHqQqWtmIk690IhmpkKA=
github.com/valyala/quicktemplate v1.6.3/go.mod h1:fwPzK2fHuYEODzJ9pkw0ipCPNHZ2tD5KW4lOuSdPKzY=
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
github.com/vmihailenco/msgpack v3.3.3+incompatible h1:wapg9xDUZDzGCNFlwc5SqI1rvcciqcxEHac4CYj89xI=
github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4=
github.com/xanzy/ssh-agent v0.3.0 h1:wUMzuKtKilRgBAD1sUb8gOwwRr2FGoBVumcjoOACClI=
github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77 h1:ESFSdwYZvkeru3RtdrYueztKhOBCSAAzS4Gf+k0tEow=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/yashtewari/glob-intersection v0.0.0-20180916065949-5c77d914dd0b h1:vVRagRXf67ESqAb72hG2C/ZwI8NtJF2u2V76EsuOHGY=
github.com/yashtewari/glob-intersection v0.0.0-20180916065949-5c77d914dd0b/go.mod h1:HptNXiXVDcJjXe9SqMd0v2FsL9f8dz4GnXgltU6q/co=
@ -1011,23 +876,15 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5 h1:dPmz1Snjq0kmkz159iL7S6WzdahUTHnHB5M56WFVifs=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/zclconf/go-cty v1.1.0 h1:uJwc9HiBOCpoKIObTQaLR+tsEXx1HBHnOsOOpcdhZgw=
github.com/zclconf/go-cty v1.1.0/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s=
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=
github.com/zwass/kit v0.0.0-20210625184505-ec5b5c5cce9c h1:TWQ2UvXPkhPxI2KmApKBOCaV6yD2N4mlvqFQ/DlPtpQ=
github.com/zwass/kit v0.0.0-20210625184505-ec5b5c5cce9c/go.mod h1:OYYulo9tUqRadRLwB0+LE914sa1ui2yL7OrcU3Q/1XY=
go.etcd.io/bbolt v1.3.2 h1:Z/90sZLPOeCy2PwprqkFa25PdkusRzaj9P8zm/KNyvk=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/etcd/api/v3 v3.5.0 h1:GsV3S+OfZEOCNXdtNkBSR7kgLobAa/SO6tCxRa0GAYw=
go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
go.etcd.io/etcd/client/pkg/v3 v3.5.0 h1:2aQv6F436YnN7I4VbI8PPYrBhu+SmrTaADcf8Mi/6PU=
go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
go.etcd.io/etcd/client/v2 v2.305.0 h1:ftQ0nOOHMcbMS3KIaDQ0g5Qcd6bhaBrQT6b89DfwLTs=
go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
@ -1039,15 +896,10 @@ go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M=
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.17.0 h1:MTjgFu6ZLKvY6Pvaqk97GlxNBuMpV4Hy/3P6tRGlI2U=
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
@ -1078,10 +930,8 @@ golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u0
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6 h1:QE6XYQK6naiK1EPAe1g/ILLxN5RBoH5xkJk3CqlMI/Y=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b h1:+qEpEAPhDZ1o0x3tHzZTQDArnOixOzGD9HUJfcg0mb4=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181023182221-1baf3a9d7d67/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@ -1098,7 +948,6 @@ golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPI
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug=
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028 h1:4+4C/Iv2U4fMZBiMCc98MG1In4gJY5YRhtpDNeDeHWs=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
@ -1270,7 +1119,6 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 h1:NusfzzA6yGQ+ua51ck7E3omNUX/JuqbFSaRGqU8CcLI=
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@ -1478,16 +1326,13 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
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-20180628173108-788fd7840127/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/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
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=
@ -1496,7 +1341,6 @@ gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU=
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
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/resty.v1 v1.12.0 h1:CuXP0Pjfw9rOuY6EP+UvtNvt5DSqHpIxILZKT/quCZI=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
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=
@ -1535,9 +1379,6 @@ mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b h1:DxJ5nJdkhDlLok9K6qO+5290kphD
mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4=
mvdan.cc/unparam v0.0.0-20200501210554-b37ab49443f7 h1:kAREL6MPwpsk1/PQPFD3Eg7WAQR5mPTWZJaBiG5LDbY=
mvdan.cc/unparam v0.0.0-20200501210554-b37ab49443f7/go.mod h1:HGC5lll35J70Y5v7vCGb9oLhHoScFwkHDJm/05RdSTc=
rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0 h1:9JKUTTIUgS6kzR9mK1YuGKv6Nl+DijDNIc0ghT58FaY=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

View file

@ -474,7 +474,11 @@ func TestAuthorizePolicies(t *testing.T) {
t.Parallel()
policy := &fleet.Policy{}
teamPolicy := &fleet.Policy{TeamID: ptr.Uint(1)}
teamPolicy := &fleet.Policy{
PolicyData: fleet.PolicyData{
TeamID: ptr.Uint(1),
},
}
runTestCases(t, []authTestCase{
{user: test.UserNoRoles, object: policy, action: write, allow: false},

View file

@ -2,6 +2,7 @@ package mysql
import (
"fmt"
"strconv"
"github.com/VividCortex/mysqlerr"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
@ -59,6 +60,9 @@ type existsError struct {
}
func alreadyExists(kind string, identifier interface{}) error {
if s, ok := identifier.(string); ok {
identifier = strconv.Quote(s)
}
return &existsError{
Identifier: identifier,
ResourceType: kind,

View file

@ -996,16 +996,14 @@ func (d *Datastore) DeleteHosts(ctx context.Context, ids []uint) error {
func (d *Datastore) ListPoliciesForHost(ctx context.Context, hid uint) (packs []*fleet.HostPolicy, err error) {
// instead of using policy_membership, we use the same query but with `where host_id=?` in the subquery
// if we don't do this, the subquery does a full table scan because the where at the end doesn't affect it
query := `SELECT
p.id,
p.query_id,
q.name AS query_name,
query := `SELECT p.*,
COALESCE(u.name, '<deleted>') AS author_name,
COALESCE(u.email, '') AS author_email,
CASE
WHEN pm.passes = 1 THEN 'pass'
WHEN pm.passes = 0 THEN 'fail'
ELSE ''
END AS response,
q.description,
coalesce(p.resolution, '') as resolution
FROM policies p
LEFT JOIN (
@ -1013,7 +1011,7 @@ func (d *Datastore) ListPoliciesForHost(ctx context.Context, hid uint) (packs []
SELECT max(id) AS id FROM policy_membership_history WHERE host_id=? GROUP BY host_id, policy_id
)
) as pm ON (p.id=pm.policy_id)
JOIN queries q ON (p.query_id=q.id)`
LEFT JOIN users u ON p.author_id = u.id`
var policies []*fleet.HostPolicy
if err := sqlx.SelectContext(ctx, d.reader, &policies, query, hid); err != nil {

View file

@ -1676,6 +1676,7 @@ func testHostsTotalAndUnseenSince(t *testing.T, ds *Datastore) {
}
func testHostsListByPolicy(t *testing.T, ds *Datastore) {
user1 := test.NewUser(t, ds, "Alice", "alice@example.com", true)
for i := 0; i < 10; i++ {
_, err := ds.NewHost(context.Background(), &fleet.Host{
DetailUpdatedAt: time.Now(),
@ -1693,7 +1694,9 @@ func testHostsListByPolicy(t *testing.T, ds *Datastore) {
filter := fleet.TeamFilter{User: test.UserAdmin}
q := test.NewQuery(t, ds, "query1", "select 1", 0, true)
p, err := ds.NewGlobalPolicy(context.Background(), q.ID, "")
p, err := ds.NewGlobalPolicy(context.Background(), &user1.ID, fleet.PolicyPayload{
QueryID: &q.ID,
})
require.NoError(t, err)
// When policy response is null, we list all hosts that haven't reported at all for the policy, or errored out
@ -1768,6 +1771,7 @@ func testHostsListBySoftware(t *testing.T, ds *Datastore) {
}
func testHostsListFailingPolicies(t *testing.T, ds *Datastore) {
user1 := test.NewUser(t, ds, "Alice", "alice@example.com", true)
for i := 0; i < 10; i++ {
_, err := ds.NewHost(context.Background(), &fleet.Host{
DetailUpdatedAt: time.Now(),
@ -1786,9 +1790,13 @@ func testHostsListFailingPolicies(t *testing.T, ds *Datastore) {
q := test.NewQuery(t, ds, "query1", "select 1", 0, true)
q2 := test.NewQuery(t, ds, "query2", "select 1", 0, true)
p, err := ds.NewGlobalPolicy(context.Background(), q.ID, "")
p, err := ds.NewGlobalPolicy(context.Background(), &user1.ID, fleet.PolicyPayload{
QueryID: &q.ID,
})
require.NoError(t, err)
p2, err := ds.NewGlobalPolicy(context.Background(), q2.ID, "")
p2, err := ds.NewGlobalPolicy(context.Background(), &user1.ID, fleet.PolicyPayload{
QueryID: &q2.ID,
})
require.NoError(t, err)
hosts := listHostsCheckCount(t, ds, filter, fleet.HostListOptions{}, 10)
@ -1831,6 +1839,7 @@ func getReads(t *testing.T, ds *Datastore) int {
rows, err := ds.writer.Query("show engine innodb status")
require.NoError(t, err)
defer rows.Close()
r := 0
for rows.Next() {
type_, name, status := "", "", ""
require.NoError(t, rows.Scan(&type_, &name, &status))
@ -1842,12 +1851,15 @@ func getReads(t *testing.T, ds *Datastore) int {
require.Len(t, parts, 4)
read, err := strconv.Atoi(parts[len(parts)-1])
require.NoError(t, err)
return read
r = read
break
}
return 0
require.NoError(t, rows.Err())
return r
}
func testHostsReadsLessRows(t *testing.T, ds *Datastore) {
user1 := test.NewUser(t, ds, "alice", "alice-123@example.com", true)
var hosts []*fleet.Host
for i := 0; i < 10; i++ {
h, err := ds.NewHost(context.Background(), &fleet.Host{
@ -1867,7 +1879,9 @@ func testHostsReadsLessRows(t *testing.T, ds *Datastore) {
h2 := hosts[1]
q := test.NewQuery(t, ds, "query1", "select 1", 0, true)
p, err := ds.NewGlobalPolicy(context.Background(), q.ID, "")
p, err := ds.NewGlobalPolicy(context.Background(), &user1.ID, fleet.PolicyPayload{
QueryID: &q.ID,
})
require.NoError(t, err)
require.NoError(t, ds.RecordPolicyQueryExecutions(context.Background(), h1, map[uint]*bool{p.ID: ptr.Bool(true)}, time.Now(), false))

View file

@ -35,19 +35,26 @@ func Up_20210818151827(tx *sql.Tx) error {
func constraintsForTable(tx *sql.Tx, table string, referencedTables map[string]struct{}) ([]string, error) {
var constraints []string
query := `SELECT DISTINCT CONSTRAINT_NAME, REFERENCED_TABLE_NAME FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE WHERE TABLE_NAME = ? AND CONSTRAINT_NAME <> 'PRIMARY'`
query := `SELECT DISTINCT CONSTRAINT_NAME, REFERENCED_TABLE_NAME
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
WHERE TABLE_NAME = ? AND CONSTRAINT_SCHEMA = DATABASE() AND CONSTRAINT_NAME <> 'PRIMARY'`
rows, err := tx.Query(query, table) //nolint
if err != nil {
return nil, errors.Wrap(err, "getting fk for scheduled_query_stats")
return nil, errors.Wrapf(err, "getting fk for %s", table)
}
for rows.Next() {
var constraintName string
var referencedTable string
var referencedTable sql.NullString
err := rows.Scan(&constraintName, &referencedTable)
if err != nil {
return nil, errors.Wrap(err, "scanning fk for scheduled_query_stats")
return nil, errors.Wrapf(err, "scanning fk for %s", table)
}
if _, ok := referencedTables[referencedTable]; ok {
if !referencedTable.Valid {
// REFERENCED_TABLE_NAME is NULL if the constraint
// is applied to columns of the current table.
continue
}
if _, ok := referencedTables[referencedTable.String]; ok {
constraints = append(constraints, constraintName)
}
}

View file

@ -0,0 +1,122 @@
package tables
import (
"database/sql"
"fmt"
"github.com/jmoiron/sqlx"
"github.com/pkg/errors"
)
func init() {
MigrationClient.AddMigration(Up_20211116184030, Down_20211116184030)
}
func Up_20211116184030(tx *sql.Tx) error {
if _, err := tx.Exec(`ALTER TABLE policies
ADD COLUMN name VARCHAR(255) NOT NULL,
ADD COLUMN query mediumtext NOT NULL,
ADD COLUMN description mediumtext NOT NULL,
ADD COLUMN author_id int(10) unsigned DEFAULT NULL,
ADD KEY idx_policies_author_id (author_id),
ADD KEY idx_policies_team_id (team_id),
ADD CONSTRAINT policies_queries_ibfk_1 FOREIGN KEY (author_id) REFERENCES users (id) ON DELETE SET NULL
`); err != nil {
return errors.Wrap(err, "adding new columns to 'policies'")
}
// Migrate existing referenced policies to be propietary policy queries.
if _, err := tx.Exec(`
UPDATE policies p
JOIN queries q
ON p.query_id = q.id
SET
p.name = q.name,
p.query = q.query,
p.description = q.description,
p.author_id = q.author_id
`); err != nil {
return errors.Wrap(err, "migrating data from 'queries' to 'policies'")
}
// Legacy policy functionality allowed creating two policies with the same
// referenced query in "queries" (same query_id). The following will
// rename such conflicting policies by appending a " (id)" suffix.
var queryIDs []uint
txx := sqlx.Tx{Tx: tx}
if err := txx.Select(&queryIDs, `
SELECT query_id FROM policies
GROUP BY query_id
HAVING COUNT(*) > 1
`); err != nil {
return errors.Wrap(err, "getting duplicates from 'policies'")
}
if len(queryIDs) == 0 {
// append 0 to avoid empty args error for `sqlx.In`
queryIDs = append(queryIDs, 0)
}
query, args, err := sqlx.In(`
UPDATE policies
SET name = CONCAT(name, " (", CONVERT(id, CHAR) ,")")
WHERE query_id IN (?)`,
queryIDs,
)
if err != nil {
return errors.Wrap(err, "error building query to rename duplicates from 'policies'")
}
if _, err := txx.Exec(query, args...); err != nil {
return errors.Wrap(err, "renaming duplicates from 'policies'")
}
// We need to add the unique key after the population of the name field (otherwise
// the creation of the unique key fails because of the empty names).
if _, err := tx.Exec(
`ALTER TABLE policies ADD UNIQUE KEY idx_policies_unique_name (name);`,
); err != nil {
return errors.Wrap(err, "adding idx_policies_unique_name")
}
// Removing foreign key to the "queries" table.
table := "policies"
referencedTables := map[string]struct{}{"queries": {}}
constraints, err := constraintsForTable(tx, table, referencedTables)
if err != nil {
return errors.Wrap(err, "getting references to queries table")
}
for _, constraint := range constraints {
_, err = tx.Exec(fmt.Sprintf(`ALTER TABLE policies DROP FOREIGN KEY %s;`, constraint))
if err != nil {
return errors.Wrapf(err, "dropping fk %s", constraint)
}
}
// Drop index and column "query_id".
indexName, err := indexNameByColumnName(tx, "policies", "query_id")
if err != nil {
return errors.Wrap(err, "getting index name to query_id")
}
if _, err := tx.Exec(`ALTER TABLE policies DROP KEY ` + indexName); err != nil {
return errors.Wrap(err, "dropping query_id index")
}
if _, err := tx.Exec(`ALTER TABLE policies DROP COLUMN query_id`); err != nil {
return errors.Wrap(err, "dropping query_id column")
}
return nil
}
func Down_20211116184030(tx *sql.Tx) error {
return nil
}
func indexNameByColumnName(tx *sql.Tx, table, column string) (string, error) {
const query = `SELECT INDEX_NAME FROM INFORMATION_SCHEMA.STATISTICS
WHERE TABLE_NAME = ? AND COLUMN_NAME = ? AND TABLE_SCHEMA = DATABASE();`
row := tx.QueryRow(query, table, column)
var indexName string
err := row.Scan(&indexName)
if err != nil {
return "", errors.Wrapf(err, "scanning for index: %s:%s", table, column)
}
return indexName, nil
}

View file

@ -2,7 +2,6 @@ package mysql
import (
"context"
"database/sql"
"fmt"
"sort"
"strings"
@ -13,16 +12,32 @@ import (
"github.com/jmoiron/sqlx"
)
func (ds *Datastore) NewGlobalPolicy(ctx context.Context, queryID uint, resolution string) (*fleet.Policy, error) {
res, err := ds.writer.ExecContext(ctx, `INSERT INTO policies (query_id, resolution) VALUES (?, ?)`, queryID, resolution)
if err != nil {
func (ds *Datastore) NewGlobalPolicy(ctx context.Context, authorID *uint, args fleet.PolicyPayload) (*fleet.Policy, error) {
if args.QueryID != nil {
q, err := ds.Query(ctx, *args.QueryID)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "fetching query from id")
}
args.Name = q.Name
args.Query = q.Query
args.Description = q.Description
}
res, err := ds.writer.ExecContext(ctx,
`INSERT INTO policies (name, query, description, resolution, author_id) VALUES (?, ?, ?, ?, ?)`,
args.Name, args.Query, args.Description, args.Resolution, authorID,
)
switch {
case err == nil:
// OK
case isDuplicate(err):
return nil, ctxerr.Wrap(ctx, alreadyExists("Policy", args.Name))
default:
return nil, ctxerr.Wrap(ctx, err, "inserting new policy")
}
lastIdInt64, err := res.LastInsertId()
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "getting last id after inserting policy")
}
return policyDB(ctx, ds.writer, uint(lastIdInt64), nil)
}
@ -40,12 +55,14 @@ func policyDB(ctx context.Context, q sqlx.QueryerContext, id uint, teamID *uint)
var policy fleet.Policy
err := sqlx.GetContext(ctx, q, &policy,
fmt.Sprintf(`SELECT
p.*,
q.name as query_name,
fmt.Sprintf(`SELECT p.*,
COALESCE(u.name, '<deleted>') AS author_name,
COALESCE(u.email, '') AS author_email,
(select count(*) from policy_membership where policy_id=p.id and passes=true) as passing_host_count,
(select count(*) from policy_membership where policy_id=p.id and passes=false) as failing_host_count
FROM policies p JOIN queries q ON (p.query_id=q.id) WHERE p.id=? AND %s`, teamWhere),
FROM policies p
LEFT JOIN users u ON p.author_id = u.id
WHERE p.id=? AND %s`, teamWhere),
args...)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "getting policy")
@ -53,6 +70,27 @@ func policyDB(ctx context.Context, q sqlx.QueryerContext, id uint, teamID *uint)
return &policy, nil
}
// SavePolicy updates some fields of the given policy on the datastore.
func (ds *Datastore) SavePolicy(ctx context.Context, p *fleet.Policy) error {
sql := `
UPDATE policies
SET name = ?, query = ?, description = ?, resolution = ?
WHERE id = ?
`
result, err := ds.writer.ExecContext(ctx, sql, p.Name, p.Query, p.Description, p.Resolution, p.ID)
if err != nil {
return ctxerr.Wrap(ctx, err, "updating policy")
}
rows, err := result.RowsAffected()
if err != nil {
return ctxerr.Wrap(ctx, err, "rows affected updating policy")
}
if rows == 0 {
return ctxerr.Wrap(ctx, notFound("Policy").WithID(p.ID))
}
return nil
}
func (ds *Datastore) RecordPolicyQueryExecutions(ctx context.Context, host *fleet.Host, results map[uint]*bool, updated time.Time, deferredSaveHost bool) error {
// Sort the results to have generated SQL queries ordered to minimize
// deadlocks. See https://github.com/fleetdm/fleet/issues/1146.
@ -135,12 +173,14 @@ func listPoliciesDB(ctx context.Context, q sqlx.QueryerContext, teamID *uint) ([
ctx,
q,
&policies,
fmt.Sprintf(`SELECT
p.*,
q.name as query_name,
fmt.Sprintf(`SELECT p.*,
COALESCE(u.name, '<deleted>') AS author_name,
COALESCE(u.email, '') AS author_email,
(select count(*) from policy_membership where policy_id=p.id and passes=true) as passing_host_count,
(select count(*) from policy_membership where policy_id=p.id and passes=false) as failing_host_count
FROM policies p JOIN queries q ON (p.query_id=q.id) WHERE %s`, teamWhere), args...,
FROM policies p
LEFT JOIN users u ON p.author_id = u.id
WHERE %s`, teamWhere), args...,
)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "listing policies")
@ -181,7 +221,7 @@ func (ds *Datastore) PolicyQueriesForHost(ctx context.Context, host *fleet.Host)
ctx,
ds.reader,
&globalRows,
`SELECT p.id, q.query FROM policies p JOIN queries q ON (p.query_id=q.id) WHERE team_id is NULL`,
`SELECT p.id, p.query FROM policies p WHERE team_id is NULL`,
)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "selecting policies for host")
@ -194,7 +234,7 @@ func (ds *Datastore) PolicyQueriesForHost(ctx context.Context, host *fleet.Host)
ctx,
ds.reader,
&teamRows,
`SELECT p.id, q.query FROM policies p JOIN queries q ON (p.query_id=q.id) WHERE team_id = ?`,
`SELECT p.id, p.query FROM policies p WHERE team_id = ?`,
*host.TeamID,
)
if err != nil {
@ -213,17 +253,32 @@ func (ds *Datastore) PolicyQueriesForHost(ctx context.Context, host *fleet.Host)
return results, nil
}
func (ds *Datastore) NewTeamPolicy(ctx context.Context, teamID uint, queryID uint, resolution string) (*fleet.Policy, error) {
res, err := ds.writer.ExecContext(ctx, `INSERT INTO policies (query_id, team_id, resolution) VALUES (?, ?, ?)`, queryID, teamID, resolution)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "inserting new team policy")
func (ds *Datastore) NewTeamPolicy(ctx context.Context, teamID uint, authorID *uint, args fleet.PolicyPayload) (*fleet.Policy, error) {
if args.QueryID != nil {
q, err := ds.Query(ctx, *args.QueryID)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "fetching query from id")
}
args.Name = q.Name
args.Query = q.Query
args.Description = q.Description
}
res, err := ds.writer.ExecContext(ctx,
`INSERT INTO policies (name, query, description, team_id, resolution, author_id) VALUES (?, ?, ?, ?, ?, ?)`,
args.Name, args.Query, args.Description, teamID, args.Resolution, authorID)
switch {
case err == nil:
// OK
case isDuplicate(err):
return nil, ctxerr.Wrap(ctx, alreadyExists("Policy", args.Name))
default:
return nil, ctxerr.Wrap(ctx, err, "inserting new policy")
}
lastIdInt64, err := res.LastInsertId()
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "getting last id after inserting policy")
}
return policyDB(ctx, ds.writer, uint(lastIdInt64), nil)
return policyDB(ctx, ds.writer, uint(lastIdInt64), &teamID)
}
func (ds *Datastore) ListTeamPolicies(ctx context.Context, teamID uint) ([]*fleet.Policy, error) {
@ -238,45 +293,35 @@ func (ds *Datastore) TeamPolicy(ctx context.Context, teamID uint, policyID uint)
return policyDB(ctx, ds.reader, policyID, &teamID)
}
func (ds *Datastore) ApplyPolicySpecs(ctx context.Context, specs []*fleet.PolicySpec) error {
// ApplyPolicySpecs applies the given policy specs, creating new policies and updating the ones that
// already exist (a policy is identified by its name and the team it belongs to).
//
// NOTE: Similar to ApplyQueries, ApplyPolicySpecs will update the author_id of the policies
// that are updated.
func (ds *Datastore) ApplyPolicySpecs(ctx context.Context, authorID uint, specs []*fleet.PolicySpec) error {
return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
sql := `
INSERT INTO policies (
name,
query,
description,
author_id,
resolution,
team_id
) VALUES ( ?, ?, ?, ?, ?, (SELECT IFNULL(MIN(id), NULL) FROM teams WHERE name = ?) )
ON DUPLICATE KEY UPDATE
name = VALUES(name),
query = VALUES(query),
description = VALUES(description),
author_id = VALUES(author_id),
resolution = VALUES(resolution),
team_id = VALUES(team_id)
`
for _, spec := range specs {
if spec.QueryName == "" {
return ctxerr.New(ctx, "query name must not be empty")
}
// We update by hand because team_id can be null and that means compound index wont work
teamCheck := `team_id is NULL`
args := []interface{}{spec.QueryName}
if spec.Team != "" {
teamCheck = `team_id=(SELECT id FROM teams WHERE name=?)`
args = append(args, spec.Team)
}
row := tx.QueryRowxContext(ctx,
fmt.Sprintf(`SELECT 1 FROM policies WHERE query_id=(SELECT id FROM queries WHERE name=?) AND %s`, teamCheck),
args...,
)
var exists int
err := row.Scan(&exists)
if err != nil && err != sql.ErrNoRows {
return ctxerr.Wrap(ctx, err, "checking policy existence")
}
if exists > 0 {
_, err = tx.ExecContext(ctx,
fmt.Sprintf(`UPDATE policies SET resolution=? WHERE query_id=(SELECT id FROM queries WHERE name=?) AND %s`, teamCheck),
append([]interface{}{spec.Resolution}, args...)...,
)
if err != nil {
return ctxerr.Wrap(ctx, err, "exec ApplyPolicySpecs update")
}
} else {
_, err = tx.ExecContext(ctx,
`INSERT INTO policies (query_id, team_id, resolution) VALUES ((SELECT id FROM queries WHERE name=?), (SELECT id FROM teams WHERE name=?),?)`,
spec.QueryName, spec.Team, spec.Resolution)
if err != nil {
return ctxerr.Wrap(ctx, err, "exec ApplyPolicySpecs insert")
}
if _, err := tx.ExecContext(ctx,
sql, spec.Name, spec.Query, spec.Description, authorID, spec.Resolution, spec.Team,
); err != nil {
return ctxerr.Wrap(ctx, err, "exec ApplyPolicySpecs insert")
}
}
return nil

View file

@ -2,12 +2,14 @@ package mysql
import (
"context"
"errors"
"fmt"
"testing"
"time"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/test"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -19,13 +21,17 @@ func TestPolicies(t *testing.T) {
name string
fn func(t *testing.T, ds *Datastore)
}{
{"NewGlobalPolicy", testPoliciesNewGlobalPolicy},
{"NewGlobalPolicyLegacy", testPoliciesNewGlobalPolicyLegacy},
{"NewGlobalPolicyProprietary", testPoliciesNewGlobalPolicyProprietary},
{"MembershipViewDeferred", func(t *testing.T, ds *Datastore) { testPoliciesMembershipView(true, t, ds) }},
{"MembershipViewNotDeferred", func(t *testing.T, ds *Datastore) { testPoliciesMembershipView(false, t, ds) }},
{"TeamPolicy", testTeamPolicy},
{"TeamPolicyLegacy", testTeamPolicyLegacy},
{"TeamPolicyProprietary", testTeamPolicyProprietary},
{"PolicyQueriesForHost", testPolicyQueriesForHost},
{"TeamPolicyTransfer", testTeamPolicyTransfer},
{"ApplyPolicySpec", testApplyPolicySpec},
{"Save", testPoliciesSave},
{"DelUser", testPoliciesDelUser},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
@ -35,7 +41,8 @@ func TestPolicies(t *testing.T) {
}
}
func testPoliciesNewGlobalPolicy(t *testing.T, ds *Datastore) {
func testPoliciesNewGlobalPolicyLegacy(t *testing.T, ds *Datastore) {
user1 := test.NewUser(t, ds, "Alice", "alice@example.com", true)
q, err := ds.NewQuery(context.Background(), &fleet.Query{
Name: "query1",
Description: "query1 desc",
@ -43,10 +50,16 @@ func testPoliciesNewGlobalPolicy(t *testing.T, ds *Datastore) {
Saved: true,
})
require.NoError(t, err)
p, err := ds.NewGlobalPolicy(context.Background(), q.ID, "")
p, err := ds.NewGlobalPolicy(context.Background(), &user1.ID, fleet.PolicyPayload{
QueryID: &q.ID,
})
require.NoError(t, err)
assert.Equal(t, "query1", p.QueryName)
assert.Equal(t, "query1", p.Name)
assert.Equal(t, "query1 desc", p.Description)
assert.Equal(t, "select 1;", p.Query)
require.NotNil(t, p.AuthorID)
assert.Equal(t, user1.ID, *p.AuthorID)
q2, err := ds.NewQuery(context.Background(), &fleet.Query{
Name: "query2",
@ -55,17 +68,25 @@ func testPoliciesNewGlobalPolicy(t *testing.T, ds *Datastore) {
Saved: true,
})
require.NoError(t, err)
_, err = ds.NewGlobalPolicy(context.Background(), q2.ID, "")
_, err = ds.NewGlobalPolicy(context.Background(), &user1.ID, fleet.PolicyPayload{
QueryID: &q2.ID,
})
require.NoError(t, err)
policies, err := ds.ListGlobalPolicies(context.Background())
require.NoError(t, err)
require.Len(t, policies, 2)
assert.Equal(t, q.ID, policies[0].QueryID)
assert.Equal(t, q2.ID, policies[1].QueryID)
assert.Equal(t, q.Name, policies[0].Name)
assert.Equal(t, q.Query, policies[0].Query)
assert.Equal(t, q.Description, policies[0].Description)
assert.Equal(t, q2.Name, policies[1].Name)
assert.Equal(t, q2.Query, policies[1].Query)
assert.Equal(t, q2.Description, policies[1].Description)
require.NotNil(t, policies[1].AuthorID)
assert.Equal(t, user1.ID, *policies[1].AuthorID)
// Cannot delete a query if it's in a policy
require.Error(t, ds.DeleteQuery(context.Background(), q.Name))
// The original query can be removed as the policy owns it's own query.
require.NoError(t, ds.DeleteQuery(context.Background(), q.Name))
_, err = ds.DeleteGlobalPolicies(context.Background(), []uint{policies[0].ID, policies[1].ID})
require.NoError(t, err)
@ -73,12 +94,93 @@ func testPoliciesNewGlobalPolicy(t *testing.T, ds *Datastore) {
policies, err = ds.ListGlobalPolicies(context.Background())
require.NoError(t, err)
require.Len(t, policies, 0)
}
// But you can delete the query if the policy is gone
require.NoError(t, ds.DeleteQuery(context.Background(), q.Name))
func testPoliciesNewGlobalPolicyProprietary(t *testing.T, ds *Datastore) {
user1 := test.NewUser(t, ds, "Alice", "alice@example.com", true)
ctx := context.Background()
p, err := ds.NewGlobalPolicy(ctx, &user1.ID, fleet.PolicyPayload{
Name: "query1",
Query: "select 1;",
Description: "query1 desc",
Resolution: "query1 resolution",
})
require.NoError(t, err)
assert.Equal(t, "query1", p.Name)
assert.Equal(t, "query1 desc", p.Description)
assert.Equal(t, "select 1;", p.Query)
require.NotNil(t, p.Resolution)
assert.Equal(t, "query1 resolution", *p.Resolution)
require.NotNil(t, p.AuthorID)
assert.Equal(t, user1.ID, *p.AuthorID)
_, err = ds.NewGlobalPolicy(ctx, &user1.ID, fleet.PolicyPayload{
Name: "query2",
Query: "select 2;",
Description: "query2 desc",
Resolution: "query2 resolution",
})
require.NoError(t, err)
policies, err := ds.ListGlobalPolicies(ctx)
require.NoError(t, err)
require.Len(t, policies, 2)
assert.Equal(t, "query1", policies[0].Name)
assert.Equal(t, "select 1;", policies[0].Query)
assert.Equal(t, "query1 desc", policies[0].Description)
require.NotNil(t, policies[0].Resolution)
assert.Equal(t, "query1 resolution", *policies[0].Resolution)
require.NotNil(t, policies[0].AuthorID)
assert.Equal(t, user1.ID, *policies[0].AuthorID)
assert.Equal(t, "query2", policies[1].Name)
assert.Equal(t, "select 2;", policies[1].Query)
assert.Equal(t, "query2 desc", policies[1].Description)
require.NotNil(t, policies[1].Resolution)
assert.Equal(t, "query2 resolution", *policies[1].Resolution)
require.NotNil(t, policies[1].AuthorID)
assert.Equal(t, user1.ID, *policies[1].AuthorID)
// Can't create a global policy with an existing name.
p3, err := ds.NewGlobalPolicy(ctx, &user1.ID, fleet.PolicyPayload{
Name: "query1",
Query: "select 3;",
Description: "query1 other description",
Resolution: "query1 other resolution",
})
require.Error(t, err)
var isExist interface {
IsExists() bool
}
require.True(t, errors.As(err, &isExist) && isExist.IsExists())
require.Nil(t, p3)
_, err = ds.DeleteGlobalPolicies(ctx, []uint{policies[0].ID, policies[1].ID})
require.NoError(t, err)
policies, err = ds.ListGlobalPolicies(ctx)
require.NoError(t, err)
require.Len(t, policies, 0)
// Now the name is available and we can create the global policy.
p3, err = ds.NewGlobalPolicy(ctx, &user1.ID, fleet.PolicyPayload{
Name: "query1",
Query: "select 3;",
Description: "query1 other description",
Resolution: "query1 other resolution",
})
require.NoError(t, err)
assert.Equal(t, "query1", p3.Name)
assert.Equal(t, "select 3;", p3.Query)
assert.Equal(t, "query1 other description", p3.Description)
require.NotNil(t, p3.Resolution)
assert.Equal(t, "query1 other resolution", *p3.Resolution)
require.NotNil(t, p3.AuthorID)
assert.Equal(t, user1.ID, *p3.AuthorID)
}
func testPoliciesMembershipView(deferred bool, t *testing.T, ds *Datastore) {
user1 := test.NewUser(t, ds, "Alice", "alice@example.com", true)
host1, err := ds.NewHost(context.Background(), &fleet.Host{
OsqueryHostID: "1234",
DetailUpdatedAt: time.Now(),
@ -110,9 +212,17 @@ func testPoliciesMembershipView(deferred bool, t *testing.T, ds *Datastore) {
Saved: true,
})
require.NoError(t, err)
p, err := ds.NewGlobalPolicy(context.Background(), q.ID, "")
p, err := ds.NewGlobalPolicy(context.Background(), &user1.ID, fleet.PolicyPayload{
QueryID: &q.ID,
})
require.NoError(t, err)
assert.Equal(t, "query1", p.Name)
assert.Equal(t, "select 1;", p.Query)
assert.Equal(t, "query1 desc", p.Description)
require.NotNil(t, p.AuthorID)
assert.Equal(t, user1.ID, *p.AuthorID)
q2, err := ds.NewQuery(context.Background(), &fleet.Query{
Name: "query2",
Description: "query2 desc",
@ -120,10 +230,16 @@ func testPoliciesMembershipView(deferred bool, t *testing.T, ds *Datastore) {
Saved: true,
})
require.NoError(t, err)
p2, err := ds.NewGlobalPolicy(context.Background(), q2.ID, "")
p2, err := ds.NewGlobalPolicy(context.Background(), &user1.ID, fleet.PolicyPayload{
QueryID: &q2.ID,
})
require.NoError(t, err)
assert.Equal(t, "query1", p.QueryName)
assert.Equal(t, "query2", p2.Name)
assert.Equal(t, "select 42;", p2.Query)
assert.Equal(t, "query2 desc", p2.Description)
require.NotNil(t, p2.AuthorID)
assert.Equal(t, user1.ID, *p2.AuthorID)
require.NoError(t, ds.RecordPolicyQueryExecutions(context.Background(), host1, map[uint]*bool{p.ID: ptr.Bool(true)}, time.Now(), deferred))
require.NoError(t, ds.RecordPolicyQueryExecutions(context.Background(), host1, map[uint]*bool{p.ID: ptr.Bool(true)}, time.Now(), deferred))
@ -168,7 +284,8 @@ func testPoliciesMembershipView(deferred bool, t *testing.T, ds *Datastore) {
assert.Equal(t, q2.Query, queries[fmt.Sprint(q2.ID)])
}
func testTeamPolicy(t *testing.T, ds *Datastore) {
func testTeamPolicyLegacy(t *testing.T, ds *Datastore) {
user1 := test.NewUser(t, ds, "Alice", "alice@example.com", true)
team1, err := ds.NewTeam(context.Background(), &fleet.Team{Name: "team1"})
require.NoError(t, err)
@ -194,13 +311,23 @@ func testTeamPolicy(t *testing.T, ds *Datastore) {
prevPolicies, err := ds.ListGlobalPolicies(context.Background())
require.NoError(t, err)
_, err = ds.NewTeamPolicy(context.Background(), 99999999, q.ID, "")
_, err = ds.NewTeamPolicy(context.Background(), 99999999, &user1.ID, fleet.PolicyPayload{
QueryID: &q.ID,
})
require.Error(t, err)
p, err := ds.NewTeamPolicy(context.Background(), team1.ID, q.ID, "some resolution")
p, err := ds.NewTeamPolicy(context.Background(), team1.ID, &user1.ID, fleet.PolicyPayload{
QueryID: &q.ID,
Resolution: "some resolution",
})
require.NoError(t, err)
assert.Equal(t, "query1", p.QueryName)
assert.Equal(t, "query1", p.Name)
assert.Equal(t, "select 1;", p.Query)
assert.Equal(t, "query1 desc", p.Description)
require.NotNil(t, p.AuthorID)
assert.Equal(t, user1.ID, *p.AuthorID)
require.NotNil(t, p.Resolution)
assert.Equal(t, "some resolution", *p.Resolution)
@ -208,28 +335,191 @@ func testTeamPolicy(t *testing.T, ds *Datastore) {
require.NoError(t, err)
require.Len(t, globalPolicies, len(prevPolicies))
_, err = ds.NewTeamPolicy(context.Background(), team2.ID, q2.ID, "")
p2, err := ds.NewTeamPolicy(context.Background(), team2.ID, &user1.ID, fleet.PolicyPayload{
QueryID: &q2.ID,
})
require.NoError(t, err)
assert.Equal(t, "query2", p2.Name)
assert.Equal(t, "select 1;", p2.Query)
assert.Equal(t, "query2 desc", p2.Description)
require.NotNil(t, p2.AuthorID)
assert.Equal(t, user1.ID, *p2.AuthorID)
teamPolicies, err := ds.ListTeamPolicies(context.Background(), team1.ID)
require.NoError(t, err)
require.Len(t, teamPolicies, 1)
assert.Equal(t, q.ID, teamPolicies[0].QueryID)
assert.Equal(t, q.Name, teamPolicies[0].Name)
assert.Equal(t, q.Query, teamPolicies[0].Query)
assert.Equal(t, q.Description, teamPolicies[0].Description)
require.NotNil(t, teamPolicies[0].AuthorID)
require.Equal(t, user1.ID, *teamPolicies[0].AuthorID)
team2Policies, err := ds.ListTeamPolicies(context.Background(), team2.ID)
require.NoError(t, err)
require.Len(t, team2Policies, 1)
assert.Equal(t, q2.ID, team2Policies[0].QueryID)
assert.Equal(t, q2.Name, team2Policies[0].Name)
assert.Equal(t, q2.Query, team2Policies[0].Query)
assert.Equal(t, q2.Description, team2Policies[0].Description)
require.NotNil(t, team2Policies[0].AuthorID)
require.Equal(t, user1.ID, *team2Policies[0].AuthorID)
_, err = ds.DeleteTeamPolicies(context.Background(), team1.ID, []uint{teamPolicies[0].ID})
require.NoError(t, err)
teamPolicies, err = ds.ListGlobalPolicies(context.Background())
teamPolicies, err = ds.ListTeamPolicies(context.Background(), team1.ID)
require.NoError(t, err)
require.Len(t, teamPolicies, 0)
}
func testTeamPolicyProprietary(t *testing.T, ds *Datastore) {
user1 := test.NewUser(t, ds, "Alice", "alice@example.com", true)
team1, err := ds.NewTeam(context.Background(), &fleet.Team{Name: "team1"})
require.NoError(t, err)
team2, err := ds.NewTeam(context.Background(), &fleet.Team{Name: "team2"})
require.NoError(t, err)
ctx := context.Background()
_, err = ds.NewGlobalPolicy(ctx, &user1.ID, fleet.PolicyPayload{
Name: "existing-query-global-1",
Query: "select 1;",
Description: "query1 desc",
Resolution: "query1 resolution",
})
require.NoError(t, err)
prevPolicies, err := ds.ListGlobalPolicies(ctx)
require.NoError(t, err)
_, err = ds.NewTeamPolicy(ctx, 99999999, &user1.ID, fleet.PolicyPayload{
Name: "query1",
Query: "select 1;",
Description: "query1 desc",
Resolution: "query1 resolution",
})
require.Error(t, err)
p, err := ds.NewTeamPolicy(ctx, team1.ID, &user1.ID, fleet.PolicyPayload{
Name: "query1",
Query: "select 1;",
Description: "query1 desc",
Resolution: "query1 resolution",
})
require.NoError(t, err)
// Can't create a team policy with an existing name.
_, err = ds.NewTeamPolicy(ctx, team1.ID, &user1.ID, fleet.PolicyPayload{
Name: "query1",
Query: "select 1;",
})
require.Error(t, err)
var isExist interface {
IsExists() bool
}
require.True(t, errors.As(err, &isExist) && isExist.IsExists(), err)
// Can't create a global policy with an existing name.
_, err = ds.NewGlobalPolicy(ctx, &user1.ID, fleet.PolicyPayload{
Name: "query1",
Query: "select 1;",
})
require.Error(t, err)
require.True(t, errors.As(err, &isExist) && isExist.IsExists(), err)
// Can't create a team policy with an existing global name.
_, err = ds.NewTeamPolicy(ctx, team1.ID, &user1.ID, fleet.PolicyPayload{
Name: "existing-query-global-1",
Query: "select 1;",
})
require.Error(t, err)
require.True(t, errors.As(err, &isExist) && isExist.IsExists(), err)
assert.Equal(t, "query1", p.Name)
assert.Equal(t, "select 1;", p.Query)
assert.Equal(t, "query1 desc", p.Description)
require.NotNil(t, p.Resolution)
assert.Equal(t, "query1 resolution", *p.Resolution)
require.NotNil(t, p.AuthorID)
assert.Equal(t, user1.ID, *p.AuthorID)
globalPolicies, err := ds.ListGlobalPolicies(ctx)
require.NoError(t, err)
require.Len(t, globalPolicies, len(prevPolicies))
p2, err := ds.NewTeamPolicy(ctx, team2.ID, &user1.ID, fleet.PolicyPayload{
Name: "query2",
Query: "select 2;",
Description: "query2 desc",
Resolution: "query2 resolution",
})
require.NoError(t, err)
assert.Equal(t, "query2", p2.Name)
assert.Equal(t, "select 2;", p2.Query)
assert.Equal(t, "query2 desc", p2.Description)
require.NotNil(t, p2.Resolution)
assert.Equal(t, "query2 resolution", *p2.Resolution)
require.NotNil(t, p2.AuthorID)
assert.Equal(t, user1.ID, *p2.AuthorID)
teamPolicies, err := ds.ListTeamPolicies(ctx, team1.ID)
require.NoError(t, err)
require.Len(t, teamPolicies, 1)
assert.Equal(t, "query1", teamPolicies[0].Name)
assert.Equal(t, "select 1;", teamPolicies[0].Query)
assert.Equal(t, "query1 desc", teamPolicies[0].Description)
require.NotNil(t, teamPolicies[0].Resolution)
assert.Equal(t, "query1 resolution", *teamPolicies[0].Resolution)
require.NotNil(t, teamPolicies[0].AuthorID)
require.Equal(t, user1.ID, *teamPolicies[0].AuthorID)
team2Policies, err := ds.ListTeamPolicies(context.Background(), team2.ID)
require.NoError(t, err)
require.Len(t, team2Policies, 1)
assert.Equal(t, "query2", team2Policies[0].Name)
assert.Equal(t, "select 2;", team2Policies[0].Query)
assert.Equal(t, "query2 desc", team2Policies[0].Description)
require.NotNil(t, team2Policies[0].Resolution)
assert.Equal(t, "query2 resolution", *team2Policies[0].Resolution)
require.NotNil(t, team2Policies[0].AuthorID)
require.Equal(t, user1.ID, *team2Policies[0].AuthorID)
// Can't create a policy with the same name on the same team.
p3, err := ds.NewTeamPolicy(ctx, team1.ID, &user1.ID, fleet.PolicyPayload{
Name: "query1",
Query: "select 2;",
Description: "query2 other description",
Resolution: "query2 other resolution",
})
require.Error(t, err)
require.Nil(t, p3)
_, err = ds.DeleteTeamPolicies(context.Background(), team1.ID, []uint{teamPolicies[0].ID})
require.NoError(t, err)
teamPolicies, err = ds.ListTeamPolicies(ctx, team1.ID)
require.NoError(t, err)
require.Len(t, teamPolicies, 0)
// Now the name is available and we can create the policy in the team.
_, err = ds.NewTeamPolicy(ctx, team1.ID, &user1.ID, fleet.PolicyPayload{
Name: "query1",
Query: "select 2;",
Description: "query2 other description",
Resolution: "query2 other resolution",
})
require.NoError(t, err)
teamPolicies, err = ds.ListTeamPolicies(ctx, team1.ID)
require.NoError(t, err)
require.Len(t, teamPolicies, 1)
assert.Equal(t, "query1", teamPolicies[0].Name)
assert.Equal(t, "select 2;", teamPolicies[0].Query)
assert.Equal(t, "query2 other description", teamPolicies[0].Description)
require.NotNil(t, teamPolicies[0].Resolution)
assert.Equal(t, "query2 other resolution", *teamPolicies[0].Resolution)
require.NotNil(t, team2Policies[0].AuthorID)
require.Equal(t, user1.ID, *team2Policies[0].AuthorID)
}
func testPolicyQueriesForHost(t *testing.T, ds *Datastore) {
user1 := test.NewUser(t, ds, "Alice", "alice@example.com", true)
team1, err := ds.NewTeam(context.Background(), &fleet.Team{Name: "team1"})
require.NoError(t, err)
@ -268,7 +558,10 @@ func testPolicyQueriesForHost(t *testing.T, ds *Datastore) {
Saved: true,
})
require.NoError(t, err)
gp, err := ds.NewGlobalPolicy(context.Background(), q.ID, "some gp resolution")
gp, err := ds.NewGlobalPolicy(context.Background(), &user1.ID, fleet.PolicyPayload{
QueryID: &q.ID,
Resolution: "some gp resolution",
})
require.NoError(t, err)
q2, err := ds.NewQuery(context.Background(), &fleet.Query{
@ -278,7 +571,10 @@ func testPolicyQueriesForHost(t *testing.T, ds *Datastore) {
Saved: true,
})
require.NoError(t, err)
tp, err := ds.NewTeamPolicy(context.Background(), team1.ID, q2.ID, "some other gp resolution")
tp, err := ds.NewTeamPolicy(context.Background(), team1.ID, &user1.ID, fleet.PolicyPayload{
QueryID: &q2.ID,
Resolution: "some other gp resolution",
})
require.NoError(t, err)
queries, err := ds.PolicyQueriesForHost(context.Background(), host1)
@ -298,10 +594,35 @@ func testPolicyQueriesForHost(t *testing.T, ds *Datastore) {
require.NoError(t, err)
require.Len(t, policies, 2)
checkPolicies := func(policies []*fleet.HostPolicy) {
assert.Equal(t, "query1", policies[0].Name)
assert.Equal(t, "select 1;", policies[0].Query)
assert.Equal(t, "query1 desc", policies[0].Description)
require.NotNil(t, policies[0].AuthorID)
assert.Equal(t, user1.ID, *policies[0].AuthorID)
assert.Equal(t, "Alice", policies[0].AuthorName)
assert.Equal(t, "alice@example.com", policies[0].AuthorEmail)
assert.NotNil(t, policies[0].Resolution)
assert.Equal(t, "some gp resolution", *policies[0].Resolution)
assert.Equal(t, "query2", policies[1].Name)
assert.Equal(t, "select 42;", policies[1].Query)
assert.Equal(t, "query2 desc", policies[1].Description)
require.NotNil(t, policies[1].AuthorID)
assert.Equal(t, user1.ID, *policies[1].AuthorID)
assert.Equal(t, "Alice", policies[1].AuthorName)
assert.Equal(t, "alice@example.com", policies[1].AuthorEmail)
assert.NotNil(t, policies[1].Resolution)
assert.Equal(t, "some other gp resolution", *policies[1].Resolution)
}
checkPolicies(policies)
policies, err = ds.ListPoliciesForHost(context.Background(), host2.ID)
require.NoError(t, err)
require.Len(t, policies, 2)
checkPolicies(policies)
assert.Equal(t, "", policies[0].Response)
require.NoError(t, ds.RecordPolicyQueryExecutions(context.Background(), host2, map[uint]*bool{gp.ID: ptr.Bool(true)}, time.Now(), false))
@ -310,10 +631,12 @@ func testPolicyQueriesForHost(t *testing.T, ds *Datastore) {
require.NoError(t, err)
require.Len(t, policies, 2)
checkPolicies(policies)
assert.Equal(t, "pass", policies[0].Response)
// insert a null resolution
res, err := ds.writer.ExecContext(context.Background(), `INSERT INTO policies (query_id) VALUES (?)`, q.ID)
// Manually insert a null resolution.
res, err := ds.writer.ExecContext(context.Background(), `INSERT INTO policies (name, query, description) VALUES (?, ?, ?)`, q.Name+"2", q.Query, q.Description)
require.NoError(t, err)
id, err := res.LastInsertId()
require.NoError(t, err)
@ -323,11 +646,16 @@ func testPolicyQueriesForHost(t *testing.T, ds *Datastore) {
require.NoError(t, err)
require.Len(t, policies, 3)
assert.Equal(t, "query1 desc", policies[0].QueryDescription)
assert.Equal(t, "some gp resolution", policies[0].Resolution)
assert.Equal(t, "query1 desc", policies[0].Description)
assert.NotNil(t, policies[0].Resolution)
assert.Equal(t, "some gp resolution", *policies[0].Resolution)
assert.NotNil(t, policies[2].Resolution)
assert.Empty(t, *policies[2].Resolution)
}
func testTeamPolicyTransfer(t *testing.T, ds *Datastore) {
user1 := test.NewUser(t, ds, "Alice", "alice@example.com", true)
team1, err := ds.NewTeam(context.Background(), &fleet.Team{Name: t.Name() + "team1"})
require.NoError(t, err)
@ -350,17 +678,28 @@ func testTeamPolicyTransfer(t *testing.T, ds *Datastore) {
host1, err = ds.Host(context.Background(), host1.ID, false)
require.NoError(t, err)
q, err := ds.NewQuery(context.Background(), &fleet.Query{
tq, err := ds.NewQuery(context.Background(), &fleet.Query{
Name: "query1",
Description: "query1 desc",
Query: "select 1;",
Saved: true,
})
require.NoError(t, err)
teamPolicy, err := ds.NewTeamPolicy(context.Background(), team1.ID, q.ID, "")
teamPolicy, err := ds.NewTeamPolicy(context.Background(), team1.ID, &user1.ID, fleet.PolicyPayload{
QueryID: &tq.ID,
})
require.NoError(t, err)
globalPolicy, err := ds.NewGlobalPolicy(context.Background(), q.ID, "")
gq, err := ds.NewQuery(context.Background(), &fleet.Query{
Name: "query2",
Description: "query2 desc",
Query: "select 2;",
Saved: true,
})
require.NoError(t, err)
globalPolicy, err := ds.NewGlobalPolicy(context.Background(), &user1.ID, fleet.PolicyPayload{
QueryID: &gq.ID,
})
require.NoError(t, err)
require.NoError(t, ds.RecordPolicyQueryExecutions(context.Background(), host1, map[uint]*bool{teamPolicy.ID: ptr.Bool(false), globalPolicy.ID: ptr.Bool(true)}, time.Now(), false))
@ -391,96 +730,274 @@ func testTeamPolicyTransfer(t *testing.T, ds *Datastore) {
}
func testApplyPolicySpec(t *testing.T, ds *Datastore) {
team1, err := ds.NewTeam(context.Background(), &fleet.Team{Name: "team1"})
user1 := test.NewUser(t, ds, "User1", "user1@example.com", true)
ctx := context.Background()
team1, err := ds.NewTeam(ctx, &fleet.Team{Name: "team1"})
require.NoError(t, err)
q, err := ds.NewQuery(context.Background(), &fleet.Query{
Name: "query1",
Description: "query1 desc",
Query: "select 1;",
Saved: true,
})
require.NoError(t, err)
q2, err := ds.NewQuery(context.Background(), &fleet.Query{
Name: "query2",
Description: "query2 desc",
Query: "select 1;",
Saved: true,
})
require.NoError(t, err)
require.NoError(t, ds.ApplyPolicySpecs(context.Background(), []*fleet.PolicySpec{
require.NoError(t, ds.ApplyPolicySpecs(ctx, user1.ID, []*fleet.PolicySpec{
{
QueryName: "query1",
Resolution: "some resolution",
Name: "query1",
Query: "select 1;",
Description: "query1 desc",
Resolution: "some resolution",
Team: "",
},
{
QueryName: "query2",
Resolution: "some other resolution",
Team: "team1",
Name: "query2",
Query: "select 2;",
Description: "query2 desc",
Resolution: "some other resolution",
Team: "team1",
},
{
QueryName: "query1",
Team: "team1",
Name: "query3",
Query: "select 3;",
Description: "query3 desc",
Resolution: "some other good resolution",
Team: "team1",
},
}))
policies, err := ds.ListGlobalPolicies(context.Background())
policies, err := ds.ListGlobalPolicies(ctx)
require.NoError(t, err)
require.Len(t, policies, 1)
assert.Equal(t, q.ID, policies[0].QueryID)
assert.Equal(t, "query1", policies[0].Name)
assert.Equal(t, "select 1;", policies[0].Query)
assert.Equal(t, "query1 desc", policies[0].Description)
require.NotNil(t, policies[0].AuthorID)
assert.Equal(t, user1.ID, *policies[0].AuthorID)
require.NotNil(t, policies[0].Resolution)
assert.Equal(t, "some resolution", *policies[0].Resolution)
teamPolicies, err := ds.ListTeamPolicies(context.Background(), team1.ID)
teamPolicies, err := ds.ListTeamPolicies(ctx, team1.ID)
require.NoError(t, err)
require.Len(t, teamPolicies, 2)
assert.Equal(t, q2.ID, teamPolicies[0].QueryID)
assert.Equal(t, "query2", teamPolicies[0].Name)
assert.Equal(t, "select 2;", teamPolicies[0].Query)
assert.Equal(t, "query2 desc", teamPolicies[0].Description)
require.NotNil(t, teamPolicies[0].AuthorID)
assert.Equal(t, user1.ID, *teamPolicies[0].AuthorID)
require.NotNil(t, teamPolicies[0].Resolution)
assert.Equal(t, "some other resolution", *teamPolicies[0].Resolution)
assert.Equal(t, q.ID, teamPolicies[1].QueryID)
assert.Equal(t, "query3", teamPolicies[1].Name)
assert.Equal(t, "select 3;", teamPolicies[1].Query)
assert.Equal(t, "query3 desc", teamPolicies[1].Description)
require.NotNil(t, teamPolicies[1].AuthorID)
assert.Equal(t, user1.ID, *teamPolicies[1].AuthorID)
require.NotNil(t, teamPolicies[1].Resolution)
assert.Equal(t, "", *teamPolicies[1].Resolution)
require.Error(t, ds.ApplyPolicySpecs(context.Background(), []*fleet.PolicySpec{
{
QueryName: "query13",
},
}))
require.Error(t, ds.ApplyPolicySpecs(context.Background(), []*fleet.PolicySpec{
{
Team: "team1",
},
}))
require.Error(t, ds.ApplyPolicySpecs(context.Background(), []*fleet.PolicySpec{
{
QueryName: "query123",
Team: "team1",
},
}))
assert.Equal(t, "some other good resolution", *teamPolicies[1].Resolution)
// Make sure apply is idempotent
require.NoError(t, ds.ApplyPolicySpecs(context.Background(), []*fleet.PolicySpec{
require.NoError(t, ds.ApplyPolicySpecs(ctx, user1.ID, []*fleet.PolicySpec{
{
QueryName: "query1",
Resolution: "some resolution",
Name: "query1",
Query: "select 1;",
Description: "query1 desc",
Resolution: "some resolution",
Team: "",
},
{
QueryName: "query2",
Resolution: "some other resolution",
Team: "team1",
Name: "query2",
Query: "select 2;",
Description: "query2 desc",
Resolution: "some other resolution",
Team: "team1",
},
{
QueryName: "query1",
Team: "team1",
Name: "query3",
Query: "select 3;",
Description: "query3 desc",
Resolution: "some other good resolution",
Team: "team1",
},
}))
policies, err = ds.ListGlobalPolicies(context.Background())
policies, err = ds.ListGlobalPolicies(ctx)
require.NoError(t, err)
require.Len(t, policies, 1)
teamPolicies, err = ds.ListTeamPolicies(ctx, team1.ID)
require.NoError(t, err)
require.Len(t, teamPolicies, 2)
// Test policy updating.
require.NoError(t, ds.ApplyPolicySpecs(ctx, user1.ID, []*fleet.PolicySpec{
{
Name: "query1",
Query: "select 1 from updated;",
Description: "query1 desc updated",
Resolution: "some resolution updated",
Team: "",
},
{
Name: "query2",
Query: "select 2 from updated;",
Description: "query2 desc updated",
Resolution: "some other resolution updated",
Team: "team1",
},
}))
policies, err = ds.ListGlobalPolicies(ctx)
require.NoError(t, err)
require.Len(t, policies, 1)
assert.Equal(t, "query1", policies[0].Name)
assert.Equal(t, "select 1 from updated;", policies[0].Query)
assert.Equal(t, "query1 desc updated", policies[0].Description)
require.NotNil(t, policies[0].AuthorID)
assert.Equal(t, user1.ID, *policies[0].AuthorID)
require.NotNil(t, policies[0].Resolution)
assert.Equal(t, "some resolution updated", *policies[0].Resolution)
teamPolicies, err = ds.ListTeamPolicies(ctx, team1.ID)
require.NoError(t, err)
require.Len(t, teamPolicies, 2)
assert.Equal(t, "query2", teamPolicies[0].Name)
assert.Equal(t, "select 2 from updated;", teamPolicies[0].Query)
assert.Equal(t, "query2 desc updated", teamPolicies[0].Description)
require.NotNil(t, teamPolicies[0].AuthorID)
assert.Equal(t, user1.ID, *teamPolicies[0].AuthorID)
assert.Equal(t, team1.ID, *teamPolicies[0].TeamID)
require.NotNil(t, teamPolicies[0].Resolution)
assert.Equal(t, "some other resolution updated", *teamPolicies[0].Resolution)
// The following will "move" the policy from global to a team.
require.NoError(t, ds.ApplyPolicySpecs(ctx, user1.ID, []*fleet.PolicySpec{
{
Name: "query1",
Query: "select 53;",
Description: "query1 desc team1",
Resolution: "some resolution team1",
Team: "team1",
},
}))
teamPolicies, err = ds.ListTeamPolicies(ctx, team1.ID)
require.NoError(t, err)
require.Len(t, teamPolicies, 3)
globalPolicies, err := ds.ListGlobalPolicies(ctx)
require.NoError(t, err)
require.Len(t, globalPolicies, 0)
// The following will "move" the policy from team to global.
require.NoError(t, ds.ApplyPolicySpecs(ctx, user1.ID, []*fleet.PolicySpec{
{
Name: "query2",
Query: "select 53;",
Description: "query2 desc global",
Resolution: "some resolution global",
},
}))
teamPolicies, err = ds.ListTeamPolicies(ctx, team1.ID)
require.NoError(t, err)
require.Len(t, teamPolicies, 2)
globalPolicies, err = ds.ListGlobalPolicies(ctx)
require.NoError(t, err)
require.Len(t, globalPolicies, 1)
}
func testPoliciesSave(t *testing.T, ds *Datastore) {
user1 := test.NewUser(t, ds, "User1", "user1@example.com", true)
ctx := context.Background()
team1, err := ds.NewTeam(ctx, &fleet.Team{Name: "team1"})
require.NoError(t, err)
err = ds.SavePolicy(ctx, &fleet.Policy{
PolicyData: fleet.PolicyData{
ID: 99999999,
Name: "non-existent query",
Query: "select 1;",
},
})
require.Error(t, err)
var nfe *notFoundError
require.True(t, errors.As(err, &nfe))
gp, err := ds.NewGlobalPolicy(ctx, &user1.ID, fleet.PolicyPayload{
Name: "global query",
Query: "select 1;",
Description: "global query desc",
Resolution: "global query resolution",
})
require.NoError(t, err)
tp1, err := ds.NewTeamPolicy(ctx, team1.ID, &user1.ID, fleet.PolicyPayload{
Name: "team1 query",
Query: "select 2;",
Description: "team1 query desc",
Resolution: "team1 query resolution",
})
require.NoError(t, err)
// Change name only of a global query.
gp.Name = "global query updated"
err = ds.SavePolicy(ctx, gp)
require.NoError(t, err)
gp, err = ds.Policy(ctx, gp.ID)
require.NoError(t, err)
assert.Equal(t, "global query updated", gp.Name)
assert.Equal(t, "select 1;", gp.Query)
assert.Equal(t, "global query desc", gp.Description)
require.NotNil(t, gp.Resolution)
assert.Equal(t, "global query resolution", *gp.Resolution)
require.NotNil(t, gp.AuthorID)
assert.Equal(t, user1.ID, *gp.AuthorID)
// Change name, query, description and resolution of a team policy.
tp1.Name = "team1 query updated"
tp1.Query = "select 12;"
tp1.Description = "team1 query desc updated"
tp1.Resolution = ptr.String("team1 query resolution updated")
err = ds.SavePolicy(ctx, tp1)
require.NoError(t, err)
tp1, err = ds.Policy(ctx, tp1.ID)
require.NoError(t, err)
assert.Equal(t, "team1 query updated", tp1.Name)
assert.Equal(t, "select 12;", tp1.Query)
assert.Equal(t, "team1 query desc updated", tp1.Description)
require.NotNil(t, tp1.Resolution)
assert.Equal(t, "team1 query resolution updated", *tp1.Resolution)
require.NotNil(t, tp1.AuthorID)
assert.Equal(t, user1.ID, *tp1.AuthorID)
}
func testPoliciesDelUser(t *testing.T, ds *Datastore) {
user1 := test.NewUser(t, ds, "User1", "user1@example.com", true)
user2 := test.NewUser(t, ds, "User2", "user2@example.com", true)
ctx := context.Background()
team1, err := ds.NewTeam(ctx, &fleet.Team{Name: "team1"})
require.NoError(t, err)
gp, err := ds.NewGlobalPolicy(ctx, &user1.ID, fleet.PolicyPayload{
Name: "global query",
Query: "select 1;",
Description: "global query desc",
Resolution: "global query resolution",
})
require.NoError(t, err)
tp, err := ds.NewTeamPolicy(ctx, team1.ID, &user2.ID, fleet.PolicyPayload{
Name: "team1 query",
Query: "select 2;",
Description: "team1 query desc",
Resolution: "team1 query resolution",
})
require.NoError(t, err)
err = ds.DeleteUser(ctx, user1.ID)
require.NoError(t, err)
err = ds.DeleteUser(ctx, user2.ID)
require.NoError(t, err)
tp, err = ds.Policy(ctx, tp.ID)
require.NoError(t, err)
assert.Nil(t, tp.AuthorID)
assert.Equal(t, "<deleted>", tp.AuthorName)
assert.Empty(t, tp.AuthorEmail)
gp, err = ds.Policy(ctx, gp.ID)
require.NoError(t, err)
assert.Nil(t, gp.AuthorID)
assert.Equal(t, "<deleted>", gp.AuthorName)
assert.Empty(t, gp.AuthorEmail)
}

View file

@ -103,7 +103,7 @@ func (d *Datastore) NewQuery(ctx context.Context, query *fleet.Query, opts ...fl
result, err := d.writer.ExecContext(ctx, sqlStatement, query.Name, query.Description, query.Query, query.Saved, query.AuthorID, query.ObserverCanRun)
if err != nil && isDuplicate(err) {
return nil, ctxerr.Wrap(ctx, alreadyExists("Query", 0))
return nil, ctxerr.Wrap(ctx, alreadyExists("Query", query.Name))
} else if err != nil {
return nil, ctxerr.Wrap(ctx, err, "creating new Query")
}

View file

@ -286,9 +286,9 @@ CREATE TABLE `migration_status_tables` (
`tstamp` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `id` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=112 DEFAULT CHARSET=utf8mb4;
) ENGINE=InnoDB AUTO_INCREMENT=113 DEFAULT CHARSET=utf8mb4;
/*!40101 SET character_set_client = @saved_cs_client */;
INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01');
INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01');
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `network_interfaces` (
@ -373,16 +373,20 @@ CREATE TABLE `password_reset_requests` (
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `policies` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`query_id` int(10) unsigned NOT NULL,
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`team_id` int(10) unsigned DEFAULT NULL,
`resolution` text,
`name` varchar(255) NOT NULL,
`query` mediumtext NOT NULL,
`description` mediumtext NOT NULL,
`author_id` int(10) unsigned DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `fk_policies_query_id` (`query_id`),
KEY `fk_policies_team_id` (`team_id`),
CONSTRAINT `policies_ibfk_1` FOREIGN KEY (`query_id`) REFERENCES `queries` (`id`),
CONSTRAINT `policies_ibfk_2` FOREIGN KEY (`team_id`) REFERENCES `teams` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
UNIQUE KEY `idx_policies_unique_name` (`name`),
KEY `idx_policies_author_id` (`author_id`),
KEY `idx_policies_team_id` (`team_id`),
CONSTRAINT `policies_ibfk_2` FOREIGN KEY (`team_id`) REFERENCES `teams` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `policies_queries_ibfk_1` FOREIGN KEY (`author_id`) REFERENCES `users` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
/*!40101 SET character_set_client = @saved_cs_client */;
SET @saved_cs_client = @@character_set_client;

View file

@ -354,15 +354,19 @@ type Datastore interface {
///////////////////////////////////////////////////////////////////////////////
// GlobalPoliciesStore
NewGlobalPolicy(ctx context.Context, queryID uint, resolution string) (*Policy, error)
NewGlobalPolicy(ctx context.Context, authorID *uint, args PolicyPayload) (*Policy, error)
Policy(ctx context.Context, id uint) (*Policy, error)
// SavePolicy updates some fields of the given policy on the datastore.
//
// It is also used to update team policies.
SavePolicy(ctx context.Context, p *Policy) error
RecordPolicyQueryExecutions(ctx context.Context, host *Host, results map[uint]*bool, updated time.Time, deferredSaveHost bool) error
ListGlobalPolicies(ctx context.Context) ([]*Policy, error)
DeleteGlobalPolicies(ctx context.Context, ids []uint) ([]uint, error)
PolicyQueriesForHost(ctx context.Context, host *Host) (map[string]string, error)
ApplyPolicySpecs(ctx context.Context, specs []*PolicySpec) error
ApplyPolicySpecs(ctx context.Context, authorID uint, specs []*PolicySpec) error
// MigrateTables creates and migrates the table schemas
MigrateTables(ctx context.Context) error
@ -376,7 +380,7 @@ type Datastore interface {
///////////////////////////////////////////////////////////////////////////////
// Team Policies
NewTeamPolicy(ctx context.Context, teamID uint, queryID uint, resolution string) (*Policy, error)
NewTeamPolicy(ctx context.Context, teamID uint, authorID *uint, args PolicyPayload) (*Policy, error)
ListTeamPolicies(ctx context.Context, teamID uint) ([]*Policy, error)
DeleteTeamPolicies(ctx context.Context, teamID uint, ids []uint) ([]uint, error)
TeamPolicy(ctx context.Context, teamID uint, policyID uint) (*Policy, error)

View file

@ -1,17 +1,134 @@
package fleet
type Policy struct {
ID uint `json:"id"`
QueryID uint `json:"query_id" db:"query_id"`
QueryName string `json:"query_name" db:"query_name"`
PassingHostCount uint `json:"passing_host_count" db:"passing_host_count"`
FailingHostCount uint `json:"failing_host_count" db:"failing_host_count"`
TeamID *uint `json:"team_id" db:"team_id"`
Resolution *string `json:"resolution,omitempty" db:"resolution"`
import (
"errors"
)
// PolicyPayload holds data for policy creation.
//
// If QueryID is not nil, then Name, Query and Description are ignored
// (such fields are fetched from the queries table).
type PolicyPayload struct {
// QueryID allows creating a policy from an existing query.
//
// Using QueryID is the old way of creating policies.
// Use Query, Name and Description instead.
QueryID *uint
// Name is the name of the policy (ignored if QueryID != nil).
Name string
// Query is the policy query (ignored if QueryID != nil).
Query string
// Description is the policy description text (ignored if QueryID != nil).
Description string
// Resolution indicate the steps needed to solve a failing policy.
Resolution string
}
var (
errPolicyEmptyName = errors.New("policy name cannot be empty")
errPolicyEmptyQuery = errors.New("policy query cannot be empty")
errPolicyIDAndQuerySet = errors.New("both fields \"queryID\" and \"query\" cannot be set")
errPolicyInvalidQuery = errors.New("invalid policy query")
)
// Verify verifies the policy payload is valid.
func (p PolicyPayload) Verify() error {
if p.QueryID != nil {
if p.Query != "" {
return errPolicyIDAndQuerySet
}
} else {
if err := verifyPolicyName(p.Name); err != nil {
return err
}
if err := verifyPolicyQuery(p.Query); err != nil {
return err
}
}
return nil
}
func verifyPolicyName(name string) error {
if name == "" {
return errPolicyEmptyName
}
return nil
}
func verifyPolicyQuery(query string) error {
if query == "" {
return errPolicyEmptyQuery
}
if validateSQLRegexp.MatchString(query) {
return errPolicyInvalidQuery
}
return nil
}
// ModifyPolicyPayload holds data for policy modification.
type ModifyPolicyPayload struct {
// Name is the name of the policy.
Name *string `json:"name"`
// Query is the policy query.
Query *string `json:"query"`
// Description is the policy description text.
Description *string `json:"description"`
// Resolution indicate the steps needed to solve a failing policy.
Resolution *string `json:"resolution"`
}
// Verify verifies the policy payload is valid.
func (p ModifyPolicyPayload) Verify() error {
if p.Name != nil {
if err := verifyPolicyName(*p.Name); err != nil {
return err
}
}
if p.Query != nil {
if err := verifyPolicyQuery(*p.Query); err != nil {
return err
}
}
return nil
}
// PolicyData holds data of a fleet policy.
type PolicyData struct {
// ID is the unique ID of a policy.
ID uint `json:"id"`
// Name is the name of the policy query.
Name string `json:"name" db:"name"`
// Query is the actual query to run on the osquery agents.
Query string `json:"query" db:"query"`
// Description describes the policy.
Description string `json:"description" db:"description"`
// AuthorID is the ID of the author of the policy.
//
// AuthorID is nil if the author is deleted from the system
AuthorID *uint `json:"author_id" db:"author_id"`
// AuthorName is retrieved with a join to the users table in the MySQL backend (using AuthorID).
AuthorName string `json:"author_name" db:"author_name"`
// AuthorEmail is retrieved with a join to the users table in the MySQL backend (using AuthorID).
AuthorEmail string `json:"author_email" db:"author_email"`
// TeamID is the ID of the team the policy belongs to.
// If TeamID is nil, then this is a global policy.
TeamID *uint `json:"team_id" db:"team_id"`
// Resolution describes how to solve a failing policy.
Resolution *string `json:"resolution,omitempty" db:"resolution"`
UpdateCreateTimestamps
}
// Policy is a fleet's policy query.
type Policy struct {
PolicyData
// PassingHostCount is the number of hosts this policy passes on.
PassingHostCount uint `json:"passing_host_count" db:"passing_host_count"`
// FailingHostCount is the number of hosts this policy fails on.
FailingHostCount uint `json:"failing_host_count" db:"failing_host_count"`
}
func (p Policy) AuthzType() string {
return "policy"
}
@ -20,17 +137,33 @@ const (
PolicyKind = "policy"
)
// HostPolicy is a fleet's policy query in the context of a host.
type HostPolicy struct {
ID uint `json:"id" db:"id"`
QueryID uint `json:"query_id" db:"query_id"`
QueryName string `json:"query_name" db:"query_name"`
QueryDescription string `json:"query_description" db:"description"`
Response string `json:"response" db:"response"`
Resolution string `json:"resolution" db:"resolution"`
PolicyData
// Response can be one of the following values:
// - "pass": if the policy was executed and passed.
// - "fail": if the policy was executed and did not pass.
// - "": if the policy did not run yet.
Response string `json:"response" db:"response"`
}
// PolicySpec is used to hold policy data to apply policy specs.
type PolicySpec struct {
QueryName string `json:"query"`
Resolution string `json:"resolution,omitempty"`
Team string `json:"team,omitempty"`
Name string `json:"name"`
Query string `json:"query"`
Description string `json:"description"`
Resolution string `json:"resolution,omitempty"`
Team string `json:"team,omitempty"`
}
// Verify verifies the policy data is valid.
func (p PolicySpec) Verify() error {
if err := verifyPolicyName(p.Name); err != nil {
return err
}
if err := verifyPolicyQuery(p.Query); err != nil {
return err
}
return nil
}

View file

@ -415,9 +415,10 @@ type Service interface {
///////////////////////////////////////////////////////////////////////////////
// GlobalPolicyService
NewGlobalPolicy(ctx context.Context, queryID uint, resolution string) (*Policy, error)
NewGlobalPolicy(ctx context.Context, p PolicyPayload) (*Policy, error)
ListGlobalPolicies(ctx context.Context) ([]*Policy, error)
DeleteGlobalPolicies(ctx context.Context, ids []uint) ([]uint, error)
ModifyGlobalPolicy(ctx context.Context, id uint, p ModifyPolicyPayload) (*Policy, error)
GetPolicyByIDQueries(ctx context.Context, policyID uint) (*Policy, error)
ApplyPolicySpecs(ctx context.Context, policies []*PolicySpec) error
@ -430,8 +431,9 @@ type Service interface {
///////////////////////////////////////////////////////////////////////////////
// Team Policies
NewTeamPolicy(ctx context.Context, teamID uint, queryID uint, resolution string) (*Policy, error)
NewTeamPolicy(ctx context.Context, teamID uint, p PolicyPayload) (*Policy, error)
ListTeamPolicies(ctx context.Context, teamID uint) ([]*Policy, error)
DeleteTeamPolicies(ctx context.Context, teamID uint, ids []uint) ([]uint, error)
ModifyTeamPolicy(ctx context.Context, teamID uint, id uint, p ModifyPolicyPayload) (*Policy, error)
GetTeamPolicyByIDQueries(ctx context.Context, teamID uint, policyID uint) (*Policy, error)
}

View file

@ -145,7 +145,7 @@ type SerialSaveHostFunc func(ctx context.Context, host *fleet.Host) error
type DeleteHostFunc func(ctx context.Context, hid uint) error
type HostFunc func(ctx context.Context, id uint) (*fleet.Host, error)
type HostFunc func(ctx context.Context, id uint, skipLoadingExtras bool) (*fleet.Host, error)
type EnrollHostFunc func(ctx context.Context, osqueryHostId string, nodeKey string, teamID *uint, cooldown time.Duration) (*fleet.Host, error)
@ -283,10 +283,12 @@ type ShouldSendStatisticsFunc func(ctx context.Context, frequency time.Duration)
type RecordStatisticsSentFunc func(ctx context.Context) error
type NewGlobalPolicyFunc func(ctx context.Context, queryID uint, resolution string) (*fleet.Policy, error)
type NewGlobalPolicyFunc func(ctx context.Context, authorID *uint, args fleet.PolicyPayload) (*fleet.Policy, error)
type PolicyFunc func(ctx context.Context, id uint) (*fleet.Policy, error)
type SavePolicyFunc func(ctx context.Context, p *fleet.Policy) error
type RecordPolicyQueryExecutionsFunc func(ctx context.Context, host *fleet.Host, results map[uint]*bool, updated time.Time, deferredSaveHost bool) error
type ListGlobalPoliciesFunc func(ctx context.Context) ([]*fleet.Policy, error)
@ -295,7 +297,7 @@ type DeleteGlobalPoliciesFunc func(ctx context.Context, ids []uint) ([]uint, err
type PolicyQueriesForHostFunc func(ctx context.Context, host *fleet.Host) (map[string]string, error)
type ApplyPolicySpecsFunc func(ctx context.Context, specs []*fleet.PolicySpec) error
type ApplyPolicySpecsFunc func(ctx context.Context, authorID uint, specs []*fleet.PolicySpec) error
type MigrateTablesFunc func(ctx context.Context) error
@ -305,7 +307,7 @@ type MigrationStatusFunc func(ctx context.Context) (*fleet.MigrationStatus, erro
type ListSoftwareFunc func(ctx context.Context, opt fleet.SoftwareListOptions) ([]fleet.Software, error)
type NewTeamPolicyFunc func(ctx context.Context, teamID uint, queryID uint, resolution string) (*fleet.Policy, error)
type NewTeamPolicyFunc func(ctx context.Context, teamID uint, authorID *uint, args fleet.PolicyPayload) (*fleet.Policy, error)
type ListTeamPoliciesFunc func(ctx context.Context, teamID uint) ([]*fleet.Policy, error)
@ -736,6 +738,9 @@ type DataStore struct {
PolicyFunc PolicyFunc
PolicyFuncInvoked bool
SavePolicyFunc SavePolicyFunc
SavePolicyFuncInvoked bool
RecordPolicyQueryExecutionsFunc RecordPolicyQueryExecutionsFunc
RecordPolicyQueryExecutionsFuncInvoked bool
@ -1125,7 +1130,7 @@ func (s *DataStore) DeleteHost(ctx context.Context, hid uint) error {
func (s *DataStore) Host(ctx context.Context, id uint, skipLoadingExtras bool) (*fleet.Host, error) {
s.HostFuncInvoked = true
return s.HostFunc(ctx, id)
return s.HostFunc(ctx, id, skipLoadingExtras)
}
func (s *DataStore) EnrollHost(ctx context.Context, osqueryHostId string, nodeKey string, teamID *uint, cooldown time.Duration) (*fleet.Host, error) {
@ -1468,9 +1473,9 @@ func (s *DataStore) RecordStatisticsSent(ctx context.Context) error {
return s.RecordStatisticsSentFunc(ctx)
}
func (s *DataStore) NewGlobalPolicy(ctx context.Context, queryID uint, resolution string) (*fleet.Policy, error) {
func (s *DataStore) NewGlobalPolicy(ctx context.Context, authorID *uint, args fleet.PolicyPayload) (*fleet.Policy, error) {
s.NewGlobalPolicyFuncInvoked = true
return s.NewGlobalPolicyFunc(ctx, queryID, resolution)
return s.NewGlobalPolicyFunc(ctx, authorID, args)
}
func (s *DataStore) Policy(ctx context.Context, id uint) (*fleet.Policy, error) {
@ -1478,6 +1483,11 @@ func (s *DataStore) Policy(ctx context.Context, id uint) (*fleet.Policy, error)
return s.PolicyFunc(ctx, id)
}
func (s *DataStore) SavePolicy(ctx context.Context, p *fleet.Policy) error {
s.SavePolicyFuncInvoked = true
return s.SavePolicyFunc(ctx, p)
}
func (s *DataStore) RecordPolicyQueryExecutions(ctx context.Context, host *fleet.Host, results map[uint]*bool, updated time.Time, deferredSaveHost bool) error {
s.RecordPolicyQueryExecutionsFuncInvoked = true
return s.RecordPolicyQueryExecutionsFunc(ctx, host, results, updated, deferredSaveHost)
@ -1498,9 +1508,9 @@ func (s *DataStore) PolicyQueriesForHost(ctx context.Context, host *fleet.Host)
return s.PolicyQueriesForHostFunc(ctx, host)
}
func (s *DataStore) ApplyPolicySpecs(ctx context.Context, specs []*fleet.PolicySpec) error {
func (s *DataStore) ApplyPolicySpecs(ctx context.Context, authorID uint, specs []*fleet.PolicySpec) error {
s.ApplyPolicySpecsFuncInvoked = true
return s.ApplyPolicySpecsFunc(ctx, specs)
return s.ApplyPolicySpecsFunc(ctx, authorID, specs)
}
func (s *DataStore) MigrateTables(ctx context.Context) error {
@ -1523,9 +1533,9 @@ func (s *DataStore) ListSoftware(ctx context.Context, opt fleet.SoftwareListOpti
return s.ListSoftwareFunc(ctx, opt)
}
func (s *DataStore) NewTeamPolicy(ctx context.Context, teamID uint, queryID uint, resolution string) (*fleet.Policy, error) {
func (s *DataStore) NewTeamPolicy(ctx context.Context, teamID uint, authorID *uint, args fleet.PolicyPayload) (*fleet.Policy, error) {
s.NewTeamPolicyFuncInvoked = true
return s.NewTeamPolicyFunc(ctx, teamID, queryID, resolution)
return s.NewTeamPolicyFunc(ctx, teamID, authorID, args)
}
func (s *DataStore) ListTeamPolicies(ctx context.Context, teamID uint) ([]*fleet.Policy, error) {

View file

@ -1,6 +1,6 @@
package service
func (c *Client) CreatePolicy(queryID uint, resolution string) error {
func (c *Client) CreatePolicy(queryID *uint, resolution string) error {
req := globalPolicyRequest{
QueryID: queryID,
Resolution: resolution,

View file

@ -2,9 +2,13 @@ package service
import (
"context"
"errors"
"fmt"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/ptr"
)
/////////////////////////////////////////////////////////////////////////////////
@ -12,8 +16,11 @@ import (
/////////////////////////////////////////////////////////////////////////////////
type globalPolicyRequest struct {
QueryID uint `json:"query_id"`
Resolution string `json:"resolution"`
QueryID *uint `json:"query_id"`
Query string `json:"query"`
Name string `json:"name"`
Description string `json:"description"`
Resolution string `json:"resolution"`
}
type globalPolicyResponse struct {
@ -25,19 +32,37 @@ func (r globalPolicyResponse) error() error { return r.Err }
func globalPolicyEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (interface{}, error) {
req := request.(*globalPolicyRequest)
resp, err := svc.NewGlobalPolicy(ctx, req.QueryID, req.Resolution)
resp, err := svc.NewGlobalPolicy(ctx, fleet.PolicyPayload{
QueryID: req.QueryID,
Query: req.Query,
Name: req.Name,
Description: req.Description,
Resolution: req.Resolution,
})
if err != nil {
return globalPolicyResponse{Err: err}, nil
}
return globalPolicyResponse{Policy: resp}, nil
}
func (svc Service) NewGlobalPolicy(ctx context.Context, queryID uint, resolution string) (*fleet.Policy, error) {
func (svc Service) NewGlobalPolicy(ctx context.Context, p fleet.PolicyPayload) (*fleet.Policy, error) {
if err := svc.authz.Authorize(ctx, &fleet.Policy{}, fleet.ActionWrite); err != nil {
return nil, err
}
return svc.ds.NewGlobalPolicy(ctx, queryID, resolution)
vc, ok := viewer.FromContext(ctx)
if !ok {
return nil, errors.New("user must be authenticated to create team policies")
}
if err := p.Verify(); err != nil {
return nil, &badRequestError{
message: fmt.Sprintf("policy payload verification: %s", err),
}
}
policy, err := svc.ds.NewGlobalPolicy(ctx, ptr.Uint(vc.UserID()), p)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "storing policy")
}
return policy, nil
}
/////////////////////////////////////////////////////////////////////////////////
@ -132,8 +157,43 @@ func (svc Service) DeleteGlobalPolicies(ctx context.Context, ids []uint) ([]uint
if err := svc.authz.Authorize(ctx, &fleet.Policy{}, fleet.ActionWrite); err != nil {
return nil, err
}
if len(ids) == 0 {
return nil, nil
}
ids, err := svc.ds.DeleteGlobalPolicies(ctx, ids)
if err != nil {
return nil, err
}
return ids, nil
}
return svc.ds.DeleteGlobalPolicies(ctx, ids)
/////////////////////////////////////////////////////////////////////////////////
// Modify
/////////////////////////////////////////////////////////////////////////////////
type modifyGlobalPolicyRequest struct {
PolicyID uint `url:"policy_id"`
fleet.ModifyPolicyPayload
}
type modifyGlobalPolicyResponse struct {
Policy *fleet.Policy `json:"policy,omitempty"`
Err error `json:"error,omitempty"`
}
func (r modifyGlobalPolicyResponse) error() error { return r.Err }
func modifyGlobalPolicyEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (interface{}, error) {
req := request.(*modifyGlobalPolicyRequest)
resp, err := svc.ModifyGlobalPolicy(ctx, req.PolicyID, req.ModifyPolicyPayload)
if err != nil {
return modifyGlobalPolicyResponse{Err: err}, nil
}
return modifyGlobalPolicyResponse{Policy: resp}, nil
}
func (svc Service) ModifyGlobalPolicy(ctx context.Context, id uint, p fleet.ModifyPolicyPayload) (*fleet.Policy, error) {
return svc.modifyPolicy(ctx, nil, id, p)
}
/////////////////////////////////////////////////////////////////////////////////
@ -162,23 +222,36 @@ func applyPolicySpecsEndpoint(ctx context.Context, request interface{}, svc flee
func (svc Service) ApplyPolicySpecs(ctx context.Context, policies []*fleet.PolicySpec) error {
checkGlobalPolicyAuth := false
for _, policy := range policies {
if err := policy.Verify(); err != nil {
return ctxerr.Wrap(ctx, err, "verifying spec")
}
if policy.Team != "" {
team, err := svc.ds.TeamByName(ctx, policy.Team)
if err != nil {
return ctxerr.Wrap(ctx, err, "getting team by name")
}
if err := svc.authz.Authorize(ctx, &fleet.Policy{TeamID: &team.ID}, fleet.ActionWrite); err != nil {
if err := svc.authz.Authorize(ctx, &fleet.Policy{
PolicyData: fleet.PolicyData{
TeamID: &team.ID,
},
}, fleet.ActionWrite); err != nil {
return err
}
continue
} else {
checkGlobalPolicyAuth = true
}
checkGlobalPolicyAuth = true
}
if checkGlobalPolicyAuth {
if err := svc.authz.Authorize(ctx, &fleet.Policy{}, fleet.ActionWrite); err != nil {
return err
}
}
return svc.ds.ApplyPolicySpecs(ctx, policies)
vc, ok := viewer.FromContext(ctx)
if !ok {
return errors.New("user must be authenticated to apply policies")
}
if err := svc.ds.ApplyPolicySpecs(ctx, vc.UserID(), policies); err != nil {
return ctxerr.Wrap(ctx, err, "applying policy specs")
}
return nil
}

View file

@ -14,8 +14,8 @@ func TestGlobalPoliciesAuth(t *testing.T) {
ds := new(mock.Store)
svc := newTestService(ds, nil, nil)
ds.NewGlobalPolicyFunc = func(ctx context.Context, queryID uint, resolution string) (*fleet.Policy, error) {
return nil, nil
ds.NewGlobalPolicyFunc = func(ctx context.Context, authorID *uint, args fleet.PolicyPayload) (*fleet.Policy, error) {
return &fleet.Policy{}, nil
}
ds.ListGlobalPoliciesFunc = func(ctx context.Context) ([]*fleet.Policy, error) {
return nil, nil
@ -29,11 +29,14 @@ func TestGlobalPoliciesAuth(t *testing.T) {
ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) {
return &fleet.Team{ID: 1}, nil
}
ds.ApplyPolicySpecsFunc = func(ctx context.Context, specs []*fleet.PolicySpec) error {
ds.ApplyPolicySpecsFunc = func(ctx context.Context, authorID uint, specs []*fleet.PolicySpec) error {
return nil
}
ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activityType string, details *map[string]interface{}) error {
return nil
}
var testCases = []struct {
testCases := []struct {
name string
user *fleet.User
shouldFailWrite bool
@ -80,7 +83,10 @@ func TestGlobalPoliciesAuth(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
ctx := viewer.NewContext(context.Background(), viewer.Viewer{User: tt.user})
_, err := svc.NewGlobalPolicy(ctx, 2, "")
_, err := svc.NewGlobalPolicy(ctx, fleet.PolicyPayload{
Name: "query1",
Query: "select 1;",
})
checkAuthErr(t, tt.shouldFailWrite, err)
_, err = svc.ListGlobalPolicies(ctx)
@ -94,7 +100,8 @@ func TestGlobalPoliciesAuth(t *testing.T) {
err = svc.ApplyPolicySpecs(ctx, []*fleet.PolicySpec{
{
QueryName: "query1",
Name: "query2",
Query: "select 1;",
},
})
checkAuthErr(t, tt.shouldFailWrite, err)

View file

@ -693,6 +693,7 @@ func attachNewStyleFleetAPIRoutes(r *mux.Router, svc fleet.Service, opts []kitht
e.GET("/api/v1/fleet/global/policies", listGlobalPoliciesEndpoint, nil)
e.GET("/api/v1/fleet/global/policies/{policy_id}", getPolicyByIDEndpoint, getPolicyByIDRequest{})
e.POST("/api/v1/fleet/global/policies/delete", deleteGlobalPoliciesEndpoint, deleteGlobalPoliciesRequest{})
e.PATCH("/api/v1/fleet/global/policies/{policy_id}", modifyGlobalPolicyEndpoint, modifyGlobalPolicyRequest{})
// Alias /api/v1/fleet/team/ -> /api/v1/fleet/teams/
e.POST("/api/v1/fleet/team/{team_id}/policies", teamPolicyEndpoint, teamPolicyRequest{})
@ -705,6 +706,7 @@ func attachNewStyleFleetAPIRoutes(r *mux.Router, svc fleet.Service, opts []kitht
e.GET("/api/v1/fleet/teams/{team_id}/policies", listTeamPoliciesEndpoint, listTeamPoliciesRequest{})
e.GET("/api/v1/fleet/teams/{team_id}/policies/{policy_id}", getTeamPolicyByIDEndpoint, getTeamPolicyByIDRequest{})
e.POST("/api/v1/fleet/teams/{team_id}/policies/delete", deleteTeamPoliciesEndpoint, deleteTeamPoliciesRequest{})
e.PATCH("/api/v1/fleet/teams/{team_id}/policies/{policy_id}", modifyTeamPolicyEndpoint, modifyTeamPolicyRequest{})
e.POST("/api/v1/fleet/spec/policies", applyPolicySpecsEndpoint, applyPolicySpecsRequest{})

View file

@ -2,9 +2,11 @@ package service
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"reflect"
"testing"
"time"
@ -126,11 +128,17 @@ func (s *integrationTestSuite) TestQueryCreationLogsActivity() {
activities := listActivitiesResponse{}
s.DoJSON("GET", "/api/v1/fleet/activities", nil, http.StatusOK, &activities)
assert.Len(t, activities.Activities, 1)
assert.Equal(t, "Test Name admin1@example.com", activities.Activities[0].ActorFullName)
require.NotNil(t, activities.Activities[0].ActorGravatar)
assert.Equal(t, "http://iii.com", *activities.Activities[0].ActorGravatar)
assert.Equal(t, "created_saved_query", activities.Activities[0].Type)
assert.GreaterOrEqual(t, len(activities.Activities), 1)
found := false
for _, activity := range activities.Activities {
if activity.Type == "created_saved_query" {
found = true
assert.Equal(t, "Test Name admin1@example.com", activity.ActorFullName)
require.NotNil(t, activity.ActorGravatar)
assert.Equal(t, "http://iii.com", *activity.ActorGravatar)
}
}
require.True(t, found)
}
func (s *integrationTestSuite) TestAppConfigAdditionalQueriesCanBeRemoved() {
@ -371,24 +379,32 @@ func (s *integrationTestSuite) TestGlobalPolicies() {
})
require.NoError(t, err)
gpParams := globalPolicyRequest{QueryID: qr.ID, Resolution: "some global resolution"}
gpParams := globalPolicyRequest{
QueryID: &qr.ID,
Resolution: "some global resolution",
}
gpResp := globalPolicyResponse{}
s.DoJSON("POST", "/api/v1/fleet/global/policies", gpParams, http.StatusOK, &gpResp)
require.NotNil(t, gpResp.Policy)
assert.Equal(t, qr.ID, gpResp.Policy.QueryID)
assert.Equal(t, qr.Name, gpResp.Policy.Name)
assert.Equal(t, qr.Query, gpResp.Policy.Query)
assert.Equal(t, qr.Description, gpResp.Policy.Description)
require.NotNil(t, gpResp.Policy.Resolution)
assert.Equal(t, "some global resolution", *gpResp.Policy.Resolution)
policiesResponse := listGlobalPoliciesResponse{}
s.DoJSON("GET", "/api/v1/fleet/global/policies", nil, http.StatusOK, &policiesResponse)
require.Len(t, policiesResponse.Policies, 1)
assert.Equal(t, qr.ID, policiesResponse.Policies[0].QueryID)
assert.Equal(t, qr.Name, policiesResponse.Policies[0].Name)
assert.Equal(t, qr.Query, policiesResponse.Policies[0].Query)
assert.Equal(t, qr.Description, policiesResponse.Policies[0].Description)
singlePolicyResponse := getPolicyByIDResponse{}
singlePolicyURL := fmt.Sprintf("/api/v1/fleet/global/policies/%d", policiesResponse.Policies[0].ID)
s.DoJSON("GET", singlePolicyURL, nil, http.StatusOK, &singlePolicyResponse)
assert.Equal(t, qr.ID, singlePolicyResponse.Policy.QueryID)
assert.Equal(t, qr.Name, singlePolicyResponse.Policy.QueryName)
assert.Equal(t, qr.Name, singlePolicyResponse.Policy.Name)
assert.Equal(t, qr.Query, singlePolicyResponse.Policy.Query)
assert.Equal(t, qr.Description, singlePolicyResponse.Policy.Description)
listHostsURL := fmt.Sprintf("/api/v1/fleet/hosts?policy_id=%d", policiesResponse.Policies[0].ID)
listHostsResp := listHostsResponse{}
@ -626,8 +642,11 @@ func (s *integrationTestSuite) TestListHosts() {
assert.Equal(t, host.ID, resp.Hosts[0].ID)
assert.Equal(t, "foo", resp.Software.Name)
user1 := test.NewUser(t, s.ds, "Alice", "alice@example.com", true)
q := test.NewQuery(t, s.ds, "query1", "select 1", 0, true)
p, err := s.ds.NewGlobalPolicy(context.Background(), q.ID, "")
p, err := s.ds.NewGlobalPolicy(context.Background(), &user1.ID, fleet.PolicyPayload{
QueryID: &q.ID,
})
require.NoError(t, err)
require.NoError(t, s.ds.RecordPolicyQueryExecutions(context.Background(), host, map[uint]*bool{p.ID: ptr.Bool(false)}, time.Now(), false))
@ -718,3 +737,412 @@ func (s *integrationTestSuite) TestGetHostSummary() {
require.Equal(t, uint(1), resp.Platforms[0].HostsCount)
require.Equal(t, team1.ID, *resp.TeamID)
}
func (s *integrationTestSuite) TestGlobalPoliciesProprietary() {
t := s.T()
for i := 0; i < 3; i++ {
_, err := s.ds.NewHost(context.Background(), &fleet.Host{
DetailUpdatedAt: time.Now(),
LabelUpdatedAt: time.Now(),
PolicyUpdatedAt: time.Now(),
SeenTime: time.Now().Add(-time.Duration(i) * time.Minute),
OsqueryHostID: fmt.Sprintf("%s%d", t.Name(), i),
NodeKey: fmt.Sprintf("%s%d", t.Name(), i),
UUID: fmt.Sprintf("%s%d", t.Name(), i),
Hostname: fmt.Sprintf("%sfoo.local%d", t.Name(), i),
})
require.NoError(t, err)
}
qr, err := s.ds.NewQuery(context.Background(), &fleet.Query{
Name: "TestQuery321",
Description: "Some description",
Query: "select * from osquery;",
ObserverCanRun: true,
})
require.NoError(t, err)
// Cannot set both QueryID and Query.
gpParams0 := globalPolicyRequest{
QueryID: &qr.ID,
Query: "select * from osquery;",
}
gpResp0 := globalPolicyResponse{}
s.DoJSON("POST", "/api/v1/fleet/global/policies", gpParams0, http.StatusBadRequest, &gpResp0)
require.Nil(t, gpResp0.Policy)
gpParams := globalPolicyRequest{
Name: "TestQuery3",
Query: "select * from osquery;",
Description: "Some description",
Resolution: "some global resolution",
}
gpResp := globalPolicyResponse{}
s.DoJSON("POST", "/api/v1/fleet/global/policies", gpParams, http.StatusOK, &gpResp)
require.NotNil(t, gpResp.Policy)
require.NotEmpty(t, gpResp.Policy.ID)
assert.Equal(t, "TestQuery3", gpResp.Policy.Name)
assert.Equal(t, "select * from osquery;", gpResp.Policy.Query)
assert.Equal(t, "Some description", gpResp.Policy.Description)
require.NotNil(t, gpResp.Policy.Resolution)
assert.Equal(t, "some global resolution", *gpResp.Policy.Resolution)
assert.NotNil(t, gpResp.Policy.AuthorID)
assert.Equal(t, "Test Name admin1@example.com", gpResp.Policy.AuthorName)
assert.Equal(t, "admin1@example.com", gpResp.Policy.AuthorEmail)
mgpParams := modifyGlobalPolicyRequest{
ModifyPolicyPayload: fleet.ModifyPolicyPayload{
Name: ptr.String("TestQuery4"),
Query: ptr.String("select * from osquery_info;"),
Description: ptr.String("Some description updated"),
Resolution: ptr.String("some global resolution updated"),
},
}
mgpResp := modifyGlobalPolicyResponse{}
s.DoJSON("PATCH", fmt.Sprintf("/api/v1/fleet/global/policies/%d", gpResp.Policy.ID), mgpParams, http.StatusOK, &mgpResp)
require.NotNil(t, gpResp.Policy)
assert.Equal(t, "TestQuery4", mgpResp.Policy.Name)
assert.Equal(t, "select * from osquery_info;", mgpResp.Policy.Query)
assert.Equal(t, "Some description updated", mgpResp.Policy.Description)
require.NotNil(t, mgpResp.Policy.Resolution)
assert.Equal(t, "some global resolution updated", *mgpResp.Policy.Resolution)
ggpResp := getPolicyByIDResponse{}
s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/global/policies/%d", gpResp.Policy.ID), getPolicyByIDRequest{}, http.StatusOK, &ggpResp)
require.NotNil(t, ggpResp.Policy)
assert.Equal(t, "TestQuery4", ggpResp.Policy.Name)
assert.Equal(t, "select * from osquery_info;", ggpResp.Policy.Query)
assert.Equal(t, "Some description updated", ggpResp.Policy.Description)
require.NotNil(t, ggpResp.Policy.Resolution)
assert.Equal(t, "some global resolution updated", *ggpResp.Policy.Resolution)
policiesResponse := listGlobalPoliciesResponse{}
s.DoJSON("GET", "/api/v1/fleet/global/policies", nil, http.StatusOK, &policiesResponse)
require.Len(t, policiesResponse.Policies, 1)
assert.Equal(t, "TestQuery4", policiesResponse.Policies[0].Name)
assert.Equal(t, "select * from osquery_info;", policiesResponse.Policies[0].Query)
assert.Equal(t, "Some description updated", policiesResponse.Policies[0].Description)
require.NotNil(t, policiesResponse.Policies[0].Resolution)
assert.Equal(t, "some global resolution updated", *policiesResponse.Policies[0].Resolution)
listHostsURL := fmt.Sprintf("/api/v1/fleet/hosts?policy_id=%d", policiesResponse.Policies[0].ID)
listHostsResp := listHostsResponse{}
s.DoJSON("GET", listHostsURL, nil, http.StatusOK, &listHostsResp)
require.Len(t, listHostsResp.Hosts, 3)
h1 := listHostsResp.Hosts[0]
h2 := listHostsResp.Hosts[1]
listHostsURL = fmt.Sprintf("/api/v1/fleet/hosts?policy_id=%d&policy_response=passing", policiesResponse.Policies[0].ID)
listHostsResp = listHostsResponse{}
s.DoJSON("GET", listHostsURL, nil, http.StatusOK, &listHostsResp)
require.Len(t, listHostsResp.Hosts, 0)
require.NoError(t, s.ds.RecordPolicyQueryExecutions(context.Background(), h1.Host, map[uint]*bool{policiesResponse.Policies[0].ID: ptr.Bool(true)}, time.Now(), false))
require.NoError(t, s.ds.RecordPolicyQueryExecutions(context.Background(), h2.Host, map[uint]*bool{policiesResponse.Policies[0].ID: nil}, time.Now(), false))
listHostsURL = fmt.Sprintf("/api/v1/fleet/hosts?policy_id=%d&policy_response=passing", policiesResponse.Policies[0].ID)
listHostsResp = listHostsResponse{}
s.DoJSON("GET", listHostsURL, nil, http.StatusOK, &listHostsResp)
require.Len(t, listHostsResp.Hosts, 1)
deletePolicyParams := deleteGlobalPoliciesRequest{IDs: []uint{policiesResponse.Policies[0].ID}}
deletePolicyResp := deleteGlobalPoliciesResponse{}
s.DoJSON("POST", "/api/v1/fleet/global/policies/delete", deletePolicyParams, http.StatusOK, &deletePolicyResp)
policiesResponse = listGlobalPoliciesResponse{}
s.DoJSON("GET", "/api/v1/fleet/global/policies", nil, http.StatusOK, &policiesResponse)
require.Len(t, policiesResponse.Policies, 0)
}
func (s *integrationTestSuite) TestTeamPoliciesProprietary() {
t := s.T()
team1, err := s.ds.NewTeam(context.Background(), &fleet.Team{
ID: 42,
Name: "team1-policies",
Description: "desc team1",
})
require.NoError(t, err)
hosts := make([]uint, 2)
for i := 0; i < 2; i++ {
h, err := s.ds.NewHost(context.Background(), &fleet.Host{
DetailUpdatedAt: time.Now(),
LabelUpdatedAt: time.Now(),
PolicyUpdatedAt: time.Now(),
SeenTime: time.Now().Add(-time.Duration(i) * time.Minute),
OsqueryHostID: fmt.Sprintf("%s%d", t.Name(), i),
NodeKey: fmt.Sprintf("%s%d", t.Name(), i),
UUID: fmt.Sprintf("%s%d", t.Name(), i),
Hostname: fmt.Sprintf("%sfoo.local%d", t.Name(), i),
})
require.NoError(t, err)
hosts[i] = h.ID
}
err = s.ds.AddHostsToTeam(context.Background(), &team1.ID, hosts)
require.NoError(t, err)
tpParams := teamPolicyRequest{
Name: "TestQuery3",
Query: "select * from osquery;",
Description: "Some description",
Resolution: "some team resolution",
}
tpResp := teamPolicyResponse{}
s.DoJSON("POST", fmt.Sprintf("/api/v1/fleet/teams/%d/policies", team1.ID), tpParams, http.StatusOK, &tpResp)
require.NotNil(t, tpResp.Policy)
require.NotEmpty(t, tpResp.Policy.ID)
assert.Equal(t, "TestQuery3", tpResp.Policy.Name)
assert.Equal(t, "select * from osquery;", tpResp.Policy.Query)
assert.Equal(t, "Some description", tpResp.Policy.Description)
require.NotNil(t, tpResp.Policy.Resolution)
assert.Equal(t, "some team resolution", *tpResp.Policy.Resolution)
assert.NotNil(t, tpResp.Policy.AuthorID)
assert.Equal(t, "Test Name admin1@example.com", tpResp.Policy.AuthorName)
assert.Equal(t, "admin1@example.com", tpResp.Policy.AuthorEmail)
mtpParams := modifyTeamPolicyRequest{
ModifyPolicyPayload: fleet.ModifyPolicyPayload{
Name: ptr.String("TestQuery4"),
Query: ptr.String("select * from osquery_info;"),
Description: ptr.String("Some description updated"),
Resolution: ptr.String("some team resolution updated"),
},
}
mtpResp := modifyTeamPolicyResponse{}
s.DoJSON("PATCH", fmt.Sprintf("/api/v1/fleet/teams/%d/policies/%d", team1.ID, tpResp.Policy.ID), mtpParams, http.StatusOK, &mtpResp)
require.NotNil(t, mtpResp.Policy)
assert.Equal(t, "TestQuery4", mtpResp.Policy.Name)
assert.Equal(t, "select * from osquery_info;", mtpResp.Policy.Query)
assert.Equal(t, "Some description updated", mtpResp.Policy.Description)
require.NotNil(t, mtpResp.Policy.Resolution)
assert.Equal(t, "some team resolution updated", *mtpResp.Policy.Resolution)
gtpResp := getPolicyByIDResponse{}
s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/teams/%d/policies/%d", team1.ID, tpResp.Policy.ID), getPolicyByIDRequest{}, http.StatusOK, &gtpResp)
require.NotNil(t, gtpResp.Policy)
assert.Equal(t, "TestQuery4", gtpResp.Policy.Name)
assert.Equal(t, "select * from osquery_info;", gtpResp.Policy.Query)
assert.Equal(t, "Some description updated", gtpResp.Policy.Description)
require.NotNil(t, gtpResp.Policy.Resolution)
assert.Equal(t, "some team resolution updated", *gtpResp.Policy.Resolution)
policiesResponse := listTeamPoliciesResponse{}
s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/teams/%d/policies", team1.ID), nil, http.StatusOK, &policiesResponse)
require.Len(t, policiesResponse.Policies, 1)
assert.Equal(t, "TestQuery4", policiesResponse.Policies[0].Name)
assert.Equal(t, "select * from osquery_info;", policiesResponse.Policies[0].Query)
assert.Equal(t, "Some description updated", policiesResponse.Policies[0].Description)
require.NotNil(t, policiesResponse.Policies[0].Resolution)
assert.Equal(t, "some team resolution updated", *policiesResponse.Policies[0].Resolution)
listHostsURL := fmt.Sprintf("/api/v1/fleet/hosts?policy_id=%d", policiesResponse.Policies[0].ID)
listHostsResp := listHostsResponse{}
s.DoJSON("GET", listHostsURL, nil, http.StatusOK, &listHostsResp)
require.Len(t, listHostsResp.Hosts, 2)
h1 := listHostsResp.Hosts[0]
h2 := listHostsResp.Hosts[1]
listHostsURL = fmt.Sprintf("/api/v1/fleet/hosts?team_id=%d&policy_id=%d&policy_response=passing", team1.ID, policiesResponse.Policies[0].ID)
listHostsResp = listHostsResponse{}
s.DoJSON("GET", listHostsURL, nil, http.StatusOK, &listHostsResp)
require.Len(t, listHostsResp.Hosts, 0)
require.NoError(t, s.ds.RecordPolicyQueryExecutions(context.Background(), h1.Host, map[uint]*bool{policiesResponse.Policies[0].ID: ptr.Bool(true)}, time.Now(), false))
require.NoError(t, s.ds.RecordPolicyQueryExecutions(context.Background(), h2.Host, map[uint]*bool{policiesResponse.Policies[0].ID: nil}, time.Now(), false))
listHostsURL = fmt.Sprintf("/api/v1/fleet/hosts?team_id=%d&policy_id=%d&policy_response=passing", team1.ID, policiesResponse.Policies[0].ID)
listHostsResp = listHostsResponse{}
s.DoJSON("GET", listHostsURL, nil, http.StatusOK, &listHostsResp)
require.Len(t, listHostsResp.Hosts, 1)
deletePolicyParams := deleteTeamPoliciesRequest{IDs: []uint{policiesResponse.Policies[0].ID}}
deletePolicyResp := deleteTeamPoliciesResponse{}
s.DoJSON("POST", fmt.Sprintf("/api/v1/fleet/teams/%d/policies/delete", team1.ID), deletePolicyParams, http.StatusOK, &deletePolicyResp)
policiesResponse = listTeamPoliciesResponse{}
s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/teams/%d/policies", team1.ID), nil, http.StatusOK, &policiesResponse)
require.Len(t, policiesResponse.Policies, 0)
}
func (s *integrationTestSuite) TestTeamPoliciesProprietaryInvalid() {
t := s.T()
team1, err := s.ds.NewTeam(context.Background(), &fleet.Team{
ID: 42,
Name: "team1-policies-2",
Description: "desc team1",
})
require.NoError(t, err)
tpParams := teamPolicyRequest{
Name: "TestQuery3-Team",
Query: "select * from osquery;",
Description: "Some description",
Resolution: "some team resolution",
}
tpResp := teamPolicyResponse{}
s.DoJSON("POST", fmt.Sprintf("/api/v1/fleet/teams/%d/policies", team1.ID), tpParams, http.StatusOK, &tpResp)
require.NotNil(t, tpResp.Policy)
teamPolicyID := tpResp.Policy.ID
gpParams := globalPolicyRequest{
Name: "TestQuery3-Global",
Query: "select * from osquery;",
Description: "Some description",
Resolution: "some global resolution",
}
gpResp := globalPolicyResponse{}
s.DoJSON("POST", "/api/v1/fleet/global/policies", gpParams, http.StatusOK, &gpResp)
require.NotNil(t, gpResp.Policy)
require.NotEmpty(t, gpResp.Policy.ID)
globalPolicyID := gpResp.Policy.ID
for _, tc := range []struct {
tname string
testUpdate bool
queryID *uint
name string
query string
}{
{
tname: "set both QueryID and Query",
testUpdate: false,
queryID: ptr.Uint(1),
name: "Some name",
query: "select * from osquery;",
},
{
tname: "empty query",
testUpdate: true,
name: "Some name",
query: "",
},
{
tname: "empty name",
testUpdate: true,
name: "",
query: "select 1;",
},
{
tname: "Invalid query",
testUpdate: true,
name: "Invalid query",
query: "ATTACH 'foo' AS bar;",
},
} {
t.Run(tc.tname, func(t *testing.T) {
tpReq := teamPolicyRequest{
QueryID: tc.queryID,
Name: tc.name,
Query: tc.query,
}
tpResp := teamPolicyResponse{}
s.DoJSON("POST", fmt.Sprintf("/api/v1/fleet/teams/%d/policies", team1.ID), tpReq, http.StatusBadRequest, &tpResp)
require.Nil(t, tpResp.Policy)
testUpdate := tc.queryID == nil
if testUpdate {
tpReq := modifyTeamPolicyRequest{
ModifyPolicyPayload: fleet.ModifyPolicyPayload{
Name: ptr.String(tc.name),
Query: ptr.String(tc.query),
},
}
tpResp := modifyTeamPolicyResponse{}
s.DoJSON("PATCH", fmt.Sprintf("/api/v1/fleet/teams/%d/policies/%d", team1.ID, teamPolicyID), tpReq, http.StatusBadRequest, &tpResp)
require.Nil(t, tpResp.Policy)
}
gpReq := globalPolicyRequest{
QueryID: tc.queryID,
Name: tc.name,
Query: tc.query,
}
gpResp := globalPolicyResponse{}
s.DoJSON("POST", "/api/v1/fleet/global/policies", gpReq, http.StatusBadRequest, &gpResp)
require.Nil(t, tpResp.Policy)
if testUpdate {
gpReq := modifyGlobalPolicyRequest{
ModifyPolicyPayload: fleet.ModifyPolicyPayload{
Name: ptr.String(tc.name),
Query: ptr.String(tc.query),
},
}
gpResp := modifyGlobalPolicyResponse{}
s.DoJSON("PATCH", fmt.Sprintf("/api/v1/fleet/global/policies/%d", globalPolicyID), gpReq, http.StatusBadRequest, &gpResp)
require.Nil(t, tpResp.Policy)
}
})
}
}
func (s *integrationTestSuite) TestHostDetailsPolicies() {
t := s.T()
hosts := s.createHosts(t)
host1 := hosts[0]
team1, err := s.ds.NewTeam(context.Background(), &fleet.Team{
ID: 42,
Name: "HostDetailsPolicies-Team",
Description: "desc team1",
})
require.NoError(t, err)
err = s.ds.AddHostsToTeam(context.Background(), &team1.ID, []uint{host1.ID})
require.NoError(t, err)
gpParams := globalPolicyRequest{
Name: "HostDetailsPolicies",
Query: "select * from osquery;",
Description: "Some description",
Resolution: "some global resolution",
}
gpResp := globalPolicyResponse{}
s.DoJSON("POST", "/api/v1/fleet/global/policies", gpParams, http.StatusOK, &gpResp)
require.NotNil(t, gpResp.Policy)
require.NotEmpty(t, gpResp.Policy.ID)
tpParams := teamPolicyRequest{
Name: "HostDetailsPolicies-Team",
Query: "select * from osquery;",
Description: "Some description",
Resolution: "some team resolution",
}
tpResp := teamPolicyResponse{}
s.DoJSON("POST", fmt.Sprintf("/api/v1/fleet/teams/%d/policies", team1.ID), tpParams, http.StatusOK, &tpResp)
require.NotNil(t, tpResp.Policy)
require.NotEmpty(t, tpResp.Policy.ID)
err = s.ds.RecordPolicyQueryExecutions(
context.Background(),
host1,
map[uint]*bool{gpResp.Policy.ID: ptr.Bool(true)},
time.Now(),
false,
)
require.NoError(t, err)
resp := s.Do("GET", fmt.Sprintf("/api/v1/fleet/hosts/%d", host1.ID), nil, http.StatusOK)
b, err := ioutil.ReadAll(resp.Body)
require.NoError(t, err)
var r struct {
Host *HostDetailResponse `json:"host"`
Err error `json:"error,omitempty"`
}
err = json.Unmarshal(b, &r)
require.NoError(t, err)
require.Nil(t, r.Err)
hd := r.Host.HostDetail
require.Len(t, hd.Policies, 2)
require.True(t, reflect.DeepEqual(gpResp.Policy.PolicyData, hd.Policies[0].PolicyData))
require.Equal(t, hd.Policies[0].Response, "pass")
require.True(t, reflect.DeepEqual(tpResp.Policy.PolicyData, hd.Policies[1].PolicyData))
require.Equal(t, hd.Policies[1].Response, "") // policy didn't "run"
// Try to create a global policy with an existing name.
s.DoJSON("POST", "/api/v1/fleet/global/policies", gpParams, http.StatusConflict, &gpResp)
// Try to create a team policy with an existing name.
s.DoJSON("POST", fmt.Sprintf("/api/v1/fleet/teams/%d/policies", team1.ID), tpParams, http.StatusConflict, &tpResp)
}

View file

@ -186,15 +186,19 @@ func (s *integrationEnterpriseTestSuite) TestTeamPolicies() {
qr, err := s.ds.NewQuery(context.Background(), &fleet.Query{Name: "TestQuery2", Description: "Some description", Query: "select * from osquery;", ObserverCanRun: true})
require.NoError(t, err)
tpParams := teamPolicyRequest{QueryID: qr.ID, Resolution: "some team resolution"}
tpParams := teamPolicyRequest{
QueryID: &qr.ID,
Resolution: "some team resolution",
}
r := teamPolicyResponse{}
s.DoJSON("POST", fmt.Sprintf("/api/v1/fleet/teams/%d/policies", team1.ID), tpParams, http.StatusOK, &r)
ts = listTeamPoliciesResponse{}
s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/teams/%d/policies", team1.ID), nil, http.StatusOK, &ts)
require.Len(t, ts.Policies, 1)
assert.Equal(t, "TestQuery2", ts.Policies[0].QueryName)
assert.Equal(t, qr.ID, ts.Policies[0].QueryID)
assert.Equal(t, "TestQuery2", ts.Policies[0].Name)
assert.Equal(t, "select * from osquery;", ts.Policies[0].Query)
assert.Equal(t, "Some description", ts.Policies[0].Description)
require.NotNil(t, ts.Policies[0].Resolution)
assert.Equal(t, "some team resolution", *ts.Policies[0].Resolution)

View file

@ -78,7 +78,7 @@ func TestRefetchHost(t *testing.T) {
host := &fleet.Host{ID: 3}
ds.HostFunc = func(ctx context.Context, hid uint) (*fleet.Host, error) {
ds.HostFunc = func(ctx context.Context, hid uint, skipLoadingExtras bool) (*fleet.Host, error) {
return host, nil
}
ds.SaveHostFunc = func(ctx context.Context, host *fleet.Host) error {
@ -97,7 +97,7 @@ func TestRefetchHostUserInTeams(t *testing.T) {
host := &fleet.Host{ID: 3, TeamID: ptr.Uint(4)}
ds.HostFunc = func(ctx context.Context, hid uint) (*fleet.Host, error) {
ds.HostFunc = func(ctx context.Context, hid uint, skipLoadingExtras bool) (*fleet.Host, error) {
return host, nil
}
ds.SaveHostFunc = func(ctx context.Context, host *fleet.Host) error {
@ -111,7 +111,8 @@ func TestRefetchHostUserInTeams(t *testing.T) {
Team: fleet.Team{ID: 4},
Role: fleet.RoleMaintainer,
},
}}
},
}
require.NoError(t, svc.RefetchHost(test.UserContext(maintainer), host.ID))
observer := &fleet.User{
@ -120,7 +121,8 @@ func TestRefetchHostUserInTeams(t *testing.T) {
Team: fleet.Team{ID: 4},
Role: fleet.RoleObserver,
},
}}
},
}
require.NoError(t, svc.RefetchHost(test.UserContext(observer), host.ID))
}
@ -196,7 +198,7 @@ func TestHostAuth(t *testing.T) {
ds.DeleteHostFunc = func(ctx context.Context, hid uint) error {
return nil
}
ds.HostFunc = func(ctx context.Context, id uint) (*fleet.Host, error) {
ds.HostFunc = func(ctx context.Context, id uint, skipLoadingExtras bool) (*fleet.Host, error) {
if id == 1 {
return teamHost, nil
}
@ -233,7 +235,7 @@ func TestHostAuth(t *testing.T) {
return nil
}
var testCases = []struct {
testCases := []struct {
name string
user *fleet.User
shouldFailGlobalWrite bool

View file

@ -327,7 +327,7 @@ func TestLabelQueries(t *testing.T) {
ds.LabelQueriesForHostFunc = func(ctx context.Context, host *fleet.Host) (map[string]string, error) {
return map[string]string{}, nil
}
ds.HostFunc = func(ctx context.Context, id uint) (*fleet.Host, error) {
ds.HostFunc = func(ctx context.Context, id uint, skipLoadingExtras bool) (*fleet.Host, error) {
return host, nil
}
ds.SaveHostFunc = func(ctx context.Context, gotHost *fleet.Host) error {
@ -696,7 +696,7 @@ func TestDetailQueriesWithEmptyStrings(t *testing.T) {
return nil
}
ds.HostFunc = func(ctx context.Context, id uint) (*fleet.Host, error) {
ds.HostFunc = func(ctx context.Context, id uint, skipLoadingExtras bool) (*fleet.Host, error) {
return &host, nil
}
@ -908,7 +908,7 @@ func TestDetailQueries(t *testing.T) {
return nil
}
ds.HostFunc = func(ctx context.Context, id uint) (*fleet.Host, error) {
ds.HostFunc = func(ctx context.Context, id uint, skipLoadingExtras bool) (*fleet.Host, error) {
return &host, nil
}
@ -1738,7 +1738,7 @@ func TestDistributedQueriesReloadsHostIfDetailsAreIn(t *testing.T) {
assert.Equal(t, ip, host.PrimaryIP)
return nil
}
ds.HostFunc = func(ctx context.Context, id uint) (*fleet.Host, error) {
ds.HostFunc = func(ctx context.Context, id uint, skipLoadingExtras bool) (*fleet.Host, error) {
require.Equal(t, uint(42), id)
return &fleet.Host{ID: 42, Platform: "darwin", PrimaryIP: ip}, nil
}
@ -1904,7 +1904,7 @@ func TestPolicyQueries(t *testing.T) {
ds.LabelQueriesForHostFunc = func(ctx context.Context, host *fleet.Host) (map[string]string, error) {
return map[string]string{}, nil
}
ds.HostFunc = func(ctx context.Context, id uint) (*fleet.Host, error) {
ds.HostFunc = func(ctx context.Context, id uint, skipLoadingExtras bool) (*fleet.Host, error) {
return host, nil
}
ds.SaveHostFunc = func(ctx context.Context, gotHost *fleet.Host) error {

View file

@ -2,7 +2,12 @@ package service
import (
"context"
"errors"
"fmt"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/contexts/logging"
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/ptr"
)
@ -12,9 +17,12 @@ import (
/////////////////////////////////////////////////////////////////////////////////
type teamPolicyRequest struct {
TeamID uint `url:"team_id"`
QueryID uint `json:"query_id"`
Resolution string `json:"resolution"`
TeamID uint `url:"team_id"`
QueryID *uint `json:"query_id"`
Query string `json:"query"`
Name string `json:"name"`
Description string `json:"description"`
Resolution string `json:"resolution"`
}
type teamPolicyResponse struct {
@ -26,19 +34,43 @@ func (r teamPolicyResponse) error() error { return r.Err }
func teamPolicyEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (interface{}, error) {
req := request.(*teamPolicyRequest)
resp, err := svc.NewTeamPolicy(ctx, req.TeamID, req.QueryID, req.Resolution)
resp, err := svc.NewTeamPolicy(ctx, req.TeamID, fleet.PolicyPayload{
QueryID: req.QueryID,
Name: req.Name,
Query: req.Query,
Description: req.Description,
Resolution: req.Resolution,
})
if err != nil {
return teamPolicyResponse{Err: err}, nil
}
return teamPolicyResponse{Policy: resp}, nil
}
func (svc Service) NewTeamPolicy(ctx context.Context, teamID uint, queryID uint, resolution string) (*fleet.Policy, error) {
if err := svc.authz.Authorize(ctx, &fleet.Policy{TeamID: ptr.Uint(teamID)}, fleet.ActionWrite); err != nil {
func (svc Service) NewTeamPolicy(ctx context.Context, teamID uint, p fleet.PolicyPayload) (*fleet.Policy, error) {
if err := svc.authz.Authorize(ctx, &fleet.Policy{
PolicyData: fleet.PolicyData{
TeamID: ptr.Uint(teamID),
},
}, fleet.ActionWrite); err != nil {
return nil, err
}
return svc.ds.NewTeamPolicy(ctx, teamID, queryID, resolution)
vc, ok := viewer.FromContext(ctx)
if !ok {
return nil, errors.New("user must be authenticated to create team policies")
}
if err := p.Verify(); err != nil {
return nil, &badRequestError{
message: fmt.Sprintf("policy payload verification: %s", err),
}
}
policy, err := svc.ds.NewTeamPolicy(ctx, teamID, ptr.Uint(vc.UserID()), p)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "creating policy")
}
return policy, nil
}
/////////////////////////////////////////////////////////////////////////////////
@ -66,7 +98,11 @@ func listTeamPoliciesEndpoint(ctx context.Context, request interface{}, svc flee
}
func (svc Service) ListTeamPolicies(ctx context.Context, teamID uint) ([]*fleet.Policy, error) {
if err := svc.authz.Authorize(ctx, &fleet.Policy{TeamID: ptr.Uint(teamID)}, fleet.ActionRead); err != nil {
if err := svc.authz.Authorize(ctx, &fleet.Policy{
PolicyData: fleet.PolicyData{
TeamID: ptr.Uint(teamID),
},
}, fleet.ActionRead); err != nil {
return nil, err
}
@ -99,7 +135,11 @@ func getTeamPolicyByIDEndpoint(ctx context.Context, request interface{}, svc fle
}
func (svc Service) GetTeamPolicyByIDQueries(ctx context.Context, teamID uint, policyID uint) (*fleet.Policy, error) {
if err := svc.authz.Authorize(ctx, &fleet.Policy{TeamID: ptr.Uint(teamID)}, fleet.ActionRead); err != nil {
if err := svc.authz.Authorize(ctx, &fleet.Policy{
PolicyData: fleet.PolicyData{
TeamID: ptr.Uint(teamID),
},
}, fleet.ActionRead); err != nil {
return nil, err
}
@ -137,9 +177,95 @@ func deleteTeamPoliciesEndpoint(ctx context.Context, request interface{}, svc fl
}
func (svc Service) DeleteTeamPolicies(ctx context.Context, teamID uint, ids []uint) ([]uint, error) {
if err := svc.authz.Authorize(ctx, &fleet.Policy{TeamID: ptr.Uint(teamID)}, fleet.ActionWrite); err != nil {
if err := svc.authz.Authorize(ctx, &fleet.Policy{
PolicyData: fleet.PolicyData{
TeamID: ptr.Uint(teamID),
},
}, fleet.ActionWrite); err != nil {
return nil, err
}
if len(ids) == 0 {
return nil, nil
}
ids, err := svc.ds.DeleteTeamPolicies(ctx, teamID, ids)
if err != nil {
return nil, err
}
return ids, nil
}
/////////////////////////////////////////////////////////////////////////////////
// Modify
/////////////////////////////////////////////////////////////////////////////////
type modifyTeamPolicyRequest struct {
TeamID uint `url:"team_id"`
PolicyID uint `url:"policy_id"`
fleet.ModifyPolicyPayload
}
type modifyTeamPolicyResponse struct {
Policy *fleet.Policy `json:"policy,omitempty"`
Err error `json:"error,omitempty"`
}
func (r modifyTeamPolicyResponse) error() error { return r.Err }
func modifyTeamPolicyEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (interface{}, error) {
req := request.(*modifyTeamPolicyRequest)
resp, err := svc.ModifyTeamPolicy(ctx, req.TeamID, req.PolicyID, req.ModifyPolicyPayload)
if err != nil {
return modifyTeamPolicyResponse{Err: err}, nil
}
return modifyTeamPolicyResponse{Policy: resp}, nil
}
func (svc Service) ModifyTeamPolicy(ctx context.Context, teamID uint, id uint, p fleet.ModifyPolicyPayload) (*fleet.Policy, error) {
return svc.modifyPolicy(ctx, &teamID, id, p)
}
func (svc Service) modifyPolicy(ctx context.Context, teamID *uint, id uint, p fleet.ModifyPolicyPayload) (*fleet.Policy, error) {
// First make sure the user can read the policies.
if err := svc.authz.Authorize(ctx, &fleet.Policy{
PolicyData: fleet.PolicyData{
TeamID: teamID,
},
}, fleet.ActionRead); err != nil {
return nil, err
}
policy, err := svc.ds.Policy(ctx, id)
if err != nil {
return nil, err
}
// Then we make sure they can modify the team's policies.
if err := svc.authz.Authorize(ctx, policy, fleet.ActionWrite); err != nil {
return nil, err
}
return svc.ds.DeleteTeamPolicies(ctx, teamID, ids)
if err := p.Verify(); err != nil {
return nil, &badRequestError{
message: fmt.Sprintf("policy payload verification: %s", err),
}
}
if p.Name != nil {
policy.Name = *p.Name
}
if p.Description != nil {
policy.Description = *p.Description
}
if p.Query != nil {
policy.Query = *p.Query
}
if p.Resolution != nil {
policy.Resolution = p.Resolution
}
logging.WithExtras(ctx, "name", policy.Name, "sql", policy.Query)
err = svc.ds.SavePolicy(ctx, policy)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "saving policy")
}
return policy, nil
}

View file

@ -16,7 +16,7 @@ func TestTeamPoliciesAuth(t *testing.T) {
ds := new(mock.Store)
svc := newTestService(ds, nil, nil)
ds.NewTeamPolicyFunc = func(ctx context.Context, teamID uint, queryID uint, resolution string) (*fleet.Policy, error) {
ds.NewTeamPolicyFunc = func(ctx context.Context, teamID uint, authorID *uint, args fleet.PolicyPayload) (*fleet.Policy, error) {
return &fleet.Policy{}, nil
}
ds.ListTeamPoliciesFunc = func(ctx context.Context, teamID uint) ([]*fleet.Policy, error) {
@ -31,11 +31,14 @@ func TestTeamPoliciesAuth(t *testing.T) {
ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) {
return &fleet.Team{ID: 1}, nil
}
ds.ApplyPolicySpecsFunc = func(ctx context.Context, specs []*fleet.PolicySpec) error {
ds.ApplyPolicySpecsFunc = func(ctx context.Context, authorID uint, specs []*fleet.PolicySpec) error {
return nil
}
ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activityType string, details *map[string]interface{}) error {
return nil
}
var testCases = []struct {
testCases := []struct {
name string
user *fleet.User
shouldFailWrite bool
@ -100,7 +103,10 @@ func TestTeamPoliciesAuth(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
ctx := viewer.NewContext(context.Background(), viewer.Viewer{User: tt.user})
_, err := svc.NewTeamPolicy(ctx, 1, 2, "")
_, err := svc.NewTeamPolicy(ctx, 1, fleet.PolicyPayload{
Name: "query1",
Query: "select 1;",
})
checkAuthErr(t, tt.shouldFailWrite, err)
_, err = svc.ListTeamPolicies(ctx, 1)
@ -114,8 +120,9 @@ func TestTeamPoliciesAuth(t *testing.T) {
err = svc.ApplyPolicySpecs(ctx, []*fleet.PolicySpec{
{
QueryName: "query1",
Team: "team1",
Name: "query1",
Query: "select 1;",
Team: "team1",
},
})
checkAuthErr(t, tt.shouldFailWrite, err)