opa.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455
  1. package opa
  2. import (
  3. "context"
  4. "fmt"
  5. "os"
  6. "strings"
  7. "github.com/mitchellh/mapstructure"
  8. "github.com/open-policy-agent/opa/rego"
  9. "github.com/porter-dev/porter/api/types"
  10. "github.com/porter-dev/porter/internal/helm"
  11. "github.com/porter-dev/porter/internal/kubernetes"
  12. "github.com/porter-dev/porter/internal/models"
  13. "github.com/porter-dev/porter/pkg/logger"
  14. "github.com/stefanmcshane/helm/pkg/release"
  15. v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  16. "k8s.io/apimachinery/pkg/runtime"
  17. "k8s.io/apimachinery/pkg/runtime/schema"
  18. "k8s.io/client-go/dynamic"
  19. )
  20. type KubernetesPolicies struct {
  21. Policies map[string]KubernetesOPAQueryCollection
  22. }
  23. type KubernetesOPARunner struct {
  24. *KubernetesPolicies
  25. cluster *models.Cluster
  26. k8sAgent *kubernetes.Agent
  27. dynamicClient dynamic.Interface
  28. }
  29. type KubernetesBuiltInKind string
  30. const (
  31. HelmRelease KubernetesBuiltInKind = "helm_release"
  32. Pod KubernetesBuiltInKind = "pod"
  33. CRDList KubernetesBuiltInKind = "crd_list"
  34. Daemonset KubernetesBuiltInKind = "daemonset"
  35. )
  36. type KubernetesOPAQueryCollection struct {
  37. Kind KubernetesBuiltInKind
  38. Match MatchParameters
  39. MustExist bool
  40. OverrideSeverity string
  41. Queries []rego.PreparedEvalQuery
  42. }
  43. type MatchParameters struct {
  44. // global cluster match parameters
  45. // KubernetesService is a matching service kind, like `eks`
  46. KubernetesService string `json:"kubernetes_service"`
  47. // parameters for Helm releases
  48. Name string `json:"name"`
  49. Namespace string `json:"namespace"`
  50. ChartName string `json:"chart_name"`
  51. // generic labels parameter
  52. Labels map[string]string `json:"labels"`
  53. // parameters for CRDs
  54. Group string `json:"group"`
  55. Version string `json:"version"`
  56. Resource string `json:"resource"`
  57. }
  58. type OPARecommenderQueryResult struct {
  59. Allow bool
  60. CategoryName string
  61. ObjectID string
  62. PolicyVersion string
  63. PolicySeverity string
  64. PolicyTitle string
  65. PolicyMessage string
  66. }
  67. type rawQueryResult struct {
  68. Allow bool `mapstructure:"ALLOW"`
  69. PolicyID string `mapstructure:"POLICY_ID"`
  70. PolicyVersion string `mapstructure:"POLICY_VERSION"`
  71. PolicySeverity string `mapstructure:"POLICY_SEVERITY"`
  72. PolicyTitle string `mapstructure:"POLICY_TITLE"`
  73. SuccessMessage string `mapstructure:"POLICY_SUCCESS_MESSAGE"`
  74. FailureMessage []string `mapstructure:"FAILURE_MESSAGE"`
  75. }
  76. func NewRunner(policies *KubernetesPolicies, cluster *models.Cluster, k8sAgent *kubernetes.Agent, dynamicClient dynamic.Interface) *KubernetesOPARunner {
  77. return &KubernetesOPARunner{policies, cluster, k8sAgent, dynamicClient}
  78. }
  79. func (runner *KubernetesOPARunner) GetRecommendations(categories []string) ([]*OPARecommenderQueryResult, error) {
  80. collectionNames := categories
  81. if len(categories) == 0 {
  82. for catName := range runner.Policies {
  83. collectionNames = append(collectionNames, catName)
  84. }
  85. }
  86. res := make([]*OPARecommenderQueryResult, 0)
  87. // ping the cluster with a version check to make sure it's reachable - if not, return an error
  88. _, err := runner.k8sAgent.Clientset.Discovery().ServerVersion()
  89. if err != nil {
  90. fmt.Printf("discovery check failed: %v\n", err.Error())
  91. } else {
  92. for _, name := range collectionNames {
  93. // look up to determine if the name is registered
  94. queryCollection, exists := runner.Policies[name]
  95. if !exists {
  96. return nil, fmt.Errorf("No policies for %s found", name)
  97. }
  98. var currResults []*OPARecommenderQueryResult
  99. var err error
  100. // look at global match parameters
  101. if s := queryCollection.Match.KubernetesService; s != "" && strings.ToLower(string(runner.cluster.ToClusterType().Service)) != s {
  102. fmt.Printf("skipping %s as it does not match the cluster service", name)
  103. continue
  104. }
  105. switch queryCollection.Kind {
  106. case HelmRelease:
  107. currResults, err = runner.runHelmReleaseQueries(name, queryCollection)
  108. case Pod:
  109. currResults, err = runner.runPodQueries(name, queryCollection)
  110. case CRDList:
  111. currResults, err = runner.runCRDListQueries(name, queryCollection)
  112. case Daemonset:
  113. currResults, err = runner.runDaemonsetQueries(name, queryCollection)
  114. default:
  115. fmt.Printf("%s is not a supported query kind", queryCollection.Kind)
  116. continue
  117. }
  118. if err != nil {
  119. fmt.Printf("%s", err.Error())
  120. continue
  121. }
  122. res = append(res, currResults...)
  123. }
  124. }
  125. return res, nil
  126. }
  127. func (runner *KubernetesOPARunner) SetK8sAgent(k8sAgent *kubernetes.Agent) {
  128. runner.k8sAgent = k8sAgent
  129. }
  130. func (runner *KubernetesOPARunner) runHelmReleaseQueries(name string, collection KubernetesOPAQueryCollection) ([]*OPARecommenderQueryResult, error) {
  131. res := make([]*OPARecommenderQueryResult, 0)
  132. helmAgent, err := helm.GetAgentFromK8sAgent("secret", collection.Match.Namespace, logger.New(false, os.Stdout), runner.k8sAgent)
  133. if err != nil {
  134. return nil, err
  135. }
  136. // get the matching helm release(s) based on the match
  137. var helmReleases []*release.Release
  138. if collection.Match.Name != "" {
  139. helmRelease, err := helmAgent.GetRelease(collection.Match.Name, 0, false)
  140. if err != nil {
  141. if collection.MustExist && strings.Contains(err.Error(), "not found") {
  142. return []*OPARecommenderQueryResult{
  143. {
  144. Allow: false,
  145. ObjectID: fmt.Sprintf("helm_release/%s/%s/%s", collection.Match.Namespace, collection.Match.Name, "exists"),
  146. CategoryName: name,
  147. PolicyVersion: "v0.0.1",
  148. PolicySeverity: getSeverity("high", collection),
  149. PolicyTitle: fmt.Sprintf("The helm release %s must exist", collection.Match.Name),
  150. PolicyMessage: "The helm release was not found on the cluster",
  151. },
  152. }, nil
  153. } else {
  154. return nil, err
  155. }
  156. } else if collection.MustExist {
  157. res = append(res, &OPARecommenderQueryResult{
  158. Allow: true,
  159. ObjectID: fmt.Sprintf("helm_release/%s/%s/%s", collection.Match.Namespace, collection.Match.Name, "exists"),
  160. CategoryName: name,
  161. PolicyVersion: "v0.0.1",
  162. PolicySeverity: getSeverity("high", collection),
  163. PolicyTitle: fmt.Sprintf("The helm release %s must exist", collection.Match.Name),
  164. PolicyMessage: "The helm release was found",
  165. })
  166. }
  167. helmReleases = append(helmReleases, helmRelease)
  168. } else if collection.Match.ChartName != "" {
  169. prefilterReleases, err := helmAgent.ListReleases(collection.Match.Namespace, &types.ReleaseListFilter{
  170. ByDate: true,
  171. StatusFilter: []string{
  172. "deployed",
  173. "pending",
  174. "pending-install",
  175. "pending-upgrade",
  176. "pending-rollback",
  177. "failed",
  178. },
  179. })
  180. if err != nil {
  181. return nil, err
  182. }
  183. for _, prefilterRelease := range prefilterReleases {
  184. if prefilterRelease.Chart.Name() == collection.Match.ChartName {
  185. helmReleases = append(helmReleases, prefilterRelease)
  186. }
  187. }
  188. } else {
  189. return nil, fmt.Errorf("invalid match parameters")
  190. }
  191. for _, helmRelease := range helmReleases {
  192. for _, query := range collection.Queries {
  193. results, err := query.Eval(
  194. context.Background(),
  195. rego.EvalInput(map[string]interface{}{
  196. "version": helmRelease.Chart.Metadata.Version,
  197. "values": helmRelease.Config,
  198. "name": helmRelease.Name,
  199. "namespace": helmRelease.Namespace,
  200. }),
  201. )
  202. if err != nil {
  203. return nil, err
  204. }
  205. if len(results) == 1 {
  206. rawQueryRes := &rawQueryResult{}
  207. err = mapstructure.Decode(results[0].Expressions[0].Value, rawQueryRes)
  208. if err != nil {
  209. return nil, err
  210. }
  211. res = append(res, rawQueryResToRecommenderQueryResult(
  212. rawQueryRes,
  213. fmt.Sprintf("helm_release/%s/%s/%s", helmRelease.Namespace, helmRelease.Name, rawQueryRes.PolicyID),
  214. name,
  215. collection,
  216. ))
  217. }
  218. }
  219. }
  220. return res, nil
  221. }
  222. func getSeverity(defaultSeverity string, collection KubernetesOPAQueryCollection) string {
  223. if collection.OverrideSeverity != "" {
  224. return collection.OverrideSeverity
  225. }
  226. return defaultSeverity
  227. }
  228. func (runner *KubernetesOPARunner) runPodQueries(name string, collection KubernetesOPAQueryCollection) ([]*OPARecommenderQueryResult, error) {
  229. res := make([]*OPARecommenderQueryResult, 0)
  230. lselArr := make([]string, 0)
  231. for k, v := range collection.Match.Labels {
  232. lselArr = append(lselArr, fmt.Sprintf("%s=%s", k, v))
  233. }
  234. lsel := strings.Join(lselArr, ",")
  235. pods, err := runner.k8sAgent.GetPodsByLabel(lsel, collection.Match.Namespace)
  236. if err != nil {
  237. return nil, err
  238. }
  239. for _, pod := range pods.Items {
  240. unstructuredPod, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&pod)
  241. if err != nil {
  242. return nil, err
  243. }
  244. for _, query := range collection.Queries {
  245. results, err := query.Eval(
  246. context.Background(),
  247. rego.EvalInput(unstructuredPod),
  248. )
  249. if err != nil {
  250. return nil, err
  251. }
  252. if len(results) == 1 {
  253. rawQueryRes := &rawQueryResult{}
  254. err = mapstructure.Decode(results[0].Expressions[0].Value, rawQueryRes)
  255. if err != nil {
  256. return nil, err
  257. }
  258. res = append(res, rawQueryResToRecommenderQueryResult(
  259. rawQueryRes,
  260. fmt.Sprintf("pod/%s/%s", pod.Namespace, pod.Name),
  261. name,
  262. collection,
  263. ))
  264. }
  265. }
  266. }
  267. return res, nil
  268. }
  269. func (runner *KubernetesOPARunner) runDaemonsetQueries(name string, collection KubernetesOPAQueryCollection) ([]*OPARecommenderQueryResult, error) {
  270. res := make([]*OPARecommenderQueryResult, 0)
  271. lselArr := make([]string, 0)
  272. for k, v := range collection.Match.Labels {
  273. lselArr = append(lselArr, fmt.Sprintf("%s=%s", k, v))
  274. }
  275. lsel := strings.Join(lselArr, ",")
  276. daemonsets, err := runner.k8sAgent.Clientset.AppsV1().DaemonSets(collection.Match.Namespace).List(context.Background(), v1.ListOptions{
  277. LabelSelector: lsel,
  278. })
  279. if err != nil {
  280. return nil, err
  281. }
  282. for _, ds := range daemonsets.Items {
  283. unstructuredDS, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&ds)
  284. if err != nil {
  285. return nil, err
  286. }
  287. for _, query := range collection.Queries {
  288. results, err := query.Eval(
  289. context.Background(),
  290. rego.EvalInput(unstructuredDS),
  291. )
  292. if err != nil {
  293. return nil, err
  294. }
  295. if len(results) == 1 {
  296. rawQueryRes := &rawQueryResult{}
  297. err = mapstructure.Decode(results[0].Expressions[0].Value, rawQueryRes)
  298. if err != nil {
  299. return nil, err
  300. }
  301. res = append(res, rawQueryResToRecommenderQueryResult(
  302. rawQueryRes,
  303. fmt.Sprintf("daemonset/%s/%s", ds.Namespace, ds.Name),
  304. name,
  305. collection,
  306. ))
  307. }
  308. }
  309. }
  310. return res, nil
  311. }
  312. func (runner *KubernetesOPARunner) runCRDListQueries(name string, collection KubernetesOPAQueryCollection) ([]*OPARecommenderQueryResult, error) {
  313. res := make([]*OPARecommenderQueryResult, 0)
  314. objRes := schema.GroupVersionResource{
  315. Group: collection.Match.Group,
  316. Version: collection.Match.Version,
  317. Resource: collection.Match.Resource,
  318. }
  319. // just case on the "core" group and unset it
  320. if collection.Match.Group == "core" {
  321. objRes.Group = ""
  322. }
  323. crdList, err := runner.dynamicClient.Resource(objRes).Namespace(collection.Match.Namespace).List(context.Background(), v1.ListOptions{})
  324. if err != nil {
  325. return nil, err
  326. }
  327. for _, crd := range crdList.Items {
  328. for _, query := range collection.Queries {
  329. results, err := query.Eval(
  330. context.Background(),
  331. rego.EvalInput(crd.Object),
  332. )
  333. if err != nil {
  334. return nil, err
  335. }
  336. if len(results) == 1 {
  337. rawQueryRes := &rawQueryResult{}
  338. err = mapstructure.Decode(results[0].Expressions[0].Value, rawQueryRes)
  339. if err != nil {
  340. return nil, err
  341. }
  342. res = append(res, rawQueryResToRecommenderQueryResult(
  343. rawQueryRes,
  344. fmt.Sprintf("%s/%s/%s/%s", collection.Match.Group, collection.Match.Version, collection.Match.Resource, rawQueryRes.PolicyID),
  345. name,
  346. collection,
  347. ))
  348. }
  349. }
  350. }
  351. return res, nil
  352. }
  353. func rawQueryResToRecommenderQueryResult(rawQueryRes *rawQueryResult, objectID, categoryName string, collection KubernetesOPAQueryCollection) *OPARecommenderQueryResult {
  354. queryRes := &OPARecommenderQueryResult{
  355. ObjectID: objectID,
  356. CategoryName: categoryName,
  357. }
  358. message := rawQueryRes.SuccessMessage
  359. // if failure, compose failure messages into single string
  360. if !rawQueryRes.Allow {
  361. message = strings.Join(rawQueryRes.FailureMessage, ". ")
  362. }
  363. queryRes.PolicyMessage = message
  364. queryRes.Allow = rawQueryRes.Allow
  365. queryRes.PolicySeverity = getSeverity(rawQueryRes.PolicySeverity, collection)
  366. queryRes.PolicyTitle = rawQueryRes.PolicyTitle
  367. queryRes.PolicyVersion = rawQueryRes.PolicyVersion
  368. return queryRes
  369. }