diff --git a/README.md b/README.md index a18bb70..4169f8d 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Use SQL to query infrastructure including servers, networks, identity and more f - **[Get started →](https://hub.steampipe.io/plugins/turbot/turbot)** - Documentation: [Table definitions & examples](https://hub.steampipe.io/plugins/turbot/turbot/tables) -- Community: [Slack Channel](https://join.slack.com/t/steampipe/shared_invite/zt-oij778tv-lYyRTWOTMQYBVAbtPSWs3g) +- Community: [Slack Channel](https://steampipe.io/community/join) - Get involved: [Issues](https://github.com/turbot/steampipe-plugin-turbot/issues) ## Quick start diff --git a/docs/index.md b/docs/index.md index b39dcc7..ac24f46 100644 --- a/docs/index.md +++ b/docs/index.md @@ -66,7 +66,7 @@ connection "turbot" { ## Get involved - Open source: https://github.com/turbot/steampipe-plugin-turbot -- Community: [Slack Channel](https://join.slack.com/t/steampipe/shared_invite/zt-oij778tv-lYyRTWOTMQYBVAbtPSWs3g) +- Community: [Slack Channel](https://steampipe.io/community/join) ## Advanced configuration options diff --git a/docs/tables/turbot_notification.md b/docs/tables/turbot_notification.md new file mode 100644 index 0000000..32aa9dd --- /dev/null +++ b/docs/tables/turbot_notification.md @@ -0,0 +1,163 @@ +# Table: turbot_notification + +Notifications represent significant events in the lifecycle of turbot infrastructure, including: + +- A history of change for a resource, e.g., my-s3-bucket. +- A log of state changes and actions performed by a control, e.g., the Tags control for my-s3-bucket. +- Changes to policy settings and policy values updated as a result. +- Records of permission grants, activations, deactivations and revocations. + +When querying this table, we recommend using at least one of these columns (usually in the `where` clause): + +- `id` +- `resource_id` +- `notification_type` +- `control_id` +- `control_type_id` +- `control_type_uri` +- `resource_type_id` +- `resource_type_uri` +- `policy_setting_type_id` +- `policy_setting_type_uri` +- `actor_identity_id` +- `create_timestamp` +- `filter` + +For more information on how to construct a `filter`, please see [Notifications examples](https://turbot.com/v5/docs/reference/filter/notifications#examples). + +## Examples + +### Find all Turbot grants activations in last 1 week using `filter` + +```sql +select + active_grant_id, + notification_type, + active_grant_type_title, + active_grant_level_title, + create_timestamp, + actor_identity_trunk_title, + active_grant_identity_trunk_title, + active_grant_valid_to_timestamp, + active_grant_identity_profile_id, + resource_title +from + turbot_notification +where + filter = 'notificationType:activeGrant createTimestamp:>T-1w' + and active_grant_type_title = 'Turbot' +order by + create_timestamp desc, + notification_type, + actor_identity_trunk_title, + resource_title; +``` + +### Find all AWS grants activations in last 7 days + +```sql +select + active_grant_id, + notification_type, + active_grant_type_title, + active_grant_level_title, + create_timestamp, + actor_identity_trunk_title, + active_grant_identity_trunk_title, + active_grant_valid_to_timestamp, + active_grant_identity_profile_id, + resource_title +from + turbot_notification +where + notification_type = 'active_grants_created' + and create_timestamp >= (current_date - interval '7' day) + and active_grant_type_title = 'AWS' +order by + create_timestamp desc, + notification_type, + actor_identity_trunk_title, + resource_title; +``` + +### Find all AWS S3 buckets created notifications in last 7 days + +```sql +select + create_timestamp, + resource_id, + resource_title, + resource_trunk_title, + actor_identity_trunk_title +from + turbot_notification +where + notification_type = 'resource_created' + and create_timestamp >= (current_date - interval '120' day) + and resource_type_uri = 'tmod:@turbot/aws-s3#/resource/types/bucket' +order by + create_timestamp desc; +``` + +### All policy settings notifications on a given resource or below in last 90 days + +```sql +select + notification_type, + create_timestamp, + policy_setting_id, + policy_setting_type_trunk_title, + policy_setting_type_uri, + resource_trunk_title, + resource_type_trunk_title, + policy_setting_type_read_only, + policy_setting_type_secret, + policy_setting_value +from + turbot_notification +where + resource_id = 191382256916538 + and create_timestamp >= (current_date - interval '90' day) + and filter = 'notificationType:policySetting level:self,descendant' +order by + create_timestamp desc; +``` + +### All policy settings notifications for AWS > Account > Regions policy + +```sql +select + notification_type, + create_timestamp, + policy_setting_id, + resource_id, + resource_trunk_title, + jsonb_pretty(policy_setting_value::jsonb) as policy_setting_value +from + turbot_notification +where + policy_setting_type_uri = 'tmod:@turbot/aws#/policy/types/regionsDefault' + and filter = 'notificationType:policySetting level:self' +order by + create_timestamp desc; +``` + +### All notifications for AWS > Account > Budget > Budget control + +```sql +select + notification_type, + create_timestamp, + control_id, + resource_trunk_title, + control_state, + control_reason +from + turbot_notification +where + control_type_uri = 'tmod:@turbot/aws#/control/types/budget' + and filter = 'notificationType:control level:self' +order by + resource_id, + create_timestamp desc; +``` diff --git a/go.mod b/go.mod index 2a4280a..3b4e39f 100644 --- a/go.mod +++ b/go.mod @@ -7,8 +7,7 @@ require ( github.com/go-yaml/yaml v2.1.0+incompatible github.com/hashicorp/terraform v0.12.0 github.com/machinebox/graphql v0.2.3-0.20180904014615-9835de6386a3 - github.com/matryer/is v1.2.0 // indirect - github.com/mattn/go-isatty v0.0.12 // indirect + github.com/matryer/is v1.4.0 // indirect github.com/mitchellh/go-homedir v1.1.0 github.com/mitchellh/mapstructure v1.3.3 github.com/pkg/errors v0.9.1 @@ -16,6 +15,7 @@ require ( github.com/turbot/go-kit v0.3.0 github.com/turbot/steampipe-plugin-sdk v1.8.2 golang.org/x/net v0.0.0-20210119194325-5f4716e94777 // indirect + gopkg.in/yaml.v2 v2.2.8 // indirect ) require ( @@ -45,6 +45,7 @@ require ( github.com/iancoleman/strcase v0.1.2 // indirect github.com/keybase/go-crypto v0.0.0-20161004153544-93f5b35093ba // indirect github.com/mattn/go-colorable v0.1.4 // indirect + github.com/mattn/go-isatty v0.0.10 // indirect github.com/mattn/go-runewidth v0.0.7 // indirect github.com/mitchellh/go-testing-interface v1.0.0 // indirect github.com/mitchellh/go-wordwrap v1.0.0 // indirect @@ -60,6 +61,5 @@ require ( google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect google.golang.org/grpc v1.41.0 // indirect google.golang.org/protobuf v1.27.1 // indirect - gopkg.in/yaml.v2 v2.2.3 // indirect gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect -) \ No newline at end of file +) diff --git a/go.sum b/go.sum index 234b69a..ba0bcec 100644 --- a/go.sum +++ b/go.sum @@ -240,8 +240,8 @@ github.com/machinebox/graphql v0.2.3-0.20180904014615-9835de6386a3/go.mod h1:F+k github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho= github.com/masterzen/simplexml v0.0.0-20160608183007-4572e39b1ab9/go.mod h1:kCEbxUJlNDEBNbdQMkPSp6yaKcRXVI6f4ddk8Riv4bc= github.com/masterzen/winrm v0.0.0-20190223112901-5e5c9a7fe54b/go.mod h1:wr1VqkwW0AB5JS0QLy5GpVMS9E3VtRoSYXUYyVk46KY= -github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A= -github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= +github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= +github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= @@ -250,9 +250,8 @@ github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNx github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW10= github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= -github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54= github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= @@ -442,7 +441,6 @@ golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502175342-a43fa875dd82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -522,8 +520,9 @@ gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.3 h1:fvjTMHxHEw/mxHbtzPi3JCcKXQRAnQTBRo6YCJSVHKI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= diff --git a/helpers/helpers_test.go b/helpers/helpers_test.go index 7a92597..2bf811b 100644 --- a/helpers/helpers_test.go +++ b/helpers/helpers_test.go @@ -2,9 +2,10 @@ package helpers import ( "encoding/json" - "github.com/stretchr/testify/assert" "log" "testing" + + "github.com/stretchr/testify/assert" ) func TestRemoveProperties(t *testing.T) { @@ -15,43 +16,43 @@ func TestRemoveProperties(t *testing.T) { expected []interface{} } tests := []test{ - test{ + { "No exclusions", []interface{}{"a", "b", "c"}, []string{}, []interface{}{"a", "b", "c"}, }, - test{ + { "String exclusions", []interface{}{"a", "b", "c"}, []string{"a"}, []interface{}{"b", "c"}, }, - test{ + { "All excluded", []interface{}{"a", "b", "c"}, []string{"a", "b", "c"}, []interface{}(nil), }, - test{ + { "Map exclusion", []interface{}{"a", "b", map[string]string{"c": "C", "d": "D"}}, []string{"c"}, []interface{}{"a", "b", map[string]string{"d": "D"}}, }, - test{ + { "2 map exclusions", []interface{}{"a", "b", map[string]string{"c": "C", "d": "D"}, map[string]string{"e": "E", "f": "F"}}, []string{"c", "f"}, []interface{}{"a", "b", map[string]string{"d": "D"}, map[string]string{"e": "E"}}, }, - test{ + { "No matching exclusions", []interface{}{"a", "b", "c"}, []string{"d"}, []interface{}{"a", "b", "c"}, }, - test{ + { "No matching exclusions with map", []interface{}{"a", "b", map[string]string{"c": "C", "d": "D"}}, []string{"e"}, @@ -72,14 +73,14 @@ func TestGetNullProperties(t *testing.T) { expected []interface{} } tests := []test{ - test{ + { "Empty object", `{ "allOf": [] }`, []interface{}{nil}, }, - test{ + { "Single exclusion", `{ "allOf": [ @@ -104,7 +105,7 @@ func TestGetNullProperties(t *testing.T) { }`, []interface{}{"Id"}, }, - test{ + { "No exclusion", `{ "allOf": [ @@ -121,7 +122,7 @@ func TestGetNullProperties(t *testing.T) { }`, []interface{}(nil), }, - test{ + { "Multiple exclusion", `{ "allOf": [ diff --git a/turbot/plugin.go b/turbot/plugin.go index bdcf4f7..81d9a49 100644 --- a/turbot/plugin.go +++ b/turbot/plugin.go @@ -23,6 +23,7 @@ func Plugin(ctx context.Context) *plugin.Plugin { TableMap: map[string]*plugin.Table{ "turbot_control": tableTurbotControl(ctx), "turbot_control_type": tableTurbotControlType(ctx), + "turbot_notification": tableTurbotNotification(ctx), "turbot_policy_setting": tableTurbotPolicySetting(ctx), "turbot_policy_type": tableTurbotPolicyType(ctx), "turbot_resource": tableTurbotResource(ctx), diff --git a/turbot/table_turbot_notification.go b/turbot/table_turbot_notification.go new file mode 100644 index 0000000..aa84b42 --- /dev/null +++ b/turbot/table_turbot_notification.go @@ -0,0 +1,616 @@ +package turbot + +import ( + "context" + "encoding/json" + "fmt" + "regexp" + "strconv" + "time" + + "github.com/turbot/go-kit/helpers" + "github.com/turbot/steampipe-plugin-sdk/grpc/proto" + "github.com/turbot/steampipe-plugin-sdk/plugin" + "github.com/turbot/steampipe-plugin-sdk/plugin/transform" +) + +func tableTurbotNotification(ctx context.Context) *plugin.Table { + return &plugin.Table{ + Name: "turbot_notification", + Description: "Notifications from the Turbot CMDB.", + List: &plugin.ListConfig{ + Hydrate: listNotification, + KeyColumns: plugin.KeyColumnSlice{ + {Name: "id", Require: plugin.Optional}, + {Name: "notification_type", Require: plugin.Optional}, + {Name: "control_id", Require: plugin.Optional}, + {Name: "control_type_id", Require: plugin.Optional}, + {Name: "control_type_uri", Require: plugin.Optional}, + {Name: "resource_id", Require: plugin.Optional}, + {Name: "resource_type_id", Require: plugin.Optional}, + {Name: "resource_type_uri", Require: plugin.Optional}, + {Name: "policy_setting_type_id", Require: plugin.Optional}, + {Name: "policy_setting_type_uri", Require: plugin.Optional}, + {Name: "actor_identity_id", Require: plugin.Optional}, + {Name: "create_timestamp", Require: plugin.Optional, Operators: []string{">", ">=", "=", "<", "<="}}, + {Name: "filter", Require: plugin.Optional}, + }, + }, + Get: &plugin.GetConfig{ + KeyColumns: plugin.SingleColumn("id"), + Hydrate: getNotification, + }, + Columns: []*plugin.Column{ + // Top columns + {Name: "id", Type: proto.ColumnType_INT, Transform: transform.FromField("Turbot.ID"), Description: "Unique identifier of the notification."}, + {Name: "process_id", Type: proto.ColumnType_INT, Transform: transform.FromField("Turbot.ProcessID"), Description: "ID of the process that created this notification."}, + {Name: "icon", Type: proto.ColumnType_STRING, Description: "Icon for this notification type."}, + {Name: "message", Type: proto.ColumnType_STRING, Description: "Message for the notification."}, + {Name: "notification_type", Type: proto.ColumnType_STRING, Description: "Type of the notification: resource, action, policySetting, control, grant, activeGrant."}, + {Name: "create_timestamp", Type: proto.ColumnType_TIMESTAMP, Transform: transform.FromField("Turbot.CreateTimestamp"), Description: "When the resource was first discovered by Turbot. (It may have been created earlier.)"}, + {Name: "filter", Type: proto.ColumnType_STRING, Transform: transform.FromQual("filter"), Description: "Filter used to search for notifications."}, + + // Actor info for the notification + {Name: "actor_identity_trunk_title", Type: proto.ColumnType_STRING, Transform: transform.FromField("Actor.Identity.Trunk.Title").NullIfZero(), Description: "Title hierarchy of the actor from the root down to the actor of this event."}, + {Name: "actor_identity_id", Type: proto.ColumnType_INT, Transform: transform.FromField("Actor.Identity.Turbot.ID").NullIfZero(), Description: "Identity ID of the actor that performed this event."}, + + // Resource info for notification + {Name: "resource_id", Type: proto.ColumnType_INT, Transform: transform.FromField("Turbot.ResourceID").NullIfZero(), Description: "ID of the resource for this notification."}, + {Name: "resource_trunk_title", Type: proto.ColumnType_STRING, Transform: transform.FromField("Resource.Trunk.Title"), Description: "Title of the resource hierarchy from the root down to this resource."}, + {Name: "resource_title", Type: proto.ColumnType_STRING, Transform: transform.FromField("Resource.Turbot.Title"), Description: "Title of the resource."}, + {Name: "resource_new_version_id", Type: proto.ColumnType_INT, Transform: transform.FromField("Turbot.ResourceNewVersionID"), Description: "Version ID of the resource after the event."}, + {Name: "resource_old_version_id", Type: proto.ColumnType_INT, Transform: transform.FromField("Turbot.ResourceOldVersionID"), Description: "Version ID of the resource before the event."}, + {Name: "resource_type_id", Type: proto.ColumnType_INT, Transform: transform.FromField("Resource.Type.Turbot.ID").NullIfZero(), Description: "ID of the resource type for this notification."}, + {Name: "resource_type_uri", Type: proto.ColumnType_STRING, Transform: transform.FromField("Resource.Type.URI"), Description: "URI of the resource type for this notification."}, + {Name: "resource_type_trunk_title", Type: proto.ColumnType_STRING, Transform: transform.FromField("Resource.Type.Trunk.Title"), Description: "Title of the resource type hierarchy from the root down to this resource."}, + {Name: "resource_data", Type: proto.ColumnType_JSON, Transform: transform.FromField("Resource.Data"), Description: "The data for this resource"}, + {Name: "resource_akas", Type: proto.ColumnType_JSON, Transform: transform.FromField("Resource.Turbot.Akas"), Description: "The globally-unique akas for this resource."}, + {Name: "resource_parent_id", Type: proto.ColumnType_INT, Transform: transform.FromField("Resource.Turbot.ParentID").NullIfZero(), Description: "The id of the parent resource of this resource."}, + {Name: "resource_path", Type: proto.ColumnType_STRING, Transform: transform.FromField("Resource.Turbot.Path"), Description: "The string of resource ids separated by \".\" from root down to this resource."}, + {Name: "resource_tags", Type: proto.ColumnType_JSON, Transform: transform.FromField("Resource.Turbot.Tags"), Description: "Tags attached to this resource."}, + + // Policy settings notification details + {Name: "policy_setting_id", Type: proto.ColumnType_INT, Transform: transform.FromField("Turbot.PolicySettingID"), Description: "ID of the policy setting for this notification."}, + {Name: "policy_setting_new_version_id", Type: proto.ColumnType_INT, Transform: transform.FromField("Turbot.PolicySettingNewVersionID"), Description: "Version ID of the policy setting after the event."}, + {Name: "policy_setting_old_version_id", Type: proto.ColumnType_INT, Transform: transform.FromField("Turbot.PolicySettingOldVersionID"), Description: "Version ID of the policy setting before the event."}, + {Name: "policy_setting_default_template", Type: proto.ColumnType_STRING, Transform: transform.FromField("PolicySetting.DefaultTemplate"), Description: "The Nunjucks template if this setting is for a calculated value."}, + {Name: "policy_setting_default_template_input", Type: proto.ColumnType_STRING, Transform: transform.FromField("PolicySetting.DefaultTemplateInput").Transform(formatPolicyFieldsValue), Description: "The GraphQL Input query if this setting is for a calculated value."}, + {Name: "policy_setting_is_calculated", Type: proto.ColumnType_BOOL, Transform: transform.FromField("PolicySetting.isCalculated"), Description: "If true this setting contains calculated inputs e.g. templateInput and template."}, + {Name: "policy_setting_type_id", Type: proto.ColumnType_INT, Transform: transform.FromField("PolicySetting.Type.Turbot.ID").NullIfZero(), Description: "ID of the policy setting type for this notification."}, + {Name: "policy_setting_type_read_only", Type: proto.ColumnType_BOOL, Transform: transform.FromField("PolicySetting.Type.ReadOnly"), Description: "If true user-defined policy settings are blocked from being created."}, + {Name: "policy_setting_type_secret", Type: proto.ColumnType_BOOL, Transform: transform.FromField("PolicySetting.Type.Secret"), Description: "If true policy value will be encrypted."}, + {Name: "policy_setting_type_trunk_title", Type: proto.ColumnType_STRING, Transform: transform.FromField("PolicySetting.Type.Trunk.Title"), Description: "This is the title of hierarchy from the root down to this policy type."}, + {Name: "policy_setting_type_uri", Type: proto.ColumnType_STRING, Transform: transform.FromField("PolicySetting.Type.URI"), Description: "URI of the policy setting type for this notification."}, + {Name: "policy_setting_value", Type: proto.ColumnType_STRING, Transform: transform.FromField("PolicySetting.Value").Transform(formatPolicyFieldsValue), Description: "The value of the policy setting after this event."}, + + // Controls notification details + {Name: "control_id", Type: proto.ColumnType_INT, Transform: transform.FromField("Turbot.ControlID"), Description: "ID of the control for this notification."}, + {Name: "control_new_version_id", Type: proto.ColumnType_INT, Transform: transform.FromField("Turbot.ControlNewVersionID"), Description: "Version ID of the control after the event."}, + {Name: "control_old_version_id", Type: proto.ColumnType_INT, Transform: transform.FromField("Turbot.ControlOldVersionID"), Description: "Version ID of the control before the event."}, + {Name: "control_details", Type: proto.ColumnType_JSON, Transform: transform.FromField("Control.Details"), Description: "Optional details provided at the last state update of this control."}, + {Name: "control_reason", Type: proto.ColumnType_STRING, Transform: transform.FromField("Control.Reason"), Description: "Optional reason provided at the last state update of this control."}, + {Name: "control_state", Type: proto.ColumnType_STRING, Transform: transform.FromField("Control.State"), Description: "The current state of the control."}, + {Name: "control_type_id", Type: proto.ColumnType_INT, Transform: transform.FromField("Control.Type.Turbot.ID"), Description: "ID of the control type for this control."}, + {Name: "control_type_trunk_title", Type: proto.ColumnType_STRING, Transform: transform.FromField("Control.Type.Trunk.Title"), Description: "This is the title of hierarchy from the root down to this control type."}, + {Name: "control_type_uri", Type: proto.ColumnType_STRING, Transform: transform.FromField("Control.Type.URI"), Description: "URI of the control type for this control."}, + + // ActiveGrants notification details + {Name: "active_grant_id", Type: proto.ColumnType_INT, Transform: fromField("Turbot.ActiveGrantsID"), Description: "Active grant ID for this notification."}, + {Name: "active_grant_new_version_id", Type: proto.ColumnType_INT, Transform: fromField("Turbot.ActiveGrantsNewVersionID"), Description: "Active grant version ID of the grant after the notification."}, + {Name: "active_grant_old_version_id", Type: proto.ColumnType_INT, Transform: fromField("Turbot.ActiveGrantsOldVersionID"), Description: "Version ID of the active grant before the event."}, + {Name: "active_grant_valid_to_timestamp", Type: proto.ColumnType_TIMESTAMP, Transform: fromField("ActiveGrant.Grant.ValidToTimestamp"), Description: "Optional end date for the active grant to expire."}, + {Name: "active_grant_identity_profile_id", Type: proto.ColumnType_STRING, Transform: fromField("ActiveGrant.Grant.Identity.ProfileID"), Description: "The identity of profile id for this active grant."}, + {Name: "active_grant_identity_trunk_title", Type: proto.ColumnType_STRING, Transform: fromField("ActiveGrant.Grant.Identity.Trunk.Title"), Description: "This is the title of hierarchy from the root down to this identity (i.e. Identity whoes access got revoked/permiited) for this active grant."}, + {Name: "active_grant_level_title", Type: proto.ColumnType_STRING, Transform: fromField("ActiveGrant.Grant.Level.Title"), Description: "The name of the active grant level."}, + {Name: "active_grant_permission_level_id", Type: proto.ColumnType_INT, Transform: fromField("ActiveGrant.Grant.PermissionLevelId"), Description: "The unique identifier for the active grant permission level."}, + {Name: "active_grant_permission_type_id", Type: proto.ColumnType_INT, Transform: fromField("ActiveGrant.Grant.PermissionTypeID"), Description: "The unique identifier for the active grant permission type."}, + {Name: "active_grant_role_name", Type: proto.ColumnType_STRING, Transform: fromField("ActiveGrant.Grant.RoleName"), Description: "Optional custom roleName for this active grant, when using existing roles rather than Turbot-managed ones."}, + {Name: "active_grant_type_title", Type: proto.ColumnType_STRING, Transform: fromField("ActiveGrant.Grant.Type.Title"), Description: "The name of the active grant type."}, + + // Grants notification details + {Name: "grant_id", Type: proto.ColumnType_INT, Transform: fromField("Turbot.GrantID"), Description: "ID of the grant for this notification."}, + {Name: "grant_new_version_id", Type: proto.ColumnType_INT, Transform: fromField("Turbot.GrantNewVersionID"), Description: "Version ID of the grant after the event."}, + {Name: "grant_old_version_id", Type: proto.ColumnType_INT, Transform: fromField("Turbot.GrantOldVersionID"), Description: "Version ID of the grant before the event."}, + {Name: "grant_valid_to_timestamp", Type: proto.ColumnType_TIMESTAMP, Transform: fromField("Grant.ValidToTimestamp"), Description: "Optional end date for the grant."}, + {Name: "grant_identity_profile_id", Type: proto.ColumnType_STRING, Transform: fromField("Grant.Identity.ProfileID"), Description: "The identity profile id for this grant."}, + {Name: "grant_identity_trunk_title", Type: proto.ColumnType_STRING, Transform: fromField("Grant.Identity.Trunk.Title"), Description: "This is the title of hierarchy from the root down to this identity (i.e. Identity whoes access got revoked/permiited) for this grant."}, + {Name: "grant_level_title", Type: proto.ColumnType_STRING, Transform: fromField("Grant.Level.Title"), Description: "The name of the permission level."}, + {Name: "grant_permission_level_id", Type: proto.ColumnType_INT, Transform: fromField("Grant.PermissionLevelId"), Description: "The unique identifier for the permission level."}, + {Name: "grant_permission_type_id", Type: proto.ColumnType_INT, Transform: fromField("Grant.PermissionTypeID"), Description: "The unique identifier for the permission type."}, + {Name: "grant_role_name", Type: proto.ColumnType_STRING, Transform: fromField("Grant.RoleName"), Description: "Optional custom roleName for this grant, when using existing roles rather than Turbot-managed ones."}, + {Name: "grant_type_title", Type: proto.ColumnType_STRING, Transform: fromField("Grant.Type.Title"), Description: "The name of the permission type."}, + }, + } +} + +const ( + queryNotificationList = ` + query notificationList($filter: [String!], $next_token: String) { + notifications(filter: $filter, paging: $next_token) { + items { + + icon + message + notificationType + data + + actor { + identity { + trunk { title } + turbot { + title + id + actorIdentityId + } + } + } + + control { + state + reason + details + type { + uri + trunk { + title + } + turbot { + id + } + } + } + + resource { + data + metadata + trunk { + title + } + turbot { + akas + parentId + path + tags + title + } + type { + uri + trunk { + title + } + turbot { + id + } + } + } + + policySetting { + isCalculated + type { + uri + readOnly + defaultTemplate + defaultTemplateInput + secret + trunk { + title + } + turbot { + id + } + } + value + } + + grant { + roleName + permissionTypeId + permissionLevelId + validToTimestamp + validFromTimestamp + level { + title + } + type { + title + } + identity { + trunk { title } + profileId: get(path: "profileId") + } + } + + activeGrant { + grant { + roleName + permissionTypeId + permissionLevelId + validToTimestamp + validFromTimestamp + level { + title + } + type { + title + } + identity { + trunk { title } + profileId: get(path: "profileId") + } + } + } + + turbot { + controlId + controlNewVersionId + controlOldVersionId + createTimestamp + grantId + grantNewVersionId + grantOldVersionId + id + policySettingId + policySettingNewVersionId + policySettingOldVersionId + processId + resourceId + resourceNewVersionId + resourceOldVersionId + grantId + grantNewVersionId + grantOldVersionId + activeGrantsId + activeGrantsNewVersionId + activeGrantsOldVersionId + type + } + + } + paging { + next + } + } + }` + + queryNotificationGet = ` + query notificationGet($id: ID!) { + notification(id: $id) { + icon + message + notificationType + data + actor { + identity { + trunk { + title + } + turbot { + title + id + actorIdentityId + } + } + } + control { + state + reason + details + type { + uri + trunk { + title + } + turbot { + id + } + } + } + resource { + data + metadata + trunk { + title + } + turbot { + akas + parentId + path + tags + title + } + type { + uri + trunk { + title + } + turbot { + id + } + } + } + policySetting { + isCalculated + type { + uri + readOnly + defaultTemplate + defaultTemplateInput + secret + trunk { + title + } + turbot { + id + } + } + value + } + grant { + roleName + permissionTypeId + permissionLevelId + validToTimestamp + validFromTimestamp + level { + title + } + type { + title + } + identity { + trunk { + title + } + profileId: get(path: "profileId") + } + } + activeGrant { + grant { + roleName + permissionTypeId + permissionLevelId + validToTimestamp + validFromTimestamp + level { + title + } + type { + title + } + identity { + trunk { + title + } + profileId: get(path: "profileId") + } + } + } + turbot { + controlId + controlNewVersionId + controlOldVersionId + createTimestamp + grantId + grantNewVersionId + grantOldVersionId + id + policySettingId + policySettingNewVersionId + policySettingOldVersionId + processId + resourceId + resourceNewVersionId + resourceOldVersionId + grantId + grantNewVersionId + grantOldVersionId + activeGrantsId + activeGrantsNewVersionId + activeGrantsOldVersionId + type + } + } + }` +) + +func listNotification(ctx context.Context, d *plugin.QueryData, _ *plugin.HydrateData) (interface{}, error) { + conn, err := connect(ctx, d) + if err != nil { + plugin.Logger(ctx).Error("turbot_notification.listNotification", "connection_error", err) + return nil, err + } + + filters := []string{} + quals := d.KeyColumnQuals + allQuals := d.Quals + filter := "" + if quals["filter"] != nil { + filter = quals["filter"].GetStringValue() + filters = append(filters, filter) + } + if quals["id"] != nil { + filters = append(filters, fmt.Sprintf("id:%s", getQualListValues(ctx, quals, "id", "int64"))) + } + + if quals["notification_type"] != nil { + filters = append(filters, fmt.Sprintf("notificationType:%s", getQualListValues(ctx, quals, "notification_type", "string"))) + } + + if quals["actor_identity_id"] != nil { + filters = append(filters, fmt.Sprintf("actorIdentityId:%s", getQualListValues(ctx, quals, "actor_identity_id", "int64"))) + } + + if quals["resource_id"] != nil { + filters = append(filters, fmt.Sprintf("resourceId:%s", getQualListValues(ctx, quals, "resource_id", "int64"))) + } + + if quals["resource_type_id"] != nil { + filters = append(filters, fmt.Sprintf("resourceTypeId:%s resourceTypeLevel:self", getQualListValues(ctx, quals, "resource_type_id", "int64"))) + } + + if quals["resource_type_uri"] != nil { + filters = append(filters, fmt.Sprintf("resourceTypeId:%s resourceTypeLevel:self", getQualListValues(ctx, quals, "resource_type_uri", "string"))) + } + + if quals["control_type_id"] != nil { + filters = append(filters, fmt.Sprintf("controlTypeId:%s controlTypeLevel:self", getQualListValues(ctx, quals, "control_type_id", "int64"))) + } + + if quals["control_type_uri"] != nil { + filters = append(filters, fmt.Sprintf("controlTypeId:%s controlTypeLevel:self", getQualListValues(ctx, quals, "control_type_uri", "string"))) + } + + if quals["policy_type_id"] != nil { + filters = append(filters, fmt.Sprintf("policyTypeId:%s policyTypeLevel:self", getQualListValues(ctx, quals, "policy_type_id", "int64"))) + } + + if quals["policy_type_uri"] != nil { + filters = append(filters, fmt.Sprintf("policyTypeId:%s policyTypeLevel:self", getQualListValues(ctx, quals, "policy_type_uri", "string"))) + } + + if allQuals["create_timestamp"] != nil { + for _, q := range allQuals["create_timestamp"].Quals { + // Subtracted 1 minute to FilterFrom time and Added 1 minute to FilterTo time to miss any results due to time conersions in steampipe + switch q.Operator { + case "=": + filters = append(filters, fmt.Sprintf("createTimestamp:'%s'", q.Value.GetTimestampValue().AsTime().Format(filterTimeFormat))) + case ">=", ">": + filters = append(filters, fmt.Sprintf("createTimestamp:>='%s'", q.Value.GetTimestampValue().AsTime().Add(-1*time.Minute).Format(filterTimeFormat))) + case "<", "<=": + filters = append(filters, fmt.Sprintf("createTimestamp:<='%s'", q.Value.GetTimestampValue().AsTime().Add(1*time.Minute).Format(filterTimeFormat))) + } + } + } + + // Default to a very large page size. Page sizes earlier in the filter string + // win, so this is only used as a fallback. + pageResults := false + // Add a limit if they haven't given one in the filter field + re := regexp.MustCompile(`(^|\s)limit:[0-9]+($|\s)`) + if !re.MatchString(filter) { + // The caller did not specify a limit, so set a high limit and page all + // results. + pageResults = true + var pageLimit int64 = 5000 + + // Adjust page limit, if less than default value + limit := d.QueryContext.Limit + if d.QueryContext.Limit != nil { + if *limit < pageLimit { + pageLimit = *limit + } + } + filters = append(filters, fmt.Sprintf("limit:%s", strconv.Itoa(int(pageLimit)))) + } + + plugin.Logger(ctx).Warn("turbot_notification.listNotification", "filters", filters) + + nextToken := "" + for { + result := &NotificationsResponse{} + err = conn.DoRequest(queryNotificationList, map[string]interface{}{"filter": filters, "next_token": nextToken}, result) + if err != nil { + plugin.Logger(ctx).Error("turbot_notification.listNotification", "query_error", err) + // Not returning for function in case of errors because of resources/policies/controls referred might be deleted and + // graphql queries may fail to retrieve few properties for such items + // return nil, err + } + for _, r := range result.Notifications.Items { + d.StreamListItem(ctx, r) + + // Context can be cancelled due to manual cancellation or the limit has been hit + if d.QueryStatus.RowsRemaining(ctx) == 0 { + return nil, nil + } + } + if !pageResults || result.Notifications.Paging.Next == "" { + break + } + nextToken = result.Notifications.Paging.Next + } + + return nil, nil +} + +func getNotification(ctx context.Context, d *plugin.QueryData, _ *plugin.HydrateData) (interface{}, error) { + conn, err := connect(ctx, d) + if err != nil { + plugin.Logger(ctx).Error("turbot_notification.getNotification", "connection_error", err) + return nil, err + } + id := d.KeyColumnQuals["id"].GetInt64Value() + result := &NotificationsGetResponse{} + err = conn.DoRequest(queryNotificationGet, map[string]interface{}{"id": id}, result) + if err != nil { + plugin.Logger(ctx).Error("turbot_notification.getNotification", "query_error", err) + return nil, err + } + return result.Notification, nil +} + +//// TRANFORM FUNCTION + +// formatPolicyValue:: Policy value can be a string, hcl or a json. +// It will transform the raw value from api into a string if a hcl or json +func formatPolicyFieldsValue(_ context.Context, d *transform.TransformData) (interface{}, error) { + var item = d.HydrateItem.(Notification) + columnName := d.ColumnName + var value interface{} + + if item.PolicySetting != nil { + if columnName == "policy_template_input" { + value = item.PolicySetting.Type.DefaultTemplateInput + } else { + value = item.PolicySetting.Value + } + } + + if value != nil { + switch val := value.(type) { + case string: + return val, nil + case []string, map[string]interface{}, interface{}: + data, err := json.Marshal(val) + if err != nil { + return nil, err + } + return string(data), nil + } + } + + return nil, nil +} + +// fromField:: generates a value by retrieving a field or a set of fields from the source item +func fromField(fieldNames ...string) *transform.ColumnTransforms { + var fieldNameArray []string + fieldNameArray = append(fieldNameArray, fieldNames...) + return &transform.ColumnTransforms{Transforms: []*transform.TransformCall{{Transform: fieldValue, Param: fieldNameArray}}} +} + +// fieldValue function is intended for the start of a transform chain. +// This returns a field value of either the hydrate call result (if present) or the root item if not +// the field name is in the 'Param' +func fieldValue(ctx context.Context, d *transform.TransformData) (interface{}, error) { + var item = d.HydrateItem + var fieldNames []string + + switch p := d.Param.(type) { + case []string: + fieldNames = p + case string: + fieldNames = []string{p} + default: + return nil, fmt.Errorf("'FieldValue' requires one or more string parameters containing property path but received %v", d.Param) + } + + for _, propertyPath := range fieldNames { + fieldValue, ok := helpers.GetNestedFieldValueFromInterface(item, propertyPath) + if ok && !helpers.IsNil(fieldValue) { + return fieldValue, nil + + } + + } + return nil, nil +} diff --git a/turbot/table_turbot_policy_setting.go b/turbot/table_turbot_policy_setting.go index 4299dac..bcbb54c 100644 --- a/turbot/table_turbot_policy_setting.go +++ b/turbot/table_turbot_policy_setting.go @@ -180,7 +180,6 @@ func listPolicySetting(ctx context.Context, d *plugin.QueryData, _ *plugin.Hydra filters = append(filters, fmt.Sprintf("limit:%s", strconv.Itoa(int(pageLimit)))) } - plugin.Logger(ctx).Trace("turbot_policy_setting.listPolicySetting", "quals", quals) plugin.Logger(ctx).Trace("turbot_policy_setting.listPolicySetting", "filters", filters) nextToken := "" diff --git a/turbot/types.go b/turbot/types.go index 209bcd9..d2bc350 100644 --- a/turbot/types.go +++ b/turbot/types.go @@ -261,6 +261,129 @@ type TurbotPolicySettingMetadata struct { ResourceID string } +type NotificationsResponse struct { + Notifications struct { + Items []Notification + Paging struct { + Next string + } + } +} + +type NotificationsGetResponse struct { + Notification Notification +} + +type Notification struct { + Icon string + Message string + NotificationType string + Data interface{} + + Actor struct { + Identity struct { + Trunk struct { + Title *string + } + Turbot struct { + Title *string + ID *string + ActorIdentityID *string + } + } + } + + Control struct { + State string + Reason string + Details interface{} + Type struct { + URI *string + Turbot struct { + ID *string + } + Trunk struct { + Title *string + } + } + } + + Resource struct { + Data interface{} + Metadata interface{} + Type struct { + URI string + Turbot struct { + ID string + } + Trunk struct { + Title string + } + } + Trunk struct { + Title string + } + Turbot struct { + Akas []string + ParentID string + Path string + Tags interface{} + Title string + } + } + + PolicySetting *struct { + isCalculated *bool + Type struct { + URI *string + ReadOnly *bool + DefaultTemplate *string + DefaultTemplateInput interface{} + Secret *bool + Trunk struct { + Title *string + } + Turbot struct { + ID string + } + } + Value interface{} + } + + ActiveGrant struct { + Grant GrantNotification + } + + Grant GrantNotification + + Turbot TurbotNotificationMetadata +} + +type TurbotNotificationMetadata struct { + ControlID *string + ControlNewVersionID *string + ControlOldVersionID *string + CreateTimestamp string + ID string + PolicySettingID *string + PolicySettingNewVersionID *string + PolicySettingOldVersionID *string + ProcessID *string + ResourceID *string + ResourceNewVersionID *string + ResourceOldVersionID *string + ResourceTypeID *string + Timestamp string + UpdateTimestamp *string + VersionID string + GrantID *string + GrantNewVersionID *string + GrantOldVersionID *string + ActiveGrantsID *string + ActiveGrantsNewVersionID *string + ActiveGrantsOldVersionID *string +} + type TagsResponse struct { Tags struct { Items []Tag @@ -295,3 +418,23 @@ type TurbotTagMetadata struct { DeleteTimestamp *string UpdateTimestamp *string } + +type GrantNotification struct { + RoleName *string + PermissionTypeID *string + PermissionLevelId *string + ValidToTimestamp *string + ValidFromTimestamp *string + Level struct { + Title *string + } + Type struct { + Title *string + } + Identity struct { + Trunk struct { + Title *string + } + ProfileID *string + } +} diff --git a/turbot/utils.go b/turbot/utils.go index 633ab65..8bad68f 100644 --- a/turbot/utils.go +++ b/turbot/utils.go @@ -14,6 +14,10 @@ import ( "github.com/turbot/steampipe-plugin-sdk/plugin/transform" ) +const ( + filterTimeFormat = "2006-01-02T15:04:05.000Z" +) + func connect(ctx context.Context, d *plugin.QueryData) (*apiClient.Client, error) { // Load connection from cache, which preserves throttling protection etc