diff --git a/go.mod b/go.mod index 891def452..67e486a0e 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/google/uuid v1.3.1 github.com/gookit/color v1.5.4 github.com/jedib0t/go-pretty/v6 v6.4.7 - github.com/jfrog/build-info-go v1.9.9 + github.com/jfrog/build-info-go v1.9.10 github.com/jfrog/gofrog v1.3.0 github.com/jfrog/jfrog-client-go v1.31.6 github.com/magiconair/properties v1.8.7 @@ -23,13 +23,12 @@ require ( github.com/stretchr/testify v1.8.4 github.com/urfave/cli v1.22.14 github.com/vbauerster/mpb/v7 v7.5.3 - golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 + golang.org/x/exp v0.0.0-20230905200255-921286631fa9 golang.org/x/mod v0.12.0 golang.org/x/sync v0.3.0 - golang.org/x/term v0.11.0 - golang.org/x/text v0.12.0 + golang.org/x/term v0.12.0 + golang.org/x/text v0.13.0 gopkg.in/yaml.v3 v3.0.1 - ) require ( @@ -86,16 +85,16 @@ require ( github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect - golang.org/x/crypto v0.12.0 // indirect - golang.org/x/net v0.14.0 // indirect - golang.org/x/sys v0.11.0 // indirect - golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846 // indirect + golang.org/x/crypto v0.13.0 // indirect + golang.org/x/net v0.15.0 // indirect + golang.org/x/sys v0.12.0 // indirect + golang.org/x/tools v0.13.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect ) -replace github.com/jfrog/jfrog-client-go => github.com/jfrog/jfrog-client-go v1.28.1-0.20230831152946-6ed2ae1aa57f +replace github.com/jfrog/jfrog-client-go => github.com/jfrog/jfrog-client-go v1.28.1-0.20230910192358-6994626b2069 -replace github.com/jfrog/build-info-go => github.com/jfrog/build-info-go v1.8.9-0.20230905120411-62d1bdd4eb38 +// replace github.com/jfrog/build-info-go => github.com/jfrog/build-info-go v1.8.9-0.20230905120411-62d1bdd4eb38 // replace github.com/jfrog/gofrog => github.com/jfrog/gofrog v1.2.6-0.20230418122323-2bf299dd6d27 diff --git a/go.sum b/go.sum index a028a4cfc..84ce912e5 100644 --- a/go.sum +++ b/go.sum @@ -194,12 +194,12 @@ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOl github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jedib0t/go-pretty/v6 v6.4.7 h1:lwiTJr1DEkAgzljsUsORmWsVn5MQjt1BPJdPCtJ6KXE= github.com/jedib0t/go-pretty/v6 v6.4.7/go.mod h1:Ndk3ase2CkQbXLLNf5QDHoYb6J9WtVfmHZu9n8rk2xs= -github.com/jfrog/build-info-go v1.8.9-0.20230905120411-62d1bdd4eb38 h1:XyAcwWP2a6a5RL861gkfgQ7MUaQ7mmDkUVoD6kMtUtQ= -github.com/jfrog/build-info-go v1.8.9-0.20230905120411-62d1bdd4eb38/go.mod h1:QEskae5fQpjeY2PBzsjWtUQVskYSNDF2sSmw/Gx44dQ= +github.com/jfrog/build-info-go v1.9.10 h1:uXnDLVxpqxoAMpXcki00QaBB+M2BoGMMpHODPkmmYOY= +github.com/jfrog/build-info-go v1.9.10/go.mod h1:ujJ8XQZMdT2tMkLSMJNyDd1pCY+duwHdjV+9or9FLIg= github.com/jfrog/gofrog v1.3.0 h1:o4zgsBZE4QyDbz2M7D4K6fXPTBJht+8lE87mS9bw7Gk= github.com/jfrog/gofrog v1.3.0/go.mod h1:IFMc+V/yf7rA5WZ74CSbXe+Lgf0iApEQLxRZVzKRUR0= -github.com/jfrog/jfrog-client-go v1.28.1-0.20230831152946-6ed2ae1aa57f h1:S6l0o2sKFLRJ+QYVB5U/PJhrnwFSmKFFY7eHpRPRH8A= -github.com/jfrog/jfrog-client-go v1.28.1-0.20230831152946-6ed2ae1aa57f/go.mod h1:uUnMrqHX7Xi+OCaZEE4b3BtsmGeOSCB7XqaEWVXEH/E= +github.com/jfrog/jfrog-client-go v1.28.1-0.20230910192358-6994626b2069 h1:vk+P6jK4Zv8+F44ZnRxXUPT14BQxjJtNKdpGdemci7A= +github.com/jfrog/jfrog-client-go v1.28.1-0.20230910192358-6994626b2069/go.mod h1:362+oa7uTTYurzBs1L0dmUTlLo7uhpAU/pwM5Zb9clg= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= @@ -350,8 +350,8 @@ golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= -golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= 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= @@ -362,8 +362,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 h1:m64FZMko/V45gv0bNmrNYoDEq8U5YUhetc9cBWKS1TQ= -golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63/go.mod h1:0v4NqG35kSWCMzLaMeX+IQrlSnVE/bqGSyC2cz/9Le8= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -427,8 +427,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= -golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= +golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= 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= @@ -511,15 +511,15 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= -golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0= -golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= +golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= 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= @@ -532,8 +532,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= -golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 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= @@ -586,8 +586,8 @@ golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= 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.12.1-0.20230815132531-74c255bcf846 h1:Vve/L0v7CXXuxUmaMGIEK/dEeq7uiqb5qBgQrZzIE7E= -golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM= +golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= 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/xray/commands/audit/audit.go b/xray/commands/audit/audit.go index 1200a0599..bb35648bc 100644 --- a/xray/commands/audit/audit.go +++ b/xray/commands/audit/audit.go @@ -165,6 +165,7 @@ func RunAudit(auditParams *AuditParams) (results *Results, err error) { if err = clientutils.ValidateMinimumVersion(clientutils.Xray, auditParams.xrayVersion, scangraph.GraphScanMinXrayVersion); err != nil { return } + results.ExtendedScanResults.XrayVersion = auditParams.xrayVersion results.ExtendedScanResults.EntitledForJas, err = isEntitledForJas(xrayManager, auditParams.xrayVersion) if err != nil { return diff --git a/xray/commands/audit/jas/applicability/applicabilitymanager.go b/xray/commands/audit/jas/applicability/applicabilitymanager.go index cbab69833..8c46f4865 100644 --- a/xray/commands/audit/jas/applicability/applicabilitymanager.go +++ b/xray/commands/audit/jas/applicability/applicabilitymanager.go @@ -1,14 +1,13 @@ package applicability import ( - "github.com/jfrog/jfrog-cli-core/v2/xray/commands/audit/jas" "path/filepath" - "strings" + + "github.com/jfrog/jfrog-cli-core/v2/xray/commands/audit/jas" "github.com/jfrog/gofrog/datastructures" "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" "github.com/jfrog/jfrog-cli-core/v2/xray/utils" - "github.com/jfrog/jfrog-client-go/utils/errorutils" "github.com/jfrog/jfrog-client-go/utils/log" "github.com/jfrog/jfrog-client-go/xray/services" "github.com/owenrumney/go-sarif/v2/sarif" @@ -22,7 +21,7 @@ const ( ) type ApplicabilityScanManager struct { - applicabilityScanResults map[string]utils.ApplicabilityStatus + applicabilityScanResults []*sarif.Run directDependenciesCves []string xrayResults []services.ScanResponse scanner *jas.JasScanner @@ -38,7 +37,7 @@ type ApplicabilityScanManager struct { // bool: true if the user is entitled to the applicability scan, false otherwise. // error: An error object (if any). func RunApplicabilityScan(xrayResults []services.ScanResponse, directDependencies []string, - scannedTechnologies []coreutils.Technology, scanner *jas.JasScanner) (results map[string]utils.ApplicabilityStatus, err error) { + scannedTechnologies []coreutils.Technology, scanner *jas.JasScanner) (results []*sarif.Run, err error) { applicabilityScanManager := newApplicabilityScanManager(xrayResults, directDependencies, scanner) if !applicabilityScanManager.shouldRunApplicabilityScan(scannedTechnologies) { log.Debug("The technologies that have been scanned are currently not supported for contextual analysis scanning, or we couldn't find any vulnerable direct dependencies. Skipping....") @@ -55,7 +54,7 @@ func RunApplicabilityScan(xrayResults []services.ScanResponse, directDependencie func newApplicabilityScanManager(xrayScanResults []services.ScanResponse, directDependencies []string, scanner *jas.JasScanner) (manager *ApplicabilityScanManager) { directDependenciesCves := extractDirectDependenciesCvesFromScan(xrayScanResults, directDependencies) return &ApplicabilityScanManager{ - applicabilityScanResults: map[string]utils.ApplicabilityStatus{}, + applicabilityScanResults: []*sarif.Run{}, directDependenciesCves: directDependenciesCves, xrayResults: xrayScanResults, scanner: scanner, @@ -111,13 +110,11 @@ func (asm *ApplicabilityScanManager) Run(wd string) (err error) { if err = asm.runAnalyzerManager(); err != nil { return } - var workingDirResults map[string]utils.ApplicabilityStatus - if workingDirResults, err = asm.getScanResults(); err != nil { + workingDirResults, err := jas.ReadJasScanRunsFromFile(asm.scanner.ResultsFileName, wd) + if err != nil { return } - for cve, result := range workingDirResults { - asm.applicabilityScanResults[cve] = result - } + asm.applicabilityScanResults = append(asm.applicabilityScanResults, workingDirResults...) return } @@ -163,37 +160,3 @@ func (asm *ApplicabilityScanManager) createConfigFile(workingDir string) error { func (asm *ApplicabilityScanManager) runAnalyzerManager() error { return asm.scanner.AnalyzerManager.Exec(asm.scanner.ConfigFileName, applicabilityScanCommand, filepath.Dir(asm.scanner.AnalyzerManager.AnalyzerManagerFullPath), asm.scanner.ServerDetails) } - -func (asm *ApplicabilityScanManager) getScanResults() (applicabilityResults map[string]utils.ApplicabilityStatus, err error) { - applicabilityResults = make(map[string]utils.ApplicabilityStatus, len(asm.directDependenciesCves)) - for _, cve := range asm.directDependenciesCves { - applicabilityResults[cve] = utils.ApplicabilityUndetermined - } - - report, err := sarif.Open(asm.scanner.ResultsFileName) - if errorutils.CheckError(err) != nil || len(report.Runs) == 0 { - return - } - // Applicability results contains one run only - for _, sarifResult := range report.Runs[0].Results { - cve := getCveFromRuleId(*sarifResult.RuleID) - if _, exists := applicabilityResults[cve]; !exists { - err = errorutils.CheckErrorf("received unexpected CVE: '%s' from RuleID: '%s' that does not exists on the requested CVEs list", cve, *sarifResult.RuleID) - return - } - applicabilityResults[cve] = resultKindToApplicabilityStatus(sarifResult.Kind) - } - return -} - -// Gets a result of one CVE from the scanner, and returns true if the CVE is applicable, false otherwise -func resultKindToApplicabilityStatus(kind *string) utils.ApplicabilityStatus { - if !(kind != nil && *kind == "pass") { - return utils.Applicable - } - return utils.NotApplicable -} - -func getCveFromRuleId(sarifRuleId string) string { - return strings.TrimPrefix(sarifRuleId, "applic_") -} diff --git a/xray/commands/audit/jas/applicability/applicabilitymanager_test.go b/xray/commands/audit/jas/applicability/applicabilitymanager_test.go index a3144a9c2..d605bd0cd 100644 --- a/xray/commands/audit/jas/applicability/applicabilitymanager_test.go +++ b/xray/commands/audit/jas/applicability/applicabilitymanager_test.go @@ -3,7 +3,6 @@ package applicability import ( "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" "github.com/jfrog/jfrog-cli-core/v2/xray/commands/audit/jas" - "github.com/jfrog/jfrog-cli-core/v2/xray/utils" "github.com/jfrog/jfrog-client-go/xray/services" "github.com/stretchr/testify/assert" "os" @@ -276,13 +275,12 @@ func TestParseResults_EmptyResults_AllCvesShouldGetUnknown(t *testing.T) { applicabilityManager.scanner.ResultsFileName = filepath.Join(jas.GetTestDataPath(), "applicability-scan", "empty-results.sarif") // Act - results, err := applicabilityManager.getScanResults() + var err error + applicabilityManager.applicabilityScanResults, err = jas.ReadJasScanRunsFromFile(applicabilityManager.scanner.ResultsFileName, scanner.WorkingDirs[0]) - // Assert - assert.NoError(t, err) - assert.Equal(t, 5, len(results)) - for _, cveResult := range results { - assert.Equal(t, utils.ApplicabilityUndetermined, cveResult) + if assert.NoError(t, err) { + assert.Len(t, applicabilityManager.applicabilityScanResults, 1) + assert.Empty(t, applicabilityManager.applicabilityScanResults[0].Results) } } @@ -294,13 +292,13 @@ func TestParseResults_ApplicableCveExist(t *testing.T) { applicabilityManager.scanner.ResultsFileName = filepath.Join(jas.GetTestDataPath(), "applicability-scan", "applicable-cve-results.sarif") // Act - results, err := applicabilityManager.getScanResults() + var err error + applicabilityManager.applicabilityScanResults, err = jas.ReadJasScanRunsFromFile(applicabilityManager.scanner.ResultsFileName, scanner.WorkingDirs[0]) - // Assert - assert.NoError(t, err) - assert.Equal(t, 5, len(results)) - assert.Equal(t, utils.Applicable, results["testCve1"]) - assert.Equal(t, utils.NotApplicable, results["testCve3"]) + if assert.NoError(t, err) && assert.NotNil(t, applicabilityManager.applicabilityScanResults) { + assert.Len(t, applicabilityManager.applicabilityScanResults, 1) + assert.NotEmpty(t, applicabilityManager.applicabilityScanResults[0].Results) + } } func TestParseResults_AllCvesNotApplicable(t *testing.T) { @@ -311,12 +309,11 @@ func TestParseResults_AllCvesNotApplicable(t *testing.T) { applicabilityManager.scanner.ResultsFileName = filepath.Join(jas.GetTestDataPath(), "applicability-scan", "no-applicable-cves-results.sarif") // Act - results, err := applicabilityManager.getScanResults() + var err error + applicabilityManager.applicabilityScanResults, err = jas.ReadJasScanRunsFromFile(applicabilityManager.scanner.ResultsFileName, scanner.WorkingDirs[0]) - // Assert - assert.NoError(t, err) - assert.Equal(t, 5, len(results)) - for _, cveResult := range results { - assert.Equal(t, utils.NotApplicable, cveResult) + if assert.NoError(t, err) && assert.NotNil(t, applicabilityManager.applicabilityScanResults) { + assert.Len(t, applicabilityManager.applicabilityScanResults, 1) + assert.NotEmpty(t, applicabilityManager.applicabilityScanResults[0].Results) } } diff --git a/xray/commands/audit/jas/common.go b/xray/commands/audit/jas/common.go index 9616976f9..068817550 100644 --- a/xray/commands/audit/jas/common.go +++ b/xray/commands/audit/jas/common.go @@ -2,6 +2,11 @@ package jas import ( "errors" + "os" + "path/filepath" + "strings" + "testing" + rtutils "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils" "github.com/jfrog/jfrog-cli-core/v2/utils/config" "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" @@ -12,14 +17,19 @@ import ( "github.com/owenrumney/go-sarif/v2/sarif" "github.com/stretchr/testify/assert" "gopkg.in/yaml.v3" - "os" - "path/filepath" - "strings" - "testing" ) var ( SkippedDirs = []string{"**/*test*/**", "**/*venv*/**", "**/*node_modules*/**", "**/*target*/**"} + + mapSeverityToScore = map[string]string{ + "": "0.0", + "unknown": "0.0", + "low": "3.9", + "medium": "6.9", + "high": "8.9", + "critical": "10", + } ) type JasScanner struct { @@ -88,46 +98,54 @@ func deleteJasProcessFiles(configFile string, resultFile string) error { return errorutils.CheckError(err) } -func GetSourceCodeScanResults(resultsFileName, workingDir string, scanType utils.JasScanType) (results []utils.SourceCodeScanResult, err error) { - // Read Sarif format results generated from the Jas scanner - report, err := sarif.Open(resultsFileName) - if errorutils.CheckError(err) != nil { - return nil, err - } - var sarifResults []*sarif.Result - if len(report.Runs) > 0 { - // Jas scanners returns results in a single run entry - sarifResults = report.Runs[0].Results +func ReadJasScanRunsFromFile(fileName, wd string) (sarifRuns []*sarif.Run, err error) { + if sarifRuns, err = utils.ReadScanRunsFromFile(fileName); err != nil { + return } - resultPointers := convertSarifResultsToSourceCodeScanResults(sarifResults, workingDir, scanType) - for _, res := range resultPointers { - results = append(results, *res) + for _, sarifRun := range sarifRuns { + // Jas reports has only one invocation + // Set the actual working directory to the invocation, not the analyzerManager directory + // Also used to calculate relative paths if needed with it + sarifRun.Invocations[0].WorkingDirectory.WithUri(wd) + // Process runs values + sarifRun.Results = excludeSuppressResults(sarifRun.Results) + addScoreToRunRules(sarifRun) } - return results, nil + return } -func convertSarifResultsToSourceCodeScanResults(sarifResults []*sarif.Result, workingDir string, scanType utils.JasScanType) []*utils.SourceCodeScanResult { - var sourceCodeScanResults []*utils.SourceCodeScanResult +func excludeSuppressResults(sarifResults []*sarif.Result) []*sarif.Result { + results := []*sarif.Result{} for _, sarifResult := range sarifResults { - // Describes a request to “suppress” a result (to exclude it from result lists) if len(sarifResult.Suppressions) > 0 { + // Describes a request to “suppress” a result (to exclude it from result lists) continue } - // Convert - currentResult := utils.GetResultIfExists(sarifResult, workingDir, sourceCodeScanResults) - if currentResult == nil { - currentResult = utils.ConvertSarifResultToSourceCodeScanResult(sarifResult, workingDir) - // Set specific Jas scan attributes - if scanType == utils.Secrets { - currentResult.Text = hideSecret(utils.GetResultLocationSnippet(sarifResult.Locations[0])) + results = append(results, sarifResult) + } + return results +} + +func addScoreToRunRules(sarifRun *sarif.Run) { + for _, sarifResult := range sarifRun.Results { + if rule, err := sarifRun.GetRuleById(*sarifResult.RuleID); err == nil { + // Add to the rule security-severity score based on results severity + score := convertToScore(utils.GetResultSeverity(sarifResult)) + if score != utils.MissingCveScore { + if rule.Properties == nil { + rule.WithProperties(sarif.NewPropertyBag().Properties) + } + rule.Properties["security-severity"] = score } - sourceCodeScanResults = append(sourceCodeScanResults, currentResult) - } - if scanType == utils.Sast { - currentResult.CodeFlow = append(currentResult.CodeFlow, utils.GetResultCodeFlows(sarifResult, workingDir)...) } } - return sourceCodeScanResults +} + +func convertToScore(severity string) string { + if level, ok := mapSeverityToScore[strings.ToLower(severity)]; ok { + return level + } + return "" } func CreateScannersConfigFile(fileName string, fileContent interface{}) error { @@ -139,13 +157,6 @@ func CreateScannersConfigFile(fileName string, fileContent interface{}) error { return errorutils.CheckError(err) } -func hideSecret(secret string) string { - if len(secret) <= 3 { - return "***" - } - return secret[:3] + strings.Repeat("*", 12) -} - var FakeServerDetails = config.ServerDetails{ Url: "platformUrl", Password: "password", diff --git a/xray/commands/audit/jas/common_test.go b/xray/commands/audit/jas/common_test.go deleted file mode 100644 index 305629f31..000000000 --- a/xray/commands/audit/jas/common_test.go +++ /dev/null @@ -1,23 +0,0 @@ -package jas - -import ( - "github.com/stretchr/testify/assert" - "testing" -) - -func TestHideSecret(t *testing.T) { - tests := []struct { - secret string - expectedOutput string - }{ - {secret: "", expectedOutput: "***"}, - {secret: "12", expectedOutput: "***"}, - {secret: "123", expectedOutput: "***"}, - {secret: "123456789", expectedOutput: "123************"}, - {secret: "3478hfnkjhvd848446gghgfh", expectedOutput: "347************"}, - } - - for _, test := range tests { - assert.Equal(t, test.expectedOutput, hideSecret(test.secret)) - } -} diff --git a/xray/commands/audit/jas/iac/iacscanner.go b/xray/commands/audit/jas/iac/iacscanner.go index c4fe18062..c4ccfdd39 100644 --- a/xray/commands/audit/jas/iac/iacscanner.go +++ b/xray/commands/audit/jas/iac/iacscanner.go @@ -6,6 +6,7 @@ import ( "github.com/jfrog/jfrog-cli-core/v2/xray/utils" "github.com/jfrog/jfrog-client-go/utils/log" + "github.com/owenrumney/go-sarif/v2/sarif" ) const ( @@ -14,7 +15,7 @@ const ( ) type IacScanManager struct { - iacScannerResults []utils.SourceCodeScanResult + iacScannerResults []*sarif.Run scanner *jas.JasScanner } @@ -26,7 +27,7 @@ type IacScanManager struct { // []utils.SourceCodeScanResult: a list of the iac violations that were found. // bool: true if the user is entitled to iac scan, false otherwise. // error: An error object (if any). -func RunIacScan(scanner *jas.JasScanner) (results []utils.SourceCodeScanResult, err error) { +func RunIacScan(scanner *jas.JasScanner) (results []*sarif.Run, err error) { iacScanManager := newIacScanManager(scanner) log.Info("Running IaC scanning...") if err = iacScanManager.scanner.Run(iacScanManager); err != nil { @@ -34,7 +35,7 @@ func RunIacScan(scanner *jas.JasScanner) (results []utils.SourceCodeScanResult, return } if len(iacScanManager.iacScannerResults) > 0 { - log.Info("Found", len(iacScanManager.iacScannerResults), "IaC vulnerabilities") + log.Info("Found", utils.GetResultsLocationCount(iacScanManager.iacScannerResults...), "IaC vulnerabilities") } results = iacScanManager.iacScannerResults return @@ -42,7 +43,7 @@ func RunIacScan(scanner *jas.JasScanner) (results []utils.SourceCodeScanResult, func newIacScanManager(scanner *jas.JasScanner) (manager *IacScanManager) { return &IacScanManager{ - iacScannerResults: []utils.SourceCodeScanResult{}, + iacScannerResults: []*sarif.Run{}, scanner: scanner, } } @@ -55,8 +56,8 @@ func (iac *IacScanManager) Run(wd string) (err error) { if err = iac.runAnalyzerManager(); err != nil { return } - var workingDirResults []utils.SourceCodeScanResult - if workingDirResults, err = jas.GetSourceCodeScanResults(scanner.ResultsFileName, wd, utils.IaC); err != nil { + workingDirResults, err := jas.ReadJasScanRunsFromFile(scanner.ResultsFileName, wd) + if err != nil { return } iac.iacScannerResults = append(iac.iacScannerResults, workingDirResults...) diff --git a/xray/commands/audit/jas/iac/iacscanner_test.go b/xray/commands/audit/jas/iac/iacscanner_test.go index a43fef61a..a2332421d 100644 --- a/xray/commands/audit/jas/iac/iacscanner_test.go +++ b/xray/commands/audit/jas/iac/iacscanner_test.go @@ -7,7 +7,6 @@ import ( "testing" "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" - "github.com/jfrog/jfrog-cli-core/v2/xray/utils" "github.com/stretchr/testify/assert" ) @@ -58,11 +57,11 @@ func TestIacParseResults_EmptyResults(t *testing.T) { // Act var err error - iacScanManager.iacScannerResults, err = jas.GetSourceCodeScanResults(iacScanManager.scanner.ResultsFileName, scanner.WorkingDirs[0], utils.IaC) - - // Assert - assert.NoError(t, err) - assert.Empty(t, iacScanManager.iacScannerResults) + iacScanManager.iacScannerResults, err = jas.ReadJasScanRunsFromFile(iacScanManager.scanner.ResultsFileName, scanner.WorkingDirs[0]) + if assert.NoError(t, err) && assert.NotNil(t, iacScanManager.iacScannerResults) { + assert.Len(t, iacScanManager.iacScannerResults, 1) + assert.Empty(t, iacScanManager.iacScannerResults[0].Results) + } } func TestIacParseResults_ResultsContainIacViolations(t *testing.T) { @@ -74,10 +73,9 @@ func TestIacParseResults_ResultsContainIacViolations(t *testing.T) { // Act var err error - iacScanManager.iacScannerResults, err = jas.GetSourceCodeScanResults(iacScanManager.scanner.ResultsFileName, scanner.WorkingDirs[0], utils.IaC) - - // Assert - assert.NoError(t, err) - assert.NotEmpty(t, iacScanManager.iacScannerResults) - assert.Equal(t, 4, len(iacScanManager.iacScannerResults)) + iacScanManager.iacScannerResults, err = jas.ReadJasScanRunsFromFile(iacScanManager.scanner.ResultsFileName, scanner.WorkingDirs[0]) + if assert.NoError(t, err) && assert.NotNil(t, iacScanManager.iacScannerResults) { + assert.Len(t, iacScanManager.iacScannerResults, 1) + assert.Len(t, iacScanManager.iacScannerResults[0].Results, 4) + } } diff --git a/xray/commands/audit/jas/sast/sastscanner.go b/xray/commands/audit/jas/sast/sastscanner.go index dc31fa9ea..35211396f 100644 --- a/xray/commands/audit/jas/sast/sastscanner.go +++ b/xray/commands/audit/jas/sast/sastscanner.go @@ -4,6 +4,8 @@ import ( "github.com/jfrog/jfrog-cli-core/v2/xray/commands/audit/jas" "github.com/jfrog/jfrog-cli-core/v2/xray/utils" "github.com/jfrog/jfrog-client-go/utils/log" + "github.com/owenrumney/go-sarif/v2/sarif" + "golang.org/x/exp/maps" ) const ( @@ -11,11 +13,11 @@ const ( ) type SastScanManager struct { - sastScannerResults []utils.SourceCodeScanResult + sastScannerResults []*sarif.Run scanner *jas.JasScanner } -func RunSastScan(scanner *jas.JasScanner) (results []utils.SourceCodeScanResult, err error) { +func RunSastScan(scanner *jas.JasScanner) (results []*sarif.Run, err error) { sastScanManager := newSastScanManager(scanner) log.Info("Running SAST scanning...") if err = sastScanManager.scanner.Run(sastScanManager); err != nil { @@ -23,7 +25,7 @@ func RunSastScan(scanner *jas.JasScanner) (results []utils.SourceCodeScanResult, return } if len(sastScanManager.sastScannerResults) > 0 { - log.Info("Found", len(sastScanManager.sastScannerResults), "SAST vulnerabilities") + log.Info("Found", utils.GetResultsLocationCount(sastScanManager.sastScannerResults...), "SAST vulnerabilities") } results = sastScanManager.sastScannerResults return @@ -31,7 +33,7 @@ func RunSastScan(scanner *jas.JasScanner) (results []utils.SourceCodeScanResult, func newSastScanManager(scanner *jas.JasScanner) (manager *SastScanManager) { return &SastScanManager{ - sastScannerResults: []utils.SourceCodeScanResult{}, + sastScannerResults: []*sarif.Run{}, scanner: scanner, } } @@ -41,14 +43,53 @@ func (ssm *SastScanManager) Run(wd string) (err error) { if err = ssm.runAnalyzerManager(wd); err != nil { return } - var workingDirResults []utils.SourceCodeScanResult - if workingDirResults, err = jas.GetSourceCodeScanResults(scanner.ResultsFileName, wd, utils.Sast); err != nil { + workingDirRuns, err := jas.ReadJasScanRunsFromFile(scanner.ResultsFileName, wd) + if err != nil { return } - ssm.sastScannerResults = append(ssm.sastScannerResults, workingDirResults...) + ssm.sastScannerResults = append(ssm.sastScannerResults, groupResultsByLocation(workingDirRuns)...) return } func (ssm *SastScanManager) runAnalyzerManager(wd string) error { return ssm.scanner.AnalyzerManager.Exec(ssm.scanner.ResultsFileName, sastScanCommand, wd, ssm.scanner.ServerDetails) } + +// In the Sast scanner, there can be multiple results with the same location. +// The only difference is that their CodeFlow values are different. +// We combine those under the same result location value +func groupResultsByLocation(sarifRuns []*sarif.Run) []*sarif.Run { + for _, sastRun := range sarifRuns { + locationToResult := map[string]*sarif.Result{} + for _, sastResult := range sastRun.Results { + resultID := getResultId(sastResult) + if result, exists := locationToResult[resultID]; exists { + result.CodeFlows = append(result.CodeFlows, sastResult.CodeFlows...) + } else { + locationToResult[resultID] = sastResult + } + } + sastRun.Results = maps.Values(locationToResult) + } + return sarifRuns +} + +// In Sast there is only one location for each result +func getResultFileName(result *sarif.Result) string { + if len(result.Locations) > 0 { + return utils.GetLocationFileName(result.Locations[0]) + } + return "" +} + +// In Sast there is only one location for each result +func getResultStartLocationInFile(result *sarif.Result) string { + if len(result.Locations) > 0 { + return utils.GetStartLocationInFile(result.Locations[0]) + } + return "" +} + +func getResultId(result *sarif.Result) string { + return getResultFileName(result) + getResultStartLocationInFile(result) + utils.GetResultSeverity(result) + utils.GetResultMsgText(result) +} diff --git a/xray/commands/audit/jas/sast/sastscanner_test.go b/xray/commands/audit/jas/sast/sastscanner_test.go index 969ab80ce..66c423403 100644 --- a/xray/commands/audit/jas/sast/sastscanner_test.go +++ b/xray/commands/audit/jas/sast/sastscanner_test.go @@ -1,11 +1,11 @@ package sast import ( - "github.com/jfrog/jfrog-cli-core/v2/xray/commands/audit/jas" "path/filepath" "testing" - "github.com/jfrog/jfrog-cli-core/v2/xray/utils" + "github.com/jfrog/jfrog-cli-core/v2/xray/commands/audit/jas" + "github.com/stretchr/testify/assert" ) @@ -34,11 +34,16 @@ func TestSastParseResults_EmptyResults(t *testing.T) { // Act var err error - sastScanManager.sastScannerResults, err = jas.GetSourceCodeScanResults(sastScanManager.scanner.ResultsFileName, scanner.WorkingDirs[0], utils.Sast) + sastScanManager.sastScannerResults, err = jas.ReadJasScanRunsFromFile(sastScanManager.scanner.ResultsFileName, scanner.WorkingDirs[0]) // Assert - assert.NoError(t, err) - assert.Empty(t, sastScanManager.sastScannerResults) + if assert.NoError(t, err) && assert.NotNil(t, sastScanManager.sastScannerResults) { + assert.Len(t, sastScanManager.sastScannerResults, 1) + assert.Empty(t, sastScanManager.sastScannerResults[0].Results) + sastScanManager.sastScannerResults = groupResultsByLocation(sastScanManager.sastScannerResults) + assert.Len(t, sastScanManager.sastScannerResults, 1) + assert.Empty(t, sastScanManager.sastScannerResults[0].Results) + } } func TestSastParseResults_ResultsContainIacViolations(t *testing.T) { @@ -50,11 +55,14 @@ func TestSastParseResults_ResultsContainIacViolations(t *testing.T) { // Act var err error - sastScanManager.sastScannerResults, err = jas.GetSourceCodeScanResults(sastScanManager.scanner.ResultsFileName, scanner.WorkingDirs[0], utils.Sast) + sastScanManager.sastScannerResults, err = jas.ReadJasScanRunsFromFile(sastScanManager.scanner.ResultsFileName, scanner.WorkingDirs[0]) // Assert - assert.NoError(t, err) - assert.NotEmpty(t, sastScanManager.sastScannerResults) - // File has 4 results, 2 of them at the same location different codeFlow - assert.Equal(t, 3, len(sastScanManager.sastScannerResults)) + if assert.NoError(t, err) && assert.NotNil(t, sastScanManager.sastScannerResults) { + assert.Len(t, sastScanManager.sastScannerResults, 1) + assert.NotEmpty(t, sastScanManager.sastScannerResults[0].Results) + sastScanManager.sastScannerResults = groupResultsByLocation(sastScanManager.sastScannerResults) + // File has 4 results, 2 of them at the same location different codeFlow + assert.Len(t, sastScanManager.sastScannerResults[0].Results, 3) + } } diff --git a/xray/commands/audit/jas/secrets/secretsscanner.go b/xray/commands/audit/jas/secrets/secretsscanner.go index ef6722ec2..cf5df05f8 100644 --- a/xray/commands/audit/jas/secrets/secretsscanner.go +++ b/xray/commands/audit/jas/secrets/secretsscanner.go @@ -1,10 +1,13 @@ package secrets import ( + "path/filepath" + "strings" + "github.com/jfrog/jfrog-cli-core/v2/xray/commands/audit/jas" "github.com/jfrog/jfrog-cli-core/v2/xray/utils" "github.com/jfrog/jfrog-client-go/utils/log" - "path/filepath" + "github.com/owenrumney/go-sarif/v2/sarif" ) const ( @@ -13,7 +16,7 @@ const ( ) type SecretScanManager struct { - secretsScannerResults []utils.SourceCodeScanResult + secretsScannerResults []*sarif.Run scanner *jas.JasScanner } @@ -24,7 +27,7 @@ type SecretScanManager struct { // Return values: // []utils.IacOrSecretResult: a list of the secrets that were found. // error: An error object (if any). -func RunSecretsScan(scanner *jas.JasScanner) (results []utils.SourceCodeScanResult, err error) { +func RunSecretsScan(scanner *jas.JasScanner) (results []*sarif.Run, err error) { secretScanManager := newSecretsScanManager(scanner) log.Info("Running secrets scanning...") if err = secretScanManager.scanner.Run(secretScanManager); err != nil { @@ -33,14 +36,14 @@ func RunSecretsScan(scanner *jas.JasScanner) (results []utils.SourceCodeScanResu } results = secretScanManager.secretsScannerResults if len(results) > 0 { - log.Info("Found", len(results), "secrets") + log.Info("Found", utils.GetResultsLocationCount(results...), "secrets") } return } func newSecretsScanManager(scanner *jas.JasScanner) (manager *SecretScanManager) { return &SecretScanManager{ - secretsScannerResults: []utils.SourceCodeScanResult{}, + secretsScannerResults: []*sarif.Run{}, scanner: scanner, } } @@ -53,11 +56,11 @@ func (s *SecretScanManager) Run(wd string) (err error) { if err = s.runAnalyzerManager(); err != nil { return } - var workingDirResults []utils.SourceCodeScanResult - if workingDirResults, err = jas.GetSourceCodeScanResults(scanner.ResultsFileName, wd, utils.Secrets); err != nil { + workingDirRuns, err := jas.ReadJasScanRunsFromFile(scanner.ResultsFileName, wd) + if err != nil { return } - s.secretsScannerResults = append(s.secretsScannerResults, workingDirResults...) + s.secretsScannerResults = append(s.secretsScannerResults, processSecretScanRuns(workingDirRuns)...) return } @@ -89,3 +92,23 @@ func (s *SecretScanManager) createConfigFile(currentWd string) error { func (s *SecretScanManager) runAnalyzerManager() error { return s.scanner.AnalyzerManager.Exec(s.scanner.ConfigFileName, secretsScanCommand, filepath.Dir(s.scanner.AnalyzerManager.AnalyzerManagerFullPath), s.scanner.ServerDetails) } + +func maskSecret(secret string) string { + if len(secret) <= 3 { + return "***" + } + return secret[:3] + strings.Repeat("*", 12) +} + +func processSecretScanRuns(sarifRuns []*sarif.Run) []*sarif.Run { + for _, secretRun := range sarifRuns { + // Hide discovered secrets value + for _, secretResult := range secretRun.Results { + for _, location := range secretResult.Locations { + secret := utils.GetLocationSnippetPointer(location) + utils.SetLocationSnippet(location, maskSecret(*secret)) + } + } + } + return sarifRuns +} diff --git a/xray/commands/audit/jas/secrets/secretsscanner_test.go b/xray/commands/audit/jas/secrets/secretsscanner_test.go index f403adc86..14e917e16 100644 --- a/xray/commands/audit/jas/secrets/secretsscanner_test.go +++ b/xray/commands/audit/jas/secrets/secretsscanner_test.go @@ -1,13 +1,13 @@ package secrets import ( - "github.com/jfrog/jfrog-cli-core/v2/xray/commands/audit/jas" "os" "path/filepath" "testing" + "github.com/jfrog/jfrog-cli-core/v2/xray/commands/audit/jas" + "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" - "github.com/jfrog/jfrog-cli-core/v2/xray/utils" "github.com/stretchr/testify/assert" ) @@ -65,11 +65,17 @@ func TestParseResults_EmptyResults(t *testing.T) { // Act var err error - secretScanManager.secretsScannerResults, err = jas.GetSourceCodeScanResults(secretScanManager.scanner.ResultsFileName, scanner.WorkingDirs[0], utils.Secrets) + secretScanManager.secretsScannerResults, err = jas.ReadJasScanRunsFromFile(secretScanManager.scanner.ResultsFileName, scanner.WorkingDirs[0]) // Assert - assert.NoError(t, err) - assert.Empty(t, secretScanManager.secretsScannerResults) + if assert.NoError(t, err) && assert.NotNil(t, secretScanManager.secretsScannerResults) { + assert.Len(t, secretScanManager.secretsScannerResults, 1) + assert.Empty(t, secretScanManager.secretsScannerResults[0].Results) + secretScanManager.secretsScannerResults = processSecretScanRuns(secretScanManager.secretsScannerResults) + assert.Len(t, secretScanManager.secretsScannerResults, 1) + assert.Empty(t, secretScanManager.secretsScannerResults[0].Results) + } + } func TestParseResults_ResultsContainSecrets(t *testing.T) { @@ -82,12 +88,18 @@ func TestParseResults_ResultsContainSecrets(t *testing.T) { // Act var err error - secretScanManager.secretsScannerResults, err = jas.GetSourceCodeScanResults(secretScanManager.scanner.ResultsFileName, scanner.WorkingDirs[0], utils.Secrets) + secretScanManager.secretsScannerResults, err = jas.ReadJasScanRunsFromFile(secretScanManager.scanner.ResultsFileName, scanner.WorkingDirs[0]) // Assert + if assert.NoError(t, err) && assert.NotNil(t, secretScanManager.secretsScannerResults) { + assert.Len(t, secretScanManager.secretsScannerResults, 1) + assert.NotEmpty(t, secretScanManager.secretsScannerResults[0].Results) + secretScanManager.secretsScannerResults = processSecretScanRuns(secretScanManager.secretsScannerResults) + assert.Len(t, secretScanManager.secretsScannerResults, 1) + assert.Len(t, secretScanManager.secretsScannerResults[0].Results, 7) + } assert.NoError(t, err) - assert.NotEmpty(t, secretScanManager.secretsScannerResults) - assert.Equal(t, 7, len(secretScanManager.secretsScannerResults)) + } func TestGetSecretsScanResults_AnalyzerManagerReturnsError(t *testing.T) { @@ -100,3 +112,20 @@ func TestGetSecretsScanResults_AnalyzerManagerReturnsError(t *testing.T) { assert.ErrorContains(t, err, "failed to run Secrets scan") assert.Nil(t, secretsResults) } + +func TestHideSecret(t *testing.T) { + tests := []struct { + secret string + expectedOutput string + }{ + {secret: "", expectedOutput: "***"}, + {secret: "12", expectedOutput: "***"}, + {secret: "123", expectedOutput: "***"}, + {secret: "123456789", expectedOutput: "123************"}, + {secret: "3478hfnkjhvd848446gghgfh", expectedOutput: "347************"}, + } + + for _, test := range tests { + assert.Equal(t, test.expectedOutput, maskSecret(test.secret)) + } +} diff --git a/xray/commands/audit/jasrunner.go b/xray/commands/audit/jasrunner.go index 9fefa7cb6..27f2c510b 100644 --- a/xray/commands/audit/jasrunner.go +++ b/xray/commands/audit/jasrunner.go @@ -54,6 +54,6 @@ func runJasScannersAndSetResults(scanResults *utils.ExtendedScanResults, directD if progress != nil { progress.SetHeadlineMsg("Running SAST scanning") } - scanResults.SastResults, err = sast.RunSastScan(scanner) + scanResults.SastScanResults, err = sast.RunSastScan(scanner) return } diff --git a/xray/commands/testdata/applicability-scan/applicable-cve-results.sarif b/xray/commands/testdata/applicability-scan/applicable-cve-results.sarif index 71b97e5d6..66aee38a5 100644 --- a/xray/commands/testdata/applicability-scan/applicable-cve-results.sarif +++ b/xray/commands/testdata/applicability-scan/applicable-cve-results.sarif @@ -6,7 +6,7 @@ "name": "JFrog Applicability Scanner", "rules": [ { - "id": "applic_CVE-2021-3807", + "id": "applic_testCve1", "fullDescription": { "text": "The scanner checks whether the vulnerable function `ansi-regex` is called.", "markdown": "The scanner checks whether the vulnerable function `ansi-regex` is called." @@ -17,7 +17,7 @@ } }, { - "id": "applic_CVE-2021-3918", + "id": "applic_testCve3", "fullDescription": { "text": "The scanner checks whether any of the following vulnerable functions are called:\n\n* `json-schema.validate` with external input to its 1st (`instance`) argument.\n* `json-schema.checkPropertyChange` with external input to its 2nd (`schema`) argument.", "markdown": "The scanner checks whether any of the following vulnerable functions are called:\n\n* `json-schema.validate` with external input to its 1st (`instance`) argument.\n* `json-schema.checkPropertyChange` with external input to its 2nd (`schema`) argument." diff --git a/xray/commands/testdata/applicability-scan/no-applicable-cves-results.sarif b/xray/commands/testdata/applicability-scan/no-applicable-cves-results.sarif index a0f9cf39e..4257bc869 100644 --- a/xray/commands/testdata/applicability-scan/no-applicable-cves-results.sarif +++ b/xray/commands/testdata/applicability-scan/no-applicable-cves-results.sarif @@ -6,7 +6,7 @@ "name": "JFrog Applicability Scanner", "rules": [ { - "id": "applic_CVE-2021-3807", + "id": "applic_testCve2", "fullDescription": { "text": "The scanner checks whether the vulnerable function `ansi-regex` is called.", "markdown": "The scanner checks whether the vulnerable function `ansi-regex` is called." @@ -17,7 +17,7 @@ } }, { - "id": "applic_CVE-2021-3918", + "id": "applic_testCve3", "fullDescription": { "text": "The scanner checks whether any of the following vulnerable functions are called:\n\n* `json-schema.validate` with external input to its 1st (`instance`) argument.\n* `json-schema.checkPropertyChange` with external input to its 2nd (`schema`) argument.", "markdown": "The scanner checks whether any of the following vulnerable functions are called:\n\n* `json-schema.validate` with external input to its 1st (`instance`) argument.\n* `json-schema.checkPropertyChange` with external input to its 2nd (`schema`) argument." @@ -26,6 +26,39 @@ "shortDescription": { "text": "Scanner for CVE-2021-3918" } + }, + { + "id": "applic_testCve4", + "fullDescription": { + "text": "The scanner checks whether the vulnerable function `ansi-regex` is called.", + "markdown": "The scanner checks whether the vulnerable function `ansi-regex` is called." + }, + "name": "CVE-2021-3807", + "shortDescription": { + "text": "Scanner for CVE-2021-3807" + } + }, + { + "id": "applic_testCve5", + "fullDescription": { + "text": "The scanner checks whether any of the following vulnerable functions are called:\n\n* `json-schema.validate` with external input to its 1st (`instance`) argument.\n* `json-schema.checkPropertyChange` with external input to its 2nd (`schema`) argument.", + "markdown": "The scanner checks whether any of the following vulnerable functions are called:\n\n* `json-schema.validate` with external input to its 1st (`instance`) argument.\n* `json-schema.checkPropertyChange` with external input to its 2nd (`schema`) argument." + }, + "name": "CVE-2021-3918", + "shortDescription": { + "text": "Scanner for CVE-2021-3918" + } + }, + { + "id": "applic_testCve1", + "fullDescription": { + "text": "The scanner checks whether the vulnerable function `ansi-regex` is called.", + "markdown": "The scanner checks whether the vulnerable function `ansi-regex` is called." + }, + "name": "CVE-2021-3807", + "shortDescription": { + "text": "Scanner for CVE-2021-3807" + } } ], "version": "APPLIC_SCANNERv0.2.3" diff --git a/xray/formats/conversion.go b/xray/formats/conversion.go index 00698c06e..47ba2213a 100644 --- a/xray/formats/conversion.go +++ b/xray/formats/conversion.go @@ -145,7 +145,7 @@ func ConvertToSecretsTableRow(rows []SourceCodeRow) (tableRows []secretsTableRow severity: rows[i].Severity, file: rows[i].File, lineColumn: rows[i].LineColumn, - text: rows[i].Text, + text: rows[i].Snippet, }) } return @@ -157,7 +157,7 @@ func ConvertToIacTableRow(rows []SourceCodeRow) (tableRows []iacTableRow) { severity: rows[i].Severity, file: rows[i].File, lineColumn: rows[i].LineColumn, - text: rows[i].Text, + text: rows[i].Snippet, }) } return @@ -169,7 +169,7 @@ func ConvertToSastTableRow(rows []SourceCodeRow) (tableRows []sastTableRow) { severity: rows[i].Severity, file: rows[i].File, lineColumn: rows[i].LineColumn, - text: rows[i].Text, + text: rows[i].Snippet, }) } return diff --git a/xray/formats/simplejsonapi.go b/xray/formats/simplejsonapi.go index 62e5278d0..5d94edc19 100644 --- a/xray/formats/simplejsonapi.go +++ b/xray/formats/simplejsonapi.go @@ -72,7 +72,7 @@ type SourceCodeRow struct { type SourceCodeLocationRow struct {File string `json:"file"` LineColumn string `json:"lineColumn"` - Text string `json:"text"` + Snippet string `json:"snippet"` } type ComponentRow struct { @@ -81,9 +81,21 @@ type ComponentRow struct { } type CveRow struct { - Id string `json:"id"` - CvssV2 string `json:"cvssV2"` - CvssV3 string `json:"cvssV3"` + Id string `json:"id"` + CvssV2 string `json:"cvssV2"` + CvssV3 string `json:"cvssV3"` + Applicability *Applicability `json:"applicability,omitempty"` +} + +type Applicability struct { + Status bool `json:"status"` + ScannerDescription string `json:"scannerDescription,omitempty"` + Evidence []Evidence `json:"evidence,omitempty"` +} + +type Evidence struct { + SourceCodeLocationRow + Reason string `json:"reason,omitempty"` } type SimpleJsonError struct { diff --git a/xray/utils/analyzermanager.go b/xray/utils/analyzermanager.go index 05dae8d4e..419262973 100644 --- a/xray/utils/analyzermanager.go +++ b/xray/utils/analyzermanager.go @@ -15,27 +15,7 @@ import ( "github.com/jfrog/jfrog-client-go/utils/io/fileutils" "github.com/jfrog/jfrog-client-go/utils/log" "github.com/jfrog/jfrog-client-go/xray/services" -) - -type SarifLevel string - -const ( - Error SarifLevel = "error" - Warning SarifLevel = "warning" - Info SarifLevel = "info" - Note SarifLevel = "note" - None SarifLevel = "none" - - SeverityDefaultValue = "Medium" -) - -var ( - // All other values (include default) mapped as 'Medium' severity - levelToSeverity = map[SarifLevel]string{ - Error: "High", - Note: "Low", - None: "Unknown", - } + "github.com/owenrumney/go-sarif/v2/sarif" ) const ( @@ -99,26 +79,15 @@ var exitCodeErrorsMap = map[int]string{ unsupportedOsExitCode: "got unsupported operating system error from analyzer manager", } -type SourceCodeLocation struct { - File string - LineColumn string - Text string -} - -type SourceCodeScanResult struct { - SourceCodeLocation - Severity string - Type string - CodeFlow []*[]SourceCodeLocation -} - type ExtendedScanResults struct { - XrayResults []services.ScanResponse - ScannedTechnologies []coreutils.Technology - ApplicabilityScanResults map[string]ApplicabilityStatus - SecretsScanResults []SourceCodeScanResult - IacScanResults []SourceCodeScanResult - SastResults []SourceCodeScanResult + XrayResults []services.ScanResponse + XrayVersion string + ScannedTechnologies []coreutils.Technology + + ApplicabilityScanResults []*sarif.Run + SecretsScanResults []*sarif.Run + IacScanResults []*sarif.Run + SastScanResults []*sarif.Run EntitledForJas bool } diff --git a/xray/utils/analyzermanager_test.go b/xray/utils/analyzermanager_test.go index 7daabaade..602d33686 100644 --- a/xray/utils/analyzermanager_test.go +++ b/xray/utils/analyzermanager_test.go @@ -25,12 +25,10 @@ func TestGetResultFileName(t *testing.T) { {PhysicalLocation: &sarif.PhysicalLocation{ArtifactLocation: &sarif.ArtifactLocation{URI: &fileNameValue}}}, }}, expectedOutput: fileNameValue}, - {result: &sarif.Result{}, - expectedOutput: ""}, } for _, test := range tests { - assert.Equal(t, test.expectedOutput, GetResultFileName(test.result)) + assert.Equal(t, test.expectedOutput, GetLocationFileName(test.result.Locations[0])) } } @@ -67,12 +65,10 @@ func TestGetResultLocationInFile(t *testing.T) { StartColumn: nil, }}}}}, expectedOutput: ""}, - {result: &sarif.Result{}, - expectedOutput: ""}, } for _, test := range tests { - assert.Equal(t, test.expectedOutput, GetResultLocationInFile(test.result)) + assert.Equal(t, test.expectedOutput, GetStartLocationInFile(test.result.Locations[0])) } } @@ -98,11 +94,11 @@ func TestExtractRelativePath(t *testing.T) { } func TestGetResultSeverity(t *testing.T) { - levelValueHigh := string(Error) - levelValueMedium := string(Warning) - levelValueMedium2 := string(Info) - levelValueLow := string(Note) - levelValueUnknown := string(None) + levelValueHigh := string(errorLevel) + levelValueMedium := string(warningLevel) + levelValueMedium2 := string(infoLevel) + levelValueLow := string(noteLevel) + levelValueUnknown := string(noneLevel) tests := []struct { result *sarif.Result diff --git a/xray/utils/resultstable.go b/xray/utils/resultstable.go index c4039f194..eb35e6b01 100644 --- a/xray/utils/resultstable.go +++ b/xray/utils/resultstable.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/jfrog/gofrog/datastructures" + "github.com/owenrumney/go-sarif/v2/sarif" "golang.org/x/exp/maps" "golang.org/x/text/cases" "golang.org/x/text/language" @@ -88,6 +89,9 @@ func prepareViolations(violations []services.Violation, extendedResults *Extende case "security": cves := convertCves(violation.Cves) applicableValue := getApplicableCveValue(extendedResults, cves) + for _, cve := range cves { + cve.Applicability = getCveApplicability(cve, extendedResults.ApplicabilityScanResults) + } currSeverity := GetSeverity(violation.Severity, applicableValue) jfrogResearchInfo := convertJfrogResearchInformation(violation.ExtendedInformation) for compIndex := 0; compIndex < len(impactedPackagesNames); compIndex++ { @@ -208,6 +212,9 @@ func prepareVulnerabilities(vulnerabilities []services.Vulnerability, extendedRe } cves := convertCves(vulnerability.Cves) applicableValue := getApplicableCveValue(extendedResults, cves) + for _, cve := range cves { + cve.Applicability = getCveApplicability(cve, extendedResults.ApplicabilityScanResults) + } currSeverity := GetSeverity(vulnerability.Severity, applicableValue) jfrogResearchInfo := convertJfrogResearchInformation(vulnerability.ExtendedInformation) for compIndex := 0; compIndex < len(impactedPackagesNames); compIndex++ { @@ -290,25 +297,29 @@ func PrepareLicenses(licenses []services.License) ([]formats.LicenseRow, error) } // Prepare secrets for all non-table formats (without style or emoji) -func PrepareSecrets(secrets []SourceCodeScanResult) []formats.SourceCodeRow { +func PrepareSecrets(secrets []*sarif.Run) []formats.SourceCodeRow { return prepareSecrets(secrets, false) } -func prepareSecrets(secrets []SourceCodeScanResult, isTable bool) []formats.SourceCodeRow { +func prepareSecrets(secrets []*sarif.Run, isTable bool) []formats.SourceCodeRow { var secretsRows []formats.SourceCodeRow - for _, secret := range secrets { - currSeverity := GetSeverity(secret.Severity, Applicable) - secretsRows = append(secretsRows, - formats.SourceCodeRow{ - SeverityDetails: formats.SeverityDetails{Severity: currSeverity.printableTitle(isTable), SeverityNumValue: currSeverity.NumValue()}, - SourceCodeLocationRow: formats.SourceCodeLocationRow{ - File: secret.File, - LineColumn: secret.LineColumn, - Text: secret.Text, - }, - Type: secret.Type, - }, - ) + for _, secretRun := range secrets { + for _, secret := range secretRun.Results { + currSeverity := GetSeverity(GetResultSeverity(secret), Applicable) + for _, location := range secret.Locations { + secretsRows = append(secretsRows, + formats.SourceCodeRow{ + SeverityDetails: formats.SeverityDetails{Severity: currSeverity.printableTitle(isTable), SeverityNumValue: currSeverity.NumValue()}, + SourceCodeLocationRow: formats.SourceCodeLocationRow{ + File: GetLocationFileName(location), + LineColumn: GetStartLocationInFile(location), + Snippet: GetLocationSnippet(location), + }, + Type: *secret.RuleID, + }, + ) + } + } } sort.Slice(secretsRows, func(i, j int) bool { @@ -318,7 +329,7 @@ func prepareSecrets(secrets []SourceCodeScanResult, isTable bool) []formats.Sour return secretsRows } -func PrintSecretsTable(secrets []SourceCodeScanResult, entitledForSecretsScan bool) error { +func PrintSecretsTable(secrets []*sarif.Run, entitledForSecretsScan bool) error { if entitledForSecretsScan { secretsRows := prepareSecrets(secrets, true) log.Output() @@ -329,25 +340,29 @@ func PrintSecretsTable(secrets []SourceCodeScanResult, entitledForSecretsScan bo } // Prepare iacs for all non-table formats (without style or emoji) -func PrepareIacs(iacs []SourceCodeScanResult) []formats.SourceCodeRow { +func PrepareIacs(iacs []*sarif.Run) []formats.SourceCodeRow { return prepareIacs(iacs, false) } -func prepareIacs(iacs []SourceCodeScanResult, isTable bool) []formats.SourceCodeRow { +func prepareIacs(iacs []*sarif.Run, isTable bool) []formats.SourceCodeRow { var iacRows []formats.SourceCodeRow - for _, iac := range iacs { - currSeverity := GetSeverity(iac.Severity, Applicable) - iacRows = append(iacRows, - formats.SourceCodeRow{ - SeverityDetails: formats.SeverityDetails{Severity: currSeverity.printableTitle(isTable), SeverityNumValue: currSeverity.NumValue()}, - SourceCodeLocationRow: formats.SourceCodeLocationRow{ - File: iac.File, - LineColumn: iac.LineColumn, - Text: iac.Text, - }, - Type: iac.Type, - }, - ) + for _, iacRun := range iacs { + for _, iac := range iacRun.Results { + currSeverity := GetSeverity(GetResultSeverity(iac), Applicable) + for _, location := range iac.Locations { + iacRows = append(iacRows, + formats.SourceCodeRow{ + SeverityDetails: formats.SeverityDetails{Severity: currSeverity.printableTitle(isTable), SeverityNumValue: currSeverity.NumValue()}, + SourceCodeLocationRow: formats.SourceCodeLocationRow{ + File: GetLocationFileName(location), + LineColumn: GetStartLocationInFile(location), + Snippet: GetResultMsgText(iac), + }, + Type: *iac.RuleID, + }, + ) + } + } } sort.Slice(iacRows, func(i, j int) bool { @@ -357,7 +372,7 @@ func prepareIacs(iacs []SourceCodeScanResult, isTable bool) []formats.SourceCode return iacRows } -func PrintIacTable(iacs []SourceCodeScanResult, entitledForIacScan bool) error { +func PrintIacTable(iacs []*sarif.Run, entitledForIacScan bool) error { if entitledForIacScan { iacRows := prepareIacs(iacs, true) log.Output() @@ -367,26 +382,32 @@ func PrintIacTable(iacs []SourceCodeScanResult, entitledForIacScan bool) error { return nil } -func PrepareSast(sasts []SourceCodeScanResult) []formats.SourceCodeRow { +func PrepareSast(sasts []*sarif.Run) []formats.SourceCodeRow { return prepareSast(sasts, false) } -func prepareSast(sasts []SourceCodeScanResult, isTable bool) []formats.SourceCodeRow { +func prepareSast(sasts []*sarif.Run, isTable bool) []formats.SourceCodeRow { var sastRows []formats.SourceCodeRow - for _, sast := range sasts { - currSeverity := GetSeverity(sast.Severity, Applicable) - sastRows = append(sastRows, - formats.SourceCodeRow{ - SeverityDetails: formats.SeverityDetails{Severity: currSeverity.printableTitle(isTable), SeverityNumValue: currSeverity.NumValue()}, - SourceCodeLocationRow: formats.SourceCodeLocationRow{ - File: sast.File, - LineColumn: sast.LineColumn, - Text: sast.Text, - }, - Type: sast.Type, - CodeFlow: toSourceCodeCodeFlowRow(sast, isTable), - }, - ) + for _, sastRun := range sasts { + for _, sast := range sastRun.Results { + currSeverity := GetSeverity(GetResultSeverity(sast), Applicable) + + flows := toSourceCodeCodeFlowRow(sast.CodeFlows, isTable) + for _, location := range sast.Locations { + sastRows = append(sastRows, + formats.SourceCodeRow{ + SeverityDetails: formats.SeverityDetails{Severity: currSeverity.printableTitle(isTable), SeverityNumValue: currSeverity.NumValue()}, + SourceCodeLocationRow: formats.SourceCodeLocationRow{ + File: GetLocationFileName(location), + LineColumn: GetStartLocationInFile(location), + Snippet: GetResultMsgText(sast), + }, + Type: *sast.RuleID, + CodeFlow: flows, + }, + ) + } + } } sort.Slice(sastRows, func(i, j int) bool { @@ -396,26 +417,28 @@ func prepareSast(sasts []SourceCodeScanResult, isTable bool) []formats.SourceCod return sastRows } -func toSourceCodeCodeFlowRow(result SourceCodeScanResult, isTable bool) (flows [][]formats.SourceCodeLocationRow) { +func toSourceCodeCodeFlowRow(flows []*sarif.CodeFlow, isTable bool) (flowRows [][]formats.SourceCodeLocationRow) { if isTable { // Not displaying in table return } - for _, flowStack := range result.CodeFlow { - rowFlow := []formats.SourceCodeLocationRow{} - for _, location := range *flowStack { - rowFlow = append(rowFlow, formats.SourceCodeLocationRow{ - File: location.File, - LineColumn: location.LineColumn, - Text: location.Text, - }) + for _, codeFlow := range flows { + for _, stackTrace := range codeFlow.ThreadFlows { + rowFlow := []formats.SourceCodeLocationRow{} + for _, stackTraceEntry := range stackTrace.Locations { + rowFlow = append(rowFlow, formats.SourceCodeLocationRow{ + File: GetLocationFileName(stackTraceEntry.Location), + LineColumn: GetStartLocationInFile(stackTraceEntry.Location), + Snippet: GetLocationSnippet(stackTraceEntry.Location), + }) + } + flowRows = append(flowRows, rowFlow) } - flows = append(flows, rowFlow) } return } -func PrintSastTable(sast []SourceCodeScanResult, entitledForSastScan bool) error { +func PrintSastTable(sast []*sarif.Run, entitledForSastScan bool) error { if entitledForSastScan { sastRows := prepareSast(sast, true) log.Output() @@ -425,14 +448,6 @@ func PrintSastTable(sast []SourceCodeScanResult, entitledForSastScan bool) error return nil } -func convertCves(cves []services.Cve) []formats.CveRow { - var cveRows []formats.CveRow - for _, cveObj := range cves { - cveRows = append(cveRows, formats.CveRow{Id: cveObj.Id, CvssV2: cveObj.CvssV2Score, CvssV3: cveObj.CvssV3Score}) - } - return cveRows -} - func convertJfrogResearchInformation(extendedInfo *services.ExtendedInformation) *formats.JfrogResearchInformation { if extendedInfo == nil { return nil @@ -875,6 +890,14 @@ func GetUniqueKey(vulnerableDependency, vulnerableVersion, xrayID string, fixVer return strings.Join([]string{vulnerableDependency, vulnerableVersion, xrayID, strconv.FormatBool(fixVersionExist)}, ":") } +func convertCves(cves []services.Cve) []formats.CveRow { + var cveRows []formats.CveRow + for _, cveObj := range cves { + cveRows = append(cveRows, formats.CveRow{Id: cveObj.Id, CvssV2: cveObj.CvssV2Score, CvssV3: cveObj.CvssV3Score}) + } + return cveRows +} + // If at least one cve is applicable - final value is applicable // Else if at least one cve is undetermined - final value is undetermined // Else (case when all cves aren't applicable) -> final value is not applicable @@ -882,19 +905,22 @@ func getApplicableCveValue(extendedResults *ExtendedScanResults, xrayCves []form if !extendedResults.EntitledForJas || len(extendedResults.ApplicabilityScanResults) == 0 { return NotScanned } - if len(xrayCves) == 0 { return ApplicabilityUndetermined } cveExistsInResult := false finalApplicableValue := NotApplicable - for _, cve := range xrayCves { - if currentCveApplicableValue, exists := extendedResults.ApplicabilityScanResults[cve.Id]; exists { - cveExistsInResult = true - if currentCveApplicableValue == Applicable { - return currentCveApplicableValue - } else if currentCveApplicableValue == ApplicabilityUndetermined { - finalApplicableValue = currentCveApplicableValue + for _, applicabilityRun := range extendedResults.ApplicabilityScanResults { + for _, cve := range xrayCves { + relatedResults := GetResultsByRuleId(applicabilityRun, GetRuleIdFromCveId(cve.Id)) + if len(relatedResults) == 0 { + finalApplicableValue = ApplicabilityUndetermined + } + for _, relatedResult := range relatedResults { + cveExistsInResult = true + if isApplicableResult(relatedResult) { + return Applicable + } } } } @@ -904,6 +930,39 @@ func getApplicableCveValue(extendedResults *ExtendedScanResults, xrayCves []form return ApplicabilityUndetermined } +func getCveApplicability(cve formats.CveRow, applicabilityScanResults []*sarif.Run) (applicability *formats.Applicability) { + if len(applicabilityScanResults) == 0 { + return nil + } + for _, applicabilityRun := range applicabilityScanResults { + description := "" + if relatedRule, _ := applicabilityRun.GetRuleById(GetRuleIdFromCveId(cve.Id)); relatedRule != nil { + description = GetRuleFullDescription(relatedRule) + } + relatedResult, _ := applicabilityRun.GetResultByRuleId(GetRuleIdFromCveId(cve.Id)) + if relatedResult == nil { + continue + } + // Set applicable details + applicability = &formats.Applicability{ + Status: isApplicableResult(relatedResult), + ScannerDescription: description, + } + // Add new evidences from locations + for _, location := range relatedResult.Locations { + applicability.Evidence = append(applicability.Evidence, formats.Evidence{ + SourceCodeLocationRow: formats.SourceCodeLocationRow{ + File: GetLocationFileName(location), + LineColumn: GetStartLocationInFile(location), + Snippet: GetLocationSnippet(location), + }, + Reason: GetResultMsgText(relatedResult), + }) + } + } + return +} + func printApplicableCveValue(applicableValue ApplicabilityStatus, isTable bool) string { if applicableValue == NotScanned { return NotScanned.String() diff --git a/xray/utils/resultstable_test.go b/xray/utils/resultstable_test.go index 36c3c33af..f608aa970 100644 --- a/xray/utils/resultstable_test.go +++ b/xray/utils/resultstable_test.go @@ -3,9 +3,11 @@ package utils import ( "errors" "fmt" - "github.com/jfrog/jfrog-cli-core/v2/xray/formats" "testing" + "github.com/jfrog/jfrog-cli-core/v2/xray/formats" + "github.com/owenrumney/go-sarif/v2/sarif" + "github.com/jfrog/jfrog-client-go/xray/services" "github.com/stretchr/testify/assert" ) @@ -426,8 +428,9 @@ func TestGetSeveritiesFormat(t *testing.T) { func TestGetApplicableCveValue(t *testing.T) { testCases := []struct { scanResults *ExtendedScanResults - cves []formats.CveRow + cves []services.Cve expectedResult ApplicabilityStatus + expectedCves []formats.CveRow }{ { scanResults: &ExtendedScanResults{EntitledForJas: false}, @@ -435,54 +438,100 @@ func TestGetApplicableCveValue(t *testing.T) { }, { scanResults: &ExtendedScanResults{ - ApplicabilityScanResults: map[string]ApplicabilityStatus{"testCve1": Applicable, "testCve2": NotApplicable}, - EntitledForJas: true, + ApplicabilityScanResults: []*sarif.Run{ + getRunWithDummyResults( + getDummyResultWithOneLocation("fileName1", 0, 1, "snippet1", "applic_testCve1", "info"), + getDummyPassingResult("applic_testCve2"), + ), + }, + EntitledForJas: true, }, cves: nil, expectedResult: ApplicabilityUndetermined, + expectedCves: nil, }, { scanResults: &ExtendedScanResults{ - ApplicabilityScanResults: map[string]ApplicabilityStatus{"testCve1": NotApplicable, "testCve2": Applicable}, - EntitledForJas: true, + ApplicabilityScanResults: []*sarif.Run{ + getRunWithDummyResults( + getDummyPassingResult("applic_testCve1"), + getDummyResultWithOneLocation("fileName2", 1, 0, "snippet2", "applic_testCve2", "warning"), + ), + }, + EntitledForJas: true, }, - cves: []formats.CveRow{{Id: "testCve2"}}, + cves: []services.Cve{{Id: "testCve2"}}, expectedResult: Applicable, + expectedCves: []formats.CveRow{{Id: "testCve2", Applicability: &formats.Applicability{Status: true}}}, }, { scanResults: &ExtendedScanResults{ - ApplicabilityScanResults: map[string]ApplicabilityStatus{"testCve1": NotApplicable, "testCve2": Applicable}, - EntitledForJas: true, + ApplicabilityScanResults: []*sarif.Run{ + getRunWithDummyResults( + getDummyPassingResult("applic_testCve1"), + getDummyResultWithOneLocation("fileName3", 0, 1, "snippet3", "applic_testCve2", "info"), + ), + }, + EntitledForJas: true, }, - cves: []formats.CveRow{{Id: "testCve3"}}, + cves: []services.Cve{{Id: "testCve3"}}, expectedResult: ApplicabilityUndetermined, + expectedCves: []formats.CveRow{{Id: "testCve3"}}, }, { scanResults: &ExtendedScanResults{ - ApplicabilityScanResults: map[string]ApplicabilityStatus{"testCve1": NotApplicable, "testCve2": NotApplicable}, - EntitledForJas: true}, - cves: []formats.CveRow{{Id: "testCve1"}, {Id: "testCve2"}}, + ApplicabilityScanResults: []*sarif.Run{ + getRunWithDummyResults( + getDummyPassingResult("applic_testCve1"), + getDummyPassingResult("applic_testCve2"), + ), + }, + EntitledForJas: true, + }, + cves: []services.Cve{{Id: "testCve1"}, {Id: "testCve2"}}, expectedResult: NotApplicable, + expectedCves: []formats.CveRow{{Id: "testCve1", Applicability: &formats.Applicability{Status: false}}, {Id: "testCve2", Applicability: &formats.Applicability{Status: false}}}, }, { scanResults: &ExtendedScanResults{ - ApplicabilityScanResults: map[string]ApplicabilityStatus{"testCve1": NotApplicable, "testCve2": Applicable}, - EntitledForJas: true, + ApplicabilityScanResults: []*sarif.Run{ + getRunWithDummyResults( + getDummyPassingResult("applic_testCve1"), + getDummyResultWithOneLocation("fileName4", 1, 0, "snippet", "applic_testCve2", "warning"), + ), + }, + EntitledForJas: true, }, - cves: []formats.CveRow{{Id: "testCve1"}, {Id: "testCve2"}}, + cves: []services.Cve{{Id: "testCve1"}, {Id: "testCve2"}}, expectedResult: Applicable, + expectedCves: []formats.CveRow{{Id: "testCve1", Applicability: &formats.Applicability{Status: false}}, {Id: "testCve2", Applicability: &formats.Applicability{Status: true}}}, }, { scanResults: &ExtendedScanResults{ - ApplicabilityScanResults: map[string]ApplicabilityStatus{"testCve1": NotApplicable, "testCve2": ApplicabilityUndetermined}, - EntitledForJas: true}, - cves: []formats.CveRow{{Id: "testCve1"}, {Id: "testCve2"}}, + ApplicabilityScanResults: []*sarif.Run{ + getRunWithDummyResults(getDummyPassingResult("applic_testCve1")), + }, + EntitledForJas: true}, + cves: []services.Cve{{Id: "testCve1"}, {Id: "testCve2"}}, expectedResult: ApplicabilityUndetermined, + expectedCves: []formats.CveRow{{Id: "testCve1", Applicability: &formats.Applicability{Status: false}}, {Id: "testCve2"}}, }, } for _, testCase := range testCases { - assert.Equal(t, testCase.expectedResult, getApplicableCveValue(testCase.scanResults, testCase.cves)) + cves := convertCves(testCase.cves) + applicableValue := getApplicableCveValue(testCase.scanResults, cves) + for i := range cves { + cves[i].Applicability = getCveApplicability(cves[i], testCase.scanResults.ApplicabilityScanResults) + } + assert.Equal(t, testCase.expectedResult, applicableValue) + if assert.True(t, len(testCase.expectedCves) == len(cves)) { + for i := range cves { + if testCase.expectedCves[i].Applicability != nil && assert.NotNil(t, cves[i].Applicability) { + assert.Equal(t, testCase.expectedCves[i].Applicability.Status, cves[i].Applicability.Status) + } + } + } } } diff --git a/xray/utils/resultwriter.go b/xray/utils/resultwriter.go index 998414af0..db86475a9 100644 --- a/xray/utils/resultwriter.go +++ b/xray/utils/resultwriter.go @@ -4,7 +4,6 @@ import ( "bytes" "encoding/json" "fmt" - "os" "strconv" "strings" @@ -28,27 +27,13 @@ const ( Sarif OutputFormat = "sarif" ) -const missingCveScore = "0" +const MissingCveScore = "0" const maxPossibleCve = 10.0 var OutputFormats = []string{string(Table), string(Json), string(SimpleJson), string(Sarif)} var CurationOutputFormats = []string{string(Table), string(Json)} -type sarifProperties struct { - Applicable string - Cves string - Headline string - Severity string - Description string - MarkdownDescription string - XrayID string - File string - LineColumn string - Type string - CodeFlows [][]formats.SourceCodeLocationRow -} - // PrintScanResults prints the scan results in the specified format. // Note that errors are printed only with SimpleJson format. // @@ -74,7 +59,7 @@ func PrintScanResults(results *ExtendedScanResults, simpleJsonError []formats.Si case Json: return PrintJson(results.getXrayScanResults()) case Sarif: - sarifFile, err := GenerateSarifFileFromScan(results, isMultipleRoots, false, "JFrog Security", coreutils.JFrogComUrl+"xray/") + sarifFile, err := GenerateSarifContentFromResults(results, isMultipleRoots, includeLicenses, false) if err != nil { return err } @@ -107,16 +92,48 @@ func printScanResultsTables(results *ExtendedScanResults, isBinaryScan, includeV return } } + ConvertRunsPathsToRelative(results.SecretsScanResults) if err = PrintSecretsTable(results.SecretsScanResults, results.EntitledForJas); err != nil { return } + ConvertRunsPathsToRelative(results.IacScanResults) if err = PrintIacTable(results.IacScanResults, results.EntitledForJas); err != nil { return } if !IsSastSupported() { return } - return PrintSastTable(results.SastResults, results.EntitledForJas) + ConvertRunsPathsToRelative(results.SastScanResults) + return PrintSastTable(results.SastScanResults, results.EntitledForJas) +} + +// The paths at Sarif runs are absolute. +// Use this method if you need to translate the file paths to relative +func ConvertRunsPathsToRelative(runs []*sarif.Run) { + for _, sarifRun := range runs { + for _, invocation := range sarifRun.Invocations { + if wd := GetInvocationWorkingDirectory(invocation); len(wd) > 0 { + ConvertRunPathsToRelative(sarifRun, wd) + } + } + } +} + +func ConvertRunPathsToRelative(sarifRun *sarif.Run, wd string) { + for _, sarifResult := range sarifRun.Results { + // Convert paths in locations + for _, location := range sarifResult.Locations { + SetLocationFileName(location, ExtractRelativePath(GetLocationFileName(location), wd)) + } + // Convert paths in code flows + for _, codeFlows := range sarifResult.CodeFlows { + for _, threadFlows := range codeFlows.ThreadFlows { + for _, location := range threadFlows.Locations { + SetLocationFileName(location.Location, ExtractRelativePath(GetLocationFileName(location.Location), wd)) + } + } + } + } } func printMessages(messages []string) { @@ -132,16 +149,22 @@ func printMessage(message string) { log.Output("💬" + message) } -func GenerateSarifFileFromScan(extendedResults *ExtendedScanResults, isMultipleRoots, markdownOutput bool, scanningTool, toolURI string) (string, error) { - report, err := sarif.New(sarif.Version210) +func GenerateSarifContentFromResults(extendedResults *ExtendedScanResults, isMultipleRoots, includeLicenses, markdownOutput bool) (sarifStr string, err error) { + report, err := NewReport() if err != nil { - return "", errorutils.CheckError(err) + return } - run := sarif.NewRunWithInformationURI(scanningTool, toolURI) - if err = convertScanToSarif(run, extendedResults, isMultipleRoots, markdownOutput); err != nil { - return "", err + xrayRun, err := convertXrayResponsesToSarifRun(extendedResults, isMultipleRoots, includeLicenses, markdownOutput) + if err != nil { + return } - report.AddRun(run) + + report.Runs = append(report.Runs, xrayRun) + report.Runs = append(report.Runs, extendedResults.ApplicabilityScanResults...) + report.Runs = append(report.Runs, extendedResults.IacScanResults...) + report.Runs = append(report.Runs, extendedResults.SecretsScanResults...) + report.Runs = append(report.Runs, extendedResults.SastScanResults...) + out, err := json.Marshal(report) if err != nil { return "", errorutils.CheckError(err) @@ -150,7 +173,109 @@ func GenerateSarifFileFromScan(extendedResults *ExtendedScanResults, isMultipleR return clientUtils.IndentJson(out), nil } -func convertScanToSimpleJson(extendedResults *ExtendedScanResults, errors []formats.SimpleJsonError, isMultipleRoots, includeLicenses, simplifiedOutput bool) (formats.SimpleJsonResults, error) { +func convertXrayResponsesToSarifRun(extendedResults *ExtendedScanResults, isMultipleRoots, includeLicenses, markdownOutput bool) (run *sarif.Run, err error) { + xrayJson, err := convertXrayScanToSimpleJson(extendedResults, isMultipleRoots, includeLicenses, true) + if err != nil { + return + } + xrayRun := sarif.NewRunWithInformationURI("JFrog Xray Sca", "https://jfrog.com/xray/") + xrayRun.Tool.Driver.Version = &extendedResults.XrayVersion + if len(xrayJson.Vulnerabilities) > 0 || len(xrayJson.SecurityViolations) > 0 { + if err = extractXrayIssuesToSarifRun(xrayRun, xrayJson, markdownOutput); err != nil { + return + } + } + run = xrayRun + return +} + +func extractXrayIssuesToSarifRun(run *sarif.Run, xrayJson formats.SimpleJsonResults, markdownOutput bool) error { + for _, vulnerability := range xrayJson.Vulnerabilities { + if err := addXrayCveIssueToSarifRun( + vulnerability.Cves, + vulnerability.IssueId, + vulnerability.Severity, + vulnerability.Technology.GetPackageDescriptor(), + vulnerability.Components, + vulnerability.Applicable, + vulnerability.ImpactedDependencyName, + vulnerability.ImpactedDependencyVersion, + vulnerability.Summary, + vulnerability.FixedVersions, + markdownOutput, + run, + ); err != nil { + return err + } + } + for _, violation := range xrayJson.SecurityViolations { + if err := addXrayCveIssueToSarifRun( + violation.Cves, + violation.IssueId, + violation.Severity, + violation.Technology.GetPackageDescriptor(), + violation.Components, + violation.Applicable, + violation.ImpactedDependencyName, + violation.ImpactedDependencyVersion, + violation.Summary, + violation.FixedVersions, + markdownOutput, + run, + ); err != nil { + return err + } + } + for _, license := range xrayJson.LicensesViolations { + msg := getVulnerabilityOrViolationSarifHeadline(license.LicenseKey, license.ImpactedDependencyName, license.ImpactedDependencyVersion) + if rule, isNewRule := addResultToSarifRun(license.LicenseKey, msg, license.Severity, nil, run); isNewRule { + rule.WithDescription("License watch violations") + } + } + return nil +} + +func addXrayCveIssueToSarifRun(cves []formats.CveRow, issueId, severity, file string, components []formats.ComponentRow, applicable, impactedDependencyName, impactedDependencyVersion, summary string, fixedVersions []string, markdownOutput bool, run *sarif.Run) error { + maxCveScore, err := findMaxCVEScore(cves) + if err != nil { + return err + } + cveId := getCves(cves, issueId) + msg := getVulnerabilityOrViolationSarifHeadline(impactedDependencyName, impactedDependencyVersion, cveId) + location := sarif.NewLocation().WithPhysicalLocation(sarif.NewPhysicalLocation().WithArtifactLocation(sarif.NewArtifactLocation().WithUri(file))) + + if rule, isNewRule := addResultToSarifRun(cveId, msg, severity, location, run); isNewRule { + cveRuleProperties := sarif.NewPropertyBag() + if maxCveScore != MissingCveScore { + cveRuleProperties.Add("security-severity", maxCveScore) + } + rule.WithProperties(cveRuleProperties.Properties) + if markdownOutput { + formattedDirectDependencies, err := getDirectDependenciesFormatted(components) + if err != nil { + return err + } + markdownDescription := getSarifTableDescription(formattedDirectDependencies, maxCveScore, applicable, fixedVersions) + "\n" + rule.WithMarkdownHelp(markdownDescription) + } else { + rule.WithDescription(summary) + } + } + return nil +} + +func addResultToSarifRun(issueId, msg, severity string, location *sarif.Location, run *sarif.Run) (rule *sarif.ReportingDescriptor, isNewRule bool) { + if rule, _ = run.GetRuleById(issueId); rule == nil { + isNewRule = true + rule = run.AddRule(issueId) + } + if result := run.CreateResultForRule(issueId).WithMessage(sarif.NewTextMessage(msg)).WithLevel(ConvertToSarifLevel(severity)); location != nil { + result.AddLocation(location) + } + return +} + +func convertXrayScanToSimpleJson(extendedResults *ExtendedScanResults, isMultipleRoots, includeLicenses, simplifiedOutput bool) (formats.SimpleJsonResults, error) { violations, vulnerabilities, licenses := SplitScanResults(extendedResults.XrayResults) jsonTable := formats.SimpleJsonResults{} if len(vulnerabilities) > 0 { @@ -169,18 +294,6 @@ func convertScanToSimpleJson(extendedResults *ExtendedScanResults, errors []form jsonTable.LicensesViolations = licViolationsJsonTable jsonTable.OperationalRiskViolations = opRiskViolationsJsonTable } - if len(extendedResults.SecretsScanResults) > 0 { - secretsRows := PrepareSecrets(extendedResults.SecretsScanResults) - jsonTable.Secrets = secretsRows - } - if len(extendedResults.IacScanResults) > 0 { - iacRows := PrepareIacs(extendedResults.IacScanResults) - jsonTable.Iacs = iacRows - } - if len(extendedResults.SastResults) > 0 { - sastRows := PrepareSast(extendedResults.SastResults) - jsonTable.Sast = sastRows - } if includeLicenses { licJsonTable, err := PrepareLicenses(licenses) if err != nil { @@ -188,99 +301,27 @@ func convertScanToSimpleJson(extendedResults *ExtendedScanResults, errors []form } jsonTable.Licenses = licJsonTable } - jsonTable.Errors = errors return jsonTable, nil } -func convertScanToSarif(run *sarif.Run, extendedResults *ExtendedScanResults, isMultipleRoots, markdownOutput bool) error { - var errors []formats.SimpleJsonError - jsonTable, err := convertScanToSimpleJson(extendedResults, errors, isMultipleRoots, true, markdownOutput) +func convertScanToSimpleJson(extendedResults *ExtendedScanResults, errors []formats.SimpleJsonError, isMultipleRoots, includeLicenses, simplifiedOutput bool) (formats.SimpleJsonResults, error) { + jsonTable, err := convertXrayScanToSimpleJson(extendedResults, isMultipleRoots, includeLicenses, simplifiedOutput) if err != nil { - return err + return formats.SimpleJsonResults{}, err } - if len(jsonTable.Vulnerabilities) > 0 || len(jsonTable.SecurityViolations) > 0 { - if err = convertToVulnerabilityOrViolationSarif(run, &jsonTable, markdownOutput); err != nil { - return err - } - } - return convertToSourceCodeResultSarif(run, &jsonTable, markdownOutput) -} - -func convertToVulnerabilityOrViolationSarif(run *sarif.Run, jsonTable *formats.SimpleJsonResults, markdownOutput bool) error { - if len(jsonTable.SecurityViolations) > 0 { - return convertViolationsToSarif(jsonTable, run, markdownOutput) - } - return convertVulnerabilitiesToSarif(jsonTable, run, markdownOutput) -} - -func convertToSourceCodeResultSarif(run *sarif.Run, jsonTable *formats.SimpleJsonResults, markdownOutput bool) (err error) { - for _, secret := range jsonTable.Secrets { - properties := getSourceCodeProperties(secret, markdownOutput, Secrets) - if err = addPropertiesToSarifRun(run, &properties); err != nil { - return - } + if len(extendedResults.SecretsScanResults) > 0 { + jsonTable.Secrets = PrepareSecrets(extendedResults.SecretsScanResults) } - - for _, iac := range jsonTable.Iacs { - properties := getSourceCodeProperties(iac, markdownOutput, IaC) - if err = addPropertiesToSarifRun(run, &properties); err != nil { - return - } + if len(extendedResults.IacScanResults) > 0 { + jsonTable.Iacs = PrepareIacs(extendedResults.IacScanResults) } - - for _, sast := range jsonTable.Sast { - properties := getSourceCodeProperties(sast, markdownOutput, Sast) - if err = addPropertiesToSarifRun(run, &properties); err != nil { - return - } + if len(extendedResults.SastScanResults) > 0 { + jsonTable.Sast = PrepareSast(extendedResults.SastScanResults) } - return -} + jsonTable.Errors = errors -func getSourceCodeProperties(sourceCodeIssue formats.SourceCodeRow, markdownOutput bool, scanType JasScanType) sarifProperties { - file := strings.TrimPrefix(sourceCodeIssue.File, string(os.PathSeparator)) - mapSeverityToScore := map[string]string{ - "": "0.0", - "unknown": "0.0", - "low": "3.9", - "medium": "6.9", - "high": "8.9", - "critical": "10", - } - severity := mapSeverityToScore[strings.ToLower(sourceCodeIssue.Severity)] - - headline := "" - secretOrFinding := "" - switch scanType { - case IaC: - headline = "Infrastructure as Code Vulnerability" - secretOrFinding = "Finding" - case Sast: - headline = sourceCodeIssue.Text - secretOrFinding = "Finding" - case Secrets: - headline = "Potential Secret Exposed" - secretOrFinding = "Secret" - } - - markdownDescription := "" - if markdownOutput { - headerRow := fmt.Sprintf("| Severity | File | Line:Column | %s |\n", secretOrFinding) - separatorRow := "| :---: | :---: | :---: | :---: |\n" - tableHeader := headerRow + separatorRow - markdownDescription = tableHeader + fmt.Sprintf("| %s | %s | %s | %s |", sourceCodeIssue.Severity, file, sourceCodeIssue.LineColumn, sourceCodeIssue.Text) - } - return sarifProperties{ - Headline: headline, - Severity: severity, - Description: sourceCodeIssue.Text, - MarkdownDescription: markdownDescription, - File: file, - LineColumn: sourceCodeIssue.LineColumn, - Type: sourceCodeIssue.Type, - CodeFlows: sourceCodeIssue.CodeFlow, - } + return jsonTable, nil } func getCves(cvesRow []formats.CveRow, issueId string) string { @@ -303,68 +344,6 @@ func getVulnerabilityOrViolationSarifHeadline(depName, version, key string) stri return fmt.Sprintf("[%s] %s %s", key, depName, version) } -func convertViolationsToSarif(jsonTable *formats.SimpleJsonResults, run *sarif.Run, markdownOutput bool) error { - for _, violation := range jsonTable.SecurityViolations { - properties, err := getViolatedDepsSarifProps(violation, markdownOutput) - if err != nil { - return err - } - if err = addPropertiesToSarifRun(run, &properties); err != nil { - return err - } - } - for _, license := range jsonTable.LicensesViolations { - if err := addPropertiesToSarifRun(run, - &sarifProperties{ - Severity: license.Severity, - Headline: getVulnerabilityOrViolationSarifHeadline(license.LicenseKey, license.ImpactedDependencyName, license.ImpactedDependencyVersion)}); err != nil { - return err - } - } - - return nil -} - -func getViolatedDepsSarifProps(vulnerabilityRow formats.VulnerabilityOrViolationRow, markdownOutput bool) (sarifProperties, error) { - cves := getCves(vulnerabilityRow.Cves, vulnerabilityRow.IssueId) - headline := getVulnerabilityOrViolationSarifHeadline(vulnerabilityRow.ImpactedDependencyName, vulnerabilityRow.ImpactedDependencyVersion, cves) - maxCveScore, err := findMaxCVEScore(vulnerabilityRow.Cves) - if err != nil { - return sarifProperties{}, err - } - formattedDirectDependencies, err := getDirectDependenciesFormatted(vulnerabilityRow.Components) - if err != nil { - return sarifProperties{}, err - } - markdownDescription := "" - if markdownOutput { - markdownDescription = getSarifTableDescription(formattedDirectDependencies, maxCveScore, vulnerabilityRow.Applicable, vulnerabilityRow.FixedVersions) + "\n" - } - return sarifProperties{ - Applicable: vulnerabilityRow.Applicable, - Cves: cves, - Headline: headline, - Severity: maxCveScore, - Description: vulnerabilityRow.Summary, - MarkdownDescription: markdownDescription, - File: vulnerabilityRow.Technology.GetPackageDescriptor(), - }, err -} - -func convertVulnerabilitiesToSarif(jsonTable *formats.SimpleJsonResults, run *sarif.Run, simplifiedOutput bool) error { - for _, vulnerability := range jsonTable.Vulnerabilities { - properties, err := getViolatedDepsSarifProps(vulnerability, simplifiedOutput) - if err != nil { - return err - } - if err = addPropertiesToSarifRun(run, &properties); err != nil { - return err - } - } - - return nil -} - func getDirectDependenciesFormatted(directDependencies []formats.ComponentRow) (string, error) { var formattedDirectDependencies strings.Builder for _, dependency := range directDependencies { @@ -388,92 +367,6 @@ func getSarifTableDescription(formattedDirectDependencies, maxCveScore, applicab maxCveScore, applicable, formattedDirectDependencies, descriptionFixVersions) } -// Adding the Xray scan results details to the sarif struct, for each issue found in the scan -func addPropertiesToSarifRun(run *sarif.Run, properties *sarifProperties) error { - pb := sarif.NewPropertyBag() - if properties.Severity != missingCveScore { - pb.Add("security-severity", properties.Severity) - } - description := properties.Description - markdownDescription := properties.MarkdownDescription - if markdownDescription != "" { - description = "" - } - location, err := getSarifLocation(properties.File, properties.LineColumn) - if err != nil { - return err - } - codeFlows, err := getCodeFlowProperties(properties) - if err != nil { - return err - } - ruleID := generateSarifRuleID(properties) - run.AddRule(ruleID). - WithDescription(description). - WithProperties(pb.Properties). - WithMarkdownHelp(markdownDescription) - run.CreateResultForRule(ruleID). - WithCodeFlows(codeFlows). - WithMessage(sarif.NewTextMessage(properties.Headline)). - AddLocation(location) - return nil -} - -func getSarifLocation(file, lineCol string) (location *sarif.Location, err error) { - line := 0 - column := 0 - if lineCol != "" { - lineColumn := strings.Split(lineCol, ":") - if line, err = strconv.Atoi(lineColumn[0]); err != nil { - return - } - if column, err = strconv.Atoi(lineColumn[1]); err != nil { - return - } - } - location = sarif.NewLocationWithPhysicalLocation( - sarif.NewPhysicalLocation(). - WithArtifactLocation( - sarif.NewSimpleArtifactLocation(file), - ).WithRegion( - sarif.NewSimpleRegion(line, line). - WithStartColumn(column)), - ) - return -} - -func getCodeFlowProperties(properties *sarifProperties) (flows []*sarif.CodeFlow, err error) { - for _, codeFlow := range properties.CodeFlows { - if len(codeFlow) == 0 { - continue - } - converted := sarif.NewCodeFlow() - locations := []*sarif.ThreadFlowLocation{} - for _, location := range codeFlow { - var convertedLocation *sarif.Location - if convertedLocation, err = getSarifLocation(location.File, location.LineColumn); err != nil { - return - } - locations = append(locations, sarif.NewThreadFlowLocation().WithLocation(convertedLocation)) - } - - converted.AddThreadFlow(sarif.NewThreadFlow().WithLocations(locations)) - flows = append(flows, converted) - } - return -} - -func generateSarifRuleID(properties *sarifProperties) string { - switch { - case properties.Cves != "": - return properties.Cves - case properties.XrayID != "": - return properties.XrayID - default: - return properties.File - } -} - func findMaxCVEScore(cves []formats.CveRow) (string, error) { maxCve := 0.0 for _, cve := range cves { diff --git a/xray/utils/resultwriter_test.go b/xray/utils/resultwriter_test.go index e50c5ae56..6f13d6a03 100644 --- a/xray/utils/resultwriter_test.go +++ b/xray/utils/resultwriter_test.go @@ -1,92 +1,12 @@ package utils import ( - "fmt" - "path" "testing" - "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" "github.com/jfrog/jfrog-cli-core/v2/xray/formats" - "github.com/jfrog/jfrog-client-go/xray/services" "github.com/stretchr/testify/assert" ) -func TestGenerateSarifFileFromScan(t *testing.T) { - extendedResults := &ExtendedScanResults{ - XrayResults: []services.ScanResponse{ - { - Vulnerabilities: []services.Vulnerability{ - { - Cves: []services.Cve{{Id: "CVE-2022-1234", CvssV3Score: "8.0"}, {Id: "CVE-2023-1234", CvssV3Score: "7.1"}}, - Summary: "A test vulnerability the harms nothing", - Severity: "High", - Components: map[string]services.Component{ - "vulnerability1": {FixedVersions: []string{"1.2.3"}}, - }, - Technology: coreutils.Go.String(), - }, - }, - }, - }, - SecretsScanResults: []SourceCodeScanResult{ - { - Severity: "Medium", - SourceCodeLocation: SourceCodeLocation{ - File: "found_secrets.js", - LineColumn: "1:18", - Text: "AAA************", - }, - Type: "entropy", - }, - }, - IacScanResults: []SourceCodeScanResult{ - { - Severity: "Medium", - SourceCodeLocation: SourceCodeLocation{ - File: "plan/nonapplicable/req_sw_terraform_azure_compute_no_pass_auth.json", - LineColumn: "229:38", - Text: "BBB************", - }, - Type: "entropy", - }, - }, - } - testCases := []struct { - name string - extendedResults *ExtendedScanResults - isMultipleRoots bool - markdownOutput bool - expectedSarifOutput string - }{ - { - name: "Scan results with vulnerabilities, secrets and IaC", - extendedResults: extendedResults, - expectedSarifOutput: "{\n \"version\": \"2.1.0\",\n \"$schema\": \"https://json.schemastore.org/sarif-2.1.0.json\",\n \"runs\": [\n {\n \"tool\": {\n \"driver\": {\n \"informationUri\": \"https://example.com/\",\n \"name\": \"JFrog Security\",\n \"rules\": [\n {\n \"id\": \"CVE-2022-1234, CVE-2023-1234\",\n \"shortDescription\": {\n \"text\": \"A test vulnerability the harms nothing\"\n },\n \"help\": {\n \"markdown\": \"\"\n },\n \"properties\": {\n \"security-severity\": \"8.0\"\n }\n },\n {\n \"id\": \"found_secrets.js\",\n \"shortDescription\": {\n \"text\": \"AAA************\"\n },\n \"help\": {\n \"markdown\": \"\"\n },\n \"properties\": {\n \"security-severity\": \"6.9\"\n }\n },\n {\n \"id\": \"plan/nonapplicable/req_sw_terraform_azure_compute_no_pass_auth.json\",\n \"shortDescription\": {\n \"text\": \"BBB************\"\n },\n \"help\": {\n \"markdown\": \"\"\n },\n \"properties\": {\n \"security-severity\": \"6.9\"\n }\n }\n ]\n }\n },\n \"results\": [\n {\n \"ruleId\": \"CVE-2022-1234, CVE-2023-1234\",\n \"ruleIndex\": 0,\n \"message\": {\n \"text\": \"[CVE-2022-1234, CVE-2023-1234] vulnerability1 \"\n },\n \"locations\": [\n {\n \"physicalLocation\": {\n \"artifactLocation\": {\n \"uri\": \"go.mod\"\n },\n \"region\": {\n \"startLine\": 0,\n \"startColumn\": 0,\n \"endLine\": 0\n }\n }\n }\n ]\n },\n {\n \"ruleId\": \"found_secrets.js\",\n \"ruleIndex\": 1,\n \"message\": {\n \"text\": \"Potential Secret Exposed\"\n },\n \"locations\": [\n {\n \"physicalLocation\": {\n \"artifactLocation\": {\n \"uri\": \"found_secrets.js\"\n },\n \"region\": {\n \"startLine\": 1,\n \"startColumn\": 18,\n \"endLine\": 1\n }\n }\n }\n ]\n },\n {\n \"ruleId\": \"plan/nonapplicable/req_sw_terraform_azure_compute_no_pass_auth.json\",\n \"ruleIndex\": 2,\n \"message\": {\n \"text\": \"Infrastructure as Code Vulnerability\"\n },\n \"locations\": [\n {\n \"physicalLocation\": {\n \"artifactLocation\": {\n \"uri\": \"plan/nonapplicable/req_sw_terraform_azure_compute_no_pass_auth.json\"\n },\n \"region\": {\n \"startLine\": 229,\n \"startColumn\": 38,\n \"endLine\": 229\n }\n }\n }\n ]\n }\n ]\n }\n ]\n}", - }, - { - name: "Scan results with vulnerabilities, secrets and IaC as Markdown", - extendedResults: extendedResults, - markdownOutput: true, - expectedSarifOutput: "{\n \"version\": \"2.1.0\",\n \"$schema\": \"https://json.schemastore.org/sarif-2.1.0.json\",\n \"runs\": [\n {\n \"tool\": {\n \"driver\": {\n \"informationUri\": \"https://example.com/\",\n \"name\": \"JFrog Security\",\n \"rules\": [\n {\n \"id\": \"CVE-2022-1234, CVE-2023-1234\",\n \"shortDescription\": {\n \"text\": \"\"\n },\n \"help\": {\n \"markdown\": \"| Severity Score | Direct Dependencies | Fixed Versions |\\n| :---: | :----: | :---: |\\n| 8.0 | | 1.2.3 |\\n\"\n },\n \"properties\": {\n \"security-severity\": \"8.0\"\n }\n },\n {\n \"id\": \"found_secrets.js\",\n \"shortDescription\": {\n \"text\": \"\"\n },\n \"help\": {\n \"markdown\": \"| Severity | File | Line:Column | Secret |\\n| :---: | :---: | :---: | :---: |\\n| Medium | found_secrets.js | 1:18 | AAA************ |\"\n },\n \"properties\": {\n \"security-severity\": \"6.9\"\n }\n },\n {\n \"id\": \"plan/nonapplicable/req_sw_terraform_azure_compute_no_pass_auth.json\",\n \"shortDescription\": {\n \"text\": \"\"\n },\n \"help\": {\n \"markdown\": \"| Severity | File | Line:Column | Finding |\\n| :---: | :---: | :---: | :---: |\\n| Medium | plan/nonapplicable/req_sw_terraform_azure_compute_no_pass_auth.json | 229:38 | BBB************ |\"\n },\n \"properties\": {\n \"security-severity\": \"6.9\"\n }\n }\n ]\n }\n },\n \"results\": [\n {\n \"ruleId\": \"CVE-2022-1234, CVE-2023-1234\",\n \"ruleIndex\": 0,\n \"message\": {\n \"text\": \"[CVE-2022-1234, CVE-2023-1234] vulnerability1 \"\n },\n \"locations\": [\n {\n \"physicalLocation\": {\n \"artifactLocation\": {\n \"uri\": \"go.mod\"\n },\n \"region\": {\n \"startLine\": 0,\n \"startColumn\": 0,\n \"endLine\": 0\n }\n }\n }\n ]\n },\n {\n \"ruleId\": \"found_secrets.js\",\n \"ruleIndex\": 1,\n \"message\": {\n \"text\": \"Potential Secret Exposed\"\n },\n \"locations\": [\n {\n \"physicalLocation\": {\n \"artifactLocation\": {\n \"uri\": \"found_secrets.js\"\n },\n \"region\": {\n \"startLine\": 1,\n \"startColumn\": 18,\n \"endLine\": 1\n }\n }\n }\n ]\n },\n {\n \"ruleId\": \"plan/nonapplicable/req_sw_terraform_azure_compute_no_pass_auth.json\",\n \"ruleIndex\": 2,\n \"message\": {\n \"text\": \"Infrastructure as Code Vulnerability\"\n },\n \"locations\": [\n {\n \"physicalLocation\": {\n \"artifactLocation\": {\n \"uri\": \"plan/nonapplicable/req_sw_terraform_azure_compute_no_pass_auth.json\"\n },\n \"region\": {\n \"startLine\": 229,\n \"startColumn\": 38,\n \"endLine\": 229\n }\n }\n }\n ]\n }\n ]\n }\n ]\n}", - }, - { - name: "Scan results without vulnerabilities", - extendedResults: &ExtendedScanResults{}, - isMultipleRoots: true, - markdownOutput: true, - expectedSarifOutput: "{\n \"version\": \"2.1.0\",\n \"$schema\": \"https://json.schemastore.org/sarif-2.1.0.json\",\n \"runs\": [\n {\n \"tool\": {\n \"driver\": {\n \"informationUri\": \"https://example.com/\",\n \"name\": \"JFrog Security\",\n \"rules\": []\n }\n },\n \"results\": []\n }\n ]\n}", - }, - } - - for _, testCase := range testCases { - t.Run(testCase.name, func(t *testing.T) { - sarifOutput, err := GenerateSarifFileFromScan(testCase.extendedResults, testCase.isMultipleRoots, testCase.markdownOutput, "JFrog Security", "https://example.com/") - assert.NoError(t, err) - assert.Equal(t, testCase.expectedSarifOutput, sarifOutput) - }) - } -} - func TestGetVulnerabilityOrViolationSarifHeadline(t *testing.T) { assert.Equal(t, "[CVE-2022-1234] loadsh 1.4.1", getVulnerabilityOrViolationSarifHeadline("loadsh", "1.4.1", "CVE-2022-1234")) assert.NotEqual(t, "[CVE-2022-1234] loadsh 1.4.1", getVulnerabilityOrViolationSarifHeadline("loadsh", "1.2.1", "CVE-2022-1234")) @@ -101,172 +21,6 @@ func TestGetCves(t *testing.T) { assert.Equal(t, issueId, getCves(nil, issueId)) } -func TestGetIacOrSecretsProperties(t *testing.T) { - testCases := []struct { - name string - row formats.SourceCodeRow - markdownOutput bool - isSecret JasScanType - expectedOutput sarifProperties - }{ - { - name: "Infrastructure as Code vulnerability without markdown output", - row: formats.SourceCodeRow{ - SeverityDetails: formats.SeverityDetails{Severity: "high"}, - SourceCodeLocationRow: formats.SourceCodeLocationRow{ - File: path.Join("path", "to", "file"), - LineColumn: "10:5", - Text: "Vulnerable code"}, - Type: "Terraform", - }, - markdownOutput: false, - isSecret: IaC, - expectedOutput: sarifProperties{ - Applicable: "", - Cves: "", - Headline: "Infrastructure as Code Vulnerability", - Severity: "8.9", - Description: "Vulnerable code", - MarkdownDescription: "", - XrayID: "", - File: path.Join("path", "to", "file"), - LineColumn: "10:5", - Type: "Terraform", - }, - }, - { - name: "Potential secret exposed with markdown output", - row: formats.SourceCodeRow{ - SeverityDetails: formats.SeverityDetails{Severity: "medium"}, - SourceCodeLocationRow: formats.SourceCodeLocationRow{ - File: path.Join("path", "to", "file"), - LineColumn: "5:3", - Text: "Potential secret"}, - Type: "AWS Secret Manager", - }, - markdownOutput: true, - isSecret: Secrets, - expectedOutput: sarifProperties{ - Applicable: "", - Cves: "", - Headline: "Potential Secret Exposed", - Severity: "6.9", - Description: "Potential secret", - MarkdownDescription: fmt.Sprintf("| Severity | File | Line:Column | Secret |\n| :---: | :---: | :---: | :---: |\n| medium | %s | 5:3 | Potential secret |", path.Join("path", "to", "file")), - XrayID: "", - File: path.Join("path", "to", "file"), - LineColumn: "5:3", - Type: "AWS Secret Manager", - }, - }, - } - - for _, testCase := range testCases { - t.Run(testCase.name, func(t *testing.T) { - output := getSourceCodeProperties(testCase.row, testCase.markdownOutput, testCase.isSecret) - assert.Equal(t, testCase.expectedOutput.Applicable, output.Applicable) - assert.Equal(t, testCase.expectedOutput.Cves, output.Cves) - assert.Equal(t, testCase.expectedOutput.Headline, output.Headline) - assert.Equal(t, testCase.expectedOutput.Severity, output.Severity) - assert.Equal(t, testCase.expectedOutput.Description, output.Description) - assert.Equal(t, testCase.expectedOutput.MarkdownDescription, output.MarkdownDescription) - assert.Equal(t, testCase.expectedOutput.XrayID, output.XrayID) - assert.Equal(t, testCase.expectedOutput.File, output.File) - assert.Equal(t, testCase.expectedOutput.LineColumn, output.LineColumn) - assert.Equal(t, testCase.expectedOutput.Type, output.Type) - }) - } -} - -func TestGetViolatedDepsSarifProps(t *testing.T) { - testCases := []struct { - name string - vulnerability formats.VulnerabilityOrViolationRow - markdownOutput bool - expectedOutput sarifProperties - }{ - { - name: "Vulnerability with markdown output", - vulnerability: formats.VulnerabilityOrViolationRow{ - Summary: "Vulnerable dependency", - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - SeverityDetails: formats.SeverityDetails{ - Severity: "high", - }, - ImpactedDependencyName: "example-package", - ImpactedDependencyVersion: "1.0.0", - ImpactedDependencyType: "npm", - Components: []formats.ComponentRow{ - {Name: "example-package", Version: "1.0.0"}, - }, - }, - Applicable: Applicable.String(), - FixedVersions: []string{"1.0.1", "1.0.2"}, - Cves: []formats.CveRow{ - {Id: "CVE-2021-1234", CvssV3: "7.2"}, - {Id: "CVE-2021-5678", CvssV3: "7.2"}, - }, - IssueId: "XRAY-12345", - }, - markdownOutput: true, - expectedOutput: sarifProperties{ - Applicable: "Applicable", - Cves: "CVE-2021-1234, CVE-2021-5678", - Headline: "[CVE-2021-1234, CVE-2021-5678] example-package 1.0.0", - Severity: "7.2", - Description: "Vulnerable dependency", - MarkdownDescription: "| Severity Score | Contextual Analysis | Direct Dependencies | Fixed Versions |\n| :---: | :---: | :---: | :---: |\n| 7.2 | Applicable | `example-package 1.0.0` | 1.0.1, 1.0.2 |\n", - }, - }, - { - name: "Vulnerability without markdown output", - vulnerability: formats.VulnerabilityOrViolationRow{ - Summary: "Vulnerable dependency", - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - SeverityDetails: formats.SeverityDetails{ - Severity: "high", - }, - ImpactedDependencyName: "example-package", - ImpactedDependencyVersion: "1.0.0", - ImpactedDependencyType: "npm", - Components: []formats.ComponentRow{ - {Name: "example-package", Version: "1.0.0"}, - }, - }, - Applicable: Applicable.String(), - FixedVersions: []string{"1.0.1", "1.0.2"}, - Cves: []formats.CveRow{ - {Id: "CVE-2021-1234", CvssV3: "7.2"}, - {Id: "CVE-2021-5678", CvssV3: "7.2"}, - }, - IssueId: "XRAY-12345", - }, - expectedOutput: sarifProperties{ - Applicable: "Applicable", - Cves: "CVE-2021-1234, CVE-2021-5678", - Headline: "[CVE-2021-1234, CVE-2021-5678] example-package 1.0.0", - Severity: "7.2", - Description: "Vulnerable dependency", - MarkdownDescription: "", - }, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - output, err := getViolatedDepsSarifProps(tc.vulnerability, tc.markdownOutput) - assert.NoError(t, err) - assert.Equal(t, tc.expectedOutput.Cves, output.Cves) - assert.Equal(t, tc.expectedOutput.Severity, output.Severity) - assert.Equal(t, tc.expectedOutput.XrayID, output.XrayID) - assert.Equal(t, tc.expectedOutput.MarkdownDescription, output.MarkdownDescription) - assert.Equal(t, tc.expectedOutput.Applicable, output.Applicable) - assert.Equal(t, tc.expectedOutput.Description, output.Description) - assert.Equal(t, tc.expectedOutput.Headline, output.Headline) - }) - } -} - func TestGetDirectDependenciesFormatted(t *testing.T) { testCases := []struct { name string diff --git a/xray/utils/sarifutils.go b/xray/utils/sarifutils.go index 0fb9ef5d9..420243441 100644 --- a/xray/utils/sarifutils.go +++ b/xray/utils/sarifutils.go @@ -1,96 +1,283 @@ package utils import ( + "fmt" "strconv" "strings" + "github.com/jfrog/gofrog/datastructures" + "github.com/jfrog/jfrog-client-go/utils/errorutils" "github.com/owenrumney/go-sarif/v2/sarif" ) -// If exists SourceCodeScanResult with the same location as the provided SarifResult, return it -func GetResultIfExists(result *sarif.Result, workingDir string, results []*SourceCodeScanResult) *SourceCodeScanResult { - file := ExtractRelativePath(GetResultFileName(result), workingDir) - lineCol := GetResultLocationInFile(result) - text := *result.Message.Text - for _, result := range results { - if result.File == file && result.LineColumn == lineCol && result.Text == text { - return result - } +type SarifLevel string + +const ( + errorLevel SarifLevel = "error" + warningLevel SarifLevel = "warning" + infoLevel SarifLevel = "info" + noteLevel SarifLevel = "note" + noneLevel SarifLevel = "none" + + SeverityDefaultValue = "Medium" +) + +var ( + // All other values (include default) mapped as 'Medium' severity + levelToSeverity = map[SarifLevel]string{ + errorLevel: "High", + noteLevel: "Low", + noneLevel: "Unknown", } - return nil + + severityToLevel = map[string]SarifLevel{ + "critical": errorLevel, + "high": errorLevel, + "medium": warningLevel, + "low": noteLevel, + "unknown": noneLevel, + } +) + +func NewReport() (*sarif.Report, error) { + report, err := sarif.New(sarif.Version210) + if err != nil { + return nil, errorutils.CheckError(err) + } + return report, nil +} + +func ReadScanRunsFromFile(fileName string) (sarifRuns []*sarif.Run, err error) { + report, err := sarif.Open(fileName) + if errorutils.CheckError(err) != nil { + err = fmt.Errorf("can't read valid Sarif run from " + fileName + ": " + err.Error()) + return + } + sarifRuns = report.Runs + return } -func ConvertSarifResultToSourceCodeScanResult(result *sarif.Result, workingDir string) *SourceCodeScanResult { - file := ExtractRelativePath(GetResultFileName(result), workingDir) - lineCol := GetResultLocationInFile(result) - text := *result.Message.Text +func AggregateMultipleRunsIntoSingle(runs []*sarif.Run, destination *sarif.Run) { + if len(runs) == 0 { + return + } + for _, run := range runs { + if run == nil || len(run.Results) == 0 { + continue + } + for _, rule := range GetRunRules(run) { + if destination.Tool.Driver != nil { + destination.Tool.Driver.AddRule(rule) + } + } + for _, result := range run.Results { + destination.AddResult(result) + } + for _, invocation := range run.Invocations { + destination.AddInvocations(invocation) + } + } +} - return &SourceCodeScanResult{ - Severity: GetResultSeverity(result), - SourceCodeLocation: SourceCodeLocation{ - File: file, - LineColumn: lineCol, - Text: text, - }, - Type: *result.RuleID, +func getRunInformationUri(run *sarif.Run) string { + if run != nil && run.Tool.Driver != nil && run.Tool.Driver.InformationURI != nil { + return *run.Tool.Driver.InformationURI } + return "" } -func GetResultCodeFlows(result *sarif.Result, workingDir string) (flows []*[]SourceCodeLocation) { - if len(result.CodeFlows) == 0 { +// Calculate new information that exists at the run and not at the source +func GetDiffFromRun(sources []*sarif.Run, targets []*sarif.Run) (runWithNewOnly *sarif.Run) { + // Combine + combinedSource := sarif.NewRunWithInformationURI(sources[0].Tool.Driver.Name, getRunInformationUri(sources[0])) + AggregateMultipleRunsIntoSingle(sources, combinedSource) + if combinedSource == nil { return } - for _, codeFlow := range result.CodeFlows { - if codeFlow == nil || len(codeFlow.ThreadFlows) == 0 { + combinedTarget := sarif.NewRunWithInformationURI(targets[0].Tool.Driver.Name, getRunInformationUri(targets[0])) + AggregateMultipleRunsIntoSingle(targets, combinedTarget) + if combinedTarget == nil { + return combinedSource + } + // Get diff + runWithNewOnly = sarif.NewRun(combinedSource.Tool).WithInvocations(combinedSource.Invocations) + for _, sourceResult := range combinedSource.Results { + targetMatchingResults := GetResultsByRuleId(combinedTarget, *sourceResult.RuleID) + if len(targetMatchingResults) == 0 { + runWithNewOnly.AddResult(sourceResult) + if rule, _ := combinedSource.GetRuleById(*sourceResult.RuleID); rule != nil { + runWithNewOnly.Tool.Driver.AddRule(rule) + } continue } - flows = append(flows, extractThreadFlows(codeFlow.ThreadFlows, workingDir)...) + for _, targetMatchingResult := range targetMatchingResults { + if len(sourceResult.Locations) > len(targetMatchingResult.Locations) || + len(sourceResult.CodeFlows) > len(targetMatchingResult.CodeFlows) { + runWithNewOnly.AddResult(sourceResult) + if rule, _ := combinedSource.GetRuleById(*sourceResult.RuleID); rule != nil { + runWithNewOnly.Tool.Driver.AddRule(rule) + } + } + } } return } -func extractThreadFlows(threadFlows []*sarif.ThreadFlow, workingDir string) (flows []*[]SourceCodeLocation) { - for _, threadFlow := range threadFlows { - if threadFlow == nil || len(threadFlow.Locations) == 0 { +// Calculate new information that exists at the result and not at the source +func GetDiffFromResult(result *sarif.Result, source *sarif.Result) *sarif.Result { + newLocations := datastructures.MakeSet[*sarif.Location]() + newCodeFlows := []*sarif.CodeFlow{} + for _, targetLocation := range result.Locations { + if !IsLocationInResult(targetLocation, source) { + newLocations.Add(targetLocation) + newCodeFlows = append(newCodeFlows, GetLocationRelatedCodeFlowsFromResult(targetLocation, result)...) continue } - flow := extractStackTraceLocations(threadFlow.Locations, workingDir) - if len(flow) > 0 { - flows = append(flows, &flow) + // Location in result, compare related code flows + for _, targetCodeFlow := range GetLocationRelatedCodeFlowsFromResult(targetLocation, result) { + for _, sourceCodeFlow := range GetLocationRelatedCodeFlowsFromResult(targetLocation, source) { + if !IsSameCodeFlow(targetCodeFlow, sourceCodeFlow) { + // Code flow does not exists at source, add it and it's related location + newLocations.Add(targetLocation) + newCodeFlows = append(newCodeFlows, targetCodeFlow) + } + } + } + } + // Create the result only with new information + return sarif.NewRuleResult(*result.RuleID). + WithKind(*result.Kind). + WithMessage(&result.Message). + WithLevel(*result.Level). + WithLocations(newLocations.ToSlice()). + WithCodeFlows(newCodeFlows) +} + +func FilterResultsByRuleIdAndMsgText(source []*sarif.Result, ruleId, msgText string) (results []*sarif.Result) { + for _, result := range source { + if ruleId == *result.RuleID && msgText == GetResultMsgText(result) { + results = append(results, result) } } return } -func extractStackTraceLocations(locations []*sarif.ThreadFlowLocation, workingDir string) (flow []SourceCodeLocation) { - for _, location := range locations { - if location == nil { - continue +func GetLocationRelatedCodeFlowsFromResult(location *sarif.Location, result *sarif.Result) (codeFlows []*sarif.CodeFlow) { + for _, codeFlow := range result.CodeFlows { + for _, stackTrace := range codeFlow.ThreadFlows { + // The threadFlow is reverse stack trace. + // The last location is the location that it relates to. + if IsSameLocation(location, stackTrace.Locations[len(stackTrace.Locations)-1].Location) { + codeFlows = append(codeFlows, codeFlow) + } } - flow = append(flow, SourceCodeLocation{ - File: ExtractRelativePath(getResultFileName(location.Location), workingDir), - LineColumn: getResultLocationInFile(location.Location), - Text: GetResultLocationSnippet(location.Location), - }) } return } -func GetResultLocationSnippet(location *sarif.Location) string { - if location != nil && location.PhysicalLocation != nil && location.PhysicalLocation.Region != nil && location.PhysicalLocation.Region.Snippet != nil { - return *location.PhysicalLocation.Region.Snippet.Text +func IsSameCodeFlow(codeFlow *sarif.CodeFlow, other *sarif.CodeFlow) bool { + if len(codeFlow.ThreadFlows) != len(other.ThreadFlows) { + return false } - return "" + // ThreadFlows is unordered list of stack trace + for _, stackTrace := range codeFlow.ThreadFlows { + foundMatch := false + for _, otherStackTrace := range other.ThreadFlows { + if len(stackTrace.Locations) != len(otherStackTrace.Locations) { + continue + } + for i, stackTraceLocation := range stackTrace.Locations { + if !IsSameLocation(stackTraceLocation.Location, otherStackTrace.Locations[i].Location) { + continue + } + } + foundMatch = true + } + if !foundMatch { + return false + } + } + return true +} + +func IsLocationInResult(location *sarif.Location, result *sarif.Result) bool { + for _, resultLocation := range result.Locations { + if IsSameLocation(location, resultLocation) { + return true + } + } + return false +} + +func IsSameLocation(location *sarif.Location, other *sarif.Location) bool { + if location == other { + return true + } + return GetLocationFileName(location) == GetLocationFileName(other) && + GetLocationSnippet(location) == GetLocationSnippet(other) && + GetLocationStartLine(location) == GetLocationStartLine(other) && + GetLocationStartColumn(location) == GetLocationStartColumn(other) && + GetLocationEndLine(location) == GetLocationEndLine(other) && + GetLocationEndColumn(location) == GetLocationEndColumn(other) +} + +func GetResultsLocationCount(runs ...*sarif.Run) (count int) { + for _, run := range runs { + for _, result := range run.Results { + count += len(result.Locations) + } + } + return } -func GetResultFileName(result *sarif.Result) string { - if len(result.Locations) > 0 { - return getResultFileName(result.Locations[0]) +func GetLevelResultsLocationCount(run *sarif.Run, level SarifLevel) (count int) { + for _, result := range run.Results { + if level == SarifLevel(*result.Level) { + count += len(result.Locations) + } + } + return +} + +func GetResultsByRuleId(run *sarif.Run, ruleId string) (results []*sarif.Result) { + for _, result := range run.Results { + if *result.RuleID == ruleId { + results = append(results, result) + } + } + return +} + +func GetResultMsgText(result *sarif.Result) string { + if result.Message.Text != nil { + return *result.Message.Text } return "" } -func getResultFileName(location *sarif.Location) string { +func GetLocationSnippet(location *sarif.Location) string { + snippet := GetLocationSnippetPointer(location) + if snippet == nil { + return "" + } + return *snippet +} + +func GetLocationSnippetPointer(location *sarif.Location) *string { + region := getLocationRegion(location) + if region != nil && region.Snippet != nil { + return region.Snippet.Text + } + return nil +} + +func SetLocationSnippet(location *sarif.Location, snippet string) { + if location != nil && location.PhysicalLocation != nil && location.PhysicalLocation.Region != nil && location.PhysicalLocation.Region.Snippet != nil { + location.PhysicalLocation.Region.Snippet.Text = &snippet + } +} + +func GetLocationFileName(location *sarif.Location) string { filePath := location.PhysicalLocation.ArtifactLocation.URI if filePath != nil { return *filePath @@ -98,14 +285,52 @@ func getResultFileName(location *sarif.Location) string { return "" } -func GetResultLocationInFile(result *sarif.Result) string { - if len(result.Locations) > 0 { - return getResultLocationInFile(result.Locations[0]) +func SetLocationFileName(location *sarif.Location, fileName string) { + if location != nil && location.PhysicalLocation != nil && location.PhysicalLocation.Region != nil && location.PhysicalLocation.Region.Snippet != nil { + location.PhysicalLocation.ArtifactLocation.URI = &fileName } - return "" } -func getResultLocationInFile(location *sarif.Location) string { +func getLocationRegion(location *sarif.Location) *sarif.Region { + if location != nil && location.PhysicalLocation != nil { + return location.PhysicalLocation.Region + } + return nil +} + +func GetLocationStartLine(location *sarif.Location) int { + region := getLocationRegion(location) + if region != nil && region.StartLine != nil { + return *region.StartLine + } + return 0 +} + +func GetLocationStartColumn(location *sarif.Location) int { + region := getLocationRegion(location) + if region != nil && region.StartColumn != nil { + return *region.StartColumn + } + return 0 +} + +func GetLocationEndLine(location *sarif.Location) int { + region := getLocationRegion(location) + if region != nil && region.EndLine != nil { + return *region.EndLine + } + return 0 +} + +func GetLocationEndColumn(location *sarif.Location) int { + region := getLocationRegion(location) + if region != nil && region.EndColumn != nil { + return *region.EndColumn + } + return 0 +} + +func GetStartLocationInFile(location *sarif.Location) string { startLine := location.PhysicalLocation.Region.StartLine startColumn := location.PhysicalLocation.Region.StartColumn if startLine != nil && startColumn != nil { @@ -128,3 +353,43 @@ func GetResultSeverity(result *sarif.Result) string { } return SeverityDefaultValue } + +func ConvertToSarifLevel(severity string) string { + if level, ok := severityToLevel[strings.ToLower(severity)]; ok { + return string(level) + } + return string(noneLevel) +} + +func isApplicableResult(result *sarif.Result) bool { + return !(result.Kind != nil && *result.Kind == "pass") +} + +func GetRuleFullDescription(rule *sarif.ReportingDescriptor) string { + if rule.FullDescription != nil && rule.FullDescription.Text != nil { + return *rule.FullDescription.Text + } + return "" +} + +func GetRuleIdFromCveId(cveId string) string { + return "applic_" + cveId +} + +func GetCveIdFromRuleId(sarifRuleId string) string { + return strings.TrimPrefix(sarifRuleId, "applic_") +} + +func GetRunRules(run *sarif.Run) []*sarif.ReportingDescriptor { + if run != nil && run.Tool.Driver != nil { + return run.Tool.Driver.Rules + } + return []*sarif.ReportingDescriptor{} +} + +func GetInvocationWorkingDirectory(invocation *sarif.Invocation) string { + if invocation.WorkingDirectory != nil && invocation.WorkingDirectory.URI != nil { + return *invocation.WorkingDirectory.URI + } + return "" +} diff --git a/xray/utils/sarifutils_test.go b/xray/utils/sarifutils_test.go new file mode 100644 index 000000000..4e0031268 --- /dev/null +++ b/xray/utils/sarifutils_test.go @@ -0,0 +1,43 @@ +package utils + +import ( + "github.com/jfrog/gofrog/datastructures" + "github.com/owenrumney/go-sarif/v2/sarif" +) + +func getRunWithDummyResults(results ...*sarif.Result) *sarif.Run { + run := sarif.NewRunWithInformationURI("", "") + ids := datastructures.MakeSet[string]() + for _, result := range results { + if !ids.Exists(*result.RuleID) { + run.Tool.Driver.Rules = append(run.Tool.Driver.Rules, sarif.NewRule(*result.RuleID)) + ids.Add(*result.RuleID) + } + } + return run.WithResults(results) +} + +func getDummyPassingResult(ruleId string) *sarif.Result { + kind := "pass" + return &sarif.Result{ + Kind: &kind, + RuleID: &ruleId, + } +} + +func getDummyResultWithOneLocation(fileName string, startLine, startCol int, snippet, ruleId string, level string) *sarif.Result { + return &sarif.Result{ + Locations: []*sarif.Location{ + { + PhysicalLocation: &sarif.PhysicalLocation{ + ArtifactLocation: &sarif.ArtifactLocation{URI: &fileName}, + Region: &sarif.Region{ + StartLine: &startLine, + StartColumn: &startCol, + Snippet: &sarif.ArtifactContent{Text: &snippet}}}, + }, + }, + Level: &level, + RuleID: &ruleId, + } +}