Skip to content
Open
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
12 changes: 7 additions & 5 deletions internal/commands/agenthooks/cx/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/checkmarx/ast-cli/internal/commands/agenthooks/sca"
"github.com/checkmarx/ast-cli/internal/commands/agenthooks/sessiontally"
"github.com/checkmarx/ast-cli/internal/wrappers"
"github.com/checkmarx/ast-cli/internal/wrappers/utils"
)

// canonical per-engine labels for the session summary (normalizes the legacy SCA/Oss split used by
Expand Down Expand Up @@ -68,7 +69,7 @@ func cxBeforeToolCall(ev agenthooks.ToolCallEvent) agenthooks.ToolVerdict {
sid := sessionIDFromToolCall(&ev)
if finding, remediation, severity := scaScanner.CheckBashInstall(ev.Command, ev.WorkDir, agent, sid); finding != "" {
sessiontally.Add(sid, engineSca, 1, 1)
logRemediationTelemetry(agent, "SCA", severity, sid)
logRemediationTelemetry(agent, "SCA", severity, sid, ev.WorkDir)
return agenthooks.DenyWithContext(finding, remediation)
}
}
Expand Down Expand Up @@ -113,15 +114,15 @@ func cxBeforeFileEdit(ev agenthooks.FileEditEvent) agenthooks.FileEditVerdict {
agent := agentToString(ev.Agent)
if blocked, reason, context, severity := asca.ScanFileEdit(&ev, telemetryWrapper, agent); blocked {
sessiontally.Add(ev.SessionID, engineAsca, 1, 1)
logRemediationTelemetry(agent, "Asca", severity, ev.SessionID)
logRemediationTelemetry(agent, "Asca", severity, ev.SessionID, ev.WorkDir)
return agenthooks.RejectEditWithContext(reason, context)
}
var kicsNote string
if kicsScanner != nil {
blocked, reason, context, note, severity := kics.ScanFileEdit(&ev, kicsScanner, telemetryWrapper, agent)
if blocked {
sessiontally.Add(ev.SessionID, engineKics, 1, 1)
logRemediationTelemetry(agent, "IaC", severity, ev.SessionID)
logRemediationTelemetry(agent, "IaC", severity, ev.SessionID, ev.WorkDir)
return agenthooks.RejectEditWithContext(reason, context)
}
kicsNote = note
Expand All @@ -130,7 +131,7 @@ func cxBeforeFileEdit(ev agenthooks.FileEditEvent) agenthooks.FileEditVerdict {
for _, diff := range ev.Changes {
if finding, remediation, severity := scaScanner.CheckManifestEdit(ev.FilePath, fullAfterContent(ev.FilePath, diff), ev.WorkDir, agent, ev.SessionID); finding != "" {
sessiontally.Add(ev.SessionID, engineSca, 1, 1)
logRemediationTelemetry(agent, "Oss", severity, ev.SessionID)
logRemediationTelemetry(agent, "Oss", severity, ev.SessionID, ev.WorkDir)
return agenthooks.RejectEditWithContext(finding, remediation)
}
}
Expand Down Expand Up @@ -246,7 +247,7 @@ func RegisterPassThrough() {
}

// logRemediationTelemetry sends telemetry when remediation context is delivered to the agent.
func logRemediationTelemetry(agent, engine, severity, sessionID string) {
func logRemediationTelemetry(agent, engine, severity, sessionID, workDir string) {
if telemetryWrapper == nil {
return
}
Expand All @@ -261,6 +262,7 @@ func logRemediationTelemetry(agent, engine, severity, sessionID string) {
SubType: "fixWithAIAssist",
ProblemSeverity: severity,
AiAgentSessionId: sessionID,
GithubURL: utils.GitHubRemoteURL(workDir),
}

if err := telemetryWrapper.SendAIDataToLog(telemetryData); err != nil {
Expand Down
39 changes: 34 additions & 5 deletions internal/commands/agenthooks/cx/hooks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package cx
import (
"encoding/json"
"os"
"os/exec"
"path/filepath"
"runtime"

Expand Down Expand Up @@ -573,13 +574,13 @@ func TestLogRemediationTelemetry(t *testing.T) {

t.Run("nil_wrapper_noop", func(t *testing.T) {
telemetryWrapper = nil
logRemediationTelemetry("Claude", "SCA", "High", "s1") // must not panic
logRemediationTelemetry("Claude", "SCA", "High", "s1", "") // must not panic
})

t.Run("sends_payload", func(t *testing.T) {
tel := &recordingTelemetry{}
telemetryWrapper = tel
logRemediationTelemetry("Cursor", "Asca", "Critical", "sess-9")
logRemediationTelemetry("Cursor", "Asca", "Critical", "sess-9", "")
if len(tel.calls) != 1 {
t.Fatalf("got %d calls, want 1", len(tel.calls))
}
Expand All @@ -601,11 +602,39 @@ func TestLogRemediationTelemetry(t *testing.T) {
t.Run("send_error_fail_open", func(t *testing.T) {
tel := &recordingTelemetry{err: os.ErrPermission}
telemetryWrapper = tel
logRemediationTelemetry("Claude", "SCA", "High", "s2") // must not panic
logRemediationTelemetry("Claude", "SCA", "High", "s2", "") // must not panic
if len(tel.calls) != 1 {
t.Fatalf("got %d calls, want 1", len(tel.calls))
}
})

t.Run("includes_github_url", func(t *testing.T) {
if _, err := exec.LookPath("git"); err != nil {
t.Skip("git not available")
}
dir := t.TempDir()
gitIn := func(args ...string) {
t.Helper()
cmd := exec.Command("git", args...)
cmd.Dir = dir
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("%v: %s", err, out)
}
}
gitIn("init")
gitIn("remote", "add", "origin", "git@github.com:acme/widgets.git")

tel := &recordingTelemetry{}
telemetryWrapper = tel
logRemediationTelemetry("Cursor", "Asca", "High", "sess", dir)
if len(tel.calls) != 1 {
t.Fatalf("got %d calls, want 1", len(tel.calls))
}
if tel.calls[0].GithubURL != "https://github.com/acme/widgets" {
t.Errorf("GithubURL = %q", tel.calls[0].GithubURL)
}
})
}

// setEmptyHomeDir redirects the OS-specific home-dir env var to a fresh empty
Expand Down Expand Up @@ -798,7 +827,7 @@ func TestLogRemediationTelemetry_NilWrapper_NoOp(t *testing.T) {

telemetryWrapper = nil
assert.NotPanics(t, func() {
logRemediationTelemetry("Claude", "SCA", "finding", "remediation")
logRemediationTelemetry("Claude", "SCA", "finding", "remediation", "")
})
}

Expand All @@ -808,7 +837,7 @@ func TestLogRemediationTelemetry_WithWrapper_Sends(t *testing.T) {

telemetryWrapper = mock.TelemetryMockWrapper{}
assert.NotPanics(t, func() {
logRemediationTelemetry("Claude", "SCA", "finding", "remediation")
logRemediationTelemetry("Claude", "SCA", "finding", "remediation", "")
})
}

Expand Down
6 changes: 4 additions & 2 deletions internal/commands/agenthooks/guardrails/asca/asca.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/checkmarx/ast-cli/internal/services/realtimeengine/ignore"
"github.com/checkmarx/ast-cli/internal/wrappers"
"github.com/checkmarx/ast-cli/internal/wrappers/grpcs"
"github.com/checkmarx/ast-cli/internal/wrappers/utils"
"github.com/spf13/viper"
)

Expand Down Expand Up @@ -43,7 +44,7 @@ func ScanFileEdit(ev *agenthooks.FileEditEvent, telemetryWrapper wrappers.Teleme
reason = ""
context = ""
}
logASCATelemetry(telemetryWrapper, agent, ev.SessionID, findingCount)
logASCATelemetry(telemetryWrapper, agent, ev.SessionID, findingCount, ev.WorkDir)
}()

if !isSupportedByASCA(ev.FilePath) {
Expand Down Expand Up @@ -165,7 +166,7 @@ func shouldUpdateVersion() bool {

// logASCATelemetry sends a telemetry event for ASCA scan results.
// Called once after ASCA scan is performed with the actual finding count.
func logASCATelemetry(telemetryWrapper wrappers.TelemetryWrapper, agent, sessionID string, totalCount int) {
func logASCATelemetry(telemetryWrapper wrappers.TelemetryWrapper, agent, sessionID string, totalCount int, workDir string) {
if telemetryWrapper == nil || totalCount == 0 {
return
}
Expand All @@ -180,6 +181,7 @@ func logASCATelemetry(telemetryWrapper wrappers.TelemetryWrapper, agent, session
SubType: "scan",
ScanType: "asca",
AiAgentSessionId: sessionID,
GithubURL: utils.GitHubRemoteURL(workDir),
}

if err := telemetryWrapper.SendAIDataToLog(telemetryData); err != nil {
Expand Down
34 changes: 31 additions & 3 deletions internal/commands/agenthooks/guardrails/asca/asca_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package asca

import (
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
Expand Down Expand Up @@ -568,7 +569,7 @@ func TestShouldUpdateVersion_DisabledReturnsFalse(t *testing.T) {

func TestLogASCATelemetry_NilWrapper_NoOp(t *testing.T) {
assert.NotPanics(t, func() {
logASCATelemetry(nil, "Claude", "", 3)
logASCATelemetry(nil, "Claude", "", 3, "")
})
}

Expand All @@ -580,7 +581,7 @@ func TestLogASCATelemetry_ZeroCount_DoesNotSend(t *testing.T) {
return nil
},
}
logASCATelemetry(telemetry, "Claude", "", 0)
logASCATelemetry(telemetry, "Claude", "", 0, "")
assert.False(t, sent)
}

Expand All @@ -592,13 +593,40 @@ func TestLogASCATelemetry_WithFindings_Sends(t *testing.T) {
return nil
},
}
logASCATelemetry(telemetry, "Claude", "", 2)
logASCATelemetry(telemetry, "Claude", "", 2, "")
assert.NotNil(t, captured)
assert.Equal(t, "Asca", captured.Engine)
assert.Equal(t, 2, captured.TotalCount)
assert.Equal(t, "Claude", captured.AIProvider)
}

func TestLogASCATelemetry_IncludesGitHubURL(t *testing.T) {
if _, err := exec.LookPath("git"); err != nil {
t.Skip("git not available")
}
dir := t.TempDir()
gitIn := func(args ...string) {
t.Helper()
cmd := exec.Command("git", args...)
cmd.Dir = dir
out, err := cmd.CombinedOutput()
assert.NoError(t, err, string(out))
}
gitIn("init")
gitIn("remote", "add", "origin", "ssh://git@github.com/acme/widgets.git")

var captured *wrappers.DataForAITelemetry
telemetry := mock.TelemetryMockWrapper{
CustomSendAIDataToLog: func(data *wrappers.DataForAITelemetry) error {
captured = data
return nil
},
}
logASCATelemetry(telemetry, "Claude", "sess", 1, dir)
assert.NotNil(t, captured)
assert.Equal(t, "https://github.com/acme/widgets", captured.GithubURL)
}

// ── findingsSummary / formatFindings ─────────────────────────────────────────

func TestFindingsSummary_IncludesRemediation(t *testing.T) {
Expand Down
6 changes: 4 additions & 2 deletions internal/commands/agenthooks/guardrails/kics/kics.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/checkmarx/ast-cli/internal/services/realtimeengine/iacrealtime"
"github.com/checkmarx/ast-cli/internal/services/realtimeengine/ignore"
"github.com/checkmarx/ast-cli/internal/wrappers"
"github.com/checkmarx/ast-cli/internal/wrappers/utils"
)

// isSupportedByKICS returns true when the file matches a KICS-supported extension or basename.
Expand Down Expand Up @@ -59,7 +60,7 @@ func ScanFileEdit(ev *agenthooks.FileEditEvent, svc *Scanner, telemetryWrapper w
note = skippedNote(ev.FilePath, fmt.Errorf("internal error: %v", r))
severity = ""
}
logKicsTelemetry(telemetryWrapper, agent, ev.SessionID, findingCount)
logKicsTelemetry(telemetryWrapper, agent, ev.SessionID, findingCount, ev.WorkDir)
}()

if !isSupportedByKICS(ev.FilePath) {
Expand Down Expand Up @@ -141,7 +142,7 @@ func highestSeverity(findings []iacrealtime.IacRealtimeResult) string {

// logKicsTelemetry sends a telemetry event for KICS scan results.
// Called once after KICS scan is performed with the actual finding count.
func logKicsTelemetry(telemetryWrapper wrappers.TelemetryWrapper, agent, sessionID string, totalCount int) {
func logKicsTelemetry(telemetryWrapper wrappers.TelemetryWrapper, agent, sessionID string, totalCount int, workDir string) {
if telemetryWrapper == nil || totalCount == 0 {
return
}
Expand All @@ -156,6 +157,7 @@ func logKicsTelemetry(telemetryWrapper wrappers.TelemetryWrapper, agent, session
SubType: "scan",
ScanType: "iac",
AiAgentSessionId: sessionID,
GithubURL: utils.GitHubRemoteURL(workDir),
}

if err := telemetryWrapper.SendAIDataToLog(telemetryData); err != nil {
Expand Down
34 changes: 31 additions & 3 deletions internal/commands/agenthooks/guardrails/kics/kics_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
Expand Down Expand Up @@ -239,7 +240,7 @@ func TestScanFileEdit_EngineDownProducesNote(t *testing.T) {

func TestLogKicsTelemetry_NilWrapper_NoOp(t *testing.T) {
assert.NotPanics(t, func() {
logKicsTelemetry(nil, "Claude", "", 3)
logKicsTelemetry(nil, "Claude", "", 3, "")
})
}

Expand All @@ -251,7 +252,7 @@ func TestLogKicsTelemetry_ZeroCount_DoesNotSend(t *testing.T) {
return nil
},
}
logKicsTelemetry(telemetry, "Claude", "", 0)
logKicsTelemetry(telemetry, "Claude", "", 0, "")
assert.False(t, sent)
}

Expand All @@ -263,7 +264,7 @@ func TestLogKicsTelemetry_WithFindings_Sends(t *testing.T) {
return nil
},
}
logKicsTelemetry(telemetry, "Claude", "sess-1", 2)
logKicsTelemetry(telemetry, "Claude", "sess-1", 2, "")
assert.NotNil(t, captured)
assert.Equal(t, "IaC", captured.Engine)
assert.Equal(t, 2, captured.TotalCount)
Expand All @@ -273,3 +274,30 @@ func TestLogKicsTelemetry_WithFindings_Sends(t *testing.T) {
assert.Equal(t, "iac", captured.ScanType)
assert.Equal(t, "sess-1", captured.AiAgentSessionId)
}

func TestLogKicsTelemetry_IncludesGitHubURL(t *testing.T) {
if _, err := exec.LookPath("git"); err != nil {
t.Skip("git not available")
}
dir := t.TempDir()
gitIn := func(args ...string) {
t.Helper()
cmd := exec.Command("git", args...)
cmd.Dir = dir
out, err := cmd.CombinedOutput()
assert.NoError(t, err, string(out))
}
gitIn("init")
gitIn("remote", "add", "origin", "https://github.com/acme/widgets.git")

var captured *wrappers.DataForAITelemetry
telemetry := mock.TelemetryMockWrapper{
CustomSendAIDataToLog: func(data *wrappers.DataForAITelemetry) error {
captured = data
return nil
},
}
logKicsTelemetry(telemetry, "Claude", "sess-1", 2, dir)
assert.NotNil(t, captured)
assert.Equal(t, "https://github.com/acme/widgets", captured.GithubURL)
}
8 changes: 7 additions & 1 deletion internal/commands/asca/asca-engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import (
"strings"

"github.com/checkmarx/ast-cli/internal/commands/util"
"github.com/checkmarx/ast-cli/internal/commands/util/printer"
commonParams "github.com/checkmarx/ast-cli/internal/params"
"github.com/checkmarx/ast-cli/internal/services"
Expand All @@ -18,7 +19,7 @@
ascaLocationParam = "asca-location"
)

func RunScanASCACommand(jwtWrapper wrappers.JWTWrapper) func(cmd *cobra.Command, args []string) error {
func RunScanASCACommand(jwtWrapper wrappers.JWTWrapper, telemetryWrapper wrappers.TelemetryWrapper) func(cmd *cobra.Command, args []string) error {

Check failure on line 22 in internal/commands/asca/asca-engine.go

View workflow job for this annotation

GitHub Actions / Lint (golangci-lint)

exported: exported function RunScanASCACommand should have comment or be unexported (revive)
return func(cmd *cobra.Command, args []string) error {
var vorpalLocation string
ASCALatestVersion, _ := cmd.Flags().GetBool(commonParams.ASCALatestVersion)
Expand Down Expand Up @@ -55,6 +56,11 @@
if err != nil {
return err
}
totalCount := 0
if scanResult != nil {
totalCount = len(scanResult.ScanDetails)
}
util.SendRealtimeScanTelemetry(telemetryWrapper, "Asca", "asca", fileSourceFlag, totalCount)
err = printer.Print(cmd.OutOrStdout(), scanResult, printer.FormatJSON)
if err != nil {
return err
Expand Down
4 changes: 2 additions & 2 deletions internal/commands/asca/asca-engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ func Test_runScanASCACommand(t *testing.T) {
cmd.Flags().String(commonParams.SourcesFlag, ttt.sourceFlag, "")
cmd.Flags().Bool(commonParams.ASCALatestVersion, ttt.engineFlag, "")
cmd.Flags().String(commonParams.FormatFlag, printer.FormatJSON, "")
runFunc := RunScanASCACommand(&mock.JWTMockWrapper{})
runFunc := RunScanASCACommand(&mock.JWTMockWrapper{}, nil)
err := runFunc(cmd, []string{})
if (err != nil) != ttt.wantErr {
t.Errorf("RunScanASCACommand() error = %v, wantErr %v", err, ttt.wantErr)
Expand Down Expand Up @@ -185,7 +185,7 @@ func Test_runScanASCAWithAscaLocationFlagCommand(t *testing.T) {
cmd.Flags().Bool(commonParams.ASCALatestVersion, ttt.engineFlag, "")
cmd.Flags().String(commonParams.FormatFlag, printer.FormatJSON, "")
cmd.Flags().String(commonParams.ASCALocationFlag, ttt.ascaLocation, "")
runFunc := RunScanASCACommand(&mock.JWTMockWrapper{})
runFunc := RunScanASCACommand(&mock.JWTMockWrapper{}, nil)
err := runFunc(cmd, []string{})
if (err != nil) != ttt.wantErr {
t.Errorf("RunScanASCACommand() error = %v, wantErr %v", err, ttt.wantErr)
Expand Down
Loading
Loading