opa.go 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365
  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/pkg/logger"
  13. "helm.sh/helm/v3/pkg/release"
  14. v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  15. "k8s.io/apimachinery/pkg/runtime"
  16. "k8s.io/apimachinery/pkg/runtime/schema"
  17. "k8s.io/client-go/dynamic"
  18. )
  19. type KubernetesPolicies struct {
  20. Policies map[string]KubernetesOPAQueryCollection
  21. }
  22. type KubernetesOPARunner struct {
  23. *KubernetesPolicies
  24. k8sAgent *kubernetes.Agent
  25. dynamicClient dynamic.Interface
  26. }
  27. type KubernetesBuiltInKind string
  28. const (
  29. HelmRelease KubernetesBuiltInKind = "helm_release"
  30. Pod KubernetesBuiltInKind = "pod"
  31. CRDList KubernetesBuiltInKind = "crd_list"
  32. )
  33. type KubernetesOPAQueryCollection struct {
  34. Kind KubernetesBuiltInKind
  35. Match MatchParameters
  36. MustExist bool
  37. Queries []rego.PreparedEvalQuery
  38. }
  39. type MatchParameters struct {
  40. Name string `json:"name"`
  41. Namespace string `json:"namespace"`
  42. ChartName string `json:"chart_name"`
  43. Labels map[string]string `json:"labels"`
  44. // parameters for CRDs
  45. Group string `json:"group"`
  46. Version string `json:"version"`
  47. Resource string `json:"resource"`
  48. }
  49. type OPARecommenderQueryResult struct {
  50. Allow bool
  51. CategoryName string
  52. ObjectID string
  53. PolicyVersion string
  54. PolicySeverity string
  55. PolicyTitle string
  56. PolicyMessage string
  57. }
  58. type rawQueryResult struct {
  59. Allow bool `mapstructure:"ALLOW"`
  60. PolicyID string `mapstructure:"POLICY_ID"`
  61. PolicyVersion string `mapstructure:"POLICY_VERSION"`
  62. PolicySeverity string `mapstructure:"POLICY_SEVERITY"`
  63. PolicyTitle string `mapstructure:"POLICY_TITLE"`
  64. SuccessMessage string `mapstructure:"POLICY_SUCCESS_MESSAGE"`
  65. FailureMessage []string `mapstructure:"FAILURE_MESSAGE"`
  66. }
  67. func NewRunner(policies *KubernetesPolicies, k8sAgent *kubernetes.Agent, dynamicClient dynamic.Interface) *KubernetesOPARunner {
  68. return &KubernetesOPARunner{policies, k8sAgent, dynamicClient}
  69. }
  70. func (runner *KubernetesOPARunner) GetRecommendations(categories []string) ([]*OPARecommenderQueryResult, error) {
  71. collectionNames := categories
  72. if len(categories) == 0 {
  73. for catName, _ := range runner.Policies {
  74. collectionNames = append(collectionNames, catName)
  75. }
  76. }
  77. res := make([]*OPARecommenderQueryResult, 0)
  78. for _, name := range collectionNames {
  79. // look up to determine if the name is registered
  80. queryCollection, exists := runner.Policies[name]
  81. if !exists {
  82. return nil, fmt.Errorf("No policies for %s found", name)
  83. }
  84. var currResults []*OPARecommenderQueryResult
  85. var err error
  86. switch queryCollection.Kind {
  87. case HelmRelease:
  88. currResults, err = runner.runHelmReleaseQueries(name, queryCollection)
  89. case Pod:
  90. currResults, err = runner.runPodQueries(name, queryCollection)
  91. case CRDList:
  92. currResults, err = runner.runCRDListQueries(name, queryCollection)
  93. default:
  94. fmt.Printf("%s is not a supported query kind", queryCollection.Kind)
  95. continue
  96. }
  97. if err != nil {
  98. fmt.Printf("%s", err.Error())
  99. continue
  100. }
  101. res = append(res, currResults...)
  102. }
  103. return res, nil
  104. }
  105. func (runner *KubernetesOPARunner) SetK8sAgent(k8sAgent *kubernetes.Agent) {
  106. runner.k8sAgent = k8sAgent
  107. }
  108. func (runner *KubernetesOPARunner) runHelmReleaseQueries(name string, collection KubernetesOPAQueryCollection) ([]*OPARecommenderQueryResult, error) {
  109. res := make([]*OPARecommenderQueryResult, 0)
  110. helmAgent, err := helm.GetAgentFromK8sAgent("secret", collection.Match.Namespace, logger.New(false, os.Stdout), runner.k8sAgent)
  111. if err != nil {
  112. return nil, err
  113. }
  114. // get the matching helm release(s) based on the match
  115. var helmReleases []*release.Release
  116. if collection.Match.Name != "" {
  117. helmRelease, err := helmAgent.GetRelease(collection.Match.Name, 0, false)
  118. if err != nil {
  119. if collection.MustExist && strings.Contains(err.Error(), "not found") {
  120. return []*OPARecommenderQueryResult{
  121. {
  122. Allow: false,
  123. ObjectID: fmt.Sprintf("helm_release/%s/%s/%s", collection.Match.Namespace, collection.Match.Name, "exists"),
  124. CategoryName: name,
  125. PolicyVersion: "v0.0.1",
  126. PolicySeverity: "high",
  127. PolicyTitle: fmt.Sprintf("The helm release %s must exist", collection.Match.Name),
  128. PolicyMessage: "The helm release was not found on the cluster",
  129. },
  130. }, nil
  131. } else {
  132. return nil, err
  133. }
  134. } else if collection.MustExist {
  135. res = append(res, &OPARecommenderQueryResult{
  136. Allow: true,
  137. ObjectID: fmt.Sprintf("helm_release/%s/%s/%s", collection.Match.Namespace, collection.Match.Name, "exists"),
  138. CategoryName: name,
  139. PolicyVersion: "v0.0.1",
  140. PolicySeverity: "high",
  141. PolicyTitle: fmt.Sprintf("The helm release %s must exist", collection.Match.Name),
  142. PolicyMessage: "The helm release was found",
  143. })
  144. }
  145. helmReleases = append(helmReleases, helmRelease)
  146. } else if collection.Match.ChartName != "" {
  147. prefilterReleases, err := helmAgent.ListReleases(collection.Match.Namespace, &types.ReleaseListFilter{
  148. ByDate: true,
  149. StatusFilter: []string{
  150. "deployed",
  151. "pending",
  152. "pending-install",
  153. "pending-upgrade",
  154. "pending-rollback",
  155. "failed",
  156. },
  157. })
  158. if err != nil {
  159. return nil, err
  160. }
  161. for _, prefilterRelease := range prefilterReleases {
  162. if prefilterRelease.Chart.Name() == collection.Match.ChartName {
  163. helmReleases = append(helmReleases, prefilterRelease)
  164. }
  165. }
  166. } else {
  167. return nil, fmt.Errorf("invalid match parameters")
  168. }
  169. for _, helmRelease := range helmReleases {
  170. for _, query := range collection.Queries {
  171. results, err := query.Eval(
  172. context.Background(),
  173. rego.EvalInput(map[string]interface{}{
  174. "version": helmRelease.Chart.Metadata.Version,
  175. "values": helmRelease.Config,
  176. }),
  177. )
  178. if err != nil {
  179. return nil, err
  180. }
  181. if len(results) == 1 {
  182. rawQueryRes := &rawQueryResult{}
  183. err = mapstructure.Decode(results[0].Expressions[0].Value, rawQueryRes)
  184. if err != nil {
  185. return nil, err
  186. }
  187. res = append(res, rawQueryResToRecommenderQueryResult(
  188. rawQueryRes,
  189. fmt.Sprintf("helm_release/%s/%s/%s", helmRelease.Namespace, helmRelease.Name, rawQueryRes.PolicyID),
  190. name,
  191. ))
  192. }
  193. }
  194. }
  195. return res, nil
  196. }
  197. func (runner *KubernetesOPARunner) runPodQueries(name string, collection KubernetesOPAQueryCollection) ([]*OPARecommenderQueryResult, error) {
  198. res := make([]*OPARecommenderQueryResult, 0)
  199. lselArr := make([]string, 0)
  200. for k, v := range collection.Match.Labels {
  201. lselArr = append(lselArr, fmt.Sprintf("%s=%s", k, v))
  202. }
  203. lsel := strings.Join(lselArr, ",")
  204. pods, err := runner.k8sAgent.GetPodsByLabel(lsel, collection.Match.Namespace)
  205. if err != nil {
  206. return nil, err
  207. }
  208. for _, pod := range pods.Items {
  209. unstructuredPod, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&pod)
  210. if err != nil {
  211. return nil, err
  212. }
  213. for _, query := range collection.Queries {
  214. results, err := query.Eval(
  215. context.Background(),
  216. rego.EvalInput(unstructuredPod),
  217. )
  218. if err != nil {
  219. return nil, err
  220. }
  221. if len(results) == 1 {
  222. rawQueryRes := &rawQueryResult{}
  223. err = mapstructure.Decode(results[0].Expressions[0].Value, rawQueryRes)
  224. if err != nil {
  225. return nil, err
  226. }
  227. res = append(res, rawQueryResToRecommenderQueryResult(
  228. rawQueryRes,
  229. fmt.Sprintf("pod/%s/%s", pod.Namespace, pod.Name),
  230. name,
  231. ))
  232. }
  233. }
  234. }
  235. return res, nil
  236. }
  237. func (runner *KubernetesOPARunner) runCRDListQueries(name string, collection KubernetesOPAQueryCollection) ([]*OPARecommenderQueryResult, error) {
  238. res := make([]*OPARecommenderQueryResult, 0)
  239. objRes := schema.GroupVersionResource{
  240. Group: collection.Match.Group,
  241. Version: collection.Match.Version,
  242. Resource: collection.Match.Resource,
  243. }
  244. crdList, err := runner.dynamicClient.Resource(objRes).Namespace(collection.Match.Namespace).List(context.Background(), v1.ListOptions{})
  245. if err != nil {
  246. return nil, err
  247. }
  248. for _, crd := range crdList.Items {
  249. for _, query := range collection.Queries {
  250. results, err := query.Eval(
  251. context.Background(),
  252. rego.EvalInput(crd.Object),
  253. )
  254. if err != nil {
  255. return nil, err
  256. }
  257. if len(results) == 1 {
  258. rawQueryRes := &rawQueryResult{}
  259. err = mapstructure.Decode(results[0].Expressions[0].Value, rawQueryRes)
  260. if err != nil {
  261. return nil, err
  262. }
  263. res = append(res, rawQueryResToRecommenderQueryResult(
  264. rawQueryRes,
  265. fmt.Sprintf("%s/%s/%s/%s", collection.Match.Group, collection.Match.Version, collection.Match.Resource, rawQueryRes.PolicyID),
  266. name,
  267. ))
  268. }
  269. }
  270. }
  271. return res, nil
  272. }
  273. func rawQueryResToRecommenderQueryResult(rawQueryRes *rawQueryResult, objectID, categoryName string) *OPARecommenderQueryResult {
  274. queryRes := &OPARecommenderQueryResult{
  275. ObjectID: objectID,
  276. CategoryName: categoryName,
  277. }
  278. message := rawQueryRes.SuccessMessage
  279. // if failure, compose failure messages into single string
  280. if !rawQueryRes.Allow {
  281. message = strings.Join(rawQueryRes.FailureMessage, ". ")
  282. }
  283. queryRes.PolicyMessage = message
  284. queryRes.Allow = rawQueryRes.Allow
  285. queryRes.PolicySeverity = rawQueryRes.PolicySeverity
  286. queryRes.PolicyTitle = rawQueryRes.PolicyTitle
  287. queryRes.PolicyVersion = rawQueryRes.PolicyVersion
  288. return queryRes
  289. }