diff --git a/apps/console/src/pages/organizations/third-parties/risks/_components/ThirdPartyRiskAssessmentRow.tsx b/apps/console/src/pages/organizations/third-parties/risks/_components/ThirdPartyRiskAssessmentRow.tsx
index f95f42ec2..a63d043b3 100644
--- a/apps/console/src/pages/organizations/third-parties/risks/_components/ThirdPartyRiskAssessmentRow.tsx
+++ b/apps/console/src/pages/organizations/third-parties/risks/_components/ThirdPartyRiskAssessmentRow.tsx
@@ -22,6 +22,7 @@ import { formatDate } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import {
Badge,
+ Markdown,
RiskBadge,
Td,
Tr,
@@ -84,15 +85,21 @@ export function ThirdPartyRiskAssessmentRow(props: ThirdPartyRiskAssessmentRowPr
{props.isExpanded && (
-
-
+
+
{__("Notes")}
:
-
- {assessment.notes}
-
+ {assessment.notes
+ ? (
+
+ )
+ : null}
|
|
diff --git a/pkg/vetting/persist.go b/pkg/vetting/persist.go
index a041a57d7..367cde64f 100644
--- a/pkg/vetting/persist.go
+++ b/pkg/vetting/persist.go
@@ -136,7 +136,7 @@ func persistVettingRiskAssessment(
}
now := time.Now()
- notes := buildRiskAssessmentNotes(result.Info)
+ notes := buildRiskAssessmentNotesFromResult(result)
assessment := &coredata.ThirdPartyRiskAssessment{
ID: gid.New(scope.GetTenantID(), coredata.ThirdPartyRiskAssessmentEntityType),
@@ -157,6 +157,14 @@ func persistVettingRiskAssessment(
return nil
}
+func buildRiskAssessmentNotesFromResult(result Result) string {
+ if notes := filterVettingDocumentNotes(result.Document); notes != "" {
+ return notes
+ }
+
+ return buildRiskAssessmentNotes(result.Info)
+}
+
func buildRiskAssessmentNotes(info ThirdPartyInfo) string {
sections := []string{"Automated vetting"}
@@ -180,6 +188,150 @@ func buildRiskAssessmentNotes(info ThirdPartyInfo) string {
return strings.Join(sections, "\n\n")
}
+// filterVettingDocumentNotes drops profile-duplicate sections from the report.
+func filterVettingDocumentNotes(document string) string {
+ document = strings.TrimSpace(document)
+ if document == "" {
+ return ""
+ }
+
+ lines := strings.Split(document, "\n")
+ out := make([]string, 0, len(lines))
+ skipUntilLevel := 0
+ inFence := false
+ fenceMarker := byte(0)
+ fenceLen := 0
+
+ for _, line := range lines {
+ if marker, length, isFence := parseMarkdownFence(line); isFence {
+ if !inFence {
+ inFence = true
+ fenceMarker = marker
+ fenceLen = length
+ } else if marker == fenceMarker && length >= fenceLen {
+ inFence = false
+ fenceMarker = 0
+ fenceLen = 0
+ }
+
+ if skipUntilLevel == 0 {
+ out = append(out, line)
+ }
+
+ continue
+ }
+
+ if !inFence {
+ level, title, isHeading := parseMarkdownHeading(line)
+ if isHeading {
+ if skipUntilLevel > 0 && level <= skipUntilLevel {
+ skipUntilLevel = 0
+ }
+
+ if skipUntilLevel == 0 && shouldDropVettingNotesSection(title) {
+ skipUntilLevel = level
+ continue
+ }
+ }
+ }
+
+ if skipUntilLevel > 0 {
+ continue
+ }
+
+ out = append(out, line)
+ }
+
+ return strings.TrimSpace(strings.Join(out, "\n"))
+}
+
+func parseMarkdownFence(line string) (marker byte, length int, ok bool) {
+ rest := line
+
+ indent := 0
+ for indent < len(rest) && indent < 3 && rest[indent] == ' ' {
+ indent++
+ }
+
+ rest = rest[indent:]
+
+ if len(rest) == 0 || (rest[0] != '`' && rest[0] != '~') {
+ return 0, 0, false
+ }
+
+ marker = rest[0]
+ for length < len(rest) && rest[length] == marker {
+ length++
+ }
+
+ if length < 3 {
+ return 0, 0, false
+ }
+
+ return marker, length, true
+}
+
+func parseMarkdownHeading(line string) (level int, title string, ok bool) {
+ rest := line
+
+ indent := 0
+ for indent < len(rest) && indent < 3 && rest[indent] == ' ' {
+ indent++
+ }
+
+ rest = rest[indent:]
+ rest = strings.TrimRight(rest, " \t")
+
+ if rest == "" || rest[0] != '#' {
+ return 0, "", false
+ }
+
+ level = 0
+ for level < len(rest) && rest[level] == '#' {
+ level++
+ if level > 6 {
+ return 0, "", false
+ }
+ }
+
+ if level == 0 || level >= len(rest) {
+ return 0, "", false
+ }
+
+ if rest[level] != ' ' && rest[level] != '\t' {
+ return 0, "", false
+ }
+
+ title = strings.TrimSpace(rest[level+1:])
+ title = strings.TrimRight(title, "#")
+ title = strings.TrimSpace(title)
+
+ if title == "" {
+ return 0, "", false
+ }
+
+ return level, title, true
+}
+
+func shouldDropVettingNotesSection(title string) bool {
+ normalized := strings.ToLower(strings.TrimSpace(title))
+ normalized = strings.Trim(normalized, "*_`")
+ normalized = strings.Join(strings.Fields(strings.ReplaceAll(normalized, "-", " ")), " ")
+
+ switch {
+ case strings.Contains(normalized, "third party classification"),
+ strings.Contains(normalized, "vendor classification"):
+ return true
+ case strings.Contains(normalized, "compliance") && strings.Contains(normalized, "certification"):
+ return true
+ case strings.Contains(normalized, "sub processor"),
+ strings.Contains(normalized, "subprocessor"):
+ return true
+ default:
+ return false
+ }
+}
+
func vettingBulletSection(title string, lines []string) string {
if len(lines) == 0 {
return ""
diff --git a/pkg/vetting/persist_test.go b/pkg/vetting/persist_test.go
index 738c2e5b7..fcbfaba03 100644
--- a/pkg/vetting/persist_test.go
+++ b/pkg/vetting/persist_test.go
@@ -28,6 +28,185 @@ import (
"go.probo.inc/probo/pkg/coredata"
)
+func TestBuildRiskAssessmentNotesFromResult_FiltersDocument(t *testing.T) {
+ t.Parallel()
+
+ document := `# Third party Assessment: Acme
+
+## Executive Summary
+Approve with conditions.
+
+## Third party Classification
+- Name: Acme
+- Description: SaaS analytics
+
+## Overall Risk Score
+Score 70/100.
+
+## Compliance & Certifications
+- SOC 2 Type II
+
+## Privacy & Data Processing
+Retention is 30 days.
+
+### Data Classification & Handling
+PII is encrypted at rest.
+
+### Sub-Processors
+| Name | Country | Purpose |
+|------|---------|---------|
+| AWS | United States | Hosting |
+
+## Security Posture
+TLS looks good.
+
+## Market Presence
+Strong brand.
+`
+
+ notes := buildRiskAssessmentNotesFromResult(
+ Result{
+ Document: document,
+ Info: ThirdPartyInfo{
+ OverallRiskScore: 70,
+ Recommendation: "APPROVE_WITH_CONDITIONS",
+ },
+ },
+ )
+
+ assert.Contains(t, notes, "Executive Summary")
+ assert.Contains(t, notes, "Approve with conditions.")
+ assert.Contains(t, notes, "Overall Risk Score")
+ assert.Contains(t, notes, "Score 70/100.")
+ assert.Contains(t, notes, "Privacy & Data Processing")
+ assert.Contains(t, notes, "Retention is 30 days.")
+ assert.Contains(t, notes, "Data Classification & Handling")
+ assert.Contains(t, notes, "PII is encrypted at rest.")
+ assert.Contains(t, notes, "Security Posture")
+ assert.Contains(t, notes, "Market Presence")
+
+ assert.NotContains(t, notes, "Third party Classification")
+ assert.NotContains(t, notes, "Name: Acme")
+ assert.NotContains(t, notes, "Compliance & Certifications")
+ assert.NotContains(t, notes, "SOC 2 Type II")
+ assert.NotContains(t, notes, "Sub-Processors")
+ assert.NotContains(t, notes, "AWS")
+ assert.NotContains(t, notes, "Automated vetting")
+}
+
+func TestBuildRiskAssessmentNotesFromResult_FallsBackWhenDocumentEmpty(t *testing.T) {
+ t.Parallel()
+
+ info := ThirdPartyInfo{
+ OverallRiskRating: "Medium",
+ OverallRiskScore: 62,
+ Recommendation: "APPROVE_WITH_CONDITIONS",
+ }
+
+ notes := buildRiskAssessmentNotesFromResult(Result{Info: info})
+
+ assert.Equal(t, buildRiskAssessmentNotes(info), notes)
+ assert.Contains(t, notes, "Automated vetting")
+}
+
+func TestShouldDropVettingNotesSection(t *testing.T) {
+ t.Parallel()
+
+ assert.True(t, shouldDropVettingNotesSection("Third party Classification"))
+ assert.True(t, shouldDropVettingNotesSection("Third-Party Classification"))
+ assert.True(t, shouldDropVettingNotesSection("Vendor Classification"))
+ assert.True(t, shouldDropVettingNotesSection("Vendor-Classification"))
+ assert.True(t, shouldDropVettingNotesSection("Compliance & Certifications"))
+ assert.True(t, shouldDropVettingNotesSection("Sub-Processors"))
+ assert.True(t, shouldDropVettingNotesSection("Subprocessors"))
+ assert.False(t, shouldDropVettingNotesSection("Executive Summary"))
+ assert.False(t, shouldDropVettingNotesSection("Security Posture"))
+ assert.False(t, shouldDropVettingNotesSection("Three-Pillar Risk Assessment"))
+ assert.False(t, shouldDropVettingNotesSection("Data Classification & Handling"))
+ assert.False(t, shouldDropVettingNotesSection("AI risk classifications"))
+}
+
+func TestFilterVettingDocumentNotes_IgnoresHeadingsInFences(t *testing.T) {
+ t.Parallel()
+
+ document := `# Assessment
+
+## Security Posture
+Looks good.
+
+` + "```" + `
+## Third party Classification
+This is an example heading inside a fence.
+` + "```" + `
+
+## Market Presence
+Strong.
+`
+
+ notes := filterVettingDocumentNotes(document)
+
+ assert.Contains(t, notes, "Security Posture")
+ assert.Contains(t, notes, "Looks good.")
+ assert.Contains(t, notes, "Third party Classification")
+ assert.Contains(t, notes, "This is an example heading inside a fence.")
+ assert.Contains(t, notes, "Market Presence")
+ assert.Contains(t, notes, "Strong.")
+}
+
+func TestFilterVettingDocumentNotes_IndentedAndTabHeadings(t *testing.T) {
+ t.Parallel()
+
+ document := `# Assessment
+
+ ## Third party Classification
+- Name: Acme
+
+##` + "\t" + `Security Posture
+TLS looks good.
+
+## Market Presence
+Strong.
+`
+
+ notes := filterVettingDocumentNotes(document)
+
+ assert.NotContains(t, notes, "Third party Classification")
+ assert.NotContains(t, notes, "Name: Acme")
+ assert.Contains(t, notes, "Security Posture")
+ assert.Contains(t, notes, "TLS looks good.")
+ assert.Contains(t, notes, "Market Presence")
+}
+
+func TestParseMarkdownHeading(t *testing.T) {
+ t.Parallel()
+
+ level, title, ok := parseMarkdownHeading("## Executive Summary")
+ assert.True(t, ok)
+ assert.Equal(t, 2, level)
+ assert.Equal(t, "Executive Summary", title)
+
+ level, title, ok = parseMarkdownHeading(" ## Indented")
+ assert.True(t, ok)
+ assert.Equal(t, 2, level)
+ assert.Equal(t, "Indented", title)
+
+ level, title, ok = parseMarkdownHeading("##\tTabbed")
+ assert.True(t, ok)
+ assert.Equal(t, 2, level)
+ assert.Equal(t, "Tabbed", title)
+
+ level, title, ok = parseMarkdownHeading("## Trailing ##")
+ assert.True(t, ok)
+ assert.Equal(t, 2, level)
+ assert.Equal(t, "Trailing", title)
+
+ _, _, ok = parseMarkdownHeading(" ## Too indented")
+ assert.False(t, ok)
+
+ _, _, ok = parseMarkdownHeading("##NoSpace")
+ assert.False(t, ok)
+}
+
func TestBuildRiskAssessmentNotes(t *testing.T) {
t.Parallel()