opa.go 9.9 KB

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