diff --git a/go.mod b/go.mod index 8e96a749b..0e70f1809 100644 --- a/go.mod +++ b/go.mod @@ -9,12 +9,12 @@ require ( github.com/go-git/go-git/v5 v5.19.1 github.com/golang/mock v1.6.0 github.com/google/go-github/v45 v45.2.0 - github.com/jfrog/build-info-go v1.13.1-0.20260713073853-4f3044bf0940 + github.com/jfrog/build-info-go v1.13.1-0.20260728083052-16a97012811d github.com/jfrog/froggit-go v1.23.1 github.com/jfrog/gofrog v1.7.6 - github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260624085155-5ba797de2616 + github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260728123939-34b27f070f2e github.com/jfrog/jfrog-cli-security v1.31.3 - github.com/jfrog/jfrog-client-go v1.55.1-0.20260624085832-de0c68a23c43 + github.com/jfrog/jfrog-client-go v1.55.1-0.20260729072925-e1104f6b9e00 github.com/owenrumney/go-sarif/v3 v3.2.3 github.com/stretchr/testify v1.11.1 github.com/urfave/cli/v2 v2.27.7 @@ -27,8 +27,8 @@ require ( github.com/Microsoft/go-winio v0.6.2 // indirect github.com/ProtonMail/go-crypto v1.4.1 // indirect github.com/andybalholm/brotli v1.2.1 // indirect - github.com/beevik/etree v1.6.0 // indirect - github.com/buger/jsonparser v1.2.0 // indirect + github.com/beevik/etree v1.7.0 // indirect + github.com/buger/jsonparser v1.3.0 // indirect github.com/c-bata/go-prompt v0.2.6 // indirect github.com/chzyer/readline v1.5.1 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect @@ -61,10 +61,10 @@ require ( github.com/hashicorp/go-retryablehttp v0.7.8 // indirect github.com/hashicorp/yamux v0.1.1 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect - github.com/jedib0t/go-pretty/v6 v6.7.10 // indirect + github.com/jedib0t/go-pretty/v6 v6.8.3 // indirect github.com/jfrog/archiver/v3 v3.6.3 // indirect github.com/jfrog/jfrog-apps-config v1.0.1 // indirect - github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260721044519-d4e49af4db2c // indirect + github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260728121041-2227ac7420a0 // indirect github.com/kevinburke/ssh_config v1.6.0 // indirect github.com/klauspost/compress v1.18.6 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect @@ -73,7 +73,7 @@ require ( github.com/manifoldco/promptui v0.9.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.22 // indirect - github.com/mattn/go-runewidth v0.0.23 // indirect + github.com/mattn/go-runewidth v0.0.27 // indirect github.com/mattn/go-tty v0.0.8 // indirect github.com/microsoft/azure-devops-go-api/azuredevops/v7 v7.1.0 // indirect github.com/minio/sha256-simd v1.0.1 // indirect @@ -114,13 +114,13 @@ require ( github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/crypto v0.52.0 // indirect - golang.org/x/mod v0.36.0 // indirect + golang.org/x/mod v0.37.0 // indirect golang.org/x/net v0.55.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/sync v0.22.0 // indirect - golang.org/x/sys v0.45.0 // indirect - golang.org/x/term v0.43.0 // indirect - golang.org/x/text v0.37.0 // indirect + golang.org/x/sys v0.47.0 // indirect + golang.org/x/term v0.45.0 // indirect + golang.org/x/text v0.40.0 // indirect golang.org/x/time v0.15.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7 // indirect google.golang.org/grpc v1.79.3 // indirect @@ -130,7 +130,7 @@ require ( gopkg.in/yaml.v3 v3.0.1 // indirect ) -// replace github.com/jfrog/jfrog-cli-security => github.com/jfrog/jfrog-cli-security dev +replace github.com/jfrog/jfrog-cli-security => github.com/jfrog/jfrog-cli-security v1.32.1-0.20260730101838-04edd1ee35e6 // replace github.com/jfrog/jfrog-cli-core/v2 => github.com/jfrog/jfrog-cli-core/v2 dev diff --git a/go.sum b/go.sum index 03ff643da..44cbb0b15 100644 --- a/go.sum +++ b/go.sum @@ -21,14 +21,14 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFI github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= -github.com/beevik/etree v1.6.0 h1:u8Kwy8pp9D9XeITj2Z0XtA5qqZEmtJtuXZRQi+j03eE= -github.com/beevik/etree v1.6.0/go.mod h1:bh4zJxiIr62SOf9pRzN7UUYaEDa9HEKafK25+sLc0Gc= +github.com/beevik/etree v1.7.0 h1:xjBk9O4p4x7D1YajePjfLzdaFC4/uYUENA7P0pv6gXA= +github.com/beevik/etree v1.7.0/go.mod h1:bh4zJxiIr62SOf9pRzN7UUYaEDa9HEKafK25+sLc0Gc= github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M= github.com/bradleyjkemp/cupaloy/v2 v2.8.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0= github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= -github.com/buger/jsonparser v1.2.0 h1:4EFcvK1kD4jyj6YqNK6skK6w+y7FHHBR+XBCtxwu/6g= -github.com/buger/jsonparser v1.2.0/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/buger/jsonparser v1.3.0 h1:2RJHo4G3vFBQSYwwEAOfHNRlKIameh/HdbhPJ/zY/0A= +github.com/buger/jsonparser v1.3.0/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/c-bata/go-prompt v0.2.6 h1:POP+nrHE+DfLYx370bedwNhsqmpCUynWPxuHi0C5vZI= github.com/c-bata/go-prompt v0.2.6/go.mod h1:/LMAke8wD2FsNu9EXNdHxNLbd9MedkPnCdfpU9wwHfY= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= @@ -138,26 +138,26 @@ github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= -github.com/jedib0t/go-pretty/v6 v6.7.10 h1:B/2qW2Bkv2L6n14PP8o1kx75kWzHOQ3YTluWzg9icac= -github.com/jedib0t/go-pretty/v6 v6.7.10/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= +github.com/jedib0t/go-pretty/v6 v6.8.3 h1:yVSk5aemoYHCvcrtqyXklwqcgHQIQzmy/oUzFlmffSQ= +github.com/jedib0t/go-pretty/v6 v6.8.3/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= github.com/jfrog/archiver/v3 v3.6.3 h1:hkAmPjBw393tPmQ07JknLNWFNZjXdy2xFEnOW9wwOxI= github.com/jfrog/archiver/v3 v3.6.3/go.mod h1:5V9l+Fte30Y4qe9dUOAd3yNTf8lmtVNuhKNrvI8PMhg= -github.com/jfrog/build-info-go v1.13.1-0.20260713073853-4f3044bf0940 h1:LthrPDN8YjDKrDNzOWS/w9NLFyeebdnWkmp8vhma0qo= -github.com/jfrog/build-info-go v1.13.1-0.20260713073853-4f3044bf0940/go.mod h1:CYRUCvLKfyARjoJXLWAxce1qNUxTEtbRKAARkV42vpE= +github.com/jfrog/build-info-go v1.13.1-0.20260728083052-16a97012811d h1:Yqbx+/9cIiZJmpyIkWDyC9Qjh4OaVeGWutADFaUzHr0= +github.com/jfrog/build-info-go v1.13.1-0.20260728083052-16a97012811d/go.mod h1:CYRUCvLKfyARjoJXLWAxce1qNUxTEtbRKAARkV42vpE= github.com/jfrog/froggit-go v1.23.1 h1:4wmaHeuptxVINbovMaeITzVhi3+VQoc/FFIjF4axzu0= github.com/jfrog/froggit-go v1.23.1/go.mod h1:wRDryqyp3oe+eHgME2mpnEQmO8XBECIPagFwj0nHmdI= github.com/jfrog/gofrog v1.7.6 h1:QmfAiRzVyaI7JYGsB7cxfAJePAZTzFz0gRWZSE27c6s= github.com/jfrog/gofrog v1.7.6/go.mod h1:ntr1txqNOZtHplmaNd7rS4f8jpA5Apx8em70oYEe7+4= github.com/jfrog/jfrog-apps-config v1.0.1 h1:mtv6k7g8A8BVhlHGlSveapqf4mJfonwvXYLipdsOFMY= github.com/jfrog/jfrog-apps-config v1.0.1/go.mod h1:8AIIr1oY9JuH5dylz2S6f8Ym2MaadPLR6noCBO4C22w= -github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260721044519-d4e49af4db2c h1:sQoW+Wwkk3Vp6bA1MS/RObrqY2Jt2qy0SxIiBhpLgz4= -github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260721044519-d4e49af4db2c/go.mod h1:1vxzqW7jHBSuTNqO2vxEnhbniwq4dj5wveDuCMJX7Yo= -github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260624085155-5ba797de2616 h1:bioFXGzf3pF2qnC3LZD1S1saWiHSekL4vdsDSWksj/4= -github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260624085155-5ba797de2616/go.mod h1:9R90mhbczGXwW5EGlDs7F08ejQU/xdoDhYHMvzBiqgE= -github.com/jfrog/jfrog-cli-security v1.31.3 h1:WVkonz9/0zuNKKz/350uguXmK771NymMu7QihQibop4= -github.com/jfrog/jfrog-cli-security v1.31.3/go.mod h1:C9DINxsrKdF5ciebWDMtt5/qfriNcKPPh1HJ5HXnotg= -github.com/jfrog/jfrog-client-go v1.55.1-0.20260624085832-de0c68a23c43 h1:akoiWauP27YxVXcRkCC8ahgDLxqiARUAVEB+KUPO2OE= -github.com/jfrog/jfrog-client-go v1.55.1-0.20260624085832-de0c68a23c43/go.mod h1:FHpjN1nTDoj96xd6obe27EOgGErqzU0rQgC96L3Ch9E= +github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260728121041-2227ac7420a0 h1:DVEYAyGJDn9ywGyBSa/MC3oWZsGC2DmPihv++/EDsJs= +github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260728121041-2227ac7420a0/go.mod h1:1vxzqW7jHBSuTNqO2vxEnhbniwq4dj5wveDuCMJX7Yo= +github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260728123939-34b27f070f2e h1:K0IK3w5a5h6SIi9yoOJ6a7DL+kuFjs5acypOxKyT2OM= +github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260728123939-34b27f070f2e/go.mod h1:MygQx8pekgPCXyXnejIAVG9S4ImGcDFmcfRPUug/0d0= +github.com/jfrog/jfrog-cli-security v1.32.1-0.20260730101838-04edd1ee35e6 h1:X3RbSz65/skNi+EDUHXYM6WwbwzkI6rkmzB6Tm2ZyJA= +github.com/jfrog/jfrog-cli-security v1.32.1-0.20260730101838-04edd1ee35e6/go.mod h1:cIpdkImygk7F/Y0h+a7dCDNQSEOvB0IsVChkvF3yqjI= +github.com/jfrog/jfrog-client-go v1.55.1-0.20260729072925-e1104f6b9e00 h1:QekOqpZ4c34Xb/eX9UCM1JMGipGoW/ZpkbwsWrx/kAQ= +github.com/jfrog/jfrog-client-go v1.55.1-0.20260729072925-e1104f6b9e00/go.mod h1:FHpjN1nTDoj96xd6obe27EOgGErqzU0rQgC96L3Ch9E= github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= github.com/kevinburke/ssh_config v1.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY= @@ -179,8 +179,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/ktrysmt/go-bitbucket v0.9.88 h1:XBjYui83tW2puG7f2GvYSAMMKIPfhpeoLCVfEJx3KVM= github.com/ktrysmt/go-bitbucket v0.9.88/go.mod h1:fx6zdyKEyiNfR9VW0npWD6ugoSUsp8JLXGyqna8bHkc= -github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= -github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/magiconair/properties v1.18.11 h1:j5ozYZl0zCjG7ahMDH0GWIobOvvUzT0BdAguG0ViKy0= +github.com/magiconair/properties v1.18.11/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= @@ -197,8 +197,8 @@ github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw= -github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/mattn/go-runewidth v0.0.27 h1:Feg/Oou5zI/wnpgDF6omIU0OokC9GxLC/WRknhVlIR0= +github.com/mattn/go-runewidth v0.0.27/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8= github.com/mattn/go-tty v0.0.3/go.mod h1:ihxohKRERHTVzN+aSVRwACLCeqIoZAWpoICkkvrWyR0= github.com/mattn/go-tty v0.0.8 h1:yxtc0Ye17/1ne/bjy993YUoyP8bJJFa9n5M9XTdwoZQ= github.com/mattn/go-tty v0.0.8/go.mod h1:f2i5ZOvXBU/tCABmLmOfzLz9azMo5wdAaElRNnJKr+k= @@ -289,8 +289,10 @@ github.com/urfave/cli v1.22.17 h1:SYzXoiPfQjHBbkYxbew5prZHS1TOLT3ierW8SYLqtVQ= github.com/urfave/cli v1.22.17/go.mod h1:b0ht0aqgH/6pBYzzxURyrM4xXNgsoT/n2ZzwQiEhNVo= github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU= github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4= -github.com/vbauerster/mpb/v8 v8.12.1 h1:pyj3yQ2ZGQJgUXm4h17QpR+eERaNz5OQ1ftPSEE/sMM= -github.com/vbauerster/mpb/v8 v8.12.1/go.mod h1:XLXRfStkw/6i5k0aQltijDHT1Z93fD1DVwmIdcFUp6k= +github.com/vbauerster/cupwriter v0.0.4 h1:9sBPe0uXWLZuWQU5lqVbhyFlxX6c09asST/YfatFAys= +github.com/vbauerster/cupwriter v0.0.4/go.mod h1:IFyzS6Xis5dnBH/rdAhrnuzg3c+KkUqEN6yE8lhJlDw= +github.com/vbauerster/mpb/v8 v8.14.0 h1:55SR80dptMfASxIG/oCEkBXgBhxeSu4GrVsjl16oKmA= +github.com/vbauerster/mpb/v8 v8.14.0/go.mod h1:HgpQPKfcWe3kbuGGPmi+jatHreMase5C3Fp5dpdAy0Q= github.com/virtuald/go-ordered-json v0.0.0-20170621173500-b18e6e673d74 h1:JwtAtbp7r/7QSyGz8mKUbYJBg2+6Cd7OjM8o/GNOcVo= github.com/virtuald/go-ordered-json v0.0.0-20170621173500-b18e6e673d74/go.mod h1:RmMWU37GKR2s6pgrIEB4ixgpVCt/cf7dnJv3fuH1J1c= github.com/xanzy/go-gitlab v0.115.0 h1:6DmtItNcVe+At/liXSgfE/DZNZrGfalQmBRmOcJjOn8= @@ -341,8 +343,8 @@ golang.org/x/exp v0.0.0-20260527015227-08cc5374adb3/go.mod h1:d2fgXJLVs4dYDHUk5l golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= -golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= +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-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -397,16 +399,16 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -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.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.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.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= -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.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= +golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -414,8 +416,8 @@ 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.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -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.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= +golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -423,8 +425,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= -golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= +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= diff --git a/scanpullrequest/scanpullrequest.go b/scanpullrequest/scanpullrequest.go index 5cc0d2888..ec7717568 100644 --- a/scanpullrequest/scanpullrequest.go +++ b/scanpullrequest/scanpullrequest.go @@ -441,6 +441,8 @@ func scanResultsToIssuesCollection(scanResults *results.SecurityCommandResults, return nil, err } issuesCollection = &issues.ScansIssuesCollection{ + ResultsPlatformURL: scanResults.ResultsPlatformUrl, + ScanStatus: simpleJsonResults.Statuses, ScaVulnerabilities: simpleJsonResults.Vulnerabilities, ScaViolations: simpleJsonResults.SecurityViolations, diff --git a/scanpullrequest/scanpullrequest_test.go b/scanpullrequest/scanpullrequest_test.go index e608be690..d5d524099 100644 --- a/scanpullrequest/scanpullrequest_test.go +++ b/scanpullrequest/scanpullrequest_test.go @@ -1429,7 +1429,9 @@ func createGitLabHandler(t *testing.T, testDir string, params GitServerParams) h default: expectedResponse = outputwriter.GetJsonBodyOutputFromFile(t, filepath.Join(testDir, "expected_response.md")) } - assert.JSONEq(t, string(expectedResponse), buf.String()) + actualResponse, foundScanResultsLink := outputwriter.StripScanResultsLinkFromJsonBody(t, buf.Bytes()) + assert.True(t, foundScanResultsLink, "the pull request comment is expected to link to the full scan results in the JFrog Platform") + assert.JSONEq(t, string(expectedResponse), string(actualResponse)) w.WriteHeader(http.StatusOK) _, err = w.Write([]byte("{}")) diff --git a/scanrepository/scanrepository.go b/scanrepository/scanrepository.go index 44d2c97c1..1fcba9bc6 100644 --- a/scanrepository/scanrepository.go +++ b/scanrepository/scanrepository.go @@ -183,7 +183,7 @@ func (sr *ScanRepositoryCmd) scanAndFixBranch(repository *utils.Repository) (tot log.Info(fmt.Sprintf("Didn't find any vulnerable dependencies with existing fix versions or that are currently supported for fixing, for %s", sr.scanDetails.RepoName)) return totalFindings, nil } - return totalFindings, sr.fixVulnerablePackages(repository, vulnerabilitiesByPathMap) + return totalFindings, sr.fixVulnerablePackages(repository, scanResults.ResultsPlatformUrl, vulnerabilitiesByPathMap) } func getTotalFindingsFromScanResults(scanResults *results.SecurityCommandResults) int { @@ -241,7 +241,7 @@ func (sr *ScanRepositoryCmd) scan() (*results.SecurityCommandResults, error) { return auditResults, nil } -func (sr *ScanRepositoryCmd) fixVulnerablePackages(repository *utils.Repository, vulnerabilitiesMap map[string]*utils.VulnerabilityDetails) error { +func (sr *ScanRepositoryCmd) fixVulnerablePackages(repository *utils.Repository, resultsPlatformURL string, vulnerabilitiesMap map[string]*utils.VulnerabilityDetails) error { var err error if repository.FrogbotConfig.AggregateFixes { aggregatedFixBranchName, e := sr.gitManager.GenerateAggregatedFixBranchName(sr.scanDetails.BaseBranch(), sr.projectTech) @@ -252,9 +252,9 @@ func (sr *ScanRepositoryCmd) fixVulnerablePackages(repository *utils.Repository, if e != nil { return e } - err = sr.aggregateFixAndOpenPullRequest(repository, vulnerabilitiesMap, aggregatedFixBranchName, existingPullRequestDetails) + err = sr.aggregateFixAndOpenPullRequest(repository, resultsPlatformURL, vulnerabilitiesMap, aggregatedFixBranchName, existingPullRequestDetails) } else { - if e := sr.fixProjectVulnerabilities(repository, vulnerabilitiesMap); e != nil { + if e := sr.fixProjectVulnerabilities(repository, resultsPlatformURL, vulnerabilitiesMap); e != nil { err = fmt.Errorf("the following errors occured while fixing vulnerabilities in '%s':\n%v", sr.scanDetails.BaseBranch(), e) } } @@ -264,10 +264,10 @@ func (sr *ScanRepositoryCmd) fixVulnerablePackages(repository *utils.Repository, return nil } -func (sr *ScanRepositoryCmd) fixProjectVulnerabilities(repository *utils.Repository, vulnerabilities map[string]*utils.VulnerabilityDetails) (err error) { +func (sr *ScanRepositoryCmd) fixProjectVulnerabilities(repository *utils.Repository, resultsPlatformURL string, vulnerabilities map[string]*utils.VulnerabilityDetails) (err error) { // Fix every vulnerability in a separate pull request and branch for _, vulnerability := range vulnerabilities { - if e := sr.fixSinglePackageAndCreatePR(repository, vulnerability); e != nil { + if e := sr.fixSinglePackageAndCreatePR(repository, resultsPlatformURL, vulnerability); e != nil { err = errors.Join(err, sr.handleUpdatePackageErrors(e)) } // Checkout back to the base branch after each fix so the next fix branch is created @@ -312,7 +312,7 @@ func (sr *ScanRepositoryCmd) handleUpdatePackageErrors(err error) error { // Creates a branch for the fixed package and open pull request against the target branch. // In case a branch already exists on remote, we skip it. -func (sr *ScanRepositoryCmd) fixSinglePackageAndCreatePR(repository *utils.Repository, vulnDetails *utils.VulnerabilityDetails) (err error) { +func (sr *ScanRepositoryCmd) fixSinglePackageAndCreatePR(repository *utils.Repository, resultsPlatformURL string, vulnDetails *utils.VulnerabilityDetails) (err error) { fixVersion := vulnDetails.SuggestedFixedVersion log.Debug("Attempting to fix", fmt.Sprintf("%s:%s", vulnDetails.ImpactedDependencyName, vulnDetails.ImpactedDependencyVersion), "with", fixVersion) fixBranchName, err := sr.gitManager.GenerateFixBranchName(sr.scanDetails.BaseBranch(), vulnDetails.ImpactedDependencyName, fixVersion) @@ -346,14 +346,14 @@ func (sr *ScanRepositoryCmd) fixSinglePackageAndCreatePR(repository *utils.Repos if err = sr.updatePackageToFixedVersion(vulnDetails); err != nil { return } - if err = sr.openFixingPullRequest(repository, fixBranchName, vulnDetails); err != nil { + if err = sr.openFixingPullRequest(repository, fixBranchName, resultsPlatformURL, vulnDetails); err != nil { return errors.Join(fmt.Errorf("failed while creating a fixing pull request for: %s with version: %s with error: ", vulnDetails.ImpactedDependencyName, fixVersion), err) } log.Info(fmt.Sprintf("Created Pull Request updating dependency '%s' to version '%s'", vulnDetails.ImpactedDependencyName, vulnDetails.SuggestedFixedVersion)) return } -func (sr *ScanRepositoryCmd) openFixingPullRequest(repository *utils.Repository, fixBranchName string, vulnDetails *utils.VulnerabilityDetails) (err error) { +func (sr *ScanRepositoryCmd) openFixingPullRequest(repository *utils.Repository, fixBranchName string, resultsPlatformURL string, vulnDetails *utils.VulnerabilityDetails) (err error) { log.Debug("Checking if there are changes to commit") isClean, err := sr.gitManager.IsClean() if err != nil { @@ -373,11 +373,11 @@ func (sr *ScanRepositoryCmd) openFixingPullRequest(repository *utils.Repository, if err = sr.gitManager.Push(false, fixBranchName); err != nil { return } - return sr.handleFixPullRequestContent(repository, fixBranchName, nil, vulnDetails) + return sr.handleFixPullRequestContent(repository, fixBranchName, nil, resultsPlatformURL, vulnDetails) } -func (sr *ScanRepositoryCmd) handleFixPullRequestContent(repository *utils.Repository, fixBranchName string, pullRequestInfo *vcsclient.PullRequestInfo, vulnerabilities ...*utils.VulnerabilityDetails) (err error) { - pullRequestTitle, prBody, extraComments, err := sr.preparePullRequestDetails(repository.FrogbotConfig.AggregateFixes, vulnerabilities...) +func (sr *ScanRepositoryCmd) handleFixPullRequestContent(repository *utils.Repository, fixBranchName string, pullRequestInfo *vcsclient.PullRequestInfo, resultsPlatformURL string, vulnerabilities ...*utils.VulnerabilityDetails) (err error) { + pullRequestTitle, prBody, extraComments, err := sr.preparePullRequestDetails(repository.FrogbotConfig.AggregateFixes, resultsPlatformURL, vulnerabilities...) if err != nil { return } @@ -414,7 +414,7 @@ func (sr *ScanRepositoryCmd) createOrUpdatePullRequest(repository *utils.Reposit // Handles the opening or updating of a pull request when the aggregate mode is active. // If a pull request is already open, Frogbot will update the branch and the pull request body. -func (sr *ScanRepositoryCmd) openAggregatedPullRequest(repository *utils.Repository, fixBranchName string, pullRequestInfo *vcsclient.PullRequestInfo, vulnerabilities []*utils.VulnerabilityDetails) (err error) { +func (sr *ScanRepositoryCmd) openAggregatedPullRequest(repository *utils.Repository, fixBranchName string, pullRequestInfo *vcsclient.PullRequestInfo, resultsPlatformURL string, vulnerabilities []*utils.VulnerabilityDetails) (err error) { commitMessage := sr.gitManager.GenerateAggregatedCommitMessage(sr.projectTech) if err = sr.cleanNewFilesMissingInRemote(); err != nil { return @@ -425,7 +425,7 @@ func (sr *ScanRepositoryCmd) openAggregatedPullRequest(repository *utils.Reposit if err = sr.gitManager.Push(true, fixBranchName); err != nil { return } - return sr.handleFixPullRequestContent(repository, fixBranchName, pullRequestInfo, vulnerabilities...) + return sr.handleFixPullRequestContent(repository, fixBranchName, pullRequestInfo, resultsPlatformURL, vulnerabilities...) } func (sr *ScanRepositoryCmd) cleanNewFilesMissingInRemote() error { @@ -460,14 +460,14 @@ func (sr *ScanRepositoryCmd) cleanNewFilesMissingInRemote() error { return err } -func (sr *ScanRepositoryCmd) preparePullRequestDetails(aggregateFixes bool, vulnerabilitiesDetails ...*utils.VulnerabilityDetails) (prTitle, prBody string, otherComments []string, err error) { +func (sr *ScanRepositoryCmd) preparePullRequestDetails(aggregateFixes bool, resultsPlatformURL string, vulnerabilitiesDetails ...*utils.VulnerabilityDetails) (prTitle, prBody string, otherComments []string, err error) { if sr.dryRun && aggregateFixes { // For testings, don't compare pull request body as scan results order may change. return sr.gitManager.GenerateAggregatedPullRequestTitle(sr.projectTech), "", []string{}, nil } vulnerabilitiesRows := utils.ExtractVulnerabilitiesDetailsToRows(vulnerabilitiesDetails) - prBody, extraComments := utils.GenerateFixPullRequestDetails(vulnerabilitiesRows, sr.OutputWriter) + prBody, extraComments := utils.GenerateFixPullRequestDetails(vulnerabilitiesRows, resultsPlatformURL, sr.OutputWriter) if aggregateFixes { var scanHash string @@ -641,7 +641,7 @@ func (sr *ScanRepositoryCmd) getOpenPullRequestBySourceBranch(branchName string) return } -func (sr *ScanRepositoryCmd) aggregateFixAndOpenPullRequest(repository *utils.Repository, vulnerabilitiesMap map[string]*utils.VulnerabilityDetails, aggregatedFixBranchName string, existingPullRequestInfo *vcsclient.PullRequestInfo) (err error) { +func (sr *ScanRepositoryCmd) aggregateFixAndOpenPullRequest(repository *utils.Repository, resultsPlatformURL string, vulnerabilitiesMap map[string]*utils.VulnerabilityDetails, aggregatedFixBranchName string, existingPullRequestInfo *vcsclient.PullRequestInfo) (err error) { log.Info("-----------------------------------------------------------------") log.Info("Starting aggregated dependencies fix") @@ -674,7 +674,7 @@ func (sr *ScanRepositoryCmd) aggregateFixAndOpenPullRequest(repository *utils.Re return } if len(fixedVulnerabilities) > 0 { - if e = sr.openAggregatedPullRequest(repository, aggregatedFixBranchName, existingPullRequestInfo, fixedVulnerabilities); e != nil { + if e = sr.openAggregatedPullRequest(repository, aggregatedFixBranchName, existingPullRequestInfo, resultsPlatformURL, fixedVulnerabilities); e != nil { err = errors.Join(err, fmt.Errorf("failed while creating aggregated pull request. Error: \n%s", e.Error())) } } diff --git a/scanrepository/scanrepository_test.go b/scanrepository/scanrepository_test.go index ec8b6ec27..b3fd9d96c 100644 --- a/scanrepository/scanrepository_test.go +++ b/scanrepository/scanrepository_test.go @@ -796,8 +796,8 @@ func TestPreparePullRequestDetails(t *testing.T) { SuggestedFixedVersion: "1.0.0", }, } - expectedPrBody, expectedExtraComments := utils.GenerateFixPullRequestDetails(utils.ExtractVulnerabilitiesDetailsToRows(vulnerabilities), cfp.OutputWriter) - prTitle, prBody, extraComments, err := cfp.preparePullRequestDetails(false, vulnerabilities...) + expectedPrBody, expectedExtraComments := utils.GenerateFixPullRequestDetails(utils.ExtractVulnerabilitiesDetailsToRows(vulnerabilities), "", cfp.OutputWriter) + prTitle, prBody, extraComments, err := cfp.preparePullRequestDetails(false, "", vulnerabilities...) assert.NoError(t, err) assert.Equal(t, "[🐸 Frogbot] Update version of package1 to 1.0.0", prTitle) assert.Equal(t, expectedPrBody, prBody) @@ -815,21 +815,32 @@ func TestPreparePullRequestDetails(t *testing.T) { }, SuggestedFixedVersion: "2.0.0", }) - expectedPrBody, expectedExtraComments = utils.GenerateFixPullRequestDetails(utils.ExtractVulnerabilitiesDetailsToRows(vulnerabilities), cfp.OutputWriter) + expectedPrBody, expectedExtraComments = utils.GenerateFixPullRequestDetails(utils.ExtractVulnerabilitiesDetailsToRows(vulnerabilities), "", cfp.OutputWriter) expectedPrBody += outputwriter.MarkdownComment("Checksum: bec823edaceb5d0478b789798e819bde") - prTitle, prBody, extraComments, err = cfp.preparePullRequestDetails(true, vulnerabilities...) + prTitle, prBody, extraComments, err = cfp.preparePullRequestDetails(true, "", vulnerabilities...) assert.NoError(t, err) assert.Equal(t, cfp.gitManager.GenerateAggregatedPullRequestTitle([]techutils.Technology{}), prTitle) assert.Equal(t, expectedPrBody, prBody) assert.ElementsMatch(t, expectedExtraComments, extraComments) cfp.OutputWriter = &outputwriter.SimplifiedOutput{} - expectedPrBody, expectedExtraComments = utils.GenerateFixPullRequestDetails(utils.ExtractVulnerabilitiesDetailsToRows(vulnerabilities), cfp.OutputWriter) + expectedPrBody, expectedExtraComments = utils.GenerateFixPullRequestDetails(utils.ExtractVulnerabilitiesDetailsToRows(vulnerabilities), "", cfp.OutputWriter) expectedPrBody += outputwriter.MarkdownComment("Checksum: bec823edaceb5d0478b789798e819bde") - prTitle, prBody, extraComments, err = cfp.preparePullRequestDetails(true, vulnerabilities...) + prTitle, prBody, extraComments, err = cfp.preparePullRequestDetails(true, "", vulnerabilities...) assert.NoError(t, err) assert.Equal(t, cfp.gitManager.GenerateAggregatedPullRequestTitle([]techutils.Technology{}), prTitle) assert.Equal(t, expectedPrBody, prBody) assert.ElementsMatch(t, expectedExtraComments, extraComments) + + const resultsURL = "https://example.jfrog.io/ui/xray/scans-list/git-repositories/test" + expectedPrBody, expectedExtraComments = utils.GenerateFixPullRequestDetails(utils.ExtractVulnerabilitiesDetailsToRows(vulnerabilities), resultsURL, cfp.OutputWriter) + expectedPrBody += outputwriter.MarkdownComment("Checksum: bec823edaceb5d0478b789798e819bde") + prTitle, prBody, extraComments, err = cfp.preparePullRequestDetails(true, resultsURL, vulnerabilities...) + assert.NoError(t, err) + assert.Equal(t, cfp.gitManager.GenerateAggregatedPullRequestTitle([]techutils.Technology{}), prTitle) + assert.Equal(t, expectedPrBody, prBody) + assert.Contains(t, prBody, resultsURL) + assert.Contains(t, prBody, "View full scan results in JFrog Platform") + assert.ElementsMatch(t, expectedExtraComments, extraComments) } // This test simulates the cleaning action of cleanNewFilesMissingInRemote. diff --git a/utils/comment.go b/utils/comment.go index 9c9d0dd3d..5faea0216 100644 --- a/utils/comment.go +++ b/utils/comment.go @@ -53,7 +53,7 @@ func HandlePullRequestCommentsAfterScan(issues *issues.ScansIssuesCollection, re // Add summary scan comment if issues.IssuesExists(repo.FrogbotConfig.ShowSecretsAsPrComment) || !repo.FrogbotConfig.HideSuccessBannerForNoIssues { - comments := generatePullRequestSummaryComment(*issues, resultContext, repo.FrogbotConfig.ShowSecretsAsPrComment, repo.OutputWriter) + comments := generatePullRequestSummaryComment(*issues, resultContext, repo.FrogbotConfig.ShowSecretsAsPrComment, issues.ResultsPlatformURL, repo.OutputWriter) if repo.OutputWriter.VcsProvider() == vcsutils.BitbucketCloud { // Bitbucket Cloud Activity feed displays comments newest-first, so post in reverse to keep summary on top. slices.Reverse(comments) @@ -102,8 +102,8 @@ func DeleteExistingPullRequestComments(repository *Repository, client vcsclient. return err } -func GenerateFixPullRequestDetails(vulnerabilities []formats.VulnerabilityOrViolationRow, writer outputwriter.OutputWriter) (description string, extraComments []string) { - content := outputwriter.GetMainCommentContent(outputwriter.GetVulnerabilitiesContent(vulnerabilities, writer), true, false, writer) +func GenerateFixPullRequestDetails(vulnerabilities []formats.VulnerabilityOrViolationRow, resultsPlatformURL string, writer outputwriter.OutputWriter) (description string, extraComments []string) { + content := outputwriter.GetMainCommentContent(outputwriter.GetVulnerabilitiesContent(vulnerabilities, writer), true, false, resultsPlatformURL, writer) if len(content) == 1 { // Limit is not reached, use the entire content as the description description = content[0] @@ -120,9 +120,9 @@ func GenerateFixPullRequestDetails(vulnerabilities []formats.VulnerabilityOrViol return } -func generatePullRequestSummaryComment(issuesCollection issues.ScansIssuesCollection, resultContext results.ResultContext, includeSecrets bool, writer outputwriter.OutputWriter) []string { +func generatePullRequestSummaryComment(issuesCollection issues.ScansIssuesCollection, resultContext results.ResultContext, includeSecrets bool, resultsPlatformURL string, writer outputwriter.OutputWriter) []string { if !issuesCollection.IssuesExists(includeSecrets) { - return outputwriter.GetMainCommentContent([]string{}, false, true, writer) + return outputwriter.GetMainCommentContent([]string{}, false, true, resultsPlatformURL, writer) } // Summary content := []string{outputwriter.ScanSummaryContent(issuesCollection, resultContext, includeSecrets, writer)} @@ -134,7 +134,7 @@ func generatePullRequestSummaryComment(issuesCollection issues.ScansIssuesCollec if vulnerabilitiesContent := outputwriter.GetVulnerabilitiesContent(issuesCollection.ScaVulnerabilities, writer); len(vulnerabilitiesContent) > 0 { content = append(content, vulnerabilitiesContent...) } - return outputwriter.GetMainCommentContent(content, true, true, writer) + return outputwriter.GetMainCommentContent(content, true, true, resultsPlatformURL, writer) } func GetSortedPullRequestComments(client vcsclient.VcsClient, repoOwner, repoName string, prID int) ([]vcsclient.CommentInfo, error) { diff --git a/utils/comment_test.go b/utils/comment_test.go index 10abfa51f..f6c95d943 100644 --- a/utils/comment_test.go +++ b/utils/comment_test.go @@ -1,10 +1,12 @@ package utils import ( + "strings" "testing" "github.com/jfrog/froggit-go/vcsclient" "github.com/jfrog/jfrog-cli-security/utils/formats" + "github.com/jfrog/jfrog-cli-security/utils/results" "github.com/jfrog/jfrog-cli-security/utils/techutils" "github.com/jfrog/jfrog-client-go/xsc/services" "github.com/stretchr/testify/assert" @@ -1008,3 +1010,98 @@ func TestGenerateSnippetReviewComment(t *testing.T) { }) } } + +func TestGenerateFixPullRequestDetailsResultsPlatformURL(t *testing.T) { + const ( + resultsURL = "https://example.jfrog.io/ui/xray/scans-list/git-repositories/test" + scanResultsLinkText = "View full scan results in JFrog Platform" + ) + vulnerabilities := []formats.VulnerabilityOrViolationRow{{ + IssueId: "XRAY-1234", + ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ + SeverityDetails: formats.SeverityDetails{Severity: "High"}, + ImpactedDependencyName: "example-package", + ImpactedDependencyVersion: "1.0.0", + }, + }} + + t.Run("includes link when URL provided", func(t *testing.T) { + description, extraComments := GenerateFixPullRequestDetails(vulnerabilities, resultsURL, &outputwriter.StandardOutput{}) + + require.NotEmpty(t, description) + assert.Contains(t, description, "XRAY-1234") + assert.Contains(t, description, resultsURL) + assert.Contains(t, description, scanResultsLinkText) + for _, comment := range extraComments { + assert.NotContains(t, comment, resultsURL) + } + }) + + t.Run("omits link when URL empty", func(t *testing.T) { + description, extraComments := GenerateFixPullRequestDetails(vulnerabilities, "", &outputwriter.StandardOutput{}) + + require.NotEmpty(t, description) + assert.Contains(t, description, "XRAY-1234") + assert.NotContains(t, description, scanResultsLinkText) + for _, comment := range extraComments { + assert.NotContains(t, comment, scanResultsLinkText) + } + }) +} + +func TestGeneratePullRequestSummaryCommentResultsPlatformURL(t *testing.T) { + const resultsURL = "https://example.jfrog.io/ui/xray/scans-list/git-repositories/test" + writer := &outputwriter.SimplifiedOutput{} + + t.Run("successful scan includes link", func(t *testing.T) { + comments := generatePullRequestSummaryComment( + issues.ScansIssuesCollection{}, + results.ResultContext{}, + false, + resultsURL, + writer, + ) + + require.Len(t, comments, 1) + assert.Contains(t, comments[0], resultsURL) + }) + + t.Run("findings scan includes link before summary", func(t *testing.T) { + scanIssues := issues.ScansIssuesCollection{ + ScaVulnerabilities: []formats.VulnerabilityOrViolationRow{{ + ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ + SeverityDetails: formats.SeverityDetails{Severity: "High"}, + ImpactedDependencyName: "example:1.0.0", + }, + }}, + } + comments := generatePullRequestSummaryComment( + scanIssues, + results.ResultContext{IncludeVulnerabilities: true}, + false, + resultsURL, + writer, + ) + + require.NotEmpty(t, comments) + assert.Contains(t, comments[0], resultsURL) + assert.Less( + t, + strings.Index(comments[0], resultsURL), + strings.Index(comments[0], "Frogbot scanned for"), + ) + }) + + t.Run("empty URL preserves current comment", func(t *testing.T) { + comments := generatePullRequestSummaryComment( + issues.ScansIssuesCollection{}, + results.ResultContext{}, + false, + "", + writer, + ) + + require.Len(t, comments, 1) + assert.NotContains(t, comments[0], "View full scan results") + }) +} diff --git a/utils/getconfiguration.go b/utils/getconfiguration.go index dd6e53701..1bee0e541 100644 --- a/utils/getconfiguration.go +++ b/utils/getconfiguration.go @@ -69,11 +69,11 @@ func (jp *JFrogPlatform) setJfProjectKeyIfExists() (err error) { type Git struct { GitProvider vcsutils.VcsProvider vcsclient.VcsInfo - RepoOwner string - RepoName string - Branches []string - PullRequestDetails vcsclient.PullRequestInfo - RepositoryCloneUrl string + RepoOwner string + RepoName string + Branches []string + PullRequestDetails vcsclient.PullRequestInfo + RepositoryCloneUrl string UploadSbomToVcs *bool UploadPrSecurityResultsToVcs bool GitlabScanResultsOutputDir string @@ -521,7 +521,7 @@ func getConfigurationProfile(xrayVersion string, jfrogServer *coreconfig.ServerD return } log.Debug(fmt.Sprintf("Searching central configuration associated to repository '%s'", jfrogServer.Url)) - if configProfile, err = xsc.GetConfigProfileByUrl(xrayVersion, jfrogServer, repoCloneUrl, projectKey); err != nil || configProfile == nil { + if configProfile, err = xsc.GetConfigProfileByUrl(xrayVersion, jfrogServer, repoCloneUrl, projectKey, ""); err != nil || configProfile == nil { return } diff --git a/utils/issues/issuescollection.go b/utils/issues/issuescollection.go index 08b95bf2b..837b7ef80 100644 --- a/utils/issues/issuescollection.go +++ b/utils/issues/issuescollection.go @@ -16,6 +16,7 @@ const FailPrRuleMessage = "one or more violations incurred by '%s' watch, which // Group issues by scan type type ScansIssuesCollection struct { + ResultsPlatformURL string formats.ScanStatus LicensesViolations []formats.LicenseViolationRow @@ -42,6 +43,8 @@ func (ic *ScansIssuesCollection) Append(issues *ScansIssuesCollection) { if issues == nil { return } + // Results platform URL + ic.ResultsPlatformURL = issues.ResultsPlatformURL // Status ic.AppendStatus(issues.ScanStatus) // Sca diff --git a/utils/issues/issuescollection_test.go b/utils/issues/issuescollection_test.go index 06e83eff3..75dfaa818 100644 --- a/utils/issues/issuescollection_test.go +++ b/utils/issues/issuescollection_test.go @@ -576,3 +576,17 @@ func TestAppendStatus(t *testing.T) { issues.AppendStatus(newStatus) assert.Equal(t, expectedStatus, issues.ScanStatus) } + +func TestAppendResultsPlatformURL(t *testing.T) { + const resultsURL = "https://example.jfrog.io/ui/xray/scans-list/git-repositories/test" + target := &ScansIssuesCollection{} + + target.Append(&ScansIssuesCollection{ResultsPlatformURL: resultsURL}) + assert.Equal(t, resultsURL, target.ResultsPlatformURL) + + target.Append(nil) + assert.Equal(t, resultsURL, target.ResultsPlatformURL) + + target.Append(&ScansIssuesCollection{}) + assert.Empty(t, target.ResultsPlatformURL) +} diff --git a/utils/outputwriter/outputcontent.go b/utils/outputwriter/outputcontent.go index bb75a89b8..7683378a2 100644 --- a/utils/outputwriter/outputcontent.go +++ b/utils/outputwriter/outputcontent.go @@ -39,6 +39,8 @@ const ( iacTitle = "🛠️ Infrastructure as Code" sastTitle = "🎯 Static Application Security Testing (SAST)" snippetsTitle = "📜 Public Code Snippet" + + scanResultsLinkText = "View full scan results in JFrog Platform" ) var ( @@ -74,10 +76,10 @@ func getFallbackCommentLocationDescription(location formats.Location) string { } // Summary comment, including banner, footer wrapping the content with a decorator -func GetMainCommentContent(contentForComments []string, issuesExists, isComment bool, writer OutputWriter) (comments []string) { +func GetMainCommentContent(contentForComments []string, issuesExists, isComment bool, resultsPlatformURL string, writer OutputWriter) (comments []string) { return ConvertContentToComments(contentForComments, writer, func(commentCount int, content string) string { if commentCount == 0 { - content = GetPRSummaryMainCommentDecorator(issuesExists, isComment, writer)(commentCount, content) + content = getMainCommentDecorator(issuesExists, isComment, resultsPlatformURL, writer)(commentCount, content) } return GetFrogbotCommentBaseDecorator(writer)(commentCount, content) }) @@ -93,8 +95,8 @@ func GetFrogbotCommentBaseDecorator(writer OutputWriter) CommentDecorator { } } -// Adding a banner and custom title to the content -func GetPRSummaryMainCommentDecorator(issuesExists, isComment bool, writer OutputWriter) CommentDecorator { +// Adding a banner, custom title and results platform URL to the content +func getMainCommentDecorator(issuesExists, isComment bool, resultsPlatformURL string, writer OutputWriter) CommentDecorator { return func(_ int, content string) string { comment := strings.Builder{} comment.WriteString(writer.Image(getPRSummaryBanner(issuesExists, isComment, writer.VcsProvider()))) @@ -102,6 +104,9 @@ func GetPRSummaryMainCommentDecorator(issuesExists, isComment bool, writer Outpu if customCommentTitle != "" { WriteContent(&comment, writer.MarkAsTitle(MarkAsBold(customCommentTitle), 2)) } + if resultsPlatformURL != "" { + WriteContent(&comment, writer.MarkAsTitle(MarkAsLink(scanResultsLinkText, resultsPlatformURL), 3)) + } if issuesExists { WriteContent(&comment, content) } diff --git a/utils/outputwriter/outputcontent_test.go b/utils/outputwriter/outputcontent_test.go index 417249b3e..ca626aee5 100644 --- a/utils/outputwriter/outputcontent_test.go +++ b/utils/outputwriter/outputcontent_test.go @@ -1,7 +1,10 @@ package outputwriter import ( + "encoding/json" + "fmt" "path/filepath" + "strings" "testing" "github.com/jfrog/froggit-go/vcsutils" @@ -12,6 +15,7 @@ import ( "github.com/jfrog/jfrog-cli-security/utils/severityutils" xrayApi "github.com/jfrog/jfrog-client-go/xray/services/utils" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/jfrog/frogbot/v3/utils/issues" ) @@ -125,7 +129,7 @@ func TestGetMainCommentContent(t *testing.T) { for _, test := range tc.cases { t.Run(tc.name+"_"+test.name, func(t *testing.T) { expectedOutput := GetExpectedTestOutput(t, test) - output := GetMainCommentContent([]string{MarkAsCodeSnippet("some content")}, tc.issuesExists, tc.isComment, test.writer) + output := GetMainCommentContent([]string{MarkAsCodeSnippet("some content")}, tc.issuesExists, tc.isComment, "", test.writer) assert.Len(t, output, 1) assert.Equal(t, expectedOutput, output[0]) }) @@ -133,6 +137,144 @@ func TestGetMainCommentContent(t *testing.T) { } } +func TestGetMainCommentContentWithResultsPlatformURL(t *testing.T) { + const ( + resultsURL = "https://example.jfrog.io/ui/xray/scans-list/git-repositories/test" + content = "scan findings" + ) + link := MarkAsLink(scanResultsLinkText, resultsURL) + + testCases := []struct { + name string + writer OutputWriter + issuesExist bool + isComment bool + content []string + expectDetail bool + }{ + { + name: "PR summary findings standard output", + writer: &StandardOutput{MarkdownOutput{hasInternetConnection: true}}, + issuesExist: true, + isComment: true, + content: []string{content}, + expectDetail: true, + }, + { + name: "PR summary findings simplified output", + writer: &SimplifiedOutput{}, + issuesExist: true, + isComment: true, + content: []string{content}, + expectDetail: true, + }, + { + name: "successful PR scan", + writer: &StandardOutput{MarkdownOutput{hasInternetConnection: true}}, + issuesExist: false, + isComment: true, + }, + { + name: "fix PR details include link", + writer: &StandardOutput{MarkdownOutput{hasInternetConnection: true}}, + issuesExist: true, + isComment: false, + content: []string{content}, + expectDetail: true, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + comments := GetMainCommentContent( + testCase.content, + testCase.issuesExist, + testCase.isComment, + resultsURL, + testCase.writer, + ) + + require.Len(t, comments, 1) + assert.Contains(t, comments[0], link) + assert.Contains(t, comments[0], testCase.writer.MarkAsTitle(link, 3)) + if testCase.expectDetail { + assert.Less(t, strings.Index(comments[0], link), strings.Index(comments[0], content)) + } + }) + } +} + +func TestGetMainCommentContentWithoutResultsPlatformURL(t *testing.T) { + writer := &StandardOutput{MarkdownOutput{hasInternetConnection: true}} + content := []string{"scan findings"} + + comments := GetMainCommentContent(content, true, true, "", writer) + + require.Len(t, comments, 1) + assert.Contains(t, comments[0], content[0]) + assert.NotContains(t, comments[0], scanResultsLinkText) +} + +// The integration tests compare the posted comment against a golden file, and rely on stripping the +// scan results link to restore a body that is byte for byte identical to the one produced without it. +func TestStripScanResultsLinkFromJsonBodyRestoresBodyWithoutLink(t *testing.T) { + const resultsURL = "https://example.jfrog.io/ui/scans-list/repositories/frogbot/scan-descendants/source_code_1785235666994.cdx.json?package_id=generic%3A%2F%2Fsha256%3Aabc%2Fsource_code.cdx.json" + content := []string{"## 📗 Scan Summary\n- Frogbot scanned for vulnerabilities and found 3 issues"} + + for _, writer := range []OutputWriter{ + &StandardOutput{MarkdownOutput{hasInternetConnection: true}}, + &SimplifiedOutput{MarkdownOutput{hasInternetConnection: true}}, + } { + t.Run(fmt.Sprintf("%T", writer), func(t *testing.T) { + withLink := GetMainCommentContent(content, true, true, resultsURL, writer) + withoutLink := GetMainCommentContent(content, true, true, "", writer) + require.Len(t, withLink, 1) + require.Len(t, withoutLink, 1) + + payload, err := json.Marshal(TestBodyResponse{Body: withLink[0]}) + require.NoError(t, err) + + stripped, found := StripScanResultsLinkFromJsonBody(t, payload) + assert.True(t, found) + + var strippedBody TestBodyResponse + require.NoError(t, json.Unmarshal(stripped, &strippedBody)) + assert.Equal(t, withoutLink[0], strippedBody.Body) + }) + } +} + +func TestStripScanResultsLinkFromJsonBodyWithoutLink(t *testing.T) { + payload, err := json.Marshal(TestBodyResponse{Body: "no link here"}) + require.NoError(t, err) + + stripped, found := StripScanResultsLinkFromJsonBody(t, payload) + assert.False(t, found) + assert.Equal(t, payload, stripped) +} + +func TestGetMainCommentContentAddsLinkOnlyToFirstSplitComment(t *testing.T) { + const resultsURL = "https://example.jfrog.io/ui/xray/scans-list/git-repositories/test" + writer := &SimplifiedOutput{MarkdownOutput{ + descriptionSizeLimit: 1200, + commentSizeLimit: 1200, + }} + + comments := GetMainCommentContent( + []string{strings.Repeat("a", 800), strings.Repeat("b", 800)}, + true, + true, + resultsURL, + writer, + ) + + require.Greater(t, len(comments), 1) + assert.Contains(t, comments[0], resultsURL) + for _, comment := range comments[1:] { + assert.NotContains(t, comment, resultsURL) + } +} + func TestScanSummaryContent(t *testing.T) { testScanStatus := formats.ScanStatus{ ScaStatusCode: utils.NewIntPtr(0), diff --git a/utils/outputwriter/testsutils.go b/utils/outputwriter/testsutils.go index 40ee5cfdf..e7e92119d 100644 --- a/utils/outputwriter/testsutils.go +++ b/utils/outputwriter/testsutils.go @@ -4,6 +4,7 @@ import ( "encoding/json" "os" "path/filepath" + "regexp" "strings" "testing" @@ -18,8 +19,24 @@ var ( testMessagesDir = filepath.Join("..", TestMessagesDir) testReviewCommentDir = filepath.Join(testMessagesDir, "reviewcomment") testSummaryCommentDir = filepath.Join(testMessagesDir, "summarycomment") + + // The scan results link embeds a hash of the uploaded results and a timestamped artifact name, + // so it changes on every run and cannot be stored in a golden file. + // The simplified rendering must be matched first, since it contains the standard one. + scanResultsLinkTitlePatterns = []*regexp.Regexp{ + scanResultsLinkTitlePattern(&SimplifiedOutput{}), + scanResultsLinkTitlePattern(&StandardOutput{}), + } ) +// scanResultsLinkTitlePattern matches the scan results link title as the given writer renders it, +// with any URL. +func scanResultsLinkTitlePattern(writer OutputWriter) *regexp.Regexp { + const urlPlaceholder = "\x00" + title := writer.MarkAsTitle(MarkAsLink(scanResultsLinkText, urlPlaceholder), 3) + return regexp.MustCompile("\n" + strings.Replace(regexp.QuoteMeta(title), urlPlaceholder, `[^)]*`, 1)) +} + type OutputTestCase struct { name string writer OutputWriter @@ -61,3 +78,21 @@ func GetJsonBodyOutputFromFile(t *testing.T, filePath string) []byte { assert.NoError(t, err) return bytes } + +// StripScanResultsLinkFromJsonBody removes the scan results platform link title from the body of a +// comment creation payload, so that the rest of the body can be compared against a golden file. +// It reports whether such a link was present. +func StripScanResultsLinkFromJsonBody(t *testing.T, payload []byte) (stripped []byte, found bool) { + var bodyRes TestBodyResponse + require.NoError(t, json.Unmarshal(payload, &bodyRes)) + for _, pattern := range scanResultsLinkTitlePatterns { + if !pattern.MatchString(bodyRes.Body) { + continue + } + bodyRes.Body = pattern.ReplaceAllString(bodyRes.Body, "") + stripped, err := json.Marshal(bodyRes) + require.NoError(t, err) + return stripped, true + } + return payload, false +} diff --git a/utils/utils.go b/utils/utils.go index b8e30ae63..5f9367584 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -547,7 +547,12 @@ func PrintScanResultsTable(scanResults *results.SecurityCommandResults) { if scanResults == nil { return } + extraMessages := []string{} + if scanResults.ResultsPlatformUrl != "" { + extraMessages = append(extraMessages, output.GetCommandResultsPlatformUrlMessage(scanResults, true)) + } if err := output.NewResultsWriter(scanResults). + SetExtraMessages(extraMessages). SetOutputFormat(format.Table). PrintScanResults(); err != nil { log.Warn(fmt.Sprintf("Failed to print scan results table: %s", err.Error())) diff --git a/utils/utils_test.go b/utils/utils_test.go index 4d406f16f..6542b714b 100644 --- a/utils/utils_test.go +++ b/utils/utils_test.go @@ -1,8 +1,8 @@ package utils import ( + "bytes" "encoding/json" - "github.com/jfrog/jfrog-client-go/xray/services" "net/http/httptest" "os" "path" @@ -18,6 +18,8 @@ import ( "github.com/jfrog/jfrog-cli-security/utils/formats" "github.com/jfrog/jfrog-cli-security/utils/results" "github.com/jfrog/jfrog-cli-security/utils/techutils" + "github.com/jfrog/jfrog-client-go/utils/log" + "github.com/jfrog/jfrog-client-go/xray/services" "github.com/stretchr/testify/assert" "github.com/jfrog/frogbot/v3/utils/gitlabreport" @@ -684,3 +686,49 @@ func TestPrintScanResultsTable(t *testing.T) { }) } } + +func TestPrintScanResultsTableResultsPlatformURL(t *testing.T) { + const resultsURL = "https://example.jfrog.io/ui/xray/scans-list/git-repositories/test" + originalLogger := log.GetLogger() + t.Cleanup(func() { + log.SetLogger(originalLogger) + }) + + testCases := []struct { + name string + resultsPlatformURL string + expectURL bool + }{ + { + name: "prints available URL", + resultsPlatformURL: resultsURL, + expectURL: true, + }, + { + name: "omits unavailable URL", + expectURL: false, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + var output bytes.Buffer + log.SetLogger(log.NewLogger(log.INFO, &output)) + scanResults := &results.SecurityCommandResults{ + ResultsMetaData: results.ResultsMetaData{ + ResultsPlatformUrl: testCase.resultsPlatformURL, + }, + Targets: []*results.TargetResults{}, + } + + PrintScanResultsTable(scanResults) + + if testCase.expectURL { + assert.Contains(t, output.String(), resultsURL) + assert.Contains(t, output.String(), "You may view the scan results in the JFrog platform") + } else { + assert.NotContains(t, output.String(), "You may view the scan results in the JFrog platform") + } + }) + } +}