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 <sacha@probo.com>
This commit is contained in:
Sacha Al Himdani
2026-07-21 15:01:48 +02:00
parent 3805afc806
commit 5c5f60d5e1
3 changed files with 344 additions and 6 deletions

View File

@@ -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
</Tr>
{props.isExpanded && (
<Tr className={clsx("border-none", isExpired && "opacity-50")}>
<Td colSpan={4}>
<div className="space-y-2">
<Td colSpan={4} className="whitespace-normal align-top">
<div className="space-y-2 max-w-4xl">
<div>
{__("Notes")}
:
</div>
<p className="text-sm text-txt-secondary whitespace-pre-wrap">
{assessment.notes}
</p>
{assessment.notes
? (
<div className="overflow-x-auto">
<div className="prose prose-sm max-w-none [&_.prose]:max-w-none">
<Markdown content={assessment.notes} />
</div>
</div>
)
: null}
</div>
</Td>
</Tr>

View File

@@ -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 ""

View File

@@ -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()