├── 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 | 
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 | [](https://codecov.io/gh/weaveworks/policy-agent)  [](https://github.com/weaveworks/policy-agent/graphs/contributors)
2 | [](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 |
--------------------------------------------------------------------------------