Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 0 additions & 31 deletions .github/workflows/scan-github-action.yml

This file was deleted.

9 changes: 9 additions & 0 deletions .trivyignore
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,12 @@ CVE-2026-48978 exp:2026-12-31
# Risk: Low - affects base OS image, not application code
# Impact: Minimal - only affects base OS components, application uses glibc runtime only
CVE-2026-6791 exp:2026-12-31

# CVE-2026-58055 (MEDIUM): libnghttp2 HTTP Request/Response Smuggling
# Library: libnghttp2-14 v1.69.0-r0
# Image: checkmarx/bash:5.3-r12 (base image)
# Status: Fixed in libnghttp2-14 >= 1.70.0-r0
# Risk: MEDIUM - HTTP/1.1 Upgrade smuggling potential
# Impact: Awaiting checkmarx/bash base image patch
# Tracking: AST-166372
CVE-2026-58055 exp:2027-02-28
6 changes: 3 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ require (
github.com/Checkmarx/containers-types v1.0.9
github.com/Checkmarx/gen-ai-prompts v0.0.0-20240807143411-708ceec12b63
github.com/Checkmarx/gen-ai-wrapper v1.0.3
github.com/Checkmarx/manifest-parser v0.1.3
github.com/Checkmarx/manifest-parser v0.1.4
github.com/Checkmarx/secret-detection v1.2.1
github.com/MakeNowJust/heredoc v1.0.0
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74
Expand All @@ -30,7 +30,7 @@ require (
github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80
github.com/xeipuuv/gojsonschema v1.2.0
golang.org/x/crypto v0.53.0
golang.org/x/sync v0.21.0
golang.org/x/sync v0.22.0
golang.org/x/text v0.39.0
google.golang.org/grpc v1.82.1
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af
Expand Down Expand Up @@ -322,7 +322,7 @@ require (
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
modernc.org/sqlite v1.46.2 // indirect
oras.land/oras-go/v2 v2.6.0 // indirect
oras.land/oras-go/v2 v2.6.2 // indirect
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
sigs.k8s.io/kustomize/api v0.21.1 // indirect
sigs.k8s.io/kustomize/kyaml v0.21.1 // indirect
Expand Down
12 changes: 6 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,8 @@ github.com/Checkmarx/gen-ai-prompts v0.0.0-20240807143411-708ceec12b63 h1:SCuTcE
github.com/Checkmarx/gen-ai-prompts v0.0.0-20240807143411-708ceec12b63/go.mod h1:MI6lfLerXU+5eTV/EPTDavgnV3owz3GPT4g/msZBWPo=
github.com/Checkmarx/gen-ai-wrapper v1.0.3 h1:p7lc/U4dFltsIxAEeWeDNW4+8ovvlJvdb5pVBLcbKs8=
github.com/Checkmarx/gen-ai-wrapper v1.0.3/go.mod h1:xwRLefezwNNnRGu1EjGS6wNiR9FVV/eP9D+oXwLViVM=
github.com/Checkmarx/manifest-parser v0.1.3 h1:cr+q7QkbkoCsoA5nQnv1/Pp23jnKWBePAwrcJNTk4x8=
github.com/Checkmarx/manifest-parser v0.1.3/go.mod h1:hh5FX5FdDieU8CKQEkged4hfOaSylpJzub8PRFXa4kA=
github.com/Checkmarx/manifest-parser v0.1.4 h1:vvioz4oFQhe7f+/ONHZIybaafKt2XOsTkKTe35n026I=
github.com/Checkmarx/manifest-parser v0.1.4/go.mod h1:hh5FX5FdDieU8CKQEkged4hfOaSylpJzub8PRFXa4kA=
github.com/Checkmarx/secret-detection v1.2.1 h1:Hzpz74dcN/L14Q86ARvPOZpKBnERzGTpy6sl1RXKOTo=
github.com/Checkmarx/secret-detection v1.2.1/go.mod h1:kbXbtIQisDdB/TNuV7r9HPclEznUyBHLQ5yr7IX7vBQ=
github.com/CycloneDX/cyclonedx-go v0.10.0 h1:7xyklU7YD+CUyGzSFIARG18NYLsKVn4QFg04qSsu+7Y=
Expand Down Expand Up @@ -1233,8 +1233,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
Expand Down Expand Up @@ -1642,8 +1642,8 @@ modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc=
oras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o=
oras.land/oras-go/v2 v2.6.2 h1:N04RXngAp1LJKTG6ifz3xHPipasEkWr+hFmInja5YKo=
oras.land/oras-go/v2 v2.6.2/go.mod h1:PlTtg4JTDJkDe8yVHpM2wz7/YDc00GVas+i4jAW2TZ4=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
Expand Down
85 changes: 71 additions & 14 deletions internal/commands/agenthooks/guardrails/kics/delta.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package kics

import (
"fmt"
"path/filepath"
"strings"

"github.com/checkmarx/ast-cli/internal/services/realtimeengine/iacrealtime"
Expand Down Expand Up @@ -72,6 +73,43 @@ func permissionDecisionReason(filePath, summary string) string {
)
}

// dockerImagePlatforms are the KICS "platform" values (result.Platform, sourced from
// KICS query metadata) whose findings concern container images rather than generic
// IaC misconfigurations. These line up with the fileType enum accepted by the
// imageRemediation MCP tool (Dockerfile, DockerCompose).
var dockerImagePlatforms = map[string]bool{
"dockerfile": true,
"dockercompose": true,
"docker compose": true,
}

// isDockerImageFinding reports whether a finding's KICS platform identifies it as a
// container image issue (Dockerfile/docker-compose) rather than generic IaC. Falls
// back to filename heuristics only when platform is unavailable (e.g. older cached
// results), since platform is scanner-reported ground truth and filenames can vary.
func isDockerImageFinding(filePath string, findings []iacrealtime.IacRealtimeResult) bool {
for i := range findings {
if findings[i].Platform != "" {
return dockerImagePlatforms[strings.ToLower(findings[i].Platform)]
}
}
return isDockerImageFileByName(filePath)
}

// isDockerImageFileByName is a filename-based fallback for when KICS platform metadata
// isn't available. Mirrors the basename conventions in params.KicsBaseFilters plus the
// docker-compose/compose naming convention (not in KicsBaseFilters since compose files
// match on the generic .yml/.yaml extensions).
func isDockerImageFileByName(filePath string) bool {
base := strings.ToLower(filepath.Base(filePath))
if base == "dockerfile" || strings.HasSuffix(base, ".dockerfile") {
return true
}
name := strings.TrimSuffix(strings.TrimSuffix(base, ".yaml"), ".yml")
return name == "docker-compose" || strings.HasPrefix(name, "docker-compose.") ||
name == "compose" || strings.HasPrefix(name, "compose.")
}

// additionalContext is injected into the agent's context window to drive remediation.
// KICS is a deterministic IaC rule engine: unlike ASCA, its findings are not caused by
// missing cross-file context, so the agent is NOT given discretion to treat findings as
Expand All @@ -94,19 +132,38 @@ func additionalContext(filePath string, findings []iacrealtime.IacRealtimeResult
"tool or shell command.\n"+
"Fix every finding below, then retry the write:\n"+
"%s"+
"For each finding, call the mcp__Checkmarx__codeRemediation tool with:\n"+
" {\n"+
" \"type\": \"iac\",\n"+
" \"metadata\": {\n"+
" \"title\": \"[Title from finding]\",\n"+
" \"description\": \"[Description from finding]\",\n"+
" \"remediationAdvice\": \"[how to harden this configuration]\"\n"+
" }\n"+
" }\n"+
"Apply the remediation guidance the tool returns, then retry the write. If a fix "+
"genuinely requires resources outside this file (for example a separate KMS key or "+
"a centrally-managed policy), add them as part of your change rather than skipping "+
"the finding.",
filePath, findingList.String(),
"%s",
filePath, findingList.String(), remediationInstructions(filePath, findings),
)
}

// remediationInstructions returns the tool-call guidance for the finding's file type.
// Dockerfile/docker-compose findings are about container images, so they must go
// through imageRemediation (base image CVEs, safer tags, hardening). All other
// KICS-supported files (Terraform, Kubernetes manifests, CloudFormation, etc.) are
// generic IaC misconfigurations and go through codeRemediation.
func remediationInstructions(filePath string, findings []iacrealtime.IacRealtimeResult) string {
if isDockerImageFinding(filePath, findings) {
return "For each finding, call the mcp__Checkmarx__imageRemediation tool with:\n" +
" {\n" +
" \"imageName\": \"[image name from the finding/file, without the tag]\",\n" +
" \"imageTag\": \"[image tag from the finding/file, e.g. latest]\",\n" +
" \"fileType\": \"[Dockerfile or DockerCompose, matching this file]\"\n" +
" }\n" +
"Apply the remediation guidance the tool returns (safer base image, pinned digest, " +
"hardening steps), then retry the write."
}
return "For each finding, call the mcp__Checkmarx__codeRemediation tool with:\n" +
" {\n" +
" \"type\": \"iac\",\n" +
" \"metadata\": {\n" +
" \"title\": \"[Title from finding]\",\n" +
" \"description\": \"[Description from finding]\",\n" +
" \"remediationAdvice\": \"[how to harden this configuration]\"\n" +
" }\n" +
" }\n" +
"Apply the remediation guidance the tool returns, then retry the write. If a fix " +
"genuinely requires resources outside this file (for example a separate KMS key or " +
"a centrally-managed policy), add them as part of your change rather than skipping " +
"the finding."
}
89 changes: 89 additions & 0 deletions internal/commands/agenthooks/guardrails/kics/delta_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ func iacResult(title, similarityID, severity string, line int) iacrealtime.IacRe
}
}

