Skip to content
Merged

Staging #1606

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
2 changes: 1 addition & 1 deletion .github/workflows/nightly-parallel.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@ permissions:

jobs:
integration-tests: # zizmor: ignore[anonymous-definition]
uses: ./.github/workflows/ci-tests.yml
uses: $/.github/workflows/ci-tests.yml
# ci-tests.yml requires ~20 CX/SCM secrets; it's a same-repo reusable workflow, not a fork/third-party target.
secrets: inherit # zizmor: ignore[secrets-inherit]
51 changes: 11 additions & 40 deletions .trivyignore
Original file line number Diff line number Diff line change
@@ -1,44 +1,6 @@
# Trivy Ignore Configuration
# These vulnerabilities are in the Wolfi Linux base image or have compatibility issues

# ============================================================================
# BASE IMAGE VULNERABILITIES (Wolfi) - Cannot be patched by CLI code
# ============================================================================

# CVE-2026-8674: glibc DNS stub resolver DoS via long search domain
# Severity: MEDIUM
# Component: zlib 1.3.2-r6 (in Wolfi base image)
# Impact: Local DNS parsing issue - requires malformed DNS configuration
# Reason: Base image vulnerability - awaiting Wolfi update to zlib 1.3.2.1_rc20260601-r0
CVE-2026-8674

# CVE-2026-86805: glibc TOCTOU privilege escalation
# Severity: MEDIUM
# Component: glibc 2.44-r7 (in base image)
# Impact: Local attacker with file system access - requires race condition exploit
# Reason: Base image vulnerability - awaiting Wolfi update to glibc 2.44-r8
CVE-2026-86805

# CVE-2026-89092: glibc nscd stack overflow
# Severity: MEDIUM
# Component: glibc 2.44-r7 (in base image)
# Impact: Local DoS via nscd service - requires nscd to be running
# Reason: Base image vulnerability - awaiting Wolfi update to glibc 2.44-r8
CVE-2026-89092

# CVE-2026-95818: glibc local DoS and information disclosure
# Severity: MEDIUM
# Component: glibc 2.44-r7 (in base image)
# Impact: Local attacker can cause denial of service
# Reason: Base image vulnerability - awaiting Wolfi update to glibc 2.44-r8
CVE-2026-95818

# CVE-2026-85091: zlib heap buffer overflow
# Severity: MEDIUM
# Component: zlib 1.3.2-r6 (in Wolfi base image)
# Impact: Heap buffer overflow in compression library
# Reason: Base image vulnerability - awaiting Wolfi update to 1.3.2.1_rc20260601-r0
CVE-2026-85091
# These vulnerabilities are in Go dependencies that cannot be updated or fixed directly.
# Review periodically and remove entries once the underlying dependency is upgraded.

# ============================================================================
# BINARY VULNERABILITIES - Compatibility Issues (Not Updated)
Expand All @@ -62,3 +24,12 @@ CVE-2026-33481
# Docker plugin system - uses only core Docker SDK for container scanning.
# Status: Risk accepted - vulnerability not exploitable in CLI usage pattern
CVE-2026-33997

# GO-2026-5932: golang.org/x/crypto/openpgp is unmaintained and unsafe by design
# Severity: UNKNOWN
# Component: golang.org/x/crypto v0.58.0 (transitive via helm.sh/helm/v3/pkg/provenance)
# Reason: Not imported by ast-cli directly; the "fixed" version is an untagged pseudo-version.
# Awaiting Helm to drop x/crypto/openpgp.
# Impact: CLI does not perform OpenPGP signing/verification.
# Status: Risk accepted
GO-2026-5932
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM checkmarx/bash:5.3-r14-365aa4c3c7b3fc@sha256:365aa4c3c7b3fcb580687c427e90ee9cda26e75cd12cae3a2b521777c0b4d3f3
FROM checkmarx/bash:5.3-r14-e52a196c3a914c@sha256:e52a196c3a914c6496c72465ad4a8f7efd1ff8ad988b328cbb92018652c23c7d
USER nonroot

