From 39633657bb5682f1c2477c2d0e2d489dc23385c0 Mon Sep 17 00:00:00 2001 From: Miguel Martinez Trivino Date: Thu, 16 Apr 2026 00:35:45 +0200 Subject: [PATCH] feat(attestation): add optional severity_score to PolicySASTFinding Signed-off-by: Miguel Martinez Trivino Made-with: Cursor --- .../frontend/attestation/v1/crafting_state.ts | 26 ++++++++++++- ...ation.v1.PolicySASTFinding.jsonschema.json | 38 +++++++++++++++++++ ...testation.v1.PolicySASTFinding.schema.json | 38 +++++++++++++++++++ .../api/attestation/v1/crafting_state.pb.go | 20 ++++++++-- .../api/attestation/v1/crafting_state.proto | 2 + pkg/policies/findings/registry_test.go | 31 +++++++++++++-- 6 files changed, 146 insertions(+), 9 deletions(-) diff --git a/app/controlplane/api/gen/frontend/attestation/v1/crafting_state.ts b/app/controlplane/api/gen/frontend/attestation/v1/crafting_state.ts index cc4ee66bb..36c0dccff 100644 --- a/app/controlplane/api/gen/frontend/attestation/v1/crafting_state.ts +++ b/app/controlplane/api/gen/frontend/attestation/v1/crafting_state.ts @@ -379,6 +379,8 @@ export interface PolicySASTFinding { codeSnippet: string; /** Suggested fix */ recommendation: string; + /** Optional numeric severity score from the scanner (scale is tool-defined) */ + severityScore?: number | undefined; } /** @@ -3412,7 +3414,16 @@ export const PolicyVulnerabilityFinding = { }; function createBasePolicySASTFinding(): PolicySASTFinding { - return { message: "", ruleId: "", severity: "", location: "", lineNumber: 0, codeSnippet: "", recommendation: "" }; + return { + message: "", + ruleId: "", + severity: "", + location: "", + lineNumber: 0, + codeSnippet: "", + recommendation: "", + severityScore: undefined, + }; } export const PolicySASTFinding = { @@ -3438,6 +3449,9 @@ export const PolicySASTFinding = { if (message.recommendation !== "") { writer.uint32(58).string(message.recommendation); } + if (message.severityScore !== undefined) { + writer.uint32(65).double(message.severityScore); + } return writer; }, @@ -3497,6 +3511,13 @@ export const PolicySASTFinding = { message.recommendation = reader.string(); continue; + case 8: + if (tag !== 65) { + break; + } + + message.severityScore = reader.double(); + continue; } if ((tag & 7) === 4 || tag === 0) { break; @@ -3515,6 +3536,7 @@ export const PolicySASTFinding = { lineNumber: isSet(object.lineNumber) ? Number(object.lineNumber) : 0, codeSnippet: isSet(object.codeSnippet) ? String(object.codeSnippet) : "", recommendation: isSet(object.recommendation) ? String(object.recommendation) : "", + severityScore: isSet(object.severityScore) ? Number(object.severityScore) : undefined, }; }, @@ -3527,6 +3549,7 @@ export const PolicySASTFinding = { message.lineNumber !== undefined && (obj.lineNumber = Math.round(message.lineNumber)); message.codeSnippet !== undefined && (obj.codeSnippet = message.codeSnippet); message.recommendation !== undefined && (obj.recommendation = message.recommendation); + message.severityScore !== undefined && (obj.severityScore = message.severityScore); return obj; }, @@ -3543,6 +3566,7 @@ export const PolicySASTFinding = { message.lineNumber = object.lineNumber ?? 0; message.codeSnippet = object.codeSnippet ?? ""; message.recommendation = object.recommendation ?? ""; + message.severityScore = object.severityScore ?? undefined; return message; }, }; diff --git a/app/controlplane/api/gen/jsonschema/attestation.v1.PolicySASTFinding.jsonschema.json b/app/controlplane/api/gen/jsonschema/attestation.v1.PolicySASTFinding.jsonschema.json index cc9bbcffc..f9b655c43 100644 --- a/app/controlplane/api/gen/jsonschema/attestation.v1.PolicySASTFinding.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/attestation.v1.PolicySASTFinding.jsonschema.json @@ -17,6 +17,25 @@ "^(rule_id)$": { "description": "Tool-specific rule identifier (e.g., java:S1234, go-sec:G101)", "type": "string" + }, + "^(severity_score)$": { + "anyOf": [ + { + "type": "number" + }, + { + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ], + "type": "string" + }, + { + "type": "string" + } + ], + "description": "Optional numeric severity score from the scanner (scale is tool-defined)" } }, "properties": { @@ -49,6 +68,25 @@ "severity": { "description": "Severity level (CRITICAL, HIGH, MEDIUM, LOW)", "type": "string" + }, + "severityScore": { + "anyOf": [ + { + "type": "number" + }, + { + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ], + "type": "string" + }, + { + "type": "string" + } + ], + "description": "Optional numeric severity score from the scanner (scale is tool-defined)" } }, "required": [ diff --git a/app/controlplane/api/gen/jsonschema/attestation.v1.PolicySASTFinding.schema.json b/app/controlplane/api/gen/jsonschema/attestation.v1.PolicySASTFinding.schema.json index 01d666207..3228052d9 100644 --- a/app/controlplane/api/gen/jsonschema/attestation.v1.PolicySASTFinding.schema.json +++ b/app/controlplane/api/gen/jsonschema/attestation.v1.PolicySASTFinding.schema.json @@ -17,6 +17,25 @@ "^(ruleId)$": { "description": "Tool-specific rule identifier (e.g., java:S1234, go-sec:G101)", "type": "string" + }, + "^(severityScore)$": { + "anyOf": [ + { + "type": "number" + }, + { + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ], + "type": "string" + }, + { + "type": "string" + } + ], + "description": "Optional numeric severity score from the scanner (scale is tool-defined)" } }, "properties": { @@ -49,6 +68,25 @@ "severity": { "description": "Severity level (CRITICAL, HIGH, MEDIUM, LOW)", "type": "string" + }, + "severity_score": { + "anyOf": [ + { + "type": "number" + }, + { + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ], + "type": "string" + }, + { + "type": "string" + } + ], + "description": "Optional numeric severity score from the scanner (scale is tool-defined)" } }, "required": [ diff --git a/pkg/attestation/crafter/api/attestation/v1/crafting_state.pb.go b/pkg/attestation/crafter/api/attestation/v1/crafting_state.pb.go index d6eadb3ae..92fc81fcd 100644 --- a/pkg/attestation/crafter/api/attestation/v1/crafting_state.pb.go +++ b/pkg/attestation/crafter/api/attestation/v1/crafting_state.pb.go @@ -810,8 +810,10 @@ type PolicySASTFinding struct { CodeSnippet string `protobuf:"bytes,6,opt,name=code_snippet,json=codeSnippet,proto3" json:"code_snippet,omitempty"` // Suggested fix Recommendation string `protobuf:"bytes,7,opt,name=recommendation,proto3" json:"recommendation,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Optional numeric severity score from the scanner (scale is tool-defined) + SeverityScore *float64 `protobuf:"fixed64,8,opt,name=severity_score,json=severityScore,proto3,oneof" json:"severity_score,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *PolicySASTFinding) Reset() { @@ -893,6 +895,13 @@ func (x *PolicySASTFinding) GetRecommendation() string { return "" } +func (x *PolicySASTFinding) GetSeverityScore() float64 { + if x != nil && x.SeverityScore != nil { + return *x.SeverityScore + } + return 0 +} + // Output schema for license violation findings from policy evaluation. // Used when a policy declares finding_type: LICENSE_VIOLATION. type PolicyLicenseViolationFinding struct { @@ -2778,7 +2787,7 @@ const file_attestation_v1_crafting_state_proto_rawDesc = "" + "\x04cwes\x18\x06 \x03(\tR\x04cwes\x12&\n" + "\x0erecommendation\x18\a \x01(\tR\x0erecommendation\x12 \n" + "\vdescription\x18\b \x01(\tR\vdescription\x12#\n" + - "\rfixed_version\x18\t \x01(\tR\ffixedVersion\"\x8a\x02\n" + + "\rfixed_version\x18\t \x01(\tR\ffixedVersion\"\xc9\x02\n" + "\x11PolicySASTFinding\x12 \n" + "\amessage\x18\x01 \x01(\tB\x06\xbaH\x03\xc8\x01\x01R\amessage\x12\x1f\n" + "\arule_id\x18\x02 \x01(\tB\x06\xbaH\x03\xc8\x01\x01R\x06ruleId\x12\"\n" + @@ -2787,7 +2796,9 @@ const file_attestation_v1_crafting_state_proto_rawDesc = "" + "\vline_number\x18\x05 \x01(\x05R\n" + "lineNumber\x12!\n" + "\fcode_snippet\x18\x06 \x01(\tR\vcodeSnippet\x12&\n" + - "\x0erecommendation\x18\a \x01(\tR\x0erecommendation\"\xb2\x02\n" + + "\x0erecommendation\x18\a \x01(\tR\x0erecommendation\x12*\n" + + "\x0eseverity_score\x18\b \x01(\x01H\x00R\rseverityScore\x88\x01\x01B\x11\n" + + "\x0f_severity_score\"\xb2\x02\n" + "\x1dPolicyLicenseViolationFinding\x12 \n" + "\amessage\x18\x01 \x01(\tB\x06\xbaH\x03\xc8\x01\x01R\amessage\x12-\n" + "\x0ecomponent_name\x18\x02 \x01(\tB\x06\xbaH\x03\xc8\x01\x01R\rcomponentName\x12!\n" + @@ -2982,6 +2993,7 @@ func file_attestation_v1_crafting_state_proto_init() { if File_attestation_v1_crafting_state_proto != nil { return } + file_attestation_v1_crafting_state_proto_msgTypes[5].OneofWrappers = []any{} file_attestation_v1_crafting_state_proto_msgTypes[7].OneofWrappers = []any{} file_attestation_v1_crafting_state_proto_msgTypes[8].OneofWrappers = []any{ (*CraftingState_InputSchema)(nil), diff --git a/pkg/attestation/crafter/api/attestation/v1/crafting_state.proto b/pkg/attestation/crafter/api/attestation/v1/crafting_state.proto index 5a19daf59..1bf6493d7 100644 --- a/pkg/attestation/crafter/api/attestation/v1/crafting_state.proto +++ b/pkg/attestation/crafter/api/attestation/v1/crafting_state.proto @@ -355,6 +355,8 @@ message PolicySASTFinding { string code_snippet = 6; // Suggested fix string recommendation = 7; + // Optional numeric severity score from the scanner (scale is tool-defined) + optional double severity_score = 8; } // Output schema for license violation findings from policy evaluation. diff --git a/pkg/policies/findings/registry_test.go b/pkg/policies/findings/registry_test.go index 65e325db5..7ca4db70c 100644 --- a/pkg/policies/findings/registry_test.go +++ b/pkg/policies/findings/registry_test.go @@ -154,6 +154,25 @@ func TestValidateFinding(t *testing.T) { assert.Equal(t, "java:S1234", f.GetRuleId()) assert.Equal(t, "HIGH", f.GetSeverity()) assert.Equal(t, "src/main/Handler.java", f.GetLocation()) + assert.Nil(t, f.SeverityScore) + }, + }, + { + name: "valid SAST finding with severity_score", + findingType: "SAST", + raw: map[string]any{ + "message": "SQL injection in handler", + "rule_id": "java:S1234", + "severity": "HIGH", + "location": "src/main/Handler.java", + "severity_score": 7.5, + }, + checkFn: func(t *testing.T, msg interface{}) { + t.Helper() + f, ok := msg.(*v1.PolicySASTFinding) + require.True(t, ok) + require.NotNil(t, f.SeverityScore) + assert.InDelta(t, 7.5, *f.SeverityScore, 1e-9) }, }, { @@ -238,6 +257,7 @@ func TestValidateFinding(t *testing.T) { } func TestSetViolationFinding(t *testing.T) { + sastSeverityScore := 4.0 tests := []struct { name string findingType string @@ -265,16 +285,19 @@ func TestSetViolationFinding(t *testing.T) { name: "set SAST finding", findingType: "SAST", finding: &v1.PolicySASTFinding{ - Message: "test", - RuleId: "go-sec:G101", - Severity: "MEDIUM", - Location: "main.go", + Message: "test", + RuleId: "go-sec:G101", + Severity: "MEDIUM", + Location: "main.go", + SeverityScore: &sastSeverityScore, }, checkFn: func(t *testing.T, v *v1.PolicyEvaluation_Violation) { t.Helper() f := v.GetSast() require.NotNil(t, f) assert.Equal(t, "go-sec:G101", f.GetRuleId()) + require.NotNil(t, f.SeverityScore) + assert.InDelta(t, 4.0, *f.SeverityScore, 1e-9) }, }, {