opa.go 10 KB

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