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/README.md b/README.md index a7fa098..afb0d78 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](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/go.mod b/go.mod index f25f670..19bdba9 100644 --- a/go.mod +++ b/go.mod @@ -1,71 +1,79 @@ 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/cloudwatchlogs v1.47.0 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.7.0 + 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/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 + 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..691c712 100644 --- a/go.sum +++ b/go.sum @@ -1,89 +1,101 @@ 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/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= +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/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.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.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= +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 +105,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 +253,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 +271,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 +364,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 +428,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/internal/evidence.go b/internal/evidence.go new file mode 100644 index 0000000..fc27f0f --- /dev/null +++ b/internal/evidence.go @@ -0,0 +1,752 @@ +package internal + +import ( + "fmt" + "strconv" + + "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: strconv.FormatBool(aws.ToBool(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: strconv.FormatBool(aws.ToBool(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, + "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: strconv.FormatBool(aws.ToBool(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/evidence_labels_test.go b/internal/evidence_labels_test.go new file mode 100644 index 0000000..7f409f1 --- /dev/null +++ b/internal/evidence_labels_test.go @@ -0,0 +1,133 @@ +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" + "github.com/compliance-framework/agent/runner/proto" +) + +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) + } + } + } +} + +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.go b/internal/network_acl.go new file mode 100644 index 0000000..99ac767 --- /dev/null +++ b/internal/network_acl.go @@ -0,0 +1,37 @@ +package internal + +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, datasets RegionDatasets) 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) + }, + 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) { + PrefixEvidenceTitles(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..4dc51dd --- /dev/null +++ b/internal/network_acl_context_test.go @@ -0,0 +1,122 @@ +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, 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"]) + } + 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, 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"]) + } +} + +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..c4fb9c0 --- /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, + } + + PrefixEvidenceTitles(evidences, "acl-123") + + if got := evidences[0].GetTitle(); got != "acl-123 | Network ACL should set required tags" { + t.Fatalf("prefixed title = %q", got) + } + if got := evidences[1].GetTitle(); got != "acl-123" { + t.Fatalf("empty title fallback = %q", got) + } +} diff --git a/internal/pagination.go b/internal/pagination.go new file mode 100644 index 0000000..721f840 --- /dev/null +++ b/internal/pagination.go @@ -0,0 +1,239 @@ +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 + } + } + } + } +} + +// 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) { + 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 + } + } + } + } +} + +// 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 new file mode 100644 index 0000000..43e45cb --- /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 PrefixEvidenceTitles(evidences []*proto.Evidence, prefix string) { + prefix = strings.TrimSpace(prefix) + if prefix == "" { + return + } + + for _, evidence := range evidences { + if evidence == nil { + continue + } + + title := strings.TrimSpace(evidence.GetTitle()) + if title == "" { + evidence.Title = prefix + continue + } + + evidence.Title = prefix + " | " + title + } +} diff --git a/internal/region_datasets.go b/internal/region_datasets.go new file mode 100644 index 0000000..24618ea --- /dev/null +++ b/internal/region_datasets.go @@ -0,0 +1,186 @@ +package internal + +import ( + "context" + "errors" + "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 + VpcAttributes map[string]VpcAttributeValues + DhcpOptions []types.DhcpOptions + 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 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) + return RegionDatasets{}, err + } + } + + 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 { + 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 +} + +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.go b/internal/route_table.go new file mode 100644 index 0000000..b0a81b3 --- /dev/null +++ b/internal/route_table.go @@ -0,0 +1,37 @@ +package internal + +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, datasets RegionDatasets) 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) + }, + 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..e224db6 --- /dev/null +++ b/internal/route_table_context_test.go @@ -0,0 +1,170 @@ +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 := 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"]) + } + 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 := requireMapValue(t, contextMap, "vpc") + 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 := 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"]) + } + 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 := 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/security_group.go b/internal/security_group.go new file mode 100644 index 0000000..a9c91c7 --- /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) { + PrefixEvidenceTitles(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..a99422f --- /dev/null +++ b/internal/subnet.go @@ -0,0 +1,37 @@ +package internal + +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, datasets RegionDatasets) 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) + }, + 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..d928b84 --- /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].GetTitle(); got != "subnet-123 | Subnet should set required tags" { + t.Fatalf("prefixed title = %q", got) + } + if got := evidences[1].GetTitle(); got != "subnet-123" { + t.Fatalf("empty title fallback = %q", got) + } +} 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..5022497 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,40 @@ 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 { + region := strings.TrimSpace(regionStr) + if region != "" { + return []string{region} + } + } + + // 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/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/internal/vpc.go b/internal/vpc.go new file mode 100644 index 0000000..f9ea78e --- /dev/null +++ b/internal/vpc.go @@ -0,0 +1,37 @@ +package internal + +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, datasets RegionDatasets) 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) + }, + 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) { + PrefixEvidenceTitles(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 f3825b0..d5e8932 100644 --- a/main.go +++ b/main.go @@ -3,162 +3,147 @@ package main import ( "context" "errors" - "fmt" - "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" "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 { - 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 } +func (l *CompliancePlugin) Init(req *proto.InitRequest, apiHelper runner.ApiHelper) (*proto.InitResponse, error) { + 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) { ctx := context.TODO() 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"), + }, + }, + }, + } + + defaultBehaviorMapping := map[string][]string{ + "aws-vpc-sg-policies": {"sg"}, + "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) + requiredDatasets := buildRequiredDatasets(policyPathsByBehavior) + deps := internal.EvaluationDependencies{ + Context: ctx, + Logger: l.logger, + ApiHelper: apiHelper, + Actors: actors, + PolicyData: l.policyData, } - 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) - 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"), - }, - }, - }, + datasets, err := internal.CollectRegionDatasets(ctx, l.logger, client, logsClient, requiredDatasets) + if err != nil { + evalStatus = proto.ExecutionStatus_FAILURE + accumulatedErrors = errors.Join(accumulatedErrors, err) + continue } - 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.", - }, + + if vpcPolicyPaths := policyPathsByBehavior["vpc"]; len(vpcPolicyPaths) > 0 { + 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 + } } - 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", - }, - }, - }, + + if subnetPolicyPaths := policyPathsByBehavior["subnet"]; len(subnetPolicyPaths) > 0 { + 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 + } } - 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)), - }, + + 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 + } } - 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) - if err != nil { - accumulatedErrors = errors.Join(accumulatedErrors, err) + if aclPolicyPaths := policyPathsByBehavior["acl"]; len(aclPolicyPaths) > 0 { + 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 } } - 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 routeTablePolicyPaths := policyPathsByBehavior["rt"]; len(routeTablePolicyPaths) > 0 { + 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 + } } + } return &proto.EvalResponse{ @@ -166,20 +151,66 @@ func (l *CompliancePlugin) Eval(request *proto.EvalRequest, apiHelper runner.Api }, accumulatedErrors } -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 +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 +} + +func supportedPolicyBehaviors() []string { + return []string{ + "vpc", + "subnet", + "sg", + "acl", + "rt", + } +} + +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 +} + +func buildRequiredDatasets(policyPathsByBehavior map[string][]string) map[string]bool { + requiredDatasets := make(map[string]bool) + for behavior, policyPaths := range policyPathsByBehavior { + if len(policyPaths) == 0 { + continue } - for _, group := range result.SecurityGroups { - if !yield(group, nil) { - return - } + switch behavior { + 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, "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": + 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") + default: + continue } } + return requiredDatasets +} + +func markRequiredDatasets(requiredDatasets map[string]bool, datasetNames ...string) { + for _, datasetName := range datasetNames { + requiredDatasets[datasetName] = true + } } func main() { @@ -197,7 +228,7 @@ func main() { goplugin.Serve(&goplugin.ServeConfig{ HandshakeConfig: runner.HandshakeConfig, Plugins: map[string]goplugin.Plugin{ - "runner": &runner.RunnerGRPCPlugin{ + "runner": &runner.RunnerV2GRPCPlugin{ Impl: compliancePluginObj, }, }, diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..d3e088f --- /dev/null +++ b/main_test.go @@ -0,0 +1,49 @@ +package main + +import ( + "reflect" + "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) + } + } +} + +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) + } + } +} + +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) + } + } +} + +func TestSupportedPolicyBehaviorsOnlyIncludesPrimaryVpcBundles(t *testing.T) { + expected := []string{"vpc", "subnet", "sg", "acl", "rt"} + if actual := supportedPolicyBehaviors(); !reflect.DeepEqual(actual, expected) { + t.Fatalf("supportedPolicyBehaviors() = %v, want exactly %v", actual, expected) + } +} 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() +}