diff --git a/testdata/messages/summarycomment/structure/summary_comment_issues_pr_not_entitled.md b/testdata/messages/summarycomment/structure/summary_comment_issues_pr_not_entitled.md
index 8011e0be6..8de14eb5b 100644
--- a/testdata/messages/summarycomment/structure/summary_comment_issues_pr_not_entitled.md
+++ b/testdata/messages/summarycomment/structure/summary_comment_issues_pr_not_entitled.md
@@ -12,9 +12,7 @@
```
some content
```
-
diff --git a/testdata/messages/summarycomment/structure/summary_comment_issues_pr_not_entitled_with_title.md b/testdata/messages/summarycomment/structure/summary_comment_issues_pr_not_entitled_with_title.md
index 96c89e5c7..428ee98d1 100644
--- a/testdata/messages/summarycomment/structure/summary_comment_issues_pr_not_entitled_with_title.md
+++ b/testdata/messages/summarycomment/structure/summary_comment_issues_pr_not_entitled_with_title.md
@@ -13,9 +13,7 @@
```
some content
```
-
- Note:
-
+Note
---
@@ -23,10 +21,7 @@ some content
**Frogbot** also supports **Contextual Analysis, Secret Detection, IaC and SAST Vulnerabilities Scanning**. This features are included as part of the [JFrog Advanced Security](https://jfrog.com/advanced-security) package, which isn't enabled on your system.
-
-
-
-
+
---
diff --git a/testdata/messages/summarycomment/structure/summary_comment_issues_simplified_not_entitled.md b/testdata/messages/summarycomment/structure/summary_comment_issues_simplified_not_entitled.md
index ed09e547e..94fad5000 100644
--- a/testdata/messages/summarycomment/structure/summary_comment_issues_simplified_not_entitled.md
+++ b/testdata/messages/summarycomment/structure/summary_comment_issues_simplified_not_entitled.md
@@ -7,11 +7,7 @@
```
some content
```
-
----
-Note:
-
----
+Note:
---
**Frogbot** also supports **Contextual Analysis, Secret Detection, IaC and SAST Vulnerabilities Scanning**. This features are included as part of the [JFrog Advanced Security](https://jfrog.com/advanced-security) package, which isn't enabled on your system.
diff --git a/testdata/messages/summarycomment/structure/summary_comment_no_issues_mr_not_entitled.md b/testdata/messages/summarycomment/structure/summary_comment_no_issues_mr_not_entitled.md
index a58ad8a9d..269ca7943 100644
--- a/testdata/messages/summarycomment/structure/summary_comment_no_issues_mr_not_entitled.md
+++ b/testdata/messages/summarycomment/structure/summary_comment_no_issues_mr_not_entitled.md
@@ -8,9 +8,7 @@
-
- Note:
-
+Note
---
@@ -18,10 +16,7 @@
**Frogbot** also supports **Contextual Analysis, Secret Detection, IaC and SAST Vulnerabilities Scanning**. This features are included as part of the [JFrog Advanced Security](https://jfrog.com/advanced-security) package, which isn't enabled on your system.
-
-
-
-
+
---
diff --git a/testdata/messages/summarycomment/structure/summary_comment_no_issues_pr_not_entitled.md b/testdata/messages/summarycomment/structure/summary_comment_no_issues_pr_not_entitled.md
index 3c88b0a97..225fea261 100644
--- a/testdata/messages/summarycomment/structure/summary_comment_no_issues_pr_not_entitled.md
+++ b/testdata/messages/summarycomment/structure/summary_comment_no_issues_pr_not_entitled.md
@@ -8,9 +8,7 @@
-
- Note:
-
+Note
---
@@ -18,10 +16,7 @@
**Frogbot** also supports **Contextual Analysis, Secret Detection, IaC and SAST Vulnerabilities Scanning**. This features are included as part of the [JFrog Advanced Security](https://jfrog.com/advanced-security) package, which isn't enabled on your system.
-
-
-
-
+
---
diff --git a/testdata/messages/summarycomment/structure/summary_comment_no_issues_pr_not_entitled_with_title.md b/testdata/messages/summarycomment/structure/summary_comment_no_issues_pr_not_entitled_with_title.md
index 3382eeb5f..62a6afc04 100644
--- a/testdata/messages/summarycomment/structure/summary_comment_no_issues_pr_not_entitled_with_title.md
+++ b/testdata/messages/summarycomment/structure/summary_comment_no_issues_pr_not_entitled_with_title.md
@@ -9,9 +9,7 @@
## **Custom title**
-
- Note:
-
+Note
---
@@ -19,10 +17,7 @@
**Frogbot** also supports **Contextual Analysis, Secret Detection, IaC and SAST Vulnerabilities Scanning**. This features are included as part of the [JFrog Advanced Security](https://jfrog.com/advanced-security) package, which isn't enabled on your system.
-
-
-
-
+
---
diff --git a/testdata/messages/summarycomment/structure/summary_comment_no_issues_simplified_not_entitled.md b/testdata/messages/summarycomment/structure/summary_comment_no_issues_simplified_not_entitled.md
index f55ec3edb..6340da3d1 100644
--- a/testdata/messages/summarycomment/structure/summary_comment_no_issues_simplified_not_entitled.md
+++ b/testdata/messages/summarycomment/structure/summary_comment_no_issues_simplified_not_entitled.md
@@ -3,11 +3,7 @@
[comment]: <> (FrogbotReviewComment)
**👍 Frogbot scanned this pull request and did not find any new security issues.**
-
----
-Note:
-
----
+Note:
---
**Frogbot** also supports **Contextual Analysis, Secret Detection, IaC and SAST Vulnerabilities Scanning**. This features are included as part of the [JFrog Advanced Security](https://jfrog.com/advanced-security) package, which isn't enabled on your system.
diff --git a/testdata/messages/summarycomment/structure/summary_comment_no_issues_simplified_not_entitled_with_title.md b/testdata/messages/summarycomment/structure/summary_comment_no_issues_simplified_not_entitled_with_title.md
index a6830f144..6039ac225 100644
--- a/testdata/messages/summarycomment/structure/summary_comment_no_issues_simplified_not_entitled_with_title.md
+++ b/testdata/messages/summarycomment/structure/summary_comment_no_issues_simplified_not_entitled_with_title.md
@@ -8,11 +8,7 @@
## **Custom title**
---
-
----
-Note:
-
----
+Note:
---
**Frogbot** also supports **Contextual Analysis, Secret Detection, IaC and SAST Vulnerabilities Scanning**. This features are included as part of the [JFrog Advanced Security](https://jfrog.com/advanced-security) package, which isn't enabled on your system.
diff --git a/testdata/messages/summarycomment/summary/summary_both_simplified.md b/testdata/messages/summarycomment/summary/summary_both_simplified.md
new file mode 100644
index 000000000..c7c9bf2c2
--- /dev/null
+++ b/testdata/messages/summarycomment/summary/summary_both_simplified.md
@@ -0,0 +1,15 @@
+
+
+---
+## 📗 Scan Summary
+
+---
+- Frogbot scanned for violations and vulnerabilities and found 13 issues
+
+| Scan Category | Status | Security Issues |
+| --------------------- | :-----------------------------------: | ----------------------------------- |
+| **Software Composition Analysis** | ✅ Done | 9 Issues Found: ❗️ 2 Critical, 🔴 3 High, 🟠 2 Medium, 🟡 1 Low, ⚪️ 1 Unknown |
+| **Contextual Analysis** | ✅ Done | - |
+| **Static Application Security Testing (SAST)** | ✅ Done | 4 Issues Found: 🔴 3 High, 🟡 1 Low |
+| **Secrets** | ✅ Done | - |
+| **Infrastructure as Code (IaC)** | ℹ️ Not Scanned | - |
\ No newline at end of file
diff --git a/testdata/messages/summarycomment/summary/summary_both_standard.md b/testdata/messages/summarycomment/summary/summary_both_standard.md
new file mode 100644
index 000000000..30f1b29a4
--- /dev/null
+++ b/testdata/messages/summarycomment/summary/summary_both_standard.md
@@ -0,0 +1,11 @@
+
+## 📗 Scan Summary
+- Frogbot scanned for violations and vulnerabilities and found 13 issues
+
+| Scan Category | Status | Security Issues |
+| --------------------- | :-----------------------------------: | ----------------------------------- |
+| **Software Composition Analysis** | ✅ Done |
9 Issues Found
2 Critical
3 High
2 Medium
1 Low
1 Unknown
|
+| **Contextual Analysis** | ✅ Done | - |
+| **Static Application Security Testing (SAST)** | ✅ Done |
4 Issues Found
3 High
1 Low
|
+| **Secrets** | ✅ Done | - |
+| **Infrastructure as Code (IaC)** | ℹ️ Not Scanned | - |
\ No newline at end of file
diff --git a/testdata/messages/summarycomment/summary/summary_error_simplified.md b/testdata/messages/summarycomment/summary/summary_error_simplified.md
new file mode 100644
index 000000000..ebe16f635
--- /dev/null
+++ b/testdata/messages/summarycomment/summary/summary_error_simplified.md
@@ -0,0 +1,7 @@
+
+
+---
+## 📗 Scan Summary
+
+---
+- Frogbot attempted to scan for violations but encountered an error.
\ No newline at end of file
diff --git a/testdata/messages/summarycomment/summary/summary_error_standard.md b/testdata/messages/summarycomment/summary/summary_error_standard.md
new file mode 100644
index 000000000..7849db553
--- /dev/null
+++ b/testdata/messages/summarycomment/summary/summary_error_standard.md
@@ -0,0 +1,3 @@
+
+## 📗 Scan Summary
+- Frogbot attempted to scan for violations but encountered an error.
\ No newline at end of file
diff --git a/testdata/messages/summarycomment/summary/summary_simplified.md b/testdata/messages/summarycomment/summary/summary_simplified.md
new file mode 100644
index 000000000..64bab849c
--- /dev/null
+++ b/testdata/messages/summarycomment/summary/summary_simplified.md
@@ -0,0 +1,15 @@
+
+
+---
+## 📗 Scan Summary
+
+---
+- Frogbot scanned for vulnerabilities and found 9 issues
+
+| Scan Category | Status | Security Issues |
+| --------------------- | :-----------------------------------: | ----------------------------------- |
+| **Software Composition Analysis** | ✅ Done | 6 Issues Found: ❗️ 1 Critical, 🔴 2 High, 🟠 1 Medium, 🟡 1 Low, ⚪️ 1 Unknown |
+| **Contextual Analysis** | ✅ Done | - |
+| **Static Application Security Testing (SAST)** | ✅ Done | 3 Issues Found: 🔴 2 High, 🟡 1 Low |
+| **Secrets** | ✅ Done | - |
+| **Infrastructure as Code (IaC)** | ℹ️ Not Scanned | - |
\ No newline at end of file
diff --git a/testdata/messages/summarycomment/summary/summary_standard.md b/testdata/messages/summarycomment/summary/summary_standard.md
new file mode 100644
index 000000000..5244ba6bf
--- /dev/null
+++ b/testdata/messages/summarycomment/summary/summary_standard.md
@@ -0,0 +1,11 @@
+
+## 📗 Scan Summary
+- Frogbot scanned for vulnerabilities and found 9 issues
+
+| Scan Category | Status | Security Issues |
+| --------------------- | :-----------------------------------: | ----------------------------------- |
+| **Software Composition Analysis** | ✅ Done |
6 Issues Found
1 Critical
2 High
1 Medium
1 Low
1 Unknown
|
+| **Contextual Analysis** | ✅ Done | - |
+| **Static Application Security Testing (SAST)** | ✅ Done |
3 Issues Found
2 High
1 Low
|
+| **Secrets** | ✅ Done | - |
+| **Infrastructure as Code (IaC)** | ℹ️ Not Scanned | - |
\ No newline at end of file
diff --git a/testdata/messages/summarycomment/summary/summary_violation_simplified.md b/testdata/messages/summarycomment/summary/summary_violation_simplified.md
new file mode 100644
index 000000000..58185dd84
--- /dev/null
+++ b/testdata/messages/summarycomment/summary/summary_violation_simplified.md
@@ -0,0 +1,15 @@
+
+
+---
+## 📗 Scan Summary
+
+---
+- Frogbot scanned for violations and found 4 issues
+
+| Scan Category | Status | Security Issues |
+| --------------------- | :-----------------------------------: | ----------------------------------- |
+| **Software Composition Analysis** | ✅ Done | 3 Issues Found: ❗️ 1 Critical, 🔴 1 High, 🟠 1 Medium |
+| **Contextual Analysis** | ✅ Done | - |
+| **Static Application Security Testing (SAST)** | ✅ Done | 1 Issues Found: 🔴 1 High |
+| **Secrets** | ✅ Done | - |
+| **Infrastructure as Code (IaC)** | ℹ️ Not Scanned | - |
\ No newline at end of file
diff --git a/testdata/messages/summarycomment/summary/summary_violation_standard.md b/testdata/messages/summarycomment/summary/summary_violation_standard.md
new file mode 100644
index 000000000..245da163e
--- /dev/null
+++ b/testdata/messages/summarycomment/summary/summary_violation_standard.md
@@ -0,0 +1,11 @@
+
+## 📗 Scan Summary
+- Frogbot scanned for violations and found 4 issues
+
+| Scan Category | Status | Security Issues |
+| --------------------- | :-----------------------------------: | ----------------------------------- |
+| **Software Composition Analysis** | ✅ Done |
3 Issues Found
1 Critical
1 High
1 Medium
|
+| **Contextual Analysis** | ✅ Done | - |
+| **Static Application Security Testing (SAST)** | ✅ Done |
1 Issues Found
1 High
|
+| **Secrets** | ✅ Done | - |
+| **Infrastructure as Code (IaC)** | ℹ️ Not Scanned | - |
\ No newline at end of file
diff --git a/testdata/messages/summarycomment/violations/license/license_violation_simplified.md b/testdata/messages/summarycomment/violations/license/license_violation_simplified.md
new file mode 100644
index 000000000..a1297e325
--- /dev/null
+++ b/testdata/messages/summarycomment/violations/license/license_violation_simplified.md
@@ -0,0 +1,69 @@
+
+
+---
+## 🚥 Policy Violations
+
+---
+
+
+
+---
+### ⚖️ License Violations
+
+---
+
+| Severity | License | Direct Dependencies | Impacted Dependency | Watch Name |
+| :---------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: |
+| High | License1 | Comp1:1.0 | Dep1:2.0 | watch |
+| High | License2 | root:1.0.0 | Dep2:3.0 | watch2 |
+| | | minimatch:1.2.3 | | |
+
+
+---
+### 🔖 Details
+
+---
+
+
+
+---
+#### [ License1 ] Dep1 2.0 (watch)
+
+---
+
+
+
+---
+### Violation Details
+
+---
+| | |
+| --------------------- | :-----------------------------------: |
+| **Policies:** | policy1, policy2 |
+| **Watch Name:** | watch |
+| **Direct Dependencies:** | Comp1:1.0 |
+| **Impacted Dependency:** | Dep1:2.0 |
+| **Full Name:** | License1 full name |
+
+
+
+
+---
+#### [ License2 ] Dep2 3.0 (watch2)
+
+---
+
+
+
+---
+### Violation Details
+
+---
+| | |
+| --------------------- | :-----------------------------------: |
+| **Policies:** | policy3 |
+| **Watch Name:** | watch2 |
+| **Direct Dependencies:** | root:1.0.0, minimatch:1.2.3 |
+| **Impacted Dependency:** | Dep2:3.0 |
+| **Full Name:** | - |
+
diff --git a/testdata/messages/summarycomment/violations/license/license_violation_standard.md b/testdata/messages/summarycomment/violations/license/license_violation_standard.md
new file mode 100644
index 000000000..1aecc3806
--- /dev/null
+++ b/testdata/messages/summarycomment/violations/license/license_violation_standard.md
@@ -0,0 +1,44 @@
+
+## 🚥 Policy Violations
+
+
+### ⚖️ License Violations
+
+
+
+| Severity | License | Direct Dependencies | Impacted Dependency | Watch Name |
+| :---------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: |
+| 
High | License1 | Comp1:1.0 | Dep1:2.0 | watch |
+| 
High | License2 | root:1.0.0
minimatch:1.2.3 | Dep2:3.0 | watch2 |
+
+
+
+
+### 🔖 Details
+
+
+
[ License1 ] Dep1 2.0 (watch)
+
+### Violation Details
+| | |
+| --------------------- | :-----------------------------------: |
+| **Policies:** | policy1, policy2 |
+| **Watch Name:** | watch |
+| **Direct Dependencies:** | Comp1:1.0 |
+| **Impacted Dependency:** | Dep1:2.0 |
+| **Full Name:** | License1 full name |
+
+
+
+
[ License2 ] Dep2 3.0 (watch2)
+
+### Violation Details
+| | |
+| --------------------- | :-----------------------------------: |
+| **Policies:** | policy3 |
+| **Watch Name:** | watch2 |
+| **Direct Dependencies:** | root:1.0.0, minimatch:1.2.3 |
+| **Impacted Dependency:** | Dep2:3.0 |
+| **Full Name:** | - |
+
+
\ No newline at end of file
diff --git a/testdata/messages/summarycomment/violations/security/security_violation_simplified.md b/testdata/messages/summarycomment/violations/security/security_violation_simplified.md
new file mode 100644
index 000000000..159980804
--- /dev/null
+++ b/testdata/messages/summarycomment/violations/security/security_violation_simplified.md
@@ -0,0 +1,106 @@
+
+
+
+---
+### 🚨 Security Violations
+
+---
+
+| Severity | ID | Contextual Analysis | Direct Dependencies | Impacted Dependency | Watch Name |
+| :---------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: |
+| Critical | CVE-1111-11111 | Not Applicable | dep1:1.0.0 | impacted:3.0.0 | - |
+| | | | dep2:2.0.0 | | |
+| High | XRAY-122345 | Undetermined | github.com/nats-io/nats-streaming-server:v0.21.0 | github.com/nats-io/nats-streaming-server:v0.21.0 | - |
+| Medium | CVE-2022-26652, CVE-2023-4321 | Applicable | component-D:v0.21.0 | component-D:v0.21.0 | - |
+| Low | - | Undetermined | github.com/mholt/archiver/v3:v3.5.1 | github.com/mholt/archiver/v3:v3.5.1 | - |
+
+
+---
+### 🔖 Details
+
+---
+
+
+
+---
+#### [ XRAY-122345 ] github.com/nats-io/nats-streaming-server v0.21.0
+
+---
+
+
+
+---
+### Violation Details
+
+---
+| | |
+| --------------------- | :-----------------------------------: |
+| **Contextual Analysis:** | Undetermined |
+| **Direct Dependencies:** | github.com/nats-io/nats-streaming-server:v0.21.0 |
+| **Impacted Dependency:** | github.com/nats-io/nats-streaming-server:v0.21.0 |
+| **Fixed Versions:** | [0.24.1] |
+| **CVSS V3:** | - |
+
+Summary XRAY-122345
+
+
+---
+### 🔬 JFrog Research Details
+
+---
+
+**Remediation:**
+some remediation
+
+
+
+---
+#### [ CVE-2022-26652, CVE-2023-4321 ] component-D v0.21.0
+
+---
+
+
+
+---
+### Violation Details
+
+---
+| | |
+| --------------------- | :-----------------------------------: |
+| **Contextual Analysis:** | Applicable |
+| **Direct Dependencies:** | component-D:v0.21.0 |
+| **Impacted Dependency:** | component-D:v0.21.0 |
+| **Fixed Versions:** | [0.24.3] |
+| **CVSS V3:** | - |
+
+
+---
+### 🔬 JFrog Research Details
+
+---
+
+**Remediation:**
+some remediation
+
+
+
+---
+#### github.com/mholt/archiver/v3 v3.5.1
+
+---
+
+
+
+---
+### Violation Details
+
+---
+| | |
+| --------------------- | :-----------------------------------: |
+| **Contextual Analysis:** | Undetermined |
+| **Direct Dependencies:** | github.com/mholt/archiver/v3:v3.5.1 |
+| **Impacted Dependency:** | github.com/mholt/archiver/v3:v3.5.1 |
+| **Fixed Versions:** | - |
+| **CVSS V3:** | - |
+
+Summary
\ No newline at end of file
diff --git a/testdata/messages/summarycomment/violations/security/security_violation_standard.md b/testdata/messages/summarycomment/violations/security/security_violation_standard.md
new file mode 100644
index 000000000..55b1d196b
--- /dev/null
+++ b/testdata/messages/summarycomment/violations/security/security_violation_standard.md
@@ -0,0 +1,67 @@
+
+
+### 🚨 Security Violations
+
+
+
+| Severity | ID | Contextual Analysis | Direct Dependencies | Impacted Dependency | Watch Name |
+| :---------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: |
+| 
Critical | CVE-1111-11111 | Not Applicable | dep1:1.0.0
dep2:2.0.0 | impacted:3.0.0 | - |
+| 
High | XRAY-122345 | Undetermined | github.com/nats-io/nats-streaming-server:v0.21.0 | github.com/nats-io/nats-streaming-server:v0.21.0 | - |
+| 
Medium | CVE-2022-26652
CVE-2023-4321 | Applicable | component-D:v0.21.0 | component-D:v0.21.0 | - |
+| 
Low | - | Undetermined | github.com/mholt/archiver/v3:v3.5.1 | github.com/mholt/archiver/v3:v3.5.1 | - |
+
+
+
+
+### 🔖 Details
+
+
+
[ XRAY-122345 ] github.com/nats-io/nats-streaming-server v0.21.0
+
+### Violation Details
+| | |
+| --------------------- | :-----------------------------------: |
+| **Contextual Analysis:** | Undetermined |
+| **Direct Dependencies:** | github.com/nats-io/nats-streaming-server:v0.21.0 |
+| **Impacted Dependency:** | github.com/nats-io/nats-streaming-server:v0.21.0 |
+| **Fixed Versions:** | [0.24.1] |
+| **CVSS V3:** | - |
+
+Summary XRAY-122345
+
+### 🔬 JFrog Research Details
+
+**Remediation:**
+some remediation
+
+
+
[ CVE-2022-26652, CVE-2023-4321 ] component-D v0.21.0
+
+### Violation Details
+| | |
+| --------------------- | :-----------------------------------: |
+| **Contextual Analysis:** | Applicable |
+| **Direct Dependencies:** | component-D:v0.21.0 |
+| **Impacted Dependency:** | component-D:v0.21.0 |
+| **Fixed Versions:** | [0.24.3] |
+| **CVSS V3:** | - |
+
+### 🔬 JFrog Research Details
+
+**Remediation:**
+some remediation
+
+
+
github.com/mholt/archiver/v3 v3.5.1
+
+### Violation Details
+| | |
+| --------------------- | :-----------------------------------: |
+| **Contextual Analysis:** | Undetermined |
+| **Direct Dependencies:** | github.com/mholt/archiver/v3:v3.5.1 |
+| **Impacted Dependency:** | github.com/mholt/archiver/v3:v3.5.1 |
+| **Fixed Versions:** | - |
+| **CVSS V3:** | - |
+
+Summary
\ No newline at end of file
diff --git a/testdata/messages/summarycomment/vulnerabilities/one_vulnerability_no_details_simplified.md b/testdata/messages/summarycomment/vulnerabilities/one_vulnerability_no_details_simplified.md
index f032744fd..899a11a99 100644
--- a/testdata/messages/summarycomment/vulnerabilities/one_vulnerability_no_details_simplified.md
+++ b/testdata/messages/summarycomment/vulnerabilities/one_vulnerability_no_details_simplified.md
@@ -1,15 +1,11 @@
----
-## 📦 Vulnerable Dependencies
---
-
+### 📦 Vulnerable Dependencies
---
-### ✍️ Summary
----
-| SEVERITY | DIRECT DEPENDENCIES | IMPACTED DEPENDENCY | FIXED VERSIONS | CVES |
+| Severity | ID | Direct Dependencies | Impacted Dependency | Fixed Versions |
| :---------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: |
-| Medium | github.com/nats-io/nats-streaming-server:v0.21.0 | github.com/nats-io/nats-streaming-server v0.21.0 | [0.24.3] | CVE-2022-26652 |
\ No newline at end of file
+| Medium | CVE-2022-26652 | github.com/nats-io/nats-streaming-server:v0.21.0 | github.com/nats-io/nats-streaming-server v0.21.0 | [0.24.3] |
\ No newline at end of file
diff --git a/testdata/messages/summarycomment/vulnerabilities/one_vulnerability_no_details_standard.md b/testdata/messages/summarycomment/vulnerabilities/one_vulnerability_no_details_standard.md
index b194f1f7c..09cfb721e 100644
--- a/testdata/messages/summarycomment/vulnerabilities/one_vulnerability_no_details_standard.md
+++ b/testdata/messages/summarycomment/vulnerabilities/one_vulnerability_no_details_standard.md
@@ -1,11 +1,11 @@
-## 📦 Vulnerable Dependencies
-### ✍️ Summary
+### 📦 Vulnerable Dependencies
+
-| SEVERITY | DIRECT DEPENDENCIES | IMPACTED DEPENDENCY | FIXED VERSIONS | CVES |
+| Severity | ID | Direct Dependencies | Impacted Dependency | Fixed Versions |
| :---------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: |
-| 
Medium | github.com/nats-io/nats-streaming-server:v0.21.0 | github.com/nats-io/nats-streaming-server v0.21.0 | [0.24.3] | CVE-2022-26652 |
+| Medium | CVE-2022-26652 | github.com/nats-io/nats-streaming-server:v0.21.0 | github.com/nats-io/nats-streaming-server v0.21.0 | [0.24.3] |
diff --git a/testdata/messages/summarycomment/vulnerabilities/one_vulnerability_simplified.md b/testdata/messages/summarycomment/vulnerabilities/one_vulnerability_simplified.md
index e2ec84fb2..41609c4b6 100644
--- a/testdata/messages/summarycomment/vulnerabilities/one_vulnerability_simplified.md
+++ b/testdata/messages/summarycomment/vulnerabilities/one_vulnerability_simplified.md
@@ -1,28 +1,46 @@
+
---
-## 📦 Vulnerable Dependencies
+### 📦 Vulnerable Dependencies
---
+| Severity | ID | Direct Dependencies | Impacted Dependency | Fixed Versions |
+| :---------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: |
+| Medium | CVE-2022-26652 | github.com/nats-io/nats-streaming-server:v0.21.0 | github.com/nats-io/nats-streaming-server v0.21.0 | [0.24.3] |
+
---
-### ✍️ Summary
+### 🔖 Details
---
-| SEVERITY | DIRECT DEPENDENCIES | IMPACTED DEPENDENCY | FIXED VERSIONS | CVES |
-| :---------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: |
-| Medium | github.com/nats-io/nats-streaming-server:v0.21.0 | github.com/nats-io/nats-streaming-server v0.21.0 | [0.24.3] | CVE-2022-26652 |
+
+
---
-### 🔬 Research Details
+### Vulnerability Details
---
+| | |
+| --------------------- | :-----------------------------------: |
+| **Contextual Analysis:** | Undetermined |
+| **Direct Dependencies:** | github.com/nats-io/nats-streaming-server:v0.21.0 |
+| **Impacted Dependency:** | github.com/nats-io/nats-streaming-server:v0.21.0 |
+| **Fixed Versions:** | [0.24.3] |
+| **CVSS V3:** | - |
+Summary CVE-2022-26652
+
+
+---
+### 🔬 JFrog Research Details
+
+---
**Description:**
Research CVE-2022-26652 details
**Remediation:**
-some remediation
\ No newline at end of file
+some remediation
diff --git a/testdata/messages/summarycomment/vulnerabilities/one_vulnerability_standard.md b/testdata/messages/summarycomment/vulnerabilities/one_vulnerability_standard.md
index 7a9f1c5fa..535a6abb1 100644
--- a/testdata/messages/summarycomment/vulnerabilities/one_vulnerability_standard.md
+++ b/testdata/messages/summarycomment/vulnerabilities/one_vulnerability_standard.md
@@ -1,21 +1,35 @@
-## 📦 Vulnerable Dependencies
-### ✍️ Summary
+### 📦 Vulnerable Dependencies
+
-| SEVERITY | DIRECT DEPENDENCIES | IMPACTED DEPENDENCY | FIXED VERSIONS | CVES |
+| Severity | ID | Direct Dependencies | Impacted Dependency | Fixed Versions |
| :---------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: |
-| 
Medium | github.com/nats-io/nats-streaming-server:v0.21.0 | github.com/nats-io/nats-streaming-server v0.21.0 | [0.24.3] | CVE-2022-26652 |
+| Medium | CVE-2022-26652 | github.com/nats-io/nats-streaming-server:v0.21.0 | github.com/nats-io/nats-streaming-server v0.21.0 | [0.24.3] |
-### 🔬 Research Details
+### 🔖 Details
+
+
+
+### Vulnerability Details
+| | |
+| --------------------- | :-----------------------------------: |
+| **Contextual Analysis:** | Undetermined |
+| **Direct Dependencies:** | github.com/nats-io/nats-streaming-server:v0.21.0 |
+| **Impacted Dependency:** | github.com/nats-io/nats-streaming-server:v0.21.0 |
+| **Fixed Versions:** | [0.24.3] |
+| **CVSS V3:** | - |
+
+Summary CVE-2022-26652
+### 🔬 JFrog Research Details
**Description:**
Research CVE-2022-26652 details
**Remediation:**
-some remediation
\ No newline at end of file
+some remediation
diff --git a/testdata/messages/summarycomment/vulnerabilities/vulnerabilities_simplified.md b/testdata/messages/summarycomment/vulnerabilities/vulnerabilities_simplified.md
index 31866fcc6..cc77fc444 100644
--- a/testdata/messages/summarycomment/vulnerabilities/vulnerabilities_simplified.md
+++ b/testdata/messages/summarycomment/vulnerabilities/vulnerabilities_simplified.md
@@ -1,53 +1,106 @@
+
---
-## 📦 Vulnerable Dependencies
+### 📦 Vulnerable Dependencies
---
+| Severity | ID | Contextual Analysis | Direct Dependencies | Impacted Dependency | Fixed Versions |
+| :---------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: |
+| Critical | CVE-1111-11111 | Not Applicable | dep1:1.0.0 | impacted 3.0.0 | 4.0.0, 5.0.0 |
+| | | | dep2:2.0.0 | | |
+| High | XRAY-122345 | Undetermined | github.com/nats-io/nats-streaming-server:v0.21.0 | github.com/nats-io/nats-streaming-server v0.21.0 | [0.24.1] |
+| Medium | CVE-2022-26652, CVE-2023-4321 | Applicable | component-D:v0.21.0 | component-D v0.21.0 | [0.24.3] |
+| Low | - | Undetermined | github.com/mholt/archiver/v3:v3.5.1 | github.com/mholt/archiver/v3 v3.5.1 | - |
+
---
-### ✍️ Summary
+### 🔖 Details
---
-| SEVERITY | CONTEXTUAL ANALYSIS | DIRECT DEPENDENCIES | IMPACTED DEPENDENCY | FIXED VERSIONS | CVES |
-| :---------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: |
-| Critical | Not Applicable | dep1:1.0.0 | impacted 3.0.0 | 4.0.0, 5.0.0 | CVE-1111-11111 |
-| | | dep2:2.0.0 | | | |
-| High | Undetermined | github.com/nats-io/nats-streaming-server:v0.21.0 | github.com/nats-io/nats-streaming-server v0.21.0 | [0.24.1] | - |
-| Medium | Applicable | component-D:v0.21.0 | component-D v0.21.0 | [0.24.3] | CVE-2022-26652, CVE-2023-4321 |
-| Low | Undetermined | github.com/mholt/archiver/v3:v3.5.1 | github.com/mholt/archiver/v3 v3.5.1 | - | - |
+
---
-### 🔬 Research Details
+#### [ XRAY-122345 ] github.com/nats-io/nats-streaming-server v0.21.0
---
+
---
-#### [ XRAY-122345 ] github.com/nats-io/nats-streaming-server v0.21.0
+### Vulnerability Details
---
+| | |
+| --------------------- | :-----------------------------------: |
+| **Contextual Analysis:** | Undetermined |
+| **Direct Dependencies:** | github.com/nats-io/nats-streaming-server:v0.21.0 |
+| **Impacted Dependency:** | github.com/nats-io/nats-streaming-server:v0.21.0 |
+| **Fixed Versions:** | [0.24.1] |
+| **CVSS V3:** | - |
-**Description:**
Summary XRAY-122345
+
+---
+### 🔬 JFrog Research Details
+
+---
+
**Remediation:**
some remediation
+
+
---
#### [ CVE-2022-26652, CVE-2023-4321 ] component-D v0.21.0
---
+
+
+---
+### Vulnerability Details
+
+---
+| | |
+| --------------------- | :-----------------------------------: |
+| **Contextual Analysis:** | Applicable |
+| **Direct Dependencies:** | component-D:v0.21.0 |
+| **Impacted Dependency:** | component-D:v0.21.0 |
+| **Fixed Versions:** | [0.24.3] |
+| **CVSS V3:** | - |
+
+
+---
+### 🔬 JFrog Research Details
+
+---
+
**Remediation:**
some remediation
+
+
+---
+#### github.com/mholt/archiver/v3 v3.5.1
+
+---
+
+
+
---
-#### github.com/mholt/archiver/v3 v3.5.1
+### Vulnerability Details
---
+| | |
+| --------------------- | :-----------------------------------: |
+| **Contextual Analysis:** | Undetermined |
+| **Direct Dependencies:** | github.com/mholt/archiver/v3:v3.5.1 |
+| **Impacted Dependency:** | github.com/mholt/archiver/v3:v3.5.1 |
+| **Fixed Versions:** | - |
+| **CVSS V3:** | - |
-**Description:**
Summary
\ No newline at end of file
diff --git a/testdata/messages/summarycomment/vulnerabilities/vulnerabilities_simplified_split1.md b/testdata/messages/summarycomment/vulnerabilities/vulnerabilities_simplified_split1.md
index e3e2343ed..237e2b70a 100644
--- a/testdata/messages/summarycomment/vulnerabilities/vulnerabilities_simplified_split1.md
+++ b/testdata/messages/summarycomment/vulnerabilities/vulnerabilities_simplified_split1.md
@@ -1,19 +1,15 @@
----
-## 📦 Vulnerable Dependencies
---
-
+### 📦 Vulnerable Dependencies
---
-### ✍️ Summary
----
-| SEVERITY | CONTEXTUAL ANALYSIS | DIRECT DEPENDENCIES | IMPACTED DEPENDENCY | FIXED VERSIONS | CVES |
+| Severity | ID | Contextual Analysis | Direct Dependencies | Impacted Dependency | Fixed Versions |
| :---------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: |
-| Critical | Not Applicable | dep1:1.0.0 | impacted 3.0.0 | 4.0.0, 5.0.0 | CVE-1111-11111 |
-| | | dep2:2.0.0 | | | |
-| High | Undetermined | github.com/nats-io/nats-streaming-server:v0.21.0 | github.com/nats-io/nats-streaming-server v0.21.0 | [0.24.1] | - |
-| Medium | Applicable | component-D:v0.21.0 | component-D v0.21.0 | [0.24.3] | CVE-2022-26652, CVE-2023-4321 |
-| Low | Undetermined | github.com/mholt/archiver/v3:v3.5.1 | github.com/mholt/archiver/v3 v3.5.1 | - | - |
\ No newline at end of file
+| Critical | CVE-1111-11111 | Not Applicable | dep1:1.0.0 | impacted 3.0.0 | 4.0.0, 5.0.0 |
+| | | | dep2:2.0.0 | | |
+| High | XRAY-122345 | Undetermined | github.com/nats-io/nats-streaming-server:v0.21.0 | github.com/nats-io/nats-streaming-server v0.21.0 | [0.24.1] |
+| Medium | CVE-2022-26652, CVE-2023-4321 | Applicable | component-D:v0.21.0 | component-D v0.21.0 | [0.24.3] |
+| Low | - | Undetermined | github.com/mholt/archiver/v3:v3.5.1 | github.com/mholt/archiver/v3 v3.5.1 | - |
\ No newline at end of file
diff --git a/testdata/messages/summarycomment/vulnerabilities/vulnerabilities_simplified_split2.md b/testdata/messages/summarycomment/vulnerabilities/vulnerabilities_simplified_split2.md
index 01b482e36..57e2fae2e 100644
--- a/testdata/messages/summarycomment/vulnerabilities/vulnerabilities_simplified_split2.md
+++ b/testdata/messages/summarycomment/vulnerabilities/vulnerabilities_simplified_split2.md
@@ -2,34 +2,105 @@
---
-### 🔬 Research Details
+### 📦 Vulnerable Dependencies
---
+
+---
+### 🔖 Details
+
+---
+
+
+
---
#### [ XRAY-122345 ] github.com/nats-io/nats-streaming-server v0.21.0
---
-**Description:**
+
+
+---
+### Vulnerability Details
+
+---
+| | |
+| --------------------- | :-----------------------------------: |
+| **Contextual Analysis:** | Undetermined |
+| **Direct Dependencies:** | github.com/nats-io/nats-streaming-server:v0.21.0 |
+| **Impacted Dependency:** | github.com/nats-io/nats-streaming-server:v0.21.0 |
+| **Fixed Versions:** | [0.24.1] |
+| **CVSS V3:** | - |
+
Summary XRAY-122345
+
+---
+### 🔬 JFrog Research Details
+
+---
+
**Remediation:**
some remediation
+
+
+---
+### 🔖 Details
+
+---
+
+
+
---
#### [ CVE-2022-26652, CVE-2023-4321 ] component-D v0.21.0
---
+
+
+---
+### Vulnerability Details
+
+---
+| | |
+| --------------------- | :-----------------------------------: |
+| **Contextual Analysis:** | Applicable |
+| **Direct Dependencies:** | component-D:v0.21.0 |
+| **Impacted Dependency:** | component-D:v0.21.0 |
+| **Fixed Versions:** | [0.24.3] |
+| **CVSS V3:** | - |
+
+
+---
+### 🔬 JFrog Research Details
+
+---
+
**Remediation:**
some remediation
+
+
+---
+#### github.com/mholt/archiver/v3 v3.5.1
+
+---
+
+
+
---
-#### github.com/mholt/archiver/v3 v3.5.1
+### Vulnerability Details
---
+| | |
+| --------------------- | :-----------------------------------: |
+| **Contextual Analysis:** | Undetermined |
+| **Direct Dependencies:** | github.com/mholt/archiver/v3:v3.5.1 |
+| **Impacted Dependency:** | github.com/mholt/archiver/v3:v3.5.1 |
+| **Fixed Versions:** | - |
+| **CVSS V3:** | - |
-**Description:**
Summary
\ No newline at end of file
diff --git a/testdata/messages/summarycomment/vulnerabilities/vulnerabilities_standard.md b/testdata/messages/summarycomment/vulnerabilities/vulnerabilities_standard.md
index a9f78101c..1280d16b8 100644
--- a/testdata/messages/summarycomment/vulnerabilities/vulnerabilities_standard.md
+++ b/testdata/messages/summarycomment/vulnerabilities/vulnerabilities_standard.md
@@ -1,50 +1,67 @@
-## 📦 Vulnerable Dependencies
-### ✍️ Summary
+### 📦 Vulnerable Dependencies
+
-| SEVERITY | CONTEXTUAL ANALYSIS | DIRECT DEPENDENCIES | IMPACTED DEPENDENCY | FIXED VERSIONS | CVES |
+| Severity | ID | Contextual Analysis | Direct Dependencies | Impacted Dependency | Fixed Versions |
| :---------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: |
-| 
Critical | Not Applicable | dep1:1.0.0
dep2:2.0.0 | impacted 3.0.0 | 4.0.0
5.0.0 | CVE-1111-11111 |
-| 
High | Undetermined | github.com/nats-io/nats-streaming-server:v0.21.0 | github.com/nats-io/nats-streaming-server v0.21.0 | [0.24.1] | - |
-| 
Medium | Applicable | component-D:v0.21.0 | component-D v0.21.0 | [0.24.3] | CVE-2022-26652
CVE-2023-4321 |
-| 
Low | Undetermined | github.com/mholt/archiver/v3:v3.5.1 | github.com/mholt/archiver/v3 v3.5.1 | - | - |
+| 
Critical | CVE-1111-11111 | Not Applicable | dep1:1.0.0
dep2:2.0.0 | impacted 3.0.0 | 4.0.0
5.0.0 |
+| 
High | XRAY-122345 | Undetermined | github.com/nats-io/nats-streaming-server:v0.21.0 | github.com/nats-io/nats-streaming-server v0.21.0 | [0.24.1] |
+| 
Medium | CVE-2022-26652
CVE-2023-4321 | Applicable | component-D:v0.21.0 | component-D v0.21.0 | [0.24.3] |
+| 
Low | - | Undetermined | github.com/mholt/archiver/v3:v3.5.1 | github.com/mholt/archiver/v3 v3.5.1 | - |
-### 🔬 Research Details
+### 🔖 Details
+
-
- [ XRAY-122345 ] github.com/nats-io/nats-streaming-server v0.21.0
-
+[ XRAY-122345 ] github.com/nats-io/nats-streaming-server v0.21.0
+### Vulnerability Details
+| | |
+| --------------------- | :-----------------------------------: |
+| **Contextual Analysis:** | Undetermined |
+| **Direct Dependencies:** | github.com/nats-io/nats-streaming-server:v0.21.0 |
+| **Impacted Dependency:** | github.com/nats-io/nats-streaming-server:v0.21.0 |
+| **Fixed Versions:** | [0.24.1] |
+| **CVSS V3:** | - |
-**Description:**
Summary XRAY-122345
+### 🔬 JFrog Research Details
+
**Remediation:**
some remediation
+
-
+
[ CVE-2022-26652, CVE-2023-4321 ] component-D v0.21.0
-
- [ CVE-2022-26652, CVE-2023-4321 ] component-D v0.21.0
-
+### Vulnerability Details
+| | |
+| --------------------- | :-----------------------------------: |
+| **Contextual Analysis:** | Applicable |
+| **Direct Dependencies:** | component-D:v0.21.0 |
+| **Impacted Dependency:** | component-D:v0.21.0 |
+| **Fixed Versions:** | [0.24.3] |
+| **CVSS V3:** | - |
+### 🔬 JFrog Research Details
**Remediation:**
some remediation
+
-
-
-
- github.com/mholt/archiver/v3 v3.5.1
-
-
+github.com/mholt/archiver/v3 v3.5.1
-**Description:**
-Summary
+### Vulnerability Details
+| | |
+| --------------------- | :-----------------------------------: |
+| **Contextual Analysis:** | Undetermined |
+| **Direct Dependencies:** | github.com/mholt/archiver/v3:v3.5.1 |
+| **Impacted Dependency:** | github.com/mholt/archiver/v3:v3.5.1 |
+| **Fixed Versions:** | - |
+| **CVSS V3:** | - |
-
+Summary
\ No newline at end of file
diff --git a/testdata/messages/summarycomment/vulnerabilities/vulnerabilities_standard_split1.md b/testdata/messages/summarycomment/vulnerabilities/vulnerabilities_standard_split1.md
index bbe11a316..11e307420 100644
--- a/testdata/messages/summarycomment/vulnerabilities/vulnerabilities_standard_split1.md
+++ b/testdata/messages/summarycomment/vulnerabilities/vulnerabilities_standard_split1.md
@@ -1,14 +1,14 @@
-## 📦 Vulnerable Dependencies
-### ✍️ Summary
+### 📦 Vulnerable Dependencies
+
-| SEVERITY | CONTEXTUAL ANALYSIS | DIRECT DEPENDENCIES | IMPACTED DEPENDENCY | FIXED VERSIONS | CVES |
+| Severity | ID | Contextual Analysis | Direct Dependencies | Impacted Dependency | Fixed Versions |
| :---------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: |
-| 
Critical | Not Applicable | dep1:1.0.0
dep2:2.0.0 | impacted 3.0.0 | 4.0.0
5.0.0 | CVE-1111-11111 |
-| 
High | Undetermined | github.com/nats-io/nats-streaming-server:v0.21.0 | github.com/nats-io/nats-streaming-server v0.21.0 | [0.24.1] | - |
-| 
Medium | Applicable | component-D:v0.21.0 | component-D v0.21.0 | [0.24.3] | CVE-2022-26652
CVE-2023-4321 |
-| 
Low | Undetermined | github.com/mholt/archiver/v3:v3.5.1 | github.com/mholt/archiver/v3 v3.5.1 | - | - |
+| 
Critical | CVE-1111-11111 | Not Applicable | dep1:1.0.0
dep2:2.0.0 | impacted 3.0.0 | 4.0.0
5.0.0 |
+| 
High | XRAY-122345 | Undetermined | github.com/nats-io/nats-streaming-server:v0.21.0 | github.com/nats-io/nats-streaming-server v0.21.0 | [0.24.1] |
+| 
Medium | CVE-2022-26652
CVE-2023-4321 | Applicable | component-D:v0.21.0 | component-D v0.21.0 | [0.24.3] |
+| 
Low | - | Undetermined | github.com/mholt/archiver/v3:v3.5.1 | github.com/mholt/archiver/v3 v3.5.1 | - |
diff --git a/testdata/messages/summarycomment/vulnerabilities/vulnerabilities_standard_split2.md b/testdata/messages/summarycomment/vulnerabilities/vulnerabilities_standard_split2.md
index c9114db28..40be1e952 100644
--- a/testdata/messages/summarycomment/vulnerabilities/vulnerabilities_standard_split2.md
+++ b/testdata/messages/summarycomment/vulnerabilities/vulnerabilities_standard_split2.md
@@ -1,36 +1,56 @@
-### 🔬 Research Details
+### 📦 Vulnerable Dependencies
-
- [ XRAY-122345 ] github.com/nats-io/nats-streaming-server v0.21.0
-
+### 🔖 Details
+
+
+[ XRAY-122345 ] github.com/nats-io/nats-streaming-server v0.21.0
+
+### Vulnerability Details
+| | |
+| --------------------- | :-----------------------------------: |
+| **Contextual Analysis:** | Undetermined |
+| **Direct Dependencies:** | github.com/nats-io/nats-streaming-server:v0.21.0 |
+| **Impacted Dependency:** | github.com/nats-io/nats-streaming-server:v0.21.0 |
+| **Fixed Versions:** | [0.24.1] |
+| **CVSS V3:** | - |
-**Description:**
Summary XRAY-122345
+### 🔬 JFrog Research Details
+
**Remediation:**
some remediation
+
-
+
[ CVE-2022-26652, CVE-2023-4321 ] component-D v0.21.0
-
- [ CVE-2022-26652, CVE-2023-4321 ] component-D v0.21.0
-
+### Vulnerability Details
+| | |
+| --------------------- | :-----------------------------------: |
+| **Contextual Analysis:** | Applicable |
+| **Direct Dependencies:** | component-D:v0.21.0 |
+| **Impacted Dependency:** | component-D:v0.21.0 |
+| **Fixed Versions:** | [0.24.3] |
+| **CVSS V3:** | - |
+### 🔬 JFrog Research Details
**Remediation:**
some remediation
+
-
-
-
- github.com/mholt/archiver/v3 v3.5.1
-
-
+github.com/mholt/archiver/v3 v3.5.1
-**Description:**
-Summary
+### Vulnerability Details
+| | |
+| --------------------- | :-----------------------------------: |
+| **Contextual Analysis:** | Undetermined |
+| **Direct Dependencies:** | github.com/mholt/archiver/v3:v3.5.1 |
+| **Impacted Dependency:** | github.com/mholt/archiver/v3:v3.5.1 |
+| **Fixed Versions:** | - |
+| **CVSS V3:** | - |
-
+Summary
\ No newline at end of file
diff --git a/testdata/projects/poetry/pyproject.toml b/testdata/projects/poetry/pyproject.toml
index c878acfc1..cc42d0070 100755
--- a/testdata/projects/poetry/pyproject.toml
+++ b/testdata/projects/poetry/pyproject.toml
@@ -3,6 +3,7 @@ name = "poetry-project"
version = "0.1.0"
description = ""
authors = ["Your Name
"]
+package-mode = false
[tool.poetry.dependencies]
python = "^3.10"
diff --git a/testdata/scanpullrequest/expected_response.md b/testdata/scanpullrequest/expected_response.md
index 9a9649763..836075a91 100644
--- a/testdata/scanpullrequest/expected_response.md
+++ b/testdata/scanpullrequest/expected_response.md
@@ -9,21 +9,47 @@
-## 📦 Vulnerable Dependencies
-### ✍️ Summary
+## 📗 Scan Summary
+- Frogbot scanned for vulnerabilities and found 1 issues
+
+| Scan Category | Status | Security Issues |
+| --------------------- | :-----------------------------------: | ----------------------------------- |
+| **Software Composition Analysis** | ✅ Done |
1 Issues Found
1 Critical
|
+| **Contextual Analysis** | ✅ Done | - |
+| **Static Application Security Testing (SAST)** | ✅ Done | Not Found |
+| **Secrets** | ✅ Done | - |
+| **Infrastructure as Code (IaC)** | ✅ Done | Not Found |
+
+### 📦 Vulnerable Dependencies
+
-| SEVERITY | CONTEXTUAL ANALYSIS | DIRECT DEPENDENCIES | IMPACTED DEPENDENCY | FIXED VERSIONS | CVES |
+| Severity | ID | Contextual Analysis | Direct Dependencies | Impacted Dependency | Fixed Versions |
| :---------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: |
-| 
Critical | Not Applicable | minimist:1.2.5 | minimist 1.2.5 | [0.2.4]
[1.2.6] | CVE-2021-44906 |
+| 
Critical | CVE-2021-44906 | Not Applicable | minimist:1.2.5 | minimist 1.2.5 | [0.2.4]
[1.2.6] |
-### 🔬 Research Details
+### 🔖 Details
+
+### Vulnerability Details
+| | |
+| --------------------- | :-----------------------------------: |
+| **Jfrog Research Severity:** |

High |
+| **Contextual Analysis:** | Not Applicable |
+| **Direct Dependencies:** | minimist:1.2.5 |
+| **Impacted Dependency:** | minimist:1.2.5 |
+| **Fixed Versions:** | [0.2.4], [1.2.6] |
+| **CVSS V3:** | 9.8 |
+
+Insufficient input validation in Minimist npm package leads to prototype pollution of constructor functions when parsing arbitrary arguments.
+
+### 🔬 JFrog Research Details
+
**Description:**
[Minimist](https://github.com/substack/minimist) is a simple and very popular argument parser. It is used by more than 14 million by Mar 2022. This package developers stopped developing it since April 2020 and its community released a [newer version](https://github.com/meszaros-lajos-gyorgy/minimist-lite) supported by the community.
@@ -44,6 +70,7 @@ This vulnerability can be triggered when the attacker-controlled input is parsed
Add the `Object.freeze(Object.prototype);` directive once at the beginning of your main JS source code file (ex. `index.js`), preferably after all your `require` directives. This will prevent any changes to the prototype object, thus completely negating prototype pollution attacks.
+
---
diff --git a/testdata/scanpullrequest/expected_response_multi_dir.md b/testdata/scanpullrequest/expected_response_multi_dir.md
index ae7d4a745..e83ab7d5d 100644
--- a/testdata/scanpullrequest/expected_response_multi_dir.md
+++ b/testdata/scanpullrequest/expected_response_multi_dir.md
@@ -9,35 +9,61 @@
-## 📦 Vulnerable Dependencies
-### ✍️ Summary
+## 📗 Scan Summary
+- Frogbot scanned for vulnerabilities and found 2 issues
+
+| Scan Category | Status | Security Issues |
+| --------------------- | :-----------------------------------: | ----------------------------------- |
+| **Software Composition Analysis** | ✅ Done |
2 Issues Found
2 High
|
+| **Contextual Analysis** | ✅ Done | - |
+| **Static Application Security Testing (SAST)** | ✅ Done | Not Found |
+| **Secrets** | ✅ Done | - |
+| **Infrastructure as Code (IaC)** | ✅ Done | Not Found |
+
+### 📦 Vulnerable Dependencies
+
-| SEVERITY | CONTEXTUAL ANALYSIS | DIRECT DEPENDENCIES | IMPACTED DEPENDENCY | FIXED VERSIONS | CVES |
+| Severity | ID | Contextual Analysis | Direct Dependencies | Impacted Dependency | Fixed Versions |
| :---------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: |
-| 
High | Not Applicable | minimatch:3.0.4 | minimatch 3.0.4 | [3.0.5] | CVE-2022-3517 |
-| 
High | Not Covered | pyjwt:1.7.1 | pyjwt 1.7.1 | [2.4.0] | CVE-2022-29217 |
+| 
High | CVE-2022-3517 | Not Applicable | minimatch:3.0.4 | minimatch 3.0.4 | [3.0.5] |
+| 
High | CVE-2022-29217 | Not Covered | pyjwt:1.7.1 | pyjwt 1.7.1 | [2.4.0] |
-### 🔬 Research Details
+### 🔖 Details
-
- [ CVE-2022-3517 ] minimatch 3.0.4
-
+[ CVE-2022-3517 ] minimatch 3.0.4
-**Description:**
-A vulnerability was found in the minimatch package. This flaw allows a Regular Expression Denial of Service (ReDoS) when calling the braceExpand function with specific arguments, resulting in a Denial of Service.
+### Vulnerability Details
+| | |
+| --------------------- | :-----------------------------------: |
+| **Contextual Analysis:** | Not Applicable |
+| **Direct Dependencies:** | minimatch:3.0.4 |
+| **Impacted Dependency:** | minimatch:3.0.4 |
+| **Fixed Versions:** | [3.0.5] |
+| **CVSS V3:** | 7.5 |
-
+A vulnerability was found in the minimatch package. This flaw allows a Regular Expression Denial of Service (ReDoS) when calling the braceExpand function with specific arguments, resulting in a Denial of Service.
-
- [ CVE-2022-29217 ] pyjwt 1.7.1
-
+[ CVE-2022-29217 ] pyjwt 1.7.1
+### Vulnerability Details
+| | |
+| --------------------- | :-----------------------------------: |
+| **Jfrog Research Severity:** |
Medium |
+| **Contextual Analysis:** | Not Covered |
+| **Direct Dependencies:** | pyjwt:1.7.1 |
+| **Impacted Dependency:** | pyjwt:1.7.1 |
+| **Fixed Versions:** | [2.4.0] |
+| **CVSS V3:** | 7.5 |
+
+Algorithm confusion in PyJWT leads to authentication bypass.
+
+### 🔬 JFrog Research Details
**Description:**
[PyJWT](https://pypi.org/project/PyJWT) is a Python implementation of the RFC 7519 standard (JSON Web Tokens). [JSON Web Tokens](https://jwt.io/) are an open, industry standard method for representing claims securely between two parties. A JWT comes with an inline signature that is meant to be verified by the receiving application. JWT supports multiple standard algorithms, and the algorithm itself is **specified in the JWT token itself**.
@@ -76,9 +102,7 @@ For example, replace the following call -
`jwt.decode(encoded_jwt, pub_key_bytes, algorithms=jwt.algorithms.get_default_algorithms())`
With -
`jwt.decode(encoded_jwt, pub_key_bytes, algorithms=["ES256"])`
-
-
-
+
---
diff --git a/utils/analytics.go b/utils/analytics.go
index aeaf31e4e..a36274b7a 100644
--- a/utils/analytics.go
+++ b/utils/analytics.go
@@ -6,11 +6,10 @@ import (
"github.com/jfrog/jfrog-cli-core/v2/utils/config"
"github.com/jfrog/jfrog-cli-security/utils/xsc"
- "github.com/jfrog/jfrog-client-go/xray/services"
xscservices "github.com/jfrog/jfrog-client-go/xsc/services"
)
-func CreateScanEvent(serviceDetails *config.ServerDetails, gitInfo *services.XscGitInfoContext, scanType string) *xscservices.XscAnalyticsGeneralEvent {
+func CreateScanEvent(serviceDetails *config.ServerDetails, gitInfo *xscservices.XscGitInfoContext, scanType string) *xscservices.XscAnalyticsGeneralEvent {
event := xsc.CreateAnalyticsEvent(xscservices.FrogbotProduct, xscservices.FrogbotType, serviceDetails)
event.ProductVersion = FrogbotVersion
event.FrogbotScanType = scanType
diff --git a/utils/analytics_test.go b/utils/analytics_test.go
index 5fc5d48eb..a722ad9dd 100644
--- a/utils/analytics_test.go
+++ b/utils/analytics_test.go
@@ -4,23 +4,22 @@ import (
"testing"
"github.com/jfrog/jfrog-cli-core/v2/utils/config"
- "github.com/jfrog/jfrog-client-go/xray/services"
xscservices "github.com/jfrog/jfrog-client-go/xsc/services"
"github.com/stretchr/testify/assert"
)
func TestCreateAnalyticsGeneralEvent(t *testing.T) {
- gitInfoContext := &services.XscGitInfoContext{
- GitRepoUrl: "http://localhost:8080/my-user/my-project.git",
- GitRepoName: "my-project",
- GitProject: "my-user",
- GitProvider: "GitHub",
- Technologies: nil,
- BranchName: "main",
- LastCommit: "https://api.github.com/repos/my-user/my-project/commits/a23ba44a0d379dida668nmb72003a82e4e11d0ba",
- CommitHash: "a23ba44a0d379dida668nmb72003a82e4e11d0ba",
- CommitMessage: ".",
- CommitAuthor: "User",
+ gitInfoContext := &xscservices.XscGitInfoContext{
+ GitRepoHttpsCloneUrl: "http://localhost:8080/my-user/my-project.git",
+ GitRepoName: "my-project",
+ GitProject: "my-user",
+ GitProvider: "GitHub",
+ Technologies: nil,
+ BranchName: "main",
+ LastCommitUrl: "https://api.github.com/repos/my-user/my-project/commits/a23ba44a0d379dida668nmb72003a82e4e11d0ba",
+ LastCommitHash: "a23ba44a0d379dida668nmb72003a82e4e11d0ba",
+ LastCommitMessage: ".",
+ LastCommitAuthor: "User",
}
serverDetails := &config.ServerDetails{
diff --git a/utils/comment.go b/utils/comment.go
index f353a17bd..6c1563a11 100644
--- a/utils/comment.go
+++ b/utils/comment.go
@@ -7,9 +7,11 @@ import (
"sort"
"strings"
+ "github.com/jfrog/frogbot/v2/utils/issues"
"github.com/jfrog/frogbot/v2/utils/outputwriter"
"github.com/jfrog/froggit-go/vcsclient"
"github.com/jfrog/jfrog-cli-security/utils/formats"
+ "github.com/jfrog/jfrog-cli-security/utils/results"
"github.com/jfrog/jfrog-client-go/utils/log"
)
@@ -31,7 +33,8 @@ const (
commentRemovalErrorMsg = "An error occurred while attempting to remove older Frogbot pull request comments:"
)
-func HandlePullRequestCommentsAfterScan(issues *IssuesCollection, repo *Repository, client vcsclient.VcsClient, pullRequestID int) (err error) {
+// In Scan PR, if there are no issues, comments will be added to the PR with a message that there are no issues.
+func HandlePullRequestCommentsAfterScan(issues *issues.ScansIssuesCollection, resultContext results.ResultContext, repo *Repository, client vcsclient.VcsClient, pullRequestID int) (err error) {
if !repo.Params.AvoidPreviousPrCommentsDeletion {
// The removal of comments may fail for various reasons,
// such as concurrent scanning of pull requests and attempts
@@ -45,8 +48,8 @@ func HandlePullRequestCommentsAfterScan(issues *IssuesCollection, repo *Reposito
}
// Add summary (SCA, license) scan comment
- if issues.IssuesExists() || repo.AddPrCommentOnSuccess {
- for _, comment := range generatePullRequestSummaryComment(issues, repo.OutputWriter) {
+ if issues.IssuesExists(repo.PullRequestSecretComments) || repo.AddPrCommentOnSuccess {
+ for _, comment := range generatePullRequestSummaryComment(*issues, resultContext, repo.PullRequestSecretComments, repo.OutputWriter) {
if err = client.AddPullRequestComment(context.Background(), repo.RepoOwner, repo.RepoName, comment, pullRequestID); err != nil {
err = errors.New("couldn't add pull request comment: " + err.Error())
return
@@ -78,7 +81,7 @@ func DeleteExistingPullRequestComments(repository *Repository, client vcsclient.
"failed to get comments. the following details were used in order to fetch the comments: <%s/%s> pull request #%d. the error received: %s",
repository.RepoOwner, repository.RepoName, int(repository.PullRequestDetails.ID), err.Error())
}
- commentsToDelete := getFrogbotComments(repository.OutputWriter, comments)
+ commentsToDelete := getFrogbotComments(comments)
// Delete
if len(commentsToDelete) > 0 {
for _, commentToDelete := range commentsToDelete {
@@ -91,7 +94,7 @@ func DeleteExistingPullRequestComments(repository *Repository, client vcsclient.
}
func GenerateFixPullRequestDetails(vulnerabilities []formats.VulnerabilityOrViolationRow, writer outputwriter.OutputWriter) (description string, extraComments []string) {
- content := outputwriter.GetPRSummaryContent(outputwriter.VulnerabilitiesContent(vulnerabilities, writer), true, false, writer)
+ content := outputwriter.GetMainCommentContent(outputwriter.GetVulnerabilitiesContent(vulnerabilities, writer), true, false, writer)
if len(content) == 1 {
// Limit is not reached, use the entire content as the description
description = content[0]
@@ -108,19 +111,22 @@ func GenerateFixPullRequestDetails(vulnerabilities []formats.VulnerabilityOrViol
return
}
-func generatePullRequestSummaryComment(issuesCollection *IssuesCollection, writer outputwriter.OutputWriter) []string {
- if !issuesCollection.IssuesExists() {
- return outputwriter.GetPRSummaryContent([]string{}, false, true, writer)
+func generatePullRequestSummaryComment(issuesCollection issues.ScansIssuesCollection, resultContext results.ResultContext, includeSecrets bool, writer outputwriter.OutputWriter) []string {
+ if !issuesCollection.IssuesExists(includeSecrets) {
+ // No Issues
+ return outputwriter.GetMainCommentContent([]string{}, false, true, writer)
}
-
- content := []string{}
- if vulnerabilitiesContent := outputwriter.VulnerabilitiesContent(issuesCollection.Vulnerabilities, writer); len(vulnerabilitiesContent) > 0 {
- content = append(content, vulnerabilitiesContent...)
+ // Summary
+ content := []string{outputwriter.ScanSummaryContent(issuesCollection, resultContext, includeSecrets, writer)}
+ // Violations
+ if violationsContent := outputwriter.PolicyViolationsContent(issuesCollection, writer); len(violationsContent) > 0 {
+ content = append(content, violationsContent...)
}
- if licensesContent := outputwriter.LicensesContent(issuesCollection.Licenses, writer); len(licensesContent) > 0 {
- content = append(content, licensesContent)
+ // Vulnerabilities
+ if vulnerabilitiesContent := outputwriter.GetVulnerabilitiesContent(issuesCollection.ScaVulnerabilities, writer); len(vulnerabilitiesContent) > 0 {
+ content = append(content, vulnerabilitiesContent...)
}
- return outputwriter.GetPRSummaryContent(content, true, true, writer)
+ return outputwriter.GetMainCommentContent(content, true, true, writer)
}
func IsFrogbotRescanComment(comment string) bool {
@@ -139,7 +145,7 @@ func GetSortedPullRequestComments(client vcsclient.VcsClient, repoOwner, repoNam
return pullRequestsComments, nil
}
-func addReviewComments(repo *Repository, pullRequestID int, client vcsclient.VcsClient, issues *IssuesCollection) (err error) {
+func addReviewComments(repo *Repository, pullRequestID int, client vcsclient.VcsClient, issues *issues.ScansIssuesCollection) (err error) {
commentsToAdd := getNewReviewComments(repo, issues)
if len(commentsToAdd) == 0 {
return
@@ -149,7 +155,7 @@ func addReviewComments(repo *Repository, pullRequestID int, client vcsclient.Vcs
log.Debug("creating a review comment for", comment.Type, comment.Location.File, comment.Location.StartLine, comment.Location.StartColumn)
if e := client.AddPullRequestReviewComments(context.Background(), repo.RepoOwner, repo.RepoName, pullRequestID, comment.CommentInfo); e != nil {
log.Debug("couldn't add pull request review comment, fallback to regular comment: " + e.Error())
- if err = client.AddPullRequestComment(context.Background(), repo.RepoOwner, repo.RepoName, outputwriter.GetFallbackReviewCommentContent(comment.CommentInfo.Content, comment.Location, repo.OutputWriter), pullRequestID); err != nil {
+ if err = client.AddPullRequestComment(context.Background(), repo.RepoOwner, repo.RepoName, outputwriter.GetFallbackReviewCommentContent(comment.CommentInfo.Content, comment.Location), pullRequestID); err != nil {
err = errors.New("couldn't add pull request comment, fallback to comment: " + err.Error())
return
}
@@ -168,7 +174,7 @@ func DeleteExistingPullRequestReviewComments(repo *Repository, pullRequestID int
}
// Delete old review comments
if len(existingComments) > 0 {
- if err = client.DeletePullRequestReviewComments(context.Background(), repo.RepoOwner, repo.RepoName, pullRequestID, getFrogbotComments(repo.OutputWriter, existingComments)...); err != nil {
+ if err = client.DeletePullRequestReviewComments(context.Background(), repo.RepoOwner, repo.RepoName, pullRequestID, getFrogbotComments(existingComments)...); err != nil {
err = errors.New("couldn't delete pull request review comment: " + err.Error())
return
}
@@ -176,9 +182,9 @@ func DeleteExistingPullRequestReviewComments(repo *Repository, pullRequestID int
return
}
-func getFrogbotComments(writer outputwriter.OutputWriter, existingComments []vcsclient.CommentInfo) (reviewComments []vcsclient.CommentInfo) {
+func getFrogbotComments(existingComments []vcsclient.CommentInfo) (reviewComments []vcsclient.CommentInfo) {
for _, comment := range existingComments {
- if outputwriter.IsFrogbotComment(comment.Content) || outputwriter.IsFrogbotSummaryComment(writer, comment.Content) {
+ if outputwriter.IsFrogbotComment(comment.Content) {
log.Debug("Deleting comment id:", comment.ID)
reviewComments = append(reviewComments, comment)
}
@@ -186,33 +192,76 @@ func getFrogbotComments(writer outputwriter.OutputWriter, existingComments []vcs
return
}
-func getNewReviewComments(repo *Repository, issues *IssuesCollection) (commentsToAdd []ReviewComment) {
+func getNewReviewComments(repo *Repository, issues *issues.ScansIssuesCollection) (commentsToAdd []ReviewComment) {
writer := repo.OutputWriter
-
- for _, vulnerability := range issues.Vulnerabilities {
- for _, cve := range vulnerability.Cves {
- if cve.Applicability != nil {
- for _, evidence := range cve.Applicability.Evidence {
- commentsToAdd = append(commentsToAdd, generateReviewComment(ApplicableComment, evidence.Location, generateApplicabilityReviewContent(evidence, cve, vulnerability, writer)))
- }
- }
- }
+ // CVE Applicable Evidence review comments
+ for _, applicableEvidences := range issues.GetApplicableEvidences() {
+ commentsToAdd = append(commentsToAdd, generateReviewComment(ApplicableComment, applicableEvidences.Evidence.Location, generateApplicabilityReviewContent(applicableEvidences, writer)))
}
- for _, iac := range issues.Iacs {
- commentsToAdd = append(commentsToAdd, generateReviewComment(IacComment, iac.Location, generateSourceCodeReviewContent(IacComment, iac, writer)))
+ // IAC review comments
+ for _, iac := range issues.IacVulnerabilities {
+ commentsToAdd = append(commentsToAdd, generateReviewComment(IacComment, iac.Location, generateSourceCodeReviewContent(IacComment, false, writer, iac)))
}
- for _, sast := range issues.Sast {
- commentsToAdd = append(commentsToAdd, generateReviewComment(SastComment, sast.Location, generateSourceCodeReviewContent(SastComment, sast, writer)))
+ for _, similarIacIssues := range groupSimilarJasIssues(issues.IacViolations) {
+ commentsToAdd = append(commentsToAdd, generateReviewComment(IacComment, similarIacIssues.Location, generateSourceCodeReviewContent(IacComment, true, writer, similarIacIssues.issues...)))
}
+ // SAST review comments
+ for _, sast := range issues.SastVulnerabilities {
+ commentsToAdd = append(commentsToAdd, generateReviewComment(SastComment, sast.Location, generateSourceCodeReviewContent(SastComment, false, writer, sast)))
+ }
+ if len(issues.SastViolations) > 0 {
+ for _, similarSastIssues := range groupSimilarJasIssues(issues.SastViolations) {
+ commentsToAdd = append(commentsToAdd, generateReviewComment(SastComment, similarSastIssues.Location, generateSourceCodeReviewContent(SastComment, true, writer, similarSastIssues.issues...)))
+ }
+ }
+ // Secrets review comments
if !repo.Params.PullRequestSecretComments {
return
}
- for _, secret := range issues.Secrets {
- commentsToAdd = append(commentsToAdd, generateReviewComment(SecretComment, secret.Location, generateSourceCodeReviewContent(SecretComment, secret, writer)))
+ for _, secret := range issues.SecretsVulnerabilities {
+ commentsToAdd = append(commentsToAdd, generateReviewComment(SecretComment, secret.Location, generateSourceCodeReviewContent(SecretComment, false, writer, secret)))
+ }
+ if len(issues.SecretsViolations) > 0 {
+ for _, similarSecretsIssues := range groupSimilarJasIssues(issues.SecretsViolations) {
+ commentsToAdd = append(commentsToAdd, generateReviewComment(SecretComment, similarSecretsIssues.Location, generateSourceCodeReviewContent(SecretComment, true, writer, similarSecretsIssues.issues...)))
+ }
+ }
+ return
+}
+
+type jasCommentIssues struct {
+ // The location of the issue that the comment will be added to.
+ formats.Location
+ // Similar issues at the same location that will be shown in the same comment.
+ issues []formats.SourceCodeRow
+}
+
+// For JAS violations we can have similar issues at the same location, we need to group similar issues to add them to the same comment based on `getSourceCodeRowId`.
+func groupSimilarJasIssues(issues []formats.SourceCodeRow) (groupedIssues []jasCommentIssues) {
+ idToIssues := make(map[string]jasCommentIssues)
+ for _, issue := range issues {
+ id := getSourceCodeRowId(issue)
+ if similarIssue, ok := idToIssues[id]; ok {
+ similarIssue.issues = append(similarIssue.issues, issue)
+ idToIssues[id] = similarIssue
+ continue
+ }
+ idToIssues[id] = jasCommentIssues{
+ Location: issue.Location,
+ issues: []formats.SourceCodeRow{issue},
+ }
+ }
+ for _, similarIssue := range idToIssues {
+ groupedIssues = append(groupedIssues, similarIssue)
}
return
}
+// Similar comment should have the same location and rule-id.
+func getSourceCodeRowId(issue formats.SourceCodeRow) string {
+ return issue.RuleId + issue.Location.ToString()
+}
+
func generateReviewComment(commentType ReviewCommentType, location formats.Location, content string) (comment ReviewComment) {
return ReviewComment{
Location: location,
@@ -227,52 +276,18 @@ func generateReviewComment(commentType ReviewCommentType, location formats.Locat
}
-func generateApplicabilityReviewContent(issue formats.Evidence, relatedCve formats.CveRow, relatedVulnerability formats.VulnerabilityOrViolationRow, writer outputwriter.OutputWriter) string {
- remediation := ""
- if relatedVulnerability.JfrogResearchInformation != nil {
- remediation = relatedVulnerability.JfrogResearchInformation.Remediation
- }
- return outputwriter.GenerateReviewCommentContent(outputwriter.ApplicableCveReviewContent(
- relatedVulnerability.Severity,
- issue.Reason,
- relatedCve.Applicability.ScannerDescription,
- relatedCve.Id,
- relatedVulnerability.Summary,
- fmt.Sprintf("%s:%s", relatedVulnerability.ImpactedDependencyName, relatedVulnerability.ImpactedDependencyVersion),
- remediation,
- writer,
- ), writer)
+func generateApplicabilityReviewContent(issue issues.ApplicableEvidences, writer outputwriter.OutputWriter) string {
+ return outputwriter.GenerateReviewCommentContent(outputwriter.ApplicableCveReviewContent(issue, writer), writer)
}
-func generateSourceCodeReviewContent(commentType ReviewCommentType, issue formats.SourceCodeRow, writer outputwriter.OutputWriter) (content string) {
+func generateSourceCodeReviewContent(commentType ReviewCommentType, violation bool, writer outputwriter.OutputWriter, similarIssues ...formats.SourceCodeRow) (content string) {
switch commentType {
case IacComment:
- return outputwriter.GenerateReviewCommentContent(outputwriter.IacReviewContent(
- issue.Severity,
- issue.Finding,
- issue.ScannerDescription,
- writer,
- ), writer)
+ return outputwriter.GenerateReviewCommentContent(outputwriter.IacReviewContent(violation, writer, similarIssues...), writer)
case SastComment:
- return outputwriter.GenerateReviewCommentContent(outputwriter.SastReviewContent(
- issue.Severity,
- issue.Finding,
- issue.ScannerDescription,
- issue.CodeFlow,
- writer,
- ), writer)
+ return outputwriter.GenerateReviewCommentContent(outputwriter.SastReviewContent(violation, writer, similarIssues...), writer)
case SecretComment:
- applicability := ""
- if issue.Applicability != nil {
- applicability = issue.Applicability.Status
- }
- return outputwriter.GenerateReviewCommentContent(outputwriter.SecretReviewContent(
- issue.Severity,
- issue.Finding,
- issue.ScannerDescription,
- applicability,
- writer,
- ), writer)
+ return outputwriter.GenerateReviewCommentContent(outputwriter.SecretReviewContent(violation, writer, similarIssues...), writer)
}
return
}
diff --git a/utils/comment_test.go b/utils/comment_test.go
index 30591b7be..9c7fb29f7 100644
--- a/utils/comment_test.go
+++ b/utils/comment_test.go
@@ -3,6 +3,7 @@ package utils
import (
"testing"
+ "github.com/jfrog/frogbot/v2/utils/issues"
"github.com/jfrog/frogbot/v2/utils/outputwriter"
"github.com/jfrog/froggit-go/vcsclient"
"github.com/jfrog/jfrog-cli-security/utils/formats"
@@ -11,7 +12,6 @@ import (
)
func TestGetFrogbotReviewComments(t *testing.T) {
- writer := &outputwriter.StandardOutput{}
testCases := []struct {
name string
existingComments []vcsclient.CommentInfo
@@ -43,24 +43,177 @@ func TestGetFrogbotReviewComments(t *testing.T) {
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
- output := getFrogbotComments(writer, tc.existingComments)
+ output := getFrogbotComments(tc.existingComments)
assert.ElementsMatch(t, tc.expectedOutput, output)
})
}
}
+func TestGroupSimilarJasIssues(t *testing.T) {
+ testCases := []struct {
+ name string
+ issues []formats.SourceCodeRow
+ groupedIssues []jasCommentIssues
+ }{
+ {
+ name: "No issues",
+ },
+ {
+ name: "Single issue",
+ issues: []formats.SourceCodeRow{
+ {
+ SeverityDetails: formats.SeverityDetails{Severity: "High"},
+ Location: formats.Location{File: "file1", StartLine: 1, StartColumn: 10, EndLine: 2, EndColumn: 11, Snippet: "snippet"},
+ Finding: "finding1",
+ ScannerInfo: formats.ScannerInfo{RuleId: "rule1"},
+ },
+ },
+ groupedIssues: []jasCommentIssues{
+ {
+ formats.Location{File: "file1", StartLine: 1, StartColumn: 10, EndLine: 2, EndColumn: 11, Snippet: "snippet"},
+ []formats.SourceCodeRow{
+ {
+ SeverityDetails: formats.SeverityDetails{Severity: "High"},
+ Location: formats.Location{File: "file1", StartLine: 1, StartColumn: 10, EndLine: 2, EndColumn: 11, Snippet: "snippet"},
+ Finding: "finding1",
+ ScannerInfo: formats.ScannerInfo{RuleId: "rule1"},
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "Multiple issues - no similar issues",
+ issues: []formats.SourceCodeRow{
+ {
+ SeverityDetails: formats.SeverityDetails{Severity: "High"},
+ Location: formats.Location{File: "file1", StartLine: 1, StartColumn: 10, EndLine: 2, EndColumn: 11, Snippet: "snippet"},
+ Finding: "finding1",
+ ScannerInfo: formats.ScannerInfo{RuleId: "rule1"},
+ },
+ {
+ SeverityDetails: formats.SeverityDetails{Severity: "High"},
+ Location: formats.Location{File: "file1", StartLine: 1, StartColumn: 10, EndLine: 2, EndColumn: 11, Snippet: "snippet"},
+ Finding: "finding1",
+ ScannerInfo: formats.ScannerInfo{RuleId: "rule2"},
+ },
+ },
+ groupedIssues: []jasCommentIssues{
+ {
+ formats.Location{File: "file1", StartLine: 1, StartColumn: 10, EndLine: 2, EndColumn: 11, Snippet: "snippet"},
+ []formats.SourceCodeRow{
+ {
+ SeverityDetails: formats.SeverityDetails{Severity: "High"},
+ Location: formats.Location{File: "file1", StartLine: 1, StartColumn: 10, EndLine: 2, EndColumn: 11, Snippet: "snippet"},
+ Finding: "finding1",
+ ScannerInfo: formats.ScannerInfo{RuleId: "rule1"},
+ },
+ },
+ },
+ {
+ formats.Location{File: "file1", StartLine: 1, StartColumn: 10, EndLine: 2, EndColumn: 11, Snippet: "snippet"},
+ []formats.SourceCodeRow{
+ {
+ SeverityDetails: formats.SeverityDetails{Severity: "High"},
+ Location: formats.Location{File: "file1", StartLine: 1, StartColumn: 10, EndLine: 2, EndColumn: 11, Snippet: "snippet"},
+ Finding: "finding1",
+ ScannerInfo: formats.ScannerInfo{RuleId: "rule2"},
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "Multiple issues - with similar issues",
+ issues: []formats.SourceCodeRow{
+ {
+ SeverityDetails: formats.SeverityDetails{Severity: "High"},
+ Location: formats.Location{File: "file1", StartLine: 1, StartColumn: 10, EndLine: 2, EndColumn: 11, Snippet: "snippet"},
+ Finding: "finding1",
+ ScannerInfo: formats.ScannerInfo{RuleId: "rule1"},
+ },
+ {
+ SeverityDetails: formats.SeverityDetails{Severity: "Medium"},
+ Location: formats.Location{File: "file1", StartLine: 1, StartColumn: 10, EndLine: 2, EndColumn: 11, Snippet: "snippet"},
+ Finding: "finding2",
+ ScannerInfo: formats.ScannerInfo{RuleId: "rule1"},
+ },
+ {
+ SeverityDetails: formats.SeverityDetails{Severity: "Low"},
+ Location: formats.Location{File: "file1", StartLine: 1, StartColumn: 10, EndLine: 2, EndColumn: 11, Snippet: "snippet"},
+ Finding: "finding3",
+ ScannerInfo: formats.ScannerInfo{RuleId: "rule2"},
+ },
+ {
+ SeverityDetails: formats.SeverityDetails{Severity: "Medium"},
+ Location: formats.Location{File: "file2", StartLine: 1, StartColumn: 10, EndLine: 2, EndColumn: 11, Snippet: "snippet"},
+ Finding: "finding2",
+ ScannerInfo: formats.ScannerInfo{RuleId: "rule1"},
+ },
+ },
+ groupedIssues: []jasCommentIssues{
+ {
+ formats.Location{File: "file1", StartLine: 1, StartColumn: 10, EndLine: 2, EndColumn: 11, Snippet: "snippet"},
+ []formats.SourceCodeRow{
+ {
+ SeverityDetails: formats.SeverityDetails{Severity: "High"},
+ Location: formats.Location{File: "file1", StartLine: 1, StartColumn: 10, EndLine: 2, EndColumn: 11, Snippet: "snippet"},
+ Finding: "finding1",
+ ScannerInfo: formats.ScannerInfo{RuleId: "rule1"},
+ },
+ {
+ SeverityDetails: formats.SeverityDetails{Severity: "Medium"},
+ Location: formats.Location{File: "file1", StartLine: 1, StartColumn: 10, EndLine: 2, EndColumn: 11, Snippet: "snippet"},
+ Finding: "finding2",
+ ScannerInfo: formats.ScannerInfo{RuleId: "rule1"},
+ },
+ },
+ },
+ {
+ formats.Location{File: "file1", StartLine: 1, StartColumn: 10, EndLine: 2, EndColumn: 11, Snippet: "snippet"},
+ []formats.SourceCodeRow{
+ {
+ SeverityDetails: formats.SeverityDetails{Severity: "Low"},
+ Location: formats.Location{File: "file1", StartLine: 1, StartColumn: 10, EndLine: 2, EndColumn: 11, Snippet: "snippet"},
+ Finding: "finding3",
+ ScannerInfo: formats.ScannerInfo{RuleId: "rule2"},
+ },
+ },
+ },
+ {
+ formats.Location{File: "file2", StartLine: 1, StartColumn: 10, EndLine: 2, EndColumn: 11, Snippet: "snippet"},
+ []formats.SourceCodeRow{
+ {
+ SeverityDetails: formats.SeverityDetails{Severity: "Medium"},
+ Location: formats.Location{File: "file2", StartLine: 1, StartColumn: 10, EndLine: 2, EndColumn: 11, Snippet: "snippet"},
+ Finding: "finding2",
+ ScannerInfo: formats.ScannerInfo{RuleId: "rule1"},
+ },
+ },
+ },
+ },
+ },
+ }
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ output := groupSimilarJasIssues(tc.issues)
+ assert.ElementsMatch(t, tc.groupedIssues, output)
+ })
+ }
+}
+
func TestGetNewReviewComments(t *testing.T) {
writer := &outputwriter.StandardOutput{}
testCases := []struct {
name string
generateSecretsComments bool
- issues *IssuesCollection
+ issues *issues.ScansIssuesCollection
expectedOutput []ReviewComment
}{
{
name: "No issues for review comments",
- issues: &IssuesCollection{
- Vulnerabilities: []formats.VulnerabilityOrViolationRow{
+ issues: &issues.ScansIssuesCollection{
+ ScaVulnerabilities: []formats.VulnerabilityOrViolationRow{
{
Summary: "summary-2",
Applicable: "Applicable",
@@ -73,13 +226,16 @@ func TestGetNewReviewComments(t *testing.T) {
Technology: techutils.Npm,
},
},
- Secrets: []formats.SourceCodeRow{
+ SecretsVulnerabilities: []formats.SourceCodeRow{
{
SeverityDetails: formats.SeverityDetails{
Severity: "High",
SeverityNumValue: 13,
},
Finding: "Secret",
+ ScannerInfo: formats.ScannerInfo{
+ RuleId: "secret-rule",
+ },
Location: formats.Location{
File: "index.js",
StartLine: 5,
@@ -96,8 +252,8 @@ func TestGetNewReviewComments(t *testing.T) {
{
name: "Secret review comments",
generateSecretsComments: true,
- issues: &IssuesCollection{
- Vulnerabilities: []formats.VulnerabilityOrViolationRow{
+ issues: &issues.ScansIssuesCollection{
+ ScaVulnerabilities: []formats.VulnerabilityOrViolationRow{
{
Summary: "summary-2",
Applicable: "Applicable",
@@ -110,7 +266,7 @@ func TestGetNewReviewComments(t *testing.T) {
Technology: techutils.Npm,
},
},
- Secrets: []formats.SourceCodeRow{
+ SecretsVulnerabilities: []formats.SourceCodeRow{
{
SeverityDetails: formats.SeverityDetails{
Severity: "High",
@@ -118,6 +274,9 @@ func TestGetNewReviewComments(t *testing.T) {
},
Finding: "secret finding",
Applicability: &formats.Applicability{Status: "Inactive"},
+ ScannerInfo: formats.ScannerInfo{
+ RuleId: "secret-rule",
+ },
Location: formats.Location{
File: "index.js",
StartLine: 5,
@@ -142,7 +301,135 @@ func TestGetNewReviewComments(t *testing.T) {
Type: SecretComment,
CommentInfo: vcsclient.PullRequestComment{
CommentInfo: vcsclient.CommentInfo{
- Content: outputwriter.GenerateReviewCommentContent(outputwriter.SecretReviewContent("High", "secret finding", "", "Inactive", writer), writer),
+ Content: outputwriter.GenerateReviewCommentContent(outputwriter.SecretReviewContent(false, writer, formats.SourceCodeRow{
+ SeverityDetails: formats.SeverityDetails{
+ Severity: "High",
+ SeverityNumValue: 13,
+ },
+ ScannerInfo: formats.ScannerInfo{
+ RuleId: "secret-rule",
+ },
+ Finding: "secret finding",
+ Applicability: &formats.Applicability{Status: "Inactive"},
+ }), writer),
+ },
+ PullRequestDiff: vcsclient.PullRequestDiff{
+ OriginalFilePath: "index.js",
+ OriginalStartLine: 5,
+ OriginalStartColumn: 6,
+ OriginalEndLine: 7,
+ OriginalEndColumn: 8,
+ NewFilePath: "index.js",
+ NewStartLine: 5,
+ NewStartColumn: 6,
+ NewEndLine: 7,
+ NewEndColumn: 8,
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "Multiple violations, one review comments",
+ generateSecretsComments: true,
+ issues: &issues.ScansIssuesCollection{
+ ScaVulnerabilities: []formats.VulnerabilityOrViolationRow{
+ {
+ Summary: "summary-2",
+ Applicable: "Applicable",
+ IssueId: "XRAY-2",
+ ImpactedDependencyDetails: formats.ImpactedDependencyDetails{
+ SeverityDetails: formats.SeverityDetails{Severity: "low"},
+ ImpactedDependencyName: "component-C",
+ },
+ Cves: []formats.CveRow{{Id: "CVE-2023-4321"}},
+ Technology: techutils.Npm,
+ },
+ },
+ SecretsViolations: []formats.SourceCodeRow{
+ {
+ SeverityDetails: formats.SeverityDetails{
+ Severity: "High",
+ SeverityNumValue: 13,
+ },
+ ScannerInfo: formats.ScannerInfo{
+ RuleId: "secret-rule",
+ },
+ Finding: "secret finding",
+ Applicability: &formats.Applicability{Status: "Inactive"},
+ Location: formats.Location{
+ File: "index.js",
+ StartLine: 5,
+ StartColumn: 6,
+ EndLine: 7,
+ EndColumn: 8,
+ Snippet: "access token exposed",
+ },
+ ViolationContext: formats.ViolationContext{
+ Watch: "watch",
+ },
+ },
+ {
+ SeverityDetails: formats.SeverityDetails{
+ Severity: "High",
+ SeverityNumValue: 13,
+ },
+ ScannerInfo: formats.ScannerInfo{
+ RuleId: "secret-rule",
+ },
+ Finding: "secret finding",
+ Applicability: &formats.Applicability{Status: "Inactive"},
+ Location: formats.Location{
+ File: "index.js",
+ StartLine: 5,
+ StartColumn: 6,
+ EndLine: 7,
+ EndColumn: 8,
+ Snippet: "access token exposed",
+ },
+ ViolationContext: formats.ViolationContext{
+ Watch: "watch2",
+ },
+ },
+ },
+ },
+ expectedOutput: []ReviewComment{
+ {
+ Location: formats.Location{
+ File: "index.js",
+ StartLine: 5,
+ StartColumn: 6,
+ EndLine: 7,
+ EndColumn: 8,
+ Snippet: "access token exposed",
+ },
+ Type: SecretComment,
+ CommentInfo: vcsclient.PullRequestComment{
+ CommentInfo: vcsclient.CommentInfo{
+ Content: outputwriter.GenerateReviewCommentContent(outputwriter.SecretReviewContent(true, writer,
+ formats.SourceCodeRow{
+ SeverityDetails: formats.SeverityDetails{Severity: "High", SeverityNumValue: 13},
+ ScannerInfo: formats.ScannerInfo{
+ RuleId: "secret-rule",
+ },
+ Finding: "secret finding",
+ Applicability: &formats.Applicability{Status: "Inactive"},
+ ViolationContext: formats.ViolationContext{
+ Watch: "watch",
+ },
+ },
+ formats.SourceCodeRow{
+ SeverityDetails: formats.SeverityDetails{Severity: "High", SeverityNumValue: 13},
+ ScannerInfo: formats.ScannerInfo{
+ RuleId: "secret-rule",
+ },
+ Finding: "secret finding",
+ Applicability: &formats.Applicability{Status: "Inactive"},
+ ViolationContext: formats.ViolationContext{
+ Watch: "watch2",
+ },
+ },
+ ), writer),
},
PullRequestDiff: vcsclient.PullRequestDiff{
OriginalFilePath: "index.js",
@@ -162,8 +449,8 @@ func TestGetNewReviewComments(t *testing.T) {
},
{
name: "With issues for review comments",
- issues: &IssuesCollection{
- Vulnerabilities: []formats.VulnerabilityOrViolationRow{
+ issues: &issues.ScansIssuesCollection{
+ ScaVulnerabilities: []formats.VulnerabilityOrViolationRow{
{
Summary: "summary-2",
Applicable: "Applicable",
@@ -176,12 +463,15 @@ func TestGetNewReviewComments(t *testing.T) {
Technology: techutils.Npm,
},
},
- Iacs: []formats.SourceCodeRow{
+ IacVulnerabilities: []formats.SourceCodeRow{
{
SeverityDetails: formats.SeverityDetails{
Severity: "High",
SeverityNumValue: 13,
},
+ ScannerInfo: formats.ScannerInfo{
+ RuleId: "aws-violation",
+ },
Finding: "Missing auto upgrade was detected",
Location: formats.Location{
File: "file1",
@@ -193,12 +483,15 @@ func TestGetNewReviewComments(t *testing.T) {
},
},
},
- Sast: []formats.SourceCodeRow{
+ SastVulnerabilities: []formats.SourceCodeRow{
{
SeverityDetails: formats.SeverityDetails{
Severity: "High",
SeverityNumValue: 13,
},
+ ScannerInfo: formats.ScannerInfo{
+ RuleId: "sast-rule",
+ },
Finding: "XSS Vulnerability",
Location: formats.Location{
File: "file1",
@@ -224,7 +517,10 @@ func TestGetNewReviewComments(t *testing.T) {
Type: ApplicableComment,
CommentInfo: vcsclient.PullRequestComment{
CommentInfo: vcsclient.CommentInfo{
- Content: outputwriter.GenerateReviewCommentContent(outputwriter.ApplicableCveReviewContent("Low", "", "", "CVE-2023-4321", "summary-2", "component-C:", "", writer), writer),
+ Content: outputwriter.GenerateReviewCommentContent(outputwriter.ApplicableCveReviewContent(issues.ApplicableEvidences{
+ Evidence: formats.Evidence{Location: formats.Location{File: "file1", StartLine: 1, StartColumn: 10, EndLine: 2, EndColumn: 11, Snippet: "snippet"}},
+ Severity: "Low", IssueId: "CVE-2023-4321", CveSummary: "summary-2", ImpactedDependency: "component-C",
+ }, writer), writer),
},
PullRequestDiff: vcsclient.PullRequestDiff{
OriginalFilePath: "file1",
@@ -252,7 +548,16 @@ func TestGetNewReviewComments(t *testing.T) {
Type: IacComment,
CommentInfo: vcsclient.PullRequestComment{
CommentInfo: vcsclient.CommentInfo{
- Content: outputwriter.GenerateReviewCommentContent(outputwriter.IacReviewContent("High", "Missing auto upgrade was detected", "", writer), writer),
+ Content: outputwriter.GenerateReviewCommentContent(outputwriter.IacReviewContent(false, writer, formats.SourceCodeRow{
+ SeverityDetails: formats.SeverityDetails{
+ Severity: "High",
+ SeverityNumValue: 13,
+ },
+ ScannerInfo: formats.ScannerInfo{
+ RuleId: "aws-violation",
+ },
+ Finding: "Missing auto upgrade was detected",
+ }), writer),
},
PullRequestDiff: vcsclient.PullRequestDiff{
OriginalFilePath: "file1",
@@ -280,7 +585,16 @@ func TestGetNewReviewComments(t *testing.T) {
Type: SastComment,
CommentInfo: vcsclient.PullRequestComment{
CommentInfo: vcsclient.CommentInfo{
- Content: outputwriter.GenerateReviewCommentContent(outputwriter.SastReviewContent("High", "XSS Vulnerability", "", [][]formats.Location{}, writer), writer),
+ Content: outputwriter.GenerateReviewCommentContent(outputwriter.SastReviewContent(false, writer, formats.SourceCodeRow{
+ SeverityDetails: formats.SeverityDetails{
+ Severity: "High",
+ SeverityNumValue: 13,
+ },
+ ScannerInfo: formats.ScannerInfo{
+ RuleId: "sast-rule",
+ },
+ Finding: "XSS Vulnerability",
+ }), writer),
},
PullRequestDiff: vcsclient.PullRequestDiff{
OriginalFilePath: "file1",
diff --git a/utils/consts.go b/utils/consts.go
index f51e44b77..97b064e6a 100644
--- a/utils/consts.go
+++ b/utils/consts.go
@@ -52,13 +52,16 @@ const (
PullRequestSecretCommentsEnv = "JF_PR_SHOW_SECRETS_COMMENTS"
// Repository environment variables - Ignored if the frogbot-config.yml file is used
- InstallCommandEnv = "JF_INSTALL_DEPS_CMD"
- MaxPnpmTreeDepthEnv = "JF_PNPM_MAX_TREE_DEPTH"
- RequirementsFileEnv = "JF_REQUIREMENTS_FILE"
- WorkingDirectoryEnv = "JF_WORKING_DIR"
- PathExclusionsEnv = "JF_PATH_EXCLUSIONS"
- jfrogWatchesEnv = "JF_WATCHES"
- jfrogProjectEnv = "JF_PROJECT"
+ InstallCommandEnv = "JF_INSTALL_DEPS_CMD"
+ MaxPnpmTreeDepthEnv = "JF_PNPM_MAX_TREE_DEPTH"
+ RequirementsFileEnv = "JF_REQUIREMENTS_FILE"
+ WorkingDirectoryEnv = "JF_WORKING_DIR"
+ PathExclusionsEnv = "JF_PATH_EXCLUSIONS"
+ jfrogWatchesEnv = "JF_WATCHES"
+ jfrogProjectEnv = "JF_PROJECT"
+ // To include vulnerabilities and violations
+ IncludeVulnerabilitiesEnv = "JF_INCLUDE_VULNERABILITIES"
+ // To include all the vulnerabilities in the source branch at PR scan
IncludeAllVulnerabilitiesEnv = "JF_INCLUDE_ALL_VULNERABILITIES"
AvoidPreviousPrCommentsDeletionEnv = "JF_AVOID_PREVIOUS_PR_COMMENTS_DELETION"
AddPrCommentOnSuccessEnv = "JF_PR_ADD_SUCCESS_COMMENT"
diff --git a/utils/issues/issuescollection.go b/utils/issues/issuescollection.go
new file mode 100644
index 000000000..e611cc9df
--- /dev/null
+++ b/utils/issues/issuescollection.go
@@ -0,0 +1,303 @@
+package issues
+
+import (
+ "github.com/jfrog/jfrog-cli-security/utils"
+ "github.com/jfrog/jfrog-cli-security/utils/formats"
+ "github.com/jfrog/jfrog-cli-security/utils/jasutils"
+ "github.com/jfrog/jfrog-cli-security/utils/results"
+ "github.com/jfrog/jfrog-cli-security/utils/severityutils"
+)
+
+// Group issues by scan type
+type ScansIssuesCollection struct {
+ formats.ScanStatus
+
+ LicensesViolations []formats.LicenseViolationRow
+
+ ScaVulnerabilities []formats.VulnerabilityOrViolationRow
+ ScaViolations []formats.VulnerabilityOrViolationRow
+
+ IacVulnerabilities []formats.SourceCodeRow
+ IacViolations []formats.SourceCodeRow
+
+ SecretsVulnerabilities []formats.SourceCodeRow
+ SecretsViolations []formats.SourceCodeRow
+
+ SastViolations []formats.SourceCodeRow
+ SastVulnerabilities []formats.SourceCodeRow
+}
+
+// General methods
+
+func (ic *ScansIssuesCollection) Append(issues *ScansIssuesCollection) {
+ if issues == nil {
+ return
+ }
+ // Status
+ ic.AppendStatus(issues.ScanStatus)
+ // Sca
+ if len(issues.ScaVulnerabilities) > 0 {
+ ic.ScaVulnerabilities = append(ic.ScaVulnerabilities, issues.ScaVulnerabilities...)
+ }
+ if len(issues.ScaViolations) > 0 {
+ ic.ScaViolations = append(ic.ScaViolations, issues.ScaViolations...)
+ }
+ if len(issues.LicensesViolations) > 0 {
+ ic.LicensesViolations = append(ic.LicensesViolations, issues.LicensesViolations...)
+ }
+ // Secrets
+ if len(issues.SecretsVulnerabilities) > 0 {
+ ic.SecretsVulnerabilities = append(ic.SecretsVulnerabilities, issues.SecretsVulnerabilities...)
+ }
+ if len(issues.SecretsViolations) > 0 {
+ ic.SecretsViolations = append(ic.SecretsViolations, issues.SecretsViolations...)
+ }
+ // Sast
+ if len(issues.SastVulnerabilities) > 0 {
+ ic.SastVulnerabilities = append(ic.SastVulnerabilities, issues.SastVulnerabilities...)
+ }
+ if len(issues.SastViolations) > 0 {
+ ic.SastViolations = append(ic.SastViolations, issues.SastViolations...)
+ }
+ // Iac
+ if len(issues.IacVulnerabilities) > 0 {
+ ic.IacVulnerabilities = append(ic.IacVulnerabilities, issues.IacVulnerabilities...)
+ }
+ if len(issues.IacViolations) > 0 {
+ ic.IacViolations = append(ic.IacViolations, issues.IacViolations...)
+ }
+}
+
+func (ic *ScansIssuesCollection) AppendStatus(scanStatus formats.ScanStatus) {
+ if ic.ScaStatusCode == nil || (*ic.ScaStatusCode == 0 && scanStatus.ScaStatusCode != nil) {
+ ic.ScaStatusCode = scanStatus.ScaStatusCode
+ }
+ if ic.IacStatusCode == nil || (*ic.IacStatusCode == 0 && scanStatus.IacStatusCode != nil) {
+ ic.IacStatusCode = scanStatus.IacStatusCode
+ }
+ if ic.SecretsStatusCode == nil || (*ic.SecretsStatusCode == 0 && scanStatus.SecretsStatusCode != nil) {
+ ic.SecretsStatusCode = scanStatus.SecretsStatusCode
+ }
+ if ic.SastStatusCode == nil || (*ic.SastStatusCode == 0 && scanStatus.SastStatusCode != nil) {
+ ic.SastStatusCode = scanStatus.SastStatusCode
+ }
+ if ic.ApplicabilityStatusCode == nil || (*ic.ApplicabilityStatusCode == 0 && scanStatus.ApplicabilityStatusCode != nil) {
+ ic.ApplicabilityStatusCode = scanStatus.ApplicabilityStatusCode
+ }
+}
+
+func (ic *ScansIssuesCollection) IsScanNotCompleted(scanType utils.SubScanType) bool {
+ status := ic.GetScanStatus(scanType)
+ // Failed or not performed scans
+ return status == nil || *status != 0
+}
+
+func (ic *ScansIssuesCollection) GetScanStatus(scanType utils.SubScanType) *int {
+ switch scanType {
+ case utils.ScaScan:
+ return ic.ScaStatusCode
+ case utils.IacScan:
+ return ic.IacStatusCode
+ case utils.SecretsScan:
+ return ic.SecretsStatusCode
+ case utils.SastScan:
+ return ic.SastStatusCode
+ case utils.ContextualAnalysisScan:
+ return ic.ApplicabilityStatusCode
+ }
+ return nil
+}
+
+// Only if performed and failed
+func (ic *ScansIssuesCollection) HasErrors() bool {
+ if scaStatus := ic.GetScanStatus(utils.ScaScan); scaStatus != nil && *scaStatus != 0 {
+ return true
+ }
+ if applicabilityStatus := ic.GetScanStatus(utils.ContextualAnalysisScan); applicabilityStatus != nil && *applicabilityStatus != 0 {
+ return true
+ }
+ if iacStatus := ic.GetScanStatus(utils.IacScan); iacStatus != nil && *iacStatus != 0 {
+ return true
+ }
+ if secretsStatus := ic.GetScanStatus(utils.SecretsScan); secretsStatus != nil && *secretsStatus != 0 {
+ return true
+ }
+ if sastStatus := ic.GetScanStatus(utils.SastScan); sastStatus != nil && *sastStatus != 0 {
+ return true
+ }
+ return false
+}
+
+func (ic *ScansIssuesCollection) GetScanIssuesSeverityCount(scanType utils.SubScanType, vulnerabilities, isViolation bool) map[severityutils.Severity]int {
+ scanDetails := map[severityutils.Severity]int{}
+ if scanType == utils.ScaScan {
+ // Count Sca issues only if requested
+ if isViolation {
+ for _, violation := range ic.ScaViolations {
+ scanDetails[severityutils.GetSeverity(violation.Severity)]++
+ }
+ for _, violation := range ic.LicensesViolations {
+ scanDetails[severityutils.GetSeverity(violation.Severity)]++
+ }
+ }
+ if vulnerabilities {
+ for _, vulnerability := range ic.ScaVulnerabilities {
+ scanDetails[severityutils.GetSeverity(vulnerability.Severity)]++
+ }
+ }
+ return scanDetails
+ }
+ jasVulnerabilities := []formats.SourceCodeRow{}
+ jasViolations := []formats.SourceCodeRow{}
+ switch scanType {
+ case utils.IacScan:
+ // Count Iac issues only if requested
+ if isViolation {
+ jasViolations = ic.IacViolations
+ }
+ if vulnerabilities {
+ jasVulnerabilities = ic.IacVulnerabilities
+ }
+ case utils.SecretsScan:
+ // Count Secrets issues only if requested
+ if isViolation {
+ jasViolations = ic.SecretsViolations
+ }
+ if vulnerabilities {
+ jasVulnerabilities = ic.SecretsVulnerabilities
+ }
+ case utils.SastScan:
+ // Count Sast issues only if requested
+ if isViolation {
+ jasViolations = ic.SastViolations
+ }
+ if vulnerabilities {
+ jasVulnerabilities = ic.SastVulnerabilities
+ }
+ }
+ // Count the issues
+ for _, issue := range jasVulnerabilities {
+ scanDetails[severityutils.GetSeverity(issue.Severity)]++
+ }
+ for _, issue := range jasViolations {
+ scanDetails[severityutils.GetSeverity(issue.Severity)]++
+ }
+ return scanDetails
+}
+
+func (ic *ScansIssuesCollection) IssuesExists(includeSecrets bool) bool {
+ return ic.ScaIssuesExists() || ic.IacIssuesExists() || ic.SastIssuesExists() || (includeSecrets && ic.SecretsIssuesExists())
+}
+
+func (ic *ScansIssuesCollection) ScaIssuesExists() bool {
+ return len(ic.ScaVulnerabilities) > 0 || len(ic.ScaViolations) > 0 || len(ic.LicensesViolations) > 0
+}
+
+func (ic *ScansIssuesCollection) IacIssuesExists() bool {
+ return len(ic.IacVulnerabilities) > 0 || len(ic.IacViolations) > 0
+}
+
+func (ic *ScansIssuesCollection) SecretsIssuesExists() bool {
+ return len(ic.SecretsVulnerabilities) > 0 || len(ic.SecretsViolations) > 0
+}
+
+func (ic *ScansIssuesCollection) SastIssuesExists() bool {
+ return len(ic.SastVulnerabilities) > 0 || len(ic.SastViolations) > 0
+}
+
+func (ic *ScansIssuesCollection) GetAllIssuesCount(includeSecrets bool) int {
+ return ic.GetTotalVulnerabilities(includeSecrets) + ic.GetTotalViolations(includeSecrets)
+}
+
+type ApplicableEvidences struct {
+ Evidence formats.Evidence
+ Severity string
+ ScannerDescription string
+ IssueId string
+ CveSummary string
+ ImpactedDependency string
+ Remediation string
+}
+
+func toApplicableEvidences(issue formats.VulnerabilityOrViolationRow, cve formats.CveRow, evidence formats.Evidence) ApplicableEvidences {
+ remediation := ""
+ if issue.JfrogResearchInformation != nil {
+ remediation = issue.JfrogResearchInformation.Remediation
+ }
+ return ApplicableEvidences{
+ Evidence: evidence,
+ Severity: issue.Severity,
+ ScannerDescription: cve.Applicability.ScannerDescription,
+ IssueId: results.GetIssueIdentifier(issue.Cves, issue.IssueId, ", "),
+ CveSummary: issue.Summary,
+ ImpactedDependency: results.GetDependencyId(issue.ImpactedDependencyName, issue.ImpactedDependencyVersion),
+ Remediation: remediation,
+ }
+}
+
+func (ic *ScansIssuesCollection) GetApplicableEvidences() (evidences []ApplicableEvidences) {
+ // Collect evidences from Violations
+ idToEvidence := map[string]ApplicableEvidences{}
+ for _, securityViolation := range ic.ScaViolations {
+ for _, cve := range securityViolation.Cves {
+ if cve.Applicability != nil && cve.Applicability.Status == jasutils.Applicable.String() {
+ // We only want applicable issues
+ for _, evidence := range cve.Applicability.Evidence {
+ issueId := results.GetIssueIdentifier(securityViolation.Cves, securityViolation.IssueId, "-")
+ id := issueId + evidence.Location.ToString()
+ if _, exists := idToEvidence[id]; exists {
+ // No need to add the same issue twice
+ continue
+ }
+ idToEvidence[id] = toApplicableEvidences(securityViolation, cve, evidence)
+ }
+ }
+ }
+ }
+ // Collect evidences from Vulnerabilities
+ for _, vulnerability := range ic.ScaVulnerabilities {
+ for _, cve := range vulnerability.Cves {
+ if cve.Applicability != nil && cve.Applicability.Status == jasutils.Applicable.String() {
+ // We only want applicable issues
+ for _, evidence := range cve.Applicability.Evidence {
+ issueId := results.GetIssueIdentifier(vulnerability.Cves, vulnerability.IssueId, "-")
+ id := issueId + evidence.Location.ToString()
+ if _, exists := idToEvidence[id]; exists {
+ // No need to add the same issue twice
+ continue
+ }
+ idToEvidence[id] = toApplicableEvidences(vulnerability, cve, evidence)
+ }
+ }
+ }
+ }
+
+ for _, evidence := range idToEvidence {
+ evidences = append(evidences, evidence)
+ }
+ return
+}
+
+// Violations
+
+func (ic *ScansIssuesCollection) GetTotalViolations(includeSecrets bool) int {
+ total := ic.GetTotalScaViolations() + len(ic.IacViolations) + len(ic.SastViolations)
+ if includeSecrets {
+ total += len(ic.SecretsViolations)
+ }
+ return total
+}
+
+func (ic *ScansIssuesCollection) GetTotalScaViolations() int {
+ return len(ic.ScaViolations) + len(ic.LicensesViolations)
+}
+
+// Vulnerabilities
+
+func (ic *ScansIssuesCollection) GetTotalVulnerabilities(includeSecrets bool) int {
+ total := len(ic.ScaVulnerabilities) + len(ic.IacVulnerabilities) + len(ic.SastVulnerabilities)
+ if includeSecrets {
+ total += len(ic.SecretsVulnerabilities)
+ }
+ return total
+}
diff --git a/utils/issues/issuescollection_test.go b/utils/issues/issuescollection_test.go
new file mode 100644
index 000000000..86cdfe2db
--- /dev/null
+++ b/utils/issues/issuescollection_test.go
@@ -0,0 +1,491 @@
+package issues
+
+import (
+ "testing"
+
+ "github.com/jfrog/jfrog-cli-security/utils"
+ "github.com/jfrog/jfrog-cli-security/utils/formats"
+ "github.com/jfrog/jfrog-cli-security/utils/severityutils"
+ "github.com/stretchr/testify/assert"
+)
+
+func getTestData() ScansIssuesCollection {
+ issuesCollection := ScansIssuesCollection{
+ ScaVulnerabilities: []formats.VulnerabilityOrViolationRow{
+ {
+ ImpactedDependencyDetails: formats.ImpactedDependencyDetails{
+ ImpactedDependencyName: "impacted-name",
+ ImpactedDependencyVersion: "1.0.0",
+ SeverityDetails: formats.SeverityDetails{Severity: "High"},
+ Components: []formats.ComponentRow{
+ {
+ Name: "vuln-pack-name1",
+ Version: "1.0.0",
+ },
+ {
+ Name: "vuln-pack-name1",
+ Version: "1.2.3",
+ },
+ {
+ Name: "vuln-pack-name2",
+ Version: "1.2.3",
+ },
+ },
+ },
+ Cves: []formats.CveRow{{
+ Id: "CVE-2021-1234",
+ Applicability: &formats.Applicability{
+ Status: "Applicable",
+ ScannerDescription: "scanner",
+ Evidence: []formats.Evidence{
+ {Reason: "reason", Location: formats.Location{File: "file1", StartLine: 1, StartColumn: 2, EndLine: 3, EndColumn: 4, Snippet: "snippet1"}},
+ {Reason: "other reason", Location: formats.Location{File: "file2", StartLine: 5, StartColumn: 6, EndLine: 7, EndColumn: 8, Snippet: "snippet2"}},
+ },
+ },
+ }},
+ JfrogResearchInformation: &formats.JfrogResearchInformation{
+ Remediation: "remediation",
+ },
+ Summary: "summary",
+ Applicable: "Applicable",
+ IssueId: "Xray-Id",
+ },
+ {
+ ImpactedDependencyDetails: formats.ImpactedDependencyDetails{
+ ImpactedDependencyName: "impacted-name2",
+ ImpactedDependencyVersion: "1.0.0",
+ SeverityDetails: formats.SeverityDetails{Severity: "Low"},
+ Components: []formats.ComponentRow{
+ {
+ Name: "vuln-pack-name3",
+ Version: "1.0.0",
+ },
+ },
+ },
+ Cves: []formats.CveRow{{
+ Id: "CVE-1111-2222",
+ Applicability: &formats.Applicability{Status: "Not Applicable"},
+ }},
+ Summary: "other summary",
+ Applicable: "Not Applicable",
+ IssueId: "Xray-Id2",
+ },
+ },
+
+ ScaViolations: []formats.VulnerabilityOrViolationRow{
+ {
+ ImpactedDependencyDetails: formats.ImpactedDependencyDetails{
+ ImpactedDependencyName: "impacted-name",
+ ImpactedDependencyVersion: "1.0.0",
+ SeverityDetails: formats.SeverityDetails{Severity: "Critical"},
+ Components: []formats.ComponentRow{
+ {
+ Name: "vuln-pack-name1",
+ Version: "1.0.0",
+ },
+ },
+ },
+ Cves: []formats.CveRow{{
+ Id: "CVE-2021-1234",
+ Applicability: &formats.Applicability{
+ Status: "Applicable",
+ ScannerDescription: "scanner",
+ Evidence: []formats.Evidence{
+ {Reason: "reason", Location: formats.Location{File: "file1", StartLine: 1, StartColumn: 2, EndLine: 3, EndColumn: 4, Snippet: "snippet1"}},
+ },
+ },
+ }},
+ JfrogResearchInformation: &formats.JfrogResearchInformation{
+ Remediation: "remediation",
+ },
+ Summary: "summary",
+ Applicable: "Applicable",
+ IssueId: "Xray-Id",
+ ViolationContext: formats.ViolationContext{
+ Watch: "watch",
+ Policies: []string{"policy1", "policy2"},
+ },
+ },
+ },
+
+ LicensesViolations: []formats.LicenseViolationRow{{
+ LicenseRow: formats.LicenseRow{
+ LicenseKey: "license1",
+ LicenseName: "license-name1",
+ ImpactedDependencyDetails: formats.ImpactedDependencyDetails{
+ SeverityDetails: formats.SeverityDetails{Severity: "Medium"},
+ Components: []formats.ComponentRow{
+ {
+ Name: "vuln-pack-name3",
+ Version: "1.0.0",
+ },
+ },
+ },
+ },
+ ViolationContext: formats.ViolationContext{
+ Watch: "lic-watch",
+ Policies: []string{"policy3"},
+ },
+ }},
+
+ IacVulnerabilities: []formats.SourceCodeRow{{SeverityDetails: formats.SeverityDetails{Severity: "Low"}}},
+ SecretsVulnerabilities: []formats.SourceCodeRow{{SeverityDetails: formats.SeverityDetails{Severity: "High"}}},
+ SecretsViolations: []formats.SourceCodeRow{{
+ SeverityDetails: formats.SeverityDetails{Severity: "High"},
+ ViolationContext: formats.ViolationContext{
+ IssueId: "secret-violation-id",
+ Watch: "watch",
+ },
+ }},
+ SastVulnerabilities: []formats.SourceCodeRow{
+ {
+ SeverityDetails: formats.SeverityDetails{Severity: "Unknown"},
+ },
+ {
+ SeverityDetails: formats.SeverityDetails{Severity: "High"},
+ },
+ },
+ }
+ return issuesCollection
+}
+
+func TestCountIssuesCollectionFindings(t *testing.T) {
+ testCases := []struct {
+ name string
+ includeSecrets bool
+ expectedFindings int
+ }{
+ {
+ name: "With Secrets",
+ includeSecrets: true,
+ expectedFindings: 9,
+ },
+ {
+ name: "No Secrets",
+ includeSecrets: false,
+ expectedFindings: 7,
+ },
+ }
+ issuesCollection := getTestData()
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ findingsAmount := issuesCollection.GetAllIssuesCount(tc.includeSecrets)
+ assert.Equal(t, tc.expectedFindings, findingsAmount)
+ })
+ }
+}
+
+func TestGetTotalVulnerabilities(t *testing.T) {
+ testCases := []struct {
+ name string
+ includeSecrets bool
+ expectedFindings int
+ }{
+ {
+ name: "With Secrets",
+ includeSecrets: true,
+ expectedFindings: 6,
+ },
+ {
+ name: "No Secrets",
+ includeSecrets: false,
+ expectedFindings: 5,
+ },
+ }
+ issuesCollection := getTestData()
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ findingsAmount := issuesCollection.GetTotalVulnerabilities(tc.includeSecrets)
+ assert.Equal(t, tc.expectedFindings, findingsAmount)
+ })
+ }
+}
+
+func TestGetTotalViolations(t *testing.T) {
+ testCases := []struct {
+ name string
+ includeSecrets bool
+ expectedFindings int
+ }{
+ {
+ name: "With Secrets",
+ includeSecrets: true,
+ expectedFindings: 3,
+ },
+ {
+ name: "No Secrets",
+ includeSecrets: false,
+ expectedFindings: 2,
+ },
+ }
+ issuesCollection := getTestData()
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ findingsAmount := issuesCollection.GetTotalViolations(tc.includeSecrets)
+ assert.Equal(t, tc.expectedFindings, findingsAmount)
+ })
+ }
+}
+
+func TestGetScanIssuesSeverityCount(t *testing.T) {
+ testCases := []struct {
+ name string
+ scanType utils.SubScanType
+ violation bool
+ vulnerabilities bool
+ expectedSeverityCount map[string]int
+ }{
+ {
+ name: "Sca Vulnerabilities",
+ scanType: utils.ScaScan,
+ vulnerabilities: true,
+ expectedSeverityCount: map[string]int{"High": 1, "Low": 1},
+ },
+ {
+ name: "Sca Violations",
+ scanType: utils.ScaScan,
+ violation: true,
+ expectedSeverityCount: map[string]int{"Critical": 1, "Medium": 1},
+ },
+ {
+ name: "Sca Vulnerabilities and Violations",
+ scanType: utils.ScaScan,
+ vulnerabilities: true,
+ violation: true,
+ expectedSeverityCount: map[string]int{"High": 1, "Low": 1, "Critical": 1, "Medium": 1},
+ },
+ {
+ name: "Iac Vulnerabilities",
+ scanType: utils.IacScan,
+ vulnerabilities: true,
+ expectedSeverityCount: map[string]int{"Low": 1},
+ },
+ {
+ name: "Iac Violations",
+ scanType: utils.IacScan,
+ violation: true,
+ expectedSeverityCount: map[string]int{},
+ },
+ {
+ name: "Iac Vulnerabilities and Violations",
+ scanType: utils.IacScan,
+ vulnerabilities: true,
+ violation: true,
+ expectedSeverityCount: map[string]int{"Low": 1},
+ },
+ {
+ name: "Secrets Vulnerabilities",
+ scanType: utils.SecretsScan,
+ vulnerabilities: true,
+ expectedSeverityCount: map[string]int{"High": 1},
+ },
+ {
+ name: "Secrets Violations",
+ scanType: utils.SecretsScan,
+ violation: true,
+ expectedSeverityCount: map[string]int{"High": 1},
+ },
+ {
+ name: "Secrets Vulnerabilities and Violations",
+ scanType: utils.SecretsScan,
+ vulnerabilities: true,
+ violation: true,
+ expectedSeverityCount: map[string]int{"High": 2},
+ },
+ {
+ name: "Sast Vulnerabilities",
+ scanType: utils.SastScan,
+ vulnerabilities: true,
+ expectedSeverityCount: map[string]int{"High": 1, "Unknown": 1},
+ },
+ {
+ name: "Sast Violations",
+ scanType: utils.SastScan,
+ violation: true,
+ expectedSeverityCount: map[string]int{},
+ },
+ {
+ name: "Sast Vulnerabilities and Violations",
+ scanType: utils.SastScan,
+ vulnerabilities: true,
+ violation: true,
+ expectedSeverityCount: map[string]int{"High": 1, "Unknown": 1},
+ },
+ }
+ issuesCollection := getTestData()
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ severityCount := issuesCollection.GetScanIssuesSeverityCount(tc.scanType, tc.vulnerabilities, tc.violation)
+ assert.Len(t, severityCount, len(tc.expectedSeverityCount))
+ for severity, count := range tc.expectedSeverityCount {
+ actualCount, ok := severityCount[severityutils.GetSeverity(severity)]
+ assert.True(t, ok)
+ assert.Equal(t, count, actualCount)
+ }
+ })
+ }
+}
+
+func TestGetApplicableEvidences(t *testing.T) {
+ testCases := []struct {
+ name string
+ issues ScansIssuesCollection
+ expectedEvidences []ApplicableEvidences
+ }{
+ {
+ name: "No Issues",
+ },
+ {
+ name: "With Issues",
+ issues: getTestData(),
+ expectedEvidences: []ApplicableEvidences{
+ {
+ Evidence: formats.Evidence{Reason: "reason", Location: formats.Location{File: "file1", StartLine: 1, StartColumn: 2, EndLine: 3, EndColumn: 4, Snippet: "snippet1"}},
+ Severity: "Critical", ScannerDescription: "scanner", IssueId: "CVE-2021-1234", CveSummary: "summary", ImpactedDependency: "impacted-name:1.0.0", Remediation: "remediation",
+ },
+ {
+ Evidence: formats.Evidence{Reason: "other reason", Location: formats.Location{File: "file2", StartLine: 5, StartColumn: 6, EndLine: 7, EndColumn: 8, Snippet: "snippet2"}},
+ Severity: "High", ScannerDescription: "scanner", IssueId: "CVE-2021-1234", CveSummary: "summary", ImpactedDependency: "impacted-name:1.0.0", Remediation: "remediation",
+ },
+ },
+ },
+ }
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ assert.ElementsMatch(t, tc.expectedEvidences, tc.issues.GetApplicableEvidences())
+ })
+ }
+}
+
+func TestIssuesExists(t *testing.T) {
+ testCases := []struct {
+ name string
+ issues ScansIssuesCollection
+ includeSecrets bool
+ expected bool
+ }{
+ {
+ name: "No Issues",
+ },
+ {
+ name: "With Issues",
+ issues: getTestData(),
+ expected: true,
+ },
+ {
+ name: "With Secrets",
+ issues: getTestData(),
+ includeSecrets: true,
+ expected: true,
+ },
+ }
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ assert.Equal(t, tc.expected, tc.issues.IssuesExists(tc.includeSecrets))
+ })
+ }
+}
+
+func TestHasErrors(t *testing.T) {
+ testCases := []struct {
+ name string
+ status formats.ScanStatus
+ expected bool
+ }{
+ {
+ name: "Some Not Scanned",
+ status: formats.ScanStatus{
+ ScaStatusCode: utils.NewIntPtr(0),
+ SastStatusCode: utils.NewIntPtr(0),
+ SecretsStatusCode: utils.NewIntPtr(0),
+ },
+ },
+ {
+ name: "All Completed",
+ status: formats.ScanStatus{
+ ScaStatusCode: utils.NewIntPtr(0),
+ SastStatusCode: utils.NewIntPtr(0),
+ SecretsStatusCode: utils.NewIntPtr(0),
+ IacStatusCode: utils.NewIntPtr(0),
+ ApplicabilityStatusCode: utils.NewIntPtr(0),
+ },
+ },
+ {
+ name: "With Errors",
+ status: formats.ScanStatus{
+ ScaStatusCode: utils.NewIntPtr(-1),
+ SastStatusCode: utils.NewIntPtr(0),
+ SecretsStatusCode: utils.NewIntPtr(33),
+ IacStatusCode: utils.NewIntPtr(0),
+ ApplicabilityStatusCode: utils.NewIntPtr(0),
+ },
+ expected: true,
+ },
+ }
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ issues := ScansIssuesCollection{ScanStatus: tc.status}
+ assert.Equal(t, tc.expected, issues.HasErrors())
+ })
+ }
+}
+
+func TestIsScanNotCompleted(t *testing.T) {
+ issues := ScansIssuesCollection{ScanStatus: formats.ScanStatus{
+ ScaStatusCode: utils.NewIntPtr(-1),
+ SastStatusCode: utils.NewIntPtr(0),
+ SecretsStatusCode: utils.NewIntPtr(33),
+ }}
+ testCases := []struct {
+ name string
+ scan utils.SubScanType
+ expected bool
+ }{
+ {
+ name: "Scanned and Passed",
+ scan: utils.SastScan,
+ },
+ {
+ name: "Scanned and unknown Failed",
+ scan: utils.ScaScan,
+ expected: true,
+ },
+ {
+ name: "Scanned and Failed",
+ scan: utils.SecretsScan,
+ expected: true,
+ },
+ {
+ name: "Not Scanned",
+ scan: utils.IacScan,
+ expected: true,
+ },
+ }
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ assert.Equal(t, tc.expected, issues.IsScanNotCompleted(tc.scan))
+ })
+ }
+}
+
+func TestAppendStatus(t *testing.T) {
+ oldStatus := formats.ScanStatus{
+ ScaStatusCode: utils.NewIntPtr(-1),
+ SastStatusCode: utils.NewIntPtr(0),
+ }
+ newStatus := formats.ScanStatus{
+ ScaStatusCode: utils.NewIntPtr(0),
+ SastStatusCode: utils.NewIntPtr(33),
+ ApplicabilityStatusCode: utils.NewIntPtr(0),
+ SecretsStatusCode: utils.NewIntPtr(51),
+ }
+ expectedStatus := formats.ScanStatus{
+ ScaStatusCode: utils.NewIntPtr(-1),
+ SastStatusCode: utils.NewIntPtr(33),
+ ApplicabilityStatusCode: utils.NewIntPtr(0),
+ SecretsStatusCode: utils.NewIntPtr(51),
+ }
+ issues := ScansIssuesCollection{ScanStatus: oldStatus}
+ issues.AppendStatus(newStatus)
+ assert.Equal(t, expectedStatus, issues.ScanStatus)
+}
diff --git a/utils/issuescollection.go b/utils/issuescollection.go
deleted file mode 100644
index 73c338d78..000000000
--- a/utils/issuescollection.go
+++ /dev/null
@@ -1,76 +0,0 @@
-package utils
-
-import (
- "github.com/jfrog/gofrog/datastructures"
- "github.com/jfrog/jfrog-cli-security/utils/formats"
-)
-
-type IssuesCollection struct {
- Vulnerabilities []formats.VulnerabilityOrViolationRow
- Iacs []formats.SourceCodeRow
- Secrets []formats.SourceCodeRow
- Sast []formats.SourceCodeRow
- Licenses []formats.LicenseRow
-}
-
-func (ic *IssuesCollection) VulnerabilitiesExists() bool {
- return len(ic.Vulnerabilities) > 0
-}
-
-func (ic *IssuesCollection) IacExists() bool {
- return len(ic.Iacs) > 0
-}
-
-func (ic *IssuesCollection) LicensesExists() bool {
- return len(ic.Licenses) > 0
-}
-
-func (ic *IssuesCollection) SecretsExists() bool {
- return len(ic.Secrets) > 0
-}
-
-func (ic *IssuesCollection) SastExists() bool {
- return len(ic.Sast) > 0
-}
-
-func (ic *IssuesCollection) IssuesExists() bool {
- return ic.VulnerabilitiesExists() || ic.IacExists() || ic.LicensesExists() || ic.SastExists()
-}
-
-func (ic *IssuesCollection) Append(issues *IssuesCollection) {
- if issues == nil {
- return
- }
- if len(issues.Vulnerabilities) > 0 {
- ic.Vulnerabilities = append(ic.Vulnerabilities, issues.Vulnerabilities...)
- }
- if len(issues.Secrets) > 0 {
- ic.Secrets = append(ic.Secrets, issues.Secrets...)
- }
- if len(issues.Sast) > 0 {
- ic.Sast = append(ic.Sast, issues.Sast...)
- }
- if len(issues.Iacs) > 0 {
- ic.Iacs = append(ic.Iacs, issues.Iacs...)
- }
- if len(issues.Licenses) > 0 {
- ic.Licenses = append(ic.Licenses, issues.Licenses...)
- }
-}
-
-func (ic *IssuesCollection) CountIssuesCollectionFindings() int {
- uniqueFindings := datastructures.MakeSet[string]()
-
- var totalFindings int
- for _, vulnerability := range ic.Vulnerabilities {
- for _, component := range vulnerability.Components {
- uniqueFindings.Add(vulnerability.IssueId + "|" + component.Name + "|" + component.Version)
- }
- }
- totalFindings += uniqueFindings.Size()
-
- totalFindings += len(ic.Iacs)
- totalFindings += len(ic.Sast)
- totalFindings += len(ic.Secrets)
- return totalFindings
-}
diff --git a/utils/issuescollection_test.go b/utils/issuescollection_test.go
deleted file mode 100644
index d6911f2fb..000000000
--- a/utils/issuescollection_test.go
+++ /dev/null
@@ -1,67 +0,0 @@
-package utils
-
-import (
- "testing"
-
- "github.com/jfrog/jfrog-cli-security/utils/formats"
- "github.com/stretchr/testify/assert"
-)
-
-func TestCountIssuesCollectionFindings(t *testing.T) {
- issuesCollection := IssuesCollection{
- Vulnerabilities: []formats.VulnerabilityOrViolationRow{
- {
- ImpactedDependencyDetails: formats.ImpactedDependencyDetails{
- Components: []formats.ComponentRow{
- {
- Name: "vuln-pack-name1",
- Version: "1.0.0",
- },
- {
- Name: "vuln-pack-name1",
- Version: "1.2.3",
- },
- {
- Name: "vuln-pack-name2",
- Version: "1.2.3",
- },
- },
- },
- IssueId: "Xray-Id",
- },
- {
- ImpactedDependencyDetails: formats.ImpactedDependencyDetails{
- Components: []formats.ComponentRow{
- {
- Name: "vuln-pack-name3",
- Version: "1.0.0",
- },
- },
- },
- IssueId: "Xray-Id2",
- },
- },
-
- Iacs: []formats.SourceCodeRow{
- {
- ScannerDescription: "Iac issue",
- },
- },
- Secrets: []formats.SourceCodeRow{
- {
- ScannerDescription: "Secret issue",
- },
- },
- Sast: []formats.SourceCodeRow{
- {
- ScannerDescription: "Sast issue1",
- },
- {
- ScannerDescription: "Sast issue2",
- },
- },
- }
-
- findingsAmount := issuesCollection.CountIssuesCollectionFindings()
- assert.Equal(t, 8, findingsAmount)
-}
diff --git a/utils/outputwriter/icons.go b/utils/outputwriter/icons.go
index 3a3892e5b..49c341a0c 100644
--- a/utils/outputwriter/icons.go
+++ b/utils/outputwriter/icons.go
@@ -27,6 +27,12 @@ const (
notApplicableLowSeveritySource ImageSource = "v2/notApplicableLow.png"
unknownSeveritySource ImageSource = "v2/applicableUnknownSeverity.png"
notApplicableUnknownSeveritySource ImageSource = "v2/notApplicableUnknown.png"
+
+ smallCriticalSeveritySource ImageSource = "v2/smallCritical.svg"
+ smallHighSeveritySource ImageSource = "v2/smallHigh.svg"
+ smallMediumSeveritySource ImageSource = "v2/smallMedium.svg"
+ smallLowSeveritySource ImageSource = "v2/smallLow.svg"
+ smallUnknownSeveritySource ImageSource = "v2/smallUnknown.svg"
)
func getSeverityTag(iconName IconName, applicability string) string {
@@ -36,40 +42,62 @@ func getSeverityTag(iconName IconName, applicability string) string {
return getApplicableIconTags(iconName)
}
+func getSmallSeverityTag(iconName IconName) string {
+ return getSmallApplicableIconTags(iconName)
+}
+
func getNotApplicableIconTags(iconName IconName) string {
switch strings.ToLower(string(iconName)) {
case "critical":
- return GetIconTag(notApplicableCriticalSeveritySource) + "
"
+ return GetIconTag(notApplicableCriticalSeveritySource, "critical (not applicable)") + "
"
case "high":
- return GetIconTag(notApplicableHighSeveritySource) + "
"
+ return GetIconTag(notApplicableHighSeveritySource, "high (not applicable)") + "
"
case "medium":
- return GetIconTag(notApplicableMediumSeveritySource) + "
"
+ return GetIconTag(notApplicableMediumSeveritySource, "medium (not applicable)") + "
"
case "low":
- return GetIconTag(notApplicableLowSeveritySource) + "
"
+ return GetIconTag(notApplicableLowSeveritySource, "low (not applicable)") + "
"
}
- return GetIconTag(notApplicableUnknownSeveritySource) + "
"
+ return GetIconTag(notApplicableUnknownSeveritySource, "unknown (not applicable)") + "
"
}
func getApplicableIconTags(iconName IconName) string {
switch strings.ToLower(string(iconName)) {
case "critical":
- return GetIconTag(criticalSeveritySource) + "
"
+ return GetIconTag(criticalSeveritySource, "critical") + "
"
case "high":
- return GetIconTag(highSeveritySource) + "
"
+ return GetIconTag(highSeveritySource, "high") + "
"
case "medium":
- return GetIconTag(mediumSeveritySource) + "
"
+ return GetIconTag(mediumSeveritySource, "medium") + "
"
case "low":
- return GetIconTag(lowSeveritySource) + "
"
+ return GetIconTag(lowSeveritySource, "low") + "
"
}
- return GetIconTag(unknownSeveritySource) + "
"
+ return GetIconTag(unknownSeveritySource, "unknown") + "
"
+}
+
+func getSmallApplicableIconTags(iconName IconName) string {
+ switch strings.ToLower(string(iconName)) {
+ case "critical":
+ return GetImgTag(smallCriticalSeveritySource, "")
+ case "high":
+ return GetImgTag(smallHighSeveritySource, "")
+ case "medium":
+ return GetImgTag(smallMediumSeveritySource, "")
+ case "low":
+ return GetImgTag(smallLowSeveritySource, "")
+ }
+ return GetImgTag(smallUnknownSeveritySource, "")
}
func GetBanner(banner ImageSource) string {
- return GetMarkdownCenterTag(MarkAsLink(GetIconTag(banner), FrogbotDocumentationUrl))
+ return GetMarkdownCenterTag(MarkAsLink(GetIconTag(banner, GetSimplifiedTitle(banner)), FrogbotDocumentationUrl))
+}
+
+func GetIconTag(imageSource ImageSource, alt string) string {
+ return fmt.Sprintf("!%s", MarkAsLink(alt, fmt.Sprintf("%s%s", baseResourceUrl, imageSource)))
}
-func GetIconTag(imageSource ImageSource) string {
- return fmt.Sprintf("!%s", MarkAsLink(GetSimplifiedTitle(imageSource), fmt.Sprintf("%s%s", baseResourceUrl, imageSource)))
+func GetImgTag(imageSource ImageSource, alt string) string {
+ return fmt.Sprintf("

", baseResourceUrl, imageSource, alt)
}
func GetSimplifiedTitle(is ImageSource) string {
diff --git a/utils/outputwriter/icons_test.go b/utils/outputwriter/icons_test.go
index 720ff4130..51434832a 100644
--- a/utils/outputwriter/icons_test.go
+++ b/utils/outputwriter/icons_test.go
@@ -6,20 +6,28 @@ import (
"github.com/stretchr/testify/assert"
)
+func TestGetSmallSeverityTag(t *testing.T) {
+ assert.Equal(t, "

", getSmallSeverityTag("Critical"))
+ assert.Equal(t, "

", getSmallSeverityTag("HiGh"))
+ assert.Equal(t, "

", getSmallSeverityTag("meDium"))
+ assert.Equal(t, "

", getSmallSeverityTag("low"))
+ assert.Equal(t, "

", getSmallSeverityTag("none"))
+}
+
func TestGetSeverityTag(t *testing.T) {
- assert.Equal(t, "
", getSeverityTag("Critical", "Undetermined"))
- assert.Equal(t, "
", getSeverityTag("HiGh", "Undetermined"))
- assert.Equal(t, "
", getSeverityTag("meDium", "Undetermined"))
- assert.Equal(t, "
", getSeverityTag("low", "Applicable"))
- assert.Equal(t, "
", getSeverityTag("none", "Applicable"))
+ assert.Equal(t, "
", getSeverityTag("Critical", "Undetermined"))
+ assert.Equal(t, "
", getSeverityTag("HiGh", "Undetermined"))
+ assert.Equal(t, "
", getSeverityTag("meDium", "Undetermined"))
+ assert.Equal(t, "
", getSeverityTag("low", "Applicable"))
+ assert.Equal(t, "
", getSeverityTag("none", "Applicable"))
}
func TestGetSeverityTagNotApplicable(t *testing.T) {
- assert.Equal(t, "
", getSeverityTag("Critical", "Not Applicable"))
- assert.Equal(t, "
", getSeverityTag("HiGh", "Not Applicable"))
- assert.Equal(t, "
", getSeverityTag("meDium", "Not Applicable"))
- assert.Equal(t, "
", getSeverityTag("low", "Not Applicable"))
- assert.Equal(t, "
", getSeverityTag("none", "Not Applicable"))
+ assert.Equal(t, "
", getSeverityTag("Critical", "Not Applicable"))
+ assert.Equal(t, "
", getSeverityTag("HiGh", "Not Applicable"))
+ assert.Equal(t, "
", getSeverityTag("meDium", "Not Applicable"))
+ assert.Equal(t, "
", getSeverityTag("low", "Not Applicable"))
+ assert.Equal(t, "
", getSeverityTag("none", "Not Applicable"))
}
func TestGetVulnerabilitiesBanners(t *testing.T) {
diff --git a/utils/outputwriter/markdowntable.go b/utils/outputwriter/markdowntable.go
index 9ca52fb8b..c661def2c 100644
--- a/utils/outputwriter/markdowntable.go
+++ b/utils/outputwriter/markdowntable.go
@@ -6,11 +6,16 @@ import (
)
const (
- tableRowFirstColumnSeparator = "| :---------------------: |"
- tableRowColumnSeparator = " :-----------------------------------: |"
- cellFirstCellPlaceholder = "| %s |"
- cellCellPlaceholder = " %s |"
- cellDefaultValue = "-"
+ cellDefaultValue = "-"
+
+ firstCellPlaceholder = "| %s |"
+ cellPlaceholder = " %s |"
+
+ centeredFirstColumnSeparator = "| :---------------------: |"
+ centeredColumnSeparator = " :-----------------------------------: |"
+
+ defaultFirstColumnSeparator = "| --------------------- |"
+ defaultColumnSeparator = " ----------------------------------- |"
// (Default value for columns) If more than one value exists in a cell, the values will be separated by the delimiter.
SeparatorDelimited MarkdownColumnType = "single"
@@ -32,8 +37,12 @@ type MarkdownColumnType string
type MarkdownColumn struct {
Name string
+ Centered bool
+ OmitEmpty bool
ColumnType MarkdownColumnType
DefaultValue string
+ // Internal flag to determine if the column should be hidden
+ shouldHideColumn bool
}
// CellData represents the data of a cell in the markdown table. Each cell can contain multiple values.
@@ -51,17 +60,38 @@ func NewCellData(values ...string) CellData {
func NewMarkdownTable(columns ...string) *MarkdownTableBuilder {
columnsInfo := []*MarkdownColumn{}
for _, column := range columns {
- columnsInfo = append(columnsInfo, &MarkdownColumn{Name: column, ColumnType: SeparatorDelimited, DefaultValue: cellDefaultValue})
+ columnsInfo = append(columnsInfo, NewMarkdownTableSingleValueColumn(column, cellDefaultValue, true))
+ }
+ return NewMarkdownTableWithColumns(columnsInfo...)
+}
+
+// Create a markdown table builder with the provided number of columns.
+func NewNoHeaderMarkdownTable(nColumns int, firstColumnCentered bool) *MarkdownTableBuilder {
+ columnsInfo := []*MarkdownColumn{}
+ for i := 0; i < nColumns; i++ {
+ columnsInfo = append(columnsInfo, NewMarkdownTableSingleValueColumn("", cellDefaultValue, i != 0 || firstColumnCentered))
}
+ return NewMarkdownTableWithColumns(columnsInfo...)
+}
+
+func NewMarkdownTableWithColumns(columnsInfo ...*MarkdownColumn) *MarkdownTableBuilder {
return &MarkdownTableBuilder{columns: columnsInfo, delimiter: simpleSeparator}
}
+func NewMarkdownTableSingleValueColumn(name, defaultValue string, centered bool) *MarkdownColumn {
+ return &MarkdownColumn{Name: name, ColumnType: SeparatorDelimited, DefaultValue: defaultValue, Centered: centered}
+}
+
// Set the delimiter that will be used to separate multiple values in a cell.
func (t *MarkdownTableBuilder) SetDelimiter(delimiter string) *MarkdownTableBuilder {
t.delimiter = delimiter
return t
}
+func (t *MarkdownTableBuilder) HasContent() bool {
+ return len(t.rows) > 0
+}
+
// Get the column information output controller by the provided name.
func (t *MarkdownTableBuilder) GetColumnInfo(name string) *MarkdownColumn {
for _, column := range t.columns {
@@ -111,22 +141,53 @@ func (t *MarkdownTableBuilder) Build() string {
return ""
}
var tableBuilder strings.Builder
+ // Calculate Hidden columns
+ for c := range t.columns {
+ // Reset shouldHideColumn flag to the defined value in the column
+ // If the column OmitEmpty flag is set, the column will be hidden if all the values in the column are empty
+ t.columns[c].shouldHideColumn = t.columns[c].OmitEmpty
+ }
+ for _, row := range t.rows {
+ for c, cell := range row {
+ // In table, empty cell = cell with no values = cell with one empty value
+ // So we want don't want to hide the column if at least one cell has a value in it
+ t.columns[c].shouldHideColumn = t.columns[c].shouldHideColumn && (len(cell) == 0 || (len(cell) == 1 && cell[0] == ""))
+ }
+ }
// Header
- for c, column := range t.columns {
- if c == 0 {
- tableBuilder.WriteString(fmt.Sprintf(cellFirstCellPlaceholder, column.Name))
+ isFirstCol := true
+ for _, column := range t.columns {
+ if column.shouldHideColumn {
+ continue
+ }
+ if isFirstCol {
+ tableBuilder.WriteString(fmt.Sprintf(firstCellPlaceholder, column.Name))
} else {
- tableBuilder.WriteString(fmt.Sprintf(cellCellPlaceholder, column.Name))
+ tableBuilder.WriteString(fmt.Sprintf(cellPlaceholder, column.Name))
}
+ isFirstCol = false
}
tableBuilder.WriteString("\n")
// Separator
- for c := range t.columns {
- if c == 0 {
- tableBuilder.WriteString(tableRowFirstColumnSeparator)
+ isFirstCol = true
+ for _, column := range t.columns {
+ if column.shouldHideColumn {
+ continue
+ }
+ if isFirstCol {
+ columnSeparator := defaultFirstColumnSeparator
+ if column.Centered {
+ columnSeparator = centeredFirstColumnSeparator
+ }
+ tableBuilder.WriteString(columnSeparator)
} else {
- tableBuilder.WriteString(tableRowColumnSeparator)
+ columnSeparator := defaultColumnSeparator
+ if column.Centered {
+ columnSeparator = centeredColumnSeparator
+ }
+ tableBuilder.WriteString(columnSeparator)
}
+ isFirstCol = false
}
// Content
for _, row := range t.rows {
@@ -161,6 +222,9 @@ func (t *MarkdownTableBuilder) getMultiValueRowsContent(row []CellData, multiVal
}
content := []string{}
for column, cell := range row {
+ if t.columns[column].shouldHideColumn {
+ continue
+ }
if column == multiValueColumnIndex {
// Multi values column separated by different rows, add the specific value for this row
content = append(content, value)
@@ -183,6 +247,9 @@ func (t *MarkdownTableBuilder) getMultiValueRowsContent(row []CellData, multiVal
func (t *MarkdownTableBuilder) getSeparatorDelimitedRowContent(row []CellData) string {
content := []string{}
for column, columnInfo := range t.columns {
+ if columnInfo.shouldHideColumn {
+ continue
+ }
content = append(content, t.getCellContent(row[column], columnInfo.DefaultValue))
}
return buildRowContent(content...)
diff --git a/utils/outputwriter/markdowntable_test.go b/utils/outputwriter/markdowntable_test.go
index 556b6e1d9..fb8ff113f 100644
--- a/utils/outputwriter/markdowntable_test.go
+++ b/utils/outputwriter/markdowntable_test.go
@@ -97,7 +97,7 @@ func TestMarkdownTableBuild(t *testing.T) {
name: "No rows",
columns: []string{"col1"},
rows: [][]string{},
- expectedOutput: "| col1 |\n" + tableRowFirstColumnSeparator,
+ expectedOutput: "| col1 |\n" + centeredFirstColumnSeparator,
},
{
name: "Same number of columns",
@@ -107,7 +107,7 @@ func TestMarkdownTableBuild(t *testing.T) {
{"row2col1", "row2col2"},
{"row3col1", "row3col2"},
},
- expectedOutput: "| col1 | col2 |\n" + tableRowFirstColumnSeparator + tableRowColumnSeparator + `
+ expectedOutput: "| col1 | col2 |\n" + centeredFirstColumnSeparator + centeredColumnSeparator + `
| row1col1 | row1col2 |
| row2col1 | row2col2 |
| row3col1 | row3col2 |`,
@@ -123,7 +123,7 @@ func TestMarkdownTableBuild(t *testing.T) {
{"row4col1"},
{"row5col1", "row5col2", "row5col3"},
},
- expectedOutput: "| col1 | col2 | col3 |\n" + tableRowFirstColumnSeparator + tableRowColumnSeparator + tableRowColumnSeparator + `
+ expectedOutput: "| col1 | col2 | col3 |\n" + centeredFirstColumnSeparator + centeredColumnSeparator + centeredColumnSeparator + `
| row1col1 | row1col2 | - |
| row2col1 | row2col2 | - |
| row3col1 | - | row3col3 |
@@ -143,6 +143,60 @@ func TestMarkdownTableBuild(t *testing.T) {
}
}
+func TestHideEmptyColumnsInTable(t *testing.T) {
+ columns := []*MarkdownColumn{
+ {Name: "col1", OmitEmpty: true},
+ {Name: "col2", OmitEmpty: true, Centered: true},
+ {Name: "col3", OmitEmpty: false, DefaultValue: "-"},
+ {Name: "col4", OmitEmpty: true},
+ }
+ testCases := []struct {
+ name string
+ rows [][]string
+ expectedOutput string
+ }{
+ {
+ name: "Defined as hidden but not empty",
+ rows: [][]string{
+ {"row1col1", "row1col2", "", "row1col4"},
+ {"row2col1", "row2col2", "", "row2col4"},
+ },
+ expectedOutput: "| col1 | col2 | col3 | col4 |\n" + defaultFirstColumnSeparator + centeredColumnSeparator + defaultColumnSeparator + defaultColumnSeparator + `
+| row1col1 | row1col2 | - | row1col4 |
+| row2col1 | row2col2 | - | row2col4 |`,
+ },
+ {
+ name: "Defined as hidden and some empty",
+ rows: [][]string{
+ {"row1col1", "", "row1col3", ""},
+ {"row2col1", "", "", ""},
+ },
+ expectedOutput: "| col1 | col3 |\n" + defaultFirstColumnSeparator + defaultColumnSeparator + `
+| row1col1 | row1col3 |
+| row2col1 | - |`,
+ },
+ {
+ name: "Defined as hidden and all empty",
+ rows: [][]string{
+ {"", "", "row1col3", ""},
+ {"", "", "", ""},
+ },
+ expectedOutput: "| col3 |\n" + defaultFirstColumnSeparator + `
+| row1col3 |
+| - |`,
+ },
+ }
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ table := NewMarkdownTableWithColumns(columns...)
+ for _, row := range tc.rows {
+ table.AddRow(row...)
+ }
+ assert.Equal(t, tc.expectedOutput, table.Build())
+ })
+ }
+}
+
func TestMultipleValuesInColumnRow(t *testing.T) {
testCases := []struct {
name string
@@ -156,7 +210,7 @@ func TestMultipleValuesInColumnRow(t *testing.T) {
rows: [][]CellData{
{{""}, {"row1col2"}, {"row1col3"}},
},
- expectedOutput: "| col1 | col2 | col3 |\n" + tableRowFirstColumnSeparator + tableRowColumnSeparator + tableRowColumnSeparator + `
+ expectedOutput: "| col1 | col2 | col3 |\n" + centeredFirstColumnSeparator + centeredColumnSeparator + centeredColumnSeparator + `
| - | row1col2 | row1col3 |`,
},
{
@@ -166,7 +220,7 @@ func TestMultipleValuesInColumnRow(t *testing.T) {
{{"row1col1"}, {"row1col2"}, {"row1col3"}},
{{"row2col1"}, {"row2col2"}, {"row2col3"}},
},
- expectedOutput: "| col1 | col2 | col3 |\n" + tableRowFirstColumnSeparator + tableRowColumnSeparator + tableRowColumnSeparator + `
+ expectedOutput: "| col1 | col2 | col3 |\n" + centeredFirstColumnSeparator + centeredColumnSeparator + centeredColumnSeparator + `
| row1col1 | row1col2 | row1col3 |
| row2col1 | row2col2 | row2col3 |`,
},
@@ -178,7 +232,7 @@ func TestMultipleValuesInColumnRow(t *testing.T) {
{{"row2col1"}, {"row2col2"}, {"row2col3val1", "row2col3val2"}},
{{"row3col1"}, {"row3col2val1", "row3col2val2", "row3col2val3"}, {"row3col3"}},
},
- expectedOutput: "| col1 | col2 | col3 |\n" + tableRowFirstColumnSeparator + tableRowColumnSeparator + tableRowColumnSeparator + `
+ expectedOutput: "| col1 | col2 | col3 |\n" + centeredFirstColumnSeparator + centeredColumnSeparator + centeredColumnSeparator + `
| row1col1 | - | row1col3 |
| row2col1 | row2col2 | row2col3val1, row2col3val2 |
| row3col1 | row3col2val1, row3col2val2, row3col2val3 | row3col3 |`,
@@ -191,7 +245,7 @@ func TestMultipleValuesInColumnRow(t *testing.T) {
{{"row2col1"}, {"row2col2"}, {"row2col3val1", "row2col3val2"}},
{{"row3col1"}, {"row3col2val1", "row3col2val2", "row3col2val3"}, {"row3col3"}},
},
- expectedOutput: "| col1 | col2 | col3 |\n" + tableRowFirstColumnSeparator + tableRowColumnSeparator + tableRowColumnSeparator + `
+ expectedOutput: "| col1 | col2 | col3 |\n" + centeredFirstColumnSeparator + centeredColumnSeparator + centeredColumnSeparator + `
| row1col1 | - | row1col3 |
| row2col1 | row2col2 | row2col3val1, row2col3val2 |
| row3col1 | row3col2val1 | row3col3 |
diff --git a/utils/outputwriter/outputcontent.go b/utils/outputwriter/outputcontent.go
index 788d5637a..f68e8b69a 100644
--- a/utils/outputwriter/outputcontent.go
+++ b/utils/outputwriter/outputcontent.go
@@ -2,28 +2,41 @@ package outputwriter
import (
"fmt"
+ "sort"
"strings"
+ "github.com/jfrog/frogbot/v2/utils/issues"
"github.com/jfrog/froggit-go/vcsutils"
+ "github.com/jfrog/jfrog-cli-security/utils"
"github.com/jfrog/jfrog-cli-security/utils/formats"
"github.com/jfrog/jfrog-cli-security/utils/jasutils"
"github.com/jfrog/jfrog-cli-security/utils/results"
+ "github.com/jfrog/jfrog-cli-security/utils/severityutils"
+ "golang.org/x/exp/maps"
)
const (
FrogbotTitlePrefix = "[🐸 Frogbot]"
FrogbotRepoUrl = "https://github.com/jfrog/frogbot"
FrogbotDocumentationUrl = "https://docs.jfrog-applications.jfrog.io/jfrog-applications/frogbot"
+ JfrogSupportUrl = "https://jfrog.com/support/"
ReviewCommentId = "FrogbotReviewComment"
- vulnerableDependenciesTitle = "📦 Vulnerable Dependencies"
- vulnerableDependenciesResearchDetailsSubTitle = "🔬 Research Details"
+ scanSummaryTitle = "📗 Scan Summary"
+ issuesDetailsSubTitle = "🔖 Details"
+ jfrogResearchDetailsSubTitle = "🔬 JFrog Research Details"
+
+ policyViolationTitle = "🚥 Policy Violations"
+ securityViolationTitle = "🚨 Security Violations"
+ licenseViolationTitle = "⚖️ License Violations"
+
+ vulnerableDependenciesTitle = "📦 Vulnerable Dependencies"
- contextualAnalysisTitle = "📦🔍 Contextual Analysis CVE Vulnerability"
//#nosec G101 -- not a secret
- secretsTitle = "🗝️ Secret Detected"
- iacTitle = "🛠️ Infrastructure as Code Vulnerability"
- sastTitle = "🎯 Static Application Security Testing (SAST) Vulnerability"
+ secretsTitle = "🤫 Secret"
+ contextualAnalysisTitle = "📦🔍 Contextual Analysis CVE"
+ iacTitle = "🛠️ Infrastructure as Code"
+ sastTitle = "🎯 Static Application Security Testing (SAST)"
)
var (
@@ -31,6 +44,44 @@ var (
jasFeaturesMsgWhenNotEnabled = MarkAsBold("Frogbot") + " also supports " + MarkAsBold("Contextual Analysis, Secret Detection, IaC and SAST Vulnerabilities Scanning") + ". This features are included as part of the " + MarkAsLink("JFrog Advanced Security", "https://jfrog.com/advanced-security") + " package, which isn't enabled on your system."
)
+// For review comment Frogbot creates on Scan PR
+func GenerateReviewCommentContent(content string, writer OutputWriter) string {
+ var contentBuilder strings.Builder
+ contentBuilder.WriteString(MarkdownComment(ReviewCommentId))
+ customCommentTitle := writer.PullRequestCommentTitle()
+ if customCommentTitle != "" {
+ WriteContent(&contentBuilder, writer.MarkAsTitle(MarkAsBold(customCommentTitle), 2))
+ }
+ WriteContent(&contentBuilder, content, footer(writer))
+ return contentBuilder.String()
+}
+
+// When can't create review comment, create a fallback comment by adding the location description to the content as a prefix
+func GetFallbackReviewCommentContent(content string, location formats.Location) string {
+ var contentBuilder strings.Builder
+ contentBuilder.WriteString(MarkdownComment(ReviewCommentId))
+ WriteContent(&contentBuilder, getFallbackCommentLocationDescription(location), content)
+ return contentBuilder.String()
+}
+
+func IsFrogbotComment(content string) bool {
+ return strings.Contains(content, ReviewCommentId)
+}
+
+func getFallbackCommentLocationDescription(location formats.Location) string {
+ return fmt.Sprintf("%s\nat %s (line %d)", MarkAsCodeSnippet(location.Snippet), MarkAsQuote(location.File), location.StartLine)
+}
+
+// Summary comment, including banner, footer wrapping the content with a decorator
+func GetMainCommentContent(contentForComments []string, issuesExists, isComment bool, writer OutputWriter) (comments []string) {
+ return ConvertContentToComments(contentForComments, writer, func(commentCount int, content string) string {
+ if commentCount == 0 {
+ content = GetPRSummaryMainCommentDecorator(issuesExists, isComment, writer)(commentCount, content)
+ }
+ return GetFrogbotCommentBaseDecorator(writer)(commentCount, content)
+ })
+}
+
// Adding markdown prefix to identify Frogbot comment and a footer with the link to the documentation
func GetFrogbotCommentBaseDecorator(writer OutputWriter) CommentDecorator {
return func(_ int, content string) string {
@@ -58,15 +109,6 @@ func GetPRSummaryMainCommentDecorator(issuesExists, isComment bool, writer Outpu
}
}
-func GetPRSummaryContent(contentForComments []string, issuesExists, isComment bool, writer OutputWriter) (comments []string) {
- return ConvertContentToComments(contentForComments, writer, func(commentCount int, content string) string {
- if commentCount == 0 {
- content = GetPRSummaryMainCommentDecorator(issuesExists, isComment, writer)(commentCount, content)
- }
- return GetFrogbotCommentBaseDecorator(writer)(commentCount, content)
- })
-}
-
func getPRSummaryBanner(issuesExists, isComment bool, provider vcsutils.VcsProvider) ImageSource {
if !isComment {
return fixCVETitleSrc(provider)
@@ -77,15 +119,6 @@ func getPRSummaryBanner(issuesExists, isComment bool, provider vcsutils.VcsProvi
return PRSummaryCommentTitleSrc(provider)
}
-// TODO: remove this at the next release, it's not used anymore and replaced by adding ReviewCommentId comment to the content
-func IsFrogbotSummaryComment(writer OutputWriter, content string) bool {
- client := writer.VcsProvider()
- return strings.Contains(content, GetBanner(NoIssuesTitleSrc(client))) ||
- strings.Contains(content, GetSimplifiedTitle(NoIssuesTitleSrc(client))) ||
- strings.Contains(content, GetBanner(PRSummaryCommentTitleSrc(client))) ||
- strings.Contains(content, GetSimplifiedTitle(PRSummaryCommentTitleSrc(client)))
-}
-
func NoIssuesTitleSrc(vcsProvider vcsutils.VcsProvider) ImageSource {
if vcsProvider == vcsutils.GitLab {
return NoVulnerabilityMrBannerSource
@@ -111,76 +144,459 @@ func untitledForJasMsg(writer OutputWriter) string {
if writer.AvoidExtraMessages() || writer.IsEntitledForJas() {
return ""
}
- return writer.MarkAsDetails("Note:", 0, fmt.Sprintf("%s\n%s", SectionDivider(), writer.MarkInCenter(jasFeaturesMsgWhenNotEnabled)))
+ return writer.MarkAsDetails("Note", 0, fmt.Sprintf("\n%s\n%s", SectionDivider(), writer.MarkInCenter(jasFeaturesMsgWhenNotEnabled)))
}
func footer(writer OutputWriter) string {
return fmt.Sprintf("%s\n%s", SectionDivider(), writer.MarkInCenter(CommentGeneratedByFrogbot))
}
-func VulnerabilitiesContent(vulnerabilities []formats.VulnerabilityOrViolationRow, writer OutputWriter) (content []string) {
- if len(vulnerabilities) == 0 {
- return []string{}
+// Summary content
+
+func ScanSummaryContent(issues issues.ScansIssuesCollection, context results.ResultContext, includeSecrets bool, writer OutputWriter) string {
+ if !issues.IssuesExists(includeSecrets) && !issues.HasErrors() {
+ return ""
+ }
+ var contentBuilder strings.Builder
+ totalIssues := 0
+ if context.HasViolationContext() {
+ totalIssues += issues.GetTotalViolations(includeSecrets)
+ }
+ if context.IncludeVulnerabilities {
+ totalIssues += issues.GetTotalVulnerabilities(includeSecrets)
+ }
+ // Title
+ WriteContent(&contentBuilder, writer.MarkAsTitle(scanSummaryTitle, 2))
+ if issues.HasErrors() {
+ WriteContent(&contentBuilder, MarkAsBullet(fmt.Sprintf("Frogbot attempted to scan for %s but encountered an error.", getResultsContextString(context))))
+ return contentBuilder.String()
+ } else {
+ WriteContent(&contentBuilder, MarkAsBullet(fmt.Sprintf("Frogbot scanned for %s and found %d issues", getResultsContextString(context), totalIssues)))
+ }
+ WriteNewLine(&contentBuilder)
+ // Create table, a row for each sub scans summary
+ secretsDetails := ""
+ if includeSecrets {
+ secretsDetails = getScanSecurityIssuesDetails(issues, context, utils.SecretsScan, writer)
+ }
+ table := NewMarkdownTableWithColumns(
+ NewMarkdownTableSingleValueColumn("Scan Category", "⚠️", false),
+ NewMarkdownTableSingleValueColumn("Status", "⚠️", true),
+ NewMarkdownTableSingleValueColumn("Security Issues", "-", false),
+ )
+ table.AddRow(MarkAsBold("Software Composition Analysis"), getSubScanResultStatus(issues.GetScanStatus(utils.ScaScan)), getScanSecurityIssuesDetails(issues, context, utils.ScaScan, writer))
+ table.AddRow(MarkAsBold("Contextual Analysis"), getSubScanResultStatus(issues.GetScanStatus(utils.ContextualAnalysisScan)), "")
+ table.AddRow(MarkAsBold("Static Application Security Testing (SAST)"), getSubScanResultStatus(issues.GetScanStatus(utils.SastScan)), getScanSecurityIssuesDetails(issues, context, utils.SastScan, writer))
+ table.AddRow(MarkAsBold("Secrets"), getSubScanResultStatus(issues.GetScanStatus(utils.SecretsScan)), secretsDetails)
+ table.AddRow(MarkAsBold("Infrastructure as Code (IaC)"), getSubScanResultStatus(issues.GetScanStatus(utils.IacScan)), getScanSecurityIssuesDetails(issues, context, utils.IacScan, writer))
+ WriteContent(&contentBuilder, table.Build())
+ return contentBuilder.String()
+}
+
+func getResultsContextString(context results.ResultContext) string {
+ out := ""
+ if context.HasViolationContext() {
+ out += "violations"
+ }
+ if context.IncludeVulnerabilities {
+ if out != "" {
+ out += " and "
+ }
+ out += "vulnerabilities"
+ }
+ return out
+}
+
+func getSubScanResultStatus(scanStatusCode *int) string {
+ if scanStatusCode == nil {
+ return "ℹ️ Not Scanned"
+ }
+ if *scanStatusCode == 0 {
+ return "✅ Done"
+ }
+ return "❌ Failed"
+}
+
+func getScanSecurityIssuesDetails(issues issues.ScansIssuesCollection, context results.ResultContext, scanType utils.SubScanType, writer OutputWriter) string {
+ if issues.HasErrors() || issues.IsScanNotCompleted(scanType) {
+ // Failed/Not scanned, no need to show the details
+ return ""
+ }
+ var severityCountMap map[severityutils.Severity]int
+ countViolations := context.HasViolationContext()
+ countVulnerabilities := context.IncludeVulnerabilities
+ switch scanType {
+ case utils.ScaScan:
+ severityCountMap = issues.GetScanIssuesSeverityCount(utils.ScaScan, countVulnerabilities, countViolations)
+ case utils.SastScan:
+ severityCountMap = issues.GetScanIssuesSeverityCount(utils.SastScan, countVulnerabilities, countViolations)
+ case utils.SecretsScan:
+ severityCountMap = issues.GetScanIssuesSeverityCount(utils.SecretsScan, countVulnerabilities, countViolations)
+ case utils.IacScan:
+ severityCountMap = issues.GetScanIssuesSeverityCount(utils.IacScan, countVulnerabilities, countViolations)
+ }
+ totalIssues := getTotalIssues(severityCountMap)
+ if totalIssues == 0 {
+ // No Issues
+ return "Not Found"
+ }
+ var contentBuilder strings.Builder
+ WriteContent(&contentBuilder, writer.MarkAsDetails(fmt.Sprintf("%d Issues Found", totalIssues), 0, toSeverityDetails(severityCountMap, writer)))
+ return contentBuilder.String()
+}
+
+func getTotalIssues(severities map[severityutils.Severity]int) (total int) {
+ for _, count := range severities {
+ total += count
}
- content = append(content, writer.MarkAsTitle(vulnerableDependenciesTitle, 2))
- content = append(content, vulnerabilitiesSummaryContent(vulnerabilities, writer))
- content = append(content, vulnerabilityDetailsContent(vulnerabilities, writer)...)
return
}
-func vulnerabilitiesSummaryContent(vulnerabilities []formats.VulnerabilityOrViolationRow, writer OutputWriter) string {
+func toSeverityDetails(severities map[severityutils.Severity]int, writer OutputWriter) string {
var contentBuilder strings.Builder
- WriteContent(&contentBuilder,
- writer.MarkAsTitle("✍️ Summary", 3),
- writer.MarkInCenter(getVulnerabilitiesSummaryTable(vulnerabilities, writer)),
- )
+ sortedSeverities := []severityutils.Severity{severityutils.Critical, severityutils.High, severityutils.Medium, severityutils.Low, severityutils.Unknown}
+ for _, severity := range sortedSeverities {
+ if count, ok := severities[severity]; ok && count > 0 {
+ if contentBuilder.Len() > 0 {
+ contentBuilder.WriteString(writer.Separator())
+ }
+ contentBuilder.WriteString(fmt.Sprintf("%s %d %s", writer.SeverityIcon(severity), count, severity.String()))
+ }
+ }
+ return contentBuilder.String()
+}
+
+// SCA (Policy) Violations
+
+// Summary content for the security violations that we can't yet have location on (SCA, License)
+func PolicyViolationsContent(issues issues.ScansIssuesCollection, writer OutputWriter) (policyViolationContent []string) {
+ if issues.GetTotalScaViolations() == 0 {
+ return []string{}
+ }
+ policyViolationContent = append(policyViolationContent, getSecurityViolationsContent(issues, writer)...)
+ policyViolationContent = append(policyViolationContent, getLicenseViolationsContent(issues, writer)...)
+ return ConvertContentToComments(policyViolationContent, writer, getDecoratorWithPolicyViolationTitle(writer))
+}
+
+func getDecoratorWithPolicyViolationTitle(writer OutputWriter) CommentDecorator {
+ return func(commentCount int, content string) string {
+ contentBuilder := strings.Builder{}
+ // Decorate each part of the split content with a title as prefix and return the content
+ WriteContent(&contentBuilder, writer.MarkAsTitle(policyViolationTitle, 2))
+ WriteContent(&contentBuilder, content)
+ return contentBuilder.String()
+ }
+}
+
+// Security Violations
+
+func getSecurityViolationsContent(issues issues.ScansIssuesCollection, writer OutputWriter) (content []string) {
+ if len(issues.ScaViolations) == 0 {
+ return []string{}
+ }
+ content = append(content, getSecurityViolationsSummaryTable(issues.ScaViolations, writer))
+ content = append(content, getScaSecurityIssueDetailsContent(issues.ScaViolations, true, writer)...)
+ return ConvertContentToComments(content, writer, getDecoratorWithSecurityViolationTitle(writer))
+}
+
+func getDecoratorWithSecurityViolationTitle(writer OutputWriter) CommentDecorator {
+ return func(commentCount int, content string) string {
+ contentBuilder := strings.Builder{}
+ // Decorate each part of the split content with a title as prefix and return the content
+ WriteContent(&contentBuilder, writer.MarkAsTitle(securityViolationTitle, 3))
+ WriteContent(&contentBuilder, content)
+ return contentBuilder.String()
+ }
+}
+
+func getSecurityViolationsSummaryTable(violations []formats.VulnerabilityOrViolationRow, writer OutputWriter) string {
+ // Construct table
+ columns := []string{"Severity", "ID"}
+ if writer.IsShowingCaColumn() {
+ columns = append(columns, "Contextual Analysis")
+ }
+ table := NewMarkdownTable(append(columns, "Direct Dependencies", "Impacted Dependency", "Watch Name")...).SetDelimiter(writer.Separator())
+ if _, ok := writer.(*SimplifiedOutput); ok {
+ // The values in this cell can be potentially large, since SimplifiedOutput does not support tags, we need to show each value in a separate row.
+ // It means that the first row will show the full details, and the following rows will show only the direct dependency.
+ // It makes it easier to read the table and less crowded with text in a single cell that could be potentially large.
+ table.GetColumnInfo("Direct Dependencies").ColumnType = MultiRowColumn
+ }
+ // Construct rows
+ for _, violation := range violations {
+ row := []CellData{{writer.FormattedSeverity(violation.Severity, violation.Applicable)}, getCveIdsCellData(violation.Cves, violation.IssueId)}
+ if writer.IsShowingCaColumn() {
+ row = append(row, NewCellData(violation.Applicable))
+ }
+ row = append(row,
+ getDirectDependenciesCellData(violation.Components),
+ NewCellData(results.GetDependencyId(violation.ImpactedDependencyName, violation.ImpactedDependencyVersion)),
+ NewCellData(violation.Watch),
+ )
+ table.AddRowWithCellData(row...)
+ }
+ return writer.MarkInCenter(table.Build())
+}
+
+// License violations
+
+func getLicenseViolationsContent(issues issues.ScansIssuesCollection, writer OutputWriter) (content []string) {
+ if len(issues.LicensesViolations) == 0 {
+ return []string{}
+ }
+ content = append(content, getLicenseViolationsSummaryTable(issues.LicensesViolations, writer))
+ content = append(content, getLicenseViolationsDetailsContent(issues.LicensesViolations, writer)...)
+ return ConvertContentToComments(content, writer, getDecoratorWithLicenseViolationTitle(writer))
+}
+
+func getDecoratorWithLicenseViolationTitle(writer OutputWriter) CommentDecorator {
+ return func(commentCount int, content string) string {
+ contentBuilder := strings.Builder{}
+ // Decorate each part of the split content with a title as prefix and return the content
+ WriteContent(&contentBuilder, writer.MarkAsTitle(licenseViolationTitle, 3))
+ WriteContent(&contentBuilder, content)
+ return contentBuilder.String()
+ }
+}
+
+func getLicenseViolationsSummaryTable(licenses []formats.LicenseViolationRow, writer OutputWriter) string {
+ table := NewMarkdownTable("Severity", "License", "Direct Dependencies", "Impacted Dependency", "Watch Name").SetDelimiter(writer.Separator())
+ if _, ok := writer.(*SimplifiedOutput); ok {
+ // The values in this cell can be potentially large, since SimplifiedOutput does not support tags, we need to show each value in a separate row.
+ // It means that the first row will show the full details, and the following rows will show only the direct dependency.
+ // It makes it easier to read the table and less crowded with text in a single cell that could be potentially large.
+ table.GetColumnInfo("Direct Dependencies").ColumnType = MultiRowColumn
+ }
+ for _, license := range licenses {
+ table.AddRowWithCellData(
+ NewCellData(writer.FormattedSeverity(license.Severity, "Applicable")),
+ NewCellData(license.LicenseKey),
+ getDirectDependenciesCellData(license.Components),
+ NewCellData(results.GetDependencyId(license.ImpactedDependencyName, license.ImpactedDependencyVersion)),
+ NewCellData(license.Watch),
+ )
+ }
+ return writer.MarkInCenter(table.Build())
+}
+
+func getLicenseViolationsDetailsContent(licenseViolations []formats.LicenseViolationRow, writer OutputWriter) (content []string) {
+ if len(licenseViolations) == 0 {
+ return
+ }
+ for _, violation := range licenseViolations {
+ if len(licenseViolations) == 1 {
+ // No need for
tag if there is only one violation, just show the details
+ content = append(content, getScaLicenseViolationDetails(violation, writer))
+ } else {
+ // Add wrap the content of each violation in a tag
+ content = append(content, "\n"+writer.MarkAsDetails(
+ getComponentIssueIdentifier(violation.LicenseKey, violation.ImpactedDependencyName, violation.ImpactedDependencyVersion, violation.Watch),
+ 4,
+ getScaLicenseViolationDetails(violation, writer),
+ ))
+ }
+ }
+ // Split content if it exceeds the size limit and decorate each comment with title as prefix
+ return ConvertContentToComments(content, writer, func(commentCount int, detailsContent string) string {
+ contentBuilder := strings.Builder{}
+ WriteContent(&contentBuilder, writer.MarkAsTitle(issuesDetailsSubTitle, 3))
+ WriteContent(&contentBuilder, detailsContent)
+ return contentBuilder.String()
+ })
+}
+
+func getScaLicenseViolationDetails(violation formats.LicenseViolationRow, writer OutputWriter) (content string) {
+ var contentBuilder strings.Builder
+ // Title
+ WriteNewLine(&contentBuilder)
+ WriteContent(&contentBuilder, writer.MarkAsTitle("Violation Details", 3))
+ // Details Table
+ directComponent := []string{}
+ for _, component := range violation.ImpactedDependencyDetails.Components {
+ directComponent = append(directComponent, results.GetDependencyId(component.Name, component.Version))
+ }
+ noHeaderTable := NewNoHeaderMarkdownTable(2, false)
+
+ noHeaderTable.AddRowWithCellData(NewCellData(MarkAsBold("Policies:")), NewCellData(violation.Policies...))
+ noHeaderTable.AddRow(MarkAsBold("Watch Name:"), violation.Watch)
+ noHeaderTable.AddRowWithCellData(NewCellData(MarkAsBold("Direct Dependencies:")), NewCellData(directComponent...))
+ noHeaderTable.AddRow(MarkAsBold("Impacted Dependency:"), results.GetDependencyId(violation.ImpactedDependencyName, violation.ImpactedDependencyVersion))
+ noHeaderTable.AddRow(MarkAsBold("Full Name:"), violation.LicenseName)
+
+ WriteContent(&contentBuilder, noHeaderTable.Build(), "\n")
return contentBuilder.String()
}
+// Sca Vulnerabilities
+
+func GetVulnerabilitiesContent(vulnerabilities []formats.VulnerabilityOrViolationRow, writer OutputWriter) (content []string) {
+ if len(vulnerabilities) == 0 {
+ return []string{}
+ }
+ content = append(content, writer.MarkInCenter(getVulnerabilitiesSummaryTable(vulnerabilities, writer)))
+ content = append(content, getScaSecurityIssueDetailsContent(vulnerabilities, false, writer)...)
+ return ConvertContentToComments(content, writer, getDecoratorWithScaVulnerabilitiesTitle(writer))
+}
+
+func getDecoratorWithScaVulnerabilitiesTitle(writer OutputWriter) CommentDecorator {
+ return func(commentCount int, content string) string {
+ contentBuilder := strings.Builder{}
+ // Decorate each part of the split content with a title as prefix and return the content
+ WriteContent(&contentBuilder, writer.MarkAsTitle(vulnerableDependenciesTitle, 3))
+ WriteContent(&contentBuilder, content)
+ return contentBuilder.String()
+ }
+}
+
func getVulnerabilitiesSummaryTable(vulnerabilities []formats.VulnerabilityOrViolationRow, writer OutputWriter) string {
// Construct table
- columns := []string{"SEVERITY"}
+ columns := []string{"Severity", "ID"}
if writer.IsShowingCaColumn() {
- columns = append(columns, "CONTEXTUAL ANALYSIS")
+ columns = append(columns, "Contextual Analysis")
}
- columns = append(columns, "DIRECT DEPENDENCIES", "IMPACTED DEPENDENCY", "FIXED VERSIONS", "CVES")
+ columns = append(columns, "Direct Dependencies", "Impacted Dependency", "Fixed Versions")
table := NewMarkdownTable(columns...).SetDelimiter(writer.Separator())
if _, ok := writer.(*SimplifiedOutput); ok {
// The values in this cell can be potentially large, since SimplifiedOutput does not support tags, we need to show each value in a separate row.
// It means that the first row will show the full details, and the following rows will show only the direct dependency.
// It makes it easier to read the table and less crowded with text in a single cell that could be potentially large.
- table.GetColumnInfo("DIRECT DEPENDENCIES").ColumnType = MultiRowColumn
+ table.GetColumnInfo("Direct Dependencies").ColumnType = MultiRowColumn
}
// Construct rows
for _, vulnerability := range vulnerabilities {
- row := []CellData{{writer.FormattedSeverity(vulnerability.Severity, vulnerability.Applicable)}}
+ row := []CellData{{writer.FormattedSeverity(vulnerability.Severity, vulnerability.Applicable)}, getCveIdsCellData(vulnerability.Cves, vulnerability.IssueId)}
if writer.IsShowingCaColumn() {
row = append(row, NewCellData(vulnerability.Applicable))
}
row = append(row,
- getDirectDependenciesCellData("%s:%s", vulnerability.Components),
+ getDirectDependenciesCellData(vulnerability.Components),
NewCellData(fmt.Sprintf("%s %s", vulnerability.ImpactedDependencyName, vulnerability.ImpactedDependencyVersion)),
NewCellData(vulnerability.FixedVersions...),
- getCveIdsCellData(vulnerability.Cves),
)
table.AddRowWithCellData(row...)
}
return table.Build()
}
-func getDirectDependenciesCellData(format string, components []formats.ComponentRow) (dependencies CellData) {
+// Applicable CVE Evidence
+
+func ApplicableCveReviewContent(issue issues.ApplicableEvidences, writer OutputWriter) string {
+ var contentBuilder strings.Builder
+ WriteContent(&contentBuilder,
+ writer.MarkAsTitle(contextualAnalysisTitle, 2),
+ writer.MarkInCenter(GetApplicabilityDescriptionTable(issue.Severity, issue.IssueId, issue.ImpactedDependency, issue.Evidence.Reason, writer)),
+ writer.MarkAsDetails("Description", 3, "\n"+issue.ScannerDescription+"\n"),
+ writer.MarkAsDetails("CVE details", 3, "\n"+issue.CveSummary+"\n"),
+ )
+ if len(issue.Remediation) > 0 {
+ WriteContent(&contentBuilder, writer.MarkAsDetails("Remediation", 3, "\n\n"+issue.Remediation+"\n\n"))
+ }
+ return contentBuilder.String()
+}
+
+func GetApplicabilityDescriptionTable(severity, issueId, impactedDependency, finding string, writer OutputWriter) string {
+ table := NewMarkdownTable("Severity", "ID", "Impacted Dependency", "Finding").AddRow(writer.FormattedSeverity(severity, "Applicable"), issueId, impactedDependency, finding)
+ return table.Build()
+}
+
+// JAS
+
+func IacReviewContent(violation bool, writer OutputWriter, issues ...formats.SourceCodeRow) string {
+ var contentBuilder strings.Builder
+ WriteContent(&contentBuilder,
+ writer.MarkAsTitle(fmt.Sprintf("%s %s", iacTitle, getIssueType(violation)), 2),
+ writer.MarkInCenter(getJasIssueDescriptionTable(writer, issues...)),
+ getJasFullDescription(violation, writer, getBaseJasDetailsTable, issues...),
+ )
+ return contentBuilder.String()
+}
+
+func SastReviewContent(violation bool, writer OutputWriter, issues ...formats.SourceCodeRow) string {
+ var contentBuilder strings.Builder
+ WriteContent(&contentBuilder,
+ writer.MarkAsTitle(fmt.Sprintf("%s %s", sastTitle, getIssueType(violation)), 2),
+ writer.MarkInCenter(getJasIssueDescriptionTable(writer, issues...)),
+ getJasFullDescription(violation, writer, getSastRuleFullDescriptionTable, issues...),
+ )
+ return contentBuilder.String()
+}
+
+func getSastRuleFullDescriptionTable(info formats.ScannerInfo, writer OutputWriter) *MarkdownTableBuilder {
+ table := getBaseJasDetailsTable(info, writer)
+ table.AddRow(MarkAsBold("Rule ID:"), info.RuleId)
+ return table
+}
+
+func SecretReviewContent(violation bool, writer OutputWriter, issues ...formats.SourceCodeRow) string {
+ var contentBuilder strings.Builder
+ WriteContent(&contentBuilder,
+ writer.MarkAsTitle(fmt.Sprintf("%s %s", secretsTitle, getIssueType(violation)), 2),
+ writer.MarkInCenter(getSecretsDescriptionTable(writer, issues...)),
+ getJasFullDescription(violation, writer, getSecretsRuleFullDescriptionTable, issues...),
+ )
+ return contentBuilder.String()
+}
+
+func getSecretsDescriptionTable(writer OutputWriter, issues ...formats.SourceCodeRow) string {
+ // Construct table
+ table := NewMarkdownTable("Severity", "ID", "Status", "Finding", "Watch Name", "Policies").SetDelimiter(writer.Separator())
+ // Hide optional columns if all empty (violations/no status)
+ table.GetColumnInfo("ID").OmitEmpty = true
+ table.GetColumnInfo("Status").OmitEmpty = true
+ table.GetColumnInfo("Watch Name").OmitEmpty = true
+ table.GetColumnInfo("Policies").OmitEmpty = true
+ // Construct rows
+ for _, issue := range issues {
+ // Determine the issue applicable status
+ applicability := jasutils.Applicable.String()
+ status := ""
+ if issue.Applicability != nil && issue.Applicability.Status != "" {
+ status = issue.Applicability.Status
+ if status == jasutils.Inactive.String() {
+ // Update the applicability status to Not Applicable for Inactive
+ applicability = jasutils.NotApplicable.String()
+ }
+ }
+ table.AddRowWithCellData(
+ NewCellData(writer.FormattedSeverity(issue.Severity, applicability)),
+ NewCellData(issue.IssueId),
+ NewCellData(status),
+ NewCellData(issue.Finding),
+ NewCellData(issue.Watch),
+ NewCellData(issue.Policies...),
+ )
+ }
+ return table.Build()
+}
+
+func getSecretsRuleFullDescriptionTable(info formats.ScannerInfo, writer OutputWriter) *MarkdownTableBuilder {
+ table := getBaseJasDetailsTable(info, writer)
+ table.AddRow(MarkAsBold("Abbreviation:"), info.RuleId)
+ return table
+}
+
+// Utilities
+
+func getIssueType(violation bool) string {
+ if violation {
+ return "Violation"
+ }
+ return "Vulnerability"
+}
+
+func getDirectDependenciesCellData(components []formats.ComponentRow) (dependencies CellData) {
if len(components) == 0 {
return NewCellData()
}
for _, component := range components {
- dependencies = append(dependencies, fmt.Sprintf(format, component.Name, component.Version))
+ dependencies = append(dependencies, results.GetDependencyId(component.Name, component.Version))
}
return
}
-func getCveIdsCellData(cveRows []formats.CveRow) (ids CellData) {
+func getCveIdsCellData(cveRows []formats.CveRow, issueId string) (ids CellData) {
if len(cveRows) == 0 {
- return NewCellData()
+ return NewCellData(issueId)
}
for _, cve := range cveRows {
ids = append(ids, cve.Id)
@@ -188,218 +604,231 @@ func getCveIdsCellData(cveRows []formats.CveRow) (ids CellData) {
return
}
-type vulnerabilityOrViolationDetails struct {
- details string
- title string
- dependencyName string
- dependencyVersion string
-}
-
-func vulnerabilityDetailsContent(vulnerabilities []formats.VulnerabilityOrViolationRow, writer OutputWriter) (content []string) {
- vulnerabilitiesWithDetails := getVulnerabilityWithDetails(vulnerabilities)
- if len(vulnerabilitiesWithDetails) == 0 {
+func getScaSecurityIssueDetailsContent(issues []formats.VulnerabilityOrViolationRow, violations bool, writer OutputWriter) (content []string) {
+ issuesWithDetails := getIssuesWithDetails(issues)
+ if len(issuesWithDetails) == 0 {
return
}
- // Prepare content for each vulnerability details
- for i := range vulnerabilitiesWithDetails {
- if len(vulnerabilitiesWithDetails) == 1 {
- content = append(content, vulnerabilitiesWithDetails[i].details)
+ for _, issue := range issuesWithDetails {
+ if len(issues) == 1 {
+ // No need for tag if there is only one issue, just show the details
+ content = append(content, getScaSecurityIssueDetails(issue, violations, writer))
} else {
- content = append(content, writer.MarkAsDetails(
- fmt.Sprintf(`%s %s %s`, vulnerabilitiesWithDetails[i].title,
- vulnerabilitiesWithDetails[i].dependencyName,
- vulnerabilitiesWithDetails[i].dependencyVersion),
- 4, vulnerabilitiesWithDetails[i].details,
+ // Add wrap the content of each issue in a tag
+ content = append(content, "\n"+writer.MarkAsDetails(
+ getComponentIssueIdentifier(results.GetIssueIdentifier(issue.Cves, issue.IssueId, ", "), issue.ImpactedDependencyName, issue.ImpactedDependencyVersion, issue.Watch), 4,
+ getScaSecurityIssueDetails(issue, violations, writer),
))
}
}
- // Split content if it exceeds the size limit and decorate it with title
+ // Split content if it exceeds the size limit and decorate it with title as prefix
return ConvertContentToComments(content, writer, func(commentCount int, detailsContent string) string {
contentBuilder := strings.Builder{}
- WriteContent(&contentBuilder, writer.MarkAsTitle(vulnerableDependenciesResearchDetailsSubTitle, 3))
+ WriteContent(&contentBuilder, writer.MarkAsTitle(issuesDetailsSubTitle, 3))
WriteContent(&contentBuilder, detailsContent)
return contentBuilder.String()
})
}
-func getVulnerabilityWithDetails(vulnerabilities []formats.VulnerabilityOrViolationRow) (vulnerabilitiesWithDetails []vulnerabilityOrViolationDetails) {
- for i := range vulnerabilities {
- vulDescriptionContent := createVulnerabilityResearchDescription(&vulnerabilities[i])
- if vulDescriptionContent == "" {
- // No content
- continue
+func getIssuesWithDetails(issues []formats.VulnerabilityOrViolationRow) (filter []formats.VulnerabilityOrViolationRow) {
+ for i := range issues {
+ if issues[i].JfrogResearchInformation != nil || issues[i].Summary != "" {
+ filter = append(filter, issues[i])
}
- vulnerabilitiesWithDetails = append(vulnerabilitiesWithDetails, vulnerabilityOrViolationDetails{
- details: vulDescriptionContent,
- title: getVulnerabilityDescriptionIdentifier(vulnerabilities[i].Cves, vulnerabilities[i].IssueId),
- dependencyName: vulnerabilities[i].ImpactedDependencyName,
- dependencyVersion: vulnerabilities[i].ImpactedDependencyVersion,
- })
}
return
}
-func createVulnerabilityResearchDescription(vulnerability *formats.VulnerabilityOrViolationRow) string {
- var descriptionBuilder strings.Builder
- vulnResearch := vulnerability.JfrogResearchInformation
- if vulnResearch == nil {
- vulnResearch = &formats.JfrogResearchInformation{Details: vulnerability.Summary}
- } else if vulnResearch.Details == "" {
- vulnResearch.Details = vulnerability.Summary
+func getComponentIssueIdentifier(key, compName, version, watch string) (id string) {
+ parts := []string{}
+ if key != "" {
+ parts = append(parts, fmt.Sprintf("[ %s ]", key))
}
+ parts = append(parts, compName, version)
+ if watch != "" {
+ parts = append(parts, fmt.Sprintf("(%s)", watch))
+ }
+ return strings.Join(parts, " ")
+}
- if vulnResearch.Details != "" {
- WriteContent(&descriptionBuilder, MarkAsBold("Description:"), vulnResearch.Details)
+func getScaSecurityIssueDetails(issue formats.VulnerabilityOrViolationRow, violations bool, writer OutputWriter) (content string) {
+ var contentBuilder strings.Builder
+ // Title
+ WriteNewLine(&contentBuilder)
+ WriteContent(&contentBuilder, writer.MarkAsTitle(fmt.Sprintf("%s Details", getIssueType(violations)), 3))
+ // Details Table
+ directComponent := []string{}
+ for _, component := range issue.ImpactedDependencyDetails.Components {
+ directComponent = append(directComponent, results.GetDependencyId(component.Name, component.Version))
}
- if vulnResearch.Remediation != "" {
- if vulnResearch.Details != "" {
- WriteNewLine(&descriptionBuilder)
- }
- WriteContent(&descriptionBuilder, MarkAsBold("Remediation:"), vulnResearch.Remediation)
+ noHeaderTable := NewNoHeaderMarkdownTable(2, false)
+ if len(issue.Policies) > 0 {
+ noHeaderTable.AddRowWithCellData(NewCellData(MarkAsBold("Policies:")), NewCellData(issue.Policies...))
}
- return descriptionBuilder.String()
-}
+ if issue.Watch != "" {
+ noHeaderTable.AddRow(MarkAsBold("Watch Name:"), issue.Watch)
+ }
+ if issue.JfrogResearchInformation != nil && issue.JfrogResearchInformation.Severity != "" {
+ severity := severityutils.Severity(issue.JfrogResearchInformation.Severity)
+ noHeaderTable.AddRow(MarkAsBold("Jfrog Research Severity:"), fmt.Sprintf("%s %s", writer.SeverityIcon(severity), severity.String()))
+ }
+ if issue.Applicable != "" {
+ noHeaderTable.AddRow(MarkAsBold("Contextual Analysis:"), issue.Applicable)
+ }
+ noHeaderTable.AddRowWithCellData(NewCellData(MarkAsBold("Direct Dependencies:")), NewCellData(directComponent...))
+ noHeaderTable.AddRow(MarkAsBold("Impacted Dependency:"), results.GetDependencyId(issue.ImpactedDependencyName, issue.ImpactedDependencyVersion))
+ noHeaderTable.AddRowWithCellData(NewCellData(MarkAsBold("Fixed Versions:")), NewCellData(issue.FixedVersions...))
-func getVulnerabilityDescriptionIdentifier(cveRows []formats.CveRow, xrayId string) string {
- identifier := results.GetIssueIdentifier(cveRows, xrayId, ", ")
- if identifier == "" {
- return ""
+ cvss := []string{}
+ for _, cve := range issue.Cves {
+ cvss = append(cvss, cve.CvssV3)
}
- return fmt.Sprintf("[ %s ]", identifier)
-}
+ noHeaderTable.AddRowWithCellData(NewCellData(MarkAsBold("CVSS V3:")), NewCellData(cvss...))
+ WriteContent(&contentBuilder, noHeaderTable.Build())
-func LicensesContent(licenses []formats.LicenseRow, writer OutputWriter) string {
- if len(licenses) == 0 {
- return ""
+ // Summary
+ summary := issue.Summary
+ if issue.JfrogResearchInformation != nil && issue.JfrogResearchInformation.Summary != "" {
+ summary = issue.JfrogResearchInformation.Summary
}
- // Title
- var contentBuilder strings.Builder
- WriteContent(&contentBuilder, writer.MarkAsTitle("⚖️ Violated Licenses", 2))
- // Content
- table := NewMarkdownTable("SEVERITY", "LICENSE", "DIRECT DEPENDENCIES", "IMPACTED DEPENDENCY").SetDelimiter(writer.Separator())
- for _, license := range licenses {
- table.AddRowWithCellData(
- NewCellData(license.Severity),
- NewCellData(license.LicenseKey),
- getDirectDependenciesCellData("%s %s", license.Components),
- NewCellData(fmt.Sprintf("%s %s", license.ImpactedDependencyName, license.ImpactedDependencyVersion)),
- )
+ if summary != "" {
+ WriteNewLine(&contentBuilder)
+ WriteContent(&contentBuilder, summary)
}
- WriteContent(&contentBuilder, writer.MarkInCenter(table.Build()))
- return contentBuilder.String()
-}
-// For review comment Frogbot creates on Scan PR
-func GenerateReviewCommentContent(content string, writer OutputWriter) string {
- var contentBuilder strings.Builder
- contentBuilder.WriteString(MarkdownComment(ReviewCommentId))
- customCommentTitle := writer.PullRequestCommentTitle()
- if customCommentTitle != "" {
- WriteContent(&contentBuilder, writer.MarkAsTitle(MarkAsBold(customCommentTitle), 2))
+ // Jfrog Research Details
+ if issue.JfrogResearchInformation == nil || (issue.JfrogResearchInformation.Details == "" && issue.JfrogResearchInformation.Remediation == "") {
+ return contentBuilder.String()
}
- WriteContent(&contentBuilder, content, footer(writer))
- return contentBuilder.String()
-}
+ WriteNewLine(&contentBuilder)
+ WriteContent(&contentBuilder, writer.MarkAsTitle(jfrogResearchDetailsSubTitle, 3))
-// When can't create review comment, create a fallback comment by adding the location description to the content as a prefix
-func GetFallbackReviewCommentContent(content string, location formats.Location, writer OutputWriter) string {
- var contentBuilder strings.Builder
- contentBuilder.WriteString(MarkdownComment(ReviewCommentId))
- WriteContent(&contentBuilder, getFallbackCommentLocationDescription(location), content)
- return contentBuilder.String()
-}
-
-func IsFrogbotComment(content string) bool {
- return strings.Contains(content, ReviewCommentId)
-}
+ if issue.JfrogResearchInformation.Details != "" {
+ WriteNewLine(&contentBuilder)
+ WriteContent(&contentBuilder, MarkAsBold("Description:"), issue.JfrogResearchInformation.Details)
+ }
+ if issue.JfrogResearchInformation.Remediation != "" {
+ WriteNewLine(&contentBuilder)
+ WriteContent(&contentBuilder, MarkAsBold("Remediation:"), issue.JfrogResearchInformation.Remediation)
+ }
-func getFallbackCommentLocationDescription(location formats.Location) string {
- return fmt.Sprintf("%s\nat %s (line %d)", MarkAsCodeSnippet(location.Snippet), MarkAsQuote(location.File), location.StartLine)
+ return contentBuilder.String() + "\n"
}
-func GetApplicabilityDescriptionTable(severity, cve, impactedDependency, finding string, writer OutputWriter) string {
- table := NewMarkdownTable("Severity", "Impacted Dependency", "Finding", "CVE").AddRow(writer.FormattedSeverity(severity, "Applicable"), impactedDependency, finding, cve)
+func getJasIssueDescriptionTable(writer OutputWriter, issues ...formats.SourceCodeRow) string {
+ // Construct table
+ table := NewMarkdownTable("Severity", "ID", "Finding", "Watch Name", "Policies").SetDelimiter(writer.Separator())
+ // Hide optional columns if all empty (not violations)
+ table.GetColumnInfo("ID").OmitEmpty = true
+ table.GetColumnInfo("Watch Name").OmitEmpty = true
+ table.GetColumnInfo("Policies").OmitEmpty = true
+ // Construct rows
+ for _, issue := range issues {
+ table.AddRowWithCellData(
+ NewCellData(writer.FormattedSeverity(issue.Severity, "Applicable")),
+ NewCellData(issue.IssueId),
+ NewCellData(issue.Finding),
+ NewCellData(issue.Watch),
+ NewCellData(issue.Policies...),
+ )
+ }
return table.Build()
}
-func ApplicableCveReviewContent(severity, finding, fullDetails, cve, cveDetails, impactedDependency, remediation string, writer OutputWriter) string {
+// For Jas we show description for each unique rule
+func getJasFullDescription(violations bool, writer OutputWriter, generateRuleTable func(formats.ScannerInfo, OutputWriter) *MarkdownTableBuilder, issues ...formats.SourceCodeRow) string {
+ // Group by scanner info
+ rulesInfo, codeFlows := groupIssuesByScanner(issues...)
+ // Write the details for each rule
var contentBuilder strings.Builder
- WriteContent(&contentBuilder,
- writer.MarkAsTitle(contextualAnalysisTitle, 2),
- writer.MarkInCenter(GetApplicabilityDescriptionTable(severity, cve, impactedDependency, finding, writer)),
- writer.MarkAsDetails("Description", 3, fullDetails),
- writer.MarkAsDetails("CVE details", 3, cveDetails),
- )
-
- if len(remediation) > 0 {
- WriteContent(&contentBuilder, writer.MarkAsDetails("Remediation", 3, remediation))
+ for _, info := range rulesInfo {
+ var scannerCodeFlows [][]formats.Location
+ if v, ok := codeFlows[info.RuleId]; ok {
+ scannerCodeFlows = v
+ }
+ title := "Full description"
+ if len(rulesInfo) > 1 {
+ title = getJasDetailsIdentifier(info)
+ }
+ WriteContent(&contentBuilder, writer.MarkAsDetails(title, 3, getJasRuleFullDescription(violations, info.ScannerDescription, generateRuleTable(info, writer), writer, scannerCodeFlows...)))
}
return contentBuilder.String()
}
-func getJasDescriptionTable(severity, finding string, writer OutputWriter) string {
- return NewMarkdownTable("Severity", "Finding").AddRow(writer.FormattedSeverity(severity, jasutils.Applicable.String()), finding).Build()
-}
-
-func getSecretsDescriptionTable(severity, finding, status string, writer OutputWriter) string {
- columns := []string{"Severity", "Finding"}
- applicability := jasutils.Applicable.String()
- if status != "" {
- columns = append(columns, "Status")
- if status == jasutils.Inactive.String() {
- applicability = jasutils.NotApplicable.String()
+func groupIssuesByScanner(issues ...formats.SourceCodeRow) (rulesInfo []formats.ScannerInfo, codeFlows map[string][][]formats.Location) {
+ rulesInfoMap := map[string]formats.ScannerInfo{}
+ codeFlows = map[string][][]formats.Location{}
+ for _, issue := range issues {
+ if _, ok := rulesInfoMap[issue.RuleId]; ok {
+ codeFlows[issue.RuleId] = append(codeFlows[issue.RuleId], issue.CodeFlow...)
+ continue
}
- return NewMarkdownTable(columns...).AddRow(writer.FormattedSeverity(severity, applicability), finding, status).Build()
+ rulesInfoMap[issue.RuleId] = issue.ScannerInfo
+ codeFlows[issue.RuleId] = issue.CodeFlow
}
- return NewMarkdownTable(columns...).AddRow(writer.FormattedSeverity(severity, applicability), finding).Build()
-}
-
-func SecretReviewContent(severity, finding, fullDetails, applicability string, writer OutputWriter) string {
- var contentBuilder strings.Builder
- WriteContent(&contentBuilder,
- writer.MarkAsTitle(secretsTitle, 2),
- writer.MarkInCenter(getSecretsDescriptionTable(severity, finding, applicability, writer)),
- writer.MarkAsDetails("Full description", 3, fullDetails),
- )
- return contentBuilder.String()
+ rulesInfo = maps.Values(rulesInfoMap)
+ // Sort by rule id
+ sort.Slice(rulesInfo, func(i, j int) bool {
+ return rulesInfo[i].RuleId < rulesInfo[j].RuleId
+ })
+ return
}
-func IacReviewContent(severity, finding, fullDetails string, writer OutputWriter) string {
- var contentBuilder strings.Builder
- WriteContent(&contentBuilder,
- writer.MarkAsTitle(iacTitle, 2),
- writer.MarkInCenter(getJasDescriptionTable(severity, finding, writer)),
- writer.MarkAsDetails("Full description", 3, fullDetails),
- )
- return contentBuilder.String()
+func getJasDetailsIdentifier(info formats.ScannerInfo) string {
+ id := info.RuleId
+ if info.ScannerShortDescription != "" {
+ id = info.ScannerShortDescription
+ }
+ return fmt.Sprintf("[ %s ]", id)
}
-func SastReviewContent(severity, finding, fullDetails string, codeFlows [][]formats.Location, writer OutputWriter) string {
+func getJasRuleFullDescription(violation bool, scannerDescription string, issueDescTable *MarkdownTableBuilder, writer OutputWriter, codeFlows ...[]formats.Location) string {
var contentBuilder strings.Builder
- WriteContent(&contentBuilder,
- writer.MarkAsTitle(sastTitle, 2),
- writer.MarkInCenter(getJasDescriptionTable(severity, finding, writer)),
- writer.MarkAsDetails("Full description", 3, fullDetails),
- )
-
+ // Separator
+ WriteNewLine(&contentBuilder)
+ // Write the vulnerability/violation details
+ WriteContent(&contentBuilder, writer.MarkAsTitle(fmt.Sprintf("%s Details", getIssueType(violation)), 3))
+ if issueDescTable != nil && issueDescTable.HasContent() {
+ WriteContent(&contentBuilder, issueDescTable.Build())
+ // Separator
+ WriteNewLine(&contentBuilder)
+ }
+ // Write the description
+ WriteContent(&contentBuilder, scannerDescription, "\n")
+ // Write the code flows if exists
if len(codeFlows) > 0 {
- WriteContent(&contentBuilder, writer.MarkAsDetails("Code Flows", 3, sastCodeFlowsReviewContent(codeFlows, writer)))
+ WriteContent(&contentBuilder, codeFlowsReviewContent(codeFlows, writer))
}
return contentBuilder.String()
}
-func sastCodeFlowsReviewContent(codeFlows [][]formats.Location, writer OutputWriter) string {
+func codeFlowsReviewContent(codeFlows [][]formats.Location, writer OutputWriter) string {
+ if len(codeFlows) == 0 {
+ return ""
+ }
var contentBuilder strings.Builder
for _, flow := range codeFlows {
- WriteContent(&contentBuilder, writer.MarkAsDetails("Vulnerable data flow analysis result", 4, sastDataFlowLocationsReviewContent(flow)))
+ WriteContent(&contentBuilder, writer.MarkAsDetails("Vulnerable data flow analysis result", 4, dataFlowLocationsReviewContent(flow)))
}
- return contentBuilder.String()
+ return writer.MarkAsDetails("Code Flows", 3, contentBuilder.String())
}
-func sastDataFlowLocationsReviewContent(flow []formats.Location) string {
+func dataFlowLocationsReviewContent(flow []formats.Location) string {
var contentBuilder strings.Builder
- for _, location := range flow {
+ for i, location := range flow {
+ if i == 0 {
+ WriteNewLine(&contentBuilder)
+ }
WriteContent(&contentBuilder, fmt.Sprintf("%s %s (at %s line %d)\n", "↘️", MarkAsQuote(location.Snippet), location.File, location.StartLine))
}
return contentBuilder.String()
}
+
+func getBaseJasDetailsTable(ruleInfo formats.ScannerInfo, writer OutputWriter) *MarkdownTableBuilder {
+ noHeaderTable := NewNoHeaderMarkdownTable(2, false).SetDelimiter(writer.Separator())
+ // General CWE attribute if exists
+ if len(ruleInfo.Cwe) > 0 {
+ noHeaderTable.AddRowWithCellData(NewCellData(MarkAsBold("CWE:")), NewCellData(ruleInfo.Cwe...))
+ }
+ return noHeaderTable
+}
diff --git a/utils/outputwriter/outputcontent_test.go b/utils/outputwriter/outputcontent_test.go
index 51945f453..29222ef0c 100644
--- a/utils/outputwriter/outputcontent_test.go
+++ b/utils/outputwriter/outputcontent_test.go
@@ -4,14 +4,18 @@ import (
"path/filepath"
"testing"
+ "github.com/jfrog/frogbot/v2/utils/issues"
"github.com/jfrog/froggit-go/vcsutils"
+ "github.com/jfrog/jfrog-cli-security/utils"
"github.com/jfrog/jfrog-cli-security/utils/formats"
"github.com/jfrog/jfrog-cli-security/utils/jasutils"
+ "github.com/jfrog/jfrog-cli-security/utils/results"
"github.com/jfrog/jfrog-cli-security/utils/severityutils"
+ xrayApi "github.com/jfrog/jfrog-client-go/xray/services/utils"
"github.com/stretchr/testify/assert"
)
-func TestGetPRSummaryContent(t *testing.T) {
+func TestGetMainCommentContent(t *testing.T) {
testCases := []struct {
name string
cases []OutputTestCase
@@ -19,7 +23,7 @@ func TestGetPRSummaryContent(t *testing.T) {
isComment bool
}{
{
- name: "Summary comment No issues found",
+ name: "Main comment No issues found",
issuesExists: false,
isComment: true,
cases: []OutputTestCase{
@@ -76,7 +80,7 @@ func TestGetPRSummaryContent(t *testing.T) {
},
},
{
- name: "Summary comment Found issues",
+ name: "Main comment Found issues",
issuesExists: true,
isComment: true,
cases: []OutputTestCase{
@@ -185,7 +189,7 @@ func TestGetPRSummaryContent(t *testing.T) {
for _, test := range tc.cases {
t.Run(tc.name+"_"+test.name, func(t *testing.T) {
expectedOutput := GetExpectedTestOutput(t, test)
- output := GetPRSummaryContent([]string{MarkAsCodeSnippet("some content")}, tc.issuesExists, tc.isComment, test.writer)
+ output := GetMainCommentContent([]string{MarkAsCodeSnippet("some content")}, tc.issuesExists, tc.isComment, test.writer)
assert.Len(t, output, 1)
assert.Equal(t, expectedOutput, output[0])
})
@@ -193,6 +197,154 @@ func TestGetPRSummaryContent(t *testing.T) {
}
}
+func TestScanSummaryContent(t *testing.T) {
+ testScanStatus := formats.ScanStatus{
+ ScaStatusCode: utils.NewIntPtr(0),
+ ApplicabilityStatusCode: utils.NewIntPtr(0),
+ SastStatusCode: utils.NewIntPtr(0),
+ SecretsStatusCode: utils.NewIntPtr(0),
+ }
+ testIssues := issues.ScansIssuesCollection{
+ ScaVulnerabilities: []formats.VulnerabilityOrViolationRow{
+ {ImpactedDependencyDetails: formats.ImpactedDependencyDetails{SeverityDetails: formats.SeverityDetails{Severity: "Critical"}}},
+ {ImpactedDependencyDetails: formats.ImpactedDependencyDetails{SeverityDetails: formats.SeverityDetails{Severity: "High"}}},
+ {ImpactedDependencyDetails: formats.ImpactedDependencyDetails{SeverityDetails: formats.SeverityDetails{Severity: "High"}}},
+ {ImpactedDependencyDetails: formats.ImpactedDependencyDetails{SeverityDetails: formats.SeverityDetails{Severity: "Medium"}}},
+ {ImpactedDependencyDetails: formats.ImpactedDependencyDetails{SeverityDetails: formats.SeverityDetails{Severity: "Low"}}},
+ {ImpactedDependencyDetails: formats.ImpactedDependencyDetails{SeverityDetails: formats.SeverityDetails{Severity: "Unknown"}}},
+ },
+ ScaViolations: []formats.VulnerabilityOrViolationRow{
+ {ImpactedDependencyDetails: formats.ImpactedDependencyDetails{SeverityDetails: formats.SeverityDetails{Severity: "Critical"}}},
+ },
+ LicensesViolations: []formats.LicenseViolationRow{
+ {LicenseRow: formats.LicenseRow{ImpactedDependencyDetails: formats.ImpactedDependencyDetails{SeverityDetails: formats.SeverityDetails{Severity: "High"}}}},
+ {LicenseRow: formats.LicenseRow{ImpactedDependencyDetails: formats.ImpactedDependencyDetails{SeverityDetails: formats.SeverityDetails{Severity: "Medium"}}}},
+ },
+ SecretsVulnerabilities: []formats.SourceCodeRow{
+ {SeverityDetails: formats.SeverityDetails{Severity: "High"}},
+ {SeverityDetails: formats.SeverityDetails{Severity: "High"}},
+ },
+ SastVulnerabilities: []formats.SourceCodeRow{
+ {SeverityDetails: formats.SeverityDetails{Severity: "High"}},
+ {SeverityDetails: formats.SeverityDetails{Severity: "High"}},
+ {SeverityDetails: formats.SeverityDetails{Severity: "Low"}},
+ },
+ SastViolations: []formats.SourceCodeRow{{SeverityDetails: formats.SeverityDetails{Severity: "High"}}},
+ }
+
+ testCases := []struct {
+ name string
+ includeSecrets bool
+ scanStatus formats.ScanStatus
+ context results.ResultContext
+ issues issues.ScansIssuesCollection
+ cases []OutputTestCase
+ }{
+ {
+ name: "No issues",
+ issues: issues.ScansIssuesCollection{},
+ scanStatus: testScanStatus,
+ cases: []OutputTestCase{
+ {
+ name: "Standard output",
+ writer: &StandardOutput{},
+ expectedOutput: []string{""},
+ },
+ {
+ name: "Simplified output",
+ writer: &SimplifiedOutput{},
+ expectedOutput: []string{""},
+ },
+ },
+ },
+ {
+ name: "Vulnerabilities",
+ issues: testIssues,
+ scanStatus: testScanStatus,
+ context: results.ResultContext{IncludeVulnerabilities: true},
+ cases: []OutputTestCase{
+ {
+ name: "Standard output",
+ writer: &StandardOutput{MarkdownOutput{hasInternetConnection: true}},
+ expectedOutputPath: []string{filepath.Join(testSummaryCommentDir, "summary", "summary_standard.md")},
+ },
+ {
+ name: "Simplified output",
+ writer: &SimplifiedOutput{MarkdownOutput{hasInternetConnection: true}},
+ expectedOutputPath: []string{filepath.Join(testSummaryCommentDir, "summary", "summary_simplified.md")},
+ },
+ },
+ },
+ {
+ name: "Violations",
+ issues: testIssues,
+ scanStatus: testScanStatus,
+ context: results.ResultContext{Watches: []string{"watch"}},
+ cases: []OutputTestCase{
+ {
+ name: "Standard output",
+ writer: &StandardOutput{MarkdownOutput{hasInternetConnection: true}},
+ expectedOutputPath: []string{filepath.Join(testSummaryCommentDir, "summary", "summary_violation_standard.md")},
+ },
+ {
+ name: "Simplified output",
+ writer: &SimplifiedOutput{MarkdownOutput{hasInternetConnection: true}},
+ expectedOutputPath: []string{filepath.Join(testSummaryCommentDir, "summary", "summary_violation_simplified.md")},
+ },
+ },
+ },
+ {
+ name: "Violations and Vulnerabilities",
+ issues: testIssues,
+ scanStatus: testScanStatus,
+ context: results.ResultContext{GitRepoHttpsCloneUrl: "url", PlatformWatches: &xrayApi.ResourcesWatchesBody{GitRepositoryWatches: []string{"watch"}}, IncludeVulnerabilities: true},
+ cases: []OutputTestCase{
+ {
+ name: "Standard output",
+ writer: &StandardOutput{MarkdownOutput{hasInternetConnection: true}},
+ expectedOutputPath: []string{filepath.Join(testSummaryCommentDir, "summary", "summary_both_standard.md")},
+ },
+ {
+ name: "Simplified output",
+ writer: &SimplifiedOutput{MarkdownOutput{hasInternetConnection: true}},
+ expectedOutputPath: []string{filepath.Join(testSummaryCommentDir, "summary", "summary_both_simplified.md")},
+ },
+ },
+ },
+ {
+ name: "with errors",
+ issues: issues.ScansIssuesCollection{},
+ scanStatus: formats.ScanStatus{
+ IacStatusCode: utils.NewIntPtr(33),
+ },
+ context: results.ResultContext{GitRepoHttpsCloneUrl: "url", PlatformWatches: &xrayApi.ResourcesWatchesBody{GitRepositoryWatches: []string{"watch"}}},
+ cases: []OutputTestCase{
+ {
+ name: "Standard output",
+ writer: &StandardOutput{MarkdownOutput{hasInternetConnection: true}},
+ expectedOutputPath: []string{filepath.Join(testSummaryCommentDir, "summary", "summary_error_standard.md")},
+ },
+ {
+ name: "Simplified output",
+ writer: &SimplifiedOutput{MarkdownOutput{hasInternetConnection: true}},
+ expectedOutputPath: []string{filepath.Join(testSummaryCommentDir, "summary", "summary_error_simplified.md")},
+ },
+ },
+ },
+ }
+
+ for _, tc := range testCases {
+ for _, test := range tc.cases {
+ t.Run(tc.name+"_"+test.name, func(t *testing.T) {
+ expectedOutput := GetExpectedTestOutput(t, test)
+ tc.issues.ScanStatus = tc.scanStatus
+ output := ScanSummaryContent(tc.issues, tc.context, tc.includeSecrets, test.writer)
+ assert.Equal(t, expectedOutput, output)
+ })
+ }
+ }
+}
+
func TestVulnerabilitiesContent(t *testing.T) {
testCases := []struct {
name string
@@ -287,96 +439,22 @@ func TestVulnerabilitiesContent(t *testing.T) {
},
},
{
- name: "multiple Vulnerabilities with Contextual Analysis",
- vulnerabilities: []formats.VulnerabilityOrViolationRow{
- {
- ImpactedDependencyDetails: formats.ImpactedDependencyDetails{
- SeverityDetails: severityutils.GetAsDetails(severityutils.Critical, jasutils.NotApplicable, false),
- ImpactedDependencyName: "impacted",
- ImpactedDependencyVersion: "3.0.0",
- Components: []formats.ComponentRow{
- {Name: "dep1", Version: "1.0.0"},
- {Name: "dep2", Version: "2.0.0"},
- },
- },
- Applicable: "Not Applicable",
- FixedVersions: []string{"4.0.0", "5.0.0"},
- Cves: []formats.CveRow{{Id: "CVE-1111-11111", Applicability: &formats.Applicability{Status: "Not Applicable"}}},
- },
- {
- Summary: "Summary XRAY-122345",
- ImpactedDependencyDetails: formats.ImpactedDependencyDetails{
- SeverityDetails: severityutils.GetAsDetails(severityutils.High, jasutils.ApplicabilityUndetermined, false),
- ImpactedDependencyName: "github.com/nats-io/nats-streaming-server",
- ImpactedDependencyVersion: "v0.21.0",
- Components: []formats.ComponentRow{
- {
- Name: "github.com/nats-io/nats-streaming-server",
- Version: "v0.21.0",
- },
- },
- },
- Applicable: "Undetermined",
- FixedVersions: []string{"[0.24.1]"},
- IssueId: "XRAY-122345",
- JfrogResearchInformation: &formats.JfrogResearchInformation{
- Remediation: "some remediation",
- },
- Cves: []formats.CveRow{{}},
- },
- {
- ImpactedDependencyDetails: formats.ImpactedDependencyDetails{
- SeverityDetails: severityutils.GetAsDetails(severityutils.Medium, jasutils.Applicable, false),
- ImpactedDependencyName: "component-D",
- ImpactedDependencyVersion: "v0.21.0",
- Components: []formats.ComponentRow{
- {
- Name: "component-D",
- Version: "v0.21.0",
- },
- },
- },
- Applicable: "Applicable",
- FixedVersions: []string{"[0.24.3]"},
- JfrogResearchInformation: &formats.JfrogResearchInformation{
- Remediation: "some remediation",
- },
- Cves: []formats.CveRow{
- {Id: "CVE-2022-26652"},
- {Id: "CVE-2023-4321", Applicability: &formats.Applicability{Status: "Applicable"}},
- },
- },
- {
- Summary: "Summary",
- ImpactedDependencyDetails: formats.ImpactedDependencyDetails{
- SeverityDetails: severityutils.GetAsDetails(severityutils.Low, jasutils.ApplicabilityUndetermined, false),
- ImpactedDependencyName: "github.com/mholt/archiver/v3",
- ImpactedDependencyVersion: "v3.5.1",
- Components: []formats.ComponentRow{
- {
- Name: "github.com/mholt/archiver/v3",
- Version: "v3.5.1",
- },
- },
- },
- Applicable: "Undetermined",
- Cves: []formats.CveRow{},
- },
- },
+ name: "multiple Vulnerabilities with Contextual Analysis",
+ vulnerabilities: getTestScaIssues(false),
cases: []OutputTestCase{
{
name: "Standard output",
- writer: &StandardOutput{MarkdownOutput{showCaColumn: true}},
+ writer: &StandardOutput{MarkdownOutput{showCaColumn: true, hasInternetConnection: true}},
expectedOutputPath: []string{filepath.Join(testSummaryCommentDir, "vulnerabilities", "vulnerabilities_standard.md")},
},
{
name: "Simplified output",
- writer: &SimplifiedOutput{MarkdownOutput{showCaColumn: true}},
+ writer: &SimplifiedOutput{MarkdownOutput{showCaColumn: true, hasInternetConnection: true}},
expectedOutputPath: []string{filepath.Join(testSummaryCommentDir, "vulnerabilities", "vulnerabilities_simplified.md")},
},
{
name: "Split Standard output",
- writer: &StandardOutput{MarkdownOutput{showCaColumn: true, descriptionSizeLimit: 1720, commentSizeLimit: 1720}},
+ writer: &StandardOutput{MarkdownOutput{showCaColumn: true, hasInternetConnection: true, descriptionSizeLimit: 1720, commentSizeLimit: 1720}},
expectedOutputPath: []string{
filepath.Join(testSummaryCommentDir, "vulnerabilities", "vulnerabilities_standard_split1.md"),
filepath.Join(testSummaryCommentDir, "vulnerabilities", "vulnerabilities_standard_split2.md"),
@@ -384,7 +462,7 @@ func TestVulnerabilitiesContent(t *testing.T) {
},
{
name: "Split Simplified output",
- writer: &SimplifiedOutput{MarkdownOutput{showCaColumn: true, descriptionSizeLimit: 1000, commentSizeLimit: 1000}},
+ writer: &SimplifiedOutput{MarkdownOutput{showCaColumn: true, hasInternetConnection: true, descriptionSizeLimit: 1000, commentSizeLimit: 2000}},
expectedOutputPath: []string{
filepath.Join(testSummaryCommentDir, "vulnerabilities", "vulnerabilities_simplified_split1.md"),
filepath.Join(testSummaryCommentDir, "vulnerabilities", "vulnerabilities_simplified_split2.md"),
@@ -392,15 +470,63 @@ func TestVulnerabilitiesContent(t *testing.T) {
},
},
},
+ }
+ for _, tc := range testCases {
+ for _, test := range tc.cases {
+ t.Run(tc.name+"_"+test.name, func(t *testing.T) {
+ expectedOutput := GetExpectedTestCaseOutput(t, test)
+ output := ConvertContentToComments(GetVulnerabilitiesContent(tc.vulnerabilities, test.writer), test.writer)
+ assert.Len(t, output, len(expectedOutput))
+ assert.ElementsMatch(t, expectedOutput, output)
+ })
+ }
+ }
+}
+
+func TestSecurityViolationsContent(t *testing.T) {
+ testCases := []struct {
+ name string
+ issues issues.ScansIssuesCollection
+ cases []OutputTestCase
+ }{
{
- name: "Split vulnerabilities content",
+ name: "No security violations",
+ issues: issues.ScansIssuesCollection{},
+ cases: []OutputTestCase{
+ {
+ name: "Standard output",
+ writer: &StandardOutput{},
+ expectedOutput: []string{""},
+ },
+ {
+ name: "Simplified output",
+ writer: &SimplifiedOutput{},
+ expectedOutput: []string{""},
+ },
+ },
+ },
+ {
+ name: "Security violations",
+ issues: issues.ScansIssuesCollection{ScaViolations: getTestScaIssues(true)},
+ cases: []OutputTestCase{
+ {
+ name: "Standard output",
+ writer: &StandardOutput{MarkdownOutput{showCaColumn: true, hasInternetConnection: true}},
+ expectedOutputPath: []string{filepath.Join(testSummaryCommentDir, "violations", "security", "security_violation_standard.md")},
+ },
+ {
+ name: "Simplified output",
+ writer: &SimplifiedOutput{MarkdownOutput{showCaColumn: true, hasInternetConnection: true}},
+ expectedOutputPath: []string{filepath.Join(testSummaryCommentDir, "violations", "security", "security_violation_simplified.md")},
+ },
+ },
},
}
for _, tc := range testCases {
for _, test := range tc.cases {
t.Run(tc.name+"_"+test.name, func(t *testing.T) {
expectedOutput := GetExpectedTestCaseOutput(t, test)
- output := ConvertContentToComments(VulnerabilitiesContent(tc.vulnerabilities, test.writer), test.writer)
+ output := ConvertContentToComments(getSecurityViolationsContent(tc.issues, test.writer), test.writer)
assert.Len(t, output, len(expectedOutput))
assert.ElementsMatch(t, expectedOutput, output)
})
@@ -408,74 +534,173 @@ func TestVulnerabilitiesContent(t *testing.T) {
}
}
+func getTestScaIssues(violations bool) []formats.VulnerabilityOrViolationRow {
+ issues := []formats.VulnerabilityOrViolationRow{
+ {
+ ImpactedDependencyDetails: formats.ImpactedDependencyDetails{
+ SeverityDetails: severityutils.GetAsDetails(severityutils.Critical, jasutils.NotApplicable, false),
+ ImpactedDependencyName: "impacted",
+ ImpactedDependencyVersion: "3.0.0",
+ Components: []formats.ComponentRow{
+ {Name: "dep1", Version: "1.0.0"},
+ {Name: "dep2", Version: "2.0.0"},
+ },
+ },
+ Applicable: "Not Applicable",
+ FixedVersions: []string{"4.0.0", "5.0.0"},
+ Cves: []formats.CveRow{{Id: "CVE-1111-11111", Applicability: &formats.Applicability{Status: "Not Applicable"}}},
+ },
+ {
+ Summary: "Summary XRAY-122345",
+ ImpactedDependencyDetails: formats.ImpactedDependencyDetails{
+ SeverityDetails: severityutils.GetAsDetails(severityutils.High, jasutils.ApplicabilityUndetermined, false),
+ ImpactedDependencyName: "github.com/nats-io/nats-streaming-server",
+ ImpactedDependencyVersion: "v0.21.0",
+ Components: []formats.ComponentRow{
+ {
+ Name: "github.com/nats-io/nats-streaming-server",
+ Version: "v0.21.0",
+ },
+ },
+ },
+ Applicable: "Undetermined",
+ FixedVersions: []string{"[0.24.1]"},
+ IssueId: "XRAY-122345",
+ JfrogResearchInformation: &formats.JfrogResearchInformation{
+ Remediation: "some remediation",
+ },
+ Cves: []formats.CveRow{},
+ },
+ {
+ ImpactedDependencyDetails: formats.ImpactedDependencyDetails{
+ SeverityDetails: severityutils.GetAsDetails(severityutils.Medium, jasutils.Applicable, false),
+ ImpactedDependencyName: "component-D",
+ ImpactedDependencyVersion: "v0.21.0",
+ Components: []formats.ComponentRow{
+ {
+ Name: "component-D",
+ Version: "v0.21.0",
+ },
+ },
+ },
+ Applicable: "Applicable",
+ FixedVersions: []string{"[0.24.3]"},
+ JfrogResearchInformation: &formats.JfrogResearchInformation{
+ Remediation: "some remediation",
+ },
+ Cves: []formats.CveRow{
+ {Id: "CVE-2022-26652"},
+ {Id: "CVE-2023-4321", Applicability: &formats.Applicability{Status: "Applicable"}},
+ },
+ },
+ {
+ Summary: "Summary",
+ ImpactedDependencyDetails: formats.ImpactedDependencyDetails{
+ SeverityDetails: severityutils.GetAsDetails(severityutils.Low, jasutils.ApplicabilityUndetermined, false),
+ ImpactedDependencyName: "github.com/mholt/archiver/v3",
+ ImpactedDependencyVersion: "v3.5.1",
+ Components: []formats.ComponentRow{
+ {
+ Name: "github.com/mholt/archiver/v3",
+ Version: "v3.5.1",
+ },
+ },
+ },
+ Applicable: "Undetermined",
+ Cves: []formats.CveRow{},
+ },
+ }
+ if violations {
+ for _, issue := range issues {
+ issue.ViolationContext = formats.ViolationContext{
+ Watch: "watch",
+ Policies: []string{"policy1", "policy2"},
+ }
+ }
+ }
+ return issues
+}
+
func TestLicensesContent(t *testing.T) {
testCases := []struct {
name string
- licenses []formats.LicenseRow
+ licenses []formats.LicenseViolationRow
cases []OutputTestCase
}{
{
name: "No license violations",
- licenses: []formats.LicenseRow{},
+ licenses: []formats.LicenseViolationRow{},
cases: []OutputTestCase{
{
name: "Standard output",
writer: &StandardOutput{},
- expectedOutput: []string{""},
+ expectedOutput: []string{},
},
{
name: "Simplified output",
writer: &SimplifiedOutput{},
- expectedOutput: []string{""},
+ expectedOutput: []string{},
},
},
},
{
name: "License violations",
- licenses: []formats.LicenseRow{
- {
- LicenseKey: "License1",
- ImpactedDependencyDetails: formats.ImpactedDependencyDetails{
-
- Components: []formats.ComponentRow{{Name: "Comp1", Version: "1.0"}},
- ImpactedDependencyName: "Dep1",
- ImpactedDependencyVersion: "2.0",
- SeverityDetails: formats.SeverityDetails{
- Severity: "High",
+ licenses: []formats.LicenseViolationRow{
+ {
+ LicenseRow: formats.LicenseRow{
+ LicenseKey: "License1",
+ LicenseName: "License1 full name",
+ ImpactedDependencyDetails: formats.ImpactedDependencyDetails{
+ Components: []formats.ComponentRow{{Name: "Comp1", Version: "1.0"}},
+ ImpactedDependencyName: "Dep1",
+ ImpactedDependencyVersion: "2.0",
+ SeverityDetails: formats.SeverityDetails{
+ Severity: "High",
+ },
},
},
+ ViolationContext: formats.ViolationContext{
+ Watch: "watch",
+ Policies: []string{"policy1", "policy2"},
+ },
},
{
- LicenseKey: "License2",
- ImpactedDependencyDetails: formats.ImpactedDependencyDetails{
- Components: []formats.ComponentRow{
- {
- Name: "root",
- Version: "1.0.0",
+ LicenseRow: formats.LicenseRow{
+ LicenseKey: "License2",
+ ImpactedDependencyDetails: formats.ImpactedDependencyDetails{
+ Components: []formats.ComponentRow{
+ {
+ Name: "root",
+ Version: "1.0.0",
+ },
+ {
+ Name: "minimatch",
+ Version: "1.2.3",
+ },
},
- {
- Name: "minimatch",
- Version: "1.2.3",
+ ImpactedDependencyName: "Dep2",
+ ImpactedDependencyVersion: "3.0",
+ SeverityDetails: formats.SeverityDetails{
+ Severity: "High",
},
},
- ImpactedDependencyName: "Dep2",
- ImpactedDependencyVersion: "3.0",
- SeverityDetails: formats.SeverityDetails{
- Severity: "High",
- },
+ },
+ ViolationContext: formats.ViolationContext{
+ Watch: "watch2",
+ Policies: []string{"policy3"},
},
},
},
cases: []OutputTestCase{
{
name: "Standard output",
- writer: &StandardOutput{},
- expectedOutputPath: []string{filepath.Join(testSummaryCommentDir, "license", "license_violation_standard.md")},
+ writer: &StandardOutput{MarkdownOutput{hasInternetConnection: true}},
+ expectedOutputPath: []string{filepath.Join(testSummaryCommentDir, "violations", "license", "license_violation_standard.md")},
},
{
name: "Simplified output",
- writer: &SimplifiedOutput{},
- expectedOutputPath: []string{filepath.Join(testSummaryCommentDir, "license", "license_violation_simplified.md")},
+ writer: &SimplifiedOutput{MarkdownOutput{hasInternetConnection: true}},
+ expectedOutputPath: []string{filepath.Join(testSummaryCommentDir, "violations", "license", "license_violation_simplified.md")},
},
},
},
@@ -483,7 +708,8 @@ func TestLicensesContent(t *testing.T) {
for _, tc := range testCases {
for _, test := range tc.cases {
t.Run(tc.name+"_"+test.name, func(t *testing.T) {
- assert.Equal(t, GetExpectedTestOutput(t, test), LicensesContent(tc.licenses, test.writer))
+ expectedOutput := GetExpectedTestCaseOutput(t, test)
+ assert.Equal(t, expectedOutput, PolicyViolationsContent(issues.ScansIssuesCollection{LicensesViolations: tc.licenses}, test.writer))
})
}
}
@@ -567,7 +793,7 @@ func TestGenerateReviewComment(t *testing.T) {
expectedOutput := GetExpectedTestOutput(t, test)
output := GenerateReviewCommentContent(content, test.writer)
if tc.location != nil {
- output = GetFallbackReviewCommentContent(content, *tc.location, test.writer)
+ output = GetFallbackReviewCommentContent(content, *tc.location)
}
assert.Equal(t, expectedOutput, output)
})
@@ -578,39 +804,48 @@ func TestGenerateReviewComment(t *testing.T) {
func TestApplicableReviewContent(t *testing.T) {
testCases := []struct {
name string
+ issue issues.ApplicableEvidences
severity, finding, fullDetails, cve, cveDetails, impactedDependency, remediation string
cases []OutputTestCase
}{
{
- name: "Applicable CVE review comment content",
- severity: "Critical",
- finding: "The vulnerable function flask.Flask.run is called",
- fullDetails: "The scanner checks whether the vulnerable `Development Server` of the `werkzeug` library is used by looking for calls to `werkzeug.serving.run_simple()`.",
- cve: "CVE-2022-29361",
- cveDetails: "cveDetails",
- impactedDependency: "werkzeug:1.0.1",
- remediation: "some remediation",
+ name: "Applicable CVE review comment content",
+ issue: issues.ApplicableEvidences{
+ Severity: "Critical",
+ IssueId: "CVE-2022-29361",
+ ScannerDescription: "The scanner checks whether the vulnerable `Development Server` of the `werkzeug` library is used by looking for calls to `werkzeug.serving.run_simple()`.",
+ CveSummary: "cveDetails",
+ ImpactedDependency: "werkzeug:1.0.1",
+ Remediation: "some remediation",
+ Evidence: formats.Evidence{
+ Reason: "The vulnerable function flask.Flask.run is called",
+ },
+ },
cases: []OutputTestCase{
{
name: "Standard output",
- writer: &StandardOutput{},
+ writer: &StandardOutput{MarkdownOutput{hasInternetConnection: true}},
expectedOutputPath: []string{filepath.Join(testReviewCommentDir, "applicable", "applicable_review_content_standard.md")},
},
{
name: "Simplified output",
- writer: &SimplifiedOutput{},
+ writer: &SimplifiedOutput{MarkdownOutput{hasInternetConnection: true}},
expectedOutputPath: []string{filepath.Join(testReviewCommentDir, "applicable", "applicable_review_content_simplified.md")},
},
},
},
{
- name: "No remediation",
- severity: "Critical",
- finding: "The vulnerable function flask.Flask.run is called",
- fullDetails: "The scanner checks whether the vulnerable `Development Server` of the `werkzeug` library is used by looking for calls to `werkzeug.serving.run_simple()`.",
- cve: "CVE-2022-29361",
- cveDetails: "cveDetails",
- impactedDependency: "werkzeug:1.0.1",
+ name: "No remediation and internet connection",
+ issue: issues.ApplicableEvidences{
+ Severity: "Critical",
+ IssueId: "CVE-2022-29361",
+ ScannerDescription: "The scanner checks whether the vulnerable `Development Server` of the `werkzeug` library is used by looking for calls to `werkzeug.serving.run_simple()`.",
+ CveSummary: "cveDetails",
+ ImpactedDependency: "werkzeug:1.0.1",
+ Evidence: formats.Evidence{
+ Reason: "The vulnerable function flask.Flask.run is called",
+ },
+ },
cases: []OutputTestCase{
{
name: "Standard output",
@@ -630,7 +865,7 @@ func TestApplicableReviewContent(t *testing.T) {
for _, test := range tc.cases {
t.Run(tc.name+"_"+test.name, func(t *testing.T) {
expectedOutput := GetExpectedTestOutput(t, test)
- assert.Equal(t, expectedOutput, ApplicableCveReviewContent(tc.severity, tc.finding, tc.fullDetails, tc.cve, tc.cveDetails, tc.impactedDependency, tc.remediation, test.writer))
+ assert.Equal(t, expectedOutput, ApplicableCveReviewContent(tc.issue, test.writer))
})
}
}
@@ -638,54 +873,124 @@ func TestApplicableReviewContent(t *testing.T) {
func TestSecretsReviewContent(t *testing.T) {
testCases := []struct {
- name string
- severity, finding, fullDetails, status string
- cases []OutputTestCase
+ name string
+ issues []formats.SourceCodeRow
+ cases []OutputTestCase
}{
{
- name: "Secret review comment content",
- severity: "Medium",
- finding: "Secret keys were found",
- fullDetails: "Storing hardcoded secrets in your source code or binary artifact could lead to several risks.\n\nIf the secret is associated with a wide scope of privileges, attackers could extract it from the source code or binary artifact and use it maliciously to attack many targets. For example, if the hardcoded password gives high-privilege access to an AWS account, the attackers may be able to query/modify company-wide sensitive data without per-user authentication.\n\n## Best practices\n\nUse safe storage when storing high-privilege secrets such as passwords and tokens, for example -\n\n* ### Environment Variables\n\nEnvironment variables are set outside of the application code, and can be dynamically passed to the application only when needed, for example -\n`SECRET_VAR=MySecret ./my_application`\nThis way, `MySecret` does not have to be hardcoded into `my_application`.\n\nNote that if your entire binary artifact is published (ex. a Docker container published to Docker Hub), the value for the environment variable must not be stored in the artifact itself (ex. inside the `Dockerfile` or one of the container's files) but rather must be passed dynamically, for example in the `docker run` call as an argument.\n\n* ### Secret management services\n\nExternal vendors offer cloud-based secret management services, that provide proper access control to each secret. The given access to each secret can be dynamically modified or even revoked. Some examples include -\n\n* [Hashicorp Vault](https://www.vaultproject.io)\n* [AWS KMS](https://aws.amazon.com/kms) (Key Management Service)\n* [Google Cloud KMS](https://cloud.google.com/security-key-management)\n\n## Least-privilege principle\n\nStoring a secret in a hardcoded manner can be made safer, by making sure the secret grants the least amount of privilege as needed by the application.\nFor example - if the application needs to read a specific table from a specific database, and the secret grants access to perform this operation **only** (meaning - no access to other tables, no write access at all) then the damage from any secret leaks is mitigated.\nThat being said, it is still not recommended to store secrets in a hardcoded manner, since this type of storage does not offer any way to revoke or moderate the usage of the secret.\n",
+ name: "Secret review comment content",
+ issues: []formats.SourceCodeRow{{
+ SeverityDetails: formats.SeverityDetails{Severity: "High"},
+ Finding: "Secret keys were found",
+ ScannerInfo: formats.ScannerInfo{
+ RuleId: "rule-id",
+ Cwe: []string{"CWE-798", "CWE-799"},
+ ScannerDescription: "Scanner Description....",
+ ScannerShortDescription: "Scanner Short Description",
+ },
+ }},
cases: []OutputTestCase{
{
name: "Standard output",
- writer: &StandardOutput{},
+ writer: &StandardOutput{MarkdownOutput{hasInternetConnection: true}},
expectedOutputPath: []string{filepath.Join(testReviewCommentDir, "secrets", "secret_review_content_no_ca_standard.md")},
},
{
name: "Simplified output",
- writer: &SimplifiedOutput{},
+ writer: &SimplifiedOutput{MarkdownOutput{hasInternetConnection: true}},
expectedOutputPath: []string{filepath.Join(testReviewCommentDir, "secrets", "secret_review_content_no_ca_simplified.md")},
},
},
},
{
- name: "Secret review comment content with applicability status",
- severity: "Medium",
- status: "Active",
- finding: "Secret keys were found",
- fullDetails: "Storing hardcoded secrets in your source code or binary artifact could lead to several risks.\n\nIf the secret is associated with a wide scope of privileges, attackers could extract it from the source code or binary artifact and use it maliciously to attack many targets. For example, if the hardcoded password gives high-privilege access to an AWS account, the attackers may be able to query/modify company-wide sensitive data without per-user authentication.\n\n## Best practices\n\nUse safe storage when storing high-privilege secrets such as passwords and tokens, for example -\n\n* ### Environment Variables\n\nEnvironment variables are set outside of the application code, and can be dynamically passed to the application only when needed, for example -\n`SECRET_VAR=MySecret ./my_application`\nThis way, `MySecret` does not have to be hardcoded into `my_application`.\n\nNote that if your entire binary artifact is published (ex. a Docker container published to Docker Hub), the value for the environment variable must not be stored in the artifact itself (ex. inside the `Dockerfile` or one of the container's files) but rather must be passed dynamically, for example in the `docker run` call as an argument.\n\n* ### Secret management services\n\nExternal vendors offer cloud-based secret management services, that provide proper access control to each secret. The given access to each secret can be dynamically modified or even revoked. Some examples include -\n\n* [Hashicorp Vault](https://www.vaultproject.io)\n* [AWS KMS](https://aws.amazon.com/kms) (Key Management Service)\n* [Google Cloud KMS](https://cloud.google.com/security-key-management)\n\n## Least-privilege principle\n\nStoring a secret in a hardcoded manner can be made safer, by making sure the secret grants the least amount of privilege as needed by the application.\nFor example - if the application needs to read a specific table from a specific database, and the secret grants access to perform this operation **only** (meaning - no access to other tables, no write access at all) then the damage from any secret leaks is mitigated.\nThat being said, it is still not recommended to store secrets in a hardcoded manner, since this type of storage does not offer any way to revoke or moderate the usage of the secret.\n",
+ name: "Secret review comment content with applicability status",
+ issues: []formats.SourceCodeRow{{
+ SeverityDetails: formats.SeverityDetails{Severity: "High"},
+ Applicability: &formats.Applicability{Status: jasutils.Active.String()},
+ Finding: "Secret keys were found",
+ ScannerInfo: formats.ScannerInfo{
+ RuleId: "rule-id",
+ Cwe: []string{"CWE-798", "CWE-799"},
+ ScannerDescription: "Scanner Description....",
+ ScannerShortDescription: "Scanner Short Description",
+ },
+ }},
cases: []OutputTestCase{
{
name: "Standard output",
- writer: &StandardOutput{},
+ writer: &StandardOutput{MarkdownOutput{hasInternetConnection: true}},
expectedOutputPath: []string{filepath.Join(testReviewCommentDir, "secrets", "secret_review_content_standard.md")},
},
{
name: "Simplified output",
- writer: &SimplifiedOutput{},
+ writer: &SimplifiedOutput{MarkdownOutput{hasInternetConnection: true}},
expectedOutputPath: []string{filepath.Join(testReviewCommentDir, "secrets", "secret_review_content_simplified.md")},
},
},
},
+ {
+ name: "Secrets violation review comment content with applicability status",
+ issues: []formats.SourceCodeRow{
+ {
+ SeverityDetails: formats.SeverityDetails{Severity: "High"},
+ Applicability: &formats.Applicability{Status: jasutils.Active.String()},
+ Finding: "Secret keys were found",
+ ScannerInfo: formats.ScannerInfo{
+ RuleId: "rule-id",
+ Cwe: []string{"CWE-798", "CWE-799"},
+ ScannerDescription: "Scanner Description....",
+ ScannerShortDescription: "Scanner Short Description",
+ },
+ ViolationContext: formats.ViolationContext{
+ Watch: "jas-watch",
+ IssueId: "secret-violation-id",
+ Policies: []string{"policy1"},
+ },
+ },
+ {
+ SeverityDetails: formats.SeverityDetails{Severity: "Critical"},
+ Applicability: &formats.Applicability{Status: jasutils.Inactive.String()},
+ Finding: "Secret keys were found",
+ ScannerInfo: formats.ScannerInfo{
+ RuleId: "rule-id",
+ Cwe: []string{"CWE-798", "CWE-799"},
+ ScannerDescription: "Scanner Description....",
+ ScannerShortDescription: "Scanner Short Description",
+ },
+ ViolationContext: formats.ViolationContext{
+ Watch: "jas-watch2",
+ IssueId: "secret-violation-id-2",
+ Policies: []string{"policy1", "policy2"},
+ },
+ },
+ },
+ cases: []OutputTestCase{
+ {
+ name: "Standard output",
+ writer: &StandardOutput{MarkdownOutput{hasInternetConnection: true}},
+ expectedOutputPath: []string{filepath.Join(testReviewCommentDir, "secrets", "secret_violation_review_content_standard.md")},
+ },
+ {
+ name: "Simplified output",
+ writer: &SimplifiedOutput{MarkdownOutput{hasInternetConnection: true}},
+ expectedOutputPath: []string{filepath.Join(testReviewCommentDir, "secrets", "secret_violation_review_content_simplified.md")},
+ },
+ },
+ },
}
for _, tc := range testCases {
for _, test := range tc.cases {
t.Run(tc.name+"_"+test.name, func(t *testing.T) {
expectedOutput := GetExpectedTestOutput(t, test)
- assert.Equal(t, expectedOutput, SecretReviewContent(tc.severity, tc.finding, tc.fullDetails, tc.status, test.writer))
+ violations := false
+ for _, issue := range tc.issues {
+ if issue.Watch != "" {
+ violations = true
+ break
+ }
+ }
+ assert.Equal(t, expectedOutput, SecretReviewContent(violations, test.writer, tc.issues...))
})
}
}
@@ -693,35 +998,77 @@ func TestSecretsReviewContent(t *testing.T) {
func TestIacReviewContent(t *testing.T) {
testCases := []struct {
- name string
- severity, finding, fullDetails string
- cases []OutputTestCase
+ name string
+ issues []formats.SourceCodeRow
+ cases []OutputTestCase
}{
{
- name: "Iac review comment content",
- severity: "Medium",
- finding: "Missing auto upgrade was detected",
- fullDetails: "Resource `google_container_node_pool` should have `management.auto_upgrade=true`\n\nVulnerable example - \n```\nresource \"google_container_node_pool\" \"vulnerable_example\" {\n management {\n auto_upgrade = false\n }\n}\n```\n",
+ name: "Iac review comment content",
+ issues: []formats.SourceCodeRow{{
+ SeverityDetails: formats.SeverityDetails{Severity: "Medium"},
+ Finding: "Missing auto upgrade was detected",
+ ScannerInfo: formats.ScannerInfo{
+ RuleId: "rule-id",
+ ScannerDescription: "Scanner Description....",
+ ScannerShortDescription: "Scanner Short Description",
+ },
+ }},
cases: []OutputTestCase{
{
name: "Standard output",
- writer: &StandardOutput{},
+ writer: &StandardOutput{MarkdownOutput{hasInternetConnection: true}},
expectedOutputPath: []string{filepath.Join(testReviewCommentDir, "iac", "iac_review_content_standard.md")},
},
{
name: "Simplified output",
- writer: &SimplifiedOutput{},
+ writer: &SimplifiedOutput{MarkdownOutput{hasInternetConnection: true}},
expectedOutputPath: []string{filepath.Join(testReviewCommentDir, "iac", "iac_review_content_simplified.md")},
},
},
},
+ {
+ name: "Iac violation review comment content",
+ issues: []formats.SourceCodeRow{{
+ SeverityDetails: formats.SeverityDetails{Severity: "Medium"},
+ Finding: "Missing auto upgrade was detected",
+ ScannerInfo: formats.ScannerInfo{
+ RuleId: "rule-id",
+ ScannerDescription: "Scanner Description....",
+ ScannerShortDescription: "Scanner Short Description",
+ },
+ ViolationContext: formats.ViolationContext{
+ IssueId: "iac-violation-id",
+ Watch: "jas-watch",
+ Policies: []string{"policy1", "policy2"},
+ },
+ }},
+ cases: []OutputTestCase{
+ {
+ name: "Standard output",
+ writer: &StandardOutput{MarkdownOutput{hasInternetConnection: true}},
+ expectedOutputPath: []string{filepath.Join(testReviewCommentDir, "iac", "iac_violation_review_content_standard.md")},
+ },
+ {
+ name: "Simplified output",
+ writer: &SimplifiedOutput{MarkdownOutput{hasInternetConnection: true}},
+ expectedOutputPath: []string{filepath.Join(testReviewCommentDir, "iac", "iac_violation_review_content_simplified.md")},
+ },
+ },
+ },
}
for _, tc := range testCases {
for _, test := range tc.cases {
t.Run(tc.name+"_"+test.name, func(t *testing.T) {
expectedOutput := GetExpectedTestOutput(t, test)
- assert.Equal(t, expectedOutput, IacReviewContent(tc.severity, tc.finding, tc.fullDetails, test.writer))
+ violations := false
+ for _, issue := range tc.issues {
+ if issue.Watch != "" {
+ violations = true
+ break
+ }
+ }
+ assert.Equal(t, expectedOutput, IacReviewContent(violations, test.writer, tc.issues...))
})
}
}
@@ -729,84 +1076,194 @@ func TestIacReviewContent(t *testing.T) {
func TestSastReviewContent(t *testing.T) {
testCases := []struct {
- name string
- severity string
- finding string
- fullDetails string
- codeFlows [][]formats.Location
- cases []OutputTestCase
+ name string
+ issues []formats.SourceCodeRow
+ cases []OutputTestCase
}{
{
- name: "Sast review comment content",
- severity: "Low",
- finding: "Stack Trace Exposure",
- fullDetails: "\n### Overview\nStack trace exposure is a type of security vulnerability that occurs when a program reveals\nsensitive information, such as the names and locations of internal files and variables,\nin error messages or other diagnostic output. This can happen when a program crashes or\nencounters an error, and the stack trace (a record of the program's call stack at the time\nof the error) is included in the output.",
- codeFlows: [][]formats.Location{
+ name: "No code flows (no internet connection)",
+ issues: []formats.SourceCodeRow{{
+ SeverityDetails: formats.SeverityDetails{Severity: "Low"},
+ Finding: "Found a Use of Insecure Random",
+ ScannerInfo: formats.ScannerInfo{
+ RuleId: "js-insecure-random",
+ Cwe: []string{"CWE-798", "CWE-799"},
+ ScannerDescription: "Scanner Description....",
+ ScannerShortDescription: "Use of Insecure Random",
+ },
+ }},
+ cases: []OutputTestCase{
{
- {
- File: "file2",
- StartLine: 1,
- StartColumn: 2,
- EndLine: 3,
- EndColumn: 4,
- Snippet: "other-snippet",
- },
- {
- File: "file",
- StartLine: 0,
- StartColumn: 0,
- EndLine: 0,
- EndColumn: 0,
- Snippet: "snippet",
- },
+ name: "Standard output",
+ writer: &StandardOutput{},
+ expectedOutputPath: []string{filepath.Join(testReviewCommentDir, "sast", "sast_review_content_no_code_flow_standard.md")},
},
{
+ name: "Simplified output",
+ writer: &SimplifiedOutput{},
+ expectedOutputPath: []string{filepath.Join(testReviewCommentDir, "sast", "sast_review_content_no_code_flow_simplified.md")},
+ },
+ },
+ },
+ {
+ name: "Sast review comment content",
+ issues: []formats.SourceCodeRow{{
+ SeverityDetails: formats.SeverityDetails{Severity: "Low"},
+ Finding: "Found a Use of Insecure Random",
+ ScannerInfo: formats.ScannerInfo{
+ RuleId: "js-insecure-random",
+ Cwe: []string{"CWE-798", "CWE-799"},
+ ScannerDescription: "Scanner Description....",
+ ScannerShortDescription: "Use of Insecure Random",
+ },
+ CodeFlow: [][]formats.Location{
{
- File: "file",
- StartLine: 10,
- StartColumn: 20,
- EndLine: 10,
- EndColumn: 30,
- Snippet: "a-snippet",
+ {
+ File: "file2",
+ StartLine: 1,
+ StartColumn: 2,
+ EndLine: 3,
+ EndColumn: 4,
+ Snippet: "other-snippet",
+ },
+ {
+ File: "file",
+ StartLine: 0,
+ StartColumn: 0,
+ EndLine: 0,
+ EndColumn: 0,
+ Snippet: "snippet",
+ },
},
{
- File: "file",
- StartLine: 0,
- StartColumn: 0,
- EndLine: 0,
- EndColumn: 0,
- Snippet: "snippet",
+ {
+ File: "file",
+ StartLine: 10,
+ StartColumn: 20,
+ EndLine: 10,
+ EndColumn: 30,
+ Snippet: "a-snippet",
+ },
+ {
+ File: "file",
+ StartLine: 0,
+ StartColumn: 0,
+ EndLine: 0,
+ EndColumn: 0,
+ Snippet: "snippet",
+ },
},
},
- },
+ }},
cases: []OutputTestCase{
{
name: "Standard output",
- writer: &StandardOutput{},
+ writer: &StandardOutput{MarkdownOutput{hasInternetConnection: true}},
expectedOutputPath: []string{filepath.Join(testReviewCommentDir, "sast", "sast_review_content_standard.md")},
},
{
name: "Simplified output",
- writer: &SimplifiedOutput{},
+ writer: &SimplifiedOutput{MarkdownOutput{hasInternetConnection: true}},
expectedOutputPath: []string{filepath.Join(testReviewCommentDir, "sast", "sast_review_content_simplified.md")},
},
},
},
{
- name: "No code flows",
- severity: "Low",
- finding: "Stack Trace Exposure",
- fullDetails: "\n### Overview\nStack trace exposure is a type of security vulnerability that occurs when a program reveals\nsensitive information, such as the names and locations of internal files and variables,\nin error messages or other diagnostic output. This can happen when a program crashes or\nencounters an error, and the stack trace (a record of the program's call stack at the time\nof the error) is included in the output.",
+ name: "Sast violation review comment content",
+ issues: []formats.SourceCodeRow{
+ {
+ SeverityDetails: formats.SeverityDetails{Severity: "Low"},
+ Finding: "Found a Use of Insecure Random",
+ ScannerInfo: formats.ScannerInfo{
+ RuleId: "js-insecure-random",
+ Cwe: []string{"CWE-798", "CWE-799"},
+ ScannerDescription: "Scanner Description....",
+ ScannerShortDescription: "Use of Insecure Random",
+ },
+ ViolationContext: formats.ViolationContext{
+ IssueId: "sast-violation-id",
+ Watch: "jas-watch",
+ Policies: []string{"policy1", "policy2"},
+ },
+ CodeFlow: [][]formats.Location{
+ {
+ {
+ File: "file2",
+ StartLine: 1,
+ StartColumn: 2,
+ EndLine: 3,
+ EndColumn: 4,
+ Snippet: "other-snippet",
+ },
+ {
+ File: "file",
+ StartLine: 0,
+ StartColumn: 0,
+ EndLine: 0,
+ EndColumn: 0,
+ Snippet: "snippet",
+ },
+ },
+ {
+ {
+ File: "file",
+ StartLine: 10,
+ StartColumn: 20,
+ EndLine: 10,
+ EndColumn: 30,
+ Snippet: "a-snippet",
+ },
+ {
+ File: "file",
+ StartLine: 0,
+ StartColumn: 0,
+ EndLine: 0,
+ EndColumn: 0,
+ Snippet: "snippet",
+ },
+ },
+ },
+ },
+ {
+ SeverityDetails: formats.SeverityDetails{Severity: "High"},
+ Finding: "Found a Use of Insecure Random",
+ ScannerInfo: formats.ScannerInfo{
+ RuleId: "js-insecure-random",
+ Cwe: []string{"CWE-798", "CWE-799"},
+ ScannerDescription: "Scanner Description....",
+ ScannerShortDescription: "Use of Insecure Random",
+ },
+ ViolationContext: formats.ViolationContext{
+ IssueId: "sast-violation-id-2",
+ Watch: "jas-watch2",
+ Policies: []string{"policy3"},
+ },
+ },
+ {
+ SeverityDetails: formats.SeverityDetails{Severity: "High"},
+ Finding: "Found An Express Not Using Helmet",
+ ScannerInfo: formats.ScannerInfo{
+ RuleId: "js-express-without-helmet",
+ ScannerDescription: "Scanner Description....",
+ ScannerShortDescription: "Express Not Using Helmet",
+ },
+ ViolationContext: formats.ViolationContext{
+ IssueId: "sast-violation-id-3",
+ Watch: "jas-watch2",
+ Policies: []string{"policy3"},
+ },
+ },
+ },
cases: []OutputTestCase{
{
name: "Standard output",
- writer: &StandardOutput{},
- expectedOutputPath: []string{filepath.Join(testReviewCommentDir, "sast", "sast_review_content_no_code_flow_standard.md")},
+ writer: &StandardOutput{MarkdownOutput{hasInternetConnection: true}},
+ expectedOutputPath: []string{filepath.Join(testReviewCommentDir, "sast", "sast_violation_review_content_standard.md")},
},
{
name: "Simplified output",
- writer: &SimplifiedOutput{},
- expectedOutputPath: []string{filepath.Join(testReviewCommentDir, "sast", "sast_review_content_no_code_flow_simplified.md")},
+ writer: &SimplifiedOutput{MarkdownOutput{hasInternetConnection: true}},
+ expectedOutputPath: []string{filepath.Join(testReviewCommentDir, "sast", "sast_violation_review_content_simplified.md")},
},
},
},
@@ -816,7 +1273,14 @@ func TestSastReviewContent(t *testing.T) {
for _, test := range tc.cases {
t.Run(tc.name+"_"+test.name, func(t *testing.T) {
expectedOutput := GetExpectedTestOutput(t, test)
- assert.Equal(t, expectedOutput, SastReviewContent(tc.severity, tc.finding, tc.fullDetails, tc.codeFlows, test.writer))
+ violations := false
+ for _, issue := range tc.issues {
+ if issue.Watch != "" {
+ violations = true
+ break
+ }
+ }
+ assert.Equal(t, expectedOutput, SastReviewContent(violations, test.writer, tc.issues...))
})
}
}
diff --git a/utils/outputwriter/outputwriter.go b/utils/outputwriter/outputwriter.go
index 865c0ed02..841ae2eb2 100644
--- a/utils/outputwriter/outputwriter.go
+++ b/utils/outputwriter/outputwriter.go
@@ -6,6 +6,7 @@ import (
"github.com/jfrog/froggit-go/vcsclient"
"github.com/jfrog/froggit-go/vcsutils"
+ "github.com/jfrog/jfrog-cli-security/utils/severityutils"
"github.com/jfrog/jfrog-client-go/utils/log"
)
@@ -105,6 +106,7 @@ type OutputWriter interface {
VcsProvider() vcsutils.VcsProvider
SetVcsProvider(provider vcsutils.VcsProvider)
// Markdown interface
+ SeverityIcon(severity severityutils.Severity) string
FormattedSeverity(severity, applicability string) string
Separator() string
MarkInCenter(content string) string
@@ -219,6 +221,10 @@ func MarkAsLink(content, link string) string {
return fmt.Sprintf("[%s](%s)", content, link)
}
+func MarkAsBullet(content string) string {
+ return fmt.Sprintf("- %s", content)
+}
+
func SectionDivider() string {
return "\n---"
}
diff --git a/utils/outputwriter/simplifiedoutput.go b/utils/outputwriter/simplifiedoutput.go
index 114e19630..3f5ff524e 100644
--- a/utils/outputwriter/simplifiedoutput.go
+++ b/utils/outputwriter/simplifiedoutput.go
@@ -3,6 +3,8 @@ package outputwriter
import (
"fmt"
"strings"
+
+ "github.com/jfrog/jfrog-cli-security/utils/severityutils"
)
const (
@@ -17,6 +19,10 @@ func (smo *SimplifiedOutput) Separator() string {
return simpleSeparator
}
+func (smo *SimplifiedOutput) SeverityIcon(severity severityutils.Severity) string {
+ return severityutils.GetSeverityIcon(severity)
+}
+
func (smo *SimplifiedOutput) FormattedSeverity(severity, _ string) string {
return severity
}
@@ -30,12 +36,16 @@ func (smo *SimplifiedOutput) MarkInCenter(content string) string {
}
func (smo *SimplifiedOutput) MarkAsDetails(summary string, subTitleDepth int, content string) string {
- return fmt.Sprintf("%s\n%s", smo.MarkAsTitle(summary, subTitleDepth), content)
+ delimiter := "\n"
+ if subTitleDepth == 0 {
+ delimiter = ": "
+ }
+ return fmt.Sprintf("%s%s%s", smo.MarkAsTitle(summary, subTitleDepth), delimiter, content)
}
func (smo *SimplifiedOutput) MarkAsTitle(title string, subTitleDepth int) string {
if subTitleDepth == 0 {
- return fmt.Sprintf("%s\n%s\n%s", SectionDivider(), title, SectionDivider())
+ return title
}
return fmt.Sprintf("%s\n%s %s\n%s", SectionDivider(), strings.Repeat("#", subTitleDepth), title, SectionDivider())
}
diff --git a/utils/outputwriter/simplifiedoutput_test.go b/utils/outputwriter/simplifiedoutput_test.go
index 6dc860cfb..5642dbb9d 100644
--- a/utils/outputwriter/simplifiedoutput_test.go
+++ b/utils/outputwriter/simplifiedoutput_test.go
@@ -126,11 +126,11 @@ func TestSimpleMarkAsDetails(t *testing.T) {
subTitleDepth int
}{
{
- name: "empty",
- summary: "",
+ name: "inline",
+ summary: "title",
subTitleDepth: 0,
- content: "",
- expectedOutput: "\n---\n\n\n---\n",
+ content: "details",
+ expectedOutput: "title: details",
},
{
name: "empty content",
@@ -184,10 +184,10 @@ func TestSimpleMarkAsTitle(t *testing.T) {
subTitleDepth int
}{
{
- name: "empty",
- title: "",
+ name: "inline",
+ title: "title",
subTitleDepth: 0,
- expectedOutput: "\n---\n\n\n---",
+ expectedOutput: "title",
},
{
name: "Main title",
diff --git a/utils/outputwriter/standardoutput.go b/utils/outputwriter/standardoutput.go
index 2a1a50aa3..7143f72b7 100644
--- a/utils/outputwriter/standardoutput.go
+++ b/utils/outputwriter/standardoutput.go
@@ -3,6 +3,8 @@ package outputwriter
import (
"fmt"
"strings"
+
+ "github.com/jfrog/jfrog-cli-security/utils/severityutils"
)
type StandardOutput struct {
@@ -13,7 +15,18 @@ func (so *StandardOutput) Separator() string {
return "
"
}
+func (so *StandardOutput) SeverityIcon(severity severityutils.Severity) string {
+ if !so.hasInternetConnection {
+ return severityutils.GetSeverityIcon(severity)
+ }
+ return getSmallSeverityTag(IconName(severity))
+
+}
+
func (so *StandardOutput) FormattedSeverity(severity, applicability string) string {
+ if !so.hasInternetConnection {
+ return severity
+ }
return fmt.Sprintf("%s%8s", getSeverityTag(IconName(severity), applicability), severity)
}
@@ -28,14 +41,11 @@ func (so *StandardOutput) MarkInCenter(content string) string {
return GetMarkdownCenterTag(content)
}
-func (so *StandardOutput) MarkAsDetails(summary string, subTitleDepth int, content string) string {
+func (so *StandardOutput) MarkAsDetails(summary string, _ int, content string) string {
if summary != "" {
- summary = fmt.Sprintf(" %s
\n", summary)
- }
- if subTitleDepth > 0 {
- summary += "
\n"
+ summary = fmt.Sprintf("%s
", summary)
}
- return fmt.Sprintf("\n%s\n%s\n\n \n", summary, content)
+ return fmt.Sprintf("%s%s
", summary, content)
}
func (so *StandardOutput) MarkAsTitle(title string, subTitleDepth int) string {
diff --git a/utils/outputwriter/standardoutput_test.go b/utils/outputwriter/standardoutput_test.go
index 133c01905..f3f235901 100644
--- a/utils/outputwriter/standardoutput_test.go
+++ b/utils/outputwriter/standardoutput_test.go
@@ -52,27 +52,42 @@ func TestStandardSeparator(t *testing.T) {
func TestStandardFormattedSeverity(t *testing.T) {
testCases := []struct {
- name string
- severity string
- applicability string
- expectedOutput string
+ name string
+ severity string
+ applicability string
+ expectedOutput string
+ internetConnection bool
}{
+ {
+ name: "Applicable severity",
+ severity: "Low",
+ applicability: "Applicable",
+ internetConnection: true,
+ expectedOutput: "
Low",
+ },
+ {
+ name: "Not applicable severity",
+ severity: "Medium",
+ applicability: "Not Applicable",
+ internetConnection: true,
+ expectedOutput: "
Medium",
+ },
{
name: "Applicable severity",
severity: "Low",
applicability: "Applicable",
- expectedOutput: "
Low",
+ expectedOutput: "Low",
},
{
name: "Not applicable severity",
severity: "Medium",
applicability: "Not Applicable",
- expectedOutput: "
Medium",
+ expectedOutput: "Medium",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
- smo := &StandardOutput{}
+ smo := &StandardOutput{MarkdownOutput: MarkdownOutput{hasInternetConnection: tc.internetConnection}}
assert.Equal(t, tc.expectedOutput, smo.FormattedSeverity(tc.severity, tc.applicability))
})
}
@@ -161,42 +176,42 @@ func TestStandardMarkAsDetails(t *testing.T) {
summary: "",
subTitleDepth: 0,
content: "",
- expectedOutput: "\n\n\n\n \n",
+ expectedOutput: "
",
},
{
name: "empty content",
summary: "summary",
subTitleDepth: 1,
content: "",
- expectedOutput: "\n summary
\n
\n\n\n\n \n",
+ expectedOutput: "summary
",
},
{
name: "empty summary",
summary: "",
subTitleDepth: 0,
content: "content",
- expectedOutput: "\n\ncontent\n\n \n",
+ expectedOutput: "content
",
},
{
name: "Main details",
summary: "summary",
subTitleDepth: 1,
content: "content",
- expectedOutput: "\n summary
\n
\n\ncontent\n\n \n",
+ expectedOutput: "summary
content
",
},
{
name: "Sub details",
summary: "summary",
subTitleDepth: 2,
content: "content",
- expectedOutput: "\n summary
\n
\n\ncontent\n\n \n",
+ expectedOutput: "summary
content
",
},
{
name: "Sub sub details",
summary: "summary",
subTitleDepth: 3,
content: "content",
- expectedOutput: "\n summary
\n
\n\ncontent\n\n \n",
+ expectedOutput: "summary
content
",
},
}
for _, tc := range testCases {
diff --git a/utils/params.go b/utils/params.go
index ad71a894f..cbc6cfe96 100644
--- a/utils/params.go
+++ b/utils/params.go
@@ -285,10 +285,11 @@ func (s *Scan) setDefaultsIfNeeded() (err error) {
}
type JFrogPlatform struct {
- XrayVersion string
- XscVersion string
- Watches []string `yaml:"watches,omitempty"`
- JFrogProjectKey string `yaml:"jfrogProjectKey,omitempty"`
+ XrayVersion string
+ XscVersion string
+ Watches []string `yaml:"watches,omitempty"`
+ IncludeVulnerabilities bool `yaml:"includeVulnerabilities,omitempty"`
+ JFrogProjectKey string `yaml:"jfrogProjectKey,omitempty"`
}
func (jp *JFrogPlatform) setDefaultsIfNeeded() (err error) {
@@ -298,7 +299,6 @@ func (jp *JFrogPlatform) setDefaultsIfNeeded() (err error) {
return
}
}
-
if jp.JFrogProjectKey == "" {
if err = readParamFromEnv(jfrogProjectEnv, &jp.JFrogProjectKey); err != nil && !e.IsMissingEnvErr(err) {
return
@@ -306,6 +306,11 @@ func (jp *JFrogPlatform) setDefaultsIfNeeded() (err error) {
// We don't want to return an error from this function if the error is of type ErrMissingEnv because JFrogPlatform environment variables are not mandatory.
err = nil
}
+ if !jp.IncludeVulnerabilities {
+ if jp.IncludeVulnerabilities, err = getBoolEnv(IncludeVulnerabilitiesEnv, false); err != nil {
+ return
+ }
+ }
return
}
@@ -329,6 +334,19 @@ type Git struct {
UseLocalRepository bool
}
+func (g *Git) GetRepositoryHttpsCloneUrl(gitClient vcsclient.VcsClient) (string, error) {
+ if g.RepositoryCloneUrl != "" {
+ return g.RepositoryCloneUrl, nil
+ }
+ // If the repository clone URL is not cached, we fetch it from the VCS provider
+ repositoryInfo, err := gitClient.GetRepositoryInfo(context.Background(), g.RepoOwner, g.RepoName)
+ if err != nil {
+ return "", fmt.Errorf("failed to fetch the repository clone URL. %s", err.Error())
+ }
+ g.RepositoryCloneUrl = repositoryInfo.CloneInfo.HTTP
+ return g.RepositoryCloneUrl, nil
+}
+
func (g *Git) setDefaultsIfNeeded(gitParamsFromEnv *Git, commandName string) (err error) {
g.RepoOwner = gitParamsFromEnv.RepoOwner
g.GitProvider = gitParamsFromEnv.GitProvider
@@ -815,7 +833,6 @@ func getConfigProfileIfExistsAndValid(xrayVersion, xscVersion string, jfrogServe
log.Debug(fmt.Sprintf("Configuration Profile usage is disabled. All configurations will be derived from environment variables and files.\nTo enable a Configuration Profile, please set %s to TRUE", JfrogUseConfigProfileEnv))
return
}
-
// Attempt to get the config profile by profile's name
profileName := getTrimmedEnv(JfrogConfigProfileEnv)
if profileName != "" {
@@ -826,14 +843,10 @@ func getConfigProfileIfExistsAndValid(xrayVersion, xscVersion string, jfrogServe
err = verifyConfigProfileValidity(configProfile)
return
}
-
// Getting repository's url in order to get repository HTTP url
- repositoryInfo, err := gitClient.GetRepositoryInfo(context.Background(), gitParams.RepoOwner, gitParams.RepoName)
- if err != nil {
- return nil, "", err
+ if repoCloneUrl, err = gitParams.GetRepositoryHttpsCloneUrl(gitClient); err != nil {
+ return
}
- repoCloneUrl = repositoryInfo.CloneInfo.HTTP
-
// Attempt to get a config profile associated with the repo URL
log.Debug(fmt.Sprintf("Configuration profile was requested. Searching profile associated to repository '%s'", jfrogServer.Url))
if configProfile, err = xsc.GetConfigProfileByUrl(xrayVersion, jfrogServer, repoCloneUrl); err != nil || configProfile == nil {
diff --git a/utils/scandetails.go b/utils/scandetails.go
index 1de80e0ba..0dd75d135 100644
--- a/utils/scandetails.go
+++ b/utils/scandetails.go
@@ -3,7 +3,6 @@ package utils
import (
"context"
"fmt"
- "os"
"path/filepath"
"time"
@@ -11,20 +10,19 @@ import (
"github.com/jfrog/froggit-go/vcsclient"
"github.com/jfrog/jfrog-cli-core/v2/utils/config"
- "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils"
"github.com/jfrog/jfrog-cli-security/commands/audit"
"github.com/jfrog/jfrog-cli-security/utils"
"github.com/jfrog/jfrog-cli-security/utils/results"
"github.com/jfrog/jfrog-cli-security/utils/severityutils"
- "github.com/jfrog/jfrog-cli-security/utils/xray/scangraph"
"github.com/jfrog/jfrog-client-go/utils/log"
- "github.com/jfrog/jfrog-client-go/xray/services"
+ xscservices "github.com/jfrog/jfrog-client-go/xsc/services"
)
type ScanDetails struct {
*Project
*Git
- *services.XrayGraphScanParams
+
+ *xscservices.XscGitInfoContext
*config.ServerDetails
client vcsclient.VcsClient
failOnInstallationErrors bool
@@ -35,13 +33,24 @@ type ScanDetails struct {
baseBranch string
configProfile *clientservices.ConfigProfile
allowPartialResults bool
- StartTime time.Time
+
+ results.ResultContext
+ MultiScanId string
+ XrayVersion string
+ XscVersion string
+ StartTime time.Time
}
func NewScanDetails(client vcsclient.VcsClient, server *config.ServerDetails, git *Git) *ScanDetails {
return &ScanDetails{client: client, ServerDetails: server, Git: git}
}
+func (sc *ScanDetails) SetJfrogVersions(xrayVersion, xscVersion string) *ScanDetails {
+ sc.XrayVersion = xrayVersion
+ sc.XscVersion = xscVersion
+ return sc
+}
+
func (sc *ScanDetails) SetDisableJas(disable bool) *ScanDetails {
sc.disableJas = disable
return sc
@@ -57,8 +66,8 @@ func (sc *ScanDetails) SetProject(project *Project) *ScanDetails {
return sc
}
-func (sc *ScanDetails) SetXrayGraphScanParams(watches []string, jfrogProjectKey string, includeLicenses bool) *ScanDetails {
- sc.XrayGraphScanParams = createXrayScanParams(watches, jfrogProjectKey, includeLicenses)
+func (sc *ScanDetails) SetResultsContext(httpCloneUrl string, watches []string, jfrogProjectKey string, includeVulnerabilities, includeLicenses bool) *ScanDetails {
+ sc.ResultContext = audit.CreateAuditResultsContext(sc.ServerDetails, sc.XrayVersion, watches, sc.RepoPath, jfrogProjectKey, httpCloneUrl, includeVulnerabilities, includeLicenses)
return sc
}
@@ -137,43 +146,6 @@ func (sc *ScanDetails) AllowPartialResults() bool {
return sc.allowPartialResults
}
-func (sc *ScanDetails) CreateCommonGraphScanParams() *scangraph.CommonGraphScanParams {
- commonParams := &scangraph.CommonGraphScanParams{
- RepoPath: sc.RepoPath,
- Watches: sc.Watches,
- ScanType: sc.ScanType,
- }
- if sc.ProjectKey == "" {
- commonParams.ProjectKey = os.Getenv(coreutils.Project)
- } else {
- commonParams.ProjectKey = sc.ProjectKey
- }
- commonParams.IncludeVulnerabilities = sc.IncludeVulnerabilities
- commonParams.IncludeLicenses = sc.IncludeLicenses
- return commonParams
-}
-
-func (sc *ScanDetails) HasViolationContext() bool {
- return sc.ProjectKey != "" || len(sc.Watches) > 0 || sc.RepoPath != ""
-}
-
-func createXrayScanParams(watches []string, project string, includeLicenses bool) (params *services.XrayGraphScanParams) {
- params = &services.XrayGraphScanParams{
- ScanType: services.Dependency,
- IncludeLicenses: includeLicenses,
- }
- if len(watches) > 0 {
- params.Watches = watches
- return
- }
- if project != "" {
- params.ProjectKey = project
- return
- }
- params.IncludeVulnerabilities = true
- return
-}
-
func (sc *ScanDetails) RunInstallAndAudit(workDirs ...string) (auditResults *results.SecurityCommandResults) {
auditBasicParams := (&utils.AuditBasicParams{}).
SetXrayVersion(sc.XrayVersion).
@@ -198,7 +170,7 @@ func (sc *ScanDetails) RunInstallAndAudit(workDirs ...string) (auditResults *res
SetMinSeverityFilter(sc.MinSeverityFilter()).
SetFixableOnly(sc.FixableOnly()).
SetGraphBasicParams(auditBasicParams).
- SetCommonGraphScanParams(sc.CreateCommonGraphScanParams()).
+ SetResultsContext(sc.ResultContext).
SetConfigProfile(sc.configProfile).
SetMultiScanId(sc.MultiScanId).
SetStartTime(sc.StartTime)
@@ -220,7 +192,7 @@ func (sc *ScanDetails) SetXscGitInfoContext(scannedBranch, gitProject string, cl
// ScannedBranch - name of the branch we are scanning.
// GitProject - [Optional] relevant for azure repos and Bitbucket server.
// Client vscClient
-func (sc *ScanDetails) createGitInfoContext(scannedBranch, gitProject string, client vcsclient.VcsClient) (gitInfo *services.XscGitInfoContext, err error) {
+func (sc *ScanDetails) createGitInfoContext(scannedBranch, gitProject string, client vcsclient.VcsClient) (gitInfo *xscservices.XscGitInfoContext, err error) {
latestCommit, err := client.GetLatestCommit(context.Background(), sc.RepoOwner, sc.RepoName, scannedBranch)
if err != nil {
return nil, fmt.Errorf("failed getting latest commit, repository: %s, branch: %s. error: %s ", sc.RepoName, scannedBranch, err.Error())
@@ -229,17 +201,17 @@ func (sc *ScanDetails) createGitInfoContext(scannedBranch, gitProject string, cl
if gitProject == "" {
gitProject = sc.RepoOwner
}
- gitInfo = &services.XscGitInfoContext{
+ gitInfo = &xscservices.XscGitInfoContext{
// Use Clone URLs as Repo Url, on browsers it will redirect to repository URLS.
- GitRepoUrl: sc.Git.RepositoryCloneUrl,
- GitRepoName: sc.RepoName,
- GitProvider: sc.GitProvider.String(),
- GitProject: gitProject,
- BranchName: scannedBranch,
- LastCommit: latestCommit.Url,
- CommitHash: latestCommit.Hash,
- CommitMessage: latestCommit.Message,
- CommitAuthor: latestCommit.AuthorName,
+ GitRepoHttpsCloneUrl: sc.Git.RepositoryCloneUrl,
+ GitRepoName: sc.RepoName,
+ GitProvider: sc.Git.GitProvider.String(),
+ GitProject: gitProject,
+ BranchName: scannedBranch,
+ LastCommitUrl: latestCommit.Url,
+ LastCommitHash: latestCommit.Hash,
+ LastCommitMessage: latestCommit.Message,
+ LastCommitAuthor: latestCommit.AuthorName,
}
return
}
diff --git a/utils/scandetails_test.go b/utils/scandetails_test.go
index 7791fc7d1..36f486e8f 100644
--- a/utils/scandetails_test.go
+++ b/utils/scandetails_test.go
@@ -1,34 +1,11 @@
package utils
import (
- "github.com/stretchr/testify/assert"
"path/filepath"
"testing"
-)
-
-func TestCreateXrayScanParams(t *testing.T) {
- // Project
- scanDetails := &ScanDetails{}
- scanDetails.SetXrayGraphScanParams(nil, "", false)
- assert.Empty(t, scanDetails.Watches)
- assert.Equal(t, "", scanDetails.ProjectKey)
- assert.True(t, scanDetails.IncludeVulnerabilities)
- assert.False(t, scanDetails.IncludeLicenses)
-
- // Watches
- scanDetails.SetXrayGraphScanParams([]string{"watch-1", "watch-2"}, "", false)
- assert.Equal(t, []string{"watch-1", "watch-2"}, scanDetails.Watches)
- assert.Equal(t, "", scanDetails.ProjectKey)
- assert.False(t, scanDetails.IncludeVulnerabilities)
- assert.False(t, scanDetails.IncludeLicenses)
- // Project
- scanDetails.SetXrayGraphScanParams(nil, "project", true)
- assert.Empty(t, scanDetails.Watches)
- assert.Equal(t, "project", scanDetails.ProjectKey)
- assert.False(t, scanDetails.IncludeVulnerabilities)
- assert.True(t, scanDetails.IncludeLicenses)
-}
+ "github.com/stretchr/testify/assert"
+)
func TestGetFullPathWorkingDirs(t *testing.T) {
sampleProject := Project{
diff --git a/utils/utils.go b/utils/utils.go
index 08d8cf35c..388484533 100644
--- a/utils/utils.go
+++ b/utils/utils.go
@@ -13,6 +13,7 @@ import (
"strings"
"sync"
+ "github.com/jfrog/frogbot/v2/utils/issues"
"github.com/jfrog/froggit-go/vcsclient"
"github.com/jfrog/gofrog/version"
"github.com/jfrog/jfrog-cli-core/v2/common/commands"
@@ -222,8 +223,8 @@ func VulnerabilityDetailsToMD5Hash(vulnerabilities ...formats.VulnerabilityOrVio
return hex.EncodeToString(hash.Sum(nil)), nil
}
-func UploadSarifResultsToGithubSecurityTab(scanResults *results.SecurityCommandResults, repo *Repository, branch string, client vcsclient.VcsClient, hasViolationContext bool) error {
- report, err := GenerateFrogbotSarifReport(scanResults, scanResults.HasMultipleTargets(), hasViolationContext, repo.AllowedLicenses)
+func UploadSarifResultsToGithubSecurityTab(scanResults *results.SecurityCommandResults, repo *Repository, branch string, client vcsclient.VcsClient) error {
+ report, err := GenerateFrogbotSarifReport(scanResults, repo.AllowedLicenses)
if err != nil {
return err
}
@@ -235,11 +236,10 @@ func UploadSarifResultsToGithubSecurityTab(scanResults *results.SecurityCommandR
return nil
}
-func GenerateFrogbotSarifReport(extendedResults *results.SecurityCommandResults, isMultipleRoots, hasViolationContext bool, allowedLicenses []string) (string, error) {
+func GenerateFrogbotSarifReport(extendedResults *results.SecurityCommandResults, allowedLicenses []string) (string, error) {
convertor := conversion.NewCommandResultsConvertor(conversion.ResultConvertParams{
- IncludeVulnerabilities: true,
- HasViolationContext: hasViolationContext,
- IsMultipleRoots: &isMultipleRoots,
+ IncludeVulnerabilities: extendedResults.IncludesVulnerabilities(),
+ HasViolationContext: extendedResults.HasViolationContext(),
AllowedLicenses: allowedLicenses,
})
sarifReport, err := convertor.ConvertToSarif(extendedResults)
@@ -336,11 +336,15 @@ func GetVulnerabiltiesUniqueID(vulnerability formats.VulnerabilityOrViolationRow
len(vulnerability.FixedVersions) > 0)
}
-func ConvertSarifPathsToRelative(issues *IssuesCollection, workingDirs ...string) {
- convertSarifPathsInCveApplicability(issues.Vulnerabilities, workingDirs...)
- convertSarifPathsInIacs(issues.Iacs, workingDirs...)
- convertSarifPathsInSecrets(issues.Secrets, workingDirs...)
- convertSarifPathsInSast(issues.Sast, workingDirs...)
+func ConvertSarifPathsToRelative(issues *issues.ScansIssuesCollection, workingDirs ...string) {
+ convertSarifPathsInCveApplicability(issues.ScaVulnerabilities, workingDirs...)
+ convertSarifPathsInIacs(issues.IacVulnerabilities, workingDirs...)
+ convertSarifPathsInSecrets(issues.SecretsVulnerabilities, workingDirs...)
+ convertSarifPathsInSast(issues.SastVulnerabilities, workingDirs...)
+ convertSarifPathsInCveApplicability(issues.ScaViolations, workingDirs...)
+ convertSarifPathsInIacs(issues.IacViolations, workingDirs...)
+ convertSarifPathsInSecrets(issues.SecretsViolations, workingDirs...)
+ convertSarifPathsInSast(issues.SastViolations, workingDirs...)
}
func convertSarifPathsInCveApplicability(vulnerabilities []formats.VulnerabilityOrViolationRow, workingDirs ...string) {