From 6fc96ed42614b738aade9bf8fc12ea2c23ede0ea Mon Sep 17 00:00:00 2001 From: James Ramirez Date: Mon, 5 Aug 2024 05:46:08 +0100 Subject: [PATCH] feat: implement GET /auth_logs/ and /auth_logs/{id} Signed-off-by: James Ramirez --- docs/coverage.md | 4 +- docs/tables/kolide_auth_log.md | 46 +++++++++++ kolide/client/auth_log.go | 52 +++++++++++++ kolide/plugin.go | 1 + kolide/table_kolide_auth_log.go | 77 +++++++++++++++++++ test/end-to-end/_query/kolide_auth_log.sql | 9 +++ .../_query/kolide_auth_log_by_id.sql | 9 +++ test/end-to-end/_results/kolide_auth_log.bash | 9 +++ test/end-to-end/kolide_auth_log.bats | 59 ++++++++++++++ test/end-to-end/kolide_auth_log_by_id.bats | 30 ++++++++ 10 files changed, 294 insertions(+), 2 deletions(-) create mode 100644 docs/tables/kolide_auth_log.md create mode 100644 kolide/client/auth_log.go create mode 100644 kolide/table_kolide_auth_log.go create mode 100644 test/end-to-end/_query/kolide_auth_log.sql create mode 100644 test/end-to-end/_query/kolide_auth_log_by_id.sql create mode 100644 test/end-to-end/_results/kolide_auth_log.bash create mode 100644 test/end-to-end/kolide_auth_log.bats create mode 100644 test/end-to-end/kolide_auth_log_by_id.bats diff --git a/docs/coverage.md b/docs/coverage.md index 55fc6c9..526e7b4 100644 --- a/docs/coverage.md +++ b/docs/coverage.md @@ -27,8 +27,8 @@ using the interactive API documentation at https://kolideapi.readme.io/reference | GET | /people/{id} | :question: | ? | ? | Ok | | GET | /person_groups | :white_check_mark: | ? | ? | [^1] | | GET | /person_groups/{id} | :white_check_mark: | ? | ? | [^1] | -| GET | /auth_logs | #27 | ? | ? | | -| GET | /auth_logs/{id} | #28 | ? | ? | | +| GET | /auth_logs | :question: | ? | ? | | +| GET | /auth_logs/{id} | :question: | ? | ? | | | GET | /device_groups/{deviceGroupId}/devices | :white_check_mark: | ? | ? | [^1] | | POST | /device_groups/{deviceGroupId}/memberships | :no_entry_sign: | | | | | DELETE | /device_groups/{deviceGroupId}/memberships/{id} | :no_entry_sign: | | | | diff --git a/docs/tables/kolide_auth_log.md b/docs/tables/kolide_auth_log.md new file mode 100644 index 0000000..7b1b64f --- /dev/null +++ b/docs/tables/kolide_auth_log.md @@ -0,0 +1,46 @@ +# Table: kolide_auth_log + +Lists the authentication attempts occurring when a user tries to sign into an App protected by Kolide Device Trust. + +## Examples + +### Basic info + +```sql +select + timestamp, + person_name, + initial_status, + result +from + kolide_auth_log; +``` + +### List all attempts from the past day + +```sql +select + timestamp, + person_name, + initial_status, + result +from + kolide_auth_log +where + timestamp > date_trunc('day', current_date) - interval '1 day'; +``` + +### List all failed attempts performed by a specific user + +```sql +select + timestamp, + initial_status, + result +from + kolide_auth_log +where + person_name = 'Dennis Nedry' + and + result = 'Fail'; +``` diff --git a/kolide/client/auth_log.go b/kolide/client/auth_log.go new file mode 100644 index 0000000..48fd6e1 --- /dev/null +++ b/kolide/client/auth_log.go @@ -0,0 +1,52 @@ +package kolide_client + +import ( + "time" +) + +type AuthLogListResponse struct { + AuthLogs []AuthLog `json:"data"` + Pagination Pagination `json:"pagination"` +} +type AuthLog struct { + Id string `json:"id"` + Timestamp time.Time `json:"timestamp"` + PersonName string `json:"person_name"` + PersonEmail string `json:"person_email"` + PersonInfo PersonInformation `json:"person_info"` + DeviceInfo DeviceInformation `json:"device_information,omitempty"` + Result string `json:"result"` + InitialStatus string `json:"initial_status"` + IpAddress string `json:"ip_address"` + AgentVersion string `json:"agent_version,omitempty"` + Country string `json:"country,omitempty"` + City string `json:"city,omitempty"` + BrowserName string `json:"browser_name"` + BrowserUserAgent string `json:"browser_user_agent"` + IssuesDisplayed []DetailedIssueInformation `json:"issues_displayed"` + Events []EventInformation `json:"events"` +} + +type PersonInformation struct { + Identifier string `json:"identifier,omitempty"` +} + +type DetailedIssueInformation struct { + Title string `json:"title,omitempty"` + BlockingStatus string `json:"blocking_status,omitempty"` + Identifier string `json:"identifier,omitempty"` +} + +type EventInformation struct { + Timestamp time.Time `json:"timestamp"` + EventType string `json:"event_type,omitempty"` + EventDescription string `json:"event_description,omitempty"` +} + +func (c *Client) GetAuthLogs(cursor string, limit int32, searches ...Search) (interface{}, error) { + return c.fetchCollection("/auth_logs/", cursor, limit, searches, new(AuthLogListResponse)) +} + +func (c *Client) GetAuthLogById(id string) (interface{}, error) { + return c.fetchResource("/auth_logs/", id, new(AuthLog)) +} diff --git a/kolide/plugin.go b/kolide/plugin.go index 82a378c..18f44a6 100644 --- a/kolide/plugin.go +++ b/kolide/plugin.go @@ -27,6 +27,7 @@ func Plugin(ctx context.Context) *plugin.Plugin { TableMap: map[string]*plugin.Table{ "kolide_admin_user": tableKolideAdminUser(ctx), "kolide_audit_log": tableKolideAuditLog(ctx), + "kolide_auth_log": tableKolideAuthLog(ctx), "kolide_check": tableKolideCheck(ctx), "kolide_deprovisioned_person": tableKolideDeprovisionedPerson(ctx), "kolide_device": tableKolideDevice(ctx), diff --git a/kolide/table_kolide_auth_log.go b/kolide/table_kolide_auth_log.go new file mode 100644 index 0000000..669de62 --- /dev/null +++ b/kolide/table_kolide_auth_log.go @@ -0,0 +1,77 @@ +package kolide + +import ( + "context" + + kolide "github.com/grendel-consulting/steampipe-plugin-kolide/kolide/client" + "github.com/turbot/steampipe-plugin-sdk/v5/grpc/proto" + "github.com/turbot/steampipe-plugin-sdk/v5/plugin" + "github.com/turbot/steampipe-plugin-sdk/v5/plugin/transform" +) + +//// TABLE DEFINITION + +func tableKolideAuthLog(_ context.Context) *plugin.Table { + return &plugin.Table{ + Name: "kolide_auth_log", + Description: "Authentication events occurring when a user tries to sign into an App protected by Kolide Device Trust.", + Columns: []*plugin.Column{ + // Filterable "top" columns + {Name: "id", Description: "Canonical identifier for this auth event.", Type: proto.ColumnType_STRING}, + {Name: "timestamp", Description: "When this event started.", Type: proto.ColumnType_TIMESTAMP}, + {Name: "ip_address", Description: "IP address of the request intiating this auth event, may be IPv4 or IPv6.", Type: proto.ColumnType_STRING}, + {Name: "agent_version", Description: "Version of the Kolide Agent running on the endpoint, if known.", Type: proto.ColumnType_STRING}, + {Name: "country", Description: "Name of the country that the session originated from, determined by IP addres and subject to the limitations of IP geocoding.", Type: proto.ColumnType_STRING}, + {Name: "city", Description: "Name of the city that the session originated from, determined by IP addres and subject to the limitations of IP geocoding.", Type: proto.ColumnType_STRING}, + {Name: "browser_name", Description: "Common name of the browser used to initiate the session, subject to the limitations and accuracy of browser detection.", Type: proto.ColumnType_STRING}, + // Other columns + {Name: "person_name", Description: "Name of the user triggering this auth event.", Type: proto.ColumnType_STRING}, + {Name: "person_email", Description: "Email of the user triggering this auth event.", Type: proto.ColumnType_STRING}, + {Name: "person_id", Description: "Canonical identifier for the user this auth event relates to.", Type: proto.ColumnType_STRING, Transform: transform.FromField("PersonInformation.Identifier")}, + {Name: "device_id", Description: "Canonical identifier for the device this auth event relates to.", Type: proto.ColumnType_STRING, Transform: transform.FromField("DeviceInformation.Identifier")}, + {Name: "result", Description: "Result of the authentication attempt, either Success or Fail.", Type: proto.ColumnType_STRING}, + {Name: "initial_status", Description: "Initial auth status of the device attempting authentication, one of All_Good, Will_Block, Blocked or Unknown if no device was detected.", Type: proto.ColumnType_STRING}, + {Name: "browser_user_agent", Description: "User agent information for the browser used to initiatie this session, subject to the limitations and accuracy of browser detection.", Type: proto.ColumnType_STRING}, + {Name: "issues_displayed", Description: "List of issue titles and blocking status that were displayed to the end user", Type: proto.ColumnType_JSON, Transform: transform.FromField("IssuesDisplayed")}, + {Name: "events", Description: "Events that occured during this authentication session", Type: proto.ColumnType_JSON, Transform: transform.FromField("Events")}, + // Steampipe standard columns + {Name: "title", Description: "Display name for this event.", Type: proto.ColumnType_STRING, Transform: transform.FromField("Result")}, + }, + List: &plugin.ListConfig{ + KeyColumns: []*plugin.KeyColumn{ + // Using Kolide API query feature, can be combined with AND (and OR) + {Name: "timestamp", Require: plugin.Optional, Operators: []string{"=", ">", "<"}}, + {Name: "city", Require: plugin.Optional, Operators: []string{"=", "~~"}}, + {Name: "country", Require: plugin.Optional, Operators: []string{"=", "~~"}}, + {Name: "ip_address", Require: plugin.Optional, Operators: []string{"=", "~~"}}, + {Name: "agent_version", Require: plugin.Optional, Operators: []string{"=", "~~"}}, + {Name: "browser_name", Require: plugin.Optional, Operators: []string{"=", "~~"}}, + }, + Hydrate: listAuthLogs, + }, + Get: &plugin.GetConfig{ + KeyColumns: plugin.SingleColumn("id"), + Hydrate: getAuthLog, + }, + } +} + +//// LIST FUNCTION + +func listAuthLogs(ctx context.Context, d *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) { + var visitor ListPredicate = func(client *kolide.Client, cursor string, limit int32, searches ...kolide.Search) (interface{}, error) { + return client.GetAuthLogs(cursor, limit, searches...) + } + + return listAnything(ctx, d, h, "kolide_auth_log.listAuthLogs", visitor, "AuthLogs") +} + +//// GET FUNCTION + +func getAuthLog(ctx context.Context, d *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) { + var visitor GetPredicate = func(client *kolide.Client, id string) (interface{}, error) { + return client.GetAuthLogById(id) + } + + return getAnything(ctx, d, h, "kolide_auth_log.getAuthLog", "id", visitor) +} diff --git a/test/end-to-end/_query/kolide_auth_log.sql b/test/end-to-end/_query/kolide_auth_log.sql new file mode 100644 index 0000000..1034a4f --- /dev/null +++ b/test/end-to-end/_query/kolide_auth_log.sql @@ -0,0 +1,9 @@ +select + timestamp, + person_name, + initial_status, + result +from + kolide_auth_log +order by + timestamp asc; diff --git a/test/end-to-end/_query/kolide_auth_log_by_id.sql b/test/end-to-end/_query/kolide_auth_log_by_id.sql new file mode 100644 index 0000000..7187b00 --- /dev/null +++ b/test/end-to-end/_query/kolide_auth_log_by_id.sql @@ -0,0 +1,9 @@ +select + timestamp, + person_name, + initial_status, + result +from + kolide_auth_log +where + id = '1234'; diff --git a/test/end-to-end/_results/kolide_auth_log.bash b/test/end-to-end/_results/kolide_auth_log.bash new file mode 100644 index 0000000..514e78b --- /dev/null +++ b/test/end-to-end/_results/kolide_auth_log.bash @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +define_test_results(){ + export EXPECTED_COUNT="0" + # export ID="" + # export PERSON_NAME="" + # export INITIAL_STATUS="" + # export RESULT="" +} diff --git a/test/end-to-end/kolide_auth_log.bats b/test/end-to-end/kolide_auth_log.bats new file mode 100644 index 0000000..079d526 --- /dev/null +++ b/test/end-to-end/kolide_auth_log.bats @@ -0,0 +1,59 @@ +# bats file_tags=table:kolide_auth_log, output:auth_log + +setup_file() { + load "${BATS_TEST_DIRNAME}/_support/globals.bash" + define_file_globals + + define_common_test_results + + if [[ -f $EXPECTED_RESULTS ]]; then + load $EXPECTED_RESULTS + fi +} + +setup() { + load "${BATS_TEST_DIRNAME}/_support/extensions.bash" + load_helpers +} + +#bats test_tags=scope:smoke +@test "can_execute_query_via_steampipe" { + steampipe query $QUERY_UNDER_TEST --output json > $QUERY_RESULTS + assert_exists $QUERY_RESULTS +} + +@test "has_expected_number_of_results" { + run bash -c "cat $QUERY_RESULTS | jq -r '.rows | length'" + + if [[ -z "$EXPECTED_COUNT" ]]; then assert_output $EXPECTED_COUNT ; else assert [ "$output" -ge "1" ] ; fi +} + +#bats test_tags=exactness:fuzzy +@test "has_expected_timestamp" { + run bash -c "cat $QUERY_RESULTS | jq -r '.rows.[0].timestamp'" + if [[ -z "$TIMESTAMP" ]]; then assert_output --partial $TIMESTAMP ; else assert_success ; fi +} + +#bats test_tags=exactness:fuzzy +@test "has_expected_person_name" { + run bash -c "cat $QUERY_RESULTS | jq -r '.rows.[0].person_name'" + if [[ -z "$PERSON_NAME" ]]; then assert_output --partial $PERSON_NAME ; else assert_success ; fi +} + +#bats test_tags=exactness:default +@test "has_expected_initial_status" { + run bash -c "cat $QUERY_RESULTS | jq -r '.rows.[0].initial_status'" + if [[ -z "$INITIAL_STATUS" ]]; then assert_output $INITIAL_STATUS ; else assert_output "full" ; fi +} + +#bats test_tags=exactness:default +@test "has_expected_result" { + run bash -c "cat $QUERY_RESULTS | jq -r '.rows.[0].result'" + if [[ -z "$RESULT" ]]; then assert_output $RESULT ; else assert_output "full" ; fi +} + +teardown_file(){ + if [[ -f $QUERY_RESULTS ]]; then + rm -f $QUERY_RESULTS + fi +} diff --git a/test/end-to-end/kolide_auth_log_by_id.bats b/test/end-to-end/kolide_auth_log_by_id.bats new file mode 100644 index 0000000..1f7971b --- /dev/null +++ b/test/end-to-end/kolide_auth_log_by_id.bats @@ -0,0 +1,30 @@ +# bats file_tags=table:kolide_auth_log, output:auth_log + +setup_file() { + load "${BATS_TEST_DIRNAME}/_support/globals.bash" + define_file_globals +} + +setup() { + load "${BATS_TEST_DIRNAME}/_support/extensions.bash" + load_helpers +} + +#bats test_tags=scope:smoke +@test "can_execute_query_via_steampipe" { + steampipe query $QUERY_UNDER_TEST --output json > $QUERY_RESULTS + assert_exists $QUERY_RESULTS +} + +@test "has_no_more_than_one_result" { + run bash -c "cat $QUERY_RESULTS | jq -r '.rows | length'" + assert [ "$output" -le "1" ] +} + +# Remaining functionality covered in kolide_auth_log.bats + +teardown_file(){ + if [[ -f $QUERY_RESULTS ]]; then + rm -f $QUERY_RESULTS + fi +}