func iacResultWithPlatform(title, platform string) iacrealtime.IacRealtimeResult {
r := iacResult(title, "sim1", "HIGH", 1)
r.Platform = platform
return r
}

// ── NewFindings ───────────────────────────────────────────────────────────────

func TestNewFindings_NilOriginalReturnsAll(t *testing.T) {
Expand Down Expand Up @@ -123,3 +129,86 @@ func TestFormatFindings_ContextContainsDoNotBypass(t *testing.T) {
t.Errorf("context should warn against bypass, got: %q", ctx)
}
}

// ── isDockerImageFinding / remediation tool routing ────────────────────────────

func TestIsDockerImageFinding_ByPlatform(t *testing.T) {
cases := []struct {
platform string
want bool
}{
{"Dockerfile", true},
{"DockerCompose", true},
{"Docker Compose", true},
{"dockerfile", true},
{"Terraform", false},
{"Kubernetes", false},
{"CloudFormation", false},
{"Ansible", false},
}
for _, c := range cases {
findings := []iacrealtime.IacRealtimeResult{
iacResultWithPlatform("SomeFinding", c.platform),
}
// Filename deliberately contradicts platform to prove platform wins.
if got := isDockerImageFinding("/project/values.yaml", findings); got != c.want {
t.Errorf("isDockerImageFinding with platform %q = %v, want %v", c.platform, got, c.want)
}
}
}

