Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
76c597d
Add --skip-default-filter flag to scan
cx-atish-jadhav Jul 31, 2026
da1f4cd
Add timeout wrapper for SCA realtime integration test
cx-atish-jadhav Jul 31, 2026
67f9c0d
Revert "Add timeout wrapper for SCA realtime integration test"
cx-atish-jadhav Jul 31, 2026
8d2e25d
Run SCA Realtime tests in isolated CI job
cx-atish-jadhav Aug 3, 2026
18279ce
Skip SCA realtime test and remove isolated CI job
cx-atish-jadhav Aug 3, 2026
e2561f1
fixing validate in integration check
cx-atish-jadhav Aug 3, 2026
3b96611
Remove unnecessary check
cx-atish-jadhav Aug 4, 2026
0cb06e4
Skip flaky sca-realtime integration test
cx-atish-jadhav Aug 4, 2026
2c17eaa
Fix t.Skip call in TestRunScaRealtimeScan
cx-atish-jadhav Aug 4, 2026
8278e84
Add .trivyignore entry for CVE-2026-58055
cx-atish-jadhav Aug 4, 2026
cf88a5f
AST-164154: Fix KICS agent-hook guardrail never scanning (empty conta…
cx-anurag-dalke Aug 5, 2026
453d5ff
Add --skip-default-filter flag to scan
cx-atish-jadhav Jul 31, 2026
48162d7
Add timeout wrapper for SCA realtime integration test
cx-atish-jadhav Jul 31, 2026
7a407c9
Revert "Add timeout wrapper for SCA realtime integration test"
cx-atish-jadhav Jul 31, 2026
5673478
Run SCA Realtime tests in isolated CI job
cx-atish-jadhav Aug 3, 2026
8f798c7
Skip SCA realtime test and remove isolated CI job
cx-atish-jadhav Aug 3, 2026
7ae5cf7
fixing validate in integration check
cx-atish-jadhav Aug 3, 2026
6344831
Remove unnecessary check
cx-atish-jadhav Aug 4, 2026
1f0c154
Skip flaky sca-realtime integration test
cx-atish-jadhav Aug 4, 2026
546271d
Fix t.Skip call in TestRunScaRealtimeScan
cx-atish-jadhav Aug 4, 2026
0ff563c
Add .trivyignore entry for CVE-2026-58055
cx-atish-jadhav Aug 4, 2026
18090c4
Merge branch 'bug/AST-154378-skipDefaultFilter' of https://github.com…
cx-atish-jadhav Aug 6, 2026
5a67573
trivy fixes
cx-atish-jadhav Aug 6, 2026
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
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
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