COPY cx /app/bin/cx
Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ require (
github.com/anchore/packageurl-go v0.1.1-0.20250220190351-d62adb6e1115 // indirect
github.com/anchore/stereoscope v0.1.0 // indirect
github.com/anchore/syft v1.21.0 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/andybalholm/brotli v1.2.7 // indirect
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
github.com/aquasecurity/go-pep440-version v0.0.1 // indirect
github.com/aquasecurity/go-version v0.0.1 // indirect
Expand Down Expand Up @@ -296,7 +296,7 @@ require (
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
golang.org/x/mod v0.41.0 // indirect
golang.org/x/net v0.60.0 // indirect
golang.org/x/net v0.62.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/sys v0.49.0 // indirect
golang.org/x/term v0.47.0 // indirect
Expand Down
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,8 @@ github.com/anchore/stereoscope v0.1.0/go.mod h1:3vasimie0IJOXvMbMpjwvwIHBDA1+192
github.com/anchore/syft v1.21.0 h1:JHmYOnEbCJsElROCCfg+3oIODw1LQLfXGkIrmXNZYsI=
github.com/anchore/syft v1.21.0/go.mod h1:8i8Yp/MiSOdqID0+6eiwE9bOJWM7fEBYitINZyr2G6s=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/andybalholm/brotli v1.2.7 h1:UgafRPGMam5ySHfRkhthMqOFA6IFoeF3D7mWIDLjG5A=
github.com/andybalholm/brotli v1.2.7/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
Expand Down Expand Up @@ -1201,8 +1201,8 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU=
golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
golang.org/x/net v0.62.0 h1:18ELalfM5+XNRwzM2nYqXsZQ9ByS1uhj2O/U7+5jqRY=
golang.org/x/net v0.62.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
Expand Down
42 changes: 37 additions & 5 deletions internal/commands/agenthooks/cx/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ import (
"github.com/checkmarx/ast-cli/internal/commands/agenthooks/guardrails/asca"
"github.com/checkmarx/ast-cli/internal/commands/agenthooks/guardrails/kics"
"github.com/checkmarx/ast-cli/internal/commands/agenthooks/guardrails/secrets"
"github.com/checkmarx/ast-cli/internal/commands/agenthooks/hostenv"
"github.com/checkmarx/ast-cli/internal/commands/agenthooks/sca"
"github.com/checkmarx/ast-cli/internal/commands/agenthooks/scannerconfig"
"github.com/checkmarx/ast-cli/internal/commands/agenthooks/sessiontally"
"github.com/checkmarx/ast-cli/internal/logger"
"github.com/checkmarx/ast-cli/internal/wrappers"
)

Expand Down Expand Up @@ -113,6 +115,9 @@ func cxBeforeToolCall(ev agenthooks.ToolCallEvent) agenthooks.ToolVerdict {
// or if the file exceeds the policy size cap. Reads do NOT count toward
// the blast-radius budget (that limit is about writes).
func cxBeforeFileEdit(ev agenthooks.FileEditEvent) agenthooks.FileEditVerdict {
if selfHealingOff(ev.Agent, "pre-file-write") {
return agenthooks.AcceptEdit()
}
bindThreshold(ev.Agent)
if ev.Agent == agenthooks.AgentCursor && len(ev.Changes) == 0 {
if reason := guardrails.ScanFileForSecrets(ev.FilePath); reason != "" {
Expand Down Expand Up @@ -290,7 +295,7 @@ func logRemediationTelemetry(agent, engine, severity, sessionID string) {

telemetryData := &wrappers.DataForAITelemetry{
AIProvider: agent,
Agent: agent + "-cli",
Agent: hostenv.TelemetryAgent(agent),
Engine: engine,
ScanType: strings.ToLower(engine),
UniqueID: wrappers.GetUniqueID(),
Expand Down Expand Up @@ -325,14 +330,41 @@ func secretsApply(agent agenthooks.AgentID) bool {
return switchesApply(agent)
}

// scannerOn is scannerconfig.Enabled, limited to the agents the switches apply to.
// scannerOn is scannerconfig.Enabled, limited to the agents the switches apply to. GitHub Copilot inside
// VS Code takes the switches from the VS Code extension's IDE file instead, while that file is valid.
func scannerOn(agent agenthooks.AgentID, s scannerconfig.Scanner) bool {
return !switchesApply(agent) || scannerconfig.Enabled(s)
if !switchesApply(agent) {
return true
}
if agent == agenthooks.AgentCopilotCLI && hostenv.InVSCode() {
return scannerconfig.IDEEnabled(s)
}
return scannerconfig.Enabled(s)
}

// selfHealingOff reports whether this GitHub Copilot hook runs inside VS Code with the VS Code extension's
// "Checkmarx Self-Healing" setting unticked (a valid IDE file saying cx_self_healing_enabled=false). The
// file-write scans then stand down without telemetry; shell commands and prompts are handled as before.
// The plugin's own hook scripts already stop before running cx in that case; this covers hooks that
// call cx directly. A missing or invalid IDE file means on, and the terminal Copilot CLI never reads it.
func selfHealingOff(agent agenthooks.AgentID, hook string) bool {
if agent != agenthooks.AgentCopilotCLI || !hostenv.InVSCode() || scannerconfig.SelfHealingEnabled() {
return false
}
logger.PrintfIfVerbose("hooks: copilot-cli %s allowed without scanning host=vscode skip_reason=self_healing_disabled", hook)
return true
}

// belowThreshold is scannerconfig.BelowThreshold, limited to the agents the switches apply to.
// belowThreshold is scannerconfig.BelowThreshold, limited to the agents the switches apply to. GitHub
// Copilot inside VS Code has no threshold setting, so it uses the IDE file's threshold, low when unset.
func belowThreshold(agent agenthooks.AgentID, severity string) bool {
return switchesApply(agent) && scannerconfig.BelowThreshold(severity)
if !switchesApply(agent) {
return false
}
if agent == agenthooks.AgentCopilotCLI && hostenv.InVSCode() {
return scannerconfig.IDEBelowThreshold(severity)
}
return scannerconfig.BelowThreshold(severity)
}

// agentToString converts agenthooks.AgentID enum to string representation for telemetry.
Expand Down
127 changes: 127 additions & 0 deletions internal/commands/agenthooks/cx/vscode_self_healing_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
//go:build !integration

package cx

import (
"os"
"path/filepath"
"testing"

agenthooks "github.com/Checkmarx/ast-cx-hooks"

"github.com/checkmarx/ast-cli/internal/commands/agenthooks/scannerconfig"
)

// PLUGIN_ROOT values as the hosts set them (AST-186850 PoC; Windows paths, any user name).
const (
rootVSCodeFromCLIInstall = `C:\Users\u\AppData\Roaming\Code\agentPlugins\file-c-3A-Users-u-copilot-installed-plugins-cx-devassist-marketplace-cx-devassist\1a15407167c`
rootVSCodeFromMarketplace = `C:\Users\u\AppData\Roaming\Code\agentPlugins\file-c-3A-Users-u-vscode-agent-plugins-marketplace-repositories-github-com-Checkmarx-cx-agentic-ai\1a1542d091b`
rootCLI = `C:\Users\u\.copilot\installed-plugins\cx-devassist-marketplace\cx-devassist`
)

// useHost sets the hook environment the host would inject.
func useHost(t *testing.T, pluginRoot, integrationID string) {
t.Helper()
t.Setenv("PLUGIN_ROOT", pluginRoot)
t.Setenv("GITHUB_COPILOT_INTEGRATION_ID", integrationID)
}

// useIDEConfig points the user's home at a temp dir holding ~/.checkmarx/cx-onboarding-ide.properties
// (and optionally cx-onboarding.properties); an empty body means no file at all.
func useIDEConfig(t *testing.T, ideBody, cliBody string) {
t.Helper()
dir := t.TempDir()
t.Cleanup(setHomeDir(dir))
cxDir := filepath.Join(dir, ".checkmarx")
if err := os.MkdirAll(cxDir, 0o755); err != nil {
t.Fatal(err)
}
for name, body := range map[string]string{"cx-onboarding-ide.properties": ideBody, "cx-onboarding.properties": cliBody} {
if body == "" {
continue
}
if err := os.WriteFile(filepath.Join(cxDir, name), []byte(body), 0o600); err != nil {
t.Fatal(err)
}
}
}

func TestCxBeforeFileEdit_SelfHealingOff(t *testing.T) {
const secret = "ghp_self_healing_test_secret_value"
const off = "cx_schema_version=1\ncx_self_healing_enabled=false\n"
cases := []struct {
name string
agent agenthooks.AgentID
root string
ide string
wantBlock bool
}{
{"VS Code + OFF allows without scanning", agenthooks.AgentCopilotCLI, rootVSCodeFromMarketplace, off, false},
{"VS Code + ON scans", agenthooks.AgentCopilotCLI, rootVSCodeFromCLIInstall, "cx_self_healing_enabled=true\n", true},
{"VS Code + no IDE file scans (default ON)", agenthooks.AgentCopilotCLI, rootVSCodeFromCLIInstall, "", true},
{"VS Code + unknown schema scans", agenthooks.AgentCopilotCLI, rootVSCodeFromCLIInstall, "cx_schema_version=2\ncx_self_healing_enabled=false\n", true},
{"terminal ignores the IDE file", agenthooks.AgentCopilotCLI, rootCLI, off, true},
{"other agents ignore it", agenthooks.AgentWindsurf, rootVSCodeFromCLIInstall, off, true},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
resetHookGlobals(t)
useIDEConfig(t, tc.ide, "")
useHost(t, tc.root, "")
secretsScanner = stubSecretsScanner(secret)
v := cxBeforeFileEdit(secretsEdit(secret, tc.agent))
if v.Permit == tc.wantBlock {
t.Fatalf("Permit=%v, wantBlock=%v", v.Permit, tc.wantBlock)
}
})
}
}

func TestScannerOn_VSCodeUsesIDEFile(t *testing.T) {
const ideSecretsOff = "cx_schema_version=1\ncx_secret_scanner_enabled=false\n"
const cliSecretsOff = "cx_secret_scanner_enabled=false\n"
cases := []struct {
name string
root string
ide, cli string
want bool
}{
{"VS Code follows the IDE file", rootVSCodeFromCLIInstall, ideSecretsOff, "", false},
{"VS Code IDE file missing key is on, even if the terminal file is off", rootVSCodeFromCLIInstall, "cx_schema_version=1\n", cliSecretsOff, true},
{"VS Code without a valid IDE file falls back to the terminal file", rootVSCodeFromCLIInstall, "", cliSecretsOff, false},
{"terminal never reads the IDE file", rootCLI, ideSecretsOff, "", true},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
useIDEConfig(t, tc.ide, tc.cli)
useHost(t, tc.root, "")
if got := scannerOn(agenthooks.AgentCopilotCLI, scannerconfig.Secrets); got != tc.want {
t.Fatalf("scannerOn = %v, want %v", got, tc.want)
}
})
}
}

func TestBelowThreshold_VSCodeDefaultsToLow(t *testing.T) {
const cliHigh = "cx_severity_threshold=high\n"
cases := []struct {
name string
root string
ide, cli string
want bool
}{
{"VS Code without a threshold blocks Medium (low)", rootVSCodeFromCLIInstall, "cx_schema_version=1\n", cliHigh, false},
{"VS Code without an IDE file blocks Medium (low)", rootVSCodeFromMarketplace, "", cliHigh, false},
{"VS Code honours a threshold in the IDE file", rootVSCodeFromCLIInstall, "cx_schema_version=1\n" + cliHigh, "", true},
{"terminal keeps the terminal threshold", rootCLI, "cx_schema_version=1\n", cliHigh, true},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
useIDEConfig(t, tc.ide, tc.cli)
useHost(t, tc.root, "")
if got := belowThreshold(agenthooks.AgentCopilotCLI, "Medium"); got != tc.want {
t.Fatalf("belowThreshold = %v, want %v", got, tc.want)
}
})
}
}
3 changes: 2 additions & 1 deletion internal/commands/agenthooks/guardrails/asca/asca.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/commands/agenthooks/hostenv"
"github.com/checkmarx/ast-cli/internal/params"
"github.com/checkmarx/ast-cli/internal/services"
"github.com/checkmarx/ast-cli/internal/services/realtimeengine/ignore"
Expand Down Expand Up @@ -192,7 +193,7 @@ func logASCATelemetry(telemetryWrapper wrappers.TelemetryWrapper, agent, session
}

telemetryData := &wrappers.DataForAITelemetry{
Agent: agent + "-cli",
Agent: hostenv.TelemetryAgent(agent),
AIProvider: agent,
Engine: "Asca",
TotalCount: totalCount,
Expand Down
3 changes: 2 additions & 1 deletion internal/commands/agenthooks/guardrails/kics/kics.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"strings"

agenthooks "github.com/Checkmarx/ast-cx-hooks"
"github.com/checkmarx/ast-cli/internal/commands/agenthooks/hostenv"
"github.com/checkmarx/ast-cli/internal/logger"
"github.com/checkmarx/ast-cli/internal/params"
"github.com/checkmarx/ast-cli/internal/services/realtimeengine/iacrealtime"
Expand Down Expand Up @@ -170,7 +171,7 @@ func logKicsTelemetry(telemetryWrapper wrappers.TelemetryWrapper, agent, session
}

telemetryData := &wrappers.DataForAITelemetry{
Agent: agent + "-cli",
Agent: hostenv.TelemetryAgent(agent),
AIProvider: agent,
Engine: "IaC",
TotalCount: totalCount,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"os"

agenthooks "github.com/Checkmarx/ast-cx-hooks"
"github.com/checkmarx/ast-cli/internal/commands/agenthooks/hostenv"
"github.com/checkmarx/ast-cli/internal/logger"
"github.com/checkmarx/ast-cli/internal/services/realtimeengine/ignore"
"github.com/checkmarx/ast-cli/internal/services/realtimeengine/secretsrealtime"
Expand Down Expand Up @@ -121,7 +122,7 @@ func logSecretsTelemetry(telemetryWrapper wrappers.TelemetryWrapper, agent, sess
}

telemetryData := &wrappers.DataForAITelemetry{
Agent: agent + "-cli",
Agent: hostenv.TelemetryAgent(agent),
AIProvider: agent,
Engine: "Secrets",
TotalCount: totalCount,
Expand Down
Loading
Loading