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 e9f545b07..cc4ee66bb 100644 --- a/app/controlplane/api/gen/frontend/attestation/v1/crafting_state.ts +++ b/app/controlplane/api/gen/frontend/attestation/v1/crafting_state.ts @@ -356,6 +356,8 @@ export interface PolicyVulnerabilityFinding { recommendation: string; /** Optional longer description of the vulnerability */ description: string; + /** Version that fixes the vulnerability (e.g., "2.0.1", "1.3.4-patch1") */ + fixedVersion: string; } /** @@ -3243,6 +3245,7 @@ function createBasePolicyVulnerabilityFinding(): PolicyVulnerabilityFinding { cwes: [], recommendation: "", description: "", + fixedVersion: "", }; } @@ -3272,6 +3275,9 @@ export const PolicyVulnerabilityFinding = { if (message.description !== "") { writer.uint32(66).string(message.description); } + if (message.fixedVersion !== "") { + writer.uint32(74).string(message.fixedVersion); + } return writer; }, @@ -3338,6 +3344,13 @@ export const PolicyVulnerabilityFinding = { message.description = reader.string(); continue; + case 9: + if (tag !== 74) { + break; + } + + message.fixedVersion = reader.string(); + continue; } if ((tag & 7) === 4 || tag === 0) { break; @@ -3357,6 +3370,7 @@ export const PolicyVulnerabilityFinding = { cwes: Array.isArray(object?.cwes) ? object.cwes.map((e: any) => String(e)) : [], recommendation: isSet(object.recommendation) ? String(object.recommendation) : "", description: isSet(object.description) ? String(object.description) : "", + fixedVersion: isSet(object.fixedVersion) ? String(object.fixedVersion) : "", }; }, @@ -3374,6 +3388,7 @@ export const PolicyVulnerabilityFinding = { } message.recommendation !== undefined && (obj.recommendation = message.recommendation); message.description !== undefined && (obj.description = message.description); + message.fixedVersion !== undefined && (obj.fixedVersion = message.fixedVersion); return obj; }, @@ -3391,6 +3406,7 @@ export const PolicyVulnerabilityFinding = { message.cwes = object.cwes?.map((e) => e) || []; message.recommendation = object.recommendation ?? ""; message.description = object.description ?? ""; + message.fixedVersion = object.fixedVersion ?? ""; return message; }, }; diff --git a/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyVulnerabilityFinding.jsonschema.json b/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyVulnerabilityFinding.jsonschema.json index 99369bcab..791e6e7c8 100644 --- a/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyVulnerabilityFinding.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyVulnerabilityFinding.jsonschema.json @@ -22,6 +22,10 @@ "description": "Vulnerability identifier (e.g., CVE-2024-1234, GHSA-xxxx)", "type": "string" }, + "^(fixed_version)$": { + "description": "Version that fixes the vulnerability (e.g., \"2.0.1\", \"1.3.4-patch1\")", + "type": "string" + }, "^(package_purl)$": { "description": "Package URL of the affected component (e.g., pkg:golang/example.com/lib@v1.0.0)", "type": "string" @@ -57,6 +61,10 @@ "description": "Vulnerability identifier (e.g., CVE-2024-1234, GHSA-xxxx)", "type": "string" }, + "fixedVersion": { + "description": "Version that fixes the vulnerability (e.g., \"2.0.1\", \"1.3.4-patch1\")", + "type": "string" + }, "message": { "description": "Human-readable violation description", "type": "string" diff --git a/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyVulnerabilityFinding.schema.json b/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyVulnerabilityFinding.schema.json index 831387d86..fe8a2dbc0 100644 --- a/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyVulnerabilityFinding.schema.json +++ b/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyVulnerabilityFinding.schema.json @@ -22,6 +22,10 @@ "description": "Vulnerability identifier (e.g., CVE-2024-1234, GHSA-xxxx)", "type": "string" }, + "^(fixedVersion)$": { + "description": "Version that fixes the vulnerability (e.g., \"2.0.1\", \"1.3.4-patch1\")", + "type": "string" + }, "^(packagePurl)$": { "description": "Package URL of the affected component (e.g., pkg:golang/example.com/lib@v1.0.0)", "type": "string" @@ -57,6 +61,10 @@ "description": "Vulnerability identifier (e.g., CVE-2024-1234, GHSA-xxxx)", "type": "string" }, + "fixed_version": { + "description": "Version that fixes the vulnerability (e.g., \"2.0.1\", \"1.3.4-patch1\")", + "type": "string" + }, "message": { "description": "Human-readable violation description", "type": "string" 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 e532c29e8..d6eadb3ae 100644 --- a/pkg/attestation/crafter/api/attestation/v1/crafting_state.pb.go +++ b/pkg/attestation/crafter/api/attestation/v1/crafting_state.pb.go @@ -692,7 +692,9 @@ type PolicyVulnerabilityFinding struct { // Suggested fix or upgrade path Recommendation string `protobuf:"bytes,7,opt,name=recommendation,proto3" json:"recommendation,omitempty"` // Optional longer description of the vulnerability - Description string `protobuf:"bytes,8,opt,name=description,proto3" json:"description,omitempty"` + Description string `protobuf:"bytes,8,opt,name=description,proto3" json:"description,omitempty"` + // Version that fixes the vulnerability (e.g., "2.0.1", "1.3.4-patch1") + FixedVersion string `protobuf:"bytes,9,opt,name=fixed_version,json=fixedVersion,proto3" json:"fixed_version,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -783,6 +785,13 @@ func (x *PolicyVulnerabilityFinding) GetDescription() string { return "" } +func (x *PolicyVulnerabilityFinding) GetFixedVersion() string { + if x != nil { + return x.FixedVersion + } + return "" +} + // Output schema for SAST findings from policy evaluation. // Used when a policy declares finding_type: SAST. type PolicySASTFinding struct { @@ -2758,7 +2767,7 @@ const file_attestation_v1_crafting_state_proto_rawDesc = "" + "\x05input\x18\x01 \x01(\fR\x05input\x12\x16\n" + "\x06output\x18\x02 \x01(\fR\x06output\"\\\n" + "\x16PolicyEvaluationBundle\x12B\n" + - "\vevaluations\x18\x01 \x03(\v2 .attestation.v1.PolicyEvaluationR\vevaluations\"\xd1\x02\n" + + "\vevaluations\x18\x01 \x03(\v2 .attestation.v1.PolicyEvaluationR\vevaluations\"\xf6\x02\n" + "\x1aPolicyVulnerabilityFinding\x12 \n" + "\amessage\x18\x01 \x01(\tB\x06\xbaH\x03\xc8\x01\x01R\amessage\x12'\n" + "\vexternal_id\x18\x02 \x01(\tB\x06\xbaH\x03\xc8\x01\x01R\n" + @@ -2768,7 +2777,8 @@ const file_attestation_v1_crafting_state_proto_rawDesc = "" + "\rcvss_v3_score\x18\x05 \x01(\x01B\x17\xbaH\x14\x12\x12\x19\x00\x00\x00\x00\x00\x00$@)\x00\x00\x00\x00\x00\x00\x00\x00R\vcvssV3Score\x12\x12\n" + "\x04cwes\x18\x06 \x03(\tR\x04cwes\x12&\n" + "\x0erecommendation\x18\a \x01(\tR\x0erecommendation\x12 \n" + - "\vdescription\x18\b \x01(\tR\vdescription\"\x8a\x02\n" + + "\vdescription\x18\b \x01(\tR\vdescription\x12#\n" + + "\rfixed_version\x18\t \x01(\tR\ffixedVersion\"\x8a\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" + diff --git a/pkg/attestation/crafter/api/attestation/v1/crafting_state.proto b/pkg/attestation/crafter/api/attestation/v1/crafting_state.proto index c5148e14f..5a19daf59 100644 --- a/pkg/attestation/crafter/api/attestation/v1/crafting_state.proto +++ b/pkg/attestation/crafter/api/attestation/v1/crafting_state.proto @@ -334,6 +334,8 @@ message PolicyVulnerabilityFinding { string recommendation = 7; // Optional longer description of the vulnerability string description = 8; + // Version that fixes the vulnerability (e.g., "2.0.1", "1.3.4-patch1") + string fixed_version = 9; } // Output schema for SAST findings from policy evaluation. diff --git a/pkg/policies/engine/rego/builtins/vulnerability_test.go b/pkg/policies/engine/rego/builtins/vulnerability_test.go index 2d7141a5c..ff5404e7f 100644 --- a/pkg/policies/engine/rego/builtins/vulnerability_test.go +++ b/pkg/policies/engine/rego/builtins/vulnerability_test.go @@ -60,6 +60,23 @@ result := object.union( "recommendation": "upgrade to v2.0.0", }, }, + { + name: "combined with object.union for fixed_version", + policy: `package test +import rego.v1 + +result := object.union( + chainloop.vulnerability("CVE found", "CVE-2024-5678", "pkg:npm/foo@1.0", "HIGH"), + {"fixed_version": "1.0.1"}, +)`, + expected: map[string]any{ + "message": "CVE found", + "external_id": "CVE-2024-5678", + "package_purl": "pkg:npm/foo@1.0", + "severity": "HIGH", + "fixed_version": "1.0.1", + }, + }, { name: "with sprintf interpolation", policy: `package test diff --git a/pkg/policies/findings/registry_test.go b/pkg/policies/findings/registry_test.go index 0564ed274..65e325db5 100644 --- a/pkg/policies/findings/registry_test.go +++ b/pkg/policies/findings/registry_test.go @@ -94,6 +94,27 @@ func TestValidateFinding(t *testing.T) { assert.Equal(t, "A buffer overflow vulnerability in the parsing module allows remote code execution.", f.GetDescription()) }, }, + { + name: "valid vulnerability finding with fixed_version", + findingType: "VULNERABILITY", + raw: map[string]any{ + "message": "Found CVE-2024-5678", + "external_id": "CVE-2024-5678", + "package_purl": "pkg:golang/example.com/lib@v1.0.0", + "severity": "HIGH", + "fixed_version": "1.0.1", + }, + checkFn: func(t *testing.T, msg interface{}) { + t.Helper() + f, ok := msg.(*v1.PolicyVulnerabilityFinding) + require.True(t, ok) + assert.Equal(t, "Found CVE-2024-5678", f.GetMessage()) + assert.Equal(t, "CVE-2024-5678", f.GetExternalId()) + assert.Equal(t, "pkg:golang/example.com/lib@v1.0.0", f.GetPackagePurl()) + assert.Equal(t, "HIGH", f.GetSeverity()) + assert.Equal(t, "1.0.1", f.GetFixedVersion()) + }, + }, { name: "vulnerability finding missing required field", findingType: "VULNERABILITY",