diff --git a/.gitignore b/.gitignore index c8388a8..26a7fe9 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ +plugin_cloud_custodian policies diff --git a/README.md b/README.md index 38ae297..69977b7 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,26 @@ # Compliance Framework - Cloud Custodian Plugin The **Cloud Custodian Plugin** runs Cloud Custodian policies in dry-run mode, -converts each policy execution into a standardized per-check payload, and then -executes CCF OPA bundles against that payload to generate evidence. +builds a full inventory baseline for each configured resource type, converts +each resource/policy pair into a standardized per-resource payload, and then +executes CCF OPA bundles against those payloads to generate evidence. ## Behavior Overview 1. Load Cloud Custodian policy YAML from config. 2. Parse top-level `policies` and iterate one policy entry per check. -3. Run each check with: +3. Register runtime-derived subject templates for each configured resource type during runner-v2 `Init`. +4. Run one unfiltered inventory collection for each unique resource type. +5. Run each configured check with: ```bash custodian run --dryrun -s ``` -4. Build a standardized payload from execution output and `resources.json`. -5. Evaluate each OPA policy bundle path from agent `EvalRequest.policyPaths`. -6. Send evidence via the plugin gRPC helper (`CreateEvidence`). +6. Compare each check's matched resources with the inventory baseline. +7. Build one standardized payload per resource per check. Matched resources are marked `non_compliant`; baseline resources not matched by that check are marked `compliant`. +8. Evaluate each OPA policy bundle path from agent `EvalRequest.policyPaths`. +9. Send evidence via the plugin gRPC helper (`CreateEvidence`). ## Safety Model @@ -37,7 +41,8 @@ All plugin config fields are strings (agent gRPC `map` contract). | `custodian_binary` | No | Path/name of Cloud Custodian executable. Default: `custodian`. | | `check_timeout_seconds` | No | Per-check timeout in seconds. Default: `300`. | | `policy_labels` | No | JSON map of labels merged into generated evidence labels. | -| `debug_dump_payloads` | No | Boolean (`true`/`false`) toggle to write standardized check payload JSON files for troubleshooting. Default: `false`. | +| `resource_identity_fields` | No | JSON object mapping Cloud Custodian resource types to ordered identity field paths. Built-in defaults are used after configured fields. Example: `{"aws.ec2":["InstanceId","Arn"]}`. | +| `debug_dump_payloads` | No | Boolean (`true`/`false`) toggle to write standardized resource payload JSON files for troubleshooting. Default: `false`. | | `debug_payload_output_dir` | No | Directory where debug payload JSON files are written. If set, debug dumping is auto-enabled. Default when enabled without explicit path: `debug-standardized-payloads`. | Validation rules: @@ -45,6 +50,7 @@ Validation rules: - At least one of `policies_yaml` or `policies_path` must be provided. - `custodian_binary` must resolve on PATH (or as explicit executable path). - `check_timeout_seconds` must be a positive integer. +- `resource_identity_fields`, when set, must be valid JSON and each resource type must include at least one field path. - Policy YAML must include top-level `policies` array. ## Example Agent Config (Inline YAML) @@ -82,13 +88,13 @@ plugins: custodian_binary: /usr/local/bin/custodian ``` -## Standardized Per-Check OPA Input +## Standardized Per-Resource OPA Input -Each policy/check iteration produces one payload with this shape: +Each resource/check iteration produces one payload with this shape: ```json { - "schema_version": "v1", + "schema_version": "v2", "source": "cloud-custodian", "check": { "name": "ec2-public-ip-check", @@ -97,6 +103,25 @@ Each policy/check iteration produces one payload with this shape: "index": 0, "metadata": {} }, + "resource": { + "id": "i-1234567890abcdef0", + "type": "aws.ec2", + "provider": "aws", + "account_id": "123456789012", + "region": "us-east-1", + "identity_fields": { + "InstanceId": "i-1234567890abcdef0" + }, + "data": {"...": "..."} + }, + "assessment": { + "status": "non_compliant", + "matched": true, + "inventory_status": "baseline", + "matched_resource_count": 3, + "artifact_path": "/tmp/ccf-cloud-custodian-123/001-ec2-public-ip-check", + "resources_path": "/tmp/ccf-cloud-custodian-123/001-ec2-public-ip-check/ec2-public-ip-check/resources.json" + }, "execution": { "status": "success", "dry_run": true, @@ -109,12 +134,6 @@ Each policy/check iteration produces one payload with this shape: "error": "", "errors": [] }, - "result": { - "matched_resource_count": 3, - "resources": [{"...": "..."}], - "artifact_path": "/tmp/ccf-cloud-custodian-123/001-ec2-public-ip-check", - "resources_path": "/tmp/ccf-cloud-custodian-123/001-ec2-public-ip-check/ec2-public-ip-check/resources.json" - }, "raw_policy": { "name": "ec2-public-ip-check", "resource": "aws.ec2", @@ -123,6 +142,17 @@ Each policy/check iteration produces one payload with this shape: } ``` +Generated evidence labels include `resource_id`, `resource_type`, `provider`, +and any available `account_id`/`region`. The resource subject also includes the +resource identifier as a link and a `resource_id` property. AWS Route53 hosted +zone identifiers such as `/hostedzone/Z123` are normalized to full ARNs such as +`arn:aws:route53:::hostedzone/Z123`. + +`assessment.inventory_status` is `baseline` for resources found in the unfiltered +inventory run. If a policy returns a resource that is not present in the baseline, +the plugin still evaluates it as `non_compliant` and sets +`inventory_status` to `missing_from_baseline`. + `provider` extraction rule: - `aws.s3` -> `aws` diff --git a/go.mod b/go.mod index 4dd6bee..147bdf9 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,9 @@ module github.com/compliance-framework/plugin-cloud-custodian -go 1.25.7 +go 1.26.1 require ( - github.com/compliance-framework/agent v0.2.1 + github.com/compliance-framework/agent v0.4.0 github.com/hashicorp/go-hclog v1.6.3 github.com/hashicorp/go-plugin v1.7.0 github.com/mitchellh/mapstructure v1.5.0 @@ -12,46 +12,51 @@ require ( require ( github.com/agnivade/levenshtein v1.2.1 // indirect - github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/compliance-framework/api v0.4.4 // indirect - github.com/defenseunicorns/go-oscal v0.6.3 // indirect + github.com/compliance-framework/api v0.15.0-rc1 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 // indirect + github.com/defenseunicorns/go-oscal v0.7.0 // indirect github.com/fatih/color v1.18.0 // indirect - github.com/go-ini/ini v1.67.0 // indirect - github.com/go-logr/logr v1.4.3 // indirect - github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-viper/mapstructure/v2 v2.4.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.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.2.0 // indirect - github.com/open-policy-agent/opa v1.4.0 // indirect - github.com/prometheus/client_golang v1.21.1 // indirect - github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.62.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.2 // 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.2.1 // indirect go.opentelemetry.io/otel v1.43.0 // indirect - go.opentelemetry.io/otel/metric v1.43.0 // indirect - go.opentelemetry.io/otel/sdk v1.43.0 // indirect - go.opentelemetry.io/otel/trace v1.43.0 // indirect - golang.org/x/net v0.48.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.43.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.32.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // 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.10 // indirect - sigs.k8s.io/yaml v1.4.0 // 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 120429d..3831abe 100644 --- a/go.sum +++ b/go.sum @@ -1,49 +1,83 @@ 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/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.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.41.2 h1:LuT2rzqNQsauaGkPK/7813XxcZ3o3yePY0Iy891T2ls= +github.com/aws/aws-sdk-go-v2 v1.41.2/go.mod h1:IvvlAZQXvTXznUPfRVfryiG1fbzE2NGK6m9u39YQ+S4= +github.com/aws/aws-sdk-go-v2/config v1.32.10 h1:9DMthfO6XWZYLfzZglAgW5Fyou2nRI5CuV44sTedKBI= +github.com/aws/aws-sdk-go-v2/config v1.32.10/go.mod h1:2rUIOnA2JaiqYmSKYmRJlcMWy6qTj1vuRFscppSBMcw= +github.com/aws/aws-sdk-go-v2/credentials v1.19.10 h1:EEhmEUFCE1Yhl7vDhNOI5OCL/iKMdkkYFTRpZXNw7m8= +github.com/aws/aws-sdk-go-v2/credentials v1.19.10/go.mod h1:RnnlFCAlxQCkN2Q379B67USkBMu1PipEEiibzYN5UTE= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 h1:Ii4s+Sq3yDfaMLpjrJsqD6SmG/Wq/P5L/hw2qa78UAY= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18/go.mod h1:6x81qnY++ovptLE6nWQeWrpXxbnlIex+4H4eYYGcqfc= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 h1:F43zk1vemYIqPAwhjTjYIz0irU2EY7sOb/F5eJ3HuyM= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18/go.mod h1:w1jdlZXrGKaJcNoL+Nnrj+k5wlpGXqnNrKoP22HvAug= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 h1:xCeWVjj0ki0l3nruoyP2slHsGArMxeiiaoPN5QZH6YQ= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18/go.mod h1:r/eLGuGCBw6l36ZRWiw6PaZwPXb6YOj+i/7MizNl5/k= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16 h1:CjMzUs78RDDv4ROu3JnJn/Ig1r6ZD7/T2DXLLRpejic= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16/go.mod h1:uVW4OLBqbJXSHJYA9svT9BluSvvwbzLQ2Crf6UPzR3c= +github.com/aws/aws-sdk-go-v2/service/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.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= -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/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.4 h1:qY6Az+CBfx9cku/tzmrPX2d0qRaAfAXnQVopDIYwlQs= -github.com/compliance-framework/api v0.4.4/go.mod h1:UjL+VppIb0jmFbViiQSKkhUfY8X9I29faML7gl0fD1M= +github.com/compliance-framework/agent v0.4.0 h1:UFAgG+w5quvu3XpU8dVPryRwkfAV0lAm0i2H/ozKY1o= +github.com/compliance-framework/agent v0.4.0/go.mod h1:0SDZLOSEXX8NTZDCeuiKYnN6ZqOdWFKaBPRss2qXl+Y= +github.com/compliance-framework/api v0.15.0-rc1 h1:I9SSnMjDWr+izQXmsZSQ1FHuaLmM8hfiO+bqDIUBLd8= +github.com/compliance-framework/api v0.15.0-rc1/go.mod h1:o4up7nNysmeqYT85ZwOOfnHaoavdBRNcnf2i9IfSYZw= 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.3 h1:3j5aBobVX+Fy2GEIRCeg9MhsAgCKceOagVEDQPMuzZc= -github.com/defenseunicorns/go-oscal v0.6.3/go.mod h1:m55Ny/RTh4xWuxVSOD/poCZs9V9GOjNtjT0NujoxI6I= -github.com/dgraph-io/badger/v4 v4.7.0 h1:Q+J8HApYAY7UMpL8d9owqiB+odzEc0zn/aqOD9jhc6Y= -github.com/dgraph-io/badger/v4 v4.7.0/go.mod h1:He7TzG3YBy3j4f5baj5B7Zl2XyfNe5bl4Udl0aPemVA= +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= @@ -67,60 +101,68 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2 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-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/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.20.2 h1:mQc3nmndL8ZBzStEo3JYF8wzmeWffDH4VbXz58sAx6Q= -github.com/go-openapi/jsonpointer v0.20.2/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs= -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.22.8 h1:/9RjDSQ0vbFR+NyjGMkFTsA1IA0fmhKSThmfGZjicbw= -github.com/go-openapi/swag v0.22.8/go.mod h1:6QT22icPLEqAM/z/TChgb4WAveCHF92+2gF0CNjHpPI= +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.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= -github.com/go-viper/mapstructure/v2 v2.4.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/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/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-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/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.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 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.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M= 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= @@ -129,38 +171,50 @@ 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/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.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo= +github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw= +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.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= -github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +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.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= @@ -191,15 +245,15 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 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.4.0 h1:IGO3xt5HhQKQq2axfa9memIFx5lCyaBlG+fXcgHpd3A= -github.com/open-policy-agent/opa v1.4.0/go.mod h1:DNzZPKqKh4U0n0ANxcCVlw8lCSv2c+h5G/3QvSYdWZ8= +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 v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= -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/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= @@ -207,36 +261,52 @@ 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.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk= -github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= -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.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= -github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= -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/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.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= -github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= +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/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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= @@ -244,24 +314,36 @@ 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.2 h1:xTHFutuitO2zqKAQ5rCROYgUb7Or/+IC3fts9/Yc7nM= -github.com/tchap/go-patricia/v2 v2.3.2/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= @@ -272,16 +354,10 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= 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.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= +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.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 h1:xJ2qHD0C1BeYVTLLR9sX12+Qb95kfeD/byKj6Ky1pXg= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0/go.mod h1:u5BF1xyjstDowA1R5QAO9JHzqK+ublenEW/dyqTjBVk= go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= @@ -290,47 +366,48 @@ go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfC go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= -go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= -go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= 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.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.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= -golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= -golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= -golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= -golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= -golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= -golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= -golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= -golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= -golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= -golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +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/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls= -google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +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.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= -google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +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= @@ -339,15 +416,15 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gorm.io/datatypes v1.2.5 h1:9UogU3jkydFVW1bIVVeoYsTpLRgwDVW3rHfJG6/Ek9I= -gorm.io/datatypes v1.2.5/go.mod h1:I5FUdlKpLb5PMqeMQhm30CQ6jXP8Rj89xkTeCSAaAD4= -gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8= -gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= -gorm.io/driver/postgres v1.5.7 h1:8ptbNJTDbEmhdr62uReG5BGkdQyeasu/FZHxI0IMGnM= -gorm.io/driver/postgres v1.5.7/go.mod h1:3e019WlBaYI5o5LIdNV+LyxCMNtLOQETBXL2h4chKpA= -gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= -gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= -gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= -gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= -sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= -sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= +gorm.io/datatypes v1.2.7 h1:ww9GAhF1aGXZY3EB3cJPJ7//JiuQo7DlQA7NNlVaTdk= +gorm.io/datatypes v1.2.7/go.mod h1:M2iO+6S3hhi4nAyYe444Pcb0dcIiOMJ7QHaUXxyiNZY= +gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg= +gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo= +gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= +gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= +gorm.io/gorm v1.30.5 h1:dvEfYwxL+i+xgCNSGGBT1lDjCzfELK8fHZxL3Ee9X0s= +gorm.io/gorm v1.30.5/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/main.go b/main.go index 1d9f096..189a51e 100644 --- a/main.go +++ b/main.go @@ -3,17 +3,19 @@ package main import ( "bytes" "context" + "crypto/sha256" + "encoding/hex" "encoding/json" "errors" "fmt" "io" "io/fs" - "maps" "net/http" "net/url" "os" "os/exec" "path/filepath" + "reflect" "slices" "strconv" "strings" @@ -30,34 +32,37 @@ import ( const ( defaultCheckTimeoutSeconds = 300 - schemaVersionV1 = "v1" + schemaVersionV2 = "v2" sourceCloudCustodian = "cloud-custodian" defaultRemotePolicyTimeout = 30 * time.Second defaultMaxRemotePolicyBytes = 1 << 20 // 1 MiB + evidenceBatchSize = 100 ) var lookPath = exec.LookPath // PluginConfig receives string-only config from the agent gRPC interface. type PluginConfig struct { - PoliciesYAML string `mapstructure:"policies_yaml"` - PoliciesPath string `mapstructure:"policies_path"` - CustodianBinary string `mapstructure:"custodian_binary"` - PolicyLabels string `mapstructure:"policy_labels"` - CheckTimeoutSeconds string `mapstructure:"check_timeout_seconds"` - DebugDumpPayloads string `mapstructure:"debug_dump_payloads"` - DebugPayloadOutputDir string `mapstructure:"debug_payload_output_dir"` + PoliciesYAML string `mapstructure:"policies_yaml"` + PoliciesPath string `mapstructure:"policies_path"` + CustodianBinary string `mapstructure:"custodian_binary"` + PolicyLabels string `mapstructure:"policy_labels"` + ResourceIdentityFields string `mapstructure:"resource_identity_fields"` + CheckTimeoutSeconds string `mapstructure:"check_timeout_seconds"` + DebugDumpPayloads string `mapstructure:"debug_dump_payloads"` + DebugPayloadOutputDir string `mapstructure:"debug_payload_output_dir"` } // ParsedConfig stores normalized and validated values for runtime use. type ParsedConfig struct { - PoliciesYAML string - PoliciesPath string - CustodianBinary string - PolicyLabels map[string]string - CheckTimeout time.Duration - DebugDumpPayloads bool - DebugPayloadOutputDir string + PoliciesYAML string + PoliciesPath string + CustodianBinary string + PolicyLabels map[string]string + ResourceIdentityFields map[string][]string + CheckTimeout time.Duration + DebugDumpPayloads bool + DebugPayloadOutputDir string } func (c *PluginConfig) Parse() (*ParsedConfig, error) { @@ -75,6 +80,35 @@ func (c *PluginConfig) Parse() (*ParsedConfig, error) { } } + resourceIdentityFields := map[string][]string{} + if strings.TrimSpace(c.ResourceIdentityFields) != "" { + if err := json.Unmarshal([]byte(c.ResourceIdentityFields), &resourceIdentityFields); err != nil { + return nil, fmt.Errorf("could not parse resource_identity_fields: %w", err) + } + normalizedIdentityFields := make(map[string][]string, len(resourceIdentityFields)) + for resourceType, fields := range resourceIdentityFields { + trimmedType := strings.TrimSpace(resourceType) + if trimmedType == "" { + return nil, errors.New("resource_identity_fields cannot contain an empty resource type") + } + normalizedFields := make([]string, 0, len(fields)) + for _, field := range fields { + field = strings.TrimSpace(field) + if field != "" { + normalizedFields = append(normalizedFields, field) + } + } + if len(normalizedFields) == 0 { + return nil, fmt.Errorf("resource_identity_fields for %q must include at least one field", trimmedType) + } + if _, exists := normalizedIdentityFields[trimmedType]; exists { + return nil, fmt.Errorf("resource_identity_fields contains duplicate resource type %q after trimming whitespace", trimmedType) + } + normalizedIdentityFields[trimmedType] = normalizedFields + } + resourceIdentityFields = normalizedIdentityFields + } + checkTimeoutSeconds := defaultCheckTimeoutSeconds if strings.TrimSpace(c.CheckTimeoutSeconds) != "" { parsedTimeout, err := strconv.Atoi(c.CheckTimeoutSeconds) @@ -115,13 +149,14 @@ func (c *PluginConfig) Parse() (*ParsedConfig, error) { } return &ParsedConfig{ - PoliciesYAML: inlineYAML, - PoliciesPath: policiesPath, - CustodianBinary: resolvedBinary, - PolicyLabels: policyLabels, - CheckTimeout: time.Duration(checkTimeoutSeconds) * time.Second, - DebugDumpPayloads: debugDumpPayloads, - DebugPayloadOutputDir: debugPayloadOutputDir, + PoliciesYAML: inlineYAML, + PoliciesPath: policiesPath, + CustodianBinary: resolvedBinary, + PolicyLabels: policyLabels, + ResourceIdentityFields: resourceIdentityFields, + CheckTimeout: time.Duration(checkTimeoutSeconds) * time.Second, + DebugDumpPayloads: debugDumpPayloads, + DebugPayloadOutputDir: debugPayloadOutputDir, }, nil } @@ -348,16 +383,6 @@ func findResourcesJSON(outputDir string) (string, error) { return found, nil } -// StandardizedCheckPayload is the per-check OPA input contract. -type StandardizedCheckPayload struct { - SchemaVersion string `json:"schema_version"` - Source string `json:"source"` - Check StandardizedCheckInfo `json:"check"` - Execution StandardizedExecution `json:"execution"` - Result StandardizedCheckResult `json:"result"` - RawPolicy map[string]interface{} `json:"raw_policy"` -} - type StandardizedCheckInfo struct { Name string `json:"name"` Resource string `json:"resource"` @@ -379,24 +404,87 @@ type StandardizedExecution struct { Errors []string `json:"errors,omitempty"` } -type StandardizedCheckResult struct { - MatchedResourceCount int `json:"matched_resource_count"` - Resources []interface{} `json:"resources"` - ArtifactPath string `json:"artifact_path,omitempty"` - ResourcesPath string `json:"resources_path,omitempty"` +// StandardizedResourcePayload is the per-resource OPA input contract. +type StandardizedResourcePayload struct { + SchemaVersion string `json:"schema_version"` + Source string `json:"source"` + Check StandardizedCheckInfo `json:"check"` + Resource StandardizedResourceInfo `json:"resource"` + Assessment StandardizedAssessment `json:"assessment"` + Execution StandardizedExecution `json:"execution"` + RawPolicy map[string]interface{} `json:"raw_policy"` } -func buildCheckPayload(check CustodianCheck, execution CustodianExecutionResult) *StandardizedCheckPayload { - status := "success" - if execution.Error != "" { - status = "error" - } +type StandardizedResourceInfo struct { + ID string `json:"id"` + Type string `json:"type"` + Provider string `json:"provider"` + AccountID string `json:"account_id,omitempty"` + Region string `json:"region,omitempty"` + IdentityFields map[string]string `json:"identity_fields,omitempty"` + Data interface{} `json:"data"` +} - durationMS := int64(execution.EndedAt.Sub(execution.StartedAt) / time.Millisecond) - if durationMS < 0 { - durationMS = 0 +type StandardizedAssessment struct { + Status string `json:"status"` + Matched bool `json:"matched"` + InventoryStatus string `json:"inventory_status"` + MatchedResourceCount int `json:"matched_resource_count"` + ArtifactPath string `json:"artifact_path,omitempty"` + ResourcesPath string `json:"resources_path,omitempty"` +} + +type ResourceRecord struct { + ID string + Type string + Provider string + AccountID string + Region string + IdentityFields map[string]string + Data interface{} +} + +type InventoryBaseline struct { + Execution CustodianExecutionResult + Records []ResourceRecord + ResourceType string + Provider string + Err error +} + +func buildResourcePayload( + check CustodianCheck, + execution CustodianExecutionResult, + record ResourceRecord, + assessment StandardizedAssessment, +) *StandardizedResourcePayload { + metadata := buildCheckMetadata(check) + return &StandardizedResourcePayload{ + SchemaVersion: schemaVersionV2, + Source: sourceCloudCustodian, + Check: StandardizedCheckInfo{ + Name: check.Name, + Resource: check.Resource, + Provider: check.Provider, + Index: check.Index, + Metadata: metadata, + }, + Resource: StandardizedResourceInfo{ + ID: record.ID, + Type: record.Type, + Provider: record.Provider, + AccountID: record.AccountID, + Region: record.Region, + IdentityFields: record.IdentityFields, + Data: record.Data, + }, + Assessment: assessment, + Execution: buildExecutionInfo(execution), + RawPolicy: check.RawPolicy, } +} +func buildCheckMetadata(check CustodianCheck) map[string]interface{} { var metadata map[string]interface{} for k, v := range check.RawPolicy { if k == "name" || k == "resource" { @@ -407,41 +495,362 @@ func buildCheckPayload(check CustodianCheck, execution CustodianExecutionResult) } metadata[k] = v } + return metadata +} + +func buildExecutionInfo(execution CustodianExecutionResult) StandardizedExecution { + status := "success" + if execution.Error != "" { + status = "error" + } + + durationMS := int64(execution.EndedAt.Sub(execution.StartedAt) / time.Millisecond) + if durationMS < 0 { + durationMS = 0 + } var executionErrors []string if len(execution.Errors) > 0 { executionErrors = append([]string{}, execution.Errors...) } - return &StandardizedCheckPayload{ - SchemaVersion: schemaVersionV1, - Source: sourceCloudCustodian, - Check: StandardizedCheckInfo{ - Name: check.Name, - Resource: check.Resource, - Provider: check.Provider, - Index: check.Index, - Metadata: metadata, - }, - Execution: StandardizedExecution{ - Status: status, - DryRun: true, - ExitCode: execution.ExitCode, - StartedAt: execution.StartedAt.UTC().Format(time.RFC3339Nano), - EndedAt: execution.EndedAt.UTC().Format(time.RFC3339Nano), - DurationMS: durationMS, - Stdout: execution.Stdout, - Stderr: execution.Stderr, - Error: execution.Error, - Errors: executionErrors, - }, - Result: StandardizedCheckResult{ - MatchedResourceCount: len(execution.Resources), - Resources: execution.Resources, - ArtifactPath: execution.ArtifactPath, - ResourcesPath: execution.ResourcesPath, + return StandardizedExecution{ + Status: status, + DryRun: true, + ExitCode: execution.ExitCode, + StartedAt: execution.StartedAt.UTC().Format(time.RFC3339Nano), + EndedAt: execution.EndedAt.UTC().Format(time.RFC3339Nano), + DurationMS: durationMS, + Stdout: execution.Stdout, + Stderr: execution.Stderr, + Error: execution.Error, + Errors: executionErrors, + } +} + +func resourceRecordDisambiguator(record ResourceRecord) string { + identityFields := map[string]string{} + for key, value := range record.IdentityFields { + if key == "resource_hash" || strings.TrimSpace(value) == "" || value == record.ID { + continue + } + identityFields[key] = value + } + if len(identityFields) > 0 { + return stableHashValue(identityFields) + } + return hashResource(record.Data) +} + +func resourceIDCollisions(records []ResourceRecord) map[string]bool { + counts := map[string]int{} + for _, record := range records { + counts[record.ID]++ + } + collisions := map[string]bool{} + for resourceID, count := range counts { + if count > 1 { + collisions[resourceID] = true + } + } + return collisions +} + +func mergeCollisionIDs(groups ...map[string]bool) map[string]bool { + merged := map[string]bool{} + for _, group := range groups { + for resourceID := range group { + merged[resourceID] = true + } + } + return merged +} + +func disambiguateResourceRecords(records []ResourceRecord, collisionIDs map[string]bool) (map[string]ResourceRecord, int) { + result := make(map[string]ResourceRecord, len(records)) + collisionCount := 0 + grouped := map[string][]ResourceRecord{} + for _, record := range records { + grouped[record.ID] = append(grouped[record.ID], record) + } + for resourceID, group := range grouped { + if !collisionIDs[resourceID] { + result[resourceID] = group[0] + continue + } + collisionCount += len(group) - 1 + for _, record := range group { + disambiguated := record + if disambiguated.IdentityFields == nil { + disambiguated.IdentityFields = map[string]string{} + } + hash := hashResource(disambiguated.Data) + disambiguated.IdentityFields["resource_hash"] = hash + baseSuffix := resourceRecordDisambiguator(disambiguated) + suffix := hash + if baseSuffix != "" && baseSuffix != hash { + suffix = fmt.Sprintf("%s-%s", baseSuffix, hash) + } + disambiguatedID := fmt.Sprintf("%s#%s", resourceID, suffix) + for i := 2; ; i++ { + if _, exists := result[disambiguatedID]; !exists { + break + } + disambiguatedID = fmt.Sprintf("%s#%s-%d", resourceID, suffix, i) + } + disambiguated.ID = disambiguatedID + result[disambiguatedID] = disambiguated + } + } + + return result, collisionCount +} + +func normalizeForHash(value interface{}) interface{} { + switch typed := value.(type) { + case map[string]interface{}: + normalized := make(map[string]interface{}, len(typed)) + for key, nested := range typed { + normalized[key] = normalizeForHash(nested) + } + return normalized + case map[interface{}]interface{}: + normalized := make(map[string]interface{}, len(typed)) + keys := make([]string, 0, len(typed)) + keyedValues := make(map[string]interface{}, len(typed)) + for key, nested := range typed { + stringKey := fmt.Sprint(key) + keys = append(keys, stringKey) + keyedValues[stringKey] = normalizeForHash(nested) + } + slices.Sort(keys) + for _, key := range keys { + normalized[key] = keyedValues[key] + } + return normalized + case []interface{}: + normalized := make([]interface{}, 0, len(typed)) + for _, item := range typed { + normalized = append(normalized, normalizeForHash(item)) + } + return normalized + } + + rv := reflect.ValueOf(value) + if !rv.IsValid() { + return nil + } + switch rv.Kind() { + case reflect.Map: + keys := rv.MapKeys() + stringKeys := make([]string, 0, len(keys)) + keyedValues := make(map[string]interface{}, len(keys)) + for _, key := range keys { + stringKey := fmt.Sprint(key.Interface()) + stringKeys = append(stringKeys, stringKey) + keyedValues[stringKey] = normalizeForHash(rv.MapIndex(key).Interface()) + } + slices.Sort(stringKeys) + normalized := make(map[string]interface{}, len(keys)) + for _, key := range stringKeys { + normalized[key] = keyedValues[key] + } + return normalized + case reflect.Slice, reflect.Array: + length := rv.Len() + normalized := make([]interface{}, 0, length) + for i := 0; i < length; i++ { + normalized = append(normalized, normalizeForHash(rv.Index(i).Interface())) + } + return normalized + default: + return value + } +} + +func stableHashValue(value interface{}) string { + content, err := json.Marshal(normalizeForHash(value)) + if err != nil { + content = []byte(fmt.Sprintf("%T:%v", value, value)) + } + sum := sha256.Sum256(content) + return hex.EncodeToString(sum[:]) +} + +func (p *CloudCustodianPlugin) buildResourceRecord(resourceType string, resource interface{}) ResourceRecord { + provider := extractProvider(resourceType) + identityFields := map[string]string{} + fieldPaths := p.identityFieldPaths(resourceType) + + resourceID := "" + for _, fieldPath := range fieldPaths { + value, ok := resourceStringAtPath(resource, fieldPath) + if !ok || value == "" { + continue + } + identityFields[fieldPath] = value + if resourceID == "" { + resourceID = value + } + } + if resourceID == "" { + resourceID = hashResource(resource) + identityFields["resource_hash"] = resourceID + } + resourceID = canonicalResourceID(resourceType, provider, resourceID) + if strings.HasPrefix(resourceID, "arn:") { + identityFields["arn"] = resourceID + } + + record := ResourceRecord{ + ID: resourceID, + Type: resourceType, + Provider: provider, + IdentityFields: identityFields, + Data: resource, + } + if accountID, ok := firstResourceString(resource, []string{"AccountId", "AccountID", "account_id", "accountId", "OwnerId", "OwnerID", "owner_id", "c7n:account-id"}); ok { + record.AccountID = accountID + } + if region, ok := firstResourceString(resource, []string{"Region", "region", "AwsRegion", "aws_region", "awsRegion", "c7n:region"}); ok { + record.Region = region + } + return record +} + +func canonicalResourceID(resourceType string, provider string, resourceID string) string { + resourceID = strings.TrimSpace(resourceID) + if resourceID == "" || strings.HasPrefix(resourceID, "arn:") { + return resourceID + } + if provider == "aws" && resourceType == "aws.hostedzone" { + hostedZoneID := strings.TrimPrefix(resourceID, "/") + if strings.HasPrefix(hostedZoneID, "hostedzone/") { + return "arn:aws:route53:::" + hostedZoneID + } + if strings.HasPrefix(hostedZoneID, "Z") { + return "arn:aws:route53:::hostedzone/" + hostedZoneID + } + } + return resourceID +} + +func (p *CloudCustodianPlugin) identityFieldPaths(resourceType string) []string { + paths := make([]string, 0) + if p.parsedConfig != nil { + if configured, ok := p.parsedConfig.ResourceIdentityFields[resourceType]; ok { + paths = append(paths, configured...) + } + } + paths = append(paths, + "Arn", + "ARN", + "arn", + "Id", + "ID", + "id", + "InstanceId", + "instance_id", + "InstanceID", + "ResourceId", + "resource_id", + "Name", + "name", + ) + return compactUniqueStrings(paths) +} + +func compactUniqueStrings(values []string) []string { + seen := map[string]bool{} + result := make([]string, 0, len(values)) + for _, value := range values { + value = strings.TrimSpace(value) + if value == "" || seen[value] { + continue + } + seen[value] = true + result = append(result, value) + } + return result +} + +func firstResourceString(resource interface{}, fieldPaths []string) (string, bool) { + for _, fieldPath := range fieldPaths { + value, ok := resourceStringAtPath(resource, fieldPath) + if ok && value != "" { + return value, true + } + } + return "", false +} + +func resourceStringAtPath(resource interface{}, fieldPath string) (string, bool) { + current := resource + for _, part := range strings.Split(fieldPath, ".") { + part = strings.TrimSpace(part) + if part == "" { + return "", false + } + switch typed := current.(type) { + case map[string]interface{}: + value, ok := typed[part] + if !ok { + return "", false + } + current = value + case map[interface{}]interface{}: + value, ok := typed[part] + if !ok { + return "", false + } + current = value + default: + return "", false + } + } + + switch value := current.(type) { + case string: + return strings.TrimSpace(value), strings.TrimSpace(value) != "" + case json.Number: + return value.String(), value.String() != "" + case float64: + return strconv.FormatFloat(value, 'f', -1, 64), true + case float32: + return strconv.FormatFloat(float64(value), 'f', -1, 32), true + case int: + return strconv.Itoa(value), true + case int64: + return strconv.FormatInt(value, 10), true + case bool: + return strconv.FormatBool(value), true + default: + return "", false + } +} + +func hashResource(resource interface{}) string { + normalized := normalizeForHash(resource) + content, err := json.Marshal(normalized) + if err != nil { + content = []byte(fmt.Sprintf("%#v", normalized)) + } + sum := sha256.Sum256(content) + return hex.EncodeToString(sum[:]) +} + +func buildInventoryCheck(resourceType string) CustodianCheck { + provider := extractProvider(resourceType) + name := fmt.Sprintf("inventory-%s", sanitizeIdentifier(resourceType)) + return CustodianCheck{ + Index: -1, + Name: name, + Resource: resourceType, + Provider: provider, + RawPolicy: map[string]interface{}{ + "name": name, + "resource": resourceType, }, - RawPolicy: check.RawPolicy, } } @@ -756,6 +1165,99 @@ func (p *CloudCustodianPlugin) Configure(req *proto.ConfigureRequest) (*proto.Co return &proto.ConfigureResponse{}, nil } +func (p *CloudCustodianPlugin) Init(req *proto.InitRequest, apiHelper runner.ApiHelper) (*proto.InitResponse, error) { + p.Logger.Debug("Cloud Custodian Plugin Init called", + "configured", p.parsedConfig != nil, + "policy_paths", req.GetPolicyPaths(), + "policy_paths_count", len(req.GetPolicyPaths()), + "checks_count", len(p.checks), + ) + if p.parsedConfig == nil { + p.Logger.Error("Cloud Custodian Plugin Init failed because plugin is not configured") + return nil, errors.New("plugin not configured") + } + + resourceTypes := p.uniqueResourceTypes() + subjectTemplates := p.buildSubjectTemplates(resourceTypes) + templateNames := make([]string, 0, len(subjectTemplates)) + for _, subjectTemplate := range subjectTemplates { + templateNames = append(templateNames, subjectTemplate.GetName()) + } + p.Logger.Debug("Cloud Custodian Plugin Init prepared subject templates", + "resource_types", resourceTypes, + "subject_template_count", len(subjectTemplates), + "subject_template_names", templateNames, + ) + + p.Logger.Debug("Cloud Custodian Plugin Init delegating subject and risk template upsert", + "policy_paths", req.GetPolicyPaths(), + "subject_template_count", len(subjectTemplates), + ) + resp, err := runner.InitWithSubjectsAndRisksFromPolicies( + context.Background(), + p.Logger, + req, + apiHelper, + subjectTemplates, + ) + if err != nil { + p.Logger.Error("Cloud Custodian Plugin Init failed while upserting subject or risk templates", "error", err) + return resp, err + } + p.Logger.Debug("Cloud Custodian Plugin Init completed", + "subject_template_count", len(subjectTemplates), + "policy_paths_count", len(req.GetPolicyPaths()), + ) + return resp, nil +} + +func (p *CloudCustodianPlugin) buildSubjectTemplates(resourceTypes []string) []*proto.SubjectTemplate { + templates := make([]*proto.SubjectTemplate, 0, len(resourceTypes)) + for _, resourceType := range resourceTypes { + provider := extractProvider(resourceType) + templates = append(templates, &proto.SubjectTemplate{ + Name: fmt.Sprintf("cloud-custodian-%s", sanitizeIdentifier(resourceType)), + // These templates represent cloud resources collected during evaluation. + Type: proto.SubjectType_SUBJECT_TYPE_RESOURCE, + TitleTemplate: "Cloud Resource: {{ .resource_type }} {{ .resource_id }}", + DescriptionTemplate: "Cloud Custodian resource {{ .resource_id }} of type {{ .resource_type }} from provider {{ .provider }}", + PurposeTemplate: "Represents a cloud resource collected by Cloud Custodian for compliance evaluation.", + IdentityLabelKeys: []string{"provider", "resource_type", "resource_id"}, + Props: []*proto.SubjectProp{ + {Name: "provider", Value: provider}, + {Name: "resource_type", Value: resourceType}, + }, + SelectorLabels: []*proto.SubjectLabelSelector{ + {Key: "source", Value: sourceCloudCustodian}, + {Key: "resource_type", Value: resourceType}, + }, + LabelSchema: []*proto.SubjectLabelSchema{ + {Key: "provider", Description: "Cloud provider derived from the Cloud Custodian resource type."}, + {Key: "resource_type", Description: "Cloud Custodian resource type such as aws.ec2 or aws.s3."}, + {Key: "resource_id", Description: "Stable resource identifier extracted from the resource data."}, + {Key: "account_id", Description: "Cloud account identifier when available in the resource data."}, + {Key: "region", Description: "Cloud region when available in the resource data."}, + }, + }) + } + return templates +} + +func (p *CloudCustodianPlugin) uniqueResourceTypes() []string { + seen := map[string]bool{} + resourceTypes := make([]string, 0) + for _, check := range p.checks { + resourceType := strings.TrimSpace(check.Resource) + if resourceType == "" || resourceType == "unknown" || len(check.ParseErrors) > 0 || seen[resourceType] { + continue + } + seen[resourceType] = true + resourceTypes = append(resourceTypes, resourceType) + } + slices.Sort(resourceTypes) + return resourceTypes +} + func (p *CloudCustodianPlugin) Eval(req *proto.EvalRequest, apiHelper runner.ApiHelper) (*proto.EvalResponse, error) { ctx := context.Background() @@ -776,68 +1278,133 @@ func (p *CloudCustodianPlugin) Eval(req *proto.EvalRequest, apiHelper runner.Api defer os.RemoveAll(executionRoot) p.Logger.Debug("Created temporary execution root", "execution_root", executionRoot) - allEvidences := make([]*proto.Evidence, 0) + pendingEvidences := make([]*proto.Evidence, 0, evidenceBatchSize) + totalEvidenceCount := 0 var accumulatedErrors error successfulPolicyRuns := 0 + hadCheckExecutionFailures := false + baselines := p.collectInventoryBaselines(ctx, executionRoot) for _, check := range p.checks { p.Logger.Debug("Processing check", "check_name", check.Name, "check_index", check.Index, "resource", check.Resource, "provider", check.Provider) - execution := CustodianExecutionResult{} if len(check.ParseErrors) > 0 { p.Logger.Warn("Skipping custodian execution due to check parse issues", "check_name", check.Name, "parse_errors", check.ParseErrors) - execution = newCheckErrorExecution(check.ParseErrors) - } else { - checkDir := filepath.Join(executionRoot, fmt.Sprintf("%03d-%s", check.Index+1, sanitizeIdentifier(check.Name))) - execution = p.executor.Execute(ctx, CustodianExecutionRequest{ - BinaryPath: p.parsedConfig.CustodianBinary, - Check: check, - Timeout: p.parsedConfig.CheckTimeout, - OutputDir: checkDir, - }) + accumulatedErrors = errors.Join(accumulatedErrors, fmt.Errorf("check %s has parse errors: %s", check.Name, strings.Join(check.ParseErrors, "; "))) + hadCheckExecutionFailures = true + continue } - payload := buildCheckPayload(check, execution) - p.Logger.Debug("Built standardized check payload", - "check_name", payload.Check.Name, - "status", payload.Execution.Status, - "matched_resource_count", payload.Result.MatchedResourceCount, - "execution_error_count", len(payload.Execution.Errors), + baseline := baselines[check.Resource] + if baseline == nil || baseline.Err != nil { + var err error + if baseline != nil && baseline.Err != nil { + err = fmt.Errorf("inventory baseline unavailable for resource type %s: %w", check.Resource, baseline.Err) + } else { + err = fmt.Errorf("inventory baseline unavailable for resource type %s", check.Resource) + } + p.Logger.Error("Skipping check due to unavailable inventory baseline", "check_name", check.Name, "resource", check.Resource, "error", err) + accumulatedErrors = errors.Join(accumulatedErrors, err) + hadCheckExecutionFailures = true + continue + } + + checkDir := filepath.Join(executionRoot, fmt.Sprintf("%03d-%s", check.Index+1, sanitizeIdentifier(check.Name))) + execution := p.executor.Execute(ctx, CustodianExecutionRequest{ + BinaryPath: p.parsedConfig.CustodianBinary, + Check: check, + Timeout: p.parsedConfig.CheckTimeout, + OutputDir: checkDir, + }) + if execution.Err != nil || execution.Error != "" { + err := formatExecutionFailure(check.Name, execution) + p.Logger.Error("Skipping resource evaluation due to check execution error", "check_name", check.Name, "error", err) + accumulatedErrors = errors.Join(accumulatedErrors, err) + hadCheckExecutionFailures = true + continue + } + + payloads := p.buildResourcePayloadsForCheck(check, execution, baseline) + payloadStats := summarizePayloadAssessments(payloads) + p.Logger.Debug("Built standardized resource payloads", + "check_name", check.Name, + "payload_count", len(payloads), + "matched_resource_count", len(execution.Resources), + "baseline_resource_count", len(baseline.Records), + "compliant_resource_count", payloadStats.Compliant, + "non_compliant_resource_count", payloadStats.NonCompliant, + "missing_from_baseline_count", payloadStats.MissingFromBaseline, ) + if len(baseline.Records) == 0 && len(execution.Resources) > 0 { + p.Logger.Warn("No compliant resource payloads can be generated because inventory baseline is empty while policy returned matched resources", + "check_name", check.Name, + "resource", check.Resource, + "matched_resource_count", len(execution.Resources), + "resources_path", execution.ResourcesPath, + ) + } if p.parsedConfig.DebugDumpPayloads { - if err := p.dumpStandardizedPayload(payload); err != nil { - p.Logger.Warn("Failed writing debug standardized payload", "check_name", payload.Check.Name, "error", err) + for _, payload := range payloads { + if err := p.dumpStandardizedPayload(payload); err != nil { + p.Logger.Warn("Failed writing debug standardized payload", "check_name", payload.Check.Name, "resource_id", payload.Resource.ID, "error", err) + } } } - evidences, evalErr, successfulRuns := p.evaluateCheckPolicies(ctx, payload, req.GetPolicyPaths()) - allEvidences = append(allEvidences, evidences...) - successfulPolicyRuns += successfulRuns - p.Logger.Debug("Completed policy evaluations for check", - "check_name", payload.Check.Name, - "successful_policy_runs", successfulRuns, - "produced_evidence_count", len(evidences), - "had_eval_error", evalErr != nil, - ) - if evalErr != nil { - accumulatedErrors = errors.Join(accumulatedErrors, evalErr) + + for _, payload := range payloads { + evidences, evalErr, successfulRuns := p.evaluateResourcePolicies(ctx, payload, req.GetPolicyPaths()) + pendingEvidences = append(pendingEvidences, evidences...) + totalEvidenceCount += len(evidences) + successfulPolicyRuns += successfulRuns + p.Logger.Debug("Completed policy evaluations for resource", + "check_name", payload.Check.Name, + "resource_id", payload.Resource.ID, + "assessment_status", payload.Assessment.Status, + "successful_policy_runs", successfulRuns, + "produced_evidence_count", len(evidences), + "had_eval_error", evalErr != nil, + ) + if evalErr != nil { + accumulatedErrors = errors.Join(accumulatedErrors, evalErr) + } + for len(pendingEvidences) >= evidenceBatchSize { + if err := p.submitEvidenceBatch(ctx, apiHelper, pendingEvidences[:evidenceBatchSize]); err != nil { + return &proto.EvalResponse{Status: proto.ExecutionStatus_FAILURE}, err + } + pendingEvidences = pendingEvidences[evidenceBatchSize:] + } } } - if len(allEvidences) > 0 { - p.Logger.Debug("Submitting evidence batch via ApiHelper", "evidence_count", len(allEvidences)) - if err := apiHelper.CreateEvidence(ctx, allEvidences); err != nil { - p.Logger.Error("Error creating evidence", "error", err) - return &proto.EvalResponse{Status: proto.ExecutionStatus_FAILURE}, err + if len(pendingEvidences) > 0 { + for len(pendingEvidences) > 0 { + batch := pendingEvidences + if len(batch) > evidenceBatchSize { + batch = batch[:evidenceBatchSize] + } + if err := p.submitEvidenceBatch(ctx, apiHelper, batch); err != nil { + return &proto.EvalResponse{Status: proto.ExecutionStatus_FAILURE}, err + } + pendingEvidences = pendingEvidences[len(batch):] + if len(pendingEvidences) == 0 { + pendingEvidences = nil + } } } else { p.Logger.Warn("No evidence generated by current evaluation run") } - if successfulPolicyRuns == 0 && len(allEvidences) == 0 { + if successfulPolicyRuns == 0 && totalEvidenceCount == 0 { if accumulatedErrors == nil { accumulatedErrors = errors.New("policy evaluation failed for all checks") } return &proto.EvalResponse{Status: proto.ExecutionStatus_FAILURE}, accumulatedErrors } + if hadCheckExecutionFailures { + if accumulatedErrors == nil { + accumulatedErrors = errors.New("one or more cloud custodian checks failed to execute") + } + return &proto.EvalResponse{Status: proto.ExecutionStatus_FAILURE}, accumulatedErrors + } if accumulatedErrors != nil { p.Logger.Warn("Completed with non-fatal policy evaluation errors", "error", accumulatedErrors) @@ -846,31 +1413,193 @@ func (p *CloudCustodianPlugin) Eval(req *proto.EvalRequest, apiHelper runner.Api return &proto.EvalResponse{Status: proto.ExecutionStatus_SUCCESS}, nil } -func (p *CloudCustodianPlugin) evaluateCheckPolicies( +func (p *CloudCustodianPlugin) submitEvidenceBatch(ctx context.Context, apiHelper runner.ApiHelper, evidences []*proto.Evidence) error { + p.Logger.Debug("Submitting evidence batch via ApiHelper", "evidence_count", len(evidences)) + if err := apiHelper.CreateEvidence(ctx, evidences); err != nil { + p.Logger.Error("Error creating evidence", "error", err) + return err + } + return nil +} + +func (p *CloudCustodianPlugin) collectInventoryBaselines(ctx context.Context, executionRoot string) map[string]*InventoryBaseline { + baselines := map[string]*InventoryBaseline{} + for _, resourceType := range p.uniqueResourceTypes() { + check := buildInventoryCheck(resourceType) + outputDir := filepath.Join(executionRoot, fmt.Sprintf("inventory-%s", sanitizeIdentifier(resourceType))) + execution := p.executor.Execute(ctx, CustodianExecutionRequest{ + BinaryPath: p.parsedConfig.CustodianBinary, + Check: check, + Timeout: p.parsedConfig.CheckTimeout, + OutputDir: outputDir, + }) + + var baselineErr error + if execution.Err != nil || execution.Error != "" { + baselineErr = formatExecutionFailure(check.Name, execution) + } + records := make([]ResourceRecord, 0, len(execution.Resources)) + for _, resource := range execution.Resources { + records = append(records, p.buildResourceRecord(resourceType, resource)) + } + collisionIDs := resourceIDCollisions(records) + _, collisionCount := disambiguateResourceRecords(records, collisionIDs) + baseline := &InventoryBaseline{ + Execution: execution, + Records: records, + ResourceType: resourceType, + Provider: extractProvider(resourceType), + Err: baselineErr, + } + baselines[resourceType] = baseline + p.Logger.Debug("Collected inventory baseline", + "resource", resourceType, + "resource_count", len(records), + "raw_resource_count", len(execution.Resources), + "id_collision_count", collisionCount, + "resources_path", execution.ResourcesPath, + "exit_code", execution.ExitCode, + "had_error", baseline.Err != nil, + "error", execution.Error, + ) + } + return baselines +} + +func (p *CloudCustodianPlugin) buildResourcePayloadsForCheck( + check CustodianCheck, + execution CustodianExecutionResult, + baseline *InventoryBaseline, +) []*StandardizedResourcePayload { + matchedRecords := make([]ResourceRecord, 0, len(execution.Resources)) + for _, resource := range execution.Resources { + matchedRecords = append(matchedRecords, p.buildResourceRecord(check.Resource, resource)) + } + collisionIDs := mergeCollisionIDs( + resourceIDCollisions(baseline.Records), + resourceIDCollisions(matchedRecords), + ) + baselineResources, baselineCollisionCount := disambiguateResourceRecords(baseline.Records, collisionIDs) + matched, collisionCount := disambiguateResourceRecords(matchedRecords, collisionIDs) + if baselineCollisionCount > 0 { + p.Logger.Warn("Detected duplicate baseline resource identifiers; disambiguating with stable secondary keys", + "check_name", check.Name, + "resource", check.Resource, + "id_collision_count", baselineCollisionCount, + ) + } + if collisionCount > 0 { + p.Logger.Warn("Detected duplicate matched resource identifiers; disambiguating with stable secondary keys", + "check_name", check.Name, + "resource", check.Resource, + "id_collision_count", collisionCount, + ) + } + + resourceIDs := make([]string, 0, len(baselineResources)+len(matched)) + seen := map[string]bool{} + for resourceID := range baselineResources { + resourceIDs = append(resourceIDs, resourceID) + seen[resourceID] = true + } + for resourceID := range matched { + if seen[resourceID] { + continue + } + resourceIDs = append(resourceIDs, resourceID) + } + slices.Sort(resourceIDs) + + payloads := make([]*StandardizedResourcePayload, 0, len(resourceIDs)) + for _, resourceID := range resourceIDs { + record, existsInBaseline := baselineResources[resourceID] + matchedRecord, isMatched := matched[resourceID] + inventoryStatus := "baseline" + if !existsInBaseline { + record = matchedRecord + inventoryStatus = "missing_from_baseline" + } + + status := "compliant" + if isMatched { + status = "non_compliant" + } + + payloads = append(payloads, buildResourcePayload(check, execution, record, StandardizedAssessment{ + Status: status, + Matched: isMatched, + InventoryStatus: inventoryStatus, + MatchedResourceCount: len(execution.Resources), + ArtifactPath: execution.ArtifactPath, + ResourcesPath: execution.ResourcesPath, + })) + } + return payloads +} + +type PayloadAssessmentStats struct { + Compliant int + NonCompliant int + MissingFromBaseline int +} + +func summarizePayloadAssessments(payloads []*StandardizedResourcePayload) PayloadAssessmentStats { + stats := PayloadAssessmentStats{} + for _, payload := range payloads { + if payload == nil { + continue + } + switch payload.Assessment.Status { + case "compliant": + stats.Compliant++ + case "non_compliant": + stats.NonCompliant++ + } + if payload.Assessment.InventoryStatus == "missing_from_baseline" { + stats.MissingFromBaseline++ + } + } + return stats +} + +func (p *CloudCustodianPlugin) evaluateResourcePolicies( ctx context.Context, - payload *StandardizedCheckPayload, + payload *StandardizedResourcePayload, policyPaths []string, ) ([]*proto.Evidence, error, int) { - p.Logger.Debug("Evaluating policy paths for check", + p.Logger.Debug("Evaluating policy paths for resource", "check_name", payload.Check.Name, - "check_status", payload.Execution.Status, + "resource_id", payload.Resource.ID, + "assessment_status", payload.Assessment.Status, "policy_paths_count", len(policyPaths), ) - labels := map[string]string{} - maps.Copy(labels, p.parsedConfig.PolicyLabels) + p.logPolicyPayload(payload) + labels := resourcePolicyLabels(p.parsedConfig.PolicyLabels) labels["source"] = sourceCloudCustodian labels["tool"] = sourceCloudCustodian if _, exists := labels["provider"]; !exists { - labels["provider"] = payload.Check.Provider + labels["provider"] = payload.Resource.Provider } - labels["type"] = "check" + labels["type"] = "resource" labels["check_name"] = payload.Check.Name labels["check_resource"] = payload.Check.Resource - labels["check_provider"] = payload.Check.Provider labels["check_status"] = payload.Execution.Status + labels["resource_type"] = payload.Resource.Type + labels["resource_id"] = payload.Resource.ID + if payload.Resource.AccountID != "" { + labels["account_id"] = payload.Resource.AccountID + } + if payload.Resource.Region != "" { + labels["region"] = payload.Resource.Region + } checkID := fmt.Sprintf("cloud-custodian-check/%s-%d", sanitizeIdentifier(payload.Check.Name), payload.Check.Index+1) providerID := fmt.Sprintf("cloud-provider/%s", sanitizeIdentifier(payload.Check.Provider)) + resourceSubjectID := fmt.Sprintf( + "cloud-custodian-resource/%s/%s", + url.PathEscape(payload.Resource.Type), + url.PathEscape(payload.Resource.ID), + ) actors := []*proto.OriginActor{ { @@ -929,6 +1658,20 @@ func (p *CloudCustodianPlugin) evaluateCheckPolicies( } subjects := []*proto.Subject{ + { + Type: proto.SubjectType_SUBJECT_TYPE_RESOURCE, + Identifier: resourceSubjectID, + Links: []*proto.Link{ + { + Href: payload.Resource.ID, + Rel: policyManager.Pointer("related"), + Text: policyManager.Pointer("Cloud resource identifier"), + }, + }, + Props: []*proto.Property{ + {Name: "resource_id", Value: payload.Resource.ID}, + }, + }, {Type: proto.SubjectType_SUBJECT_TYPE_INVENTORY_ITEM, Identifier: checkID}, {Type: proto.SubjectType_SUBJECT_TYPE_COMPONENT, Identifier: providerID}, } @@ -939,13 +1682,13 @@ func (p *CloudCustodianPlugin) evaluateCheckPolicies( Steps: []*proto.Step{ {Title: "Load Policy", Description: "Load one Cloud Custodian policy entry from the configured policy document."}, {Title: "Run Dry-Run Check", Description: "Execute Cloud Custodian using --dryrun and capture generated artifacts."}, - {Title: "Build Standardized Payload", Description: "Convert execution output and matched resources into standardized OPA input."}, + {Title: "Build Resource Payload", Description: "Compare policy matches with inventory baseline and build one standardized OPA input for this resource."}, }, }, { Title: "Evaluate OPA Policy Bundles", Steps: []*proto.Step{ - {Title: "Evaluate Check Payload", Description: "Run policy bundles against the standardized Cloud Custodian check payload."}, + {Title: "Evaluate Resource Payload", Description: "Run policy bundles against the standardized Cloud Custodian resource payload."}, }, }, } @@ -955,7 +1698,7 @@ func (p *CloudCustodianPlugin) evaluateCheckPolicies( successfulRuns := 0 for _, policyPath := range policyPaths { - p.Logger.Trace("Running policy path for check", "check_name", payload.Check.Name, "policy_path", policyPath) + p.Logger.Trace("Running policy path for resource", "check_name", payload.Check.Name, "resource_id", payload.Resource.ID, "policy_path", policyPath) evidences, err := p.evaluator.Generate( ctx, policyPath, @@ -969,24 +1712,27 @@ func (p *CloudCustodianPlugin) evaluateCheckPolicies( ) allEvidences = append(allEvidences, evidences...) if err != nil { - p.Logger.Warn("Policy path evaluation failed for check", + p.Logger.Warn("Policy path evaluation failed for resource", "check_name", payload.Check.Name, + "resource_id", payload.Resource.ID, "policy_path", policyPath, "error", err, ) - accumulatedErrors = errors.Join(accumulatedErrors, fmt.Errorf("policy %s failed for check %s: %w", policyPath, payload.Check.Name, err)) + accumulatedErrors = errors.Join(accumulatedErrors, fmt.Errorf("policy %s failed for check %s resource %s: %w", policyPath, payload.Check.Name, payload.Resource.ID, err)) continue } - p.Logger.Trace("Policy path evaluation succeeded for check", + p.Logger.Trace("Policy path evaluation succeeded for resource", "check_name", payload.Check.Name, + "resource_id", payload.Resource.ID, "policy_path", policyPath, "evidence_count", len(evidences), ) successfulRuns++ } - p.Logger.Debug("Completed policy path loop for check", + p.Logger.Debug("Completed policy path loop for resource", "check_name", payload.Check.Name, + "resource_id", payload.Resource.ID, "successful_runs", successfulRuns, "evidence_count", len(allEvidences), "had_errors", accumulatedErrors != nil, @@ -994,7 +1740,55 @@ func (p *CloudCustodianPlugin) evaluateCheckPolicies( return allEvidences, accumulatedErrors, successfulRuns } -func (p *CloudCustodianPlugin) dumpStandardizedPayload(payload *StandardizedCheckPayload) error { +func resourcePolicyLabels(policyLabels map[string]string) map[string]string { + labels := map[string]string{} + for key, value := range policyLabels { + if isReservedResourceLabel(key) { + continue + } + labels[key] = value + } + return labels +} + +func isReservedResourceLabel(label string) bool { + switch label { + case "assessment", "assessment_status", "check_provider": + return true + default: + return false + } +} + +func formatExecutionFailure(checkName string, execution CustodianExecutionResult) error { + switch { + case execution.Error != "" && execution.Err != nil: + return fmt.Errorf("custodian policy execution failed for check %s: %s: %w", checkName, execution.Error, execution.Err) + case execution.Error != "": + return fmt.Errorf("custodian policy execution failed for check %s: %s", checkName, execution.Error) + case execution.Err != nil: + return fmt.Errorf("custodian policy execution failed for check %s: %w", checkName, execution.Err) + default: + return fmt.Errorf("custodian policy execution failed for check %s", checkName) + } +} + +func (p *CloudCustodianPlugin) logPolicyPayload(payload *StandardizedResourcePayload) { + if payload == nil || !p.Logger.IsDebug() { + return + } + + p.Logger.Debug("Policy payload", + "check_name", payload.Check.Name, + "resource_id", payload.Resource.ID, + "assessment_status", payload.Assessment.Status, + "resource_type", payload.Resource.Type, + "provider", payload.Resource.Provider, + "debug_dump_payloads", p.parsedConfig != nil && p.parsedConfig.DebugDumpPayloads, + ) +} + +func (p *CloudCustodianPlugin) dumpStandardizedPayload(payload *StandardizedResourcePayload) error { if payload == nil { return errors.New("payload is nil") } @@ -1007,9 +1801,16 @@ func (p *CloudCustodianPlugin) dumpStandardizedPayload(payload *StandardizedChec return fmt.Errorf("marshal payload: %w", err) } - fileName := fmt.Sprintf("%03d-%s-%d.json", + sanitizedCheckName := sanitizeIdentifier(payload.Check.Name) + sanitizedResourceID := sanitizeIdentifier(payload.Resource.ID) + if len(sanitizedResourceID) > 50 { + shortHash := hashResource(payload.Resource.ID)[:12] + sanitizedResourceID = sanitizedResourceID[:50] + "-" + shortHash + } + fileName := fmt.Sprintf("%03d-%s-%s-%d.json", payload.Check.Index+1, - sanitizeIdentifier(payload.Check.Name), + sanitizedCheckName, + sanitizedResourceID, time.Now().UTC().UnixNano(), ) outputPath := filepath.Join(p.parsedConfig.DebugPayloadOutputDir, fileName) @@ -1019,6 +1820,7 @@ func (p *CloudCustodianPlugin) dumpStandardizedPayload(payload *StandardizedChec p.Logger.Debug("Wrote standardized payload debug file", "check_name", payload.Check.Name, + "resource_id", payload.Resource.ID, "output_path", outputPath, "bytes", len(content), ) @@ -1100,7 +1902,7 @@ func main() { goplugin.Serve(&goplugin.ServeConfig{ HandshakeConfig: runner.HandshakeConfig, Plugins: map[string]goplugin.Plugin{ - "runner": &runner.RunnerGRPCPlugin{Impl: plugin}, + "runner": &runner.RunnerV2GRPCPlugin{Impl: plugin}, }, GRPCServer: goplugin.DefaultGRPCServer, }) diff --git a/main_test.go b/main_test.go index dc32453..266b71b 100644 --- a/main_test.go +++ b/main_test.go @@ -85,6 +85,47 @@ func TestPluginConfigParse(t *testing.T) { } }) + t.Run("parse resource identity fields", func(t *testing.T) { + parsed, err := (&PluginConfig{ + PoliciesYAML: "x", + ResourceIdentityFields: `{"aws.ec2":[" InstanceId ","Arn",""]}`, + }).Parse() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + got := parsed.ResourceIdentityFields["aws.ec2"] + if len(got) != 2 || got[0] != "InstanceId" || got[1] != "Arn" { + t.Fatalf("unexpected normalized resource identity fields: %#v", got) + } + }) + + t.Run("reject invalid resource identity fields json", func(t *testing.T) { + _, err := (&PluginConfig{PoliciesYAML: "x", ResourceIdentityFields: "{"}).Parse() + if err == nil { + t.Fatalf("expected error for invalid resource_identity_fields json") + } + }) + + t.Run("reject empty resource type in resource identity fields", func(t *testing.T) { + _, err := (&PluginConfig{ + PoliciesYAML: "x", + ResourceIdentityFields: `{"":["Id"]}`, + }).Parse() + if err == nil { + t.Fatalf("expected error for empty resource type key") + } + }) + + t.Run("reject empty resource identity field list", func(t *testing.T) { + _, err := (&PluginConfig{ + PoliciesYAML: "x", + ResourceIdentityFields: `{"aws.ec2":[" ",""]}`, + }).Parse() + if err == nil { + t.Fatalf("expected error for empty resource identity field list") + } + }) + t.Run("reject invalid timeout", func(t *testing.T) { _, err := (&PluginConfig{PoliciesYAML: "x", CheckTimeoutSeconds: "abc"}).Parse() if err == nil { @@ -374,86 +415,6 @@ sleep 2 }) } -func TestBuildCheckPayload(t *testing.T) { - now := time.Now().UTC() - check := CustodianCheck{ - Index: 0, - Name: "s3-check", - Resource: "aws.s3", - Provider: "aws", - RawPolicy: map[string]interface{}{ - "name": "s3-check", - "resource": "aws.s3", - "mode": map[string]interface{}{ - "type": "periodic", - }, - }, - } - - success := buildCheckPayload(check, CustodianExecutionResult{ - StartedAt: now, - EndedAt: now.Add(2 * time.Second), - ExitCode: 0, - Stdout: "ok", - Resources: []interface{}{map[string]interface{}{"id": "a"}}, - ArtifactPath: "/tmp/out", - ResourcesPath: "/tmp/out/s3/resources.json", - }) - - if success.SchemaVersion != "v1" { - t.Fatalf("expected schema version v1") - } - if success.Execution.Status != "success" { - t.Fatalf("expected success status") - } - if success.Result.MatchedResourceCount != 1 { - t.Fatalf("expected matched count 1, got %d", success.Result.MatchedResourceCount) - } - if success.Check.Metadata["mode"] == nil { - t.Fatalf("expected metadata to include non-name/resource fields") - } - - failure := buildCheckPayload(check, newCheckErrorExecution([]string{"parse failure"})) - if failure.Execution.Status != "error" { - t.Fatalf("expected error status") - } - if failure.Result.MatchedResourceCount != 0 { - t.Fatalf("expected matched count 0 for failed payload") - } - if len(failure.Execution.Errors) != 1 || failure.Execution.Errors[0] != "parse failure" { - t.Fatalf("expected structured execution errors in payload, got %v", failure.Execution.Errors) - } - - minimalCheck := CustodianCheck{ - Index: 1, - Name: "minimal-check", - Resource: "aws.s3", - Provider: "aws", - RawPolicy: map[string]interface{}{ - "name": "minimal-check", - "resource": "aws.s3", - }, - } - minimal := buildCheckPayload(minimalCheck, CustodianExecutionResult{ - StartedAt: now, - EndedAt: now, - ExitCode: 0, - Resources: []interface{}{}, - }) - - rawJSON, err := json.Marshal(minimal) - if err != nil { - t.Fatalf("failed to marshal minimal payload: %v", err) - } - jsonText := string(rawJSON) - if strings.Contains(jsonText, `"metadata":{}`) { - t.Fatalf("expected metadata to be omitted when empty, got: %s", jsonText) - } - if strings.Contains(jsonText, `"errors":[]`) { - t.Fatalf("expected execution.errors to be omitted when empty, got: %s", jsonText) - } -} - type fakeExecutor struct { calls []CustodianExecutionRequest results map[string]CustodianExecutionResult @@ -469,9 +430,10 @@ func (f *fakeExecutor) Execute(ctx context.Context, req CustodianExecutionReques } type fakePolicyEvaluator struct { - calls []string - failChecks map[string]bool - labelsSeen []map[string]string + calls []string + failChecks map[string]bool + labelsSeen []map[string]string + subjectsSeen [][]*proto.Subject } func (f *fakePolicyEvaluator) Generate( @@ -485,28 +447,36 @@ func (f *fakePolicyEvaluator) Generate( activities []*proto.Activity, data interface{}, ) ([]*proto.Evidence, error) { - payload, ok := data.(*StandardizedCheckPayload) + payload, ok := data.(*StandardizedResourcePayload) if !ok { return nil, errors.New("unexpected payload type") } - f.calls = append(f.calls, fmt.Sprintf("%s|%s|%s", payload.Check.Name, policyPath, payload.Execution.Status)) + f.calls = append(f.calls, fmt.Sprintf("%s|%s|%s|%s", payload.Check.Name, payload.Resource.ID, policyPath, payload.Assessment.Status)) copiedLabels := map[string]string{} for k, v := range labels { copiedLabels[k] = v } f.labelsSeen = append(f.labelsSeen, copiedLabels) + f.subjectsSeen = append(f.subjectsSeen, subjects) if f.failChecks[payload.Check.Name] { return nil, errors.New("forced evaluator error") } - return []*proto.Evidence{{UUID: fmt.Sprintf("%s-%s", payload.Check.Name, policyPath), Labels: labels}}, nil + evidenceLabels := map[string]string{} + for k, v := range labels { + evidenceLabels[k] = v + } + return []*proto.Evidence{{UUID: fmt.Sprintf("%s-%s-%s", payload.Check.Name, payload.Resource.ID, policyPath), Labels: evidenceLabels}}, nil } type fakeAPIHelper struct { - calls int - evidence []*proto.Evidence - err error + calls int + evidence []*proto.Evidence + err error + subjectTemplates []*proto.SubjectTemplate + riskTemplateCalls int + riskTemplatePackages []string } func (f *fakeAPIHelper) CreateEvidence(ctx context.Context, evidence []*proto.Evidence) error { @@ -515,11 +485,37 @@ func (f *fakeAPIHelper) CreateEvidence(ctx context.Context, evidence []*proto.Ev return f.err } +func (f *fakeAPIHelper) UpsertRiskTemplates(ctx context.Context, packageName string, riskTemplates []*proto.RiskTemplate) error { + f.riskTemplateCalls++ + f.riskTemplatePackages = append(f.riskTemplatePackages, packageName) + return nil +} + +func (f *fakeAPIHelper) UpsertSubjectTemplates(ctx context.Context, subjectTemplates []*proto.SubjectTemplate) error { + f.subjectTemplates = append(f.subjectTemplates, subjectTemplates...) + return nil +} + func TestEvalLoopBehavior(t *testing.T) { now := time.Now().UTC() - t.Run("continues on check execution errors and submits evidence", func(t *testing.T) { + t.Run("returns failure when a check execution fails but still submits successful evidence", func(t *testing.T) { executor := &fakeExecutor{results: map[string]CustodianExecutionResult{ + "inventory-aws-s3": { + StartedAt: now, + EndedAt: now.Add(5 * time.Millisecond), + ExitCode: 0, + Resources: []interface{}{ + map[string]interface{}{"id": "1"}, + map[string]interface{}{"id": "2"}, + }, + }, + "inventory-aws-ec2": { + StartedAt: now, + EndedAt: now.Add(5 * time.Millisecond), + ExitCode: 0, + Resources: []interface{}{map[string]interface{}{"id": "ec2-1"}}, + }, "check-a": { StartedAt: now, EndedAt: now.Add(20 * time.Millisecond), @@ -554,14 +550,14 @@ func TestEvalLoopBehavior(t *testing.T) { } resp, err := plugin.Eval(&proto.EvalRequest{PolicyPaths: []string{"bundle-a", "bundle-b"}}, apiHelper) - if err != nil { - t.Fatalf("unexpected eval error: %v", err) + if err == nil { + t.Fatalf("expected eval failure when one check execution fails") } - if resp.GetStatus() != proto.ExecutionStatus_SUCCESS { - t.Fatalf("expected success status, got %s", resp.GetStatus().String()) + if resp.GetStatus() != proto.ExecutionStatus_FAILURE { + t.Fatalf("expected failure status, got %s", resp.GetStatus().String()) } - if len(executor.calls) != 2 { - t.Fatalf("expected 2 executor calls, got %d", len(executor.calls)) + if len(executor.calls) != 4 { + t.Fatalf("expected 4 executor calls, got %d", len(executor.calls)) } if len(evaluator.calls) != 4 { t.Fatalf("expected 4 evaluator calls, got %d", len(evaluator.calls)) @@ -573,20 +569,29 @@ func TestEvalLoopBehavior(t *testing.T) { t.Fatalf("expected 4 evidences, got %d", len(apiHelper.evidence)) } - hasErrorStatusPayload := false + hasCompliantPayload := false + hasNonCompliantPayload := false for _, call := range evaluator.calls { - if strings.Contains(call, "check-b|") && strings.HasSuffix(call, "|error") { - hasErrorStatusPayload = true - break + if strings.Contains(call, "check-a|1|") && strings.HasSuffix(call, "|non_compliant") { + hasNonCompliantPayload = true + } + if strings.Contains(call, "check-a|2|") && strings.HasSuffix(call, "|compliant") { + hasCompliantPayload = true } } - if !hasErrorStatusPayload { - t.Fatalf("expected check-b payload to carry error execution status") + if !hasCompliantPayload || !hasNonCompliantPayload { + t.Fatalf("expected resource payloads to include compliant and non_compliant statuses, got %v", evaluator.calls) } }) t.Run("fails when all policy evaluations fail", func(t *testing.T) { executor := &fakeExecutor{results: map[string]CustodianExecutionResult{ + "inventory-aws-s3": { + StartedAt: now, + EndedAt: now, + ExitCode: 0, + Resources: []interface{}{map[string]interface{}{"id": "1"}}, + }, "check-a": { StartedAt: now, EndedAt: now, @@ -625,6 +630,12 @@ func TestEvalLoopBehavior(t *testing.T) { t.Run("preserves user provider label and adds source labels", func(t *testing.T) { executor := &fakeExecutor{results: map[string]CustodianExecutionResult{ + "inventory-aws-s3": { + StartedAt: now, + EndedAt: now, + ExitCode: 0, + Resources: []interface{}{map[string]interface{}{"id": "1"}}, + }, "check-a": { StartedAt: now, EndedAt: now, @@ -639,7 +650,14 @@ func TestEvalLoopBehavior(t *testing.T) { plugin := &CloudCustodianPlugin{ Logger: hclog.NewNullLogger(), parsedConfig: &ParsedConfig{ - PolicyLabels: map[string]string{"provider": "custom-provider", "team": "platform"}, + PolicyLabels: map[string]string{ + "provider": "custom-provider", + "team": "platform", + "resource_id": "must-not-leak", + "assessment": "must-not-leak", + "assessment_status": "must-not-leak", + "check_provider": "must-not-leak", + }, CheckTimeout: 30 * time.Second, }, checks: []CustodianCheck{ @@ -670,12 +688,140 @@ func TestEvalLoopBehavior(t *testing.T) { if labels["tool"] != sourceCloudCustodian { t.Fatalf("expected tool label, got: %s", labels["tool"]) } - if labels["check_provider"] != "aws" { - t.Fatalf("expected check_provider label to be aws, got: %s", labels["check_provider"]) + if _, ok := labels["check_provider"]; ok { + t.Fatalf("expected check_provider label to be removed, got labels: %v", labels) + } + if _, ok := labels["assessment_status"]; ok { + t.Fatalf("expected assessment_status label to be removed, got labels: %v", labels) + } + if _, ok := labels["assessment"]; ok { + t.Fatalf("expected assessment label to be removed, got labels: %v", labels) + } + if labels["resource_id"] != "1" { + t.Fatalf("expected resource_id label to be set, got: %s", labels["resource_id"]) + } + if len(apiHelper.evidence) == 0 { + t.Fatalf("expected submitted evidence") + } + evidenceLabels := apiHelper.evidence[0].Labels + if evidenceLabels["resource_id"] != "1" { + t.Fatalf("expected submitted evidence resource_id label to be set, got: %s", evidenceLabels["resource_id"]) + } + if len(evaluator.subjectsSeen) == 0 || len(evaluator.subjectsSeen[0]) == 0 { + t.Fatalf("expected evaluator to capture subjects") + } + resourceSubject := evaluator.subjectsSeen[0][0] + expectedResourceSubjectID := "cloud-custodian-resource/aws.s3/1" + if resourceSubject.Identifier != expectedResourceSubjectID { + t.Fatalf("expected escaped resource subject identifier %q, got %q", expectedResourceSubjectID, resourceSubject.Identifier) + } + if len(resourceSubject.Links) != 1 || resourceSubject.Links[0].Href != "1" { + t.Fatalf("expected resource subject link to contain resource id, got %#v", resourceSubject.Links) + } + if len(resourceSubject.Props) != 1 || resourceSubject.Props[0].Name != "resource_id" || resourceSubject.Props[0].Value != "1" { + t.Fatalf("expected resource_id subject prop, got %#v", resourceSubject.Props) + } + }) + + t.Run("flushes evidence in bounded batches", func(t *testing.T) { + baselineResources := make([]interface{}, 0, evidenceBatchSize+1) + for i := 0; i < evidenceBatchSize+1; i++ { + baselineResources = append(baselineResources, map[string]interface{}{ + "id": fmt.Sprintf("resource-%03d", i), + }) + } + + executor := &fakeExecutor{results: map[string]CustodianExecutionResult{ + "inventory-aws-s3": { + StartedAt: now, + EndedAt: now, + ExitCode: 0, + Resources: baselineResources, + }, + "check-a": { + StartedAt: now, + EndedAt: now, + ExitCode: 0, + Resources: []interface{}{}, + }, + }} + + evaluator := &fakePolicyEvaluator{} + apiHelper := &fakeAPIHelper{} + + plugin := &CloudCustodianPlugin{ + Logger: hclog.NewNullLogger(), + parsedConfig: &ParsedConfig{ + PolicyLabels: map[string]string{}, + CheckTimeout: 30 * time.Second, + }, + checks: []CustodianCheck{ + {Index: 0, Name: "check-a", Resource: "aws.s3", Provider: "aws", RawPolicy: map[string]interface{}{"name": "check-a", "resource": "aws.s3"}}, + }, + executor: executor, + evaluator: evaluator, + } + + resp, err := plugin.Eval(&proto.EvalRequest{PolicyPaths: []string{"bundle-a"}}, apiHelper) + if err != nil { + t.Fatalf("unexpected eval error: %v", err) + } + if resp.GetStatus() != proto.ExecutionStatus_SUCCESS { + t.Fatalf("expected success status, got %s", resp.GetStatus().String()) + } + if apiHelper.calls != 2 { + t.Fatalf("expected CreateEvidence twice for batched submission, got %d", apiHelper.calls) + } + if len(apiHelper.evidence) != evidenceBatchSize+1 { + t.Fatalf("expected %d evidences total, got %d", evidenceBatchSize+1, len(apiHelper.evidence)) } }) } +func TestEvalFailsWhenInventoryBaselineErrors(t *testing.T) { + now := time.Now().UTC() + executor := &fakeExecutor{results: map[string]CustodianExecutionResult{ + "inventory-aws-s3": { + StartedAt: now, + EndedAt: now.Add(5 * time.Millisecond), + ExitCode: 1, + Error: "inventory execution failed", + Err: errors.New("inventory execution failed"), + Resources: []interface{}{}, + }, + "check-a": { + StartedAt: now, + EndedAt: now.Add(20 * time.Millisecond), + ExitCode: 0, + Resources: []interface{}{map[string]interface{}{"id": "1"}}, + }, + }} + + apiHelper := &fakeAPIHelper{} + plugin := &CloudCustodianPlugin{ + Logger: hclog.NewNullLogger(), + parsedConfig: &ParsedConfig{ + CheckTimeout: 30 * time.Second, + }, + checks: []CustodianCheck{ + {Index: 0, Name: "check-a", Resource: "aws.s3", Provider: "aws", RawPolicy: map[string]interface{}{"name": "check-a", "resource": "aws.s3"}}, + }, + executor: executor, + evaluator: &fakePolicyEvaluator{}, + } + + resp, err := plugin.Eval(&proto.EvalRequest{PolicyPaths: []string{"bundle-a"}}, apiHelper) + if err == nil { + t.Fatal("expected eval failure when inventory baseline errors") + } + if resp.GetStatus() != proto.ExecutionStatus_FAILURE { + t.Fatalf("expected failure status, got %s", resp.GetStatus().String()) + } + if apiHelper.calls != 0 { + t.Fatalf("expected no evidence submitted when baseline unavailable, got %d calls", apiHelper.calls) + } +} + func TestConfigureLoadsChecks(t *testing.T) { stubLookPath(t, func(binary string) (string, error) { return "/usr/local/bin/" + binary, nil @@ -702,6 +848,261 @@ func TestConfigureLoadsChecks(t *testing.T) { } } +func TestBuildResourceRecordCanonicalizesHostedZoneARN(t *testing.T) { + plugin := &CloudCustodianPlugin{parsedConfig: &ParsedConfig{}} + record := plugin.buildResourceRecord("aws.hostedzone", map[string]interface{}{ + "Id": "/hostedzone/Z0819711ZIJQWWE99PT", + "Name": "example.com.", + }) + + expected := "arn:aws:route53:::hostedzone/Z0819711ZIJQWWE99PT" + if record.ID != expected { + t.Fatalf("expected hosted zone arn %q, got %q", expected, record.ID) + } + if record.IdentityFields["arn"] != expected { + t.Fatalf("expected identity fields to include synthesized arn, got %#v", record.IdentityFields) + } +} + +func TestBuildResourcePayloadsForCheckDisambiguatesDuplicateResourceIDs(t *testing.T) { + plugin := &CloudCustodianPlugin{ + Logger: hclog.NewNullLogger(), + parsedConfig: &ParsedConfig{ + ResourceIdentityFields: map[string][]string{ + "aws.s3": {"Name"}, + }, + }, + } + + resources := []interface{}{ + map[string]interface{}{ + "Name": "shared-name", + "Arn": "arn:aws:s3:::baseline-one", + }, + map[string]interface{}{ + "Name": "shared-name", + "Arn": "arn:aws:s3:::baseline-two", + }, + } + baselineRecords := make([]ResourceRecord, 0, len(resources)) + for _, resource := range resources { + baselineRecords = append(baselineRecords, plugin.buildResourceRecord("aws.s3", resource)) + } + baseline := &InventoryBaseline{ + ResourceType: "aws.s3", + Provider: "aws", + Records: baselineRecords, + } + + execution := CustodianExecutionResult{ + Resources: resources, + } + + payloads := plugin.buildResourcePayloadsForCheck( + CustodianCheck{Name: "duplicate-id-check", Resource: "aws.s3", Provider: "aws"}, + execution, + baseline, + ) + if len(payloads) != 2 { + t.Fatalf("expected two payloads for duplicate resource IDs, got %d", len(payloads)) + } + seenResourceIDs := make(map[string]struct{}, len(payloads)) + for _, payload := range payloads { + if payload.Assessment.Status != "non_compliant" { + t.Fatalf("expected duplicate matched resources to remain non_compliant, got %s", payload.Assessment.Status) + } + if payload.Resource.ID == "" { + t.Fatalf("expected disambiguated resource ID to be non-empty, got empty ID in payload %#v", payload) + } + seenResourceIDs[payload.Resource.ID] = struct{}{} + } + if len(seenResourceIDs) != 2 { + t.Fatalf("expected duplicate matched resources to have distinct resource IDs, got IDs %#v", seenResourceIDs) + } +} + +func TestResourceStringAtPath(t *testing.T) { + tests := []struct { + name string + resource interface{} + path string + want string + ok bool + }{ + { + name: "dotted path through nested string map", + resource: map[string]interface{}{ + "metadata": map[string]interface{}{ + "uid": "abc-123", + }, + }, + path: "metadata.uid", + want: "abc-123", + ok: true, + }, + { + name: "dotted path through nested interface map", + resource: map[interface{}]interface{}{ + "metadata": map[interface{}]interface{}{ + "uid": "xyz-789", + }, + }, + path: "metadata.uid", + want: "xyz-789", + ok: true, + }, + { + name: "json number conversion", + resource: map[string]interface{}{ + "id": json.Number("42"), + }, + path: "id", + want: "42", + ok: true, + }, + { + name: "float64 conversion", + resource: map[string]interface{}{ + "id": 42.5, + }, + path: "id", + want: "42.5", + ok: true, + }, + { + name: "float32 conversion", + resource: map[string]interface{}{ + "id": float32(7.25), + }, + path: "id", + want: "7.25", + ok: true, + }, + { + name: "int conversion", + resource: map[string]interface{}{ + "id": 17, + }, + path: "id", + want: "17", + ok: true, + }, + { + name: "int64 conversion", + resource: map[string]interface{}{ + "id": int64(9001), + }, + path: "id", + want: "9001", + ok: true, + }, + { + name: "bool conversion", + resource: map[string]interface{}{ + "id": true, + }, + path: "id", + want: "true", + ok: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, ok := resourceStringAtPath(tt.resource, tt.path) + if ok != tt.ok { + t.Fatalf("expected ok=%t, got %t", tt.ok, ok) + } + if got != tt.want { + t.Fatalf("expected value %q, got %q", tt.want, got) + } + }) + } +} + +func TestHashResourceUsesFullSHA256Digest(t *testing.T) { + got := hashResource(map[string]interface{}{"id": "example", "name": "bucket"}) + if len(got) != 64 { + t.Fatalf("expected full sha256 hex digest length 64, got %d (%q)", len(got), got) + } +} + +func TestFormatExecutionFailure(t *testing.T) { + t.Run("uses both error string and wrapped error", func(t *testing.T) { + err := formatExecutionFailure("check-a", CustodianExecutionResult{ + Error: "dryrun failed", + Err: errors.New("exit status 1"), + }) + if !strings.Contains(err.Error(), "dryrun failed") { + t.Fatalf("expected formatted error to include execution.Error, got %v", err) + } + if !strings.Contains(err.Error(), "exit status 1") { + t.Fatalf("expected formatted error to include execution.Err, got %v", err) + } + }) + + t.Run("uses wrapped error when error string is empty", func(t *testing.T) { + err := formatExecutionFailure("check-a", CustodianExecutionResult{ + Err: errors.New("exit status 1"), + }) + if !strings.Contains(err.Error(), "exit status 1") { + t.Fatalf("expected formatted error to include execution.Err, got %v", err) + } + }) +} + +func TestInitUpsertsSubjectAndRiskTemplates(t *testing.T) { + policyDir := t.TempDir() + rego := `package compliance_framework.cloud_custodian_test + +risk_templates := [{ + "name": "public_bucket", + "title": "Public bucket", + "statement": "A bucket is publicly accessible.", + "likelihood_hint": "medium", + "impact_hint": "high", + "violation_ids": ["cloud.public_bucket"], +}] +` + if err := os.WriteFile(filepath.Join(policyDir, "risk.rego"), []byte(rego), 0o600); err != nil { + t.Fatalf("failed to write risk policy: %v", err) + } + + apiHelper := &fakeAPIHelper{} + plugin := &CloudCustodianPlugin{ + Logger: hclog.NewNullLogger(), + parsedConfig: &ParsedConfig{}, + checks: []CustodianCheck{ + {Index: 0, Name: "s3-check", Resource: "aws.s3", Provider: "aws", RawPolicy: map[string]interface{}{"name": "s3-check", "resource": "aws.s3"}}, + {Index: 1, Name: "s3-check-2", Resource: "aws.s3", Provider: "aws", RawPolicy: map[string]interface{}{"name": "s3-check-2", "resource": "aws.s3"}}, + {Index: 2, Name: "ec2-check", Resource: "aws.ec2", Provider: "aws", RawPolicy: map[string]interface{}{"name": "ec2-check", "resource": "aws.ec2"}}, + }, + } + + resp, err := plugin.Init(&proto.InitRequest{PolicyPaths: []string{policyDir}}, apiHelper) + if err != nil { + t.Fatalf("unexpected init error: %v", err) + } + if resp == nil { + t.Fatalf("expected init response") + } + if len(apiHelper.subjectTemplates) != 2 { + t.Fatalf("expected two unique subject templates, got %d", len(apiHelper.subjectTemplates)) + } + if apiHelper.subjectTemplates[0].GetType() != proto.SubjectType_SUBJECT_TYPE_RESOURCE { + t.Fatalf("expected resource subject template type") + } + if got := apiHelper.subjectTemplates[0].GetIdentityLabelKeys(); len(got) != 3 || got[2] != "resource_id" { + t.Fatalf("expected resource_id identity label, got %v", got) + } + if apiHelper.riskTemplateCalls != 1 { + t.Fatalf("expected one risk template upsert, got %d", apiHelper.riskTemplateCalls) + } + if len(apiHelper.riskTemplatePackages) != 1 || apiHelper.riskTemplatePackages[0] != "compliance_framework.cloud_custodian_test" { + t.Fatalf("unexpected risk template packages: %v", apiHelper.riskTemplatePackages) + } +} + func TestDumpStandardizedPayload(t *testing.T) { plugin := &CloudCustodianPlugin{ Logger: hclog.NewNullLogger(), @@ -711,8 +1112,8 @@ func TestDumpStandardizedPayload(t *testing.T) { }, } - err := plugin.dumpStandardizedPayload(&StandardizedCheckPayload{ - SchemaVersion: "v1", + err := plugin.dumpStandardizedPayload(&StandardizedResourcePayload{ + SchemaVersion: "v2", Source: "cloud-custodian", Check: StandardizedCheckInfo{ Name: "check-a", @@ -724,9 +1125,15 @@ func TestDumpStandardizedPayload(t *testing.T) { Status: "success", DryRun: true, }, - Result: StandardizedCheckResult{ - MatchedResourceCount: 0, - Resources: []interface{}{}, + Resource: StandardizedResourceInfo{ + ID: "resource-1", + Type: "aws.s3", + Provider: "aws", + Data: map[string]interface{}{"id": "resource-1"}, + }, + Assessment: StandardizedAssessment{ + Status: "compliant", + InventoryStatus: "baseline", }, RawPolicy: map[string]interface{}{"name": "check-a", "resource": "aws.s3"}, }) @@ -745,7 +1152,7 @@ func TestDumpStandardizedPayload(t *testing.T) { if err != nil { t.Fatalf("failed to read dumped payload file: %v", err) } - if !strings.Contains(string(content), "\"schema_version\": \"v1\"") { + if !strings.Contains(string(content), "\"schema_version\": \"v2\"") { t.Fatalf("dumped payload file content does not look like standardized payload json") } }