func TestIsDockerImageFinding_FallsBackToFilenameWhenPlatformEmpty(t *testing.T) {
cases := map[string]bool{
"/project/Dockerfile": true,
"/project/api.dockerfile": true,
"/project/docker-compose.yml": true,
"/project/docker-compose.yaml": true,
"/project/docker-compose.prod.yml": true,
"/project/compose.yaml": true,
"/project/main.tf": false,
"/project/deployment.yaml": false,
"/project/values.yaml": false,
}
for path, want := range cases {
findings := []iacrealtime.IacRealtimeResult{iacResult("SomeFinding", "sim1", "HIGH", 1)}
if got := isDockerImageFinding(path, findings); got != want {
t.Errorf("isDockerImageFinding(%q) with no platform = %v, want %v", path, got, want)
}
}
}

func TestFormatFindings_DockerfilePlatformUsesImageRemediation(t *testing.T) {
findings := []iacrealtime.IacRealtimeResult{
iacResultWithPlatform("VulnerableBaseImage", "Dockerfile"),
}
_, ctx := formatFindings("/project/Dockerfile", findings)
if !strings.Contains(ctx, "mcp__Checkmarx__imageRemediation") {
t.Errorf("Dockerfile context should call imageRemediation, got: %q", ctx)
}
if strings.Contains(ctx, "mcp__Checkmarx__codeRemediation") {
t.Errorf("Dockerfile context should not call codeRemediation, got: %q", ctx)
}
}

func TestFormatFindings_DockerComposePlatformUsesImageRemediation(t *testing.T) {
findings := []iacrealtime.IacRealtimeResult{
iacResultWithPlatform("VulnerableBaseImage", "DockerCompose"),
}
_, ctx := formatFindings("/project/stack.yml", findings)
if !strings.Contains(ctx, "mcp__Checkmarx__imageRemediation") {
t.Errorf("docker-compose context should call imageRemediation, got: %q", ctx)
}
}

