├── docs ├── imgs │ ├── mutation.png │ ├── policies.png │ ├── agent-hla.png │ ├── violations.png │ ├── check-agent-1.png │ ├── how-to-solve.png │ ├── policy-details.png │ └── policy-violations.png ├── versioning.md ├── release_steps.md ├── workflows.md ├── examples │ └── policy-agent-helmrelease.yaml ├── policy.md ├── development.md └── code_structure.md ├── Dockerfile ├── codecov.yml ├── helm ├── Chart.yaml ├── values.yaml ├── README.md └── crds │ └── pac.weave.works_policysets.yaml ├── pkg ├── logger │ ├── go.mod │ ├── README.md │ └── go.sum ├── policy-core │ ├── domain │ │ ├── testData │ │ │ ├── entity-1.yaml │ │ │ └── mutated-entity-1.yaml │ │ ├── interface.go │ │ ├── policy_config.go │ │ ├── mock │ │ │ ├── policies.go │ │ │ └── sink.go │ │ ├── mutation_test.go │ │ ├── policy.go │ │ ├── entity.go │ │ └── mutation.go │ ├── Makefile │ ├── validation │ │ ├── interface.go │ │ ├── mock │ │ │ └── mock.go │ │ └── common.go │ ├── README.md │ └── go.mod ├── uuid-go │ ├── go.mod │ ├── go.sum │ ├── sql.go │ └── uuid.go ├── opa-core │ ├── domain.go │ ├── go.mod │ ├── core.go │ └── core_test.go └── log │ └── log.go ├── test └── integration │ ├── data │ ├── values.yaml │ ├── resources │ │ ├── mutation_test_resources.yaml │ │ ├── audit_test_resources.yaml │ │ └── admission_test_resources.yaml │ └── state │ │ └── policyconfigs.yaml │ ├── test_utils.go │ └── deploy.sh ├── .github ├── dependabot.yml ├── workflows │ ├── label.yaml │ ├── changelog_configuration.json │ ├── release_helm_repo.yml │ ├── integration_test.yml │ ├── release.yml │ └── build.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ ├── spike.md │ ├── story.md │ ├── bug_report.md │ └── docs.md └── pull_request_template.md ├── policies ├── kustomization.yaml ├── ControllerMinimumReplicaCount.yaml ├── ControllerContainerBlockSysctls.yaml ├── ControllerReadOnlyFileSystem.yaml ├── ControllerContainerAllowingPrivilegeEscalation.yaml └── ControllerContainerRunningAsRoot.yaml ├── config └── crd │ ├── patches │ ├── cainjection_in_policies.yaml │ └── webhook_in_policies.yaml │ ├── kustomizeconfig.yaml │ ├── kustomization.yaml │ └── bases │ └── pac.weave.works_policysets.yaml ├── CODE_OF_CONDUCT.md ├── .gitignore ├── api ├── .dccache ├── go.mod ├── v1 │ ├── groupversion_info.go │ └── policy_types.go ├── v2beta1 │ └── groupversion_info.go ├── v2beta2 │ ├── groupversion_info.go │ ├── policyset_types.go │ └── policyconfig_types.go └── v2beta3 │ ├── groupversion_info.go │ └── policyconfig_types.go ├── internal ├── auditor │ ├── types.go │ └── auditor.go ├── utils │ ├── utils_test.go │ └── utils.go ├── policies │ ├── test_utils.go │ ├── policy.go │ ├── policy_test.go │ └── policyconfig.go ├── sink │ ├── filesystem │ │ ├── types.go │ │ └── filesystem.go │ ├── elastic │ │ ├── elastic_test.go │ │ ├── utils.go │ │ └── schema.json │ ├── flux-notification │ │ ├── flux_notification_test.go │ │ └── flux_notification.go │ └── k8s-event │ │ └── k8s_event.go ├── entities │ └── mock │ │ └── mock.go ├── mutation │ └── mutation.go ├── terraform │ └── terraform.go ├── admission │ ├── testdata │ │ └── testdata.go │ └── admission.go └── clients │ └── kube │ └── kube.go ├── hack └── boilerplate.go.txt ├── CONTRIBUTING.md ├── PROJECT ├── README.md └── configuration └── config.go /docs/imgs/mutation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weaveworks/policy-agent/HEAD/docs/imgs/mutation.png -------------------------------------------------------------------------------- /docs/imgs/policies.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weaveworks/policy-agent/HEAD/docs/imgs/policies.png -------------------------------------------------------------------------------- /docs/imgs/agent-hla.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weaveworks/policy-agent/HEAD/docs/imgs/agent-hla.png -------------------------------------------------------------------------------- /docs/imgs/violations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weaveworks/policy-agent/HEAD/docs/imgs/violations.png -------------------------------------------------------------------------------- /docs/imgs/check-agent-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weaveworks/policy-agent/HEAD/docs/imgs/check-agent-1.png -------------------------------------------------------------------------------- /docs/imgs/how-to-solve.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weaveworks/policy-agent/HEAD/docs/imgs/how-to-solve.png -------------------------------------------------------------------------------- /docs/imgs/policy-details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weaveworks/policy-agent/HEAD/docs/imgs/policy-details.png -------------------------------------------------------------------------------- /docs/imgs/policy-violations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weaveworks/policy-agent/HEAD/docs/imgs/policy-violations.png -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.15 2 | 3 | COPY bin/agent / 4 | 5 | RUN mkdir /logs && chmod -R 777 /logs 6 | 7 | ENTRYPOINT ["/agent"] 8 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | target: auto # auto compares coverage to the previous base commit 6 | threshold: 1.5% # percentage of coverage allowed to be decreased 7 | -------------------------------------------------------------------------------- /helm/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | appVersion: "2.6.0" 3 | description: A Helm chart for Kubernetes to configure the policy agent 4 | name: policy-agent 5 | version: 2.6.0 6 | maintainers: 7 | - name: Weaveworks 8 | email: support@weave.works 9 | -------------------------------------------------------------------------------- /pkg/logger/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/weaveworks/policy-agent/pkg/logger 2 | 3 | go 1.20 4 | 5 | require go.uber.org/zap v1.24.0 6 | 7 | require ( 8 | go.uber.org/atomic v1.7.0 // indirect 9 | go.uber.org/multierr v1.6.0 // indirect 10 | ) 11 | -------------------------------------------------------------------------------- /test/integration/data/values.yaml: -------------------------------------------------------------------------------- 1 | imageTag: test 2 | config: 3 | admission: 4 | mutate: true 5 | enabled: true 6 | sinks: 7 | k8sEventsSink: 8 | enabled: true 9 | audit: 10 | enabled: true 11 | sinks: 12 | k8sEventsSink: 13 | enabled: true 14 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | reviewers: 8 | - "weaveworks/timber-wolf" 9 | # Only do security updates not version updates. 10 | open-pull-requests-limit: 0 11 | -------------------------------------------------------------------------------- /policies/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | resources: 4 | - ControllerContainerAllowingPrivilegeEscalation.yaml 5 | - ControllerContainerBlockSysctls.yaml 6 | - ControllerContainerRunningAsRoot.yaml 7 | - ControllerMinimumReplicaCount.yaml 8 | - ControllerReadOnlyFileSystem.yaml 9 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_policies.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 7 | name: policies.magalix.com 8 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Community Code of Conduct 2 | 3 | Weaveworks follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md). 4 | 5 | Instances of abusive, harassing, or otherwise unacceptable behavior 6 | may be reported by contacting a Weaveworks project maintainer, or 7 | Alexis Richardson (alexis@weave.works). 8 | -------------------------------------------------------------------------------- /pkg/policy-core/domain/testData/entity-1.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: app-1 5 | labels: 6 | app: app-1 7 | spec: 8 | replicas: 2 9 | template: 10 | metadata: 11 | labels: 12 | app: app-1 13 | spec: 14 | containers: 15 | - name: container-1 16 | securityContext: 17 | privileged: true 18 | -------------------------------------------------------------------------------- /pkg/uuid-go/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/weaveworks/policy-agent/pkg/uuid-go 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8 7 | github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b 8 | ) 9 | 10 | require ( 11 | github.com/kr/pretty v0.2.0 // indirect 12 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect 13 | ) 14 | -------------------------------------------------------------------------------- /pkg/policy-core/Makefile: -------------------------------------------------------------------------------- 1 | mock: 2 | mockgen -package mock -destination domain/mock/policies.go github.com/weaveworks/policy-agent/pkg/policy-core/domain PolicyValidationSink 3 | mockgen -package mock -destination domain/mock/sink.go github.com/weaveworks/policy-agent/pkg/policy-core/domain PoliciesSource 4 | mockgen -package mock -destination validation/mock/mock.go github.com/weaveworks/policy-agent/pkg/policy-core/validation Validator -------------------------------------------------------------------------------- /pkg/policy-core/validation/interface.go: -------------------------------------------------------------------------------- 1 | package validation 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/weaveworks/policy-agent/pkg/policy-core/domain" 7 | ) 8 | 9 | // Validator is responsible for validating policies 10 | type Validator interface { 11 | // Validate returns validation results for the specified entity 12 | Validate(ctx context.Context, entity domain.Entity, trigger string) (*domain.PolicyValidationSummary, error) 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | policies-service 8 | __debug_bin 9 | # Test binary, built with `go test -c` 10 | *.test 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | 15 | # Dependency directories (remove the comment below to include it) 16 | # vendor/ 17 | bin/ 18 | build/ 19 | local.yml 20 | policy-agent 21 | go.work 22 | go.work.sum 23 | -------------------------------------------------------------------------------- /pkg/policy-core/domain/testData/mutated-entity-1.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: app-1 5 | labels: 6 | app: app-1 7 | owner: test 8 | pac.weave.works/mutated: "" 9 | spec: 10 | replicas: 2 11 | template: 12 | metadata: 13 | labels: 14 | app: app-1 15 | spec: 16 | containers: 17 | - name: container-1 18 | securityContext: 19 | privileged: false 20 | 21 | -------------------------------------------------------------------------------- /api/.dccache: -------------------------------------------------------------------------------- 1 | {"/home/chaddad/magalix/magalix-policy-agent/api/v1/groupversion_info.go":[1185,1647338529796.7593,"173f27519d10892baad10eb91aa41fab7690cfaf9084abb8fca7ceef94a4a685"],"/home/chaddad/magalix/magalix-policy-agent/api/v1/policy_types.go":[4203,1647778591324.3938,"a1ff882fed2989b9ab15846e3089a345f4fe4df01d2daf62320e2b662b4867ce"],"/home/chaddad/magalix/magalix-policy-agent/api/v1/zz_generated.deepcopy.go":[4863,1647777616397.6992,"10d40ad93d1ba4bd1915eefb9b8a12ca7a3d7402aaa919d975d6afbdd2299f6c"]} -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_policies.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables a conversion webhook for the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | name: policies.magalix.com 6 | spec: 7 | conversion: 8 | strategy: Webhook 9 | webhook: 10 | clientConfig: 11 | service: 12 | namespace: system 13 | name: webhook-service 14 | path: /convert 15 | conversionReviewVersions: 16 | - v1 17 | -------------------------------------------------------------------------------- /.github/workflows/label.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: 3 | types: [opened, labeled, unlabeled, synchronize] 4 | name: label 5 | jobs: 6 | labelCheck: 7 | name: Check that PR has a label for use in release notes 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Pull request label check 11 | uses: mheap/github-action-required-labels@v1 12 | with: 13 | mode: exactly 14 | count: 1 15 | labels: "bug, enhancement, test, exclude from release notes" 16 | -------------------------------------------------------------------------------- /internal/auditor/types.go: -------------------------------------------------------------------------------- 1 | package auditor 2 | 3 | import "context" 4 | 5 | type AuditEventType string 6 | 7 | const ( 8 | AuditEventTypeInitial AuditEventType = "initial-audit" 9 | AuditEventTypePeriodical AuditEventType = "periodic-audit" 10 | entitiesSizeLimit = 50 11 | TypeAudit = "Audit" 12 | ) 13 | 14 | type AuditEvent struct { 15 | Type AuditEventType 16 | Data interface{} 17 | } 18 | 19 | type AuditEventListener func(ctx context.Context, auditEvent AuditEvent) 20 | -------------------------------------------------------------------------------- /pkg/policy-core/domain/interface.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import "context" 4 | 5 | // PoliciesSource acts as a source for policies 6 | type PoliciesSource interface { 7 | // GetAll returns all available policies 8 | GetAll(ctx context.Context) ([]Policy, error) 9 | GetPolicyConfig(ctx context.Context, entity Entity) (*PolicyConfig, error) 10 | } 11 | 12 | // PolicyValidationSink acts as a sink to send the results of a validation to 13 | type PolicyValidationSink interface { 14 | // Write saves the results 15 | Write(ctx context.Context, PolicyValidations []PolicyValidation) error 16 | } 17 | -------------------------------------------------------------------------------- /config/crd/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # This file is for teaching kustomize how to substitute name and namespace reference in CRD 2 | nameReference: 3 | - kind: Service 4 | version: v1 5 | fieldSpecs: 6 | - kind: CustomResourceDefinition 7 | version: v1 8 | group: apiextensions.k8s.io 9 | path: spec/conversion/webhook/clientConfig/service/name 10 | 11 | namespace: 12 | - kind: CustomResourceDefinition 13 | version: v1 14 | group: apiextensions.k8s.io 15 | path: spec/conversion/webhook/clientConfig/service/namespace 16 | create: false 17 | 18 | varReference: 19 | - path: metadata/annotations 20 | -------------------------------------------------------------------------------- /test/integration/data/resources/mutation_test_resources.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: test-mutation-deployment 5 | namespace: default 6 | labels: 7 | owner: test 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | app: test-mutation-deployment 13 | template: 14 | metadata: 15 | labels: 16 | app: test-mutation-deployment 17 | spec: 18 | containers: 19 | - name: ubuntu 20 | image: ubuntu:latest 21 | command: ["sleep", "100d"] 22 | securityContext: 23 | privileged: false 24 | -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ -------------------------------------------------------------------------------- /test/integration/test_utils.go: -------------------------------------------------------------------------------- 1 | package integration 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os/exec" 7 | 8 | v1 "k8s.io/api/core/v1" 9 | "sigs.k8s.io/controller-runtime/pkg/client" 10 | ) 11 | 12 | func kubectl(args ...string) error { 13 | cmd := exec.Command("kubectl", args...) 14 | stdout, err := cmd.Output() 15 | fmt.Println(string(stdout)) 16 | return err 17 | } 18 | 19 | func listViolationEvents(ctx context.Context, c client.Client, opts []client.ListOption) (*v1.EventList, error) { 20 | var events v1.EventList 21 | if err := c.List(ctx, &events, opts...); err != nil { 22 | return nil, err 23 | } 24 | return &events, nil 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/changelog_configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "categories": [ 3 | { 4 | "title": "## 🚀 Enhancements", 5 | "labels": ["enhancement"] 6 | }, 7 | { 8 | "title": "## 🐛 Bugs", 9 | "labels": ["bug"] 10 | }, 11 | { 12 | "title": "## 🧪 Tests", 13 | "labels": ["test"] 14 | } 15 | ], 16 | "ignore_labels": [ 17 | "null" 18 | ], 19 | "template": "${{CHANGELOG}}\n\n
\nUncategorized\n\n${{UNCATEGORIZED}}\n
", 20 | "pr_template": "- PR: #${{NUMBER}} - ${{TITLE}}", 21 | "empty_template": "- no changes", 22 | "base_branches": [ 23 | "dev" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /docs/versioning.md: -------------------------------------------------------------------------------- 1 | # Agent and policies versioning 2 | 3 | ## Agent versioning 4 | - Increase major version in cases of `Policy` CRD api schema change or any breaking changes. 5 | - Minor version for new features 6 | - Patch version for bug fixes 7 | 8 | The pipeline pushes an image with the same tag to dockerhub and no image is pushed with the latest version anymore. 9 | 10 | ## Policy CRD versioning 11 | 12 | Schema definition is its own go submodule. The versioning should follow the agent major version so the schema API version is consistent. 13 | 14 | > Updating and releasing new version of the [Policy library](https://github.com/weaveworks/policy-library) should be considered if Policy CRDs has a new changes. 15 | -------------------------------------------------------------------------------- /docs/release_steps.md: -------------------------------------------------------------------------------- 1 | # Release Steps 2 | 3 | ## Policy Agent 4 | 5 | - Update the [HelmChart](../helm/Chart.yaml) version. 6 | - Update the HelmChart [values.yaml](../helm/values.yaml) version. 7 | - Create a pull request from `dev` to `master`. 8 | - Create a new tag and push it to origin as the following example: 9 | 10 | ```bash 11 | git tag vx.x.x # replace x with your new tag 12 | git push origin vx.x.x # replace x with your new tag 13 | ``` 14 | 15 | - The `release` workflow will create a new tag for policy agent and will push the new docker image. 16 | 17 | ## Releasing packages under `pkg` 18 | 19 | - Create and push a new tag with the updated version for each packages if it has new changes. 20 | -------------------------------------------------------------------------------- /pkg/opa-core/domain.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/open-policy-agent/opa/ast" 8 | ) 9 | 10 | // Policy contains policy and metedata 11 | type Policy struct { 12 | module *ast.Module 13 | pkg string 14 | } 15 | 16 | type OPAError interface { 17 | GetDetails() interface{} 18 | } 19 | 20 | // NoValidError indicates 21 | type NoValidError struct { 22 | Details interface{} 23 | } 24 | 25 | func (e NoValidError) Error() string { 26 | details, err := json.Marshal(e.Details) 27 | if err != nil { 28 | return fmt.Sprintf("error while parsing error details: %+v", err) 29 | } 30 | return string(details) 31 | } 32 | 33 | func (e NoValidError) GetDetails() interface{} { 34 | return e.Details 35 | } 36 | -------------------------------------------------------------------------------- /docs/workflows.md: -------------------------------------------------------------------------------- 1 | # Workflows 2 | 3 | ## Pull request label check workflow 4 | 5 | Currently, we have `Pull request label check` workflow to Check that PR has a label for use in release notes. 6 | Pull requests require exactly one label from the allowed labels: 7 | 8 | 1. 🚀 **Enhancements** `enhancement`: New feature or request 9 | 2. 🐛 Bugs `bug`: Something isn't working 10 | 3. 🧪 Tests `test`: Mark a PR as being about tests 11 | 4. Uncategorized `exclude from release notes`: Use this label to exclude a PR from the release notes 12 | 13 | ## Build Changelog and Github Release workflow 14 | `Build Changelog and Github Release` workflow is triggered by creating a versioned tag. 15 | This workflow creates a release and generates release notes from Pull requests labels based on changelog configuration. -------------------------------------------------------------------------------- /internal/utils/utils_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestGetFluxObject(t *testing.T) { 11 | fluxObj := GetFluxObject(map[string]string{}) 12 | 13 | if fluxObj != nil { 14 | t.Error("unexpected flux object") 15 | } 16 | 17 | for apiVersion, kind := range fluxControllerKindMap { 18 | fluxObj := GetFluxObject(map[string]string{ 19 | fmt.Sprintf("%s/name", apiVersion): "my-app", 20 | fmt.Sprintf("%s/namespace", apiVersion): "default", 21 | }) 22 | 23 | assert.NotEqual(t, fluxObj, nil) 24 | 25 | assert.Equal(t, fluxObj.GetAPIVersion(), apiVersion) 26 | assert.Equal(t, fluxObj.GetKind(), kind) 27 | assert.Equal(t, fluxObj.GetNamespace(), "default") 28 | assert.Equal(t, fluxObj.GetName(), "my-app") 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /helm/values.yaml: -------------------------------------------------------------------------------- 1 | 2 | image: weaveworks/policy-agent 3 | imageTag: v2.6.0 4 | failurePolicy: Fail 5 | 6 | # If you don't want to use cert-manager, set useCertManager to false and provide your own certs 7 | useCertManager: true 8 | certificate: "" 9 | key: "" 10 | caCertificate: "" 11 | 12 | # exclude namespaces by admission controller 13 | # If not set, The policy agent will exclude only it's namespace ({{.Release.Namespace}}) 14 | excludeNamespaces: 15 | # - policy-system 16 | # - flux-system 17 | # - kube-system 18 | 19 | persistence: 20 | enabled: false 21 | # claimStorage: 1Gi 22 | # storageClassName: standard 23 | 24 | config: 25 | accountId: "" 26 | clusterId: "" 27 | admission: 28 | # mutate: true // enable mutation policies 29 | enabled: true 30 | sinks: 31 | k8sEventsSink: 32 | enabled: true 33 | audit: 34 | enabled: false 35 | -------------------------------------------------------------------------------- /internal/utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | 6 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 7 | ) 8 | 9 | var fluxControllerKindMap = map[string]string{ 10 | "helm.toolkit.fluxcd.io": "HelmRelease", 11 | "kustomize.toolkit.fluxcd.io": "Kustomization", 12 | } 13 | 14 | func GetFluxObject(labels map[string]string) *unstructured.Unstructured { 15 | for apiVersion, kind := range fluxControllerKindMap { 16 | name, ok := labels[fmt.Sprintf("%s/name", apiVersion)] 17 | if !ok { 18 | continue 19 | } 20 | 21 | namespace, ok := labels[fmt.Sprintf("%s/namespace", apiVersion)] 22 | if !ok { 23 | continue 24 | } 25 | 26 | obj := unstructured.Unstructured{} 27 | obj.SetAPIVersion(apiVersion) 28 | obj.SetKind(kind) 29 | obj.SetNamespace(namespace) 30 | obj.SetName(name) 31 | 32 | return &obj 33 | } 34 | return nil 35 | } 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 💡 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: 'enhancement' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 13 | 14 | **Problem** 15 | 18 | 19 | **Solution** 20 | 23 | 24 | **Additional context** 25 | 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/spike.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Spike 🔍 3 | about: Research item (normally created by project members) 4 | title: '' 5 | labels: 'type/spike' 6 | assignees: '' 7 | 8 | --- 9 | **Question** 10 | 13 | 14 | **Short description** 15 | 18 | 19 | **Goal** 20 | 24 | 25 | **Timebox** 26 | 29 | 30 | --- 31 | 32 | **Outcome** 33 | 40 | 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/story.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Story ✍ 3 | about: Agile story (normally created by project members) 4 | title: '' 5 | labels: 'type/story' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Short description** 11 | As a [User Role] I want to [feature/functionality] so that [business outcome/value]. 12 | 13 | **Acceptance criteria** 14 | - [ ] _does X_ 15 | - [ ] _displays Y_ 16 | - [ ] _..._ 17 | 18 | **Design materials** 19 | - Screenshots 20 | - Design diagrams 21 | - WEP 22 | - PR/FAQ 23 | 24 | **Work breakdown** 25 | (optional)Task breakdown of the work 26 | - [ ] _create X_ 27 | - [ ] _design X_ 28 | - [ ] _..._ 29 | 30 | 35 | 36 | -------------------------------------------------------------------------------- /internal/policies/test_utils.go: -------------------------------------------------------------------------------- 1 | package crd 2 | 3 | import ( 4 | "context" 5 | 6 | "k8s.io/apimachinery/pkg/runtime" 7 | "sigs.k8s.io/controller-runtime/pkg/cache/informertest" 8 | "sigs.k8s.io/controller-runtime/pkg/client" 9 | "sigs.k8s.io/controller-runtime/pkg/client/fake" 10 | ) 11 | 12 | type FakeCache struct { 13 | informertest.FakeInformers 14 | client client.Client 15 | } 16 | 17 | func NewFakeCache(s *runtime.Scheme, objs ...runtime.Object) *FakeCache { 18 | cl := fake.NewClientBuilder().WithScheme(s).WithRuntimeObjects(objs...).Build() 19 | return &FakeCache{client: cl} 20 | } 21 | 22 | func (c *FakeCache) List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error { 23 | return c.client.List(ctx, list, opts...) 24 | } 25 | 26 | func (c *FakeCache) Get(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error { 27 | return c.client.Get(ctx, key, obj) 28 | } 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 🐛 3 | about: Create a report to help us improve 4 | title: "" 5 | labels: "bug" 6 | assignees: "" 7 | --- 8 | 9 | **Describe the bug** 10 | 11 | 14 | 15 | **Environment** 16 | 17 | - Agent Version 18 | - Kubernetes version 19 | 20 | 24 | 25 | **To Reproduce** 26 | Steps to reproduce the behavior: 27 | 28 | 35 | 36 | **Expected behavior** 37 | 38 | 41 | 42 | **Actual Behavior** 43 | 44 | 47 | 48 | **Additional Context (screenshots, logs, etc)** 49 | -------------------------------------------------------------------------------- /test/integration/deploy.sh: -------------------------------------------------------------------------------- 1 | echo "[*] Creating test cluster ..." 2 | kind delete cluster --name test 3 | kind create cluster --name test 4 | 5 | kind load docker-image weaveworks/policy-agent:${VERSION} --name test 6 | 7 | kubectl create namespace flux-system 8 | 9 | echo "[*] Installing cert-manager ..." 10 | helm repo add jetstack https://charts.jetstack.io 11 | helm repo update 12 | helm install cert-manager jetstack/cert-manager --namespace cert-manager --create-namespace --version v1.10.1 --set installCRDs=true --wait --timeout 120s 13 | 14 | echo "[*] Apply test resources ..." 15 | kubectl apply -f data/resources/audit_test_resources.yaml 16 | kubectl apply -f ../../helm/crds 17 | 18 | echo "[*] Apply cluster resources" 19 | kubectl apply -f data/state 20 | 21 | echo "[*] Installing policy agent helm chart on namespace ${NAMESPACE} ..." 22 | helm install policy-agent ../../helm -n ${NAMESPACE} -f ../../helm/values.yaml -f data/values.yaml --create-namespace --wait --timeout 60s 23 | -------------------------------------------------------------------------------- /api/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/weaveworks/policy-agent/api 2 | 3 | go 1.19 4 | 5 | require ( 6 | k8s.io/apiextensions-apiserver v0.26.1 7 | k8s.io/apimachinery v0.26.1 8 | sigs.k8s.io/controller-runtime v0.14.4 9 | ) 10 | 11 | require ( 12 | github.com/go-logr/logr v1.2.3 // indirect 13 | github.com/gogo/protobuf v1.3.2 // indirect 14 | github.com/google/gofuzz v1.1.0 // indirect 15 | github.com/json-iterator/go v1.1.12 // indirect 16 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 17 | github.com/modern-go/reflect2 v1.0.2 // indirect 18 | golang.org/x/net v0.3.1-0.20221206200815-1e63c2f08a10 // indirect 19 | golang.org/x/text v0.5.0 // indirect 20 | gopkg.in/inf.v0 v0.9.1 // indirect 21 | gopkg.in/yaml.v2 v2.4.0 // indirect 22 | k8s.io/klog/v2 v2.80.1 // indirect 23 | k8s.io/utils v0.0.0-20221128185143-99ec85e7a448 // indirect 24 | sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect 25 | sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect 26 | ) 27 | -------------------------------------------------------------------------------- /config/crd/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # This kustomization.yaml is not intended to be run by itself, 2 | # since it depends on service name and namespace that are out of this kustomize package. 3 | # It should be run by config/default 4 | resources: 5 | - bases/magalix.com_policies.yaml 6 | #+kubebuilder:scaffold:crdkustomizeresource 7 | 8 | patchesStrategicMerge: 9 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. 10 | # patches here are for enabling the conversion webhook for each CRD 11 | #- patches/webhook_in_policies.yaml 12 | #+kubebuilder:scaffold:crdkustomizewebhookpatch 13 | 14 | # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. 15 | # patches here are for enabling the CA injection for each CRD 16 | #- patches/cainjection_in_policies.yaml 17 | #+kubebuilder:scaffold:crdkustomizecainjectionpatch 18 | 19 | # the following config is for teaching kustomize how to do kustomization for CRDs. 20 | configurations: 21 | - kustomizeconfig.yaml 22 | -------------------------------------------------------------------------------- /internal/sink/filesystem/types.go: -------------------------------------------------------------------------------- 1 | package filesystem 2 | 3 | import "time" 4 | 5 | type Result struct { 6 | ID string `json:"id"` 7 | AccountID string `json:"account_id"` 8 | ClusterID string `json:"cluster_id"` 9 | PolicyID string `json:"policy_id"` 10 | Status string `json:"status"` 11 | Type string `json:"type"` 12 | Provider string `json:"provider"` 13 | EntityName string `json:"entity_name"` 14 | EntityType string `json:"entity_type"` 15 | EntityNamespace string `json:"entity_namespace"` 16 | CreatedAt time.Time `json:"created_at"` 17 | Message string `json:"message"` 18 | Info map[string]interface{} `json:"info"` 19 | CategoryID string `json:"category_id"` 20 | Severity string `json:"severity"` 21 | } 22 | -------------------------------------------------------------------------------- /test/integration/data/resources/audit_test_resources.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: deployment-1 5 | namespace: default 6 | spec: 7 | replicas: 1 8 | selector: 9 | matchLabels: 10 | app: deployment-1 11 | template: 12 | metadata: 13 | labels: 14 | app: deployment-1 15 | spec: 16 | containers: 17 | - name: ubuntu 18 | image: ubuntu:latest 19 | command: ["sleep", "100d"] 20 | securityContext: 21 | privileged: true 22 | 23 | --- 24 | 25 | apiVersion: apps/v1 26 | kind: Deployment 27 | metadata: 28 | name: deployment-2 29 | namespace: default 30 | spec: 31 | replicas: 1 32 | selector: 33 | matchLabels: 34 | app: deployment-2 35 | template: 36 | metadata: 37 | labels: 38 | app: deployment-2 39 | spec: 40 | containers: 41 | - name: ubuntu 42 | image: ubuntu:latest 43 | command: ["sleep", "100d"] 44 | securityContext: 45 | privileged: true 46 | -------------------------------------------------------------------------------- /pkg/policy-core/domain/policy_config.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | type ConfigMatchApplication struct { 4 | Kind string `json:"kind"` 5 | Name string `json:"name"` 6 | Namespace string `json:"namespace"` 7 | } 8 | 9 | type ConfigMatchResource struct { 10 | Kind string `json:"kind"` 11 | Name string `json:"name"` 12 | Namespace string `json:"namespace"` 13 | } 14 | 15 | type PolicyConfigMatch struct { 16 | Namespaces []string `json:"namespaces,omitempty"` 17 | Applications []ConfigMatchApplication `json:"apps,omitempty"` 18 | Resources []ConfigMatchResource `json:"resources,omitempty"` 19 | } 20 | 21 | type PolicyConfigParameter struct { 22 | Value interface{} 23 | ConfigRef string 24 | } 25 | 26 | type PolicyConfigConfig struct { 27 | Parameters map[string]PolicyConfigParameter `json:"parameters"` 28 | } 29 | 30 | // PolicyConfig represents a policy config 31 | type PolicyConfig struct { 32 | Config map[string]PolicyConfigConfig `json:"config"` 33 | Match PolicyConfigMatch `json:"match"` 34 | } 35 | -------------------------------------------------------------------------------- /pkg/uuid-go/go.sum: -------------------------------------------------------------------------------- 1 | github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8 h1:DujepqpGd1hyOd7aW59XpK7Qymp8iy83xq74fLr21is= 2 | github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= 3 | github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= 4 | github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 5 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 6 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 7 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 8 | github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b h1:gQZ0qzfKHQIybLANtM3mBXNUtOfsCFXeTsnBqCsx1KM= 9 | github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 10 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 11 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 12 | -------------------------------------------------------------------------------- /internal/sink/elastic/elastic_test.go: -------------------------------------------------------------------------------- 1 | package elastic 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/weaveworks/policy-agent/pkg/policy-core/domain" 10 | ) 11 | 12 | const ( 13 | address = "http://localhost:9200" 14 | indexName = "test_audit_validation" 15 | ) 16 | 17 | func TestWriteElasticsearchSink(t *testing.T) { 18 | var auditEvents []domain.PolicyValidation 19 | expectedCount := 4 20 | 21 | for i := 0; i < expectedCount; i++ { 22 | auditEvents = append(auditEvents, GeneratePolicyValidationObject()) 23 | } 24 | 25 | sink, err := NewElasticSearchSink( 26 | address, "", "", indexName, "insert", 27 | ) 28 | if err != nil { 29 | t.Fatal("Error initializing elasticsearch sink", err) 30 | } 31 | ctx, cancel := context.WithCancel(context.Background()) 32 | defer cancel() 33 | go sink.Start(ctx) 34 | sink.Write(ctx, auditEvents) 35 | time.Sleep(12 * time.Second) 36 | 37 | actualCount, err := getCount(sink.elasticClient, sink.indexName) 38 | if err != nil { 39 | t.Error("Error getting index count") 40 | } 41 | assert.Equal(t, expectedCount, actualCount, "Error getting index count") 42 | } 43 | -------------------------------------------------------------------------------- /.github/workflows/release_helm_repo.yml: -------------------------------------------------------------------------------- 1 | name: Release Charts 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | with: 15 | path: master 16 | fetch-depth: 0 17 | 18 | - name: Checkout 19 | uses: actions/checkout@v2 20 | with: 21 | path: gh-pages 22 | ref: gh-pages 23 | fetch-depth: 0 24 | 25 | - name: Install Helm 26 | uses: azure/setup-helm@v1 27 | 28 | - name: Release 29 | shell: bash 30 | run: | 31 | helm package master/helm 32 | mkdir -p gh-pages/releases 33 | mv policy-agent-* gh-pages/releases/ 34 | cd gh-pages 35 | helm repo index releases --url https://weaveworks.github.io/policy-agent/releases 36 | mv releases/index.yaml index.yaml 37 | git config user.name "$GITHUB_ACTOR" 38 | git config user.email "$GITHUB_ACTOR@users.noreply.github.com" 39 | git add releases index.yaml 40 | git commit -m "update release charts" 41 | git push 42 | -------------------------------------------------------------------------------- /pkg/policy-core/README.md: -------------------------------------------------------------------------------- 1 | # Policy Core 2 | 3 | Contains policy validation tools and defines its expected domain objects and interfaces used by Weave Policy Agent. 4 | 5 | ## Policy Domain 6 | 7 | Defines the structures for `Policy` objects (Policy, Entity, PolicyConfig, PolicyValidation) also the mocking tools for unit testing. 8 | 9 | ### Generate mock data 10 | 11 | After making any changes to the policy domain or policy validator, use the following command to generate mock data 12 | 13 | ```bash 14 | make mock 15 | ``` 16 | 17 | ## Policy Validation 18 | 19 | Validates specific entity against chosen policies also can push the validation result to different sinks. To use the validator call a new instance from `OPAValidator` with the following method then call the `validate` method 20 | 21 | ```go 22 | // NewOPAValidator returns an opa validator to validate entities 23 | validator := NewOPAValidator( 24 | policiesSource domain.PoliciesSource, 25 | writeCompliance bool, 26 | validationType string, 27 | accountID string, 28 | clusterID string, 29 | mutate bool, 30 | resultsSinks ...domain.PolicyValidationSink, 31 | ) 32 | validator.validate(ctx context.Context, entity domain.Entity, trigger string) 33 | ``` 34 | -------------------------------------------------------------------------------- /.github/workflows/integration_test.yml: -------------------------------------------------------------------------------- 1 | name: integration 2 | 3 | on: 4 | push: 5 | branches: [ master, dev ] 6 | pull_request: 7 | branches: [ master, dev ] 8 | 9 | jobs: 10 | integration: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | namespace: [policy-system, test-system] 15 | steps: 16 | - name: Checkout repo 17 | uses: actions/checkout@v3 18 | - name: Install Helm 19 | uses: azure/setup-helm@v3 20 | - name: Install kubectl 21 | uses: azure/setup-kubectl@v3 22 | - name: Install kind 23 | uses: helm/kind-action@v1.3.0 24 | with: 25 | install_only: true 26 | - name: setup go 27 | uses: actions/setup-go@v4 28 | with: 29 | go-version: '1.20.x' 30 | cache: true 31 | - name: Run tests 32 | env: 33 | NAMESPACE: ${{ matrix.namespace }} 34 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 35 | run: | 36 | make build 37 | 38 | export VERSION=test 39 | docker build -t weaveworks/policy-agent:${VERSION} . 40 | 41 | cd test/integration 42 | bash deploy.sh 43 | go test -v ./... 44 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/docs.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Documentation improvement 📖 3 | about: Let us know about mistakes or missing information 4 | title: "" 5 | labels: "documentation" 6 | assignees: "" 7 | --- 8 | 9 | 13 | 14 | **What issue with the docs have you found?** 15 | 16 | - [ ] Missing information 17 | - [ ] Incorrect information 18 | - [ ] Something else 19 | 20 | **Describe what you are trying to do** 21 | 22 | 25 | 26 | **Which agent docs version are you using?** 27 | 28 | 31 | 32 | **Which pages are affected?** 33 | 34 | 38 | 39 | **Detail the issues you found and the improvements that you would like to see** 40 | 41 | 45 | 46 | **Would you be able or interested to contribute this work to the docs?** 47 | 48 | 51 | -------------------------------------------------------------------------------- /pkg/uuid-go/sql.go: -------------------------------------------------------------------------------- 1 | package uuid 2 | 3 | import ( 4 | "database/sql/driver" 5 | satori "github.com/satori/go.uuid" 6 | ) 7 | 8 | // Value implements the driver.Valuer interface. 9 | func (uuid UUID) Value() (driver.Value, error) { 10 | return satori.UUID(uuid).Value() 11 | } 12 | 13 | // Scan implements the sql.Scanner interface. 14 | func (uuid *UUID) Scan(src interface{}) error { 15 | id := satori.UUID(*uuid) 16 | idPtr := &id 17 | err := idPtr.Scan(src) 18 | if err != nil { 19 | return err 20 | } 21 | *uuid = UUID(id) 22 | return nil 23 | } 24 | 25 | // NullUUID can be used with the standard sql package to represent a 26 | // UUID value that can be NULL in the database 27 | type NullUUID struct { 28 | UUID UUID 29 | Valid bool 30 | } 31 | 32 | // Value implements the driver.Valuer interface. 33 | func (u NullUUID) Value() (driver.Value, error) { 34 | if !u.Valid { 35 | return nil, nil 36 | } 37 | // Delegate to UUID Value function 38 | return u.UUID.Value() 39 | } 40 | 41 | // Scan implements the sql.Scanner interface. 42 | func (u *NullUUID) Scan(src interface{}) error { 43 | if src == nil { 44 | u.UUID, u.Valid = Nil, false 45 | return nil 46 | } 47 | 48 | // Delegate to UUID Scan function 49 | u.Valid = true 50 | return u.UUID.Scan(src) 51 | } 52 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 9 | Closes 10 | 11 | 12 | **What changed?** 13 | 14 | 15 | 16 | **Why was this change made?** 17 | 18 | 19 | 25 | **How was this change implemented?** 26 | 27 | 28 | 33 | **How did you validate the change?** 34 | 35 | 36 | 39 | **Release notes** 40 | 41 | 42 | 43 | **Documentation Changes** 44 | -------------------------------------------------------------------------------- /pkg/logger/README.md: -------------------------------------------------------------------------------- 1 | # Logger 2 | 3 | - a wrapper over `uber/zap` logger 4 | - There are 4 levels that can be used; info, error, debug, and warning. 5 | 6 | ## Usage 7 | 8 | It's usually not ideal to pass `logger` instance across you application, this can be seen as unnecessary pollution. That's why **global** logger is provided by default with `Info` as its default log level. 9 | 10 | ```go 11 | import "github.com/weaveworks/policy-agent/pkg/logger" 12 | 13 | logger.Info("this is a log") 14 | ``` 15 | 16 | ### Change log level 17 | 18 | To change the log level of the global logger you can simply use the `Config` function 19 | 20 | ```go 21 | import "github.com/weaveworks/policy-agent/pkg/logger" 22 | 23 | logger.Config(logger.DebugLevel) 24 | logger.Debug("this is a debug log") 25 | ``` 26 | 27 | ### Create Custom logger 28 | 29 | sometimes a custom logger is ideal. For example creating a logger that have `request-id` on all the logs. you can create custom logger by using `With` or `New` function 30 | 31 | ```go 32 | import "github.com/weaveworks/policy-agent/pkg/logger" 33 | // using with will have the global logger log level 34 | customLogger := logger.With("requestId", reqID) 35 | customLogger.Info("this is a debug log") 36 | 37 | l := logger.New(logger.DebugLevel) 38 | l.Debug("this is a debug log") 39 | ``` 40 | -------------------------------------------------------------------------------- /api/v1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package v1 contains API Schema definitions for the v1 API group 18 | //+kubebuilder:object:generate=true 19 | //+groupName=pac.weave.works 20 | package v1 21 | 22 | import ( 23 | "k8s.io/apimachinery/pkg/runtime/schema" 24 | "sigs.k8s.io/controller-runtime/pkg/scheme" 25 | ) 26 | 27 | var ( 28 | // GroupVersion is group version used to register these objects 29 | GroupVersion = schema.GroupVersion{Group: "pac.weave.works", Version: "v1"} 30 | 31 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 32 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 33 | 34 | // AddToScheme adds the types in this group-version to the given scheme. 35 | AddToScheme = SchemeBuilder.AddToScheme 36 | ) 37 | -------------------------------------------------------------------------------- /api/v2beta1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package v2beta1 contains API Schema definitions for the v2beta1 API group 18 | //+kubebuilder:object:generate=true 19 | //+groupName=pac.weave.works 20 | package v2beta1 21 | 22 | import ( 23 | "k8s.io/apimachinery/pkg/runtime/schema" 24 | "sigs.k8s.io/controller-runtime/pkg/scheme" 25 | ) 26 | 27 | var ( 28 | // GroupVersion is group version used to register these objects 29 | GroupVersion = schema.GroupVersion{Group: "pac.weave.works", Version: "v2beta1"} 30 | 31 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 32 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 33 | 34 | // AddToScheme adds the types in this group-version to the given scheme. 35 | AddToScheme = SchemeBuilder.AddToScheme 36 | ) 37 | -------------------------------------------------------------------------------- /api/v2beta2/groupversion_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package v2beta2 contains API Schema definitions for the v2beta2 API group 18 | //+kubebuilder:object:generate=true 19 | //+groupName=pac.weave.works 20 | package v2beta2 21 | 22 | import ( 23 | "k8s.io/apimachinery/pkg/runtime/schema" 24 | "sigs.k8s.io/controller-runtime/pkg/scheme" 25 | ) 26 | 27 | var ( 28 | // GroupVersion is group version used to register these objects 29 | GroupVersion = schema.GroupVersion{Group: "pac.weave.works", Version: "v2beta2"} 30 | 31 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 32 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 33 | 34 | // AddToScheme adds the types in this group-version to the given scheme. 35 | AddToScheme = SchemeBuilder.AddToScheme 36 | ) 37 | -------------------------------------------------------------------------------- /api/v2beta3/groupversion_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package v2beta3 contains API Schema definitions for the v2beta3 API group 18 | // +kubebuilder:object:generate=true 19 | // +groupName=pac.weave.works 20 | package v2beta3 21 | 22 | import ( 23 | "k8s.io/apimachinery/pkg/runtime/schema" 24 | "sigs.k8s.io/controller-runtime/pkg/scheme" 25 | ) 26 | 27 | var ( 28 | // GroupVersion is group version used to register these objects 29 | GroupVersion = schema.GroupVersion{Group: "pac.weave.works", Version: "v2beta3"} 30 | 31 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 32 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 33 | 34 | // AddToScheme adds the types in this group-version to the given scheme. 35 | AddToScheme = SchemeBuilder.AddToScheme 36 | ) 37 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Weave Policy Agent is a [Mozilla Public License(MPL2)](LICENSE) project. This is a Weaveworks 4 | open source product with a community led by Weaveworks :heart: 5 | 6 | We welcome improvements to reporting issues and documentation as well as to code. 7 | 8 | ## Understanding how to run development process 9 | 10 | The [internal guide](docs/development.md) **is a work in progress** but aims to cover all aspects of how to 11 | interact with the project and how to get involved in development as smoothly as possible. 12 | 13 | ## Acceptance Policy 14 | 15 | These things will make a PR more likely to be accepted: 16 | 17 | - a well-described requirement 18 | - tests for new code 19 | - tests for old code! 20 | - new code and tests follow the conventions in old code and tests 21 | - a good commit message (see below) 22 | 23 | In general, we will merge a PR once at least one maintainer has endorsed it. For substantial changes, more people may become involved, and you might get asked to resubmit the PR or divide the changes into more than one PR. 24 | 25 | ## Format of the Commit Message 26 | 27 | Limit the subject to 50 characters and write as the continuation of the sentence "If applied, this commit will ..." 28 | Explain what and why in the body, if more than a trivial change; wrap it at 72 characters. 29 | The [following article](https://cbea.ms/git-commit/#seven-rules) has some more helpful advice on documenting your work. 30 | -------------------------------------------------------------------------------- /test/integration/data/state/policyconfigs.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: pac.weave.works/v2beta3 2 | kind: PolicyConfig 3 | metadata: 4 | name: namespace-config 5 | spec: 6 | match: 7 | namespaces: 8 | - default 9 | config: 10 | weave.policies.containers-minimum-replica-count: 11 | parameters: 12 | replica_count: 3 13 | --- 14 | 15 | apiVersion: pac.weave.works/v2beta3 16 | kind: PolicyConfig 17 | metadata: 18 | name: helm-app-config 19 | spec: 20 | match: 21 | apps: 22 | - kind: HelmRelease 23 | name: helm-app 24 | namespace: flux-system 25 | config: 26 | weave.policies.containers-minimum-replica-count: 27 | parameters: 28 | replica_count: 4 29 | 30 | --- 31 | 32 | apiVersion: pac.weave.works/v2beta3 33 | kind: PolicyConfig 34 | metadata: 35 | name: kustomize-app-config 36 | spec: 37 | match: 38 | apps: 39 | - kind: Kustomization 40 | name: kustomize-app 41 | namespace: flux-system 42 | config: 43 | weave.policies.containers-minimum-replica-count: 44 | parameters: 45 | replica_count: 5 46 | 47 | --- 48 | 49 | apiVersion: pac.weave.works/v2beta3 50 | kind: PolicyConfig 51 | metadata: 52 | name: resource-config 53 | spec: 54 | match: 55 | resources: 56 | - kind: Deployment 57 | name: test-deployment 58 | namespace: default 59 | config: 60 | weave.policies.containers-minimum-replica-count: 61 | parameters: 62 | replica_count: 6 63 | -------------------------------------------------------------------------------- /docs/examples/policy-agent-helmrelease.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: source.toolkit.fluxcd.io/v1beta2 3 | kind: HelmRepository 4 | metadata: 5 | creationTimestamp: null 6 | name: policy-agent 7 | namespace: flux-system 8 | spec: 9 | interval: 1m0s 10 | timeout: 1m0s 11 | url: https://weaveworks.github.io/policy-agent/ 12 | status: {} 13 | --- 14 | apiVersion: helm.toolkit.fluxcd.io/v2beta1 15 | kind: HelmRelease 16 | metadata: 17 | name: policy-agent 18 | namespace: flux-system 19 | spec: 20 | chart: 21 | spec: 22 | chart: policy-agent 23 | sourceRef: 24 | apiVersion: source.toolkit.fluxcd.io/v1beta2 25 | kind: HelmRepository 26 | name: policy-agent 27 | namespace: flux-system 28 | version: 2.5.0 29 | interval: 10m0s 30 | targetNamespace: policy-system 31 | install: 32 | createNamespace: true 33 | values: 34 | caCertificate: "" 35 | certificate: "" 36 | config: 37 | accountId: "" 38 | admission: 39 | enabled: false 40 | sinks: 41 | k8sEventsSink: 42 | enabled: true 43 | audit: 44 | enabled: true 45 | sinks: 46 | k8sEventsSink: 47 | enabled: true 48 | clusterId: "" 49 | excludeNamespaces: 50 | - kube-system 51 | - flux-system 52 | failurePolicy: Fail 53 | image: weaveworks/policy-agent 54 | key: "" 55 | persistence: 56 | enabled: false 57 | useCertManager: true 58 | status: {} 59 | -------------------------------------------------------------------------------- /PROJECT: -------------------------------------------------------------------------------- 1 | domain: weave.works 2 | layout: 3 | - go.kubebuilder.io/v3 4 | projectName: policy-agent 5 | repo: github.com/weaveworks/policy-agent 6 | resources: 7 | - api: 8 | crdVersion: v1 9 | namespaced: false 10 | domain: weave.works 11 | kind: Policy 12 | path: api/v1 13 | version: v1 14 | - api: 15 | crdVersion: v2beta1 16 | namespaced: false 17 | domain: weave.works 18 | kind: Policy 19 | path: api/v1 20 | version: v2beta1 21 | - api: 22 | crdVersion: v2beta1 23 | namespaced: false 24 | domain: weave.works 25 | kind: PolicySet 26 | path: api/v1 27 | version: v2beta1 28 | - api: 29 | crdVersion: v2beta2 30 | namespaced: false 31 | domain: weave.works 32 | kind: Policy 33 | path: api/v1 34 | version: v2beta2 35 | - api: 36 | crdVersion: v2beta2 37 | namespaced: false 38 | domain: weave.works 39 | kind: PolicySet 40 | path: api/v1 41 | version: v2beta2 42 | - api: 43 | crdVersion: v2beta2 44 | namespaced: false 45 | domain: weave.works 46 | kind: PolicyConfig 47 | path: api/v1 48 | version: v2beta2 49 | - api: 50 | crdVersion: v2beta3 51 | namespaced: false 52 | domain: weave.works 53 | kind: Policy 54 | path: api/v1 55 | version: v2beta3 56 | - api: 57 | crdVersion: v2beta3 58 | namespaced: false 59 | domain: weave.works 60 | kind: PolicyConfig 61 | path: api/v1 62 | version: v2beta3 63 | webhooks: 64 | # defaulting: true 65 | validation: true 66 | webhookVersion: v1 67 | version: "4" 68 | -------------------------------------------------------------------------------- /pkg/logger/go.sum: -------------------------------------------------------------------------------- 1 | github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 6 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 7 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 8 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 9 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 10 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= 11 | go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= 12 | go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 13 | go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= 14 | go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= 15 | go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= 16 | go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= 17 | go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= 18 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 19 | -------------------------------------------------------------------------------- /pkg/opa-core/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/weaveworks/policy-agent/pkg/opa-core 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/open-policy-agent/opa v0.51.0 7 | k8s.io/api v0.26.3 8 | k8s.io/apimachinery v0.26.3 9 | ) 10 | 11 | require ( 12 | github.com/OneOfOne/xxhash v1.2.8 // indirect 13 | github.com/agnivade/levenshtein v1.1.1 // indirect 14 | github.com/ghodss/yaml v1.0.0 // indirect 15 | github.com/go-logr/logr v1.2.4 // indirect 16 | github.com/gobwas/glob v0.2.3 // indirect 17 | github.com/gogo/protobuf v1.3.2 // indirect 18 | github.com/google/gofuzz v1.1.0 // indirect 19 | github.com/json-iterator/go v1.1.12 // indirect 20 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 21 | github.com/modern-go/reflect2 v1.0.2 // indirect 22 | github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect 23 | github.com/tchap/go-patricia/v2 v2.3.1 // indirect 24 | github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect 25 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect 26 | github.com/yashtewari/glob-intersection v0.1.0 // indirect 27 | golang.org/x/net v0.8.0 // indirect 28 | golang.org/x/text v0.8.0 // indirect 29 | gopkg.in/inf.v0 v0.9.1 // indirect 30 | gopkg.in/yaml.v2 v2.4.0 // indirect 31 | k8s.io/klog/v2 v2.80.1 // indirect 32 | k8s.io/utils v0.0.0-20221107191617-1a15be271d1d // indirect 33 | sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect 34 | sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect 35 | ) 36 | -------------------------------------------------------------------------------- /docs/policy.md: -------------------------------------------------------------------------------- 1 | # Policy CRD 2 | 3 | This is the main resource and it is used to define policies which will be evaluated by the policy agent. 4 | 5 | It uses [OPA Rego Language](https://www.openpolicyagent.org/docs/latest/policy-language) to evaluate the entities. 6 | 7 | ## Schema 8 | 9 | You can find the cutom resource schema [here](../config/crd/bases/pac.weave.works_policies.yaml) 10 | 11 | 12 | ## Policy Library 13 | 14 | Weaveworks offers an extensive policy library to Weave GitOps Assured and Enterprise customers. The library contains over 150 policies that cover security, best practices, and standards like SOC2, GDPR, PCI-DSS, HIPAA, Mitre Attack, and more. 15 | 16 | ## Tenant Policy 17 | 18 | It is used in [Multi Tenancy](https://docs.gitops.weave.works/docs/enterprise/multi-tenancy/) feature in [Weave GitOps Enterprise](https://docs.gitops.weave.works/docs/enterprise/intro/) 19 | 20 | Tenant policies has a special tag `tenancy`. 21 | 22 | ## Mutating Resources 23 | 24 | 25 | ![](./imgs/mutation.png) 26 | 27 | Starting from version `v2.2.0`, the policy agent will support mutating resources. 28 | 29 | To enable mutating resources policies must have field `mutate` set to `true` and the rego code should return the `violating_key` and the `recommended_value` in the violation response. The mutation webhook will use the `violating_key` and `recommended_value` to mutate the resource and return the new mutated resource. 30 | 31 | Example 32 | 33 | ``` 34 | result = { 35 | "issue_detected": true, 36 | "msg": sprintf("Replica count must be greater than or equal to '%v'; found '%v'.", [min_replica_count, replicas]), 37 | "violating_key": "spec.replicas", 38 | "recommended_value": min_replica_count 39 | } 40 | ``` 41 | -------------------------------------------------------------------------------- /pkg/policy-core/validation/mock/mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: github.com/weaveworks/policy-agent/pkg/policy-core/validation (interfaces: Validator) 3 | 4 | // Package mock is a generated GoMock package. 5 | package mock 6 | 7 | import ( 8 | context "context" 9 | reflect "reflect" 10 | 11 | domain "github.com/weaveworks/policy-agent/pkg/policy-core/domain" 12 | gomock "github.com/golang/mock/gomock" 13 | ) 14 | 15 | // MockValidator is a mock of Validator interface. 16 | type MockValidator struct { 17 | ctrl *gomock.Controller 18 | recorder *MockValidatorMockRecorder 19 | } 20 | 21 | // MockValidatorMockRecorder is the mock recorder for MockValidator. 22 | type MockValidatorMockRecorder struct { 23 | mock *MockValidator 24 | } 25 | 26 | // NewMockValidator creates a new mock instance. 27 | func NewMockValidator(ctrl *gomock.Controller) *MockValidator { 28 | mock := &MockValidator{ctrl: ctrl} 29 | mock.recorder = &MockValidatorMockRecorder{mock} 30 | return mock 31 | } 32 | 33 | // EXPECT returns an object that allows the caller to indicate expected use. 34 | func (m *MockValidator) EXPECT() *MockValidatorMockRecorder { 35 | return m.recorder 36 | } 37 | 38 | // Validate mocks base method. 39 | func (m *MockValidator) Validate(arg0 context.Context, arg1 domain.Entity, arg2 string) (*domain.PolicyValidationSummary, error) { 40 | m.ctrl.T.Helper() 41 | ret := m.ctrl.Call(m, "Validate", arg0, arg1, arg2) 42 | ret0, _ := ret[0].(*domain.PolicyValidationSummary) 43 | ret1, _ := ret[1].(error) 44 | return ret0, ret1 45 | } 46 | 47 | // Validate indicates an expected call of Validate. 48 | func (mr *MockValidatorMockRecorder) Validate(arg0, arg1, arg2 interface{}) *gomock.Call { 49 | mr.mock.ctrl.T.Helper() 50 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Validate", reflect.TypeOf((*MockValidator)(nil).Validate), arg0, arg1, arg2) 51 | } 52 | -------------------------------------------------------------------------------- /pkg/policy-core/domain/mock/policies.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: github.com/weaveworks/policy-agent/pkg/policy-core/domain (interfaces: PolicyValidationSink) 3 | 4 | // Package mock is a generated GoMock package. 5 | package mock 6 | 7 | import ( 8 | context "context" 9 | reflect "reflect" 10 | 11 | domain "github.com/weaveworks/policy-agent/pkg/policy-core/domain" 12 | gomock "github.com/golang/mock/gomock" 13 | ) 14 | 15 | // MockPolicyValidationSink is a mock of PolicyValidationSink interface. 16 | type MockPolicyValidationSink struct { 17 | ctrl *gomock.Controller 18 | recorder *MockPolicyValidationSinkMockRecorder 19 | } 20 | 21 | // MockPolicyValidationSinkMockRecorder is the mock recorder for MockPolicyValidationSink. 22 | type MockPolicyValidationSinkMockRecorder struct { 23 | mock *MockPolicyValidationSink 24 | } 25 | 26 | // NewMockPolicyValidationSink creates a new mock instance. 27 | func NewMockPolicyValidationSink(ctrl *gomock.Controller) *MockPolicyValidationSink { 28 | mock := &MockPolicyValidationSink{ctrl: ctrl} 29 | mock.recorder = &MockPolicyValidationSinkMockRecorder{mock} 30 | return mock 31 | } 32 | 33 | // EXPECT returns an object that allows the caller to indicate expected use. 34 | func (m *MockPolicyValidationSink) EXPECT() *MockPolicyValidationSinkMockRecorder { 35 | return m.recorder 36 | } 37 | 38 | // Write mocks base method. 39 | func (m *MockPolicyValidationSink) Write(arg0 context.Context, arg1 []domain.PolicyValidation) error { 40 | m.ctrl.T.Helper() 41 | ret := m.ctrl.Call(m, "Write", arg0, arg1) 42 | ret0, _ := ret[0].(error) 43 | return ret0 44 | } 45 | 46 | // Write indicates an expected call of Write. 47 | func (mr *MockPolicyValidationSinkMockRecorder) Write(arg0, arg1 interface{}) *gomock.Call { 48 | mr.mock.ctrl.T.Helper() 49 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Write", reflect.TypeOf((*MockPolicyValidationSink)(nil).Write), arg0, arg1) 50 | } 51 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Build Changelog and Github Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | jobs: 9 | releaser: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3.5.0 13 | with: 14 | fetch-depth: 0 15 | 16 | - name: setup go 17 | uses: actions/setup-go@v4 18 | with: 19 | go-version: '1.20.x' 20 | cache: true 21 | 22 | - name: Build binary 23 | run: | 24 | make build 25 | 26 | - name: Build docker image 27 | run: | 28 | make image VERSION=${{ github.ref_name }} 29 | 30 | - name: Scan The Image 31 | run: | 32 | REPO=policy-agent 33 | VERSION=${{ github.ref_name }} 34 | 35 | echo scanning ${REPO}:${VERSION} 36 | curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sudo sh -s -- -b /usr/local/bin 37 | grype "${REPO}:${VERSION}" --scope all-layers > /tmp/report.txt #--fail-on high to fail on sev high 38 | 39 | - name: Login to Docker Hub 40 | uses: docker/login-action@v1 41 | with: 42 | registry: docker.io 43 | username: ${{ secrets.DOCKER_USER }} 44 | password: ${{ secrets.DOCKER_PASSWORD }} 45 | 46 | - name: Release and push to Docker Registry 47 | run: | 48 | make push@weaveworks tag-file=new-tag version-file=new-version VERSION=${{ github.ref_name }} 49 | 50 | - name: Build Changelog 51 | id: github_release 52 | uses: mikepenz/release-changelog-builder-action@v2 53 | with: 54 | configuration: "${{ github.workspace }}/.github/workflows/changelog_configuration.json" 55 | env: 56 | GITHUB_TOKEN: ${{ secrets.BUILD_BOT_PERSONAL_ACCESS_TOKEN }} 57 | 58 | - name: Release 59 | uses: softprops/action-gh-release@v1 60 | with: 61 | body: ${{steps.github_release.outputs.changelog}} 62 | -------------------------------------------------------------------------------- /pkg/policy-core/domain/mutation_test.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "sigs.k8s.io/kustomize/kyaml/yaml" 9 | ) 10 | 11 | func TestMutation(t *testing.T) { 12 | violationKey1 := "spec.template.spec.containers[0].securityContext.privileged" 13 | violationKey2 := "metadata.labels.owner" 14 | 15 | tests := []struct { 16 | entityFile string 17 | mutatedEntityFile string 18 | Occurrences []Occurrence 19 | FixedOccurrenceCount int 20 | }{ 21 | { 22 | entityFile: "testData/entity-1.yaml", 23 | mutatedEntityFile: "testData/mutated-entity-1.yaml", 24 | Occurrences: []Occurrence{ 25 | { 26 | ViolatingKey: &violationKey1, 27 | RecommendedValue: false, 28 | }, 29 | { 30 | ViolatingKey: &violationKey2, 31 | RecommendedValue: "test", 32 | }, 33 | }, 34 | FixedOccurrenceCount: 2, 35 | }, 36 | } 37 | 38 | for _, tt := range tests { 39 | entity, err := getEntityFromFile(tt.entityFile) 40 | assert.Nil(t, err) 41 | 42 | result, err := NewMutationResult(entity) 43 | assert.Nil(t, err) 44 | 45 | occurrences, err := result.Mutate(tt.Occurrences) 46 | assert.Nil(t, err) 47 | 48 | var fixedOccurrenceCount int 49 | for i := range occurrences { 50 | if occurrences[i].Mutated { 51 | fixedOccurrenceCount++ 52 | } 53 | } 54 | 55 | mutated, err := result.NewResource() 56 | assert.Nil(t, err) 57 | 58 | expectedMutatedEntity, err := os.ReadFile(tt.mutatedEntityFile) 59 | assert.Nil(t, err) 60 | 61 | assert.YAMLEq(t, string(expectedMutatedEntity), string(mutated)) 62 | } 63 | 64 | } 65 | 66 | func getEntityFromFile(path string) (Entity, error) { 67 | raw, err := os.ReadFile(path) 68 | if err != nil { 69 | return Entity{}, err 70 | } 71 | 72 | var m map[string]interface{} 73 | err = yaml.Unmarshal(raw, &m) 74 | if err != nil { 75 | return Entity{}, err 76 | } 77 | 78 | return NewEntityFromSpec(m), nil 79 | } 80 | -------------------------------------------------------------------------------- /helm/README.md: -------------------------------------------------------------------------------- 1 | # Policy Agent Helm Release 2 | 3 | ## Installation 4 | ```bash 5 | helm repo add policy-agent https://weaveworks.github.io/policy-agent/ 6 | ``` 7 | 8 | ## Configuration 9 | 10 | List of available variables: 11 | 12 | 13 | | Key | Type | Default | Description | 14 | |-----------------------|---------------|---------------------------|-----------------------------------------------------------------------------------------------------------| 15 | | `image` | `string` | `weaveworks/policy-agent` | docker image. | 16 | | `useCertManager` | `boolean` | `true` | use [cert-manager](https://cert-manager.io/) to manage agent's TLS certificate. | 17 | | `certificate` | `string` | | TLS certificate. Not needed if `useCertManager` is set to `true`. | 18 | | `key` | `string` | | TLS key. Not needed if `useCertManager` is set to `true`. | 19 | | `caCertificate` | `string` | | TLS CA Certificate . Not needed if `useCertManager` is set to `true`. | 20 | | `failurePolicy` | `string` | `Fail` | Whether to fail or ignore when the admission controller request fails. Available values `Fail`, `Ignore` | 21 | | `excludeNamespaces` | `[]string` | | List of namespaces to ignore by the admission controller. | 22 | | `config` | `object` | | Agent configuration. See agent's configuration [guide](../docs/README.md#configuration). | 23 | -------------------------------------------------------------------------------- /internal/entities/mock/mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: github.com/weaveworks/policy-agent/pkg/policy-core/domain (interfaces: EntitiesSource) 3 | 4 | // Package mock is a generated GoMock package. 5 | package mock 6 | 7 | import ( 8 | context "context" 9 | reflect "reflect" 10 | 11 | gomock "github.com/golang/mock/gomock" 12 | domain "github.com/weaveworks/policy-agent/pkg/policy-core/domain" 13 | ) 14 | 15 | // MockEntitiesSource is a mock of EntitiesSource interface. 16 | type MockEntitiesSource struct { 17 | ctrl *gomock.Controller 18 | recorder *MockEntitiesSourceMockRecorder 19 | } 20 | 21 | // MockEntitiesSourceMockRecorder is the mock recorder for MockEntitiesSource. 22 | type MockEntitiesSourceMockRecorder struct { 23 | mock *MockEntitiesSource 24 | } 25 | 26 | // NewMockEntitiesSource creates a new mock instance. 27 | func NewMockEntitiesSource(ctrl *gomock.Controller) *MockEntitiesSource { 28 | mock := &MockEntitiesSource{ctrl: ctrl} 29 | mock.recorder = &MockEntitiesSourceMockRecorder{mock} 30 | return mock 31 | } 32 | 33 | // EXPECT returns an object that allows the caller to indicate expected use. 34 | func (m *MockEntitiesSource) EXPECT() *MockEntitiesSourceMockRecorder { 35 | return m.recorder 36 | } 37 | 38 | // Kind mocks base method. 39 | func (m *MockEntitiesSource) Kind() string { 40 | m.ctrl.T.Helper() 41 | ret := m.ctrl.Call(m, "Kind") 42 | ret0, _ := ret[0].(string) 43 | return ret0 44 | } 45 | 46 | // Kind indicates an expected call of Kind. 47 | func (mr *MockEntitiesSourceMockRecorder) Kind() *gomock.Call { 48 | mr.mock.ctrl.T.Helper() 49 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Kind", reflect.TypeOf((*MockEntitiesSource)(nil).Kind)) 50 | } 51 | 52 | // List mocks base method. 53 | func (m *MockEntitiesSource) List(arg0 context.Context, arg1 *domain.ListOptions) (*domain.EntitiesList, error) { 54 | m.ctrl.T.Helper() 55 | ret := m.ctrl.Call(m, "List", arg0, arg1) 56 | ret0, _ := ret[0].(*domain.EntitiesList) 57 | ret1, _ := ret[1].(error) 58 | return ret0, ret1 59 | } 60 | 61 | // List indicates an expected call of List. 62 | func (mr *MockEntitiesSourceMockRecorder) List(arg0, arg1 interface{}) *gomock.Call { 63 | mr.mock.ctrl.T.Helper() 64 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockEntitiesSource)(nil).List), arg0, arg1) 65 | } 66 | -------------------------------------------------------------------------------- /docs/development.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | ## Running locally 4 | 5 | While the agent can be run like any go binary this won't be ideal in certain situations. Mainly to try the audit functionality the agent instance needs to be reachable by the cluster so it can send the admission request to the webhook server. This means that the agent needs to be run as a workload inside a cluster with a service so it can be reached from the API server. It might also be possible that you want to test the way user handle permissions and you would need to define a `ClusterRole` for the agent. 6 | 7 | The easiest way to achieve this is by using the provided helm chart. 8 | 9 | You will need [cert-manager](https://cert-manager.io/docs/installation/) on your cluster before using the agent locally. 10 | 11 | The first step would be to build the agent binary this is by done by running the following in the repo root: 12 | 13 | ```bash 14 | make build 15 | ``` 16 | 17 | This will generate a binary at `bin/agent` but we won't be using directly. We will build a local image: 18 | 19 | ```bash 20 | docker build . -t agent-test:{chart-version} 21 | ``` 22 | 23 | Notice that the tag version needs to be the same as the helm chart version. You can give any name to the image. The version could be found in `./helm/Chart.yaml` 24 | 25 | Now you have an image that can be added to your local cluster. This varies depending on which provider you are using. 26 | 27 | If you are using `kind`: 28 | 29 | ```bash 30 | kind load docker-image agent-test:1.0.0 --name {clustername} 31 | ``` 32 | 33 | With `minikube` you will need to run this in a shell before building an image and that will build the image inside the `minikube` cluster: 34 | 35 | 36 | ```bash 37 | eval $(minikube docker-env) 38 | ``` 39 | 40 | Next you need to create your values file, you can configure the agent inside as necessary overriding the default values. The following needs to be configured: 41 | 42 | ```yaml 43 | image: agent-test 44 | config: 45 | accountId: "agent-dev" 46 | clusterId: "wge-dev" 47 | ``` 48 | 49 | Create `policy-system` namespace to install the chart in 50 | 51 | ```bash 52 | kubectl create ns policy-system 53 | ``` 54 | 55 | Then you can finally run the agent: 56 | 57 | ```bash 58 | helm install agent -f {values-file-path} helm -n policy-system 59 | ``` 60 | 61 | When agent pod is ready, it should be good to go. 62 | -------------------------------------------------------------------------------- /internal/sink/elastic/utils.go: -------------------------------------------------------------------------------- 1 | package elastic 2 | 3 | import ( 4 | "encoding/json" 5 | "strings" 6 | "time" 7 | 8 | "github.com/elastic/go-elasticsearch/v7" 9 | "github.com/pkg/errors" 10 | "github.com/weaveworks/policy-agent/pkg/policy-core/domain" 11 | "github.com/weaveworks/policy-agent/pkg/uuid-go" 12 | ) 13 | 14 | func getCount(client *elasticsearch.Client, index string) (int, error) { 15 | res, err := client.Count(client.Count.WithIndex(index)) 16 | if err != nil || res.StatusCode != 200 { 17 | return 0, errors.WithMessagef(err, "Cannot get index count") 18 | } 19 | 20 | var response struct { 21 | Count int `json:"count"` 22 | } 23 | err = json.NewDecoder(res.Body).Decode(&response) 24 | if err != nil { 25 | return 0, errors.WithMessagef(err, "failed to decode index count response") 26 | } 27 | return response.Count, nil 28 | } 29 | 30 | // RandomText generates random name 31 | func RandomText() string { 32 | return strings.ReplaceAll(uuid.NewV4().String(), "-", "") 33 | } 34 | 35 | // RandomUUID generates random uuid 36 | func RandomUUID() string { 37 | return uuid.NewV4().String() 38 | } 39 | 40 | func GeneratePolicyValidationObject() domain.PolicyValidation { 41 | violatingkey := RandomText() 42 | return domain.PolicyValidation{ 43 | ID: RandomUUID(), 44 | AccountID: RandomUUID(), 45 | ClusterID: RandomUUID(), 46 | Policy: domain.Policy{ 47 | Name: RandomText(), 48 | ID: RandomUUID(), 49 | Description: RandomText(), 50 | HowToSolve: RandomText(), 51 | Category: RandomText(), 52 | Tags: []string{RandomText()}, 53 | Severity: RandomText(), 54 | Standards: []domain.PolicyStandard{ 55 | { 56 | ID: RandomText(), 57 | Controls: []string{RandomText()}, 58 | }, 59 | }, 60 | }, 61 | Entity: domain.Entity{ 62 | ID: RandomUUID(), 63 | Name: RandomText(), 64 | Kind: RandomText(), 65 | Namespace: RandomText(), 66 | APIVersion: RandomText(), 67 | Manifest: map[string]interface{}{ 68 | "test": RandomText(), 69 | }, 70 | }, 71 | Occurrences: []domain.Occurrence{ 72 | { 73 | Message: RandomText(), 74 | ViolatingKey: &violatingkey, 75 | RecommendedValue: false, 76 | }, 77 | }, 78 | Status: RandomText(), 79 | Trigger: RandomText(), 80 | CreatedAt: time.Now().Round(0), 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /pkg/log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "github.com/go-logr/logr" 5 | "github.com/weaveworks/policy-agent/pkg/logger" 6 | "go.uber.org/zap" 7 | "go.uber.org/zap/zapcore" 8 | ) 9 | 10 | // ControllerLogSink provides logging for the controller manager, implements github.com/go-logr/logr.LogSink 11 | type ControllerLogSink struct { 12 | accountID string 13 | clusterID string 14 | baseLog *zap.Logger 15 | sugarLog logger.Logger 16 | } 17 | 18 | // NewControllerLog returns a logger for controller manager 19 | func NewControllerLog(accountID, clusterID string) logr.Logger { 20 | sink := ControllerLogSink{ 21 | accountID: accountID, 22 | clusterID: clusterID, 23 | } 24 | return logr.New(&sink) 25 | } 26 | 27 | // Init initializes the logger with the needed configuration 28 | func (c *ControllerLogSink) Init(info logr.RuntimeInfo) { 29 | log := logger.NewZapLogger(logger.InfoLevel) 30 | sugarLog := log.WithOptions(zap.AddCallerSkip(info.CallDepth + 1)).Sugar() 31 | sugarLog = sugarLog.With("accountID", c.accountID, "clusterID", c.clusterID) 32 | c.sugarLog = sugarLog 33 | c.baseLog = log 34 | } 35 | 36 | // Enabled check if a log level is enabled 37 | func (c *ControllerLogSink) Enabled(level int) bool { 38 | return c.baseLog.Core().Enabled(zapcore.Level(level)) 39 | } 40 | 41 | // Info logs a non-error message with the given key/value pairs as context 42 | func (c *ControllerLogSink) Info(_ int, msg string, keysAndValues ...interface{}) { 43 | c.sugarLog.Infow(msg, keysAndValues...) 44 | } 45 | 46 | // Error logs an error, with the given message and key/value pairs as context 47 | func (c *ControllerLogSink) Error(err error, msg string, keysAndValues ...interface{}) { 48 | c.sugarLog.Errorw(msg, "error", err, keysAndValues) 49 | } 50 | 51 | // WithValues returns a new LogSink with additional key/value pairs 52 | func (c *ControllerLogSink) WithValues(keysAndValues ...interface{}) logr.LogSink { 53 | return &ControllerLogSink{ 54 | accountID: c.accountID, 55 | clusterID: c.clusterID, 56 | baseLog: c.baseLog, 57 | sugarLog: c.sugarLog.With(keysAndValues...), 58 | } 59 | } 60 | 61 | // WithName returns a new LogSink with the specified name appended 62 | func (c *ControllerLogSink) WithName(name string) logr.LogSink { 63 | return &ControllerLogSink{ 64 | accountID: c.accountID, 65 | clusterID: c.clusterID, 66 | baseLog: c.baseLog, 67 | sugarLog: c.sugarLog.Named(name), 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /test/integration/data/resources/admission_test_resources.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: orphan-deployment 5 | namespace: default 6 | spec: 7 | replicas: 1 8 | selector: 9 | matchLabels: 10 | app: orphan-deployment 11 | template: 12 | metadata: 13 | labels: 14 | app: orphan-deployment 15 | spec: 16 | containers: 17 | - name: ubuntu 18 | image: ubuntu:latest 19 | command: ["sleep", "100d"] 20 | securityContext: 21 | privileged: true 22 | 23 | --- 24 | 25 | apiVersion: apps/v1 26 | kind: Deployment 27 | metadata: 28 | name: test-deployment 29 | namespace: default 30 | spec: 31 | replicas: 1 32 | selector: 33 | matchLabels: 34 | app: test-deployment 35 | template: 36 | metadata: 37 | labels: 38 | app: test-deployment 39 | spec: 40 | containers: 41 | - name: ubuntu 42 | image: ubuntu:latest 43 | command: ["sleep", "100d"] 44 | securityContext: 45 | privileged: true 46 | 47 | --- 48 | 49 | apiVersion: apps/v1 50 | kind: Deployment 51 | metadata: 52 | name: helm-app-deployment 53 | namespace: default 54 | labels: 55 | helm.toolkit.fluxcd.io/name: helm-app 56 | helm.toolkit.fluxcd.io/namespace: flux-system 57 | spec: 58 | replicas: 1 59 | selector: 60 | matchLabels: 61 | app: helm-app-deployment 62 | template: 63 | metadata: 64 | labels: 65 | app: helm-app-deployment 66 | spec: 67 | containers: 68 | - name: ubuntu 69 | image: ubuntu:latest 70 | command: ["sleep", "100d"] 71 | securityContext: 72 | privileged: true 73 | 74 | --- 75 | 76 | apiVersion: apps/v1 77 | kind: Deployment 78 | metadata: 79 | name: kustomize-app-deployment 80 | namespace: default 81 | labels: 82 | kustomize.toolkit.fluxcd.io/name: kustomize-app 83 | kustomize.toolkit.fluxcd.io/namespace: flux-system 84 | spec: 85 | replicas: 1 86 | selector: 87 | matchLabels: 88 | app: kustomize-app-deployment 89 | template: 90 | metadata: 91 | labels: 92 | app: kustomize-app-deployment 93 | spec: 94 | containers: 95 | - name: ubuntu 96 | image: ubuntu:latest 97 | command: ["sleep", "100d"] 98 | securityContext: 99 | privileged: true 100 | 101 | -------------------------------------------------------------------------------- /policies/ControllerMinimumReplicaCount.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: pac.weave.works/v2beta3 2 | kind: Policy 3 | metadata: 4 | name: weave.policies.containers-minimum-replica-count 5 | spec: 6 | id: weave.policies.containers-minimum-replica-count 7 | name: Containers Minimum Replica Count 8 | enforce: true 9 | description: "Use this Policy to to check the replica count of your workloads. The value set in the Policy is greater than or equal to the amount desired, so if the replica count is lower than what is specified, the Policy will be in violation. \n" 10 | how_to_solve: | 11 | The replica count should be a value equal or greater than what is set in the Policy. 12 | ``` 13 | spec: 14 | replicas: 15 | ``` 16 | https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#scaling-a-deployment 17 | category: weave.categories.reliability 18 | severity: medium 19 | targets: 20 | kinds: 21 | - Deployment 22 | - StatefulSet 23 | - ReplicaSet 24 | - ReplicationController 25 | - HorizontalPodAutoscaler 26 | standards: 27 | - id: weave.standards.soc2-type-i 28 | controls: 29 | - weave.controls.soc2-type-i.2.1.1 30 | tags: [soc2-type1] 31 | parameters: 32 | - name: replica_count 33 | type: integer 34 | required: true 35 | value: 2 36 | code: | 37 | package weave.advisor.pods.replica_count 38 | 39 | import future.keywords.in 40 | 41 | min_replica_count := input.parameters.replica_count 42 | 43 | controller_input := input.review.object 44 | 45 | violation[result] { 46 | not replicas >= min_replica_count 47 | result = { 48 | "issue detected": true, 49 | "msg": sprintf("Replica count must be greater than or equal to '%v'; found '%v'.", [min_replica_count, replicas]), 50 | "violating_key": violating_key, 51 | "recommended_value": min_replica_count, 52 | } 53 | } 54 | 55 | replicas := controller_input.spec.replicas { 56 | controller_input.kind in {"Deployment", "StatefulSet", "ReplicaSet", "ReplicationController"} 57 | } else := controller_input.spec.minReplicas { 58 | controller_input.kind == "HorizontalPodAutoscaler" 59 | } 60 | 61 | violating_key := "spec.replicas" { 62 | controller_input.kind in {"Deployment", "StatefulSet", "ReplicaSet", "ReplicationController"} 63 | } else := "spec.minReplicas" { 64 | controller_input.kind == "HorizontalPodAutoscaler" 65 | } 66 | 67 | -------------------------------------------------------------------------------- /pkg/policy-core/domain/mock/sink.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: github.com/weaveworks/policy-agent/pkg/policy-core/domain (interfaces: PoliciesSource) 3 | 4 | // Package mock is a generated GoMock package. 5 | package mock 6 | 7 | import ( 8 | context "context" 9 | reflect "reflect" 10 | 11 | domain "github.com/weaveworks/policy-agent/pkg/policy-core/domain" 12 | gomock "github.com/golang/mock/gomock" 13 | ) 14 | 15 | // MockPoliciesSource is a mock of PoliciesSource interface. 16 | type MockPoliciesSource struct { 17 | ctrl *gomock.Controller 18 | recorder *MockPoliciesSourceMockRecorder 19 | } 20 | 21 | // MockPoliciesSourceMockRecorder is the mock recorder for MockPoliciesSource. 22 | type MockPoliciesSourceMockRecorder struct { 23 | mock *MockPoliciesSource 24 | } 25 | 26 | // NewMockPoliciesSource creates a new mock instance. 27 | func NewMockPoliciesSource(ctrl *gomock.Controller) *MockPoliciesSource { 28 | mock := &MockPoliciesSource{ctrl: ctrl} 29 | mock.recorder = &MockPoliciesSourceMockRecorder{mock} 30 | return mock 31 | } 32 | 33 | // EXPECT returns an object that allows the caller to indicate expected use. 34 | func (m *MockPoliciesSource) EXPECT() *MockPoliciesSourceMockRecorder { 35 | return m.recorder 36 | } 37 | 38 | // GetAll mocks base method. 39 | func (m *MockPoliciesSource) GetAll(arg0 context.Context) ([]domain.Policy, error) { 40 | m.ctrl.T.Helper() 41 | ret := m.ctrl.Call(m, "GetAll", arg0) 42 | ret0, _ := ret[0].([]domain.Policy) 43 | ret1, _ := ret[1].(error) 44 | return ret0, ret1 45 | } 46 | 47 | // GetAll indicates an expected call of GetAll. 48 | func (mr *MockPoliciesSourceMockRecorder) GetAll(arg0 interface{}) *gomock.Call { 49 | mr.mock.ctrl.T.Helper() 50 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAll", reflect.TypeOf((*MockPoliciesSource)(nil).GetAll), arg0) 51 | } 52 | 53 | // GetPolicyConfig mocks base method. 54 | func (m *MockPoliciesSource) GetPolicyConfig(arg0 context.Context, arg1 domain.Entity) (*domain.PolicyConfig, error) { 55 | m.ctrl.T.Helper() 56 | ret := m.ctrl.Call(m, "GetPolicyConfig", arg0, arg1) 57 | ret0, _ := ret[0].(*domain.PolicyConfig) 58 | ret1, _ := ret[1].(error) 59 | return ret0, ret1 60 | } 61 | 62 | // GetPolicyConfig indicates an expected call of GetPolicyConfig. 63 | func (mr *MockPoliciesSourceMockRecorder) GetPolicyConfig(arg0, arg1 interface{}) *gomock.Call { 64 | mr.mock.ctrl.T.Helper() 65 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPolicyConfig", reflect.TypeOf((*MockPoliciesSource)(nil).GetPolicyConfig), arg0, arg1) 66 | } 67 | -------------------------------------------------------------------------------- /pkg/policy-core/domain/policy.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | v1 "k8s.io/api/core/v1" 5 | ) 6 | 7 | // PolicyTargets is used to match entities with the required fields specified by the policy 8 | type PolicyTargets struct { 9 | Kinds []string `json:"kinds"` 10 | Labels []map[string]string `json:"labels"` 11 | Namespaces []string `json:"namespaces"` 12 | } 13 | 14 | // PolicyParameters defines a needed input in a policy 15 | type PolicyParameters struct { 16 | Name string `json:"name"` 17 | Type string `json:"type"` 18 | Value interface{} `json:"value"` 19 | Required bool `json:"required"` 20 | ConfigRef string `json:"config_ref,omitempty"` 21 | } 22 | 23 | type PolicyStandard struct { 24 | ID string `json:"id"` 25 | Controls []string `json:"controls"` 26 | } 27 | 28 | // PolicyExclusions are the structure which resources should not be evaluated against the policy 29 | type PolicyExclusions struct { 30 | Namespaces []string `json:"namespaces"` 31 | Resources []string `json:"resources"` 32 | Labels map[string]string `json:"labels"` 33 | } 34 | 35 | // Policy represents a policy 36 | type Policy struct { 37 | Name string `json:"name"` 38 | ID string `json:"id"` 39 | Code string `json:"code"` 40 | Enforce bool `json:"enforce"` 41 | Parameters []PolicyParameters `json:"parameters"` 42 | Targets PolicyTargets `json:"targets"` 43 | Description string `json:"description"` 44 | HowToSolve string `json:"how_to_solve"` 45 | Category string `json:"category"` 46 | Tags []string `json:"tags"` 47 | Severity string `json:"severity"` 48 | Standards []PolicyStandard `json:"standards"` 49 | Reference interface{} `json:"-"` 50 | GitCommit string `json:"git_commit,omitempty"` 51 | Mutate bool `json:"mutate"` 52 | Exclude PolicyExclusions `json:"exclude"` 53 | } 54 | 55 | // ObjectRef returns the kubernetes object reference of the policy 56 | func (p *Policy) ObjectRef() *v1.ObjectReference { 57 | if obj, ok := p.Reference.(v1.ObjectReference); ok { 58 | return &obj 59 | } 60 | return nil 61 | } 62 | 63 | // GetParametersMap returns policy parameters as a map 64 | func (p *Policy) GetParametersMap() map[string]interface{} { 65 | res := make(map[string]interface{}) 66 | for _, param := range p.Parameters { 67 | res[param.Name] = param.Value 68 | } 69 | return res 70 | } 71 | -------------------------------------------------------------------------------- /policies/ControllerContainerBlockSysctls.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: pac.weave.works/v2beta3 2 | kind: Policy 3 | metadata: 4 | name: weave.policies.container-block-sysctl 5 | spec: 6 | id: weave.policies.container-block-sysctl 7 | name: Container Block Sysctls 8 | enforce: true 9 | description: "Setting sysctls can allow containers unauthorized escalated privileges to a Kubernetes node. \n" 10 | how_to_solve: "You should not set `securityContext.sysctls` \n```\n...\n spec:\n securityContext:\n sysctls\n```\nhttps://kubernetes.io/docs/tasks/configure-pod-container/security-context/\n" 11 | category: weave.categories.pod-security 12 | severity: high 13 | targets: {kinds: [Deployment, Job, ReplicationController, ReplicaSet, DaemonSet, StatefulSet, CronJob]} 14 | standards: 15 | - id: weave.standards.pci-dss 16 | controls: 17 | - weave.controls.pci-dss.2.2.4 18 | - weave.controls.pci-dss.2.2.5 19 | - id: weave.standards.cis-benchmark 20 | controls: 21 | - weave.controls.cis-benchmark.5.2.6 22 | - id: weave.standards.mitre-attack 23 | controls: 24 | - weave.controls.mitre-attack.4.1 25 | - id: weave.standards.nist-800-190 26 | controls: 27 | - weave.controls.nist-800-190.3.3.1 28 | - id: weave.standards.gdpr 29 | controls: 30 | - weave.controls.gdpr.24 31 | - weave.controls.gdpr.25 32 | - weave.controls.gdpr.32 33 | tags: [pci-dss, cis-benchmark, mitre-attack, nist800-190, gdpr, default] 34 | exclude: 35 | namespaces: 36 | - kube-system 37 | code: | 38 | package weave.advisor.podSecurity.block_sysctls 39 | 40 | import future.keywords.in 41 | 42 | 43 | violation[result] { 44 | controller_spec.securityContext.sysctls 45 | result = { 46 | "issue detected": true, 47 | "msg": "Adding sysctls could lead to unauthorized escalated privileges to the underlying node", 48 | "violating_key": "spec.template.spec.securityContext.sysctls" 49 | } 50 | } 51 | 52 | ###### Functions 53 | isArrayContains(array, str) { 54 | array[_] = str 55 | } 56 | 57 | # Initial Setup 58 | controller_input = input.review.object 59 | 60 | controller_spec = controller_input.spec.template.spec { 61 | isArrayContains({"StatefulSet", "DaemonSet", "Deployment", "Job", "ReplicaSet"}, controller_input.kind) 62 | } else = controller_input.spec { 63 | controller_input.kind == "Pod" 64 | } else = controller_input.spec.jobTemplate.spec.template.spec { 65 | controller_input.kind == "CronJob" 66 | } 67 | -------------------------------------------------------------------------------- /internal/mutation/mutation.go: -------------------------------------------------------------------------------- 1 | package mutation 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | 9 | "github.com/weaveworks/policy-agent/pkg/logger" 10 | "github.com/weaveworks/policy-agent/pkg/policy-core/domain" 11 | "github.com/weaveworks/policy-agent/pkg/policy-core/validation" 12 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 13 | ctrl "sigs.k8s.io/controller-runtime" 14 | ctrlAdmission "sigs.k8s.io/controller-runtime/pkg/webhook/admission" 15 | ) 16 | 17 | type MutationHandler struct { 18 | validator validation.Validator 19 | } 20 | 21 | func NewMutationHandler(validator validation.Validator) *MutationHandler { 22 | return &MutationHandler{ 23 | validator: validator, 24 | } 25 | } 26 | 27 | func (m *MutationHandler) handleErrors(err error, errMsg string) ctrlAdmission.Response { 28 | logger.Errorw("validating mutation request error", "error", err, "error-message", errMsg) 29 | errRsp := ctrlAdmission.ValidationResponse(false, errMsg) 30 | errRsp.Result.Code = http.StatusInternalServerError 31 | return errRsp 32 | } 33 | 34 | func (m *MutationHandler) Handle(ctx context.Context, req ctrlAdmission.Request) ctrlAdmission.Response { 35 | if req.Namespace == metav1.NamespacePublic || req.Namespace == metav1.NamespaceSystem { 36 | return ctrlAdmission.ValidationResponse(true, "exclude default system namespaces") 37 | } 38 | 39 | var entitySpec map[string]interface{} 40 | err := json.Unmarshal(req.Object.Raw, &entitySpec) 41 | if err != nil { 42 | return m.handleErrors(err, fmt.Sprintf("failed to unmarshal entity %s/%s spec into a map", req.Namespace, req.Name)) 43 | } 44 | 45 | entity := domain.NewEntityFromSpec(entitySpec) 46 | result, err := m.validator.Validate(ctx, entity, string(req.AdmissionRequest.Operation)) 47 | if err != nil { 48 | return m.handleErrors(err, fmt.Sprintf("failed to validate entity %s/%s", req.Namespace, req.Name)) 49 | } 50 | 51 | if result.Mutation != nil { 52 | logger.Infow("mutating resource", "name", req.Name, "namespace", req.Namespace) 53 | mutated, err := result.Mutation.NewResource() 54 | if err != nil { 55 | return m.handleErrors(err, fmt.Sprintf("failed to mutate entity %s/%s ", req.Namespace, req.Name)) 56 | } 57 | return ctrlAdmission.PatchResponseFromRaw(result.Mutation.OldResource(), mutated) 58 | } 59 | 60 | return ctrlAdmission.Allowed("") 61 | } 62 | 63 | // Run starts the mutation webhook server 64 | func (m *MutationHandler) Run(mgr ctrl.Manager) error { 65 | webhook := ctrlAdmission.Webhook{Handler: m} 66 | mgr.GetWebhookServer().Register("/mutation", &webhook) 67 | return nil 68 | } 69 | -------------------------------------------------------------------------------- /docs/code_structure.md: -------------------------------------------------------------------------------- 1 | # Code structure 2 | 3 | ## api 4 | 5 | This folder contains the defintion of new CRDs needed by the agent, mainly the policies CRD. It defines all the required objects to make the Kubernetes API client aware of the new policy object and allows operations that are possible to the other built-in Kubernets objects. 6 | 7 | ## config 8 | 9 | This holds the generated manifests from the structs defined in the `api` folder. The files are generated by the tool `controller-gen` which comes packaged with `kubebuilder`. Currently the only files used are the CRD manifests. 10 | 11 | ## configuration 12 | 13 | This module is responsible for getting the agent configuration and making sure that it is a valid one. It uses `viper` and reads from a configuration file. 14 | 15 | ## internal 16 | 17 | This contains the crux of the agent's logic. All the functionalities of the agent live there(audit, admission,..) as well as the various sinks code. 18 | 19 | ### admission 20 | 21 | This contains the admission module. It uses the `controller-runtime` Kubernetes package to register a callback that will be called when the agent recieves an admission request. 22 | 23 | ### auditor 24 | 25 | Performs the audit functionality. It triggers per the specified interval and then lists all the resources that the agent has resources on and performs the validation. 26 | 27 | ### clients 28 | 29 | Contains client code to contact any external services. 30 | 31 | #### kube 32 | 33 | Contains code that make use of the Kubernetes client to contact the API server. 34 | 35 | 36 | ### entities 37 | 38 | This module mainly is used to return the entities that will be be part of the audit. It does that by checking which resources the agent has permissions on and then checks the api resources existing on the cluster and return those who were matched if the agent has a permission to list them. 39 | 40 | ### policies 41 | 42 | Contains the implementations of the `PoliciesSource` interface that is responsible for returning the policies to the validation operation. It contains the `crd` implementation which fetches them from the Kubernets API. 43 | 44 | ### sink 45 | 46 | Contains the implementations of `PolicyValidationSink`, responsible of writing the validation results to a specified source: 47 | 48 | - elastic: writes to an elastic search instance 49 | - filesystem: writes to a configured file path 50 | - flux-notification: integrates with flux and writes to its notification controller 51 | - k8s-event: write to a Kubernetes event on the cluster 52 | 53 | ### terraform 54 | 55 | Contains the terraform server that is used by the `terraform controller` to validate terraform plans against terraform policies 56 | -------------------------------------------------------------------------------- /pkg/policy-core/validation/common.go: -------------------------------------------------------------------------------- 1 | package validation 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/weaveworks/policy-agent/pkg/policy-core/domain" 8 | ) 9 | 10 | func matchEntity(entity domain.Entity, policy domain.Policy) bool { 11 | var matchKind bool 12 | var matchNamespace bool 13 | var matchLabel bool 14 | 15 | if len(policy.Targets.Kinds) == 0 { 16 | matchKind = true 17 | } else { 18 | resourceKind := entity.Kind 19 | for _, kind := range policy.Targets.Kinds { 20 | if resourceKind == kind { 21 | matchKind = true 22 | break 23 | } 24 | } 25 | } 26 | 27 | if len(policy.Targets.Namespaces) == 0 { 28 | matchNamespace = true 29 | } else { 30 | resourceNamespace := entity.Namespace 31 | for _, namespace := range policy.Targets.Namespaces { 32 | if resourceNamespace == namespace { 33 | matchNamespace = true 34 | break 35 | } 36 | } 37 | } 38 | 39 | if len(policy.Targets.Labels) == 0 { 40 | matchLabel = true 41 | } else { 42 | outer: 43 | for _, obj := range policy.Targets.Labels { 44 | for key, val := range obj { 45 | entityVal, ok := entity.Labels[key] 46 | if ok { 47 | if val != "*" && val != entityVal { 48 | continue 49 | } 50 | matchLabel = true 51 | break outer 52 | } 53 | } 54 | } 55 | } 56 | 57 | return matchKind && matchNamespace && matchLabel 58 | } 59 | 60 | // isExcluded evaluates the policy exclusion against the requested entity 61 | func isExcluded(entity domain.Entity, policy domain.Policy) bool { 62 | resourceNamespace := entity.Namespace 63 | for _, namespace := range policy.Exclude.Namespaces { 64 | if resourceNamespace == namespace { 65 | return true 66 | } 67 | } 68 | 69 | resourceName := fmt.Sprintf("%s/%s", entity.Namespace, entity.Name) 70 | for _, resource := range policy.Exclude.Resources { 71 | if resourceName == resource { 72 | return true 73 | } 74 | } 75 | 76 | for key, val := range policy.Exclude.Labels { 77 | entityVal, ok := entity.Labels[key] 78 | if ok { 79 | if val != "*" && val != entityVal { 80 | continue 81 | } 82 | return true 83 | } 84 | } 85 | 86 | return false 87 | } 88 | 89 | func writeToSinks( 90 | ctx context.Context, 91 | resultsSinks []domain.PolicyValidationSink, 92 | PolicyValidationSummary domain.PolicyValidationSummary, 93 | writeCompliance bool) { 94 | for _, resutsSink := range resultsSinks { 95 | if len(PolicyValidationSummary.Violations) > 0 { 96 | resutsSink.Write(ctx, PolicyValidationSummary.Violations) 97 | } 98 | if writeCompliance && len(PolicyValidationSummary.Compliances) > 0 { 99 | resutsSink.Write(ctx, PolicyValidationSummary.Compliances) 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /pkg/policy-core/domain/entity.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "context" 5 | 6 | v1 "k8s.io/api/core/v1" 7 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 8 | "k8s.io/apimachinery/pkg/types" 9 | ) 10 | 11 | // Entity represents a kubernetes resource 12 | type Entity struct { 13 | ID string `json:"id"` 14 | Name string `json:"name"` 15 | APIVersion string `json:"apiVersion"` 16 | Kind string `json:"kind"` 17 | Namespace string `json:"namespace"` 18 | Manifest map[string]interface{} `json:"manifest"` 19 | ResourceVersion string `json:"resource_version"` 20 | Labels map[string]string `json:"-"` 21 | GitCommit string `json:"-"` 22 | HasParent bool `json:"has_parent"` 23 | } 24 | 25 | // ObjectRef returns the kubernetes object reference of the entity 26 | func (e *Entity) ObjectRef() *v1.ObjectReference { 27 | return &v1.ObjectReference{ 28 | APIVersion: e.APIVersion, 29 | Kind: e.Kind, 30 | UID: types.UID(e.ID), 31 | Name: e.Name, 32 | Namespace: e.Namespace, 33 | ResourceVersion: e.ResourceVersion, 34 | } 35 | } 36 | 37 | // NewEntityFromSpec takes map representing a Kubernetes entity and parses it into Entity struct 38 | func NewEntityFromSpec(entitySpec map[string]interface{}) Entity { 39 | kubeEntity := unstructured.Unstructured{Object: entitySpec} 40 | if metadata, ok := entitySpec["metadata"].(map[string]interface{}); ok { 41 | delete(metadata, "managedFields") 42 | return Entity{ 43 | ID: string(kubeEntity.GetUID()), 44 | Name: kubeEntity.GetName(), 45 | APIVersion: kubeEntity.GetAPIVersion(), 46 | Kind: kubeEntity.GetKind(), 47 | Namespace: kubeEntity.GetNamespace(), 48 | Manifest: entitySpec, 49 | ResourceVersion: kubeEntity.GetResourceVersion(), 50 | Labels: kubeEntity.GetLabels(), 51 | HasParent: len(kubeEntity.GetOwnerReferences()) != 0, 52 | } 53 | } 54 | return Entity{} 55 | 56 | } 57 | 58 | // EntitiesList a grouping of Entity objects 59 | type EntitiesList struct { 60 | HasNext bool 61 | // KeySet used to fetch next batch of entities 62 | KeySet string 63 | Data []Entity 64 | } 65 | 66 | // EntitiesSource responsible for fetching entities of a spcific K8s kind 67 | type EntitiesSource interface { 68 | // List returns entities 69 | List(ctx context.Context, listOptions *ListOptions) (*EntitiesList, error) 70 | // Kind returns kind of entities it retrieves 71 | Kind() string 72 | } 73 | 74 | // ListOptions configures the wanted return of a list operation 75 | type ListOptions struct { 76 | Limit int 77 | KeySet string 78 | } 79 | -------------------------------------------------------------------------------- /internal/terraform/terraform.go: -------------------------------------------------------------------------------- 1 | package terraform 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/weaveworks/policy-agent/pkg/logger" 9 | "github.com/weaveworks/policy-agent/pkg/policy-core/domain" 10 | "github.com/weaveworks/policy-agent/pkg/policy-core/validation" 11 | ctrl "sigs.k8s.io/controller-runtime" 12 | ) 13 | 14 | const ( 15 | TypeTFAdmission = "TFAdmission" 16 | ) 17 | 18 | type Response struct { 19 | Passed bool `json:"passed"` 20 | Violations []domain.PolicyValidation `json:"violations"` 21 | } 22 | 23 | // TerraformHandler listens to terraform validation requests and validates them using a validator 24 | type TerraformHandler struct { 25 | logLevel string 26 | validator validation.Validator 27 | } 28 | 29 | // NewTerraformHandler returns an terraform validation handler that listens to terraform validating requests 30 | func NewTerraformHandler(logLevel string, validator validation.Validator) *TerraformHandler { 31 | return &TerraformHandler{ 32 | logLevel: logLevel, 33 | validator: validator, 34 | } 35 | } 36 | 37 | // Handle validates terraform validation requests 38 | func (a *TerraformHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { 39 | var entitySpec map[string]interface{} 40 | err := json.NewDecoder(req.Body).Decode(&entitySpec) 41 | if err != nil { 42 | http.Error( 43 | rw, 44 | fmt.Sprintf("invalid request body, error: %v", err), 45 | http.StatusBadRequest, 46 | ) 47 | return 48 | } 49 | 50 | entity := domain.NewEntityFromSpec(entitySpec) 51 | logger.Infow("received valid request", "namespace", entity.Namespace, "name", entity.Name) 52 | 53 | result, err := a.validator.Validate(req.Context(), entity, "terraform") 54 | if err != nil { 55 | http.Error( 56 | rw, 57 | fmt.Sprintf("failed to validate resource, error: %v", err), 58 | http.StatusInternalServerError, 59 | ) 60 | return 61 | } 62 | 63 | var response Response 64 | if len(result.Violations) > 0 { 65 | for i := range result.Violations { 66 | if _, ok := result.Violations[i].Entity.Manifest["status"]; ok { 67 | result.Violations[i].Entity.Manifest["status"] = nil 68 | } 69 | } 70 | response.Violations = result.Violations 71 | } else { 72 | response.Passed = true 73 | } 74 | 75 | logger.Infow( 76 | "resource is validated", 77 | "namespace", entity.Namespace, 78 | "name", entity.Name, 79 | "passed", 80 | response.Passed, 81 | "violations", 82 | len(response.Violations), 83 | ) 84 | 85 | rw.Header().Set("Content-Type", "application/json") 86 | rw.WriteHeader(http.StatusOK) 87 | json.NewEncoder(rw).Encode(response) 88 | } 89 | 90 | // Run starts the webhook server 91 | func (a *TerraformHandler) Run(mgr ctrl.Manager) error { 92 | mgr.GetWebhookServer().Register("/terraform/admission", a) 93 | return nil 94 | } 95 | -------------------------------------------------------------------------------- /policies/ControllerReadOnlyFileSystem.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: pac.weave.works/v2beta3 2 | kind: Policy 3 | metadata: 4 | name: weave.policies.containers-read-only-root-filesystem 5 | spec: 6 | id: weave.policies.containers-read-only-root-filesystem 7 | name: Containers Read Only Root Filesystem 8 | enforce: true 9 | description: "This Policy will cause a violation if the root file system is not mounted as specified. As a security practice, the root file system should be read-only or expose risk to your nodes if compromised. \n\nThis Policy requires containers must run with a read-only root filesystem (i.e. no writable layer).\n" 10 | how_to_solve: "Set `readOnlyRootFilesystem` in your `securityContext` to the value specified in the Policy. \n```\n...\n spec:\n containers:\n - securityContext:\n readOnlyRootFilesystem: \n```\n\nhttps://kubernetes.io/docs/concepts/policy/pod-security-policy/#volumes-and-file-systems\n" 11 | category: weave.categories.pod-security 12 | severity: high 13 | targets: {kinds: [Deployment, Job, ReplicationController, ReplicaSet, DaemonSet, StatefulSet, CronJob]} 14 | standards: 15 | - id: weave.standards.mitre-attack 16 | controls: 17 | - weave.controls.mitre-attack.3.2 18 | - id: weave.standards.nist-800-190 19 | controls: 20 | - weave.controls.nist-800-190.4.4.4 21 | tags: [mitre-attack, nist800-190] 22 | parameters: 23 | - name: read_only 24 | type: boolean 25 | required: true 26 | value: true 27 | code: | 28 | package weave.advisor.podSecurity.enforce_ro_fs 29 | 30 | import future.keywords.in 31 | 32 | read_only = input.parameters.read_only 33 | violation[result] { 34 | some i 35 | containers := controller_spec.containers[i] 36 | root_fs := containers.securityContext.readOnlyRootFilesystem 37 | not root_fs == read_only 38 | result = { 39 | "issue detected": true, 40 | "msg": sprintf("readOnlyRootFilesystem should equal '%v'; detected '%v'", [read_only, root_fs]), 41 | "recommended_value": read_only, 42 | "violating_key": sprintf("spec.template.spec.containers[%v].securityContext.readOnlyRootFilesystem", [i]) 43 | } 44 | } 45 | 46 | # Controller input 47 | controller_input = input.review.object 48 | 49 | # controller_container acts as an iterator to get containers from the template 50 | controller_spec = controller_input.spec.template.spec { 51 | contains_kind(controller_input.kind, {"StatefulSet" , "DaemonSet", "Deployment", "Job"}) 52 | } else = controller_input.spec { 53 | controller_input.kind == "Pod" 54 | } else = controller_input.spec.jobTemplate.spec.template.spec { 55 | controller_input.kind == "CronJob" 56 | } 57 | 58 | contains_kind(kind, kinds) { 59 | kinds[_] = kind 60 | } 61 | 62 | -------------------------------------------------------------------------------- /pkg/policy-core/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/weaveworks/policy-agent/pkg/policy-core 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/golang/mock v1.6.0 7 | github.com/hashicorp/go-multierror v1.1.1 8 | github.com/stretchr/testify v1.8.1 9 | github.com/weaveworks/policy-agent/pkg/logger v1.1.0 10 | github.com/weaveworks/policy-agent/pkg/opa-core v1.1.0 11 | github.com/weaveworks/policy-agent/pkg/uuid-go v0.1.0 12 | k8s.io/api v0.26.3 13 | k8s.io/apimachinery v0.26.3 14 | sigs.k8s.io/kustomize/kyaml v0.14.1 15 | ) 16 | 17 | require ( 18 | github.com/OneOfOne/xxhash v1.2.8 // indirect 19 | github.com/agnivade/levenshtein v1.1.1 // indirect 20 | github.com/davecgh/go-spew v1.1.1 // indirect 21 | github.com/ghodss/yaml v1.0.0 // indirect 22 | github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8 // indirect 23 | github.com/go-errors/errors v1.4.2 // indirect 24 | github.com/go-logr/logr v1.2.4 // indirect 25 | github.com/go-openapi/jsonpointer v0.19.6 // indirect 26 | github.com/go-openapi/jsonreference v0.20.1 // indirect 27 | github.com/go-openapi/swag v0.22.3 // indirect 28 | github.com/gobwas/glob v0.2.3 // indirect 29 | github.com/gogo/protobuf v1.3.2 // indirect 30 | github.com/golang/protobuf v1.5.3 // indirect 31 | github.com/google/gnostic v0.5.7-v3refs // indirect 32 | github.com/google/go-cmp v0.5.9 // indirect 33 | github.com/google/gofuzz v1.2.0 // indirect 34 | github.com/hashicorp/errwrap v1.1.0 // indirect 35 | github.com/josharian/intern v1.0.0 // indirect 36 | github.com/json-iterator/go v1.1.12 // indirect 37 | github.com/mailru/easyjson v0.7.7 // indirect 38 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 39 | github.com/modern-go/reflect2 v1.0.2 // indirect 40 | github.com/open-policy-agent/opa v0.51.0 // indirect 41 | github.com/pmezard/go-difflib v1.0.0 // indirect 42 | github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect 43 | github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b // indirect 44 | github.com/tchap/go-patricia/v2 v2.3.1 // indirect 45 | github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect 46 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect 47 | github.com/yashtewari/glob-intersection v0.1.0 // indirect 48 | go.uber.org/atomic v1.10.0 // indirect 49 | go.uber.org/multierr v1.11.0 // indirect 50 | go.uber.org/zap v1.24.0 // indirect 51 | golang.org/x/net v0.9.0 // indirect 52 | golang.org/x/text v0.9.0 // indirect 53 | google.golang.org/protobuf v1.28.1 // indirect 54 | gopkg.in/inf.v0 v0.9.1 // indirect 55 | gopkg.in/yaml.v2 v2.4.0 // indirect 56 | gopkg.in/yaml.v3 v3.0.1 // indirect 57 | k8s.io/klog/v2 v2.90.1 // indirect 58 | k8s.io/kube-openapi v0.0.0-20230109183929-3758b55a6596 // indirect 59 | k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect 60 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect 61 | sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect 62 | ) 63 | -------------------------------------------------------------------------------- /internal/sink/filesystem/filesystem.go: -------------------------------------------------------------------------------- 1 | package filesystem 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "os" 8 | 9 | "github.com/weaveworks/policy-agent/pkg/logger" 10 | "github.com/weaveworks/policy-agent/pkg/policy-core/domain" 11 | ) 12 | 13 | const ( 14 | kubernetespProvider = "Kubernetes" 15 | ) 16 | 17 | type FileSystemSink struct { 18 | File *os.File 19 | PolicyValidationChan chan domain.PolicyValidation 20 | cancelWorker context.CancelFunc 21 | } 22 | 23 | // NewFileSystemSink returns a sink that writes results to the file system 24 | func NewFileSystemSink(filePath string) (*FileSystemSink, error) { 25 | file, err := os.OpenFile(filePath, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600) 26 | if err != nil { 27 | return nil, fmt.Errorf("failed to open file %s to write validation results: %w", filePath, err) 28 | } 29 | return &FileSystemSink{ 30 | File: file, 31 | PolicyValidationChan: make(chan domain.PolicyValidation, 50), 32 | }, nil 33 | } 34 | 35 | // Start starts the writer worker 36 | func (f *FileSystemSink) Start(ctx context.Context) error { 37 | cancelCtx, cancel := context.WithCancel(ctx) 38 | f.cancelWorker = cancel 39 | return f.WritePolicyValidationWorker(cancelCtx) 40 | } 41 | 42 | func (f *FileSystemSink) writeValidationResutl(policyValidation domain.PolicyValidation) error { 43 | err := json.NewEncoder(f.File).Encode(policyValidation) 44 | if err != nil { 45 | return fmt.Errorf("failed to write result to file: %w", err) 46 | } 47 | return nil 48 | } 49 | 50 | // WritePolicyValidationWorker worker that listens on results and admits them to a file 51 | func (f *FileSystemSink) WritePolicyValidationWorker(_ context.Context) error { 52 | for { 53 | select { 54 | case result := <-f.PolicyValidationChan: 55 | err := f.writeValidationResutl(result) 56 | if err != nil { 57 | logger.Errorw( 58 | fmt.Sprintf("error while writing %s results", result.Type), 59 | "error", err, 60 | "policy-id", result.Policy.ID, 61 | "entity-name", result.Entity.Name, 62 | "entity-type", result.Entity.Kind, 63 | "status", result.Status, 64 | ) 65 | } 66 | } 67 | } 68 | } 69 | 70 | // Write adds results to buffer, implements github.com/weaveworks/policy-agent/pkg/policy-core/domain.PolicyValidationSink 71 | func (f *FileSystemSink) Write(_ context.Context, policyValidations []domain.PolicyValidation) error { 72 | for i := range policyValidations { 73 | PolicyValidation := policyValidations[i] 74 | f.PolicyValidationChan <- PolicyValidation 75 | } 76 | 77 | return nil 78 | } 79 | 80 | // Stop stops file writer worker and commits all results to disk 81 | func (f *FileSystemSink) Stop() error { 82 | defer f.File.Close() 83 | 84 | f.cancelWorker() 85 | err := f.File.Sync() 86 | if err != nil { 87 | msg := fmt.Sprintf("failed to write all validations results to file, %s", err) 88 | logger.Error(msg) 89 | return fmt.Errorf(msg) 90 | } 91 | return nil 92 | } 93 | -------------------------------------------------------------------------------- /internal/sink/flux-notification/flux_notification_test.go: -------------------------------------------------------------------------------- 1 | package flux_notification 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/weaveworks/policy-agent/pkg/policy-core/domain" 10 | "github.com/weaveworks/policy-agent/pkg/uuid-go" 11 | v1 "k8s.io/api/core/v1" 12 | "k8s.io/apimachinery/pkg/types" 13 | "k8s.io/client-go/tools/record" 14 | ) 15 | 16 | func TestFluxNotificationSink(t *testing.T) { 17 | policy := domain.Policy{ 18 | ID: uuid.NewV4().String(), 19 | Name: "policy", 20 | Code: "code", 21 | Description: "description", 22 | HowToSolve: "how_to_solve", 23 | Category: "category", 24 | Severity: "severity", 25 | Reference: v1.ObjectReference{ 26 | UID: types.UID(uuid.NewV4().String()), 27 | APIVersion: "pac.weave.works/v1", 28 | Kind: "Policy", 29 | Name: "my-policy", 30 | ResourceVersion: "1", 31 | }, 32 | } 33 | 34 | helmReleaseEntity := domain.Entity{ 35 | ID: uuid.NewV4().String(), 36 | APIVersion: "v1", 37 | Kind: "Deployment", 38 | Name: "my-entity", 39 | Namespace: "default", 40 | Manifest: map[string]interface{}{}, 41 | ResourceVersion: "1", 42 | Labels: map[string]string{ 43 | "helm.toolkit.fluxcd.io/name": "my-helm-release", 44 | "helm.toolkit.fluxcd.io/namespace": "flux-system", 45 | }, 46 | } 47 | kustomizationEntity := domain.Entity{ 48 | ID: uuid.NewV4().String(), 49 | APIVersion: "v1", 50 | Kind: "Deployment", 51 | Name: "my-entity", 52 | Namespace: "default", 53 | Manifest: map[string]interface{}{}, 54 | ResourceVersion: "1", 55 | Labels: map[string]string{ 56 | "kustomize.toolkit.fluxcd.io/name": "my-kustomization", 57 | "kustomize.toolkit.fluxcd.io/namespace": "flux-system", 58 | }, 59 | } 60 | 61 | results := []domain.PolicyValidation{ 62 | { 63 | ID: uuid.NewV4().String(), 64 | Policy: policy, 65 | Entity: helmReleaseEntity, 66 | Status: domain.PolicyValidationStatusViolating, 67 | Message: "message", 68 | Type: "Admission", 69 | Trigger: "Admission", 70 | CreatedAt: time.Now(), 71 | }, 72 | { 73 | ID: uuid.NewV4().String(), 74 | Policy: policy, 75 | Entity: kustomizationEntity, 76 | Status: domain.PolicyValidationStatusCompliant, 77 | Message: "message", 78 | Type: "Admission", 79 | Trigger: "Admission", 80 | CreatedAt: time.Now(), 81 | }, 82 | } 83 | 84 | recorder := record.NewFakeRecorder(10) 85 | sink, err := NewFluxNotificationSink(recorder, "", "", "") 86 | if err != nil { 87 | t.Error(err) 88 | } 89 | 90 | ctx := context.Background() 91 | go sink.Start(ctx) 92 | 93 | err = sink.Write(ctx, results) 94 | if err != nil { 95 | t.Error(err) 96 | } 97 | 98 | time.Sleep(1 * time.Second) 99 | 100 | assert.Equal(t, len(recorder.Events), 2) 101 | } 102 | -------------------------------------------------------------------------------- /pkg/uuid-go/uuid.go: -------------------------------------------------------------------------------- 1 | package uuid 2 | 3 | import ( 4 | "encoding" 5 | 6 | "github.com/globalsign/mgo/bson" 7 | satori "github.com/satori/go.uuid" 8 | ) 9 | 10 | const ( 11 | Size = satori.Size 12 | ) 13 | 14 | type UUID satori.UUID 15 | 16 | var ( 17 | Nil = UUID{} 18 | ) 19 | 20 | var ( 21 | _ bson.Getter = (*UUID)(nil) 22 | _ bson.Setter = (*UUID)(nil) 23 | _ encoding.TextMarshaler = (*UUID)(nil) 24 | _ encoding.TextUnmarshaler = (*UUID)(nil) 25 | ) 26 | 27 | func FromString(raw string) (UUID, error) { 28 | id, err := satori.FromString(raw) 29 | return UUID(id), err 30 | } 31 | 32 | func FromStringSlice(raws []string) ([]UUID, error) { 33 | uuids := make([]UUID, 0, len(raws)) 34 | 35 | for _, raw := range raws { 36 | uid, err := FromString(raw) 37 | if err != nil { 38 | return nil, err 39 | } 40 | uuids = append(uuids, uid) 41 | } 42 | return uuids, nil 43 | } 44 | 45 | func FromBytes(raw []byte) (UUID, error) { 46 | id, err := satori.FromBytes(raw) 47 | return UUID(id), err 48 | } 49 | 50 | func (uuid UUID) String() string { 51 | return satori.UUID(uuid).String() 52 | } 53 | 54 | func (uuid UUID) Bytes() []byte { 55 | return satori.UUID(uuid).Bytes() 56 | } 57 | 58 | // NewV1 returns UUID based on current timestamp and MAC address. 59 | func NewV1() UUID { 60 | id := satori.Must(satori.NewV1()) 61 | return UUID(id) 62 | } 63 | 64 | // NewV2 returns DCE Security UUID based on POSIX UID/GID. 65 | func NewV2(domain byte) UUID { 66 | id := satori.Must(satori.NewV2(domain)) 67 | return UUID(id) 68 | } 69 | 70 | // NewV3 returns UUID based on MD5 hash of namespace UUID and name. 71 | func NewV3(ns UUID, name string) UUID { 72 | id := satori.NewV3(satori.UUID(ns), name) 73 | return UUID(id) 74 | } 75 | 76 | // NewV4 returns random generated UUID. 77 | func NewV4() UUID { 78 | id := satori.Must(satori.NewV4()) 79 | return UUID(id) 80 | } 81 | 82 | // NewV5 returns UUID based on SHA-1 hash of namespace UUID and name. 83 | func NewV5(ns UUID, name string) UUID { 84 | id := satori.NewV5(satori.UUID(ns), name) 85 | return UUID(id) 86 | } 87 | 88 | func (uuid UUID) GetBSON() (interface{}, error) { 89 | return uuid.String(), nil 90 | } 91 | 92 | func IsNil(id UUID) bool { 93 | return id == Nil 94 | } 95 | 96 | func (uuid UUID) IsNil() bool { 97 | return IsNil(uuid) 98 | } 99 | 100 | func (uuid *UUID) SetBSON(raw bson.Raw) error { 101 | var str string 102 | err := raw.Unmarshal(&str) 103 | if err != nil { 104 | return err 105 | } 106 | 107 | if str == "" { 108 | *uuid = Nil 109 | return nil 110 | } 111 | 112 | id, err := FromString(str) 113 | if err != nil { 114 | return err 115 | } 116 | 117 | *uuid = id 118 | 119 | return nil 120 | } 121 | 122 | func (uuid UUID) MarshalText() ([]byte, error) { 123 | return satori.UUID(uuid).MarshalText() 124 | } 125 | 126 | func (uuid *UUID) UnmarshalText(data []byte) error { 127 | id := satori.UUID(*uuid) 128 | err := id.UnmarshalText(data) 129 | if err != nil { 130 | return err 131 | } 132 | 133 | *uuid = UUID(id) 134 | return nil 135 | } 136 | -------------------------------------------------------------------------------- /pkg/policy-core/domain/mutation.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "regexp" 7 | "strings" 8 | 9 | "github.com/weaveworks/policy-agent/pkg/logger" 10 | "sigs.k8s.io/kustomize/kyaml/yaml" 11 | ) 12 | 13 | var ( 14 | jsonPathArrRegex = regexp.MustCompile("^([a-zA-Z0-9]+)\\[([0-9]+)\\]") 15 | ) 16 | 17 | const ( 18 | mutatedLabel = "pac.weave.works/mutated" 19 | ) 20 | 21 | type MutationResult struct { 22 | raw []byte 23 | node *yaml.RNode 24 | } 25 | 26 | // NewMutationResult create new MutationResult object 27 | func NewMutationResult(entity Entity) (*MutationResult, error) { 28 | raw, err := json.Marshal(entity.Manifest) 29 | if err != nil { 30 | return nil, fmt.Errorf("failed to marshal entity %s. error: %w", entity.Name, err) 31 | } 32 | 33 | var ynode yaml.Node 34 | err = yaml.Unmarshal(raw, &ynode) 35 | if err != nil { 36 | return nil, fmt.Errorf("failed to unmarshal entity %s. error: %w", entity.Name, err) 37 | } 38 | 39 | return &MutationResult{ 40 | raw: raw, 41 | node: yaml.NewRNode(&ynode), 42 | }, nil 43 | } 44 | 45 | // Mutate mutate resource by applying the recommended values of the given occurrences 46 | func (m *MutationResult) Mutate(occurrences []Occurrence) ([]Occurrence, error) { 47 | var mutated bool 48 | for i, occurrence := range occurrences { 49 | if occurrence.ViolatingKey == nil || occurrence.RecommendedValue == nil { 50 | continue 51 | } 52 | 53 | path := parseKeyPath(*occurrence.ViolatingKey) 54 | pathGetter := yaml.LookupCreate(yaml.MappingNode, path...) 55 | node, err := m.node.Pipe(pathGetter) 56 | if err != nil { 57 | logger.Errorw("failed while getting field's node", "error", err) 58 | continue 59 | } 60 | 61 | if node == nil { 62 | logger.Errorw("field not found", "path", occurrence.ViolatingKey) 63 | continue 64 | } 65 | 66 | value := occurrence.RecommendedValue 67 | if number, ok := value.(json.Number); ok { 68 | value, err = number.Float64() 69 | if err != nil { 70 | logger.Errorw("failed to parse number", "error", err) 71 | continue 72 | } 73 | } 74 | 75 | err = node.Document().Encode(value) 76 | if err != nil { 77 | logger.Errorw("failed to encode recommended value", "path", occurrence.ViolatingKey, "value", occurrence.RecommendedValue) 78 | continue 79 | } 80 | 81 | occurrences[i].Mutated = true 82 | mutated = true 83 | } 84 | if mutated { 85 | labels := m.node.GetLabels() 86 | labels[mutatedLabel] = "" 87 | m.node.SetLabels(labels) 88 | } 89 | return occurrences, nil 90 | } 91 | 92 | // OldResource return old resource before mutation 93 | func (m *MutationResult) OldResource() []byte { 94 | return m.raw 95 | } 96 | 97 | // NewResource return mutated resource 98 | func (m *MutationResult) NewResource() ([]byte, error) { 99 | return m.node.MarshalJSON() 100 | } 101 | 102 | func parseKeyPath(path string) []string { 103 | var keys []string 104 | parts := strings.Split(path, ".") 105 | for _, part := range parts { 106 | groups := jsonPathArrRegex.FindStringSubmatch(part) 107 | if groups == nil { 108 | keys = append(keys, part) 109 | } else { 110 | keys = append(keys, groups[1:]...) 111 | } 112 | } 113 | return keys 114 | } 115 | -------------------------------------------------------------------------------- /internal/admission/testdata/testdata.go: -------------------------------------------------------------------------------- 1 | package testdata 2 | 3 | var ( 4 | ValidadmissionBody = []byte(` 5 | { 6 | "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", 7 | 8 | "kind": {"group":"apps","version":"v1","kind":"Deployment"}, 9 | "resource": {"group":"apps","version":"v1","resource":"deployments"}, 10 | "subResource": "scale", 11 | "requestKind": {"group":"apps","version":"v1","kind":"Deployment"}, 12 | "requestResource": {"group":"apps","version":"v1","resource":"deployments"}, 13 | "requestSubResource": "scale", 14 | 15 | "name": "nginx-deployment", 16 | "namespace": "unit-testing", 17 | 18 | "operation": "CREATE", 19 | 20 | "userInfo": { 21 | "username": "admin", 22 | "uid": "014fbff9a07c", 23 | "groups": ["system:authenticated","my-admin-group"] 24 | }, 25 | 26 | "object": { 27 | "apiVersion": "apps/v1", 28 | "kind": "Deployment", 29 | "metadata": { 30 | "name": "nginx-deployment", 31 | "labels": { 32 | "app": "nginx" 33 | }, 34 | "namespace": "unit-testing" 35 | }, 36 | "spec": { 37 | "replicas": 3, 38 | "selector": { 39 | "matchLabels": { 40 | "app": "nginx" 41 | } 42 | }, 43 | "template": { 44 | "metadata": { 45 | "labels": { 46 | "app": "nginx" 47 | } 48 | }, 49 | "spec": { 50 | "containers": [ 51 | { 52 | "name": "nginx", 53 | "image": "nginx:latest", 54 | "ports": [ 55 | { 56 | "containerPort": 80 57 | } 58 | ] 59 | } 60 | ] 61 | } 62 | } 63 | } 64 | }, 65 | 66 | "dryRun": false 67 | } 68 | `) 69 | InvalidadmissionEntity = []byte(` 70 | { 71 | "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", 72 | 73 | "kind": {"group":"apps","version":"v1","kind":"Deployment"}, 74 | "resource": {"group":"apps","version":"v1","resource":"deployments"}, 75 | "subResource": "scale", 76 | "requestKind": {"group":"apps","version":"v1","kind":"Deployment"}, 77 | "requestResource": {"group":"apps","version":"v1","resource":"deployments"}, 78 | "requestSubResource": "scale", 79 | 80 | "name": "nginx-deployment", 81 | "namespace": "unit-testing", 82 | 83 | "operation": "CREATE", 84 | 85 | "userInfo": { 86 | "username": "admin", 87 | "uid": "014fbff9a07c", 88 | "groups": ["system:authenticated","my-admin-group"] 89 | }, 90 | 91 | "object": "invalid entity", 92 | 93 | "dryRun": false 94 | } 95 | `) 96 | SkippedadmissionBody = []byte(` 97 | { 98 | "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", 99 | 100 | "kind": {"group":"apps","version":"v1","kind":"Deployment"}, 101 | "resource": {"group":"apps","version":"v1","resource":"deployments"}, 102 | "subResource": "scale", 103 | "requestKind": {"group":"apps","version":"v1","kind":"Deployment"}, 104 | "requestResource": {"group":"apps","version":"v1","resource":"deployments"}, 105 | "requestSubResource": "scale", 106 | 107 | "name": "nginx-deployment", 108 | "namespace": "kube-system", 109 | 110 | "operation": "CREATE", 111 | 112 | "userInfo": { 113 | "username": "admin", 114 | "uid": "014fbff9a07c", 115 | "groups": ["system:authenticated","my-admin-group"] 116 | }, 117 | 118 | "object": { 119 | }, 120 | 121 | "dryRun": false 122 | } 123 | `) 124 | ) 125 | -------------------------------------------------------------------------------- /pkg/opa-core/core.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "reflect" 8 | "strings" 9 | 10 | "github.com/open-policy-agent/opa/ast" 11 | "github.com/open-policy-agent/opa/rego" 12 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 13 | "k8s.io/apimachinery/pkg/runtime" 14 | 15 | admissionV1 "k8s.io/api/admission/v1" 16 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 17 | ) 18 | 19 | // Parse constructs OPA policy from string 20 | func Parse(content, ruleQuery string) (Policy, error) { 21 | // validate module 22 | module, err := ast.ParseModule("", content) 23 | if err != nil { 24 | return Policy{}, err 25 | } 26 | 27 | if module == nil { 28 | return Policy{}, fmt.Errorf("Failed to parse module: empty content") 29 | } 30 | 31 | var valid bool 32 | for _, rule := range module.Rules { 33 | if rule.Head.Name == ast.Var(ruleQuery) { 34 | valid = true 35 | break 36 | } 37 | } 38 | 39 | if !valid { 40 | return Policy{}, fmt.Errorf("rule `%s` is not found", ruleQuery) 41 | } 42 | 43 | policy := Policy{ 44 | module: module, 45 | pkg: strings.Split(module.Package.String(), "package ")[1], 46 | } 47 | 48 | return policy, nil 49 | } 50 | 51 | // Eval validates data against given policy 52 | // returns error if there're any violations found 53 | func (p Policy) Eval(data interface{}, query string) error { 54 | rego := rego.New( 55 | rego.Query(fmt.Sprintf("data.%s.%s", p.pkg, query)), 56 | rego.ParsedModule(p.module), 57 | rego.Input(data), 58 | ) 59 | 60 | // Run evaluation. 61 | rs, err := rego.Eval(context.Background()) 62 | if err != nil { 63 | return err 64 | } 65 | for _, r := range rs { 66 | for _, expr := range r.Expressions { 67 | switch reflect.TypeOf(expr.Value).Kind() { 68 | case reflect.Slice: 69 | s := expr.Value.([]interface{}) 70 | if len(s) > 0 { 71 | err := NoValidError{ 72 | Details: s, 73 | } 74 | return err 75 | } 76 | case reflect.Map: 77 | s := expr.Value.(map[string]interface{}) 78 | err := NoValidError{ 79 | Details: s, 80 | } 81 | return err 82 | case reflect.String: 83 | s := expr.Value.(string) 84 | err := NoValidError{ 85 | Details: s, 86 | } 87 | return err 88 | } 89 | } 90 | } 91 | return nil 92 | } 93 | 94 | // EvalGateKeeperCompliant modifies the data to be Gatekeeper compliant and validates data against given policy 95 | // returns error if there're any violations found 96 | func (p Policy) EvalGateKeeperCompliant(data map[string]interface{}, parameters map[string]interface{}, query string) error { 97 | 98 | obj := unstructured.Unstructured{ 99 | Object: data, 100 | } 101 | 102 | bytesData, err := json.Marshal(data) 103 | if err != nil { 104 | return err 105 | } 106 | 107 | req := admissionV1.AdmissionRequest{ 108 | Name: obj.GetName(), 109 | Kind: metav1.GroupVersionKind{ 110 | Kind: obj.GetObjectKind().GroupVersionKind().Kind, 111 | Version: obj.GetObjectKind().GroupVersionKind().Version, 112 | Group: obj.GetObjectKind().GroupVersionKind().Group, 113 | }, 114 | Object: runtime.RawExtension{ 115 | Raw: bytesData, 116 | }, 117 | } 118 | input := map[string]interface{}{"review": req, "parameters": parameters} 119 | 120 | return p.Eval(input, query) 121 | } 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![codecov](https://codecov.io/gh/weaveworks/policy-agent/branch/dev/graph/badge.svg?token=5HALYBWEIQ)](https://codecov.io/gh/weaveworks/policy-agent) ![build](https://github.com/weaveworks/policy-agent/actions/workflows/build.yml/badge.svg?branch=dev) [![Contributors](https://img.shields.io/github/contributors/weaveworks/policy-agent)](https://github.com/weaveworks/policy-agent/graphs/contributors) 2 | [![Release](https://img.shields.io/github/v/release/weaveworks/policy-agent?include_prereleases)](https://github.com/weaveworks/policy-agent/releases/latest) 3 | 4 | # Weave Policy Agent 5 | 6 | Weave Policy Agent is a policy-as-code engine built on Open Policy Agent (OPA) that ensures security, compliance, and best practices for Kubernetes applications. Designed for GitOps workflows, especially Flux, it enables fine-grained policies for Flux applications and tenants, ensuring isolation and compliance across Kubernetes deployments. 7 | 8 | ## Features 9 | 10 | #### Prevent violating K8s resources via admission controller 11 | Weave Policy Agent uses the Kubernetes admission controller to monitor any Kubernetes Resource changes and prevent the ones violating the policies from getting deployed. 12 | 13 | #### Prevent violating terraform plans via `tf-controller` 14 | If you are using flux's terraform controller ([tf-controller](https://github.com/weaveworks/tf-controller)) to apply and sync your terraform plans, you can use Weave Policy Agent to prevent violating plans from being applied to your cluster. 15 | 16 | #### Audit runtime compliance 17 | The agent scans Kubernetes resources on the cluster and reports runtime violations at a configurable frequency. 18 | 19 | #### Advanced features for flux 20 | While the agent works natively with Kubernetes resources, Weave Policy Agent has specific features allowing fine-grained policy configurations to flux applications and tenants, as well as alerting integration with flux's `notification-controller` 21 | 22 | #### Observability via WeaveGitOps UI 23 | Policies and violations can be displayed on WeaveGitOps Dashboards allowing better observability of the cluster's compliance. 24 | 25 | #### Example Policies 26 | Example policies that target K8s and Flux best practices are available [here](policies). Users can as well write their policies in Rego using the agent policy CRD. 27 | 28 | ## Getting started 29 | 30 | To get started, check out this [guide](docs/getting-started.md) on how to install the policy agent to your Kubernetes cluster and explore violations. 31 | 32 | ## Documentation 33 | 34 | Policy agent guides for running the agent in Weave GitOps Enterprise, and leveraging all its capabilities, are available at [docs.gitops.weave.works](https://docs.gitops.weave.works/docs/policy/intro/). 35 | 36 | Refer to this [doc](docs/README.md) for documentation on the high-level architecture and the different components that make up the agent. 37 | 38 | ## Contribution 39 | 40 | Need help or want to contribute? Please see the links below. 41 | 45 | - Have feature proposals or want to contribute? 46 | - Please create a [Github issue](https://github.com/weaveworks/weave-policy-agent/issues). 47 | - Learn more about contributing [here](./CONTRIBUTING.md). 48 | -------------------------------------------------------------------------------- /internal/sink/flux-notification/flux_notification.go: -------------------------------------------------------------------------------- 1 | package flux_notification 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/weaveworks/policy-agent/internal/utils" 8 | "github.com/weaveworks/policy-agent/pkg/logger" 9 | "github.com/weaveworks/policy-agent/pkg/policy-core/domain" 10 | "k8s.io/client-go/tools/record" 11 | ) 12 | 13 | const ( 14 | resultChanSize int = 50 15 | ) 16 | 17 | type FluxNotificationSink struct { 18 | recorder record.EventRecorder 19 | resultChan chan domain.PolicyValidation 20 | cancelWorker context.CancelFunc 21 | accountID string 22 | clusterID string 23 | } 24 | 25 | // NewFluxNotificationSink returns a sink that sends results to flux notification controller 26 | func NewFluxNotificationSink(recorder record.EventRecorder, webhook, accountID, clusterID string) (*FluxNotificationSink, error) { 27 | return &FluxNotificationSink{ 28 | recorder: recorder, 29 | resultChan: make(chan domain.PolicyValidation, resultChanSize), 30 | accountID: accountID, 31 | clusterID: clusterID, 32 | }, nil 33 | } 34 | 35 | // Start starts the writer worker 36 | func (f *FluxNotificationSink) Start(ctx context.Context) error { 37 | ctx, cancel := context.WithCancel(ctx) 38 | f.cancelWorker = cancel 39 | return f.writeWorker(ctx) 40 | } 41 | 42 | // Stop stops worker 43 | func (f *FluxNotificationSink) Stop() { 44 | f.cancelWorker() 45 | } 46 | 47 | // Write adds results to buffer, implements github.com/weaveworks/policy-agent/pkg/policy-core/domain.PolicyValidationSink 48 | func (f *FluxNotificationSink) Write(_ context.Context, results []domain.PolicyValidation) error { 49 | logger.Debugw("writing validation results", "sink", "flux_notification", "count", len(results)) 50 | for _, result := range results { 51 | f.resultChan <- result 52 | } 53 | return nil 54 | } 55 | 56 | func (f *FluxNotificationSink) writeWorker(ctx context.Context) error { 57 | for { 58 | select { 59 | case result := <-f.resultChan: 60 | f.write(result) 61 | case <-ctx.Done(): 62 | logger.Info("stopping write worker ...") 63 | return nil 64 | } 65 | } 66 | } 67 | 68 | func (f *FluxNotificationSink) write(result domain.PolicyValidation) { 69 | fluxObject := utils.GetFluxObject(result.Entity.Labels) 70 | if fluxObject == nil { 71 | logger.Debugw( 72 | fmt.Sprintf("discarding %s result for orphan entity", result.Type), 73 | "kind", result.Entity.Kind, 74 | "name", result.Entity.Name, 75 | "namespace", result.Entity.Namespace, 76 | ) 77 | return 78 | } 79 | 80 | event, err := domain.NewK8sEventFromPolicyValidation(result) 81 | if err != nil { 82 | logger.Errorw( 83 | "failed to create event from policy validation for flux notification", 84 | "error", 85 | err, 86 | "entity_kind", result.Entity.Kind, 87 | "entity_name", result.Entity.Name, 88 | "entity_namespace", result.Entity.Namespace, 89 | "policy", result.Policy.ID, 90 | ) 91 | return 92 | } 93 | 94 | logger.Debugw( 95 | "sending event ...", 96 | "type", event.Type, 97 | "entity_kind", result.Entity.Kind, 98 | "entity_name", result.Entity.Name, 99 | "entity_namespace", result.Entity.Namespace, 100 | "policy", result.Policy.ID, 101 | ) 102 | 103 | f.recorder.AnnotatedEventf( 104 | fluxObject, 105 | event.Annotations, 106 | event.Type, 107 | event.Reason, 108 | event.Message, 109 | ) 110 | } 111 | -------------------------------------------------------------------------------- /api/v2beta2/policyset_types.go: -------------------------------------------------------------------------------- 1 | package v2beta2 2 | 3 | import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 4 | 5 | const ( 6 | PolicySetResourceName = "policysets" 7 | PolicySetKind = "PolicySet" 8 | PolicySetListKind = "PolicySetList" 9 | PolicySetAuditMode = "audit" 10 | PolicySetAdmissionMode = "admission" 11 | PolicySetTFAdmissionMode = "tf-admission" 12 | ) 13 | 14 | var ( 15 | PolicySetGroupVersionResource = GroupVersion.WithResource(PolicySetResourceName) 16 | ) 17 | 18 | type PolicySetFilters struct { 19 | IDs []string `json:"ids,omitempty"` 20 | Categories []string `json:"categories,omitempty"` 21 | Severities []string `json:"severities,omitempty"` 22 | Standards []string `json:"standards,omitempty"` 23 | Tags []string `json:"tags,omitempty"` 24 | } 25 | 26 | type PolicySetSpec struct { 27 | //+optional 28 | Name string `json:"name"` 29 | //+kubebuilder:validation:Enum=audit;admission;tf-admission 30 | // Mode is the policy set mode, must be one of audit,admission,tf-admission 31 | Mode string `json:"mode"` 32 | Filters PolicySetFilters `json:"filters"` 33 | } 34 | 35 | //+kubebuilder:object:root=true 36 | //+kubebuilder:printcolumn:name="Mode",type=string,JSONPath=`.spec.mode` 37 | //+kubebuilder:resource:scope=Cluster 38 | //+kubebuilder:storageversion 39 | 40 | // PolicySet is the Schema for the policysets API 41 | type PolicySet struct { 42 | metav1.TypeMeta `json:",inline"` 43 | metav1.ObjectMeta `json:"metadata,omitempty"` 44 | Spec PolicySetSpec `json:"spec,omitempty"` 45 | } 46 | 47 | // Match check if policy matches the policyset or not 48 | func (ps *PolicySet) Match(policy Policy) bool { 49 | if len(ps.Spec.Filters.IDs) > 0 { 50 | for _, id := range ps.Spec.Filters.IDs { 51 | if policy.Name == id { 52 | return true 53 | } 54 | } 55 | } 56 | if len(ps.Spec.Filters.Categories) > 0 { 57 | for _, category := range ps.Spec.Filters.Categories { 58 | if policy.Spec.Category == category { 59 | return true 60 | } 61 | } 62 | } 63 | if len(ps.Spec.Filters.Severities) > 0 { 64 | for _, severity := range ps.Spec.Filters.Severities { 65 | if policy.Spec.Severity == severity { 66 | return true 67 | } 68 | } 69 | } 70 | if len(ps.Spec.Filters.Standards) > 0 { 71 | standards := map[string]struct{}{} 72 | for _, standard := range ps.Spec.Filters.Standards { 73 | standards[standard] = struct{}{} 74 | } 75 | for _, standard := range policy.Spec.Standards { 76 | if _, ok := standards[standard.ID]; ok { 77 | return true 78 | } 79 | } 80 | } 81 | if len(ps.Spec.Filters.Tags) > 0 { 82 | tags := map[string]struct{}{} 83 | for _, tag := range ps.Spec.Filters.Tags { 84 | tags[tag] = struct{}{} 85 | } 86 | for _, tag := range policy.Spec.Tags { 87 | if _, ok := tags[tag]; ok { 88 | return true 89 | } 90 | } 91 | } 92 | return false 93 | } 94 | 95 | // +kubebuilder:object:root=true 96 | // +kubebuilder:resource:scope=Cluster 97 | // +kubebuilder:storageversion 98 | 99 | // PolicySetList contains a list of PolicySet 100 | type PolicySetList struct { 101 | metav1.TypeMeta `json:",inline"` 102 | metav1.ListMeta `json:"metadata,omitempty"` 103 | Items []PolicySet `json:"items"` 104 | } 105 | 106 | func init() { 107 | SchemeBuilder.Register( 108 | &PolicySet{}, 109 | &PolicySetList{}, 110 | ) 111 | } 112 | -------------------------------------------------------------------------------- /internal/auditor/auditor.go: -------------------------------------------------------------------------------- 1 | package auditor 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/weaveworks/policy-agent/pkg/logger" 8 | "github.com/weaveworks/policy-agent/pkg/policy-core/domain" 9 | "github.com/weaveworks/policy-agent/pkg/policy-core/validation" 10 | ) 11 | 12 | // AuditorController performs audit on a regular interval by using entitites sources to retrieve resources 13 | type AuditorController struct { 14 | entitiesSources []domain.EntitiesSource 15 | auditEvent chan AuditEvent 16 | validator validation.Validator 17 | auditEventListener AuditEventListener 18 | auditInterval time.Duration 19 | } 20 | 21 | // NewAuditController returns a new instance of AuditController with an audit event listener 22 | func NewAuditController(validator validation.Validator, auditInterval time.Duration, entitiesSources ...domain.EntitiesSource) *AuditorController { 23 | auditController := &AuditorController{ 24 | entitiesSources: entitiesSources, 25 | auditEvent: make(chan AuditEvent, 1), 26 | validator: validator, 27 | auditInterval: auditInterval, 28 | } 29 | auditController.auditEventListener = auditController.doAudit 30 | return auditController 31 | } 32 | 33 | // RegisterAuditEventListener adds a listener that reacts to audit events, replaces existing listener 34 | func (a *AuditorController) RegisterAuditEventListener(auditEventListener AuditEventListener) { 35 | a.auditEventListener = auditEventListener 36 | } 37 | 38 | // Start starts the audit controller 39 | func (a *AuditorController) Start(ctx context.Context) error { 40 | logger.Info("starting audit controller...") 41 | auditTicker := time.NewTicker(a.auditInterval) 42 | defer auditTicker.Stop() 43 | for { 44 | select { 45 | case <-ctx.Done(): 46 | logger.Info("stopping audit controller...") 47 | return nil 48 | case <-auditTicker.C: 49 | auditEvent := AuditEvent{Type: AuditEventTypePeriodical} 50 | a.auditEventListener(ctx, auditEvent) 51 | case event := <-a.auditEvent: 52 | a.auditEventListener(ctx, event) 53 | } 54 | } 55 | } 56 | 57 | // doAudit lists available entities and performs validation on each entity 58 | func (a *AuditorController) doAudit(ctx context.Context, auditEvent AuditEvent) { 59 | logger.Infof("starting %s", auditEvent.Type) 60 | for i := range a.entitiesSources { 61 | hasNext := true 62 | keySet := "" 63 | entitySource := a.entitiesSources[i] 64 | for hasNext { 65 | opts := domain.ListOptions{ 66 | Limit: entitiesSizeLimit, 67 | KeySet: keySet, 68 | } 69 | entitiesList, err := entitySource.List(ctx, &opts) 70 | if err != nil { 71 | logger.Errorw("failed to list entities during audit", "kind", entitySource.Kind(), "error", err) 72 | break 73 | } 74 | hasNext = entitiesList.HasNext 75 | keySet = entitiesList.KeySet 76 | 77 | for idx := range entitiesList.Data { 78 | entity := entitiesList.Data[idx] 79 | if entity.HasParent { 80 | continue 81 | } 82 | _, err := a.validator.Validate(ctx, entity, string(auditEvent.Type)) 83 | if err != nil { 84 | logger.Errorw( 85 | "failed to validate entity during audit", 86 | "entity-kind", entity.Kind, 87 | "entity-name", entity.Name, 88 | "error", err) 89 | } 90 | } 91 | } 92 | } 93 | logger.Info("finished audit") 94 | } 95 | 96 | // Audit triggers an audit with specified audit type 97 | func (a *AuditorController) Audit(auditType AuditEventType, data interface{}) { 98 | a.auditEvent <- AuditEvent{ 99 | Type: auditType, 100 | Data: data, 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /internal/policies/policy.go: -------------------------------------------------------------------------------- 1 | package crd 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | 8 | pacv2 "github.com/weaveworks/policy-agent/api/v2beta3" 9 | "github.com/weaveworks/policy-agent/pkg/logger" 10 | "github.com/weaveworks/policy-agent/pkg/policy-core/domain" 11 | v1 "k8s.io/api/core/v1" 12 | ctrl "sigs.k8s.io/controller-runtime" 13 | ctrlCache "sigs.k8s.io/controller-runtime/pkg/cache" 14 | "sigs.k8s.io/controller-runtime/pkg/client" 15 | ) 16 | 17 | type PoliciesWatcher struct { 18 | cache ctrlCache.Cache 19 | Provider string 20 | } 21 | 22 | // NewPoliciesWatcher returns a policies source that fetches them from Kubernetes API 23 | func NewPoliciesWatcher(ctx context.Context, mgr ctrl.Manager, provider string) (*PoliciesWatcher, error) { 24 | return &PoliciesWatcher{ 25 | cache: mgr.GetCache(), 26 | Provider: provider, 27 | }, nil 28 | } 29 | 30 | // GetAll returns all policies, implements github.com/weaveworks/policy-agent/pkg/policy-core/domain.PoliciesSource 31 | func (p *PoliciesWatcher) GetAll(ctx context.Context) ([]domain.Policy, error) { 32 | policiesCRD := &pacv2.PolicyList{} 33 | err := p.cache.List(ctx, policiesCRD, &client.ListOptions{}) 34 | if err != nil { 35 | return nil, fmt.Errorf("error while retrieving policies CRD from cache: %w", err) 36 | } 37 | 38 | logger.Debugw("retrieved CRD policies from cache", "count", len(policiesCRD.Items)) 39 | 40 | var policies []domain.Policy 41 | for i := range policiesCRD.Items { 42 | if !p.match(policiesCRD.Items[i]) { 43 | continue 44 | } 45 | 46 | policyCRD := policiesCRD.Items[i].Spec 47 | policy := domain.Policy{ 48 | Name: policyCRD.Name, 49 | ID: policyCRD.ID, 50 | Code: policyCRD.Code, 51 | Enforce: policyCRD.Enforce, 52 | Targets: domain.PolicyTargets{ 53 | Kinds: policyCRD.Targets.Kinds, 54 | Labels: policyCRD.Targets.Labels, 55 | Namespaces: policyCRD.Targets.Namespaces, 56 | }, 57 | Description: policyCRD.Description, 58 | HowToSolve: policyCRD.HowToSolve, 59 | Category: policyCRD.Category, 60 | Tags: policyCRD.Tags, 61 | Severity: policyCRD.Severity, 62 | Reference: v1.ObjectReference{ 63 | APIVersion: policiesCRD.Items[i].APIVersion, 64 | Kind: policiesCRD.Items[i].Kind, 65 | UID: policiesCRD.Items[i].UID, 66 | Name: policiesCRD.Items[i].Name, 67 | Namespace: policiesCRD.Items[i].Namespace, 68 | ResourceVersion: policiesCRD.Items[i].ResourceVersion, 69 | }, 70 | Mutate: policyCRD.Mutate, 71 | Exclude: domain.PolicyExclusions{ 72 | Namespaces: policyCRD.Exclude.Namespaces, 73 | Resources: policyCRD.Exclude.Resources, 74 | Labels: policyCRD.Exclude.Labels, 75 | }, 76 | } 77 | 78 | for _, standardCRD := range policyCRD.Standards { 79 | standard := domain.PolicyStandard{ 80 | ID: standardCRD.ID, 81 | Controls: standardCRD.Controls, 82 | } 83 | policy.Standards = append(policy.Standards, standard) 84 | } 85 | 86 | for k := range policyCRD.Parameters { 87 | paramCRD := policyCRD.Parameters[k] 88 | param := domain.PolicyParameters{ 89 | Name: paramCRD.Name, 90 | Type: paramCRD.Type, 91 | Required: paramCRD.Required, 92 | } 93 | if paramCRD.Value != nil { 94 | err = json.Unmarshal(paramCRD.Value.Raw, ¶m.Value) 95 | if err != nil { 96 | logger.Errorw("failed to load policy parameter value", "error", err) 97 | } 98 | } 99 | policy.Parameters = append(policy.Parameters, param) 100 | } 101 | 102 | policies = append(policies, policy) 103 | } 104 | return policies, nil 105 | } 106 | 107 | func (p *PoliciesWatcher) match(policy pacv2.Policy) bool { 108 | // check provider 109 | return policy.Spec.Provider == p.Provider 110 | } 111 | -------------------------------------------------------------------------------- /internal/clients/kube/kube.go: -------------------------------------------------------------------------------- 1 | package kube 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io/ioutil" 7 | "strings" 8 | 9 | authv1 "k8s.io/api/authorization/v1" 10 | meta "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 12 | "k8s.io/apimachinery/pkg/runtime/schema" 13 | 14 | "k8s.io/client-go/discovery" 15 | "k8s.io/client-go/dynamic" 16 | "k8s.io/client-go/kubernetes" 17 | "k8s.io/client-go/rest" 18 | ) 19 | 20 | // KubeClient provides interface to various k8s api calls 21 | type KubeClient struct { 22 | ClientSet kubernetes.Interface 23 | DynamicClient dynamic.Interface 24 | DiscoveryClient discovery.DiscoveryInterface 25 | } 26 | 27 | // NewKubeClient returns a new instance of KubeClient 28 | func NewKubeClient(config *rest.Config) (*KubeClient, error) { 29 | clientSet, err := kubernetes.NewForConfig(config) 30 | if err != nil { 31 | return nil, fmt.Errorf("unable to create clientset for kube client, error: %w", err) 32 | } 33 | dynamicClient, err := dynamic.NewForConfig(config) 34 | if err != nil { 35 | return nil, fmt.Errorf("unable to create dynamic client for kube client, error: %w", err) 36 | } 37 | return &KubeClient{ 38 | ClientSet: clientSet, 39 | DynamicClient: dynamicClient, 40 | DiscoveryClient: clientSet.DiscoveryClient}, nil 41 | 42 | } 43 | 44 | // GetAgentPermissions retrieves allowed permissions for the agent 45 | func (k *KubeClient) GetAgentPermissions(ctx context.Context) (*authv1.SelfSubjectRulesReview, error) { 46 | 47 | rulesSpec := authv1.SelfSubjectRulesReview{ 48 | Spec: authv1.SelfSubjectRulesReviewSpec{ 49 | Namespace: "kube-system", 50 | }, 51 | Status: authv1.SubjectRulesReviewStatus{ 52 | Incomplete: false, 53 | }, 54 | } 55 | 56 | subjectRules, err := k.ClientSet.AuthorizationV1().SelfSubjectRulesReviews().Create(ctx, &rulesSpec, meta.CreateOptions{}) 57 | if err != nil { 58 | return nil, fmt.Errorf("unable to get agent permissions, error: %w", err) 59 | } 60 | 61 | return subjectRules, nil 62 | } 63 | 64 | // ListResourceItems returns items from a specific reource group version 65 | func (k *KubeClient) ListResourceItems( 66 | ctx context.Context, 67 | resource schema.GroupVersionResource, 68 | namespace string, 69 | listOptions meta.ListOptions) (*unstructured.UnstructuredList, error) { 70 | 71 | list, err := k.DynamicClient.Resource(resource).Namespace(namespace).List(ctx, listOptions) 72 | if err != nil { 73 | return nil, fmt.Errorf("unable to list resource %s in namespace %s: %w", resource.Resource, namespace, err) 74 | } 75 | return list, nil 76 | } 77 | 78 | // GetAPIResources returns all available api resources in the cluster 79 | func (k *KubeClient) GetAPIResources(ctx context.Context) ([]*meta.APIResourceList, error) { 80 | apiResourcesList, err := k.DiscoveryClient.ServerPreferredResources() 81 | if err != nil { 82 | return nil, fmt.Errorf("failed to get server api resources: %w", err) 83 | } 84 | return apiResourcesList, nil 85 | } 86 | 87 | func (k *KubeClient) GetServerVersion() (string, error) { 88 | version, err := k.DiscoveryClient.ServerVersion() 89 | if err != nil { 90 | return "", fmt.Errorf("unable to get cluster version: %w", err) 91 | } 92 | 93 | return version.String(), nil 94 | } 95 | 96 | func (k *KubeClient) GetClusterProvider(ctx context.Context) (string, error) { 97 | nodes, err := k.ClientSet.CoreV1().Nodes().List(ctx, meta.ListOptions{}) 98 | if err != nil { 99 | return "", fmt.Errorf("unable to list cluster nodes: %w", err) 100 | } 101 | 102 | node := nodes.Items[0] 103 | return strings.Split(node.Spec.ProviderID, ":")[0], nil 104 | } 105 | 106 | func (k *KubeClient) GetAgentNamespace() string { 107 | namespace := "undefined" 108 | namespaceBytes, err := ioutil.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace") 109 | if err == nil { 110 | namespace = string(namespaceBytes) 111 | } 112 | return namespace 113 | } 114 | -------------------------------------------------------------------------------- /configuration/config.go: -------------------------------------------------------------------------------- 1 | package configuration 2 | 3 | import ( 4 | "path/filepath" 5 | "strings" 6 | 7 | "github.com/spf13/viper" 8 | "github.com/weaveworks/policy-agent/pkg/logger" 9 | ) 10 | 11 | type SinksConfig struct { 12 | FilesystemSink *FileSystemSink 13 | FluxNotificationSink *FluxNotificationSink 14 | K8sEventsSink *K8sEventsSink 15 | ElasticSink *ElasticSink 16 | } 17 | 18 | type K8sEventsSink struct { 19 | Enabled bool 20 | } 21 | 22 | type FileSystemSink struct { 23 | FileName string 24 | } 25 | 26 | type FluxNotificationSink struct { 27 | Address string 28 | } 29 | 30 | type AdmissionWebhook struct { 31 | Listen int 32 | CertDir string 33 | } 34 | 35 | type ElasticSink struct { 36 | IndexName string 37 | Address string 38 | Username string 39 | Password string 40 | InsertionMode string 41 | } 42 | 43 | type AdmissionConfig struct { 44 | Enabled bool 45 | Webhook AdmissionWebhook 46 | Sinks SinksConfig 47 | Mutate bool 48 | } 49 | 50 | type AuditConfig struct { 51 | WriteCompliance bool 52 | Enabled bool 53 | Sinks SinksConfig 54 | Interval uint 55 | } 56 | 57 | type TFAdmissionConfig struct { 58 | Enabled bool 59 | Sinks SinksConfig 60 | } 61 | 62 | type Config struct { 63 | KubeConfigFile string 64 | AccountID string 65 | ClusterID string 66 | 67 | LogLevel string 68 | 69 | ProbesListen string 70 | MetricsAddress string 71 | 72 | Admission AdmissionConfig 73 | Audit AuditConfig 74 | TFAdmission TFAdmissionConfig 75 | } 76 | 77 | func GetAgentConfiguration(filePath string) Config { 78 | dir, file := filepath.Split(filePath) 79 | 80 | viper.AutomaticEnv() 81 | replacer := strings.NewReplacer(".", "_") 82 | viper.SetEnvKeyReplacer(replacer) 83 | 84 | viper.SetConfigName(file) 85 | viper.SetConfigType("yaml") 86 | viper.AddConfigPath(dir) 87 | 88 | err := viper.ReadInConfig() 89 | if err != nil { 90 | logger.Fatal(err) 91 | } 92 | viper.SetDefault("kubeConfigFile", "") 93 | viper.SetDefault("metricsAddress", ":8080") 94 | viper.SetDefault("probesListen", ":9000") 95 | viper.SetDefault("logLevel", "info") 96 | viper.SetDefault("admission.webhook.listen", 8443) 97 | viper.SetDefault("admission.webhook.certDir", "/certs") 98 | viper.SetDefault("audit.interval", 24) 99 | 100 | checkRequiredFields() 101 | 102 | c := Config{} 103 | 104 | err = viper.Unmarshal(&c) 105 | if err != nil { 106 | logger.Fatal(err) 107 | } 108 | 109 | if c.Admission.Enabled && c.Admission.Sinks.ElasticSink != nil { 110 | c.Admission.Sinks.ElasticSink.Address = getField( 111 | "admission.sinks.elasticSink.address") 112 | c.Admission.Sinks.ElasticSink.IndexName = getField( 113 | "admission.sinks.elasticSink.indexname") 114 | c.Admission.Sinks.ElasticSink.Username = getField( 115 | "admission.sinks.elasticSink.username") 116 | c.Admission.Sinks.ElasticSink.Password = getField( 117 | "admission.sinks.elasticSink.password") 118 | } 119 | 120 | if c.Audit.Enabled && c.Audit.Sinks.ElasticSink != nil { 121 | c.Audit.Sinks.ElasticSink.Address = getField( 122 | "audit.sinks.elasticSink.address") 123 | c.Audit.Sinks.ElasticSink.IndexName = getField( 124 | "audit.sinks.elasticSink.indexname") 125 | c.Audit.Sinks.ElasticSink.Username = getField( 126 | "audit.sinks.elasticSink.username") 127 | c.Audit.Sinks.ElasticSink.Password = getField( 128 | "audit.sinks.elasticSink.password") 129 | } 130 | 131 | return c 132 | } 133 | 134 | func checkRequiredFields() { 135 | requiredFields := []string{ 136 | "accountId", 137 | "clusterId", 138 | } 139 | 140 | for _, field := range requiredFields { 141 | if !viper.IsSet(field) { 142 | logger.Fatalw( 143 | "missing key in agent configuration file", 144 | "key", field, 145 | ) 146 | } 147 | } 148 | } 149 | 150 | func getField(key string) string { 151 | value, _ := viper.Get(key).(string) 152 | return value 153 | } 154 | -------------------------------------------------------------------------------- /policies/ControllerContainerAllowingPrivilegeEscalation.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: pac.weave.works/v2beta3 2 | kind: Policy 3 | metadata: 4 | name: weave.policies.containers-running-with-privilege-escalation 5 | spec: 6 | id: weave.policies.containers-running-with-privilege-escalation 7 | name: Containers Running With Privilege Escalation 8 | enforce: true 9 | description: "Containers are running with PrivilegeEscalation configured. Setting this Policy to `true` allows child processes to gain more privileges than its parent process. \n\nThis Policy gates whether or not a user is allowed to set the security context of a container to `allowPrivilegeEscalation` to `true`. The default value for this is `false` so no child process of a container can gain more privileges than its parent.\n\nThere are 2 parameters for this Policy:\n- exclude_namespace (string) : This sets a namespace you want to exclude from Policy compliance checking. \n- allow_privilege_escalation (bool) : This checks for the value of `allowPrivilegeEscalation` in your spec. \n" 10 | how_to_solve: | 11 | Check the following path to see what the PrivilegeEscalation value is set to. 12 | ``` 13 | ... 14 | spec: 15 | containers: 16 | securityContext: 17 | allowPrivilegeEscalation: 18 | ``` 19 | https://kubernetes.io/docs/tasks/configure-pod-container/security-context/ 20 | category: weave.categories.pod-security 21 | severity: high 22 | targets: {kinds: [Deployment, Job, ReplicationController, ReplicaSet, DaemonSet, StatefulSet, CronJob]} 23 | standards: 24 | - id: weave.standards.pci-dss 25 | controls: 26 | - weave.controls.pci-dss.2.2.4 27 | - weave.controls.pci-dss.2.2.5 28 | - id: weave.standards.cis-benchmark 29 | controls: 30 | - weave.controls.cis-benchmark.5.2.5 31 | - id: weave.standards.mitre-attack 32 | controls: 33 | - weave.controls.mitre-attack.4.1 34 | - id: weave.standards.nist-800-190 35 | controls: 36 | - weave.controls.nist-800-190.3.3.2 37 | - id: weave.standards.gdpr 38 | controls: 39 | - weave.controls.gdpr.25 40 | - weave.controls.gdpr.32 41 | - weave.controls.gdpr.24 42 | - id: weave.standards.soc2-type-i 43 | controls: 44 | - weave.controls.soc2-type-i.1.6.1 45 | tags: [pci-dss, cis-benchmark, mitre-attack, nist800-190, gdpr, default, soc2-type1] 46 | parameters: 47 | - name: allow_privilege_escalation 48 | type: boolean 49 | required: true 50 | value: false 51 | exclude: 52 | namespaces: 53 | - kube-system 54 | code: | 55 | package weave.advisor.podSecurity.privilegeEscalation 56 | 57 | import future.keywords.in 58 | 59 | allow_privilege_escalation := input.parameters.allow_privilege_escalation 60 | 61 | violation[result] { 62 | some i 63 | containers := controller_spec.containers[i] 64 | allow_priv := containers.securityContext.allowPrivilegeEscalation 65 | not allow_priv == allow_privilege_escalation 66 | result = { 67 | "issue detected": true, 68 | "msg": sprintf("Container %s privilegeEscalation should be set to '%v'; detected '%v'", [containers[i].name, allow_privilege_escalation, allow_priv]), 69 | "violating_key": sprintf("spec.template.spec.containers[%v].securityContext.allowPrivilegeEscalation", [i]), 70 | "recommended_value": allow_privilege_escalation 71 | } 72 | } 73 | 74 | is_array_contains(array,str) { 75 | array[_] = str 76 | } 77 | 78 | # Controller input 79 | controller_input = input.review.object 80 | 81 | # controller_container acts as an iterator to get containers from the template 82 | controller_spec = controller_input.spec.template.spec { 83 | contains_kind(controller_input.kind, {"StatefulSet" , "DaemonSet", "Deployment", "Job"}) 84 | } else = controller_input.spec { 85 | controller_input.kind == "Pod" 86 | } else = controller_input.spec.jobTemplate.spec.template.spec { 87 | controller_input.kind == "CronJob" 88 | } 89 | 90 | contains_kind(kind, kinds) { 91 | kinds[_] = kind 92 | } 93 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: [ master, dev ] 6 | pull_request: 7 | branches: [ master, dev ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3.5.0 14 | with: 15 | fetch-depth: 0 16 | 17 | - name: setup go 18 | uses: actions/setup-go@v4 19 | with: 20 | go-version: '1.20.x' 21 | cache: true 22 | 23 | - name: Get branch name (merge) 24 | if: github.event_name != 'pull_request' 25 | run: echo "BRANCH_NAME=$(echo ${GITHUB_REF#refs/heads/} | tr / -)" >> $GITHUB_ENV 26 | 27 | - name: Get branch name (pull request) 28 | if: github.event_name == 'pull_request' 29 | run: | 30 | git fetch -a 31 | echo "BRANCH_NAME=$(echo ${GITHUB_HEAD_REF} | tr / -)" >> $GITHUB_ENV 32 | 33 | - name: Build binary 34 | run: | 35 | make build 36 | 37 | - name: Running ElasticSearch 38 | run: | 39 | docker run -itd --name elasticsearch -p 9200:9200 -e "discovery.type=single-node" -e "xpack.security.enabled=false" docker.elastic.co/elasticsearch/elasticsearch:8.3.3 40 | sleep 20 41 | timeout 120 sh -c 'until nc -z $0 $1; do echo "waiting for elasticsearch to start on port 9200"; sleep 5; done' localhost 9200 42 | 43 | - name: run test 44 | env: 45 | CODECOV_TOKEN: ${{secrets.CODECOV_TOKEN}} 46 | run: | 47 | echo " 48 | go 1.20 49 | 50 | use ( 51 | . 52 | ./api 53 | ./pkg/logger 54 | ./pkg/opa-core 55 | ./pkg/policy-core 56 | ./pkg/uuid-go 57 | )" >> go.work 58 | make test 59 | bash <(curl -s https://codecov.io/bash) -F unit 60 | 61 | - name: Build docker image 62 | run: | 63 | COMMIT_COUNT=$(git rev-list --count HEAD) 64 | SHORT_HASH=$(git rev-parse --short HEAD) 65 | VERSION=${{ env.BRANCH_NAME }}-${COMMIT_COUNT}-${SHORT_HASH} 66 | 67 | if [[ ${{ env.BRANCH_NAME }} == "master" ]]; then 68 | export VERSION=master 69 | fi 70 | 71 | if [[ ${{ env.BRANCH_NAME }} == "dev" ]]; then 72 | export VERSION=dev 73 | fi 74 | 75 | make image VERSION=$VERSION 76 | 77 | - name: Scan The Image 78 | run: | 79 | REPO=policy-agent 80 | COMMIT_COUNT=$(git rev-list --count HEAD) 81 | SHORT_HASH=$(git rev-parse --short HEAD) 82 | VERSION=${{ env.BRANCH_NAME }}-${COMMIT_COUNT}-${SHORT_HASH} 83 | 84 | if [[ ${{ env.BRANCH_NAME }} == "master" ]]; then 85 | export VERSION=master 86 | fi 87 | 88 | if [[ ${{ env.BRANCH_NAME }} == "dev" ]]; then 89 | export VERSION=dev 90 | fi 91 | 92 | echo scanning ${REPO}:${VERSION} 93 | curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sudo sh -s -- -b /usr/local/bin 94 | grype "${REPO}:${VERSION}" --scope all-layers > /tmp/report.txt #--fail-on high to fail on sev high 95 | 96 | - name: Login to Docker Hub 97 | if: github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/master' 98 | uses: docker/login-action@v1 99 | with: 100 | registry: docker.io 101 | username: ${{ secrets.DOCKER_USER }} 102 | password: ${{ secrets.DOCKER_PASSWORD }} 103 | 104 | 105 | - name: Create image and push to Docker Registry 106 | if: github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/master' 107 | run: | 108 | 109 | if [[ ${{ env.BRANCH_NAME }} == "master" ]]; then 110 | export VERSION=master 111 | fi 112 | 113 | if [[ ${{ env.BRANCH_NAME }} == "dev" ]]; then 114 | export VERSION=dev 115 | fi 116 | 117 | make push@weaveworks tag-file=new-tag version-file=new-version VERSION=$VERSION 118 | -------------------------------------------------------------------------------- /internal/sink/k8s-event/k8s_event.go: -------------------------------------------------------------------------------- 1 | package k8s_event 2 | 3 | import ( 4 | "context" 5 | "os" 6 | 7 | "github.com/weaveworks/policy-agent/internal/utils" 8 | "github.com/weaveworks/policy-agent/pkg/logger" 9 | "github.com/weaveworks/policy-agent/pkg/policy-core/domain" 10 | v1 "k8s.io/api/core/v1" 11 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 | 13 | "k8s.io/client-go/kubernetes" 14 | ) 15 | 16 | const ( 17 | resultChanSize int = 50 18 | ) 19 | 20 | type K8sEventSink struct { 21 | kubeClient kubernetes.Interface 22 | resultChan chan domain.PolicyValidation 23 | cancelWorker context.CancelFunc 24 | accountID string 25 | clusterID string 26 | reportingController string 27 | reportingInstance string 28 | } 29 | 30 | // NewK8sEventSink returns a sink that sends results to kubernetes events queue 31 | func NewK8sEventSink(kubeClient kubernetes.Interface, accountID, clusterID, reportingController string) (*K8sEventSink, error) { 32 | hostname, err := os.Hostname() 33 | if err != nil { 34 | return nil, err 35 | } 36 | 37 | return &K8sEventSink{ 38 | kubeClient: kubeClient, 39 | resultChan: make(chan domain.PolicyValidation, resultChanSize), 40 | accountID: accountID, 41 | clusterID: clusterID, 42 | reportingController: reportingController, 43 | reportingInstance: hostname, 44 | }, nil 45 | } 46 | 47 | // Start starts the writer worker 48 | func (k *K8sEventSink) Start(ctx context.Context) error { 49 | ctx, cancel := context.WithCancel(ctx) 50 | k.cancelWorker = cancel 51 | return k.writeWorker(ctx) 52 | } 53 | 54 | // Stop stops worker 55 | func (k *K8sEventSink) Stop() { 56 | k.cancelWorker() 57 | } 58 | 59 | // Write adds results to buffer, implements github.com/weaveworks/policy-agent/pkg/policy-core/domain.PolicyValidationSink 60 | func (k *K8sEventSink) Write(_ context.Context, results []domain.PolicyValidation) error { 61 | logger.Infow("writing validation results", "sink", "k8s_events", "count", len(results)) 62 | for _, result := range results { 63 | k.resultChan <- result 64 | } 65 | return nil 66 | } 67 | 68 | func (f *K8sEventSink) writeWorker(ctx context.Context) error { 69 | for { 70 | select { 71 | case result := <-f.resultChan: 72 | f.write(ctx, result) 73 | case <-ctx.Done(): 74 | logger.Info("stopping write worker ...") 75 | return nil 76 | } 77 | } 78 | } 79 | 80 | func (k *K8sEventSink) write(ctx context.Context, result domain.PolicyValidation) { 81 | event, err := domain.NewK8sEventFromPolicyValidation(result) 82 | if err != nil { 83 | logger.Errorw( 84 | "failed to create event from policy validation", 85 | "error", 86 | err, 87 | "entity_kind", result.Entity.Kind, 88 | "entity_name", result.Entity.Name, 89 | "entity_namespace", result.Entity.Namespace, 90 | "policy", result.Policy.ID, 91 | ) 92 | return 93 | } 94 | 95 | fluxObject := utils.GetFluxObject(result.Entity.Labels) 96 | if fluxObject != nil { 97 | event.InvolvedObject = v1.ObjectReference{ 98 | UID: fluxObject.GetUID(), 99 | APIVersion: fluxObject.GetAPIVersion(), 100 | Kind: fluxObject.GetKind(), 101 | Name: fluxObject.GetName(), 102 | Namespace: fluxObject.GetNamespace(), 103 | ResourceVersion: fluxObject.GetResourceVersion(), 104 | FieldPath: result.Policy.ID, 105 | } 106 | event.Namespace = fluxObject.GetNamespace() 107 | } 108 | 109 | event.ReportingController = k.reportingController 110 | event.ReportingInstance = k.reportingInstance 111 | event.Source = v1.EventSource{Component: k.reportingController} 112 | 113 | logger.Debugw( 114 | "sending event ...", 115 | "type", event.Type, 116 | "entity_kind", result.Entity.Kind, 117 | "entity_name", result.Entity.Name, 118 | "entity_namespace", result.Entity.Namespace, 119 | "policy", result.Policy.ID, 120 | ) 121 | 122 | _, err = k.kubeClient.CoreV1().Events(event.Namespace).Create(ctx, event, metav1.CreateOptions{}) 123 | if err != nil { 124 | logger.Errorw("failed to send event", "error", err) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /internal/sink/elastic/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "properties": { 3 | "account_id": { 4 | "type": "keyword" 5 | }, 6 | "cluster_id": { 7 | "type": "keyword" 8 | }, 9 | "created_at": { 10 | "type": "date", 11 | "format": "date_optional_time" 12 | }, 13 | "entity": { 14 | "properties": { 15 | "apiVersion": { 16 | "type": "keyword" 17 | }, 18 | "has_parent": { 19 | "type": "boolean" 20 | }, 21 | "id": { 22 | "type": "keyword" 23 | }, 24 | "kind": { 25 | "type": "keyword" 26 | }, 27 | "manifest": { 28 | "dynamic": false, 29 | "properties": {} 30 | }, 31 | "name": { 32 | "type": "keyword" 33 | }, 34 | "namespace": { 35 | "type": "keyword" 36 | }, 37 | "resource_version": { 38 | "type": "keyword" 39 | } 40 | } 41 | }, 42 | "id": { 43 | "type": "keyword" 44 | }, 45 | "message": { 46 | "type": "keyword" 47 | }, 48 | "occurrences": { 49 | "type": "nested", 50 | "dynamic": "false" 51 | }, 52 | "policy": { 53 | "properties": { 54 | "category": { 55 | "type": "keyword" 56 | }, 57 | "code": { 58 | "type": "keyword" 59 | }, 60 | "description": { 61 | "type": "keyword" 62 | }, 63 | "enabled": { 64 | "type": "boolean" 65 | }, 66 | "how_to_solve": { 67 | "type": "keyword" 68 | }, 69 | "id": { 70 | "type": "keyword" 71 | }, 72 | "name": { 73 | "type": "keyword" 74 | }, 75 | "parameters": { 76 | "type": "nested", 77 | "properties": { 78 | "name": { 79 | "type": "keyword" 80 | }, 81 | "required": { 82 | "type": "boolean" 83 | }, 84 | "type": { 85 | "type": "keyword" 86 | }, 87 | "value": { 88 | "type": "keyword" 89 | } 90 | } 91 | }, 92 | "severity": { 93 | "type": "keyword" 94 | }, 95 | "standards": { 96 | "type": "nested", 97 | "properties": { 98 | "controls": { 99 | "type": "keyword" 100 | }, 101 | "id": { 102 | "type": "keyword" 103 | } 104 | } 105 | }, 106 | "tags": { 107 | "type": "keyword" 108 | }, 109 | "targets": { 110 | "properties": { 111 | "kinds": { 112 | "type": "keyword" 113 | }, 114 | "labels": { 115 | "type": "keyword" 116 | }, 117 | "namespaces": { 118 | "type": "keyword" 119 | } 120 | } 121 | } 122 | } 123 | }, 124 | "source": { 125 | "type": "keyword" 126 | }, 127 | "status": { 128 | "type": "keyword" 129 | }, 130 | "trigger": { 131 | "type": "keyword" 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /pkg/opa-core/core_test.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | type testCaseParsePolicy struct { 8 | name string 9 | content string 10 | hasError bool 11 | } 12 | 13 | func TestParse(t *testing.T) { 14 | cases := []testCaseParsePolicy{ 15 | { 16 | name: "single rule", 17 | content: ` 18 | package core 19 | violation[issue] { 20 | issue = "test" 21 | }`}, 22 | { 23 | name: "multiple rules at once", 24 | content: ` 25 | package core 26 | violation[issue] { 27 | issue = "test" 28 | } 29 | violation[issue] { 30 | issue = "test" 31 | } 32 | `, 33 | hasError: false, 34 | }, 35 | { 36 | name: "invalid syntax", 37 | content: ` 38 | package core 39 | issue = "test issue") 40 | `, 41 | hasError: true, 42 | }, 43 | { 44 | name: "invalid syntax", 45 | content: ` 46 | package core 47 | issue = "test issue") 48 | `, 49 | hasError: true, 50 | }, 51 | { 52 | name: "empty content", 53 | content: "", 54 | hasError: true, 55 | }, 56 | { 57 | name: "policy without package", 58 | content: ` 59 | violation[issue] { 60 | x = 3 61 | } 62 | `, 63 | hasError: true, 64 | }, 65 | } 66 | 67 | for _, c := range cases { 68 | _, err := Parse(c.content, "violation") 69 | if c.hasError { 70 | if err == nil { 71 | t.Errorf("[%s]: passed but should have been failed", c.name) 72 | } 73 | } else { 74 | if err != nil { 75 | t.Errorf("[%s]: %v", c.name, err) 76 | } 77 | } 78 | } 79 | } 80 | 81 | type testCaseEval struct { 82 | name string 83 | content string 84 | violationMsg string 85 | hasViolation bool 86 | } 87 | 88 | func TestEval(t *testing.T) { 89 | cases := []testCaseEval{ 90 | { 91 | name: "rule has no violation", 92 | content: ` 93 | package core 94 | violation[issue] { 95 | 1 == 2 96 | issue = "violation test" 97 | }`, 98 | }, 99 | { 100 | name: "rule has an empty violation", 101 | content: ` 102 | package core 103 | violation[issue] { 104 | issue = "" 105 | }`, 106 | violationMsg: "[\"\"]", 107 | hasViolation: true, 108 | }, 109 | { 110 | name: "rule has a bool violation", 111 | content: ` 112 | package core 113 | violation[issue] { 114 | issue = true 115 | }`, 116 | violationMsg: "[true]", 117 | hasViolation: true, 118 | }, 119 | } 120 | 121 | for _, c := range cases { 122 | policy, err := Parse(c.content, "violation") 123 | err = policy.Eval("{}", "violation") 124 | 125 | if c.hasViolation { 126 | if err == nil { 127 | t.Errorf("[%s]: passed but should have been failed", c.name) 128 | } else if err.Error() != c.violationMsg { 129 | t.Errorf("[%s]: expected error msg '%s' but got %s", c.name, c.violationMsg, err) 130 | } 131 | } else { 132 | if err != nil { 133 | t.Errorf("[%s]: %v", c.name, err) 134 | } 135 | } 136 | } 137 | } 138 | 139 | func TestEvalGateKeeperCompliant(t *testing.T) { 140 | cases := []testCaseEval{ 141 | { 142 | name: "rule has no violation", 143 | content: ` 144 | package magalix.advisor.labels.missing_label 145 | 146 | label := input.parameters.label 147 | 148 | violation[result] { 149 | result=input.review.name 150 | } 151 | `, 152 | hasViolation: true, 153 | violationMsg: "[\"kubernetes-downwardapi-volume-example\"]", 154 | }, 155 | } 156 | 157 | for _, c := range cases { 158 | policy, err := Parse(c.content, "violation") 159 | err = policy.EvalGateKeeperCompliant( 160 | map[string]interface{}{ 161 | "apiVersion": "v1", "kind": "Pod", 162 | "metadata": map[string]interface{}{"name": "kubernetes-downwardapi-volume-example", 163 | "labels": map[string]interface{}{"zone": "us-est-coast", "cluster": "test-cluster1", "rack": "rack-22"}, 164 | "annotations": map[string]interface{}{"build": "two", "builder": "john-doe"}}}, 165 | map[string]interface{}{"probe": "livenessProbe"}, 166 | "violation", 167 | ) 168 | 169 | if c.hasViolation { 170 | if err == nil { 171 | t.Errorf("[%s]: passed but should have been failed", c.name) 172 | } else if err.Error() != c.violationMsg { 173 | t.Errorf("[%s]: expected error msg '%s' but got %s", c.name, c.violationMsg, err) 174 | } 175 | } else { 176 | if err != nil { 177 | t.Errorf("[%s]: %v", c.name, err) 178 | } 179 | } 180 | } 181 | 182 | } 183 | -------------------------------------------------------------------------------- /internal/admission/admission.go: -------------------------------------------------------------------------------- 1 | package admission 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | "strings" 9 | 10 | "github.com/weaveworks/policy-agent/pkg/logger" 11 | "github.com/weaveworks/policy-agent/pkg/policy-core/domain" 12 | "github.com/weaveworks/policy-agent/pkg/policy-core/validation" 13 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 14 | ctrl "sigs.k8s.io/controller-runtime" 15 | ctrlAdmission "sigs.k8s.io/controller-runtime/pkg/webhook/admission" 16 | ) 17 | 18 | // AdmissionHandler listens to admission requests and validates them using a validator 19 | type AdmissionHandler struct { 20 | logLevel string 21 | validator validation.Validator 22 | } 23 | 24 | const ( 25 | TypeAdmission = "Admission" 26 | DebugLevel = "debug" 27 | ExcludedefaultNamespacesMsg = "default kubernetes namespaces are excluded" 28 | ErrGettingAdmissionEntity = "failed to get entity info from admission request" 29 | ErrValidatingResource = "failed to validate resource" 30 | ) 31 | 32 | // NewAdmissionHandler returns an admission handler that listens to k8s validating requests 33 | func NewAdmissionHandler( 34 | logLevel string, 35 | validator validation.Validator) *AdmissionHandler { 36 | return &AdmissionHandler{ 37 | logLevel: logLevel, 38 | validator: validator, 39 | } 40 | } 41 | 42 | func (a *AdmissionHandler) handleErrors(err error, errMsg string) ctrlAdmission.Response { 43 | logger.Errorw("validating admission request error", "error", err, "error-message", errMsg) 44 | errRsp := ctrlAdmission.ValidationResponse(false, errMsg) 45 | errRsp.Result.Code = http.StatusInternalServerError 46 | return errRsp 47 | } 48 | 49 | // Handle validates admission requests, implements interface at sigs.k8s.io/controller-runtime/pkg/webhook/admission.Handler 50 | func (a *AdmissionHandler) Handle(ctx context.Context, req ctrlAdmission.Request) ctrlAdmission.Response { 51 | namespace := req.Namespace 52 | if namespace == metav1.NamespacePublic || namespace == metav1.NamespaceSystem { 53 | return ctrlAdmission.ValidationResponse(true, ExcludedefaultNamespacesMsg) 54 | } 55 | 56 | if a.logLevel == DebugLevel { 57 | logger.Debugw("admission request body", "payload", req) 58 | } 59 | 60 | var entitySpec map[string]interface{} 61 | err := json.Unmarshal(req.Object.Raw, &entitySpec) 62 | if err != nil { 63 | return a.handleErrors(err, ErrGettingAdmissionEntity) 64 | } 65 | 66 | entity := domain.NewEntityFromSpec(entitySpec) 67 | result, err := a.validator.Validate(ctx, entity, string(req.AdmissionRequest.Operation)) 68 | if err != nil { 69 | return a.handleErrors(err, ErrValidatingResource) 70 | } 71 | 72 | if len(result.Violations) > 0 { 73 | // If a resource has multiple policies evaluated 74 | // and any of those policies are violated and has the enforce flag equals true 75 | // then the resource submission is blocked. 76 | allowed := true 77 | for _, violation := range result.Violations { 78 | if violation.Enforced { 79 | allowed = false 80 | break 81 | } 82 | } 83 | 84 | return ctrlAdmission.ValidationResponse(allowed, generateResponse(result.Violations)) 85 | } 86 | 87 | return ctrlAdmission.ValidationResponse(true, "") 88 | } 89 | 90 | // Run starts the admission webhook server 91 | func (a *AdmissionHandler) Run(mgr ctrl.Manager) error { 92 | webhook := ctrlAdmission.Webhook{Handler: a} 93 | mgr.GetWebhookServer().Register("/admission", &webhook) 94 | 95 | return nil 96 | } 97 | 98 | func generateResponse(violations []domain.PolicyValidation) string { 99 | var buffer strings.Builder 100 | for _, violation := range violations { 101 | buffer.WriteString("==================================================================\n") 102 | buffer.WriteString(fmt.Sprintf("Policy : %s\n", violation.Policy.ID)) 103 | 104 | if violation.Entity.Namespace == "" { 105 | buffer.WriteString(fmt.Sprintf("Entity : %s/%s\n", strings.ToLower(violation.Entity.Kind), violation.Entity.Name)) 106 | } else { 107 | buffer.WriteString(fmt.Sprintf("Entity : %s/%s in namespace: %s\n", strings.ToLower(violation.Entity.Kind), violation.Entity.Name, violation.Entity.Namespace)) 108 | } 109 | 110 | buffer.WriteString("Occurrences:\n") 111 | for _, occurrence := range violation.Occurrences { 112 | buffer.WriteString(fmt.Sprintf("- %s\n", occurrence.Message)) 113 | } 114 | } 115 | return buffer.String() 116 | } 117 | -------------------------------------------------------------------------------- /api/v2beta2/policyconfig_types.go: -------------------------------------------------------------------------------- 1 | package v2beta2 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | ) 10 | 11 | const ( 12 | PolicyConfigResourceName = "policyconfigs" 13 | PolicyConfigKind = "PolicyConfig" 14 | PolicyConfigListKind = "PolicyListConfig" 15 | ) 16 | 17 | var ( 18 | PolicyConfigGroupVersionResource = GroupVersion.WithResource(PolicyConfigResourceName) 19 | ) 20 | 21 | // PolicyConfigStatus will hold the policies ids that don't exist in the cluster 22 | type PolicyConfigStatus struct { 23 | Status string `json:"status,omitempty"` 24 | MissingPolicies []string `json:"missingPolicies,omitempty"` 25 | } 26 | type PolicyTargetApplication struct { 27 | //+kubebuilder:validation:Enum=HelmRelease;Kustomization 28 | Kind string `json:"kind"` 29 | Name string `json:"name"` 30 | //+optional 31 | Namespace string `json:"namespace"` 32 | } 33 | 34 | func (at *PolicyTargetApplication) ID() string { 35 | return fmt.Sprintf("%s/%s:%s", strings.ToLower(at.Kind), at.Name, at.Namespace) 36 | } 37 | 38 | type PolicyTargetResource struct { 39 | Kind string `json:"kind"` 40 | Name string `json:"name"` 41 | // +optional 42 | Namespace string `json:"namespace"` 43 | } 44 | 45 | func (rt *PolicyTargetResource) ID() string { 46 | return fmt.Sprintf("%s/%s:%s", strings.ToLower(rt.Kind), rt.Name, rt.Namespace) 47 | } 48 | 49 | type PolicyConfigTarget struct { 50 | //+optional 51 | Workspaces []string `json:"workspaces,omitempty"` 52 | //+optional 53 | Namespaces []string `json:"namespaces,omitempty"` 54 | //+optional 55 | Applications []PolicyTargetApplication `json:"apps,omitempty"` 56 | //+optional 57 | Resources []PolicyTargetResource `json:"resources,omitempty"` 58 | } 59 | 60 | type PolicyConfigConfig struct { 61 | Parameters map[string]apiextensionsv1.JSON `json:"parameters"` 62 | } 63 | 64 | type PolicyConfigSpec struct { 65 | Config map[string]PolicyConfigConfig `json:"config"` 66 | Match PolicyConfigTarget `json:"match"` 67 | } 68 | 69 | // +kubebuilder:object:root=true 70 | // +kubebuilder:resource:scope=Cluster 71 | // +kubebuilder:subresource:status 72 | // +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.status` 73 | // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` 74 | 75 | // PolicyConfig is the Schema for the policyconfigs API 76 | type PolicyConfig struct { 77 | metav1.TypeMeta `json:",inline"` 78 | metav1.ObjectMeta `json:"metadata,omitempty"` 79 | Spec PolicyConfigSpec `json:"spec,omitempty"` 80 | Status PolicyConfigStatus `json:"status,omitempty"` 81 | } 82 | 83 | // SetPolicyConfigStatus sets policy config status 84 | func (c *PolicyConfig) SetPolicyConfigStatus(missingPolicies []string) { 85 | if len(missingPolicies) > 0 { 86 | c.Status.Status = "Warning" 87 | } else { 88 | c.Status.Status = "OK" 89 | } 90 | c.Status.MissingPolicies = missingPolicies 91 | } 92 | 93 | func (c *PolicyConfig) Validate() error { 94 | var target string 95 | 96 | if c.Spec.Match.Workspaces != nil { 97 | target = "workspaces" 98 | } 99 | 100 | if c.Spec.Match.Namespaces != nil { 101 | if target != "" { 102 | return fmt.Errorf("cannot target %s and namespaces in same policy config", target) 103 | } 104 | target = "namespaces" 105 | } 106 | 107 | if c.Spec.Match.Applications != nil { 108 | if target != "" { 109 | return fmt.Errorf("cannot target %s and apps in same policy config", target) 110 | } 111 | target = "apps" 112 | } 113 | 114 | if c.Spec.Match.Resources != nil { 115 | if target != "" { 116 | return fmt.Errorf("cannot target %s and resources in same policy config", target) 117 | } 118 | target = "resources" 119 | } 120 | 121 | if target == "" { 122 | return fmt.Errorf("policy config must target namespace, application or resource") 123 | } 124 | 125 | return nil 126 | } 127 | 128 | // +kubebuilder:object:root=true 129 | // +kubebuilder:resource:scope=Cluster 130 | // +kubebuilder:storageversion 131 | 132 | // PolicyConfigList contains a list of PolicyConfig 133 | type PolicyConfigList struct { 134 | metav1.TypeMeta `json:",inline"` 135 | metav1.ListMeta `json:"metadata,omitempty"` 136 | Items []PolicyConfig `json:"items"` 137 | } 138 | 139 | func init() { 140 | SchemeBuilder.Register( 141 | &PolicyConfig{}, 142 | &PolicyConfigList{}, 143 | ) 144 | } 145 | -------------------------------------------------------------------------------- /api/v2beta3/policyconfig_types.go: -------------------------------------------------------------------------------- 1 | package v2beta3 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | ) 10 | 11 | const ( 12 | PolicyConfigResourceName = "policyconfigs" 13 | PolicyConfigKind = "PolicyConfig" 14 | PolicyConfigListKind = "PolicyListConfig" 15 | ) 16 | 17 | var ( 18 | PolicyConfigGroupVersionResource = GroupVersion.WithResource(PolicyConfigResourceName) 19 | ) 20 | 21 | // PolicyConfigStatus will hold the policies ids that don't exist in the cluster 22 | type PolicyConfigStatus struct { 23 | Status string `json:"status,omitempty"` 24 | MissingPolicies []string `json:"missingPolicies,omitempty"` 25 | } 26 | type PolicyTargetApplication struct { 27 | //+kubebuilder:validation:Enum=HelmRelease;Kustomization 28 | Kind string `json:"kind"` 29 | Name string `json:"name"` 30 | //+optional 31 | Namespace string `json:"namespace"` 32 | } 33 | 34 | func (at *PolicyTargetApplication) ID() string { 35 | return fmt.Sprintf("%s/%s:%s", strings.ToLower(at.Kind), at.Name, at.Namespace) 36 | } 37 | 38 | type PolicyTargetResource struct { 39 | Kind string `json:"kind"` 40 | Name string `json:"name"` 41 | // +optional 42 | Namespace string `json:"namespace"` 43 | } 44 | 45 | func (rt *PolicyTargetResource) ID() string { 46 | return fmt.Sprintf("%s/%s:%s", strings.ToLower(rt.Kind), rt.Name, rt.Namespace) 47 | } 48 | 49 | type PolicyConfigTarget struct { 50 | //+optional 51 | Workspaces []string `json:"workspaces,omitempty"` 52 | //+optional 53 | Namespaces []string `json:"namespaces,omitempty"` 54 | //+optional 55 | Applications []PolicyTargetApplication `json:"apps,omitempty"` 56 | //+optional 57 | Resources []PolicyTargetResource `json:"resources,omitempty"` 58 | } 59 | 60 | type PolicyConfigConfig struct { 61 | Parameters map[string]apiextensionsv1.JSON `json:"parameters"` 62 | } 63 | 64 | type PolicyConfigSpec struct { 65 | Config map[string]PolicyConfigConfig `json:"config"` 66 | Match PolicyConfigTarget `json:"match"` 67 | } 68 | 69 | // +kubebuilder:object:root=true 70 | // +kubebuilder:resource:scope=Cluster 71 | // +kubebuilder:storageversion 72 | // +kubebuilder:subresource:status 73 | // +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.status` 74 | // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` 75 | 76 | // PolicyConfig is the Schema for the policyconfigs API 77 | type PolicyConfig struct { 78 | metav1.TypeMeta `json:",inline"` 79 | metav1.ObjectMeta `json:"metadata,omitempty"` 80 | Spec PolicyConfigSpec `json:"spec,omitempty"` 81 | Status PolicyConfigStatus `json:"status,omitempty"` 82 | } 83 | 84 | // SetPolicyConfigStatus sets policy config status 85 | func (c *PolicyConfig) SetPolicyConfigStatus(missingPolicies []string) { 86 | if len(missingPolicies) > 0 { 87 | c.Status.Status = "Warning" 88 | } else { 89 | c.Status.Status = "OK" 90 | } 91 | c.Status.MissingPolicies = missingPolicies 92 | } 93 | 94 | func (c *PolicyConfig) Validate() error { 95 | var target string 96 | 97 | if c.Spec.Match.Workspaces != nil { 98 | target = "workspaces" 99 | } 100 | 101 | if c.Spec.Match.Namespaces != nil { 102 | if target != "" { 103 | return fmt.Errorf("cannot target %s and namespaces in same policy config", target) 104 | } 105 | target = "namespaces" 106 | } 107 | 108 | if c.Spec.Match.Applications != nil { 109 | if target != "" { 110 | return fmt.Errorf("cannot target %s and apps in same policy config", target) 111 | } 112 | target = "apps" 113 | } 114 | 115 | if c.Spec.Match.Resources != nil { 116 | if target != "" { 117 | return fmt.Errorf("cannot target %s and resources in same policy config", target) 118 | } 119 | target = "resources" 120 | } 121 | 122 | if target == "" { 123 | return fmt.Errorf("policy config must target namespace, application or resource") 124 | } 125 | 126 | return nil 127 | } 128 | 129 | // +kubebuilder:object:root=true 130 | // +kubebuilder:resource:scope=Cluster 131 | // +kubebuilder:storageversion 132 | 133 | // PolicyConfigList contains a list of PolicyConfig 134 | type PolicyConfigList struct { 135 | metav1.TypeMeta `json:",inline"` 136 | metav1.ListMeta `json:"metadata,omitempty"` 137 | Items []PolicyConfig `json:"items"` 138 | } 139 | 140 | func init() { 141 | SchemeBuilder.Register( 142 | &PolicyConfig{}, 143 | &PolicyConfigList{}, 144 | ) 145 | } 146 | -------------------------------------------------------------------------------- /policies/ControllerContainerRunningAsRoot.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: pac.weave.works/v2beta3 2 | kind: Policy 3 | metadata: 4 | name: weave.policies.container-running-as-root 5 | spec: 6 | id: weave.policies.container-running-as-root 7 | name: Container Running As Root 8 | enforce: true 9 | description: "Running as root gives the container full access to all resources in the VM it is running on. Containers should not run with such access rights unless required by design. This Policy enforces that the `securityContext.runAsNonRoot` attribute is set to `true`. \n" 10 | how_to_solve: "You should set `securityContext.runAsNonRoot` to `true`. Not setting it will default to giving the container root user rights on the VM that it is running on. \n```\n...\n spec:\n securityContext:\n runAsNonRoot: true\n```\nhttps://kubernetes.io/docs/tasks/configure-pod-container/security-context/\n" 11 | category: weave.categories.pod-security 12 | severity: high 13 | targets: {kinds: [Deployment, Job, ReplicationController, ReplicaSet, DaemonSet, StatefulSet, CronJob]} 14 | standards: 15 | - id: weave.standards.pci-dss 16 | controls: 17 | - weave.controls.pci-dss.2.2.4 18 | - weave.controls.pci-dss.2.2.5 19 | - id: weave.standards.cis-benchmark 20 | controls: 21 | - weave.controls.cis-benchmark.5.2.6 22 | - id: weave.standards.mitre-attack 23 | controls: 24 | - weave.controls.mitre-attack.4.1 25 | - id: weave.standards.nist-800-190 26 | controls: 27 | - weave.controls.nist-800-190.3.3.1 28 | - id: weave.standards.gdpr 29 | controls: 30 | - weave.controls.gdpr.24 31 | - weave.controls.gdpr.25 32 | - weave.controls.gdpr.32 33 | tags: [pci-dss, cis-benchmark, mitre-attack, nist800-190, gdpr, default] 34 | exclude: 35 | namespaces: 36 | - kube-system 37 | code: | 38 | package weave.advisor.podSecurity.runningAsRoot 39 | 40 | import future.keywords.in 41 | 42 | # Check for missing securityContext.runAsNonRoot (missing in both, pod and container) 43 | violation[result] { 44 | 45 | controller_spec.securityContext 46 | not controller_spec.securityContext.runAsNonRoot 47 | not controller_spec.securityContext.runAsNonRoot == false 48 | 49 | some i 50 | containers := controller_spec.containers[i] 51 | containers.securityContext 52 | not containers.securityContext.runAsNonRoot 53 | not containers.securityContext.runAsNonRoot == false 54 | 55 | result = { 56 | "issue detected": true, 57 | "msg": sprintf("Container missing spec.template.spec.containers[%v].securityContext.runAsNonRoot while Pod spec.template.spec.securityContext.runAsNonRoot is not defined as well.", [i]), 58 | "violating_key": sprintf("spec.template.spec.containers[%v].securityContext", [i]), 59 | } 60 | } 61 | 62 | # Container security context 63 | # Check if containers.securityContext.runAsNonRoot exists and = false 64 | violation[result] { 65 | 66 | some i 67 | containers := controller_spec.containers[i] 68 | containers.securityContext 69 | containers.securityContext.runAsNonRoot == false 70 | 71 | result = { 72 | "issue detected": true, 73 | "msg": sprintf("Container spec.template.spec.containers[%v].securityContext.runAsNonRoot should be set to true ", [i]), 74 | "violating_key": sprintf("spec.template.spec.containers[%v].securityContext.runAsNonRoot", [i]), 75 | "recommended_value": true, 76 | } 77 | } 78 | 79 | # Pod security context 80 | # Check if spec.securityContext.runAsNonRoot exists and = false 81 | violation[result] { 82 | 83 | controller_spec.securityContext 84 | controller_spec.securityContext.runAsNonRoot == false 85 | 86 | result = { 87 | "issue detected": true, 88 | "msg": "Pod spec.template.spec.securityContext.runAsNonRoot should be set to true", 89 | "violating_key": "spec.template.spec.securityContext.runAsNonRoot", 90 | "recommended_value": true, 91 | } 92 | } 93 | 94 | controller_input = input.review.object 95 | 96 | controller_spec = controller_input.spec.template.spec { 97 | contains(controller_input.kind, {"StatefulSet", "DaemonSet", "Deployment", "Job", "ReplicaSet"}) 98 | } else = controller_input.spec { 99 | controller_input.kind == "Pod" 100 | } else = controller_input.spec.jobTemplate.spec.template.spec { 101 | controller_input.kind == "CronJob" 102 | } 103 | 104 | contains(kind, kinds) { 105 | kinds[_] = kind 106 | } 107 | -------------------------------------------------------------------------------- /api/v1/policy_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1 18 | 19 | import ( 20 | apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 21 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 22 | ) 23 | 24 | const ( 25 | ResourceName = "policies" 26 | Kind = "Policy" 27 | ) 28 | 29 | var GroupVersionResource = GroupVersion.WithResource(ResourceName) 30 | 31 | // PolicyParameters defines a needed input in a policy 32 | type PolicyParameters struct { 33 | // Name is a descriptive name of a policy parameter 34 | Name string `json:"name"` 35 | // Type is the type of that parameter, integer, string,... 36 | Type string `json:"type"` 37 | // Required specifies if this is a necessary value or not 38 | Required bool `json:"required"` 39 | // +optional 40 | // Value is the value for that parameter 41 | Value *apiextensionsv1.JSON `json:"value,omitempty"` 42 | } 43 | 44 | // PolicyTargets are filters used to determine which resources should be evaluated against a policy 45 | type PolicyTargets struct { 46 | // Kinds is a list of Kubernetes kinds that are supported by this policy 47 | Kinds []string `json:"kinds"` 48 | // +optional 49 | // Labels is a list of Kubernetes labels that are needed to evaluate the policy against a resource 50 | // this filter is statisfied if only one label existed, using * for value make it so it will match if the key exists regardless of its value 51 | Labels []map[string]string `json:"labels"` 52 | // +optional 53 | // Namespaces is a list of Kubernetes namespaces that a resource needs to be a part of to evaluate against this policy 54 | Namespaces []string `json:"namespaces"` 55 | } 56 | 57 | // PolicySpec defines the desired state of Policy 58 | // It describes all that is needed to evaluate a resource against a rego code 59 | // +kubebuilder:object:generate:true 60 | type PolicySpec struct { 61 | // Name is the policy name 62 | Name string `json:"name"` 63 | // ID is the policy unique identifier 64 | ID string `json:"id"` 65 | // Code contains the policy rego code 66 | Code string `json:"code"` 67 | // +optional 68 | // Enable specifies if this policy should be used for evaluation or not 69 | Enable string `json:"enable,omitempty"` 70 | // +optional 71 | // Parameters are the inputs needed for the policy validation 72 | Parameters []PolicyParameters `json:"parameters,omitempty"` 73 | // +optional 74 | // Targets describes the required metadata that needs to be matched to evaluate a resource against the policy 75 | // all values specified need to exist in the resource to be considered for evaluation 76 | Targets PolicyTargets `json:"targets,omitempty"` 77 | // Description is a summary of what that policy validates 78 | Description string `json:"description"` 79 | // HowToSolve is a description of the steps required to solve the issues reported by the policy 80 | HowToSolve string `json:"how_to_solve"` 81 | // Category specifies under which grouping this policy should be included 82 | Category string `json:"category"` 83 | // +optional 84 | // Tags is a list of tags associated with that policy 85 | Tags []string `json:"tags,omitempty"` 86 | // +kubebuilder:validation:Enum=low;medium;high 87 | // Severity is a measure of the impact of that policy, can be low, medium or high 88 | Severity string `json:"severity"` 89 | // +optional 90 | // Controls is a list of policy controls that this policy falls under 91 | Controls []string `json:"controls,omitempty"` 92 | } 93 | 94 | //+kubebuilder:unservedversion 95 | //+kubebuilder:object:root=true 96 | //+kubebuilder:resource:scope=Cluster 97 | 98 | // Policy is the Schema for the policies API 99 | type Policy struct { 100 | metav1.TypeMeta `json:",inline"` 101 | metav1.ObjectMeta `json:"metadata,omitempty"` 102 | 103 | Spec PolicySpec `json:"spec,omitempty"` 104 | } 105 | 106 | // +kubebuilder:unservedversion 107 | // +kubebuilder:object:root=true 108 | // +kubebuilder:resource:scope=Cluster 109 | 110 | // PolicyList contains a list of Policy 111 | type PolicyList struct { 112 | metav1.TypeMeta `json:",inline"` 113 | metav1.ListMeta `json:"metadata,omitempty"` 114 | Items []Policy `json:"items"` 115 | } 116 | 117 | func init() { 118 | SchemeBuilder.Register(&Policy{}, &PolicyList{}) 119 | } 120 | -------------------------------------------------------------------------------- /internal/policies/policy_test.go: -------------------------------------------------------------------------------- 1 | package crd 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | pacv2 "github.com/weaveworks/policy-agent/api/v2beta3" 10 | corev1 "k8s.io/api/core/v1" 11 | apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 12 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 13 | "k8s.io/apimachinery/pkg/runtime" 14 | ) 15 | 16 | func TestGetPolicies(t *testing.T) { 17 | policies := []pacv2.Policy{ 18 | { 19 | TypeMeta: v1.TypeMeta{ 20 | APIVersion: pacv2.GroupVersion.Identifier(), 21 | Kind: pacv2.PolicyKind, 22 | }, 23 | ObjectMeta: v1.ObjectMeta{ 24 | Name: "policy-1", 25 | }, 26 | Spec: pacv2.PolicySpec{ 27 | ID: "policy-1", 28 | Provider: "kubernetes", 29 | Category: "category-x", 30 | Severity: "severity-x", 31 | Standards: []pacv2.PolicyStandard{ 32 | {ID: "standard-x"}, 33 | }, 34 | Tags: []string{"tag-x"}, 35 | Parameters: []pacv2.PolicyParameters{ 36 | { 37 | Name: "x", 38 | Type: "string", 39 | Value: &apiextensionsv1.JSON{Raw: []byte(`"test"`)}, 40 | Required: true, 41 | }, 42 | }, 43 | }, 44 | }, 45 | { 46 | TypeMeta: v1.TypeMeta{ 47 | APIVersion: pacv2.GroupVersion.Identifier(), 48 | Kind: pacv2.PolicyKind, 49 | }, 50 | ObjectMeta: v1.ObjectMeta{ 51 | Name: "policy-2", 52 | }, 53 | Spec: pacv2.PolicySpec{ 54 | ID: "policy-2", 55 | Provider: "kubernetes", 56 | Category: "category-y", 57 | Severity: "severity-y", 58 | Standards: []pacv2.PolicyStandard{ 59 | {ID: "standard-y"}, 60 | }, 61 | Tags: []string{"tag-y"}, 62 | }, 63 | }, 64 | { 65 | TypeMeta: v1.TypeMeta{ 66 | APIVersion: pacv2.GroupVersion.Identifier(), 67 | Kind: pacv2.PolicyKind, 68 | }, 69 | ObjectMeta: v1.ObjectMeta{ 70 | Name: "policy-3", 71 | }, 72 | Spec: pacv2.PolicySpec{ 73 | ID: "policy-3", 74 | Provider: "kubernetes", 75 | Category: "category-z", 76 | Severity: "severity-z", 77 | Standards: []pacv2.PolicyStandard{ 78 | {ID: "standard-z"}, 79 | }, 80 | Tags: []string{"tag-z", "tenancy"}, 81 | }, 82 | }, 83 | { 84 | TypeMeta: v1.TypeMeta{ 85 | APIVersion: pacv2.GroupVersion.Identifier(), 86 | Kind: pacv2.PolicyKind, 87 | }, 88 | ObjectMeta: v1.ObjectMeta{ 89 | Name: "policy-4", 90 | }, 91 | Spec: pacv2.PolicySpec{ 92 | ID: "policy-4", 93 | Provider: "terraform", 94 | Category: "category-x", 95 | Severity: "severity-x", 96 | Standards: []pacv2.PolicyStandard{ 97 | {ID: "standard-x"}, 98 | }, 99 | Tags: []string{"tag-x"}, 100 | }, 101 | }, 102 | { 103 | TypeMeta: v1.TypeMeta{ 104 | APIVersion: pacv2.GroupVersion.Identifier(), 105 | Kind: pacv2.PolicyKind, 106 | }, 107 | ObjectMeta: v1.ObjectMeta{ 108 | Name: "policy-5", 109 | }, 110 | Spec: pacv2.PolicySpec{ 111 | ID: "policy-5", 112 | Provider: "terraform", 113 | Category: "category-y", 114 | Severity: "severity-y", 115 | Standards: []pacv2.PolicyStandard{ 116 | {ID: "standard-y"}, 117 | }, 118 | Tags: []string{"tag-y"}, 119 | }, 120 | }, 121 | } 122 | 123 | cases := []struct { 124 | description string 125 | policies []pacv2.Policy 126 | provider string 127 | expectedPolicies []string 128 | }{ 129 | { 130 | policies: policies, 131 | provider: pacv2.PolicyKubernetesProvider, 132 | expectedPolicies: []string{"policy-1", "policy-2", "policy-3"}, 133 | }, 134 | { 135 | policies: policies, 136 | provider: pacv2.PolicyKubernetesProvider, 137 | expectedPolicies: []string{"policy-1", "policy-2", "policy-3"}, 138 | }, 139 | { 140 | policies: policies, 141 | provider: pacv2.PolicyTerraformProvider, 142 | expectedPolicies: []string{"policy-4", "policy-5"}, 143 | }, 144 | } 145 | 146 | for i := range cases { 147 | schema := runtime.NewScheme() 148 | pacv2.AddToScheme(schema) 149 | corev1.AddToScheme(schema) 150 | 151 | var items []runtime.Object 152 | for idx := range cases[i].policies { 153 | item := cases[i].policies[idx] 154 | items = append(items, &item) 155 | } 156 | 157 | cache := NewFakeCache(schema, items...) 158 | 159 | watcher := PoliciesWatcher{ 160 | cache: cache, 161 | Provider: cases[i].provider, 162 | } 163 | 164 | policies, err := watcher.GetAll(context.Background()) 165 | if err != nil { 166 | t.Error(err) 167 | } 168 | 169 | var ids []string 170 | for _, policy := range policies { 171 | ids = append(ids, policy.ID) 172 | } 173 | 174 | assert.Equal(t, ids, cases[i].expectedPolicies, fmt.Sprintf("testcase: #%d", i)) 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /internal/policies/policyconfig.go: -------------------------------------------------------------------------------- 1 | package crd 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | 8 | pacv2 "github.com/weaveworks/policy-agent/api/v2beta3" 9 | "github.com/weaveworks/policy-agent/internal/utils" 10 | "github.com/weaveworks/policy-agent/pkg/policy-core/domain" 11 | v1 "k8s.io/api/core/v1" 12 | apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 13 | "sigs.k8s.io/controller-runtime/pkg/client" 14 | ) 15 | 16 | const ( 17 | tenantLabel = "toolkit.fluxcd.io/tenant" 18 | ) 19 | 20 | func (p *PoliciesWatcher) GetPolicyConfig(ctx context.Context, entity domain.Entity) (*domain.PolicyConfig, error) { 21 | configs := pacv2.PolicyConfigList{} 22 | err := p.cache.List(ctx, &configs) 23 | if err != nil { 24 | return nil, err 25 | } 26 | 27 | var workspaces, namespaces, apps, appsWithNamespace, resources, resourcesWithNamespace []pacv2.PolicyConfig 28 | 29 | var entityWorkspace string 30 | 31 | if entity.Namespace != "" { 32 | var ns v1.Namespace 33 | if err := p.cache.Get(ctx, client.ObjectKey{Name: entity.Namespace}, &ns); err != nil { 34 | return nil, fmt.Errorf("failed to get entity namespace: %w", err) 35 | } 36 | entityWorkspace = ns.GetLabels()[tenantLabel] 37 | } 38 | 39 | for _, config := range configs.Items { 40 | if entityWorkspace != "" { 41 | for _, workspace := range config.Spec.Match.Workspaces { 42 | if workspace == entityWorkspace { 43 | workspaces = append(workspaces, config) 44 | break 45 | } 46 | } 47 | } 48 | 49 | for _, namespace := range config.Spec.Match.Namespaces { 50 | if namespace == entity.Namespace { 51 | namespaces = append(namespaces, config) 52 | break 53 | } 54 | } 55 | 56 | if fluxApp := utils.GetFluxObject(entity.Labels); fluxApp != nil { 57 | for _, app := range config.Spec.Match.Applications { 58 | if app.Name == fluxApp.GetName() && app.Kind == fluxApp.GetKind() { 59 | if app.Namespace == "" { 60 | apps = append(apps, config) 61 | break 62 | } else if app.Namespace == fluxApp.GetNamespace() { 63 | appsWithNamespace = append(appsWithNamespace, config) 64 | break 65 | } 66 | } 67 | } 68 | } 69 | 70 | for _, resource := range config.Spec.Match.Resources { 71 | if resource.Name == entity.Name && resource.Kind == entity.Kind { 72 | if resource.Namespace != "" { 73 | if resource.Namespace == entity.Namespace { 74 | resourcesWithNamespace = append(resourcesWithNamespace, config) 75 | } 76 | } else { 77 | resources = append(resources, config) 78 | } 79 | break 80 | } 81 | } 82 | } 83 | 84 | allConfigs := []pacv2.PolicyConfig{} 85 | allConfigs = append(allConfigs, workspaces...) 86 | allConfigs = append(allConfigs, namespaces...) 87 | allConfigs = append(allConfigs, apps...) 88 | allConfigs = append(allConfigs, appsWithNamespace...) 89 | allConfigs = append(allConfigs, resources...) 90 | allConfigs = append(allConfigs, resourcesWithNamespace...) 91 | 92 | return override(allConfigs) 93 | } 94 | 95 | func override(configs []pacv2.PolicyConfig) (*domain.PolicyConfig, error) { 96 | configCRD := pacv2.PolicyConfig{ 97 | Spec: pacv2.PolicyConfigSpec{ 98 | Config: make(map[string]pacv2.PolicyConfigConfig), 99 | }, 100 | } 101 | 102 | // store last policy config used to bind parameters 103 | confHistory := map[string]map[string]string{} 104 | for _, config := range configs { 105 | for policyID, policyConfig := range config.Spec.Config { 106 | // if no policy config exists, initialize a new one 107 | if _, ok := configCRD.Spec.Config[policyID]; !ok { 108 | configCRD.Spec.Config[policyID] = pacv2.PolicyConfigConfig{ 109 | Parameters: make(map[string]apiextensionsv1.JSON), 110 | } 111 | } 112 | for k, v := range policyConfig.Parameters { 113 | // override policy parameter value 114 | configCRD.Spec.Config[policyID].Parameters[k] = v 115 | if _, ok := confHistory[policyID]; !ok { 116 | confHistory[policyID] = make(map[string]string) 117 | } 118 | // store policy config name to config hisotry 119 | confHistory[policyID][k] = config.GetName() 120 | } 121 | } 122 | } 123 | 124 | config := domain.PolicyConfig{ 125 | Config: make(map[string]domain.PolicyConfigConfig), 126 | } 127 | for policyID, policyConfig := range configCRD.Spec.Config { 128 | config.Config[policyID] = domain.PolicyConfigConfig{ 129 | Parameters: make(map[string]domain.PolicyConfigParameter), 130 | } 131 | for k, v := range policyConfig.Parameters { 132 | var value interface{} 133 | err := json.Unmarshal(v.Raw, &value) 134 | if err != nil { 135 | return nil, err 136 | } 137 | config.Config[policyID].Parameters[k] = domain.PolicyConfigParameter{ 138 | Value: value, 139 | ConfigRef: confHistory[policyID][k], 140 | } 141 | } 142 | } 143 | 144 | return &config, nil 145 | } 146 | -------------------------------------------------------------------------------- /helm/crds/pac.weave.works_policysets.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | controller-gen.kubebuilder.io/version: v0.8.0 7 | creationTimestamp: null 8 | name: policysets.pac.weave.works 9 | spec: 10 | group: pac.weave.works 11 | names: 12 | kind: PolicySet 13 | listKind: PolicySetList 14 | plural: policysets 15 | singular: policyset 16 | scope: Cluster 17 | versions: 18 | - name: v2beta1 19 | schema: 20 | openAPIV3Schema: 21 | description: PolicySet is the Schema for the policysets API 22 | properties: 23 | apiVersion: 24 | description: 'APIVersion defines the versioned schema of this representation 25 | of an object. Servers should convert recognized schemas to the latest 26 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 27 | type: string 28 | kind: 29 | description: 'Kind is a string value representing the REST resource this 30 | object represents. Servers may infer this from the endpoint the client 31 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 32 | type: string 33 | metadata: 34 | type: object 35 | spec: 36 | properties: 37 | filters: 38 | properties: 39 | categories: 40 | items: 41 | type: string 42 | type: array 43 | ids: 44 | items: 45 | type: string 46 | type: array 47 | severities: 48 | items: 49 | type: string 50 | type: array 51 | standards: 52 | items: 53 | type: string 54 | type: array 55 | tags: 56 | items: 57 | type: string 58 | type: array 59 | type: object 60 | id: 61 | type: string 62 | name: 63 | type: string 64 | required: 65 | - filters 66 | - id 67 | - name 68 | type: object 69 | type: object 70 | served: false 71 | storage: false 72 | - additionalPrinterColumns: 73 | - jsonPath: .spec.mode 74 | name: Mode 75 | type: string 76 | name: v2beta2 77 | schema: 78 | openAPIV3Schema: 79 | description: PolicySet is the Schema for the policysets API 80 | properties: 81 | apiVersion: 82 | description: 'APIVersion defines the versioned schema of this representation 83 | of an object. Servers should convert recognized schemas to the latest 84 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 85 | type: string 86 | kind: 87 | description: 'Kind is a string value representing the REST resource this 88 | object represents. Servers may infer this from the endpoint the client 89 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 90 | type: string 91 | metadata: 92 | type: object 93 | spec: 94 | properties: 95 | filters: 96 | properties: 97 | categories: 98 | items: 99 | type: string 100 | type: array 101 | ids: 102 | items: 103 | type: string 104 | type: array 105 | severities: 106 | items: 107 | type: string 108 | type: array 109 | standards: 110 | items: 111 | type: string 112 | type: array 113 | tags: 114 | items: 115 | type: string 116 | type: array 117 | type: object 118 | mode: 119 | description: Mode is the policy set mode, must be one of audit,admission,tf-admission 120 | enum: 121 | - audit 122 | - admission 123 | - tf-admission 124 | type: string 125 | name: 126 | type: string 127 | required: 128 | - filters 129 | - mode 130 | type: object 131 | type: object 132 | served: true 133 | storage: true 134 | subresources: {} 135 | status: 136 | acceptedNames: 137 | kind: "" 138 | plural: "" 139 | conditions: [] 140 | storedVersions: [] 141 | -------------------------------------------------------------------------------- /config/crd/bases/pac.weave.works_policysets.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | controller-gen.kubebuilder.io/version: v0.8.0 7 | creationTimestamp: null 8 | name: policysets.pac.weave.works 9 | spec: 10 | group: pac.weave.works 11 | names: 12 | kind: PolicySet 13 | listKind: PolicySetList 14 | plural: policysets 15 | singular: policyset 16 | scope: Cluster 17 | versions: 18 | - name: v2beta1 19 | schema: 20 | openAPIV3Schema: 21 | description: PolicySet is the Schema for the policysets API 22 | properties: 23 | apiVersion: 24 | description: 'APIVersion defines the versioned schema of this representation 25 | of an object. Servers should convert recognized schemas to the latest 26 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 27 | type: string 28 | kind: 29 | description: 'Kind is a string value representing the REST resource this 30 | object represents. Servers may infer this from the endpoint the client 31 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 32 | type: string 33 | metadata: 34 | type: object 35 | spec: 36 | properties: 37 | filters: 38 | properties: 39 | categories: 40 | items: 41 | type: string 42 | type: array 43 | ids: 44 | items: 45 | type: string 46 | type: array 47 | severities: 48 | items: 49 | type: string 50 | type: array 51 | standards: 52 | items: 53 | type: string 54 | type: array 55 | tags: 56 | items: 57 | type: string 58 | type: array 59 | type: object 60 | id: 61 | type: string 62 | name: 63 | type: string 64 | required: 65 | - filters 66 | - id 67 | - name 68 | type: object 69 | type: object 70 | served: false 71 | storage: false 72 | - additionalPrinterColumns: 73 | - jsonPath: .spec.mode 74 | name: Mode 75 | type: string 76 | name: v2beta2 77 | schema: 78 | openAPIV3Schema: 79 | description: PolicySet is the Schema for the policysets API 80 | properties: 81 | apiVersion: 82 | description: 'APIVersion defines the versioned schema of this representation 83 | of an object. Servers should convert recognized schemas to the latest 84 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 85 | type: string 86 | kind: 87 | description: 'Kind is a string value representing the REST resource this 88 | object represents. Servers may infer this from the endpoint the client 89 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 90 | type: string 91 | metadata: 92 | type: object 93 | spec: 94 | properties: 95 | filters: 96 | properties: 97 | categories: 98 | items: 99 | type: string 100 | type: array 101 | ids: 102 | items: 103 | type: string 104 | type: array 105 | severities: 106 | items: 107 | type: string 108 | type: array 109 | standards: 110 | items: 111 | type: string 112 | type: array 113 | tags: 114 | items: 115 | type: string 116 | type: array 117 | type: object 118 | mode: 119 | description: Mode is the policy set mode, must be one of audit,admission,tf-admission 120 | enum: 121 | - audit 122 | - admission 123 | - tf-admission 124 | type: string 125 | name: 126 | type: string 127 | required: 128 | - filters 129 | - mode 130 | type: object 131 | type: object 132 | served: true 133 | storage: true 134 | subresources: {} 135 | status: 136 | acceptedNames: 137 | kind: "" 138 | plural: "" 139 | conditions: [] 140 | storedVersions: [] 141 | --------------------------------------------------------------------------------