opa.go 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  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. }),
  178. )
  179. if err != nil {
  180. return nil, err
  181. }
  182. if len(results) == 1 {
  183. rawQueryRes := &rawQueryResult{}
  184. err = mapstructure.Decode(results[0].Expressions[0].Value, rawQueryRes)
  185. if err != nil {
  186. return nil, err
  187. }
  188. res = append(res, rawQueryResToRecommenderQueryResult(
  189. rawQueryRes,
  190. fmt.Sprintf("helm_release/%s/%s/%s", helmRelease.Namespace, helmRelease.Name, rawQueryRes.PolicyID),
  191. name,
  192. collection,
  193. ))
  194. }
  195. }
  196. }
  197. return res, nil
  198. }
  199. func getSeverity(defaultSeverity string, collection KubernetesOPAQueryCollection) string {
  200. if collection.OverrideSeverity != "" {
  201. return collection.OverrideSeverity
  202. }
  203. return defaultSeverity
  204. }
  205. func (runner *KubernetesOPARunner) runPodQueries(name string, collection KubernetesOPAQueryCollection) ([]*OPARecommenderQueryResult, error) {
  206. res := make([]*OPARecommenderQueryResult, 0)
  207. lselArr := make([]string, 0)
  208. for k, v := range collection.Match.Labels {
  209. lselArr = append(lselArr, fmt.Sprintf("%s=%s", k, v))
  210. }
  211. lsel := strings.Join(lselArr, ",")
  212. pods, err := runner.k8sAgent.GetPodsByLabel(lsel, collection.Match.Namespace)
  213. if err != nil {
  214. return nil, err
  215. }
  216. for _, pod := range pods.Items {
  217. unstructuredPod, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&pod)
  218. if err != nil {
  219. return nil, err
  220. }
  221. for _, query := range collection.Queries {
  222. results, err := query.Eval(
  223. context.Background(),
  224. rego.EvalInput(unstructuredPod),
  225. )
  226. if err != nil {
  227. return nil, err
  228. }
  229. if len(results) == 1 {
  230. rawQueryRes := &rawQueryResult{}
  231. err = mapstructure.Decode(results[0].Expressions[0].Value, rawQueryRes)
  232. if err != nil {
  233. return nil, err
  234. }
  235. res = append(res, rawQueryResToRecommenderQueryResult(
  236. rawQueryRes,
  237. fmt.Sprintf("pod/%s/%s", pod.Namespace, pod.Name),
  238. name,
  239. collection,
  240. ))
  241. }
  242. }
  243. }
  244. return res, nil
  245. }
  246. func (runner *KubernetesOPARunner) runCRDListQueries(name string, collection KubernetesOPAQueryCollection) ([]*OPARecommenderQueryResult, error) {
  247. res := make([]*OPARecommenderQueryResult, 0)
  248. objRes := schema.GroupVersionResource{
  249. Group: collection.Match.Group,
  250. Version: collection.Match.Version,
  251. Resource: collection.Match.Resource,
  252. }
  253. // just case on the "core" group and unset it
  254. if collection.Match.Group == "core" {
  255. objRes.Group = ""
  256. }
  257. crdList, err := runner.dynamicClient.Resource(objRes).Namespace(collection.Match.Namespace).List(context.Background(), v1.ListOptions{})
  258. if err != nil {
  259. return nil, err
  260. }
  261. for _, crd := range crdList.Items {
  262. for _, query := range collection.Queries {
  263. results, err := query.Eval(
  264. context.Background(),
  265. rego.EvalInput(crd.Object),
  266. )
  267. if err != nil {
  268. return nil, err
  269. }
  270. if len(results) == 1 {
  271. rawQueryRes := &rawQueryResult{}
  272. err = mapstructure.Decode(results[0].Expressions[0].Value, rawQueryRes)
  273. if err != nil {
  274. return nil, err
  275. }
  276. res = append(res, rawQueryResToRecommenderQueryResult(
  277. rawQueryRes,
  278. fmt.Sprintf("%s/%s/%s/%s", collection.Match.Group, collection.Match.Version, collection.Match.Resource, rawQueryRes.PolicyID),
  279. name,
  280. collection,
  281. ))
  282. }
  283. }
  284. }
  285. return res, nil
  286. }
  287. func rawQueryResToRecommenderQueryResult(rawQueryRes *rawQueryResult, objectID, categoryName string, collection KubernetesOPAQueryCollection) *OPARecommenderQueryResult {
  288. queryRes := &OPARecommenderQueryResult{
  289. ObjectID: objectID,
  290. CategoryName: categoryName,
  291. }
  292. message := rawQueryRes.SuccessMessage
  293. // if failure, compose failure messages into single string
  294. if !rawQueryRes.Allow {
  295. message = strings.Join(rawQueryRes.FailureMessage, ". ")
  296. }
  297. queryRes.PolicyMessage = message
  298. queryRes.Allow = rawQueryRes.Allow
  299. queryRes.PolicySeverity = getSeverity(rawQueryRes.PolicySeverity, collection)
  300. queryRes.PolicyTitle = rawQueryRes.PolicyTitle
  301. queryRes.PolicyVersion = rawQueryRes.PolicyVersion
  302. return queryRes
  303. }