From 35614d6448c48c01ca36f5e2e4253c23e7097c04 Mon Sep 17 00:00:00 2001 From: Reece Bedding Date: Tue, 19 May 2026 13:47:50 +0100 Subject: [PATCH 01/10] feat: upgrade to plugin v2 protocol --- .github/workflows/build-and-upload.yml | 4 +- .gitignore | 3 + Makefile | 37 ++ go.mod | 112 +++--- go.sum | 516 ++++++++++++++----------- main.go | 13 +- 6 files changed, 394 insertions(+), 291 deletions(-) create mode 100644 Makefile diff --git a/.github/workflows/build-and-upload.yml b/.github/workflows/build-and-upload.yml index 25b8b35..3560bfd 100644 --- a/.github/workflows/build-and-upload.yml +++ b/.github/workflows/build-and-upload.yml @@ -22,7 +22,7 @@ jobs: - name: Authenticate gooci cli run: gooci login ghcr.io --username ${{ github.actor }} --password ${{ secrets.GITHUB_TOKEN }} - name: gooci Upload Version - run: gooci upload dist/ ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}:${{github.ref_name}} + run: gooci upload dist/ ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}:${{github.ref_name}} --annotate="org.ccf.plugin.protocol.version=2" - name: gooci Upload Latest if: "!github.event.release.prerelease" - run: gooci upload dist/ ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}:latest + run: gooci upload dist/ ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}:latest --annotate="org.ccf.plugin.protocol.version=2" diff --git a/.gitignore b/.gitignore index d6f94c4..03edf09 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ dist/ .DS_Store # TODO: Change this to match the specific plugin name /plugin-* + +.ai/* +.config/ \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..fe7b156 --- /dev/null +++ b/Makefile @@ -0,0 +1,37 @@ +# The help target prints out all targets with their descriptions organized +# beneath their categories. The categories are represented by '##@' and the +# target descriptions by '##'. The awk commands is responsible for reading the +# entire set of makefiles included in this invocation, looking for lines of the +# file as xyz: ## something, and then pretty-format the target and help. Then, +# if there's a line with ##@ something, that gets pretty-printed as a category. +# More info on the usage of ANSI catalog characters for terminal formatting: +# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters +# More info on the awk command: +# http://linuxcommand.org/lc3_adv_awk.php + +# Check if OPA CLI is installed +OPA := $(shell command -v opa 2> /dev/null) +ifeq ($(OPA),) +$(error "opa CLI not found. Please install it: https://www.openpolicyagent.org/docs/latest/cli/") +endif + +##@ Help +help: ## Display this concise help, ie only the porcelain target + @awk 'BEGIN {FS = ":.*##"; printf "\033[1mUsage\033[0m\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-30s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) + +help-all: ## Display all help items, ie including plumbing targets + @awk 'BEGIN {FS = ":.*#"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?#/ { printf " \033[36m%-25s\033[0m %s\n", $$1, $$2 } /^#@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) + + +test: + @go test ./... + +clean: # Cleanup build artifacts + @rm -rf dist/* + +build: clean ## Build the plugin package + @mkdir -p dist/ + @go build -o dist/plugin main.go + +run: build ## Execute the Concom agent with the built plugin + @../agent/dist/./concom agent --config ./.config/config.yaml \ No newline at end of file diff --git a/go.mod b/go.mod index f25f670..5a60162 100644 --- a/go.mod +++ b/go.mod @@ -1,71 +1,77 @@ module github.com/compliance-framework/plugin-aws-networking-security -go 1.23.2 +go 1.26.1 require ( - github.com/aws/aws-sdk-go-v2 v1.36.3 - github.com/aws/aws-sdk-go-v2/config v1.29.9 + github.com/aws/aws-sdk-go-v2 v1.41.2 + github.com/aws/aws-sdk-go-v2/config v1.32.10 github.com/aws/aws-sdk-go-v2/service/ec2 v1.208.0 - github.com/compliance-framework/agent v0.2.1 - github.com/hashicorp/go-hclog v1.5.0 - github.com/hashicorp/go-plugin v1.6.2 + github.com/compliance-framework/agent v0.6.2 + github.com/hashicorp/go-hclog v1.6.3 + github.com/hashicorp/go-plugin v1.7.0 ) require ( - github.com/OneOfOne/xxhash v1.2.8 // indirect - github.com/agnivade/levenshtein v1.2.0 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.17.62 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.25.1 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.1 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.33.17 // indirect - github.com/aws/smithy-go v1.22.2 // indirect - github.com/beorn7/perks v1.0.1 // indirect + github.com/agnivade/levenshtein v1.2.1 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.19.10 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.0.6 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 // indirect + github.com/aws/smithy-go v1.24.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/compliance-framework/api v0.4.0 // indirect - github.com/defenseunicorns/go-oscal v0.6.2 // indirect - github.com/fatih/color v1.15.0 // indirect - github.com/go-ini/ini v1.67.0 // indirect - github.com/go-logr/logr v1.4.2 // indirect - github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-viper/mapstructure/v2 v2.3.0 // indirect + github.com/compliance-framework/api v0.16.0 // indirect + github.com/containerd/errdefs/pkg v0.3.0 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 // indirect + github.com/defenseunicorns/go-oscal v0.7.0 // indirect + github.com/docker/docker v28.5.2+incompatible // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/gobwas/glob v0.2.3 // indirect + github.com/goccy/go-json v0.10.5 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/gorilla/mux v1.8.1 // indirect - github.com/hashicorp/yamux v0.1.1 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect + github.com/hashicorp/yamux v0.1.2 // indirect + github.com/lestrrat-go/blackmagic v1.0.4 // indirect + github.com/lestrrat-go/dsig v1.0.0 // indirect + github.com/lestrrat-go/dsig-secp256k1 v1.0.0 // indirect + github.com/lestrrat-go/httpcc v1.0.1 // indirect + github.com/lestrrat-go/httprc/v3 v3.0.4 // indirect + github.com/lestrrat-go/jwx/v3 v3.0.13 // indirect + github.com/lestrrat-go/option/v2 v2.0.0 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/moby/sys/user v0.3.0 // indirect - github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/oklog/run v1.0.0 // indirect - github.com/open-policy-agent/opa v1.0.0 // indirect - github.com/prometheus/client_golang v1.20.5 // indirect - github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.57.0 // indirect - github.com/prometheus/procfs v0.15.1 // indirect - github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect - github.com/sirupsen/logrus v1.9.3 // indirect - github.com/tchap/go-patricia/v2 v2.3.1 // indirect + github.com/oklog/run v1.2.0 // indirect + github.com/open-policy-agent/opa v1.14.1 // indirect + github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 // indirect + github.com/segmentio/asm v1.2.1 // indirect + github.com/sirupsen/logrus v1.9.4 // indirect + github.com/tchap/go-patricia/v2 v2.3.3 // indirect + github.com/valyala/fastjson v1.6.10 // indirect + github.com/vektah/gqlparser/v2 v2.5.32 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/yashtewari/glob-intersection v0.2.0 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/otel v1.35.0 // indirect - go.opentelemetry.io/otel/metric v1.35.0 // indirect - go.opentelemetry.io/otel/sdk v1.33.0 // indirect - go.opentelemetry.io/otel/trace v1.35.0 // indirect - golang.org/x/net v0.38.0 // indirect - golang.org/x/sys v0.33.0 // indirect - golang.org/x/text v0.24.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 // indirect - google.golang.org/grpc v1.69.2 // indirect - google.golang.org/protobuf v1.36.1 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect - sigs.k8s.io/yaml v1.4.0 // indirect + go.opentelemetry.io/otel/metric v1.42.0 // indirect + go.opentelemetry.io/otel/trace v1.42.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.1 // indirect + go.yaml.in/yaml/v2 v2.4.4 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/crypto v0.49.0 // indirect + golang.org/x/net v0.52.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.42.0 // indirect + golang.org/x/text v0.35.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect + google.golang.org/grpc v1.79.3 // indirect + google.golang.org/protobuf v1.36.11 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/go.sum b/go.sum index 60dddb8..d46a426 100644 --- a/go.sum +++ b/go.sum @@ -1,89 +1,97 @@ dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= -filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= -filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +filippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw= +filippo.io/edwards25519 v1.1.1/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8= -github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q= -github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= -github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= -github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= -github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= -github.com/agnivade/levenshtein v1.2.0 h1:U9L4IOT0Y3i0TIlUIDJ7rVUziKi/zPbrJGaFrtYH3SY= -github.com/agnivade/levenshtein v1.2.0/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU= +github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM= +github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= -github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM= -github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg= -github.com/aws/aws-sdk-go-v2/config v1.29.9 h1:Kg+fAYNaJeGXp1vmjtidss8O2uXIsXwaRqsQJKXVr+0= -github.com/aws/aws-sdk-go-v2/config v1.29.9/go.mod h1:oU3jj2O53kgOU4TXq/yipt6ryiooYjlkqqVaZk7gY/U= -github.com/aws/aws-sdk-go-v2/credentials v1.17.62 h1:fvtQY3zFzYJ9CfixuAQ96IxDrBajbBWGqjNTCa79ocU= -github.com/aws/aws-sdk-go-v2/credentials v1.17.62/go.mod h1:ElETBxIQqcxej++Cs8GyPBbgMys5DgQPTwo7cUPDKt8= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= +github.com/aws/aws-sdk-go-v2 v1.41.2 h1:LuT2rzqNQsauaGkPK/7813XxcZ3o3yePY0Iy891T2ls= +github.com/aws/aws-sdk-go-v2 v1.41.2/go.mod h1:IvvlAZQXvTXznUPfRVfryiG1fbzE2NGK6m9u39YQ+S4= +github.com/aws/aws-sdk-go-v2/config v1.32.10 h1:9DMthfO6XWZYLfzZglAgW5Fyou2nRI5CuV44sTedKBI= +github.com/aws/aws-sdk-go-v2/config v1.32.10/go.mod h1:2rUIOnA2JaiqYmSKYmRJlcMWy6qTj1vuRFscppSBMcw= +github.com/aws/aws-sdk-go-v2/credentials v1.19.10 h1:EEhmEUFCE1Yhl7vDhNOI5OCL/iKMdkkYFTRpZXNw7m8= +github.com/aws/aws-sdk-go-v2/credentials v1.19.10/go.mod h1:RnnlFCAlxQCkN2Q379B67USkBMu1PipEEiibzYN5UTE= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 h1:Ii4s+Sq3yDfaMLpjrJsqD6SmG/Wq/P5L/hw2qa78UAY= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18/go.mod h1:6x81qnY++ovptLE6nWQeWrpXxbnlIex+4H4eYYGcqfc= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 h1:F43zk1vemYIqPAwhjTjYIz0irU2EY7sOb/F5eJ3HuyM= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18/go.mod h1:w1jdlZXrGKaJcNoL+Nnrj+k5wlpGXqnNrKoP22HvAug= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 h1:xCeWVjj0ki0l3nruoyP2slHsGArMxeiiaoPN5QZH6YQ= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18/go.mod h1:r/eLGuGCBw6l36ZRWiw6PaZwPXb6YOj+i/7MizNl5/k= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16 h1:CjMzUs78RDDv4ROu3JnJn/Ig1r6ZD7/T2DXLLRpejic= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16/go.mod h1:uVW4OLBqbJXSHJYA9svT9BluSvvwbzLQ2Crf6UPzR3c= github.com/aws/aws-sdk-go-v2/service/ec2 v1.208.0 h1:qzT4wyLo7ssa4QU8Xcf+h+iyCF4WTeQtM8fjr+UUKyI= github.com/aws/aws-sdk-go-v2/service/ec2 v1.208.0/go.mod h1:ouvGEfHbLaIlWwpDpOVWPWR+YwO0HDv3vm5tYLq8ImY= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY= -github.com/aws/aws-sdk-go-v2/service/sso v1.25.1 h1:8JdC7Gr9NROg1Rusk25IcZeTO59zLxsKgE0gkh5O6h0= -github.com/aws/aws-sdk-go-v2/service/sso v1.25.1/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.1 h1:KwuLovgQPcdjNMfFt9OhUd9a2OwcOKhxfvF4glTzLuA= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.1/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs= -github.com/aws/aws-sdk-go-v2/service/sts v1.33.17 h1:PZV5W8yk4OtH1JAuhV2PXwwO9v5G5Aoj+eMCn4T+1Kc= -github.com/aws/aws-sdk-go-v2/service/sts v1.33.17/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4= -github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ= -github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 h1:CeY9LUdur+Dxoeldqoun6y4WtJ3RQtzk0JMP2gfUay0= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5/go.mod h1:AZLZf2fMaahW5s/wMRciu1sYbdsikT/UHwbUjOdEVTc= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 h1:LTRCYFlnnKFlKsyIQxKhJuDuA3ZkrDQMRYm6rXiHlLY= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18/go.mod h1:XhwkgGG6bHSd00nO/mexWTcTjgd6PjuvWQMqSn2UaEk= +github.com/aws/aws-sdk-go-v2/service/sesv2 v1.59.0 h1:HQYog9wJM8D9aF0bOVzzWbjpWZ7exyjc3rLb7P8Qb8E= +github.com/aws/aws-sdk-go-v2/service/sesv2 v1.59.0/go.mod h1:p0iz0in3/mt3aS2Ovk3aKeOq5vwM/V3prQG9nlBO/OM= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.6 h1:MzORe+J94I+hYu2a6XmV5yC9huoTv8NRcCrUNedDypQ= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.6/go.mod h1:hXzcHLARD7GeWnifd8j9RWqtfIgxj4/cAtIVIK7hg8g= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 h1:7oGD8KPfBOJGXiCoRKrrrQkbvCp8N++u36hrLMPey6o= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.11/go.mod h1:0DO9B5EUJQlIDif+XJRWCljZRKsAFKh3gpFz7UnDtOo= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 h1:edCcNp9eGIUDUCrzoCu1jWAXLGFIizeqkdkKgRlJwWc= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15/go.mod h1:lyRQKED9xWfgkYC/wmmYfv7iVIM68Z5OQ88ZdcV1QbU= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 h1:NITQpgo9A5NrDZ57uOWj+abvXSb83BbyggcUBVksN7c= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.7/go.mod h1:sks5UWBhEuWYDPdwlnRFn1w7xWdH29Jcpe+/PJQefEs= +github.com/aws/smithy-go v1.24.1 h1:VbyeNfmYkWoxMVpGUAbQumkODcYmfMRfZ8yQiH30SK0= +github.com/aws/smithy-go v1.24.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= -github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= -github.com/bytecodealliance/wasmtime-go/v3 v3.0.2 h1:3uZCA/BLTIu+DqCfguByNMJa2HVHpXvjfy0Dy7g6fuA= -github.com/bytecodealliance/wasmtime-go/v3 v3.0.2/go.mod h1:RnUjnIXxEJcL6BgCvNyzCCRzZcxCgsZCi+RNlvYor5Q= -github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= -github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= -github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= +github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= +github.com/bytecodealliance/wasmtime-go/v39 v39.0.1 h1:RibaT47yiyCRxMOj/l2cvL8cWiWBSqDXHyqsa9sGcCE= +github.com/bytecodealliance/wasmtime-go/v39 v39.0.1/go.mod h1:miR4NYIEBXeDNamZIzpskhJ0z/p8al+lwMWylQ/ZJb4= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/compliance-framework/agent v0.2.1 h1:I2cvHRdkBiIXeud7STptpg0+pzHBSUMiFxIuI4EzdGc= -github.com/compliance-framework/agent v0.2.1/go.mod h1:fpUMZejzNNfwadGnrN8HpAAyka+UANx8LVhiLZeoPhg= -github.com/compliance-framework/api v0.4.0 h1:wMptuEwrHfUykAmDuXIAc5VeLgVBmZUHJH1ASPUkLaY= -github.com/compliance-framework/api v0.4.0/go.mod h1:ViBBOdBQ5dG/AdBGPIzEiJ0vuPDyChogBPVE2VfKwKY= +github.com/compliance-framework/agent v0.6.2 h1:4Ha3kTDpoAXDsGOnczeVXdf56dl7h2XNxIfawWJc+LI= +github.com/compliance-framework/agent v0.6.2/go.mod h1:k6sNhVQXviFHbz/Fe/jOkfBZ+AFLnRPIuOH2aaaCTNo= +github.com/compliance-framework/api v0.16.0 h1:0HO5a5N80ktJLeLD5GVeTk7cK7PO9Xj5WN4SR+KGBH0= +github.com/compliance-framework/api v0.16.0/go.mod h1:BupcN8mQFgB0/2+YShU/r4BUYoGwzSjbz2esdOUaX/4= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= -github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= -github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/containerd/platforms v1.0.0-rc.2 h1:0SPgaNZPVWGEi4grZdV8VRYQn78y+nm6acgLGv/QzE4= +github.com/containerd/platforms v1.0.0-rc.2/go.mod h1:J71L7B+aiM5SdIEqmd9wp6THLVRzJGXfNuWCZCllLA4= +github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc= +github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8= github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/defenseunicorns/go-oscal v0.6.2 h1:oLkMAJYVMq73Rm+9efcyaKq5SLMditjB6wv7o3XXpq8= -github.com/defenseunicorns/go-oscal v0.6.2/go.mod h1:UHp2yK9ty2mYJDun7oNhbstCq6SAAwP4YGbw9n7uG6o= -github.com/dgraph-io/badger/v3 v3.2103.5 h1:ylPa6qzbjYRQMU6jokoj4wzcaweHylt//CH0AKt0akg= -github.com/dgraph-io/badger/v3 v3.2103.5/go.mod h1:4MPiseMeDQ3FNCYwRbbcBOGJLf5jsE0PPFzRiKjtcdw= -github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= -github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3hK/4HUq48LQ6Wwqo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= +github.com/defenseunicorns/go-oscal v0.7.0 h1:Ji9Yw3zEkbUfKZ8Gotoi9ExjUV/h3jmFLJBCYWkDN3E= +github.com/defenseunicorns/go-oscal v0.7.0/go.mod h1:OPuLRz6v7qhSaKIUgr+bK6ykhYq7FpZozSn2cVZJhMs= +github.com/dgraph-io/badger/v4 v4.9.1 h1:DocZXZkg5JJHJPtUErA0ibyHxOVUDVoXLSCV6t8NC8w= +github.com/dgraph-io/badger/v4 v4.9.1/go.mod h1:5/MEx97uzdPUHR4KtkNt8asfI2T4JiEiQlV7kWUo8c0= +github.com/dgraph-io/ristretto/v2 v2.2.0 h1:bkY3XzJcXoMuELV8F+vS8kzNgicwQFAaGINAEJdWGOM= +github.com/dgraph-io/ristretto/v2 v2.2.0/go.mod h1:RZrm63UmcBAaYWC1DotLYBmTvgkrs0+XhBd7Npn7/zI= github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo= github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/docker v28.0.1+incompatible h1:FCHjSRdXhNRFjlHMTv4jUNlIBbTeRjrWfeFuJp7jpo0= -github.com/docker/docker v28.0.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= +github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= @@ -93,125 +101,140 @@ github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+m github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I= github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= -github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= -github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= -github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI= -github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= -github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= -github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= -github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= +github.com/foxcpp/go-mockdns v1.2.0 h1:omK3OrHRD1IWJz1FuFBCFquhXslXoF17OvBS6JPzZF0= +github.com/foxcpp/go-mockdns v1.2.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik= +github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= -github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= -github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= +github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= -github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= -github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs= -github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= -github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M= -github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= -github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= -github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4= +github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80= +github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8= +github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4= +github.com/go-openapi/spec v0.22.2 h1:KEU4Fb+Lp1qg0V4MxrSCPv403ZjBl8Lx1a83gIPU8Qc= +github.com/go-openapi/spec v0.22.2/go.mod h1:iIImLODL2loCh3Vnox8TY2YWYJZjMAKYyLH2Mu8lOZs= +github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4= +github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU= +github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= +github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= +github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA= +github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY= +github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s= +github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE= +github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8= +github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0= +github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw= +github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE= +github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw= +github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.24.0 h1:KHQckvo8G6hlWnrPX4NJJ+aBfWNAE/HH+qdL2cBpCmg= -github.com/go-playground/validator/v10 v10.24.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus= -github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= -github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= -github.com/go-viper/mapstructure/v2 v2.3.0 h1:27XbWsHIqhbdR5TIC911OfYvgSaW93HM+dX7970Q7jk= -github.com/go-viper/mapstructure/v2 v2.3.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688= +github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU= +github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= +github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= +github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= -github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= -github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= -github.com/golang/glog v1.2.2 h1:1+mZ9upx1Dh6FmUTFR1naJ77miKiXgALjWOZ3NVFPmY= -github.com/golang/glog v1.2.2/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= -github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/flatbuffers v2.0.8+incompatible h1:ivUb1cGomAB101ZM1T0nOiWz9pSrTMoa9+EiY7igmkM= -github.com/google/flatbuffers v2.0.8+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q= +github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= -github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 h1:TmHmbvxPmaegwhDubVz0lICL0J5Ka2vwTzhoePEXsGE= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI= -github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= -github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= -github.com/hashicorp/go-plugin v1.6.2 h1:zdGAEd0V1lCaU0u+MxWQhtSDQmahpkwOun8U8EiRVog= -github.com/hashicorp/go-plugin v1.6.2/go.mod h1:CkgLQ5CZqNmdL9U9JzM532t8ZiYQ35+pj3b1FD37R0Q= -github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= -github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-plugin v1.7.0 h1:YghfQH/0QmPNc/AZMTFE3ac8fipZyZECHdDPshfk+mA= +github.com/hashicorp/go-plugin v1.7.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8= +github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= +github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA= -github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw= -github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= -github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= -github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= -github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= -github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw= +github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94= +github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= -github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= -github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= +github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY= -github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g= +github.com/labstack/echo-contrib v0.17.4 h1:g5mfsrJfJTKv+F5uNKCyrjLK7js+ZW6HTjg4FnDxxgk= +github.com/labstack/echo-contrib v0.17.4/go.mod h1:9O7ZPAHUeMGTOAfg80YqQduHzt0CzLak36PZRldYrZ0= +github.com/labstack/echo/v4 v4.13.4 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcXEA= +github.com/labstack/echo/v4 v4.13.4/go.mod h1:g63b33BZ5vZzcIUF8AtRH40DrTlXnx4UMC8rBdndmjQ= github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lestrrat-go/blackmagic v1.0.4 h1:IwQibdnf8l2KoO+qC3uT4OaTWsW7tuRQXy9TRN9QanA= +github.com/lestrrat-go/blackmagic v1.0.4/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw= +github.com/lestrrat-go/dsig v1.0.0 h1:OE09s2r9Z81kxzJYRn07TFM9XA4akrUdoMwr0L8xj38= +github.com/lestrrat-go/dsig v1.0.0/go.mod h1:dEgoOYYEJvW6XGbLasr8TFcAxoWrKlbQvmJgCR0qkDo= +github.com/lestrrat-go/dsig-secp256k1 v1.0.0 h1:JpDe4Aybfl0soBvoVwjqDbp+9S1Y2OM7gcrVVMFPOzY= +github.com/lestrrat-go/dsig-secp256k1 v1.0.0/go.mod h1:CxUgAhssb8FToqbL8NjSPoGQlnO4w3LG1P0qPWQm/NU= +github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= +github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= +github.com/lestrrat-go/httprc/v3 v3.0.4 h1:pXyH2ppK8GYYggygxJ3TvxpCZnbEUWc9qSwRTTApaLA= +github.com/lestrrat-go/httprc/v3 v3.0.4/go.mod h1:mSMtkZW92Z98M5YoNNztbRGxbXHql7tSitCvaxvo9l0= +github.com/lestrrat-go/jwx/v3 v3.0.13 h1:AdHKiPIYeCSnOJtvdpipPg/0SuFh9rdkN+HF3O0VdSk= +github.com/lestrrat-go/jwx/v3 v3.0.13/go.mod h1:2m0PV1A9tM4b/jVLMx8rh6rBl7F6WGb3EG2hufN9OQU= +github.com/lestrrat-go/option/v2 v2.0.0 h1:XxrcaJESE1fokHy3FpaQ/cXW8ZsIdWcdFzzLOcID3Ss= +github.com/lestrrat-go/option/v2 v2.0.0/go.mod h1:oSySsmzMoR0iRzCDCaUfsCzxQHUEuhOViQObyy7S6Vg= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM= github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8= +github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU= github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= @@ -226,16 +249,17 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= -github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= -github.com/open-policy-agent/opa v1.0.0 h1:fZsEwxg1knpPvUn0YDJuJZBcbVg4G3zKpWa3+CnYK+I= -github.com/open-policy-agent/opa v1.0.0/go.mod h1:+JyoH12I0+zqyC1iX7a2tmoQlipwAEGvOhVJMhmy+rM= +github.com/oklog/run v1.2.0 h1:O8x3yXwah4A73hJdlrwo/2X6J62gE5qTMusH0dvz60E= +github.com/oklog/run v1.2.0/go.mod h1:mgDbKRSwPhJfesJ4PntqFUbKQRZ50NgmZTSPlFA0YFk= +github.com/open-policy-agent/opa v1.14.1 h1:MhurLB9mSbXmojYFCmGbiC1Uagu1+aFAV4XVotDA86M= +github.com/open-policy-agent/opa v1.14.1/go.mod h1:B5gykwJ2l0g0wZS4ClCcpfSSEx51n4NHpTsWfuPwqnQ= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= -github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= -github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -243,61 +267,91 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= -github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= -github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= -github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= -github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.57.0 h1:Ro/rKjwdq9mZn1K5QPctzh+MA4Lp0BuYk5ZZEVhoNcY= -github.com/prometheus/common v0.57.0/go.mod h1:7uRPFSUTbfZWsJ7MHY56sqt7hLQu3bxXHDnNhl8E9qI= -github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= -github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= -github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= -github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= -github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= -github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= +github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= +github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= +github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= +github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8AOIL7EB/X911+m4EHsnWEHeJ0c+3TTBrg= +github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/riverqueue/river v0.30.1 h1:lpwmDT3zD+iDtF4tD50e/Y23UHpIeBUffVTDr2khN+s= +github.com/riverqueue/river v0.30.1/go.mod h1:x9tVfiCrbOctSAmaYP00iE5YlO8zh3Y9leFk6wP6aCk= +github.com/riverqueue/river/riverdriver v0.30.1 h1:p04cz/Ald1Js/STZ9qYrY5/TBJgjQeVPFltxidFYBBo= +github.com/riverqueue/river/riverdriver v0.30.1/go.mod h1:WBB9w6LftQtoZgRhNstqhP7MyBKt09XJkzluSNwMMoY= +github.com/riverqueue/river/riverdriver/riverpgxv5 v0.30.1 h1:nEStDftvm2jvGlJLliJR+n24PCJsoc4CgGzuop2Yzig= +github.com/riverqueue/river/riverdriver/riverpgxv5 v0.30.1/go.mod h1:4oSf8jYWZaEwmJ3R5LmOMiGlV9uuvCWOJ3uyBfTwWCc= +github.com/riverqueue/river/rivershared v0.30.1 h1:ytYlTtMppDV2rJRJ2j55mNf9uQDMPFudOmT4le6/9Ig= +github.com/riverqueue/river/rivershared v0.30.1/go.mod h1:PfmUHWkF6/fJ1CpjC4cG8eKciBXgMuIHgcRcIuHMc34= +github.com/riverqueue/river/rivertype v0.30.1 h1:jR7M5UlkA7KRxEbII+LOkD9oQMMz60AEdHh2We1APHY= +github.com/riverqueue/river/rivertype v0.30.1/go.mod h1:rWpgI59doOWS6zlVocROcwc00fZ1RbzRwsRTU8CDguw= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4= +github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= +github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0= +github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= +github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= +github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0ZqmLjXs= github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= -github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= -github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= -github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4= -github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= -github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= -github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= -github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= +github.com/slack-go/slack v0.20.0 h1:gbDdbee8+Z2o+DWx05Spq3GzbrLLleiRwHUKs+hZLSU= +github.com/slack-go/slack v0.20.0/go.mod h1:K81UmCivcYd/5Jmz8vLBfuyoZ3B4rQC2GHVXHteXiAE= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/swaggo/echo-swagger v1.4.1 h1:Yf0uPaJWp1uRtDloZALyLnvdBeoEL5Kc7DtnjzO/TUk= github.com/swaggo/echo-swagger v1.4.1/go.mod h1:C8bSi+9yH2FLZsnhqMZLIZddpUxZdBYuNHbtaS1Hljc= -github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw= -github.com/swaggo/files/v2 v2.0.0/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM= -github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A= -github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg= -github.com/tchap/go-patricia/v2 v2.3.1 h1:6rQp39lgIYZ+MHmdEq4xzuk1t7OdC35z/xm0BGhTkes= -github.com/tchap/go-patricia/v2 v2.3.1/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k= +github.com/swaggo/files/v2 v2.0.2 h1:Bq4tgS/yxLB/3nwOMcul5oLEUKa877Ykgz3CJMVbQKU= +github.com/swaggo/files/v2 v2.0.2/go.mod h1:TVqetIzZsO9OhHX1Am9sRf9LdrFZqoK49N37KON/jr0= +github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI= +github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg= +github.com/tchap/go-patricia/v2 v2.3.3 h1:xfNEsODumaEcCcY3gI0hYPZ/PcpVv5ju6RMAhgwZDDc= +github.com/tchap/go-patricia/v2 v2.3.3/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k= github.com/testcontainers/testcontainers-go v0.37.0 h1:L2Qc0vkTw2EHWQ08djon0D2uw7Z/PtHS/QzZZ5Ra/hg= github.com/testcontainers/testcontainers-go v0.37.0/go.mod h1:QPzbxZhQ6Bclip9igjLFj6z0hs01bU8lrl2dHQmgFGM= github.com/testcontainers/testcontainers-go/modules/postgres v0.37.0 h1:hsVwFkS6s+79MbKEO+W7A1wNIw1fmkMtF4fg83m6kbc= github.com/testcontainers/testcontainers-go/modules/postgres v0.37.0/go.mod h1:Qj/eGbRbO/rEYdcRLmN+bEojzatP/+NS1y8ojl2PQsc= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM= +github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fastjson v1.6.10 h1:/yjJg8jaVQdYR3arGxPE2X5z89xrlhS0eGXdv+ADTh4= +github.com/valyala/fastjson v1.6.10/go.mod h1:e6FubmQouUNP73jtMLmcbxS6ydWIpOfhz34TSfO3JaE= github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/vektah/gqlparser/v2 v2.5.32 h1:k9QPJd4sEDTL+qB4ncPLflqTJ3MmjB9SrVzJrawpFSc= +github.com/vektah/gqlparser/v2 v2.5.32/go.mod h1:c1I28gSOVNzlfc4WuDlqU7voQnsqI6OG2amkBAFmgts= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= @@ -306,64 +360,62 @@ github.com/yashtewari/glob-intersection v0.2.0 h1:8iuHdN88yYuCzCdjt0gDe+6bAhUwBe github.com/yashtewari/glob-intersection v0.2.0/go.mod h1:LK7pIC3piUjovexikBbJ26Yml7g8xa5bsjfx2v1fwok= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= -go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= -go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= -go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 h1:5pojmb1U1AogINhN3SurB+zm/nIcusopeBNp42f45QM= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0/go.mod h1:57gTHJSE5S1tqg+EKsLPlTWhpHMsWlVmer+LA926XiA= -go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= -go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= -go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM= -go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM= -go.opentelemetry.io/otel/sdk/metric v1.31.0 h1:i9hxxLJF/9kkvfHppyLL55aW7iIJz4JjxTeYusH7zMc= -go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8= -go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= -go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= -go.opentelemetry.io/proto/otlp v1.4.0 h1:TA9WRvW6zMwP+Ssb6fLoUIuirti1gGbP28GcKG1jgeg= -go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY= -go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= -go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= -go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= -golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= -golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= -golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= -golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= -golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= -golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0= +go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho= +go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc= +go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4= +go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI= +go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= +go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= +go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA= +go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc= +go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY= +go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= +go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= +golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= +golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= +golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= +golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= +golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= +golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= -golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= -golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= -golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= -golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= -google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q= -google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 h1:TqExAhdPaB60Ux47Cn0oLV07rGnxZzIsaRhQaqS666A= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA= -google.golang.org/grpc v1.69.2 h1:U3S9QEtbXC0bYNvRtcoklF3xGtLViumSYxWykJS+7AU= -google.golang.org/grpc v1.69.2/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= -google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= -google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= +golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= +golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= +google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= @@ -372,15 +424,15 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gorm.io/datatypes v1.2.5 h1:9UogU3jkydFVW1bIVVeoYsTpLRgwDVW3rHfJG6/Ek9I= -gorm.io/datatypes v1.2.5/go.mod h1:I5FUdlKpLb5PMqeMQhm30CQ6jXP8Rj89xkTeCSAaAD4= -gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8= -gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= -gorm.io/driver/postgres v1.5.7 h1:8ptbNJTDbEmhdr62uReG5BGkdQyeasu/FZHxI0IMGnM= -gorm.io/driver/postgres v1.5.7/go.mod h1:3e019WlBaYI5o5LIdNV+LyxCMNtLOQETBXL2h4chKpA= -gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= -gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= -gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= -gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= -sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= -sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= +gorm.io/datatypes v1.2.7 h1:ww9GAhF1aGXZY3EB3cJPJ7//JiuQo7DlQA7NNlVaTdk= +gorm.io/datatypes v1.2.7/go.mod h1:M2iO+6S3hhi4nAyYe444Pcb0dcIiOMJ7QHaUXxyiNZY= +gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg= +gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo= +gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= +gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= +gorm.io/gorm v1.30.5 h1:dvEfYwxL+i+xgCNSGGBT1lDjCzfELK8fHZxL3Ee9X0s= +gorm.io/gorm v1.30.5/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/main.go b/main.go index f3825b0..b947724 100644 --- a/main.go +++ b/main.go @@ -4,6 +4,10 @@ import ( "context" "errors" "fmt" + "iter" + "os" + "slices" + "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/ec2" @@ -14,9 +18,6 @@ import ( "github.com/compliance-framework/plugin-aws-networking-security/internal" "github.com/hashicorp/go-hclog" goplugin "github.com/hashicorp/go-plugin" - "iter" - "os" - "slices" ) type CompliancePlugin struct { @@ -29,6 +30,10 @@ func (l *CompliancePlugin) Configure(req *proto.ConfigureRequest) (*proto.Config return &proto.ConfigureResponse{}, nil } +func (l *CompliancePlugin) Init(req *proto.InitRequest, apiHelper runner.ApiHelper) (*proto.InitResponse, error) { + return &proto.InitResponse{}, nil +} + func (l *CompliancePlugin) Eval(request *proto.EvalRequest, apiHelper runner.ApiHelper) (*proto.EvalResponse, error) { ctx := context.TODO() evalStatus := proto.ExecutionStatus_SUCCESS @@ -197,7 +202,7 @@ func main() { goplugin.Serve(&goplugin.ServeConfig{ HandshakeConfig: runner.HandshakeConfig, Plugins: map[string]goplugin.Plugin{ - "runner": &runner.RunnerGRPCPlugin{ + "runner": &runner.RunnerV2GRPCPlugin{ Impl: compliancePluginObj, }, }, From e95b2154108d200b8b62779d6ba883977a41a838 Mon Sep 17 00:00:00 2001 From: Reece Bedding Date: Wed, 20 May 2026 10:43:23 +0100 Subject: [PATCH 02/10] feat: add enhanced VPC related collectors --- go.mod | 2 + go.sum | 4 + internal/evidence.go | 752 +++++++++++++++++++++++++++++++++++++++++ internal/pagination.go | 201 +++++++++++ internal/types.go | 147 ++++++++ internal/util.go | 39 +++ main.go | 513 +++++++++++++++++++++------- 7 files changed, 1535 insertions(+), 123 deletions(-) create mode 100644 internal/evidence.go create mode 100644 internal/pagination.go create mode 100644 internal/types.go diff --git a/go.mod b/go.mod index 5a60162..4fb2a4d 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.26.1 require ( github.com/aws/aws-sdk-go-v2 v1.41.2 github.com/aws/aws-sdk-go-v2/config v1.32.10 + github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.47.0 github.com/aws/aws-sdk-go-v2/service/ec2 v1.208.0 github.com/compliance-framework/agent v0.6.2 github.com/hashicorp/go-hclog v1.6.3 @@ -13,6 +14,7 @@ require ( require ( github.com/agnivade/levenshtein v1.2.1 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.19.10 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 // indirect diff --git a/go.sum b/go.sum index d46a426..e954fdc 100644 --- a/go.sum +++ b/go.sum @@ -16,6 +16,8 @@ github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= github.com/aws/aws-sdk-go-v2 v1.41.2 h1:LuT2rzqNQsauaGkPK/7813XxcZ3o3yePY0Iy891T2ls= github.com/aws/aws-sdk-go-v2 v1.41.2/go.mod h1:IvvlAZQXvTXznUPfRVfryiG1fbzE2NGK6m9u39YQ+S4= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 h1:zAybnyUQXIZ5mok5Jqwlf58/TFE7uvd3IAsa1aF9cXs= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10/go.mod h1:qqvMj6gHLR/EXWZw4ZbqlPbQUyenf4h82UQUlKc+l14= github.com/aws/aws-sdk-go-v2/config v1.32.10 h1:9DMthfO6XWZYLfzZglAgW5Fyou2nRI5CuV44sTedKBI= github.com/aws/aws-sdk-go-v2/config v1.32.10/go.mod h1:2rUIOnA2JaiqYmSKYmRJlcMWy6qTj1vuRFscppSBMcw= github.com/aws/aws-sdk-go-v2/credentials v1.19.10 h1:EEhmEUFCE1Yhl7vDhNOI5OCL/iKMdkkYFTRpZXNw7m8= @@ -30,6 +32,8 @@ github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEG github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16 h1:CjMzUs78RDDv4ROu3JnJn/Ig1r6ZD7/T2DXLLRpejic= github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16/go.mod h1:uVW4OLBqbJXSHJYA9svT9BluSvvwbzLQ2Crf6UPzR3c= +github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.47.0 h1:lLkvA+uOu/nB/UeAUoldkSPGIzZANxpEEHA+iP6kvQs= +github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.47.0/go.mod h1:uo14VBn5cNk/BPGTPz3kyLBxgpgOObgO8lmz+H7Z4Ck= github.com/aws/aws-sdk-go-v2/service/ec2 v1.208.0 h1:qzT4wyLo7ssa4QU8Xcf+h+iyCF4WTeQtM8fjr+UUKyI= github.com/aws/aws-sdk-go-v2/service/ec2 v1.208.0/go.mod h1:ouvGEfHbLaIlWwpDpOVWPWR+YwO0HDv3vm5tYLq8ImY= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 h1:CeY9LUdur+Dxoeldqoun6y4WtJ3RQtzk0JMP2gfUay0= diff --git a/internal/evidence.go b/internal/evidence.go new file mode 100644 index 0000000..d74524d --- /dev/null +++ b/internal/evidence.go @@ -0,0 +1,752 @@ +package internal + +import ( + "fmt" + + "github.com/aws/aws-sdk-go-v2/aws" + cloudwatchlogstypes "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs/types" + "github.com/aws/aws-sdk-go-v2/service/ec2/types" + "github.com/compliance-framework/agent/runner/proto" +) + +// VpcEvidenceContext contains the evidence context for a VPC +type VpcEvidenceContext struct { + Labels map[string]string + Components []*proto.Component + Inventory []*proto.InventoryItem + Subjects []*proto.Subject +} + +// SubnetEvidenceContext contains the evidence context for a Subnet +type SubnetEvidenceContext struct { + Labels map[string]string + Components []*proto.Component + Inventory []*proto.InventoryItem + Subjects []*proto.Subject +} + +// SecurityGroupEvidenceContext contains the evidence context for a Security Group +type SecurityGroupEvidenceContext struct { + Labels map[string]string + Components []*proto.Component + Inventory []*proto.InventoryItem + Subjects []*proto.Subject +} + +// NetworkAclEvidenceContext contains the evidence context for a Network ACL +type NetworkAclEvidenceContext struct { + Labels map[string]string + Components []*proto.Component + Inventory []*proto.InventoryItem + Subjects []*proto.Subject +} + +// RouteTableEvidenceContext contains the evidence context for a Route Table +type RouteTableEvidenceContext struct { + Labels map[string]string + Components []*proto.Component + Inventory []*proto.InventoryItem + Subjects []*proto.Subject +} + +// InternetGatewayEvidenceContext contains the evidence context for an Internet Gateway +type InternetGatewayEvidenceContext struct { + Labels map[string]string + Components []*proto.Component + Inventory []*proto.InventoryItem + Subjects []*proto.Subject +} + +// VpcEndpointEvidenceContext contains the evidence context for a VPC Endpoint +type VpcEndpointEvidenceContext struct { + Labels map[string]string + Components []*proto.Component + Inventory []*proto.InventoryItem + Subjects []*proto.Subject +} + +// FlowLogEvidenceContext contains the evidence context for a Flow Log +type FlowLogEvidenceContext struct { + Labels map[string]string + Components []*proto.Component + Inventory []*proto.InventoryItem + Subjects []*proto.Subject +} + +// LogGroupEvidenceContext contains the evidence context for a Log Group +type LogGroupEvidenceContext struct { + Labels map[string]string + Components []*proto.Component + Inventory []*proto.InventoryItem + Subjects []*proto.Subject +} + +// BuildVpcEvidenceContext builds evidence context for a VPC +func BuildVpcEvidenceContext(vpc types.Vpc, region string) VpcEvidenceContext { + metadata := GetResourceMetadata(ResourceTypeVPC) + vpcId := aws.ToString(vpc.VpcId) + + labels := map[string]string{ + "provider": "aws", + "type": string(ResourceTypeVPC), + "vpc-id": vpcId, + "cidr": aws.ToString(vpc.CidrBlock), + "region": region, + } + + components := []*proto.Component{ + { + Identifier: metadata.ComponentID, + Type: metadata.ComponentType, + Title: metadata.ComponentTitle, + Description: metadata.ComponentDesc, + Purpose: metadata.ComponentPurpose, + }, + } + + inventory := []*proto.InventoryItem{ + { + Identifier: fmt.Sprintf("%s/%s", metadata.LabelPrefix, vpcId), + Type: metadata.InventoryType, + Title: fmt.Sprintf("%s [%s]", metadata.ComponentTitle, vpcId), + Props: []*proto.Property{ + { + Name: "vpc-id", + Value: vpcId, + }, + { + Name: "cidr-block", + Value: aws.ToString(vpc.CidrBlock), + }, + { + Name: "state", + Value: string(vpc.State), + }, + { + Name: "is-default", + Value: fmt.Sprintf("%v", vpc.IsDefault), + }, + }, + ImplementedComponents: []*proto.InventoryItemImplementedComponent{ + { + Identifier: metadata.ComponentID, + }, + }, + }, + } + + subjects := []*proto.Subject{ + { + Type: proto.SubjectType_SUBJECT_TYPE_COMPONENT, + Identifier: metadata.ComponentID, + }, + { + Type: proto.SubjectType_SUBJECT_TYPE_INVENTORY_ITEM, + Identifier: fmt.Sprintf("%s/%s", metadata.LabelPrefix, vpcId), + }, + } + + return VpcEvidenceContext{ + Labels: labels, + Components: components, + Inventory: inventory, + Subjects: subjects, + } +} + +// BuildSubnetEvidenceContext builds evidence context for a Subnet +func BuildSubnetEvidenceContext(subnet types.Subnet, region string) SubnetEvidenceContext { + metadata := GetResourceMetadata(ResourceTypeSubnet) + subnetId := aws.ToString(subnet.SubnetId) + vpcId := aws.ToString(subnet.VpcId) + + labels := map[string]string{ + "provider": "aws", + "type": string(ResourceTypeSubnet), + "subnet-id": subnetId, + "vpc-id": vpcId, + "cidr": aws.ToString(subnet.CidrBlock), + "az": aws.ToString(subnet.AvailabilityZone), + "region": region, + } + + components := []*proto.Component{ + { + Identifier: metadata.ComponentID, + Type: metadata.ComponentType, + Title: metadata.ComponentTitle, + Description: metadata.ComponentDesc, + Purpose: metadata.ComponentPurpose, + }, + } + + inventory := []*proto.InventoryItem{ + { + Identifier: fmt.Sprintf("%s/%s", metadata.LabelPrefix, subnetId), + Type: metadata.InventoryType, + Title: fmt.Sprintf("%s [%s]", metadata.ComponentTitle, subnetId), + Props: []*proto.Property{ + { + Name: "subnet-id", + Value: subnetId, + }, + { + Name: "vpc-id", + Value: vpcId, + }, + { + Name: "cidr-block", + Value: aws.ToString(subnet.CidrBlock), + }, + { + Name: "availability-zone", + Value: aws.ToString(subnet.AvailabilityZone), + }, + { + Name: "state", + Value: string(subnet.State), + }, + { + Name: "map-public-ip-on-launch", + Value: fmt.Sprintf("%v", subnet.MapPublicIpOnLaunch), + }, + }, + ImplementedComponents: []*proto.InventoryItemImplementedComponent{ + { + Identifier: metadata.ComponentID, + }, + }, + }, + } + + subjects := []*proto.Subject{ + { + Type: proto.SubjectType_SUBJECT_TYPE_COMPONENT, + Identifier: metadata.ComponentID, + }, + { + Type: proto.SubjectType_SUBJECT_TYPE_INVENTORY_ITEM, + Identifier: fmt.Sprintf("%s/%s", metadata.LabelPrefix, subnetId), + }, + } + + return SubnetEvidenceContext{ + Labels: labels, + Components: components, + Inventory: inventory, + Subjects: subjects, + } +} + +// BuildSecurityGroupEvidenceContext builds evidence context for a Security Group +func BuildSecurityGroupEvidenceContext(group types.SecurityGroup, region string) SecurityGroupEvidenceContext { + metadata := GetResourceMetadata(ResourceTypeSecurityGroup) + groupId := aws.ToString(group.GroupId) + vpcId := aws.ToString(group.VpcId) + + labels := map[string]string{ + "provider": "aws", + "type": string(ResourceTypeSecurityGroup), + "group-id": groupId, + "_vpc-id": vpcId, + "region": region, + } + + components := []*proto.Component{ + { + Identifier: metadata.ComponentID, + Type: metadata.ComponentType, + Title: metadata.ComponentTitle, + Description: metadata.ComponentDesc, + Purpose: metadata.ComponentPurpose, + }, + } + + inventory := []*proto.InventoryItem{ + { + Identifier: fmt.Sprintf("%s/%s", metadata.LabelPrefix, groupId), + Type: metadata.InventoryType, + Title: fmt.Sprintf("%s [%s]", metadata.ComponentTitle, groupId), + Props: []*proto.Property{ + { + Name: "group-id", + Value: groupId, + }, + { + Name: "group-name", + Value: aws.ToString(group.GroupName), + }, + { + Name: "vpc-id", + Value: vpcId, + }, + }, + ImplementedComponents: []*proto.InventoryItemImplementedComponent{ + { + Identifier: metadata.ComponentID, + }, + }, + }, + } + + subjects := []*proto.Subject{ + { + Type: proto.SubjectType_SUBJECT_TYPE_COMPONENT, + Identifier: metadata.ComponentID, + }, + { + Type: proto.SubjectType_SUBJECT_TYPE_INVENTORY_ITEM, + Identifier: fmt.Sprintf("%s/%s", metadata.LabelPrefix, groupId), + }, + } + + return SecurityGroupEvidenceContext{ + Labels: labels, + Components: components, + Inventory: inventory, + Subjects: subjects, + } +} + +// BuildNetworkAclEvidenceContext builds evidence context for a Network ACL +func BuildNetworkAclEvidenceContext(acl types.NetworkAcl, region string) NetworkAclEvidenceContext { + metadata := GetResourceMetadata(ResourceTypeNetworkAcl) + aclId := aws.ToString(acl.NetworkAclId) + vpcId := aws.ToString(acl.VpcId) + + labels := map[string]string{ + "provider": "aws", + "type": string(ResourceTypeNetworkAcl), + "acl-id": aclId, + "vpc-id": vpcId, + "is-default": fmt.Sprintf("%v", acl.IsDefault), + "region": region, + } + + components := []*proto.Component{ + { + Identifier: metadata.ComponentID, + Type: metadata.ComponentType, + Title: metadata.ComponentTitle, + Description: metadata.ComponentDesc, + Purpose: metadata.ComponentPurpose, + }, + } + + inventory := []*proto.InventoryItem{ + { + Identifier: fmt.Sprintf("%s/%s", metadata.LabelPrefix, aclId), + Type: metadata.InventoryType, + Title: fmt.Sprintf("%s [%s]", metadata.ComponentTitle, aclId), + Props: []*proto.Property{ + { + Name: "acl-id", + Value: aclId, + }, + { + Name: "vpc-id", + Value: vpcId, + }, + { + Name: "is-default", + Value: fmt.Sprintf("%v", acl.IsDefault), + }, + { + Name: "owner-id", + Value: aws.ToString(acl.OwnerId), + }, + }, + ImplementedComponents: []*proto.InventoryItemImplementedComponent{ + { + Identifier: metadata.ComponentID, + }, + }, + }, + } + + subjects := []*proto.Subject{ + { + Type: proto.SubjectType_SUBJECT_TYPE_COMPONENT, + Identifier: metadata.ComponentID, + }, + { + Type: proto.SubjectType_SUBJECT_TYPE_INVENTORY_ITEM, + Identifier: fmt.Sprintf("%s/%s", metadata.LabelPrefix, aclId), + }, + } + + return NetworkAclEvidenceContext{ + Labels: labels, + Components: components, + Inventory: inventory, + Subjects: subjects, + } +} + +// BuildRouteTableEvidenceContext builds evidence context for a Route Table +func BuildRouteTableEvidenceContext(routeTable types.RouteTable, region string) RouteTableEvidenceContext { + metadata := GetResourceMetadata(ResourceTypeRouteTable) + rtId := aws.ToString(routeTable.RouteTableId) + vpcId := aws.ToString(routeTable.VpcId) + + labels := map[string]string{ + "provider": "aws", + "type": string(ResourceTypeRouteTable), + "route-table-id": rtId, + "vpc-id": vpcId, + "region": region, + } + + components := []*proto.Component{ + { + Identifier: metadata.ComponentID, + Type: metadata.ComponentType, + Title: metadata.ComponentTitle, + Description: metadata.ComponentDesc, + Purpose: metadata.ComponentPurpose, + }, + } + + inventory := []*proto.InventoryItem{ + { + Identifier: fmt.Sprintf("%s/%s", metadata.LabelPrefix, rtId), + Type: metadata.InventoryType, + Title: fmt.Sprintf("%s [%s]", metadata.ComponentTitle, rtId), + Props: []*proto.Property{ + { + Name: "route-table-id", + Value: rtId, + }, + { + Name: "vpc-id", + Value: vpcId, + }, + { + Name: "owner-id", + Value: aws.ToString(routeTable.OwnerId), + }, + }, + ImplementedComponents: []*proto.InventoryItemImplementedComponent{ + { + Identifier: metadata.ComponentID, + }, + }, + }, + } + + subjects := []*proto.Subject{ + { + Type: proto.SubjectType_SUBJECT_TYPE_COMPONENT, + Identifier: metadata.ComponentID, + }, + { + Type: proto.SubjectType_SUBJECT_TYPE_INVENTORY_ITEM, + Identifier: fmt.Sprintf("%s/%s", metadata.LabelPrefix, rtId), + }, + } + + return RouteTableEvidenceContext{ + Labels: labels, + Components: components, + Inventory: inventory, + Subjects: subjects, + } +} + +// BuildInternetGatewayEvidenceContext builds evidence context for an Internet Gateway +func BuildInternetGatewayEvidenceContext(igw types.InternetGateway, region string) InternetGatewayEvidenceContext { + metadata := GetResourceMetadata(ResourceTypeInternetGateway) + igwId := aws.ToString(igw.InternetGatewayId) + + // Get VPC ID if attached + var vpcId string + if len(igw.Attachments) > 0 { + vpcId = aws.ToString(igw.Attachments[0].VpcId) + } + + labels := map[string]string{ + "provider": "aws", + "type": string(ResourceTypeInternetGateway), + "igw-id": igwId, + "vpc-id": vpcId, + "region": region, + } + + components := []*proto.Component{ + { + Identifier: metadata.ComponentID, + Type: metadata.ComponentType, + Title: metadata.ComponentTitle, + Description: metadata.ComponentDesc, + Purpose: metadata.ComponentPurpose, + }, + } + + inventory := []*proto.InventoryItem{ + { + Identifier: fmt.Sprintf("%s/%s", metadata.LabelPrefix, igwId), + Type: metadata.InventoryType, + Title: fmt.Sprintf("%s [%s]", metadata.ComponentTitle, igwId), + Props: []*proto.Property{ + { + Name: "igw-id", + Value: igwId, + }, + { + Name: "vpc-id", + Value: vpcId, + }, + { + Name: "owner-id", + Value: aws.ToString(igw.OwnerId), + }, + }, + ImplementedComponents: []*proto.InventoryItemImplementedComponent{ + { + Identifier: metadata.ComponentID, + }, + }, + }, + } + + subjects := []*proto.Subject{ + { + Type: proto.SubjectType_SUBJECT_TYPE_COMPONENT, + Identifier: metadata.ComponentID, + }, + { + Type: proto.SubjectType_SUBJECT_TYPE_INVENTORY_ITEM, + Identifier: fmt.Sprintf("%s/%s", metadata.LabelPrefix, igwId), + }, + } + + return InternetGatewayEvidenceContext{ + Labels: labels, + Components: components, + Inventory: inventory, + Subjects: subjects, + } +} + +// BuildVpcEndpointEvidenceContext builds evidence context for a VPC Endpoint +func BuildVpcEndpointEvidenceContext(endpoint types.VpcEndpoint, region string) VpcEndpointEvidenceContext { + metadata := GetResourceMetadata(ResourceTypeVpcEndpoint) + endpointId := aws.ToString(endpoint.VpcEndpointId) + vpcId := aws.ToString(endpoint.VpcId) + + labels := map[string]string{ + "provider": "aws", + "type": string(ResourceTypeVpcEndpoint), + "endpoint-id": endpointId, + "vpc-id": vpcId, + "service-name": aws.ToString(endpoint.ServiceName), + "state": string(endpoint.State), + "region": region, + } + + components := []*proto.Component{ + { + Identifier: metadata.ComponentID, + Type: metadata.ComponentType, + Title: metadata.ComponentTitle, + Description: metadata.ComponentDesc, + Purpose: metadata.ComponentPurpose, + }, + } + + inventory := []*proto.InventoryItem{ + { + Identifier: fmt.Sprintf("%s/%s", metadata.LabelPrefix, endpointId), + Type: metadata.InventoryType, + Title: fmt.Sprintf("%s [%s]", metadata.ComponentTitle, endpointId), + Props: []*proto.Property{ + { + Name: "endpoint-id", + Value: endpointId, + }, + { + Name: "vpc-id", + Value: vpcId, + }, + { + Name: "service-name", + Value: aws.ToString(endpoint.ServiceName), + }, + { + Name: "state", + Value: string(endpoint.State), + }, + }, + ImplementedComponents: []*proto.InventoryItemImplementedComponent{ + { + Identifier: metadata.ComponentID, + }, + }, + }, + } + + subjects := []*proto.Subject{ + { + Type: proto.SubjectType_SUBJECT_TYPE_COMPONENT, + Identifier: metadata.ComponentID, + }, + { + Type: proto.SubjectType_SUBJECT_TYPE_INVENTORY_ITEM, + Identifier: fmt.Sprintf("%s/%s", metadata.LabelPrefix, endpointId), + }, + } + + return VpcEndpointEvidenceContext{ + Labels: labels, + Components: components, + Inventory: inventory, + Subjects: subjects, + } +} + +// BuildFlowLogEvidenceContext builds evidence context for a Flow Log +func BuildFlowLogEvidenceContext(flowLog types.FlowLog, region string) FlowLogEvidenceContext { + metadata := GetResourceMetadata(ResourceTypeFlowLog) + flowLogId := aws.ToString(flowLog.FlowLogId) + + labels := map[string]string{ + "provider": "aws", + "type": string(ResourceTypeFlowLog), + "flow-log-id": flowLogId, + "resource-id": aws.ToString(flowLog.ResourceId), + "traffic-type": string(flowLog.TrafficType), + "flow-log-status": aws.ToString(flowLog.FlowLogStatus), + "region": region, + } + + components := []*proto.Component{ + { + Identifier: metadata.ComponentID, + Type: metadata.ComponentType, + Title: metadata.ComponentTitle, + Description: metadata.ComponentDesc, + Purpose: metadata.ComponentPurpose, + }, + } + + inventory := []*proto.InventoryItem{ + { + Identifier: fmt.Sprintf("%s/%s", metadata.LabelPrefix, flowLogId), + Type: metadata.InventoryType, + Title: fmt.Sprintf("%s [%s]", metadata.ComponentTitle, flowLogId), + Props: []*proto.Property{ + { + Name: "flow-log-id", + Value: flowLogId, + }, + { + Name: "resource-id", + Value: aws.ToString(flowLog.ResourceId), + }, + { + Name: "traffic-type", + Value: string(flowLog.TrafficType), + }, + { + Name: "flow-log-status", + Value: aws.ToString(flowLog.FlowLogStatus), + }, + { + Name: "log-group-name", + Value: aws.ToString(flowLog.LogGroupName), + }, + }, + ImplementedComponents: []*proto.InventoryItemImplementedComponent{ + { + Identifier: metadata.ComponentID, + }, + }, + }, + } + + subjects := []*proto.Subject{ + { + Type: proto.SubjectType_SUBJECT_TYPE_COMPONENT, + Identifier: metadata.ComponentID, + }, + { + Type: proto.SubjectType_SUBJECT_TYPE_INVENTORY_ITEM, + Identifier: fmt.Sprintf("%s/%s", metadata.LabelPrefix, flowLogId), + }, + } + + return FlowLogEvidenceContext{ + Labels: labels, + Components: components, + Inventory: inventory, + Subjects: subjects, + } +} + +// BuildLogGroupEvidenceContext builds evidence context for a Log Group +func BuildLogGroupEvidenceContext(logGroup cloudwatchlogstypes.LogGroup, region string) LogGroupEvidenceContext { + metadata := GetResourceMetadata(ResourceTypeLogGroup) + logGroupName := aws.ToString(logGroup.LogGroupName) + + labels := map[string]string{ + "provider": "aws", + "type": string(ResourceTypeLogGroup), + "log-group-name": logGroupName, + "region": region, + } + + components := []*proto.Component{ + { + Identifier: metadata.ComponentID, + Type: metadata.ComponentType, + Title: metadata.ComponentTitle, + Description: metadata.ComponentDesc, + Purpose: metadata.ComponentPurpose, + }, + } + + inventory := []*proto.InventoryItem{ + { + Identifier: fmt.Sprintf("%s/%s", metadata.LabelPrefix, logGroupName), + Type: metadata.InventoryType, + Title: fmt.Sprintf("%s [%s]", metadata.ComponentTitle, logGroupName), + Props: []*proto.Property{ + { + Name: "log-group-name", + Value: logGroupName, + }, + { + Name: "retention-in-days", + Value: fmt.Sprintf("%d", aws.ToInt32(logGroup.RetentionInDays)), + }, + { + Name: "stored-bytes", + Value: fmt.Sprintf("%d", aws.ToInt64(logGroup.StoredBytes)), + }, + }, + ImplementedComponents: []*proto.InventoryItemImplementedComponent{ + { + Identifier: metadata.ComponentID, + }, + }, + }, + } + + subjects := []*proto.Subject{ + { + Type: proto.SubjectType_SUBJECT_TYPE_COMPONENT, + Identifier: metadata.ComponentID, + }, + { + Type: proto.SubjectType_SUBJECT_TYPE_INVENTORY_ITEM, + Identifier: fmt.Sprintf("%s/%s", metadata.LabelPrefix, logGroupName), + }, + } + + return LogGroupEvidenceContext{ + Labels: labels, + Components: components, + Inventory: inventory, + Subjects: subjects, + } +} diff --git a/internal/pagination.go b/internal/pagination.go new file mode 100644 index 0000000..60e6ec3 --- /dev/null +++ b/internal/pagination.go @@ -0,0 +1,201 @@ +package internal + +import ( + "context" + "iter" + + cloudwatchlogs "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs" + cloudwatchlogstypes "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs/types" + "github.com/aws/aws-sdk-go-v2/service/ec2" + "github.com/aws/aws-sdk-go-v2/service/ec2/types" +) + +// PaginatedDescribeVpcs returns an iterator over all VPCs in the account/region +func PaginatedDescribeVpcs(ctx context.Context, client *ec2.Client) iter.Seq2[types.Vpc, error] { + return func(yield func(types.Vpc, error) bool) { + paginator := ec2.NewDescribeVpcsPaginator(client, &ec2.DescribeVpcsInput{}) + for paginator.HasMorePages() { + page, err := paginator.NextPage(ctx) + if err != nil { + yield(types.Vpc{}, err) + return + } + for _, vpc := range page.Vpcs { + if !yield(vpc, nil) { + return + } + } + } + } +} + +// PaginatedDescribeSubnets returns an iterator over all subnets in the account/region +func PaginatedDescribeSubnets(ctx context.Context, client *ec2.Client) iter.Seq2[types.Subnet, error] { + return func(yield func(types.Subnet, error) bool) { + paginator := ec2.NewDescribeSubnetsPaginator(client, &ec2.DescribeSubnetsInput{}) + for paginator.HasMorePages() { + page, err := paginator.NextPage(ctx) + if err != nil { + yield(types.Subnet{}, err) + return + } + for _, subnet := range page.Subnets { + if !yield(subnet, nil) { + return + } + } + } + } +} + +// PaginatedDescribeSecurityGroups returns an iterator over all security groups in the account/region +func PaginatedDescribeSecurityGroups(ctx context.Context, client *ec2.Client) iter.Seq2[types.SecurityGroup, error] { + return func(yield func(types.SecurityGroup, error) bool) { + paginator := ec2.NewDescribeSecurityGroupsPaginator(client, &ec2.DescribeSecurityGroupsInput{}) + for paginator.HasMorePages() { + page, err := paginator.NextPage(ctx) + if err != nil { + yield(types.SecurityGroup{}, err) + return + } + for _, sg := range page.SecurityGroups { + if !yield(sg, nil) { + return + } + } + } + } +} + +// PaginatedDescribeNetworkAcls returns an iterator over all network ACLs in the account/region +func PaginatedDescribeNetworkAcls(ctx context.Context, client *ec2.Client) iter.Seq2[types.NetworkAcl, error] { + return func(yield func(types.NetworkAcl, error) bool) { + paginator := ec2.NewDescribeNetworkAclsPaginator(client, &ec2.DescribeNetworkAclsInput{}) + for paginator.HasMorePages() { + page, err := paginator.NextPage(ctx) + if err != nil { + yield(types.NetworkAcl{}, err) + return + } + for _, acl := range page.NetworkAcls { + if !yield(acl, nil) { + return + } + } + } + } +} + +// PaginatedDescribeRouteTables returns an iterator over all route tables in the account/region +func PaginatedDescribeRouteTables(ctx context.Context, client *ec2.Client) iter.Seq2[types.RouteTable, error] { + return func(yield func(types.RouteTable, error) bool) { + paginator := ec2.NewDescribeRouteTablesPaginator(client, &ec2.DescribeRouteTablesInput{}) + for paginator.HasMorePages() { + page, err := paginator.NextPage(ctx) + if err != nil { + yield(types.RouteTable{}, err) + return + } + for _, rt := range page.RouteTables { + if !yield(rt, nil) { + return + } + } + } + } +} + +// PaginatedDescribeInternetGateways returns an iterator over all internet gateways in the account/region +func PaginatedDescribeInternetGateways(ctx context.Context, client *ec2.Client) iter.Seq2[types.InternetGateway, error] { + return func(yield func(types.InternetGateway, error) bool) { + paginator := ec2.NewDescribeInternetGatewaysPaginator(client, &ec2.DescribeInternetGatewaysInput{}) + for paginator.HasMorePages() { + page, err := paginator.NextPage(ctx) + if err != nil { + yield(types.InternetGateway{}, err) + return + } + for _, igw := range page.InternetGateways { + if !yield(igw, nil) { + return + } + } + } + } +} + +// PaginatedDescribeFlowLogs returns an iterator over all flow logs in the account/region +func PaginatedDescribeFlowLogs(ctx context.Context, client *ec2.Client) iter.Seq2[types.FlowLog, error] { + return func(yield func(types.FlowLog, error) bool) { + paginator := ec2.NewDescribeFlowLogsPaginator(client, &ec2.DescribeFlowLogsInput{}) + for paginator.HasMorePages() { + page, err := paginator.NextPage(ctx) + if err != nil { + yield(types.FlowLog{}, err) + return + } + for _, fl := range page.FlowLogs { + if !yield(fl, nil) { + return + } + } + } + } +} + +// PaginatedDescribeVpcEndpoints returns an iterator over all VPC endpoints in the account/region +func PaginatedDescribeVpcEndpoints(ctx context.Context, client *ec2.Client) iter.Seq2[types.VpcEndpoint, error] { + return func(yield func(types.VpcEndpoint, error) bool) { + paginator := ec2.NewDescribeVpcEndpointsPaginator(client, &ec2.DescribeVpcEndpointsInput{}) + for paginator.HasMorePages() { + page, err := paginator.NextPage(ctx) + if err != nil { + yield(types.VpcEndpoint{}, err) + return + } + for _, endpoint := range page.VpcEndpoints { + if !yield(endpoint, nil) { + return + } + } + } + } +} + +// PaginatedDescribeTransitGatewayAttachments returns an iterator over all transit gateway attachments in the account/region +func PaginatedDescribeTransitGatewayAttachments(ctx context.Context, client *ec2.Client) iter.Seq2[types.TransitGatewayAttachment, error] { + return func(yield func(types.TransitGatewayAttachment, error) bool) { + paginator := ec2.NewDescribeTransitGatewayAttachmentsPaginator(client, &ec2.DescribeTransitGatewayAttachmentsInput{}) + for paginator.HasMorePages() { + page, err := paginator.NextPage(ctx) + if err != nil { + yield(types.TransitGatewayAttachment{}, err) + return + } + for _, attachment := range page.TransitGatewayAttachments { + if !yield(attachment, nil) { + return + } + } + } + } +} + +// PaginatedDescribeLogGroups returns an iterator over all CloudWatch Logs log groups +func PaginatedDescribeLogGroups(ctx context.Context, client *cloudwatchlogs.Client) iter.Seq2[cloudwatchlogstypes.LogGroup, error] { + return func(yield func(cloudwatchlogstypes.LogGroup, error) bool) { + paginator := cloudwatchlogs.NewDescribeLogGroupsPaginator(client, &cloudwatchlogs.DescribeLogGroupsInput{}) + for paginator.HasMorePages() { + page, err := paginator.NextPage(ctx) + if err != nil { + yield(cloudwatchlogstypes.LogGroup{}, err) + return + } + for _, lg := range page.LogGroups { + if !yield(lg, nil) { + return + } + } + } + } +} diff --git a/internal/types.go b/internal/types.go new file mode 100644 index 0000000..c523c5d --- /dev/null +++ b/internal/types.go @@ -0,0 +1,147 @@ +package internal + +// ResourceType represents the type of AWS VPC-related resource +type ResourceType string + +const ( + ResourceTypeVPC ResourceType = "vpc" + ResourceTypeSubnet ResourceType = "subnet" + ResourceTypeSecurityGroup ResourceType = "security-group" + ResourceTypeNetworkAcl ResourceType = "nacl" + ResourceTypeRouteTable ResourceType = "route-table" + ResourceTypeFlowLog ResourceType = "flow-log" + ResourceTypeVpcEndpoint ResourceType = "vpc-endpoint" + ResourceTypeInternetGateway ResourceType = "internet-gateway" + ResourceTypeTransitGateway ResourceType = "transit-gateway" + ResourceTypeLogGroup ResourceType = "log-group" +) + +// ResourceMetadata contains metadata for building evidence context for a resource type +type ResourceMetadata struct { + Type ResourceType + ComponentID string + ComponentTitle string + ComponentType string + ComponentDesc string + ComponentPurpose string + InventoryType string + LabelPrefix string +} + +// GetResourceMetadata returns metadata for a given resource type +func GetResourceMetadata(resourceType ResourceType) ResourceMetadata { + switch resourceType { + case ResourceTypeVPC: + return ResourceMetadata{ + Type: ResourceTypeVPC, + ComponentID: "common-components/amazon-vpc", + ComponentTitle: "Amazon VPC", + ComponentType: "service", + ComponentDesc: "Amazon Virtual Private Cloud (VPC) provides a logically isolated section of the AWS Cloud where you can launch AWS resources in a virtual network that you define. VPCs enable network segmentation, control over IP address ranges, subnet configuration, and network gateways.", + ComponentPurpose: "To provide network isolation and segmentation for AWS resources, enabling secure network architecture design with control over IP addressing, subnets, routing, and network gateways.", + InventoryType: "network", + LabelPrefix: "aws-vpc", + } + case ResourceTypeSubnet: + return ResourceMetadata{ + Type: ResourceTypeSubnet, + ComponentID: "common-components/amazon-subnet", + ComponentTitle: "Amazon Subnet", + ComponentType: "service", + ComponentDesc: "Amazon VPC Subnets are segments of a VPC's IP address range where you can launch AWS resources. Subnets can be public or private, with public subnets having a route to an internet gateway and private subnets lacking direct internet access.", + ComponentPurpose: "To provide network segmentation within a VPC, allowing isolation of resources and control over network accessibility through public/private subnet design.", + InventoryType: "network-segment", + LabelPrefix: "aws-subnet", + } + case ResourceTypeSecurityGroup: + return ResourceMetadata{ + Type: ResourceTypeSecurityGroup, + ComponentID: "common-components/amazon-security-group", + ComponentTitle: "Amazon Security Groups", + ComponentType: "service", + ComponentDesc: "Amazon Security Groups act as virtual firewalls for AWS resources such as EC2 instances and RDS databases. They control inbound and outbound traffic at the instance level using rule-based configurations tied to ports, protocols, and CIDR ranges. Security Groups are stateful and can reference other groups to enforce dynamic trust boundaries within a VPC.", + ComponentPurpose: "To enforce network segmentation and access control policies at the resource level, providing a configurable and auditable security boundary for cloud-based assets in support of least privilege and Zero Trust architectures.", + InventoryType: "firewall", + LabelPrefix: "aws-security-group", + } + case ResourceTypeNetworkAcl: + return ResourceMetadata{ + Type: ResourceTypeNetworkAcl, + ComponentID: "common-components/amazon-network-acl", + ComponentTitle: "Amazon Network ACL", + ComponentType: "service", + ComponentDesc: "Amazon Network Access Control Lists (NACLs) act as stateless firewalls for controlling inbound and outbound traffic at the subnet level. NACLs use numbered rules to allow or deny traffic based on protocol, port, and source/destination CIDR ranges.", + ComponentPurpose: "To provide subnet-level network traffic control as an additional layer of defense alongside security groups, enabling network segmentation and access control at the subnet boundary.", + InventoryType: "network-control", + LabelPrefix: "aws-nacl", + } + case ResourceTypeRouteTable: + return ResourceMetadata{ + Type: ResourceTypeRouteTable, + ComponentID: "common-components/amazon-route-table", + ComponentTitle: "Amazon Route Table", + ComponentType: "service", + ComponentDesc: "Amazon Route Tables contain a set of rules (routes) that determine where network traffic is directed within a VPC. Each subnet in a VPC must be associated with a route table, which controls the routing for that subnet.", + ComponentPurpose: "To control network traffic routing within a VPC, enabling communication between subnets, to internet gateways, to VPC endpoints, and to other network resources.", + InventoryType: "network-routing", + LabelPrefix: "aws-route-table", + } + case ResourceTypeFlowLog: + return ResourceMetadata{ + Type: ResourceTypeFlowLog, + ComponentID: "common-components/amazon-flow-log", + ComponentTitle: "Amazon VPC Flow Logs", + ComponentType: "service", + ComponentDesc: "Amazon VPC Flow Logs capture information about the IP traffic going to and from network interfaces in a VPC. Flow log data can be published to CloudWatch Logs or S3 for analysis, auditing, and network monitoring.", + ComponentPurpose: "To provide visibility into network traffic patterns for security monitoring, compliance auditing, and network troubleshooting, supporting detection of unauthorized access and network anomalies.", + InventoryType: "network-monitoring", + LabelPrefix: "aws-flow-log", + } + case ResourceTypeVpcEndpoint: + return ResourceMetadata{ + Type: ResourceTypeVpcEndpoint, + ComponentID: "common-components/amazon-vpc-endpoint", + ComponentTitle: "Amazon VPC Endpoint", + ComponentType: "service", + ComponentDesc: "Amazon VPC Endpoints enable private connections between your VPC and supported AWS services without requiring an internet gateway, NAT device, VPN connection, or AWS Direct Connect connection. Endpoints are horizontally scalable and highly available.", + ComponentPurpose: "To enable private communication between VPC resources and AWS services, keeping traffic within the AWS network for improved security, reduced latency, and lower data transfer costs.", + InventoryType: "network-endpoint", + LabelPrefix: "aws-vpc-endpoint", + } + case ResourceTypeInternetGateway: + return ResourceMetadata{ + Type: ResourceTypeInternetGateway, + ComponentID: "common-components/amazon-internet-gateway", + ComponentTitle: "Amazon Internet Gateway", + ComponentType: "service", + ComponentDesc: "Amazon Internet Gateways enable communication between resources in your VPC and the internet. An internet gateway supports IPv4 and IPv6 traffic and does not cause availability risks or bandwidth constraints.", + ComponentPurpose: "To provide internet access for resources in public subnets, enabling outbound internet traffic and inbound access from the internet to publicly accessible resources.", + InventoryType: "network-gateway", + LabelPrefix: "aws-internet-gateway", + } + case ResourceTypeTransitGateway: + return ResourceMetadata{ + Type: ResourceTypeTransitGateway, + ComponentID: "common-components/amazon-transit-gateway", + ComponentTitle: "Amazon Transit Gateway", + ComponentType: "service", + ComponentDesc: "Amazon Transit Gateway acts as a regional cloud router that simplifies network topology by connecting VPCs and on-premises networks through a central hub. Transit Gateway scales with your network growth and provides inter-Region connectivity.", + ComponentPurpose: "To provide centralized network connectivity across multiple VPCs and on-premises networks, simplifying network architecture and reducing operational complexity for large-scale cloud deployments.", + InventoryType: "network-gateway", + LabelPrefix: "aws-transit-gateway", + } + case ResourceTypeLogGroup: + return ResourceMetadata{ + Type: ResourceTypeLogGroup, + ComponentID: "common-components/amazon-log-group", + ComponentTitle: "Amazon CloudWatch Logs", + ComponentType: "service", + ComponentDesc: "Amazon CloudWatch Logs enables you to centralize logs from all your systems, applications, and AWS services. Log groups organize log streams and can be used for monitoring, troubleshooting, and auditing.", + ComponentPurpose: "To provide centralized log management and analysis capabilities, enabling operational monitoring, security auditing, and compliance verification across AWS resources.", + InventoryType: "logging", + LabelPrefix: "aws-log-group", + } + default: + return ResourceMetadata{} + } +} diff --git a/internal/util.go b/internal/util.go index feb597f..5d89872 100644 --- a/internal/util.go +++ b/internal/util.go @@ -1,5 +1,10 @@ package internal +import ( + "os" + "strings" +) + func MergeMaps(maps ...map[string]string) map[string]string { result := make(map[string]string) for _, imap := range maps { @@ -13,3 +18,37 @@ func MergeMaps(maps ...map[string]string) map[string]string { func StringAddressed(str string) *string { return &str } + +// ResolveRegions resolves the list of AWS regions from config or environment +// Priority: config["regions"] > config["region"] > AWS_REGION env var +func ResolveRegions(config map[string]string) []string { + // Check for comma-separated regions list + if regionsStr, ok := config["regions"]; ok && regionsStr != "" { + regionParts := strings.Split(regionsStr, ",") + regions := make([]string, 0, len(regionParts)) + seen := make(map[string]bool) + for _, r := range regionParts { + r = strings.TrimSpace(r) + if r != "" && !seen[r] { + seen[r] = true + regions = append(regions, r) + } + } + if len(regions) > 0 { + return regions + } + } + + // Check for single region + if regionStr, ok := config["region"]; ok && regionStr != "" { + return []string{strings.TrimSpace(regionStr)} + } + + // Fall back to environment variable + if regionEnv := os.Getenv("AWS_REGION"); regionEnv != "" { + return []string{regionEnv} + } + + // Default to us-east-1 if nothing is configured + return []string{"us-east-1"} +} diff --git a/main.go b/main.go index b947724..257b6c4 100644 --- a/main.go +++ b/main.go @@ -3,15 +3,11 @@ package main import ( "context" "errors" - "fmt" - "iter" - "os" "slices" - "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs" "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/aws/aws-sdk-go-v2/service/ec2/types" policyManager "github.com/compliance-framework/agent/policy-manager" "github.com/compliance-framework/agent/runner" "github.com/compliance-framework/agent/runner/proto" @@ -39,152 +35,423 @@ func (l *CompliancePlugin) Eval(request *proto.EvalRequest, apiHelper runner.Api evalStatus := proto.ExecutionStatus_SUCCESS var accumulatedErrors error - cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(os.Getenv("AWS_REGION"))) - if err != nil { - l.logger.Error("unable to load SDK config", "error", err) - evalStatus = proto.ExecutionStatus_FAILURE - accumulatedErrors = errors.Join(accumulatedErrors, err) + // Resolve regions from config or environment + regions := internal.ResolveRegions(l.config) + + // Common actors for all evidence + actors := []*proto.OriginActor{ + { + Title: "The Continuous Compliance Framework", + Type: "assessment-platform", + Links: []*proto.Link{ + { + Href: "https://compliance-framework.github.io/docs/", + Rel: internal.StringAddressed("reference"), + Text: internal.StringAddressed("The Continuous Compliance Framework"), + }, + }, + }, + { + Title: "Continuous Compliance Framework - AWS Networking Security Plugin", + Type: "tool", + Links: []*proto.Link{ + { + Href: "https://github.com/compliance-framework/plugin-aws-networking-security", + Rel: internal.StringAddressed("reference"), + Text: internal.StringAddressed("The Continuous Compliance Framework AWS Networking Security Plugin"), + }, + }, + }, } - client := ec2.NewFromConfig(cfg) + // Iterate over each configured region + for _, region := range regions { + l.logger.Info("Collecting resources in region", "region", region) - // Run policy checks - for group, err := range getSecurityGroups(ctx, client) { + cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(region)) if err != nil { - l.logger.Error("unable to get instance", "error", err) + l.logger.Error("unable to load SDK config for region", "region", region, "error", err) evalStatus = proto.ExecutionStatus_FAILURE accumulatedErrors = errors.Join(accumulatedErrors, err) - break + continue } - labels := map[string]string{ - "provider": "aws", - "type": "security-group", - "group-id": aws.ToString(group.GroupId), - "_vpc-id": aws.ToString(group.VpcId), + client := ec2.NewFromConfig(cfg) + logsClient := cloudwatchlogs.NewFromConfig(cfg) + + // Collect and evaluate VPCs + for vpc, err := range internal.PaginatedDescribeVpcs(ctx, client) { + if err != nil { + l.logger.Error("unable to get VPC", "error", err) + evalStatus = proto.ExecutionStatus_FAILURE + accumulatedErrors = errors.Join(accumulatedErrors, err) + break + } + + vpcCtx := internal.BuildVpcEvidenceContext(vpc, region) + activities := make([]*proto.Activity, 0) + evidences := make([]*proto.Evidence, 0) + + for _, policyPath := range request.GetPolicyPaths() { + processor := policyManager.NewPolicyProcessor( + l.logger, + internal.MergeMaps( + vpcCtx.Labels, + map[string]string{}, + ), + vpcCtx.Subjects, + vpcCtx.Components, + vpcCtx.Inventory, + actors, + activities, + ) + evidence, err := processor.GenerateResults(ctx, policyPath, vpc) + evidences = slices.Concat(evidences, evidence) + if err != nil { + accumulatedErrors = errors.Join(accumulatedErrors, err) + } + } + + if err = apiHelper.CreateEvidence(ctx, evidences); err != nil { + l.logger.Error("Failed to send evidences", "error", err) + return &proto.EvalResponse{ + Status: proto.ExecutionStatus_FAILURE, + }, err + } } - activities := make([]*proto.Activity, 0) - evidences := make([]*proto.Evidence, 0) - - actors := []*proto.OriginActor{ - { - Title: "The Continuous Compliance Framework", - Type: "assessment-platform", - Links: []*proto.Link{ - { - Href: "https://compliance-framework.github.io/docs/", - Rel: internal.StringAddressed("reference"), - Text: internal.StringAddressed("The Continuous Compliance Framework"), - }, - }, - }, - { - Title: "Continuous Compliance Framework - Local SSH Plugin", - Type: "tool", - Links: []*proto.Link{ - { - Href: "https://github.com/compliance-framework/plugin-local-ssh", - Rel: internal.StringAddressed("reference"), - Text: internal.StringAddressed("The Continuous Compliance Framework' Local SSH Plugin"), - }, - }, - }, + // Collect and evaluate Subnets + for subnet, err := range internal.PaginatedDescribeSubnets(ctx, client) { + if err != nil { + l.logger.Error("unable to get Subnet", "error", err) + evalStatus = proto.ExecutionStatus_FAILURE + accumulatedErrors = errors.Join(accumulatedErrors, err) + break + } + + subnetCtx := internal.BuildSubnetEvidenceContext(subnet, region) + activities := make([]*proto.Activity, 0) + evidences := make([]*proto.Evidence, 0) + + for _, policyPath := range request.GetPolicyPaths() { + processor := policyManager.NewPolicyProcessor( + l.logger, + internal.MergeMaps( + subnetCtx.Labels, + map[string]string{}, + ), + subnetCtx.Subjects, + subnetCtx.Components, + subnetCtx.Inventory, + actors, + activities, + ) + evidence, err := processor.GenerateResults(ctx, policyPath, subnet) + evidences = slices.Concat(evidences, evidence) + if err != nil { + accumulatedErrors = errors.Join(accumulatedErrors, err) + } + } + + if err = apiHelper.CreateEvidence(ctx, evidences); err != nil { + l.logger.Error("Failed to send evidences", "error", err) + return &proto.EvalResponse{ + Status: proto.ExecutionStatus_FAILURE, + }, err + } } - components := []*proto.Component{ - { - Identifier: "common-components/amazon-security-group", - Type: "service", - Title: "Amazon Security Groups", - Description: "Amazon Security Groups act as virtual firewalls for AWS resources such as EC2 instances and RDS databases. They control inbound and outbound traffic at the instance level using rule-based configurations tied to ports, protocols, and CIDR ranges. Security Groups are stateful and can reference other groups to enforce dynamic trust boundaries within a VPC.", - Purpose: "To enforce network segmentation and access control policies at the resource level, providing a configurable and auditable security boundary for cloud-based assets in support of least privilege and Zero Trust architectures.", - }, + + // Collect and evaluate Security Groups + for group, err := range internal.PaginatedDescribeSecurityGroups(ctx, client) { + if err != nil { + l.logger.Error("unable to get Security Group", "error", err) + evalStatus = proto.ExecutionStatus_FAILURE + accumulatedErrors = errors.Join(accumulatedErrors, err) + break + } + + sgCtx := internal.BuildSecurityGroupEvidenceContext(group, region) + activities := make([]*proto.Activity, 0) + evidences := make([]*proto.Evidence, 0) + + for _, policyPath := range request.GetPolicyPaths() { + processor := policyManager.NewPolicyProcessor( + l.logger, + internal.MergeMaps( + sgCtx.Labels, + map[string]string{}, + ), + sgCtx.Subjects, + sgCtx.Components, + sgCtx.Inventory, + actors, + activities, + ) + evidence, err := processor.GenerateResults(ctx, policyPath, group) + evidences = slices.Concat(evidences, evidence) + if err != nil { + accumulatedErrors = errors.Join(accumulatedErrors, err) + } + } + + if err = apiHelper.CreateEvidence(ctx, evidences); err != nil { + l.logger.Error("Failed to send evidences", "error", err) + return &proto.EvalResponse{ + Status: proto.ExecutionStatus_FAILURE, + }, err + } } - inventory := []*proto.InventoryItem{ - { - Identifier: fmt.Sprintf("aws-security-group/%s", aws.ToString(group.GroupId)), - Type: "firewall", - Title: fmt.Sprintf("Amazon Security Group [%s]", aws.ToString(group.GroupId)), - Props: []*proto.Property{ - { - Name: "group-id", - Value: aws.ToString(group.GroupId), - }, - { - Name: "group-name", - Value: aws.ToString(group.GroupName), - }, - { - Name: "vpc-id", - Value: aws.ToString(group.VpcId), - }, - }, - ImplementedComponents: []*proto.InventoryItemImplementedComponent{ - { - Identifier: "common-components/amazon-security-group", - }, - }, - }, + + // Collect and evaluate Network ACLs + for acl, err := range internal.PaginatedDescribeNetworkAcls(ctx, client) { + if err != nil { + l.logger.Error("unable to get Network ACL", "error", err) + evalStatus = proto.ExecutionStatus_FAILURE + accumulatedErrors = errors.Join(accumulatedErrors, err) + break + } + + aclCtx := internal.BuildNetworkAclEvidenceContext(acl, region) + activities := make([]*proto.Activity, 0) + evidences := make([]*proto.Evidence, 0) + + for _, policyPath := range request.GetPolicyPaths() { + processor := policyManager.NewPolicyProcessor( + l.logger, + internal.MergeMaps( + aclCtx.Labels, + map[string]string{}, + ), + aclCtx.Subjects, + aclCtx.Components, + aclCtx.Inventory, + actors, + activities, + ) + evidence, err := processor.GenerateResults(ctx, policyPath, acl) + evidences = slices.Concat(evidences, evidence) + if err != nil { + accumulatedErrors = errors.Join(accumulatedErrors, err) + } + } + + if err = apiHelper.CreateEvidence(ctx, evidences); err != nil { + l.logger.Error("Failed to send evidences", "error", err) + return &proto.EvalResponse{ + Status: proto.ExecutionStatus_FAILURE, + }, err + } } - subjects := []*proto.Subject{ - { - Type: proto.SubjectType_SUBJECT_TYPE_COMPONENT, - Identifier: "common-components/amazon-security-group", - }, - { - Type: proto.SubjectType_SUBJECT_TYPE_INVENTORY_ITEM, - Identifier: fmt.Sprintf("aws-security-group/%s", aws.ToString(group.GroupId)), - }, + + // Collect and evaluate Route Tables + for rt, err := range internal.PaginatedDescribeRouteTables(ctx, client) { + if err != nil { + l.logger.Error("unable to get Route Table", "error", err) + evalStatus = proto.ExecutionStatus_FAILURE + accumulatedErrors = errors.Join(accumulatedErrors, err) + break + } + + rtCtx := internal.BuildRouteTableEvidenceContext(rt, region) + activities := make([]*proto.Activity, 0) + evidences := make([]*proto.Evidence, 0) + + for _, policyPath := range request.GetPolicyPaths() { + processor := policyManager.NewPolicyProcessor( + l.logger, + internal.MergeMaps( + rtCtx.Labels, + map[string]string{}, + ), + rtCtx.Subjects, + rtCtx.Components, + rtCtx.Inventory, + actors, + activities, + ) + evidence, err := processor.GenerateResults(ctx, policyPath, rt) + evidences = slices.Concat(evidences, evidence) + if err != nil { + accumulatedErrors = errors.Join(accumulatedErrors, err) + } + } + + if err = apiHelper.CreateEvidence(ctx, evidences); err != nil { + l.logger.Error("Failed to send evidences", "error", err) + return &proto.EvalResponse{ + Status: proto.ExecutionStatus_FAILURE, + }, err + } } - for _, policyPath := range request.GetPolicyPaths() { - // Explicitly reset steps to make things readable - processor := policyManager.NewPolicyProcessor( - l.logger, - internal.MergeMaps( - labels, - map[string]string{}, - ), - subjects, - components, - inventory, - actors, - activities, - ) - evidence, err := processor.GenerateResults(ctx, policyPath, group) - evidences = slices.Concat(evidences, evidence) + // Collect and evaluate Internet Gateways + for igw, err := range internal.PaginatedDescribeInternetGateways(ctx, client) { if err != nil { + l.logger.Error("unable to get Internet Gateway", "error", err) + evalStatus = proto.ExecutionStatus_FAILURE accumulatedErrors = errors.Join(accumulatedErrors, err) + break + } + + igwCtx := internal.BuildInternetGatewayEvidenceContext(igw, region) + activities := make([]*proto.Activity, 0) + evidences := make([]*proto.Evidence, 0) + + for _, policyPath := range request.GetPolicyPaths() { + processor := policyManager.NewPolicyProcessor( + l.logger, + internal.MergeMaps( + igwCtx.Labels, + map[string]string{}, + ), + igwCtx.Subjects, + igwCtx.Components, + igwCtx.Inventory, + actors, + activities, + ) + evidence, err := processor.GenerateResults(ctx, policyPath, igw) + evidences = slices.Concat(evidences, evidence) + if err != nil { + accumulatedErrors = errors.Join(accumulatedErrors, err) + } + } + + if err = apiHelper.CreateEvidence(ctx, evidences); err != nil { + l.logger.Error("Failed to send evidences", "error", err) + return &proto.EvalResponse{ + Status: proto.ExecutionStatus_FAILURE, + }, err } } - if err = apiHelper.CreateEvidence(ctx, evidences); err != nil { - l.logger.Error("Failed to send evidences", "error", err) - return &proto.EvalResponse{ - Status: proto.ExecutionStatus_FAILURE, - }, err + // Collect and evaluate VPC Endpoints + for endpoint, err := range internal.PaginatedDescribeVpcEndpoints(ctx, client) { + if err != nil { + l.logger.Error("unable to get VPC Endpoint", "error", err) + evalStatus = proto.ExecutionStatus_FAILURE + accumulatedErrors = errors.Join(accumulatedErrors, err) + break + } + + endpointCtx := internal.BuildVpcEndpointEvidenceContext(endpoint, region) + activities := make([]*proto.Activity, 0) + evidences := make([]*proto.Evidence, 0) + + for _, policyPath := range request.GetPolicyPaths() { + processor := policyManager.NewPolicyProcessor( + l.logger, + internal.MergeMaps( + endpointCtx.Labels, + map[string]string{}, + ), + endpointCtx.Subjects, + endpointCtx.Components, + endpointCtx.Inventory, + actors, + activities, + ) + evidence, err := processor.GenerateResults(ctx, policyPath, endpoint) + evidences = slices.Concat(evidences, evidence) + if err != nil { + accumulatedErrors = errors.Join(accumulatedErrors, err) + } + } + + if err = apiHelper.CreateEvidence(ctx, evidences); err != nil { + l.logger.Error("Failed to send evidences", "error", err) + return &proto.EvalResponse{ + Status: proto.ExecutionStatus_FAILURE, + }, err + } } - } - return &proto.EvalResponse{ - Status: evalStatus, - }, accumulatedErrors -} + // Collect and evaluate Flow Logs + for flowLog, err := range internal.PaginatedDescribeFlowLogs(ctx, client) { + if err != nil { + l.logger.Error("unable to get Flow Log", "error", err) + evalStatus = proto.ExecutionStatus_FAILURE + accumulatedErrors = errors.Join(accumulatedErrors, err) + break + } -func getSecurityGroups(ctx context.Context, client *ec2.Client) iter.Seq2[types.SecurityGroup, error] { - return func(yield func(types.SecurityGroup, error) bool) { - result, err := client.DescribeSecurityGroups(ctx, &ec2.DescribeSecurityGroupsInput{}) - if err != nil { - yield(types.SecurityGroup{}, err) - return + flowLogCtx := internal.BuildFlowLogEvidenceContext(flowLog, region) + activities := make([]*proto.Activity, 0) + evidences := make([]*proto.Evidence, 0) + + for _, policyPath := range request.GetPolicyPaths() { + processor := policyManager.NewPolicyProcessor( + l.logger, + internal.MergeMaps( + flowLogCtx.Labels, + map[string]string{}, + ), + flowLogCtx.Subjects, + flowLogCtx.Components, + flowLogCtx.Inventory, + actors, + activities, + ) + evidence, err := processor.GenerateResults(ctx, policyPath, flowLog) + evidences = slices.Concat(evidences, evidence) + if err != nil { + accumulatedErrors = errors.Join(accumulatedErrors, err) + } + } + + if err = apiHelper.CreateEvidence(ctx, evidences); err != nil { + l.logger.Error("Failed to send evidences", "error", err) + return &proto.EvalResponse{ + Status: proto.ExecutionStatus_FAILURE, + }, err + } } - for _, group := range result.SecurityGroups { - if !yield(group, nil) { - return + // Collect and evaluate Log Groups + for logGroup, err := range internal.PaginatedDescribeLogGroups(ctx, logsClient) { + if err != nil { + l.logger.Error("unable to get Log Group", "error", err) + evalStatus = proto.ExecutionStatus_FAILURE + accumulatedErrors = errors.Join(accumulatedErrors, err) + break + } + + logGroupCtx := internal.BuildLogGroupEvidenceContext(logGroup, region) + activities := make([]*proto.Activity, 0) + evidences := make([]*proto.Evidence, 0) + + for _, policyPath := range request.GetPolicyPaths() { + processor := policyManager.NewPolicyProcessor( + l.logger, + internal.MergeMaps( + logGroupCtx.Labels, + map[string]string{}, + ), + logGroupCtx.Subjects, + logGroupCtx.Components, + logGroupCtx.Inventory, + actors, + activities, + ) + evidence, err := processor.GenerateResults(ctx, policyPath, logGroup) + evidences = slices.Concat(evidences, evidence) + if err != nil { + accumulatedErrors = errors.Join(accumulatedErrors, err) + } + } + + if err = apiHelper.CreateEvidence(ctx, evidences); err != nil { + l.logger.Error("Failed to send evidences", "error", err) + return &proto.EvalResponse{ + Status: proto.ExecutionStatus_FAILURE, + }, err } } } + + return &proto.EvalResponse{ + Status: evalStatus, + }, accumulatedErrors } func main() { From 8bd49dedfdfbb4a1b0e58d0098ffe189a0f4a16b Mon Sep 17 00:00:00 2001 From: Reece Bedding Date: Tue, 26 May 2026 13:21:30 +0100 Subject: [PATCH 03/10] chore: refactor policy evaluation and add VPC resource collectors --- go.mod | 2 +- go.sum | 4 +- internal/flow_log.go | 18 ++ internal/internet_gateway.go | 18 ++ internal/log_group.go | 18 ++ internal/network_acl.go | 18 ++ internal/pagination.go | 19 ++ internal/policy_evaluation.go | 128 +++++++++ internal/region_datasets.go | 134 ++++++++++ internal/route_table.go | 18 ++ internal/security_group.go | 299 +++++++++++++++++++++ internal/subnet.go | 18 ++ internal/vpc.go | 18 ++ internal/vpc_endpoint.go | 18 ++ main.go | 479 ++++++++++------------------------ 15 files changed, 863 insertions(+), 346 deletions(-) create mode 100644 internal/flow_log.go create mode 100644 internal/internet_gateway.go create mode 100644 internal/log_group.go create mode 100644 internal/network_acl.go create mode 100644 internal/policy_evaluation.go create mode 100644 internal/region_datasets.go create mode 100644 internal/route_table.go create mode 100644 internal/security_group.go create mode 100644 internal/subnet.go create mode 100644 internal/vpc.go create mode 100644 internal/vpc_endpoint.go diff --git a/go.mod b/go.mod index 4fb2a4d..19bdba9 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/aws/aws-sdk-go-v2/config v1.32.10 github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.47.0 github.com/aws/aws-sdk-go-v2/service/ec2 v1.208.0 - github.com/compliance-framework/agent v0.6.2 + github.com/compliance-framework/agent v0.7.0 github.com/hashicorp/go-hclog v1.6.3 github.com/hashicorp/go-plugin v1.7.0 ) diff --git a/go.sum b/go.sum index e954fdc..691c712 100644 --- a/go.sum +++ b/go.sum @@ -62,8 +62,8 @@ github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqy github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/compliance-framework/agent v0.6.2 h1:4Ha3kTDpoAXDsGOnczeVXdf56dl7h2XNxIfawWJc+LI= -github.com/compliance-framework/agent v0.6.2/go.mod h1:k6sNhVQXviFHbz/Fe/jOkfBZ+AFLnRPIuOH2aaaCTNo= +github.com/compliance-framework/agent v0.7.0 h1:ZNuztQKLNvazIqe9QVV9OjERCPOt3GlZ1/wv9iLOwtU= +github.com/compliance-framework/agent v0.7.0/go.mod h1:k6sNhVQXviFHbz/Fe/jOkfBZ+AFLnRPIuOH2aaaCTNo= github.com/compliance-framework/api v0.16.0 h1:0HO5a5N80ktJLeLD5GVeTk7cK7PO9Xj5WN4SR+KGBH0= github.com/compliance-framework/api v0.16.0/go.mod h1:BupcN8mQFgB0/2+YShU/r4BUYoGwzSjbz2esdOUaX/4= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= diff --git a/internal/flow_log.go b/internal/flow_log.go new file mode 100644 index 0000000..772ada6 --- /dev/null +++ b/internal/flow_log.go @@ -0,0 +1,18 @@ +package internal + +import "github.com/aws/aws-sdk-go-v2/service/ec2/types" + +func EvaluateFlowLogPolicies(deps EvaluationDependencies, policyPaths []string, flowLogs []types.FlowLog, region string) ResourceEvaluationErrors { + return evaluateResources( + deps, + policyPaths, + flowLogs, + func(flowLog types.FlowLog) ResourceEvidenceContext { + flowLogCtx := BuildFlowLogEvidenceContext(flowLog, region) + return newResourceEvidenceContext(flowLogCtx.Labels, flowLogCtx.Subjects, flowLogCtx.Components, flowLogCtx.Inventory) + }, + buildRawResourceInput[types.FlowLog], + nil, + nil, + ) +} diff --git a/internal/internet_gateway.go b/internal/internet_gateway.go new file mode 100644 index 0000000..dac9b05 --- /dev/null +++ b/internal/internet_gateway.go @@ -0,0 +1,18 @@ +package internal + +import "github.com/aws/aws-sdk-go-v2/service/ec2/types" + +func EvaluateInternetGatewayPolicies(deps EvaluationDependencies, policyPaths []string, internetGateways []types.InternetGateway, region string) ResourceEvaluationErrors { + return evaluateResources( + deps, + policyPaths, + internetGateways, + func(internetGateway types.InternetGateway) ResourceEvidenceContext { + internetGatewayCtx := BuildInternetGatewayEvidenceContext(internetGateway, region) + return newResourceEvidenceContext(internetGatewayCtx.Labels, internetGatewayCtx.Subjects, internetGatewayCtx.Components, internetGatewayCtx.Inventory) + }, + buildRawResourceInput[types.InternetGateway], + nil, + nil, + ) +} diff --git a/internal/log_group.go b/internal/log_group.go new file mode 100644 index 0000000..dbc01de --- /dev/null +++ b/internal/log_group.go @@ -0,0 +1,18 @@ +package internal + +import cloudwatchlogstypes "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs/types" + +func EvaluateLogGroupPolicies(deps EvaluationDependencies, policyPaths []string, logGroups []cloudwatchlogstypes.LogGroup, region string) ResourceEvaluationErrors { + return evaluateResources( + deps, + policyPaths, + logGroups, + func(logGroup cloudwatchlogstypes.LogGroup) ResourceEvidenceContext { + logGroupCtx := BuildLogGroupEvidenceContext(logGroup, region) + return newResourceEvidenceContext(logGroupCtx.Labels, logGroupCtx.Subjects, logGroupCtx.Components, logGroupCtx.Inventory) + }, + buildRawResourceInput[cloudwatchlogstypes.LogGroup], + nil, + nil, + ) +} diff --git a/internal/network_acl.go b/internal/network_acl.go new file mode 100644 index 0000000..5540b78 --- /dev/null +++ b/internal/network_acl.go @@ -0,0 +1,18 @@ +package internal + +import "github.com/aws/aws-sdk-go-v2/service/ec2/types" + +func EvaluateNetworkAclPolicies(deps EvaluationDependencies, policyPaths []string, networkAcls []types.NetworkAcl, region string) ResourceEvaluationErrors { + return evaluateResources( + deps, + policyPaths, + networkAcls, + func(acl types.NetworkAcl) ResourceEvidenceContext { + aclCtx := BuildNetworkAclEvidenceContext(acl, region) + return newResourceEvidenceContext(aclCtx.Labels, aclCtx.Subjects, aclCtx.Components, aclCtx.Inventory) + }, + buildRawResourceInput[types.NetworkAcl], + nil, + nil, + ) +} diff --git a/internal/pagination.go b/internal/pagination.go index 60e6ec3..0d0049e 100644 --- a/internal/pagination.go +++ b/internal/pagination.go @@ -67,6 +67,25 @@ func PaginatedDescribeSecurityGroups(ctx context.Context, client *ec2.Client) it } } +// PaginatedDescribeNetworkInterfaces returns an iterator over all network interfaces in the account/region +func PaginatedDescribeNetworkInterfaces(ctx context.Context, client *ec2.Client) iter.Seq2[types.NetworkInterface, error] { + return func(yield func(types.NetworkInterface, error) bool) { + paginator := ec2.NewDescribeNetworkInterfacesPaginator(client, &ec2.DescribeNetworkInterfacesInput{}) + for paginator.HasMorePages() { + page, err := paginator.NextPage(ctx) + if err != nil { + yield(types.NetworkInterface{}, err) + return + } + for _, networkInterface := range page.NetworkInterfaces { + if !yield(networkInterface, nil) { + return + } + } + } + } +} + // PaginatedDescribeNetworkAcls returns an iterator over all network ACLs in the account/region func PaginatedDescribeNetworkAcls(ctx context.Context, client *ec2.Client) iter.Seq2[types.NetworkAcl, error] { return func(yield func(types.NetworkAcl, error) bool) { diff --git a/internal/policy_evaluation.go b/internal/policy_evaluation.go new file mode 100644 index 0000000..e6b6604 --- /dev/null +++ b/internal/policy_evaluation.go @@ -0,0 +1,128 @@ +package internal + +import ( + "context" + "errors" + "strings" + + policyManager "github.com/compliance-framework/agent/policy-manager" + "github.com/compliance-framework/agent/runner" + "github.com/compliance-framework/agent/runner/proto" + "github.com/hashicorp/go-hclog" +) + +type EvaluationDependencies struct { + Context context.Context + Logger hclog.Logger + ApiHelper runner.ApiHelper + Actors []*proto.OriginActor + PolicyData map[string]interface{} +} + +type ResourceEvidenceContext struct { + Labels map[string]string + Components []*proto.Component + Inventory []*proto.InventoryItem + Subjects []*proto.Subject +} + +type ResourceEvaluationErrors struct { + Fatal error + NonFatal error + InputBuildFailure bool +} + +func evaluateResources[T any](deps EvaluationDependencies, policyPaths []string, resources []T, buildContext func(T) ResourceEvidenceContext, buildInput func(T) (interface{}, error), onInputError func(T, error), afterGenerate func([]*proto.Evidence, T)) ResourceEvaluationErrors { + var accumulatedErrors error + inputBuildFailure := false + + for _, resource := range resources { + resourceCtx := buildContext(resource) + input, err := buildInput(resource) + if err != nil { + inputBuildFailure = true + if onInputError != nil { + onInputError(resource, err) + } + accumulatedErrors = errors.Join(accumulatedErrors, err) + continue + } + + evidences, err := generateResourceEvidences(deps.Context, deps.Logger, deps.Actors, policyPaths, input, resourceCtx.Labels, resourceCtx.Subjects, resourceCtx.Components, resourceCtx.Inventory, deps.PolicyData) + if afterGenerate != nil { + afterGenerate(evidences, resource) + } + if err != nil { + accumulatedErrors = errors.Join(accumulatedErrors, err) + } + if len(evidences) == 0 { + continue + } + if err = deps.ApiHelper.CreateEvidence(deps.Context, evidences); err != nil { + deps.Logger.Error("Failed to send evidences", "error", err) + return ResourceEvaluationErrors{Fatal: err, NonFatal: accumulatedErrors, InputBuildFailure: inputBuildFailure} + } + } + + return ResourceEvaluationErrors{NonFatal: accumulatedErrors, InputBuildFailure: inputBuildFailure} +} + +func newResourceEvidenceContext(labels map[string]string, subjects []*proto.Subject, components []*proto.Component, inventory []*proto.InventoryItem) ResourceEvidenceContext { + return ResourceEvidenceContext{ + Labels: labels, + Components: components, + Inventory: inventory, + Subjects: subjects, + } +} + +func buildRawResourceInput[T any](resource T) (interface{}, error) { + return resource, nil +} + +func generateResourceEvidences(ctx context.Context, logger hclog.Logger, actors []*proto.OriginActor, policyPaths []string, input interface{}, labels map[string]string, subjects []*proto.Subject, components []*proto.Component, inventory []*proto.InventoryItem, policyData map[string]interface{}) ([]*proto.Evidence, error) { + activities := make([]*proto.Activity, 0) + evidences := make([]*proto.Evidence, 0) + var accumulatedErrors error + + for _, policyPath := range policyPaths { + processor := policyManager.NewPolicyProcessor( + logger, + labels, + subjects, + components, + inventory, + actors, + activities, + policyData, + ) + evidence, err := processor.GenerateResults(ctx, policyPath, input) + evidences = append(evidences, evidence...) + if err != nil { + accumulatedErrors = errors.Join(accumulatedErrors, err) + } + } + + return evidences, accumulatedErrors +} + +func PrefixSecurityGroupEvidenceTitles(evidences []*proto.Evidence, groupName string) { + groupName = strings.TrimSpace(groupName) + if groupName == "" { + return + } + + for _, evidence := range evidences { + if evidence == nil { + continue + } + + title := strings.TrimSpace(evidence.Title) + if title == "" { + evidence.Title = groupName + continue + } + + evidence.Title = groupName + " | " + title + } +} diff --git a/internal/region_datasets.go b/internal/region_datasets.go new file mode 100644 index 0000000..d7cc568 --- /dev/null +++ b/internal/region_datasets.go @@ -0,0 +1,134 @@ +package internal + +import ( + "context" + "iter" + + cloudwatchlogs "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs" + cloudwatchlogstypes "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs/types" + "github.com/aws/aws-sdk-go-v2/service/ec2" + "github.com/aws/aws-sdk-go-v2/service/ec2/types" + "github.com/hashicorp/go-hclog" +) + +type RegionDatasets struct { + Vpcs []types.Vpc + Subnets []types.Subnet + SecurityGroups []types.SecurityGroup + NetworkInterfaces []types.NetworkInterface + NetworkAcls []types.NetworkAcl + RouteTables []types.RouteTable + InternetGateways []types.InternetGateway + VpcEndpoints []types.VpcEndpoint + FlowLogs []types.FlowLog + LogGroups []cloudwatchlogstypes.LogGroup + TransitGatewayAttachments []types.TransitGatewayAttachment +} + +func CollectRegionDatasets(ctx context.Context, logger hclog.Logger, client *ec2.Client, logsClient *cloudwatchlogs.Client, requiredDatasets map[string]bool) (RegionDatasets, error) { + var ( + datasets RegionDatasets + err error + ) + + if requiredDatasets["vpcs"] { + datasets.Vpcs, err = collectSequence(PaginatedDescribeVpcs(ctx, client)) + if err != nil { + logger.Error("unable to get VPC", "error", err) + return RegionDatasets{}, err + } + } + + if requiredDatasets["subnets"] { + datasets.Subnets, err = collectSequence(PaginatedDescribeSubnets(ctx, client)) + if err != nil { + logger.Error("unable to get Subnet", "error", err) + return RegionDatasets{}, err + } + } + + if requiredDatasets["security_groups"] { + datasets.SecurityGroups, err = collectSequence(PaginatedDescribeSecurityGroups(ctx, client)) + if err != nil { + logger.Error("unable to get Security Group", "error", err) + return RegionDatasets{}, err + } + } + + if requiredDatasets["network_interfaces"] { + datasets.NetworkInterfaces, err = collectSequence(PaginatedDescribeNetworkInterfaces(ctx, client)) + if err != nil { + logger.Error("unable to get Network Interface", "error", err) + return RegionDatasets{}, err + } + } + + if requiredDatasets["network_acls"] { + datasets.NetworkAcls, err = collectSequence(PaginatedDescribeNetworkAcls(ctx, client)) + if err != nil { + logger.Error("unable to get Network ACL", "error", err) + return RegionDatasets{}, err + } + } + + if requiredDatasets["route_tables"] { + datasets.RouteTables, err = collectSequence(PaginatedDescribeRouteTables(ctx, client)) + if err != nil { + logger.Error("unable to get Route Table", "error", err) + return RegionDatasets{}, err + } + } + + if requiredDatasets["internet_gateways"] { + datasets.InternetGateways, err = collectSequence(PaginatedDescribeInternetGateways(ctx, client)) + if err != nil { + logger.Error("unable to get Internet Gateway", "error", err) + return RegionDatasets{}, err + } + } + + if requiredDatasets["vpc_endpoints"] { + datasets.VpcEndpoints, err = collectSequence(PaginatedDescribeVpcEndpoints(ctx, client)) + if err != nil { + logger.Error("unable to get VPC Endpoint", "error", err) + return RegionDatasets{}, err + } + } + + if requiredDatasets["flow_logs"] { + datasets.FlowLogs, err = collectSequence(PaginatedDescribeFlowLogs(ctx, client)) + if err != nil { + logger.Error("unable to get Flow Log", "error", err) + return RegionDatasets{}, err + } + } + + if requiredDatasets["log_groups"] { + datasets.LogGroups, err = collectSequence(PaginatedDescribeLogGroups(ctx, logsClient)) + if err != nil { + logger.Error("unable to get Log Group", "error", err) + return RegionDatasets{}, err + } + } + + if requiredDatasets["transit_gateway_attachments"] { + datasets.TransitGatewayAttachments, err = collectSequence(PaginatedDescribeTransitGatewayAttachments(ctx, client)) + if err != nil { + logger.Error("unable to get Transit Gateway Attachment", "error", err) + return RegionDatasets{}, err + } + } + + return datasets, nil +} + +func collectSequence[T any](seq iter.Seq2[T, error]) ([]T, error) { + items := make([]T, 0) + for item, err := range seq { + if err != nil { + return nil, err + } + items = append(items, item) + } + return items, nil +} diff --git a/internal/route_table.go b/internal/route_table.go new file mode 100644 index 0000000..fa42e8b --- /dev/null +++ b/internal/route_table.go @@ -0,0 +1,18 @@ +package internal + +import "github.com/aws/aws-sdk-go-v2/service/ec2/types" + +func EvaluateRouteTablePolicies(deps EvaluationDependencies, policyPaths []string, routeTables []types.RouteTable, region string) ResourceEvaluationErrors { + return evaluateResources( + deps, + policyPaths, + routeTables, + func(routeTable types.RouteTable) ResourceEvidenceContext { + routeTableCtx := BuildRouteTableEvidenceContext(routeTable, region) + return newResourceEvidenceContext(routeTableCtx.Labels, routeTableCtx.Subjects, routeTableCtx.Components, routeTableCtx.Inventory) + }, + buildRawResourceInput[types.RouteTable], + nil, + nil, + ) +} diff --git a/internal/security_group.go b/internal/security_group.go new file mode 100644 index 0000000..5eb8b75 --- /dev/null +++ b/internal/security_group.go @@ -0,0 +1,299 @@ +package internal + +import ( + "encoding/json" + + "github.com/aws/aws-sdk-go-v2/aws" + cloudwatchlogstypes "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs/types" + "github.com/aws/aws-sdk-go-v2/service/ec2/types" + "github.com/compliance-framework/agent/runner/proto" +) + +func EvaluateSecurityGroupPolicies(deps EvaluationDependencies, policyPaths []string, securityGroups []types.SecurityGroup, region string, datasets RegionDatasets) ResourceEvaluationErrors { + return evaluateResources( + deps, + policyPaths, + securityGroups, + func(group types.SecurityGroup) ResourceEvidenceContext { + sgCtx := BuildSecurityGroupEvidenceContext(group, region) + return newResourceEvidenceContext(sgCtx.Labels, sgCtx.Subjects, sgCtx.Components, sgCtx.Inventory) + }, + func(group types.SecurityGroup) (interface{}, error) { + return BuildSecurityGroupPolicyInput(group, region, datasets) + }, + func(group types.SecurityGroup, err error) { + deps.Logger.Error("unable to build security group policy input", "group_id", aws.ToString(group.GroupId), "region", region, "error", err) + }, + func(evidences []*proto.Evidence, group types.SecurityGroup) { + PrefixSecurityGroupEvidenceTitles(evidences, aws.ToString(group.GroupName)) + }, + ) +} + +func BuildSecurityGroupPolicyInput(group types.SecurityGroup, region string, datasets RegionDatasets) (map[string]interface{}, error) { + securityGroupValue, err := toInterfaceMap(group) + if err != nil { + return nil, err + } + + contextValue, err := toInterfaceMap(buildSecurityGroupSupplementaryContext(group, region, datasets)) + if err != nil { + return nil, err + } + + return map[string]interface{}{ + "security_group": securityGroupValue, + "sg_context": contextValue, + }, nil +} + +func buildSecurityGroupSupplementaryContext(group types.SecurityGroup, region string, datasets RegionDatasets) map[string]interface{} { + groupID := aws.ToString(group.GroupId) + vpcID := aws.ToString(group.VpcId) + attachedNetworkInterfaces := filterNetworkInterfacesBySecurityGroup(datasets.NetworkInterfaces, groupID) + attachedSubnetIDs := subnetIDsFromNetworkInterfaces(attachedNetworkInterfaces) + attachedNetworkInterfaceIDs := networkInterfaceIDs(attachedNetworkInterfaces) + + return map[string]interface{}{ + "current": map[string]interface{}{ + "group_id": groupID, + "group_name": aws.ToString(group.GroupName), + "vpc_id": vpcID, + "region": region, + "is_default": aws.ToString(group.GroupName) == "default", + "tags_present": len(group.Tags) > 0, + }, + "vpc": findVpcByID(datasets.Vpcs, vpcID), + "security_groups_in_vpc": filterSecurityGroupsByVpc(datasets.SecurityGroups, vpcID), + "attached_network_interfaces": attachedNetworkInterfaces, + "attached_subnets": filterSubnetsByIDs(datasets.Subnets, attachedSubnetIDs), + "route_tables_in_vpc": filterRouteTablesByVpc(datasets.RouteTables, vpcID), + "route_tables_for_attached_subnets": filterRouteTablesForSubnetIDs(datasets.RouteTables, vpcID, attachedSubnetIDs), + "network_acls_in_vpc": filterNetworkAclsByVpc(datasets.NetworkAcls, vpcID), + "network_acls_for_attached_subnets": filterNetworkAclsForSubnetIDs(datasets.NetworkAcls, attachedSubnetIDs), + "internet_gateways_for_vpc": filterInternetGatewaysByVpc(datasets.InternetGateways, vpcID), + "vpc_endpoints_for_vpc": filterVpcEndpointsByVpc(datasets.VpcEndpoints, vpcID), + "flow_logs_for_related_resources": filterFlowLogsByResourceIDs(datasets.FlowLogs, combineIDSets(singletonIDSet(vpcID), attachedSubnetIDs, attachedNetworkInterfaceIDs)), + "log_groups_for_related_flow_logs": filterLogGroupsForFlowLogs(datasets.LogGroups, datasets.FlowLogs, combineIDSets(singletonIDSet(vpcID), attachedSubnetIDs, attachedNetworkInterfaceIDs)), + "transit_gateway_attachments_for_vpc": filterTransitGatewayAttachmentsByResourceID(datasets.TransitGatewayAttachments, vpcID), + } +} + +func toInterfaceMap(value interface{}) (map[string]interface{}, error) { + content, err := json.Marshal(value) + if err != nil { + return nil, err + } + + result := make(map[string]interface{}) + if err := json.Unmarshal(content, &result); err != nil { + return nil, err + } + + return result, nil +} + +func singletonIDSet(id string) map[string]bool { + ids := make(map[string]bool) + if id != "" { + ids[id] = true + } + return ids +} + +func combineIDSets(sets ...map[string]bool) map[string]bool { + combined := make(map[string]bool) + for _, set := range sets { + for id := range set { + combined[id] = true + } + } + return combined +} + +func filterNetworkInterfacesBySecurityGroup(networkInterfaces []types.NetworkInterface, groupID string) []types.NetworkInterface { + filtered := make([]types.NetworkInterface, 0) + for _, networkInterface := range networkInterfaces { + for _, group := range networkInterface.Groups { + if aws.ToString(group.GroupId) == groupID { + filtered = append(filtered, networkInterface) + break + } + } + } + return filtered +} + +func subnetIDsFromNetworkInterfaces(networkInterfaces []types.NetworkInterface) map[string]bool { + ids := make(map[string]bool) + for _, networkInterface := range networkInterfaces { + subnetID := aws.ToString(networkInterface.SubnetId) + if subnetID != "" { + ids[subnetID] = true + } + } + return ids +} + +func networkInterfaceIDs(networkInterfaces []types.NetworkInterface) map[string]bool { + ids := make(map[string]bool) + for _, networkInterface := range networkInterfaces { + networkInterfaceID := aws.ToString(networkInterface.NetworkInterfaceId) + if networkInterfaceID != "" { + ids[networkInterfaceID] = true + } + } + return ids +} + +func findVpcByID(vpcs []types.Vpc, vpcID string) *types.Vpc { + for _, vpc := range vpcs { + if aws.ToString(vpc.VpcId) == vpcID { + vpcCopy := vpc + return &vpcCopy + } + } + return nil +} + +func filterSecurityGroupsByVpc(securityGroups []types.SecurityGroup, vpcID string) []types.SecurityGroup { + filtered := make([]types.SecurityGroup, 0) + for _, securityGroup := range securityGroups { + if aws.ToString(securityGroup.VpcId) == vpcID { + filtered = append(filtered, securityGroup) + } + } + return filtered +} + +func filterSubnetsByIDs(subnets []types.Subnet, subnetIDs map[string]bool) []types.Subnet { + filtered := make([]types.Subnet, 0) + for _, subnet := range subnets { + if subnetIDs[aws.ToString(subnet.SubnetId)] { + filtered = append(filtered, subnet) + } + } + return filtered +} + +func filterRouteTablesByVpc(routeTables []types.RouteTable, vpcID string) []types.RouteTable { + filtered := make([]types.RouteTable, 0) + for _, routeTable := range routeTables { + if aws.ToString(routeTable.VpcId) == vpcID { + filtered = append(filtered, routeTable) + } + } + return filtered +} + +func filterRouteTablesForSubnetIDs(routeTables []types.RouteTable, vpcID string, subnetIDs map[string]bool) []types.RouteTable { + filtered := make([]types.RouteTable, 0) + for _, routeTable := range routeTables { + if aws.ToString(routeTable.VpcId) != vpcID { + continue + } + + include := false + for _, association := range routeTable.Associations { + if subnetIDs[aws.ToString(association.SubnetId)] || aws.ToBool(association.Main) { + include = true + break + } + } + if include { + filtered = append(filtered, routeTable) + } + } + return filtered +} + +func filterNetworkAclsByVpc(networkAcls []types.NetworkAcl, vpcID string) []types.NetworkAcl { + filtered := make([]types.NetworkAcl, 0) + for _, networkAcl := range networkAcls { + if aws.ToString(networkAcl.VpcId) == vpcID { + filtered = append(filtered, networkAcl) + } + } + return filtered +} + +func filterNetworkAclsForSubnetIDs(networkAcls []types.NetworkAcl, subnetIDs map[string]bool) []types.NetworkAcl { + filtered := make([]types.NetworkAcl, 0) + for _, networkAcl := range networkAcls { + include := false + for _, association := range networkAcl.Associations { + if subnetIDs[aws.ToString(association.SubnetId)] { + include = true + break + } + } + if include { + filtered = append(filtered, networkAcl) + } + } + return filtered +} + +func filterInternetGatewaysByVpc(internetGateways []types.InternetGateway, vpcID string) []types.InternetGateway { + filtered := make([]types.InternetGateway, 0) + for _, internetGateway := range internetGateways { + for _, attachment := range internetGateway.Attachments { + if aws.ToString(attachment.VpcId) == vpcID { + filtered = append(filtered, internetGateway) + break + } + } + } + return filtered +} + +func filterVpcEndpointsByVpc(vpcEndpoints []types.VpcEndpoint, vpcID string) []types.VpcEndpoint { + filtered := make([]types.VpcEndpoint, 0) + for _, vpcEndpoint := range vpcEndpoints { + if aws.ToString(vpcEndpoint.VpcId) == vpcID { + filtered = append(filtered, vpcEndpoint) + } + } + return filtered +} + +func filterFlowLogsByResourceIDs(flowLogs []types.FlowLog, resourceIDs map[string]bool) []types.FlowLog { + filtered := make([]types.FlowLog, 0) + for _, flowLog := range flowLogs { + if resourceIDs[aws.ToString(flowLog.ResourceId)] { + filtered = append(filtered, flowLog) + } + } + return filtered +} + +func filterLogGroupsForFlowLogs(logGroups []cloudwatchlogstypes.LogGroup, flowLogs []types.FlowLog, resourceIDs map[string]bool) []cloudwatchlogstypes.LogGroup { + logGroupNames := make(map[string]bool) + for _, flowLog := range flowLogs { + if !resourceIDs[aws.ToString(flowLog.ResourceId)] { + continue + } + logGroupName := aws.ToString(flowLog.LogGroupName) + if logGroupName != "" { + logGroupNames[logGroupName] = true + } + } + + filtered := make([]cloudwatchlogstypes.LogGroup, 0) + for _, logGroup := range logGroups { + if logGroupNames[aws.ToString(logGroup.LogGroupName)] { + filtered = append(filtered, logGroup) + } + } + return filtered +} + +func filterTransitGatewayAttachmentsByResourceID(attachments []types.TransitGatewayAttachment, resourceID string) []types.TransitGatewayAttachment { + filtered := make([]types.TransitGatewayAttachment, 0) + for _, attachment := range attachments { + if aws.ToString(attachment.ResourceId) == resourceID { + filtered = append(filtered, attachment) + } + } + return filtered +} diff --git a/internal/subnet.go b/internal/subnet.go new file mode 100644 index 0000000..aa40398 --- /dev/null +++ b/internal/subnet.go @@ -0,0 +1,18 @@ +package internal + +import "github.com/aws/aws-sdk-go-v2/service/ec2/types" + +func EvaluateSubnetPolicies(deps EvaluationDependencies, policyPaths []string, subnets []types.Subnet, region string) ResourceEvaluationErrors { + return evaluateResources( + deps, + policyPaths, + subnets, + func(subnet types.Subnet) ResourceEvidenceContext { + subnetCtx := BuildSubnetEvidenceContext(subnet, region) + return newResourceEvidenceContext(subnetCtx.Labels, subnetCtx.Subjects, subnetCtx.Components, subnetCtx.Inventory) + }, + buildRawResourceInput[types.Subnet], + nil, + nil, + ) +} diff --git a/internal/vpc.go b/internal/vpc.go new file mode 100644 index 0000000..df49ca0 --- /dev/null +++ b/internal/vpc.go @@ -0,0 +1,18 @@ +package internal + +import "github.com/aws/aws-sdk-go-v2/service/ec2/types" + +func EvaluateVpcPolicies(deps EvaluationDependencies, policyPaths []string, vpcs []types.Vpc, region string) ResourceEvaluationErrors { + return evaluateResources( + deps, + policyPaths, + vpcs, + func(vpc types.Vpc) ResourceEvidenceContext { + vpcCtx := BuildVpcEvidenceContext(vpc, region) + return newResourceEvidenceContext(vpcCtx.Labels, vpcCtx.Subjects, vpcCtx.Components, vpcCtx.Inventory) + }, + buildRawResourceInput[types.Vpc], + nil, + nil, + ) +} diff --git a/internal/vpc_endpoint.go b/internal/vpc_endpoint.go new file mode 100644 index 0000000..50cdef6 --- /dev/null +++ b/internal/vpc_endpoint.go @@ -0,0 +1,18 @@ +package internal + +import "github.com/aws/aws-sdk-go-v2/service/ec2/types" + +func EvaluateVpcEndpointPolicies(deps EvaluationDependencies, policyPaths []string, vpcEndpoints []types.VpcEndpoint, region string) ResourceEvaluationErrors { + return evaluateResources( + deps, + policyPaths, + vpcEndpoints, + func(vpcEndpoint types.VpcEndpoint) ResourceEvidenceContext { + vpcEndpointCtx := BuildVpcEndpointEvidenceContext(vpcEndpoint, region) + return newResourceEvidenceContext(vpcEndpointCtx.Labels, vpcEndpointCtx.Subjects, vpcEndpointCtx.Components, vpcEndpointCtx.Inventory) + }, + buildRawResourceInput[types.VpcEndpoint], + nil, + nil, + ) +} diff --git a/main.go b/main.go index 257b6c4..b08584d 100644 --- a/main.go +++ b/main.go @@ -3,12 +3,10 @@ package main import ( "context" "errors" - "slices" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs" "github.com/aws/aws-sdk-go-v2/service/ec2" - policyManager "github.com/compliance-framework/agent/policy-manager" "github.com/compliance-framework/agent/runner" "github.com/compliance-framework/agent/runner/proto" "github.com/compliance-framework/plugin-aws-networking-security/internal" @@ -17,12 +15,18 @@ import ( ) type CompliancePlugin struct { - logger hclog.Logger - config map[string]string + logger hclog.Logger + config map[string]string + policyData map[string]interface{} } func (l *CompliancePlugin) Configure(req *proto.ConfigureRequest) (*proto.ConfigureResponse, error) { l.config = req.GetConfig() + if req.GetPolicyData() != nil { + l.policyData = req.GetPolicyData().AsMap() + } else { + l.policyData = nil + } return &proto.ConfigureResponse{}, nil } @@ -64,6 +68,20 @@ func (l *CompliancePlugin) Eval(request *proto.EvalRequest, apiHelper runner.Api }, } + defaultBehaviorMapping := map[string][]string{ + "aws-vpc-sg-policies": {"sg"}, + } + policyEval := request.WithDefaultPolicyBehavior(defaultBehaviorMapping) + policyPathsByBehavior := buildPolicyPathsByBehavior(policyEval) + requiredDatasets := buildRequiredDatasets(policyPathsByBehavior) + deps := internal.EvaluationDependencies{ + Context: ctx, + Logger: l.logger, + ApiHelper: apiHelper, + Actors: actors, + PolicyData: l.policyData, + } + // Iterate over each configured region for _, region := range regions { l.logger.Info("Collecting resources in region", "region", region) @@ -79,379 +97,154 @@ func (l *CompliancePlugin) Eval(request *proto.EvalRequest, apiHelper runner.Api client := ec2.NewFromConfig(cfg) logsClient := cloudwatchlogs.NewFromConfig(cfg) - // Collect and evaluate VPCs - for vpc, err := range internal.PaginatedDescribeVpcs(ctx, client) { - if err != nil { - l.logger.Error("unable to get VPC", "error", err) - evalStatus = proto.ExecutionStatus_FAILURE - accumulatedErrors = errors.Join(accumulatedErrors, err) - break - } - - vpcCtx := internal.BuildVpcEvidenceContext(vpc, region) - activities := make([]*proto.Activity, 0) - evidences := make([]*proto.Evidence, 0) - - for _, policyPath := range request.GetPolicyPaths() { - processor := policyManager.NewPolicyProcessor( - l.logger, - internal.MergeMaps( - vpcCtx.Labels, - map[string]string{}, - ), - vpcCtx.Subjects, - vpcCtx.Components, - vpcCtx.Inventory, - actors, - activities, - ) - evidence, err := processor.GenerateResults(ctx, policyPath, vpc) - evidences = slices.Concat(evidences, evidence) - if err != nil { - accumulatedErrors = errors.Join(accumulatedErrors, err) - } - } - - if err = apiHelper.CreateEvidence(ctx, evidences); err != nil { - l.logger.Error("Failed to send evidences", "error", err) - return &proto.EvalResponse{ - Status: proto.ExecutionStatus_FAILURE, - }, err - } + datasets, err := internal.CollectRegionDatasets(ctx, l.logger, client, logsClient, requiredDatasets) + if err != nil { + evalStatus = proto.ExecutionStatus_FAILURE + accumulatedErrors = errors.Join(accumulatedErrors, err) + continue } - // Collect and evaluate Subnets - for subnet, err := range internal.PaginatedDescribeSubnets(ctx, client) { - if err != nil { - l.logger.Error("unable to get Subnet", "error", err) - evalStatus = proto.ExecutionStatus_FAILURE - accumulatedErrors = errors.Join(accumulatedErrors, err) - break - } - - subnetCtx := internal.BuildSubnetEvidenceContext(subnet, region) - activities := make([]*proto.Activity, 0) - evidences := make([]*proto.Evidence, 0) - - for _, policyPath := range request.GetPolicyPaths() { - processor := policyManager.NewPolicyProcessor( - l.logger, - internal.MergeMaps( - subnetCtx.Labels, - map[string]string{}, - ), - subnetCtx.Subjects, - subnetCtx.Components, - subnetCtx.Inventory, - actors, - activities, - ) - evidence, err := processor.GenerateResults(ctx, policyPath, subnet) - evidences = slices.Concat(evidences, evidence) - if err != nil { - accumulatedErrors = errors.Join(accumulatedErrors, err) - } - } - - if err = apiHelper.CreateEvidence(ctx, evidences); err != nil { - l.logger.Error("Failed to send evidences", "error", err) - return &proto.EvalResponse{ - Status: proto.ExecutionStatus_FAILURE, - }, err + if vpcPolicyPaths := policyPathsByBehavior["vpc"]; len(vpcPolicyPaths) > 0 { + result := internal.EvaluateVpcPolicies(deps, vpcPolicyPaths, datasets.Vpcs, region) + if fatal := applyResourceEvaluationErrors(result, &evalStatus, &accumulatedErrors, false); fatal != nil { + return &proto.EvalResponse{Status: proto.ExecutionStatus_FAILURE}, fatal } } - // Collect and evaluate Security Groups - for group, err := range internal.PaginatedDescribeSecurityGroups(ctx, client) { - if err != nil { - l.logger.Error("unable to get Security Group", "error", err) - evalStatus = proto.ExecutionStatus_FAILURE - accumulatedErrors = errors.Join(accumulatedErrors, err) - break - } - - sgCtx := internal.BuildSecurityGroupEvidenceContext(group, region) - activities := make([]*proto.Activity, 0) - evidences := make([]*proto.Evidence, 0) - - for _, policyPath := range request.GetPolicyPaths() { - processor := policyManager.NewPolicyProcessor( - l.logger, - internal.MergeMaps( - sgCtx.Labels, - map[string]string{}, - ), - sgCtx.Subjects, - sgCtx.Components, - sgCtx.Inventory, - actors, - activities, - ) - evidence, err := processor.GenerateResults(ctx, policyPath, group) - evidences = slices.Concat(evidences, evidence) - if err != nil { - accumulatedErrors = errors.Join(accumulatedErrors, err) - } - } - - if err = apiHelper.CreateEvidence(ctx, evidences); err != nil { - l.logger.Error("Failed to send evidences", "error", err) - return &proto.EvalResponse{ - Status: proto.ExecutionStatus_FAILURE, - }, err + if subnetPolicyPaths := policyPathsByBehavior["subnet"]; len(subnetPolicyPaths) > 0 { + result := internal.EvaluateSubnetPolicies(deps, subnetPolicyPaths, datasets.Subnets, region) + if fatal := applyResourceEvaluationErrors(result, &evalStatus, &accumulatedErrors, false); fatal != nil { + return &proto.EvalResponse{Status: proto.ExecutionStatus_FAILURE}, fatal } } - // Collect and evaluate Network ACLs - for acl, err := range internal.PaginatedDescribeNetworkAcls(ctx, client) { - if err != nil { - l.logger.Error("unable to get Network ACL", "error", err) - evalStatus = proto.ExecutionStatus_FAILURE - accumulatedErrors = errors.Join(accumulatedErrors, err) - break - } - - aclCtx := internal.BuildNetworkAclEvidenceContext(acl, region) - activities := make([]*proto.Activity, 0) - evidences := make([]*proto.Evidence, 0) - - for _, policyPath := range request.GetPolicyPaths() { - processor := policyManager.NewPolicyProcessor( - l.logger, - internal.MergeMaps( - aclCtx.Labels, - map[string]string{}, - ), - aclCtx.Subjects, - aclCtx.Components, - aclCtx.Inventory, - actors, - activities, - ) - evidence, err := processor.GenerateResults(ctx, policyPath, acl) - evidences = slices.Concat(evidences, evidence) - if err != nil { - accumulatedErrors = errors.Join(accumulatedErrors, err) - } - } - - if err = apiHelper.CreateEvidence(ctx, evidences); err != nil { - l.logger.Error("Failed to send evidences", "error", err) - return &proto.EvalResponse{ - Status: proto.ExecutionStatus_FAILURE, - }, err + if sgPolicyPaths := policyPathsByBehavior["sg"]; len(sgPolicyPaths) > 0 { + result := internal.EvaluateSecurityGroupPolicies(deps, sgPolicyPaths, datasets.SecurityGroups, region, datasets) + if fatal := applyResourceEvaluationErrors(result, &evalStatus, &accumulatedErrors, true); fatal != nil { + return &proto.EvalResponse{Status: proto.ExecutionStatus_FAILURE}, fatal } } - // Collect and evaluate Route Tables - for rt, err := range internal.PaginatedDescribeRouteTables(ctx, client) { - if err != nil { - l.logger.Error("unable to get Route Table", "error", err) - evalStatus = proto.ExecutionStatus_FAILURE - accumulatedErrors = errors.Join(accumulatedErrors, err) - break - } - - rtCtx := internal.BuildRouteTableEvidenceContext(rt, region) - activities := make([]*proto.Activity, 0) - evidences := make([]*proto.Evidence, 0) - - for _, policyPath := range request.GetPolicyPaths() { - processor := policyManager.NewPolicyProcessor( - l.logger, - internal.MergeMaps( - rtCtx.Labels, - map[string]string{}, - ), - rtCtx.Subjects, - rtCtx.Components, - rtCtx.Inventory, - actors, - activities, - ) - evidence, err := processor.GenerateResults(ctx, policyPath, rt) - evidences = slices.Concat(evidences, evidence) - if err != nil { - accumulatedErrors = errors.Join(accumulatedErrors, err) - } - } - - if err = apiHelper.CreateEvidence(ctx, evidences); err != nil { - l.logger.Error("Failed to send evidences", "error", err) - return &proto.EvalResponse{ - Status: proto.ExecutionStatus_FAILURE, - }, err + if aclPolicyPaths := policyPathsByBehavior["acl"]; len(aclPolicyPaths) > 0 { + result := internal.EvaluateNetworkAclPolicies(deps, aclPolicyPaths, datasets.NetworkAcls, region) + if fatal := applyResourceEvaluationErrors(result, &evalStatus, &accumulatedErrors, false); fatal != nil { + return &proto.EvalResponse{Status: proto.ExecutionStatus_FAILURE}, fatal } } - // Collect and evaluate Internet Gateways - for igw, err := range internal.PaginatedDescribeInternetGateways(ctx, client) { - if err != nil { - l.logger.Error("unable to get Internet Gateway", "error", err) - evalStatus = proto.ExecutionStatus_FAILURE - accumulatedErrors = errors.Join(accumulatedErrors, err) - break - } - - igwCtx := internal.BuildInternetGatewayEvidenceContext(igw, region) - activities := make([]*proto.Activity, 0) - evidences := make([]*proto.Evidence, 0) - - for _, policyPath := range request.GetPolicyPaths() { - processor := policyManager.NewPolicyProcessor( - l.logger, - internal.MergeMaps( - igwCtx.Labels, - map[string]string{}, - ), - igwCtx.Subjects, - igwCtx.Components, - igwCtx.Inventory, - actors, - activities, - ) - evidence, err := processor.GenerateResults(ctx, policyPath, igw) - evidences = slices.Concat(evidences, evidence) - if err != nil { - accumulatedErrors = errors.Join(accumulatedErrors, err) - } + if routeTablePolicyPaths := policyPathsByBehavior["rt"]; len(routeTablePolicyPaths) > 0 { + result := internal.EvaluateRouteTablePolicies(deps, routeTablePolicyPaths, datasets.RouteTables, region) + if fatal := applyResourceEvaluationErrors(result, &evalStatus, &accumulatedErrors, false); fatal != nil { + return &proto.EvalResponse{Status: proto.ExecutionStatus_FAILURE}, fatal } + } - if err = apiHelper.CreateEvidence(ctx, evidences); err != nil { - l.logger.Error("Failed to send evidences", "error", err) - return &proto.EvalResponse{ - Status: proto.ExecutionStatus_FAILURE, - }, err + if internetGatewayPolicyPaths := policyPathsByBehavior["igw"]; len(internetGatewayPolicyPaths) > 0 { + result := internal.EvaluateInternetGatewayPolicies(deps, internetGatewayPolicyPaths, datasets.InternetGateways, region) + if fatal := applyResourceEvaluationErrors(result, &evalStatus, &accumulatedErrors, false); fatal != nil { + return &proto.EvalResponse{Status: proto.ExecutionStatus_FAILURE}, fatal } } - // Collect and evaluate VPC Endpoints - for endpoint, err := range internal.PaginatedDescribeVpcEndpoints(ctx, client) { - if err != nil { - l.logger.Error("unable to get VPC Endpoint", "error", err) - evalStatus = proto.ExecutionStatus_FAILURE - accumulatedErrors = errors.Join(accumulatedErrors, err) - break + if vpcEndpointPolicyPaths := policyPathsByBehavior["endpoint"]; len(vpcEndpointPolicyPaths) > 0 { + result := internal.EvaluateVpcEndpointPolicies(deps, vpcEndpointPolicyPaths, datasets.VpcEndpoints, region) + if fatal := applyResourceEvaluationErrors(result, &evalStatus, &accumulatedErrors, false); fatal != nil { + return &proto.EvalResponse{Status: proto.ExecutionStatus_FAILURE}, fatal } + } - endpointCtx := internal.BuildVpcEndpointEvidenceContext(endpoint, region) - activities := make([]*proto.Activity, 0) - evidences := make([]*proto.Evidence, 0) - - for _, policyPath := range request.GetPolicyPaths() { - processor := policyManager.NewPolicyProcessor( - l.logger, - internal.MergeMaps( - endpointCtx.Labels, - map[string]string{}, - ), - endpointCtx.Subjects, - endpointCtx.Components, - endpointCtx.Inventory, - actors, - activities, - ) - evidence, err := processor.GenerateResults(ctx, policyPath, endpoint) - evidences = slices.Concat(evidences, evidence) - if err != nil { - accumulatedErrors = errors.Join(accumulatedErrors, err) - } + if flowLogPolicyPaths := policyPathsByBehavior["flow-log"]; len(flowLogPolicyPaths) > 0 { + result := internal.EvaluateFlowLogPolicies(deps, flowLogPolicyPaths, datasets.FlowLogs, region) + if fatal := applyResourceEvaluationErrors(result, &evalStatus, &accumulatedErrors, false); fatal != nil { + return &proto.EvalResponse{Status: proto.ExecutionStatus_FAILURE}, fatal } + } - if err = apiHelper.CreateEvidence(ctx, evidences); err != nil { - l.logger.Error("Failed to send evidences", "error", err) - return &proto.EvalResponse{ - Status: proto.ExecutionStatus_FAILURE, - }, err + if logGroupPolicyPaths := policyPathsByBehavior["log-group"]; len(logGroupPolicyPaths) > 0 { + result := internal.EvaluateLogGroupPolicies(deps, logGroupPolicyPaths, datasets.LogGroups, region) + if fatal := applyResourceEvaluationErrors(result, &evalStatus, &accumulatedErrors, false); fatal != nil { + return &proto.EvalResponse{Status: proto.ExecutionStatus_FAILURE}, fatal } } + } - // Collect and evaluate Flow Logs - for flowLog, err := range internal.PaginatedDescribeFlowLogs(ctx, client) { - if err != nil { - l.logger.Error("unable to get Flow Log", "error", err) - evalStatus = proto.ExecutionStatus_FAILURE - accumulatedErrors = errors.Join(accumulatedErrors, err) - break - } + return &proto.EvalResponse{ + Status: evalStatus, + }, accumulatedErrors +} - flowLogCtx := internal.BuildFlowLogEvidenceContext(flowLog, region) - activities := make([]*proto.Activity, 0) - evidences := make([]*proto.Evidence, 0) - - for _, policyPath := range request.GetPolicyPaths() { - processor := policyManager.NewPolicyProcessor( - l.logger, - internal.MergeMaps( - flowLogCtx.Labels, - map[string]string{}, - ), - flowLogCtx.Subjects, - flowLogCtx.Components, - flowLogCtx.Inventory, - actors, - activities, - ) - evidence, err := processor.GenerateResults(ctx, policyPath, flowLog) - evidences = slices.Concat(evidences, evidence) - if err != nil { - accumulatedErrors = errors.Join(accumulatedErrors, err) - } - } +func applyResourceEvaluationErrors(result internal.ResourceEvaluationErrors, evalStatus *proto.ExecutionStatus, accumulatedErrors *error, failOnInputBuild bool) error { + if result.NonFatal != nil { + *accumulatedErrors = errors.Join(*accumulatedErrors, result.NonFatal) + } + if failOnInputBuild && result.InputBuildFailure { + *evalStatus = proto.ExecutionStatus_FAILURE + } + return result.Fatal +} - if err = apiHelper.CreateEvidence(ctx, evidences); err != nil { - l.logger.Error("Failed to send evidences", "error", err) - return &proto.EvalResponse{ - Status: proto.ExecutionStatus_FAILURE, - }, err - } - } +func supportedPolicyBehaviors() []string { + return []string{ + "vpc", + "subnet", + "sg", + "acl", + "rt", + "igw", + "endpoint", + "flow-log", + "log-group", + } +} - // Collect and evaluate Log Groups - for logGroup, err := range internal.PaginatedDescribeLogGroups(ctx, logsClient) { - if err != nil { - l.logger.Error("unable to get Log Group", "error", err) - evalStatus = proto.ExecutionStatus_FAILURE - accumulatedErrors = errors.Join(accumulatedErrors, err) - break - } +func buildPolicyPathsByBehavior(request *proto.EvalRequest) map[string][]string { + policyPathsByBehavior := make(map[string][]string) + for _, behavior := range supportedPolicyBehaviors() { + policyPaths := request.PolicyPathsForBehavior(behavior) + if len(policyPaths) > 0 { + policyPathsByBehavior[behavior] = policyPaths + } + } + return policyPathsByBehavior +} - logGroupCtx := internal.BuildLogGroupEvidenceContext(logGroup, region) - activities := make([]*proto.Activity, 0) - evidences := make([]*proto.Evidence, 0) - - for _, policyPath := range request.GetPolicyPaths() { - processor := policyManager.NewPolicyProcessor( - l.logger, - internal.MergeMaps( - logGroupCtx.Labels, - map[string]string{}, - ), - logGroupCtx.Subjects, - logGroupCtx.Components, - logGroupCtx.Inventory, - actors, - activities, - ) - evidence, err := processor.GenerateResults(ctx, policyPath, logGroup) - evidences = slices.Concat(evidences, evidence) - if err != nil { - accumulatedErrors = errors.Join(accumulatedErrors, err) - } - } +func buildRequiredDatasets(policyPathsByBehavior map[string][]string) map[string]bool { + requiredDatasets := make(map[string]bool) + for behavior, policyPaths := range policyPathsByBehavior { + if len(policyPaths) == 0 { + continue + } - if err = apiHelper.CreateEvidence(ctx, evidences); err != nil { - l.logger.Error("Failed to send evidences", "error", err) - return &proto.EvalResponse{ - Status: proto.ExecutionStatus_FAILURE, - }, err - } + switch behavior { + case "vpc": + markRequiredDatasets(requiredDatasets, "vpcs") + case "subnet": + markRequiredDatasets(requiredDatasets, "subnets") + case "sg": + markRequiredDatasets(requiredDatasets, "vpcs", "subnets", "security_groups", "network_interfaces", "network_acls", "route_tables", "internet_gateways", "vpc_endpoints", "flow_logs", "log_groups", "transit_gateway_attachments") + case "acl": + markRequiredDatasets(requiredDatasets, "network_acls") + case "rt": + markRequiredDatasets(requiredDatasets, "route_tables") + case "igw": + markRequiredDatasets(requiredDatasets, "internet_gateways") + case "endpoint": + markRequiredDatasets(requiredDatasets, "vpc_endpoints") + case "flow-log": + markRequiredDatasets(requiredDatasets, "flow_logs") + case "log-group": + markRequiredDatasets(requiredDatasets, "log_groups") + default: + continue } } + return requiredDatasets +} - return &proto.EvalResponse{ - Status: evalStatus, - }, accumulatedErrors +func markRequiredDatasets(requiredDatasets map[string]bool, datasetNames ...string) { + for _, datasetName := range datasetNames { + requiredDatasets[datasetName] = true + } } func main() { From bf6e508707e33078dd320312112a28b033d1fe75 Mon Sep 17 00:00:00 2001 From: Reece Bedding Date: Tue, 26 May 2026 15:47:20 +0100 Subject: [PATCH 04/10] feat: add VPC specific enhanced data collections --- internal/pagination.go | 19 +++++ internal/policy_evaluation.go | 21 ++++++ internal/region_datasets.go | 18 +++++ internal/vpc.go | 29 ++++++-- internal/vpc_context.go | 133 ++++++++++++++++++++++++++++++++++ internal/vpc_context_test.go | 121 +++++++++++++++++++++++++++++++ internal/vpc_test.go | 34 +++++++++ main.go | 5 +- 8 files changed, 373 insertions(+), 7 deletions(-) create mode 100644 internal/vpc_context.go create mode 100644 internal/vpc_context_test.go create mode 100644 internal/vpc_test.go diff --git a/internal/pagination.go b/internal/pagination.go index 0d0049e..721f840 100644 --- a/internal/pagination.go +++ b/internal/pagination.go @@ -218,3 +218,22 @@ func PaginatedDescribeLogGroups(ctx context.Context, client *cloudwatchlogs.Clie } } } + +// PaginatedDescribeDhcpOptions returns an iterator over all DHCP option sets in the account/region +func PaginatedDescribeDhcpOptions(ctx context.Context, client *ec2.Client) iter.Seq2[types.DhcpOptions, error] { + return func(yield func(types.DhcpOptions, error) bool) { + paginator := ec2.NewDescribeDhcpOptionsPaginator(client, &ec2.DescribeDhcpOptionsInput{}) + for paginator.HasMorePages() { + page, err := paginator.NextPage(ctx) + if err != nil { + yield(types.DhcpOptions{}, err) + return + } + for _, options := range page.DhcpOptions { + if !yield(options, nil) { + return + } + } + } + } +} diff --git a/internal/policy_evaluation.go b/internal/policy_evaluation.go index e6b6604..cb45f80 100644 --- a/internal/policy_evaluation.go +++ b/internal/policy_evaluation.go @@ -126,3 +126,24 @@ func PrefixSecurityGroupEvidenceTitles(evidences []*proto.Evidence, groupName st evidence.Title = groupName + " | " + title } } + +func PrefixVpcEvidenceTitles(evidences []*proto.Evidence, vpcID string) { + vpcID = strings.TrimSpace(vpcID) + if vpcID == "" { + return + } + + for _, evidence := range evidences { + if evidence == nil { + continue + } + + title := strings.TrimSpace(evidence.Title) + if title == "" { + evidence.Title = vpcID + continue + } + + evidence.Title = vpcID + " | " + title + } +} diff --git a/internal/region_datasets.go b/internal/region_datasets.go index d7cc568..176a2d5 100644 --- a/internal/region_datasets.go +++ b/internal/region_datasets.go @@ -13,6 +13,8 @@ import ( type RegionDatasets struct { Vpcs []types.Vpc + VpcAttributes map[string]VpcAttributeValues + DhcpOptions []types.DhcpOptions Subnets []types.Subnet SecurityGroups []types.SecurityGroup NetworkInterfaces []types.NetworkInterface @@ -39,6 +41,22 @@ func CollectRegionDatasets(ctx context.Context, logger hclog.Logger, client *ec2 } } + if requiredDatasets["vpc_attributes"] { + datasets.VpcAttributes, err = CollectVpcAttributes(ctx, client, datasets.Vpcs) + if err != nil { + logger.Error("unable to get VPC attributes", "error", err) + return RegionDatasets{}, err + } + } + + if requiredDatasets["dhcp_options"] { + datasets.DhcpOptions, err = collectSequence(PaginatedDescribeDhcpOptions(ctx, client)) + if err != nil { + logger.Error("unable to get DHCP Options", "error", err) + return RegionDatasets{}, err + } + } + if requiredDatasets["subnets"] { datasets.Subnets, err = collectSequence(PaginatedDescribeSubnets(ctx, client)) if err != nil { diff --git a/internal/vpc.go b/internal/vpc.go index df49ca0..1a9133e 100644 --- a/internal/vpc.go +++ b/internal/vpc.go @@ -1,8 +1,12 @@ package internal -import "github.com/aws/aws-sdk-go-v2/service/ec2/types" +import ( + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/ec2/types" + "github.com/compliance-framework/agent/runner/proto" +) -func EvaluateVpcPolicies(deps EvaluationDependencies, policyPaths []string, vpcs []types.Vpc, region string) ResourceEvaluationErrors { +func EvaluateVpcPolicies(deps EvaluationDependencies, policyPaths []string, vpcs []types.Vpc, region string, datasets RegionDatasets) ResourceEvaluationErrors { return evaluateResources( deps, policyPaths, @@ -11,8 +15,23 @@ func EvaluateVpcPolicies(deps EvaluationDependencies, policyPaths []string, vpcs vpcCtx := BuildVpcEvidenceContext(vpc, region) return newResourceEvidenceContext(vpcCtx.Labels, vpcCtx.Subjects, vpcCtx.Components, vpcCtx.Inventory) }, - buildRawResourceInput[types.Vpc], - nil, - nil, + func(vpc types.Vpc) (interface{}, error) { + return BuildVpcPolicyInput(vpc, region, datasets) + }, + func(vpc types.Vpc, err error) { + deps.Logger.Error("unable to build VPC policy input", "vpc_id", aws.ToString(vpc.VpcId), "region", region, "error", err) + }, + func(evidences []*proto.Evidence, vpc types.Vpc) { + PrefixVpcEvidenceTitles(evidences, VpcDisplayName(vpc)) + }, ) } + +func VpcDisplayName(vpc types.Vpc) string { + for _, tag := range vpc.Tags { + if aws.ToString(tag.Key) == "Name" && aws.ToString(tag.Value) != "" { + return aws.ToString(tag.Value) + } + } + return aws.ToString(vpc.VpcId) +} diff --git a/internal/vpc_context.go b/internal/vpc_context.go new file mode 100644 index 0000000..018f2cc --- /dev/null +++ b/internal/vpc_context.go @@ -0,0 +1,133 @@ +package internal + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/ec2" + "github.com/aws/aws-sdk-go-v2/service/ec2/types" +) + +type VpcAttributeValues struct { + EnableDnsSupport *bool `json:"enable_dns_support,omitempty"` + EnableDnsHostnames *bool `json:"enable_dns_hostnames,omitempty"` + EnableNetworkAddressUsageMetrics *bool `json:"enable_network_address_usage_metrics,omitempty"` +} + +func CollectVpcAttributes(ctx context.Context, client *ec2.Client, vpcs []types.Vpc) (map[string]VpcAttributeValues, error) { + attributesByVpcID := make(map[string]VpcAttributeValues) + for _, vpc := range vpcs { + vpcID := aws.ToString(vpc.VpcId) + if vpcID == "" { + continue + } + + attributes, err := describeVpcAttributes(ctx, client, vpcID) + if err != nil { + return nil, err + } + attributesByVpcID[vpcID] = attributes + } + return attributesByVpcID, nil +} + +func describeVpcAttributes(ctx context.Context, client *ec2.Client, vpcID string) (VpcAttributeValues, error) { + var attributes VpcAttributeValues + + dnsSupport, err := describeVpcAttribute(ctx, client, vpcID, types.VpcAttributeNameEnableDnsSupport) + if err != nil { + return VpcAttributeValues{}, err + } + attributes.EnableDnsSupport = attributeBooleanValue(dnsSupport.EnableDnsSupport) + + dnsHostnames, err := describeVpcAttribute(ctx, client, vpcID, types.VpcAttributeNameEnableDnsHostnames) + if err != nil { + return VpcAttributeValues{}, err + } + attributes.EnableDnsHostnames = attributeBooleanValue(dnsHostnames.EnableDnsHostnames) + + networkAddressUsageMetrics, err := describeVpcAttribute(ctx, client, vpcID, types.VpcAttributeNameEnableNetworkAddressUsageMetrics) + if err != nil { + return VpcAttributeValues{}, err + } + attributes.EnableNetworkAddressUsageMetrics = attributeBooleanValue(networkAddressUsageMetrics.EnableNetworkAddressUsageMetrics) + + return attributes, nil +} + +func describeVpcAttribute(ctx context.Context, client *ec2.Client, vpcID string, attribute types.VpcAttributeName) (*ec2.DescribeVpcAttributeOutput, error) { + return client.DescribeVpcAttribute(ctx, &ec2.DescribeVpcAttributeInput{ + VpcId: aws.String(vpcID), + Attribute: attribute, + }) +} + +func BuildVpcPolicyInput(vpc types.Vpc, region string, datasets RegionDatasets) (map[string]interface{}, error) { + vpcValue, err := toInterfaceMap(vpc) + if err != nil { + return nil, err + } + + contextValue, err := toInterfaceMap(buildVpcSupplementaryContext(vpc, region, datasets)) + if err != nil { + return nil, err + } + + return map[string]interface{}{ + "vpc": vpcValue, + "vpc_context": contextValue, + }, nil +} + +func buildVpcSupplementaryContext(vpc types.Vpc, region string, datasets RegionDatasets) map[string]interface{} { + vpcID := aws.ToString(vpc.VpcId) + flowLogs := filterFlowLogsByResourceIDs(datasets.FlowLogs, singletonIDSet(vpcID)) + + return map[string]interface{}{ + "current": map[string]interface{}{ + "vpc_id": vpcID, + "region": region, + "cidr_block": aws.ToString(vpc.CidrBlock), + "dhcp_options_id": aws.ToString(vpc.DhcpOptionsId), + "is_default": aws.ToBool(vpc.IsDefault), + "state": string(vpc.State), + "tags_present": len(vpc.Tags) > 0, + }, + "attributes": datasets.VpcAttributes[vpcID], + "dhcp_options": findDhcpOptionsByID(datasets.DhcpOptions, aws.ToString(vpc.DhcpOptionsId)), + "subnets_in_vpc": filterSubnetsByVpc(datasets.Subnets, vpcID), + "route_tables_in_vpc": filterRouteTablesByVpc(datasets.RouteTables, vpcID), + "internet_gateways_for_vpc": filterInternetGatewaysByVpc(datasets.InternetGateways, vpcID), + "vpc_endpoints_for_vpc": filterVpcEndpointsByVpc(datasets.VpcEndpoints, vpcID), + "flow_logs_for_vpc": flowLogs, + "log_groups_for_vpc_flow_logs": filterLogGroupsForFlowLogs(datasets.LogGroups, flowLogs, singletonIDSet(vpcID)), + "transit_gateway_attachments_for_vpc": filterTransitGatewayAttachmentsByResourceID(datasets.TransitGatewayAttachments, vpcID), + } +} + +func filterSubnetsByVpc(subnets []types.Subnet, vpcID string) []types.Subnet { + filtered := make([]types.Subnet, 0) + for _, subnet := range subnets { + if aws.ToString(subnet.VpcId) == vpcID { + filtered = append(filtered, subnet) + } + } + return filtered +} + +func findDhcpOptionsByID(dhcpOptions []types.DhcpOptions, dhcpOptionsID string) *types.DhcpOptions { + for _, options := range dhcpOptions { + if aws.ToString(options.DhcpOptionsId) == dhcpOptionsID { + optionsCopy := options + return &optionsCopy + } + } + return nil +} + +func attributeBooleanValue(value *types.AttributeBooleanValue) *bool { + if value == nil { + return nil + } + return value.Value +} diff --git a/internal/vpc_context_test.go b/internal/vpc_context_test.go new file mode 100644 index 0000000..1a91a92 --- /dev/null +++ b/internal/vpc_context_test.go @@ -0,0 +1,121 @@ +package internal + +import ( + "testing" + + "github.com/aws/aws-sdk-go-v2/aws" + cloudwatchlogstypes "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs/types" + "github.com/aws/aws-sdk-go-v2/service/ec2/types" +) + +func TestBuildVpcPolicyInputIncludesVpcContext(t *testing.T) { + enableDnsSupport := true + enableDnsHostnames := false + enableNetworkAddressUsageMetrics := true + + vpc := types.Vpc{ + VpcId: aws.String("vpc-123"), + CidrBlock: aws.String("10.0.0.0/16"), + DhcpOptionsId: aws.String("dopt-123"), + IsDefault: aws.Bool(false), + State: types.VpcStateAvailable, + Tags: []types.Tag{{Key: aws.String("Owner"), Value: aws.String("platform")}}, + } + + input, err := BuildVpcPolicyInput(vpc, "eu-west-2", RegionDatasets{ + VpcAttributes: map[string]VpcAttributeValues{ + "vpc-123": { + EnableDnsSupport: &enableDnsSupport, + EnableDnsHostnames: &enableDnsHostnames, + EnableNetworkAddressUsageMetrics: &enableNetworkAddressUsageMetrics, + }, + }, + DhcpOptions: []types.DhcpOptions{ + {DhcpOptionsId: aws.String("dopt-123")}, + {DhcpOptionsId: aws.String("dopt-other")}, + }, + Subnets: []types.Subnet{ + {SubnetId: aws.String("subnet-123"), VpcId: aws.String("vpc-123")}, + {SubnetId: aws.String("subnet-other"), VpcId: aws.String("vpc-other")}, + }, + RouteTables: []types.RouteTable{ + {RouteTableId: aws.String("rtb-123"), VpcId: aws.String("vpc-123")}, + {RouteTableId: aws.String("rtb-other"), VpcId: aws.String("vpc-other")}, + }, + InternetGateways: []types.InternetGateway{ + {InternetGatewayId: aws.String("igw-123"), Attachments: []types.InternetGatewayAttachment{{VpcId: aws.String("vpc-123")}}}, + {InternetGatewayId: aws.String("igw-other"), Attachments: []types.InternetGatewayAttachment{{VpcId: aws.String("vpc-other")}}}, + }, + VpcEndpoints: []types.VpcEndpoint{ + {VpcEndpointId: aws.String("vpce-123"), VpcId: aws.String("vpc-123")}, + {VpcEndpointId: aws.String("vpce-other"), VpcId: aws.String("vpc-other")}, + }, + FlowLogs: []types.FlowLog{ + {FlowLogId: aws.String("fl-123"), ResourceId: aws.String("vpc-123"), LogGroupName: aws.String("/aws/vpc/flow")}, + {FlowLogId: aws.String("fl-other"), ResourceId: aws.String("vpc-other"), LogGroupName: aws.String("/aws/vpc/other")}, + }, + LogGroups: []cloudwatchlogstypes.LogGroup{ + {LogGroupName: aws.String("/aws/vpc/flow")}, + {LogGroupName: aws.String("/aws/vpc/other")}, + }, + TransitGatewayAttachments: []types.TransitGatewayAttachment{ + {TransitGatewayAttachmentId: aws.String("tgw-attach-123"), ResourceId: aws.String("vpc-123")}, + {TransitGatewayAttachmentId: aws.String("tgw-attach-other"), ResourceId: aws.String("vpc-other")}, + }, + }) + if err != nil { + t.Fatalf("BuildVpcPolicyInput returned error: %v", err) + } + + if _, ok := input["vpc"].(map[string]interface{}); !ok { + t.Fatalf("input[vpc] should contain the raw VPC map") + } + + contextMap, ok := input["vpc_context"].(map[string]interface{}) + if !ok { + t.Fatalf("input[vpc_context] should be a map") + } + + current := contextMap["current"].(map[string]interface{}) + if current["vpc_id"] != "vpc-123" { + t.Fatalf("current.vpc_id = %v, want vpc-123", current["vpc_id"]) + } + if current["region"] != "eu-west-2" { + t.Fatalf("current.region = %v, want eu-west-2", current["region"]) + } + + attributes := contextMap["attributes"].(map[string]interface{}) + if attributes["enable_dns_support"] != true { + t.Fatalf("attributes.enable_dns_support = %v, want true", attributes["enable_dns_support"]) + } + if attributes["enable_dns_hostnames"] != false { + t.Fatalf("attributes.enable_dns_hostnames = %v, want false", attributes["enable_dns_hostnames"]) + } + if attributes["enable_network_address_usage_metrics"] != true { + t.Fatalf("attributes.enable_network_address_usage_metrics = %v, want true", attributes["enable_network_address_usage_metrics"]) + } + + assertOneItem(t, contextMap, "subnets_in_vpc") + assertOneItem(t, contextMap, "route_tables_in_vpc") + assertOneItem(t, contextMap, "internet_gateways_for_vpc") + assertOneItem(t, contextMap, "vpc_endpoints_for_vpc") + assertOneItem(t, contextMap, "flow_logs_for_vpc") + assertOneItem(t, contextMap, "log_groups_for_vpc_flow_logs") + assertOneItem(t, contextMap, "transit_gateway_attachments_for_vpc") + + dhcpOptions := contextMap["dhcp_options"].(map[string]interface{}) + if dhcpOptions["DhcpOptionsId"] != "dopt-123" { + t.Fatalf("dhcp_options.DhcpOptionsId = %v, want dopt-123", dhcpOptions["DhcpOptionsId"]) + } +} + +func assertOneItem(t *testing.T, values map[string]interface{}, key string) { + t.Helper() + items, ok := values[key].([]interface{}) + if !ok { + t.Fatalf("%s should be a list", key) + } + if len(items) != 1 { + t.Fatalf("len(%s) = %d, want 1", key, len(items)) + } +} diff --git a/internal/vpc_test.go b/internal/vpc_test.go new file mode 100644 index 0000000..3fd18b8 --- /dev/null +++ b/internal/vpc_test.go @@ -0,0 +1,34 @@ +package internal + +import ( + "testing" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/ec2/types" +) + +func TestVpcDisplayNameUsesNameTag(t *testing.T) { + vpc := types.Vpc{ + VpcId: aws.String("vpc-123"), + Tags: []types.Tag{ + {Key: aws.String("Name"), Value: aws.String("production-vpc")}, + }, + } + + if got := VpcDisplayName(vpc); got != "production-vpc" { + t.Fatalf("VpcDisplayName() = %q, want production-vpc", got) + } +} + +func TestVpcDisplayNameFallsBackToID(t *testing.T) { + vpc := types.Vpc{ + VpcId: aws.String("vpc-123"), + Tags: []types.Tag{ + {Key: aws.String("Environment"), Value: aws.String("prod")}, + }, + } + + if got := VpcDisplayName(vpc); got != "vpc-123" { + t.Fatalf("VpcDisplayName() = %q, want vpc-123", got) + } +} diff --git a/main.go b/main.go index b08584d..38e1606 100644 --- a/main.go +++ b/main.go @@ -70,6 +70,7 @@ func (l *CompliancePlugin) Eval(request *proto.EvalRequest, apiHelper runner.Api defaultBehaviorMapping := map[string][]string{ "aws-vpc-sg-policies": {"sg"}, + "aws-vpc-policies": {"vpc"}, } policyEval := request.WithDefaultPolicyBehavior(defaultBehaviorMapping) policyPathsByBehavior := buildPolicyPathsByBehavior(policyEval) @@ -105,7 +106,7 @@ func (l *CompliancePlugin) Eval(request *proto.EvalRequest, apiHelper runner.Api } if vpcPolicyPaths := policyPathsByBehavior["vpc"]; len(vpcPolicyPaths) > 0 { - result := internal.EvaluateVpcPolicies(deps, vpcPolicyPaths, datasets.Vpcs, region) + result := internal.EvaluateVpcPolicies(deps, vpcPolicyPaths, datasets.Vpcs, region, datasets) if fatal := applyResourceEvaluationErrors(result, &evalStatus, &accumulatedErrors, false); fatal != nil { return &proto.EvalResponse{Status: proto.ExecutionStatus_FAILURE}, fatal } @@ -217,7 +218,7 @@ func buildRequiredDatasets(policyPathsByBehavior map[string][]string) map[string switch behavior { case "vpc": - markRequiredDatasets(requiredDatasets, "vpcs") + markRequiredDatasets(requiredDatasets, "vpcs", "vpc_attributes", "dhcp_options", "subnets", "route_tables", "internet_gateways", "vpc_endpoints", "flow_logs", "log_groups", "transit_gateway_attachments") case "subnet": markRequiredDatasets(requiredDatasets, "subnets") case "sg": From f73bdca89209c9bbd3d2ab992d199b2330d99bc3 Mon Sep 17 00:00:00 2001 From: Reece Bedding Date: Thu, 28 May 2026 14:27:29 +0100 Subject: [PATCH 05/10] feat: add NACL specific enhanced collections --- internal/evidence.go | 11 ++- internal/network_acl.go | 29 +++++-- internal/network_acl_context.go | 87 ++++++++++++++++++++ internal/network_acl_context_test.go | 116 +++++++++++++++++++++++++++ internal/network_acl_test.go | 47 +++++++++++ internal/policy_evaluation.go | 21 +++++ main.go | 9 ++- main_test.go | 15 ++++ 8 files changed, 320 insertions(+), 15 deletions(-) create mode 100644 internal/network_acl_context.go create mode 100644 internal/network_acl_context_test.go create mode 100644 internal/network_acl_test.go create mode 100644 main_test.go diff --git a/internal/evidence.go b/internal/evidence.go index d74524d..220a657 100644 --- a/internal/evidence.go +++ b/internal/evidence.go @@ -315,12 +315,11 @@ func BuildNetworkAclEvidenceContext(acl types.NetworkAcl, region string) Network vpcId := aws.ToString(acl.VpcId) labels := map[string]string{ - "provider": "aws", - "type": string(ResourceTypeNetworkAcl), - "acl-id": aclId, - "vpc-id": vpcId, - "is-default": fmt.Sprintf("%v", acl.IsDefault), - "region": region, + "provider": "aws", + "type": string(ResourceTypeNetworkAcl), + "acl-id": aclId, + "vpc-id": vpcId, + "region": region, } components := []*proto.Component{ diff --git a/internal/network_acl.go b/internal/network_acl.go index 5540b78..7629e8f 100644 --- a/internal/network_acl.go +++ b/internal/network_acl.go @@ -1,8 +1,12 @@ package internal -import "github.com/aws/aws-sdk-go-v2/service/ec2/types" +import ( + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/ec2/types" + "github.com/compliance-framework/agent/runner/proto" +) -func EvaluateNetworkAclPolicies(deps EvaluationDependencies, policyPaths []string, networkAcls []types.NetworkAcl, region string) ResourceEvaluationErrors { +func EvaluateNetworkAclPolicies(deps EvaluationDependencies, policyPaths []string, networkAcls []types.NetworkAcl, region string, datasets RegionDatasets) ResourceEvaluationErrors { return evaluateResources( deps, policyPaths, @@ -11,8 +15,23 @@ func EvaluateNetworkAclPolicies(deps EvaluationDependencies, policyPaths []strin aclCtx := BuildNetworkAclEvidenceContext(acl, region) return newResourceEvidenceContext(aclCtx.Labels, aclCtx.Subjects, aclCtx.Components, aclCtx.Inventory) }, - buildRawResourceInput[types.NetworkAcl], - nil, - nil, + func(acl types.NetworkAcl) (interface{}, error) { + return BuildNetworkAclPolicyInput(acl, region, datasets) + }, + func(acl types.NetworkAcl, err error) { + deps.Logger.Error("unable to build Network ACL policy input", "network_acl_id", aws.ToString(acl.NetworkAclId), "region", region, "error", err) + }, + func(evidences []*proto.Evidence, acl types.NetworkAcl) { + PrefixNetworkAclEvidenceTitles(evidences, NetworkAclDisplayName(acl)) + }, ) } + +func NetworkAclDisplayName(acl types.NetworkAcl) string { + for _, tag := range acl.Tags { + if aws.ToString(tag.Key) == "Name" && aws.ToString(tag.Value) != "" { + return aws.ToString(tag.Value) + } + } + return aws.ToString(acl.NetworkAclId) +} diff --git a/internal/network_acl_context.go b/internal/network_acl_context.go new file mode 100644 index 0000000..1e00493 --- /dev/null +++ b/internal/network_acl_context.go @@ -0,0 +1,87 @@ +package internal + +import ( + "sort" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/ec2/types" +) + +func BuildNetworkAclPolicyInput(networkAcl types.NetworkAcl, region string, datasets RegionDatasets) (map[string]interface{}, error) { + networkAclValue, err := toInterfaceMap(networkAcl) + if err != nil { + return nil, err + } + + contextValue, err := toInterfaceMap(buildNetworkAclSupplementaryContext(networkAcl, region, datasets)) + if err != nil { + return nil, err + } + + return map[string]interface{}{ + "network_acl": networkAclValue, + "nacl_context": contextValue, + }, nil +} + +func buildNetworkAclSupplementaryContext(networkAcl types.NetworkAcl, region string, datasets RegionDatasets) map[string]interface{} { + networkAclID := aws.ToString(networkAcl.NetworkAclId) + vpcID := aws.ToString(networkAcl.VpcId) + associatedSubnetIDs := networkAclAssociatedSubnetIDs(networkAcl) + relatedResourceIDs := combineIDSets(singletonIDSet(vpcID), associatedSubnetIDs) + flowLogsForVpc := filterFlowLogsByResourceIDs(datasets.FlowLogs, singletonIDSet(vpcID)) + flowLogsForAssociatedSubnets := filterFlowLogsByResourceIDs(datasets.FlowLogs, associatedSubnetIDs) + + return map[string]interface{}{ + "current": map[string]interface{}{ + "network_acl_id": networkAclID, + "vpc_id": vpcID, + "region": region, + "is_default": aws.ToBool(networkAcl.IsDefault), + "association_count": len(networkAcl.Associations), + "entry_count": len(networkAcl.Entries), + "associated_subnet_ids": sortedIDSetValues(associatedSubnetIDs), + "has_subnet_associations": len(networkAcl.Associations) > 0, + "tags_present": len(networkAcl.Tags) > 0, + }, + "vpc": findVpcByID(datasets.Vpcs, vpcID), + "associated_subnets": filterSubnetsByIDs(datasets.Subnets, associatedSubnetIDs), + "route_tables_in_vpc": filterRouteTablesByVpc(datasets.RouteTables, vpcID), + "route_tables_for_associated_subnets": filterRouteTablesForSubnetIDs(datasets.RouteTables, vpcID, associatedSubnetIDs), + "internet_gateways_for_vpc": filterInternetGatewaysByVpc(datasets.InternetGateways, vpcID), + "flow_logs_for_vpc": flowLogsForVpc, + "flow_logs_for_associated_subnets": flowLogsForAssociatedSubnets, + "log_groups_for_related_flow_logs": filterLogGroupsForFlowLogs(datasets.LogGroups, append(flowLogsForVpc, flowLogsForAssociatedSubnets...), relatedResourceIDs), + "network_interfaces_in_associated_subnets": filterNetworkInterfacesBySubnetIDs(datasets.NetworkInterfaces, associatedSubnetIDs), + } +} + +func networkAclAssociatedSubnetIDs(networkAcl types.NetworkAcl) map[string]bool { + subnetIDs := make(map[string]bool) + for _, association := range networkAcl.Associations { + subnetID := aws.ToString(association.SubnetId) + if subnetID != "" { + subnetIDs[subnetID] = true + } + } + return subnetIDs +} + +func filterNetworkInterfacesBySubnetIDs(networkInterfaces []types.NetworkInterface, subnetIDs map[string]bool) []types.NetworkInterface { + filtered := make([]types.NetworkInterface, 0) + for _, networkInterface := range networkInterfaces { + if subnetIDs[aws.ToString(networkInterface.SubnetId)] { + filtered = append(filtered, networkInterface) + } + } + return filtered +} + +func sortedIDSetValues(values map[string]bool) []string { + ids := make([]string, 0, len(values)) + for id := range values { + ids = append(ids, id) + } + sort.Strings(ids) + return ids +} diff --git a/internal/network_acl_context_test.go b/internal/network_acl_context_test.go new file mode 100644 index 0000000..775de47 --- /dev/null +++ b/internal/network_acl_context_test.go @@ -0,0 +1,116 @@ +package internal + +import ( + "testing" + + "github.com/aws/aws-sdk-go-v2/aws" + cloudwatchlogstypes "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs/types" + "github.com/aws/aws-sdk-go-v2/service/ec2/types" +) + +func TestBuildNetworkAclPolicyInputIncludesNaclContext(t *testing.T) { + networkAcl := types.NetworkAcl{ + NetworkAclId: aws.String("acl-123"), + VpcId: aws.String("vpc-123"), + IsDefault: aws.Bool(false), + Tags: []types.Tag{{Key: aws.String("Owner"), Value: aws.String("platform")}}, + Associations: []types.NetworkAclAssociation{ + {NetworkAclAssociationId: aws.String("aclassoc-123"), NetworkAclId: aws.String("acl-123"), SubnetId: aws.String("subnet-123")}, + }, + Entries: []types.NetworkAclEntry{ + {RuleNumber: aws.Int32(100), RuleAction: types.RuleActionAllow, CidrBlock: aws.String("10.0.0.0/16")}, + }, + } + + input, err := BuildNetworkAclPolicyInput(networkAcl, "eu-west-2", RegionDatasets{ + Vpcs: []types.Vpc{ + {VpcId: aws.String("vpc-123")}, + {VpcId: aws.String("vpc-other")}, + }, + Subnets: []types.Subnet{ + {SubnetId: aws.String("subnet-123"), VpcId: aws.String("vpc-123")}, + {SubnetId: aws.String("subnet-other"), VpcId: aws.String("vpc-other")}, + }, + RouteTables: []types.RouteTable{ + {RouteTableId: aws.String("rtb-123"), VpcId: aws.String("vpc-123"), Associations: []types.RouteTableAssociation{{SubnetId: aws.String("subnet-123")}}}, + {RouteTableId: aws.String("rtb-other"), VpcId: aws.String("vpc-other"), Associations: []types.RouteTableAssociation{{SubnetId: aws.String("subnet-other")}}}, + }, + InternetGateways: []types.InternetGateway{ + {InternetGatewayId: aws.String("igw-123"), Attachments: []types.InternetGatewayAttachment{{VpcId: aws.String("vpc-123")}}}, + {InternetGatewayId: aws.String("igw-other"), Attachments: []types.InternetGatewayAttachment{{VpcId: aws.String("vpc-other")}}}, + }, + FlowLogs: []types.FlowLog{ + {FlowLogId: aws.String("fl-vpc"), ResourceId: aws.String("vpc-123"), LogGroupName: aws.String("/aws/vpc/flow")}, + {FlowLogId: aws.String("fl-subnet"), ResourceId: aws.String("subnet-123"), LogGroupName: aws.String("/aws/vpc/subnet")}, + {FlowLogId: aws.String("fl-other"), ResourceId: aws.String("vpc-other"), LogGroupName: aws.String("/aws/vpc/other")}, + }, + LogGroups: []cloudwatchlogstypes.LogGroup{ + {LogGroupName: aws.String("/aws/vpc/flow")}, + {LogGroupName: aws.String("/aws/vpc/subnet")}, + {LogGroupName: aws.String("/aws/vpc/other")}, + }, + NetworkInterfaces: []types.NetworkInterface{ + {NetworkInterfaceId: aws.String("eni-123"), SubnetId: aws.String("subnet-123")}, + {NetworkInterfaceId: aws.String("eni-other"), SubnetId: aws.String("subnet-other")}, + }, + }) + if err != nil { + t.Fatalf("BuildNetworkAclPolicyInput returned error: %v", err) + } + + networkAclMap, ok := input["network_acl"].(map[string]interface{}) + if !ok { + t.Fatalf("input[network_acl] should contain the raw Network ACL map") + } + if networkAclMap["IsDefault"] != false { + t.Fatalf("network_acl.IsDefault = %v, want false", networkAclMap["IsDefault"]) + } + + contextMap, ok := input["nacl_context"].(map[string]interface{}) + if !ok { + t.Fatalf("input[nacl_context] should be a map") + } + + current := contextMap["current"].(map[string]interface{}) + if current["network_acl_id"] != "acl-123" { + t.Fatalf("current.network_acl_id = %v, want acl-123", current["network_acl_id"]) + } + if current["region"] != "eu-west-2" { + t.Fatalf("current.region = %v, want eu-west-2", current["region"]) + } + if current["association_count"] != float64(1) { + t.Fatalf("current.association_count = %v, want 1", current["association_count"]) + } + if current["entry_count"] != float64(1) { + t.Fatalf("current.entry_count = %v, want 1", current["entry_count"]) + } + if current["is_default"] != false { + t.Fatalf("current.is_default = %v, want false", current["is_default"]) + } + + assertOneItem(t, contextMap, "associated_subnets") + assertOneItem(t, contextMap, "route_tables_in_vpc") + assertOneItem(t, contextMap, "route_tables_for_associated_subnets") + assertOneItem(t, contextMap, "internet_gateways_for_vpc") + assertOneItem(t, contextMap, "network_interfaces_in_associated_subnets") + + assertItemCount(t, contextMap, "flow_logs_for_vpc", 1) + assertItemCount(t, contextMap, "flow_logs_for_associated_subnets", 1) + assertItemCount(t, contextMap, "log_groups_for_related_flow_logs", 2) + + vpc := contextMap["vpc"].(map[string]interface{}) + if vpc["VpcId"] != "vpc-123" { + t.Fatalf("vpc.VpcId = %v, want vpc-123", vpc["VpcId"]) + } +} + +func assertItemCount(t *testing.T, values map[string]interface{}, key string, want int) { + t.Helper() + items, ok := values[key].([]interface{}) + if !ok { + t.Fatalf("%s should be a list", key) + } + if len(items) != want { + t.Fatalf("len(%s) = %d, want %d", key, len(items), want) + } +} diff --git a/internal/network_acl_test.go b/internal/network_acl_test.go new file mode 100644 index 0000000..7e5aea7 --- /dev/null +++ b/internal/network_acl_test.go @@ -0,0 +1,47 @@ +package internal + +import ( + "testing" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/ec2/types" + "github.com/compliance-framework/agent/runner/proto" +) + +func TestNetworkAclDisplayNameUsesNameTag(t *testing.T) { + acl := types.NetworkAcl{ + NetworkAclId: aws.String("acl-123"), + Tags: []types.Tag{ + {Key: aws.String("Name"), Value: aws.String("prod-public-nacl")}, + }, + } + + if got := NetworkAclDisplayName(acl); got != "prod-public-nacl" { + t.Fatalf("NetworkAclDisplayName() = %q, want prod-public-nacl", got) + } +} + +func TestNetworkAclDisplayNameFallsBackToID(t *testing.T) { + acl := types.NetworkAcl{NetworkAclId: aws.String("acl-123")} + + if got := NetworkAclDisplayName(acl); got != "acl-123" { + t.Fatalf("NetworkAclDisplayName() = %q, want acl-123", got) + } +} + +func TestPrefixNetworkAclEvidenceTitles(t *testing.T) { + evidences := []*proto.Evidence{ + {Title: "Network ACL should set required tags"}, + {Title: ""}, + nil, + } + + PrefixNetworkAclEvidenceTitles(evidences, "acl-123") + + if got := evidences[0].Title; got != "acl-123 | Network ACL should set required tags" { + t.Fatalf("prefixed title = %q", got) + } + if got := evidences[1].Title; got != "acl-123" { + t.Fatalf("empty title fallback = %q", got) + } +} diff --git a/internal/policy_evaluation.go b/internal/policy_evaluation.go index cb45f80..266db90 100644 --- a/internal/policy_evaluation.go +++ b/internal/policy_evaluation.go @@ -147,3 +147,24 @@ func PrefixVpcEvidenceTitles(evidences []*proto.Evidence, vpcID string) { evidence.Title = vpcID + " | " + title } } + +func PrefixNetworkAclEvidenceTitles(evidences []*proto.Evidence, networkAclName string) { + networkAclName = strings.TrimSpace(networkAclName) + if networkAclName == "" { + return + } + + for _, evidence := range evidences { + if evidence == nil { + continue + } + + title := strings.TrimSpace(evidence.Title) + if title == "" { + evidence.Title = networkAclName + continue + } + + evidence.Title = networkAclName + " | " + title + } +} diff --git a/main.go b/main.go index 38e1606..acc1ad9 100644 --- a/main.go +++ b/main.go @@ -69,8 +69,9 @@ func (l *CompliancePlugin) Eval(request *proto.EvalRequest, apiHelper runner.Api } defaultBehaviorMapping := map[string][]string{ - "aws-vpc-sg-policies": {"sg"}, - "aws-vpc-policies": {"vpc"}, + "aws-vpc-sg-policies": {"sg"}, + "aws-vpc-policies": {"vpc"}, + "aws-vpc-nacl-policies": {"acl"}, } policyEval := request.WithDefaultPolicyBehavior(defaultBehaviorMapping) policyPathsByBehavior := buildPolicyPathsByBehavior(policyEval) @@ -127,7 +128,7 @@ func (l *CompliancePlugin) Eval(request *proto.EvalRequest, apiHelper runner.Api } if aclPolicyPaths := policyPathsByBehavior["acl"]; len(aclPolicyPaths) > 0 { - result := internal.EvaluateNetworkAclPolicies(deps, aclPolicyPaths, datasets.NetworkAcls, region) + result := internal.EvaluateNetworkAclPolicies(deps, aclPolicyPaths, datasets.NetworkAcls, region, datasets) if fatal := applyResourceEvaluationErrors(result, &evalStatus, &accumulatedErrors, false); fatal != nil { return &proto.EvalResponse{Status: proto.ExecutionStatus_FAILURE}, fatal } @@ -224,7 +225,7 @@ func buildRequiredDatasets(policyPathsByBehavior map[string][]string) map[string case "sg": markRequiredDatasets(requiredDatasets, "vpcs", "subnets", "security_groups", "network_interfaces", "network_acls", "route_tables", "internet_gateways", "vpc_endpoints", "flow_logs", "log_groups", "transit_gateway_attachments") case "acl": - markRequiredDatasets(requiredDatasets, "network_acls") + markRequiredDatasets(requiredDatasets, "vpcs", "subnets", "network_acls", "route_tables", "internet_gateways", "flow_logs", "log_groups", "network_interfaces") case "rt": markRequiredDatasets(requiredDatasets, "route_tables") case "igw": diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..eabb9ca --- /dev/null +++ b/main_test.go @@ -0,0 +1,15 @@ +package main + +import "testing" + +func TestBuildRequiredDatasetsForAclPolicies(t *testing.T) { + required := buildRequiredDatasets(map[string][]string{ + "acl": {"/tmp/policies"}, + }) + + for _, dataset := range []string{"vpcs", "subnets", "network_acls", "route_tables", "internet_gateways", "flow_logs", "log_groups", "network_interfaces"} { + if !required[dataset] { + t.Fatalf("expected %s to be required for acl policies", dataset) + } + } +} From 49d9796003900386c2b72e8ec0abc0dde9134a4a Mon Sep 17 00:00:00 2001 From: Reece Bedding Date: Thu, 28 May 2026 15:46:30 +0100 Subject: [PATCH 06/10] feat: add subnet specific enhanced collections --- internal/policy_evaluation.go | 52 ++----------- internal/security_group.go | 2 +- internal/subnet.go | 29 ++++++-- internal/subnet_context.go | 96 ++++++++++++++++++++++++ internal/subnet_context_test.go | 125 ++++++++++++++++++++++++++++++++ internal/subnet_test.go | 47 ++++++++++++ main.go | 11 +-- main_test.go | 12 +++ 8 files changed, 316 insertions(+), 58 deletions(-) create mode 100644 internal/subnet_context.go create mode 100644 internal/subnet_context_test.go create mode 100644 internal/subnet_test.go diff --git a/internal/policy_evaluation.go b/internal/policy_evaluation.go index 266db90..79ecd32 100644 --- a/internal/policy_evaluation.go +++ b/internal/policy_evaluation.go @@ -106,9 +106,9 @@ func generateResourceEvidences(ctx context.Context, logger hclog.Logger, actors return evidences, accumulatedErrors } -func PrefixSecurityGroupEvidenceTitles(evidences []*proto.Evidence, groupName string) { - groupName = strings.TrimSpace(groupName) - if groupName == "" { +func PrefixEvidenceTitles(evidences []*proto.Evidence, prefix string) { + prefix = strings.TrimSpace(prefix) + if prefix == "" { return } @@ -119,52 +119,10 @@ func PrefixSecurityGroupEvidenceTitles(evidences []*proto.Evidence, groupName st title := strings.TrimSpace(evidence.Title) if title == "" { - evidence.Title = groupName + evidence.Title = prefix continue } - evidence.Title = groupName + " | " + title - } -} - -func PrefixVpcEvidenceTitles(evidences []*proto.Evidence, vpcID string) { - vpcID = strings.TrimSpace(vpcID) - if vpcID == "" { - return - } - - for _, evidence := range evidences { - if evidence == nil { - continue - } - - title := strings.TrimSpace(evidence.Title) - if title == "" { - evidence.Title = vpcID - continue - } - - evidence.Title = vpcID + " | " + title - } -} - -func PrefixNetworkAclEvidenceTitles(evidences []*proto.Evidence, networkAclName string) { - networkAclName = strings.TrimSpace(networkAclName) - if networkAclName == "" { - return - } - - for _, evidence := range evidences { - if evidence == nil { - continue - } - - title := strings.TrimSpace(evidence.Title) - if title == "" { - evidence.Title = networkAclName - continue - } - - evidence.Title = networkAclName + " | " + title + evidence.Title = prefix + " | " + title } } diff --git a/internal/security_group.go b/internal/security_group.go index 5eb8b75..a9c91c7 100644 --- a/internal/security_group.go +++ b/internal/security_group.go @@ -25,7 +25,7 @@ func EvaluateSecurityGroupPolicies(deps EvaluationDependencies, policyPaths []st deps.Logger.Error("unable to build security group policy input", "group_id", aws.ToString(group.GroupId), "region", region, "error", err) }, func(evidences []*proto.Evidence, group types.SecurityGroup) { - PrefixSecurityGroupEvidenceTitles(evidences, aws.ToString(group.GroupName)) + PrefixEvidenceTitles(evidences, aws.ToString(group.GroupName)) }, ) } diff --git a/internal/subnet.go b/internal/subnet.go index aa40398..a99422f 100644 --- a/internal/subnet.go +++ b/internal/subnet.go @@ -1,8 +1,12 @@ package internal -import "github.com/aws/aws-sdk-go-v2/service/ec2/types" +import ( + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/ec2/types" + "github.com/compliance-framework/agent/runner/proto" +) -func EvaluateSubnetPolicies(deps EvaluationDependencies, policyPaths []string, subnets []types.Subnet, region string) ResourceEvaluationErrors { +func EvaluateSubnetPolicies(deps EvaluationDependencies, policyPaths []string, subnets []types.Subnet, region string, datasets RegionDatasets) ResourceEvaluationErrors { return evaluateResources( deps, policyPaths, @@ -11,8 +15,23 @@ func EvaluateSubnetPolicies(deps EvaluationDependencies, policyPaths []string, s subnetCtx := BuildSubnetEvidenceContext(subnet, region) return newResourceEvidenceContext(subnetCtx.Labels, subnetCtx.Subjects, subnetCtx.Components, subnetCtx.Inventory) }, - buildRawResourceInput[types.Subnet], - nil, - nil, + func(subnet types.Subnet) (interface{}, error) { + return BuildSubnetPolicyInput(subnet, region, datasets) + }, + func(subnet types.Subnet, err error) { + deps.Logger.Error("unable to build Subnet policy input", "subnet_id", aws.ToString(subnet.SubnetId), "region", region, "error", err) + }, + func(evidences []*proto.Evidence, subnet types.Subnet) { + PrefixEvidenceTitles(evidences, SubnetDisplayName(subnet)) + }, ) } + +func SubnetDisplayName(subnet types.Subnet) string { + for _, tag := range subnet.Tags { + if aws.ToString(tag.Key) == "Name" && aws.ToString(tag.Value) != "" { + return aws.ToString(tag.Value) + } + } + return aws.ToString(subnet.SubnetId) +} diff --git a/internal/subnet_context.go b/internal/subnet_context.go new file mode 100644 index 0000000..ed9a8c7 --- /dev/null +++ b/internal/subnet_context.go @@ -0,0 +1,96 @@ +package internal + +import ( + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/ec2/types" +) + +func BuildSubnetPolicyInput(subnet types.Subnet, region string, datasets RegionDatasets) (map[string]interface{}, error) { + subnetValue, err := toInterfaceMap(subnet) + if err != nil { + return nil, err + } + + contextValue, err := toInterfaceMap(buildSubnetSupplementaryContext(subnet, region, datasets)) + if err != nil { + return nil, err + } + + return map[string]interface{}{ + "subnet": subnetValue, + "subnet_context": contextValue, + }, nil +} + +func buildSubnetSupplementaryContext(subnet types.Subnet, region string, datasets RegionDatasets) map[string]interface{} { + subnetID := aws.ToString(subnet.SubnetId) + vpcID := aws.ToString(subnet.VpcId) + subnetIDs := singletonIDSet(subnetID) + relatedResourceIDs := combineIDSets(singletonIDSet(vpcID), subnetIDs) + flowLogsForVpc := filterFlowLogsByResourceIDs(datasets.FlowLogs, singletonIDSet(vpcID)) + flowLogsForSubnet := filterFlowLogsByResourceIDs(datasets.FlowLogs, subnetIDs) + + return map[string]interface{}{ + "current": map[string]interface{}{ + "subnet_id": subnetID, + "vpc_id": vpcID, + "region": region, + "availability_zone": aws.ToString(subnet.AvailabilityZone), + "availability_zone_id": aws.ToString(subnet.AvailabilityZoneId), + "cidr_block": aws.ToString(subnet.CidrBlock), + "ipv6_cidr_block_count": len(subnet.Ipv6CidrBlockAssociationSet), + "state": string(subnet.State), + "map_public_ip_on_launch": aws.ToBool(subnet.MapPublicIpOnLaunch), + "available_ip_address_count": aws.ToInt32(subnet.AvailableIpAddressCount), + "tags_present": len(subnet.Tags) > 0, + }, + "vpc": findVpcByID(datasets.Vpcs, vpcID), + "route_tables_in_vpc": filterRouteTablesByVpc(datasets.RouteTables, vpcID), + "route_table_for_subnet": findRouteTableForSubnet(datasets.RouteTables, vpcID, subnetID), + "explicit_route_table_association": hasExplicitRouteTableAssociation(datasets.RouteTables, subnetID), + "network_acls_for_subnet": filterNetworkAclsForSubnetIDs(datasets.NetworkAcls, subnetIDs), + "internet_gateways_for_vpc": filterInternetGatewaysByVpc(datasets.InternetGateways, vpcID), + "flow_logs_for_vpc": flowLogsForVpc, + "flow_logs_for_subnet": flowLogsForSubnet, + "log_groups_for_related_flow_logs": filterLogGroupsForFlowLogs(datasets.LogGroups, append(flowLogsForVpc, flowLogsForSubnet...), relatedResourceIDs), + } +} + +func findRouteTableForSubnet(routeTables []types.RouteTable, vpcID string, subnetID string) *types.RouteTable { + for _, routeTable := range routeTables { + if aws.ToString(routeTable.VpcId) != vpcID { + continue + } + for _, association := range routeTable.Associations { + if aws.ToString(association.SubnetId) == subnetID { + routeTableCopy := routeTable + return &routeTableCopy + } + } + } + + for _, routeTable := range routeTables { + if aws.ToString(routeTable.VpcId) != vpcID { + continue + } + for _, association := range routeTable.Associations { + if aws.ToBool(association.Main) { + routeTableCopy := routeTable + return &routeTableCopy + } + } + } + + return nil +} + +func hasExplicitRouteTableAssociation(routeTables []types.RouteTable, subnetID string) bool { + for _, routeTable := range routeTables { + for _, association := range routeTable.Associations { + if aws.ToString(association.SubnetId) == subnetID { + return true + } + } + } + return false +} diff --git a/internal/subnet_context_test.go b/internal/subnet_context_test.go new file mode 100644 index 0000000..1df7728 --- /dev/null +++ b/internal/subnet_context_test.go @@ -0,0 +1,125 @@ +package internal + +import ( + "testing" + + "github.com/aws/aws-sdk-go-v2/aws" + cloudwatchlogstypes "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs/types" + "github.com/aws/aws-sdk-go-v2/service/ec2/types" +) + +func TestBuildSubnetPolicyInputIncludesSubnetContext(t *testing.T) { + subnet := types.Subnet{ + SubnetId: aws.String("subnet-123"), + VpcId: aws.String("vpc-123"), + CidrBlock: aws.String("10.0.1.0/24"), + AvailabilityZone: aws.String("eu-west-2a"), + AvailabilityZoneId: aws.String("euw2-az1"), + MapPublicIpOnLaunch: aws.Bool(false), + AvailableIpAddressCount: aws.Int32(120), + Tags: []types.Tag{{Key: aws.String("Owner"), Value: aws.String("platform")}}, + } + + input, err := BuildSubnetPolicyInput(subnet, "eu-west-2", RegionDatasets{ + Vpcs: []types.Vpc{ + {VpcId: aws.String("vpc-123")}, + {VpcId: aws.String("vpc-other")}, + }, + RouteTables: []types.RouteTable{ + {RouteTableId: aws.String("rtb-main"), VpcId: aws.String("vpc-123"), Associations: []types.RouteTableAssociation{{Main: aws.Bool(true)}}}, + {RouteTableId: aws.String("rtb-explicit"), VpcId: aws.String("vpc-123"), Associations: []types.RouteTableAssociation{{SubnetId: aws.String("subnet-123")}}}, + {RouteTableId: aws.String("rtb-other"), VpcId: aws.String("vpc-other"), Associations: []types.RouteTableAssociation{{SubnetId: aws.String("subnet-other")}}}, + }, + NetworkAcls: []types.NetworkAcl{ + {NetworkAclId: aws.String("acl-123"), VpcId: aws.String("vpc-123"), Associations: []types.NetworkAclAssociation{{SubnetId: aws.String("subnet-123")}}}, + {NetworkAclId: aws.String("acl-other"), VpcId: aws.String("vpc-other"), Associations: []types.NetworkAclAssociation{{SubnetId: aws.String("subnet-other")}}}, + }, + InternetGateways: []types.InternetGateway{ + {InternetGatewayId: aws.String("igw-123"), Attachments: []types.InternetGatewayAttachment{{VpcId: aws.String("vpc-123")}}}, + {InternetGatewayId: aws.String("igw-other"), Attachments: []types.InternetGatewayAttachment{{VpcId: aws.String("vpc-other")}}}, + }, + FlowLogs: []types.FlowLog{ + {FlowLogId: aws.String("fl-vpc"), ResourceId: aws.String("vpc-123"), LogGroupName: aws.String("/aws/vpc/flow")}, + {FlowLogId: aws.String("fl-subnet"), ResourceId: aws.String("subnet-123"), LogGroupName: aws.String("/aws/vpc/subnet")}, + {FlowLogId: aws.String("fl-other"), ResourceId: aws.String("vpc-other"), LogGroupName: aws.String("/aws/vpc/other")}, + }, + LogGroups: []cloudwatchlogstypes.LogGroup{ + {LogGroupName: aws.String("/aws/vpc/flow")}, + {LogGroupName: aws.String("/aws/vpc/subnet")}, + {LogGroupName: aws.String("/aws/vpc/other")}, + }, + }) + if err != nil { + t.Fatalf("BuildSubnetPolicyInput returned error: %v", err) + } + + subnetMap, ok := input["subnet"].(map[string]interface{}) + if !ok { + t.Fatalf("input[subnet] should contain the raw Subnet map") + } + if subnetMap["SubnetId"] != "subnet-123" { + t.Fatalf("subnet.SubnetId = %v, want subnet-123", subnetMap["SubnetId"]) + } + + contextMap, ok := input["subnet_context"].(map[string]interface{}) + if !ok { + t.Fatalf("input[subnet_context] should be a map") + } + + current := contextMap["current"].(map[string]interface{}) + if current["subnet_id"] != "subnet-123" { + t.Fatalf("current.subnet_id = %v, want subnet-123", current["subnet_id"]) + } + if current["region"] != "eu-west-2" { + t.Fatalf("current.region = %v, want eu-west-2", current["region"]) + } + if current["map_public_ip_on_launch"] != false { + t.Fatalf("current.map_public_ip_on_launch = %v, want false", current["map_public_ip_on_launch"]) + } + if current["available_ip_address_count"] != float64(120) { + t.Fatalf("current.available_ip_address_count = %v, want 120", current["available_ip_address_count"]) + } + + if contextMap["explicit_route_table_association"] != true { + t.Fatalf("explicit_route_table_association = %v, want true", contextMap["explicit_route_table_association"]) + } + + routeTable := contextMap["route_table_for_subnet"].(map[string]interface{}) + if routeTable["RouteTableId"] != "rtb-explicit" { + t.Fatalf("route_table_for_subnet.RouteTableId = %v, want rtb-explicit", routeTable["RouteTableId"]) + } + + assertOneItem(t, contextMap, "network_acls_for_subnet") + assertOneItem(t, contextMap, "internet_gateways_for_vpc") + assertItemCount(t, contextMap, "route_tables_in_vpc", 2) + assertItemCount(t, contextMap, "flow_logs_for_vpc", 1) + assertItemCount(t, contextMap, "flow_logs_for_subnet", 1) + assertItemCount(t, contextMap, "log_groups_for_related_flow_logs", 2) + + vpc := contextMap["vpc"].(map[string]interface{}) + if vpc["VpcId"] != "vpc-123" { + t.Fatalf("vpc.VpcId = %v, want vpc-123", vpc["VpcId"]) + } +} + +func TestBuildSubnetPolicyInputFallsBackToMainRouteTable(t *testing.T) { + subnet := types.Subnet{SubnetId: aws.String("subnet-123"), VpcId: aws.String("vpc-123")} + + input, err := BuildSubnetPolicyInput(subnet, "eu-west-2", RegionDatasets{ + RouteTables: []types.RouteTable{ + {RouteTableId: aws.String("rtb-main"), VpcId: aws.String("vpc-123"), Associations: []types.RouteTableAssociation{{Main: aws.Bool(true)}}}, + }, + }) + if err != nil { + t.Fatalf("BuildSubnetPolicyInput returned error: %v", err) + } + + contextMap := input["subnet_context"].(map[string]interface{}) + if contextMap["explicit_route_table_association"] != false { + t.Fatalf("explicit_route_table_association = %v, want false", contextMap["explicit_route_table_association"]) + } + routeTable := contextMap["route_table_for_subnet"].(map[string]interface{}) + if routeTable["RouteTableId"] != "rtb-main" { + t.Fatalf("route_table_for_subnet.RouteTableId = %v, want rtb-main", routeTable["RouteTableId"]) + } +} diff --git a/internal/subnet_test.go b/internal/subnet_test.go new file mode 100644 index 0000000..8e26418 --- /dev/null +++ b/internal/subnet_test.go @@ -0,0 +1,47 @@ +package internal + +import ( + "testing" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/ec2/types" + "github.com/compliance-framework/agent/runner/proto" +) + +func TestSubnetDisplayNameUsesNameTag(t *testing.T) { + subnet := types.Subnet{ + SubnetId: aws.String("subnet-123"), + Tags: []types.Tag{ + {Key: aws.String("Name"), Value: aws.String("prod-private-a")}, + }, + } + + if got := SubnetDisplayName(subnet); got != "prod-private-a" { + t.Fatalf("SubnetDisplayName() = %q, want prod-private-a", got) + } +} + +func TestSubnetDisplayNameFallsBackToID(t *testing.T) { + subnet := types.Subnet{SubnetId: aws.String("subnet-123")} + + if got := SubnetDisplayName(subnet); got != "subnet-123" { + t.Fatalf("SubnetDisplayName() = %q, want subnet-123", got) + } +} + +func TestPrefixSubnetEvidenceTitles(t *testing.T) { + evidences := []*proto.Evidence{ + {Title: "Subnet should set required tags"}, + {Title: ""}, + nil, + } + + PrefixEvidenceTitles(evidences, "subnet-123") + + if got := evidences[0].Title; got != "subnet-123 | Subnet should set required tags" { + t.Fatalf("prefixed title = %q", got) + } + if got := evidences[1].Title; got != "subnet-123" { + t.Fatalf("empty title fallback = %q", got) + } +} diff --git a/main.go b/main.go index acc1ad9..fb80282 100644 --- a/main.go +++ b/main.go @@ -69,9 +69,10 @@ func (l *CompliancePlugin) Eval(request *proto.EvalRequest, apiHelper runner.Api } defaultBehaviorMapping := map[string][]string{ - "aws-vpc-sg-policies": {"sg"}, - "aws-vpc-policies": {"vpc"}, - "aws-vpc-nacl-policies": {"acl"}, + "aws-vpc-sg-policies": {"sg"}, + "aws-vpc-policies": {"vpc"}, + "aws-vpc-subnet-policies": {"subnet"}, + "aws-vpc-nacl-policies": {"acl"}, } policyEval := request.WithDefaultPolicyBehavior(defaultBehaviorMapping) policyPathsByBehavior := buildPolicyPathsByBehavior(policyEval) @@ -114,7 +115,7 @@ func (l *CompliancePlugin) Eval(request *proto.EvalRequest, apiHelper runner.Api } if subnetPolicyPaths := policyPathsByBehavior["subnet"]; len(subnetPolicyPaths) > 0 { - result := internal.EvaluateSubnetPolicies(deps, subnetPolicyPaths, datasets.Subnets, region) + result := internal.EvaluateSubnetPolicies(deps, subnetPolicyPaths, datasets.Subnets, region, datasets) if fatal := applyResourceEvaluationErrors(result, &evalStatus, &accumulatedErrors, false); fatal != nil { return &proto.EvalResponse{Status: proto.ExecutionStatus_FAILURE}, fatal } @@ -221,7 +222,7 @@ func buildRequiredDatasets(policyPathsByBehavior map[string][]string) map[string case "vpc": markRequiredDatasets(requiredDatasets, "vpcs", "vpc_attributes", "dhcp_options", "subnets", "route_tables", "internet_gateways", "vpc_endpoints", "flow_logs", "log_groups", "transit_gateway_attachments") case "subnet": - markRequiredDatasets(requiredDatasets, "subnets") + markRequiredDatasets(requiredDatasets, "vpcs", "subnets", "route_tables", "network_acls", "internet_gateways", "flow_logs", "log_groups") case "sg": markRequiredDatasets(requiredDatasets, "vpcs", "subnets", "security_groups", "network_interfaces", "network_acls", "route_tables", "internet_gateways", "vpc_endpoints", "flow_logs", "log_groups", "transit_gateway_attachments") case "acl": diff --git a/main_test.go b/main_test.go index eabb9ca..84f9086 100644 --- a/main_test.go +++ b/main_test.go @@ -13,3 +13,15 @@ func TestBuildRequiredDatasetsForAclPolicies(t *testing.T) { } } } + +func TestBuildRequiredDatasetsForSubnetPolicies(t *testing.T) { + required := buildRequiredDatasets(map[string][]string{ + "subnet": {"/tmp/policies"}, + }) + + for _, dataset := range []string{"vpcs", "subnets", "route_tables", "network_acls", "internet_gateways", "flow_logs", "log_groups"} { + if !required[dataset] { + t.Fatalf("expected %s to be required for subnet policies", dataset) + } + } +} From c4488f24f1635e104a33caaa9bdce6ae8d50ffb0 Mon Sep 17 00:00:00 2001 From: Reece Bedding Date: Fri, 29 May 2026 12:45:32 +0100 Subject: [PATCH 07/10] feat: add routetable specific enhanced collections --- internal/network_acl.go | 2 +- internal/network_acl_test.go | 2 +- internal/route_table.go | 29 ++- internal/route_table_context.go | 292 +++++++++++++++++++++++++++ internal/route_table_context_test.go | 150 ++++++++++++++ internal/vpc.go | 2 +- main.go | 5 +- main_test.go | 12 ++ 8 files changed, 484 insertions(+), 10 deletions(-) create mode 100644 internal/route_table_context.go create mode 100644 internal/route_table_context_test.go diff --git a/internal/network_acl.go b/internal/network_acl.go index 7629e8f..99ac767 100644 --- a/internal/network_acl.go +++ b/internal/network_acl.go @@ -22,7 +22,7 @@ func EvaluateNetworkAclPolicies(deps EvaluationDependencies, policyPaths []strin deps.Logger.Error("unable to build Network ACL policy input", "network_acl_id", aws.ToString(acl.NetworkAclId), "region", region, "error", err) }, func(evidences []*proto.Evidence, acl types.NetworkAcl) { - PrefixNetworkAclEvidenceTitles(evidences, NetworkAclDisplayName(acl)) + PrefixEvidenceTitles(evidences, NetworkAclDisplayName(acl)) }, ) } diff --git a/internal/network_acl_test.go b/internal/network_acl_test.go index 7e5aea7..1cd3fb7 100644 --- a/internal/network_acl_test.go +++ b/internal/network_acl_test.go @@ -36,7 +36,7 @@ func TestPrefixNetworkAclEvidenceTitles(t *testing.T) { nil, } - PrefixNetworkAclEvidenceTitles(evidences, "acl-123") + PrefixEvidenceTitles(evidences, "acl-123") if got := evidences[0].Title; got != "acl-123 | Network ACL should set required tags" { t.Fatalf("prefixed title = %q", got) diff --git a/internal/route_table.go b/internal/route_table.go index fa42e8b..b0a81b3 100644 --- a/internal/route_table.go +++ b/internal/route_table.go @@ -1,8 +1,12 @@ package internal -import "github.com/aws/aws-sdk-go-v2/service/ec2/types" +import ( + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/ec2/types" + "github.com/compliance-framework/agent/runner/proto" +) -func EvaluateRouteTablePolicies(deps EvaluationDependencies, policyPaths []string, routeTables []types.RouteTable, region string) ResourceEvaluationErrors { +func EvaluateRouteTablePolicies(deps EvaluationDependencies, policyPaths []string, routeTables []types.RouteTable, region string, datasets RegionDatasets) ResourceEvaluationErrors { return evaluateResources( deps, policyPaths, @@ -11,8 +15,23 @@ func EvaluateRouteTablePolicies(deps EvaluationDependencies, policyPaths []strin routeTableCtx := BuildRouteTableEvidenceContext(routeTable, region) return newResourceEvidenceContext(routeTableCtx.Labels, routeTableCtx.Subjects, routeTableCtx.Components, routeTableCtx.Inventory) }, - buildRawResourceInput[types.RouteTable], - nil, - nil, + func(routeTable types.RouteTable) (interface{}, error) { + return BuildRouteTablePolicyInput(routeTable, region, datasets) + }, + func(routeTable types.RouteTable, err error) { + deps.Logger.Error("unable to build Route Table policy input", "route_table_id", aws.ToString(routeTable.RouteTableId), "region", region, "error", err) + }, + func(evidences []*proto.Evidence, routeTable types.RouteTable) { + PrefixEvidenceTitles(evidences, RouteTableDisplayName(routeTable)) + }, ) } + +func RouteTableDisplayName(routeTable types.RouteTable) string { + for _, tag := range routeTable.Tags { + if aws.ToString(tag.Key) == "Name" && aws.ToString(tag.Value) != "" { + return aws.ToString(tag.Value) + } + } + return aws.ToString(routeTable.RouteTableId) +} diff --git a/internal/route_table_context.go b/internal/route_table_context.go new file mode 100644 index 0000000..f2126ad --- /dev/null +++ b/internal/route_table_context.go @@ -0,0 +1,292 @@ +package internal + +import ( + "strings" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/ec2/types" +) + +func BuildRouteTablePolicyInput(routeTable types.RouteTable, region string, datasets RegionDatasets) (map[string]interface{}, error) { + routeTableValue, err := toInterfaceMap(routeTable) + if err != nil { + return nil, err + } + + contextValue, err := toInterfaceMap(buildRouteTableSupplementaryContext(routeTable, region, datasets)) + if err != nil { + return nil, err + } + + return map[string]interface{}{ + "route_table": routeTableValue, + "route_table_context": contextValue, + }, nil +} + +func buildRouteTableSupplementaryContext(routeTable types.RouteTable, region string, datasets RegionDatasets) map[string]interface{} { + routeTableID := aws.ToString(routeTable.RouteTableId) + vpcID := aws.ToString(routeTable.VpcId) + explicitSubnetIDs := routeTableExplicitSubnetIDs(routeTable) + implicitSubnetIDs := routeTableImplicitSubnetIDs(routeTable, datasets.RouteTables, datasets.Subnets, vpcID) + effectiveSubnetIDs := combineIDSets(explicitSubnetIDs, implicitSubnetIDs) + gatewayAssociationIDs := routeTableGatewayAssociationIDs(routeTable) + routeSummaries := summarizeRoutes(routeTable.Routes) + blackholeRoutes := filterRouteSummariesByState(routeSummaries, "blackhole") + + return map[string]interface{}{ + "current": map[string]interface{}{ + "route_table_id": routeTableID, + "vpc_id": vpcID, + "owner_id": aws.ToString(routeTable.OwnerId), + "region": region, + "is_main": routeTableIsMain(routeTable), + "association_count": len(routeTable.Associations), + "explicit_subnet_association_count": len(explicitSubnetIDs), + "implicit_subnet_association_count": len(implicitSubnetIDs), + "effective_subnet_association_count": len(effectiveSubnetIDs), + "gateway_association_count": len(gatewayAssociationIDs), + "route_count": len(routeTable.Routes), + "blackhole_route_count": len(blackholeRoutes), + "explicitly_associated_subnet_ids": sortedIDSetValues(explicitSubnetIDs), + "implicitly_associated_subnet_ids": sortedIDSetValues(implicitSubnetIDs), + "effectively_associated_subnet_ids": sortedIDSetValues(effectiveSubnetIDs), + "gateway_association_ids": sortedIDSetValues(gatewayAssociationIDs), + "has_blackhole_routes": len(blackholeRoutes) > 0, + "has_ipv4_default_route_to_internet_gateway": hasIPv4DefaultRouteToInternetGateway(routeTable.Routes), + "has_ipv6_default_route_to_internet_gateway": hasIPv6DefaultRouteToInternetGateway(routeTable.Routes), + "has_default_route_to_internet_gateway": hasDefaultRouteToInternetGateway(routeTable.Routes), + "has_ipv6_default_route_to_egress_only_gateway": hasIPv6DefaultRouteToEgressOnlyInternetGateway(routeTable.Routes), + "has_default_route_to_nat_gateway": hasDefaultRouteToNatGateway(routeTable.Routes), + "has_default_route_to_transit_gateway": hasDefaultRouteToTransitGateway(routeTable.Routes), + "has_default_route_to_vpc_peering_connection": hasDefaultRouteToVpcPeeringConnection(routeTable.Routes), + "has_gateway_endpoint_routes": len(filterVpcEndpointsByRouteTable(datasets.VpcEndpoints, vpcID, routeTableID)) > 0, + "propagating_vgw_count": len(routeTable.PropagatingVgws), + "tags_present": len(routeTable.Tags) > 0, + }, + "vpc": findVpcByID(datasets.Vpcs, vpcID), + "subnets_in_vpc": filterSubnetsByVpc(datasets.Subnets, vpcID), + "explicitly_associated_subnets": filterSubnetsByIDs(datasets.Subnets, explicitSubnetIDs), + "implicitly_associated_subnets": filterSubnetsByIDs(datasets.Subnets, implicitSubnetIDs), + "effectively_associated_subnets": filterSubnetsByIDs(datasets.Subnets, effectiveSubnetIDs), + "internet_gateways_for_vpc": filterInternetGatewaysByVpc(datasets.InternetGateways, vpcID), + "vpc_endpoints_for_vpc": filterVpcEndpointsByVpc(datasets.VpcEndpoints, vpcID), + "vpc_endpoints_for_route_table": filterVpcEndpointsByRouteTable(datasets.VpcEndpoints, vpcID, routeTableID), + "transit_gateway_attachments_for_vpc": filterTransitGatewayAttachmentsByResourceID(datasets.TransitGatewayAttachments, vpcID), + "route_summaries": routeSummaries, + "blackhole_routes": blackholeRoutes, + } +} + +func routeTableIsMain(routeTable types.RouteTable) bool { + for _, association := range routeTable.Associations { + if aws.ToBool(association.Main) { + return true + } + } + return false +} + +func routeTableExplicitSubnetIDs(routeTable types.RouteTable) map[string]bool { + subnetIDs := make(map[string]bool) + for _, association := range routeTable.Associations { + subnetID := aws.ToString(association.SubnetId) + if subnetID != "" { + subnetIDs[subnetID] = true + } + } + return subnetIDs +} + +func routeTableImplicitSubnetIDs(routeTable types.RouteTable, routeTables []types.RouteTable, subnets []types.Subnet, vpcID string) map[string]bool { + if !routeTableIsMain(routeTable) { + return map[string]bool{} + } + + explicitSubnetIDsInVpc := make(map[string]bool) + for _, candidate := range routeTables { + if aws.ToString(candidate.VpcId) != vpcID { + continue + } + for subnetID := range routeTableExplicitSubnetIDs(candidate) { + explicitSubnetIDsInVpc[subnetID] = true + } + } + + implicitSubnetIDs := make(map[string]bool) + for _, subnet := range subnets { + if aws.ToString(subnet.VpcId) != vpcID { + continue + } + subnetID := aws.ToString(subnet.SubnetId) + if subnetID != "" && !explicitSubnetIDsInVpc[subnetID] { + implicitSubnetIDs[subnetID] = true + } + } + return implicitSubnetIDs +} + +func routeTableGatewayAssociationIDs(routeTable types.RouteTable) map[string]bool { + gatewayIDs := make(map[string]bool) + for _, association := range routeTable.Associations { + gatewayID := aws.ToString(association.GatewayId) + if gatewayID != "" { + gatewayIDs[gatewayID] = true + } + } + return gatewayIDs +} + +func summarizeRoutes(routes []types.Route) []map[string]interface{} { + summaries := make([]map[string]interface{}, 0, len(routes)) + for _, route := range routes { + targetType, targetID := routeTarget(route) + summaries = append(summaries, map[string]interface{}{ + "destination": routeDestination(route), + "destination_type": routeDestinationType(route), + "target_type": targetType, + "target_id": targetID, + "state": string(route.State), + "origin": string(route.Origin), + }) + } + return summaries +} + +func filterRouteSummariesByState(routeSummaries []map[string]interface{}, state string) []map[string]interface{} { + filtered := make([]map[string]interface{}, 0) + for _, routeSummary := range routeSummaries { + if routeSummary["state"] == state { + filtered = append(filtered, routeSummary) + } + } + return filtered +} + +func routeDestination(route types.Route) string { + switch { + case aws.ToString(route.DestinationCidrBlock) != "": + return aws.ToString(route.DestinationCidrBlock) + case aws.ToString(route.DestinationIpv6CidrBlock) != "": + return aws.ToString(route.DestinationIpv6CidrBlock) + case aws.ToString(route.DestinationPrefixListId) != "": + return aws.ToString(route.DestinationPrefixListId) + default: + return "" + } +} + +func routeDestinationType(route types.Route) string { + switch { + case aws.ToString(route.DestinationCidrBlock) != "": + return "ipv4_cidr" + case aws.ToString(route.DestinationIpv6CidrBlock) != "": + return "ipv6_cidr" + case aws.ToString(route.DestinationPrefixListId) != "": + return "prefix_list" + default: + return "unknown" + } +} + +func routeTarget(route types.Route) (string, string) { + targets := []struct { + targetType string + targetID string + }{ + {"carrier_gateway", aws.ToString(route.CarrierGatewayId)}, + {"core_network", aws.ToString(route.CoreNetworkArn)}, + {"egress_only_internet_gateway", aws.ToString(route.EgressOnlyInternetGatewayId)}, + {"gateway", aws.ToString(route.GatewayId)}, + {"instance", aws.ToString(route.InstanceId)}, + {"local_gateway", aws.ToString(route.LocalGatewayId)}, + {"nat_gateway", aws.ToString(route.NatGatewayId)}, + {"network_interface", aws.ToString(route.NetworkInterfaceId)}, + {"transit_gateway", aws.ToString(route.TransitGatewayId)}, + {"vpc_peering_connection", aws.ToString(route.VpcPeeringConnectionId)}, + } + for _, target := range targets { + if target.targetID != "" { + return target.targetType, target.targetID + } + } + return "unknown", "" +} + +func hasIPv4DefaultRouteToInternetGateway(routes []types.Route) bool { + for _, route := range routes { + if aws.ToString(route.DestinationCidrBlock) == "0.0.0.0/0" && strings.HasPrefix(aws.ToString(route.GatewayId), "igw-") { + return true + } + } + return false +} + +func hasIPv6DefaultRouteToInternetGateway(routes []types.Route) bool { + for _, route := range routes { + if aws.ToString(route.DestinationIpv6CidrBlock) == "::/0" && strings.HasPrefix(aws.ToString(route.GatewayId), "igw-") { + return true + } + } + return false +} + +func hasDefaultRouteToInternetGateway(routes []types.Route) bool { + return hasIPv4DefaultRouteToInternetGateway(routes) || hasIPv6DefaultRouteToInternetGateway(routes) +} + +func hasIPv6DefaultRouteToEgressOnlyInternetGateway(routes []types.Route) bool { + for _, route := range routes { + if aws.ToString(route.DestinationIpv6CidrBlock) == "::/0" && aws.ToString(route.EgressOnlyInternetGatewayId) != "" { + return true + } + } + return false +} + +func hasDefaultRouteToNatGateway(routes []types.Route) bool { + for _, route := range routes { + if isDefaultRoute(route) && aws.ToString(route.NatGatewayId) != "" { + return true + } + } + return false +} + +func hasDefaultRouteToTransitGateway(routes []types.Route) bool { + for _, route := range routes { + if isDefaultRoute(route) && aws.ToString(route.TransitGatewayId) != "" { + return true + } + } + return false +} + +func hasDefaultRouteToVpcPeeringConnection(routes []types.Route) bool { + for _, route := range routes { + if isDefaultRoute(route) && aws.ToString(route.VpcPeeringConnectionId) != "" { + return true + } + } + return false +} + +func isDefaultRoute(route types.Route) bool { + return aws.ToString(route.DestinationCidrBlock) == "0.0.0.0/0" || aws.ToString(route.DestinationIpv6CidrBlock) == "::/0" +} + +func filterVpcEndpointsByRouteTable(vpcEndpoints []types.VpcEndpoint, vpcID string, routeTableID string) []types.VpcEndpoint { + filtered := make([]types.VpcEndpoint, 0) + for _, endpoint := range vpcEndpoints { + if aws.ToString(endpoint.VpcId) != vpcID { + continue + } + for _, endpointRouteTableID := range endpoint.RouteTableIds { + if endpointRouteTableID == routeTableID { + filtered = append(filtered, endpoint) + break + } + } + } + return filtered +} diff --git a/internal/route_table_context_test.go b/internal/route_table_context_test.go new file mode 100644 index 0000000..19ed9ae --- /dev/null +++ b/internal/route_table_context_test.go @@ -0,0 +1,150 @@ +package internal + +import ( + "testing" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/ec2/types" +) + +func TestBuildRouteTablePolicyInputIncludesRouteTableContext(t *testing.T) { + routeTable := types.RouteTable{ + RouteTableId: aws.String("rtb-public"), + VpcId: aws.String("vpc-123"), + OwnerId: aws.String("123456789012"), + Associations: []types.RouteTableAssociation{ + {SubnetId: aws.String("subnet-public")}, + }, + Routes: []types.Route{ + {DestinationCidrBlock: aws.String("10.0.0.0/16"), GatewayId: aws.String("local"), State: types.RouteStateActive, Origin: types.RouteOriginCreateRouteTable}, + {DestinationCidrBlock: aws.String("0.0.0.0/0"), GatewayId: aws.String("igw-123"), State: types.RouteStateActive, Origin: types.RouteOriginCreateRoute}, + {DestinationCidrBlock: aws.String("10.20.0.0/16"), TransitGatewayId: aws.String("tgw-123"), State: types.RouteStateBlackhole, Origin: types.RouteOriginCreateRoute}, + }, + Tags: []types.Tag{{Key: aws.String("Name"), Value: aws.String("public-rt")}}, + } + + input, err := BuildRouteTablePolicyInput(routeTable, "eu-west-2", RegionDatasets{ + Vpcs: []types.Vpc{ + {VpcId: aws.String("vpc-123")}, + {VpcId: aws.String("vpc-other")}, + }, + Subnets: []types.Subnet{ + {SubnetId: aws.String("subnet-public"), VpcId: aws.String("vpc-123")}, + {SubnetId: aws.String("subnet-private"), VpcId: aws.String("vpc-123")}, + {SubnetId: aws.String("subnet-other"), VpcId: aws.String("vpc-other")}, + }, + InternetGateways: []types.InternetGateway{ + {InternetGatewayId: aws.String("igw-123"), Attachments: []types.InternetGatewayAttachment{{VpcId: aws.String("vpc-123")}}}, + {InternetGatewayId: aws.String("igw-other"), Attachments: []types.InternetGatewayAttachment{{VpcId: aws.String("vpc-other")}}}, + }, + VpcEndpoints: []types.VpcEndpoint{ + {VpcEndpointId: aws.String("vpce-123"), VpcId: aws.String("vpc-123"), RouteTableIds: []string{"rtb-public"}}, + {VpcEndpointId: aws.String("vpce-other"), VpcId: aws.String("vpc-other"), RouteTableIds: []string{"rtb-public"}}, + }, + TransitGatewayAttachments: []types.TransitGatewayAttachment{ + {TransitGatewayAttachmentId: aws.String("tgw-attach-123"), ResourceId: aws.String("vpc-123")}, + {TransitGatewayAttachmentId: aws.String("tgw-attach-other"), ResourceId: aws.String("vpc-other")}, + }, + }) + if err != nil { + t.Fatalf("BuildRouteTablePolicyInput returned error: %v", err) + } + + routeTableMap, ok := input["route_table"].(map[string]interface{}) + if !ok { + t.Fatalf("input[route_table] should contain the raw RouteTable map") + } + if routeTableMap["RouteTableId"] != "rtb-public" { + t.Fatalf("route_table.RouteTableId = %v, want rtb-public", routeTableMap["RouteTableId"]) + } + + contextMap, ok := input["route_table_context"].(map[string]interface{}) + if !ok { + t.Fatalf("input[route_table_context] should be a map") + } + + current := contextMap["current"].(map[string]interface{}) + if current["route_table_id"] != "rtb-public" { + t.Fatalf("current.route_table_id = %v, want rtb-public", current["route_table_id"]) + } + if current["region"] != "eu-west-2" { + t.Fatalf("current.region = %v, want eu-west-2", current["region"]) + } + if current["is_main"] != false { + t.Fatalf("current.is_main = %v, want false", current["is_main"]) + } + if current["explicit_subnet_association_count"] != float64(1) { + t.Fatalf("current.explicit_subnet_association_count = %v, want 1", current["explicit_subnet_association_count"]) + } + if current["effective_subnet_association_count"] != float64(1) { + t.Fatalf("current.effective_subnet_association_count = %v, want 1", current["effective_subnet_association_count"]) + } + if current["has_default_route_to_internet_gateway"] != true { + t.Fatalf("current.has_default_route_to_internet_gateway = %v, want true", current["has_default_route_to_internet_gateway"]) + } + if current["has_blackhole_routes"] != true { + t.Fatalf("current.has_blackhole_routes = %v, want true", current["has_blackhole_routes"]) + } + if current["blackhole_route_count"] != float64(1) { + t.Fatalf("current.blackhole_route_count = %v, want 1", current["blackhole_route_count"]) + } + if current["has_gateway_endpoint_routes"] != true { + t.Fatalf("current.has_gateway_endpoint_routes = %v, want true", current["has_gateway_endpoint_routes"]) + } + + assertOneItem(t, contextMap, "explicitly_associated_subnets") + assertOneItem(t, contextMap, "effectively_associated_subnets") + assertOneItem(t, contextMap, "internet_gateways_for_vpc") + assertOneItem(t, contextMap, "vpc_endpoints_for_route_table") + assertOneItem(t, contextMap, "transit_gateway_attachments_for_vpc") + assertItemCount(t, contextMap, "subnets_in_vpc", 2) + assertItemCount(t, contextMap, "route_summaries", 3) + assertItemCount(t, contextMap, "blackhole_routes", 1) + + vpc := contextMap["vpc"].(map[string]interface{}) + if vpc["VpcId"] != "vpc-123" { + t.Fatalf("vpc.VpcId = %v, want vpc-123", vpc["VpcId"]) + } +} + +func TestBuildRouteTablePolicyInputIncludesImplicitMainRouteAssociations(t *testing.T) { + mainRouteTable := types.RouteTable{ + RouteTableId: aws.String("rtb-main"), + VpcId: aws.String("vpc-123"), + Associations: []types.RouteTableAssociation{{Main: aws.Bool(true)}}, + } + + input, err := BuildRouteTablePolicyInput(mainRouteTable, "eu-west-2", RegionDatasets{ + Subnets: []types.Subnet{ + {SubnetId: aws.String("subnet-main-a"), VpcId: aws.String("vpc-123")}, + {SubnetId: aws.String("subnet-explicit"), VpcId: aws.String("vpc-123")}, + {SubnetId: aws.String("subnet-other"), VpcId: aws.String("vpc-other")}, + }, + RouteTables: []types.RouteTable{ + mainRouteTable, + {RouteTableId: aws.String("rtb-explicit"), VpcId: aws.String("vpc-123"), Associations: []types.RouteTableAssociation{{SubnetId: aws.String("subnet-explicit")}}}, + }, + }) + if err != nil { + t.Fatalf("BuildRouteTablePolicyInput returned error: %v", err) + } + + contextMap := input["route_table_context"].(map[string]interface{}) + current := contextMap["current"].(map[string]interface{}) + if current["is_main"] != true { + t.Fatalf("current.is_main = %v, want true", current["is_main"]) + } + if current["implicit_subnet_association_count"] != float64(1) { + t.Fatalf("current.implicit_subnet_association_count = %v, want 1", current["implicit_subnet_association_count"]) + } + if current["effective_subnet_association_count"] != float64(1) { + t.Fatalf("current.effective_subnet_association_count = %v, want 1", current["effective_subnet_association_count"]) + } + + implicitSubnetIDs := current["implicitly_associated_subnet_ids"].([]interface{}) + if len(implicitSubnetIDs) != 1 || implicitSubnetIDs[0] != "subnet-main-a" { + t.Fatalf("implicitly_associated_subnet_ids = %v, want [subnet-main-a]", implicitSubnetIDs) + } + assertOneItem(t, contextMap, "implicitly_associated_subnets") + assertOneItem(t, contextMap, "effectively_associated_subnets") +} diff --git a/internal/vpc.go b/internal/vpc.go index 1a9133e..f9ea78e 100644 --- a/internal/vpc.go +++ b/internal/vpc.go @@ -22,7 +22,7 @@ func EvaluateVpcPolicies(deps EvaluationDependencies, policyPaths []string, vpcs deps.Logger.Error("unable to build VPC policy input", "vpc_id", aws.ToString(vpc.VpcId), "region", region, "error", err) }, func(evidences []*proto.Evidence, vpc types.Vpc) { - PrefixVpcEvidenceTitles(evidences, VpcDisplayName(vpc)) + PrefixEvidenceTitles(evidences, VpcDisplayName(vpc)) }, ) } diff --git a/main.go b/main.go index fb80282..e26eaef 100644 --- a/main.go +++ b/main.go @@ -73,6 +73,7 @@ func (l *CompliancePlugin) Eval(request *proto.EvalRequest, apiHelper runner.Api "aws-vpc-policies": {"vpc"}, "aws-vpc-subnet-policies": {"subnet"}, "aws-vpc-nacl-policies": {"acl"}, + "aws-vpc-rt-policies": {"rt"}, } policyEval := request.WithDefaultPolicyBehavior(defaultBehaviorMapping) policyPathsByBehavior := buildPolicyPathsByBehavior(policyEval) @@ -136,7 +137,7 @@ func (l *CompliancePlugin) Eval(request *proto.EvalRequest, apiHelper runner.Api } if routeTablePolicyPaths := policyPathsByBehavior["rt"]; len(routeTablePolicyPaths) > 0 { - result := internal.EvaluateRouteTablePolicies(deps, routeTablePolicyPaths, datasets.RouteTables, region) + result := internal.EvaluateRouteTablePolicies(deps, routeTablePolicyPaths, datasets.RouteTables, region, datasets) if fatal := applyResourceEvaluationErrors(result, &evalStatus, &accumulatedErrors, false); fatal != nil { return &proto.EvalResponse{Status: proto.ExecutionStatus_FAILURE}, fatal } @@ -228,7 +229,7 @@ func buildRequiredDatasets(policyPathsByBehavior map[string][]string) map[string case "acl": markRequiredDatasets(requiredDatasets, "vpcs", "subnets", "network_acls", "route_tables", "internet_gateways", "flow_logs", "log_groups", "network_interfaces") case "rt": - markRequiredDatasets(requiredDatasets, "route_tables") + markRequiredDatasets(requiredDatasets, "vpcs", "subnets", "route_tables", "internet_gateways", "vpc_endpoints", "transit_gateway_attachments") case "igw": markRequiredDatasets(requiredDatasets, "internet_gateways") case "endpoint": diff --git a/main_test.go b/main_test.go index 84f9086..64b4d90 100644 --- a/main_test.go +++ b/main_test.go @@ -25,3 +25,15 @@ func TestBuildRequiredDatasetsForSubnetPolicies(t *testing.T) { } } } + +func TestBuildRequiredDatasetsForRouteTablePolicies(t *testing.T) { + required := buildRequiredDatasets(map[string][]string{ + "rt": {"/tmp/policies"}, + }) + + for _, dataset := range []string{"vpcs", "subnets", "route_tables", "internet_gateways", "vpc_endpoints", "transit_gateway_attachments"} { + if !required[dataset] { + t.Fatalf("expected %s to be required for route table policies", dataset) + } + } +} From 661fb218f5aa009c44185f9b6aeddc42e78e19f3 Mon Sep 17 00:00:00 2001 From: Reece Bedding Date: Fri, 29 May 2026 13:38:30 +0100 Subject: [PATCH 08/10] chore: update readme --- README.md | 85 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 84 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a7fa098..3f093ee 100644 --- a/README.md +++ b/README.md @@ -1 +1,84 @@ -# AWS networking sercurity plugin +# AWS VPC CCF Plugin + +This plugin collects read-only AWS VPC networking data from EC2 and CloudWatch Logs, evaluates CCF Rego policy bundles, and emits evidence back through the CCF agent. + +## Supported resource families + +The collector can evaluate policies for: + +- VPCs +- subnets +- security groups +- network ACLs +- route tables + +## How it fits in CCF + +The CCF agent starts this binary through HashiCorp `go-plugin`, passes configuration and policy paths over gRPC, and receives generated evidence through the runner callback. This repository does not call the CCF API directly. + +## Default policy bundle mapping + +| Repository | Behavior | Primary input | +| --- | --- | --- | +| `plugin-aws-vpc-policies` | `vpc` | `input.vpc` + `input.vpc_context` | +| `plugin-aws-vpc-subnet-policies` | `subnet` | `input.subnet` + `input.subnet_context` | +| `plugin-aws-vpc-sg-policies` | `sg` | `input.security_group` + `input.sg_context` | +| `plugin-aws-vpc-nacl-policies` | `acl` | `input.network_acl` + `input.nacl_context` | +| `plugin-aws-vpc-rt-policies` | `rt` | `input.route_table` + `input.route_table_context` | + +## Configuration + +The plugin expects: + +- AWS credentials through the default AWS SDK credential chain +- target regions from `config.regions` or `config.region` +- `AWS_REGION` as a fallback when plugin config does not provide a region + +Any agent-supplied `policy_data` is passed through to Rego as `data.*`. + +## Data collected + +Depending on the selected policy bundles, the plugin can collect and correlate: + +- VPCs and VPC attributes +- DHCP options +- subnets +- route tables +- internet gateways +- VPC endpoints +- security groups +- network ACLs +- flow logs +- related CloudWatch log groups +- transit gateway attachments +- network interfaces + +## Development + +Run the local test suite with: + +```shell +go test ./... +``` + +Or use the Makefile wrapper: + +```shell +make test +``` + +Build the plugin binary with: + +```shell +make build +``` + +This writes the compiled plugin to `dist/plugin`. + +## Related repositories + +- `../plugin-aws-vpc-policies` +- `../plugin-aws-vpc-subnet-policies` +- `../plugin-aws-vpc-sg-policies` +- `../plugin-aws-vpc-nacl-policies` +- `../plugin-aws-vpc-rt-policies` From 0932e3e07ba7b872a78a87ef46ac6899b9408053 Mon Sep 17 00:00:00 2001 From: Reece Bedding Date: Mon, 1 Jun 2026 11:40:16 +0100 Subject: [PATCH 09/10] feat: add subject templates --- internal/evidence.go | 38 +++++----- internal/evidence_labels_test.go | 95 ++++++++++++++++++++++++ internal/flow_log.go | 18 ----- internal/internet_gateway.go | 18 ----- internal/log_group.go | 18 ----- internal/vpc_endpoint.go | 18 ----- main.go | 42 +---------- main_test.go | 19 +++++ subject_templates.go | 110 ++++++++++++++++++++++++++++ subject_templates_test.go | 120 +++++++++++++++++++++++++++++++ 10 files changed, 365 insertions(+), 131 deletions(-) create mode 100644 internal/evidence_labels_test.go delete mode 100644 internal/flow_log.go delete mode 100644 internal/internet_gateway.go delete mode 100644 internal/log_group.go delete mode 100644 internal/vpc_endpoint.go create mode 100644 subject_templates.go create mode 100644 subject_templates_test.go diff --git a/internal/evidence.go b/internal/evidence.go index 220a657..75a26be 100644 --- a/internal/evidence.go +++ b/internal/evidence.go @@ -89,7 +89,7 @@ func BuildVpcEvidenceContext(vpc types.Vpc, region string) VpcEvidenceContext { labels := map[string]string{ "provider": "aws", "type": string(ResourceTypeVPC), - "vpc-id": vpcId, + "vpc_id": vpcId, "cidr": aws.ToString(vpc.CidrBlock), "region": region, } @@ -163,8 +163,8 @@ func BuildSubnetEvidenceContext(subnet types.Subnet, region string) SubnetEviden labels := map[string]string{ "provider": "aws", "type": string(ResourceTypeSubnet), - "subnet-id": subnetId, - "vpc-id": vpcId, + "subnet_id": subnetId, + "vpc_id": vpcId, "cidr": aws.ToString(subnet.CidrBlock), "az": aws.ToString(subnet.AvailabilityZone), "region": region, @@ -247,8 +247,8 @@ func BuildSecurityGroupEvidenceContext(group types.SecurityGroup, region string) labels := map[string]string{ "provider": "aws", "type": string(ResourceTypeSecurityGroup), - "group-id": groupId, - "_vpc-id": vpcId, + "group_id": groupId, + "vpc_id": vpcId, "region": region, } @@ -317,8 +317,8 @@ func BuildNetworkAclEvidenceContext(acl types.NetworkAcl, region string) Network labels := map[string]string{ "provider": "aws", "type": string(ResourceTypeNetworkAcl), - "acl-id": aclId, - "vpc-id": vpcId, + "acl_id": aclId, + "vpc_id": vpcId, "region": region, } @@ -391,8 +391,8 @@ func BuildRouteTableEvidenceContext(routeTable types.RouteTable, region string) labels := map[string]string{ "provider": "aws", "type": string(ResourceTypeRouteTable), - "route-table-id": rtId, - "vpc-id": vpcId, + "route_table_id": rtId, + "vpc_id": vpcId, "region": region, } @@ -466,8 +466,8 @@ func BuildInternetGatewayEvidenceContext(igw types.InternetGateway, region strin labels := map[string]string{ "provider": "aws", "type": string(ResourceTypeInternetGateway), - "igw-id": igwId, - "vpc-id": vpcId, + "igw_id": igwId, + "vpc_id": vpcId, "region": region, } @@ -536,9 +536,9 @@ func BuildVpcEndpointEvidenceContext(endpoint types.VpcEndpoint, region string) labels := map[string]string{ "provider": "aws", "type": string(ResourceTypeVpcEndpoint), - "endpoint-id": endpointId, - "vpc-id": vpcId, - "service-name": aws.ToString(endpoint.ServiceName), + "endpoint_id": endpointId, + "vpc_id": vpcId, + "service_name": aws.ToString(endpoint.ServiceName), "state": string(endpoint.State), "region": region, } @@ -611,10 +611,10 @@ func BuildFlowLogEvidenceContext(flowLog types.FlowLog, region string) FlowLogEv labels := map[string]string{ "provider": "aws", "type": string(ResourceTypeFlowLog), - "flow-log-id": flowLogId, - "resource-id": aws.ToString(flowLog.ResourceId), - "traffic-type": string(flowLog.TrafficType), - "flow-log-status": aws.ToString(flowLog.FlowLogStatus), + "flow_log_id": flowLogId, + "resource_id": aws.ToString(flowLog.ResourceId), + "traffic_type": string(flowLog.TrafficType), + "flow_log_status": aws.ToString(flowLog.FlowLogStatus), "region": region, } @@ -690,7 +690,7 @@ func BuildLogGroupEvidenceContext(logGroup cloudwatchlogstypes.LogGroup, region labels := map[string]string{ "provider": "aws", "type": string(ResourceTypeLogGroup), - "log-group-name": logGroupName, + "log_group_name": logGroupName, "region": region, } diff --git a/internal/evidence_labels_test.go b/internal/evidence_labels_test.go new file mode 100644 index 0000000..8d75f2a --- /dev/null +++ b/internal/evidence_labels_test.go @@ -0,0 +1,95 @@ +package internal + +import ( + "strings" + "testing" + + "github.com/aws/aws-sdk-go-v2/aws" + cloudwatchlogstypes "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs/types" + "github.com/aws/aws-sdk-go-v2/service/ec2/types" +) + +func TestEvidenceLabelKeysUseUnderscoreNotation(t *testing.T) { + contexts := []struct { + name string + labels map[string]string + }{ + { + name: "vpc", + labels: BuildVpcEvidenceContext(types.Vpc{ + VpcId: aws.String("vpc-123"), + CidrBlock: aws.String("10.0.0.0/16"), + }, "eu-west-2").Labels, + }, + { + name: "subnet", + labels: BuildSubnetEvidenceContext(types.Subnet{ + SubnetId: aws.String("subnet-123"), + VpcId: aws.String("vpc-123"), + CidrBlock: aws.String("10.0.1.0/24"), + AvailabilityZone: aws.String("eu-west-2a"), + }, "eu-west-2").Labels, + }, + { + name: "security-group", + labels: BuildSecurityGroupEvidenceContext(types.SecurityGroup{ + GroupId: aws.String("sg-123"), + VpcId: aws.String("vpc-123"), + }, "eu-west-2").Labels, + }, + { + name: "network-acl", + labels: BuildNetworkAclEvidenceContext(types.NetworkAcl{ + NetworkAclId: aws.String("acl-123"), + VpcId: aws.String("vpc-123"), + }, "eu-west-2").Labels, + }, + { + name: "route-table", + labels: BuildRouteTableEvidenceContext(types.RouteTable{ + RouteTableId: aws.String("rtb-123"), + VpcId: aws.String("vpc-123"), + }, "eu-west-2").Labels, + }, + { + name: "internet-gateway", + labels: BuildInternetGatewayEvidenceContext(types.InternetGateway{ + InternetGatewayId: aws.String("igw-123"), + Attachments: []types.InternetGatewayAttachment{ + {VpcId: aws.String("vpc-123")}, + }, + }, "eu-west-2").Labels, + }, + { + name: "vpc-endpoint", + labels: BuildVpcEndpointEvidenceContext(types.VpcEndpoint{ + VpcEndpointId: aws.String("vpce-123"), + VpcId: aws.String("vpc-123"), + ServiceName: aws.String("com.amazonaws.eu-west-2.s3"), + }, "eu-west-2").Labels, + }, + { + name: "flow-log", + labels: BuildFlowLogEvidenceContext(types.FlowLog{ + FlowLogId: aws.String("fl-123"), + ResourceId: aws.String("vpc-123"), + TrafficType: types.TrafficTypeAll, + FlowLogStatus: aws.String("ACTIVE"), + }, "eu-west-2").Labels, + }, + { + name: "log-group", + labels: BuildLogGroupEvidenceContext(cloudwatchlogstypes.LogGroup{ + LogGroupName: aws.String("/aws/vpc/flowlogs"), + }, "eu-west-2").Labels, + }, + } + + for _, context := range contexts { + for key := range context.labels { + if strings.Contains(key, "-") { + t.Fatalf("%s evidence label key %q must use underscore notation", context.name, key) + } + } + } +} diff --git a/internal/flow_log.go b/internal/flow_log.go deleted file mode 100644 index 772ada6..0000000 --- a/internal/flow_log.go +++ /dev/null @@ -1,18 +0,0 @@ -package internal - -import "github.com/aws/aws-sdk-go-v2/service/ec2/types" - -func EvaluateFlowLogPolicies(deps EvaluationDependencies, policyPaths []string, flowLogs []types.FlowLog, region string) ResourceEvaluationErrors { - return evaluateResources( - deps, - policyPaths, - flowLogs, - func(flowLog types.FlowLog) ResourceEvidenceContext { - flowLogCtx := BuildFlowLogEvidenceContext(flowLog, region) - return newResourceEvidenceContext(flowLogCtx.Labels, flowLogCtx.Subjects, flowLogCtx.Components, flowLogCtx.Inventory) - }, - buildRawResourceInput[types.FlowLog], - nil, - nil, - ) -} diff --git a/internal/internet_gateway.go b/internal/internet_gateway.go deleted file mode 100644 index dac9b05..0000000 --- a/internal/internet_gateway.go +++ /dev/null @@ -1,18 +0,0 @@ -package internal - -import "github.com/aws/aws-sdk-go-v2/service/ec2/types" - -func EvaluateInternetGatewayPolicies(deps EvaluationDependencies, policyPaths []string, internetGateways []types.InternetGateway, region string) ResourceEvaluationErrors { - return evaluateResources( - deps, - policyPaths, - internetGateways, - func(internetGateway types.InternetGateway) ResourceEvidenceContext { - internetGatewayCtx := BuildInternetGatewayEvidenceContext(internetGateway, region) - return newResourceEvidenceContext(internetGatewayCtx.Labels, internetGatewayCtx.Subjects, internetGatewayCtx.Components, internetGatewayCtx.Inventory) - }, - buildRawResourceInput[types.InternetGateway], - nil, - nil, - ) -} diff --git a/internal/log_group.go b/internal/log_group.go deleted file mode 100644 index dbc01de..0000000 --- a/internal/log_group.go +++ /dev/null @@ -1,18 +0,0 @@ -package internal - -import cloudwatchlogstypes "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs/types" - -func EvaluateLogGroupPolicies(deps EvaluationDependencies, policyPaths []string, logGroups []cloudwatchlogstypes.LogGroup, region string) ResourceEvaluationErrors { - return evaluateResources( - deps, - policyPaths, - logGroups, - func(logGroup cloudwatchlogstypes.LogGroup) ResourceEvidenceContext { - logGroupCtx := BuildLogGroupEvidenceContext(logGroup, region) - return newResourceEvidenceContext(logGroupCtx.Labels, logGroupCtx.Subjects, logGroupCtx.Components, logGroupCtx.Inventory) - }, - buildRawResourceInput[cloudwatchlogstypes.LogGroup], - nil, - nil, - ) -} diff --git a/internal/vpc_endpoint.go b/internal/vpc_endpoint.go deleted file mode 100644 index 50cdef6..0000000 --- a/internal/vpc_endpoint.go +++ /dev/null @@ -1,18 +0,0 @@ -package internal - -import "github.com/aws/aws-sdk-go-v2/service/ec2/types" - -func EvaluateVpcEndpointPolicies(deps EvaluationDependencies, policyPaths []string, vpcEndpoints []types.VpcEndpoint, region string) ResourceEvaluationErrors { - return evaluateResources( - deps, - policyPaths, - vpcEndpoints, - func(vpcEndpoint types.VpcEndpoint) ResourceEvidenceContext { - vpcEndpointCtx := BuildVpcEndpointEvidenceContext(vpcEndpoint, region) - return newResourceEvidenceContext(vpcEndpointCtx.Labels, vpcEndpointCtx.Subjects, vpcEndpointCtx.Components, vpcEndpointCtx.Inventory) - }, - buildRawResourceInput[types.VpcEndpoint], - nil, - nil, - ) -} diff --git a/main.go b/main.go index e26eaef..d5e8932 100644 --- a/main.go +++ b/main.go @@ -31,7 +31,8 @@ func (l *CompliancePlugin) Configure(req *proto.ConfigureRequest) (*proto.Config } func (l *CompliancePlugin) Init(req *proto.InitRequest, apiHelper runner.ApiHelper) (*proto.InitResponse, error) { - return &proto.InitResponse{}, nil + ctx := context.Background() + return runner.InitWithSubjectsAndRisksFromPolicies(ctx, l.logger, req, apiHelper, buildSubjectTemplates()) } func (l *CompliancePlugin) Eval(request *proto.EvalRequest, apiHelper runner.ApiHelper) (*proto.EvalResponse, error) { @@ -143,33 +144,6 @@ func (l *CompliancePlugin) Eval(request *proto.EvalRequest, apiHelper runner.Api } } - if internetGatewayPolicyPaths := policyPathsByBehavior["igw"]; len(internetGatewayPolicyPaths) > 0 { - result := internal.EvaluateInternetGatewayPolicies(deps, internetGatewayPolicyPaths, datasets.InternetGateways, region) - if fatal := applyResourceEvaluationErrors(result, &evalStatus, &accumulatedErrors, false); fatal != nil { - return &proto.EvalResponse{Status: proto.ExecutionStatus_FAILURE}, fatal - } - } - - if vpcEndpointPolicyPaths := policyPathsByBehavior["endpoint"]; len(vpcEndpointPolicyPaths) > 0 { - result := internal.EvaluateVpcEndpointPolicies(deps, vpcEndpointPolicyPaths, datasets.VpcEndpoints, region) - if fatal := applyResourceEvaluationErrors(result, &evalStatus, &accumulatedErrors, false); fatal != nil { - return &proto.EvalResponse{Status: proto.ExecutionStatus_FAILURE}, fatal - } - } - - if flowLogPolicyPaths := policyPathsByBehavior["flow-log"]; len(flowLogPolicyPaths) > 0 { - result := internal.EvaluateFlowLogPolicies(deps, flowLogPolicyPaths, datasets.FlowLogs, region) - if fatal := applyResourceEvaluationErrors(result, &evalStatus, &accumulatedErrors, false); fatal != nil { - return &proto.EvalResponse{Status: proto.ExecutionStatus_FAILURE}, fatal - } - } - - if logGroupPolicyPaths := policyPathsByBehavior["log-group"]; len(logGroupPolicyPaths) > 0 { - result := internal.EvaluateLogGroupPolicies(deps, logGroupPolicyPaths, datasets.LogGroups, region) - if fatal := applyResourceEvaluationErrors(result, &evalStatus, &accumulatedErrors, false); fatal != nil { - return &proto.EvalResponse{Status: proto.ExecutionStatus_FAILURE}, fatal - } - } } return &proto.EvalResponse{ @@ -194,10 +168,6 @@ func supportedPolicyBehaviors() []string { "sg", "acl", "rt", - "igw", - "endpoint", - "flow-log", - "log-group", } } @@ -230,14 +200,6 @@ func buildRequiredDatasets(policyPathsByBehavior map[string][]string) map[string markRequiredDatasets(requiredDatasets, "vpcs", "subnets", "network_acls", "route_tables", "internet_gateways", "flow_logs", "log_groups", "network_interfaces") case "rt": markRequiredDatasets(requiredDatasets, "vpcs", "subnets", "route_tables", "internet_gateways", "vpc_endpoints", "transit_gateway_attachments") - case "igw": - markRequiredDatasets(requiredDatasets, "internet_gateways") - case "endpoint": - markRequiredDatasets(requiredDatasets, "vpc_endpoints") - case "flow-log": - markRequiredDatasets(requiredDatasets, "flow_logs") - case "log-group": - markRequiredDatasets(requiredDatasets, "log_groups") default: continue } diff --git a/main_test.go b/main_test.go index 64b4d90..a66494f 100644 --- a/main_test.go +++ b/main_test.go @@ -37,3 +37,22 @@ func TestBuildRequiredDatasetsForRouteTablePolicies(t *testing.T) { } } } + +func TestSupportedPolicyBehaviorsOnlyIncludesPrimaryVpcBundles(t *testing.T) { + behaviors := map[string]bool{} + for _, behavior := range supportedPolicyBehaviors() { + behaviors[behavior] = true + } + + for _, expected := range []string{"vpc", "subnet", "sg", "acl", "rt"} { + if !behaviors[expected] { + t.Fatalf("expected behavior %s to be supported", expected) + } + } + + for _, contextOnly := range []string{"igw", "endpoint", "flow-log", "log-group"} { + if behaviors[contextOnly] { + t.Fatalf("context-only behavior %s must not be supported directly", contextOnly) + } + } +} diff --git a/subject_templates.go b/subject_templates.go new file mode 100644 index 0000000..7b5db69 --- /dev/null +++ b/subject_templates.go @@ -0,0 +1,110 @@ +package main + +import "github.com/compliance-framework/agent/runner/proto" + +func buildSubjectTemplates() []*proto.SubjectTemplate { + return []*proto.SubjectTemplate{ + { + Name: "aws-vpc", + Type: proto.SubjectType_SUBJECT_TYPE_COMPONENT, + TitleTemplate: `AWS VPC {{ .vpc_id }} in {{ .region }}`, + DescriptionTemplate: `Amazon VPC {{ .vpc_id }} with CIDR {{ .cidr }} in AWS region {{ .region }}.`, + PurposeTemplate: "Represents an AWS VPC evaluated for networking compliance posture.", + IdentityLabelKeys: []string{"provider", "region", "vpc_id"}, + SelectorLabels: selectorLabelsForType("vpc"), + LabelSchema: labelSchema( + label("provider", "Cloud provider for the evaluated resource"), + label("type", "VPC plugin resource type"), + label("vpc_id", "AWS VPC identifier"), + label("cidr", "Primary IPv4 CIDR block associated with the VPC"), + label("region", "AWS region containing the VPC"), + ), + }, + { + Name: "aws-vpc-subnet", + Type: proto.SubjectType_SUBJECT_TYPE_COMPONENT, + TitleTemplate: `AWS subnet {{ .subnet_id }} in {{ .region }}`, + DescriptionTemplate: `Amazon VPC subnet {{ .subnet_id }} in VPC {{ .vpc_id }} with CIDR {{ .cidr }}.`, + PurposeTemplate: "Represents an AWS VPC subnet evaluated for network segmentation and routing posture.", + IdentityLabelKeys: []string{"provider", "region", "subnet_id"}, + SelectorLabels: selectorLabelsForType("subnet"), + LabelSchema: labelSchema( + label("provider", "Cloud provider for the evaluated resource"), + label("type", "VPC plugin resource type"), + label("subnet_id", "AWS subnet identifier"), + label("vpc_id", "AWS VPC identifier containing the subnet"), + label("cidr", "IPv4 CIDR block associated with the subnet"), + label("az", "AWS availability zone containing the subnet"), + label("region", "AWS region containing the subnet"), + ), + }, + { + Name: "aws-vpc-security-group", + Type: proto.SubjectType_SUBJECT_TYPE_COMPONENT, + TitleTemplate: `AWS security group {{ .group_id }} in {{ .region }}`, + DescriptionTemplate: `Amazon VPC security group {{ .group_id }} in VPC {{ .vpc_id }}.`, + PurposeTemplate: "Represents an AWS security group evaluated for network access-control posture.", + IdentityLabelKeys: []string{"provider", "region", "group_id"}, + SelectorLabels: selectorLabelsForType("security-group"), + LabelSchema: labelSchema( + label("provider", "Cloud provider for the evaluated resource"), + label("type", "VPC plugin resource type"), + label("group_id", "AWS security group identifier"), + label("vpc_id", "AWS VPC identifier containing the security group"), + label("region", "AWS region containing the security group"), + ), + }, + { + Name: "aws-vpc-network-acl", + Type: proto.SubjectType_SUBJECT_TYPE_COMPONENT, + TitleTemplate: `AWS network ACL {{ .acl_id }} in {{ .region }}`, + DescriptionTemplate: `Amazon VPC network ACL {{ .acl_id }} in VPC {{ .vpc_id }}.`, + PurposeTemplate: "Represents an AWS network ACL evaluated for subnet-level network control posture.", + IdentityLabelKeys: []string{"provider", "region", "acl_id"}, + SelectorLabels: selectorLabelsForType("nacl"), + LabelSchema: labelSchema( + label("provider", "Cloud provider for the evaluated resource"), + label("type", "VPC plugin resource type"), + label("acl_id", "AWS network ACL identifier"), + label("vpc_id", "AWS VPC identifier containing the network ACL"), + label("region", "AWS region containing the network ACL"), + ), + }, + { + Name: "aws-vpc-route-table", + Type: proto.SubjectType_SUBJECT_TYPE_COMPONENT, + TitleTemplate: `AWS route table {{ .route_table_id }} in {{ .region }}`, + DescriptionTemplate: `Amazon VPC route table {{ .route_table_id }} in VPC {{ .vpc_id }}.`, + PurposeTemplate: "Represents an AWS route table evaluated for network routing and reachability posture.", + IdentityLabelKeys: []string{"provider", "region", "route_table_id"}, + SelectorLabels: selectorLabelsForType("route-table"), + LabelSchema: labelSchema( + label("provider", "Cloud provider for the evaluated resource"), + label("type", "VPC plugin resource type"), + label("route_table_id", "AWS route table identifier"), + label("vpc_id", "AWS VPC identifier containing the route table"), + label("region", "AWS region containing the route table"), + ), + }, + } +} + +func selectorLabelsForType(resourceType string) []*proto.SubjectLabelSelector { + return []*proto.SubjectLabelSelector{ + { + Key: "type", + Value: resourceType, + }, + } +} + +func label(key string, description string) *proto.SubjectLabelSchema { + return &proto.SubjectLabelSchema{ + Key: key, + Description: description, + } +} + +func labelSchema(labels ...*proto.SubjectLabelSchema) []*proto.SubjectLabelSchema { + return labels +} diff --git a/subject_templates_test.go b/subject_templates_test.go new file mode 100644 index 0000000..62eda2a --- /dev/null +++ b/subject_templates_test.go @@ -0,0 +1,120 @@ +package main + +import ( + "bytes" + "strings" + "testing" + "text/template" + + "github.com/compliance-framework/agent/runner/proto" +) + +func TestBuildSubjectTemplatesIncludesVpcResourceFamilies(t *testing.T) { + templates := buildSubjectTemplates() + if len(templates) != 5 { + t.Fatalf("expected five subject templates, got %d", len(templates)) + } + + names := map[string]bool{} + for _, template := range templates { + names[template.Name] = true + } + + for _, expected := range []string{ + "aws-vpc", + "aws-vpc-subnet", + "aws-vpc-security-group", + "aws-vpc-network-acl", + "aws-vpc-route-table", + } { + if !names[expected] { + t.Fatalf("missing subject template %s", expected) + } + } +} + +func TestSubjectTemplatesHaveSelectorsAndSchemasForIdentity(t *testing.T) { + for _, subjectTemplate := range buildSubjectTemplates() { + if subjectTemplate.Type != proto.SubjectType_SUBJECT_TYPE_COMPONENT { + t.Fatalf("template %s must use component subject type", subjectTemplate.Name) + } + if len(subjectTemplate.SelectorLabels) == 0 { + t.Fatalf("template %s missing selector labels", subjectTemplate.Name) + } + if !containsSchemaKey(subjectTemplate.LabelSchema, "type") { + t.Fatalf("template %s must declare type selector in label schema", subjectTemplate.Name) + } + + for _, selector := range subjectTemplate.SelectorLabels { + if strings.Contains(selector.Key, "-") { + t.Fatalf("template %s selector key %s must use underscore notation", subjectTemplate.Name, selector.Key) + } + } + + for _, identityKey := range subjectTemplate.IdentityLabelKeys { + if strings.Contains(identityKey, "-") { + t.Fatalf("template %s identity key %s must use underscore notation", subjectTemplate.Name, identityKey) + } + if !containsSchemaKey(subjectTemplate.LabelSchema, identityKey) { + t.Fatalf("template %s identity key %s missing from label schema", subjectTemplate.Name, identityKey) + } + } + + for _, field := range subjectTemplate.LabelSchema { + if strings.Contains(field.Key, "-") { + t.Fatalf("template %s schema key %s must use underscore notation", subjectTemplate.Name, field.Key) + } + } + } +} + +func TestSubjectTemplateTitleAndDescriptionRenderWithUnderscoreLabels(t *testing.T) { + labels := map[string]string{ + "provider": "aws", + "type": "route-table", + "vpc_id": "vpc-123", + "cidr": "10.0.0.0/16", + "region": "eu-west-2", + "subnet_id": "subnet-123", + "az": "eu-west-2a", + "group_id": "sg-123", + "acl_id": "acl-123", + "route_table_id": "rtb-123", + } + + for _, subjectTemplate := range buildSubjectTemplates() { + for fieldName, templateText := range map[string]string{ + "title": subjectTemplate.TitleTemplate, + "description": subjectTemplate.DescriptionTemplate, + "purpose": subjectTemplate.PurposeTemplate, + } { + if rendered := renderSubjectTemplate(t, subjectTemplate.Name, fieldName, templateText, labels); rendered == "" { + t.Fatalf("template %s rendered empty %s", subjectTemplate.Name, fieldName) + } + } + } +} + +func containsSchemaKey(schema []*proto.SubjectLabelSchema, target string) bool { + for _, field := range schema { + if field.Key == target { + return true + } + } + return false +} + +func renderSubjectTemplate(t *testing.T, templateName, fieldName, templateText string, labels map[string]string) string { + t.Helper() + + parsed, err := template.New(templateName + "-" + fieldName).Option("missingkey=zero").Parse(templateText) + if err != nil { + t.Fatalf("template %s has invalid %s template: %v", templateName, fieldName, err) + } + + var buf bytes.Buffer + if err := parsed.Execute(&buf, labels); err != nil { + t.Fatalf("template %s failed to render %s template: %v", templateName, fieldName, err) + } + return buf.String() +} From 8eb3a815921864611e949b1b7774407289c08391 Mon Sep 17 00:00:00 2001 From: Reece Bedding Date: Mon, 1 Jun 2026 12:15:27 +0100 Subject: [PATCH 10/10] fix: copilot issues --- README.md | 10 +++---- internal/evidence.go | 7 ++--- internal/evidence_labels_test.go | 38 ++++++++++++++++++++++++++ internal/network_acl_context_test.go | 10 +++++-- internal/network_acl_test.go | 4 +-- internal/policy_evaluation.go | 2 +- internal/region_datasets.go | 36 ++++++++++++++++++++++++- internal/region_datasets_test.go | 40 ++++++++++++++++++++++++++++ internal/route_table_context_test.go | 30 +++++++++++++++++---- internal/subnet_test.go | 4 +-- internal/util.go | 7 +++-- internal/util_test.go | 12 +++++++++ main_test.go | 23 +++++----------- 13 files changed, 184 insertions(+), 39 deletions(-) create mode 100644 internal/region_datasets_test.go create mode 100644 internal/util_test.go diff --git a/README.md b/README.md index 3f093ee..afb0d78 100644 --- a/README.md +++ b/README.md @@ -77,8 +77,8 @@ This writes the compiled plugin to `dist/plugin`. ## Related repositories -- `../plugin-aws-vpc-policies` -- `../plugin-aws-vpc-subnet-policies` -- `../plugin-aws-vpc-sg-policies` -- `../plugin-aws-vpc-nacl-policies` -- `../plugin-aws-vpc-rt-policies` +- [plugin-aws-vpc-policies](https://github.com/compliance-framework/plugin-aws-vpc-policies) +- [plugin-aws-vpc-subnet-policies](https://github.com/compliance-framework/plugin-aws-vpc-subnet-policies) +- [plugin-aws-vpc-sg-policies](https://github.com/compliance-framework/plugin-aws-vpc-sg-policies) +- [plugin-aws-vpc-nacl-policies](https://github.com/compliance-framework/plugin-aws-vpc-nacl-policies) +- [plugin-aws-vpc-rt-policies](https://github.com/compliance-framework/plugin-aws-vpc-rt-policies) diff --git a/internal/evidence.go b/internal/evidence.go index 75a26be..fc27f0f 100644 --- a/internal/evidence.go +++ b/internal/evidence.go @@ -2,6 +2,7 @@ package internal import ( "fmt" + "strconv" "github.com/aws/aws-sdk-go-v2/aws" cloudwatchlogstypes "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs/types" @@ -124,7 +125,7 @@ func BuildVpcEvidenceContext(vpc types.Vpc, region string) VpcEvidenceContext { }, { Name: "is-default", - Value: fmt.Sprintf("%v", vpc.IsDefault), + Value: strconv.FormatBool(aws.ToBool(vpc.IsDefault)), }, }, ImplementedComponents: []*proto.InventoryItemImplementedComponent{ @@ -208,7 +209,7 @@ func BuildSubnetEvidenceContext(subnet types.Subnet, region string) SubnetEviden }, { Name: "map-public-ip-on-launch", - Value: fmt.Sprintf("%v", subnet.MapPublicIpOnLaunch), + Value: strconv.FormatBool(aws.ToBool(subnet.MapPublicIpOnLaunch)), }, }, ImplementedComponents: []*proto.InventoryItemImplementedComponent{ @@ -348,7 +349,7 @@ func BuildNetworkAclEvidenceContext(acl types.NetworkAcl, region string) Network }, { Name: "is-default", - Value: fmt.Sprintf("%v", acl.IsDefault), + Value: strconv.FormatBool(aws.ToBool(acl.IsDefault)), }, { Name: "owner-id", diff --git a/internal/evidence_labels_test.go b/internal/evidence_labels_test.go index 8d75f2a..7f409f1 100644 --- a/internal/evidence_labels_test.go +++ b/internal/evidence_labels_test.go @@ -7,6 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" cloudwatchlogstypes "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs/types" "github.com/aws/aws-sdk-go-v2/service/ec2/types" + "github.com/compliance-framework/agent/runner/proto" ) func TestEvidenceLabelKeysUseUnderscoreNotation(t *testing.T) { @@ -93,3 +94,40 @@ func TestEvidenceLabelKeysUseUnderscoreNotation(t *testing.T) { } } } + +func TestEvidenceBoolPointerPropertiesRenderAsBoolStrings(t *testing.T) { + vpcCtx := BuildVpcEvidenceContext(types.Vpc{ + VpcId: aws.String("vpc-123"), + IsDefault: aws.Bool(true), + }, "eu-west-2") + assertInventoryProperty(t, vpcCtx.Inventory[0].Props, "is-default", "true") + + subnetCtx := BuildSubnetEvidenceContext(types.Subnet{ + SubnetId: aws.String("subnet-123"), + VpcId: aws.String("vpc-123"), + MapPublicIpOnLaunch: aws.Bool(false), + }, "eu-west-2") + assertInventoryProperty(t, subnetCtx.Inventory[0].Props, "map-public-ip-on-launch", "false") + + aclCtx := BuildNetworkAclEvidenceContext(types.NetworkAcl{ + NetworkAclId: aws.String("acl-123"), + VpcId: aws.String("vpc-123"), + IsDefault: aws.Bool(true), + }, "eu-west-2") + assertInventoryProperty(t, aclCtx.Inventory[0].Props, "is-default", "true") +} + +func assertInventoryProperty(t *testing.T, props []*proto.Property, name string, expected string) { + t.Helper() + + for _, prop := range props { + if prop.Name == name { + if prop.Value != expected { + t.Fatalf("property %s = %q, want %q", name, prop.Value, expected) + } + return + } + } + + t.Fatalf("property %s not found", name) +} diff --git a/internal/network_acl_context_test.go b/internal/network_acl_context_test.go index 775de47..4dc51dd 100644 --- a/internal/network_acl_context_test.go +++ b/internal/network_acl_context_test.go @@ -71,7 +71,10 @@ func TestBuildNetworkAclPolicyInputIncludesNaclContext(t *testing.T) { t.Fatalf("input[nacl_context] should be a map") } - current := contextMap["current"].(map[string]interface{}) + current, ok := contextMap["current"].(map[string]interface{}) + if !ok { + t.Fatalf("nacl_context.current should be a map") + } if current["network_acl_id"] != "acl-123" { t.Fatalf("current.network_acl_id = %v, want acl-123", current["network_acl_id"]) } @@ -98,7 +101,10 @@ func TestBuildNetworkAclPolicyInputIncludesNaclContext(t *testing.T) { assertItemCount(t, contextMap, "flow_logs_for_associated_subnets", 1) assertItemCount(t, contextMap, "log_groups_for_related_flow_logs", 2) - vpc := contextMap["vpc"].(map[string]interface{}) + vpc, ok := contextMap["vpc"].(map[string]interface{}) + if !ok { + t.Fatalf("nacl_context.vpc should be a map") + } if vpc["VpcId"] != "vpc-123" { t.Fatalf("vpc.VpcId = %v, want vpc-123", vpc["VpcId"]) } diff --git a/internal/network_acl_test.go b/internal/network_acl_test.go index 1cd3fb7..c4fb9c0 100644 --- a/internal/network_acl_test.go +++ b/internal/network_acl_test.go @@ -38,10 +38,10 @@ func TestPrefixNetworkAclEvidenceTitles(t *testing.T) { PrefixEvidenceTitles(evidences, "acl-123") - if got := evidences[0].Title; got != "acl-123 | Network ACL should set required tags" { + if got := evidences[0].GetTitle(); got != "acl-123 | Network ACL should set required tags" { t.Fatalf("prefixed title = %q", got) } - if got := evidences[1].Title; got != "acl-123" { + if got := evidences[1].GetTitle(); got != "acl-123" { t.Fatalf("empty title fallback = %q", got) } } diff --git a/internal/policy_evaluation.go b/internal/policy_evaluation.go index 79ecd32..43e45cb 100644 --- a/internal/policy_evaluation.go +++ b/internal/policy_evaluation.go @@ -117,7 +117,7 @@ func PrefixEvidenceTitles(evidences []*proto.Evidence, prefix string) { continue } - title := strings.TrimSpace(evidence.Title) + title := strings.TrimSpace(evidence.GetTitle()) if title == "" { evidence.Title = prefix continue diff --git a/internal/region_datasets.go b/internal/region_datasets.go index 176a2d5..24618ea 100644 --- a/internal/region_datasets.go +++ b/internal/region_datasets.go @@ -2,6 +2,7 @@ package internal import ( "context" + "errors" "iter" cloudwatchlogs "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs" @@ -33,7 +34,14 @@ func CollectRegionDatasets(ctx context.Context, logger hclog.Logger, client *ec2 err error ) - if requiredDatasets["vpcs"] { + if requiresEC2Client(requiredDatasets) && client == nil { + return RegionDatasets{}, errors.New("ec2 client is required for requested region datasets") + } + if requiredDatasets["log_groups"] && logsClient == nil { + return RegionDatasets{}, errors.New("cloudwatch logs client is required for requested region datasets") + } + + if requiresVpcCollection(requiredDatasets) { datasets.Vpcs, err = collectSequence(PaginatedDescribeVpcs(ctx, client)) if err != nil { logger.Error("unable to get VPC", "error", err) @@ -150,3 +158,29 @@ func collectSequence[T any](seq iter.Seq2[T, error]) ([]T, error) { } return items, nil } + +func requiresVpcCollection(requiredDatasets map[string]bool) bool { + return requiredDatasets["vpcs"] || requiredDatasets["vpc_attributes"] +} + +func requiresEC2Client(requiredDatasets map[string]bool) bool { + for _, datasetName := range []string{ + "vpcs", + "vpc_attributes", + "dhcp_options", + "subnets", + "security_groups", + "network_interfaces", + "network_acls", + "route_tables", + "internet_gateways", + "vpc_endpoints", + "flow_logs", + "transit_gateway_attachments", + } { + if requiredDatasets[datasetName] { + return true + } + } + return false +} diff --git a/internal/region_datasets_test.go b/internal/region_datasets_test.go new file mode 100644 index 0000000..deac718 --- /dev/null +++ b/internal/region_datasets_test.go @@ -0,0 +1,40 @@ +package internal + +import ( + "context" + "strings" + "testing" + + "github.com/aws/aws-sdk-go-v2/service/ec2" + "github.com/hashicorp/go-hclog" +) + +func TestCollectRegionDatasetsRequiresEC2ClientForEC2Datasets(t *testing.T) { + _, err := CollectRegionDatasets(context.Background(), hclog.NewNullLogger(), nil, nil, map[string]bool{ + "vpcs": true, + }) + if err == nil || !strings.Contains(err.Error(), "ec2 client is required") { + t.Fatalf("CollectRegionDatasets error = %v, want EC2 client required error", err) + } +} + +func TestCollectRegionDatasetsRequiresLogsClientForLogGroups(t *testing.T) { + _, err := CollectRegionDatasets(context.Background(), hclog.NewNullLogger(), ec2.New(ec2.Options{}), nil, map[string]bool{ + "log_groups": true, + }) + if err == nil || !strings.Contains(err.Error(), "cloudwatch logs client is required") { + t.Fatalf("CollectRegionDatasets error = %v, want CloudWatch Logs client required error", err) + } +} + +func TestCollectRegionDatasetsAllowsNilClientsWhenNoDatasetsRequired(t *testing.T) { + if _, err := CollectRegionDatasets(context.Background(), hclog.NewNullLogger(), nil, nil, nil); err != nil { + t.Fatalf("CollectRegionDatasets returned error with no required datasets: %v", err) + } +} + +func TestVpcAttributesRequireVpcCollection(t *testing.T) { + if !requiresVpcCollection(map[string]bool{"vpc_attributes": true}) { + t.Fatal("vpc_attributes should require VPC collection") + } +} diff --git a/internal/route_table_context_test.go b/internal/route_table_context_test.go index 19ed9ae..e224db6 100644 --- a/internal/route_table_context_test.go +++ b/internal/route_table_context_test.go @@ -63,7 +63,7 @@ func TestBuildRouteTablePolicyInputIncludesRouteTableContext(t *testing.T) { t.Fatalf("input[route_table_context] should be a map") } - current := contextMap["current"].(map[string]interface{}) + current := requireMapValue(t, contextMap, "current") if current["route_table_id"] != "rtb-public" { t.Fatalf("current.route_table_id = %v, want rtb-public", current["route_table_id"]) } @@ -101,7 +101,7 @@ func TestBuildRouteTablePolicyInputIncludesRouteTableContext(t *testing.T) { assertItemCount(t, contextMap, "route_summaries", 3) assertItemCount(t, contextMap, "blackhole_routes", 1) - vpc := contextMap["vpc"].(map[string]interface{}) + vpc := requireMapValue(t, contextMap, "vpc") if vpc["VpcId"] != "vpc-123" { t.Fatalf("vpc.VpcId = %v, want vpc-123", vpc["VpcId"]) } @@ -129,8 +129,8 @@ func TestBuildRouteTablePolicyInputIncludesImplicitMainRouteAssociations(t *test t.Fatalf("BuildRouteTablePolicyInput returned error: %v", err) } - contextMap := input["route_table_context"].(map[string]interface{}) - current := contextMap["current"].(map[string]interface{}) + contextMap := requireMapValue(t, input, "route_table_context") + current := requireMapValue(t, contextMap, "current") if current["is_main"] != true { t.Fatalf("current.is_main = %v, want true", current["is_main"]) } @@ -141,10 +141,30 @@ func TestBuildRouteTablePolicyInputIncludesImplicitMainRouteAssociations(t *test t.Fatalf("current.effective_subnet_association_count = %v, want 1", current["effective_subnet_association_count"]) } - implicitSubnetIDs := current["implicitly_associated_subnet_ids"].([]interface{}) + implicitSubnetIDs := requireListValue(t, current, "implicitly_associated_subnet_ids") if len(implicitSubnetIDs) != 1 || implicitSubnetIDs[0] != "subnet-main-a" { t.Fatalf("implicitly_associated_subnet_ids = %v, want [subnet-main-a]", implicitSubnetIDs) } assertOneItem(t, contextMap, "implicitly_associated_subnets") assertOneItem(t, contextMap, "effectively_associated_subnets") } + +func requireMapValue(t *testing.T, values map[string]interface{}, key string) map[string]interface{} { + t.Helper() + + value, ok := values[key].(map[string]interface{}) + if !ok { + t.Fatalf("%s should be a map", key) + } + return value +} + +func requireListValue(t *testing.T, values map[string]interface{}, key string) []interface{} { + t.Helper() + + value, ok := values[key].([]interface{}) + if !ok { + t.Fatalf("%s should be a list", key) + } + return value +} diff --git a/internal/subnet_test.go b/internal/subnet_test.go index 8e26418..d928b84 100644 --- a/internal/subnet_test.go +++ b/internal/subnet_test.go @@ -38,10 +38,10 @@ func TestPrefixSubnetEvidenceTitles(t *testing.T) { PrefixEvidenceTitles(evidences, "subnet-123") - if got := evidences[0].Title; got != "subnet-123 | Subnet should set required tags" { + if got := evidences[0].GetTitle(); got != "subnet-123 | Subnet should set required tags" { t.Fatalf("prefixed title = %q", got) } - if got := evidences[1].Title; got != "subnet-123" { + if got := evidences[1].GetTitle(); got != "subnet-123" { t.Fatalf("empty title fallback = %q", got) } } diff --git a/internal/util.go b/internal/util.go index 5d89872..5022497 100644 --- a/internal/util.go +++ b/internal/util.go @@ -40,8 +40,11 @@ func ResolveRegions(config map[string]string) []string { } // Check for single region - if regionStr, ok := config["region"]; ok && regionStr != "" { - return []string{strings.TrimSpace(regionStr)} + if regionStr, ok := config["region"]; ok { + region := strings.TrimSpace(regionStr) + if region != "" { + return []string{region} + } } // Fall back to environment variable diff --git a/internal/util_test.go b/internal/util_test.go new file mode 100644 index 0000000..094c06f --- /dev/null +++ b/internal/util_test.go @@ -0,0 +1,12 @@ +package internal + +import "testing" + +func TestResolveRegionsIgnoresWhitespaceOnlyRegionConfig(t *testing.T) { + t.Setenv("AWS_REGION", "eu-west-2") + + regions := ResolveRegions(map[string]string{"region": " \t "}) + if len(regions) != 1 || regions[0] != "eu-west-2" { + t.Fatalf("ResolveRegions() = %v, want [eu-west-2]", regions) + } +} diff --git a/main_test.go b/main_test.go index a66494f..d3e088f 100644 --- a/main_test.go +++ b/main_test.go @@ -1,6 +1,9 @@ package main -import "testing" +import ( + "reflect" + "testing" +) func TestBuildRequiredDatasetsForAclPolicies(t *testing.T) { required := buildRequiredDatasets(map[string][]string{ @@ -39,20 +42,8 @@ func TestBuildRequiredDatasetsForRouteTablePolicies(t *testing.T) { } func TestSupportedPolicyBehaviorsOnlyIncludesPrimaryVpcBundles(t *testing.T) { - behaviors := map[string]bool{} - for _, behavior := range supportedPolicyBehaviors() { - behaviors[behavior] = true - } - - for _, expected := range []string{"vpc", "subnet", "sg", "acl", "rt"} { - if !behaviors[expected] { - t.Fatalf("expected behavior %s to be supported", expected) - } - } - - for _, contextOnly := range []string{"igw", "endpoint", "flow-log", "log-group"} { - if behaviors[contextOnly] { - t.Fatalf("context-only behavior %s must not be supported directly", contextOnly) - } + expected := []string{"vpc", "subnet", "sg", "acl", "rt"} + if actual := supportedPolicyBehaviors(); !reflect.DeepEqual(actual, expected) { + t.Fatalf("supportedPolicyBehaviors() = %v, want exactly %v", actual, expected) } }