diff --git a/go.mod b/go.mod index 0d3a97fa..2b257ecc 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/checkmarx/ast-cli -go 1.26.4 +go 1.26.5 require ( github.com/Checkmarx/ast-cx-hooks v1.0.4 @@ -29,24 +29,23 @@ require ( github.com/stretchr/testify v1.11.1 github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 github.com/xeipuuv/gojsonschema v1.2.0 - golang.org/x/crypto v0.52.0 - golang.org/x/sync v0.20.0 - golang.org/x/text v0.37.0 - google.golang.org/grpc v1.80.0 + golang.org/x/crypto v0.53.0 + golang.org/x/sync v0.21.0 + golang.org/x/text v0.39.0 + google.golang.org/grpc v1.81.1 google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af gopkg.in/yaml.v3 v3.0.1 gotest.tools v2.2.0+incompatible ) require ( - cyphar.com/go-pathrs v0.2.1 // indirect + cyphar.com/go-pathrs v0.2.4 // indirect github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 // indirect github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20240914100643-eb91380d8434 // indirect github.com/Masterminds/semver v1.5.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/clipperhouse/displaywidth v0.10.0 // indirect github.com/clipperhouse/uax29/v2 v2.6.0 // indirect - github.com/coreos/go-systemd/v22 v22.7.0 // indirect github.com/distribution/distribution/v3 v3.1.1 // indirect github.com/docker/docker v28.0.3+incompatible // indirect github.com/docker/go-events v0.0.0-20250808211157-605354379745 // indirect @@ -83,7 +82,7 @@ require ( github.com/Masterminds/sprig/v3 v3.3.0 // indirect github.com/Masterminds/squirrel v1.5.4 // indirect github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29 // indirect - github.com/Microsoft/hcsshim v0.15.0-rc.1 // indirect + github.com/Microsoft/hcsshim v0.15.0-rc.3 // indirect github.com/ProtonMail/go-crypto v1.4.0 // indirect github.com/acobaugh/osrelease v0.1.0 // indirect github.com/adrg/xdg v0.5.3 // indirect @@ -119,24 +118,23 @@ require ( github.com/charmbracelet/x/term v0.2.2 // indirect github.com/cloudflare/circl v1.6.3 // indirect github.com/containerd/cgroups/v3 v3.1.3 // indirect - github.com/containerd/containerd v1.7.30 // indirect - github.com/containerd/containerd/api v1.9.0 // indirect + github.com/containerd/containerd v1.7.33 // indirect + github.com/containerd/containerd/api v1.10.0 // indirect github.com/containerd/continuity v0.5.0 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/fifo v1.1.0 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v1.0.0-rc.4 // indirect - github.com/containerd/stargz-snapshotter/estargz v0.18.2 // indirect github.com/containerd/ttrpc v1.2.8 // indirect - github.com/containerd/typeurl/v2 v2.2.3 // indirect + github.com/containerd/typeurl/v2 v2.3.0 // indirect github.com/cyphar/filepath-securejoin v0.6.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/deitch/magic v0.0.0-20240306090643-c67ab88f10cb // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/cli v29.4.3+incompatible // indirect github.com/docker/docker-credential-helpers v0.9.5 // indirect - github.com/docker/go-connections v0.6.0 // indirect + github.com/docker/go-connections v0.7.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect github.com/dustin/go-humanize v1.0.1 // indirect @@ -174,7 +172,7 @@ require ( github.com/google/btree v1.1.3 // indirect github.com/google/gnostic-models v0.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect - github.com/google/go-containerregistry v0.21.5 // indirect + github.com/google/go-containerregistry v0.21.6 // indirect github.com/google/licensecheck v0.3.1 // indirect github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 // indirect github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect @@ -199,7 +197,7 @@ require ( github.com/json-iterator/go v1.1.12 // indirect github.com/kastenhq/goversion v0.0.0-20230811215019-93b2f8823953 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect - github.com/klauspost/compress v1.18.5 // indirect + github.com/klauspost/compress v1.18.6 // indirect github.com/klauspost/pgzip v1.2.6 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect @@ -271,7 +269,6 @@ require ( github.com/therootcompany/xz v1.0.1 // indirect github.com/ulikunitz/xz v0.5.15 // indirect github.com/vbatts/go-mtree v0.7.0 // indirect - github.com/vbatts/tar-split v0.12.2 // indirect github.com/vifraa/gopom v1.0.0 // indirect github.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651 // indirect github.com/wagoodman/go-progress v0.0.0-20260303201901-10176f79b2c0 // indirect @@ -288,20 +285,20 @@ require ( go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect - go.opentelemetry.io/otel v1.43.0 // indirect - go.opentelemetry.io/otel/metric v1.43.0 // indirect - go.opentelemetry.io/otel/trace v1.43.0 // indirect + go.opentelemetry.io/otel v1.44.0 // indirect + go.opentelemetry.io/otel/metric v1.44.0 // indirect + go.opentelemetry.io/otel/trace v1.44.0 // indirect go.uber.org/mock v0.6.0 // indirect - go.yaml.in/yaml/v2 v2.4.3 // indirect + go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect - golang.org/x/mod v0.35.0 // indirect - golang.org/x/net v0.55.0 // indirect + golang.org/x/mod v0.37.0 // indirect + golang.org/x/net v0.56.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect - golang.org/x/sys v0.45.0 // indirect - golang.org/x/term v0.43.0 // indirect + golang.org/x/sys v0.46.0 // indirect + golang.org/x/term v0.44.0 // indirect golang.org/x/time v0.15.0 // indirect - golang.org/x/tools v0.44.0 // indirect + golang.org/x/tools v0.47.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d // indirect @@ -333,9 +330,7 @@ require ( sigs.k8s.io/yaml v1.6.0 // indirect ) -replace github.com/containerd/containerd => github.com/containerd/containerd v1.7.33 - -replace github.com/containerd/containerd/v2 => github.com/containerd/containerd/v2 v2.1.5 +replace github.com/docker/go-connections => github.com/docker/go-connections v0.6.0 replace github.com/opencontainers/selinux => github.com/opencontainers/selinux v1.13.0 diff --git a/go.sum b/go.sum index d9aa5f1a..9e851447 100644 --- a/go.sum +++ b/go.sum @@ -45,8 +45,8 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -cyphar.com/go-pathrs v0.2.1 h1:9nx1vOgwVvX1mNBWDu93+vaceedpbsDqo+XuBGL40b8= -cyphar.com/go-pathrs v0.2.1/go.mod h1:y8f1EMG7r+hCuFf/rXsKqMJrJAUoADZGNh5/vZPKcGc= +cyphar.com/go-pathrs v0.2.4 h1:iD/mge36swa1UFKdINkr1Frkpp6wZsy3YYEildj9cLY= +cyphar.com/go-pathrs v0.2.4/go.mod h1:y8f1EMG7r+hCuFf/rXsKqMJrJAUoADZGNh5/vZPKcGc= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= @@ -105,8 +105,8 @@ github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA4 github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29 h1:0kQAzHq8vLs7Pptv+7TxjdETLf/nIqJpIB4oC6Ba4vY= github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29/go.mod h1:ZWa7ssZJT30CCDGJ7fk/2SBTq9BIQrrVjrcss0UW2s0= -github.com/Microsoft/hcsshim v0.15.0-rc.1 h1:FbbwtQmiD+BVHynGkx5S65JkLyhkEiiTP8nrpmg2SZw= -github.com/Microsoft/hcsshim v0.15.0-rc.1/go.mod h1:HWvvUPIy9HF6LotILj1G4VyS065rcLQ6tqj6tMUdOfI= +github.com/Microsoft/hcsshim v0.15.0-rc.3 h1:ZTNzOp0QwJ1EiL3zopSOawIG0j7zAvzJx0rBmcR6HJ0= +github.com/Microsoft/hcsshim v0.15.0-rc.3/go.mod h1:VhDiwXgb8cEJxO9H57YL4NNIYqvZKpqvSDcimLyo7m8= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8= github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q= @@ -255,8 +255,8 @@ github.com/containerd/cgroups/v3 v3.1.3 h1:eUNflyMddm18+yrDmZPn3jI7C5hJ9ahABE5q6 github.com/containerd/cgroups/v3 v3.1.3/go.mod h1:PKZ2AcWmSBsY/tJUVhtS/rluX0b1uq1GmPO1ElCmbOw= github.com/containerd/containerd v1.7.33 h1:iAkYGC/ifR/V+0eR4iXWHNGYUF0DF2PmGV5iz4Irj5M= github.com/containerd/containerd v1.7.33/go.mod h1:gSbSCVjPCdkfJCjyrzz7aRC+xFlqVbatNpfHfVCYGUM= -github.com/containerd/containerd/api v1.9.0 h1:HZ/licowTRazus+wt9fM6r/9BQO7S0vD5lMcWspGIg0= -github.com/containerd/containerd/api v1.9.0/go.mod h1:GhghKFmTR3hNtyznBoQ0EMWr9ju5AqHjcZPsSpTKutI= +github.com/containerd/containerd/api v1.10.0 h1:5n0oHYVBwN4VhoX9fFykCV9dF1/BvAXeg2F8W6UYq1o= +github.com/containerd/containerd/api v1.10.0/go.mod h1:NBm1OAk8ZL+LG8R0ceObGxT5hbUYj7CzTmR3xh0DlMM= github.com/containerd/continuity v0.5.0 h1:7a85HZpCSs+1Zps0Ee3DPSuAWY+0SJM1JNM51nlEVDg= github.com/containerd/continuity v0.5.0/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= @@ -269,12 +269,10 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v1.0.0-rc.4 h1:M42JrUT4zfZTqtkUwkr0GzmUWbfyO5VO0Q5b3op97T4= github.com/containerd/platforms v1.0.0-rc.4/go.mod h1:lKlMXyLybmBedS/JJm11uDofzI8L2v0J2ZbYvNsbq1A= -github.com/containerd/stargz-snapshotter/estargz v0.18.2 h1:yXkZFYIzz3eoLwlTUZKz2iQ4MrckBxJjkmD16ynUTrw= -github.com/containerd/stargz-snapshotter/estargz v0.18.2/go.mod h1:XyVU5tcJ3PRpkA9XS2T5us6Eg35yM0214Y+wvrZTBrY= github.com/containerd/ttrpc v1.2.8 h1:xbVu6D4qF2jihdh9rDVOKqUMiFBQk6YctTdo1zk087Y= github.com/containerd/ttrpc v1.2.8/go.mod h1:wyZW2K79t4Hfcxl+GUvkZqRBzJlqFFvgEeeWXa42tyE= -github.com/containerd/typeurl/v2 v2.2.3 h1:yNA/94zxWdvYACdYO8zofhrTVuQY73fFU1y++dYSw40= -github.com/containerd/typeurl/v2 v2.2.3/go.mod h1:95ljDnPfD3bAbDJRugOiShd/DlAAsxGtUBhJxIn7SCk= +github.com/containerd/typeurl/v2 v2.3.0 h1:HZHPhRWo5XMy3QGQoPrUzbW/2ckwjfweHmOwlkIrPAQ= +github.com/containerd/typeurl/v2 v2.3.0/go.mod h1:Qk+PAdUYArVj41TnGi6rJ+48RF0PkcTc4i/taoBcK0w= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= @@ -502,8 +500,8 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/go-containerregistry v0.21.5 h1:KTJG9Pn/jC0VdZR6ctV3/jcN+q6/Iqlx0sTVz3ywZlM= -github.com/google/go-containerregistry v0.21.5/go.mod h1:ySvMuiWg+dOsRW0Hw8GYwfMwBlNRTmpYBFJPlkco5zU= +github.com/google/go-containerregistry v0.21.6 h1:T+yqQIlJXKrM98Om4DlW3GoWQAmhZuLMwoDOvVrtiUM= +github.com/google/go-containerregistry v0.21.6/go.mod h1:U7MMSBIJynke2MVQrQk19NP9k/uQsGz/h0amIFSHMbo= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/jsonschema-go v0.4.3 h1:/DBOLZTfDow7pe2GmaJNhltueGTtDKICi8V8p+DQPd0= github.com/google/jsonschema-go v0.4.3/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= @@ -656,8 +654,8 @@ github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= -github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= -github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= +github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= @@ -989,8 +987,6 @@ github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/vbatts/go-mtree v0.7.0 h1:ytmOc3MTRidZiBi9VBCyZ2BHe4fZS47L5v7BVXDWW4E= github.com/vbatts/go-mtree v0.7.0/go.mod h1:EjdpFC+LZy1TXbRGNa1MKKgjQ+7ew3foMFJK8o4/TdY= -github.com/vbatts/tar-split v0.12.2 h1:w/Y6tjxpeiFMR47yzZPlPj/FcPLpXbTUi/9H7d3CPa4= -github.com/vbatts/tar-split v0.12.2/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA= github.com/vifraa/gopom v1.0.0 h1:L9XlKbyvid8PAIK8nr0lihMApJQg/12OBvMA28BcWh0= github.com/vifraa/gopom v1.0.0/go.mod h1:oPa1dcrGrtlO37WPDBm5SqHAT+wTgF8An1Q71Z6Vv4o= github.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651 h1:jIVmlAFIqV3d+DOxazTR9v+zgj8+VYuQBzPgBZvWBHA= @@ -1052,8 +1048,8 @@ go.opentelemetry.io/contrib/exporters/autoexport v0.67.0 h1:4fnRcNpc6YFtG3zsFw9a go.opentelemetry.io/contrib/exporters/autoexport v0.67.0/go.mod h1:qTvIHMFKoxW7HXg02gm6/Wofhq5p3Ib/A/NNt1EoBSQ= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo= -go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= -go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= +go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.18.0 h1:deI9UQMoGFgrg5iLPgzueqFPHevDl+28YKfSpPTI6rY= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.18.0/go.mod h1:PFx9NgpNUKXdf7J4Q3agRxMs3Y07QhTCVipKmLsMKnU= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 h1:HIBTQ3VO5aupLKjC90JgMqpezVXwFuq6Ryjn0/izoag= @@ -1078,16 +1074,16 @@ go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0 h1:s/1iRkCKDfhlh1J go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0/go.mod h1:UI3wi0FXg1Pofb8ZBiBLhtMzgoTm1TYkMvn71fAqDzs= go.opentelemetry.io/otel/log v0.19.0 h1:KUZs/GOsw79TBBMfDWsXS+KZ4g2Ckzksd1ymzsIEbo4= go.opentelemetry.io/otel/log v0.19.0/go.mod h1:5DQYeGmxVIr4n0/BcJvF4upsraHjg6vudJJpnkL6Ipk= -go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= -go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= +go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= go.opentelemetry.io/otel/sdk/log v0.19.0 h1:scYVLqT22D2gqXItnWiocLUKGH9yvkkeql5dBDiXyko= go.opentelemetry.io/otel/sdk/log v0.19.0/go.mod h1:vFBowwXGLlW9AvpuF7bMgnNI95LiW10szrOdvzBHlAg= go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= -go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= -go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= +go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= @@ -1098,8 +1094,8 @@ go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= -go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= -go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= +go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -1116,8 +1112,8 @@ golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= -golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= -golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= +golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= +golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -1157,8 +1153,8 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= -golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= +golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= +golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1204,8 +1200,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.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= -golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= +golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= +golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= 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= @@ -1237,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.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +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/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= @@ -1315,13 +1311,13 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= -golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= -golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= +golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= +golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1332,8 +1328,8 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= -golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus= +golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1394,8 +1390,8 @@ golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= -golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= +golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= +golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1539,8 +1535,8 @@ google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnD google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= -google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= -google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= +google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ= +google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= diff --git a/internal/commands/agenthooks/cx/hooks.go b/internal/commands/agenthooks/cx/hooks.go index 14b81c7f..c2f0ac03 100644 --- a/internal/commands/agenthooks/cx/hooks.go +++ b/internal/commands/agenthooks/cx/hooks.go @@ -3,17 +3,61 @@ package cx import ( "log" "os" + "path/filepath" "strings" agenthooks "github.com/Checkmarx/ast-cx-hooks" + "github.com/Checkmarx/ast-cx-hooks/claude" "github.com/Checkmarx/ast-cx-hooks/cursor" "github.com/checkmarx/ast-cli/internal/commands/agenthooks/guardrails" "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/sca" + "github.com/checkmarx/ast-cli/internal/commands/agenthooks/sessiontally" "github.com/checkmarx/ast-cli/internal/wrappers" ) +// canonical per-engine labels for the session summary (normalizes the legacy SCA/Oss split used by +// the per-event remediation telemetry; ASCA already uses "Asca"). +const ( + engineAsca = "Asca" + engineSca = "Sca" + engineKics = "Kics" +) + +// markerDirPerm / markerFilePerm are the permissions used when creating the session findings +// marker file's directory and the file itself. +const ( + markerDirPerm = 0o700 + markerFilePerm = 0o600 +) + +// sessionFindingsMarker is the path of the per-session marker file that records +// whether at least one real Checkmarx finding was blocked this session. +// cxBeforeFileEdit / cxBeforeToolCall create it on any scanner denial; +// cxWhenAgentIdle consumes it to decide whether to force a summary turn. +func sessionFindingsMarkerPath() string { + home, err := os.UserHomeDir() + if err != nil { + return "" + } + return filepath.Join(home, ".checkmarx", ".session-cx-findings") +} + +// touchSessionFindingsMarker creates the marker file best-effort. +// A write failure must never affect the scan decision that called it. +func touchSessionFindingsMarker() { + path := sessionFindingsMarkerPath() + if path == "" { + return + } + _ = os.MkdirAll(filepath.Dir(path), markerDirPerm) + f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, markerFilePerm) + if err == nil { + _ = f.Close() + } +} + // scaScanner is the package-level SCA scanner used by the guardrail handlers. // It is set by RegisterGuardrails so the handlers (free functions registered // with the agenthooks library) can reach it without an injection mechanism. @@ -25,11 +69,21 @@ var kicsScanner *kics.Scanner var telemetryWrapper wrappers.TelemetryWrapper -// cxWhenAgentIdle: agent finished its turn. Nothing to enforce yet. func cxWhenAgentIdle(_ agenthooks.AgentIdleEvent) agenthooks.IdleVerdict { return agenthooks.Resume() } +// sessionIDFromToolCall recovers the Claude session id for a ToolCall event. Unlike FileEditEvent, +// the library's ToolCallEvent carries no SessionID field; only Claude's raw payload is assertable +// (mirrors promptWorkspaceRoots' Raw type-assertion). Every non-Claude agent falls back to "" (the +// shared default tally bucket), which the Stop handler also reads and clears. +func sessionIDFromToolCall(ev *agenthooks.ToolCallEvent) string { + if e, ok := ev.Raw.(*claude.PreToolUseEvent); ok { + return e.SessionID + } + return "" +} + // cxBeforeToolCall gates shell execution against the organization's blacklist, // tool rules, and the SCA guardrail (malicious / vulnerable package installs). func cxBeforeToolCall(ev agenthooks.ToolCallEvent) agenthooks.ToolVerdict { @@ -45,7 +99,10 @@ func cxBeforeToolCall(ev agenthooks.ToolCallEvent) agenthooks.ToolVerdict { } if scaScanner != nil { if finding, remediation := scaScanner.CheckBashInstall(ev.Command, ev.WorkDir); finding != "" { + touchSessionFindingsMarker() agent := agentToString(ev.Agent) + sid := sessionIDFromToolCall(&ev) + sessiontally.Add(sid, engineSca, 1, 1) logRemediationTelemetry(agent, "SCA", finding, remediation) return agenthooks.DenyWithContext(finding, remediation) } @@ -90,17 +147,23 @@ func cxBeforeFileEdit(ev agenthooks.FileEditEvent) agenthooks.FileEditVerdict { } agent := agentToString(ev.Agent) if blocked, reason, context := asca.ScanFileEdit(ev, telemetryWrapper, agent); blocked { + touchSessionFindingsMarker() + sessiontally.Add(ev.SessionID, engineAsca, 1, 1) logRemediationTelemetry(agent, "Asca", reason, context) return agenthooks.RejectEditWithContext(reason, context) } if kicsScanner != nil { if blocked, reason, context := kics.ScanFileEdit(ev, kicsScanner); blocked { + touchSessionFindingsMarker() + sessiontally.Add(ev.SessionID, engineKics, 1, 1) return agenthooks.RejectEditWithContext(reason, context) } } if scaScanner != nil { for _, diff := range ev.Changes { if finding, remediation := scaScanner.CheckManifestEdit(ev.FilePath, fullAfterContent(ev.FilePath, diff), ev.WorkDir); finding != "" { + touchSessionFindingsMarker() + sessiontally.Add(ev.SessionID, engineSca, 1, 1) logRemediationTelemetry(agent, "Oss", finding, remediation) return agenthooks.RejectEditWithContext(finding, remediation) } diff --git a/internal/commands/agenthooks/cx/hooks_test.go b/internal/commands/agenthooks/cx/hooks_test.go new file mode 100644 index 00000000..0ac3b4b0 --- /dev/null +++ b/internal/commands/agenthooks/cx/hooks_test.go @@ -0,0 +1,22 @@ +//go:build !integration + +package cx + +import ( + "testing" + + agenthooks "github.com/Checkmarx/ast-cx-hooks" + "github.com/Checkmarx/ast-cx-hooks/claude" +) + +func TestSessionIDFromToolCall(t *testing.T) { + claudeEv := agenthooks.ToolCallEvent{ + Raw: &claude.PreToolUseEvent{EventBase: claude.EventBase{SessionID: "S9"}}, + } + if got := sessionIDFromToolCall(&claudeEv); got != "S9" { + t.Errorf("claude raw: want S9, got %q", got) + } + if got := sessionIDFromToolCall(&agenthooks.ToolCallEvent{Raw: nil}); got != "" { + t.Errorf("nil raw: want empty, got %q", got) + } +} diff --git a/internal/commands/agenthooks/guardrails/policy.go b/internal/commands/agenthooks/guardrails/policy.go index 420827d6..0c6a13c4 100644 --- a/internal/commands/agenthooks/guardrails/policy.go +++ b/internal/commands/agenthooks/guardrails/policy.go @@ -488,7 +488,8 @@ func ResolveRestrictedPaths(globalPaths, rulePaths []string, strategy string) [] // strip the leading slash before a drive letter so PathUnderAny's prefix match // lines up with policy entries like "C:\\foo\\bar\\". func NormalizeWorkspaceRoot(root string) string { - r := filepath.ToSlash(root) + // Explicit replace (not filepath.ToSlash) so backslashes normalize on any host OS, not just Windows. + r := strings.ReplaceAll(root, "\\", "/") if len(r) >= 3 && r[0] == '/' && isASCIILetter(r[1]) && r[2] == ':' { r = r[1:] } diff --git a/internal/commands/agenthooks/sca/diff.go b/internal/commands/agenthooks/sca/diff.go index e51e8318..a3254153 100644 --- a/internal/commands/agenthooks/sca/diff.go +++ b/internal/commands/agenthooks/sca/diff.go @@ -11,16 +11,17 @@ import ( // before, keyed by name+version. A version bump on an existing package is // reported as added (its new version is new). // -// Both before and after are parsed via manifest-parser's factory; we write -// them to temp files using a name that matches the format so the factory -// selects the right parser. An empty/missing before parses as zero packages -// (so every after-package is "added"). -func AddedPackages(format Format, before, after []byte) ([]Package, error) { - beforePkgs, err := parseManifestBytes(format, before) +// manifestPath is the real path of the edited file — used only for its +// basename, so manifest-parser's factory picks the parser that matches the +// file being diffed (IsManifest already confirmed it recognises the name). +// Both before and after are parsed via that same parser. An empty/missing +// before parses as zero packages (so every after-package is "added"). +func AddedPackages(manifestPath string, before, after []byte) ([]Package, error) { + beforePkgs, err := parseManifestBytes(manifestPath, before) if err != nil { return nil, err } - afterPkgs, err := parseManifestBytes(format, after) + afterPkgs, err := parseManifestBytes(manifestPath, after) if err != nil { return nil, err } @@ -39,7 +40,7 @@ func AddedPackages(format Format, before, after []byte) ([]Package, error) { return added, nil } -func parseManifestBytes(format Format, content []byte) ([]Package, error) { +func parseManifestBytes(manifestPath string, content []byte) ([]Package, error) { if len(content) == 0 { return nil, nil } @@ -49,7 +50,7 @@ func parseManifestBytes(format Format, content []byte) ([]Package, error) { } defer os.RemoveAll(dir) - path := filepath.Join(dir, format.SynthFileName()) + path := filepath.Join(dir, filepath.Base(manifestPath)) if writeErr := os.WriteFile(path, content, 0600); writeErr != nil { return nil, writeErr } diff --git a/internal/commands/agenthooks/sca/diff_test.go b/internal/commands/agenthooks/sca/diff_test.go index 19844ab5..b24c647b 100644 --- a/internal/commands/agenthooks/sca/diff_test.go +++ b/internal/commands/agenthooks/sca/diff_test.go @@ -6,10 +6,13 @@ import ( "testing" ) +// wantComExampleBar is the added-package name shared by the Gradle and Sbt "added package" tests. +const wantComExampleBar = "com.example:bar" + func TestAddedPackages_Npm_AddedNewPackage(t *testing.T) { before := []byte(`{"name":"x","version":"1.0.0","dependencies":{"lodash":"4.17.21"}}`) after := []byte(`{"name":"x","version":"1.0.0","dependencies":{"lodash":"4.17.21","axios":"1.0.0"}}`) - added, err := AddedPackages(FormatNpmPackageJson, before, after) + added, err := AddedPackages("package.json", before, after) if err != nil { t.Fatalf("AddedPackages: %v", err) } @@ -21,7 +24,7 @@ func TestAddedPackages_Npm_AddedNewPackage(t *testing.T) { func TestAddedPackages_Npm_VersionBumpCountsAsAdded(t *testing.T) { before := []byte(`{"name":"x","version":"1.0.0","dependencies":{"lodash":"4.17.0"}}`) after := []byte(`{"name":"x","version":"1.0.0","dependencies":{"lodash":"4.17.21"}}`) - added, err := AddedPackages(FormatNpmPackageJson, before, after) + added, err := AddedPackages("package.json", before, after) if err != nil { t.Fatalf("AddedPackages: %v", err) } @@ -33,7 +36,7 @@ func TestAddedPackages_Npm_VersionBumpCountsAsAdded(t *testing.T) { func TestAddedPackages_Npm_RemovedPackageIsIgnored(t *testing.T) { before := []byte(`{"name":"x","version":"1.0.0","dependencies":{"lodash":"4.17.21","axios":"1.0.0"}}`) after := []byte(`{"name":"x","version":"1.0.0","dependencies":{"lodash":"4.17.21"}}`) - added, err := AddedPackages(FormatNpmPackageJson, before, after) + added, err := AddedPackages("package.json", before, after) if err != nil { t.Fatalf("AddedPackages: %v", err) } @@ -44,7 +47,7 @@ func TestAddedPackages_Npm_RemovedPackageIsIgnored(t *testing.T) { func TestAddedPackages_Npm_NewFile(t *testing.T) { after := []byte(`{"name":"x","version":"1.0.0","dependencies":{"lodash":"4.17.21","axios":"1.0.0"}}`) - added, err := AddedPackages(FormatNpmPackageJson, nil, after) + added, err := AddedPackages("package.json", nil, after) if err != nil { t.Fatalf("AddedPackages: %v", err) } @@ -56,7 +59,7 @@ func TestAddedPackages_Npm_NewFile(t *testing.T) { func TestAddedPackages_Pypi_AddedPackage(t *testing.T) { before := []byte("requests==2.25.1\n") after := []byte("requests==2.25.1\nflask==2.0.0\n") - added, err := AddedPackages(FormatPypiRequirements, before, after) + added, err := AddedPackages("requirements.txt", before, after) if err != nil { t.Fatalf("AddedPackages: %v", err) } @@ -65,10 +68,35 @@ func TestAddedPackages_Pypi_AddedPackage(t *testing.T) { } } +func TestAddedPackages_Gradle_AddedPackage(t *testing.T) { + before := []byte("dependencies {\n implementation 'com.example:foo:1.0.0'\n}\n") + after := []byte("dependencies {\n implementation 'com.example:foo:1.0.0'\n implementation 'com.example:bar:2.0.0'\n}\n") + added, err := AddedPackages("build.gradle", before, after) + if err != nil { + t.Fatalf("AddedPackages: %v", err) + } + if len(added) != 1 || added[0].Name != wantComExampleBar { + t.Errorf("got added=%v, want [com.example:bar]", added) + } +} + +func TestAddedPackages_Sbt_AddedPackage(t *testing.T) { + before := []byte(`libraryDependencies += "com.example" % "foo" % "1.0.0"` + "\n") + after := []byte(`libraryDependencies += "com.example" % "foo" % "1.0.0"` + "\n" + + `libraryDependencies += "com.example" % "bar" % "2.0.0"` + "\n") + added, err := AddedPackages("build.sbt", before, after) + if err != nil { + t.Fatalf("AddedPackages: %v", err) + } + if len(added) != 1 || added[0].Name != wantComExampleBar { + t.Errorf("got added=%v, want [com.example:bar]", added) + } +} + func TestAddedPackages_UnparseableContent(t *testing.T) { // Note: behaviour for unparseable content depends on the upstream parser. // We assert that errors flow back to the caller, not that any specific // content causes an error — the caller's contract is "treat errors as // fail-open" so callers don't depend on a particular outcome here. - _, _ = AddedPackages(FormatNpmPackageJson, nil, []byte("{not valid json")) + _, _ = AddedPackages("package.json", nil, []byte("{not valid json")) } diff --git a/internal/commands/agenthooks/sca/manifests.go b/internal/commands/agenthooks/sca/manifests.go index 0d670234..0652c728 100644 --- a/internal/commands/agenthooks/sca/manifests.go +++ b/internal/commands/agenthooks/sca/manifests.go @@ -5,6 +5,8 @@ package sca import ( "path/filepath" "strings" + + "github.com/checkmarx/ast-cli/internal/services/realtimeengine/ossrealtime" ) // Format identifies one of the manifest shapes that the SCA guardrails care @@ -21,26 +23,51 @@ const ( FormatDotnetCsproj FormatDotnetDirectoryPackagesProps FormatDotnetPackagesConfig + FormatGradleBuild + FormatGradleVersionCatalog + FormatSbtBuild +) + +// gradleBuildFileName and gradleVersionCatalogFileName are the canonical basenames for the Gradle +// manifest formats, shared between the classifier switch below and SynthFileName. +const ( + gradleBuildFileName = "build.gradle" + gradleVersionCatalogFileName = "libs.versions.toml" ) // IsManifest reports whether path names a manifest file the OSS realtime -// scanner can analyse. The rules mirror manifest-parser's selectManifestFile: +// scanner can analyse. ossrealtime.IsSupportedManifestFile is the single +// source of truth for which filenames the scanner accepts — it is also used +// to gate the actual scan, so hooks and the scanner can never drift apart on +// what counts as a supported manifest. The switch below only classifies an +// already-accepted path into the Format the guardrails need to re-synthesise +// a minimal manifest for the added packages (see Synthesize). // -// - *.csproj → Dotnet csproj -// - requirements*.txt, packages*.txt → Pypi requirements -// - pom.xml → Maven -// - package.json → Npm -// - Directory.Packages.props → Dotnet central package management -// - packages.config → Dotnet legacy -// - go.mod → Go modules +// - *.csproj → Dotnet csproj +// - *.sbt → Sbt build +// - requirements*.txt, packages*.txt, constraint*.txt → Pypi requirements +// - setup.cfg, setup.py, pyproject.toml → Pypi (alt. formats) +// - pom.xml → Maven +// - package.json → Npm +// - Directory.Packages.props → Dotnet central package management +// - packages.config → Dotnet legacy +// - go.mod → Go modules +// - build.gradle, build.gradle.kts → Gradle build +// - libs.versions.toml → Gradle version catalog func IsManifest(path string) (Format, bool) { + if !ossrealtime.IsSupportedManifestFile(path) { + return FormatUnknown, false + } + base := filepath.Base(path) ext := filepath.Ext(base) switch { case ext == ".csproj": return FormatDotnetCsproj, true - case ext == ".txt" && (strings.HasPrefix(base, "requirement") || strings.HasPrefix(base, "packages")): + case ext == ".sbt": + return FormatSbtBuild, true + case ext == ".txt" && (strings.HasPrefix(base, "requirement") || strings.HasPrefix(base, "packages") || strings.HasPrefix(base, "constraint")): return FormatPypiRequirements, true case base == "pom.xml": return FormatMavenPom, true @@ -52,6 +79,12 @@ func IsManifest(path string) (Format, bool) { return FormatDotnetPackagesConfig, true case base == "go.mod": return FormatGoMod, true + case base == gradleBuildFileName, base == gradleBuildFileName+".kts": + return FormatGradleBuild, true + case base == gradleVersionCatalogFileName: + return FormatGradleVersionCatalog, true + case base == "setup.cfg", base == "setup.py", base == "pyproject.toml": + return FormatPypiRequirements, true } return FormatUnknown, false } @@ -70,6 +103,10 @@ func (f Format) ManagerName() string { return "maven" case FormatDotnetCsproj, FormatDotnetDirectoryPackagesProps, FormatDotnetPackagesConfig: return "nuget" + case FormatGradleBuild, FormatGradleVersionCatalog: + return "gradle" + case FormatSbtBuild: + return "sbt" } return "" } @@ -93,6 +130,12 @@ func (f Format) SynthFileName() string { return "Directory.Packages.props" case FormatDotnetPackagesConfig: return "packages.config" + case FormatGradleBuild: + return gradleBuildFileName + case FormatGradleVersionCatalog: + return gradleVersionCatalogFileName + case FormatSbtBuild: + return "synth.sbt" } return "" } diff --git a/internal/commands/agenthooks/sca/manifests_test.go b/internal/commands/agenthooks/sca/manifests_test.go index 801f22e5..74784cf4 100644 --- a/internal/commands/agenthooks/sca/manifests_test.go +++ b/internal/commands/agenthooks/sca/manifests_test.go @@ -21,6 +21,16 @@ func TestIsManifest(t *testing.T) { {"Project.csproj", true, FormatDotnetCsproj}, {"Directory.Packages.props", true, FormatDotnetDirectoryPackagesProps}, {"packages.config", true, FormatDotnetPackagesConfig}, + {"build.gradle", true, FormatGradleBuild}, + {"build.gradle.kts", true, FormatGradleBuild}, + {"/repo/app/build.gradle.kts", true, FormatGradleBuild}, + {"libs.versions.toml", true, FormatGradleVersionCatalog}, + {"build.sbt", true, FormatSbtBuild}, + {"plugins.sbt", true, FormatSbtBuild}, + {"constraints.txt", true, FormatPypiRequirements}, + {"setup.cfg", true, FormatPypiRequirements}, + {"setup.py", true, FormatPypiRequirements}, + {"pyproject.toml", true, FormatPypiRequirements}, // Negatives. {"main.go", false, FormatUnknown}, diff --git a/internal/commands/agenthooks/sca/prompts.go b/internal/commands/agenthooks/sca/prompts.go index d80268d0..b128cfad 100644 --- a/internal/commands/agenthooks/sca/prompts.go +++ b/internal/commands/agenthooks/sca/prompts.go @@ -35,20 +35,23 @@ func DenyVulnerable(pkgs []ossrealtime.OssPackage, workDir string) (finding, rem } // remediationNote returns the action steps to include as additionalContext for malicious packages. -// Remediation must go through the MCP tool; if it is unavailable Claude must self-recover -// by running the registration helper rather than asking the user. +// Remediation must go through the cx-devassist-sca skill (or MCP fallback); if MCP is unavailable +// Claude must self-recover by running the registration helper rather than asking the user. func remediationNote(subject, goal string) string { return fmt.Sprintf( "Action required:\n"+ - " 1. Use mcp__Checkmarx__packageRemediation for each %s to find the %s.\n"+ + " 1. For each %s, invoke the cx-devassist:cx-devassist-sca skill — "+ + "the findings are already in context so it will skip the scan and go directly to "+ + "MCP-driven remediation to find the %s; the skill also handles MCP unavailability and self-recovery.\n"+ + " 2. If that skill is not available in this session, use mcp__Checkmarx__packageRemediation for each %s.\n"+ " This is the only supported remediation path — do not attempt manual version selection.\n"+ - " 2. If mcp__Checkmarx__packageRemediation is unavailable, recover it yourself — do NOT ask\n"+ + " 3. If mcp__Checkmarx__packageRemediation is unavailable, recover it yourself — do NOT ask\n"+ " the user. Run: bash \"${CLAUDE_PLUGIN_ROOT}/mcp/cx_mcp_register.sh\"\n"+ " Then tell the user: 'I have registered the Checkmarx MCP. Please run /mcp (or restart\n"+ " Claude Code) to load it, then retry.' Do not proceed until the MCP is available.\n"+ - " 3. If no safe version exists, the user can acknowledge the finding via\n"+ + " 4. If no safe version exists, the user can acknowledge the finding via\n"+ " the Checkmarx Dev Assist interface.", - subject, goal) + subject, goal, subject) } // vulnerableRemediationNote returns the action steps for vulnerable packages. @@ -68,13 +71,16 @@ func vulnerableRemediationNote(pkgs []ossrealtime.OssPackage, workDir string) st } return fmt.Sprintf( "Action required:\n"+ - " 1. Use mcp__Checkmarx__packageRemediation for each affected package to find the non-vulnerable version.\n"+ + " 1. For each affected package, invoke the cx-devassist:cx-devassist-sca skill — "+ + "the findings are already in context so it will skip the scan and go directly to "+ + "MCP-driven remediation to find non-vulnerable versions; the skill also handles MCP unavailability and self-recovery.\n"+ + " 2. If that skill is not available in this session, use mcp__Checkmarx__packageRemediation for each affected package.\n"+ " This is the only supported remediation path — do not attempt manual version selection.\n"+ - " 2. If mcp__Checkmarx__packageRemediation is unavailable, recover it yourself — do NOT ask\n"+ + " 3. If mcp__Checkmarx__packageRemediation is unavailable, recover it yourself — do NOT ask\n"+ " the user. Run: bash \"${CLAUDE_PLUGIN_ROOT}/mcp/cx_mcp_register.sh\"\n"+ " Then tell the user: 'I have registered the Checkmarx MCP. Please run /mcp (or restart\n"+ " Claude Code) to load it, then retry.' Do not proceed until the MCP is available.\n"+ - " 3. If no safe version exists for a package, suppress it by running the corresponding command\n"+ + " 4. If no safe version exists for a package, suppress it by running the corresponding command\n"+ " and inform the user that no safer version is available:\n%s", suppressCmds.String()) } diff --git a/internal/commands/agenthooks/sca/sca.go b/internal/commands/agenthooks/sca/sca.go index af01f0d0..0564a20a 100644 --- a/internal/commands/agenthooks/sca/sca.go +++ b/internal/commands/agenthooks/sca/sca.go @@ -53,7 +53,7 @@ func (s *Scanner) CheckManifestEdit(filePath string, afterContent []byte, workDi return "", "" } before, _ := os.ReadFile(filePath) // missing → empty before - added, err := AddedPackages(format, before, afterContent) + added, err := AddedPackages(filePath, before, afterContent) if err != nil || len(added) == 0 { return "", "" } diff --git a/internal/commands/agenthooks/sca/sca_test.go b/internal/commands/agenthooks/sca/sca_test.go index 7fe717ee..ce41affe 100644 --- a/internal/commands/agenthooks/sca/sca_test.go +++ b/internal/commands/agenthooks/sca/sca_test.go @@ -43,8 +43,11 @@ func TestCheckBashInstall_MaliciousMentionsMCP(t *testing.T) { if !strings.Contains(remediation, "mcp__Checkmarx__packageRemediation") { t.Errorf("expected remediation to reference MCP tool, got %q", remediation) } - if !strings.Contains(remediation, "install or enable") { - t.Errorf("expected remediation to mention installing/enabling MCP, got %q", remediation) + if !strings.Contains(remediation, "cx-devassist:cx-devassist-sca") { + t.Errorf("expected remediation to reference cx-devassist-sca skill, got %q", remediation) + } + if !strings.Contains(remediation, "cx_mcp_register.sh") { + t.Errorf("expected remediation to mention MCP registration script, got %q", remediation) } if !strings.Contains(remediation, "Dev Assist") { t.Errorf("expected remediation to mention Dev Assist fallback, got %q", remediation) @@ -60,6 +63,9 @@ func TestCheckBashInstall_Vulnerable(t *testing.T) { if !strings.Contains(remediation, "mcp__Checkmarx__packageRemediation") { t.Errorf("expected remediation to reference MCP tool, got %q", remediation) } + if !strings.Contains(remediation, "cx-devassist:cx-devassist-sca") { + t.Errorf("expected remediation to reference cx-devassist-sca skill, got %q", remediation) + } if !strings.Contains(remediation, "ignore-vulnerability") { t.Errorf("expected remediation to include ignore command, got %q", remediation) } diff --git a/internal/commands/agenthooks/sca/synth.go b/internal/commands/agenthooks/sca/synth.go index f0f99f34..b4f07734 100644 --- a/internal/commands/agenthooks/sca/synth.go +++ b/internal/commands/agenthooks/sca/synth.go @@ -36,6 +36,12 @@ func Synthesize(format Format, pkgs []Package, dir string) (string, error) { content = synthDirectoryPackagesProps(pkgs) case FormatDotnetPackagesConfig: content = synthPackagesConfig(pkgs) + case FormatGradleBuild: + content = synthGradleBuild(pkgs) + case FormatGradleVersionCatalog: + content = synthGradleVersionCatalog(pkgs) + case FormatSbtBuild: + content = synthSbt(pkgs) default: return "", fmt.Errorf("sca: unsupported synth format %d", format) } @@ -147,3 +153,50 @@ func synthPackagesConfig(pkgs []Package) []byte { b.WriteString("\n") return []byte(b.String()) } + +// synthGradleBuild writes one `implementation 'group:artifact:version'` line +// per package — the simplest form the Gradle parser's dependency regexes +// recognise. Name is "group:artifact" (mirrors Maven's groupId:artifactId). +func synthGradleBuild(pkgs []Package) []byte { + var b strings.Builder + for _, p := range pkgs { + group, artifact := splitMavenName(p.Name) + v := p.Version + if v == "" { + v = "latest" + } + fmt.Fprintf(&b, "implementation '%s:%s:%s'\n", group, artifact, v) + } + return []byte(b.String()) +} + +// synthGradleVersionCatalog writes a minimal libs.versions.toml [libraries] +// section — one inline "group:artifact:version" entry per package. +func synthGradleVersionCatalog(pkgs []Package) []byte { + var b strings.Builder + b.WriteString("[libraries]\n") + for i, p := range pkgs { + group, artifact := splitMavenName(p.Name) + v := p.Version + if v == "" { + v = "latest" + } + fmt.Fprintf(&b, "dep%d = \"%s:%s:%s\"\n", i, group, artifact, v) + } + return []byte(b.String()) +} + +// synthSbt writes one `libraryDependencies += "group" % "artifact" % "version"` +// line per package, the form the SBT parser's dependency regex recognises. +func synthSbt(pkgs []Package) []byte { + var b strings.Builder + for _, p := range pkgs { + group, artifact := splitMavenName(p.Name) + v := p.Version + if v == "" { + v = "latest" + } + fmt.Fprintf(&b, "libraryDependencies += \"%s\" %% \"%s\" %% \"%s\"\n", group, artifact, v) + } + return []byte(b.String()) +} diff --git a/internal/commands/agenthooks/sca/synth_test.go b/internal/commands/agenthooks/sca/synth_test.go index 3cf8fa34..83e6e75b 100644 --- a/internal/commands/agenthooks/sca/synth_test.go +++ b/internal/commands/agenthooks/sca/synth_test.go @@ -86,6 +86,27 @@ func TestSynthesize_PackagesConfig(t *testing.T) { }) } +func TestSynthesize_GradleBuild(t *testing.T) { + roundTrip(t, FormatGradleBuild, []Package{ + {Name: "com.example:foo", Version: "1.0.0"}, + {Name: "com.example:bar", Version: "2.0.0"}, + }) +} + +func TestSynthesize_GradleVersionCatalog(t *testing.T) { + roundTrip(t, FormatGradleVersionCatalog, []Package{ + {Name: "com.example:foo", Version: "1.0.0"}, + {Name: "com.example:bar", Version: "2.0.0"}, + }) +} + +func TestSynthesize_Sbt(t *testing.T) { + roundTrip(t, FormatSbtBuild, []Package{ + {Name: "com.example:foo", Version: "1.0.0"}, + {Name: "com.example:bar", Version: "2.0.0"}, + }) +} + func TestSynthesize_UnsupportedFormat(t *testing.T) { dir, _ := os.MkdirTemp("", "synth-test-") defer os.RemoveAll(dir) diff --git a/internal/commands/agenthooks/sessiontally/sessiontally.go b/internal/commands/agenthooks/sessiontally/sessiontally.go new file mode 100644 index 00000000..9d2196d8 --- /dev/null +++ b/internal/commands/agenthooks/sessiontally/sessiontally.go @@ -0,0 +1,193 @@ +// Package sessiontally accumulates per-engine scan counts across an AI-agent session so the +// session-end (Stop) hook can emit a per-engine telemetry summary. +// +// Why on disk: every `cx hooks ` invocation is a FRESH OS process (see +// internal/commands/agenthooks.go HookDispatchCommands). No in-memory state survives between the +// per-edit/per-tool detect events and the session-end idle event, so the running tally must live on +// disk — the same reason the sibling `.session-cx-findings` marker exists. +// +// Design guarantees (mirroring touchSessionFindingsMarker's contract): every function here is +// BEST-EFFORT and must NEVER return an error or panic into its caller — a tally write failure must +// never affect a scan verdict. Writes are append-only NDJSON so concurrent hook processes (parallel +// tool calls / multi-file edits) never lose records to a read-modify-write race without needing a +// lock file; Load folds the records into a per-engine map. +// +// Session scoping: files are keyed by a sanitized session id. Events whose session id could not be +// resolved (e.g. a non-Claude ToolCall, whose library event carries no SessionID) are written to a +// shared "default" bucket that Load also reads and Clear also removes. Two concurrent sessions can +// therefore both feed the default bucket and one session's Clear can discard the other's +// default-bucket counts — accepted best-effort noise, since the summary itself is best-effort. +package sessiontally + +import ( + "bufio" + "encoding/json" + "os" + "path/filepath" + "regexp" + "strings" + "time" +) + +// Counts is the per-engine roll-up Load returns. JSON tags let it serialize cleanly into the local +// session-summary audit log. +type Counts struct { + VulnerabilitiesFound int `json:"vulnerabilitiesFound"` + RemediationsOffered int `json:"remediationsOffered"` +} + +const ( + filePrefix = ".session-cx-tallies-" + fileSuffix = ".json" + defaultKey = "default" + maxAgeHours = 24 + maxIDLen = 128 + dirPerm = 0o700 + filePerm = 0o600 + // scanBufSize/scanMaxSize bound bufio.Scanner's growth when folding a tally file: + // start at 64KiB, allow growth up to 1MiB per line before Scan reports an error. + scanBufSize = 64 * 1024 + scanMaxSize = 1024 * 1024 +) + +// Only filename-safe characters survive; everything else (path separators, "..", spaces, control +// bytes) collapses to '_', so a hostile session id can never escape the ~/.checkmarx directory. +var sanitizeRe = regexp.MustCompile(`[^A-Za-z0-9._-]`) + +// record is one append-only NDJSON line. +type record struct { + Engine string `json:"engine"` + Found int `json:"found"` + RemOffered int `json:"remOffered"` +} + +func baseDir() (string, bool) { + home, err := os.UserHomeDir() + if err != nil || home == "" { + return "", false + } + return filepath.Join(home, ".checkmarx"), true +} + +func sanitize(sessionID string) string { + s := sanitizeRe.ReplaceAllString(sessionID, "_") + if s == "" { + s = defaultKey + } + if len(s) > maxIDLen { + s = s[:maxIDLen] + } + return s +} + +func tallyPath(sessionID string) (string, bool) { + dir, ok := baseDir() + if !ok { + return "", false + } + return filepath.Join(dir, filePrefix+sanitize(sessionID)+fileSuffix), true +} + +// Add appends one best-effort NDJSON record for (sessionID, engine). Never returns or panics. +func Add(sessionID, engine string, foundDelta, remOfferedDelta int) { + defer func() { _ = recover() }() + path, ok := tallyPath(sessionID) + if !ok { + return + } + if err := os.MkdirAll(filepath.Dir(path), dirPerm); err != nil { + return + } + line, err := json.Marshal(record{Engine: engine, Found: foundDelta, RemOffered: remOfferedDelta}) + if err != nil { + return + } + f, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, filePerm) + if err != nil { + return + } + defer func() { _ = f.Close() }() + _, _ = f.Write(append(line, '\n')) +} + +// Load folds the session's tally file AND the shared default file into a per-engine map. Best-effort: +// any read/parse error is skipped and whatever parsed so far is returned. Also opportunistically +// removes stale tally files (>24h) so a session whose Stop hook never fired cannot leak forever. +func Load(sessionID string) map[string]Counts { + out := map[string]Counts{} + defer func() { _ = recover() }() + seen := map[string]bool{} + for _, sid := range []string{sessionID, defaultKey} { + path, ok := tallyPath(sid) + if !ok || seen[path] { + continue + } + seen[path] = true + foldFile(path, out) + } + cleanupStale() + return out +} + +func foldFile(path string, out map[string]Counts) { + f, err := os.Open(path) + if err != nil { + return + } + defer func() { _ = f.Close() }() + sc := bufio.NewScanner(f) + sc.Buffer(make([]byte, 0, scanBufSize), scanMaxSize) + for sc.Scan() { + line := strings.TrimSpace(sc.Text()) + if line == "" { + continue + } + var r record + if json.Unmarshal([]byte(line), &r) != nil || r.Engine == "" { + continue + } + c := out[r.Engine] + c.VulnerabilitiesFound += r.Found + c.RemediationsOffered += r.RemOffered + out[r.Engine] = c + } +} + +// Clear removes the session's tally file AND the shared default file. Best-effort. +func Clear(sessionID string) { + defer func() { _ = recover() }() + seen := map[string]bool{} + for _, sid := range []string{sessionID, defaultKey} { + path, ok := tallyPath(sid) + if !ok || seen[path] { + continue + } + seen[path] = true + _ = os.Remove(path) + } +} + +func cleanupStale() { + dir, ok := baseDir() + if !ok { + return + } + entries, err := os.ReadDir(dir) + if err != nil { + return + } + cutoff := time.Now().Add(-maxAgeHours * time.Hour) + for _, e := range entries { + name := e.Name() + if !strings.HasPrefix(name, filePrefix) || !strings.HasSuffix(name, fileSuffix) { + continue + } + info, err := e.Info() + if err != nil { + continue + } + if info.ModTime().Before(cutoff) { + _ = os.Remove(filepath.Join(dir, name)) + } + } +} diff --git a/internal/commands/agenthooks/sessiontally/sessiontally_test.go b/internal/commands/agenthooks/sessiontally/sessiontally_test.go new file mode 100644 index 00000000..dbe153c7 --- /dev/null +++ b/internal/commands/agenthooks/sessiontally/sessiontally_test.go @@ -0,0 +1,114 @@ +//go:build !integration + +package sessiontally + +import ( + "os" + "sync" + "testing" +) + +// sandbox points ~/.checkmarx at a temp dir on every OS (os.UserHomeDir uses USERPROFILE on Windows, +// HOME elsewhere), so tests never touch the real home directory. +func sandbox(t *testing.T) { + t.Helper() + home := t.TempDir() + t.Setenv("HOME", home) + t.Setenv("USERPROFILE", home) +} + +func TestAddLoadClearRoundTrip(t *testing.T) { + sandbox(t) + Add("S1", "Asca", 2, 2) + Add("S1", "Asca", 1, 1) + Add("S1", "Sca", 3, 1) + + got := Load("S1") + if got["Asca"].VulnerabilitiesFound != 3 || got["Asca"].RemediationsOffered != 3 { + t.Errorf("Asca fold wrong: %+v", got["Asca"]) + } + if got["Sca"].VulnerabilitiesFound != 3 || got["Sca"].RemediationsOffered != 1 { + t.Errorf("Sca fold wrong: %+v", got["Sca"]) + } + + Clear("S1") + if n := len(Load("S1")); n != 0 { + t.Errorf("expected empty after Clear, got %d engines", n) + } +} + +func TestConcurrentAddDoesNotLoseRecords(t *testing.T) { + sandbox(t) + var wg sync.WaitGroup + for i := 0; i < 50; i++ { + wg.Add(1) + go func() { defer wg.Done(); Add("S1", "Asca", 1, 0) }() + } + wg.Wait() + if got := Load("S1")["Asca"].VulnerabilitiesFound; got != 50 { + t.Errorf("concurrent append lost records: got %d want 50", got) + } +} + +func TestEmptyIDUsesDefaultBucketAndIsMergedAndCleared(t *testing.T) { + sandbox(t) + Add("", "Sca", 1, 1) // empty id → shared default bucket + if got := Load("S1")["Sca"].VulnerabilitiesFound; got != 1 { + t.Errorf("Load should merge the default bucket: got %d", got) + } + Clear("S1") // Clear removes the default bucket too + if n := len(Load("other")); n != 0 { + t.Errorf("default bucket not cleared: got %d engines", n) + } +} + +func TestMissingHomeIsNoOpNoPanic(t *testing.T) { + t.Setenv("HOME", "") + t.Setenv("USERPROFILE", "") + // os.UserHomeDir fails → every function must no-op without panicking. + Add("S1", "Asca", 1, 1) + if n := len(Load("S1")); n != 0 { + t.Errorf("Load with no home should be empty, got %d", n) + } + Clear("S1") +} + +func TestHostileIDStaysInsideCheckmarxDir(t *testing.T) { + sandbox(t) + id := "../../etc/passwd weird/id" + Add(id, "Asca", 1, 1) + if Load(id)["Asca"].VulnerabilitiesFound != 1 { + t.Errorf("sanitized id round-trip failed") + } + dir, ok := baseDir() + if !ok { + t.Fatal("baseDir unavailable") + } + entries, err := os.ReadDir(dir) + if err != nil || len(entries) == 0 { + t.Fatalf("expected a tally file inside %s (err=%v)", dir, err) + } + // The traversal must not have created a file outside ~/.checkmarx. + if _, err := os.Stat("etc/passwd weird"); err == nil { + t.Errorf("hostile id escaped the sandbox directory") + } +} + +func TestMalformedLinesSkipped(t *testing.T) { + sandbox(t) + Add("S1", "Asca", 1, 1) + path, ok := tallyPath("S1") + if !ok { + t.Fatal("no tally path") + } + f, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, 0o600) + if err != nil { + t.Fatal(err) + } + _, _ = f.WriteString("not json\n{}\n{\"engine\":\"\",\"found\":9}\n") + _ = f.Close() + + if got := Load("S1")["Asca"].VulnerabilitiesFound; got != 1 { + t.Errorf("malformed lines should be skipped, Asca=%d want 1", got) + } +} diff --git a/internal/services/realtimeengine/ossrealtime/oss-realtime.go b/internal/services/realtimeengine/ossrealtime/oss-realtime.go index bcf53af4..65d61903 100644 --- a/internal/services/realtimeengine/ossrealtime/oss-realtime.go +++ b/internal/services/realtimeengine/ossrealtime/oss-realtime.go @@ -179,6 +179,14 @@ func getPackageEntryFromPackageMap( return &entry } +// IsSupportedManifestFile reports whether filePath names a manifest file the +// OSS realtime scanner is able to parse and scan. Exported so other callers +// (e.g. the agent-hooks SCA guardrails) can gate on the same rule set instead +// of re-implementing their own list of supported manifest files. +func IsSupportedManifestFile(filePath string) bool { + return validateSupportedManifestFile(filePath) == nil +} + // validateSupportedManifestFile checks if the manifest file format is supported by OSS realtime scanner. func validateSupportedManifestFile(filePath string) error { manifestFileName := filepath.Base(filePath)