func TestFormatFindings_TerraformUsesCodeRemediation(t *testing.T) {
findings := []iacrealtime.IacRealtimeResult{
iacResultWithPlatform("OpenSecurityGroup", "Terraform"),
}
_, ctx := formatFindings("/project/main.tf", findings)
if !strings.Contains(ctx, "mcp__Checkmarx__codeRemediation") {
t.Errorf("Terraform context should call codeRemediation, got: %q", ctx)
}
if strings.Contains(ctx, "mcp__Checkmarx__imageRemediation") {
t.Errorf("Terraform context should not call imageRemediation, got: %q", ctx)
}
}
4 changes: 4 additions & 0 deletions internal/commands/agenthooks/guardrails/kics/kics.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"strings"

agenthooks "github.com/Checkmarx/ast-cx-hooks"
"github.com/checkmarx/ast-cli/internal/logger"
"github.com/checkmarx/ast-cli/internal/params"
"github.com/checkmarx/ast-cli/internal/services/realtimeengine/ignore"
)
Expand Down Expand Up @@ -45,6 +46,7 @@ func isSupportedByKICS(filePath string) bool {
func ScanFileEdit(ev agenthooks.FileEditEvent, svc *Scanner) (blocked bool, reason, context string) {
defer func() {
if r := recover(); r != nil {
logger.PrintfIfVerbose("kics guardrail: recovered from panic, failing open: %v", r)
blocked = false
reason = ""
context = ""
Expand All @@ -70,6 +72,7 @@ func ScanFileEdit(ev agenthooks.FileEditEvent, svc *Scanner) (blocked bool, reas
newResults, err := svc.scan(stagedNew)
if err != nil {
// Fail open: Docker unavailable, image pull failure, feature flag disabled, etc.
logger.PrintfIfVerbose("kics guardrail: scan of proposed content failed, failing open: %v", err)
return false, "", ""
}
if len(newResults) == 0 {
Expand All @@ -92,6 +95,7 @@ func ScanFileEdit(ev agenthooks.FileEditEvent, svc *Scanner) (blocked bool, reas
origResults, err := svc.scan(stagedOrig)
if err != nil {
// Fail open on original scan error
logger.PrintfIfVerbose("kics guardrail: scan of original content failed, failing open: %v", err)
return false, "", ""
}

Expand Down
32 changes: 31 additions & 1 deletion internal/commands/agenthooks/guardrails/kics/scanner.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package kics

import (
"os"
"os/exec"

"github.com/checkmarx/ast-cli/internal/params"
"github.com/checkmarx/ast-cli/internal/services/realtimeengine/iacrealtime"
"github.com/checkmarx/ast-cli/internal/wrappers"
)
Expand All @@ -27,7 +31,33 @@ func NewScannerWithFunc(f func(path string) ([]iacrealtime.IacRealtimeResult, er
return &Scanner{scan: f}
}

// defaultContainerEngine mirrors the "docker" default of the --engine flag on
// the manual `cx scan iac-realtime` command (internal/commands/scan.go), used
// when neither an override nor auto-detection finds a usable engine.
const defaultContainerEngine = "docker"

// resolveContainerEngine picks the container engine name to pass to
// RunIacRealtimeScan. The guardrail is invoked as `cx hooks <route>` with only
// stdin JSON (no --engine flag like the manual `cx scan iac-realtime`
// command), so it resolves the engine itself:
// 1. HooksContainerEngineEnv, if set — lets a Podman/Colima-only user (or the
// agent plugin's own hook environment) override the choice explicitly.
// 2. Auto-detect via PATH lookup: try "docker" then "podman", first one found wins.
// 3. defaultContainerEngine, if neither resolves — preserves prior behavior
// and existing error messaging when no engine is installed at all.
func resolveContainerEngine() string {
if engine := os.Getenv(params.HooksContainerEngineEnv); engine != "" {
return engine
}
for _, engine := range []string{"docker", "podman"} {
if _, err := exec.LookPath(engine); err == nil {
return engine
}
}
return defaultContainerEngine
}

func (s *Scanner) runRealScan(path string) ([]iacrealtime.IacRealtimeResult, error) {
svc := iacrealtime.NewIacRealtimeService(s.jwt, s.ff, iacrealtime.NewContainerManager())
return svc.RunIacRealtimeScan(path, "", existingIgnoreFilePath())
return svc.RunIacRealtimeScan(path, resolveContainerEngine(), existingIgnoreFilePath())
}
Loading
Loading