feat: update to recent v2 support + simplify#14
Conversation
📝 WalkthroughWalkthroughRefactors the plugin to use a typed PluginConfig, changes policy evaluation to produce Evidence via a PolicyProcessor, updates plugin lifecycle to store logger and policyData, adds a Makefile for developer workflows, updates module dependencies to Go 1.26.1, annotates GoReleaser uploads, and expands README usage examples. ChangesPlugin Configuration and Evaluation
Developer Workflow and Build Infrastructure
Sequence DiagramsequenceDiagram
participant Request as EvalRequest
participant Fetcher as DataFetcher
participant Evaluator as PolicyEvaluator
participant Processor as PolicyProcessor
participant Helper as ApiHelper
Request->>Fetcher: FetchData()
Fetcher-->>Request: map[string]any
Request->>Evaluator: Eval(ctx, input, policyPaths, policyData, labels)
Evaluator->>Processor: NewPolicyProcessor(merged labels, activities, policyData)
Processor->>Processor: GenerateResults per policyPath
Processor-->>Evaluator: []*Evidence, error
Request->>Helper: CreateEvidence(evidence)
Helper-->>Request: EvalResponse with ExecutionStatus_SUCCESS
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
| // Eval is used to run policies against the data you've collected. You could also consider an | ||
| // `EvalAndSend` by passing in the `apiHelper` that sends the observations directly to the API. | ||
| func (pe *PolicyEvaluator) Eval(data map[string]interface{}, policyPaths []string) (proto.ExecutionStatus, error) { | ||
| func (pe *PolicyEvaluator) Eval(ctx context.Context, input map[string]interface{}, policyPaths []string, policyData map[string]interface{}, labels map[string]string) ([]*proto.Evidence, error) { |
There was a problem hiding this comment.
We could use this example to also fill in the labels from the PolicyLabels.. currently this is ignored (nil argument)
| evalStatus, err := policyEvaluator.Eval(data, request.PolicyPaths) | ||
| policyEvaluator := internal.NewPolicyEvaluator(ctx, l.logger, activities) | ||
|
|
||
| evidences, err := policyEvaluator.Eval(ctx, data, request.PolicyPaths, l.policyData, nil) |
| func (l *CompliancePlugin) Init(req *proto.InitRequest, apiHelper runner.ApiHelper) (*proto.InitResponse, error) { | ||
| ctx := context.Background() | ||
| subjectTemplates := []*proto.SubjectTemplate{ | ||
| { | ||
| Name: "ec2-instance", | ||
| Type: proto.SubjectType_SUBJECT_TYPE_COMPONENT, | ||
| TitleTemplate: "EC2 Instance {{ .resource_id }} in {{ .account_id }}/{{ .region }}", | ||
| DescriptionTemplate: "AWS EC2 Instance {{ .resource_id }}.", | ||
| PurposeTemplate: "Represents an AWS EC2 instance evaluated for compliance posture.", | ||
| IdentityLabelKeys: []string{"account_id", "region", "resource_id"}, | ||
| LabelSchema: []*proto.SubjectLabelSchema{ | ||
| {Key: "account_id", Description: "AWS account ID"}, | ||
| {Key: "region", Description: "AWS region"}, | ||
| {Key: "resource_id", Description: "EC2 Instance ID"}, | ||
| {Key: "resource_arn", Description: "AWS resource ARN"}, | ||
| {Key: "resource_type", Description: "EC2 normalized resource type"}, | ||
| }, | ||
| }, | ||
| } | ||
| return runner.InitWithSubjectsAndRisksFromPolicies(ctx, l.logger, req, apiHelper, subjectTemplates) | ||
| } | ||
|
|
There was a problem hiding this comment.
Worth adding a docstring here explaining how to build these Init methods - i.e. Labels used on templates must be added to LabelSchema, _plugin is automatically added to the Schema + as an Identity Label Key etc; to use templated keys they need to be within the schema, and that TYPE MUST BE COMPONENT FOR AUTOMATION TO TRIGGER. DONT USE OTHER TYPES AS OF TODAY (this happened ** SO MANY TIMES ** when using agents)
| plugins: | ||
| myplugin: | ||
| config: | ||
| anykey: "anyval" | ||
| protocol_version: 2 | ||
| source: /path/to/dist/plugin | ||
| policies: | ||
| - /path/to/policy/bundle.tar.gz |
There was a problem hiding this comment.
add a policy_data / policy_behavior example here would also be useful.
There was a problem hiding this comment.
Actionable comments posted: 7
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
internal/eval.go (1)
32-59:⚠️ Potential issue | 🟠 Major | 🏗️ Heavy liftThe activity pipeline is empty, so evidence loses its execution trail.
processoronly receivespe.stepActivities, but this method never attaches the per-policystepsit builds, andmain.goseedsstepActivitieswith an empty slice. The fetch steps built ininternal/data.goalso have no path intoPolicyProcessor, so generated evidence carries no fetch/compile/execute provenance.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/eval.go` around lines 32 - 59, The PolicyProcessor is created with only pe.stepActivities so the per-policy `steps` (and the fetch steps produced in internal/data.go) never get recorded; before calling policyManager.NewPolicyProcessor in internal/eval.go, append the local `steps` slice (and any fetch-step slices produced earlier) into the `activities` slice so the processor receives the full execution trail (e.g., merge `steps` into `activities` prior to calling policyManager.NewPolicyProcessor), then call `processor.GenerateResults(ctx, policyPath, input)` as-is.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/build-and-upload.yml:
- Line 25: The workflow run command embeds unsanitized GitHub context variables
(github.repository_owner, github.event.repository.name, github.ref_name) into
the shell invocation which can lead to template/shell injection; fix by moving
those contexts into environment variables and using the env names in the run
step (or validate/sanitize them before use) so the invocation uses safe values
(e.g., set REPO_OWNER, REPO_NAME, REF_NAME from the contexts and reference those
in the gooci upload command), or add an input-validation step that rejects
unexpected characters in github.ref_name before running the upload.
- Line 25: The --annotate argument in the gooci upload commands has an
unterminated string; update both occurrences of the gooci upload invocation so
the annotation value is properly quoted (e.g. close the opening double quote
after the annotation like --annotate="org.ccf.plugin.protocol.version=2")
ensuring each gooci upload command’s --annotate parameter ends with a matching
closing quote.
In `@go.mod`:
- Line 18: Update the transitive Docker dependency by bumping the testcontainers
library: in go.mod replace or update the
github.com/testcontainers/testcontainers-go version to a newer release that
pulls Moby/docker >= 29.3.1 (so github.com/docker/docker transitive
vulnerabilities are patched), then run go mod tidy to refresh the module graph
and vendor (if used); verify via `go list -m all` or `go mod graph` that
github.com/docker/docker now resolves to a patched version and CI uses trusted
images for Docker-backed tests.
In `@internal/eval.go`:
- Around line 47-50: The code is merging hardcoded placeholder labels into every
evidence record via MergeMaps(labels, map[string]string{ "provider":
"my_plugin", "additional_label": "value", }), so remove or replace that
hardcoded map; update the call in the MergeMaps usage (where labels is combined)
to either merge only real runtime labels or omit the second map entirely, or
derive values from actual config/vars (e.g., provider and additional_label from
settings) before calling MergeMaps to avoid stamping placeholder metadata on
emitted evidence.
In `@main.go`:
- Around line 57-62: The parsed config's PolicyLabels are being stored
(l.config.PolicyLabels) but never passed to the policy evaluator so
user-provided policy_labels are ignored; update both Configure/Eval call sites
to pass the persisted labels into policyEvaluator.Eval by replacing the nil
labels argument with l.config.PolicyLabels (or the local
parsedConfig.PolicyLabels where appropriate) so Eval uses the parsed
policy_labels from internal.ParseConfig/rawConfig; ensure you reference
policyEvaluator.Eval, Configure, Eval, rawConfig, parsedConfig,
internal.ParseConfig and l.config.PolicyLabels when making the change.
In `@Makefile`:
- Around line 26-34: Add a .PHONY declaration for non-file targets to prevent
Make from treating them as actual files: declare .PHONY with the list of targets
(help, help-all, test, clean, build, run) near the top of the Makefile (e.g.,
after the OPA check), so targets like test, clean, build, run are always
executed regardless of same-named files.
- Around line 36-37: The run target currently uses a hardcoded agent path
"../agent/dist/./concom"; make this configurable by introducing a Makefile
variable (e.g., AGENT_BIN or AGENT_PATH) with a sensible default and replace the
literal path in the run recipe with that variable, and also add an existence
check for $(AGENT_BIN) before executing (print an error and exit non‑zero if
missing) so the target is portable and fails fast when the binary is not found.
---
Outside diff comments:
In `@internal/eval.go`:
- Around line 32-59: The PolicyProcessor is created with only pe.stepActivities
so the per-policy `steps` (and the fetch steps produced in internal/data.go)
never get recorded; before calling policyManager.NewPolicyProcessor in
internal/eval.go, append the local `steps` slice (and any fetch-step slices
produced earlier) into the `activities` slice so the processor receives the full
execution trail (e.g., merge `steps` into `activities` prior to calling
policyManager.NewPolicyProcessor), then call `processor.GenerateResults(ctx,
policyPath, input)` as-is.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 5eefabca-2b4d-4a6a-92e1-481c0c69fe8d
⛔ Files ignored due to path filters (1)
go.sumis excluded by!**/*.sum
📒 Files selected for processing (9)
.github/workflows/build-and-upload.ymlMakefileREADME.mdgo.modinternal/config.gointernal/data.gointernal/eval.gointernal/util.gomain.go
| run: gooci login ghcr.io --username ${{ github.actor }} --password ${{ secrets.GITHUB_TOKEN }} | ||
| - name: gooci Upload Version | ||
| run: gooci upload dist/ ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}:${{github.ref_name}} | ||
| run: gooci upload dist/ ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}:${{github.ref_name}} --annotate="org.ccf.plugin.protocol.version=2 |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial | ⚖️ Poor tradeoff
Potential template injection via GitHub context variables.
The workflow uses unsanitized GitHub context variables (github.repository_owner, github.event.repository.name, github.ref_name) in shell commands. In certain scenarios (e.g., workflow running from a fork or with crafted repository names), these could inject arbitrary commands.
For a plugin template used in a trusted context, this risk may be acceptable. However, if this workflow will run on pull requests from forks or in untrusted environments, consider sanitizing the inputs or using environment variables instead.
🛡️ Defensive alternatives
Option 1: Use environment variables
- name: gooci Upload Version
env:
REPO_OWNER: ${{ github.repository_owner }}
REPO_NAME: ${{ github.event.repository.name }}
REF_NAME: ${{ github.ref_name }}
run: gooci upload dist/ "ghcr.io/${REPO_OWNER}/${REPO_NAME}:${REF_NAME}" --annotate="org.ccf.plugin.protocol.version=2"Option 2: Add input validation
- name: Validate inputs
run: |
if [[ ! "${{ github.ref_name }}" =~ ^[a-zA-Z0-9._-]+$ ]]; then
echo "Invalid ref name"
exit 1
fiAlso applies to: 28-28
🧰 Tools
🪛 zizmor (1.25.2)
[warning] 25-25: code injection via template expansion (template-injection): may expand into attacker-controllable code
(template-injection)
[error] 25-25: code injection via template expansion (template-injection): may expand into attacker-controllable code
(template-injection)
[error] 25-25: code injection via template expansion (template-injection): may expand into attacker-controllable code
(template-injection)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/build-and-upload.yml at line 25, The workflow run command
embeds unsanitized GitHub context variables (github.repository_owner,
github.event.repository.name, github.ref_name) into the shell invocation which
can lead to template/shell injection; fix by moving those contexts into
environment variables and using the env names in the run step (or
validate/sanitize them before use) so the invocation uses safe values (e.g., set
REPO_OWNER, REPO_NAME, REF_NAME from the contexts and reference those in the
gooci upload command), or add an input-validation step that rejects unexpected
characters in github.ref_name before running the upload.
Missing closing quote in annotation argument.
Both gooci upload commands have unterminated strings in the --annotate parameter. The opening quote for the annotation value is never closed, which will cause shell syntax errors.
🐛 Proposed fix
- name: gooci Upload Version
- run: gooci upload dist/ ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}:${{github.ref_name}} --annotate="org.ccf.plugin.protocol.version=2
+ run: gooci upload dist/ ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}:${{github.ref_name}} --annotate="org.ccf.plugin.protocol.version=2"
- name: gooci Upload Latest
if: "!github.event.release.prerelease"
- run: gooci upload dist/ ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}:latest --annotate="org.ccf.plugin.protocol.version=2
+ run: gooci upload dist/ ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}:latest --annotate="org.ccf.plugin.protocol.version=2"Also applies to: 28-28
🧰 Tools
🪛 zizmor (1.25.2)
[warning] 25-25: code injection via template expansion (template-injection): may expand into attacker-controllable code
(template-injection)
[error] 25-25: code injection via template expansion (template-injection): may expand into attacker-controllable code
(template-injection)
[error] 25-25: code injection via template expansion (template-injection): may expand into attacker-controllable code
(template-injection)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/build-and-upload.yml at line 25, The --annotate argument
in the gooci upload commands has an unterminated string; update both occurrences
of the gooci upload invocation so the annotation value is properly quoted (e.g.
close the opening double quote after the annotation like
--annotate="org.ccf.plugin.protocol.version=2") ensuring each gooci upload
command’s --annotate parameter ends with a matching closing quote.
| github.com/containerd/errdefs/pkg v0.3.0 // indirect | ||
| github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 // indirect | ||
| github.com/defenseunicorns/go-oscal v0.7.0 // indirect | ||
| github.com/docker/docker v28.5.2+incompatible // indirect |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Check if docker package is actually imported/used in the codebase
echo "=== Checking for direct docker imports ==="
rg -n --type=go 'github\.com/docker/docker' -g '!go.mod' -g '!go.sum'
echo ""
echo "=== Checking which dependency brings in docker ==="
go mod why github.com/docker/docker
echo ""
echo "=== Checking for latest docker version and patches ==="
curl -s https://proxy.golang.org/github.com/docker/docker/@latest | jq -r '.Version'Repository: compliance-framework/plugin-template
Length of output: 9640
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== rg for docker/docker imports (code only) ==="
rg -n --type=go 'github\.com/docker/docker' -g '!go.mod' -g '!go.sum' || true
echo
echo "=== go mod why (module-level) ==="
go mod why -m github.com/docker/docker || true
echo
echo "=== go list deps (including tests) that reference docker/docker ==="
go list -deps -test ./... 2>/dev/null | rg -n 'github\.com/docker/docker' || true
echo
echo "=== go mod graph edges that mention docker/docker (filtered) ==="
go mod graph | rg 'github\.com/docker/docker' || trueRepository: compliance-framework/plugin-template
Length of output: 863
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Find any packages in this module that import docker/docker subpackages ==="
rg -n --type=go 'github\.com/docker/docker/(.+)?' -g '!go.mod' -g '!go.sum' || true
echo
echo "=== Identify packages whose resolved module is github.com/docker/docker (deps including tests) ==="
# Uses -json to map packages -> module path, then filters for github.com/docker/docker
go list -deps -test -json ./... 2>/dev/null \
| jq -r 'select(.Module != null and .Module.Path=="github.com/docker/docker") | .ImportPath' \
| sort -u | sed '/^$/d' || true
echo
echo "=== Does testcontainers-go appear in non-test packages? ==="
rg -n --type=go 'github\.com/testcontainers/testcontainers-go' -g '!go.mod' -g '!go.sum' . || trueRepository: compliance-framework/plugin-template
Length of output: 320
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Inspect go list -deps -test -json for packages belonging to github.com/docker/docker ==="
go list -deps -test -json ./... 2>/dev/null \
| rg -n '"Module":\s*{' -n || true
# More direct: print ImportPath values where the JSON shows Module.Path == github.com/docker/docker
go list -deps -test -json ./... 2>/dev/null \
| python3 - <<'PY'
import sys, json, re
buf=[]
for line in sys.stdin:
buf.append(line)
if line.strip() == '}':
pass
PYRepository: compliance-framework/plugin-template
Length of output: 283
🏁 Script executed:
#!/bin/bash
set -euo pipefail
pkgs=(
"github.com/compliance-framework/agent/runner"
"github.com/compliance-framework/api/sdk"
"github.com/compliance-framework/api/sdk.test"
)
for p in "${pkgs[@]}"; do
echo "===== $p ====="
dir="$(go list -f '{{.Dir}}' "$p" 2>/dev/null || true)"
echo "Dir: ${dir:-<unknown>}"
if [ -n "$dir" ] && [ "$dir" != "<unknown>" ]; then
rg -n --hidden --no-ignore -S 'github\.com/docker/docker' "$dir" || true
rg -n --hidden --no-ignore -S 'docker\.go' "$dir" || true
fi
echo
done
echo "===== Any docker/docker imports across this module (all .go files) ====="
rg -n --type=go 'github\.com/docker/docker' . -g '!go.mod' -g '!go.sum' || trueRepository: compliance-framework/plugin-template
Length of output: 499
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== docker/docker packages in NON-TEST deps (import graph) ==="
go list -deps -json ./... 2>/dev/null \
| jq -r 'select(.Module != null and .Module.Path=="github.com/docker/docker") | .ImportPath' \
| sort -u || true
echo
echo "=== docker/docker packages in TEST deps (import graph) ==="
go list -deps -test -json ./... 2>/dev/null \
| jq -r 'select(.Module != null and .Module.Path=="github.com/docker/docker") | .ImportPath' \
| sort -u || true
echo
echo "=== modules requiring docker/docker in NON-TEST build ==="
go mod why -m github.com/docker/docker 2>/dev/null || trueRepository: compliance-framework/plugin-template
Length of output: 546
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== rg for testcontainers-go usage (excluding vendor) ==="
rg -n --hidden --no-ignore -S 'testcontainers[-.]go' . -g '!go.mod' -g '!go.sum' || true
echo
echo "=== rg for docker client imports in repo (excluding go.mod/sum) ==="
rg -n --hidden --no-ignore -S 'github\.com/docker/docker' . -g '!go.mod' -g '!go.sum' || true
echo
echo "=== List any _test.go files that reference testcontainers ==="
rg -n --hidden --no-ignore -S 'testcontainers[-.]go' . -g '*_test.go' || trueRepository: compliance-framework/plugin-template
Length of output: 270
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Importers of github.com/docker/docker (test vs non-test) ==="
for mode in "" "-test"; do
echo "--- mode: ${mode:-non-test} ---"
go list $mode -deps -json ./... 2>/dev/null \
| jq -r '
select(.Module and (.Module.Path=="github.com/docker/docker"))
| .ImportPath
' | sort -u | head -n 200
done
echo
echo "=== Importers that pull in github.com/testcontainers/testcontainers-go (test vs non-test) ==="
for mode in "" "-test"; do
echo "--- mode: ${mode:-non-test} ---"
go list $mode -deps -json ./... 2>/dev/null \
| jq -r '
select(.Module and (.Module.Path=="github.com/testcontainers/testcontainers-go"))
| .ImportPath
' | sort -u | head -n 200
done
echo
echo "=== Raw module paths present for deps mentioning docker/docker (sanity) ==="
go list -deps -test -json ./... 2>/dev/null | jq -r '
select(.Module and (.Module.Path|contains("docker/docker")))
| (.Module.Path + " " + (.Version // ""))
' | sort -u | head -n 50Repository: compliance-framework/plugin-template
Length of output: 403
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect dependency module sources (not this repo) for docker/testcontainers usage
mods=(
"/home/jailuser/go/pkg/mod/github.com/compliance-framework/agent@v0.7.0"
"/home/jailuser/go/pkg/mod/github.com/compliance-framework/api@v0.16.0"
)
for d in "${mods[@]}"; do
echo "===== Searching in $d for docker/docker ====="
test -d "$d" && rg -n --hidden --no-ignore -S 'github\.com/docker/docker' "$d" || echo "missing"
echo
echo "===== Searching in $d for testcontainers-go ====="
test -d "$d" && rg -n --hidden --no-ignore -S 'github\.com/testcontainers/testcontainers-go' "$d" || echo "missing"
echo
echo "===== Searching in $d for docker client usage indicators ====="
test -d "$d" && rg -n --hidden --no-ignore -S 'docker\.NewClient|client\.NewClient|dockertest|testcontainers' "$d" || echo "missing"
echo "----------------------------------------"
doneRepository: compliance-framework/plugin-template
Length of output: 7955
🌐 Web query:
GO-2026-4883 Off-by-one error in plugin privilege validation github.com/docker/docker
💡 Result:
GO-2026-4883 refers to an off-by-one error in plugin privilege validation within the Moby project (the core component of Docker), tracked as CVE-2026-33997 and GHSA-pxq6-2prw-chj9 [1][2]. The vulnerability arises from an error in the daemon's privilege comparison logic, specifically within the isEqual() function, where the loop responsible for comparing requested privileges against approved ones incorrectly started at index 1 instead of index 0 [3]. This flaw leads to two security consequences: 1. Privilege Validation Bypass: The first privilege (alphabetically) is never validated, allowing a malicious plugin to request different values for privileges such as "allow-all-devices," which can grant unrestricted read-write access to host devices [3]. 2. Complete Bypass for Single Privileges: Plugins that request exactly one privilege trigger zero iterations of the comparison loop, effectively bypassing validation entirely [3][4]. This issue is patched in Moby version 29.3.1 [4][2]. Users and projects using the github.com/docker/docker module are advised to update their dependencies, although the Docker/Moby project has also moved toward deprecating the github.com/docker/docker module in favor of maintained modules like github.com/moby/moby/client and github.com/moby/moby/api [5].
Citations:
- 1: https://pkg.go.dev/vuln/GO-2026-4883
- 2: https://cvefeed.io/vuln/detail/CVE-2026-33997
- 3: moby/moby@f4d6f25
- 4: https://osv.dev/vulnerability/CVE-2026-33997
- 5: Replace deprecated github.com/docker/docker dependency flagged by govulncheck (GO-2026-4883) and (GO-2026-4887) elastic/beats#50227
🌐 Web query:
GHSA-x86f-5xw2-fm2r Container binary execution on host via PUT /containers/{id}/archive github.com/docker/docker
💡 Result:
GHSA-x86f-5xw2-fm2r (also tracked as CVE-2026-41567) is a security vulnerability in Docker Engine that allows for arbitrary code execution on the host with daemon (root) privileges [1][2][3]. The vulnerability occurs when handling PUT /containers/{id}/archive requests that contain compressed archives (specifically xz or gzip) [1][2]. The Docker daemon incorrectly resolves external system binaries required for decompression from the container's filesystem instead of the host's filesystem [1][3]. A malicious container image containing a trojanized decompression binary can exploit this behavior, allowing the binary to execute with the full privileges of the host daemon when an archive is uploaded to the container [1][2][4]. Key points regarding this vulnerability: - Affected Component: github.com/docker/docker (and related moby/moby components) [1][5][2]. - Attack Vector: Requires a user to run a container from a malicious image and subsequently upload a compressed archive (xz or gzip) into that container [1][2]. - Exploitation Examples: This can be triggered by calling the PUT /containers/{id}/archive API directly or by piping a compressed archive through commands like docker cp - [1][2][4]. - Limitations: Standard docker cp usage is not affected because the CLI sends uncompressed tar by default [4]. Decompression formats using pure Go implementations (such as bzip2, zstd, and gzip when an unpigz binary is absent) are also not affected [1][2]. Recommended mitigations include: - Only running containers from trusted images [1][2]. - Using authorization plugins to limit access to the PUT /containers/{id}/archive endpoint [1][2]. - Avoiding the piping of compressed archives into containers created from untrusted sources [1][2]. As of May 28, 2026, advisories indicate that a fix may still be pending for the primary github.com/docker/docker module path [5].
Citations:
- 1: https://osv.dev/vulnerability/GHSA-x86f-5xw2-fm2r
- 2: GHSA-x86f-5xw2-fm2r
- 3: https://dbugs.ptsecurity.com/vulnerability/PT-2026-41765
- 4: https://scout.docker.com/vulnerabilities/id/CVE-2026-41567/my-images
- 5: https://images.chainguard.dev/security/CGA-x673-v2cr-g46w
🌐 Web query:
GHSA-vp62-88p7-qqf5 Race condition in docker cp (arbitrary file creation) github.com/docker/docker
💡 Result:
GHSA-vp62-88p7-qqf5 (also tracked as CVE-2026-41568) is a security vulnerability in the Docker Engine related to a race condition in the docker cp command [1][2][3][4]. Vulnerability Details The vulnerability exists because of a race condition that occurs when the Docker daemon sets up a temporary filesystem view by bind-mounting volumes while executing a docker cp command (or equivalent API calls like PUT/HEAD /containers/{id}/archive) [1][3]. During this setup process: 1. The daemon resolves the target mount path within the container's root filesystem [1][4]. 2. It then creates the mountpoint (file or directory) if it does not already exist [1][4]. An attacker with a running malicious container can exploit the timing window between these two steps [1][3]. By rapidly creating and swapping path components for symlinks pointing to arbitrary locations on the host, the attacker can cause the daemon to follow these symlinks when it creates the mountpoint [1][4]. Consequently, the daemon creates empty files or directories at arbitrary absolute paths on the host filesystem with root privileges [1][4]. Distinction from Related Vulnerabilities This advisory (GHSA-vp62-88p7-qqf5) specifically addresses the race condition during mountpoint creation [3]. A separate, related vulnerability, tracked as GHSA-rg2x-37c3-w2rh (CVE-2026-42306), involves a different race condition that occurs during the subsequent mount syscall, which allows for bind mount redirection [3][5][6]. Mitigation and Recommendations - Avoid using docker cp with untrusted running containers [1][6]. - Only run containers from trusted images [1][6]. - Utilize authorization plugins to restrict access to the affected archive API endpoints (PUT /containers/{id}/archive and HEAD /containers/{id}/archive) [1][6]. - Ensure your Docker environment is updated to a patched version, as fixes have been implemented to scope mountpoint creation to the container root, preventing escapes even if symlinks are swapped [1].
Citations:
- 1: https://osv.dev/vulnerability/GHSA-vp62-88p7-qqf5
- 2: GHSA-vp62-88p7-qqf5
- 3: https://advisories.gitlab.com/golang/github.com/docker/docker/CVE-2026-41568/
- 4: https://www.resolvedsecurity.com/vulnerability-catalog/CVE-2026-41568
- 5: https://osv.dev/vulnerability/GHSA-rg2x-37c3-w2rh
- 6: https://dbugs.ptsecurity.com/vulnerability/PT-2026-41767
🌐 Web query:
GHSA-rg2x-37c3-w2rh Race condition in docker cp (bind mount redirection) github.com/docker/docker
💡 Result:
GHSA-rg2x-37c3-w2rh is a security advisory describing a race condition in the docker cp command that allows a malicious container to perform bind mount redirection to an arbitrary path on the host filesystem [1][2]. When a user initiates a docker cp operation, the Docker daemon sets up a temporary filesystem view by bind-mounting volumes into a private mount namespace [1]. During this setup process, a mount destination is created inside the container root, and a bind mount is subsequently attached to the container-relative path [1]. The vulnerability arises because there is a race condition between the creation of this mountpoint and the execution of the mount syscall [1]. A process running inside a malicious container can exploit this window by replacing the mount destination (or one of its parent path components) with a symbolic link pointing to an arbitrary location on the host [1]. When the mount syscall is executed, it follows the symlink, resulting in the volume being bind-mounted onto the host path rather than the intended location inside the container [1]. The impact of this exploit depends on the nature of the mount: - If the volume is writable, an attacker could potentially overwrite arbitrary host files at the redirected path with the volume's contents [1]. - If the volume is read-only, the host path is masked by the mount for the duration of the copy operation, which can cause a denial of service on the host [1]. Requirements for exploitation include [1]: - The container must have at least one volume mount. - A process inside the container must be capable of rapidly creating and swapping symlinks at the target mount destination. - An operator must trigger a docker cp command into the affected container (or call the corresponding PUT/HEAD /containers/{id}/archive API endpoints) [1]. Mitigation strategies recommended by the advisory include [1]: - Running only containers from trusted images. - Avoiding the use of docker cp with untrusted or running containers. - Utilizing authorization plugins to restrict access to the archive API endpoints [1]. This vulnerability is distinct from a related issue, tracked as GHSA-vp62-88p7-qqf5, which involves a race condition during mountpoint creation that allows for the creation of arbitrary empty files or directories on the host [3][4].
Citations:
- 1: https://osv.dev/vulnerability/GHSA-rg2x-37c3-w2rh
- 2: GHSA-rg2x-37c3-w2rh
- 3: https://osv.dev/vulnerability/GHSA-vp62-88p7-qqf5
- 4: https://advisories.gitlab.com/golang/github.com/docker/docker/CVE-2026-41568/
🌐 Web query:
GHSA-x744-4wpc-v9h2 AuthZ plugin bypass github.com/docker/docker
💡 Result:
GHSA-x744-4wpc-v9h2 (also tracked as CVE-2026-34040) is a security vulnerability in Moby (the engine powering Docker) that allows an attacker to bypass authorization (AuthZ) plugins [1][2][3]. Under specific circumstances, a specially-crafted API request can cause the Docker daemon to forward the request to an AuthZ plugin without including the request body [1][3]. If an AuthZ plugin relies on introspecting this body to make access control decisions, it may incorrectly allow a request that it otherwise would have denied [1][3]. Key details include: - Impact: Users are only affected if they utilize AuthZ plugins that rely on request body inspection for security decisions [1][3]. - Status: This issue is identified as an incomplete fix for a previous, related vulnerability, CVE-2024-41110 [1][3]. - Affected Versions: Versions of Moby prior to 29.3.1 [4][2][3]. - Resolution: The issue was patched in version 29.3.1 [4][2][3]. - Mitigations: If updating immediately is not possible, users are advised to restrict access to the Docker API to trusted parties following the principle of least privilege and avoid using AuthZ plugins that rely on request body inspection [1][3].
Citations:
- 1: https://osv.dev/vulnerability/GHSA-x744-4wpc-v9h2
- 2: https://nvd.nist.gov/vuln/detail/CVE-2026-34040
- 3: GHSA-x744-4wpc-v9h2
- 4: https://cve.circl.lu/vuln/ghsa-x744-4wpc-v9h2
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Direct imports of github.com/docker/docker (excluding go.mod/go.sum) ==="
rg -n --type=go 'github\.com/docker/docker' -g '!go.mod' -g '!go.sum' . || true
echo
echo "=== docker/docker required for non-test builds? ==="
go list -deps ./... 2>/dev/null | rg -n 'github\.com/docker/docker' || true
echo
echo "=== docker/docker required only for test builds? ==="
go list -deps -test ./... 2>/dev/null | rg -n 'github\.com/docker/docker' || true
echo
echo "=== go mod why ==="
go mod why -m github.com/docker/docker || trueRepository: compliance-framework/plugin-template
Length of output: 568
Transitive github.com/docker/docker vulns are mainly relevant to Docker-backed integration tests/CI; update transitive deps.
go.mod pins github.com/docker/docker v28.5.2+incompatible // indirect (line 18). go mod why shows it’s pulled in via github.com/testcontainers/testcontainers-go used by github.com/compliance-framework/api integration tests (api/sdk.test) and dependency graph from api/sdk/agent/runner, not via direct imports in this repo.
Mapped HIGH advisories for this dependency:
- GO-2026-4883 (patched in Moby 29.3.1)
- GHSA-x86f-5xw2-fm2r (PUT
/containers/{id}/archive/ “docker cp” path) - GHSA-vp62-88p7-qqf5 (docker cp race / mountpoint creation)
- GHSA-rg2x-37c3-w2rh (docker cp bind-mount redirection race)
- GHSA-x744-4wpc-v9h2 (AuthZ plugin bypass; patched in 29.3.1)
Action: bump github.com/testcontainers/testcontainers-go (and run go mod tidy) so the resulting Docker/Moby (github.com/docker/docker) version is updated to a patched release; ensure CI runs only trusted images/containers when exercising these Docker API flows.
🧰 Tools
🪛 OSV Scanner (2.3.8)
[HIGH] 18-18: github.com/docker/docker 28.5.2+incompatible: Moby has an Off-by-one error in its plugin privilege validation in github.com/docker/docker
(GO-2026-4883)
[HIGH] 18-18: github.com/docker/docker 28.5.2+incompatible: Moby has AuthZ plugin bypass when provided oversized request bodies in github.com/docker/docker
(GO-2026-4887)
[HIGH] 18-18: github.com/docker/docker 28.5.2+incompatible: Moby has an Off-by-one error in its plugin privilege validation
[HIGH] 18-18: github.com/docker/docker 28.5.2+incompatible: Docker: Race condition in docker cp allows bind mount redirection to host path
[HIGH] 18-18: github.com/docker/docker 28.5.2+incompatible: Docker: Race condition in docker cp allows creation of arbitrary empty files on the host via symlink swap
[HIGH] 18-18: github.com/docker/docker 28.5.2+incompatible: Moby has AuthZ plugin bypass when provided oversized request bodies
[HIGH] 18-18: github.com/docker/docker 28.5.2+incompatible: Docker: PUT /containers/{id}/archive executes container binary on the host
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@go.mod` at line 18, Update the transitive Docker dependency by bumping the
testcontainers library: in go.mod replace or update the
github.com/testcontainers/testcontainers-go version to a newer release that
pulls Moby/docker >= 29.3.1 (so github.com/docker/docker transitive
vulnerabilities are patched), then run go mod tidy to refresh the module graph
and vendor (if used); verify via `go list -m all` or `go mod graph` that
github.com/docker/docker now resolves to a patched version and CI uses trusted
images for Docker-backed tests.
| MergeMaps(labels, map[string]string{ | ||
| "provider": "my_plugin", | ||
| "additional_label": "value", | ||
| }), |
There was a problem hiding this comment.
Remove the template placeholder labels before emitting evidence.
Hardcoding provider: "my_plugin" and additional_label: "value" will stamp placeholder metadata onto every evidence record.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@internal/eval.go` around lines 47 - 50, The code is merging hardcoded
placeholder labels into every evidence record via MergeMaps(labels,
map[string]string{ "provider": "my_plugin", "additional_label": "value", }), so
remove or replace that hardcoded map; update the call in the MergeMaps usage
(where labels is combined) to either merge only real runtime labels or omit the
second map entirely, or derive values from actual config/vars (e.g., provider
and additional_label from settings) before calling MergeMaps to avoid stamping
placeholder metadata on emitted evidence.
| test: | ||
| @go test ./... | ||
|
|
||
| clean: # Cleanup build artifacts | ||
| @rm -rf dist/* | ||
|
|
||
| build: clean ## Build the plugin package | ||
| @mkdir -p dist/ | ||
| @go build -o dist/plugin main.go |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial | ⚡ Quick win
Declare non-file targets as .PHONY.
Targets that don't produce files (help, help-all, test, clean, build, run) should be declared .PHONY to prevent make from treating them as file targets. Without this, make will skip the target if a file with that name exists in the directory.
♻️ Proposed fix
Add this line near the top of the Makefile, after the OPA check:
endif
+.PHONY: help help-all test clean build run
+
##@ Help
help: ## Display this concise help, ie only the porcelain target🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@Makefile` around lines 26 - 34, Add a .PHONY declaration for non-file targets
to prevent Make from treating them as actual files: declare .PHONY with the list
of targets (help, help-all, test, clean, build, run) near the top of the
Makefile (e.g., after the OPA check), so targets like test, clean, build, run
are always executed regardless of same-named files.
| run: build ## Execute the Concom agent with the built plugin | ||
| @../agent/dist/./concom agent --config ./.config/config.yaml No newline at end of file |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial | ⚡ Quick win
Consider making the agent path configurable.
The hardcoded relative path ../agent/dist/./concom assumes a specific directory structure. While documented in the README, this makes the target fragile and less portable.
♻️ Proposed improvements
Option 1: Add a configurable variable
+# Path to the Concom agent binary (override with: make run AGENT_PATH=/custom/path)
+AGENT_PATH ?= ../agent/dist/concom
+
##@ Help run: build ## Execute the Concom agent with the built plugin
- @../agent/dist/./concom agent --config ./.config/config.yaml
+ @$(AGENT_PATH) agent --config ./.config/config.yamlOption 2: Add an existence check
run: build ## Execute the Concom agent with the built plugin
+ `@test` -f ../agent/dist/concom || (echo "Error: agent not found at ../agent/dist/concom" && exit 1)
@../agent/dist/./concom agent --config ./.config/config.yaml📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| run: build ## Execute the Concom agent with the built plugin | |
| @../agent/dist/./concom agent --config ./.config/config.yaml | |
| run: build ## Execute the Concom agent with the built plugin | |
| `@test` -f ../agent/dist/concom || (echo "Error: agent not found at ../agent/dist/concom" && exit 1) | |
| @../agent/dist/./concom agent --config ./.config/config.yaml |
🧰 Tools
🪛 checkmake (0.3.2)
[warning] 36-36: Required target "all" is missing from the Makefile.
(minphony)
[warning] 36-36: Required target "clean" must be declared PHONY.
(minphony)
[warning] 36-36: Required target "test" must be declared PHONY.
(minphony)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@Makefile` around lines 36 - 37, The run target currently uses a hardcoded
agent path "../agent/dist/./concom"; make this configurable by introducing a
Makefile variable (e.g., AGENT_BIN or AGENT_PATH) with a sensible default and
replace the literal path in the run recipe with that variable, and also add an
existence check for $(AGENT_BIN) before executing (print an error and exit
non‑zero if missing) so the target is portable and fails fast when the binary is
not found.
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
main.go (1)
177-180:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winInconsistent error return pattern.
Other error paths (lines 143, 149-151, 172-174) return
&proto.EvalResponse{Status: proto.ExecutionStatus_FAILURE}alongside the error, but this path returnsnil. If the caller inspects the response status before checking the error, this could cause a nil pointer dereference.Proposed fix for consistency
if err := apiHelper.CreateEvidence(ctx, evidences); err != nil { l.logger.Error("Error creating evidence", "error", err) - return nil, err + return &proto.EvalResponse{Status: proto.ExecutionStatus_FAILURE}, fmt.Errorf("failed to create evidence: %w", err) }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@main.go` around lines 177 - 180, The CreateEvidence error path currently returns nil, err which is inconsistent with other paths; change the return to mirror others by returning a failure EvalResponse and the error: return &proto.EvalResponse{Status: proto.ExecutionStatus_FAILURE}, err. Update the block where apiHelper.CreateEvidence(ctx, evidences) is called (and the l.logger.Error("Error creating evidence", "error", err) line) to return &proto.EvalResponse{Status: proto.ExecutionStatus_FAILURE}, err instead of nil, err so callers always get a non-nil response.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@main.go`:
- Line 157: Replace the direct proto field access request.PolicyPaths with the
nil-safe getter request.GetPolicyPaths() when calling policyEvaluator.Eval;
update the call in the expression evidences, err := policyEvaluator.Eval(ctx,
data, request.PolicyPaths, l.policyData, l.config.PolicyLabels) to use
request.GetPolicyPaths() so it matches other proto accesses like req.GetConfig()
and req.GetPolicyData() and avoids potential nil dereference.
In `@README.md`:
- Around line 49-50: Update the two YAML comment strings to fix
spelling/wording: change "Policy behaviour can be defined to later filter
policies to specific bundles per execution" to use "behavior" instead of
"behaviour", and change "This is useful if your plugin proccesses more than 1
type of component" to use "processes" instead of "proccesses" (i.e., replace the
exact substrings "behaviour" -> "behavior" and "proccesses" -> "processes" in
the README comment lines).
---
Outside diff comments:
In `@main.go`:
- Around line 177-180: The CreateEvidence error path currently returns nil, err
which is inconsistent with other paths; change the return to mirror others by
returning a failure EvalResponse and the error: return
&proto.EvalResponse{Status: proto.ExecutionStatus_FAILURE}, err. Update the
block where apiHelper.CreateEvidence(ctx, evidences) is called (and the
l.logger.Error("Error creating evidence", "error", err) line) to return
&proto.EvalResponse{Status: proto.ExecutionStatus_FAILURE}, err instead of nil,
err so callers always get a non-nil response.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 57e74caf-5b95-4124-a2b6-6815e71472af
📒 Files selected for processing (2)
README.mdmain.go
|
|
||
| policyEvaluator := internal.NewPolicyEvaluator(ctx, l.logger, stepActivities) | ||
| // Simple use case for evaluating all policies against the data collected | ||
| evidences, err := policyEvaluator.Eval(ctx, data, request.PolicyPaths, l.policyData, l.config.PolicyLabels) |
There was a problem hiding this comment.
Use proto getter for nil-safety.
Static analysis flags direct proto field access. Use request.GetPolicyPaths() to ensure nil-safe access consistent with other proto field accesses in this file (e.g., req.GetConfig(), req.GetPolicyData()).
Proposed fix
- evidences, err := policyEvaluator.Eval(ctx, data, request.PolicyPaths, l.policyData, l.config.PolicyLabels)
+ evidences, err := policyEvaluator.Eval(ctx, data, request.GetPolicyPaths(), l.policyData, l.config.PolicyLabels)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| evidences, err := policyEvaluator.Eval(ctx, data, request.PolicyPaths, l.policyData, l.config.PolicyLabels) | |
| evidences, err := policyEvaluator.Eval(ctx, data, request.GetPolicyPaths(), l.policyData, l.config.PolicyLabels) |
🧰 Tools
🪛 golangci-lint (2.12.2)
[error] 157-157: avoid direct access to proto field request.PolicyPaths, use request.GetPolicyPaths() instead
(protogetter)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@main.go` at line 157, Replace the direct proto field access
request.PolicyPaths with the nil-safe getter request.GetPolicyPaths() when
calling policyEvaluator.Eval; update the call in the expression evidences, err
:= policyEvaluator.Eval(ctx, data, request.PolicyPaths, l.policyData,
l.config.PolicyLabels) to use request.GetPolicyPaths() so it matches other proto
accesses like req.GetConfig() and req.GetPolicyData() and avoids potential nil
dereference.
| # Policy behaviour can be defined to later filter policies to specific bundles per execution | ||
| # This is useful if your plugin proccesses more than 1 type of component |
There was a problem hiding this comment.
Fix spelling/wording in YAML comments.
Line 49-50 has user-facing typos/inconsistent spelling (behaviour, proccesses). Please normalize to behavior and processes for clarity and consistency with the rest of the docs/code terminology.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@README.md` around lines 49 - 50, Update the two YAML comment strings to fix
spelling/wording: change "Policy behaviour can be defined to later filter
policies to specific bundles per execution" to use "behavior" instead of
"behaviour", and change "This is useful if your plugin proccesses more than 1
type of component" to use "processes" instead of "proccesses" (i.e., replace the
exact substrings "behaviour" -> "behavior" and "proccesses" -> "processes" in
the README comment lines).
Summary by CodeRabbit
New Features
Documentation
Chores