├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── go.mod ├── kube-query.go ├── tables ├── containers.go ├── containers_test.go ├── deployments.go ├── deployments_test.go ├── nodes.go ├── nodes_test.go ├── pods.go ├── pods_test.go ├── types.go ├── volumes.go └── volumes_test.go └── utils ├── common.go └── helpers.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/* 2 | go.sum 3 | kube-query 4 | conf* 5 | .vscode 6 | .gitignore 7 | *.exe* 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | clean: 2 | rm -f kube-query 3 | 4 | build: 5 | go build -o kube-query kube-query.go 6 | 7 | all: 8 | make clean 9 | make build -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kube-query 2 | 3 | kube-query is an extension for [osquery](https://osquery.io), letting you visualize your cluster using sql queries. 4 | 5 | Deployment 6 | === 7 | ### Prerequisites 8 | #### Go v1.12+ 9 | 10 | ### build 11 | ```bash 12 | $ export GO111MODULE=on 13 | $ go build kube-query.go 14 | ``` 15 | 16 | ## Running kube-query 17 | **When running kube-query, you should always pass the `-kubeconfig` flag, specifying the path to your kubeconfig file.** 18 | 19 | ## osqueryi 20 | When using the [osqueryi tool](https://osquery.readthedocs.io/en/stable/introduction/using-osqueryi/) you can easily register kube-query by passing the -socket parameter to kube-query on another process. For example: 21 | `./kube-query -socket="/path/to/osquery/socket" -kubeconfig="/path/to/kubeconfig.yml"` 22 | 23 | One way to get the path to the osquery socket is like this: 24 | ``` 25 | osqueryi --nodisable_extensions 26 | osquery> select value from osquery_flags where name = 'extensions_socket'; 27 | +-----------------------------------+ 28 | | value | 29 | +-----------------------------------+ 30 | | /Users/USERNAME/.osquery/shell.em | 31 | +-----------------------------------+ 32 | ``` 33 | 34 | There are many other options to automatically [register extensions](https://osquery.readthedocs.io/en/stable/deployment/extensions/). 35 | 36 | ### 37 | 38 | Example Queries 39 | === 40 | ```sql 41 | # query all kube-system pods 42 | SELECT * FROM kubernetes_pods WHERE namespace="kube-system"; 43 | 44 | # query all containers created by kubernetes 45 | SELECT * FROM kubernetes_containers; 46 | 47 | # query all pods that runs with a privileged container 48 | SELECT * 49 | FROM kubernetes_containers 50 | JOIN kubernetes_pods 51 | ON kubernetes_containers.pod_uid=kubernetes_pods.uid 52 | WHERE privileged="True"; 53 | ``` 54 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/aquasecurity/kube-query 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/googleapis/gnostic v0.3.1 // indirect 7 | github.com/imdario/mergo v0.3.7 // indirect 8 | github.com/kolide/osquery-go v0.0.0-20190113061206-be0a8de4cf1d 9 | github.com/stretchr/testify v1.3.0 10 | golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586 // indirect 11 | golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 // indirect 12 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 // indirect 13 | k8s.io/api v0.0.0-20190831074750-7364b6bdad65 14 | k8s.io/apimachinery v0.0.0-20190831074630-461753078381 15 | k8s.io/client-go v0.0.0-20190831074946-3fe2abece89e 16 | k8s.io/metrics v0.0.0-20190831080339-bd7772846802 17 | k8s.io/utils v0.0.0-20190809000727-6c36bc71fc4a // indirect 18 | ) 19 | -------------------------------------------------------------------------------- /kube-query.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | "os" 7 | 8 | "github.com/aquasecurity/kube-query/tables" 9 | "github.com/aquasecurity/kube-query/utils" 10 | osqueryTable "github.com/kolide/osquery-go/plugin/table" 11 | ) 12 | 13 | func main() { 14 | // Parsing flags 15 | kubeconfig := flag.String("kubeconfig", "", "absolute path to the kubeconfig file (can be set by KUBECONFIG environment variable)") 16 | socketPath := flag.String("socket", "", "absolute path to the osquery socket") 17 | 18 | // currently we do not care for these flags, but they must be set for the auto loader of osquery 19 | // the verbose flag is optionally given. 20 | flag.String("timeout", "", "flag for specifying wait time before registering on autoload") 21 | flag.String("interval", "", "flag for specifying wait time before registering on autoload") 22 | flag.Bool("verbose", false, "show more verbose messages (not yet implemented)") 23 | 24 | flag.Parse() 25 | if len(*kubeconfig) == 0 { 26 | // if not specified from flag, try getting from env variable 27 | if *kubeconfig = os.Getenv("KUBECONFIG"); len(*kubeconfig) == 0 { 28 | log.Fatal("Kubeconfig was not specified. set KUBECONFIG environment variable or pass the --kubeconfig flag") 29 | os.Exit(1) 30 | } 31 | } 32 | if len(*socketPath) == 0 { 33 | log.Fatal("Socket was not specified, set the --socket flag") 34 | os.Exit(1) 35 | } 36 | 37 | // initializing clients and extension 38 | kubeclient, err := utils.CreateKubeClient(*kubeconfig) 39 | if err != nil { 40 | log.Fatalf("Error on creating kube-client: %s", err) 41 | panic(err) 42 | } 43 | metricsclient, err := utils.CreateMetricsClient(*kubeconfig) 44 | if err != nil { 45 | log.Fatalf("Error on creating the metrics client: %s", err) 46 | panic(err) 47 | } 48 | extension, err := utils.CreateOsQueryExtension("kube-query", *socketPath) 49 | if err != nil { 50 | log.Fatalf("Error on registering osquery extension: %s", err) 51 | panic(err) 52 | } 53 | 54 | // creating tables and appending to list 55 | tableList := []tables.Table{ 56 | tables.NewPodsTable(kubeclient), 57 | tables.NewContainersTable(kubeclient), 58 | tables.NewVolumesTable(kubeclient), 59 | tables.NewNodesTable(kubeclient, metricsclient), // specific columns use the metrics client 60 | tables.NewDeploymentsTable(kubeclient), 61 | } 62 | 63 | // Registering all tables 64 | for _, t := range tableList { 65 | // Create and register a new table plugin with the server. 66 | extension.RegisterPlugin(osqueryTable.NewPlugin(t.Name(), t.Columns(), t.Generate)) 67 | } 68 | 69 | if err := extension.Run(); err != nil { 70 | log.Fatalf("Error in registering tables: %s", err) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /tables/containers.go: -------------------------------------------------------------------------------- 1 | package tables 2 | 3 | import ( 4 | "context" 5 | "log" 6 | 7 | "github.com/aquasecurity/kube-query/utils" 8 | "github.com/kolide/osquery-go/plugin/table" 9 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | "k8s.io/client-go/kubernetes" 11 | ) 12 | 13 | // ContainersTable implements the Table interface, 14 | // Uses kubeclient to extract information about pods 15 | type ContainersTable struct { 16 | columns []table.ColumnDefinition 17 | name string 18 | client kubernetes.Interface 19 | } 20 | 21 | // NewContainersTable creates a new ContainersTable 22 | // saves given initialized kubernetes client 23 | func NewContainersTable(kubeclient kubernetes.Interface) *ContainersTable { 24 | columns := []table.ColumnDefinition{ 25 | table.TextColumn("name"), 26 | table.TextColumn("pod_uid"), 27 | table.TextColumn("image"), 28 | table.TextColumn("privileged"), // TODO: should we make this an IntegerColumn? 29 | } 30 | return &ContainersTable{ 31 | name: "kubernetes_containers", 32 | columns: columns, 33 | client: kubeclient, 34 | } 35 | } 36 | 37 | // Name Returns name of table 38 | func (t *ContainersTable) Name() string { 39 | return t.name 40 | } 41 | 42 | // Columns Retrieves the initialized columns 43 | func (t *ContainersTable) Columns() []table.ColumnDefinition { 44 | return t.columns 45 | } 46 | 47 | // Generate uses the api to retrieve information on all pods 48 | func (t *ContainersTable) Generate(ctx context.Context, queryContext table.QueryContext) ([]map[string]string, error) { 49 | pods, err := t.client.CoreV1().Pods("").List(metav1.ListOptions{}) 50 | if err != nil { 51 | log.Println("could not get pods from k8s api") 52 | return nil, err 53 | } 54 | 55 | // TODO: think of an efficient way to create the slice without reallocating 56 | rows := make([]map[string]string, 0, len(pods.Items)) 57 | 58 | for _, pod := range pods.Items { 59 | for _, container := range pod.Spec.Containers { 60 | currRow := map[string]string{ 61 | "name": container.Name, 62 | "pod_uid": string(pod.UID), 63 | "image": container.Image, 64 | "privileged": "False", 65 | } 66 | if container.SecurityContext != nil { 67 | if container.SecurityContext.Privileged != nil { 68 | currRow["privileged"] = utils.Bool2str(*container.SecurityContext.Privileged) 69 | } 70 | } 71 | rows = append(rows, currRow) 72 | } 73 | } 74 | return rows, nil 75 | } 76 | -------------------------------------------------------------------------------- /tables/containers_test.go: -------------------------------------------------------------------------------- 1 | package tables 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "testing" 7 | 8 | "github.com/kolide/osquery-go/plugin/table" 9 | "github.com/stretchr/testify/assert" 10 | v1 "k8s.io/api/core/v1" 11 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 | "k8s.io/apimachinery/pkg/runtime" 13 | testclient "k8s.io/client-go/kubernetes/fake" 14 | "k8s.io/client-go/kubernetes/typed/core/v1/fake" 15 | k8stesting "k8s.io/client-go/testing" 16 | ) 17 | 18 | func TestNewContainersTable(t *testing.T) { 19 | tc := testclient.NewSimpleClientset() 20 | ct := NewContainersTable(tc) 21 | 22 | expectedColumns := []table.ColumnDefinition{ 23 | table.TextColumn("name"), 24 | table.TextColumn("pod_uid"), 25 | table.TextColumn("image"), 26 | table.TextColumn("privileged"), 27 | } 28 | 29 | assert.Equal(t, &ContainersTable{ 30 | columns: expectedColumns, 31 | name: "kubernetes_containers", 32 | client: tc, 33 | }, ct) 34 | 35 | assert.Equal(t, "kubernetes_containers", ct.Name()) 36 | assert.Equal(t, expectedColumns, ct.Columns()) 37 | } 38 | 39 | func isPrivileged(b bool) *bool { 40 | return &b 41 | } 42 | 43 | func TestContainersTable_Generate(t *testing.T) { 44 | tc := testclient.NewSimpleClientset() 45 | _, _ = tc.CoreV1().Pods("testing-namespace").Create(&v1.Pod{ 46 | TypeMeta: metav1.TypeMeta{ 47 | Kind: "Pod", 48 | }, 49 | ObjectMeta: metav1.ObjectMeta{ 50 | Namespace: "testing-namespace", 51 | UID: "test-uid", 52 | }, 53 | Spec: v1.PodSpec{ 54 | Containers: []v1.Container{ 55 | { 56 | Name: "test-container-1", 57 | Image: "test-container-image", 58 | SecurityContext: &v1.SecurityContext{ 59 | Privileged: isPrivileged(true), 60 | }, 61 | }, { 62 | Name: "test-container-2", 63 | Image: "test-container-image", 64 | }, 65 | }, 66 | }, 67 | Status: v1.PodStatus{}, 68 | }) 69 | 70 | ct := NewContainersTable(tc) 71 | 72 | m, err := ct.Generate(context.TODO(), table.QueryContext{}) 73 | assert.NoError(t, err) 74 | assert.Equal(t, []map[string]string{ 75 | {"image": "test-container-image", "name": "test-container-1", "pod_uid": "test-uid", "privileged": "True"}, 76 | {"image": "test-container-image", "name": "test-container-2", "pod_uid": "test-uid", "privileged": "False"}, 77 | }, m) 78 | 79 | t.Run("sad path, list pods returns an error", func(t *testing.T) { 80 | tc := testclient.NewSimpleClientset() 81 | tc.CoreV1().(*fake.FakeCoreV1).Fake.PrependReactor("list", "pods", func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) { 82 | return true, nil, errors.New("unable to list pods") 83 | }) 84 | ct := NewContainersTable(tc) 85 | genTable, err := ct.Generate(context.TODO(), table.QueryContext{}) 86 | assert.Equal(t, errors.New("unable to list pods"), err) 87 | assert.Nil(t, genTable) 88 | }) 89 | } 90 | -------------------------------------------------------------------------------- /tables/deployments.go: -------------------------------------------------------------------------------- 1 | package tables 2 | 3 | import ( 4 | "context" 5 | "log" 6 | 7 | "github.com/aquasecurity/kube-query/utils" 8 | "github.com/kolide/osquery-go/plugin/table" 9 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | "k8s.io/client-go/kubernetes" 11 | ) 12 | 13 | // DeploymentsTable implements the Table interface, 14 | // Uses kubeclient to extract information about pods 15 | type DeploymentsTable struct { 16 | columns []table.ColumnDefinition 17 | name string 18 | client kubernetes.Interface 19 | } 20 | 21 | // NewDeploymentsTable creates a new DeploymentsTable 22 | // saves given initialized kubernetes client 23 | func NewDeploymentsTable(kubeclient kubernetes.Interface) *DeploymentsTable { 24 | columns := []table.ColumnDefinition{ 25 | table.TextColumn("name"), 26 | table.TextColumn("namespace"), 27 | table.TextColumn("selector"), 28 | } 29 | return &DeploymentsTable{ 30 | name: "kubernetes_deployments", 31 | columns: columns, 32 | client: kubeclient, 33 | } 34 | } 35 | 36 | // Name Returns name of table 37 | func (t *DeploymentsTable) Name() string { 38 | return t.name 39 | } 40 | 41 | // Columns Retrieves the initialized columns 42 | func (t *DeploymentsTable) Columns() []table.ColumnDefinition { 43 | return t.columns 44 | } 45 | 46 | // Generate uses the api to retrieve information on all pods 47 | func (t *DeploymentsTable) Generate(ctx context.Context, queryContext table.QueryContext) ([]map[string]string, error) { 48 | depls, err := t.client.ExtensionsV1beta1().Deployments("").List(metav1.ListOptions{}) 49 | if err != nil { 50 | log.Println("could not get deployments from k8s api") 51 | return nil, err 52 | } 53 | rows := make([]map[string]string, len(depls.Items)) 54 | for i, depl := range depls.Items { 55 | rows[i] = map[string]string{ 56 | "name": depl.Name, 57 | "namespace": depl.Namespace, 58 | // TODO: check whether Selector is always using LabelSelector 59 | "selector": utils.Map2Str(depl.Spec.Selector.MatchLabels), 60 | } 61 | } 62 | return rows, nil 63 | } 64 | -------------------------------------------------------------------------------- /tables/deployments_test.go: -------------------------------------------------------------------------------- 1 | package tables 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "testing" 7 | 8 | "github.com/kolide/osquery-go/plugin/table" 9 | "github.com/stretchr/testify/assert" 10 | "k8s.io/api/extensions/v1beta1" 11 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 | "k8s.io/apimachinery/pkg/runtime" 13 | testclient "k8s.io/client-go/kubernetes/fake" 14 | "k8s.io/client-go/kubernetes/typed/core/v1/fake" 15 | k8stesting "k8s.io/client-go/testing" 16 | ) 17 | 18 | func TestNewDeploymentsTable(t *testing.T) { 19 | tc := testclient.NewSimpleClientset() 20 | dt := NewDeploymentsTable(tc) 21 | 22 | expectedColumns := []table.ColumnDefinition{ 23 | table.TextColumn("name"), 24 | table.TextColumn("namespace"), 25 | table.TextColumn("selector"), 26 | } 27 | 28 | assert.Equal(t, &DeploymentsTable{ 29 | name: "kubernetes_deployments", 30 | columns: expectedColumns, 31 | client: tc, 32 | }, dt) 33 | 34 | assert.Equal(t, "kubernetes_deployments", dt.Name()) 35 | assert.Equal(t, expectedColumns, dt.Columns()) 36 | } 37 | 38 | func TestDeploymentsTable_Generate(t *testing.T) { 39 | tc := testclient.NewSimpleClientset() 40 | _, _ = tc.ExtensionsV1beta1().Deployments("testing-namespace").Create(&v1beta1.Deployment{ 41 | TypeMeta: v1.TypeMeta{ 42 | Kind: "Deployment", 43 | }, 44 | ObjectMeta: v1.ObjectMeta{ 45 | Name: "testing-deployment", 46 | Namespace: "testing-namespace", 47 | }, 48 | Spec: v1beta1.DeploymentSpec{ 49 | Selector: &v1.LabelSelector{ 50 | MatchLabels: map[string]string{"foo": "bar"}, 51 | }, 52 | }, 53 | Status: v1beta1.DeploymentStatus{}, 54 | }) 55 | 56 | ct := NewDeploymentsTable(tc) 57 | 58 | m, err := ct.Generate(context.TODO(), table.QueryContext{}) 59 | assert.NoError(t, err) 60 | assert.Equal(t, []map[string]string{ 61 | {"name": "testing-deployment", "namespace": "testing-namespace", "selector": "foo=bar,"}, 62 | }, m) 63 | 64 | t.Run("sad path, list deployments returns an error", func(t *testing.T) { 65 | tc := testclient.NewSimpleClientset() 66 | tc.CoreV1().(*fake.FakeCoreV1).Fake.PrependReactor("list", "deployments", func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) { 67 | return true, nil, errors.New("unable to list deployments") 68 | }) 69 | ct := NewDeploymentsTable(tc) 70 | genTable, err := ct.Generate(context.TODO(), table.QueryContext{}) 71 | assert.Equal(t, errors.New("unable to list deployments"), err) 72 | assert.Nil(t, genTable) 73 | }) 74 | } 75 | -------------------------------------------------------------------------------- /tables/nodes.go: -------------------------------------------------------------------------------- 1 | package tables 2 | 3 | import ( 4 | "context" 5 | "log" 6 | 7 | // "github.com/aquasecurity/kube-query/utils" 8 | "github.com/kolide/osquery-go/plugin/table" 9 | corev1 "k8s.io/api/core/v1" 10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | "k8s.io/client-go/kubernetes" 12 | metrics "k8s.io/metrics/pkg/client/clientset/versioned" 13 | ) 14 | 15 | // NodesTable implements the Table interface, 16 | // Uses kubeclient to extract information about pods 17 | type NodesTable struct { 18 | columns []table.ColumnDefinition 19 | name string 20 | client kubernetes.Interface 21 | metricsClient metrics.Interface 22 | } 23 | 24 | // NewNodesTable creates a new NodesTable 25 | // saves given initialized kubernetes client 26 | func NewNodesTable(kubeclient kubernetes.Interface, mc metrics.Interface) *NodesTable { 27 | columns := []table.ColumnDefinition{ 28 | table.TextColumn("name"), 29 | table.TextColumn("role"), 30 | table.TextColumn("external_ip"), 31 | table.TextColumn("kernel_version"), 32 | table.TextColumn("kubelet_version"), 33 | table.TextColumn("cpu_usage"), 34 | table.TextColumn("memory_usage"), 35 | } 36 | return &NodesTable{ 37 | name: "kubernetes_nodes", 38 | columns: columns, 39 | client: kubeclient, 40 | metricsClient: mc, 41 | } 42 | } 43 | 44 | // Name Returns name of table 45 | func (t *NodesTable) Name() string { 46 | return t.name 47 | } 48 | 49 | // Columns Retrieves the initialized columns 50 | func (t *NodesTable) Columns() []table.ColumnDefinition { 51 | return t.columns 52 | } 53 | 54 | // Generate uses the api to retrieve information on all pods 55 | func (t *NodesTable) Generate(ctx context.Context, queryContext table.QueryContext) ([]map[string]string, error) { 56 | nodes, err := t.client.CoreV1().Nodes().List(metav1.ListOptions{}) 57 | if err != nil { 58 | log.Println("could not list nodes from k8s api") 59 | return nil, err 60 | } 61 | 62 | rows := make([]map[string]string, len(nodes.Items)) 63 | for i, node := range nodes.Items { 64 | currRow := map[string]string{ 65 | "name": node.Status.NodeInfo.BootID, 66 | "kernel_version": node.Status.NodeInfo.KernelVersion, 67 | "kubelet_version": node.Status.NodeInfo.KubeletVersion, 68 | "role": "slave", // default to slave, unless decided master 69 | } 70 | 71 | // checking if it's a master node 72 | if _, hasMasterRoleLabel := node.ObjectMeta.Labels["node-role.kubernetes.io/master"]; hasMasterRoleLabel { 73 | currRow["role"] = "master" 74 | } 75 | 76 | // setting addresses 77 | for _, address := range node.Status.Addresses { 78 | if address.Type == corev1.NodeHostName { 79 | currRow["name"] = address.Address 80 | } else if address.Type == corev1.NodeExternalIP { 81 | currRow["external_ip"] = address.Address 82 | } 83 | } 84 | 85 | // if nodename exists, we extract metrics using the name 86 | if nodename := currRow["name"]; nodename != "" { 87 | nodemetric, err := t.metricsClient.MetricsV1beta1().NodeMetricses().Get(nodename, metav1.GetOptions{}) 88 | if err == nil { 89 | // nodemetric.Usage is of type *resource.Quantity. from: 90 | // https://github.com/kubernetes/apimachinery/blob/master/pkg/api/resource/quantity.go 91 | if cpu, ok := nodemetric.Usage[corev1.ResourceCPU]; ok { 92 | currRow["cpu_usage"] = cpu.String() 93 | } 94 | if memory, ok := nodemetric.Usage[corev1.ResourceMemory]; ok { 95 | currRow["memory_usage"] = memory.String() 96 | } 97 | } 98 | } 99 | 100 | rows[i] = currRow 101 | } 102 | return rows, nil 103 | } 104 | -------------------------------------------------------------------------------- /tables/nodes_test.go: -------------------------------------------------------------------------------- 1 | package tables 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "testing" 7 | 8 | "k8s.io/client-go/kubernetes/typed/core/v1/fake" 9 | 10 | "github.com/kolide/osquery-go/plugin/table" 11 | "github.com/stretchr/testify/assert" 12 | v1 "k8s.io/api/core/v1" 13 | "k8s.io/apimachinery/pkg/api/resource" 14 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 15 | "k8s.io/apimachinery/pkg/runtime" 16 | testclient "k8s.io/client-go/kubernetes/fake" 17 | k8stesting "k8s.io/client-go/testing" 18 | "k8s.io/metrics/pkg/apis/metrics/v1beta1" 19 | metricsclient "k8s.io/metrics/pkg/client/clientset/versioned/fake" 20 | ) 21 | 22 | func TestNewNodesTable(t *testing.T) { 23 | kc := testclient.NewSimpleClientset() 24 | mc := metricsclient.NewSimpleClientset() 25 | 26 | expectedColumns := []table.ColumnDefinition{ 27 | table.TextColumn("name"), 28 | table.TextColumn("role"), 29 | table.TextColumn("external_ip"), 30 | table.TextColumn("kernel_version"), 31 | table.TextColumn("kubelet_version"), 32 | table.TextColumn("cpu_usage"), 33 | table.TextColumn("memory_usage"), 34 | } 35 | 36 | nt := NewNodesTable(kc, mc) 37 | assert.Equal(t, &NodesTable{ 38 | name: "kubernetes_nodes", 39 | columns: expectedColumns, 40 | client: kc, 41 | metricsClient: mc, 42 | }, nt) 43 | 44 | assert.Equal(t, "kubernetes_nodes", nt.Name()) 45 | assert.Equal(t, expectedColumns, nt.Columns()) 46 | 47 | } 48 | 49 | func TestNodesTable_Generate(t *testing.T) { 50 | kc := testclient.NewSimpleClientset() 51 | mc := metricsclient.NewSimpleClientset() 52 | 53 | _, _ = kc.CoreV1().Nodes().Create(&v1.Node{ 54 | TypeMeta: metav1.TypeMeta{}, 55 | ObjectMeta: metav1.ObjectMeta{ 56 | Name: "node-slave", 57 | }, 58 | Spec: v1.NodeSpec{}, 59 | Status: v1.NodeStatus{ 60 | NodeInfo: v1.NodeSystemInfo{ 61 | BootID: "node-slave", 62 | KernelVersion: "1.2.3", 63 | KubeletVersion: "4.5.6", 64 | }, 65 | Addresses: []v1.NodeAddress{ 66 | { 67 | Type: v1.NodeExternalIP, 68 | Address: "1.2.3.4", 69 | }, 70 | { 71 | Type: v1.NodeHostName, 72 | Address: "node-slave-hostname", 73 | }, 74 | }, 75 | }, 76 | }) 77 | 78 | _, _ = kc.CoreV1().Nodes().Create(&v1.Node{ 79 | TypeMeta: metav1.TypeMeta{}, 80 | ObjectMeta: metav1.ObjectMeta{ 81 | Name: "node-master", 82 | Labels: map[string]string{"node-role.kubernetes.io/master": "masterlabel"}, 83 | }, 84 | Spec: v1.NodeSpec{}, 85 | Status: v1.NodeStatus{ 86 | NodeInfo: v1.NodeSystemInfo{ 87 | BootID: "node-master", 88 | KernelVersion: "1.2.3", 89 | KubeletVersion: "4.5.6", 90 | }, 91 | }, 92 | }) 93 | 94 | mc.Fake.PrependReactor("get", "nodes", func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) { 95 | return true, &v1beta1.NodeMetrics{ 96 | TypeMeta: metav1.TypeMeta{}, 97 | ObjectMeta: metav1.ObjectMeta{}, 98 | Timestamp: metav1.Time{}, 99 | Window: metav1.Duration{}, 100 | Usage: v1.ResourceList{ 101 | "cpu": resource.NewQuantity(16, resource.BinarySI).DeepCopy(), 102 | "memory": resource.NewQuantity(10*1024*1024*1024, resource.BinarySI).DeepCopy(), 103 | }, 104 | }, nil 105 | }) 106 | 107 | nt := NewNodesTable(kc, mc) 108 | m, err := nt.Generate(context.TODO(), table.QueryContext{}) 109 | assert.NoError(t, err) 110 | 111 | assert.Equal(t, []map[string]string{ 112 | { 113 | "cpu_usage": "16", "external_ip": "1.2.3.4", "kernel_version": "1.2.3", "kubelet_version": "4.5.6", 114 | "memory_usage": "10Gi", "name": "node-slave-hostname", "role": "slave", 115 | }, 116 | { 117 | "cpu_usage": "16", "kernel_version": "1.2.3", "kubelet_version": "4.5.6", 118 | "memory_usage": "10Gi", "name": "node-master", "role": "master", 119 | }, 120 | }, m) 121 | 122 | t.Run("sad path, node list returns an error", func(t *testing.T) { 123 | kc := testclient.NewSimpleClientset() 124 | kc.CoreV1().(*fake.FakeCoreV1).Fake.PrependReactor("list", "nodes", func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) { 125 | return true, nil, errors.New("unable to get nodes") 126 | }) 127 | mc := metricsclient.NewSimpleClientset() 128 | ct := NewNodesTable(kc, mc) 129 | 130 | genTable, err := ct.Generate(context.TODO(), table.QueryContext{}) 131 | assert.Equal(t, errors.New("unable to get nodes"), err) 132 | assert.Nil(t, genTable) 133 | }) 134 | } 135 | -------------------------------------------------------------------------------- /tables/pods.go: -------------------------------------------------------------------------------- 1 | package tables 2 | 3 | import ( 4 | "context" 5 | "log" 6 | 7 | "github.com/kolide/osquery-go/plugin/table" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | "k8s.io/client-go/kubernetes" 10 | ) 11 | 12 | // PodsTable implements the Table interface, 13 | // Uses kubeclient to extract information about pods 14 | type PodsTable struct { 15 | columns []table.ColumnDefinition 16 | name string 17 | client kubernetes.Interface 18 | } 19 | 20 | // NewPodsTable creates a new PodsTable 21 | // saves given initialized kubernetes client 22 | func NewPodsTable(kubeclient kubernetes.Interface) *PodsTable { 23 | columns := []table.ColumnDefinition{ 24 | table.TextColumn("uid"), 25 | table.TextColumn("name"), 26 | table.TextColumn("namespace"), 27 | table.TextColumn("ip"), 28 | table.TextColumn("service_account"), 29 | table.TextColumn("node_name"), 30 | table.TextColumn("phase"), 31 | } 32 | return &PodsTable{ 33 | name: "kubernetes_pods", 34 | columns: columns, 35 | client: kubeclient, 36 | } 37 | } 38 | 39 | // Name Returns name of table 40 | func (t *PodsTable) Name() string { 41 | return t.name 42 | } 43 | 44 | // Columns Retrieves the initialized columns 45 | func (t *PodsTable) Columns() []table.ColumnDefinition { 46 | return t.columns 47 | } 48 | 49 | // Generate uses the api to retrieve information on all pods 50 | func (t *PodsTable) Generate(ctx context.Context, queryContext table.QueryContext) ([]map[string]string, error) { 51 | pods, err := t.client.CoreV1().Pods("").List(metav1.ListOptions{}) 52 | if err != nil { 53 | log.Println("could not get pods from k8s api") 54 | return nil, err 55 | } 56 | rows := make([]map[string]string, len(pods.Items)) 57 | for i, pod := range pods.Items { 58 | rows[i] = map[string]string{ 59 | "uid": string(pod.UID), // UID is an alias to string 60 | "name": pod.Name, 61 | "namespace": pod.Namespace, 62 | "ip": pod.Status.PodIP, 63 | "phase": string(pod.Status.Phase), 64 | "service_account": pod.Spec.ServiceAccountName, 65 | "node_name": pod.Spec.NodeName, 66 | } 67 | } 68 | return rows, nil 69 | } 70 | -------------------------------------------------------------------------------- /tables/pods_test.go: -------------------------------------------------------------------------------- 1 | package tables 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "testing" 7 | 8 | "k8s.io/apimachinery/pkg/runtime" 9 | "k8s.io/client-go/kubernetes/typed/core/v1/fake" 10 | k8stesting "k8s.io/client-go/testing" 11 | 12 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 13 | 14 | v1 "k8s.io/api/core/v1" 15 | 16 | "github.com/kolide/osquery-go/plugin/table" 17 | "github.com/stretchr/testify/assert" 18 | testclient "k8s.io/client-go/kubernetes/fake" 19 | ) 20 | 21 | func TestNewPodsTable(t *testing.T) { 22 | tc := testclient.NewSimpleClientset() 23 | 24 | expectedColumns := []table.ColumnDefinition{ 25 | table.TextColumn("uid"), 26 | table.TextColumn("name"), 27 | table.TextColumn("namespace"), 28 | table.TextColumn("ip"), 29 | table.TextColumn("service_account"), 30 | table.TextColumn("node_name"), 31 | table.TextColumn("phase"), 32 | } 33 | 34 | pt := NewPodsTable(tc) 35 | assert.Equal(t, &PodsTable{ 36 | name: "kubernetes_pods", 37 | client: tc, 38 | columns: expectedColumns, 39 | }, pt) 40 | 41 | assert.Equal(t, "kubernetes_pods", pt.Name()) 42 | assert.Equal(t, expectedColumns, pt.Columns()) 43 | } 44 | 45 | func TestPodsTable_Generate(t *testing.T) { 46 | tc := testclient.NewSimpleClientset() 47 | 48 | _, _ = tc.CoreV1().Pods("testing-namespace").Create(&v1.Pod{ 49 | TypeMeta: metav1.TypeMeta{}, 50 | ObjectMeta: metav1.ObjectMeta{ 51 | Name: "foopod", 52 | Namespace: "testing-namespace", 53 | UID: "foo-123-bar-456-baz", 54 | }, 55 | Spec: v1.PodSpec{ 56 | ServiceAccountName: "testing-service-account-name", 57 | NodeName: "testing-node-name", 58 | }, 59 | Status: v1.PodStatus{ 60 | PodIP: "1.2.3.4", 61 | Phase: "running", 62 | }, 63 | }) 64 | 65 | _, _ = tc.CoreV1().Pods("testing-namespace").Create(&v1.Pod{ 66 | TypeMeta: metav1.TypeMeta{}, 67 | ObjectMeta: metav1.ObjectMeta{ 68 | Name: "bazpod", 69 | Namespace: "testing-namespace", 70 | UID: "baz-789-foo-123-bar", 71 | }, 72 | Spec: v1.PodSpec{ 73 | ServiceAccountName: "testing-service-account-name", 74 | NodeName: "testing-node-name", 75 | }, 76 | Status: v1.PodStatus{ 77 | PodIP: "4.5.6.7", 78 | Phase: "finished", 79 | }, 80 | }) 81 | 82 | pt := NewPodsTable(tc) 83 | m, err := pt.Generate(context.TODO(), table.QueryContext{}) 84 | assert.NoError(t, err) 85 | 86 | assert.Equal(t, []map[string]string{ 87 | {"ip": "1.2.3.4", "name": "foopod", "namespace": "testing-namespace", "node_name": "testing-node-name", 88 | "phase": "running", "service_account": "testing-service-account-name", "uid": "foo-123-bar-456-baz"}, 89 | {"ip": "4.5.6.7", "name": "bazpod", "namespace": "testing-namespace", "node_name": "testing-node-name", 90 | "phase": "finished", "service_account": "testing-service-account-name", "uid": "baz-789-foo-123-bar"}, 91 | }, m) 92 | 93 | t.Run("sad path, list pod returns an error", func(t *testing.T) { 94 | tc := testclient.NewSimpleClientset() 95 | tc.CoreV1().(*fake.FakeCoreV1).Fake.PrependReactor("list", "pods", func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) { 96 | return true, nil, errors.New("unable to list pods") 97 | }) 98 | ct := NewPodsTable(tc) 99 | genTable, err := ct.Generate(context.TODO(), table.QueryContext{}) 100 | assert.Equal(t, errors.New("unable to list pods"), err) 101 | assert.Nil(t, genTable) 102 | }) 103 | } 104 | -------------------------------------------------------------------------------- /tables/types.go: -------------------------------------------------------------------------------- 1 | package tables 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/kolide/osquery-go/plugin/table" 7 | ) 8 | 9 | // Table inteface defines the basic Table implementation mechanism for os-query 10 | type Table interface { 11 | // TODO: change the return value of Genreate() to be map to inteface{} ? 12 | Generate(ctx context.Context, queryContext table.QueryContext) ([]map[string]string, error) 13 | Columns() []table.ColumnDefinition 14 | Name() string 15 | } 16 | -------------------------------------------------------------------------------- /tables/volumes.go: -------------------------------------------------------------------------------- 1 | package tables 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "log" 7 | 8 | "github.com/kolide/osquery-go/plugin/table" 9 | corev1 "k8s.io/api/core/v1" 10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | "k8s.io/client-go/kubernetes" 12 | ) 13 | 14 | // VolumesTable implements the Table interface, 15 | // Uses kubeclient to extract information about pods 16 | type VolumesTable struct { 17 | columns []table.ColumnDefinition 18 | name string 19 | client kubernetes.Interface 20 | } 21 | 22 | type VolumeRow struct { 23 | volume *corev1.Volume 24 | fromPod string 25 | } 26 | 27 | // NewVolumesTable creates a new VolumesTable 28 | // saves given initialized kubernetes client 29 | func NewVolumesTable(kubeclient kubernetes.Interface) *VolumesTable { 30 | columns := []table.ColumnDefinition{ 31 | table.TextColumn("name"), 32 | table.TextColumn("type"), 33 | table.TextColumn("source"), 34 | table.TextColumn("from_pod"), 35 | } 36 | return &VolumesTable{ 37 | name: "kubernetes_volumes", 38 | columns: columns, 39 | client: kubeclient, 40 | } 41 | } 42 | 43 | // Name Returns name of table 44 | func (t *VolumesTable) Name() string { 45 | return t.name 46 | } 47 | 48 | // Columns Retrieves the initialized columns 49 | func (t *VolumesTable) Columns() []table.ColumnDefinition { 50 | return t.columns 51 | } 52 | 53 | // Generate uses the api to retrieve information on all pods 54 | func (t *VolumesTable) Generate(ctx context.Context, queryContext table.QueryContext) ([]map[string]string, error) { 55 | allVolumes := t.getVolumesFromAllPods() 56 | rows := make([]map[string]string, len(allVolumes)) 57 | 58 | for i, volumeRow := range allVolumes { 59 | rows[i] = map[string]string{ 60 | "name": volumeRow.volume.Name, 61 | "from_pod": volumeRow.fromPod, 62 | } 63 | // if the volume source is not with zero value 64 | if (corev1.VolumeSource{}) != volumeRow.volume.VolumeSource { 65 | rows[i]["type"], rows[i]["source"] = t.getPathAndTypeFromVolume(&volumeRow.volume.VolumeSource) 66 | } 67 | } 68 | return rows, nil 69 | } 70 | 71 | // getPathAndTypeFromVolume gets the 72 | func (t *VolumesTable) getPathAndTypeFromVolume(volume *corev1.VolumeSource) (string, string) { 73 | var typ, source string 74 | // Because the VolumeSource struct contains alot of optional fields, 75 | // We use the marshal unmarshal to filter the zero values, and get the 76 | // json name representation of the only non zero type in the struct 77 | if bytes, err := json.Marshal(*volume); err == nil { 78 | output := make(map[string]map[string]interface{}) 79 | _ = json.Unmarshal(bytes, &output) 80 | for k, v := range output { 81 | typ = k 82 | strRepr, _ := json.Marshal(v) 83 | source = string(strRepr) 84 | } 85 | } 86 | return typ, source 87 | } 88 | 89 | func (t *VolumesTable) getVolumesFromAllPods() []*VolumeRow { 90 | pods, err := t.client.CoreV1().Pods("").List(metav1.ListOptions{}) 91 | if err != nil { 92 | log.Println("could not get pods from k8s api") 93 | return nil 94 | } 95 | volumes := make([]*VolumeRow, 0) 96 | for _, pod := range pods.Items { 97 | if pod.Spec.Volumes != nil { 98 | for _, volume := range pod.Spec.Volumes { 99 | volumes = append(volumes, &VolumeRow{ 100 | volume: volume.DeepCopy(), 101 | fromPod: pod.Name, 102 | }) 103 | } 104 | } 105 | } 106 | return volumes 107 | } 108 | -------------------------------------------------------------------------------- /tables/volumes_test.go: -------------------------------------------------------------------------------- 1 | package tables 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "testing" 7 | 8 | "k8s.io/apimachinery/pkg/runtime" 9 | "k8s.io/client-go/kubernetes/typed/core/v1/fake" 10 | k8stesting "k8s.io/client-go/testing" 11 | 12 | v1 "k8s.io/api/core/v1" 13 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 14 | 15 | "github.com/kolide/osquery-go/plugin/table" 16 | "github.com/stretchr/testify/assert" 17 | testclient "k8s.io/client-go/kubernetes/fake" 18 | ) 19 | 20 | func TestNewVolumesTable(t *testing.T) { 21 | tc := testclient.NewSimpleClientset() 22 | dt := NewVolumesTable(tc) 23 | 24 | expectedColumns := []table.ColumnDefinition{ 25 | table.TextColumn("name"), 26 | table.TextColumn("type"), 27 | table.TextColumn("source"), 28 | table.TextColumn("from_pod"), 29 | } 30 | 31 | assert.Equal(t, &VolumesTable{ 32 | name: "kubernetes_volumes", 33 | columns: expectedColumns, 34 | client: tc, 35 | }, dt) 36 | 37 | assert.Equal(t, "kubernetes_volumes", dt.Name()) 38 | assert.Equal(t, expectedColumns, dt.Columns()) 39 | } 40 | 41 | func TestVolumesTable_Generate(t *testing.T) { 42 | tc := testclient.NewSimpleClientset() 43 | _, _ = tc.CoreV1().Pods("testing-namespace").Create(&v1.Pod{ 44 | TypeMeta: metav1.TypeMeta{}, 45 | ObjectMeta: metav1.ObjectMeta{ 46 | Name: "foo-pod-with-two-volumes", 47 | }, 48 | Spec: v1.PodSpec{ 49 | Volumes: []v1.Volume{ 50 | { 51 | Name: "volume-1", 52 | VolumeSource: v1.VolumeSource{ 53 | HostPath: &v1.HostPathVolumeSource{ 54 | Path: "/foo1/bar/baz", 55 | Type: nil, 56 | }, 57 | }, 58 | }, 59 | { 60 | Name: "volume-2", 61 | VolumeSource: v1.VolumeSource{ 62 | HostPath: &v1.HostPathVolumeSource{ 63 | Path: "/foo2/bar/baz", 64 | Type: nil, 65 | }, 66 | }, 67 | }, 68 | }, 69 | }, 70 | Status: v1.PodStatus{}, 71 | }) 72 | 73 | dt := NewVolumesTable(tc) 74 | genTable, err := dt.Generate(context.TODO(), table.QueryContext{}) 75 | assert.NoError(t, err) 76 | assert.Equal(t, []map[string]string{ 77 | { 78 | "from_pod": "foo-pod-with-two-volumes", "name": "volume-1", "source": `{"path":"/foo1/bar/baz"}`, "type": "hostPath", 79 | }, 80 | { 81 | "from_pod": "foo-pod-with-two-volumes", "name": "volume-2", "source": `{"path":"/foo2/bar/baz"}`, "type": "hostPath", 82 | }, 83 | }, genTable) 84 | 85 | t.Run("sad path, list pod returns an error", func(t *testing.T) { 86 | tc := testclient.NewSimpleClientset() 87 | tc.CoreV1().(*fake.FakeCoreV1).Fake.PrependReactor("list", "pods", func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) { 88 | return true, nil, errors.New("unable to list pods") 89 | }) 90 | ct := NewVolumesTable(tc) 91 | genTable, err := ct.Generate(context.TODO(), table.QueryContext{}) 92 | assert.NoError(t, err) 93 | assert.Empty(t, genTable) 94 | }) 95 | } 96 | -------------------------------------------------------------------------------- /utils/common.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "github.com/kolide/osquery-go" 5 | "k8s.io/client-go/kubernetes" 6 | "k8s.io/client-go/tools/clientcmd" 7 | metrics "k8s.io/metrics/pkg/client/clientset/versioned" 8 | ) 9 | 10 | // CreateKubeClient Creates the kubernetes client using the kubeconfig path 11 | func CreateKubeClient(kubeconfig string) (kubernetes.Interface, error) { 12 | // use the current context in kubeconfig 13 | config, err := clientcmd.BuildConfigFromFlags("", kubeconfig) 14 | if err != nil { 15 | return nil, err 16 | } 17 | // create the clientset 18 | client, err := kubernetes.NewForConfig(config) 19 | if err != nil { 20 | return nil, err 21 | } 22 | return client, nil 23 | } 24 | 25 | // CreateMetricsClient creates a metrics client to use for fetching metrics data of cluster 26 | func CreateMetricsClient(kubeconfig string) (*metrics.Clientset, error) { 27 | config, err := clientcmd.BuildConfigFromFlags("", kubeconfig) 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | mc, err := metrics.NewForConfig(config) 33 | if err != nil { 34 | return nil, err 35 | } 36 | return mc, nil 37 | } 38 | 39 | // CreateOsQueryExtension Generates and registers an osquery extension 40 | // using given osquery socket path 41 | func CreateOsQueryExtension(name, socket string) (server *osquery.ExtensionManagerServer, err error) { 42 | server, err = osquery.NewExtensionManagerServer(name, socket) 43 | return 44 | } 45 | -------------------------------------------------------------------------------- /utils/helpers.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | func Bool2str(b bool) string { 8 | if b { 9 | return "True" 10 | } 11 | return "False" 12 | } 13 | 14 | func Map2Str(m interface{}) (merged string) { 15 | for k, v := range m.(map[string]string) { 16 | merged += fmt.Sprintf("%v=%v,", k, v) 17 | } 18 | return 19 | } --------------------------------------------------------------------------------