From 5c5f60d5e1b9c92aecb5a92ceba51c0acac91f54 Mon Sep 17 00:00:00 2001 From: Sacha Al Himdani Date: Tue, 21 Jul 2026 15:01:48 +0200 Subject: [PATCH] Render longer vetting notes as markdown Keep more of the orchestrator assessment text, skipping profile fields already on the third party, and render the notes as markdown in the console. Signed-off-by: Sacha Al Himdani --- .../ThirdPartyRiskAssessmentRow.tsx | 17 +- pkg/vetting/persist.go | 154 ++++++++++++++- pkg/vetting/persist_test.go | 179 ++++++++++++++++++ 3 files changed, 344 insertions(+), 6 deletions(-) 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()